@statewalker/webrun-rpc 0.5.1 → 0.8.0

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
@@ -238,8 +238,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
238
238
  error
239
239
  });
240
240
  };
241
- port.addEventListener("message", onMessage);
242
- return () => port.removeEventListener("message", onMessage);
241
+ const listener = (event) => {
242
+ onMessage(event);
243
+ };
244
+ port.addEventListener("message", listener);
245
+ return () => port.removeEventListener("message", listener);
243
246
  }
244
247
  //#endregion
245
248
  //#region src/recieve.ts
@@ -822,7 +825,11 @@ function receiveChunks(port, channelName, controller, touch) {
822
825
  * outstanding at a time (spec D11/D12), with no per-chunk deadline (spec D8).
823
826
  */
824
827
  async function sendChunks(port, channelName, output, { maxMessageSize, log }, signal, touch) {
825
- const stream = throughAbort(maxMessageSize ? toChunks(maxMessageSize)(output) : output, signal);
828
+ const framed = maxMessageSize ? toChunks(maxMessageSize)(output) : output;
829
+ const stream = throughAbort(framed, signal);
830
+ const producer = output;
831
+ const returnProducer = () => void Promise.resolve(producer.return?.()).catch(() => {});
832
+ if (framed !== output) signal.addEventListener("abort", returnProducer, { once: true });
826
833
  try {
827
834
  await sendIterator(async ({ done, value, error }) => {
828
835
  if (signal.aborted) return;
@@ -846,9 +853,55 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
846
853
  } catch (err) {
847
854
  if (signal.aborted) return;
848
855
  throw err;
856
+ } finally {
857
+ signal.removeEventListener("abort", returnProducer);
849
858
  }
850
859
  }
851
860
  //#endregion
861
+ //#region src/port-credit.ts
862
+ /**
863
+ * Runs `fn` on a later task, never a microtask: a producer looping on
864
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
865
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
866
+ * are clamped to 4 ms in browsers.
867
+ */
868
+ const setImmediateFn = globalThis.setImmediate;
869
+ /** @internal */
870
+ const nextTask = setImmediateFn ? (fn) => {
871
+ setImmediateFn(fn);
872
+ } : (() => {
873
+ let channel;
874
+ const queue = [];
875
+ return (fn) => {
876
+ if (!channel) {
877
+ channel = new MessageChannel();
878
+ channel.port1.onmessage = () => queue.shift()?.();
879
+ }
880
+ queue.push(fn);
881
+ channel.port2.postMessage(0);
882
+ };
883
+ })();
884
+ const states = /* @__PURE__ */ new WeakMap();
885
+ /** @internal Registered by the multiplexer for each port with a window. */
886
+ function setPortSendState(port, state) {
887
+ states.set(port, state);
888
+ }
889
+ /**
890
+ * Bytes queued on `port` because its window is spent (0 for a port without
891
+ * flow control). A sender that should not queue without bound waits for
892
+ * `whenPortReady` before posting more.
893
+ */
894
+ function portBufferedAmount(port) {
895
+ return states.get(port)?.bufferedAmount() ?? 0;
896
+ }
897
+ /**
898
+ * Resolves once at most `below` bytes are queued on `port`, always on a later
899
+ * task (for a port without flow control, on the next one).
900
+ */
901
+ function whenPortReady(port, below = 0) {
902
+ return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
903
+ }
904
+ //#endregion
852
905
  //#region src/virtual-port.ts
853
906
  /**
854
907
  * One virtual port.
@@ -865,6 +918,14 @@ function newVirtualPort(send, requestClose) {
865
918
  if (closing.signal.aborted) return;
866
919
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
867
920
  };
921
+ const close = (reason) => {
922
+ if (closed) return;
923
+ closed = true;
924
+ const notify = requestClose;
925
+ listeners.clear();
926
+ notify(reason);
927
+ abort(reason);
928
+ };
868
929
  const port = {
869
930
  addEventListener(_type, listener) {
870
931
  listeners.add(listener);
@@ -876,24 +937,28 @@ function newVirtualPort(send, requestClose) {
876
937
  if (closed) return;
877
938
  send(message, transfer);
878
939
  },
879
- close() {
880
- if (closed) return;
881
- closed = true;
882
- const notify = requestClose;
883
- listeners.clear();
884
- notify();
885
- abort();
886
- }
940
+ close
887
941
  };
888
942
  setPortCloseSignal(port, closing.signal);
943
+ const dispose = Symbol.dispose;
944
+ if (dispose) Object.defineProperty(port, dispose, {
945
+ value: () => close(),
946
+ configurable: true
947
+ });
889
948
  return {
890
949
  port,
891
950
  deliver(payload) {
892
951
  if (closed) return;
893
952
  const event = new MessageEvent("message", { data: payload });
953
+ let pending;
894
954
  for (const listener of [...listeners]) try {
895
- listener(event);
955
+ const result = listener(event);
956
+ if (result && typeof result.then === "function") {
957
+ pending ??= [];
958
+ pending.push(result);
959
+ }
896
960
  } catch {}
961
+ return pending ? Promise.allSettled(pending) : void 0;
897
962
  },
898
963
  markClosed(reason) {
899
964
  closed = true;
@@ -902,6 +967,9 @@ function newVirtualPort(send, requestClose) {
902
967
  },
903
968
  isClosed() {
904
969
  return closed;
970
+ },
971
+ hasListeners() {
972
+ return listeners.size > 0;
905
973
  }
906
974
  };
907
975
  }
@@ -912,8 +980,39 @@ function newVirtualPort(send, requestClose) {
912
980
  * multiplexer when it is closed (`PortMux.close()`).
913
981
  */
914
982
  const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
983
+ /**
984
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
985
+ * whose codec passes unknown envelopes through reads them as a close of that
986
+ * id, which must therefore not be a real port (0 is the initiator's first).
987
+ */
988
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
989
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
990
+ const MAX_TIMER_MS = 2 ** 31 - 1;
915
991
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
916
992
  const DEFAULT_MAX_PORTS = 1024;
993
+ /** Default ceiling on one message reassembled from frames (`maxReassembledBytes`). */
994
+ const DEFAULT_MAX_REASSEMBLED_BYTES = 67108864;
995
+ /**
996
+ * What a message costs a port's window when the codec cannot say
997
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
998
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
999
+ * from the same structured value, so they agree.
1000
+ */
1001
+ function estimateMessageSize(payload) {
1002
+ let budget = 256;
1003
+ const walk = (v, depth) => {
1004
+ if (budget-- <= 0) return 0;
1005
+ if (typeof v === "string") return v.length;
1006
+ if (v instanceof ArrayBuffer) return v.byteLength;
1007
+ if (ArrayBuffer.isView(v)) return v.byteLength;
1008
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
1009
+ let n = 16;
1010
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
1011
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
1012
+ return n;
1013
+ };
1014
+ return Math.max(64, walk(payload, 0));
1015
+ }
917
1016
  /**
918
1017
  * The default `PortMux`: emulates multiplexing over a single port.
919
1018
  *
@@ -921,36 +1020,274 @@ const DEFAULT_MAX_PORTS = 1024;
921
1020
  * instead — this implementation is for transports that offer one pipe.
922
1021
  */
923
1022
  function multiplexPort(port, options) {
924
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
1023
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
1024
+ const maxReassembledBytes = options.maxReassembledBytes ?? 67108864;
1025
+ const misses = keepalive?.misses ?? 3;
1026
+ if (keepalive && !(Number.isFinite(keepalive.intervalMs) && keepalive.intervalMs > 0 && keepalive.intervalMs <= MAX_TIMER_MS && Number.isSafeInteger(misses) && misses > 0)) throw new RangeError(`webrun-rpc: keepalive needs an intervalMs in (0, ${MAX_TIMER_MS}] and a positive integer misses`);
1027
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
1028
+ if (!(Number.isSafeInteger(maxReassembledBytes) && maxReassembledBytes > 0)) throw new RangeError(`webrun-rpc: maxReassembledBytes must be a positive integer, got ${maxReassembledBytes}`);
925
1029
  const open = /* @__PURE__ */ new Map();
1030
+ let reassembling = 0;
1031
+ const freePartial = (entry) => {
1032
+ if (!entry?.partial) return;
1033
+ reassembling -= entry.partial.charged;
1034
+ entry.partial = void 0;
1035
+ };
1036
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
1037
+ const entry = open.get(id);
1038
+ if (entry?.handle !== record) return;
1039
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
1040
+ if (!record.hasListeners()) {
1041
+ release("leaked");
1042
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
1043
+ }
1044
+ try {
1045
+ onLeak?.(info);
1046
+ } catch {}
1047
+ }) : void 0;
1048
+ const touch = (id) => {
1049
+ const entry = open.get(id);
1050
+ if (entry) entry.lastActivity = Date.now();
1051
+ };
926
1052
  let nextId = side === "initiator" ? 0 : 1;
927
1053
  let muxClosed = false;
1054
+ /** This end counts encoded bytes and reassembles frames. */
1055
+ const exactCodec = codec.encode != null && codec.decode != null;
928
1056
  const post = (envelope, transfer) => {
929
1057
  if (muxClosed) return;
1058
+ codecPost(envelope, transfer);
1059
+ };
1060
+ /** Like `post`, also while closing: the mux's own close notices. */
1061
+ const codecPost = (envelope, transfer) => {
930
1062
  try {
931
1063
  codec.post(port, envelope, transfer);
932
1064
  } catch {}
933
1065
  };
934
- const attach = (id) => {
935
- let self;
936
- self = newVirtualPort((payload, transfer) => post({
937
- type: "message",
938
- id,
939
- payload
940
- }, transfer), (reason) => {
1066
+ const outgoing = /* @__PURE__ */ new Map();
1067
+ const ring = [];
1068
+ let pumping = false;
1069
+ const pump = async () => {
1070
+ pumping = true;
1071
+ try {
1072
+ while (ring.length && !muxClosed) {
1073
+ if (port.whenDrained) await port.whenDrained();
1074
+ if (muxClosed) break;
1075
+ const id = ring.shift();
1076
+ const queue = outgoing.get(id);
1077
+ const item = queue?.shift();
1078
+ if (queue?.length) ring.push(id);
1079
+ else outgoing.delete(id);
1080
+ item?.();
1081
+ }
1082
+ } catch (err) {
1083
+ close(err).catch(() => {});
1084
+ } finally {
1085
+ pumping = false;
1086
+ }
1087
+ };
1088
+ const sendOnPort = (id, item) => {
1089
+ if (frameSize == null) {
1090
+ item();
1091
+ return;
1092
+ }
1093
+ let queue = outgoing.get(id);
1094
+ if (!queue) {
1095
+ queue = [];
1096
+ outgoing.set(id, queue);
1097
+ ring.push(id);
1098
+ }
1099
+ queue.push(item);
1100
+ if (!pumping) pump();
1101
+ };
1102
+ const dropOutgoing = (id) => {
1103
+ if (!outgoing.delete(id)) return;
1104
+ const at = ring.indexOf(id);
1105
+ if (at >= 0) ring.splice(at, 1);
1106
+ };
1107
+ const postBytes = (bytes) => {
1108
+ if (muxClosed) return;
1109
+ try {
1110
+ port.postMessage(bytes);
1111
+ } catch {}
1112
+ };
1113
+ const releaseWaiters = (c) => {
1114
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
1115
+ };
1116
+ const flush = (c) => {
1117
+ while (c.queue.length) {
1118
+ const next = c.queue[0];
1119
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
1120
+ c.queue.shift();
1121
+ c.queued -= next.size;
1122
+ c.outstanding += next.size;
1123
+ next.send();
1124
+ }
1125
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
1126
+ c.waiters.splice(c.waiters.indexOf(w), 1);
1127
+ w.resolve();
1128
+ }
1129
+ };
1130
+ const grant = (id, c, bytes) => {
1131
+ c.unconsumed -= bytes;
1132
+ c.toGrant += bytes;
1133
+ const send = () => {
1134
+ c.grantScheduled = false;
1135
+ if (!c.toGrant || !open.has(id)) return;
941
1136
  post({
1137
+ type: "credit",
1138
+ id,
1139
+ bytes: c.toGrant
1140
+ });
1141
+ c.toGrant = 0;
1142
+ };
1143
+ if (c.toGrant >= c.window / 4) {
1144
+ send();
1145
+ return;
1146
+ }
1147
+ if (c.grantScheduled) return;
1148
+ c.grantScheduled = true;
1149
+ setTimeout(send, 0);
1150
+ };
1151
+ const attach = (id, meta, portWindow, opener, exact) => {
1152
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
1153
+ window: portWindow,
1154
+ enforced: false,
1155
+ opener,
1156
+ exact,
1157
+ outstanding: 0,
1158
+ queue: [],
1159
+ queued: 0,
1160
+ unconsumed: 0,
1161
+ waiters: [],
1162
+ toGrant: 0,
1163
+ grantScheduled: false
1164
+ } : void 0;
1165
+ const self = newVirtualPort((payload, transfer) => {
1166
+ touch(id);
1167
+ const envelope = {
1168
+ type: "message",
1169
+ id,
1170
+ payload
1171
+ };
1172
+ if (!c) {
1173
+ post(envelope, transfer);
1174
+ return;
1175
+ }
1176
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
1177
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
1178
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
1179
+ const held = !bytes && waits ? {
1180
+ ...envelope,
1181
+ payload: structuredClone(payload, { transfer })
1182
+ } : void 0;
1183
+ const send = (whole = false) => {
1184
+ if (!bytes) {
1185
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
1186
+ return;
1187
+ }
1188
+ if (!whole && frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
1189
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
1190
+ const data = bytes.subarray(at, at + frameSize);
1191
+ const frame = at + frameSize >= bytes.byteLength ? {
1192
+ type: "frame",
1193
+ id,
1194
+ data,
1195
+ last: true
1196
+ } : {
1197
+ type: "frame",
1198
+ id,
1199
+ data
1200
+ };
1201
+ sendOnPort(id, () => post(frame));
1202
+ }
1203
+ return;
1204
+ }
1205
+ sendOnPort(id, () => postBytes(bytes));
1206
+ };
1207
+ c.queue.push({
1208
+ send,
1209
+ size
1210
+ });
1211
+ c.queued += size;
1212
+ flush(c);
1213
+ }, (reason) => {
1214
+ release(reason);
1215
+ record.markClosed(reason);
1216
+ });
1217
+ function release(reason) {
1218
+ freePartial(open.get(id));
1219
+ open.delete(id);
1220
+ leaks?.unregister(record);
1221
+ if (c?.queue.length) {
1222
+ for (const item of c.queue.splice(0)) item.send(true);
1223
+ c.queued = 0;
1224
+ }
1225
+ sendOnPort(id, () => post({
942
1226
  type: "close",
943
1227
  id,
944
1228
  reason
1229
+ }));
1230
+ releaseWaiters(c);
1231
+ }
1232
+ const { markClosed, isClosed, hasListeners } = self;
1233
+ if (c) {
1234
+ const deliver = self.deliver;
1235
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
1236
+ c.unconsumed += size;
1237
+ const done = deliver(payload);
1238
+ if (done) done.then(() => grant(id, c, size));
1239
+ else grant(id, c, size);
1240
+ return done;
1241
+ };
1242
+ setPortSendState(self.port, {
1243
+ bufferedAmount: () => c.queued,
1244
+ ready: (below = 0) => new Promise((resolve) => {
1245
+ if (c.queued <= below || isClosed()) nextTask(resolve);
1246
+ else c.waiters.push({
1247
+ below,
1248
+ resolve: () => nextTask(resolve)
1249
+ });
1250
+ })
945
1251
  });
946
- open.delete(id);
947
- self.markClosed();
1252
+ }
1253
+ const record = {
1254
+ deliver: self.deliver,
1255
+ markClosed,
1256
+ isClosed,
1257
+ hasListeners,
1258
+ close: (reason) => {
1259
+ if (isClosed()) return;
1260
+ release(reason);
1261
+ markClosed(reason);
1262
+ }
1263
+ };
1264
+ leaks?.register(self.port, {
1265
+ id,
1266
+ record,
1267
+ release
1268
+ }, record);
1269
+ const now = Date.now();
1270
+ open.set(id, {
1271
+ id,
1272
+ meta,
1273
+ openedAt: now,
1274
+ lastActivity: now,
1275
+ handle: record,
1276
+ credit: c
948
1277
  });
949
- open.set(id, self);
950
1278
  return self;
951
1279
  };
952
- const handleEnvelope = (envelope) => {
1280
+ const handleEnvelope = (envelope, event) => {
953
1281
  if (muxClosed) return;
1282
+ heardSincePing = true;
1283
+ if (envelope.type === "ping" || envelope.type === "pong") {
1284
+ peerKeepsAlive = true;
1285
+ if (envelope.type === "ping") post({
1286
+ type: "pong",
1287
+ id: KEEPALIVE_ID
1288
+ });
1289
+ return;
1290
+ }
954
1291
  if (envelope.type === "open") {
955
1292
  if (open.has(envelope.id)) return;
956
1293
  if (open.size >= maxPorts) {
@@ -961,7 +1298,18 @@ function multiplexPort(port, options) {
961
1298
  });
962
1299
  return;
963
1300
  }
964
- const handle = attach(envelope.id);
1301
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
1302
+ const credit = open.get(envelope.id)?.credit;
1303
+ if (credit) post(credit.exact ? {
1304
+ type: "credit",
1305
+ id: envelope.id,
1306
+ bytes: 0,
1307
+ exact: true
1308
+ } : {
1309
+ type: "credit",
1310
+ id: envelope.id,
1311
+ bytes: 0
1312
+ });
965
1313
  let accepted = false;
966
1314
  if (onPort) try {
967
1315
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -976,57 +1324,215 @@ function multiplexPort(port, options) {
976
1324
  id: envelope.id,
977
1325
  reason: "rejected"
978
1326
  });
1327
+ return;
979
1328
  }
980
1329
  return;
981
1330
  }
982
- const handle = open.get(envelope.id);
1331
+ const entry = open.get(envelope.id);
1332
+ if (envelope.type === "credit") {
1333
+ const c = entry?.credit;
1334
+ if (!c || !(envelope.bytes >= 0)) return;
1335
+ if (!c.enforced && c.opener) {
1336
+ if (c.exact && envelope.exact !== true) {
1337
+ c.exact = false;
1338
+ c.outstanding = 0;
1339
+ }
1340
+ post({
1341
+ type: "credit",
1342
+ id: envelope.id,
1343
+ bytes: 0
1344
+ });
1345
+ }
1346
+ c.enforced = true;
1347
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
1348
+ flush(c);
1349
+ return;
1350
+ }
983
1351
  if (envelope.type === "message") {
984
- if (!handle) return;
985
- handle.deliver(envelope.payload);
1352
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
1353
+ if (!entry) return;
1354
+ touch(envelope.id);
1355
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
986
1356
  return;
987
1357
  }
988
- if (!handle) return;
1358
+ if (envelope.type === "frame") {
1359
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
1360
+ touch(envelope.id);
1361
+ const c = entry.credit;
1362
+ const owed = c.unconsumed + c.toGrant;
1363
+ const partial = entry.partial ?? {
1364
+ chunks: [],
1365
+ bytes: 0,
1366
+ owed,
1367
+ charged: 0,
1368
+ ...owed > 0 ? { room: c.window - owed } : {}
1369
+ };
1370
+ entry.partial = partial;
1371
+ const size = envelope.data.byteLength;
1372
+ const charged = partial.room == null ? Math.max(0, partial.bytes + size - c.window) : 0;
1373
+ const overrun = partial.room != null && partial.bytes + size > partial.room ? `what the window left (${partial.room} of ${c.window} bytes, ${partial.owed} still owed)` : reassembling - partial.charged + charged > maxReassembledBytes ? `maxReassembledBytes (${maxReassembledBytes} bytes past the window, across all ports)` : void 0;
1374
+ if (overrun) {
1375
+ freePartial(entry);
1376
+ dropOutgoing(envelope.id);
1377
+ open.delete(envelope.id);
1378
+ releaseWaiters(c);
1379
+ entry.handle.markClosed(/* @__PURE__ */ new RangeError(`webrun-rpc: a framed message passed ${overrun}`));
1380
+ post({
1381
+ type: "close",
1382
+ id: envelope.id,
1383
+ reason: "max-reassembled-bytes"
1384
+ });
1385
+ return;
1386
+ }
1387
+ partial.bytes += size;
1388
+ reassembling += charged - partial.charged;
1389
+ partial.charged = charged;
1390
+ partial.chunks.push(envelope.data);
1391
+ if (!envelope.last) return;
1392
+ freePartial(entry);
1393
+ const whole = new Uint8Array(partial.bytes);
1394
+ let at = 0;
1395
+ for (const chunk of partial.chunks) {
1396
+ whole.set(chunk, at);
1397
+ at += chunk.byteLength;
1398
+ }
1399
+ const inner = codec.decode(whole);
1400
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
1401
+ entry.handle.deliver(inner.payload, whole.byteLength);
1402
+ return;
1403
+ }
1404
+ dropOutgoing(envelope.id);
1405
+ if (!entry) return;
1406
+ freePartial(entry);
989
1407
  open.delete(envelope.id);
990
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
1408
+ releaseWaiters(entry.credit);
1409
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
991
1410
  };
