Last active
January 18, 2024 21:38
-
-
Save nberlette/b0f9d82c7399ed6237816c40ef11e887 to your computer and use it in GitHub Desktop.
Chainable Stage 3 Decorators (experimental)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*~----------------------------------------~*\ | |
* CHAINABLE DECORATORS UTILITY CLASS * | |
* FOR TYPESCRIPT 5.0 * | |
*~----------------------------------------~* | |
* © 2023-2024 NICHOLAS BERLETTE • MIT * | |
\*~----------------------------------------~*/ | |
// deno-lint-ignore-file no-explicit-any ban-types | |
import type { | |
IsomorphicDecorator, | |
DecoratorArgs, | |
Decorator, | |
DecoratorOrDecoratorFactory, | |
DecoratorFactory, | |
UnionToTuple, | |
Is, | |
} from "./helpers.ts"; | |
import { has } from "./helpers.ts"; | |
export type Chained< | |
T extends {}, | |
E extends readonly (readonly [keyof T, ...unknown[]])[] = [], | |
> = | |
& Chainable<T extends Chainable<infer U> ? U : {}> | |
& (E[number][0] extends infer A extends readonly unknown[] | |
? { args: UnionToTuple<A> } | |
: unknown) | |
& { | |
readonly [K in Exclude<keyof T, E[number][0]>]: T[K] extends | |
(...args: infer A) => infer R ? A extends DecoratorArgs ? | |
& (<const Args extends readonly unknown[]>( | |
...args: Args | |
) => Chained<T, [...E, [K, ...Args]]>) | |
& Chained<T, [...E, [K]]> | |
: R extends Decorator ? | |
& (<const Args extends A>( | |
...args: Args | |
) => Chained<T, [...E, [K, ...Args]]>) | |
& Chained<T, [...E, [K]]> | |
: (() => Chained<T, [...E, [K]]>) & Chained<T, [...E, [K]]> | |
: never; | |
}; | |
export interface Chainable< | |
T extends Record<string, DecoratorOrDecoratorFactory> = {}, | |
> extends IsomorphicDecorator<unknown, unknown> { | |
<This, Value>(): IsomorphicDecorator<This, Value>; | |
constructor: (typeof Chainable) & { | |
new <const D extends T>( | |
decorators: D, | |
): Chained< | |
{ | |
[ | |
K in keyof D as D[K] extends Decorator ? K | |
: D[K] extends DecoratorFactory<any, any, any> ? K | |
: never | |
]: D[K] extends DecoratorFactory ? D[K] | |
: D[K] & (() => D[K]); | |
} | |
>; | |
}; | |
} | |
export class Chainable< | |
const T extends Record<string, DecoratorOrDecoratorFactory> = {}, | |
> extends Function { | |
/** | |
* Performs extensive checks to provide a bulletproof confirmation that a pair of | |
* arguments belong to a Stage 3 Decorator Function, immediately exiting if any | |
* of the checks fails to pass (for performance). Checks include: | |
* - IF length < 2 OR length > 2 | |
* => FALSE | |
* | |
* -> target = args[0] , context = args[1] | |
* | |
* - IF typeof context !== "object" | |
* => FALSE | |
* - IF "kind" NOT in context | |
* => FALSE | |
* - IF context.kind !== "class" | "field" | "getter" | "setter" | "accessor" | "method" | |
* => FALSE | |
* - IF "access" NOT in context | |
* && context.kind NOT !== "class" | |
* => FALSE | |
* - IF keyof context.access !== "has" | "get" | "set" | |
* => FALSE | |
* - IF "addInitializer" NOT in context | |
* || typeof context.addInitializer !== "function" | |
* => FALSE | |
* - IF context.kind === "class" | |
* && IF typeof context.name !== "string" | "undefined" | |
* => FALSE | |
* ELSE IF typeof context.name !== "string" | "symbol" | |
* => FALSE | |
* - IF context.kind !== "class" | |
* && IF ( "static" NOT in context || "private" NOT in context ) | |
* => FALSE | |
* - IF context.kind === ( "method" || "getter" || "setter" ) | |
* && IF typeof target !== "function" | |
* => FALSE | |
* - IF context.kind === "accessor" | |
* && IF typeof target !== "object" | |
* || "get" NOT in target | |
* || "set" NOT in target | |
* => FALSE | |
* - IF context.kind === "field" | |
* && IF typeof target !== "undefined" | |
* => FALSE | |
* ... | |
* - IF you are still here... | |
* => TRUE :) | |
*/ | |
static isDecoratorArgs<T extends Decorator = Decorator<any, any>>( | |
it: unknown, | |
): it is DecoratorArgs<T> { | |
if (it == null || typeof it !== "object" || !Array.isArray(it)) { | |
return false; | |
} | |
if (it.length !== 2) return false; | |
const [target, context] = it as DecoratorArgs<T>; | |
if (!("kind" in context)) return false; | |
const { kind } = context; | |
if ( | |
!["class", "field", "getter", "setter", "accessor", "method"].includes( | |
kind, | |
) | |
) return false; | |
if ( | |
!("access" in context) || !("name" in context) || | |
!("addInitializer" in context && | |
typeof context.addInitializer === "function") | |
) return false; | |
if ( | |
!("access" in context) || context.access == null || | |
typeof context.access !== "object" | |
) { | |
return false; | |
} | |
if (kind === "field" && target !== undefined) return false; | |
if ( | |
!Reflect.ownKeys(context.access).every((k) => | |
["has", "get", "set"].includes(String(k)) | |
) | |
) { | |
return false; | |
} | |
return true; | |
} | |
static use<const T extends Record<string, (...args: any) => any>>( | |
decorators: T, | |
): Chained<T> { | |
return new Chainable({ ...decorators }) as Chained<T>; | |
} | |
private stack: (readonly [name: string, decorator: Decorator])[] = []; | |
private decorators: Record<string, DecoratorOrDecoratorFactory> = {}; | |
constructor(decorators: T) { | |
super("return this"); | |
const chain = new Proxy(this, { | |
apply: (_target, _thisArg, args) => { | |
const composed = this.decorate.call(this); | |
if (Chainable.isDecoratorArgs(args)) { | |
return Reflect.apply(composed, this, args); | |
} else { | |
return (target: any, context: any) => composed(target, context); | |
} | |
}, | |
get: (target, p) => { | |
if (has(target.decorators, p)) { | |
return Reflect.get(this.decorators, p); | |
} | |
switch (p) { | |
case "constructor": | |
return Chainable; | |
case "stack": | |
return this.stack; | |
case "decorators": | |
return this.decorators; | |
case "decorate": | |
return this.decorate.bind(this); | |
default: { | |
if (has(this, p)) { | |
const v = this[p]; | |
if (typeof v === "function") return v.bind(this); | |
return v; | |
} | |
const v = Reflect.get(target, p); | |
if (typeof v === "function") return v.bind(target); | |
return v; | |
} | |
} | |
}, | |
ownKeys: (target) => { | |
return [ | |
...new Set( | |
Reflect.ownKeys(target.decorators).concat(Reflect.ownKeys(target)), | |
), | |
]; | |
}, | |
getPrototypeOf: () => { | |
return Chainable.prototype; | |
}, | |
has: (target, p) => { | |
return has(target.decorators, p) || has(target, p); | |
}, | |
}); | |
for (let name in decorators) { | |
const _name = name; | |
const fn = decorators[name]; | |
let i = 1; | |
while (has(chain, name) || has(Chainable.prototype, name)) { | |
name = `${_name}${++i}` as typeof name; | |
} | |
this.decorators[name] = new Proxy<DecoratorOrDecoratorFactory>( | |
(...args: any[]) => fn(...args), | |
{ | |
apply: (_that, _thisArg, args) => { | |
if (Chainable.isDecoratorArgs(args)) { | |
const D: Decorator<T, any> = fn; | |
chain.stack.push([name, D]); | |
return chain; | |
} else { | |
const F: Decorator<T, any> = Reflect.apply(fn, undefined, args); | |
chain.stack.push([name, F]); | |
return chain; | |
} | |
}, | |
get: (target, p, receiver) => { | |
switch (p) { | |
case "stack": | |
return this.stack; | |
case "decorate": | |
return this.decorate.bind(this); | |
case name: { | |
if (!has(chain, name) && has(target, name)) { | |
return Reflect.get(target, name); | |
} | |
return Reflect.get(target, name, receiver); | |
} | |
default: { | |
if (has(chain.decorators, p)) { | |
return Reflect.get(chain.decorators, p); | |
} | |
if (has(target, p)) { | |
return Reflect.get(target, p) ?? | |
Reflect.get(target, p, receiver); | |
} | |
if (has(chain, p)) { | |
return Reflect.get(chain, p); | |
} | |
return Reflect.get(target, p, receiver); | |
} | |
} | |
}, | |
ownKeys: (target) => { | |
return [ | |
...new Set( | |
Reflect.ownKeys(chain.decorators).concat( | |
Reflect.ownKeys(target), | |
).filter((k) => | |
k !== name && !chain.stack.find(([name]) => k === name) | |
), | |
), | |
]; | |
}, | |
has: (target, p) => { | |
return has(target, p) || has(chain.decorators, p); | |
}, | |
getPrototypeOf: () => Chainable.prototype, | |
}, | |
); | |
} | |
return chain; | |
} | |
public decorate<T, V>(): IsomorphicDecorator<T, V> { | |
const stack = [...new Set(this.stack)]; // dedupe | |
this.stack.length = 0; | |
return decorator; | |
function decorator(target: any, context: any): any { | |
let result: any = target; // the value to be decorated | |
const accessorResult: ClassAccessorDecoratorResult<any, any> = { | |
get: undefined!, | |
set: undefined!, | |
init: undefined!, | |
}; | |
if (context) context.metadata ??= {}; | |
let done = false; | |
while (!done) { | |
// apply decorators in reverse order, as per spec | |
const entry = stack.pop()!; | |
if (!entry) return result; | |
const [_decoratorName, fn] = Array.from(entry); | |
if (typeof fn === "function") { | |
try { | |
// all the added factories and decorators are pushed to the stack as | |
// plain ol decorators, so they can be called via the same signature. | |
// any args passed into a decorator factory are applied to it, and | |
// the returned decorator is then added to the stack :) | |
let newTarget = target; | |
if ( | |
context.kind === "class" || | |
context.kind === "method" || | |
context.kind === "getter" || | |
context.kind === "setter" | |
) { | |
if (typeof result === "function") newTarget = result; | |
result = fn(newTarget as never, context) ?? result; | |
} else if (context.kind === "accessor") { | |
if (typeof result === "object") { | |
const { | |
get = accessorResult.get ??= function (this: T) { | |
return context.access.get(this); | |
}, | |
set = accessorResult.set ??= function (this: T, v: V) { | |
context.access.set(this, v); | |
}, | |
} = result ?? {}; | |
newTarget = { get, set }; | |
} | |
const newResult = fn(newTarget as never, context) ?? result; | |
if (typeof newResult === "object" && newResult != null) { | |
if ("get" in newResult && typeof newResult.get === "function") { | |
accessorResult.get = newResult.get; | |
} | |
if ("set" in newResult && typeof newResult.set === "function") { | |
accessorResult.set = newResult.set; | |
} | |
if ( | |
"init" in newResult && typeof newResult.init === "function" | |
) { | |
accessorResult.init = newResult.init; | |
} | |
} | |
result = accessorResult; | |
} else if (context.kind === "field") { | |
result = fn(undefined!, context) ?? result; | |
} else { | |
throw new TypeError("Invalid decorator context."); | |
} | |
} catch (e) { | |
// gracefully handle errors in applying the decorators | |
console.warn(e); | |
continue; | |
} | |
} | |
if (stack.length === 0 && fn == undefined) { | |
done = true; | |
return result; | |
} | |
} | |
return result; | |
} | |
} | |
[decoratorName: string | symbol]: unknown; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Chainable } from "./chainable.ts"; | |
import type { | |
ClassAccessorDecoratorFunction, | |
ClassMethodDecoratorFunction, | |
ClassGetterDecoratorFunction, | |
ClassSetterDecoratorFunction, | |
ClassFieldDecoratorFunction, | |
ClassDecoratorFunction, | |
Is, | |
} from "./helpers.ts"; | |
// improved debugging experience | |
declare global { | |
interface CallSite { | |
getThis(): unknown; | |
getFunctionName(): string | undefined; | |
getTypeName(): string | null; | |
getMethodName(): string | null; | |
getLineNumber(): number; | |
getColumnNumber(): number; | |
getFileName(): string | null; | |
isEval(): boolean; | |
getEvalOrigin(): string | undefined; | |
isToplevel(): boolean; | |
isPromiseAny(): boolean; | |
isPromiseAll(): boolean; | |
isConstructor(): boolean; | |
isAsync(): boolean; | |
getPromiseIndex(): number | null; | |
} | |
interface ErrorConstructor { | |
stackTraceLimit: number; | |
prepareStackTrace: (error: Error, stack: CallSite[]) => unknown; | |
} | |
} | |
Error.stackTraceLimit = 100; | |
Error.prepareStackTrace = (error, stack) => | |
stack.map((site) => { | |
const callsite = { | |
error, | |
this: site.getThis(), | |
functionName: site.getFunctionName(), | |
typeName: site.getTypeName(), | |
methodName: site.getMethodName(), | |
lineNumber: site.getLineNumber(), | |
columnNumber: site.getColumnNumber(), | |
evalOrigin: site.getEvalOrigin(), | |
fileName: site.getFileName(), | |
isEval: site.isEval(), | |
isToplevel: site.isToplevel(), | |
isConstructor: site.isConstructor(), | |
}; | |
console.log(callsite); | |
return callsite; | |
}); | |
// for downleveling to targets lower than esnext | |
if (!("metadata" in Symbol) || typeof Symbol.metadata !== "symbol") { | |
Object.defineProperty(Symbol, "metadata", { | |
value: Symbol("Symbol.metadata"), | |
}); | |
} | |
const SymbolMetadata: unique symbol = (Symbol as any).metadata; | |
const MEMOIZE_CACHE = Symbol("MEMOIZE_CACHE"); | |
const chain = Chainable.use({ | |
log<T extends string>(label: T) { | |
return (target: unknown, context: DecoratorContext) => { | |
console.log(label, target, context); | |
return target; | |
}; | |
}, | |
memo, | |
bound<T extends object, V extends (this: T, ...args: any) => any>(): ( | |
target: V, | |
context: ClassMethodDecoratorContext<T, V>, | |
) => V { | |
return (target, context) => { | |
const { kind, name, addInitializer, private: isPrivate } = context || {}; | |
if (typeof context !== "object" || context == null) { | |
throw new TypeError( | |
"Expected Stage 3 decorator support but encountered a legacy implementation at runtime. " + | |
"Check your TypeScript version - 5.0+ is required. If using Deno, v1.40.0+ is required.", | |
); | |
} | |
if (kind !== "method") { | |
throw new TypeError( | |
"@bound is not supported on non-method/non-function class members.", | |
); | |
} | |
if (isPrivate) { | |
throw new TypeError("@bound is not supported on private methods."); | |
} | |
addInitializer(function (this: any) { | |
this[name] = this[name]?.bind?.(this); | |
const type = typeof this[name]; | |
if (type !== "function") { | |
throw new TypeError( | |
`Expected "${ | |
String(name) | |
}" to be a function, but received: ${type}.`, | |
); | |
} | |
}); | |
return target; | |
}; | |
}, | |
enumerable<T extends boolean>(value: T) { | |
return function decorator<This, V>( | |
target: V, | |
context: DecoratorContext, | |
): void { | |
if (typeof context !== "object" || context == null) { | |
throw new TypeError( | |
"Expected Stage 3 decorator support but encountered a legacy implementation at runtime. Check your TypeScript version (5.0+ is required).", | |
); | |
} | |
const { kind, name, addInitializer } = context; | |
const v = !!value; | |
addInitializer(function (this: any) { | |
if (name && name in this) { | |
const descriptor = Reflect.getOwnPropertyDescriptor(this, name) ?? {}; | |
if (kind === "getter") { | |
descriptor.get ??= target as () => V; | |
} else if (kind === "setter") { | |
descriptor.set ??= target as (v: V) => void; | |
} else if (kind === "accessor") { | |
descriptor.get ??= | |
(target as ClassAccessorDecoratorTarget<This, V>).get; | |
descriptor.set ??= | |
(target as ClassAccessorDecoratorTarget<This, V>).set; | |
} else { | |
descriptor.get = descriptor.set = undefined!; | |
descriptor.value ??= target; | |
descriptor.writable ??= true; | |
} | |
descriptor.enumerable = v; | |
descriptor.configurable = true; | |
if (["getter", "setter", "accessor"].includes(kind)) { | |
delete descriptor.writable; | |
delete descriptor.value; | |
} else { | |
delete descriptor.get; | |
delete descriptor.set; | |
} | |
Reflect.defineProperty(this, name, descriptor); | |
} | |
}); | |
return target as any; | |
}; | |
}, | |
}); | |
class Foo { | |
@chain.log("Foo: hiding instance field...").enumerable(false) | |
foobar: string = "1"; | |
@chain.log("Foo: binding proto method...").bound() | |
myFunction() { | |
return this.foobar; | |
} | |
accessor rgba = [234, 234, 234, 1] as readonly [ | |
number, | |
number, | |
number, | |
number?, | |
]; | |
accessor xyz = [0.456, 0.123, 0.987] as readonly [number, number, number]; | |
@chain.log("hsla").enumerable(true).memo("rgba") | |
get hsla() { | |
const [r, g, b] = this.rgba; | |
return [(r / 255) * 360, (g / 255) * 100, (b / 255) * 100]; | |
} | |
@chain.log("Foo: memoizing lab").memo("xyz").enumerable(true) | |
get lab() { | |
const [x, y, z] = this.xyz; | |
return [x * 360, y * 127, z * 100]; | |
} | |
} | |
const foo = new Foo(); | |
console.log("rgba", foo.rgba); | |
console.log("hsla #1", foo.hsla); | |
console.log("hsla #2", foo.hsla, "(memoized based on rgba's value)"); | |
console.log("changing the rgba value...", foo.rgba = [100, 100, 100]); | |
console.log("hsla #3", foo.hsla); | |
console.log("hsla #3", foo.hsla); | |
console.log("xyz", foo.xyz); | |
console.log("lab #1", foo.lab); | |
console.log("lab #2", foo.lab, "(memoized based on xyz's value)"); | |
console.log("changing the xyz value...", foo.xyz = [0.1, 0.1, 0.1]); | |
console.log("lab #3", foo.lab); | |
console.log("lab #4", foo.lab); | |
console.log(foo); | |
console.log(JSON.stringify(Object.getOwnPropertyDescriptors(foo), null, 2)); | |
export type Memoized<T extends object, K extends keyof T | T[keyof T]> = T & { | |
[MEMOIZE_CACHE]?: WeakMap<T, Map<K, unknown>>; | |
}; | |
export function clearCache< | |
const T extends { | |
readonly [SymbolMetadata]: { [MEMOIZE_CACHE]: WeakMap<object, unknown> }; | |
}, | |
>(obj: T): boolean; | |
export function clearCache(obj: object): boolean; | |
export function clearCache(obj: object): boolean { | |
if (typeof obj === "object" && obj != null && !Array.isArray(obj)) { | |
try { | |
if (SymbolMetadata in obj) { | |
const metadata = | |
(obj[SymbolMetadata] ??= Object.create(null)) as Record< | |
PropertyKey, | |
unknown | |
>; | |
if ( | |
MEMOIZE_CACHE in metadata && | |
metadata[MEMOIZE_CACHE] instanceof WeakMap | |
) { | |
if (metadata[MEMOIZE_CACHE].has(obj)) { | |
metadata[MEMOIZE_CACHE].delete(obj); | |
return true; | |
} else { | |
return false; | |
} | |
} else { | |
(metadata[MEMOIZE_CACHE] = new WeakMap())!.set(obj, new Map()); | |
return true; | |
} | |
} | |
} catch { | |
return false; | |
} | |
} | |
return false; | |
} | |
/** | |
* Decorator for memoizing a method or getter based on a cache key. If called | |
* as a decorator factory, the first argument is used to create the cache key: | |
* if the key is a string and it is the name of an existing property on the | |
* target object, the value of that property is used as the cache key (so the | |
* decorated method or getter will return a cached value so long as the value | |
* of the specified property does not change. if the value changes, the cache | |
* is cleared and the method or getter will be called and cached again). If | |
* the key is a function, it is called with the target object as its only | |
* argument, and the return value of that function is used as the cache key. | |
*/ | |
export function memo< | |
This extends object, | |
Key extends PropertyKey & {} | keyof This, | |
Value, | |
>( | |
key: Key | (() => Key), | |
): { | |
( | |
getter: (this: This) => Value, | |
context: ClassGetterDecoratorContext<This, Value>, | |
): ((this: This) => Value) | void; | |
}; | |
export function memo< | |
This extends object, | |
Key extends PropertyKey & {} | keyof This, | |
Value extends (this: This, ...args: any) => any, | |
>( | |
key: Key | (() => Key), | |
): { | |
// deno-lint-ignore no-explicit-any | |
( | |
method: Value, | |
context: ClassMethodDecoratorContext<This, Value>, | |
): Value | void; | |
}; | |
export function memo<This, Value>( | |
getter: (this: This) => Value, | |
context: ClassGetterDecoratorContext<This, Value>, | |
): ((this: This) => Value) | void; | |
// deno-lint-ignore no-explicit-aeny | |
export function memo<This, Value extends (this: This, ...args: any) => any>( | |
method: Value, | |
context: ClassMethodDecoratorContext<This, Value>, | |
): Value | void; | |
export function memo<This, Value>( | |
...args: | |
| readonly [label: PropertyKey | (() => PropertyKey)] | |
| readonly [ | |
target: (this: This) => Value, | |
context: ClassGetterDecoratorContext<This, Value>, | |
] | |
| readonly [ | |
method: Value, | |
context: ClassMethodDecoratorContext< | |
This, | |
Is<Value, (this: This, ...args: any) => any> | |
>, | |
] | |
): typeof args extends | |
readonly [(this: This) => Value, ClassGetterDecoratorContext<any, any>] | |
? ((this: This) => Value) | void | |
: typeof args extends readonly [Value, ClassMethodDecoratorContext<any, any>] | |
? Is<Value, (this: This, ...args: any) => any> | void | |
: | |
| ClassGetterDecoratorFunction<This, Value> | |
| ClassMethodDecoratorFunction<This, Value> | |
| void { | |
if (args.length === 1) { | |
const [target] = args; | |
if (typeof target === "function") { | |
return memoizeDecoratorFactory(target()); | |
} else if ( | |
typeof target === "string" || | |
typeof target === "symbol" || | |
typeof target === "number" | |
) { | |
return memoizeDecoratorFactory(target); | |
} | |
} else if (args.length === 2) { | |
const [target, context] = args as readonly any[]; | |
memoizeDecoratorFactory()(target, context); | |
} | |
return memoizeDecoratorFactory(); | |
} | |
function memoizeDecoratorFactory< | |
const K extends PropertyKey | (() => PropertyKey), | |
Key extends PropertyKey = K extends PropertyKey ? K | |
// deno-lint-ignore no-explicit-any | |
: K extends ((...args: any) => infer R extends PropertyKey) ? R | |
: never, | |
>(cacheKey?: K): { | |
<This, Value>( | |
target: (this: This) => Value, | |
context: ClassGetterDecoratorContext<This, Value>, | |
): ((this: This) => Value) | void; | |
<This, Value>( | |
target: ClassAccessorDecoratorTarget<This, Value>, | |
context: ClassAccessorDecoratorContext<This, Value>, | |
): ClassAccessorDecoratorFunction<This, Value> | void; | |
<This, Value extends (this: This, ...args: unknown[]) => unknown>( | |
target: Value, | |
context: ClassMethodDecoratorContext<This, Value>, | |
): Value | void; | |
} { | |
return function memoizeDecorator< | |
This extends object, | |
// deno-lint-ignore no-explicit-any | |
Value, | |
>( | |
target: | |
| ClassAccessorDecoratorTarget<This, Value> | |
| Is<Value, (this: This, ...args: unknown[]) => unknown> | |
| ((this: This) => Value), | |
context: | |
| ClassAccessorDecoratorContext<This, Value> | |
| ClassMethodDecoratorContext< | |
This, | |
Is<Value, (this: This, ...args: unknown[]) => unknown> | |
> | |
| ClassGetterDecoratorContext<This, Value>, | |
): any { | |
const { name, kind, metadata = Object.create(null) } = context; | |
metadata[MEMOIZE_CACHE] ??= new WeakMap(); | |
const caches = metadata[MEMOIZE_CACHE] as WeakMap<This, Map<Key, Value>>; | |
const getCacheKey = (thisArg: This, args?: ArrayLike<unknown>): Key => { | |
let key: Key = "" as Key; | |
if ( | |
typeof cacheKey === "string" || | |
typeof cacheKey === "number" || | |
typeof cacheKey === "symbol" | |
) { | |
// deno-lint-ignore no-explicit-any | |
key = cacheKey in thisArg ? (thisArg as any)[cacheKey] : cacheKey; | |
} | |
if (typeof cacheKey === "function") { | |
key = cacheKey.call(thisArg) as Key; | |
} | |
return [name.toString(), key.toString(), ...Array.from(args ?? [])].join( | |
":", | |
) as Key; | |
}; | |
const getCachedValue = ( | |
thisArg: This, | |
args?: ArrayLike<unknown>, | |
): Value => { | |
const cache: Map<Key, Value> = caches.get(thisArg) ?? | |
caches.set(thisArg, new Map()).get(thisArg)!; | |
const key = getCacheKey(thisArg, args); | |
let value = cache.get(key)!; | |
if (value === undefined) { | |
if (kind === "method" || kind === "getter") { | |
value = (target as Function).apply(thisArg, args); | |
} else if (kind === "accessor") { | |
value = (target as ClassAccessorDecoratorTarget<This, Value>).get | |
.call(thisArg)!; | |
} else { | |
throw new TypeError("Invalid decorator context"); | |
} | |
cache.set(key, value); | |
} | |
return value; | |
}; | |
if (kind === "method") { | |
return function (this: This, ...args: unknown[]) { | |
return getCachedValue(this, args); | |
}; | |
} else if (kind === "getter") { | |
return function (this: This) { | |
return getCachedValue(this); | |
}; | |
} else if (kind === "accessor") { | |
return { | |
get() { | |
return getCachedValue(this); | |
}, | |
set(value: Value) { | |
clearCache(metadata); | |
context.access.set(this, value); | |
}, | |
} satisfies ClassAccessorDecoratorResult<This, Value>; | |
} else { | |
throw new TypeError("Invalid decorator context"); | |
} | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// #region Types | |
export interface ClassDecoratorFunction<T = any, V = any> { | |
<This extends T, Value extends Is<V, AbstractConstructor<This>>>( | |
target: Value, | |
context: ClassDecoratorContext<Value>, | |
): void | Value; | |
} | |
export interface ClassFieldDecoratorFunction<T = any, V = any> { | |
<This extends T, Value extends V>( | |
target: undefined, | |
context: ClassFieldDecoratorContext<This, Value>, | |
): void | ((this: This, initialValue: Value) => Value); | |
} | |
export interface ClassMethodDecoratorFunction<T = any, V = any> { | |
<This extends T, Value extends Is<V, (this: This, ...args: any) => unknown>>( | |
target: Value, | |
context: ClassMethodDecoratorContext<This, Value>, | |
): void | Value; | |
} | |
export interface ClassAccessorDecoratorFunction<T = any, V = any> { | |
<This extends T, Value extends V>( | |
target: ClassAccessorDecoratorTarget<This, Value>, | |
context: ClassAccessorDecoratorContext<This, Value>, | |
): void | ClassAccessorDecoratorResult<This, Value>; | |
} | |
export interface ClassGetterDecoratorFunction<T = any, V = any> { | |
<This extends T, Value extends V>( | |
target: (this: This) => Value, | |
context: ClassGetterDecoratorContext<This, Value>, | |
): void | ((this: This) => Value); | |
} | |
export interface ClassSetterDecoratorFunction<T = any, V = any> { | |
<This extends T, Value extends V>( | |
target: (this: This, newValue: Value) => void, | |
context: ClassSetterDecoratorContext<This, Value>, | |
): void | ((this: This, newValue: Value) => void); | |
} | |
export type Decorator<T = any, V = any> = | |
| ClassDecoratorFunction<T, V> | |
| ClassFieldDecoratorFunction<T, V> | |
| ClassMethodDecoratorFunction<T, V> | |
| ClassAccessorDecoratorFunction<T, V> | |
| ClassGetterDecoratorFunction<T, V> | |
| ClassSetterDecoratorFunction<T, V>; | |
export type DecoratorFactory< | |
This = any, | |
Value = any, | |
Args extends readonly any[] = any, | |
> = { | |
<const A extends Args>(...args: A): Decorator<This, Value>; | |
}; | |
export type DecoratorOrDecoratorFactory< | |
This = any, | |
Value = any, | |
Args extends readonly any[] = any, | |
> = Decorator<This, Value> & DecoratorFactory<This, Value, Args>; | |
export type DecoratorArgs<D extends Decorator = Decorator<any, any>> = D extends | |
(target: infer T, context: infer C extends DecoratorContext) => any | |
? readonly [target: T, context: C] | |
: Parameters<D>; | |
export interface IsomorphicDecorator<T, V> { | |
<Class extends AbstractConstructor<V>>( | |
target: Class, | |
context: ClassDecoratorContext<Class>, | |
): void | Class; | |
<This extends T, Value extends V>( | |
target: undefined, | |
context: ClassFieldDecoratorContext<This, Value>, | |
): void | ((this: This, initialValue: Value) => Value); | |
<This extends T, Value extends Is<V, (this: This, ...args: any) => any>>( | |
target: Value, | |
context: ClassMethodDecoratorContext<This, Value>, | |
): void | Value; | |
<This extends T, Value extends V>( | |
target: (this: This) => Value, | |
context: ClassGetterDecoratorContext<This, Value>, | |
): void | ((this: This) => Value); | |
<This extends T, Value extends V>( | |
target: (this: This, newValue: Value) => void, | |
context: ClassSetterDecoratorContext<This, Value>, | |
): void | ((this: This, newValue: Value) => void); | |
<This extends T, Value extends V>( | |
target: ClassAccessorDecoratorTarget<This, Value>, | |
context: ClassAccessorDecoratorContext<This, Value>, | |
): void | ClassAccessorDecoratorResult<This, Value>; | |
} | |
// #endregion Types | |
export type Is<T, U = unknown> = T extends U ? U extends T ? U : T | |
: [(T & U)] extends [never] ? (U & Omit<T, keyof U>) | |
: (T & U); | |
export type Constructor<T = any, A extends any[] = any[]> = new ( | |
...args: A | |
) => T; | |
export type Class< | |
Prototype = any, | |
Args extends any[] = any[], | |
Static extends {} = {}, | |
> = | |
& Constructor<Prototype, Args> | |
& { prototype: Prototype } | |
& { | |
[K in keyof Static as [Static[K]] extends [never] ? never : K]: | |
& ThisType<Class<Prototype, Args, Static>> | |
& Static[K]; | |
}; | |
export type AbstractConstructor<T = any, A extends any[] = any[]> = | |
abstract new (...args: A) => T; | |
export type AbstractClass< | |
Prototype = any, | |
Args extends any[] = any[], | |
Static extends {} = {}, | |
> = | |
& AbstractConstructor<Prototype, Args> | |
& { prototype: Prototype } | |
& { | |
[K in keyof Static as [Static[K]] extends [never] ? never : K]: | |
& ThisType<AbstractClass<Prototype, Args, Static>> | |
& Static[K]; | |
}; | |
/** UnionToIntersection<{ a: string } | { b: string }> = { a: string } & { b: string } */ | |
export type UnionToIntersection<U> = ( | |
U extends unknown ? (arg: U) => 0 : never | |
) extends (arg: infer I) => 0 ? I | |
: never; | |
/** LastInUnion<1 | 2> = 2 */ | |
export type LastInUnion<U> = UnionToIntersection< | |
U extends unknown ? (x: U) => 0 : never | |
> extends (x: infer L) => 0 ? L | |
: never; | |
/** UnionToTuple<1 | 2> = [1, 2] */ | |
export type UnionToTuple<U, Last = LastInUnion<U>> = [U] extends [never] ? [] | |
: [...UnionToTuple<Exclude<U, Last>>, Last]; | |
export function has<const T extends object, K extends PropertyKey>( | |
o: T, | |
k: K, | |
own?: boolean, | |
): o is T & { [P in K]: K extends keyof T ? T[K] : unknown }; | |
export function has(o: object, k: PropertyKey, own?: boolean): boolean; | |
export function has(o: object, k: PropertyKey, own?: boolean): boolean { | |
if (o != null && typeof o === "object") { | |
if ( | |
typeof k === "string" || typeof k === "symbol" || typeof k === "number" | |
) { | |
return own ? Object.hasOwn(o, k) : Reflect.has(o, k); | |
} | |
} | |
return false; | |
} | |
const $bind = Function.prototype.bind; | |
export function bindSafe< | |
T, | |
const A extends readonly unknown[], | |
const B extends readonly unknown[], | |
R = unknown, | |
// deno-lint-ignore no-explicit-any | |
U extends Record<string | symbol, any> = Record<never, never>, | |
>( | |
target: ((...args: [...A, ...B]) => R) & U, | |
thisArg: T, | |
...args: A | |
): ((this: T, ...args: B) => R) & typeof target { | |
const props = Object.getOwnPropertyDescriptors(target); | |
const value = target.name; | |
const fn = $bind.call(target, thisArg, ...args); | |
const length = Math.max(target.length - args.length, 0); | |
Reflect.defineProperty(fn, "name", { value, configurable: true }); | |
Reflect.defineProperty(fn, "length", { value: length, configurable: true }); | |
Reflect.defineProperty(fn, "toString", { | |
value: function toString(this: typeof target) { | |
let str = Function.prototype.toString.call(this); | |
const check = () => str.includes("[native code]"); | |
if (check()) str = this.toString(); | |
if (check()) str = `function anonymous() { [native code] }`; | |
return str; | |
}.bind(target), | |
}); | |
for (const key in props) { | |
// skip the constructor and name properties | |
if (key === "constructor") continue; | |
const descriptor = props[key]; | |
if (!descriptor || !descriptor.configurable) continue; | |
const { value, get, set } = descriptor; | |
if (value && typeof value === "function") { | |
// bind static methods to the original thisArg | |
// - note: this uses the new bind method, not the original, meaning it | |
// may recursively bind any nested methods all the way down the chain | |
descriptor.value = bindSafe(value, target); | |
} else { | |
// bind static getters/setters to the original thisArg | |
// - note these use the original Function.prototype.bind since they don't | |
// require any special logic here. | |
if (get && typeof get === "function") { | |
descriptor.get = $bind.call(get, target); | |
} | |
if (set && typeof set === "function") { | |
descriptor.set = $bind.call(set, target); | |
} | |
} | |
Reflect.defineProperty(fn, key, descriptor); | |
} | |
return fn; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment