@statewalker/webrun-rpc 0.5.1 → 0.7.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
@@ -849,6 +852,50 @@ async function sendChunks(port, channelName, output, { maxMessageSize, log }, si
849
852
  }
850
853
  }
851
854
  //#endregion
855
+ //#region src/port-credit.ts
856
+ /**
857
+ * Runs `fn` on a later task, never a microtask: a producer looping on
858
+ * `whenPortReady` must let the transport's own events run. `setImmediate`
859
+ * where it exists; elsewhere a `MessageChannel`, because nested `setTimeout`s
860
+ * are clamped to 4 ms in browsers.
861
+ */
862
+ const setImmediateFn = globalThis.setImmediate;
863
+ /** @internal */
864
+ const nextTask = setImmediateFn ? (fn) => {
865
+ setImmediateFn(fn);
866
+ } : (() => {
867
+ let channel;
868
+ const queue = [];
869
+ return (fn) => {
870
+ if (!channel) {
871
+ channel = new MessageChannel();
872
+ channel.port1.onmessage = () => queue.shift()?.();
873
+ }
874
+ queue.push(fn);
875
+ channel.port2.postMessage(0);
876
+ };
877
+ })();
878
+ const states = /* @__PURE__ */ new WeakMap();
879
+ /** @internal Registered by the multiplexer for each port with a window. */
880
+ function setPortSendState(port, state) {
881
+ states.set(port, state);
882
+ }
883
+ /**
884
+ * Bytes queued on `port` because its window is spent (0 for a port without
885
+ * flow control). A sender that should not queue without bound waits for
886
+ * `whenPortReady` before posting more.
887
+ */
888
+ function portBufferedAmount(port) {
889
+ return states.get(port)?.bufferedAmount() ?? 0;
890
+ }
891
+ /**
892
+ * Resolves once at most `below` bytes are queued on `port`, always on a later
893
+ * task (for a port without flow control, on the next one).
894
+ */
895
+ function whenPortReady(port, below = 0) {
896
+ return states.get(port)?.ready(below) ?? new Promise((resolve) => nextTask(resolve));
897
+ }
898
+ //#endregion
852
899
  //#region src/virtual-port.ts
853
900
  /**
854
901
  * One virtual port.
@@ -865,6 +912,14 @@ function newVirtualPort(send, requestClose) {
865
912
  if (closing.signal.aborted) return;
866
913
  closing.abort(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the port was closed"));
867
914
  };
915
+ const close = (reason) => {
916
+ if (closed) return;
917
+ closed = true;
918
+ const notify = requestClose;
919
+ listeners.clear();
920
+ notify(reason);
921
+ abort(reason);
922
+ };
868
923
  const port = {
869
924
  addEventListener(_type, listener) {
870
925
  listeners.add(listener);
@@ -876,24 +931,28 @@ function newVirtualPort(send, requestClose) {
876
931
  if (closed) return;
877
932
  send(message, transfer);
878
933
  },
879
- close() {
880
- if (closed) return;
881
- closed = true;
882
- const notify = requestClose;
883
- listeners.clear();
884
- notify();
885
- abort();
886
- }
934
+ close
887
935
  };
888
936
  setPortCloseSignal(port, closing.signal);
937
+ const dispose = Symbol.dispose;
938
+ if (dispose) Object.defineProperty(port, dispose, {
939
+ value: () => close(),
940
+ configurable: true
941
+ });
889
942
  return {
890
943
  port,
891
944
  deliver(payload) {
892
945
  if (closed) return;
893
946
  const event = new MessageEvent("message", { data: payload });
947
+ let pending;
894
948
  for (const listener of [...listeners]) try {
895
- listener(event);
949
+ const result = listener(event);
950
+ if (result && typeof result.then === "function") {
951
+ pending ??= [];
952
+ pending.push(result);
953
+ }
896
954
  } catch {}
955
+ return pending ? Promise.allSettled(pending) : void 0;
897
956
  },
898
957
  markClosed(reason) {
899
958
  closed = true;
@@ -902,6 +961,9 @@ function newVirtualPort(send, requestClose) {
902
961
  },
903
962
  isClosed() {
904
963
  return closed;
964
+ },
965
+ hasListeners() {
966
+ return listeners.size > 0;
905
967
  }
906
968
  };
907
969
  }
@@ -912,45 +974,303 @@ function newVirtualPort(send, requestClose) {
912
974
  * multiplexer when it is closed (`PortMux.close()`).
913
975
  */
