UNPKG

nctx

Version:

NodeJS Contextual Dependency Injection using native async_hooks - IoC

261 lines (230 loc) 5.85 kB
const { AsyncLocalStorage } = require("node:async_hooks"); const defaultsDeep = require("lodash/defaultsDeep"); const merge = require("lodash/merge"); const Registry = require("./registry"); const { isObjectKey, composeReducer } = require("./utils"); const { handler, requireHandler } = require("./proxy-handlers"); class Context { static create(name) { return new Context(name); } static async provide( ctxArr = [], callback, ref, syncFollowers = true, forceOverride = false ) { if (!Array.isArray(ctxArr)) { ctxArr = [ctxArr]; } const reducer = composeReducer( ...ctxArr.map( (ctx) => (func) => () => ctx.provide(func, ref, syncFollowers, forceOverride) ) ); return reducer(callback)(); } static fork(ctxArr = [], callback, deepFork = false, syncFollowers = true) { if (!Array.isArray(ctxArr)) { ctxArr = [ctxArr]; } const reducer = composeReducer( ...ctxArr.map( (ctx) => (func) => () => ctx.fork(func, deepFork, syncFollowers) ) ); return reducer(callback)(); } constructor(name = Symbol()) { this.asyncLocalStorage = new AsyncLocalStorage(); this.name = name; this.store = new Map(); this.sharedRefStore = new Map(); this.fallbackCtx = null; this.followedByCtx = new Set(); this.proxy = new Proxy(this, handler); this.proxyRequire = new Proxy(this, requireHandler); } storeRequire() { const store = this.asyncLocalStorage.getStore(); if (!store) { throw new Error( `calling context "${this.name.toString()}" from unprovided env, please use provide in a parent async branch` ); } return store; } provide(callback, ref, syncFollowers = true, forceOverride = false) { if (syncFollowers) { return Context.provide( [this, ...this.followedByCtx], callback, ref, false, forceOverride ); } if (this.isProvided() && !forceOverride) { if (ref) { this.share(ref); } return callback(); } const registry = Registry.create(); return this.asyncLocalStorage.run(registry, () => { if (ref) { this.share(ref); } return callback(); }); } isProvided() { return !!this.asyncLocalStorage.getStore(); } getDefault(key) { if (this.isProvided()) { return this.get(key); } } fork(callback, deepFork = false, syncFollowers = true) { if (syncFollowers) { return Context.fork( [this, ...this.followedByCtx], callback, deepFork, false ); } const parentRegistry = this.storeRequire(); const registry = Registry.create(); if (deepFork) { defaultsDeep(registry.obj, parentRegistry.obj); } else { Object.assign(registry.obj, parentRegistry.obj); } registry.map = new Map(parentRegistry.map); registry.parent = parentRegistry; return this.asyncLocalStorage.run(registry, () => { return callback(); }); } unfollow(ctx) { ctx.unfollowedBy(this); return this; } follow(ctx) { ctx.followedBy(this); return this; } followedBy(ctx) { this.followedByCtx.add(ctx); return this; } unfollowedBy(ctx) { this.followedByCtx.delete(ctx); return this; } fallback(ctx) { this.fallbackCtx = ctx; return this; } merge(...params) { const registry = this.storeRequire(); const { obj, map } = registry; for (const [key, val] of Object.entries(params)) { if (isObjectKey(key)) { if ( typeof val === "object" && val !== null && typeof obj[key] === "object" && obj[key] !== null ) { merge(obj, val); } else { obj[key] = val; } } else { map.set(key, val); } } } share(ref) { if (!this.sharedRefStore.has(ref)) { const sharedCtx = this.storeRequire(); this.sharedRefStore.set(ref, sharedCtx); } else { const sharedCtx = this.sharedRefStore.get(ref); const ctx = this.storeRequire(); ctx.replaceBy(sharedCtx); } return this; } endShare(ref) { this.sharedRefStore.delete(ref); return this; } get(key) { if (key === undefined) { return this.proxy; } if (Array.isArray(key)) { return key.map((k) => this.get(k)); } const registry = this.storeRequire(); const v = registry.get(key); if (v === undefined && this.fallbackCtx) { return this.fallbackCtx.get(key); } return v; } set(key, val) { const registry = this.storeRequire(); registry.set(key, val); return val; } assign(obj) { const registry = this.storeRequire(); registry.assign(obj); return obj; } replace(key, callback) { const value = callback(this.get(key)); this.set(key, value); return value; } getParent(key) { if (Array.isArray(key)) { return key.map((k) => this.getParent(k)); } const registry = this.storeRequire(); return registry.parent.get(key); } setParent(key, val) { const registry = this.storeRequire(); registry.parent.set(key, val); return val; } assignParent(obj) { const registry = this.storeRequire(); registry.parent.assign(obj); return obj; } require(key, strict = true) { if (key === undefined) { return this.proxyRequire; } if (Array.isArray(key)) { return key.map((k) => this.require(k)); } const val = this.get(key); if ((strict && val === undefined) || (!strict && !val)) { throw new Error( `missing required context value for "${key}" in context "${this.name.toString()}"` ); } return val; } } module.exports = Context;