992
1411
  const listener = (event) => {
993
1412
  const envelope = codec.read(event);
994
- if (envelope) handleEnvelope(envelope);
1413
+ if (envelope) handleEnvelope(envelope, event);
1414
+ };
1415
+ let peerKeepsAlive = false;
1416
+ let heardSincePing = true;
1417
+ let unanswered = 0;
1418
+ let pinger;
1419
+ const tick = () => {
1420
+ if (!keepalive) return;
1421
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
1422
+ if (heardSincePing) unanswered = 0;
1423
+ else if (peerKeepsAlive && !backlogged) unanswered++;
1424
+ if (unanswered >= misses) {
1425
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
1426
+ return;
1427
+ }
1428
+ heardSincePing = false;
1429
+ post({
1430
+ type: "ping",
1431
+ id: KEEPALIVE_ID
1432
+ });
1433
+ };
1434
+ const close = async (reason) => {
1435
+ if (muxClosed) return;
1436
+ clearInterval(pinger);
1437
+ muxClosed = true;
1438
+ signal?.removeEventListener("abort", onTransportAbort);
1439
+ port.removeEventListener("message", listener);
1440
+ const closedNow = /* @__PURE__ */ new Set();
1441
+ for (const [id, { handle, credit }] of [...open]) {
1442
+ closedNow.add(id);
1443
+ codecPost({
1444
+ type: "close",
1445
+ id
1446
+ });
1447
+ open.delete(id);
1448
+ releaseWaiters(credit);
1449
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
1450
+ }
1451
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
1452
+ type: "close",
1453
+ id
1454
+ });
1455
+ outgoing.clear();
1456
+ ring.length = 0;
1457
+ await port.close?.();
1458
+ };
1459
+ const onTransportAbort = () => {
1460
+ close(signal?.reason).catch(() => {});
995
1461
  };