914
976
  const MUX_CLOSED_MESSAGE = "webrun-rpc: the multiplexer was closed";
977
+ /**
978
+ * The id of keepalive's `ping` / `pong`: one no port ever takes. An older mux
979
+ * whose codec passes unknown envelopes through reads them as a close of that
980
+ * id, which must therefore not be a real port (0 is the initiator's first).
981
+ */
982
+ const KEEPALIVE_ID = Number.MAX_SAFE_INTEGER;
983
+ /** The longest delay a timer holds; past it `setInterval` fires at once. */
984
+ const MAX_TIMER_MS = 2 ** 31 - 1;
915
985
  /** Ceiling on concurrently open virtual ports. Bounds the id table only. */
916
986
  const DEFAULT_MAX_PORTS = 1024;
917
987
  /**
988
+ * What a message costs a port's window when the codec cannot say
989
+ * (`PortCodec.encode`): its bytes and string lengths, found in its values up
990
+ * to 4 levels deep (at most 256 values), and at least 64. Both ends compute it
991
+ * from the same structured value, so they agree.
992
+ */
993
+ function estimateMessageSize(payload) {
994
+ let budget = 256;
995
+ const walk = (v, depth) => {
996
+ if (budget-- <= 0) return 0;
997
+ if (typeof v === "string") return v.length;
998
+ if (v instanceof ArrayBuffer) return v.byteLength;
999
+ if (ArrayBuffer.isView(v)) return v.byteLength;
1000
+ if (v === null || typeof v !== "object" || depth >= 4) return 8;
1001
+ let n = 16;
1002
+ if (Array.isArray(v)) for (const x of v) n += walk(x, depth + 1);
1003
+ else for (const k in v) n += k.length + walk(v[k], depth + 1);
1004
+ return n;
1005
+ };
1006
+ return Math.max(64, walk(payload, 0));
1007
+ }
1008
+ /**
918
1009
  * The default `PortMux`: emulates multiplexing over a single port.
919
1010
  *
920
1011
  * A transport that already multiplexes natively supplies its own `PortMux`
921
1012
  * instead — this implementation is for transports that offer one pipe.
922
1013
  */
