@actuallab/fusion-rpc 12.1.62 → 12.1.100

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
@@ -46,14 +46,32 @@ var RpcOutboundComputeCall = class extends import_rpc.RpcOutboundCall {
46
46
  // src/fusion-hub.ts
47
47
  var FUSION_CALL_TYPE_ID = 1;
48
48
  var FUSION_INVALIDATE_METHOD = "$sys-c.Invalidate:0";
49
+ var FusionSystemCallHandler = class extends import_rpc2.RpcSystemCallHandler {
50
+ handle(message, args, peer) {
51
+ const method = message.Method;
52
+ const relatedId = message.RelatedId ?? 0;
53
+ if (method === FUSION_INVALIDATE_METHOD) {
54
+ const call = peer.outbound.remove(relatedId);
55
+ if (call instanceof RpcOutboundComputeCall) {
56
+ call.whenInvalidated.resolve();
57
+ }
58
+ return;
59
+ }
60
+ super.handle(message, args, peer);
61
+ }
62
+ };
49
63
  function defineComputeService(name, methods) {
50
64
  const withCallType = {};
51
65
  for (const [methodName, input] of Object.entries(methods)) {
52
66
  withCallType[methodName] = { ...input, callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID };
53
67
  }
54
- return (0, import_rpc2.defineRpcService)(name, withCallType, { ctOffset: 1 });
68
+ return (0, import_rpc2.defineRpcService)(name, withCallType);
55
69
  }
56
70
  var FusionHub = class extends import_rpc2.RpcHub {
71
+ constructor(hubId) {
72
+ super(hubId);
73
+ this.systemCallHandler = new FusionSystemCallHandler();
74
+ }
57
75
  /** Accept an incoming WebSocket and create a server peer. */
58
76
  acceptConnection(ws) {
59
77
  const ref = `server://${crypto.randomUUID()}`;
@@ -69,28 +87,14 @@ var FusionHub = class extends import_rpc2.RpcHub {
69
87
  peer.accept(conn);
70
88
  return peer;
71
89
  }
72
- /** Handle compute-specific system calls ($sys-c.Invalidate), then delegate to base. */
73
- handleSystemCall(message, args, outbound, inbound) {
74
- const method = message.Method;
75
- const relatedId = message.RelatedId ?? 0;
76
- if (method === FUSION_INVALIDATE_METHOD) {
77
- const call = outbound.remove(relatedId);
78
- if (call instanceof RpcOutboundComputeCall) {
79
- call.whenInvalidated.resolve();
80
- }
81
- return;
82
- }
83
- super.handleSystemCall(message, args, outbound, inbound);
84
- }
85
- /** Override to apply ctOffset=1 default and FUSION_CALL_TYPE_ID for compute methods. */
90
+ /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
86
91
  _buildServiceDef(cls) {
87
92
  const svcMeta = (0, import_rpc2.getServiceMeta)(cls);
88
93
  if (svcMeta === void 0) throw new Error("Contract class missing @rpcService metadata");
89
94
  const methodsMeta = (0, import_rpc2.getMethodsMeta)(cls) ?? {};
90
- const ctOffset = svcMeta.ctOffset ?? 1;
91
95
  const methods = /* @__PURE__ */ new Map();
92
96
  for (const [name, meta] of Object.entries(methodsMeta)) {
93
- const wireArgCount = meta.argCount + ctOffset;
97
+ const wireArgCount = meta.argCount + 1;
94
98
  const mapKey = `${name}:${wireArgCount}`;
95
99
  methods.set(mapKey, {
96
100
  name,
@@ -98,8 +102,8 @@ var FusionHub = class extends import_rpc2.RpcHub {
98
102
  argCount: meta.argCount,
99
103
  wireArgCount,
100
104
  callTypeId: meta.compute === true ? FUSION_CALL_TYPE_ID : 0,
101
- stream: meta.stream ?? false,
102
- noWait: meta.noWait ?? false
105
+ stream: meta.returns === import_rpc2.RpcType.stream,
106
+ noWait: meta.returns === import_rpc2.RpcType.noWait
103
107
  });
104
108
  }
105
109
  return { name: svcMeta.name, methods };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/fusion-hub.ts","../src/rpc-outbound-compute-call.ts"],"sourcesContent":["export { FusionHub, defineComputeService, FUSION_CALL_TYPE_ID } 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 serializeMessage,\r\n defineRpcService,\r\n wireMethodName,\r\n type RpcConnection,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcServiceImpl,\r\n type RpcDispatchContext,\r\n type RpcPeer,\r\n type RpcMessage,\r\n type RpcCallOptions,\r\n RpcOutboundCallTracker,\r\n RpcInboundCallTracker,\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/** 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] = { ...input, callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID };\r\n }\r\n return defineRpcService(name, withCallType, { ctOffset: 1 });\r\n}\r\n\r\n/** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */\r\nexport class FusionHub extends RpcHub {\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 /** Handle compute-specific system calls ($sys-c.Invalidate), then delegate to base. */\r\n override handleSystemCall(\r\n message: RpcMessage,\r\n args: unknown[],\r\n outbound: RpcOutboundCallTracker,\r\n inbound: RpcInboundCallTracker,\r\n ): 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 // Server invalidated a compute call — remove from tracker and resolve\r\n const call = outbound.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.handleSystemCall(message, args, outbound, inbound);\r\n }\r\n\r\n /** Override to apply ctOffset=1 default and FUSION_CALL_TYPE_ID for compute methods. */\r\n protected override _buildServiceDef(cls: abstract new (...args: any[]) => any): RpcServiceDef {\r\n const svcMeta = getServiceMeta(cls);\r\n if (svcMeta === undefined) throw new Error(\"Contract class missing @rpcService metadata\");\r\n\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n // FusionHub defaults ctOffset to 1 (CancellationToken convention)\r\n const ctOffset = svcMeta.ctOffset ?? 1;\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 + ctOffset;\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: (meta as any).compute === true ? FUSION_CALL_TYPE_ID : 0,\r\n stream: meta.stream ?? false,\r\n noWait: meta.noWait ?? false,\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, impl: object,\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(methodDef.name, fn as ComputeFunctionImpl);\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, 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(...args: unknown[]) {\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) => new RpcOutboundComputeCall(id, m),\r\n };\r\n const outboundCall = peer.call(wireName, args, callOptions) as RpcOutboundComputeCall;\r\n const value = await outboundCall.result.promise;\r\n // Wire server invalidation → local computed invalidation\r\n if (computeCtx) {\r\n outboundCall.whenInvalidated.promise.then(() => computeCtx.computed.invalidate());\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 = 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(args: unknown[]): 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 { FusionHub, defineComputeService, FUSION_CALL_TYPE_ID } 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 type RpcConnection,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcServiceImpl,\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.outbound.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] = { ...input, callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID };\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 }\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(cls: abstract new (...args: any[]) => any): RpcServiceDef {\r\n const svcMeta = getServiceMeta(cls);\r\n if (svcMeta === undefined) 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: (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 });\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, impl: object,\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(methodDef.name, fn as ComputeFunctionImpl);\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, 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(...args: unknown[]) {\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) => new RpcOutboundComputeCall(id, m),\r\n };\r\n const outboundCall = peer.call(wireName, args, callOptions) as RpcOutboundComputeCall;\r\n const value = await outboundCall.result.promise;\r\n // Wire server invalidation → local computed invalidation\r\n if (computeCtx) {\r\n outboundCall.whenInvalidated.promise.then(() => computeCtx.computed.invalidate());\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 = 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(args: unknown[]): 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"]}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { RpcHub, WebSocketLike, RpcServerPeer, RpcConnection, RpcMessage, RpcOutboundCallTracker, RpcInboundCallTracker, RpcServiceDef, RpcMethodDef, RpcPeer, RpcMethodDefInput, RpcOutboundCall } from '@actuallab/rpc';
1
+ import { RpcHub, WebSocketLike, RpcServerPeer, RpcConnection, 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. */
@@ -7,13 +7,12 @@ declare const FUSION_CALL_TYPE_ID = 1;
7
7
  declare function defineComputeService(name: string, methods: Record<string, RpcMethodDefInput>): RpcServiceDef;
8
8
  /** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */
9
9
  declare class FusionHub extends RpcHub {
10
+ constructor(hubId?: string);
10
11
  /** Accept an incoming WebSocket and create a server peer. */
11
12
  acceptConnection(ws: WebSocketLike): RpcServerPeer;
12
13
  /** Accept an RpcConnection and create a server peer. */
13
14
  acceptRpcConnection(conn: RpcConnection): RpcServerPeer;
14
- /** Handle compute-specific system calls ($sys-c.Invalidate), then delegate to base. */
15
- handleSystemCall(message: RpcMessage, args: unknown[], outbound: RpcOutboundCallTracker, inbound: RpcInboundCallTracker): void;
16
- /** Override to apply ctOffset=1 default and FUSION_CALL_TYPE_ID for compute methods. */
15
+ /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
17
16
  protected _buildServiceDef(cls: abstract new (...args: any[]) => any): RpcServiceDef;
18
17
  protected _wrapServerMethod(methodDef: RpcMethodDef, fn: (...args: unknown[]) => unknown, impl: object): (...args: unknown[]) => unknown;
19
18
  protected _createClientMethod(peer: RpcPeer, methodDef: RpcMethodDef): (...args: unknown[]) => unknown;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { RpcHub, WebSocketLike, RpcServerPeer, RpcConnection, RpcMessage, RpcOutboundCallTracker, RpcInboundCallTracker, RpcServiceDef, RpcMethodDef, RpcPeer, RpcMethodDefInput, RpcOutboundCall } from '@actuallab/rpc';
1
+ import { RpcHub, WebSocketLike, RpcServerPeer, RpcConnection, 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. */
@@ -7,13 +7,12 @@ declare const FUSION_CALL_TYPE_ID = 1;
7
7
  declare function defineComputeService(name: string, methods: Record<string, RpcMethodDefInput>): RpcServiceDef;
8
8
  /** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */
9
9
  declare class FusionHub extends RpcHub {
10
+ constructor(hubId?: string);
10
11
  /** Accept an incoming WebSocket and create a server peer. */
11
12
  acceptConnection(ws: WebSocketLike): RpcServerPeer;
12
13
  /** Accept an RpcConnection and create a server peer. */
13
14
  acceptRpcConnection(conn: RpcConnection): RpcServerPeer;
14
- /** Handle compute-specific system calls ($sys-c.Invalidate), then delegate to base. */
15
- handleSystemCall(message: RpcMessage, args: unknown[], outbound: RpcOutboundCallTracker, inbound: RpcInboundCallTracker): void;
16
- /** Override to apply ctOffset=1 default and FUSION_CALL_TYPE_ID for compute methods. */
15
+ /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
17
16
  protected _buildServiceDef(cls: abstract new (...args: any[]) => any): RpcServiceDef;
18
17
  protected _wrapServerMethod(methodDef: RpcMethodDef, fn: (...args: unknown[]) => unknown, impl: object): (...args: unknown[]) => unknown;
19
18
  protected _createClientMethod(peer: RpcPeer, methodDef: RpcMethodDef): (...args: unknown[]) => unknown;
package/dist/index.js CHANGED
@@ -2,9 +2,11 @@
2
2
  import {
3
3
  RpcHub,
4
4
  RpcWebSocketConnection,
5
+ RpcSystemCallHandler,
5
6
  serializeMessage,
6
7
  defineRpcService,
7
8
  wireMethodName,
9
+ RpcType,
8
10
  getServiceMeta,
9
11
  getMethodsMeta
10
12
  } from "@actuallab/rpc";
@@ -28,14 +30,32 @@ var RpcOutboundComputeCall = class extends RpcOutboundCall {
28
30
  // src/fusion-hub.ts
29
31
  var FUSION_CALL_TYPE_ID = 1;
30
32
  var FUSION_INVALIDATE_METHOD = "$sys-c.Invalidate:0";
33
+ var FusionSystemCallHandler = class extends RpcSystemCallHandler {
34
+ handle(message, args, peer) {
35
+ const method = message.Method;
36
+ const relatedId = message.RelatedId ?? 0;
37
+ if (method === FUSION_INVALIDATE_METHOD) {
38
+ const call = peer.outbound.remove(relatedId);
39
+ if (call instanceof RpcOutboundComputeCall) {
40
+ call.whenInvalidated.resolve();
41
+ }
42
+ return;
43
+ }
44
+ super.handle(message, args, peer);
45
+ }
46
+ };
31
47
  function defineComputeService(name, methods) {
32
48
  const withCallType = {};
33
49
  for (const [methodName, input] of Object.entries(methods)) {
34
50
  withCallType[methodName] = { ...input, callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID };
35
51
  }
36
- return defineRpcService(name, withCallType, { ctOffset: 1 });
52
+ return defineRpcService(name, withCallType);
37
53
  }
38
54
  var FusionHub = class extends RpcHub {
55
+ constructor(hubId) {
56
+ super(hubId);
57
+ this.systemCallHandler = new FusionSystemCallHandler();
58
+ }
39
59
  /** Accept an incoming WebSocket and create a server peer. */
40
60
  acceptConnection(ws) {
41
61
  const ref = `server://${crypto.randomUUID()}`;
@@ -51,28 +71,14 @@ var FusionHub = class extends RpcHub {
51
71
  peer.accept(conn);
52
72
  return peer;
53
73
  }
54
- /** Handle compute-specific system calls ($sys-c.Invalidate), then delegate to base. */
55
- handleSystemCall(message, args, outbound, inbound) {
56
- const method = message.Method;
57
- const relatedId = message.RelatedId ?? 0;
58
- if (method === FUSION_INVALIDATE_METHOD) {
59
- const call = outbound.remove(relatedId);
60
- if (call instanceof RpcOutboundComputeCall) {
61
- call.whenInvalidated.resolve();
62
- }
63
- return;
64
- }
65
- super.handleSystemCall(message, args, outbound, inbound);
66
- }
67
- /** Override to apply ctOffset=1 default and FUSION_CALL_TYPE_ID for compute methods. */
74
+ /** Override to apply FUSION_CALL_TYPE_ID for compute methods. */
68
75
  _buildServiceDef(cls) {
69
76
  const svcMeta = getServiceMeta(cls);
70
77
  if (svcMeta === void 0) throw new Error("Contract class missing @rpcService metadata");
71
78
  const methodsMeta = getMethodsMeta(cls) ?? {};
72
- const ctOffset = svcMeta.ctOffset ?? 1;
73
79
  const methods = /* @__PURE__ */ new Map();
74
80
  for (const [name, meta] of Object.entries(methodsMeta)) {
75
- const wireArgCount = meta.argCount + ctOffset;
81
+ const wireArgCount = meta.argCount + 1;
76
82
  const mapKey = `${name}:${wireArgCount}`;
77
83
  methods.set(mapKey, {
78
84
  name,
@@ -80,8 +86,8 @@ var FusionHub = class extends RpcHub {
80
86
  argCount: meta.argCount,
81
87
  wireArgCount,
82
88
  callTypeId: meta.compute === true ? FUSION_CALL_TYPE_ID : 0,
83
- stream: meta.stream ?? false,
84
- noWait: meta.noWait ?? false
89
+ stream: meta.returns === RpcType.stream,
90
+ noWait: meta.returns === RpcType.noWait
85
91
  });
86
92
  }
87
93
  return { name: svcMeta.name, methods };
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 serializeMessage,\r\n defineRpcService,\r\n wireMethodName,\r\n type RpcConnection,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcServiceImpl,\r\n type RpcDispatchContext,\r\n type RpcPeer,\r\n type RpcMessage,\r\n type RpcCallOptions,\r\n RpcOutboundCallTracker,\r\n RpcInboundCallTracker,\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/** 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] = { ...input, callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID };\r\n }\r\n return defineRpcService(name, withCallType, { ctOffset: 1 });\r\n}\r\n\r\n/** Central coordinator for Fusion + RPC — manages compute services, invalidation wiring. */\r\nexport class FusionHub extends RpcHub {\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 /** Handle compute-specific system calls ($sys-c.Invalidate), then delegate to base. */\r\n override handleSystemCall(\r\n message: RpcMessage,\r\n args: unknown[],\r\n outbound: RpcOutboundCallTracker,\r\n inbound: RpcInboundCallTracker,\r\n ): 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 // Server invalidated a compute call — remove from tracker and resolve\r\n const call = outbound.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.handleSystemCall(message, args, outbound, inbound);\r\n }\r\n\r\n /** Override to apply ctOffset=1 default and FUSION_CALL_TYPE_ID for compute methods. */\r\n protected override _buildServiceDef(cls: abstract new (...args: any[]) => any): RpcServiceDef {\r\n const svcMeta = getServiceMeta(cls);\r\n if (svcMeta === undefined) throw new Error(\"Contract class missing @rpcService metadata\");\r\n\r\n const methodsMeta = getMethodsMeta(cls) ?? {};\r\n // FusionHub defaults ctOffset to 1 (CancellationToken convention)\r\n const ctOffset = svcMeta.ctOffset ?? 1;\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 + ctOffset;\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: (meta as any).compute === true ? FUSION_CALL_TYPE_ID : 0,\r\n stream: meta.stream ?? false,\r\n noWait: meta.noWait ?? false,\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, impl: object,\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(methodDef.name, fn as ComputeFunctionImpl);\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, 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(...args: unknown[]) {\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) => new RpcOutboundComputeCall(id, m),\r\n };\r\n const outboundCall = peer.call(wireName, args, callOptions) as RpcOutboundComputeCall;\r\n const value = await outboundCall.result.promise;\r\n // Wire server invalidation → local computed invalidation\r\n if (computeCtx) {\r\n outboundCall.whenInvalidated.promise.then(() => computeCtx.computed.invalidate());\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 = 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(args: unknown[]): 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 serializeMessage,\r\n defineRpcService,\r\n wireMethodName,\r\n RpcType,\r\n type RpcConnection,\r\n type WebSocketLike,\r\n type RpcServiceDef,\r\n type RpcMethodDef,\r\n type RpcMethodDefInput,\r\n type RpcServiceImpl,\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.outbound.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] = { ...input, callTypeId: input.callTypeId ?? FUSION_CALL_TYPE_ID };\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 }\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(cls: abstract new (...args: any[]) => any): RpcServiceDef {\r\n const svcMeta = getServiceMeta(cls);\r\n if (svcMeta === undefined) 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: (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 });\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, impl: object,\r\n ): (...args: unknown[]) => unknown {\r\n if (methodDef.callTypeId !== FUSION_CALL_TYPE_ID)\r\n return fn;\r\n\r\n // Route through ComputeFunction + wire invalidation → $sys-c.Invalidate\r\n const cf = new ComputeFunction(methodDef.name, fn as ComputeFunctionImpl);\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, 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(...args: unknown[]) {\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) => new RpcOutboundComputeCall(id, m),\r\n };\r\n const outboundCall = peer.call(wireName, args, callOptions) as RpcOutboundComputeCall;\r\n const value = await outboundCall.result.promise;\r\n // Wire server invalidation → local computed invalidation\r\n if (computeCtx) {\r\n outboundCall.whenInvalidated.promise.then(() => computeCtx.computed.invalidate());\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 = 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(args: unknown[]): 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuallab/fusion-rpc",
3
- "version": "12.1.62",
3
+ "version": "12.1.100",
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": "12.1.62",
30
- "@actuallab/fusion": "12.1.62",
31
- "@actuallab/rpc": "12.1.62"
29
+ "@actuallab/core": "12.1.100",
30
+ "@actuallab/fusion": "12.1.100",
31
+ "@actuallab/rpc": "12.1.100"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"