@actuallab/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
@@ -31,14 +31,20 @@ __export(index_exports, {
31
31
  RpcInboundCall: () => RpcInboundCall,
32
32
  RpcInboundCallTracker: () => RpcInboundCallTracker,
33
33
  RpcMessageChannelConnection: () => RpcMessageChannelConnection,
34
+ RpcObjectKind: () => RpcObjectKind,
34
35
  RpcOutboundCall: () => RpcOutboundCall,
35
36
  RpcOutboundCallTracker: () => RpcOutboundCallTracker,
36
37
  RpcPeer: () => RpcPeer,
37
38
  RpcPeerConnectionKind: () => RpcPeerConnectionKind,
38
39
  RpcPeerStateKind: () => RpcPeerStateKind,
39
40
  RpcPeerStateMonitor: () => RpcPeerStateMonitor,
41
+ RpcRemoteObjectTracker: () => RpcRemoteObjectTracker,
40
42
  RpcServerPeer: () => RpcServerPeer,
41
43
  RpcServiceHost: () => RpcServiceHost,
44
+ RpcSharedObjectTracker: () => RpcSharedObjectTracker,
45
+ RpcStream: () => RpcStream,
46
+ RpcStreamSender: () => RpcStreamSender,
47
+ RpcSystemCallHandler: () => RpcSystemCallHandler,
42
48
  RpcSystemCallSender: () => RpcSystemCallSender,
43
49
  RpcSystemCalls: () => RpcSystemCalls,
44
50
  RpcType: () => RpcType,
@@ -52,9 +58,10 @@ __export(index_exports, {
52
58
  getMethodsMeta: () => getMethodsMeta,
53
59
  getServiceMeta: () => getServiceMeta,
54
60
  getStateDescription: () => getStateDescription,
55
- handleSystemCall: () => handleSystemCall,
56
61
  isConnected: () => isConnected,
57
62
  likelyConnected: () => likelyConnected,
63
+ parseStreamRef: () => parseStreamRef,
64
+ resolveStreamRefs: () => resolveStreamRefs,
58
65
  rpcMethod: () => rpcMethod,
59
66
  rpcService: () => rpcService,
60
67
  serializeFrame: () => serializeFrame,
@@ -76,7 +83,12 @@ var RpcSystemCalls = {
76
83
  error: "$sys.Error:1",
77
84
  cancel: "$sys.Cancel:0",
78
85
  keepAlive: "$sys.KeepAlive:1",
79
- reconnect: "$sys.Reconnect:3"
86
+ reconnect: "$sys.Reconnect:3",
87
+ ack: "$sys.Ack:2",
88
+ ackEnd: "$sys.AckEnd:1",
89
+ item: "$sys.I:2",
90
+ batch: "$sys.B:2",
91
+ end: "$sys.End:2"
80
92
  };
81
93
  var ENVELOPE_DELIMITER = "\n";
82
94
  var ARG_DELIMITER = "";
@@ -236,18 +248,18 @@ function createMessageChannelPair() {
236
248
  var RpcType = {
237
249
  object: /* @__PURE__ */ Symbol("object"),
238
250
  stream: /* @__PURE__ */ Symbol("stream"),
251
+ noWait: /* @__PURE__ */ Symbol("noWait"),
239
252
  void: /* @__PURE__ */ Symbol("void")
240
253
  };
241
254
  function wireMethodName(def) {
242
255
  return `${def.serviceName}.${def.name}:${def.wireArgCount}`;
243
256
  }
244
- function defineRpcService(name, methods, options) {
245
- const ctOffset = options?.ctOffset ?? 0;
257
+ function defineRpcService(name, methods) {
246
258
  const map = /* @__PURE__ */ new Map();
247
259
  for (const [key, input] of Object.entries(methods)) {
248
260
  const colonIndex = key.indexOf(":");
249
261
  const cleanName = colonIndex >= 0 ? key.substring(0, colonIndex) : key;
250
- const wireArgCount = input.args.length + ctOffset;
262
+ const wireArgCount = input.wireArgCount ?? input.args.length + 1;
251
263
  const mapKey = `${cleanName}:${wireArgCount}`;
252
264
  map.set(mapKey, {
253
265
  name: cleanName,
@@ -255,8 +267,8 @@ function defineRpcService(name, methods, options) {
255
267
  argCount: input.args.length,
256
268
  wireArgCount,
257
269
  callTypeId: input.callTypeId ?? 0,
258
- stream: input.stream ?? input.returns === RpcType.stream,
259
- noWait: input.noWait ?? false
270
+ stream: input.returns === RpcType.stream,
271
+ noWait: input.returns === RpcType.noWait
260
272
  });
261
273
  }
262
274
  return { name, methods: map };
@@ -361,40 +373,270 @@ var RpcInboundCallTracker = class {
361
373
  }
362
374
  };
363
375
 
364
- // src/rpc-system-call-handler.ts
365
- function handleSystemCall(message, args, outboundTracker, inboundTracker) {
366
- const method = message.Method;
367
- const relatedId = message.RelatedId ?? 0;
368
- switch (method) {
369
- case RpcSystemCalls.ok: {
370
- const call = outboundTracker.get(relatedId);
371
- if (call !== void 0) {
372
- if (call.removeOnOk) {
373
- outboundTracker.remove(relatedId);
376
+ // src/rpc-stream.ts
377
+ var import_core4 = require("@actuallab/core");
378
+
379
+ // src/rpc-object.ts
380
+ var RpcObjectKind = { Local: 0, Remote: 1 };
381
+
382
+ // src/rpc-stream.ts
383
+ function parseStreamRef(value) {
384
+ if (typeof value !== "string") return null;
385
+ const parts = value.split(",");
386
+ if (parts.length < 4 || parts.length > 5) return null;
387
+ const hostId = parts[0];
388
+ const localId = parseInt(parts[1], 10);
389
+ const ackPeriod = parseInt(parts[2], 10);
390
+ const ackAdvance = parseInt(parts[3], 10);
391
+ if (isNaN(localId) || isNaN(ackPeriod) || isNaN(ackAdvance)) return null;
392
+ const allowReconnect = parts.length < 5 || parts[4] !== "0";
393
+ return { hostId, localId, ackPeriod, ackAdvance, allowReconnect };
394
+ }
395
+ var RpcStream = class {
396
+ id;
397
+ kind = RpcObjectKind.Remote;
398
+ allowReconnect;
399
+ peer;
400
+ ackPeriod;
401
+ ackAdvance;
402
+ _buffer = [];
403
+ _nextExpectedIndex = 0;
404
+ _consumerWaiting = null;
405
+ _completed = false;
406
+ _completionError = null;
407
+ _disposed = false;
408
+ _started = false;
409
+ _iterating = false;
410
+ _ackSentUpTo = 0;
411
+ constructor(ref, peer) {
412
+ this.id = { hostId: ref.hostId, localId: ref.localId };
413
+ this.allowReconnect = ref.allowReconnect;
414
+ this.peer = peer;
415
+ this.ackPeriod = ref.ackPeriod;
416
+ this.ackAdvance = ref.ackAdvance;
417
+ }
418
+ /** Called by system call handler when a single item arrives ($sys.I). */
419
+ onItem(index, item) {
420
+ if (this._disposed || this._completed) return;
421
+ if (index > this._nextExpectedIndex) {
422
+ this._sendAck(this._nextExpectedIndex, true);
423
+ return;
424
+ }
425
+ if (index < this._nextExpectedIndex) {
426
+ this._maybeSendAck(index + 1);
427
+ return;
428
+ }
429
+ this._buffer.push(item);
430
+ this._nextExpectedIndex = index + 1;
431
+ this._maybeSendAck(this._nextExpectedIndex);
432
+ this._notifyConsumer();
433
+ }
434
+ /** Called by system call handler when a batch arrives ($sys.B). */
435
+ onBatch(index, items) {
436
+ if (this._disposed || this._completed) return;
437
+ if (index > this._nextExpectedIndex) {
438
+ this._sendAck(this._nextExpectedIndex, true);
439
+ return;
440
+ }
441
+ if (index < this._nextExpectedIndex) {
442
+ this._maybeSendAck(index + items.length);
443
+ return;
444
+ }
445
+ for (const item of items) this._buffer.push(item);
446
+ this._nextExpectedIndex = index + items.length;
447
+ this._maybeSendAck(this._nextExpectedIndex);
448
+ this._notifyConsumer();
449
+ }
450
+ /** Called by system call handler when the stream ends ($sys.End). */
451
+ onEnd(index, error) {
452
+ if (this._disposed || this._completed) return;
453
+ this._completed = true;
454
+ this._completionError = error;
455
+ this._notifyConsumer();
456
+ }
457
+ // -- IRpcObject --
458
+ reconnect() {
459
+ if (this.allowReconnect) {
460
+ this._sendAck(this._nextExpectedIndex, true);
461
+ } else {
462
+ this.disconnect();
463
+ }
464
+ }
465
+ disconnect() {
466
+ if (this._completed) return;
467
+ this._completed = true;
468
+ this._completionError = new Error("Peer disconnected.");
469
+ this._notifyConsumer();
470
+ }
471
+ // -- AsyncIterable --
472
+ [Symbol.asyncIterator]() {
473
+ if (this._iterating) throw new Error("RpcStream can only be iterated once.");
474
+ this._iterating = true;
475
+ let bufferIndex = 0;
476
+ let consumedIndex = 0;
477
+ const self = this;
478
+ return {
479
+ async next() {
480
+ if (!self._started) {
481
+ self._started = true;
482
+ self._sendAck(0, true);
374
483
  }
375
- call.result.resolve(args[0]);
484
+ while (true) {
485
+ if (bufferIndex < self._buffer.length) {
486
+ const value = self._buffer[bufferIndex];
487
+ bufferIndex++;
488
+ consumedIndex++;
489
+ return { value, done: false };
490
+ }
491
+ if (self._completed) {
492
+ if (self._completionError) throw self._completionError;
493
+ return { value: void 0, done: true };
494
+ }
495
+ self._consumerWaiting = new import_core4.PromiseSource();
496
+ await self._consumerWaiting.promise;
497
+ }
498
+ },
499
+ async return() {
500
+ self._sendAckEnd();
501
+ self.dispose();
502
+ return { value: void 0, done: true };
376
503
  }
377
- break;
504
+ };
505
+ }
506
+ dispose() {
507
+ if (this._disposed) return;
508
+ this._disposed = true;
509
+ this._completed = true;
510
+ this.peer.remoteObjects.unregister(this);
511
+ this._notifyConsumer();
512
+ }
513
+ // -- Private --
514
+ _notifyConsumer() {
515
+ if (this._consumerWaiting !== null) {
516
+ this._consumerWaiting.resolve();
517
+ this._consumerWaiting = null;
378
518
  }
379
- case RpcSystemCalls.error: {
380
- const call = outboundTracker.remove(relatedId);
381
- if (call !== void 0) {
382
- const errorInfo = args[0];
383
- const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
384
- call.result.reject(new Error(msg));
385
- }
386
- break;
519
+ }
520
+ _maybeSendAck(nextIndex) {
521
+ if (this.ackPeriod <= 0) return;
522
+ const threshold = this._ackSentUpTo + this.ackPeriod;
523
+ if (nextIndex >= threshold) {
524
+ this._sendAck(nextIndex, false);
525
+ }
526
+ }
527
+ _sendAck(nextIndex, _mustReset) {
528
+ this._ackSentUpTo = nextIndex;
529
+ const conn = this.peer.connection;
530
+ if (conn) {
531
+ this.peer.hub.systemCallSender.ack(conn, this.id.localId, nextIndex, this.id.hostId);
387
532
  }
388
- case RpcSystemCalls.cancel: {
389
- inboundTracker.remove(relatedId);
390
- break;
533
+ }
534
+ _sendAckEnd() {
535
+ const conn = this.peer.connection;
536
+ if (conn) {
537
+ this.peer.hub.systemCallSender.ackEnd(conn, this.id.localId, this.id.hostId);
391
538
  }
392
- case RpcSystemCalls.keepAlive: {
393
- break;
539
+ }
540
+ };
541
+ function resolveStreamRefs(value, peer) {
542
+ if (value === null || value === void 0) return value;
543
+ if (typeof value === "string") {
544
+ const ref = parseStreamRef(value);
545
+ if (ref !== null) {
546
+ const stream = new RpcStream(ref, peer);
547
+ peer.remoteObjects.register(stream);
548
+ return stream;
394
549
  }
550
+ return value;
395
551
  }
552
+ if (Array.isArray(value)) {
553
+ for (let i = 0; i < value.length; i++) {
554
+ value[i] = resolveStreamRefs(value[i], peer);
555
+ }
556
+ return value;
557
+ }
558
+ if (typeof value === "object") {
559
+ for (const key of Object.keys(value)) {
560
+ value[key] = resolveStreamRefs(
561
+ value[key],
562
+ peer
563
+ );
564
+ }
565
+ return value;
566
+ }
567
+ return value;
396
568
  }
397
569
 
570
+ // src/rpc-system-call-handler.ts
571
+ var RpcSystemCallHandler = class {
572
+ handle(message, args, peer) {
573
+ const method = message.Method;
574
+ const relatedId = message.RelatedId ?? 0;
575
+ switch (method) {
576
+ case RpcSystemCalls.ok: {
577
+ const call = peer.outbound.get(relatedId);
578
+ if (call !== void 0) {
579
+ if (call.removeOnOk) {
580
+ peer.outbound.remove(relatedId);
581
+ }
582
+ call.result.resolve(args[0]);
583
+ }
584
+ break;
585
+ }
586
+ case RpcSystemCalls.error: {
587
+ const call = peer.outbound.remove(relatedId);
588
+ if (call !== void 0) {
589
+ const errorInfo = args[0];
590
+ const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
591
+ call.result.reject(new Error(msg));
592
+ }
593
+ break;
594
+ }
595
+ case RpcSystemCalls.cancel: {
596
+ peer.inbound.remove(relatedId);
597
+ break;
598
+ }
599
+ case RpcSystemCalls.keepAlive: {
600
+ break;
601
+ }
602
+ case RpcSystemCalls.item: {
603
+ const stream = peer.remoteObjects.get(relatedId);
604
+ if (stream) stream.onItem(args[0], resolveStreamRefs(args[1], peer));
605
+ break;
606
+ }
607
+ case RpcSystemCalls.batch: {
608
+ const stream = peer.remoteObjects.get(relatedId);
609
+ if (stream) {
610
+ const items = args[1];
611
+ for (let i = 0; i < items.length; i++) items[i] = resolveStreamRefs(items[i], peer);
612
+ stream.onBatch(args[0], items);
613
+ }
614
+ break;
615
+ }
616
+ case RpcSystemCalls.end: {
617
+ const stream = peer.remoteObjects.get(relatedId);
618
+ if (stream) {
619
+ const errorInfo = args[1];
620
+ const msg = errorInfo?.Message ?? errorInfo?.message;
621
+ const error = msg ? new Error(msg) : null;
622
+ stream.onEnd(args[0], error);
623
+ }
624
+ break;
625
+ }
626
+ case RpcSystemCalls.ack: {
627
+ const sender = peer.sharedObjects.get(relatedId);
628
+ sender?.onAck(args[0], args[1]);
629
+ break;
630
+ }
631
+ case RpcSystemCalls.ackEnd: {
632
+ const sender = peer.sharedObjects.get(relatedId);
633
+ sender?.onAckEnd(args[0]);
634
+ break;
635
+ }
636
+ }
637
+ }
638
+ };
639
+
398
640
  // src/rpc-system-call-sender.ts
399
641
  var RpcSystemCallSender = class {
400
642
  handshake(conn, peerId, hubId, index) {
@@ -427,17 +669,198 @@ var RpcSystemCallSender = class {
427
669
  const msg = serializeMessage({ Method: RpcSystemCalls.keepAlive }, [activeCallIds]);
428
670
  conn.send(msg);
429
671
  }
672
+ item(conn, localId, index, item) {
673
+ const msg = serializeMessage(
674
+ { Method: RpcSystemCalls.item, RelatedId: localId },
675
+ [index, item]
676
+ );
677
+ conn.send(msg);
678
+ }
679
+ batch(conn, localId, index, items) {
680
+ const msg = serializeMessage(
681
+ { Method: RpcSystemCalls.batch, RelatedId: localId },
682
+ [index, items]
683
+ );
684
+ conn.send(msg);
685
+ }
686
+ end(conn, localId, index, error = null) {
687
+ const msg = serializeMessage(
688
+ { Method: RpcSystemCalls.end, RelatedId: localId },
689
+ [index, error]
690
+ );
691
+ conn.send(msg);
692
+ }
693
+ ack(conn, localId, nextIndex, hostId) {
694
+ const msg = serializeMessage(
695
+ { Method: RpcSystemCalls.ack, RelatedId: localId },
696
+ [nextIndex, hostId]
697
+ );
698
+ conn.send(msg);
699
+ }
700
+ ackEnd(conn, localId, hostId) {
701
+ const msg = serializeMessage(
702
+ { Method: RpcSystemCalls.ackEnd, RelatedId: localId },
703
+ [hostId]
704
+ );
705
+ conn.send(msg);
706
+ }
430
707
  };
431
708
 
432
- // src/rpc-peer.ts
709
+ // src/rpc-stream-sender.ts
433
710
  var import_core5 = require("@actuallab/core");
711
+ var DEFAULT_ACK_PERIOD = 256;
712
+ var DEFAULT_ACK_ADVANCE = 128;
713
+ var RpcStreamSender = class {
714
+ id;
715
+ kind = RpcObjectKind.Local;
716
+ allowReconnect;
717
+ peer;
718
+ ackPeriod;
719
+ ackAdvance;
720
+ _nextIndex = 0;
721
+ _ended = false;
722
+ _started = new import_core5.PromiseSource();
723
+ constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true) {
724
+ const localId = peer.sharedObjects.nextId();
725
+ this.id = { hostId: peer.hub.hubId, localId };
726
+ this.allowReconnect = allowReconnect;
727
+ this.peer = peer;
728
+ this.ackPeriod = ackPeriod;
729
+ this.ackAdvance = ackAdvance;
730
+ }
731
+ /** Returns the stream reference string for the $sys.Ok response. */
732
+ toRef() {
733
+ return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"}`;
734
+ }
735
+ /** Called by system call handler when $sys.Ack is received from the client. */
736
+ onAck(_nextIndex, _hostId) {
737
+ if (!this._started.isCompleted) {
738
+ this._started.resolve();
739
+ }
740
+ }
741
+ /** Called by system call handler when $sys.AckEnd is received from the client. */
742
+ onAckEnd(_hostId) {
743
+ this._ended = true;
744
+ this.peer.sharedObjects.unregister(this);
745
+ }
746
+ /** Send a single item to the client. */
747
+ sendItem(item) {
748
+ if (this._ended) return;
749
+ const conn = this.peer.connection;
750
+ if (!conn) return;
751
+ this.peer.hub.systemCallSender.item(conn, this.id.localId, this._nextIndex, item);
752
+ this._nextIndex++;
753
+ }
754
+ /** Send a batch of items to the client. */
755
+ sendBatch(items) {
756
+ if (this._ended || items.length === 0) return;
757
+ const conn = this.peer.connection;
758
+ if (!conn) return;
759
+ this.peer.hub.systemCallSender.batch(conn, this.id.localId, this._nextIndex, items);
760
+ this._nextIndex += items.length;
761
+ }
762
+ /** Signal stream completion to the client. */
763
+ sendEnd(error) {
764
+ if (this._ended) return;
765
+ this._ended = true;
766
+ const conn = this.peer.connection;
767
+ if (!conn) return;
768
+ const errorInfo = error ? { Message: error.message } : null;
769
+ this.peer.hub.systemCallSender.end(conn, this.id.localId, this._nextIndex, errorInfo);
770
+ }
771
+ /**
772
+ * Consume an AsyncIterable and send all items to the client.
773
+ * Waits for the client's initial Ack before starting to pump items.
774
+ */
775
+ async writeFrom(source) {
776
+ await this._started.promise;
777
+ try {
778
+ for await (const item of source) {
779
+ if (this._ended) return;
780
+ this.sendItem(item);
781
+ }
782
+ if (!this._ended) {
783
+ this.sendEnd();
784
+ }
785
+ } catch (e) {
786
+ if (!this._ended) {
787
+ this.sendEnd(e instanceof Error ? e : new Error(String(e)));
788
+ }
789
+ }
790
+ }
791
+ // -- IRpcObject --
792
+ reconnect() {
793
+ }
794
+ disconnect() {
795
+ this._ended = true;
796
+ if (!this._started.isCompleted) {
797
+ this._started.resolve();
798
+ }
799
+ this.peer.sharedObjects.unregister(this);
800
+ }
801
+ };
802
+
803
+ // src/rpc-remote-object-tracker.ts
804
+ var RpcRemoteObjectTracker = class {
805
+ _objects = /* @__PURE__ */ new Map();
806
+ register(obj) {
807
+ this._objects.set(obj.id.localId, obj);
808
+ }
809
+ get(localId) {
810
+ return this._objects.get(localId);
811
+ }
812
+ unregister(obj) {
813
+ this._objects.delete(obj.id.localId);
814
+ }
815
+ disconnectAll() {
816
+ for (const obj of this._objects.values()) {
817
+ obj.disconnect();
818
+ }
819
+ this._objects.clear();
820
+ }
821
+ reconnectAll() {
822
+ for (const obj of this._objects.values()) {
823
+ if (obj.allowReconnect)
824
+ obj.reconnect();
825
+ else
826
+ obj.disconnect();
827
+ }
828
+ }
829
+ };
830
+
831
+ // src/rpc-shared-object-tracker.ts
832
+ var RpcSharedObjectTracker = class {
833
+ _nextId = 1;
834
+ _objects = /* @__PURE__ */ new Map();
835
+ nextId() {
836
+ return this._nextId++;
837
+ }
838
+ register(obj) {
839
+ this._objects.set(obj.id.localId, obj);
840
+ }
841
+ get(localId) {
842
+ return this._objects.get(localId);
843
+ }
844
+ unregister(obj) {
845
+ this._objects.delete(obj.id.localId);
846
+ }
847
+ disconnectAll() {
848
+ for (const obj of this._objects.values()) {
849
+ obj.disconnect();
850
+ }
851
+ this._objects.clear();
852
+ }
853
+ };
854
+
855
+ // src/rpc-peer.ts
856
+ var import_core7 = require("@actuallab/core");
434
857
 
435
858
  // src/rpc-client-peer-reconnect-delayer.ts
436
- var import_core4 = require("@actuallab/core");
437
- var RpcClientPeerReconnectDelayer = class extends import_core4.RetryDelayer {
859
+ var import_core6 = require("@actuallab/core");
860
+ var RpcClientPeerReconnectDelayer = class extends import_core6.RetryDelayer {
438
861
  constructor() {
439
862
  super();
440
- this.delays = import_core4.RetryDelaySeq.exp(1e3, 6e4);
863
+ this.delays = import_core6.RetryDelaySeq.exp(1e3, 6e4);
441
864
  }
442
865
  };
443
866
 
@@ -456,14 +879,19 @@ var RpcPeer = class {
456
879
  _connection;
457
880
  outbound = new RpcOutboundCallTracker();
458
881
  inbound = new RpcInboundCallTracker();
459
- connected = new import_core5.EventHandlerSet();
460
- disconnected = new import_core5.EventHandlerSet();
882
+ remoteObjects = new RpcRemoteObjectTracker();
883
+ sharedObjects = new RpcSharedObjectTracker();
884
+ connected = new import_core7.EventHandlerSet();
885
+ disconnected = new import_core7.EventHandlerSet();
461
886
  _pendingSends = [];
462
887
  _keepAliveTimer;
463
888
  constructor(ref, hub) {
464
889
  this.ref = ref;
465
890
  this._hub = hub;
466
891
  }
892
+ get hub() {
893
+ return this._hub;
894
+ }
467
895
  get connection() {
468
896
  return this._connection;
469
897
  }
@@ -529,6 +957,8 @@ var RpcPeer = class {
529
957
  }
530
958
  close() {
531
959
  this._stopKeepAlive();
960
+ this.remoteObjects.disconnectAll();
961
+ this.sharedObjects.disconnectAll();
532
962
  for (const call of this._pendingSends) {
533
963
  if (!call.result.isCompleted)
534
964
  call.result.reject(new Error("Peer closed."));
@@ -562,7 +992,7 @@ var RpcPeer = class {
562
992
  return;
563
993
  }
564
994
  if (method.startsWith("$sys")) {
565
- this._hub.handleSystemCall(message, args, this.outbound, this.inbound);
995
+ this._hub.systemCallHandler.handle(message, args, this);
566
996
  return;
567
997
  }
568
998
  this._handleInbound(message, args);
@@ -582,7 +1012,12 @@ var RpcPeer = class {
582
1012
  try {
583
1013
  const context = this._connection !== void 0 ? { __rpcDispatch: true, callId: relatedId, connection: this._connection } : void 0;
584
1014
  const result = await serviceHost.dispatch(method, args, context);
585
- if (!isNoWait && this._connection !== void 0) {
1015
+ if (methodDef?.stream === true && this._connection !== void 0) {
1016
+ const sender = new RpcStreamSender(this);
1017
+ this.sharedObjects.register(sender);
1018
+ this._hub.systemCallSender.ok(this._connection, relatedId, sender.toRef());
1019
+ void sender.writeFrom(result);
1020
+ } else if (!isNoWait && this._connection !== void 0) {
586
1021
  this._hub.systemCallSender.ok(this._connection, relatedId, result);
587
1022
  }
588
1023
  } catch (e) {
@@ -635,9 +1070,9 @@ var RpcClientPeer = class extends RpcPeer {
635
1070
  serializationFormat;
636
1071
  /** Builds the WebSocket URL for this peer. Replace to customize URL construction. */
637
1072
  connectionUrlResolver = defaultConnectionUrlResolver;
638
- peerChanged = new import_core5.EventHandlerSet();
1073
+ peerChanged = new import_core7.EventHandlerSet();
639
1074
  reconnectDelayer = new RpcClientPeerReconnectDelayer();
640
- reconnectsAtChanged = new import_core5.EventHandlerSet();
1075
+ reconnectsAtChanged = new import_core7.EventHandlerSet();
641
1076
  get reconnectsAt() {
642
1077
  return this._reconnectsAt;
643
1078
  }
@@ -667,7 +1102,7 @@ var RpcClientPeer = class extends RpcPeer {
667
1102
  const ws = wsFactory?.(connUrl) ?? new WebSocket(connUrl);
668
1103
  const conn = new RpcWebSocketConnection(ws);
669
1104
  this._connectionKind = 1 /* Connecting */;
670
- this._pendingHandshake = new import_core5.PromiseSource();
1105
+ this._pendingHandshake = new import_core7.PromiseSource();
671
1106
  this.setupConnection(conn);
672
1107
  this._connection = void 0;
673
1108
  const closedRejection = new Promise((_, reject) => conn.closed.add(() => reject(new Error("Connection failed"))));
@@ -723,6 +1158,11 @@ var RpcClientPeer = class extends RpcPeer {
723
1158
  _reconnect(_isPeerChanged) {
724
1159
  if (this._connection === void 0) return;
725
1160
  const conn = this._connection;
1161
+ if (_isPeerChanged) {
1162
+ this.remoteObjects.disconnectAll();
1163
+ } else {
1164
+ this.remoteObjects.reconnectAll();
1165
+ }
726
1166
  const trackerCalls = [...this.outbound.values()];
727
1167
  for (const call of trackerCalls) {
728
1168
  if (!call.removeOnOk && call.result.isCompleted) {
@@ -805,12 +1245,10 @@ function getMethodsMeta(cls) {
805
1245
  function getServiceMeta(cls) {
806
1246
  return cls[Symbol.metadata]?.[SERVICE_META];
807
1247
  }
808
- function rpcService(serviceName, options) {
1248
+ function rpcService(serviceName) {
809
1249
  return function(_target, context) {
810
1250
  const meta = context.metadata[SERVICE_META] ??= {};
811
1251
  meta.name = serviceName;
812
- if (options?.ctOffset !== void 0)
813
- meta.ctOffset = options.ctOffset;
814
1252
  };
815
1253
  }
816
1254
  function rpcMethod(options) {
@@ -820,8 +1258,7 @@ function rpcMethod(options) {
820
1258
  methods[methodName] = {
821
1259
  ...methods[methodName],
822
1260
  argCount: target.length,
823
- stream: options?.stream ?? false,
824
- noWait: options?.noWait ?? false
1261
+ returns: options?.returns
825
1262
  };
826
1263
  return target;
827
1264
  };
@@ -833,6 +1270,7 @@ var RpcHub = class {
833
1270
  peers = /* @__PURE__ */ new Map();
834
1271
  serviceHost;
835
1272
  systemCallSender = new RpcSystemCallSender();
1273
+ systemCallHandler = new RpcSystemCallHandler();
836
1274
  constructor(hubId) {
837
1275
  this.hubId = hubId ?? crypto.randomUUID();
838
1276
  this.serviceHost = new RpcServiceHost();
@@ -906,19 +1344,14 @@ var RpcHub = class {
906
1344
  for (const peer of this.peers.values()) peer.close();
907
1345
  this.peers.clear();
908
1346
  }
909
- /** Dispatch an inbound system call. Override in FusionHub to handle compute-specific system calls. */
910
- handleSystemCall(message, args, outbound, inbound) {
911
- handleSystemCall(message, args, outbound, inbound);
912
- }
913
1347
  /** Build an RpcServiceDef from decorator metadata on a contract class. Override in FusionHub for compute support. */
914
1348
  _buildServiceDef(cls) {
915
1349
  const svcMeta = getServiceMeta(cls);
916
1350
  if (svcMeta === void 0) throw new Error("Contract class missing @rpcService metadata");
917
1351
  const methodsMeta = getMethodsMeta(cls) ?? {};
918
- const ctOffset = svcMeta.ctOffset ?? 0;
919
1352
  const methods = /* @__PURE__ */ new Map();
920
1353
  for (const [name, meta] of Object.entries(methodsMeta)) {
921
- const wireArgCount = meta.argCount + ctOffset;
1354
+ const wireArgCount = meta.argCount + 1;
922
1355
  const mapKey = `${name}:${wireArgCount}`;
923
1356
  methods.set(mapKey, {
924
1357
  name,
@@ -926,8 +1359,8 @@ var RpcHub = class {
926
1359
  argCount: meta.argCount,
927
1360
  wireArgCount,
928
1361
  callTypeId: 0,
929
- stream: meta.stream ?? false,
930
- noWait: meta.noWait ?? false
1362
+ stream: meta.returns === RpcType.stream,
1363
+ noWait: meta.returns === RpcType.noWait
931
1364
  });
932
1365
  }
933
1366
  return { name: svcMeta.name, methods };
@@ -951,11 +1384,24 @@ var RpcHub = class {
951
1384
  peer.callNoWait(wireName, callArgs);
952
1385
  };
953
1386
  }
1387
+ if (methodDef.stream) {
1388
+ return async (...args) => {
1389
+ const callArgs = args.slice(0, methodDef.argCount);
1390
+ const outboundCall = peer.call(wireName, callArgs);
1391
+ const result = await outboundCall.result.promise;
1392
+ const ref = parseStreamRef(result);
1393
+ if (!ref) throw new Error(`Expected stream reference, got: ${JSON.stringify(result)}`);
1394
+ const stream = new RpcStream(ref, peer);
1395
+ peer.remoteObjects.register(stream);
1396
+ return stream;
1397
+ };
1398
+ }
954
1399
  const callOptions = methodDef.callTypeId !== 0 ? { callTypeId: methodDef.callTypeId } : void 0;
955
1400
  return async (...args) => {
956
1401
  const callArgs = args.slice(0, methodDef.argCount);
957
1402
  const outboundCall = peer.call(wireName, callArgs, callOptions);
958
- return outboundCall.result.promise;
1403
+ const result = await outboundCall.result.promise;
1404
+ return resolveStreamRefs(result, peer);
959
1405
  };
960
1406
  }
961
1407
  };
@@ -1041,13 +1487,13 @@ function getStateDescription(state, useLastError = false) {
1041
1487
  }
1042
1488
 
1043
1489
  // src/rpc-peer-state-monitor.ts
1044
- var import_core6 = require("@actuallab/core");
1490
+ var import_core8 = require("@actuallab/core");
1045
1491
  var JustConnectedPeriodMs = 1500;
1046
1492
  var JustDisconnectedPeriodMs = 3e3;
1047
1493
  var MinReconnectsInMs = 1e3;
1048
1494
  var RpcPeerStateMonitor = class {
1049
1495
  peer;
1050
- stateChanged = new import_core6.EventHandlerSet();
1496
+ stateChanged = new import_core8.EventHandlerSet();
1051
1497
  _rawConnected;
1052
1498
  _connectedAt = 0;
1053
1499
  _disconnectedAt = 0;
@@ -1166,14 +1612,20 @@ var RpcPeerStateMonitor = class {
1166
1612
  RpcInboundCall,
1167
1613
  RpcInboundCallTracker,
1168
1614
  RpcMessageChannelConnection,
1615
+ RpcObjectKind,
1169
1616
  RpcOutboundCall,
1170
1617
  RpcOutboundCallTracker,
1171
1618
  RpcPeer,
1172
1619
  RpcPeerConnectionKind,
1173
1620
  RpcPeerStateKind,
1174
1621
  RpcPeerStateMonitor,
1622
+ RpcRemoteObjectTracker,
1175
1623
  RpcServerPeer,
1176
1624
  RpcServiceHost,
1625
+ RpcSharedObjectTracker,
1626
+ RpcStream,
1627
+ RpcStreamSender,
1628
+ RpcSystemCallHandler,
1177
1629
  RpcSystemCallSender,
1178
1630
  RpcSystemCalls,
1179
1631
  RpcType,
@@ -1187,9 +1639,10 @@ var RpcPeerStateMonitor = class {
1187
1639
  getMethodsMeta,
1188
1640
  getServiceMeta,
1189
1641
  getStateDescription,
1190
- handleSystemCall,
1191
1642
  isConnected,
1192
1643
  likelyConnected,
1644
+ parseStreamRef,
1645
+ resolveStreamRefs,
1193
1646
  rpcMethod,
1194
1647
  rpcService,
1195
1648
  serializeFrame,