@actuallab/fusion-rpc 15.0.17 → 15.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/fusion-hub.ts","../src/rpc-outbound-compute-call.ts"],"sourcesContent":["export {\r\n FusionHub,\r\n defineComputeService,\r\n FUSION_CALL_TYPE_ID,\r\n} from './fusion-hub.js';\r\nexport { RpcOutboundComputeCall } from './rpc-outbound-compute-call.js';\r\n","// .NET counterparts:\r\n// RpcComputeServiceDef — extends RpcServiceDef with ComputedOptionsProvider.\r\n// RpcComputeMethodDef — extends RpcMethodDef with ComputedOptions and sets\r\n// CallType = RpcComputeCallType (custom call type with its own\r\n// RpcOutboundComputeCall / RpcInboundComputeCall factories).\r\n// RpcInboundComputeCallHandler — an IRpcMiddleware that wraps inbound compute\r\n// calls in a ComputeContext, captures the Computed<T> instance, and stores\r\n// it on RpcInboundComputeCall.Computed for invalidation tracking.\r\n// RemoteComputeServiceInterceptor — the client-side interceptor that creates\r\n// compute-aware outbound calls, tracking server invalidation signals.\r\n//\r\n// In .NET this is split across ~10 files using DI, middleware, interceptors,\r\n// and the RpcComputeCallType custom call type. TS consolidates it into a\r\n// single FusionHub subclass with two method overrides.\r\n//\r\n// Omitted from .NET:\r\n// - RpcComputeCallType / typed RpcInboundComputeCall<T> / RpcOutboundComputeCall<T> —\r\n// .NET uses a separate call type with custom factories, result handling, and\r\n// stage tracking. TS extends the base RpcOutboundComputeCall with a\r\n// whenInvalidated promise (sufficient for the TS use case).\r\n// - ComputedOptionsProvider / ComputedOptions per method — .NET resolves\r\n// per-method caching/invalidation options via DI. TS uses default\r\n// ComputeFunction behavior for all methods.\r\n// - RpcInboundComputeCallHandler middleware — .NET wraps the inbound call in\r\n// ComputeContext using the middleware pipeline. TS wraps it directly in\r\n// _wrapServerMethod by calling cf.invoke() (which sets up\r\n// ComputeContext internally).\r\n// - LocalExecutionMode / ConstrainedEntry — .NET controls whether compute calls\r\n// can execute locally or must go remote (for Fusion's consistency guarantees).\r\n// TS always executes locally (server-side) or remotely (client-side).\r\n// - Invalidation via RpcInboundComputeCall.Computed.Invalidated event — .NET\r\n// attaches an invalidation callback to the captured Computed<T> that calls\r\n// SystemCallSender.Complete(peer, call, invalidatedResult). TS uses\r\n// $sys-c.Invalidate system call instead (equivalent wire effect).\r\n// - RpcOutboundComputeCall.SetError → SetInvalidatedUnsafe — .NET invalidates\r\n// the local computed when an error or reroute occurs. TS resolves\r\n// whenInvalidated on disconnect via rejectAll().\r\n\r\nimport {\r\n RpcHub,\r\n RpcServerPeer,\r\n RpcWebSocketConnection,\r\n RpcSystemCallHandler,\r\n RpcSystemCallSender,\r\n RpcSystemCalls,\r\n defineRpcService,\r\n wireMethodName,\r\n type RpcConnection,\r\n type RpcSerializationFormat,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcDispatchContext,\r\n type RpcPeer,\r\n type RpcMessage,\r\n type RpcCallOptions,\r\n getMethodsMeta,\r\n} from '@actuallab/rpc';\r\nimport {\r\n ComputeFunction,\r\n ComputeContext,\r\n type Computed,\r\n type ComputeFunctionImpl,\r\n} from '@actuallab/fusion';\r\nimport {\r\n AsyncContext,\r\n abortSignalKey,\r\n cancellationError,\r\n isCancellation,\r\n} from '@actuallab/core';\r\nimport { RpcOutboundComputeCall } from './rpc-outbound-compute-call.js';\r\n\r\n/** Call type ID for Fusion compute calls — matches .NET's RpcComputeCallType.Id. */\r\nexport const FUSION_CALL_TYPE_ID = 1;\r\n\r\n/** Wire method name for invalidation system call. */\r\nconst FUSION_INVALIDATE_METHOD = '$sys-c.Invalidate:0';\r\n\r\n/** Max attempts for a compute call cancelled server-side before the result —\r\n * mirrors .NET ComputedCancellationReprocessingOptions.ClientDefault.MaxTryCount. */\r\nconst COMPUTE_CALL_MAX_TRY_COUNT = 3;\r\n\r\n/** Handles Fusion-specific system calls ($sys-c.Invalidate), delegating the rest to base. */\r\nclass FusionSystemCallHandler extends RpcSystemCallHandler {\r\n override handle(message: RpcMessage, args: unknown[], peer: RpcPeer): void {\r\n const method = message.Method;\r\n const relatedId = message.RelatedId ?? 0;\r\n\r\n // The caller dropped its computed, so we no longer owe it an invalidation.\r\n // Released here rather than off the inbound call's abort signal because a\r\n // completed inbound call may already have been evicted from the tracker.\r\n if (method === RpcSystemCalls.cancel)\r\n releaseInboundComputed(peer, relatedId);\r\n\r\n if (method === FUSION_INVALIDATE_METHOD) {\r\n // C# RpcComputeSystemCalls.Invalidate: Get + type check BEFORE any\r\n // removal — a non-compute call with this id must not be evicted.\r\n const call = peer.outboundCalls.get(relatedId);\r\n if (call instanceof RpcOutboundComputeCall) {\r\n peer.outboundCalls.remove(relatedId);\r\n // A pre-result invalidation settles the call as cancelled\r\n // (SetInvalidated → ResultSource.TrySetCanceled): the compute\r\n // client path treats the cancellation as a transparent retry.\r\n if (!call.result.isCompleted)\r\n call.result.reject(cancellationError('Compute call invalidated before result.'));\r\n call.whenInvalidated.resolve();\r\n }\r\n return;\r\n }\r\n\r\n super.handle(message, args, peer);\r\n }\r\n}\r\n\r\n/** Sends the Fusion $sys-c.Invalidate system call — mirrors .NET's RpcComputeSystemCallSender. */\r\nclass FusionSystemCallSender extends RpcSystemCallSender {\r\n invalidate(\r\n conn: RpcConnection,\r\n format: RpcSerializationFormat,\r\n relatedId: number\r\n ): void {\r\n this._send(conn, format, {\r\n Method: FUSION_INVALIDATE_METHOD,\r\n RelatedId: relatedId,\r\n });\r\n }\r\n}\r\n\r\n/** Creates a compute service definition — all methods default to FUSION_CALL_TYPE_ID. */\r\nexport function defineComputeService(\r\n name: string,\r\n methods: Record<string, RpcMethodDefInput>\r\n): RpcServiceDef {\r\n const withCallType: Record<string, RpcMethodDefInput> = {};\r\n for (const [methodName, input] of Object.entries(methods)) {\r\n withCallType[methodName] = {\r\n ...input,\r\n callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID,\r\n };\r\n }\r\n return defineRpcService(name, withCallType);\r\n}\r\n\r\n/** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */\r\nexport class FusionHub extends RpcHub {\r\n declare readonly systemCallSender: FusionSystemCallSender;\r\n\r\n constructor(hubId?: string) {\r\n super(hubId);\r\n this.systemCallHandler = new FusionSystemCallHandler();\r\n // Register Fusion-specific system call for compact format hash resolution\r\n this.registry.register(FUSION_INVALIDATE_METHOD);\r\n }\r\n\r\n protected override _createSystemCallSender(): FusionSystemCallSender {\r\n return new FusionSystemCallSender();\r\n }\r\n\r\n /** Accept an incoming WebSocket and create a server peer. */\r\n acceptConnection(ws: WebSocketLike): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const conn = new RpcWebSocketConnection(ws);\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Accept an RpcConnection and create a server peer. */\r\n acceptRpcConnection(conn: RpcConnection): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Delegate to the base builder, then patch callTypeId for compute methods —\r\n * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */\r\n protected override _buildServiceDef(\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n cls: abstract new (...args: any[]) => any\r\n ): RpcServiceDef {\r\n const def = super._buildServiceDef(cls);\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n const methods = new Map<string, RpcMethodDef>();\r\n for (const [mapKey, methodDef] of def.methods) {\r\n const isCompute =\r\n (methodsMeta[methodDef.name] as { compute?: boolean } | undefined)\r\n ?.compute === true;\r\n methods.set(\r\n mapKey,\r\n isCompute\r\n ? { ...methodDef, callTypeId: FUSION_CALL_TYPE_ID }\r\n : methodDef\r\n );\r\n }\r\n\r\n return { name: def.name, methods };\r\n }\r\n\r\n protected override _wrapServerMethod(\r\n methodDef: RpcMethodDef,\r\n fn: (...args: unknown[]) => unknown,\r\n impl: object\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID) return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(\r\n methodDef.name,\r\n fn as ComputeFunctionImpl\r\n );\r\n return async (...args: unknown[]) => {\r\n const context = extractDispatchContext(args);\r\n // Truncate to the declared arity: ComputeFunction.buildKey folds every argument\r\n // into the cache key, so an extra wire argument would otherwise give the same\r\n // logical call a fresh key and re-run the handler on every request (I3).\r\n const cleanArgs = args.slice(0, methodDef.argCount);\r\n\r\n const computed = await cf.invoke(impl, cleanArgs);\r\n\r\n // IsRegularCall parity (F7): a Regular call to a compute method returns\r\n // the result immediately, with no invalidation tracking.\r\n if (context?.callType === FUSION_CALL_TYPE_ID) {\r\n const { peer, callId } = context;\r\n // ComputedRegistry holds computeds weakly, so without this the only\r\n // reference left after the wrapper returns is the whenInvalidated\r\n // subscription stored on the computed itself — GC then takes both and\r\n // the caller never learns its value went stale (I1). .NET's equivalent\r\n // is the strong RpcInboundComputeCall.Computed, released on Unregister.\r\n retainInboundComputed(peer, callId, computed);\r\n // ProcessStage2 parity (F1): whenInvalidated() resolves immediately\r\n // for an already-invalidated computed, so an invalidation landing\r\n // during computation still produces the send.\r\n void computed.whenInvalidated(context.signal).then(\r\n () => {\r\n releaseInboundComputed(peer, callId);\r\n // C# sends the result (ProcessStage1Plus) before the\r\n // invalidation (ProcessStage2). Here the dispatch loop sends\r\n // $sys.Ok after this wrapper returns, so defer past that turn —\r\n // a client drops an Invalidate that precedes its result.\r\n setTimeout(() => {\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n this.systemCallSender.invalidate(\r\n conn,\r\n peer.serializationFormat,\r\n callId\r\n );\r\n }, 0);\r\n },\r\n () => releaseInboundComputed(peer, callId)\r\n );\r\n }\r\n\r\n return computed.value;\r\n };\r\n }\r\n\r\n protected override _createClientMethod(\r\n peer: RpcPeer,\r\n methodDef: RpcMethodDef\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return super._createClientMethod(peer, methodDef);\r\n\r\n const wireName = wireMethodName(methodDef);\r\n // Local ComputeFunction whose impl makes an RPC call\r\n const rpcImpl: ComputeFunctionImpl = async function (\r\n ...args: unknown[]\r\n ) {\r\n // ComputeFunction.invoke threads its child AsyncContext as a trailing argument —\r\n // strip it before the wire call so it's never serialized, and use it to resolve\r\n // the ComputeContext + caller AbortSignal synchronously (AsyncContext.current\r\n // won't survive the await below without ALS, K3).\r\n const last = args[args.length - 1];\r\n const threadedCtx = last instanceof AsyncContext ? last : undefined;\r\n const callArgs = threadedCtx ? args.slice(0, -1) : args;\r\n const current = threadedCtx ?? AsyncContext.current;\r\n const computeCtx = ComputeContext.from(current);\r\n const signal = current?.get(abortSignalKey);\r\n const callOptions: RpcCallOptions = {\r\n callTypeId: FUSION_CALL_TYPE_ID,\r\n outboundCallFactory: (id, m) =>\r\n new RpcOutboundComputeCall(id, m),\r\n signal,\r\n };\r\n\r\n // C# RemoteComputeMethodFunction: a server-side (pre-result)\r\n // cancellation is retried transparently, bounded by MaxTryCount.\r\n let tryIndex = 0;\r\n for (;;) {\r\n // Node's AsyncLocalStorage stamps every promise created under a store with\r\n // a strong reference to it, and the call's `result`/`whenInvalidated` promises\r\n // outlive the computation inside peer.outboundCalls — created here they would\r\n // pin the ambient ComputeContext, and with it the computed (I10).\r\n const outboundCall = AsyncContext.empty.run(() =>\r\n peer.call(wireName, callArgs, callOptions)\r\n ) as RpcOutboundComputeCall;\r\n try {\r\n const value = await outboundCall.result;\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n return value;\r\n } catch (e) {\r\n // Caller-initiated abort: never retry, never cache (K6).\r\n if (signal?.aborted)\r\n throw e;\r\n // Server-side pre-result invalidation: retry while connected.\r\n if (\r\n isCancellation(e) &&\r\n peer.isConnected &&\r\n ++tryIndex < COMPUTE_CALL_MAX_TRY_COUNT\r\n )\r\n continue;\r\n\r\n // Genuine remote error: still track server invalidation so\r\n // the error computed is refreshed (C# SetError parity, F3).\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n throw e;\r\n }\r\n }\r\n };\r\n const cf = new ComputeFunction(methodDef.name, rpcImpl);\r\n const syntheticInstance = {};\r\n return (...args: unknown[]) => {\r\n const last = args[args.length - 1];\r\n const sliced = args.slice(0, methodDef.argCount);\r\n const invokeArgs =\r\n last instanceof AsyncContext ? [...sliced, last] : sliced;\r\n return cf.invoke(syntheticInstance, invokeArgs).then(c => c.value);\r\n };\r\n }\r\n}\r\n\r\n// Strong Computed references the server owes an invalidation for, keyed by inbound\r\n// call id. Per peer, so they die with the peer even if the caller never releases them.\r\nconst inboundComputeds = new WeakMap<RpcPeer, Map<number, Computed<unknown>>>();\r\n\r\nfunction retainInboundComputed(\r\n peer: RpcPeer,\r\n callId: number,\r\n computed: Computed<unknown>\r\n): void {\r\n let computeds = inboundComputeds.get(peer);\r\n if (computeds === undefined) {\r\n computeds = new Map<number, Computed<unknown>>();\r\n inboundComputeds.set(peer, computeds);\r\n }\r\n computeds.set(callId, computed);\r\n}\r\n\r\nfunction releaseInboundComputed(peer: RpcPeer, callId: number): void {\r\n inboundComputeds.get(peer)?.delete(callId);\r\n}\r\n\r\n// The TS analogue of .NET's `~RemoteComputed() => Dispose()`: the computed becoming\r\n// unreachable is what unregisters its outbound call and notifies the server.\r\nconst outboundCallReleaser = new FinalizationRegistry<{\r\n peer: RpcPeer;\r\n call: RpcOutboundComputeCall;\r\n}>(({ peer, call }) => {\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n});\r\n\r\n// Wires both directions of the client computed ↔ outbound compute call binding:\r\n// - server invalidation → local computed invalidation (BindWhenInvalidatedToCall)\r\n// - local computed invalidation → release the call + $sys.Cancel, unless the\r\n// server already invalidated it (BindToCallFromOnInvalidated parity, F4).\r\nfunction bindComputeCall(\r\n peer: RpcPeer,\r\n computeCtx: ComputeContext | undefined,\r\n call: RpcOutboundComputeCall\r\n): void {\r\n const computed = computeCtx?.computed;\r\n if (computed === undefined)\r\n return;\r\n\r\n // Two things conspire to pin `computed` here, both fixed below (I10):\r\n // - V8 gives every closure declared in one scope the same Context object, so the\r\n // call-side reaction must live in its own function or it reaches `computed`\r\n // strongly through the computed-side one;\r\n // - Node's AsyncLocalStorage stamps every promise created under a store with a strong\r\n // reference to it, so a reaction created under the ambient ComputeContext pins the\r\n // very computed we only mean to hold weakly.\r\n const computedRef = new WeakRef(computed);\r\n AsyncContext.empty.run(() => bindCallToComputed(call, computedRef));\r\n AsyncContext.empty.run(() => bindComputedToCall(peer, call, computed));\r\n outboundCallReleaser.register(computed, { peer, call }, computed);\r\n}\r\n\r\n// A compute call stays registered in peer.outboundCalls after $sys.Ok, so this reaction\r\n// must reach the computed weakly — a strong capture pins it and its whole result payload\r\n// from a GC root, and ComputedRegistry's WeakRef can then never be reclaimed (I10).\r\nfunction bindCallToComputed(\r\n call: RpcOutboundComputeCall,\r\n computedRef: WeakRef<Computed<unknown>>\r\n): void {\r\n void call.whenInvalidated.then(() => computedRef.deref()?.invalidate());\r\n}\r\n\r\nfunction bindComputedToCall(\r\n peer: RpcPeer,\r\n call: RpcOutboundComputeCall,\r\n computed: Computed<unknown>\r\n): void {\r\n void computed.whenInvalidated().then(() => {\r\n outboundCallReleaser.unregister(computed);\r\n if (call.whenInvalidated.isCompleted)\r\n return;\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n });\r\n}\r\n\r\nfunction extractDispatchContext(\r\n args: unknown[]\r\n): RpcDispatchContext | undefined {\r\n const last = args[args.length - 1];\r\n if (last !== null && typeof last === 'object' && '__rpcDispatch' in last) {\r\n return last as RpcDispatchContext;\r\n }\r\n return undefined;\r\n}\r\n","import { PromiseSource } from '@actuallab/core';\r\nimport { RpcOutboundCall } from '@actuallab/rpc';\r\n\r\n/** Tracks a pending outbound compute RPC call — stays in tracker until invalidated. */\r\nexport class RpcOutboundComputeCall extends RpcOutboundCall {\r\n override readonly removeOnOk = false;\r\n readonly whenInvalidated = new PromiseSource<void>();\r\n\r\n // A completed compute call is always self-invalidated on reconnect (the\r\n // documented invalidate-on-reconnect simplification, F6), so it must not be\r\n // reported to the server in `$sys.Reconnect` — that would re-arm stage-2\r\n // invalidation tracking for a computed we're about to drop. Report null,\r\n // mirroring C# RpcOutboundComputeCall.GetReconnectStage's peer-change branch.\r\n override getReconnectStage(isPeerChanged: boolean): number | null {\r\n if (this.result.isCompleted)\r\n return null;\r\n\r\n return super.getReconnectStage(isPeerChanged);\r\n }\r\n\r\n override onDisconnect(): void {\r\n this.whenInvalidated.resolve();\r\n }\r\n}\r\n"],"mappings":"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","names":["import_rpc","import_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/fusion-hub.ts","../src/rpc-outbound-compute-call.ts"],"sourcesContent":["export {\r\n FusionHub,\r\n defineComputeService,\r\n FUSION_CALL_TYPE_ID,\r\n} from './fusion-hub.js';\r\nexport { RpcOutboundComputeCall } from './rpc-outbound-compute-call.js';\r\n","// .NET counterparts:\r\n// RpcComputeServiceDef — extends RpcServiceDef with ComputedOptionsProvider.\r\n// RpcComputeMethodDef — extends RpcMethodDef with ComputedOptions and sets\r\n// CallType = RpcComputeCallType (custom call type with its own\r\n// RpcOutboundComputeCall / RpcInboundComputeCall factories).\r\n// RpcInboundComputeCallHandler — an IRpcMiddleware that wraps inbound compute\r\n// calls in a ComputeContext, captures the Computed<T> instance, and stores\r\n// it on RpcInboundComputeCall.Computed for invalidation tracking.\r\n// RemoteComputeServiceInterceptor — the client-side interceptor that creates\r\n// compute-aware outbound calls, tracking server invalidation signals.\r\n//\r\n// In .NET this is split across ~10 files using DI, middleware, interceptors,\r\n// and the RpcComputeCallType custom call type. TS consolidates it into a\r\n// single FusionHub subclass with two method overrides.\r\n//\r\n// Omitted from .NET:\r\n// - RpcComputeCallType / typed RpcInboundComputeCall<T> / RpcOutboundComputeCall<T> —\r\n// .NET uses a separate call type with custom factories, result handling, and\r\n// stage tracking. TS extends the base RpcOutboundComputeCall with a\r\n// whenInvalidated promise (sufficient for the TS use case).\r\n// - ComputedOptionsProvider / ComputedOptions per method — .NET resolves\r\n// per-method caching/invalidation options via DI. TS uses default\r\n// ComputeFunction behavior for all methods.\r\n// - RpcInboundComputeCallHandler middleware — .NET wraps the inbound call in\r\n// ComputeContext using the middleware pipeline. TS wraps it directly in\r\n// _wrapServerMethod by calling cf.invoke() (which sets up\r\n// ComputeContext internally).\r\n// - LocalExecutionMode / ConstrainedEntry — .NET controls whether compute calls\r\n// can execute locally or must go remote (for Fusion's consistency guarantees).\r\n// TS always executes locally (server-side) or remotely (client-side).\r\n// - Invalidation via RpcInboundComputeCall.Computed.Invalidated event — .NET\r\n// attaches an invalidation callback to the captured Computed<T> that calls\r\n// SystemCallSender.Complete(peer, call, invalidatedResult). TS uses\r\n// $sys-c.Invalidate system call instead (equivalent wire effect).\r\n// - RpcOutboundComputeCall.SetError → SetInvalidatedUnsafe — .NET invalidates\r\n// the local computed when an error or reroute occurs. TS resolves\r\n// whenInvalidated on disconnect via rejectAll().\r\n\r\nimport {\r\n RpcHub,\r\n RpcServerPeer,\r\n RpcWebSocketConnection,\r\n RpcSystemCallHandler,\r\n RpcSystemCallSender,\r\n RpcSystemCalls,\r\n defineRpcService,\r\n wireMethodName,\r\n type RpcConnection,\r\n type RpcSerializationFormat,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcDispatchContext,\r\n type RpcPeer,\r\n type RpcMessage,\r\n type RpcCallOptions,\r\n getMethodsMeta,\r\n} from '@actuallab/rpc';\r\nimport {\r\n ComputeFunction,\r\n ComputeContext,\r\n type Computed,\r\n type ComputeFunctionImpl,\r\n} from '@actuallab/fusion';\r\nimport {\r\n AsyncContext,\r\n abortSignalKey,\r\n cancellationError,\r\n isCancellation,\r\n} from '@actuallab/core';\r\nimport { RpcOutboundComputeCall } from './rpc-outbound-compute-call.js';\r\n\r\n/** Call type ID for Fusion compute calls — matches .NET's RpcComputeCallType.Id. */\r\nexport const FUSION_CALL_TYPE_ID = 1;\r\n\r\n/** Wire method name for invalidation system call. */\r\nconst FUSION_INVALIDATE_METHOD = '$sys-c.Invalidate:0';\r\n\r\n/** Max attempts for a compute call cancelled server-side before the result —\r\n * mirrors .NET ComputedCancellationReprocessingOptions.ClientDefault.MaxTryCount. */\r\nconst COMPUTE_CALL_MAX_TRY_COUNT = 3;\r\n\r\n/** Handles Fusion-specific system calls ($sys-c.Invalidate), delegating the rest to base. */\r\nclass FusionSystemCallHandler extends RpcSystemCallHandler {\r\n override handle(message: RpcMessage, args: unknown[], peer: RpcPeer): void {\r\n const method = message.Method;\r\n const relatedId = message.RelatedId ?? 0;\r\n\r\n // The caller dropped its computed, so we no longer owe it an invalidation.\r\n // Released here rather than off the inbound call's abort signal because a\r\n // completed inbound call may already have been evicted from the tracker.\r\n if (method === RpcSystemCalls.cancel)\r\n releaseInboundComputed(peer, relatedId);\r\n\r\n if (method === FUSION_INVALIDATE_METHOD) {\r\n // C# RpcComputeSystemCalls.Invalidate: Get + type check BEFORE any\r\n // removal — a non-compute call with this id must not be evicted.\r\n const call = peer.outboundCalls.get(relatedId);\r\n if (call instanceof RpcOutboundComputeCall) {\r\n peer.outboundCalls.remove(relatedId);\r\n // A pre-result invalidation settles the call as cancelled\r\n // (SetInvalidated → ResultSource.TrySetCanceled): the compute\r\n // client path treats the cancellation as a transparent retry.\r\n if (!call.result.isCompleted)\r\n call.result.reject(cancellationError('Compute call invalidated before result.'));\r\n call.whenInvalidated.resolve();\r\n }\r\n return;\r\n }\r\n\r\n super.handle(message, args, peer);\r\n }\r\n}\r\n\r\n/** Sends the Fusion $sys-c.Invalidate system call — mirrors .NET's RpcComputeSystemCallSender. */\r\nclass FusionSystemCallSender extends RpcSystemCallSender {\r\n invalidate(\r\n conn: RpcConnection,\r\n format: RpcSerializationFormat,\r\n relatedId: number\r\n ): void {\r\n this._send(conn, format, {\r\n Method: FUSION_INVALIDATE_METHOD,\r\n RelatedId: relatedId,\r\n });\r\n }\r\n}\r\n\r\n/** Creates a compute service definition — all methods default to FUSION_CALL_TYPE_ID. */\r\nexport function defineComputeService(\r\n name: string,\r\n methods: Record<string, RpcMethodDefInput>\r\n): RpcServiceDef {\r\n const withCallType: Record<string, RpcMethodDefInput> = {};\r\n for (const [methodName, input] of Object.entries(methods)) {\r\n withCallType[methodName] = {\r\n ...input,\r\n callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID,\r\n };\r\n }\r\n return defineRpcService(name, withCallType);\r\n}\r\n\r\n/** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */\r\nexport class FusionHub extends RpcHub {\r\n declare readonly systemCallSender: FusionSystemCallSender;\r\n\r\n constructor(hubId?: string) {\r\n super(hubId);\r\n this.systemCallHandler = new FusionSystemCallHandler();\r\n // Register Fusion-specific system call for compact format hash resolution\r\n this.registry.register(FUSION_INVALIDATE_METHOD);\r\n }\r\n\r\n protected override _createSystemCallSender(): FusionSystemCallSender {\r\n return new FusionSystemCallSender();\r\n }\r\n\r\n /** Accept an incoming WebSocket and create a server peer. */\r\n acceptConnection(ws: WebSocketLike): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const conn = new RpcWebSocketConnection(ws);\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Accept an RpcConnection and create a server peer. */\r\n acceptRpcConnection(conn: RpcConnection): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Delegate to the base builder, then patch callTypeId for compute methods —\r\n * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */\r\n protected override _buildServiceDef(\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n cls: abstract new (...args: any[]) => any\r\n ): RpcServiceDef {\r\n const def = super._buildServiceDef(cls);\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n const methods = new Map<string, RpcMethodDef>();\r\n for (const [mapKey, methodDef] of def.methods) {\r\n const isCompute =\r\n (methodsMeta[methodDef.name] as { compute?: boolean } | undefined)\r\n ?.compute === true;\r\n methods.set(\r\n mapKey,\r\n isCompute\r\n ? { ...methodDef, callTypeId: FUSION_CALL_TYPE_ID }\r\n : methodDef\r\n );\r\n }\r\n\r\n return { name: def.name, methods };\r\n }\r\n\r\n protected override _wrapServerMethod(\r\n methodDef: RpcMethodDef,\r\n fn: (...args: unknown[]) => unknown,\r\n impl: object\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID) return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(\r\n methodDef.name,\r\n fn as ComputeFunctionImpl\r\n );\r\n return async (...args: unknown[]) => {\r\n const context = extractDispatchContext(args);\r\n // Truncate to the declared arity: ComputeFunction.buildKey folds every argument\r\n // into the cache key, so an extra wire argument would otherwise give the same\r\n // logical call a fresh key and re-run the handler on every request (I3).\r\n const cleanArgs = args.slice(0, methodDef.argCount);\r\n\r\n const computed = await cf.invoke(impl, cleanArgs);\r\n\r\n // IsRegularCall parity (F7): a Regular call to a compute method returns\r\n // the result immediately, with no invalidation tracking.\r\n if (context?.callType === FUSION_CALL_TYPE_ID) {\r\n const { peer, callId } = context;\r\n // ComputedRegistry holds computeds weakly, so without this the only\r\n // reference left after the wrapper returns is the whenInvalidated\r\n // subscription stored on the computed itself — GC then takes both and\r\n // the caller never learns its value went stale (I1). .NET's equivalent\r\n // is the strong RpcInboundComputeCall.Computed, released on Unregister.\r\n retainInboundComputed(peer, callId, computed);\r\n // TODO(AY): Obsolete as of 2026-10-24, to be removed in ~6mo.\r\n // A cancellation result always invalidates the computed on the client at once, so this\r\n // invalidation is redundant for it. It's still sent, since clients that predate this rule\r\n // keep such a call registered until they get it. Once they are obsolete, skip it for\r\n // cancellation results (like C# RpcInboundComputeCall.ProcessStage1Plus) - but only after\r\n // isCancellationTypeName (rpc-error.ts) covers every cancellation type in use: a custom\r\n // OperationCanceledException subclass isn't recognized there, and relies on this message.\r\n // ProcessStage2 parity (F1): whenInvalidated() resolves immediately\r\n // for an already-invalidated computed, so an invalidation landing\r\n // during computation still produces the send.\r\n void computed.whenInvalidated(context.signal).then(\r\n () => {\r\n releaseInboundComputed(peer, callId);\r\n // C# sends the result (ProcessStage1Plus) before the\r\n // invalidation (ProcessStage2). Here the dispatch loop sends\r\n // $sys.Ok after this wrapper returns, so defer past that turn —\r\n // a client drops an Invalidate that precedes its result.\r\n setTimeout(() => {\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n this.systemCallSender.invalidate(\r\n conn,\r\n peer.serializationFormat,\r\n callId\r\n );\r\n }, 0);\r\n },\r\n () => releaseInboundComputed(peer, callId)\r\n );\r\n }\r\n\r\n return computed.value;\r\n };\r\n }\r\n\r\n protected override _createClientMethod(\r\n peer: RpcPeer,\r\n methodDef: RpcMethodDef\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return super._createClientMethod(peer, methodDef);\r\n\r\n const wireName = wireMethodName(methodDef);\r\n // Local ComputeFunction whose impl makes an RPC call\r\n const rpcImpl: ComputeFunctionImpl = async function (\r\n ...args: unknown[]\r\n ) {\r\n // ComputeFunction.invoke threads its child AsyncContext as a trailing argument —\r\n // strip it before the wire call so it's never serialized, and use it to resolve\r\n // the ComputeContext + caller AbortSignal synchronously (AsyncContext.current\r\n // won't survive the await below without ALS, K3).\r\n const last = args[args.length - 1];\r\n const threadedCtx = last instanceof AsyncContext ? last : undefined;\r\n const callArgs = threadedCtx ? args.slice(0, -1) : args;\r\n const current = threadedCtx ?? AsyncContext.current;\r\n const computeCtx = ComputeContext.from(current);\r\n const signal = current?.get(abortSignalKey);\r\n const callOptions: RpcCallOptions = {\r\n callTypeId: FUSION_CALL_TYPE_ID,\r\n outboundCallFactory: (id, m) =>\r\n new RpcOutboundComputeCall(id, m),\r\n signal,\r\n };\r\n\r\n // C# RemoteComputeMethodFunction: a server-side (pre-result)\r\n // cancellation is retried transparently, bounded by MaxTryCount.\r\n let tryIndex = 0;\r\n for (;;) {\r\n // Node's AsyncLocalStorage stamps every promise created under a store with\r\n // a strong reference to it, and the call's `result`/`whenInvalidated` promises\r\n // outlive the computation inside peer.outboundCalls — created here they would\r\n // pin the ambient ComputeContext, and with it the computed (I10).\r\n const outboundCall = AsyncContext.empty.run(() =>\r\n peer.call(wireName, callArgs, callOptions)\r\n ) as RpcOutboundComputeCall;\r\n try {\r\n const value = await outboundCall.result;\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n return value;\r\n } catch (e) {\r\n // Caller-initiated abort: never retry, never cache (K6).\r\n if (signal?.aborted)\r\n throw e;\r\n // Server-side pre-result invalidation: retry while connected.\r\n if (\r\n isCancellation(e) &&\r\n peer.isConnected &&\r\n ++tryIndex < COMPUTE_CALL_MAX_TRY_COUNT\r\n )\r\n continue;\r\n\r\n // Genuine remote error: still track server invalidation so\r\n // the error computed is refreshed (C# SetError parity, F3).\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n throw e;\r\n }\r\n }\r\n };\r\n const cf = new ComputeFunction(methodDef.name, rpcImpl);\r\n const syntheticInstance = {};\r\n return (...args: unknown[]) => {\r\n const last = args[args.length - 1];\r\n const sliced = args.slice(0, methodDef.argCount);\r\n const invokeArgs =\r\n last instanceof AsyncContext ? [...sliced, last] : sliced;\r\n return cf.invoke(syntheticInstance, invokeArgs).then(c => c.value);\r\n };\r\n }\r\n}\r\n\r\n// Strong Computed references the server owes an invalidation for, keyed by inbound\r\n// call id. Per peer, so they die with the peer even if the caller never releases them.\r\nconst inboundComputeds = new WeakMap<RpcPeer, Map<number, Computed<unknown>>>();\r\n\r\nfunction retainInboundComputed(\r\n peer: RpcPeer,\r\n callId: number,\r\n computed: Computed<unknown>\r\n): void {\r\n let computeds = inboundComputeds.get(peer);\r\n if (computeds === undefined) {\r\n computeds = new Map<number, Computed<unknown>>();\r\n inboundComputeds.set(peer, computeds);\r\n }\r\n computeds.set(callId, computed);\r\n}\r\n\r\nfunction releaseInboundComputed(peer: RpcPeer, callId: number): void {\r\n inboundComputeds.get(peer)?.delete(callId);\r\n}\r\n\r\n// The TS analogue of .NET's `~RemoteComputed() => Dispose()`: the computed becoming\r\n// unreachable is what unregisters its outbound call and notifies the server.\r\nconst outboundCallReleaser = new FinalizationRegistry<{\r\n peer: RpcPeer;\r\n call: RpcOutboundComputeCall;\r\n}>(({ peer, call }) => {\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n});\r\n\r\n// Wires both directions of the client computed ↔ outbound compute call binding:\r\n// - server invalidation → local computed invalidation (BindWhenInvalidatedToCall)\r\n// - local computed invalidation → release the call + $sys.Cancel, unless the\r\n// server already invalidated it (BindToCallFromOnInvalidated parity, F4).\r\nfunction bindComputeCall(\r\n peer: RpcPeer,\r\n computeCtx: ComputeContext | undefined,\r\n call: RpcOutboundComputeCall\r\n): void {\r\n const computed = computeCtx?.computed;\r\n if (computed === undefined)\r\n return;\r\n\r\n // Two things conspire to pin `computed` here, both fixed below (I10):\r\n // - V8 gives every closure declared in one scope the same Context object, so the\r\n // call-side reaction must live in its own function or it reaches `computed`\r\n // strongly through the computed-side one;\r\n // - Node's AsyncLocalStorage stamps every promise created under a store with a strong\r\n // reference to it, so a reaction created under the ambient ComputeContext pins the\r\n // very computed we only mean to hold weakly.\r\n const computedRef = new WeakRef(computed);\r\n AsyncContext.empty.run(() => bindCallToComputed(call, computedRef));\r\n AsyncContext.empty.run(() => bindComputedToCall(peer, call, computed));\r\n outboundCallReleaser.register(computed, { peer, call }, computed);\r\n}\r\n\r\n// A compute call stays registered in peer.outboundCalls after $sys.Ok, so this reaction\r\n// must reach the computed weakly — a strong capture pins it and its whole result payload\r\n// from a GC root, and ComputedRegistry's WeakRef can then never be reclaimed (I10).\r\nfunction bindCallToComputed(\r\n call: RpcOutboundComputeCall,\r\n computedRef: WeakRef<Computed<unknown>>\r\n): void {\r\n void call.whenInvalidated.then(() => computedRef.deref()?.invalidate());\r\n}\r\n\r\nfunction bindComputedToCall(\r\n peer: RpcPeer,\r\n call: RpcOutboundComputeCall,\r\n computed: Computed<unknown>\r\n): void {\r\n void computed.whenInvalidated().then(() => {\r\n outboundCallReleaser.unregister(computed);\r\n if (call.whenInvalidated.isCompleted)\r\n return;\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n });\r\n}\r\n\r\nfunction extractDispatchContext(\r\n args: unknown[]\r\n): RpcDispatchContext | undefined {\r\n const last = args[args.length - 1];\r\n if (last !== null && typeof last === 'object' && '__rpcDispatch' in last) {\r\n return last as RpcDispatchContext;\r\n }\r\n return undefined;\r\n}\r\n","import { PromiseSource } from '@actuallab/core';\r\nimport { RpcOutboundCall } from '@actuallab/rpc';\r\n\r\n/** Tracks a pending outbound compute RPC call — stays in tracker until invalidated. */\r\nexport class RpcOutboundComputeCall extends RpcOutboundCall {\r\n override readonly removeOnOk = false;\r\n readonly whenInvalidated = new PromiseSource<void>();\r\n\r\n // A completed compute call is always self-invalidated on reconnect (the\r\n // documented invalidate-on-reconnect simplification, F6), so it must not be\r\n // reported to the server in `$sys.Reconnect` — that would re-arm stage-2\r\n // invalidation tracking for a computed we're about to drop. Report null,\r\n // mirroring C# RpcOutboundComputeCall.GetReconnectStage's peer-change branch.\r\n override getReconnectStage(isPeerChanged: boolean): number | null {\r\n if (this.result.isCompleted)\r\n return null;\r\n\r\n return super.getReconnectStage(isPeerChanged);\r\n }\r\n\r\n override onDisconnect(): void {\r\n this.whenInvalidated.resolve();\r\n }\r\n}\r\n"],"mappings":"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","names":["import_rpc","import_core"]}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fusion-hub.ts","../src/rpc-outbound-compute-call.ts"],"sourcesContent":["// .NET counterparts:\r\n// RpcComputeServiceDef — extends RpcServiceDef with ComputedOptionsProvider.\r\n// RpcComputeMethodDef — extends RpcMethodDef with ComputedOptions and sets\r\n// CallType = RpcComputeCallType (custom call type with its own\r\n// RpcOutboundComputeCall / RpcInboundComputeCall factories).\r\n// RpcInboundComputeCallHandler — an IRpcMiddleware that wraps inbound compute\r\n// calls in a ComputeContext, captures the Computed<T> instance, and stores\r\n// it on RpcInboundComputeCall.Computed for invalidation tracking.\r\n// RemoteComputeServiceInterceptor — the client-side interceptor that creates\r\n// compute-aware outbound calls, tracking server invalidation signals.\r\n//\r\n// In .NET this is split across ~10 files using DI, middleware, interceptors,\r\n// and the RpcComputeCallType custom call type. TS consolidates it into a\r\n// single FusionHub subclass with two method overrides.\r\n//\r\n// Omitted from .NET:\r\n// - RpcComputeCallType / typed RpcInboundComputeCall<T> / RpcOutboundComputeCall<T> —\r\n// .NET uses a separate call type with custom factories, result handling, and\r\n// stage tracking. TS extends the base RpcOutboundComputeCall with a\r\n// whenInvalidated promise (sufficient for the TS use case).\r\n// - ComputedOptionsProvider / ComputedOptions per method — .NET resolves\r\n// per-method caching/invalidation options via DI. TS uses default\r\n// ComputeFunction behavior for all methods.\r\n// - RpcInboundComputeCallHandler middleware — .NET wraps the inbound call in\r\n// ComputeContext using the middleware pipeline. TS wraps it directly in\r\n// _wrapServerMethod by calling cf.invoke() (which sets up\r\n// ComputeContext internally).\r\n// - LocalExecutionMode / ConstrainedEntry — .NET controls whether compute calls\r\n// can execute locally or must go remote (for Fusion's consistency guarantees).\r\n// TS always executes locally (server-side) or remotely (client-side).\r\n// - Invalidation via RpcInboundComputeCall.Computed.Invalidated event — .NET\r\n// attaches an invalidation callback to the captured Computed<T> that calls\r\n// SystemCallSender.Complete(peer, call, invalidatedResult). TS uses\r\n// $sys-c.Invalidate system call instead (equivalent wire effect).\r\n// - RpcOutboundComputeCall.SetError → SetInvalidatedUnsafe — .NET invalidates\r\n// the local computed when an error or reroute occurs. TS resolves\r\n// whenInvalidated on disconnect via rejectAll().\r\n\r\nimport {\r\n RpcHub,\r\n RpcServerPeer,\r\n RpcWebSocketConnection,\r\n RpcSystemCallHandler,\r\n RpcSystemCallSender,\r\n RpcSystemCalls,\r\n defineRpcService,\r\n wireMethodName,\r\n type RpcConnection,\r\n type RpcSerializationFormat,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcDispatchContext,\r\n type RpcPeer,\r\n type RpcMessage,\r\n type RpcCallOptions,\r\n getMethodsMeta,\r\n} from '@actuallab/rpc';\r\nimport {\r\n ComputeFunction,\r\n ComputeContext,\r\n type Computed,\r\n type ComputeFunctionImpl,\r\n} from '@actuallab/fusion';\r\nimport {\r\n AsyncContext,\r\n abortSignalKey,\r\n cancellationError,\r\n isCancellation,\r\n} from '@actuallab/core';\r\nimport { RpcOutboundComputeCall } from './rpc-outbound-compute-call.js';\r\n\r\n/** Call type ID for Fusion compute calls — matches .NET's RpcComputeCallType.Id. */\r\nexport const FUSION_CALL_TYPE_ID = 1;\r\n\r\n/** Wire method name for invalidation system call. */\r\nconst FUSION_INVALIDATE_METHOD = '$sys-c.Invalidate:0';\r\n\r\n/** Max attempts for a compute call cancelled server-side before the result —\r\n * mirrors .NET ComputedCancellationReprocessingOptions.ClientDefault.MaxTryCount. */\r\nconst COMPUTE_CALL_MAX_TRY_COUNT = 3;\r\n\r\n/** Handles Fusion-specific system calls ($sys-c.Invalidate), delegating the rest to base. */\r\nclass FusionSystemCallHandler extends RpcSystemCallHandler {\r\n override handle(message: RpcMessage, args: unknown[], peer: RpcPeer): void {\r\n const method = message.Method;\r\n const relatedId = message.RelatedId ?? 0;\r\n\r\n // The caller dropped its computed, so we no longer owe it an invalidation.\r\n // Released here rather than off the inbound call's abort signal because a\r\n // completed inbound call may already have been evicted from the tracker.\r\n if (method === RpcSystemCalls.cancel)\r\n releaseInboundComputed(peer, relatedId);\r\n\r\n if (method === FUSION_INVALIDATE_METHOD) {\r\n // C# RpcComputeSystemCalls.Invalidate: Get + type check BEFORE any\r\n // removal — a non-compute call with this id must not be evicted.\r\n const call = peer.outboundCalls.get(relatedId);\r\n if (call instanceof RpcOutboundComputeCall) {\r\n peer.outboundCalls.remove(relatedId);\r\n // A pre-result invalidation settles the call as cancelled\r\n // (SetInvalidated → ResultSource.TrySetCanceled): the compute\r\n // client path treats the cancellation as a transparent retry.\r\n if (!call.result.isCompleted)\r\n call.result.reject(cancellationError('Compute call invalidated before result.'));\r\n call.whenInvalidated.resolve();\r\n }\r\n return;\r\n }\r\n\r\n super.handle(message, args, peer);\r\n }\r\n}\r\n\r\n/** Sends the Fusion $sys-c.Invalidate system call — mirrors .NET's RpcComputeSystemCallSender. */\r\nclass FusionSystemCallSender extends RpcSystemCallSender {\r\n invalidate(\r\n conn: RpcConnection,\r\n format: RpcSerializationFormat,\r\n relatedId: number\r\n ): void {\r\n this._send(conn, format, {\r\n Method: FUSION_INVALIDATE_METHOD,\r\n RelatedId: relatedId,\r\n });\r\n }\r\n}\r\n\r\n/** Creates a compute service definition — all methods default to FUSION_CALL_TYPE_ID. */\r\nexport function defineComputeService(\r\n name: string,\r\n methods: Record<string, RpcMethodDefInput>\r\n): RpcServiceDef {\r\n const withCallType: Record<string, RpcMethodDefInput> = {};\r\n for (const [methodName, input] of Object.entries(methods)) {\r\n withCallType[methodName] = {\r\n ...input,\r\n callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID,\r\n };\r\n }\r\n return defineRpcService(name, withCallType);\r\n}\r\n\r\n/** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */\r\nexport class FusionHub extends RpcHub {\r\n declare readonly systemCallSender: FusionSystemCallSender;\r\n\r\n constructor(hubId?: string) {\r\n super(hubId);\r\n this.systemCallHandler = new FusionSystemCallHandler();\r\n // Register Fusion-specific system call for compact format hash resolution\r\n this.registry.register(FUSION_INVALIDATE_METHOD);\r\n }\r\n\r\n protected override _createSystemCallSender(): FusionSystemCallSender {\r\n return new FusionSystemCallSender();\r\n }\r\n\r\n /** Accept an incoming WebSocket and create a server peer. */\r\n acceptConnection(ws: WebSocketLike): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const conn = new RpcWebSocketConnection(ws);\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Accept an RpcConnection and create a server peer. */\r\n acceptRpcConnection(conn: RpcConnection): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Delegate to the base builder, then patch callTypeId for compute methods —\r\n * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */\r\n protected override _buildServiceDef(\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n cls: abstract new (...args: any[]) => any\r\n ): RpcServiceDef {\r\n const def = super._buildServiceDef(cls);\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n const methods = new Map<string, RpcMethodDef>();\r\n for (const [mapKey, methodDef] of def.methods) {\r\n const isCompute =\r\n (methodsMeta[methodDef.name] as { compute?: boolean } | undefined)\r\n ?.compute === true;\r\n methods.set(\r\n mapKey,\r\n isCompute\r\n ? { ...methodDef, callTypeId: FUSION_CALL_TYPE_ID }\r\n : methodDef\r\n );\r\n }\r\n\r\n return { name: def.name, methods };\r\n }\r\n\r\n protected override _wrapServerMethod(\r\n methodDef: RpcMethodDef,\r\n fn: (...args: unknown[]) => unknown,\r\n impl: object\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID) return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(\r\n methodDef.name,\r\n fn as ComputeFunctionImpl\r\n );\r\n return async (...args: unknown[]) => {\r\n const context = extractDispatchContext(args);\r\n // Truncate to the declared arity: ComputeFunction.buildKey folds every argument\r\n // into the cache key, so an extra wire argument would otherwise give the same\r\n // logical call a fresh key and re-run the handler on every request (I3).\r\n const cleanArgs = args.slice(0, methodDef.argCount);\r\n\r\n const computed = await cf.invoke(impl, cleanArgs);\r\n\r\n // IsRegularCall parity (F7): a Regular call to a compute method returns\r\n // the result immediately, with no invalidation tracking.\r\n if (context?.callType === FUSION_CALL_TYPE_ID) {\r\n const { peer, callId } = context;\r\n // ComputedRegistry holds computeds weakly, so without this the only\r\n // reference left after the wrapper returns is the whenInvalidated\r\n // subscription stored on the computed itself — GC then takes both and\r\n // the caller never learns its value went stale (I1). .NET's equivalent\r\n // is the strong RpcInboundComputeCall.Computed, released on Unregister.\r\n retainInboundComputed(peer, callId, computed);\r\n // ProcessStage2 parity (F1): whenInvalidated() resolves immediately\r\n // for an already-invalidated computed, so an invalidation landing\r\n // during computation still produces the send.\r\n void computed.whenInvalidated(context.signal).then(\r\n () => {\r\n releaseInboundComputed(peer, callId);\r\n // C# sends the result (ProcessStage1Plus) before the\r\n // invalidation (ProcessStage2). Here the dispatch loop sends\r\n // $sys.Ok after this wrapper returns, so defer past that turn —\r\n // a client drops an Invalidate that precedes its result.\r\n setTimeout(() => {\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n this.systemCallSender.invalidate(\r\n conn,\r\n peer.serializationFormat,\r\n callId\r\n );\r\n }, 0);\r\n },\r\n () => releaseInboundComputed(peer, callId)\r\n );\r\n }\r\n\r\n return computed.value;\r\n };\r\n }\r\n\r\n protected override _createClientMethod(\r\n peer: RpcPeer,\r\n methodDef: RpcMethodDef\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return super._createClientMethod(peer, methodDef);\r\n\r\n const wireName = wireMethodName(methodDef);\r\n // Local ComputeFunction whose impl makes an RPC call\r\n const rpcImpl: ComputeFunctionImpl = async function (\r\n ...args: unknown[]\r\n ) {\r\n // ComputeFunction.invoke threads its child AsyncContext as a trailing argument —\r\n // strip it before the wire call so it's never serialized, and use it to resolve\r\n // the ComputeContext + caller AbortSignal synchronously (AsyncContext.current\r\n // won't survive the await below without ALS, K3).\r\n const last = args[args.length - 1];\r\n const threadedCtx = last instanceof AsyncContext ? last : undefined;\r\n const callArgs = threadedCtx ? args.slice(0, -1) : args;\r\n const current = threadedCtx ?? AsyncContext.current;\r\n const computeCtx = ComputeContext.from(current);\r\n const signal = current?.get(abortSignalKey);\r\n const callOptions: RpcCallOptions = {\r\n callTypeId: FUSION_CALL_TYPE_ID,\r\n outboundCallFactory: (id, m) =>\r\n new RpcOutboundComputeCall(id, m),\r\n signal,\r\n };\r\n\r\n // C# RemoteComputeMethodFunction: a server-side (pre-result)\r\n // cancellation is retried transparently, bounded by MaxTryCount.\r\n let tryIndex = 0;\r\n for (;;) {\r\n // Node's AsyncLocalStorage stamps every promise created under a store with\r\n // a strong reference to it, and the call's `result`/`whenInvalidated` promises\r\n // outlive the computation inside peer.outboundCalls — created here they would\r\n // pin the ambient ComputeContext, and with it the computed (I10).\r\n const outboundCall = AsyncContext.empty.run(() =>\r\n peer.call(wireName, callArgs, callOptions)\r\n ) as RpcOutboundComputeCall;\r\n try {\r\n const value = await outboundCall.result;\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n return value;\r\n } catch (e) {\r\n // Caller-initiated abort: never retry, never cache (K6).\r\n if (signal?.aborted)\r\n throw e;\r\n // Server-side pre-result invalidation: retry while connected.\r\n if (\r\n isCancellation(e) &&\r\n peer.isConnected &&\r\n ++tryIndex < COMPUTE_CALL_MAX_TRY_COUNT\r\n )\r\n continue;\r\n\r\n // Genuine remote error: still track server invalidation so\r\n // the error computed is refreshed (C# SetError parity, F3).\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n throw e;\r\n }\r\n }\r\n };\r\n const cf = new ComputeFunction(methodDef.name, rpcImpl);\r\n const syntheticInstance = {};\r\n return (...args: unknown[]) => {\r\n const last = args[args.length - 1];\r\n const sliced = args.slice(0, methodDef.argCount);\r\n const invokeArgs =\r\n last instanceof AsyncContext ? [...sliced, last] : sliced;\r\n return cf.invoke(syntheticInstance, invokeArgs).then(c => c.value);\r\n };\r\n }\r\n}\r\n\r\n// Strong Computed references the server owes an invalidation for, keyed by inbound\r\n// call id. Per peer, so they die with the peer even if the caller never releases them.\r\nconst inboundComputeds = new WeakMap<RpcPeer, Map<number, Computed<unknown>>>();\r\n\r\nfunction retainInboundComputed(\r\n peer: RpcPeer,\r\n callId: number,\r\n computed: Computed<unknown>\r\n): void {\r\n let computeds = inboundComputeds.get(peer);\r\n if (computeds === undefined) {\r\n computeds = new Map<number, Computed<unknown>>();\r\n inboundComputeds.set(peer, computeds);\r\n }\r\n computeds.set(callId, computed);\r\n}\r\n\r\nfunction releaseInboundComputed(peer: RpcPeer, callId: number): void {\r\n inboundComputeds.get(peer)?.delete(callId);\r\n}\r\n\r\n// The TS analogue of .NET's `~RemoteComputed() => Dispose()`: the computed becoming\r\n// unreachable is what unregisters its outbound call and notifies the server.\r\nconst outboundCallReleaser = new FinalizationRegistry<{\r\n peer: RpcPeer;\r\n call: RpcOutboundComputeCall;\r\n}>(({ peer, call }) => {\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n});\r\n\r\n// Wires both directions of the client computed ↔ outbound compute call binding:\r\n// - server invalidation → local computed invalidation (BindWhenInvalidatedToCall)\r\n// - local computed invalidation → release the call + $sys.Cancel, unless the\r\n// server already invalidated it (BindToCallFromOnInvalidated parity, F4).\r\nfunction bindComputeCall(\r\n peer: RpcPeer,\r\n computeCtx: ComputeContext | undefined,\r\n call: RpcOutboundComputeCall\r\n): void {\r\n const computed = computeCtx?.computed;\r\n if (computed === undefined)\r\n return;\r\n\r\n // Two things conspire to pin `computed` here, both fixed below (I10):\r\n // - V8 gives every closure declared in one scope the same Context object, so the\r\n // call-side reaction must live in its own function or it reaches `computed`\r\n // strongly through the computed-side one;\r\n // - Node's AsyncLocalStorage stamps every promise created under a store with a strong\r\n // reference to it, so a reaction created under the ambient ComputeContext pins the\r\n // very computed we only mean to hold weakly.\r\n const computedRef = new WeakRef(computed);\r\n AsyncContext.empty.run(() => bindCallToComputed(call, computedRef));\r\n AsyncContext.empty.run(() => bindComputedToCall(peer, call, computed));\r\n outboundCallReleaser.register(computed, { peer, call }, computed);\r\n}\r\n\r\n// A compute call stays registered in peer.outboundCalls after $sys.Ok, so this reaction\r\n// must reach the computed weakly — a strong capture pins it and its whole result payload\r\n// from a GC root, and ComputedRegistry's WeakRef can then never be reclaimed (I10).\r\nfunction bindCallToComputed(\r\n call: RpcOutboundComputeCall,\r\n computedRef: WeakRef<Computed<unknown>>\r\n): void {\r\n void call.whenInvalidated.then(() => computedRef.deref()?.invalidate());\r\n}\r\n\r\nfunction bindComputedToCall(\r\n peer: RpcPeer,\r\n call: RpcOutboundComputeCall,\r\n computed: Computed<unknown>\r\n): void {\r\n void computed.whenInvalidated().then(() => {\r\n outboundCallReleaser.unregister(computed);\r\n if (call.whenInvalidated.isCompleted)\r\n return;\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n });\r\n}\r\n\r\nfunction extractDispatchContext(\r\n args: unknown[]\r\n): RpcDispatchContext | undefined {\r\n const last = args[args.length - 1];\r\n if (last !== null && typeof last === 'object' && '__rpcDispatch' in last) {\r\n return last as RpcDispatchContext;\r\n }\r\n return undefined;\r\n}\r\n","import { PromiseSource } from '@actuallab/core';\r\nimport { RpcOutboundCall } from '@actuallab/rpc';\r\n\r\n/** Tracks a pending outbound compute RPC call — stays in tracker until invalidated. */\r\nexport class RpcOutboundComputeCall extends RpcOutboundCall {\r\n override readonly removeOnOk = false;\r\n readonly whenInvalidated = new PromiseSource<void>();\r\n\r\n // A completed compute call is always self-invalidated on reconnect (the\r\n // documented invalidate-on-reconnect simplification, F6), so it must not be\r\n // reported to the server in `$sys.Reconnect` — that would re-arm stage-2\r\n // invalidation tracking for a computed we're about to drop. Report null,\r\n // mirroring C# RpcOutboundComputeCall.GetReconnectStage's peer-change branch.\r\n override getReconnectStage(isPeerChanged: boolean): number | null {\r\n if (this.result.isCompleted)\r\n return null;\r\n\r\n return super.getReconnectStage(isPeerChanged);\r\n }\r\n\r\n override onDisconnect(): void {\r\n this.whenInvalidated.resolve();\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/fusion-hub.ts","../src/rpc-outbound-compute-call.ts"],"sourcesContent":["// .NET counterparts:\r\n// RpcComputeServiceDef — extends RpcServiceDef with ComputedOptionsProvider.\r\n// RpcComputeMethodDef — extends RpcMethodDef with ComputedOptions and sets\r\n// CallType = RpcComputeCallType (custom call type with its own\r\n// RpcOutboundComputeCall / RpcInboundComputeCall factories).\r\n// RpcInboundComputeCallHandler — an IRpcMiddleware that wraps inbound compute\r\n// calls in a ComputeContext, captures the Computed<T> instance, and stores\r\n// it on RpcInboundComputeCall.Computed for invalidation tracking.\r\n// RemoteComputeServiceInterceptor — the client-side interceptor that creates\r\n// compute-aware outbound calls, tracking server invalidation signals.\r\n//\r\n// In .NET this is split across ~10 files using DI, middleware, interceptors,\r\n// and the RpcComputeCallType custom call type. TS consolidates it into a\r\n// single FusionHub subclass with two method overrides.\r\n//\r\n// Omitted from .NET:\r\n// - RpcComputeCallType / typed RpcInboundComputeCall<T> / RpcOutboundComputeCall<T> —\r\n// .NET uses a separate call type with custom factories, result handling, and\r\n// stage tracking. TS extends the base RpcOutboundComputeCall with a\r\n// whenInvalidated promise (sufficient for the TS use case).\r\n// - ComputedOptionsProvider / ComputedOptions per method — .NET resolves\r\n// per-method caching/invalidation options via DI. TS uses default\r\n// ComputeFunction behavior for all methods.\r\n// - RpcInboundComputeCallHandler middleware — .NET wraps the inbound call in\r\n// ComputeContext using the middleware pipeline. TS wraps it directly in\r\n// _wrapServerMethod by calling cf.invoke() (which sets up\r\n// ComputeContext internally).\r\n// - LocalExecutionMode / ConstrainedEntry — .NET controls whether compute calls\r\n// can execute locally or must go remote (for Fusion's consistency guarantees).\r\n// TS always executes locally (server-side) or remotely (client-side).\r\n// - Invalidation via RpcInboundComputeCall.Computed.Invalidated event — .NET\r\n// attaches an invalidation callback to the captured Computed<T> that calls\r\n// SystemCallSender.Complete(peer, call, invalidatedResult). TS uses\r\n// $sys-c.Invalidate system call instead (equivalent wire effect).\r\n// - RpcOutboundComputeCall.SetError → SetInvalidatedUnsafe — .NET invalidates\r\n// the local computed when an error or reroute occurs. TS resolves\r\n// whenInvalidated on disconnect via rejectAll().\r\n\r\nimport {\r\n RpcHub,\r\n RpcServerPeer,\r\n RpcWebSocketConnection,\r\n RpcSystemCallHandler,\r\n RpcSystemCallSender,\r\n RpcSystemCalls,\r\n defineRpcService,\r\n wireMethodName,\r\n type RpcConnection,\r\n type RpcSerializationFormat,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcDispatchContext,\r\n type RpcPeer,\r\n type RpcMessage,\r\n type RpcCallOptions,\r\n getMethodsMeta,\r\n} from '@actuallab/rpc';\r\nimport {\r\n ComputeFunction,\r\n ComputeContext,\r\n type Computed,\r\n type ComputeFunctionImpl,\r\n} from '@actuallab/fusion';\r\nimport {\r\n AsyncContext,\r\n abortSignalKey,\r\n cancellationError,\r\n isCancellation,\r\n} from '@actuallab/core';\r\nimport { RpcOutboundComputeCall } from './rpc-outbound-compute-call.js';\r\n\r\n/** Call type ID for Fusion compute calls — matches .NET's RpcComputeCallType.Id. */\r\nexport const FUSION_CALL_TYPE_ID = 1;\r\n\r\n/** Wire method name for invalidation system call. */\r\nconst FUSION_INVALIDATE_METHOD = '$sys-c.Invalidate:0';\r\n\r\n/** Max attempts for a compute call cancelled server-side before the result —\r\n * mirrors .NET ComputedCancellationReprocessingOptions.ClientDefault.MaxTryCount. */\r\nconst COMPUTE_CALL_MAX_TRY_COUNT = 3;\r\n\r\n/** Handles Fusion-specific system calls ($sys-c.Invalidate), delegating the rest to base. */\r\nclass FusionSystemCallHandler extends RpcSystemCallHandler {\r\n override handle(message: RpcMessage, args: unknown[], peer: RpcPeer): void {\r\n const method = message.Method;\r\n const relatedId = message.RelatedId ?? 0;\r\n\r\n // The caller dropped its computed, so we no longer owe it an invalidation.\r\n // Released here rather than off the inbound call's abort signal because a\r\n // completed inbound call may already have been evicted from the tracker.\r\n if (method === RpcSystemCalls.cancel)\r\n releaseInboundComputed(peer, relatedId);\r\n\r\n if (method === FUSION_INVALIDATE_METHOD) {\r\n // C# RpcComputeSystemCalls.Invalidate: Get + type check BEFORE any\r\n // removal — a non-compute call with this id must not be evicted.\r\n const call = peer.outboundCalls.get(relatedId);\r\n if (call instanceof RpcOutboundComputeCall) {\r\n peer.outboundCalls.remove(relatedId);\r\n // A pre-result invalidation settles the call as cancelled\r\n // (SetInvalidated → ResultSource.TrySetCanceled): the compute\r\n // client path treats the cancellation as a transparent retry.\r\n if (!call.result.isCompleted)\r\n call.result.reject(cancellationError('Compute call invalidated before result.'));\r\n call.whenInvalidated.resolve();\r\n }\r\n return;\r\n }\r\n\r\n super.handle(message, args, peer);\r\n }\r\n}\r\n\r\n/** Sends the Fusion $sys-c.Invalidate system call — mirrors .NET's RpcComputeSystemCallSender. */\r\nclass FusionSystemCallSender extends RpcSystemCallSender {\r\n invalidate(\r\n conn: RpcConnection,\r\n format: RpcSerializationFormat,\r\n relatedId: number\r\n ): void {\r\n this._send(conn, format, {\r\n Method: FUSION_INVALIDATE_METHOD,\r\n RelatedId: relatedId,\r\n });\r\n }\r\n}\r\n\r\n/** Creates a compute service definition — all methods default to FUSION_CALL_TYPE_ID. */\r\nexport function defineComputeService(\r\n name: string,\r\n methods: Record<string, RpcMethodDefInput>\r\n): RpcServiceDef {\r\n const withCallType: Record<string, RpcMethodDefInput> = {};\r\n for (const [methodName, input] of Object.entries(methods)) {\r\n withCallType[methodName] = {\r\n ...input,\r\n callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID,\r\n };\r\n }\r\n return defineRpcService(name, withCallType);\r\n}\r\n\r\n/** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */\r\nexport class FusionHub extends RpcHub {\r\n declare readonly systemCallSender: FusionSystemCallSender;\r\n\r\n constructor(hubId?: string) {\r\n super(hubId);\r\n this.systemCallHandler = new FusionSystemCallHandler();\r\n // Register Fusion-specific system call for compact format hash resolution\r\n this.registry.register(FUSION_INVALIDATE_METHOD);\r\n }\r\n\r\n protected override _createSystemCallSender(): FusionSystemCallSender {\r\n return new FusionSystemCallSender();\r\n }\r\n\r\n /** Accept an incoming WebSocket and create a server peer. */\r\n acceptConnection(ws: WebSocketLike): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const conn = new RpcWebSocketConnection(ws);\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Accept an RpcConnection and create a server peer. */\r\n acceptRpcConnection(conn: RpcConnection): RpcServerPeer {\r\n const ref = `server://${crypto.randomUUID()}`;\r\n const peer = this.getServerPeer(ref);\r\n peer.accept(conn);\r\n return peer;\r\n }\r\n\r\n /** Delegate to the base builder, then patch callTypeId for compute methods —\r\n * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */\r\n protected override _buildServiceDef(\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n cls: abstract new (...args: any[]) => any\r\n ): RpcServiceDef {\r\n const def = super._buildServiceDef(cls);\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n const methods = new Map<string, RpcMethodDef>();\r\n for (const [mapKey, methodDef] of def.methods) {\r\n const isCompute =\r\n (methodsMeta[methodDef.name] as { compute?: boolean } | undefined)\r\n ?.compute === true;\r\n methods.set(\r\n mapKey,\r\n isCompute\r\n ? { ...methodDef, callTypeId: FUSION_CALL_TYPE_ID }\r\n : methodDef\r\n );\r\n }\r\n\r\n return { name: def.name, methods };\r\n }\r\n\r\n protected override _wrapServerMethod(\r\n methodDef: RpcMethodDef,\r\n fn: (...args: unknown[]) => unknown,\r\n impl: object\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID) return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(\r\n methodDef.name,\r\n fn as ComputeFunctionImpl\r\n );\r\n return async (...args: unknown[]) => {\r\n const context = extractDispatchContext(args);\r\n // Truncate to the declared arity: ComputeFunction.buildKey folds every argument\r\n // into the cache key, so an extra wire argument would otherwise give the same\r\n // logical call a fresh key and re-run the handler on every request (I3).\r\n const cleanArgs = args.slice(0, methodDef.argCount);\r\n\r\n const computed = await cf.invoke(impl, cleanArgs);\r\n\r\n // IsRegularCall parity (F7): a Regular call to a compute method returns\r\n // the result immediately, with no invalidation tracking.\r\n if (context?.callType === FUSION_CALL_TYPE_ID) {\r\n const { peer, callId } = context;\r\n // ComputedRegistry holds computeds weakly, so without this the only\r\n // reference left after the wrapper returns is the whenInvalidated\r\n // subscription stored on the computed itself — GC then takes both and\r\n // the caller never learns its value went stale (I1). .NET's equivalent\r\n // is the strong RpcInboundComputeCall.Computed, released on Unregister.\r\n retainInboundComputed(peer, callId, computed);\r\n // TODO(AY): Obsolete as of 2026-10-24, to be removed in ~6mo.\r\n // A cancellation result always invalidates the computed on the client at once, so this\r\n // invalidation is redundant for it. It's still sent, since clients that predate this rule\r\n // keep such a call registered until they get it. Once they are obsolete, skip it for\r\n // cancellation results (like C# RpcInboundComputeCall.ProcessStage1Plus) - but only after\r\n // isCancellationTypeName (rpc-error.ts) covers every cancellation type in use: a custom\r\n // OperationCanceledException subclass isn't recognized there, and relies on this message.\r\n // ProcessStage2 parity (F1): whenInvalidated() resolves immediately\r\n // for an already-invalidated computed, so an invalidation landing\r\n // during computation still produces the send.\r\n void computed.whenInvalidated(context.signal).then(\r\n () => {\r\n releaseInboundComputed(peer, callId);\r\n // C# sends the result (ProcessStage1Plus) before the\r\n // invalidation (ProcessStage2). Here the dispatch loop sends\r\n // $sys.Ok after this wrapper returns, so defer past that turn —\r\n // a client drops an Invalidate that precedes its result.\r\n setTimeout(() => {\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n this.systemCallSender.invalidate(\r\n conn,\r\n peer.serializationFormat,\r\n callId\r\n );\r\n }, 0);\r\n },\r\n () => releaseInboundComputed(peer, callId)\r\n );\r\n }\r\n\r\n return computed.value;\r\n };\r\n }\r\n\r\n protected override _createClientMethod(\r\n peer: RpcPeer,\r\n methodDef: RpcMethodDef\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return super._createClientMethod(peer, methodDef);\r\n\r\n const wireName = wireMethodName(methodDef);\r\n // Local ComputeFunction whose impl makes an RPC call\r\n const rpcImpl: ComputeFunctionImpl = async function (\r\n ...args: unknown[]\r\n ) {\r\n // ComputeFunction.invoke threads its child AsyncContext as a trailing argument —\r\n // strip it before the wire call so it's never serialized, and use it to resolve\r\n // the ComputeContext + caller AbortSignal synchronously (AsyncContext.current\r\n // won't survive the await below without ALS, K3).\r\n const last = args[args.length - 1];\r\n const threadedCtx = last instanceof AsyncContext ? last : undefined;\r\n const callArgs = threadedCtx ? args.slice(0, -1) : args;\r\n const current = threadedCtx ?? AsyncContext.current;\r\n const computeCtx = ComputeContext.from(current);\r\n const signal = current?.get(abortSignalKey);\r\n const callOptions: RpcCallOptions = {\r\n callTypeId: FUSION_CALL_TYPE_ID,\r\n outboundCallFactory: (id, m) =>\r\n new RpcOutboundComputeCall(id, m),\r\n signal,\r\n };\r\n\r\n // C# RemoteComputeMethodFunction: a server-side (pre-result)\r\n // cancellation is retried transparently, bounded by MaxTryCount.\r\n let tryIndex = 0;\r\n for (;;) {\r\n // Node's AsyncLocalStorage stamps every promise created under a store with\r\n // a strong reference to it, and the call's `result`/`whenInvalidated` promises\r\n // outlive the computation inside peer.outboundCalls — created here they would\r\n // pin the ambient ComputeContext, and with it the computed (I10).\r\n const outboundCall = AsyncContext.empty.run(() =>\r\n peer.call(wireName, callArgs, callOptions)\r\n ) as RpcOutboundComputeCall;\r\n try {\r\n const value = await outboundCall.result;\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n return value;\r\n } catch (e) {\r\n // Caller-initiated abort: never retry, never cache (K6).\r\n if (signal?.aborted)\r\n throw e;\r\n // Server-side pre-result invalidation: retry while connected.\r\n if (\r\n isCancellation(e) &&\r\n peer.isConnected &&\r\n ++tryIndex < COMPUTE_CALL_MAX_TRY_COUNT\r\n )\r\n continue;\r\n\r\n // Genuine remote error: still track server invalidation so\r\n // the error computed is refreshed (C# SetError parity, F3).\r\n bindComputeCall(peer, computeCtx, outboundCall);\r\n throw e;\r\n }\r\n }\r\n };\r\n const cf = new ComputeFunction(methodDef.name, rpcImpl);\r\n const syntheticInstance = {};\r\n return (...args: unknown[]) => {\r\n const last = args[args.length - 1];\r\n const sliced = args.slice(0, methodDef.argCount);\r\n const invokeArgs =\r\n last instanceof AsyncContext ? [...sliced, last] : sliced;\r\n return cf.invoke(syntheticInstance, invokeArgs).then(c => c.value);\r\n };\r\n }\r\n}\r\n\r\n// Strong Computed references the server owes an invalidation for, keyed by inbound\r\n// call id. Per peer, so they die with the peer even if the caller never releases them.\r\nconst inboundComputeds = new WeakMap<RpcPeer, Map<number, Computed<unknown>>>();\r\n\r\nfunction retainInboundComputed(\r\n peer: RpcPeer,\r\n callId: number,\r\n computed: Computed<unknown>\r\n): void {\r\n let computeds = inboundComputeds.get(peer);\r\n if (computeds === undefined) {\r\n computeds = new Map<number, Computed<unknown>>();\r\n inboundComputeds.set(peer, computeds);\r\n }\r\n computeds.set(callId, computed);\r\n}\r\n\r\nfunction releaseInboundComputed(peer: RpcPeer, callId: number): void {\r\n inboundComputeds.get(peer)?.delete(callId);\r\n}\r\n\r\n// The TS analogue of .NET's `~RemoteComputed() => Dispose()`: the computed becoming\r\n// unreachable is what unregisters its outbound call and notifies the server.\r\nconst outboundCallReleaser = new FinalizationRegistry<{\r\n peer: RpcPeer;\r\n call: RpcOutboundComputeCall;\r\n}>(({ peer, call }) => {\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n});\r\n\r\n// Wires both directions of the client computed ↔ outbound compute call binding:\r\n// - server invalidation → local computed invalidation (BindWhenInvalidatedToCall)\r\n// - local computed invalidation → release the call + $sys.Cancel, unless the\r\n// server already invalidated it (BindToCallFromOnInvalidated parity, F4).\r\nfunction bindComputeCall(\r\n peer: RpcPeer,\r\n computeCtx: ComputeContext | undefined,\r\n call: RpcOutboundComputeCall\r\n): void {\r\n const computed = computeCtx?.computed;\r\n if (computed === undefined)\r\n return;\r\n\r\n // Two things conspire to pin `computed` here, both fixed below (I10):\r\n // - V8 gives every closure declared in one scope the same Context object, so the\r\n // call-side reaction must live in its own function or it reaches `computed`\r\n // strongly through the computed-side one;\r\n // - Node's AsyncLocalStorage stamps every promise created under a store with a strong\r\n // reference to it, so a reaction created under the ambient ComputeContext pins the\r\n // very computed we only mean to hold weakly.\r\n const computedRef = new WeakRef(computed);\r\n AsyncContext.empty.run(() => bindCallToComputed(call, computedRef));\r\n AsyncContext.empty.run(() => bindComputedToCall(peer, call, computed));\r\n outboundCallReleaser.register(computed, { peer, call }, computed);\r\n}\r\n\r\n// A compute call stays registered in peer.outboundCalls after $sys.Ok, so this reaction\r\n// must reach the computed weakly — a strong capture pins it and its whole result payload\r\n// from a GC root, and ComputedRegistry's WeakRef can then never be reclaimed (I10).\r\nfunction bindCallToComputed(\r\n call: RpcOutboundComputeCall,\r\n computedRef: WeakRef<Computed<unknown>>\r\n): void {\r\n void call.whenInvalidated.then(() => computedRef.deref()?.invalidate());\r\n}\r\n\r\nfunction bindComputedToCall(\r\n peer: RpcPeer,\r\n call: RpcOutboundComputeCall,\r\n computed: Computed<unknown>\r\n): void {\r\n void computed.whenInvalidated().then(() => {\r\n outboundCallReleaser.unregister(computed);\r\n if (call.whenInvalidated.isCompleted)\r\n return;\r\n if (peer.outboundCalls.remove(call.callId) === undefined)\r\n return;\r\n call.whenInvalidated.resolve();\r\n const conn = peer.wireConnection;\r\n if (conn !== undefined)\r\n peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);\r\n });\r\n}\r\n\r\nfunction extractDispatchContext(\r\n args: unknown[]\r\n): RpcDispatchContext | undefined {\r\n const last = args[args.length - 1];\r\n if (last !== null && typeof last === 'object' && '__rpcDispatch' in last) {\r\n return last as RpcDispatchContext;\r\n }\r\n return undefined;\r\n}\r\n","import { PromiseSource } from '@actuallab/core';\r\nimport { RpcOutboundCall } from '@actuallab/rpc';\r\n\r\n/** Tracks a pending outbound compute RPC call — stays in tracker until invalidated. */\r\nexport class RpcOutboundComputeCall extends RpcOutboundCall {\r\n override readonly removeOnOk = false;\r\n readonly whenInvalidated = new PromiseSource<void>();\r\n\r\n // A completed compute call is always self-invalidated on reconnect (the\r\n // documented invalidate-on-reconnect simplification, F6), so it must not be\r\n // reported to the server in `$sys.Reconnect` — that would re-arm stage-2\r\n // invalidation tracking for a computed we're about to drop. Report null,\r\n // mirroring C# RpcOutboundComputeCall.GetReconnectStage's peer-change branch.\r\n override getReconnectStage(isPeerChanged: boolean): number | null {\r\n if (this.result.isCompleted)\r\n return null;\r\n\r\n return super.getReconnectStage(isPeerChanged);\r\n }\r\n\r\n override onDisconnect(): void {\r\n this.whenInvalidated.resolve();\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuallab/fusion-rpc",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.1.6",
|
|
4
4
|
"description": "Bridge between Fusion and RPC - FusionHub, compute service registration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -26,12 +26,17 @@
|
|
|
26
26
|
"clean": "rm -rf dist"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@actuallab/core": "15.
|
|
30
|
-
"@actuallab/fusion": "15.
|
|
31
|
-
"@actuallab/rpc": "15.
|
|
29
|
+
"@actuallab/core": "15.1.6",
|
|
30
|
+
"@actuallab/fusion": "15.1.6",
|
|
31
|
+
"@actuallab/rpc": "15.1.6"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/ActualLab/Fusion.git",
|
|
39
|
+
"directory": "ts/packages/fusion-rpc"
|
|
40
|
+
},
|
|
36
41
|
"license": "MIT"
|
|
37
42
|
}
|