923
1014
  function multiplexPort(port, options) {
924
- const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize } = options;
1015
+ const { codec, onPort, side = "initiator", maxPorts = DEFAULT_MAX_PORTS, maxMessageSize, signal, window, frameSize, keepalive, onLeak } = options;
1016
+ const misses = keepalive?.misses ?? 3;
1017
+ 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`);
1018
+ if (frameSize != null && !(Number.isSafeInteger(frameSize) && frameSize > 0)) throw new RangeError(`webrun-rpc: frameSize must be a positive integer, got ${frameSize}`);
925
1019
  const open = /* @__PURE__ */ new Map();
1020
+ const leaks = onLeak ? new FinalizationRegistry(({ id, record, release }) => {
1021
+ const entry = open.get(id);
1022
+ if (entry?.handle !== record) return;
1023
+ const { handle: _, credit: __, partial: ___, ...info } = entry;
1024
+ if (!record.hasListeners()) {
1025
+ release("leaked");
1026
+ record.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the port was dropped without close()"));
1027
+ }
1028
+ try {
1029
+ onLeak?.(info);
1030
+ } catch {}
1031
+ }) : void 0;
1032
+ const touch = (id) => {
1033
+ const entry = open.get(id);
1034
+ if (entry) entry.lastActivity = Date.now();
1035
+ };
926
1036
  let nextId = side === "initiator" ? 0 : 1;
927
1037
  let muxClosed = false;
1038
+ /** This end counts encoded bytes and reassembles frames. */
1039
+ const exactCodec = codec.encode != null && codec.decode != null;
928
1040
  const post = (envelope, transfer) => {
929
1041
  if (muxClosed) return;
1042
+ codecPost(envelope, transfer);
1043
+ };
1044
+ /** Like `post`, also while closing: the mux's own close notices. */
1045
+ const codecPost = (envelope, transfer) => {
930
1046
  try {
931
1047
  codec.post(port, envelope, transfer);
932
1048
  } catch {}
933
1049
  };
934
- const attach = (id) => {
935
- let self;
936
- self = newVirtualPort((payload, transfer) => post({
937
- type: "message",
938
- id,
939
- payload
940
- }, transfer), (reason) => {
1050
+ const outgoing = /* @__PURE__ */ new Map();
1051
+ const ring = [];
1052
+ let pumping = false;
1053
+ const pump = async () => {
1054
+ pumping = true;
1055
+ try {
1056
+ while (ring.length && !muxClosed) {
1057
+ if (port.whenDrained) await port.whenDrained();
1058
+ if (muxClosed) break;
1059
+ const id = ring.shift();
1060
+ const queue = outgoing.get(id);
1061
+ const item = queue?.shift();
1062
+ if (queue?.length) ring.push(id);
1063
+ else outgoing.delete(id);
1064
+ item?.();
1065
+ }
1066
+ } catch (err) {
1067
+ close(err).catch(() => {});
1068
+ } finally {
1069
+ pumping = false;
1070
+ }
1071
+ };
1072
+ const sendOnPort = (id, item) => {
1073
+ if (frameSize == null) {
1074
+ item();
1075
+ return;
1076
+ }
1077
+ let queue = outgoing.get(id);
1078
+ if (!queue) {
1079
+ queue = [];
1080
+ outgoing.set(id, queue);
1081
+ ring.push(id);
1082
+ }
1083
+ queue.push(item);
1084
+ if (!pumping) pump();
1085
+ };
1086
+ const dropOutgoing = (id) => {
1087
+ if (!outgoing.delete(id)) return;
1088
+ const at = ring.indexOf(id);
1089
+ if (at >= 0) ring.splice(at, 1);
1090
+ };
1091
+ const postBytes = (bytes) => {
1092
+ if (muxClosed) return;
1093
+ try {
1094
+ port.postMessage(bytes);
1095
+ } catch {}
1096
+ };
1097
+ const releaseWaiters = (c) => {
1098
+ for (const w of c?.waiters.splice(0) ?? []) w.resolve();
1099
+ };
1100
+ const flush = (c) => {
1101
+ while (c.queue.length) {
1102
+ const next = c.queue[0];
1103
+ if (c.enforced && c.outstanding > 0 && c.outstanding + next.size > c.window) break;
1104
+ c.queue.shift();
1105
+ c.queued -= next.size;
1106
+ c.outstanding += next.size;
1107
+ next.send();
1108
+ }
1109
+ for (const w of [...c.waiters]) if (c.queued <= w.below) {
1110
+ c.waiters.splice(c.waiters.indexOf(w), 1);
1111
+ w.resolve();
1112
+ }
1113
+ };
1114
+ const grant = (id, c, bytes) => {
1115
+ c.unconsumed -= bytes;
1116
+ c.toGrant += bytes;
1117
+ const send = () => {
1118
+ c.grantScheduled = false;
1119
+ if (!c.toGrant || !open.has(id)) return;
941
1120
  post({
1121
+ type: "credit",
1122
+ id,
1123
+ bytes: c.toGrant
1124
+ });
1125
+ c.toGrant = 0;
1126
+ };
1127
+ if (c.toGrant >= c.window / 4) {
1128
+ send();
1129
+ return;
1130
+ }
1131
+ if (c.grantScheduled) return;
1132
+ c.grantScheduled = true;
1133
+ setTimeout(send, 0);
1134
+ };
1135
+ const attach = (id, meta, portWindow, opener, exact) => {
1136
+ const c = typeof portWindow === "number" && Number.isSafeInteger(portWindow) && portWindow > 0 ? {
1137
+ window: portWindow,
1138
+ enforced: false,
1139
+ opener,
1140
+ exact,
1141
+ outstanding: 0,
1142
+ queue: [],
1143
+ queued: 0,
1144
+ unconsumed: 0,
1145
+ waiters: [],
1146
+ toGrant: 0,
1147
+ grantScheduled: false
1148
+ } : void 0;
1149
+ const self = newVirtualPort((payload, transfer) => {
1150
+ touch(id);
1151
+ const envelope = {
1152
+ type: "message",
1153
+ id,
1154
+ payload
1155
+ };
1156
+ if (!c) {
1157
+ post(envelope, transfer);
1158
+ return;
1159
+ }
1160
+ const bytes = c.exact ? codec.encode?.(envelope) : void 0;
1161
+ const size = bytes?.byteLength ?? estimateMessageSize(payload);
1162
+ const waits = frameSize != null || c.queue.length > 0 || c.enforced && c.outstanding > 0 && c.outstanding + size > c.window;
1163
+ const held = !bytes && waits ? {
1164
+ ...envelope,
1165
+ payload: structuredClone(payload, { transfer })
1166
+ } : void 0;
1167
+ const send = () => {
1168
+ if (!bytes) {
1169
+ sendOnPort(id, held ? () => post(held) : () => post(envelope, transfer));
1170
+ return;
1171
+ }
1172
+ if (frameSize != null && c.enforced && c.exact && bytes.byteLength > frameSize) {
1173
+ for (let at = 0; at < bytes.byteLength; at += frameSize) {
1174
+ const data = bytes.subarray(at, at + frameSize);
1175
+ const frame = at + frameSize >= bytes.byteLength ? {
1176
+ type: "frame",
1177
+ id,
1178
+ data,
1179
+ last: true
1180
+ } : {
1181
+ type: "frame",
1182
+ id,
1183
+ data
1184
+ };
1185
+ sendOnPort(id, () => post(frame));
1186
+ }
1187
+ return;
1188
+ }
1189
+ sendOnPort(id, () => postBytes(bytes));
1190
+ };
1191
+ c.queue.push({
1192
+ send,
1193
+ size
1194
+ });
1195
+ c.queued += size;
1196
+ flush(c);
1197
+ }, (reason) => {
1198
+ release(reason);
1199
+ record.markClosed(reason);
1200
+ });
1201
+ function release(reason) {
1202
+ open.delete(id);
1203
+ leaks?.unregister(record);
1204
+ if (c?.queue.length) {
1205
+ for (const item of c.queue.splice(0)) item.send();
1206
+ c.queued = 0;
1207
+ }
1208
+ sendOnPort(id, () => post({
942
1209
  type: "close",
943
1210
  id,
944
1211
  reason
1212
+ }));
1213
+ releaseWaiters(c);
1214
+ }
1215
+ const { markClosed, isClosed, hasListeners } = self;
1216
+ if (c) {
1217
+ const deliver = self.deliver;
1218
+ self.deliver = (payload, size = estimateMessageSize(payload)) => {
1219
+ c.unconsumed += size;
1220
+ const done = deliver(payload);
1221
+ if (done) done.then(() => grant(id, c, size));
1222
+ else grant(id, c, size);
1223
+ return done;
1224
+ };
1225
+ setPortSendState(self.port, {
1226
+ bufferedAmount: () => c.queued,
1227
+ ready: (below = 0) => new Promise((resolve) => {
1228
+ if (c.queued <= below || isClosed()) nextTask(resolve);
1229
+ else c.waiters.push({
1230
+ below,
1231
+ resolve: () => nextTask(resolve)
1232
+ });
1233
+ })
945
1234
  });
946
- open.delete(id);
947
- self.markClosed();
1235
+ }
1236
+ const record = {
1237
+ deliver: self.deliver,
1238
+ markClosed,
1239
+ isClosed,
1240
+ hasListeners,
1241
+ close: (reason) => {
1242
+ if (isClosed()) return;
1243
+ release(reason);
1244
+ markClosed(reason);
1245
+ }
1246
+ };
1247
+ leaks?.register(self.port, {
1248
+ id,
1249
+ record,
1250
+ release
1251
+ }, record);
1252
+ const now = Date.now();
1253
+ open.set(id, {
1254
+ id,
1255
+ meta,
1256
+ openedAt: now,
1257
+ lastActivity: now,
1258
+ handle: record,
1259
+ credit: c
948
1260
  });
949
- open.set(id, self);
950
1261
  return self;
951
1262
  };
952
- const handleEnvelope = (envelope) => {
1263
+ const handleEnvelope = (envelope, event) => {
953
1264
  if (muxClosed) return;
1265
+ heardSincePing = true;
1266
+ if (envelope.type === "ping" || envelope.type === "pong") {
1267
+ peerKeepsAlive = true;
1268
+ if (envelope.type === "ping") post({
1269
+ type: "pong",
1270
+ id: KEEPALIVE_ID
1271
+ });
1272
+ return;
1273
+ }
954
1274
  if (envelope.type === "open") {
955
1275
  if (open.has(envelope.id)) return;
956
1276
  if (open.size >= maxPorts) {
@@ -961,7 +1281,18 @@ function multiplexPort(port, options) {
961
1281
  });
962
1282
  return;
963
1283
  }
964
- const handle = attach(envelope.id);
1284
+ const handle = attach(envelope.id, envelope.meta, envelope.window, false, envelope.exact === true && exactCodec);
1285
+ const credit = open.get(envelope.id)?.credit;
1286
+ if (credit) post(credit.exact ? {
1287
+ type: "credit",
1288
+ id: envelope.id,
1289
+ bytes: 0,
1290
+ exact: true
1291
+ } : {
1292
+ type: "credit",
1293
+ id: envelope.id,
1294
+ bytes: 0
1295
+ });
965
1296
  let accepted = false;
966
1297
  if (onPort) try {
967
1298
  accepted = onPort(handle.port, envelope.meta) !== false;
@@ -976,57 +1307,191 @@ function multiplexPort(port, options) {
976
1307
  id: envelope.id,
977
1308
  reason: "rejected"
978
1309
  });
1310
+ return;
1311
+ }
1312
+ return;
1313
+ }
1314
+ const entry = open.get(envelope.id);
1315
+ if (envelope.type === "credit") {
1316
+ const c = entry?.credit;
1317
+ if (!c || !(envelope.bytes >= 0)) return;
1318
+ if (!c.enforced && c.opener) {
1319
+ if (c.exact && envelope.exact !== true) {
1320
+ c.exact = false;
1321
+ c.outstanding = 0;
1322
+ }
1323
+ post({
1324
+ type: "credit",
1325
+ id: envelope.id,
1326
+ bytes: 0
1327
+ });
979
1328
  }
1329
+ c.enforced = true;
1330
+ c.outstanding = Math.max(0, c.outstanding - envelope.bytes);
1331
+ flush(c);
980
1332
  return;
981
1333
  }
982
- const handle = open.get(envelope.id);
983
1334
  if (envelope.type === "message") {
984
- if (!handle) return;
985
- handle.deliver(envelope.payload);
1335
+ const sizeOf = (c) => (c.exact ? byteLengthOf(event.data) : void 0) ?? estimateMessageSize(envelope.payload);
1336
+ if (!entry) return;
1337
+ touch(envelope.id);
1338
+ entry.handle.deliver(envelope.payload, entry.credit ? sizeOf(entry.credit) : void 0);
1339
+ return;
1340
+ }
1341
+ if (envelope.type === "frame") {
1342
+ if (!entry?.credit?.exact || !codec.decode || !(envelope.data instanceof Uint8Array)) return;
1343
+ touch(envelope.id);
1344
+ const partial = entry.partial ?? {
1345
+ chunks: [],
1346
+ bytes: 0
1347
+ };
1348
+ entry.partial = partial;
1349
+ partial.chunks.push(envelope.data);
1350
+ partial.bytes += envelope.data.byteLength;
1351
+ if (!envelope.last) return;
1352
+ entry.partial = void 0;
1353
+ const whole = new Uint8Array(partial.bytes);
1354
+ let at = 0;
1355
+ for (const chunk of partial.chunks) {
1356
+ whole.set(chunk, at);
1357
+ at += chunk.byteLength;
1358
+ }
1359
+ const inner = codec.decode(whole);
1360
+ if (inner?.type !== "message" || inner.id !== envelope.id) return;
1361
+ entry.handle.deliver(inner.payload, whole.byteLength);
986
1362
  return;
987
1363
  }
988
- if (!handle) return;
1364
+ dropOutgoing(envelope.id);
1365
+ if (!entry) return;
989
1366
  open.delete(envelope.id);
990
- handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
1367
+ releaseWaiters(entry.credit);
1368
+ entry.handle.markClosed(/* @__PURE__ */ new Error("webrun-rpc: the peer closed the port"));
991
1369
  };
992
1370
  const listener = (event) => {
993
1371
  const envelope = codec.read(event);
994
- if (envelope) handleEnvelope(envelope);
1372
+ if (envelope) handleEnvelope(envelope, event);
1373
+ };
1374
+ let peerKeepsAlive = false;
1375
+ let heardSincePing = true;
1376
+ let unanswered = 0;
1377
+ let pinger;
1378
+ const tick = () => {
1379
+ if (!keepalive) return;
1380
+ const backlogged = (port.bufferedAmount ?? 0) > 0;
1381
+ if (heardSincePing) unanswered = 0;
1382
+ else if (peerKeepsAlive && !backlogged) unanswered++;
1383
+ if (unanswered >= misses) {
1384
+ close(/* @__PURE__ */ new Error(`webrun-rpc: the peer did not answer ${misses} keepalive pings, ${keepalive.intervalMs} ms apart`)).catch(() => {});
1385
+ return;
1386
+ }
1387
+ heardSincePing = false;
1388
+ post({
1389
+ type: "ping",
1390
+ id: KEEPALIVE_ID
1391
+ });
1392
+ };
1393
+ const close = async (reason) => {
1394
+ if (muxClosed) return;
1395
+ clearInterval(pinger);
1396
+ muxClosed = true;
1397
+ signal?.removeEventListener("abort", onTransportAbort);
1398
+ port.removeEventListener("message", listener);
1399
+ const closedNow = /* @__PURE__ */ new Set();
1400
+ for (const [id, { handle, credit }] of [...open]) {
1401
+ closedNow.add(id);
1402
+ codecPost({
1403
+ type: "close",
1404
+ id
1405
+ });
1406
+ open.delete(id);
1407
+ releaseWaiters(credit);
1408
+ handle.markClosed(reason ?? /* @__PURE__ */ new Error("webrun-rpc: the multiplexer was closed"));
1409
+ }
1410
+ for (const id of outgoing.keys()) if (!closedNow.has(id)) codecPost({
1411
+ type: "close",
1412
+ id
1413
+ });
1414
+ outgoing.clear();
1415
+ ring.length = 0;
1416
+ await port.close?.();
1417
+ };
1418
+ const onTransportAbort = () => {
1419
+ close(signal?.reason).catch(() => {});
995
1420
  };
996
1421
  port.addEventListener("message", listener);
997
1422
  port.start?.();
1423
+ if (keepalive) {
1424
+ pinger = setInterval(tick, keepalive.intervalMs);
1425
+ pinger.unref?.();
1426
+ }
1427
+ if (signal?.aborted) close(signal.reason).catch(() => {});
1428
+ else signal?.addEventListener("abort", onTransportAbort, { once: true });
998
1429
  return {
999
1430
  maxMessageSize,
1000
- async openPort(meta) {
1431
+ async openPort(meta, { signal: portSignal } = {}) {
1001
1432
  if (muxClosed) throw new Error("webrun-rpc: the multiplexer is closed");
1002
- if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached`);
1433
+ if (portSignal?.aborted) throw portSignal.reason;
1434
+ if (open.size >= maxPorts) throw new RangeError(`webrun-rpc: maxPorts (${maxPorts}) reached; open ports by kind: ${describeKinds(open.values())}`);
1003
1435
  while (open.has(nextId)) nextId += 2;
