@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/README.md +237 -12
- package/dist/connect-serve.d.ts +16 -1
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +592 -64
- package/dist/listen-port.d.ts.map +1 -1
- package/dist/message-target.d.ts +14 -1
- package/dist/message-target.d.ts.map +1 -1
- package/dist/multiplex-port.d.ts +13 -2
- package/dist/multiplex-port.d.ts.map +1 -1
- package/dist/port-credit.d.ts +27 -0
- package/dist/port-credit.d.ts.map +1 -0
- package/dist/port-types.d.ts +153 -6
- package/dist/port-types.d.ts.map +1 -1
- package/dist/structured-codec.d.ts.map +1 -1
- package/dist/transfer-port-mux.d.ts.map +1 -1
- package/dist/virtual-port.d.ts +8 -2
- package/dist/virtual-port.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/connect-serve.ts +31 -10
- package/src/duplex-over-port.ts +8 -0
- package/src/index.ts +15 -2
- package/src/listen-port.ts +9 -2
- package/src/message-target.ts +14 -1
- package/src/multiplex-port.ts +682 -33
- package/src/port-credit.ts +61 -0
- package/src/port-types.ts +137 -8
- package/src/structured-codec.ts +9 -1
- package/src/transfer-port-mux.ts +32 -8
- package/src/virtual-port.ts +41 -11
package/dist/index.js
CHANGED
|
@@ -238,8 +238,11 @@ function listenPort(port, handler, { channelName = "", log = () => {} } = {}) {
|
|
|
238
238
|
error
|
|
239
239
|
});
|
|
240
240
|
};
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
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
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
947
|
-
|
|
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
|
|
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
|
-
|
|
985
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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) =>
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1284
|
-
|
|
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 };
|