@actuallab/fusion-rpc 13.0.25 → 13.0.163

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 CHANGED
@@ -38,6 +38,16 @@ var import_rpc = require("@actuallab/rpc");
38
38
  var RpcOutboundComputeCall = class extends import_rpc.RpcOutboundCall {
39
39
  removeOnOk = false;
40
40
  whenInvalidated = new import_core.PromiseSource();
41
+ // A completed compute call is always self-invalidated on reconnect (the
42
+ // documented invalidate-on-reconnect simplification, F6), so it must not be
43
+ // reported to the server in `$sys.Reconnect` — that would re-arm stage-2
44
+ // invalidation tracking for a computed we're about to drop. Report null,
45
+ // mirroring C# RpcOutboundComputeCall.GetReconnectStage's peer-change branch.
46
+ getReconnectStage(isPeerChanged) {
47
+ if (this.result.isCompleted)
48
+ return null;
49
+ return super.getReconnectStage(isPeerChanged);
50
+ }
41
51
  onDisconnect() {
42
52
  this.whenInvalidated.resolve();
43
53
  }
@@ -46,13 +56,17 @@ var RpcOutboundComputeCall = class extends import_rpc.RpcOutboundCall {
46
56
  // src/fusion-hub.ts
47
57
  var FUSION_CALL_TYPE_ID = 1;
48
58
  var FUSION_INVALIDATE_METHOD = "$sys-c.Invalidate:0";
59
+ var COMPUTE_CALL_MAX_TRY_COUNT = 3;
49
60
  var FusionSystemCallHandler = class extends import_rpc2.RpcSystemCallHandler {
50
61
  handle(message, args, peer) {
51
62
  const method = message.Method;
52
63
  const relatedId = message.RelatedId ?? 0;
53
64
  if (method === FUSION_INVALIDATE_METHOD) {
54
- const call = peer.outboundCalls.remove(relatedId);
65
+ const call = peer.outboundCalls.get(relatedId);
55
66
  if (call instanceof RpcOutboundComputeCall) {
67
+ peer.outboundCalls.remove(relatedId);
68
+ if (!call.result.isCompleted)
69
+ call.result.reject((0, import_core2.cancellationError)("Compute call invalidated before result."));
56
70
  call.whenInvalidated.resolve();
57
71
  }
58
72
  return;
@@ -60,6 +74,14 @@ var FusionSystemCallHandler = class extends import_rpc2.RpcSystemCallHandler {
60
74
  super.handle(message, args, peer);
61
75
  }
62
76
  };
77
+ var FusionSystemCallSender = class extends import_rpc2.RpcSystemCallSender {
78
+ invalidate(conn, format, relatedId) {
79
+ this._send(conn, format, {
80
+ Method: FUSION_INVALIDATE_METHOD,
81
+ RelatedId: relatedId
82
+ });
83
+ }
84
+ };
63
85
  function defineComputeService(name, methods) {
64
86
  const withCallType = {};
65
87
  for (const [methodName, input] of Object.entries(methods)) {
@@ -76,6 +98,9 @@ var FusionHub = class extends import_rpc2.RpcHub {
76
98
  this.systemCallHandler = new FusionSystemCallHandler();
77
99
  this.registry.register(FUSION_INVALIDATE_METHOD);
78
100
  }
101
+ _createSystemCallSender() {
102
+ return new FusionSystemCallSender();
103
+ }
79
104
  /** Accept an incoming WebSocket and create a server peer. */
80
105
  acceptConnection(ws) {
81
106
  const ref = `server://${crypto.randomUUID()}`;
@@ -91,31 +116,20 @@ var FusionHub = class extends import_rpc2.RpcHub {
91
116
  peer.accept(conn);
92
117
  return peer;
93
118
  }
94
- /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
119
+ /** Delegate to the base builder, then patch callTypeId for compute methods —
120
+ * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */
95
121
  _buildServiceDef(cls) {
96
- const svcMeta = (0, import_rpc2.getServiceMeta)(cls);
97
- if (svcMeta === void 0)
98
- throw new Error("Contract class missing @rpcService metadata");
122
+ const def = super._buildServiceDef(cls);
99
123
  const methodsMeta = (0, import_rpc2.getMethodsMeta)(cls) ?? {};
100
124
  const methods = /* @__PURE__ */ new Map();
101
- for (const [name, meta] of Object.entries(methodsMeta)) {
102
- const wireArgCount = meta.argCount + 1;
103
- const mapKey = `${name}:${wireArgCount}`;
104
- methods.set(mapKey, {
105
- name,
106
- serviceName: svcMeta.name,
107
- argCount: meta.argCount,
108
- wireArgCount,
109
- callTypeId: (
110
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
111
- meta.compute === true ? FUSION_CALL_TYPE_ID : 0
112
- ),
113
- stream: meta.returns === import_rpc2.RpcType.stream,
114
- noWait: meta.returns === import_rpc2.RpcType.noWait,
115
- remoteExecutionMode: import_rpc2.RpcRemoteExecutionMode.Default
116
- });
125
+ for (const [mapKey, methodDef] of def.methods) {
126
+ const isCompute = methodsMeta[methodDef.name]?.compute === true;
127
+ methods.set(
128
+ mapKey,
129
+ isCompute ? { ...methodDef, callTypeId: FUSION_CALL_TYPE_ID } : methodDef
130
+ );
117
131
  }
118
- return { name: svcMeta.name, methods };
132
+ return { name: def.name, methods };
119
133
  }
120
134
  _wrapServerMethod(methodDef, fn, impl) {
121
135
  if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID) return fn;
@@ -127,13 +141,18 @@ var FusionHub = class extends import_rpc2.RpcHub {
127
141
  const context = extractDispatchContext(args);
128
142
  const cleanArgs = context !== void 0 ? args.slice(0, -1) : args;
129
143
  const computed = await cf.invoke(impl, cleanArgs);
130
- if (context !== void 0) {
131
- computed.onInvalidated.add(() => {
132
- const msg = (0, import_rpc2.serializeMessage)({
133
- Method: FUSION_INVALIDATE_METHOD,
134
- RelatedId: context.callId
135
- });
136
- context.connection.send(msg);
144
+ if (context?.callType === FUSION_CALL_TYPE_ID) {
145
+ const { peer, callId } = context;
146
+ void computed.whenInvalidated().then(() => {
147
+ setTimeout(() => {
148
+ const conn = peer.connection;
149
+ if (conn !== void 0)
150
+ this.systemCallSender.invalidate(
151
+ conn,
152
+ peer.serializationFormat,
153
+ callId
154
+ );
155
+ }, 0);
137
156
  });
138
157
  }
139
158
  return computed.value;
@@ -144,23 +163,37 @@ var FusionHub = class extends import_rpc2.RpcHub {
144
163
  return super._createClientMethod(peer, methodDef);
145
164
  const wireName = (0, import_rpc2.wireMethodName)(methodDef);
146
165
  const rpcImpl = async function(...args) {
147
- const computeCtx = import_fusion.ComputeContext.from(import_core2.AsyncContext.current);
166
+ const last = args[args.length - 1];
167
+ const threadedCtx = last instanceof import_core2.AsyncContext ? last : void 0;
168
+ const callArgs = threadedCtx ? args.slice(0, -1) : args;
169
+ const current = threadedCtx ?? import_core2.AsyncContext.current;
170
+ const computeCtx = import_fusion.ComputeContext.from(current);
171
+ const signal = current?.get(import_core2.abortSignalKey);
148
172
  const callOptions = {
149
173
  callTypeId: FUSION_CALL_TYPE_ID,
150
- outboundCallFactory: (id, m) => new RpcOutboundComputeCall(id, m)
174
+ outboundCallFactory: (id, m) => new RpcOutboundComputeCall(id, m),
175
+ signal
151
176
  };
152
- const outboundCall = peer.call(
153
- wireName,
154
- args,
155
- callOptions
156
- );
157
- const value = await outboundCall.result;
158
- if (computeCtx) {
159
- void outboundCall.whenInvalidated.then(
160
- () => computeCtx.computed.invalidate()
177
+ let tryIndex = 0;
178
+ for (; ; ) {
179
+ const outboundCall = peer.call(
180
+ wireName,
181
+ callArgs,
182
+ callOptions
161
183
  );
184
+ try {
185
+ const value = await outboundCall.result;
186
+ bindComputeCall(peer, computeCtx, outboundCall);
187
+ return value;
188
+ } catch (e) {
189
+ if (signal?.aborted)
190
+ throw e;
191
+ if ((0, import_core2.isCancellation)(e) && peer.isConnected && ++tryIndex < COMPUTE_CALL_MAX_TRY_COUNT)
192
+ continue;
193
+ bindComputeCall(peer, computeCtx, outboundCall);
194
+ throw e;
195
+ }
162
196
  }
163
- return value;
164
197
  };
165
198
  const cf = new import_fusion.ComputeFunction(methodDef.name, rpcImpl);
166
199
  const syntheticInstance = {};
@@ -172,6 +205,22 @@ var FusionHub = class extends import_rpc2.RpcHub {
172
205
  };
173
206
  }
174
207
  };
208
+ function bindComputeCall(peer, computeCtx, call) {
209
+ const computed = computeCtx?.computed;
210
+ if (computed === void 0)
211
+ return;
212
+ void call.whenInvalidated.then(() => computed.invalidate());
213
+ void computed.whenInvalidated().then(() => {
214
+ if (call.whenInvalidated.isCompleted)
215
+ return;
216
+ if (peer.outboundCalls.remove(call.callId) === void 0)
217
+ return;
218
+ call.whenInvalidated.resolve();
219
+ const conn = peer.connection;
220
+ if (conn !== void 0)
221
+ peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);
222
+ });
223
+ }
175
224
  function extractDispatchContext(args) {
176
225
  const last = args[args.length - 1];
177
226
  if (last !== null && typeof last === "object" && "__rpcDispatch" in last) {
@@ -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 serializeMessage,\r\n defineRpcService,\r\n wireMethodName,\r\n RpcType,\r\n RpcRemoteExecutionMode,\r\n type RpcConnection,\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 getServiceMeta,\r\n getMethodsMeta,\r\n} from '@actuallab/rpc';\r\nimport {\r\n ComputeFunction,\r\n ComputeContext,\r\n type ComputeFunctionImpl,\r\n} from '@actuallab/fusion';\r\nimport { AsyncContext } 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/** 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 if (method === FUSION_INVALIDATE_METHOD) {\r\n const call = peer.outboundCalls.remove(relatedId);\r\n if (call instanceof RpcOutboundComputeCall) {\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/** 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 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 /** 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 /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */\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 svcMeta = getServiceMeta(cls);\r\n if (svcMeta === undefined)\r\n throw new Error('Contract class missing @rpcService metadata');\r\n\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n const methods = new Map<string, RpcMethodDef>();\r\n\r\n for (const [name, meta] of Object.entries(methodsMeta)) {\r\n const wireArgCount = meta.argCount + 1;\r\n const mapKey = `${name}:${wireArgCount}`;\r\n methods.set(mapKey, {\r\n name,\r\n serviceName: svcMeta.name,\r\n argCount: meta.argCount,\r\n wireArgCount,\r\n callTypeId:\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access\r\n (meta as any).compute === true ? FUSION_CALL_TYPE_ID : 0,\r\n stream: meta.returns === RpcType.stream,\r\n noWait: meta.returns === RpcType.noWait,\r\n remoteExecutionMode: RpcRemoteExecutionMode.Default,\r\n });\r\n }\r\n\r\n return { name: svcMeta.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 const cleanArgs = context !== undefined ? args.slice(0, -1) : args;\r\n\r\n const computed = await cf.invoke(impl, cleanArgs);\r\n\r\n // Wire invalidation → send $sys-c.Invalidate to the client\r\n if (context !== undefined) {\r\n computed.onInvalidated.add(() => {\r\n const msg = serializeMessage({\r\n Method: FUSION_INVALIDATE_METHOD,\r\n RelatedId: context.callId,\r\n });\r\n context.connection.send(msg);\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 // Capture ComputeContext synchronously — AsyncContext.current is the child\r\n // context set by ComputeFunction.invoke() and won't survive the await below.\r\n const computeCtx = ComputeContext.from(AsyncContext.current);\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 };\r\n const outboundCall = peer.call(\r\n wireName,\r\n args,\r\n callOptions\r\n ) as RpcOutboundComputeCall;\r\n const value = await outboundCall.result;\r\n // Wire server invalidation → local computed invalidation\r\n if (computeCtx) {\r\n void outboundCall.whenInvalidated.then(() =>\r\n computeCtx.computed.invalidate()\r\n );\r\n }\r\n return value;\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\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 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 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 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 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 const cleanArgs = context !== undefined ? args.slice(0, -1) : args;\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 // 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().then(() => {\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.connection;\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 }\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 const outboundCall = peer.call(\r\n wireName,\r\n callArgs,\r\n 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// 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 void call.whenInvalidated.then(() => computed.invalidate());\r\n void computed.whenInvalidated().then(() => {\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.connection;\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.d.cts CHANGED
@@ -1,18 +1,25 @@
1
- import { RpcHub, WebSocketLike, RpcServerPeer, RpcConnection, RpcServiceDef, RpcMethodDef, RpcPeer, RpcMethodDefInput, RpcOutboundCall } from '@actuallab/rpc';
1
+ import { RpcHub, RpcSystemCallSender, RpcConnection, RpcSerializationFormat, WebSocketLike, RpcServerPeer, RpcServiceDef, RpcMethodDef, RpcPeer, RpcMethodDefInput, RpcOutboundCall } from '@actuallab/rpc';
2
2
  import { PromiseSource } from '@actuallab/core';
3
3
 
4
4
  /** Call type ID for Fusion compute calls — matches .NET's RpcComputeCallType.Id. */
5
5
  declare const FUSION_CALL_TYPE_ID = 1;
6
+ /** Sends the Fusion $sys-c.Invalidate system call — mirrors .NET's RpcComputeSystemCallSender. */
7
+ declare class FusionSystemCallSender extends RpcSystemCallSender {
8
+ invalidate(conn: RpcConnection, format: RpcSerializationFormat, relatedId: number): void;
9
+ }
6
10
  /** Creates a compute service definition — all methods default to FUSION_CALL_TYPE_ID. */
7
11
  declare function defineComputeService(name: string, methods: Record<string, RpcMethodDefInput>): RpcServiceDef;
8
12
  /** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */
9
13
  declare class FusionHub extends RpcHub {
14
+ readonly systemCallSender: FusionSystemCallSender;
10
15
  constructor(hubId?: string);
16
+ protected _createSystemCallSender(): FusionSystemCallSender;
11
17
  /** Accept an incoming WebSocket and create a server peer. */
12
18
  acceptConnection(ws: WebSocketLike): RpcServerPeer;
13
19
  /** Accept an RpcConnection and create a server peer. */
14
20
  acceptRpcConnection(conn: RpcConnection): RpcServerPeer;
15
- /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
21
+ /** Delegate to the base builder, then patch callTypeId for compute methods —
22
+ * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */
16
23
  protected _buildServiceDef(cls: abstract new (...args: any[]) => any): RpcServiceDef;
17
24
  protected _wrapServerMethod(methodDef: RpcMethodDef, fn: (...args: unknown[]) => unknown, impl: object): (...args: unknown[]) => unknown;
18
25
  protected _createClientMethod(peer: RpcPeer, methodDef: RpcMethodDef): (...args: unknown[]) => unknown;
@@ -22,6 +29,7 @@ declare class FusionHub extends RpcHub {
22
29
  declare class RpcOutboundComputeCall extends RpcOutboundCall {
23
30
  readonly removeOnOk = false;
24
31
  readonly whenInvalidated: PromiseSource<void>;
32
+ getReconnectStage(isPeerChanged: boolean): number | null;
25
33
  onDisconnect(): void;
26
34
  }
27
35
 
package/dist/index.d.ts CHANGED
@@ -1,18 +1,25 @@
1
- import { RpcHub, WebSocketLike, RpcServerPeer, RpcConnection, RpcServiceDef, RpcMethodDef, RpcPeer, RpcMethodDefInput, RpcOutboundCall } from '@actuallab/rpc';
1
+ import { RpcHub, RpcSystemCallSender, RpcConnection, RpcSerializationFormat, WebSocketLike, RpcServerPeer, RpcServiceDef, RpcMethodDef, RpcPeer, RpcMethodDefInput, RpcOutboundCall } from '@actuallab/rpc';
2
2
  import { PromiseSource } from '@actuallab/core';
3
3
 
4
4
  /** Call type ID for Fusion compute calls — matches .NET's RpcComputeCallType.Id. */
5
5
  declare const FUSION_CALL_TYPE_ID = 1;
6
+ /** Sends the Fusion $sys-c.Invalidate system call — mirrors .NET's RpcComputeSystemCallSender. */
7
+ declare class FusionSystemCallSender extends RpcSystemCallSender {
8
+ invalidate(conn: RpcConnection, format: RpcSerializationFormat, relatedId: number): void;
9
+ }
6
10
  /** Creates a compute service definition — all methods default to FUSION_CALL_TYPE_ID. */
7
11
  declare function defineComputeService(name: string, methods: Record<string, RpcMethodDefInput>): RpcServiceDef;
8
12
  /** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */
9
13
  declare class FusionHub extends RpcHub {
14
+ readonly systemCallSender: FusionSystemCallSender;
10
15
  constructor(hubId?: string);
16
+ protected _createSystemCallSender(): FusionSystemCallSender;
11
17
  /** Accept an incoming WebSocket and create a server peer. */
12
18
  acceptConnection(ws: WebSocketLike): RpcServerPeer;
13
19
  /** Accept an RpcConnection and create a server peer. */
14
20
  acceptRpcConnection(conn: RpcConnection): RpcServerPeer;
15
- /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
21
+ /** Delegate to the base builder, then patch callTypeId for compute methods —
22
+ * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */
16
23
  protected _buildServiceDef(cls: abstract new (...args: any[]) => any): RpcServiceDef;
17
24
  protected _wrapServerMethod(methodDef: RpcMethodDef, fn: (...args: unknown[]) => unknown, impl: object): (...args: unknown[]) => unknown;
18
25
  protected _createClientMethod(peer: RpcPeer, methodDef: RpcMethodDef): (...args: unknown[]) => unknown;
@@ -22,6 +29,7 @@ declare class FusionHub extends RpcHub {
22
29
  declare class RpcOutboundComputeCall extends RpcOutboundCall {
23
30
  readonly removeOnOk = false;
24
31
  readonly whenInvalidated: PromiseSource<void>;
32
+ getReconnectStage(isPeerChanged: boolean): number | null;
25
33
  onDisconnect(): void;
26
34
  }
27
35
 
package/dist/index.js CHANGED
@@ -3,19 +3,21 @@ import {
3
3
  RpcHub,
4
4
  RpcWebSocketConnection,
5
5
  RpcSystemCallHandler,
6
- serializeMessage,
6
+ RpcSystemCallSender,
7
7
  defineRpcService,
8
8
  wireMethodName,
9
- RpcType,
10
- RpcRemoteExecutionMode,
11
- getServiceMeta,
12
9
  getMethodsMeta
13
10
  } from "@actuallab/rpc";
14
11
  import {
15
12
  ComputeFunction,
16
13
  ComputeContext
17
14
  } from "@actuallab/fusion";
18
- import { AsyncContext } from "@actuallab/core";
15
+ import {
16
+ AsyncContext,
17
+ abortSignalKey,
18
+ cancellationError,
19
+ isCancellation
20
+ } from "@actuallab/core";
19
21
 
20
22
  // src/rpc-outbound-compute-call.ts
21
23
  import { PromiseSource } from "@actuallab/core";
@@ -23,6 +25,16 @@ import { RpcOutboundCall } from "@actuallab/rpc";
23
25
  var RpcOutboundComputeCall = class extends RpcOutboundCall {
24
26
  removeOnOk = false;
25
27
  whenInvalidated = new PromiseSource();
28
+ // A completed compute call is always self-invalidated on reconnect (the
29
+ // documented invalidate-on-reconnect simplification, F6), so it must not be
30
+ // reported to the server in `$sys.Reconnect` — that would re-arm stage-2
31
+ // invalidation tracking for a computed we're about to drop. Report null,
32
+ // mirroring C# RpcOutboundComputeCall.GetReconnectStage's peer-change branch.
33
+ getReconnectStage(isPeerChanged) {
34
+ if (this.result.isCompleted)
35
+ return null;
36
+ return super.getReconnectStage(isPeerChanged);
37
+ }
26
38
  onDisconnect() {
27
39
  this.whenInvalidated.resolve();
28
40
  }
@@ -31,13 +43,17 @@ var RpcOutboundComputeCall = class extends RpcOutboundCall {
31
43
  // src/fusion-hub.ts
32
44
  var FUSION_CALL_TYPE_ID = 1;
33
45
  var FUSION_INVALIDATE_METHOD = "$sys-c.Invalidate:0";
46
+ var COMPUTE_CALL_MAX_TRY_COUNT = 3;
34
47
  var FusionSystemCallHandler = class extends RpcSystemCallHandler {
35
48
  handle(message, args, peer) {
36
49
  const method = message.Method;
37
50
  const relatedId = message.RelatedId ?? 0;
38
51
  if (method === FUSION_INVALIDATE_METHOD) {
39
- const call = peer.outboundCalls.remove(relatedId);
52
+ const call = peer.outboundCalls.get(relatedId);
40
53
  if (call instanceof RpcOutboundComputeCall) {
54
+ peer.outboundCalls.remove(relatedId);
55
+ if (!call.result.isCompleted)
56
+ call.result.reject(cancellationError("Compute call invalidated before result."));
41
57
  call.whenInvalidated.resolve();
42
58
  }
43
59
  return;
@@ -45,6 +61,14 @@ var FusionSystemCallHandler = class extends RpcSystemCallHandler {
45
61
  super.handle(message, args, peer);
46
62
  }
47
63
  };
64
+ var FusionSystemCallSender = class extends RpcSystemCallSender {
65
+ invalidate(conn, format, relatedId) {
66
+ this._send(conn, format, {
67
+ Method: FUSION_INVALIDATE_METHOD,
68
+ RelatedId: relatedId
69
+ });
70
+ }
71
+ };
48
72
  function defineComputeService(name, methods) {
49
73
  const withCallType = {};
50
74
  for (const [methodName, input] of Object.entries(methods)) {
@@ -61,6 +85,9 @@ var FusionHub = class extends RpcHub {
61
85
  this.systemCallHandler = new FusionSystemCallHandler();
62
86
  this.registry.register(FUSION_INVALIDATE_METHOD);
63
87
  }
88
+ _createSystemCallSender() {
89
+ return new FusionSystemCallSender();
90
+ }
64
91
  /** Accept an incoming WebSocket and create a server peer. */
65
92
  acceptConnection(ws) {
66
93
  const ref = `server://${crypto.randomUUID()}`;
@@ -76,31 +103,20 @@ var FusionHub = class extends RpcHub {
76
103
  peer.accept(conn);
77
104
  return peer;
78
105
  }
79
- /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
106
+ /** Delegate to the base builder, then patch callTypeId for compute methods —
107
+ * keeps the base's noWait / remoteExecutionMode handling instead of drifting. */
80
108
  _buildServiceDef(cls) {
81
- const svcMeta = getServiceMeta(cls);
82
- if (svcMeta === void 0)
83
- throw new Error("Contract class missing @rpcService metadata");
109
+ const def = super._buildServiceDef(cls);
84
110
  const methodsMeta = getMethodsMeta(cls) ?? {};
85
111
  const methods = /* @__PURE__ */ new Map();
86
- for (const [name, meta] of Object.entries(methodsMeta)) {
87
- const wireArgCount = meta.argCount + 1;
88
- const mapKey = `${name}:${wireArgCount}`;
89
- methods.set(mapKey, {
90
- name,
91
- serviceName: svcMeta.name,
92
- argCount: meta.argCount,
93
- wireArgCount,
94
- callTypeId: (
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
96
- meta.compute === true ? FUSION_CALL_TYPE_ID : 0
97
- ),
98
- stream: meta.returns === RpcType.stream,
99
- noWait: meta.returns === RpcType.noWait,
100
- remoteExecutionMode: RpcRemoteExecutionMode.Default
101
- });
112
+ for (const [mapKey, methodDef] of def.methods) {
113
+ const isCompute = methodsMeta[methodDef.name]?.compute === true;
114
+ methods.set(
115
+ mapKey,
116
+ isCompute ? { ...methodDef, callTypeId: FUSION_CALL_TYPE_ID } : methodDef
117
+ );
102
118
  }
103
- return { name: svcMeta.name, methods };
119
+ return { name: def.name, methods };
104
120
  }
105
121
  _wrapServerMethod(methodDef, fn, impl) {
106
122
  if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID) return fn;
@@ -112,13 +128,18 @@ var FusionHub = class extends RpcHub {
112
128
  const context = extractDispatchContext(args);
113
129
  const cleanArgs = context !== void 0 ? args.slice(0, -1) : args;
114
130
  const computed = await cf.invoke(impl, cleanArgs);
115
- if (context !== void 0) {
116
- computed.onInvalidated.add(() => {
117
- const msg = serializeMessage({
118
- Method: FUSION_INVALIDATE_METHOD,
119
- RelatedId: context.callId
120
- });
121
- context.connection.send(msg);
131
+ if (context?.callType === FUSION_CALL_TYPE_ID) {
132
+ const { peer, callId } = context;
133
+ void computed.whenInvalidated().then(() => {
134
+ setTimeout(() => {
135
+ const conn = peer.connection;
136
+ if (conn !== void 0)
137
+ this.systemCallSender.invalidate(
138
+ conn,
139
+ peer.serializationFormat,
140
+ callId
141
+ );
142
+ }, 0);
122
143
  });
123
144
  }
124
145
  return computed.value;
@@ -129,23 +150,37 @@ var FusionHub = class extends RpcHub {
129
150
  return super._createClientMethod(peer, methodDef);
130
151
  const wireName = wireMethodName(methodDef);
131
152
  const rpcImpl = async function(...args) {
132
- const computeCtx = ComputeContext.from(AsyncContext.current);
153
+ const last = args[args.length - 1];
154
+ const threadedCtx = last instanceof AsyncContext ? last : void 0;
155
+ const callArgs = threadedCtx ? args.slice(0, -1) : args;
156
+ const current = threadedCtx ?? AsyncContext.current;
157
+ const computeCtx = ComputeContext.from(current);
158
+ const signal = current?.get(abortSignalKey);
133
159
  const callOptions = {
134
160
  callTypeId: FUSION_CALL_TYPE_ID,
135
- outboundCallFactory: (id, m) => new RpcOutboundComputeCall(id, m)
161
+ outboundCallFactory: (id, m) => new RpcOutboundComputeCall(id, m),
162
+ signal
136
163
  };
137
- const outboundCall = peer.call(
138
- wireName,
139
- args,
140
- callOptions
141
- );
142
- const value = await outboundCall.result;
143
- if (computeCtx) {
144
- void outboundCall.whenInvalidated.then(
145
- () => computeCtx.computed.invalidate()
164
+ let tryIndex = 0;
165
+ for (; ; ) {
166
+ const outboundCall = peer.call(
167
+ wireName,
168
+ callArgs,
169
+ callOptions
146
170
  );
171
+ try {
172
+ const value = await outboundCall.result;
173
+ bindComputeCall(peer, computeCtx, outboundCall);
174
+ return value;
175
+ } catch (e) {
176
+ if (signal?.aborted)
177
+ throw e;
178
+ if (isCancellation(e) && peer.isConnected && ++tryIndex < COMPUTE_CALL_MAX_TRY_COUNT)
179
+ continue;
180
+ bindComputeCall(peer, computeCtx, outboundCall);
181
+ throw e;
182
+ }
147
183
  }
148
- return value;
149
184
  };
150
185
  const cf = new ComputeFunction(methodDef.name, rpcImpl);
151
186
  const syntheticInstance = {};
@@ -157,6 +192,22 @@ var FusionHub = class extends RpcHub {
157
192
  };
158
193
  }
159
194
  };
195
+ function bindComputeCall(peer, computeCtx, call) {
196
+ const computed = computeCtx?.computed;
197
+ if (computed === void 0)
198
+ return;
199
+ void call.whenInvalidated.then(() => computed.invalidate());
200
+ void computed.whenInvalidated().then(() => {
201
+ if (call.whenInvalidated.isCompleted)
202
+ return;
203
+ if (peer.outboundCalls.remove(call.callId) === void 0)
204
+ return;
205
+ call.whenInvalidated.resolve();
206
+ const conn = peer.connection;
207
+ if (conn !== void 0)
208
+ peer.hub.systemCallSender.cancel(conn, peer.serializationFormat, call.callId);
209
+ });
210
+ }
160
211
  function extractDispatchContext(args) {
161
212
  const last = args[args.length - 1];
162
213
  if (last !== null && typeof last === "object" && "__rpcDispatch" in last) {
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 serializeMessage,\r\n defineRpcService,\r\n wireMethodName,\r\n RpcType,\r\n RpcRemoteExecutionMode,\r\n type RpcConnection,\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 getServiceMeta,\r\n getMethodsMeta,\r\n} from '@actuallab/rpc';\r\nimport {\r\n ComputeFunction,\r\n ComputeContext,\r\n type ComputeFunctionImpl,\r\n} from '@actuallab/fusion';\r\nimport { AsyncContext } 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/** 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 if (method === FUSION_INVALIDATE_METHOD) {\r\n const call = peer.outboundCalls.remove(relatedId);\r\n if (call instanceof RpcOutboundComputeCall) {\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/** 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 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 /** 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 /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */\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 svcMeta = getServiceMeta(cls);\r\n if (svcMeta === undefined)\r\n throw new Error('Contract class missing @rpcService metadata');\r\n\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n const methods = new Map<string, RpcMethodDef>();\r\n\r\n for (const [name, meta] of Object.entries(methodsMeta)) {\r\n const wireArgCount = meta.argCount + 1;\r\n const mapKey = `${name}:${wireArgCount}`;\r\n methods.set(mapKey, {\r\n name,\r\n serviceName: svcMeta.name,\r\n argCount: meta.argCount,\r\n wireArgCount,\r\n callTypeId:\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access\r\n (meta as any).compute === true ? FUSION_CALL_TYPE_ID : 0,\r\n stream: meta.returns === RpcType.stream,\r\n noWait: meta.returns === RpcType.noWait,\r\n remoteExecutionMode: RpcRemoteExecutionMode.Default,\r\n });\r\n }\r\n\r\n return { name: svcMeta.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 const cleanArgs = context !== undefined ? args.slice(0, -1) : args;\r\n\r\n const computed = await cf.invoke(impl, cleanArgs);\r\n\r\n // Wire invalidation → send $sys-c.Invalidate to the client\r\n if (context !== undefined) {\r\n computed.onInvalidated.add(() => {\r\n const msg = serializeMessage({\r\n Method: FUSION_INVALIDATE_METHOD,\r\n RelatedId: context.callId,\r\n });\r\n context.connection.send(msg);\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 // Capture ComputeContext synchronously — AsyncContext.current is the child\r\n // context set by ComputeFunction.invoke() and won't survive the await below.\r\n const computeCtx = ComputeContext.from(AsyncContext.current);\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 };\r\n const outboundCall = peer.call(\r\n wireName,\r\n args,\r\n callOptions\r\n ) as RpcOutboundComputeCall;\r\n const value = await outboundCall.result;\r\n // Wire server invalidation → local computed invalidation\r\n if (computeCtx) {\r\n void outboundCall.whenInvalidated.then(() =>\r\n computeCtx.computed.invalidate()\r\n );\r\n }\r\n return value;\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\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 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 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 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 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 const cleanArgs = context !== undefined ? args.slice(0, -1) : args;\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 // 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().then(() => {\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.connection;\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 }\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 const outboundCall = peer.call(\r\n wireName,\r\n callArgs,\r\n 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// 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 void call.whenInvalidated.then(() => computed.invalidate());\r\n void computed.whenInvalidated().then(() => {\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.connection;\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": "13.0.25",
3
+ "version": "13.0.163",
4
4
  "description": "Bridge between Fusion and RPC - FusionHub, compute service registration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -26,9 +26,9 @@
26
26
  "clean": "rm -rf dist"
27
27
  },
28
28
  "dependencies": {
29
- "@actuallab/core": "13.0.25",
30
- "@actuallab/fusion": "13.0.25",
31
- "@actuallab/rpc": "13.0.25"
29
+ "@actuallab/core": "13.0.163",
30
+ "@actuallab/fusion": "13.0.163",
31
+ "@actuallab/rpc": "13.0.163"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"