1004
1436
  const id = nextId;
1005
1437
  nextId += 2;
1006
- const handle = attach(id);
1007
- post({
1438
+ const handle = attach(id, meta, window, true, exactCodec);
1439
+ post(window ? exactCodec ? {
1440
+ type: "open",
1441
+ id,
1442
+ meta,
1443
+ window,
1444
+ exact: true
1445
+ } : {
1446
+ type: "open",
1447
+ id,
1448
+ meta,
1449
+ window
1450
+ } : {
1008
1451
  type: "open",
1009
1452
  id,
1010
1453
  meta
1011
1454
  });
1455
+ if (portSignal) {
1456
+ const record = open.get(id)?.handle;
1457
+ const onAbort = () => {
1458
+ if (record && open.get(id)?.handle === record) record.close(portSignal.reason);
1459
+ };
1460
+ portSignal.addEventListener("abort", onAbort, { once: true });
1461
+ getPortCloseSignal(handle.port)?.addEventListener("abort", () => portSignal.removeEventListener("abort", onAbort), { once: true });
1462
+ }
1012
1463
  return handle.port;
1013
1464
  },
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?.();
1465
+ close,
1466
+ ports() {
1467
+ return [...open.values()].map(({ handle: _, credit, ...info }) => credit ? {
1468
+ ...info,
1469
+ ...credit.enforced ? { window: credit.window } : {},
1470
+ queued: credit.queued,
1471
+ outstanding: credit.outstanding,
1472
+ unconsumed: credit.unconsumed
1473
+ } : info);
1027
1474
  }
1028
1475
  };
