@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/README.md +206 -12
- package/dist/connect-serve.d.ts +14 -1
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +539 -55
- 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 +11 -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 +140 -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 +11 -1
- package/src/index.ts +9 -1
- package/src/listen-port.ts +9 -2
- package/src/message-target.ts +14 -1
- package/src/multiplex-port.ts +610 -33
- package/src/port-credit.ts +61 -0
- package/src/port-types.ts +124 -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
|
|
@@ -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
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
947
|
-
|
|
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
|
-
|
|
985
|
-
|
|
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
|
-
|
|
1364
|
+
dropOutgoing(envelope.id);
|
|
1365
|
+
if (!entry) return;
|
|
989
1366
|
open.delete(envelope.id);
|
|
990
|
-
|
|
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 (
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1284
|
-
|
|
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 };
|