@actuallab/rpc 12.2.23 → 12.3.2

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.js CHANGED
@@ -645,12 +645,200 @@ var RpcInboundCallTracker = class {
645
645
 
646
646
  // src/rpc-stream.ts
647
647
  import Denque from "denque";
648
- import { PromiseSource as PromiseSource3 } from "@actuallab/core";
648
+ import { PromiseSource as PromiseSource4 } from "@actuallab/core";
649
649
 
650
650
  // src/rpc-object.ts
651
651
  var RpcObjectKind = { Local: 0, Remote: 1 };
652
652
 
653
+ // src/rpc-stream-sender.ts
654
+ import { PromiseSource as PromiseSource3 } from "@actuallab/core";
655
+ var DEFAULT_ACK_PERIOD = 256;
656
+ var DEFAULT_ACK_ADVANCE = 128;
657
+ var RpcStreamSender = class _RpcStreamSender {
658
+ static _emptyGuid = "00000000-0000-0000-0000-000000000000";
659
+ id;
660
+ kind = RpcObjectKind.Local;
661
+ allowReconnect;
662
+ isRealTime;
663
+ canSkipTo;
664
+ peer;
665
+ ackPeriod;
666
+ ackAdvance;
667
+ _nextIndex = 0;
668
+ _ended = false;
669
+ _started = new PromiseSource3();
670
+ _lastAckedIndex = 0;
671
+ _ackWaiting = null;
672
+ _resetRequested = false;
673
+ constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true) {
674
+ const localId = peer.sharedObjects.nextId();
675
+ this.id = { hostId: peer.hub.hubId, localId };
676
+ this.allowReconnect = allowReconnect;
677
+ this.isRealTime = isRealTime;
678
+ this.canSkipTo = canSkipTo;
679
+ this.peer = peer;
680
+ this.ackPeriod = ackPeriod;
681
+ this.ackAdvance = ackAdvance;
682
+ }
683
+ /** Returns the stream reference string for the $sys.Ok response. */
684
+ toRef() {
685
+ return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
686
+ }
687
+ /** Called by system call handler when $sys.Ack is received from the client. */
688
+ onAck(nextIndex, hostId) {
689
+ const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
690
+ if (!this._started.isCompleted) {
691
+ this._started.resolve();
692
+ }
693
+ if (mustReset && this.isRealTime) {
694
+ this._nextIndex = nextIndex;
695
+ this._lastAckedIndex = nextIndex;
696
+ this._resetRequested = true;
697
+ } else {
698
+ if (mustReset || nextIndex > this._lastAckedIndex) {
699
+ this._lastAckedIndex = nextIndex;
700
+ }
701
+ }
702
+ if (this._ackWaiting) {
703
+ this._ackWaiting.resolve();
704
+ this._ackWaiting = null;
705
+ }
706
+ }
707
+ /** Called by system call handler when $sys.AckEnd is received from the client. */
708
+ onAckEnd(_hostId) {
709
+ this._ended = true;
710
+ this.peer.sharedObjects.unregister(this);
711
+ }
712
+ /** Send a single item to the client. */
713
+ sendItem(item) {
714
+ if (this._ended) return;
715
+ const conn = this.peer.connection;
716
+ if (!conn) return;
717
+ this.peer.hub.systemCallSender.item(
718
+ conn,
719
+ this.peer.format,
720
+ this.id.localId,
721
+ this._nextIndex,
722
+ item
723
+ );
724
+ this._nextIndex++;
725
+ }
726
+ /** Send a batch of items to the client. */
727
+ sendBatch(items) {
728
+ if (this._ended || items.length === 0) return;
729
+ const conn = this.peer.connection;
730
+ if (!conn) return;
731
+ this.peer.hub.systemCallSender.batch(
732
+ conn,
733
+ this.peer.format,
734
+ this.id.localId,
735
+ this._nextIndex,
736
+ items
737
+ );
738
+ this._nextIndex += items.length;
739
+ }
740
+ /** Signal stream completion to the client. */
741
+ sendEnd(error) {
742
+ if (this._ended) return;
743
+ this._ended = true;
744
+ const conn = this.peer.connection;
745
+ if (!conn) return;
746
+ const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
747
+ this.peer.hub.systemCallSender.end(
748
+ conn,
749
+ this.peer.format,
750
+ this.id.localId,
751
+ this._nextIndex,
752
+ errorInfo
753
+ );
754
+ }
755
+ /**
756
+ * Consume an AsyncIterable and send all items to the client.
757
+ * Waits for the client's initial Ack before starting to pump items.
758
+ *
759
+ * When `isRealTime` is true, applies flow control based on `ackAdvance`:
760
+ * if the sender is `ackAdvance` items ahead of the last ACK, it enters
761
+ * skip mode — draining the source and discarding items until it finds
762
+ * one where `canSkipTo` returns true.
763
+ */
764
+ async writeFrom(source) {
765
+ await this._started.promise;
766
+ const iterator = source[Symbol.asyncIterator]();
767
+ try {
768
+ while (true) {
769
+ const next = await iterator.next();
770
+ if (next.done || this._ended) break;
771
+ const item = next.value;
772
+ if (this._resetRequested) {
773
+ if (!this.canSkipTo(item)) continue;
774
+ this._resetRequested = false;
775
+ this.sendItem(item);
776
+ continue;
777
+ }
778
+ if (this._nextIndex < this._lastAckedIndex + this.ackAdvance) {
779
+ this.sendItem(item);
780
+ continue;
781
+ }
782
+ if (!this.isRealTime) {
783
+ await this._waitForAckBudget();
784
+ if (this._ended) return;
785
+ this.sendItem(item);
786
+ continue;
787
+ }
788
+ let latestSkipTarget = this.canSkipTo(item) ? item : void 0;
789
+ let sourceExhausted = false;
790
+ while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
791
+ if (this._ended) return;
792
+ const n = await iterator.next();
793
+ if (n.done) {
794
+ sourceExhausted = true;
795
+ break;
796
+ }
797
+ if (this.canSkipTo(n.value)) {
798
+ latestSkipTarget = n.value;
799
+ }
800
+ }
801
+ if (this._ended) return;
802
+ if (latestSkipTarget !== void 0) {
803
+ this.sendItem(latestSkipTarget);
804
+ }
805
+ if (sourceExhausted) {
806
+ if (!this._ended) this.sendEnd();
807
+ return;
808
+ }
809
+ }
810
+ if (!this._ended) {
811
+ this.sendEnd();
812
+ }
813
+ } catch (e) {
814
+ if (!this._ended) {
815
+ this.sendEnd(e instanceof Error ? e : new Error(String(e)));
816
+ }
817
+ }
818
+ }
819
+ /** Wait until _nextIndex < _lastAckedIndex + ackAdvance. */
820
+ async _waitForAckBudget() {
821
+ while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
822
+ if (this._ended) return;
823
+ this._ackWaiting = new PromiseSource3();
824
+ await this._ackWaiting.promise;
825
+ }
826
+ }
827
+ // -- IRpcObject --
828
+ reconnect() {
829
+ }
830
+ disconnect() {
831
+ this._ended = true;
832
+ if (!this._started.isCompleted) {
833
+ this._started.resolve();
834
+ }
835
+ this.peer.sharedObjects.unregister(this);
836
+ }
837
+ };
838
+
653
839
  // src/rpc-stream.ts
