import { assert } from './ReplayProtocolClient'; type UnknownFunction = (...args: never) => unknown; type DropDependencies = F extends (dependencies: any, ...args: infer A) => infer R ? (...args: A) => R : never; type CallableDependencies> = { [K in keyof Dependencies]: 'fn' extends keyof Dependencies[K] ? Dependencies[K]['fn'] extends UnknownFunction ? DropDependencies : never : Dependencies[K]; }; interface InjectableFunction< Dependencies extends Record = any, Args extends unknown[] = never, R = unknown, > { dependencies: Dependencies; fn: (dependencies: CallableDependencies, ...args: Args) => R; asCallString: (...args: Args) => string; } type Dependency = UnknownFunction | InjectableFunction; function getAllDependencies( dependencies: Record, bindableNames: Set, allDependencies: Record = {}, ) { for (const [key, value] of Object.entries(dependencies)) { if ('fn' in value) { allDependencies[key] = value.fn; bindableNames.add(key); getAllDependencies(value.dependencies, bindableNames, allDependencies); } else { allDependencies[key] = value; } } return allDependencies; } function bindDependencies(dependencies: Record, bindableNames: string[]) { for (const name of bindableNames) { dependencies[name] = dependencies[name].bind(null, dependencies); } return dependencies; } function serializeValue(value: unknown): string { switch (typeof value) { case 'symbol': throw new Error("Symbols can't be serialized"); case 'function': throw new Error('Functions should be injected as dependencies'); case 'undefined': return 'undefined'; case 'object': if (Array.isArray(value)) { return `[${value.map(serializeValue).join(', ')}]`; } // fallthrough default: return JSON.stringify(value); } } function asCallString(dependencies: Record, fn: UnknownFunction, ...args: unknown[]) { const bindableNames = new Set(); const dependenciesString = Object.entries(getAllDependencies(dependencies, bindableNames)) .map(([key, value]) => `${JSON.stringify(key)}: ${value}`) .join(',\n'); const boundDependencies = `(${bindDependencies})({\n${dependenciesString}\n}, [${Array.from(bindableNames) .map((name) => JSON.stringify(name)) .join(', ')}])`; const argsString = args.map(serializeValue).join(', '); return `(${fn})(${boundDependencies}, ${argsString})`; } function validateDependencies(rootDependencies: Record, dependencies: Record) { for (const [key, value] of Object.entries(dependencies)) { assert( !(key in rootDependencies) || rootDependencies[key] === dependencies[key], `"${key}" dependency is not the same as the root dependency at the same key`, ); if ('dependencies' in value) { validateDependencies(rootDependencies, value.dependencies); } } } export function createInjectableFunction, Args extends unknown[], R>( dependencies: Dependencies, fn: (dependencies: CallableDependencies, ...args: Args) => R, ): InjectableFunction { validateDependencies(dependencies, dependencies); return { dependencies, fn, asCallString: asCallString.bind(null, dependencies, fn), }; }