From 8ee0d048f56809041f21fcc5ea3fad38e0f468c7 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 15 May 2026 10:18:04 +0900 Subject: [PATCH 1/3] feat(devframe): expose `devframe/rpc/dump` sub-export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lift dump helpers (`dumpFunctions`, `createClientFromDump`, `collectStaticRpcDump`, `serializeDumpError`, `reviveDumpError`, the `StaticRpcDump*` types, …) into a dedicated `devframe/rpc/dump` entry. The same symbols stay re-exported from `devframe/rpc` and are now flagged `@deprecated` so consumers see the migration hint in their editor; the deprecation JSDoc is re-declared per symbol so it survives tsdown's dts bundling. --- packages/devframe/package.json | 1 + packages/devframe/src/rpc/dump/index.ts | 3 ++ packages/devframe/src/rpc/index.ts | 53 +++++++++++++++++-- packages/devframe/tsdown.config.ts | 1 + .../tsnapi/devframe/rpc/dump.snapshot.d.ts | 18 +++++++ .../tsnapi/devframe/rpc/dump.snapshot.js | 11 ++++ 6 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.d.ts create mode 100644 tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.js diff --git a/packages/devframe/package.json b/packages/devframe/package.json index 1a0fdbc..536c50e 100644 --- a/packages/devframe/package.json +++ b/packages/devframe/package.json @@ -34,6 +34,7 @@ "./recipes/open-helpers": "./dist/recipes/open-helpers.mjs", "./rpc": "./dist/rpc/index.mjs", "./rpc/client": "./dist/rpc/client.mjs", + "./rpc/dump": "./dist/rpc/dump.mjs", "./rpc/server": "./dist/rpc/server.mjs", "./rpc/transports/ws-client": "./dist/rpc/transports/ws-client.mjs", "./rpc/transports/ws-server": "./dist/rpc/transports/ws-server.mjs", diff --git a/packages/devframe/src/rpc/dump/index.ts b/packages/devframe/src/rpc/dump/index.ts index 5539392..b6ce390 100644 --- a/packages/devframe/src/rpc/dump/index.ts +++ b/packages/devframe/src/rpc/dump/index.ts @@ -13,6 +13,9 @@ import { logger } from '../diagnostics' import { validateDefinitions } from '../validation' import { reviveDumpError, serializeDumpError } from './error' +export * from './error' +export * from './static' + function getDumpRecordKey(functionName: string, args: any[]): string { const argsHash = hash(args) return `${functionName}---${argsHash}` diff --git a/packages/devframe/src/rpc/index.ts b/packages/devframe/src/rpc/index.ts index 6531e5b..8201c21 100644 --- a/packages/devframe/src/rpc/index.ts +++ b/packages/devframe/src/rpc/index.ts @@ -1,10 +1,57 @@ +import type { + StaticRpcDumpCollection as _StaticRpcDumpCollection, + StaticRpcDumpFile as _StaticRpcDumpFile, + StaticRpcDumpManifest as _StaticRpcDumpManifest, + StaticRpcDumpManifestQueryEntry as _StaticRpcDumpManifestQueryEntry, + StaticRpcDumpManifestStaticEntry as _StaticRpcDumpManifestStaticEntry, + StaticRpcDumpManifestValue as _StaticRpcDumpManifestValue, + StaticRpcDumpSerialization as _StaticRpcDumpSerialization, +} from './dump/static' +import { + createClientFromDump as _createClientFromDump, + dumpFunctions as _dumpFunctions, + getDefinitionsWithDumps as _getDefinitionsWithDumps, +} from './dump' +import { + reviveDumpError as _reviveDumpError, + serializeDumpError as _serializeDumpError, +} from './dump/error' +import { + collectStaticRpcDump as _collectStaticRpcDump, +} from './dump/static' + export * from './cache' export * from './collector' export * from './define' -export * from './dump' -export * from './dump/error' -export * from './dump/static' export * from './handler' export * from './serialization' export * from './types' export * from './validation' + +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export const collectStaticRpcDump = _collectStaticRpcDump +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export const createClientFromDump = _createClientFromDump +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export const dumpFunctions = _dumpFunctions +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export const getDefinitionsWithDumps = _getDefinitionsWithDumps +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export const reviveDumpError = _reviveDumpError +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export const serializeDumpError = _serializeDumpError + +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export type StaticRpcDumpCollection = _StaticRpcDumpCollection +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export type StaticRpcDumpFile = _StaticRpcDumpFile +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export type StaticRpcDumpManifest = _StaticRpcDumpManifest +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export type StaticRpcDumpManifestQueryEntry = _StaticRpcDumpManifestQueryEntry +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export type StaticRpcDumpManifestStaticEntry = _StaticRpcDumpManifestStaticEntry +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export type StaticRpcDumpManifestValue = _StaticRpcDumpManifestValue +/** @deprecated Import from `devframe/rpc/dump` instead. */ +export type StaticRpcDumpSerialization = _StaticRpcDumpSerialization diff --git a/packages/devframe/tsdown.config.ts b/packages/devframe/tsdown.config.ts index 9c68fd5..3ee1ebd 100644 --- a/packages/devframe/tsdown.config.ts +++ b/packages/devframe/tsdown.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ 'index': 'src/index.ts', 'rpc/index': 'src/rpc/index.ts', 'rpc/client': 'src/rpc/client.ts', + 'rpc/dump': 'src/rpc/dump/index.ts', 'rpc/server': 'src/rpc/server.ts', 'rpc/transports/ws-client': 'src/rpc/transports/ws-client.ts', 'rpc/transports/ws-server': 'src/rpc/transports/ws-server.ts', diff --git a/tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.d.ts new file mode 100644 index 0000000..811447b --- /dev/null +++ b/tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.d.ts @@ -0,0 +1,18 @@ +/** + * Generated by tsnapi — public API snapshot of `devframe/rpc/dump` + */ +// #region Other +export { collectStaticRpcDump } +export { createClientFromDump } +export { dumpFunctions } +export { getDefinitionsWithDumps } +export { reviveDumpError } +export { serializeDumpError } +export { StaticRpcDumpCollection } +export { StaticRpcDumpFile } +export { StaticRpcDumpManifest } +export { StaticRpcDumpManifestQueryEntry } +export { StaticRpcDumpManifestStaticEntry } +export { StaticRpcDumpManifestValue } +export { StaticRpcDumpSerialization } +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.js b/tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.js new file mode 100644 index 0000000..9bc5766 --- /dev/null +++ b/tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.js @@ -0,0 +1,11 @@ +/** + * Generated by tsnapi — public API snapshot of `devframe/rpc/dump` + */ +// #region Other +export { collectStaticRpcDump } +export { createClientFromDump } +export { dumpFunctions } +export { getDefinitionsWithDumps } +export { reviveDumpError } +export { serializeDumpError } +// #endregion \ No newline at end of file From 16dd47771f67effd1c4629d94b2ef8aff8fa5b4c Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 15 May 2026 10:32:15 +0900 Subject: [PATCH 2/3] fix(devframe): wire `devframe/rpc/dump` alias and drop barrel cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `devframe/rpc/dump` to `alias.ts` so monorepo source/tests/examples resolve the new subpath directly without falling back to `dist/`. - Switch `rpc/dump/static.ts` to import `getRpcHandler` from `../handler` and `dumpFunctions` from `./index`, removing the cycle `rpc/dump → dump/static → devframe/rpc → rpc/dump`. - Refresh the `devframe/rpc` runtime tsnapi snapshot to reflect that the deprecated dump re-exports are now plain `const` re-declarations. --- alias.ts | 1 + packages/devframe/src/rpc/dump/static.ts | 5 +- .../tsnapi/devframe/rpc.snapshot.js | 46 +++++++++++++++---- tsconfig.base.json | 3 ++ 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/alias.ts b/alias.ts index 374c780..d0cb424 100644 --- a/alias.ts +++ b/alias.ts @@ -9,6 +9,7 @@ export const alias = { 'devframe/rpc/transports/ws-server': r('devframe/src/rpc/transports/ws-server.ts'), 'devframe/rpc/transports/ws-client': r('devframe/src/rpc/transports/ws-client.ts'), 'devframe/rpc/client': r('devframe/src/rpc/client.ts'), + 'devframe/rpc/dump': r('devframe/src/rpc/dump/index.ts'), 'devframe/rpc/server': r('devframe/src/rpc/server.ts'), 'devframe/rpc': r('devframe/src/rpc'), 'devframe/types': r('devframe/src/types/index.ts'), diff --git a/packages/devframe/src/rpc/dump/static.ts b/packages/devframe/src/rpc/dump/static.ts index 27839a2..9398100 100644 --- a/packages/devframe/src/rpc/dump/static.ts +++ b/packages/devframe/src/rpc/dump/static.ts @@ -1,8 +1,9 @@ -import type { RpcDumpRecord, RpcFunctionDefinitionAny } from 'devframe/rpc' +import type { RpcDumpRecord, RpcFunctionDefinitionAny } from '../types' import { DEVTOOLS_RPC_DUMP_DIRNAME, } from 'devframe/constants' -import { dumpFunctions, getRpcHandler } from 'devframe/rpc' +import { getRpcHandler } from '../handler' +import { dumpFunctions } from './index' export type StaticRpcDumpSerialization = 'json' | 'structured-clone' diff --git a/tests/__snapshots__/tsnapi/devframe/rpc.snapshot.js b/tests/__snapshots__/tsnapi/devframe/rpc.snapshot.js index 7d6dafa..ddf5283 100644 --- a/tests/__snapshots__/tsnapi/devframe/rpc.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/rpc.snapshot.js @@ -1,19 +1,49 @@ /** * Generated by tsnapi — public API snapshot of `devframe/rpc` */ +// #region Classes +export class RpcCacheManager { + cacheMap + options + keySerializer + constructor(_) {} + updateOptions(_) {} + cached(_, _) {} + apply(_, _) {} + validate(_) {} + clear(_) {} +} +export class RpcFunctionsCollectorBase { + context + definitions + functions + _onChanged + constructor(_) {} + register(_, _) {} + update(_, _) {} + onChanged(_) {} + async getHandler(_) {} + getSchema(_) {} + has(_) {} + get(_) {} + list() {} +} +// #endregion + +// #region Variables +export var collectStaticRpcDump /* const */ +export var createClientFromDump /* const */ +export var dumpFunctions /* const */ +export var getDefinitionsWithDumps /* const */ +export var reviveDumpError /* const */ +export var serializeDumpError /* const */ +// #endregion + // #region Other -export { collectStaticRpcDump } -export { createClientFromDump } export { createDefineWrapperWithContext } export { defineRpcFunction } -export { dumpFunctions } -export { getDefinitionsWithDumps } export { getRpcHandler } export { getRpcResolvedSetupResult } -export { reviveDumpError } -export { RpcCacheManager } -export { RpcFunctionsCollectorBase } -export { serializeDumpError } export { strictJsonStringify } export { STRUCTURED_CLONE_PREFIX } export { validateDefinition } diff --git a/tsconfig.base.json b/tsconfig.base.json index 3dcca98..ae47da5 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,9 @@ "devframe/rpc/client": [ "./packages/devframe/src/rpc/client.ts" ], + "devframe/rpc/dump": [ + "./packages/devframe/src/rpc/dump/index.ts" + ], "devframe/rpc/server": [ "./packages/devframe/src/rpc/server.ts" ], From 7b7e4efc97fcfd82f0b71d9a2f544c1275056d2e Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 15 May 2026 10:46:51 +0900 Subject: [PATCH 3/3] refactor(devframe): split dump runtime into `collect.ts`, keep `dump/index.ts` as barrel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move `dumpFunctions`, `createClientFromDump`, `getDefinitionsWithDumps` and their private helpers from `rpc/dump/index.ts` into a new `rpc/dump/collect.ts`. `index.ts` is now a three-line barrel re-exporting `./collect`, `./error`, `./static`. Repoint `static.ts` at `./collect` so the barrel does not reintroduce a `dump/static → dump → dump/static` cycle. --- packages/devframe/src/rpc/dump/collect.ts | 278 +++++++++++++++++++++ packages/devframe/src/rpc/dump/index.ts | 280 +--------------------- packages/devframe/src/rpc/dump/static.ts | 2 +- 3 files changed, 280 insertions(+), 280 deletions(-) create mode 100644 packages/devframe/src/rpc/dump/collect.ts diff --git a/packages/devframe/src/rpc/dump/collect.ts b/packages/devframe/src/rpc/dump/collect.ts new file mode 100644 index 0000000..5539392 --- /dev/null +++ b/packages/devframe/src/rpc/dump/collect.ts @@ -0,0 +1,278 @@ +import type { + BirpcReturn, + RpcDefinitionsToFunctions, + RpcDumpClientOptions, + RpcDumpCollectionOptions, + RpcDumpDefinition, + RpcDumpStore, + RpcFunctionDefinitionAny, +} from '../types' +import { hash } from 'devframe/utils/hash' +import pLimit from 'p-limit' +import { logger } from '../diagnostics' +import { validateDefinitions } from '../validation' +import { reviveDumpError, serializeDumpError } from './error' + +function getDumpRecordKey(functionName: string, args: any[]): string { + const argsHash = hash(args) + return `${functionName}---${argsHash}` +} + +function getDumpFallbackKey(functionName: string): string { + return `${functionName}---fallback` +} + +async function resolveGetter(valueOrGetter: T | (() => Promise)): Promise { + return typeof valueOrGetter === 'function' + ? await (valueOrGetter as () => Promise)() + : valueOrGetter +} + +/** + * Collects pre-computed dumps by executing functions with their defined input combinations. + * Static functions without dump config automatically get `{ inputs: [[]] }`. + * + * @example + * ```ts + * const store = await dumpFunctions([greet], context, { concurrency: 10 }) + * ``` + */ +export async function dumpFunctions< + T extends readonly RpcFunctionDefinitionAny[], +>( + definitions: T, + context?: any, + options: RpcDumpCollectionOptions = {}, +): Promise>> { + validateDefinitions(definitions) + const concurrency = options.concurrency === true + ? 5 + : options.concurrency === false || options.concurrency == null + ? 1 + : options.concurrency + + const store: RpcDumpStore = { + definitions: {}, + records: {}, + } + + // #region Definition resolution + interface TaskResolution { + handler: (...args: any[]) => any + dump: RpcDumpDefinition + definition: RpcFunctionDefinitionAny + } + + const tasksResolutions: (() => Promise)[] = definitions.map(definition => async () => { + if (definition.type === 'event' || definition.type === 'action') { + return undefined + } + + // Fresh setup results for each context to avoid caching issues + const setupResult = definition.setup + ? await Promise.resolve(definition.setup(context)) + : {} + + const handler = setupResult.handler || definition.handler + if (!handler) { + throw logger.DF0024({ name: definition.name }).throw() + } + + let dump = setupResult.dump ?? definition.dump + if (!dump && definition.type === 'static') { + dump = { inputs: [[]] } + } + if (!dump && definition.snapshot) { + // Sugar: run the handler once with no args, store the result as + // both the no-args record and the fallback. Any client call then + // resolves to the same snapshot — matching NMI's "getPayload() + // always returns the baked dump" shape. + dump = async (_ctx, h) => { + const output = await Promise.resolve(h(...([] as unknown as any[]))) + return { + records: [{ inputs: [] as any, output }], + fallback: output, + } + } + } + + if (!dump) { + return undefined + } + + if (typeof dump === 'function') { + dump = await Promise.resolve(dump(context, handler)) + } + + // Only add to definitions if it has a dump + store.definitions[definition.name] = { + name: definition.name, + type: definition.type, + } + + return { + handler, + dump, + definition, + } + }) + + let functionsToDump: TaskResolution[] = [] + if (concurrency <= 1) { + for (const task of tasksResolutions) { + const resolution = await task() + if (resolution) { + functionsToDump.push(resolution) + } + } + } + else { + const limit = pLimit(concurrency) + functionsToDump = (await Promise.all(tasksResolutions.map(task => limit(task)))).filter(x => !!x) + } + // #endregion + + // #region Dump execution + const dumpTasks: Array<() => Promise> = [] + for (const { definition, handler, dump } of functionsToDump) { + const { inputs, records, fallback } = dump + + // Add pre-defined records + if (records) { + for (const record of records) { + const recordKey = getDumpRecordKey(definition.name, record.inputs) + store.records[recordKey] = record + } + } + + // Add fallback record + if ('fallback' in dump) { + const fallbackKey = getDumpFallbackKey(definition.name) + store.records[fallbackKey] = { + inputs: [], + output: fallback, + } + } + + // Add input records execution tasks + if (inputs) { + for (const input of inputs) { + dumpTasks.push(async () => { + const recordKey = getDumpRecordKey(definition.name, input) + + try { + const output = await Promise.resolve(handler(...input)) + store.records[recordKey] = { + inputs: input, + output, + } + } + catch (error: unknown) { + store.records[recordKey] = { + inputs: input, + error: serializeDumpError(error), + } + } + }) + } + } + } + + if (concurrency <= 1) { + for (const task of dumpTasks) { + await task() + } + } + else { + const limit = pLimit(concurrency) + await Promise.all(dumpTasks.map(task => limit(task))) + } + // #endregion + + return store +} + +/** + * Creates a client that serves pre-computed results from a dump store. + * Uses argument hashing to match calls to stored records. + * + * @example + * ```ts + * const client = createClientFromDump(store) + * await client.greet('Alice') + * ``` + */ +export function createClientFromDump>( + store: RpcDumpStore, + options: RpcDumpClientOptions = {}, +): BirpcReturn { + const { onMiss } = options + + const client = new Proxy({} as T, { + get(_, functionName: string) { + if (!(functionName in store.definitions)) { + throw logger.DF0025({ name: functionName }).throw() + } + + return async (...args: any[]) => { + const recordKey = getDumpRecordKey(functionName, args) + + const recordOrGetter = store.records[recordKey] + + if (recordOrGetter) { + const record = await resolveGetter(recordOrGetter) + + if (record.error) { + throw reviveDumpError(record.error) + } + + if (typeof record.output === 'function') { + return await record.output() + } + + return record.output + } + + onMiss?.(functionName, args) + + const fallbackKey = getDumpFallbackKey(functionName) + if (fallbackKey in store.records) { + const fallbackOrGetter = store.records[fallbackKey] + + const fallbackRecord = await resolveGetter(fallbackOrGetter) + + if (fallbackRecord && typeof fallbackRecord.output === 'function') { + return await fallbackRecord.output() + } + if (fallbackRecord) + return fallbackRecord.output + } + + throw logger.DF0026({ name: functionName, args: JSON.stringify(args) }).throw() + } + }, + has(_, functionName: string) { + return functionName in store.definitions + }, + ownKeys() { + return Object.keys(store.definitions) + }, + getOwnPropertyDescriptor(_, functionName: string) { + return functionName in store.definitions + ? { configurable: true, enumerable: true, value: undefined } + : undefined + }, + }) + + return client as any as BirpcReturn +} + +/** + * Filters function definitions to only those with dump definitions. + * Note: Only checks the definition itself, not setup results. + */ +export function getDefinitionsWithDumps( + definitions: T, +): RpcFunctionDefinitionAny[] { + return definitions.filter(def => def.dump !== undefined) +} diff --git a/packages/devframe/src/rpc/dump/index.ts b/packages/devframe/src/rpc/dump/index.ts index b6ce390..c6c377c 100644 --- a/packages/devframe/src/rpc/dump/index.ts +++ b/packages/devframe/src/rpc/dump/index.ts @@ -1,281 +1,3 @@ -import type { - BirpcReturn, - RpcDefinitionsToFunctions, - RpcDumpClientOptions, - RpcDumpCollectionOptions, - RpcDumpDefinition, - RpcDumpStore, - RpcFunctionDefinitionAny, -} from '../types' -import { hash } from 'devframe/utils/hash' -import pLimit from 'p-limit' -import { logger } from '../diagnostics' -import { validateDefinitions } from '../validation' -import { reviveDumpError, serializeDumpError } from './error' - +export * from './collect' export * from './error' export * from './static' - -function getDumpRecordKey(functionName: string, args: any[]): string { - const argsHash = hash(args) - return `${functionName}---${argsHash}` -} - -function getDumpFallbackKey(functionName: string): string { - return `${functionName}---fallback` -} - -async function resolveGetter(valueOrGetter: T | (() => Promise)): Promise { - return typeof valueOrGetter === 'function' - ? await (valueOrGetter as () => Promise)() - : valueOrGetter -} - -/** - * Collects pre-computed dumps by executing functions with their defined input combinations. - * Static functions without dump config automatically get `{ inputs: [[]] }`. - * - * @example - * ```ts - * const store = await dumpFunctions([greet], context, { concurrency: 10 }) - * ``` - */ -export async function dumpFunctions< - T extends readonly RpcFunctionDefinitionAny[], ->( - definitions: T, - context?: any, - options: RpcDumpCollectionOptions = {}, -): Promise>> { - validateDefinitions(definitions) - const concurrency = options.concurrency === true - ? 5 - : options.concurrency === false || options.concurrency == null - ? 1 - : options.concurrency - - const store: RpcDumpStore = { - definitions: {}, - records: {}, - } - - // #region Definition resolution - interface TaskResolution { - handler: (...args: any[]) => any - dump: RpcDumpDefinition - definition: RpcFunctionDefinitionAny - } - - const tasksResolutions: (() => Promise)[] = definitions.map(definition => async () => { - if (definition.type === 'event' || definition.type === 'action') { - return undefined - } - - // Fresh setup results for each context to avoid caching issues - const setupResult = definition.setup - ? await Promise.resolve(definition.setup(context)) - : {} - - const handler = setupResult.handler || definition.handler - if (!handler) { - throw logger.DF0024({ name: definition.name }).throw() - } - - let dump = setupResult.dump ?? definition.dump - if (!dump && definition.type === 'static') { - dump = { inputs: [[]] } - } - if (!dump && definition.snapshot) { - // Sugar: run the handler once with no args, store the result as - // both the no-args record and the fallback. Any client call then - // resolves to the same snapshot — matching NMI's "getPayload() - // always returns the baked dump" shape. - dump = async (_ctx, h) => { - const output = await Promise.resolve(h(...([] as unknown as any[]))) - return { - records: [{ inputs: [] as any, output }], - fallback: output, - } - } - } - - if (!dump) { - return undefined - } - - if (typeof dump === 'function') { - dump = await Promise.resolve(dump(context, handler)) - } - - // Only add to definitions if it has a dump - store.definitions[definition.name] = { - name: definition.name, - type: definition.type, - } - - return { - handler, - dump, - definition, - } - }) - - let functionsToDump: TaskResolution[] = [] - if (concurrency <= 1) { - for (const task of tasksResolutions) { - const resolution = await task() - if (resolution) { - functionsToDump.push(resolution) - } - } - } - else { - const limit = pLimit(concurrency) - functionsToDump = (await Promise.all(tasksResolutions.map(task => limit(task)))).filter(x => !!x) - } - // #endregion - - // #region Dump execution - const dumpTasks: Array<() => Promise> = [] - for (const { definition, handler, dump } of functionsToDump) { - const { inputs, records, fallback } = dump - - // Add pre-defined records - if (records) { - for (const record of records) { - const recordKey = getDumpRecordKey(definition.name, record.inputs) - store.records[recordKey] = record - } - } - - // Add fallback record - if ('fallback' in dump) { - const fallbackKey = getDumpFallbackKey(definition.name) - store.records[fallbackKey] = { - inputs: [], - output: fallback, - } - } - - // Add input records execution tasks - if (inputs) { - for (const input of inputs) { - dumpTasks.push(async () => { - const recordKey = getDumpRecordKey(definition.name, input) - - try { - const output = await Promise.resolve(handler(...input)) - store.records[recordKey] = { - inputs: input, - output, - } - } - catch (error: unknown) { - store.records[recordKey] = { - inputs: input, - error: serializeDumpError(error), - } - } - }) - } - } - } - - if (concurrency <= 1) { - for (const task of dumpTasks) { - await task() - } - } - else { - const limit = pLimit(concurrency) - await Promise.all(dumpTasks.map(task => limit(task))) - } - // #endregion - - return store -} - -/** - * Creates a client that serves pre-computed results from a dump store. - * Uses argument hashing to match calls to stored records. - * - * @example - * ```ts - * const client = createClientFromDump(store) - * await client.greet('Alice') - * ``` - */ -export function createClientFromDump>( - store: RpcDumpStore, - options: RpcDumpClientOptions = {}, -): BirpcReturn { - const { onMiss } = options - - const client = new Proxy({} as T, { - get(_, functionName: string) { - if (!(functionName in store.definitions)) { - throw logger.DF0025({ name: functionName }).throw() - } - - return async (...args: any[]) => { - const recordKey = getDumpRecordKey(functionName, args) - - const recordOrGetter = store.records[recordKey] - - if (recordOrGetter) { - const record = await resolveGetter(recordOrGetter) - - if (record.error) { - throw reviveDumpError(record.error) - } - - if (typeof record.output === 'function') { - return await record.output() - } - - return record.output - } - - onMiss?.(functionName, args) - - const fallbackKey = getDumpFallbackKey(functionName) - if (fallbackKey in store.records) { - const fallbackOrGetter = store.records[fallbackKey] - - const fallbackRecord = await resolveGetter(fallbackOrGetter) - - if (fallbackRecord && typeof fallbackRecord.output === 'function') { - return await fallbackRecord.output() - } - if (fallbackRecord) - return fallbackRecord.output - } - - throw logger.DF0026({ name: functionName, args: JSON.stringify(args) }).throw() - } - }, - has(_, functionName: string) { - return functionName in store.definitions - }, - ownKeys() { - return Object.keys(store.definitions) - }, - getOwnPropertyDescriptor(_, functionName: string) { - return functionName in store.definitions - ? { configurable: true, enumerable: true, value: undefined } - : undefined - }, - }) - - return client as any as BirpcReturn -} - -/** - * Filters function definitions to only those with dump definitions. - * Note: Only checks the definition itself, not setup results. - */ -export function getDefinitionsWithDumps( - definitions: T, -): RpcFunctionDefinitionAny[] { - return definitions.filter(def => def.dump !== undefined) -} diff --git a/packages/devframe/src/rpc/dump/static.ts b/packages/devframe/src/rpc/dump/static.ts index 9398100..2120d05 100644 --- a/packages/devframe/src/rpc/dump/static.ts +++ b/packages/devframe/src/rpc/dump/static.ts @@ -3,7 +3,7 @@ import { DEVTOOLS_RPC_DUMP_DIRNAME, } from 'devframe/constants' import { getRpcHandler } from '../handler' -import { dumpFunctions } from './index' +import { dumpFunctions } from './collect' export type StaticRpcDumpSerialization = 'json' | 'structured-clone'