@andrew_l/context
Version:
Like composition api but for Node.
196 lines (189 loc) • 4.91 kB
JavaScript
import { noop, isPromise, catchError, captureStackTrace, isFunction, assert } from '@andrew_l/toolkit';
let idSec = 0;
let ALS;
let currentScope = null;
import('node:async_hooks').then((r) => {
ALS = new r.AsyncLocalStorage();
}).catch(noop);
class Scope {
constructor(detached = false) {
this.detached = detached;
this.id = ++idSec;
if (!detached) {
this.parent = getCurrentScope();
}
}
/**
* @internal
*/
id;
/**
* @internal
*/
providers = /* @__PURE__ */ new Map();
/**
* @internal
*/
parent = null;
/**
* @internal
*/
cleanups = [];
/**
* @internal
*/
_activeRuns = 0;
run(fn) {
this._activeRuns++;
const onComplete = ([err, result]) => {
this._activeRuns--;
if (this._activeRuns === 0) {
this.stop();
}
if (err) {
throw err;
}
return result;
};
const r = runInScope(this, fn);
if (isPromise(r)) {
return r.then(onComplete);
}
return onComplete(r);
}
stop() {
for (let i = 0, l = this.cleanups.length; i < l; i++) {
this.cleanups[i]();
}
this.cleanups.length = 0;
}
get active() {
return this._activeRuns > 0;
}
}
function createScope(detached) {
return new Scope(detached);
}
function getCurrentScope() {
if (ALS) {
return ALS.getStore() ?? null;
}
return currentScope;
}
function setCurrentScope(scope) {
if (ALS) {
ALS.enterWith(scope);
} else {
currentScope = scope;
}
}
function runInScope(scope, fn) {
if (ALS) {
return catchError(() => ALS.run(scope, fn));
}
const prevScope = getCurrentScope();
const onComplete = ([err, result2]) => {
if (prevScope) {
setCurrentScope(prevScope);
} else {
unsetCurrentScope();
}
return [err, result2];
};
const result = catchError(fn);
if (isPromise(result)) {
return result.then(onComplete);
}
return onComplete(result);
}
const unsetCurrentScope = () => {
if (ALS) {
ALS.enterWith(null);
} else {
currentScope = null;
}
};
function provide(key, value, enterWith) {
let currentScope = getCurrentScope();
if (!currentScope) {
if (enterWith) {
currentScope = new Scope();
setCurrentScope(currentScope);
} else {
console.warn(
`provide() is called when there is no active scope to be associated with.
` + captureStackTrace(provide)
);
return;
}
}
currentScope.providers.set(key, value);
}
function inject(key, defaultValue) {
let currentScope = getCurrentScope();
if (!currentScope) {
console.warn(
`inject() is called when there is no active scope to be associated with.
` + captureStackTrace(inject)
);
return;
}
const handled = /* @__PURE__ */ new WeakSet();
let value;
do {
value = currentScope.providers.get(key);
handled.add(currentScope);
currentScope = currentScope.parent;
} while (currentScope && value === void 0 && !handled.has(currentScope));
if (value === void 0 && defaultValue !== void 0) {
value = isFunction(defaultValue) ? defaultValue() : defaultValue;
}
return value;
}
function hasInjectionContext() {
return !!getCurrentScope();
}
function createContext(providerName, contextName) {
const symbolDescription = typeof providerName === "string" && !contextName ? `${providerName}Context` : contextName;
const injectionKey = Symbol(symbolDescription);
const injectContext = (fallback) => {
const context = inject(injectionKey, fallback);
if (context) return context;
if (context === null) return context;
throw new Error(
`Injection \`${injectionKey.toString()}\` not found. Must be used within ${Array.isArray(providerName) ? `one of the following providers: ${providerName.join(", ")}` : `\`${providerName}\``}`
);
};
const provideContext = (contextValue) => {
provide(injectionKey, contextValue);
return contextValue;
};
return [injectContext, provideContext];
}
function onScopeDispose(fn) {
const activeScope = getCurrentScope();
assert.ok(
activeScope,
"onScopeDispose() is called when there is no active scope to be associated with." + captureStackTrace(onScopeDispose)
);
activeScope.cleanups.push(fn);
}
function withContext(fn, detached = false) {
return function(...args) {
const scope = createScope(detached);
return scope.run(fn.bind(this, ...args));
};
}
function runWithContext(fn, isolated = false) {
return withContext(fn, isolated)();
}
function bindContext(fn) {
const activeScope = getCurrentScope();
assert.ok(
activeScope,
"bindContext() is called when there is no active scope to be associated with."
);
return () => activeScope.run(fn);
}
export { bindContext, createContext, getCurrentScope, hasInjectionContext, inject, onScopeDispose, provide, runWithContext, withContext };
//# sourceMappingURL=index.mjs.map