@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.cjs CHANGED
@@ -744,12 +744,200 @@ var RpcInboundCallTracker = class {
744
744
 
745
745
  // src/rpc-stream.ts
746
746
  var import_denque = __toESM(require("denque"), 1);
747
- var import_core4 = require("@actuallab/core");
747
+ var import_core5 = require("@actuallab/core");
748
748
 
749
749
  // src/rpc-object.ts
750
750
  var RpcObjectKind = { Local: 0, Remote: 1 };
751
751
 
752
+ // src/rpc-stream-sender.ts
753
+ var import_core4 = require("@actuallab/core");
754
+ var DEFAULT_ACK_PERIOD = 256;
755
+ var DEFAULT_ACK_ADVANCE = 128;
756
+ var RpcStreamSender = class _RpcStreamSender {
757
+ static _emptyGuid = "00000000-0000-0000-0000-000000000000";
758
+ id;
759
+ kind = RpcObjectKind.Local;
760
+ allowReconnect;
761
+ isRealTime;
762
+ canSkipTo;
763
+ peer;
764
+ ackPeriod;
765
+ ackAdvance;
766
+ _nextIndex = 0;
767
+ _ended = false;
768
+ _started = new import_core4.PromiseSource();
769
+ _lastAckedIndex = 0;
770
+ _ackWaiting = null;
771
+ _resetRequested = false;
772
+ constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true) {
773
+ const localId = peer.sharedObjects.nextId();
774
+ this.id = { hostId: peer.hub.hubId, localId };
775
+ this.allowReconnect = allowReconnect;
776
+ this.isRealTime = isRealTime;
777
+ this.canSkipTo = canSkipTo;
778
+ this.peer = peer;
779
+ this.ackPeriod = ackPeriod;
780
+ this.ackAdvance = ackAdvance;
781
+ }
782
+ /** Returns the stream reference string for the $sys.Ok response. */
783
+ toRef() {
784
+ return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
785
+ }
786
+ /** Called by system call handler when $sys.Ack is received from the client. */
787
+ onAck(nextIndex, hostId) {
788
+ const mustReset = hostId !== "" && hostId !== _RpcStreamSender._emptyGuid;
789
+ if (!this._started.isCompleted) {
790
+ this._started.resolve();
791
+ }
792
+ if (mustReset && this.isRealTime) {
793
+ this._nextIndex = nextIndex;
794
+ this._lastAckedIndex = nextIndex;
795
+ this._resetRequested = true;
796
+ } else {
797
+ if (mustReset || nextIndex > this._lastAckedIndex) {
798
+ this._lastAckedIndex = nextIndex;
799
+ }
800
+ }
801
+ if (this._ackWaiting) {
802
+ this._ackWaiting.resolve();
803
+ this._ackWaiting = null;
804
+ }
805
+ }
806
+ /** Called by system call handler when $sys.AckEnd is received from the client. */
807
+ onAckEnd(_hostId) {
808
+ this._ended = true;
809
+ this.peer.sharedObjects.unregister(this);
810
+ }
811
+ /** Send a single item to the client. */
812
+ sendItem(item) {
813
+ if (this._ended) return;
814
+ const conn = this.peer.connection;
815
+ if (!conn) return;
816
+ this.peer.hub.systemCallSender.item(
817
+ conn,
818
+ this.peer.format,
819
+ this.id.localId,
820
+ this._nextIndex,
821
+ item
822
+ );
823
+ this._nextIndex++;
824
+ }
825
+ /** Send a batch of items to the client. */
826
+ sendBatch(items) {
827
+ if (this._ended || items.length === 0) return;
828
+ const conn = this.peer.connection;
829
+ if (!conn) return;
830
+ this.peer.hub.systemCallSender.batch(
831
+ conn,
832
+ this.peer.format,
833
+ this.id.localId,
834
+ this._nextIndex,
835
+ items
836
+ );
837
+ this._nextIndex += items.length;
838
+ }
839
+ /** Signal stream completion to the client. */
840
+ sendEnd(error) {
841
+ if (this._ended) return;
842
+ this._ended = true;
843
+ const conn = this.peer.connection;
844
+ if (!conn) return;
845
+ const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
846
+ this.peer.hub.systemCallSender.end(
847
+ conn,
848
+ this.peer.format,
849
+ this.id.localId,
850
+ this._nextIndex,
851
+ errorInfo
852
+ );
853
+ }
854
+ /**
855
+ * Consume an AsyncIterable and send all items to the client.
856
+ * Waits for the client's initial Ack before starting to pump items.
857
+ *
858
+ * When `isRealTime` is true, applies flow control based on `ackAdvance`:
859
+ * if the sender is `ackAdvance` items ahead of the last ACK, it enters
860
+ * skip mode — draining the source and discarding items until it finds
861
+ * one where `canSkipTo` returns true.
862
+ */
863
+ async writeFrom(source) {
864
+ await this._started.promise;
865
+ const iterator = source[Symbol.asyncIterator]();
866
+ try {
867
+ while (true) {
868
+ const next = await iterator.next();
869
+ if (next.done || this._ended) break;
870
+ const item = next.value;
871
+ if (this._resetRequested) {
872
+ if (!this.canSkipTo(item)) continue;
873
+ this._resetRequested = false;
874
+ this.sendItem(item);
875
+ continue;
876
+ }
877
+ if (this._nextIndex < this._lastAckedIndex + this.ackAdvance) {
878
+ this.sendItem(item);
879
+ continue;
880
+ }
881
+ if (!this.isRealTime) {
882
+ await this._waitForAckBudget();
883
+ if (this._ended) return;
884
+ this.sendItem(item);
885
+ continue;
886
+ }
887
+ let latestSkipTarget = this.canSkipTo(item) ? item : void 0;
888
+ let sourceExhausted = false;
889
+ while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
890
+ if (this._ended) return;
891
+ const n = await iterator.next();
892
+ if (n.done) {
893
+ sourceExhausted = true;
894
+ break;
895
+ }
896
+ if (this.canSkipTo(n.value)) {
897
+ latestSkipTarget = n.value;
898
+ }
899
+ }
900
+ if (this._ended) return;
901
+ if (latestSkipTarget !== void 0) {
902
+ this.sendItem(latestSkipTarget);
903
+ }
904
+ if (sourceExhausted) {
905
+ if (!this._ended) this.sendEnd();
906
+ return;
907
+ }
908
+ }
909
+ if (!this._ended) {
910
+ this.sendEnd();
911
+ }
912
+ } catch (e) {
913
+ if (!this._ended) {
914
+ this.sendEnd(e instanceof Error ? e : new Error(String(e)));
915
+ }
916
+ }
917
+ }
918
+ /** Wait until _nextIndex < _lastAckedIndex + ackAdvance. */
919
+ async _waitForAckBudget() {
920
+ while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
921
+ if (this._ended) return;
922
+ this._ackWaiting = new import_core4.PromiseSource();
923
+ await this._ackWaiting.promise;
924
+ }
925
+ }
926
+ // -- IRpcObject --
927
+ reconnect() {
928
+ }
929
+ disconnect() {
930
+ this._ended = true;
931
+ if (!this._started.isCompleted) {
932
+ this._started.resolve();
933
+ }
934
+ this.peer.sharedObjects.unregister(this);
935
+ }
936
+ };
937
+
752
938
  // src/rpc-stream.ts