996
1462
  port.addEventListener("message", listener);
997
1463
  port.start?.();
1464
+ if (keepalive) {
1465
+ pinger = setInterval(tick, keepalive.intervalMs);
1466
+ pinger.unref?.();
1467
+ }
1468
+ if (signal?.aborted) close(signal.reason).catch(() => {});
1469
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
998
1470
  return {
999
1471
  maxMessageSize,
1000
- async openPort(meta) {
1472
+ async openPort(meta, { signal: portSignal } = {}) {
1001
1473
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
1002
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
1474
+ if (portSignal?.aborted) throw portSignal.reason;
1475
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
1003
1476
  while (open.has(nextId)) nextId += 2;
1004
1477
  const id = nextId;
1005
1478
  nextId += 2;
1006
- const handle = attach(id);
1007
- post({
1479
+ const handle = attach(id, meta, window, true, exactCodec);
1480
+ post(window ? exactCodec ? {
1481
+ type: "open",
1482
+ id,
1483
+ meta,
1484
+ window,
1485
+ exact: true
1486
+ } : {
1487
+ type: "open",
1488
+ id,
1489
+ meta,
1490
+ window
1491
+ } : {
1008
1492
  type: "open",
1009
1493
  id,
1010
1494
  meta
1011
1495
  });
1496
+ if (portSignal) {
1497
+ const record = open.get(id)?.handle;
1498
+ const onAbort = () => {
1499
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
1500
+ };
1501
+ portSignal.addEventListener("abort", onAbort, { once: true });
1502
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
1503
+ }
1012
1504
  return handle.port;
1013
1505
  },
1014
- async close() {
1015
- if (muxClosed) return;
1016
- for (const [id, handle] of [...open]) {
1017
- post({
1018
- type: "close",
1019
- id
1020
- });
1021
- open.delete(id);
1022
- handle.markClosed(/* @__PURE__ */ new Error(MUX_CLOSED_MESSAGE));
1023
- }
1024
- muxClosed = true;
1025
- port.removeEventListener("message", listener);
1026
- await port.close?.();
1506
+ close,
1507
+ ports() {
1508
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
1509
+ ...info,
1510
+ ...credit.enforced ? { window: credit.window } : {},
1511
+ queued: credit.queued,
1512
+ outstanding: credit.outstanding,
1513
+ unconsumed: credit.unconsumed
1514
+ } : info);
1027
1515
  }
1028
1516
  };
1029
1517
  }
1518
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
1519
+ function byteLengthOf(data) {
1520
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
1521
+ }
1522
+ /**
1523
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
1524
+ * a full table names who filled it. A port without a kind counts as `"?"`.
1525
+ */
1526
+ function describeKinds(entries) {
1527
+ const counts = /* @__PURE__ */ new Map();
1528
+ for (const { meta } of entries) {
1529
+ const kind = meta?.kind;
1530
+ const name = typeof kind === "string" ? kind : "?";
1531
+ counts.set(name, (counts.get(name) ?? 0) + 1);
1532
+ }
1533
+ if (!counts.size) return "none";
1534
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
1535
+ }
1030
1536
  //#endregion
1031
1537
  //#region src/connect-serve.ts
1032
1538
  /**
@@ -1090,14 +1596,17 @@ const serve = async ({ mux: factory, timeout }, handler) => {
1090
1596
  const mux = await factory((port) => {
1091
1597
  let off;
1092
1598
  let finished = false;
1093
- const tracked = (input) => (async function* () {
1094
- try {
1095
- yield* handler(input);
1096
- } finally {
1097
- finished = true;
1098
- if (off) live.delete(off);
1099
- }
1100
- })();
1599
+ const tracked = (input) => {
1600
+ const output = handler(input);
1601
+ return returningAtOnce((async function* () {
1602
+ try {
1603
+ yield* output;
1604
+ } finally {
1605
+ finished = true;
1606
+ if (off) live.delete(off);
1607
+ }
1608
+ })(), () => void output.return(void 0).catch(() => {}));
1609
+ };
1101
1610
  off = serveDuplexOverPort(port, tracked, streamOptions);
1102
1611
  if (!finished) live.add(off);
1103
1612
  });
@@ -1201,7 +1710,7 @@ function isEnvelope(value) {
1201
1710
  const candidate = value;
1202
1711
  if (typeof candidate.id !== "number") return false;
1203
1712
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
1204
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
1713
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
1205
1714
  }
1206
1715
  /**
1207
1716
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -1247,7 +1756,7 @@ const PORT_TRANSFER = "webrun-rpc:port-transfer";
1247
1756
  */
1248
1757
  function transferPortMux(target, options = {}) {
1249
1758
  const { onPort, maxMessageSize } = options;
1250
- const issued = /* @__PURE__ */ new Set();
1759
+ const issued = /* @__PURE__ */ new Map();
1251
1760
  let closed = false;
1252
1761
  const listener = (event) => {
1253
1762
  if (closed) return;
@@ -1266,28 +1775,47 @@ function transferPortMux(target, options = {}) {
1266
1775
  port.close();
1267
1776
  return;
1268
1777
  }
1269
- issued.add(port);
1778
+ const forget = () => issued.delete(port);
1779
+ issued.set(port, forget);
1780
+ const close = port.close.bind(port);
1781
+ port.close = () => {
1782
+ forget();
1783
+ close();
1784
+ };
1270
1785
  };
1271
1786
  target.addEventListener("message", listener);
1272
1787
  target.start?.();
1273
1788
  return {
1274
1789
  maxMessageSize,
1275
- async openPort(meta) {
1790
+ async openPort(meta, { signal } = {}) {
1276
1791
  if (closed) throw new Error("webrun-rpc: the multiplexer is closed");
1792
+ if (signal?.aborted) throw signal.reason;
1277
1793
  const channel = new MessageChannel();
1278
1794
  channel.port1.start();
1279
1795
  target.postMessage({
1280
1796
  type: PORT_TRANSFER,
1281
1797
  meta
1282
1798
  }, [channel.port2]);
1283
- issued.add(channel.port1);
1284
- return channel.port1;
1799
+ const port = channel.port1;
1800
+ const onAbort = () => port.close();
1801
+ signal?.addEventListener("abort", onAbort, { once: true });
1802
+ const forget = () => {
1803
+ signal?.removeEventListener("abort", onAbort);
1804
+ issued.delete(port);
1805
+ };
1806
+ issued.set(port, forget);
1807
+ const close = port.close.bind(port);
1808
+ port.close = () => {
1809
+ forget();
1810
+ close();
1811
+ };
1812
+ return port;
1285
1813
  },
1286
1814
  async close() {
1287
1815
  if (closed) return;
1288
1816
  closed = true;
1289
1817
  target.removeEventListener("message", listener);
1290
- for (const port of issued) try {
1818
+ for (const port of [...issued.keys()]) try {
1291
1819
  port.close();
1292
1820
  } catch {}
1293
1821
  issued.clear();
@@ -1296,4 +1824,4 @@ function transferPortMux(target, options = {}) {
1296
1824
  };
1297
1825
  }
1298
1826
  //#endregion
1299
- export { CANCEL_CHANNEL_TYPE, DEFAULT_MAX_PORTS, MUX_CLOSED_MESSAGE, NO_TIMEOUT, PORT_TRANSFER, STREAM_ABORT, byteChannelFromMessagePort, callBidi, callPort, connect, duplexOverPort, getPortCloseSignal, ioHandle, ioSend, listenBidi, listenCancelChannel, listenPort, multiplexPort, overPipe, overPorts, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux };
1827
+ export { CANCEL_CHANNEL_TYPE, DEFAULT_MAX_PORTS, DEFAULT_MAX_REASSEMBLED_BYTES, MUX_CLOSED_MESSAGE, NO_TIMEOUT, PORT_TRANSFER, STREAM_ABORT, byteChannelFromMessagePort, callBidi, callPort, connect, duplexOverPort, getPortCloseSignal, ioHandle, ioSend, listenBidi, listenCancelChannel, listenPort, multiplexPort, overPipe, overPorts, portBufferedAmount, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux, whenPortReady };