1029
1476
  }
1477
+ /** The byte length of a received message's data, or `undefined` if it is not bytes. */
1478
+ function byteLengthOf(data) {
1479
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) return data.byteLength;
1480
+ }
1481
+ /**
1482
+ * `"http: 1000, ws: 24"`: the three commonest `meta.kind`s among `entries`, so
1483
+ * a full table names who filled it. A port without a kind counts as `"?"`.
1484
+ */
1485
+ function describeKinds(entries) {
1486
+ const counts = /* @__PURE__ */ new Map();
1487
+ for (const { meta } of entries) {
1488
+ const kind = meta?.kind;
1489
+ const name = typeof kind === "string" ? kind : "?";
1490
+ counts.set(name, (counts.get(name) ?? 0) + 1);
1491
+ }
1492
+ if (!counts.size) return "none";
1493
+ return [...counts].sort((x, y) => y[1] - x[1]).slice(0, 3).map(([name, n]) => `${name}: ${n}`).join(", ");
1494
+ }
1030
1495
  //#endregion
1031
1496
  //#region src/connect-serve.ts
1032
1497
  /**
@@ -1201,7 +1666,7 @@ function isEnvelope(value) {
1201
1666
  const candidate = value;
1202
1667
  if (typeof candidate.id !== "number") return false;
1203
1668
  if (!Number.isInteger(candidate.id) || candidate.id < 0) return false;
1204
- return candidate.type === "open" || candidate.type === "message" || candidate.type === "close";
1669
+ return candidate.type === "open" || candidate.type === "message" || candidate.type === "close" || candidate.type === "credit" || candidate.type === "frame" || candidate.type === "ping" || candidate.type === "pong";
1205
1670
  }
1206
1671
  /**
1207
1672
  * For ports whose messages are structured values — a real `MessagePort`, a
@@ -1247,7 +1712,7 @@ const PORT_TRANSFER = "webrun-rpc:port-transfer";
1247
1712
  */
