174 lines
3.7 KiB
TypeScript
174 lines
3.7 KiB
TypeScript
type TypeCTXRef = { [key: string]: any };
|
|
|
|
export interface ICTXFN {
|
|
(context: Context): any;
|
|
}
|
|
interface Init {
|
|
(ctxRef?: TypeCTXRef, parentContext?: Context | void): TypeCTXRef | null;
|
|
}
|
|
|
|
type ContextOptions = {
|
|
use: () => Context | void;
|
|
set: (value: any) => any;
|
|
addQueryableProperties: (ctxRef: TypeCTXRef) => TQueryableProperties;
|
|
init?: Init;
|
|
};
|
|
|
|
type TQueryableProperties = { [key: string]: true };
|
|
|
|
export type TCTX = {
|
|
use: () => Context | void;
|
|
run: (ctxRef: TypeCTXRef, fn: ICTXFN) => any;
|
|
bind: (
|
|
ctxRef: TypeCTXRef,
|
|
fn: (...args: any[]) => any,
|
|
...args: any[]
|
|
) => (...runTimeArgs: any[]) => any;
|
|
};
|
|
|
|
const getInnerName = (name: string): string => `__${name}`;
|
|
|
|
class Context {
|
|
private _parentContext: Context | null = null;
|
|
[key: string]: any;
|
|
|
|
static is(value: any): value is Context {
|
|
return value instanceof Context;
|
|
}
|
|
|
|
constructor(
|
|
{ use, set, addQueryableProperties, init }: ContextOptions,
|
|
ctxRef: TypeCTXRef
|
|
) {
|
|
const ctx = use();
|
|
|
|
const usedRef =
|
|
typeof init === 'function' ? init(ctxRef, ctx) ?? ctxRef : ctxRef;
|
|
|
|
const queryableProperties = addQueryableProperties(usedRef);
|
|
|
|
if (usedRef && typeof usedRef === 'object') {
|
|
for (const key in queryableProperties) {
|
|
if (Object.prototype.hasOwnProperty.call(usedRef, key)) {
|
|
this[getInnerName(key)] = usedRef[key];
|
|
}
|
|
this.addLookupProperty(key);
|
|
}
|
|
}
|
|
|
|
if (ctx) {
|
|
this.setParentContext(ctx);
|
|
}
|
|
|
|
set(this);
|
|
}
|
|
|
|
addLookupProperty(key: string) {
|
|
const innerName = getInnerName(key);
|
|
|
|
Object.defineProperty(this, key, {
|
|
get() {
|
|
return this.lookup(innerName);
|
|
},
|
|
set(value) {
|
|
throw new Error(
|
|
`Context: Unable to set "${key}" to \`${JSON.stringify(
|
|
value
|
|
)}\`. Context properties cannot be set directly. Use context.run() instead.`
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
// @ts-ignore - we actually do use lookup
|
|
private lookup(key: string) {
|
|
let ctx: Context = this;
|
|
do {
|
|
if (ctx.hasOwnProperty(key)) {
|
|
return ctx[key];
|
|
}
|
|
if (Context.is(ctx.parentContext)) {
|
|
ctx = ctx.parentContext;
|
|
} else {
|
|
return;
|
|
}
|
|
} while (ctx);
|
|
}
|
|
|
|
private setParentContext(parentContext: Context) {
|
|
if (Context.is(this)) {
|
|
this._parentContext = parentContext;
|
|
}
|
|
}
|
|
|
|
get parentContext(): Context | null {
|
|
return this._parentContext;
|
|
}
|
|
}
|
|
|
|
function createContext(init?: Init) {
|
|
const storage = {
|
|
ctx: undefined,
|
|
};
|
|
|
|
const queryableProperties: TQueryableProperties = {};
|
|
|
|
function addQueryableProperties(ctxRef: TypeCTXRef): TQueryableProperties {
|
|
if (!ctxRef || typeof ctxRef !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
for (const key in ctxRef) {
|
|
if (Object.prototype.hasOwnProperty.call(ctxRef, key)) {
|
|
queryableProperties[key] = true;
|
|
}
|
|
}
|
|
|
|
return queryableProperties;
|
|
}
|
|
|
|
function use(): Context | void {
|
|
return storage.ctx;
|
|
}
|
|
function set(value: any) {
|
|
return (storage.ctx = value);
|
|
}
|
|
function clear() {
|
|
const ctx = use();
|
|
|
|
if (!Context.is(ctx)) {
|
|
return;
|
|
}
|
|
|
|
set(ctx.parentContext);
|
|
}
|
|
function run(ctxRef: TypeCTXRef, fn: ICTXFN) {
|
|
const ctx = new Context({ set, use, addQueryableProperties, init }, ctxRef);
|
|
|
|
const res = fn(ctx);
|
|
|
|
clear();
|
|
return res;
|
|
}
|
|
|
|
function bind(
|
|
ctxRef: TypeCTXRef,
|
|
fn: (...args: any[]) => any,
|
|
...args: any[]
|
|
) {
|
|
return function(...runTimeArgs: any[]) {
|
|
return run(ctxRef, function() {
|
|
return fn(...args, ...runTimeArgs);
|
|
});
|
|
};
|
|
}
|
|
|
|
return {
|
|
use,
|
|
run,
|
|
bind,
|
|
};
|
|
}
|
|
|
|
export default createContext;
|