939
+ var DEFAULT_ACK_PERIOD2 = 30;
940
+ var DEFAULT_ACK_ADVANCE2 = 61;
753
941
  function parseStreamRef(value) {
754
942
  if (typeof value === "string") {
755
943
  const parts = value.split(",");
@@ -782,17 +970,21 @@ function parseStreamRef(value) {
782
970
  }
783
971
  var RpcStream = class _RpcStream {
784
972
  id;
785
- kind = RpcObjectKind.Remote;
973
+ kind;
786
974
  allowReconnect;
787
- peer;
975
+ isRealTime;
976
+ canSkipTo;
788
977
  ackPeriod;
789
978
  ackAdvance;
790
- // Double-ended queue so consumption via `shift()` is O(1) and the
791
- // backing store frees memory as items are drained. The previous
792
- // `T[]` buffer kept every item for the life of the stream because
793
- // the iterator tracked a read index instead of shifting — fine for
794
- // short runs but a slow leak in the browser where a video call can
795
- // last an hour.
979
+ // Local (origin) mode
980
+ localSource;
981
+ // Remote (target) mode
982
+ peer;
983
+ // Local-only state
984
+ _sender;
985
+ _toRefValue;
986
+ _whenSent;
987
+ // Remote-only state
796
988
  _buffer = new import_denque.default();
797
989
  _nextExpectedIndex = 0;
798
990
  _consumerWaiting = null;
@@ -802,12 +994,76 @@ var RpcStream = class _RpcStream {
802
994
  _started = false;
803
995
  _iterating = false;
804
996
  _ackSentUpTo = 0;
805
- constructor(ref, peer) {
806
- this.id = { hostId: ref.hostId, localId: ref.localId };
807
- this.allowReconnect = ref.allowReconnect;
808
- this.peer = peer;
809
- this.ackPeriod = ref.ackPeriod;
810
- this.ackAdvance = ref.ackAdvance;
997
+ constructor(sourceOrRef, optionsOrPeer) {
998
+ if (_isAsyncIterable(sourceOrRef)) {
999
+ const options = optionsOrPeer ?? {};
1000
+ this.kind = RpcObjectKind.Local;
1001
+ this.localSource = sourceOrRef;
1002
+ this.id = { hostId: "", localId: 0 };
1003
+ this.allowReconnect = options.allowReconnect ?? true;
1004
+ this.isRealTime = options.isRealTime ?? false;
1005
+ this.canSkipTo = options.canSkipTo ?? (() => true);
1006
+ this.ackPeriod = options.ackPeriod ?? DEFAULT_ACK_PERIOD2;
1007
+ this.ackAdvance = options.ackAdvance ?? DEFAULT_ACK_ADVANCE2;
1008
+ } else {
1009
+ const ref = sourceOrRef;
1010
+ const peer = optionsOrPeer;
1011
+ this.kind = RpcObjectKind.Remote;
1012
+ this.id = { hostId: ref.hostId, localId: ref.localId };
1013
+ this.peer = peer;
1014
+ this.allowReconnect = ref.allowReconnect;
1015
+ this.isRealTime = ref.isRealTime;
1016
+ this.canSkipTo = () => true;
1017
+ this.ackPeriod = ref.ackPeriod;
1018
+ this.ackAdvance = ref.ackAdvance;
1019
+ }
1020
+ }
1021
+ /**
1022
+ * For local streams: resolves when the sender finishes pumping (after `toRef()` was called).
1023
+ * Rejects if the source throws. Throws if `toRef()` hasn't been called yet.
1024
+ */
1025
+ get whenSent() {
1026
+ if (!this._whenSent)
1027
+ throw new Error("toRef() must be called before awaiting whenSent.");
1028
+ return this._whenSent;
1029
+ }
1030
+ /**
1031
+ * For local streams: bind to a peer, create and register an `RpcStreamSender`,
1032
+ * start pumping items from the local source in the background, and return the
1033
+ * stream reference.
1034
+ *
1035
+ * For binary peers the ref is a MessagePack-compatible object
1036
+ * (`{ SerializedId, AckPeriod, AckAdvance, AllowReconnect }`);
1037
+ * for text peers it is a comma-separated string.
1038
+ *
1039
+ * Can only be called once per stream.
1040
+ */
1041
+ toRef(peer) {
1042
+ if (this.kind !== RpcObjectKind.Local)
1043
+ throw new Error("toRef() is only valid for local streams.");
1044
+ if (this._sender) {
1045
+ if (this._sender.peer !== peer)
1046
+ throw new Error("toRef() was already called with a different peer.");
1047
+ return this._toRefValue;
1048
+ }
1049
+ const sender = new RpcStreamSender(
1050
+ peer,
1051
+ this.ackPeriod,
1052
+ this.ackAdvance,
1053
+ this.allowReconnect,
1054
+ this.isRealTime,
1055
+ this.canSkipTo
1056
+ );
1057
+ peer.sharedObjects.register(sender);
1058
+ this._sender = sender;
1059
+ this._whenSent = sender.writeFrom(this.localSource);
1060
+ this._toRefValue = peer.format.isBinary ? {
1061
+ SerializedId: [sender.id.hostId, sender.id.localId],
1062
+ AckPeriod: sender.ackPeriod,
1063
+ AckAdvance: sender.ackAdvance,
1064
+ AllowReconnect: sender.allowReconnect
1065
+ } : sender.toRef();
1066
+ return this._toRefValue;
811
1067
  }
812
1068
  /** Called by system call handler when a single item arrives ($sys.I). */
813
1069
  onItem(index, item) {
@@ -872,6 +1128,7 @@ var RpcStream = class _RpcStream {
872
1128
  }
873
1129
  // -- IRpcObject --
874
1130
  reconnect() {
1131
+ if (this.kind === RpcObjectKind.Local) return;
875
1132
  if (this.allowReconnect) {
876
1133
  this._sendAck(this._nextExpectedIndex, true);
877
1134
  } else {
@@ -879,6 +1136,7 @@ var RpcStream = class _RpcStream {
879
1136
  }
880
1137
  }
881
1138
  disconnect() {
1139
+ if (this.kind === RpcObjectKind.Local) return;
882
1140
  if (this._completed) return;
883
1141
  this._completed = true;
884
1142
  this._completionError = new Error("Peer disconnected.");
@@ -886,6 +1144,9 @@ var RpcStream = class _RpcStream {
886
1144
  }
887
1145
  // -- AsyncIterable --
888
1146
  [Symbol.asyncIterator]() {
1147
+ if (this.kind === RpcObjectKind.Local) {
1148
+ return this.localSource[Symbol.asyncIterator]();
1149
+ }
889
1150
  if (this._iterating)
890
1151
  throw new Error("RpcStream can only be iterated once.");
891
1152
  this._iterating = true;
@@ -905,7 +1166,7 @@ var RpcStream = class _RpcStream {
905
1166
  if (self._completionError) throw self._completionError;
906
1167
  return { value: void 0, done: true };
907
1168
  }
908
- self._consumerWaiting = new import_core4.PromiseSource();
1169
+ self._consumerWaiting = new import_core5.PromiseSource();
909
1170
  await self._consumerWaiting.promise;
910
1171
  }
911
1172
  },
@@ -920,7 +1181,9 @@ var RpcStream = class _RpcStream {
920
1181
  if (this._disposed) return;
921
1182
  this._disposed = true;
922
1183
  this._completed = true;
923
- this.peer.remoteObjects.unregister(this);
1184
+ if (this.kind === RpcObjectKind.Remote) {
1185
+ this.peer.remoteObjects.unregister(this);
1186
+ }
924
1187
  this._notifyConsumer();
925
1188
  }
926
1189
  // -- Private --
@@ -964,6 +1227,9 @@ var RpcStream = class _RpcStream {
964
1227
  }
965
1228
  }
966
1229
  };
1230
+ function _isAsyncIterable(value) {
1231
+ return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
1232
+ }
967
1233
  function resolveStreamRefs(value, peer) {
968
1234
  if (value === null || value === void 0) return value;
969
1235
  if (typeof value === "string") {
@@ -1218,177 +1484,6 @@ var RpcSystemCallSender = class _RpcSystemCallSender {
1218
1484
  }
1219
1485
  };
1220
1486
 
1221
- // src/rpc-stream-sender.ts
1222
- var import_core5 = require("@actuallab/core");
1223
- var DEFAULT_ACK_PERIOD = 256;
1224
- var DEFAULT_ACK_ADVANCE = 128;
1225
- var RpcStreamSender = class {
1226
- id;
1227
- kind = RpcObjectKind.Local;
1228
- allowReconnect;
1229
- isRealTime;
1230
- canSkipTo;
1231
- peer;
1232
- ackPeriod;
1233
- ackAdvance;
1234
- _nextIndex = 0;
1235
- _ended = false;
1236
- _started = new import_core5.PromiseSource();
1237
- _lastAckedIndex = 0;
1238
- _ackWaiting = null;
1239
- constructor(peer, ackPeriod = DEFAULT_ACK_PERIOD, ackAdvance = DEFAULT_ACK_ADVANCE, allowReconnect = true, isRealTime = false, canSkipTo = () => true) {
1240
- const localId = peer.sharedObjects.nextId();
1241
- this.id = { hostId: peer.hub.hubId, localId };
1242
- this.allowReconnect = allowReconnect;
1243
- this.isRealTime = isRealTime;
1244
- this.canSkipTo = canSkipTo;
1245
- this.peer = peer;
1246
- this.ackPeriod = ackPeriod;
1247
- this.ackAdvance = ackAdvance;
1248
- }
1249
- /** Returns the stream reference string for the $sys.Ok response. */
1250
- toRef() {
1251
- return `${this.id.hostId},${this.id.localId},${this.ackPeriod},${this.ackAdvance},${this.allowReconnect ? "1" : "0"},${this.isRealTime ? "1" : "0"}`;
1252
- }
1253
- /** Called by system call handler when $sys.Ack is received from the client. */
1254
- onAck(nextIndex, _hostId) {
1255
- if (!this._started.isCompleted) {
1256
- this._started.resolve();
1257
- }
1258
- if (nextIndex > this._lastAckedIndex) {
1259
- this._lastAckedIndex = nextIndex;
1260
- }
1261
- if (this._ackWaiting) {
1262
- this._ackWaiting.resolve();
1263
- this._ackWaiting = null;
1264
- }
1265
- }
1266
- /** Called by system call handler when $sys.AckEnd is received from the client. */
1267
- onAckEnd(_hostId) {
1268
- this._ended = true;
1269
- this.peer.sharedObjects.unregister(this);
1270
- }
1271
- /** Send a single item to the client. */
1272
- sendItem(item) {
1273
- if (this._ended) return;
1274
- const conn = this.peer.connection;
1275
- if (!conn) return;
1276
- this.peer.hub.systemCallSender.item(
1277
- conn,
1278
- this.peer.format,
1279
- this.id.localId,
1280
- this._nextIndex,
1281
- item
1282
- );
1283
- this._nextIndex++;
1284
- }
1285
- /** Send a batch of items to the client. */
1286
- sendBatch(items) {
1287
- if (this._ended || items.length === 0) return;
1288
- const conn = this.peer.connection;
1289
- if (!conn) return;
1290
- this.peer.hub.systemCallSender.batch(
1291
- conn,
1292
- this.peer.format,
1293
- this.id.localId,
1294
- this._nextIndex,
1295
- items
1296
- );
1297
- this._nextIndex += items.length;
1298
- }
1299
- /** Signal stream completion to the client. */
1300
- sendEnd(error) {
1301
- if (this._ended) return;
1302
- this._ended = true;
1303
- const conn = this.peer.connection;
1304
- if (!conn) return;
1305
- const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
1306
- this.peer.hub.systemCallSender.end(
1307
- conn,
1308
- this.peer.format,
1309
- this.id.localId,
1310
- this._nextIndex,
1311
- errorInfo
1312
- );
1313
- }
1314
- /**
1315
- * Consume an AsyncIterable and send all items to the client.
1316
- * Waits for the client's initial Ack before starting to pump items.
1317
- *
1318
- * When `isRealTime` is true, applies flow control based on `ackAdvance`:
1319
- * if the sender is `ackAdvance` items ahead of the last ACK, it enters
1320
- * skip mode — draining the source and discarding items until it finds
1321
- * one where `canSkipTo` returns true.
1322
- */
1323
- async writeFrom(source) {
1324
- await this._started.promise;
1325
- const iterator = source[Symbol.asyncIterator]();
1326
- try {
1327
- while (true) {
1328
- const next = await iterator.next();
1329
- if (next.done || this._ended) break;
1330
- const item = next.value;
1331
- if (this._nextIndex < this._lastAckedIndex + this.ackAdvance) {
1332
- this.sendItem(item);
1333
- continue;
1334
- }
1335
- if (!this.isRealTime) {
1336
- await this._waitForAckBudget();
1337
- if (this._ended) return;
1338
- this.sendItem(item);
1339
- continue;
1340
- }
1341
- let latestSkipTarget = this.canSkipTo(item) ? item : void 0;
1342
- let sourceExhausted = false;
1343
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
1344
- if (this._ended) return;
1345
- const n = await iterator.next();
1346
- if (n.done) {
1347
- sourceExhausted = true;
1348
- break;
1349
- }
1350
- if (this.canSkipTo(n.value)) {
1351
- latestSkipTarget = n.value;
1352
- }
1353
- }
1354
- if (this._ended) return;
1355
- if (latestSkipTarget !== void 0) {
1356
- this.sendItem(latestSkipTarget);
1357
- }
1358
- if (sourceExhausted) {
1359
- if (!this._ended) this.sendEnd();
1360
- return;
1361
- }
1362
- }
1363
- if (!this._ended) {
1364
- this.sendEnd();
1365
- }
1366
- } catch (e) {
1367
- if (!this._ended) {
1368
- this.sendEnd(e instanceof Error ? e : new Error(String(e)));
1369
- }
1370
- }
1371
- }
1372
- /** Wait until _nextIndex < _lastAckedIndex + ackAdvance. */
1373
- async _waitForAckBudget() {
1374
- while (this._nextIndex >= this._lastAckedIndex + this.ackAdvance) {
1375
- if (this._ended) return;
1376
- this._ackWaiting = new import_core5.PromiseSource();
1377
- await this._ackWaiting.promise;
1378
- }
1379
- }
1380
- // -- IRpcObject --
1381
- reconnect() {
1382
- }
1383
- disconnect() {
1384
- this._ended = true;
1385
- if (!this._started.isCompleted) {
1386
- this._started.resolve();
1387
- }
1388
- this.peer.sharedObjects.unregister(this);
1389
- }
1390
- };
1391
-
1392
1487
  // src/rpc-remote-object-tracker.ts
1393
1488
  var RpcRemoteObjectTracker = class {
1394
1489
  _objects = /* @__PURE__ */ new Map();
@@ -1809,15 +1904,13 @@ var RpcPeer = class {
1809
1904
  context
1810
1905
  );
1811
1906
  if (methodDef?.stream === true && this._connection !== void 0) {
1812
- const sender = new RpcStreamSender(this);
1813
- this.sharedObjects.register(sender);
1907
+ const stream = result instanceof RpcStream ? result : new RpcStream(result);
1814
1908
  this._hub.systemCallSender.ok(
1815
1909
  this._connection,
1816
1910
  this.format,
1817
1911
  relatedId,
1818
- sender.toRef()
1912
+ stream.toRef(this)
1819
1913
  );
1820
- void sender.writeFrom(result);
1821
1914
  } else if (!isNoWait && this._connection !== void 0) {
1822
1915
  this._hub.systemCallSender.ok(
1823
1916
  this._connection,