export interface FunctionProxyState { readonly proxy: T readonly target: T } export interface PatchedFunctionProxyState< Key extends PropertyKey = PropertyKey, Value extends UnknownFunction = UnknownFunction, > extends FunctionProxyState { /** Must have a function valued property whose key is {@link key}. */ readonly parent: Record readonly key: Key before: HookNode> | undefined instead: InsteadHookNode | undefined after: HookNode> | undefined } export interface HookNode { /** Checked so that hook node lists are not cleared if an unpatch function is called more than once. */ unpatched: boolean /** Can be undefined only in instances that are not in a hook node list. */ hook: Hook | undefined prev: HookNode | undefined next: HookNode | undefined } export interface InsteadHookNode extends HookNode>, FunctionProxyState { readonly hook: InsteadHook prev: InsteadHookNode | undefined next: InsteadHookNode | undefined } /** @throws {TypeError} When `hookNode` is defined, its hook and that of all other hook nodes in its list must also be defined. */ function applyHooks(hookNode: HookNode<(arg: T) => T> | undefined, arg: T) { while (hookNode !== undefined) { // Save a reference to the next hook node in case the current hook calls unpatch. const { next, hook } = hookNode // Indirectly call the hook so as to not give it access to its hook node. arg = hook!(arg) hookNode = next } return arg } export const patchedFunctionProxyHandler = { apply( state: PatchedFunctionProxyState, receiver: ThisParameterType, args: Parameters, ) { args = applyHooks(state.before, args) const { instead } = state let result: ReturnType = Reflect.apply(instead === undefined ? state.target : instead.proxy, receiver, args) result = applyHooks(state.after, result) return result }, construct>( state: PatchedFunctionProxyState, args: ConstructorParameters, ctor: AbstractNewable, ) { args = applyHooks(state.before, args) const { instead } = state let result: InstanceType = Reflect.construct( instead === undefined ? state.target : instead.proxy, args, ctor, ) result = applyHooks(state.after, result) return result }, defineProperty: (state, key, descriptor) => Reflect.defineProperty(state.target, key, descriptor), deleteProperty: (state, key) => Reflect.deleteProperty(state.target, key), get: (state, key, receiver: unknown) => Reflect.get(state.target, key, receiver), getOwnPropertyDescriptor: (state, key) => Reflect.getOwnPropertyDescriptor(state.target, key), getPrototypeOf: state => Reflect.getPrototypeOf(state.target), has: (state, key) => Reflect.has(state.target, key), isExtensible: state => Reflect.isExtensible(state.target), ownKeys: state => Reflect.ownKeys(state.target), preventExtensions: state => Reflect.preventExtensions(state.target), set: (state, key, value: unknown, receiver: unknown) => Reflect.set(state.target, key, value, receiver), setPrototypeOf: (state, prototype) => Reflect.setPrototypeOf(state.target, prototype), } as const satisfies Required> interface PatchedFunctionProxyStateMap extends WeakMap { readonly delete: (key: UnknownFunction) => boolean readonly get: (key: K) => PatchedFunctionProxyState | undefined readonly has: (key: UnknownFunction) => boolean readonly set: (key: K, value: PatchedFunctionProxyState) => this } /** proxy -> state */ export const patchedFunctionProxyStates: PatchedFunctionProxyStateMap = new WeakMap() export function createPatchedFunctionProxy( target: Value, parent: Record, key: FiniteDomain, before: HookNode> | undefined, instead: InsteadHookNode | undefined, after: HookNode> | undefined, ): PatchedFunctionProxyState { // biome-ignore lint/complexity/useArrowFunction: We need a function that has a constructor const state = function () {} // The handler makes the proxy behave as state.target. const proxy: Value = new Proxy(state, patchedFunctionProxyHandler) as any state.proxy = proxy state.target = target state.parent = parent state.key = key state.before = before state.instead = instead state.after = after patchedFunctionProxyStates.set(proxy, state) parent[key] = proxy return state } export function unproxy(state: PatchedFunctionProxyState) { const { parent, key } = state if (parent[key] === state.proxy) parent[key] = state.target }