1248
1713
  function transferPortMux(target, options = {}) {
1249
1714
  const { onPort, maxMessageSize } = options;
1250
- const issued = /* @__PURE__ */ new Set();
1715
+ const issued = /* @__PURE__ */ new Map();
1251
1716
  let closed = false;
1252
1717
  const listener = (event) => {
1253
1718
  if (closed) return;
@@ -1266,28 +1731,47 @@ function transferPortMux(target, options = {}) {
1266
1731
  port.close();
1267
1732
  return;
1268
1733
  }
1269
- issued.add(port);
1734
+ const forget = () => issued.delete(port);
1735
+ issued.set(port, forget);
1736
+ const close = port.close.bind(port);
1737
+ port.close = () => {
1738
+ forget();
1739
+ close();
1740
+ };
1270
1741
  };
1271
1742
  target.addEventListener("message", listener);
1272
1743
  target.start?.();
1273
1744
  return {
1274
1745
  maxMessageSize,
1275
- async openPort(meta) {
1746
+ async openPort(meta, { signal } = {}) {
1276
1747
  if (closed) throw new Error("webrun-rpc: the multiplexer is closed");
1748
+ if (signal?.aborted) throw signal.reason;
1277
1749
  const channel = new MessageChannel();
1278
1750
  channel.port1.start();
1279
1751
  target.postMessage({
1280
1752
  type: PORT_TRANSFER,
1281
1753
  meta
1282
1754
  }, [channel.port2]);
1283
- issued.add(channel.port1);
1284
- return channel.port1;
1755
+ const port = channel.port1;
1756
+ const onAbort = () => port.close();
1757
+ signal?.addEventListener("abort", onAbort, { once: true });
1758
+ const forget = () => {
1759
+ signal?.removeEventListener("abort", onAbort);
1760
+ issued.delete(port);
1761
+ };
1762
+ issued.set(port, forget);
1763
+ const close = port.close.bind(port);
1764
+ port.close = () => {
1765
+ forget();
1766
+ close();
1767
+ };
1768
+ return port;
1285
1769
  },
1286
1770
  async close() {
1287
1771
  if (closed) return;
1288
1772
  closed = true;
1289
1773
  target.removeEventListener("message", listener);
1290
- for (const port of issued) try {
1774
+ for (const port of [...issued.keys()]) try {
1291
1775
  port.close();
1292
1776
  } catch {}
1293
1777
  issued.clear();
@@ -1296,4 +1780,4 @@ function transferPortMux(target, options = {}) {
1296
1780
  };
1297
1781
  }
1298
1782
  //#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 };
1783
+ 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, portBufferedAmount, postCancelChannel, recieve, send, serve, serveDuplexOverPort, setPortCloseSignal, structuredCodec, transferPortMux, whenPortReady };