840
+ var DEFAULT_ACK_PERIOD2 = 30;
841
+ var DEFAULT_ACK_ADVANCE2 = 61;
654
842
  function parseStreamRef(value) {
655
843
  if (typeof value === "string") {
656
844
  const parts = value.split(",");
@@ -683,17 +871,21 @@ function parseStreamRef(value) {
683
871
  }
684
872
  var RpcStream = class _RpcStream {
685
873
  id;
686
- kind = RpcObjectKind.Remote;
874
+ kind;
687
875
  allowReconnect;
688
- peer;
876
+ isRealTime;
877
+ canSkipTo;
689
878
  ackPeriod;
690
879
  ackAdvance;
691
- // Double-ended queue so consumption via `shift()` is O(1) and the
692
- // backing store frees memory as items are drained. The previous
693
- // `T[]` buffer kept every item for the life of the stream because
694
- // the iterator tracked a read index instead of shifting — fine for
695
- // short runs but a slow leak in the browser where a video call can
696
- // last an hour.
880
+ // Local (origin) mode
881
+ localSource;
882
+ // Remote (target) mode
883
+ peer;
884
+ // Local-only state
885
+ _sender;
886
+ _toRefValue;
887
+ _whenSent;
888
+ // Remote-only state
697
889
  _buffer = new Denque();
698
890
  _nextExpectedIndex = 0;
699
891
  _consumerWaiting = null;
@@ -703,12 +895,76 @@ var RpcStream = class _RpcStream {
703
895
  _started = false;
704
896
  _iterating = false;
705
897
  _ackSentUpTo = 0;
706
- constructor(ref, peer) {
707
- this.id = { hostId: ref.hostId, localId: ref.localId };
708
- this.allowReconnect = ref.allowReconnect;
709
- this.peer = peer;
710
- this.ackPeriod = ref.ackPeriod;
711
- this.ackAdvance = ref.ackAdvance;
898
+ constructor(sourceOrRef, optionsOrPeer) {
899
+ if (_isAsyncIterable(sourceOrRef)) {
900
+ const options = optionsOrPeer ?? {};
901
+ this.kind = RpcObjectKind.Local;
902
+ this.localSource = sourceOrRef;
903
+ this.id = { hostId: "", localId: 0 };
904
+ this.allowReconnect = options.allowReconnect ?? true;
905
+ this.isRealTime = options.isRealTime ?? false;
906
+ this.canSkipTo = options.canSkipTo ?? (() => true);
907
+ this.ackPeriod = options.ackPeriod ?? DEFAULT_ACK_PERIOD2;
908
+ this.ackAdvance = options.ackAdvance ?? DEFAULT_ACK_ADVANCE2;
909
+ } else {
910
+ const ref = sourceOrRef;
911
+ const peer = optionsOrPeer;
912
+ this.kind = RpcObjectKind.Remote;
913
+ this.id = { hostId: ref.hostId, localId: ref.localId };
914
+ this.peer = peer;
915
+ this.allowReconnect = ref.allowReconnect;
916
+ this.isRealTime = ref.isRealTime;
917
+ this.canSkipTo = () => true;
918
+ this.ackPeriod = ref.ackPeriod;
919
+ this.ackAdvance = ref.ackAdvance;
920
+ }
921
+ }
922
+ /**
923
+ * For local streams: resolves when the sender finishes pumping (after `toRef()` was called).
924
+ * Rejects if the source throws. Throws if `toRef()` hasn't been called yet.
925
+ */
926
+ get whenSent() {
927
+ if (!this._whenSent)
928
+ throw new Error("toRef() must be called before awaiting whenSent.");
929
+ return this._whenSent;
930
+ }
931
+ /**
932
+ * For local streams: bind to a peer, create and register an `RpcStreamSender`,
933
+ * start pumping items from the local source in the background, and return the
934
+ * stream reference.
935
+ *
936
+ * For binary peers the ref is a MessagePack-compatible object
937
+ * (`{ SerializedId, AckPeriod, AckAdvance, AllowReconnect }`);
938
+ * for text peers it is a comma-separated string.
939
+ *
940
+ * Can only be called once per stream.
941
+ */
942
+ toRef(peer) {
943
+ if (this.kind !== RpcObjectKind.Local)
944
+ throw new Error("toRef() is only valid for local streams.");
945
+ if (this._sender) {
946
+ if (this._sender.peer !== peer)
947
+ throw new Error("toRef() was already called with a different peer.");
948
+ return this._toRefValue;
949
+ }
950
+ const sender = new RpcStreamSender(
951
+ peer,
952
+ this.ackPeriod,
953
+ this.ackAdvance,
954
+ this.allowReconnect,
955
+ this.isRealTime,
956
+ this.canSkipTo
957
+ );
958
+ peer.sharedObjects.register(sender);
959
+ this._sender = sender;
960
+ this._whenSent = sender.writeFrom(this.localSource);
961
+ this._toRefValue = peer.format.isBinary ? {
962
+ SerializedId: [sender.id.hostId, sender.id.localId],
963
+ AckPeriod: sender.ackPeriod,
964
+ AckAdvance: sender.ackAdvance,
965
+ AllowReconnect: sender.allowReconnect
966
+ } : sender.toRef();
967
+ return this._toRefValue;
712
968
  }
713
969
  /** Called by system call handler when a single item arrives ($sys.I). */
714
970
  onItem(index, item) {
@@ -773,6 +1029,7 @@ var RpcStream = class _RpcStream {
773
1029
  }
774
1030
  // -- IRpcObject --
775
1031
  reconnect() {
1032
+ if (this.kind === RpcObjectKind.Local) return;
776
1033
  if (this.allowReconnect) {
777
1034
  this._sendAck(this._nextExpectedIndex, true);
778
1035
  } else {
@@ -780,6 +1037,7 @@ var RpcStream = class _RpcStream {
780
1037
  }
781
1038
  }
782
1039
  disconnect() {
1040
+ if (this.kind === RpcObjectKind.Local) return;
783
1041
  if (this._completed) return;
784
1042
  this._completed = true;
785
1043
  this._completionError = new Error("Peer disconnected.");
@@ -787,6 +1045,9 @@ var RpcStream = class _RpcStream {
787
1045
  }
788
1046
  // -- AsyncIterable --
789
1047
  [Symbol.asyncIterator]() {
1048
+ if (this.kind === RpcObjectKind.Local) {
1049
+ return this.localSource[Symbol.asyncIterator]();
1050
+ }
790
1051
  if (this._iterating)
791
1052
  throw new Error("RpcStream can only be iterated once.");
792
1053
  this._iterating = true;
@@ -806,7 +1067,7 @@ var RpcStream = class _RpcStream {
806
1067
  if (self._completionError) throw self._completionError;
807
1068
  return { value: void 0, done: true };
808
1069
  }
809
- self._consumerWaiting = new PromiseSource3();
1070
+ self._consumerWaiting = new PromiseSource4();
810
1071
  await self._consumerWaiting.promise;
811
1072
  }
812
1073
  },
@@ -821,7 +1082,9 @@ var RpcStream = class _RpcStream {
821
1082
  if (this._disposed) return;
822
1083
  this._disposed = true;
823
1084
  this._completed = true;
824
- this.peer.remoteObjects.unregister(this);
1085
+ if (this.kind === RpcObjectKind.Remote) {
1086
+ this.peer.remoteObjects.unregister(this);
1087
+ }
825
1088
  this._notifyConsumer();
826
1089
  }
827
1090
  // -- Private --
@@ -865,6 +1128,9 @@ var RpcStream = class _RpcStream {
865
1128
  }
866
1129
  }
867
1130
  };
1131
+ function _isAsyncIterable(value) {
1132
+ return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
1133
+ }
868
1134
  function resolveStreamRefs(value, peer) {
869
1135
  if (value === null || value === void 0) return value;
870
1136
  if (typeof value === "string") {
@@ -1119,177 +1385,6 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
1119
1385
  }
1120
1386
  };
1121
1387
 
1122
- // src/rpc-stream-sender.ts
1123
- import { PromiseSource as PromiseSource4 } from "@actuallab/core";
1124
- var DEFAULT_ACK_PERIOD = 256;
1125
- var DEFAULT_ACK_ADVANCE = 128;
1126
- var RpcStreamSender = class {
1127
- id;
1128
- kind = RpcObjectKind.Local;
1129
- allowReconnect;
1130
- isRealTime;
1131
- canSkipTo;
1132
- peer;
1133
- ackPeriod;
1134
- ackAdvance;
1135
- _nextIndex = 0;
1136
- _ended = false;
1137
- _started = new PromiseSource4();
1138
- _lastAckedIndex = 0;
1139
- _ackWaiting = null;
1140
- constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true) {
1141
- const localId = peer.sharedObjects.nextId();
1142
- this.id = { hostId: peer.hub.hubId, localId };
1143
- this.allowReconnect = allowReconnect;
1144
- this.isRealTime = isRealTime;
1145
- this.canSkipTo = canSkipTo;
1146
- this.peer = peer;
1147
- this.ackPeriod = ackPeriod;
1148
- this.ackAdvance = ackAdvance;
1149
- }
1150
- /** Returns the stream reference string for the $sys.Ok response. */
1151
- toRef() {
1152
- return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
1153
- }
1154
- /** Called by system call handler when $sys.Ack is received from the client. */
1155
- onAck(nextIndex, _hostId) {
1156
- if (!this._started.isCompleted) {
1157
- this._started.resolve();
1158
- }
1159
- if (nextIndex > this._lastAckedIndex) {
1160
- this._lastAckedIndex = nextIndex;
1161
- }
1162
- if (this._ackWaiting) {
1163
- this._ackWaiting.resolve();
1164
- this._ackWaiting = null;
1165
- }
1166
- }
1167
- /** Called by system call handler when $sys.AckEnd is received from the client. */
1168
- onAckEnd(_hostId) {
1169
- this._ended = true;
1170
- this.peer.sharedObjects.unregister(this);
1171
- }
1172
- /** Send a single item to the client. */
1173
- sendItem(item) {
1174
- if (this._ended) return;
1175
- const conn = this.peer.connection;
1176
- if (!conn) return;
1177
- this.peer.hub.systemCallSender.item(
1178
- conn,
1179
- this.peer.format,
1180
- this.id.localId,
1181
- this._nextIndex,
1182
- item
1183
- );
1184
- this._nextIndex++;
1185
- }
1186
- /** Send a batch of items to the client. */
1187
- sendBatch(items) {
1188
- if (this._ended || items.length === 0) return;
1189
- const conn = this.peer.connection;
1190
- if (!conn) return;
1191
- this.peer.hub.systemCallSender.batch(
1192
- conn,
1193
- this.peer.format,
1194
- this.id.localId,
1195
- this._nextIndex,
1196
- items
1197
- );
1198
- this._nextIndex += items.length;
1199
- }
1200
- /** Signal stream completion to the client. */
1201
- sendEnd(error) {
1202
- if (this._ended) return;
1203
- this._ended = true;
1204
- const conn = this.peer.connection;
1205
- if (!conn) return;
1206
- const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
1207
- this.peer.hub.systemCallSender.end(
1208
- conn,
1209
- this.peer.format,
1210
- this.id.localId,
1211
- this._nextIndex,
1212
- errorInfo
1213
- );
1214
- }
1215
- /**
1216
- * Consume an AsyncIterable and send all items to the client.
1217
- * Waits for the client's initial Ack before starting to pump items.
1218
- *
1219
- * When `isRealTime` is true, applies flow control based on `ackAdvance`:
1220
- * if the sender is `ackAdvance` items ahead of the last ACK, it enters
1221
- * skip mode — draining the source and discarding items until it finds
1222
- * one where `canSkipTo` returns true.
1223
- */
1224
- async writeFrom(source) {
1225
- await this._started.promise;
1226
- const iterator = source[Symbol.asyncIterator]();
1227
- try {
1228
- while (true) {
1229
- const next = await iterator.next();
1230
- if (next.done || this._ended) break;
1231
- const item = next.value;
1232
- if (this._nextIndex < this._lastAckedIndex + this.ackAdvance) {
1233
- this.sendItem(item);
1234
- continue;
1235
- }
1236
- if (!this.isRealTime) {
1237
- await this._waitForAckBudget();
1238
- if (this._ended) return;
1239
- this.sendItem(item);
1240
- continue;
1241
- }
1242
- let latestSkipTarget = this.canSkipTo(item) ? item : void 0;
1243
- let sourceExhausted = false;
1244
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
1245
- if (this._ended) return;
1246
- const n = await iterator.next();
1247
- if (n.done) {
1248
- sourceExhausted = true;
1249
- break;
1250
- }
1251
- if (this.canSkipTo(n.value)) {
1252
- latestSkipTarget = n.value;
1253
- }
1254
- }
1255
- if (this._ended) return;
1256
- if (latestSkipTarget !== void 0) {
1257
- this.sendItem(latestSkipTarget);
1258
- }
1259
- if (sourceExhausted) {
1260
- if (!this._ended) this.sendEnd();
1261
- return;
1262
- }
1263
- }
1264
- if (!this._ended) {
1265
- this.sendEnd();
1266
- }
1267
- } catch (e) {
1268
- if (!this._ended) {
1269
- this.sendEnd(e instanceof Error ? e : new Error(String(e)));
1270
- }
1271
- }
1272
- }
1273
- /** Wait until _nextIndex < _lastAckedIndex + ackAdvance. */
1274
- async _waitForAckBudget() {
1275
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
1276
- if (this._ended) return;
1277
- this._ackWaiting = new PromiseSource4();
1278
- await this._ackWaiting.promise;
1279
- }
1280
- }
1281
- // -- IRpcObject --
1282
- reconnect() {
1283
- }
1284
- disconnect() {
1285
- this._ended = true;
1286
- if (!this._started.isCompleted) {
1287
- this._started.resolve();
1288
- }
1289
- this.peer.sharedObjects.unregister(this);
1290
- }
1291
- };
1292
-
1293
1388
  // src/rpc-remote-object-tracker.ts
1294
1389
  var RpcRemoteObjectTracker = class {
1295
1390
  _objects = /* @__PURE__ */ new Map();
@@ -1710,15 +1805,13 @@ var RpcPeer = class {
1710
1805
  context
1711
1806
  );
1712
1807
  if (methodDef?.stream === true && this._connection !== void 0) {
1713
- const sender = new RpcStreamSender(this);
1714
- this.sharedObjects.register(sender);
1808
+ const stream = result instanceof RpcStream ? result : new RpcStream(result);
1715
1809
  this._hub.systemCallSender.ok(
1716
1810
  this._connection,
1717
1811
  this.format,
1718
1812
  relatedId,
1719
- sender.toRef()
1813
+ stream.toRef(this)
1720
1814
  );
1721
- void sender.writeFrom(result);
1722
1815
  } else if (!isNoWait && this._connection !== void 0) {
1723
1816
  this._hub.systemCallSender.ok(
1724
1817
  this._connection,