@actuallab/rpc 12.3.33 → 12.3.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +419 -230
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -39
- package/dist/index.d.ts +128 -39
- package/dist/index.js +413 -226
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -33,12 +33,14 @@ __export(index_exports, {
|
|
|
33
33
|
ARG_DELIMITER: () => ARG_DELIMITER,
|
|
34
34
|
ENVELOPE_DELIMITER: () => ENVELOPE_DELIMITER,
|
|
35
35
|
FRAME_DELIMITER: () => FRAME_DELIMITER,
|
|
36
|
+
HANDSHAKE_TIMEOUT_MS: () => HANDSHAKE_TIMEOUT_MS,
|
|
36
37
|
IncreasingSeqCompressor: () => IncreasingSeqCompressor,
|
|
37
38
|
RPC_CLOSE_CODE_UNSUPPORTED_FORMAT: () => RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
|
|
38
39
|
RpcCallStage: () => RpcCallStage,
|
|
39
40
|
RpcCallTypeId: () => RpcCallTypeId,
|
|
40
41
|
RpcClientPeer: () => RpcClientPeer,
|
|
41
42
|
RpcClientPeerReconnectDelayer: () => RpcClientPeerReconnectDelayer,
|
|
43
|
+
RpcConnectionState: () => RpcConnectionState,
|
|
42
44
|
RpcHub: () => RpcHub,
|
|
43
45
|
RpcInboundCall: () => RpcInboundCall,
|
|
44
46
|
RpcInboundCallTracker: () => RpcInboundCallTracker,
|
|
@@ -51,7 +53,7 @@ __export(index_exports, {
|
|
|
51
53
|
RpcOutboundCall: () => RpcOutboundCall,
|
|
52
54
|
RpcOutboundCallTracker: () => RpcOutboundCallTracker,
|
|
53
55
|
RpcPeer: () => RpcPeer,
|
|
54
|
-
|
|
56
|
+
RpcPeerRefBuilder: () => RpcPeerRefBuilder,
|
|
55
57
|
RpcPeerStateKind: () => RpcPeerStateKind,
|
|
56
58
|
RpcPeerStateMonitor: () => RpcPeerStateMonitor,
|
|
57
59
|
RpcRemoteExecutionMode: () => RpcRemoteExecutionMode,
|
|
@@ -1058,7 +1060,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
1058
1060
|
if (conn) {
|
|
1059
1061
|
this.peer.hub.systemCallSender.item(
|
|
1060
1062
|
conn,
|
|
1061
|
-
this.peer.
|
|
1063
|
+
this.peer.serializationFormat,
|
|
1062
1064
|
this.id.localId,
|
|
1063
1065
|
this._nextIndex,
|
|
1064
1066
|
item
|
|
@@ -1073,7 +1075,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
1073
1075
|
if (conn) {
|
|
1074
1076
|
this.peer.hub.systemCallSender.batch(
|
|
1075
1077
|
conn,
|
|
1076
|
-
this.peer.
|
|
1078
|
+
this.peer.serializationFormat,
|
|
1077
1079
|
this.id.localId,
|
|
1078
1080
|
this._nextIndex,
|
|
1079
1081
|
items
|
|
@@ -1090,7 +1092,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
1090
1092
|
const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
|
|
1091
1093
|
this.peer.hub.systemCallSender.end(
|
|
1092
1094
|
conn,
|
|
1093
|
-
this.peer.
|
|
1095
|
+
this.peer.serializationFormat,
|
|
1094
1096
|
this.id.localId,
|
|
1095
1097
|
this._nextIndex,
|
|
1096
1098
|
errorInfo
|
|
@@ -1545,7 +1547,7 @@ var RpcStream = class _RpcStream {
|
|
|
1545
1547
|
this._sender = sender;
|
|
1546
1548
|
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
1547
1549
|
this._whenSent = sender.writeFrom(source);
|
|
1548
|
-
this._toRefValue = peer.
|
|
1550
|
+
this._toRefValue = peer.serializationFormat.isBinary ? {
|
|
1549
1551
|
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
1550
1552
|
AckPeriod: sender.ackPeriod,
|
|
1551
1553
|
AckAdvance: sender.ackAdvance,
|
|
@@ -1695,7 +1697,7 @@ var RpcStream = class _RpcStream {
|
|
|
1695
1697
|
const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
|
|
1696
1698
|
this.peer.hub.systemCallSender.ack(
|
|
1697
1699
|
conn,
|
|
1698
|
-
this.peer.
|
|
1700
|
+
this.peer.serializationFormat,
|
|
1699
1701
|
this.id.localId,
|
|
1700
1702
|
nextIndex,
|
|
1701
1703
|
hostId
|
|
@@ -1707,7 +1709,7 @@ var RpcStream = class _RpcStream {
|
|
|
1707
1709
|
if (conn) {
|
|
1708
1710
|
this.peer.hub.systemCallSender.ackEnd(
|
|
1709
1711
|
conn,
|
|
1710
|
-
this.peer.
|
|
1712
|
+
this.peer.serializationFormat,
|
|
1711
1713
|
this.id.localId,
|
|
1712
1714
|
this.id.hostId
|
|
1713
1715
|
);
|
|
@@ -1754,9 +1756,8 @@ function base64Encode(bytes) {
|
|
|
1754
1756
|
const chunkSize = 32768;
|
|
1755
1757
|
let binary = "";
|
|
1756
1758
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
1757
|
-
binary += String.fromCharCode
|
|
1758
|
-
|
|
1759
|
-
bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
|
|
1759
|
+
binary += String.fromCharCode(
|
|
1760
|
+
...bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
|
|
1760
1761
|
);
|
|
1761
1762
|
}
|
|
1762
1763
|
return btoa(binary);
|
|
@@ -1777,11 +1778,11 @@ var RpcSystemCallHandler = class {
|
|
|
1777
1778
|
debugLog?.log(`'${peer.ref}': handle ${method}#${relatedId}`);
|
|
1778
1779
|
switch (method) {
|
|
1779
1780
|
case RpcSystemCalls.ok: {
|
|
1780
|
-
const call = peer.
|
|
1781
|
+
const call = peer.outboundCalls.get(relatedId);
|
|
1781
1782
|
if (call !== void 0) {
|
|
1782
1783
|
call.completedStage |= RpcCallStage.ResultReady;
|
|
1783
1784
|
if (call.removeOnOk) {
|
|
1784
|
-
peer.
|
|
1785
|
+
peer.outboundCalls.remove(relatedId);
|
|
1785
1786
|
}
|
|
1786
1787
|
call.result.resolve(args[0]);
|
|
1787
1788
|
}
|
|
@@ -1792,7 +1793,7 @@ var RpcSystemCallHandler = class {
|
|
|
1792
1793
|
break;
|
|
1793
1794
|
}
|
|
1794
1795
|
case RpcSystemCalls.error: {
|
|
1795
|
-
const call = peer.
|
|
1796
|
+
const call = peer.outboundCalls.remove(relatedId);
|
|
1796
1797
|
if (call !== void 0) {
|
|
1797
1798
|
const errorInfo = args[0];
|
|
1798
1799
|
const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
|
|
@@ -1807,10 +1808,11 @@ var RpcSystemCallHandler = class {
|
|
|
1807
1808
|
break;
|
|
1808
1809
|
}
|
|
1809
1810
|
case RpcSystemCalls.cancel: {
|
|
1810
|
-
peer.
|
|
1811
|
+
peer.inboundCalls.remove(relatedId);
|
|
1811
1812
|
break;
|
|
1812
1813
|
}
|
|
1813
1814
|
case RpcSystemCalls.keepAlive: {
|
|
1815
|
+
peer.notifyKeepAliveReceived();
|
|
1814
1816
|
break;
|
|
1815
1817
|
}
|
|
1816
1818
|
case RpcSystemCalls.item: {
|
|
@@ -1901,7 +1903,7 @@ var RpcSystemCallHandler = class {
|
|
|
1901
1903
|
if (bytes === null) continue;
|
|
1902
1904
|
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
1903
1905
|
for (const callId of callIds) {
|
|
1904
|
-
if (peer.
|
|
1906
|
+
if (peer.inboundCalls.get(callId) === void 0) {
|
|
1905
1907
|
unknownSet.add(callId);
|
|
1906
1908
|
}
|
|
1907
1909
|
}
|
|
@@ -1911,8 +1913,8 @@ var RpcSystemCallHandler = class {
|
|
|
1911
1913
|
} catch {
|
|
1912
1914
|
}
|
|
1913
1915
|
const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
|
|
1914
|
-
const responseValue = peer.
|
|
1915
|
-
peer.hub.systemCallSender.ok(peer.connection, peer.
|
|
1916
|
+
const responseValue = peer.serializationFormat.isBinary ? responseBytes : base64Encode(responseBytes);
|
|
1917
|
+
peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
|
|
1916
1918
|
}
|
|
1917
1919
|
};
|
|
1918
1920
|
function _toBytes(value) {
|
|
@@ -2107,16 +2109,7 @@ var RpcSharedObjectTracker = class {
|
|
|
2107
2109
|
};
|
|
2108
2110
|
|
|
2109
2111
|
// src/rpc-peer.ts
|
|
2110
|
-
var import_core8 = require("@actuallab/core");
|
|
2111
|
-
|
|
2112
|
-
// src/rpc-client-peer-reconnect-delayer.ts
|
|
2113
2112
|
var import_core7 = require("@actuallab/core");
|
|
2114
|
-
var RpcClientPeerReconnectDelayer = class extends import_core7.RetryDelayer {
|
|
2115
|
-
constructor() {
|
|
2116
|
-
super();
|
|
2117
|
-
this.delays = import_core7.RetryDelaySeq.exp(1e3, 6e4);
|
|
2118
|
-
}
|
|
2119
|
-
};
|
|
2120
2113
|
|
|
2121
2114
|
// src/rpc-serialization-format.ts
|
|
2122
2115
|
var import_msgpack4 = require("@msgpack/msgpack");
|
|
@@ -2277,55 +2270,106 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
|
|
|
2277
2270
|
|
|
2278
2271
|
// src/rpc-peer.ts
|
|
2279
2272
|
var RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
|
|
2273
|
+
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
2274
|
+
var KEEP_ALIVE_PERIOD_MS = 15e3;
|
|
2275
|
+
var KEEP_ALIVE_TIMEOUT_MS = 55e3;
|
|
2280
2276
|
var { debugLog: debugLog2, infoLog, warnLog: warnLog6, errorLog: errorLog2 } = getLogs("RpcPeer");
|
|
2281
2277
|
var defaultConnectionUrlResolver = (peer) => {
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2278
|
+
const formatKey = peer.serializationFormat.key;
|
|
2279
|
+
try {
|
|
2280
|
+
const url = new URL(peer.ref);
|
|
2281
|
+
url.searchParams.set("clientId", peer.clientId);
|
|
2282
|
+
url.searchParams.set("f", formatKey);
|
|
2283
|
+
return url.toString();
|
|
2284
|
+
} catch {
|
|
2285
|
+
const sep = peer.ref.includes("?") ? "&" : "?";
|
|
2286
|
+
return peer.ref + sep + `clientId=${peer.clientId}&f=${formatKey}`;
|
|
2287
|
+
}
|
|
2284
2288
|
};
|
|
2289
|
+
function parseFormatFromUrl(url) {
|
|
2290
|
+
try {
|
|
2291
|
+
const f = new URL(url).searchParams.get("f");
|
|
2292
|
+
return f ?? void 0;
|
|
2293
|
+
} catch {
|
|
2294
|
+
return void 0;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2285
2297
|
var RpcPeer = class {
|
|
2298
|
+
hub;
|
|
2286
2299
|
/** Routing/addressing key — used as key in hub.peers (URL for clients, "server://{uuid}" for servers). */
|
|
2287
2300
|
ref;
|
|
2288
2301
|
/** Auto-generated GUID — used in handshakes only. */
|
|
2289
2302
|
id = crypto.randomUUID();
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
outbound = new RpcOutboundCallTracker();
|
|
2293
|
-
inbound = new RpcInboundCallTracker();
|
|
2303
|
+
outboundCalls = new RpcOutboundCallTracker();
|
|
2304
|
+
inboundCalls = new RpcInboundCallTracker();
|
|
2294
2305
|
remoteObjects = new RpcRemoteObjectTracker();
|
|
2295
2306
|
sharedObjects = new RpcSharedObjectTracker();
|
|
2296
|
-
|
|
2297
|
-
|
|
2307
|
+
/** Fired on every state transition with the new value. Initial state is
|
|
2308
|
+
* `Disconnected`; `Connecting` is entered when the peer starts an
|
|
2309
|
+
* attempt (the reconnect delay is part of this phase); `Connected` is
|
|
2310
|
+
* entered only after the handshake has succeeded. */
|
|
2311
|
+
connectionStateChanged = new import_core7.EventHandlerSet();
|
|
2312
|
+
_connection;
|
|
2313
|
+
_connectionState = 0 /* Disconnected */;
|
|
2298
2314
|
_pendingSends = [];
|
|
2299
2315
|
_keepAliveTimer;
|
|
2300
|
-
|
|
2316
|
+
_keepAliveWatchdog;
|
|
2317
|
+
/** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
|
|
2318
|
+
* for the current connection. Useful for diagnostics. */
|
|
2319
|
+
_lastKeepAliveAt = 0;
|
|
2320
|
+
/** Outbound keep-alive send period (per-peer override for tests). */
|
|
2321
|
+
keepAlivePeriodMs = KEEP_ALIVE_PERIOD_MS;
|
|
2322
|
+
/** Inbound keep-alive silence tolerance (per-peer override for tests). */
|
|
2323
|
+
keepAliveTimeoutMs = KEEP_ALIVE_TIMEOUT_MS;
|
|
2324
|
+
constructor(hub, ref) {
|
|
2325
|
+
this.hub = hub;
|
|
2301
2326
|
this.ref = ref;
|
|
2302
|
-
this._hub = hub;
|
|
2303
|
-
}
|
|
2304
|
-
get hub() {
|
|
2305
|
-
return this._hub;
|
|
2306
2327
|
}
|
|
2307
2328
|
get connection() {
|
|
2308
2329
|
return this._connection;
|
|
2309
2330
|
}
|
|
2331
|
+
get connectionState() {
|
|
2332
|
+
return this._connectionState;
|
|
2333
|
+
}
|
|
2310
2334
|
get isConnected() {
|
|
2311
2335
|
return this._connection?.isOpen ?? false;
|
|
2312
2336
|
}
|
|
2337
|
+
_setConnectionState(value) {
|
|
2338
|
+
if (this._connectionState === value) return;
|
|
2339
|
+
this._connectionState = value;
|
|
2340
|
+
if (value === 2 /* Connected */)
|
|
2341
|
+
this._armKeepAliveWatchdog();
|
|
2342
|
+
else if (value === 0 /* Disconnected */)
|
|
2343
|
+
this._disarmKeepAliveWatchdog();
|
|
2344
|
+
this.connectionStateChanged.trigger(value);
|
|
2345
|
+
}
|
|
2346
|
+
/** Resolves on the next transition into `Connected` — or immediately if
|
|
2347
|
+
* already in that state. */
|
|
2348
|
+
whenConnected() {
|
|
2349
|
+
if (this._connectionState === 2 /* Connected */)
|
|
2350
|
+
return Promise.resolve();
|
|
2351
|
+
return new Promise((resolve) => {
|
|
2352
|
+
const handler = (state) => {
|
|
2353
|
+
if (state !== 2 /* Connected */) return;
|
|
2354
|
+
this.connectionStateChanged.remove(handler);
|
|
2355
|
+
resolve();
|
|
2356
|
+
};
|
|
2357
|
+
this.connectionStateChanged.add(handler);
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2313
2360
|
setupConnection(conn) {
|
|
2314
2361
|
this._connection = conn;
|
|
2315
2362
|
conn.messageReceived.add((raw) => this._handleMessage(raw));
|
|
2316
2363
|
conn.closed.add((ev) => {
|
|
2317
2364
|
this._connection = void 0;
|
|
2318
|
-
this._stopKeepAlive();
|
|
2319
2365
|
if (ev.reason)
|
|
2320
2366
|
infoLog?.log(`'${this.ref}': Disconnected: ${ev.reason} (code=${ev.code})`);
|
|
2321
2367
|
else
|
|
2322
2368
|
infoLog?.log(`'${this.ref}': Disconnected`);
|
|
2323
|
-
this.
|
|
2369
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2324
2370
|
});
|
|
2325
2371
|
void conn.whenConnected.then(() => {
|
|
2326
|
-
infoLog?.log(`'${this.ref}':
|
|
2327
|
-
this.connected.trigger();
|
|
2328
|
-
this._startKeepAlive();
|
|
2372
|
+
infoLog?.log(`'${this.ref}': WS open`);
|
|
2329
2373
|
});
|
|
2330
2374
|
}
|
|
2331
2375
|
/**
|
|
@@ -2333,29 +2377,29 @@ var RpcPeer = class {
|
|
|
2333
2377
|
* If not set explicitly, defaults to the resolver's default (JSON /
|
|
2334
2378
|
* json5np). Subclasses can pre-assign in their constructor.
|
|
2335
2379
|
*/
|
|
2336
|
-
|
|
2337
|
-
get
|
|
2338
|
-
return this.
|
|
2380
|
+
_serializationFormat;
|
|
2381
|
+
get serializationFormat() {
|
|
2382
|
+
return this._serializationFormat ?? RpcSerializationFormatResolver.Default.defaultFormat;
|
|
2339
2383
|
}
|
|
2340
|
-
set
|
|
2341
|
-
this.
|
|
2384
|
+
set serializationFormat(value) {
|
|
2385
|
+
this._serializationFormat = value;
|
|
2342
2386
|
}
|
|
2343
2387
|
call(method, args, options) {
|
|
2344
|
-
const callId = this.
|
|
2388
|
+
const callId = this.outboundCalls.nextId();
|
|
2345
2389
|
const outboundCall = options?.outboundCallFactory ? options.outboundCallFactory(callId, method) : new RpcOutboundCall(callId, method, options?.remoteExecutionMode ?? 7);
|
|
2346
2390
|
const envelope = {
|
|
2347
2391
|
Method: method,
|
|
2348
2392
|
RelatedId: callId,
|
|
2349
2393
|
CallType: options?.callTypeId ?? 0
|
|
2350
2394
|
};
|
|
2351
|
-
outboundCall.serializedWireData = this.
|
|
2395
|
+
outboundCall.serializedWireData = this.serializationFormat.serializeMessage(
|
|
2352
2396
|
envelope,
|
|
2353
2397
|
args,
|
|
2354
2398
|
this._connection?.encoder,
|
|
2355
|
-
this.
|
|
2399
|
+
this.hub.registry
|
|
2356
2400
|
);
|
|
2357
2401
|
if (this._connection !== void 0) {
|
|
2358
|
-
this.
|
|
2402
|
+
this.outboundCalls.register(outboundCall);
|
|
2359
2403
|
this._sendWireData(outboundCall.serializedWireData);
|
|
2360
2404
|
} else {
|
|
2361
2405
|
this._pendingSends.push(outboundCall);
|
|
@@ -2363,11 +2407,11 @@ var RpcPeer = class {
|
|
|
2363
2407
|
const signal = options?.signal;
|
|
2364
2408
|
if (signal !== void 0) {
|
|
2365
2409
|
const onAbort = () => {
|
|
2366
|
-
if (this.
|
|
2410
|
+
if (this.outboundCalls.remove(callId) !== void 0) {
|
|
2367
2411
|
outboundCall.result.reject(new Error("Call cancelled."));
|
|
2368
2412
|
outboundCall.onDisconnect();
|
|
2369
2413
|
if (this._connection !== void 0)
|
|
2370
|
-
this.
|
|
2414
|
+
this.hub.systemCallSender.cancel(this._connection, this.serializationFormat, callId);
|
|
2371
2415
|
} else {
|
|
2372
2416
|
const idx = this._pendingSends.findIndex(
|
|
2373
2417
|
(c) => c.callId === callId
|
|
@@ -2389,17 +2433,16 @@ var RpcPeer = class {
|
|
|
2389
2433
|
callNoWait(method, args) {
|
|
2390
2434
|
if (this._connection === void 0) return;
|
|
2391
2435
|
const envelope = { Method: method, RelatedId: 0 };
|
|
2392
|
-
const wireData = this.
|
|
2436
|
+
const wireData = this.serializationFormat.serializeMessage(
|
|
2393
2437
|
envelope,
|
|
2394
2438
|
args,
|
|
2395
2439
|
this._connection.encoder,
|
|
2396
|
-
this.
|
|
2440
|
+
this.hub.registry
|
|
2397
2441
|
);
|
|
2398
2442
|
this._sendWireData(wireData);
|
|
2399
2443
|
}
|
|
2400
2444
|
close() {
|
|
2401
2445
|
infoLog?.log(`'${this.ref}': Stopping`);
|
|
2402
|
-
this._stopKeepAlive();
|
|
2403
2446
|
this.remoteObjects.disconnectAll();
|
|
2404
2447
|
this.sharedObjects.disconnectAll();
|
|
2405
2448
|
for (const call of this._pendingSends) {
|
|
@@ -2408,10 +2451,11 @@ var RpcPeer = class {
|
|
|
2408
2451
|
call.onDisconnect();
|
|
2409
2452
|
}
|
|
2410
2453
|
this._pendingSends.length = 0;
|
|
2411
|
-
this.
|
|
2412
|
-
this.
|
|
2454
|
+
this.outboundCalls.rejectAll(new Error("Peer closed."));
|
|
2455
|
+
this.outboundCalls.invalidateAll();
|
|
2413
2456
|
this._connection?.close();
|
|
2414
|
-
this.
|
|
2457
|
+
this.hub.peers.delete(this.ref);
|
|
2458
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2415
2459
|
infoLog?.log(`'${this.ref}': Stopped`);
|
|
2416
2460
|
}
|
|
2417
2461
|
/** Send any messages buffered while disconnected. Call after connection + handshake are ready. */
|
|
@@ -2419,7 +2463,7 @@ var RpcPeer = class {
|
|
|
2419
2463
|
if (this._pendingSends.length === 0 || this._connection === void 0)
|
|
2420
2464
|
return;
|
|
2421
2465
|
for (const call of this._pendingSends) {
|
|
2422
|
-
this.
|
|
2466
|
+
this.outboundCalls.register(call);
|
|
2423
2467
|
this._sendWireData(call.serializedWireData);
|
|
2424
2468
|
}
|
|
2425
2469
|
this._pendingSends.length = 0;
|
|
@@ -2468,7 +2512,7 @@ var RpcPeer = class {
|
|
|
2468
2512
|
return;
|
|
2469
2513
|
}
|
|
2470
2514
|
if (method.startsWith("$sys")) {
|
|
2471
|
-
this.
|
|
2515
|
+
this.hub.systemCallHandler.handle(message, args, this);
|
|
2472
2516
|
return;
|
|
2473
2517
|
}
|
|
2474
2518
|
this._handleInbound(message, args);
|
|
@@ -2477,12 +2521,12 @@ var RpcPeer = class {
|
|
|
2477
2521
|
const method = message.Method ?? "";
|
|
2478
2522
|
const relatedId = message.RelatedId ?? 0;
|
|
2479
2523
|
const call = new RpcInboundCall(relatedId, method, args);
|
|
2480
|
-
const methodDef = this.
|
|
2524
|
+
const methodDef = this.hub.serviceHost.getMethodDef(method);
|
|
2481
2525
|
const isNoWait = methodDef?.noWait === true;
|
|
2482
2526
|
if (!isNoWait) {
|
|
2483
|
-
this.
|
|
2527
|
+
this.inboundCalls.register(call);
|
|
2484
2528
|
}
|
|
2485
|
-
const serviceHost = this.
|
|
2529
|
+
const serviceHost = this.hub.serviceHost;
|
|
2486
2530
|
void (async () => {
|
|
2487
2531
|
try {
|
|
2488
2532
|
const context = this._connection !== void 0 ? {
|
|
@@ -2497,25 +2541,25 @@ var RpcPeer = class {
|
|
|
2497
2541
|
);
|
|
2498
2542
|
if (methodDef?.stream === true && this._connection !== void 0) {
|
|
2499
2543
|
const stream = result instanceof RpcStream ? result : new RpcStream(result);
|
|
2500
|
-
this.
|
|
2544
|
+
this.hub.systemCallSender.ok(
|
|
2501
2545
|
this._connection,
|
|
2502
|
-
this.
|
|
2546
|
+
this.serializationFormat,
|
|
2503
2547
|
relatedId,
|
|
2504
2548
|
stream.toRef(this)
|
|
2505
2549
|
);
|
|
2506
2550
|
} else if (!isNoWait && this._connection !== void 0) {
|
|
2507
|
-
this.
|
|
2551
|
+
this.hub.systemCallSender.ok(
|
|
2508
2552
|
this._connection,
|
|
2509
|
-
this.
|
|
2553
|
+
this.serializationFormat,
|
|
2510
2554
|
relatedId,
|
|
2511
2555
|
result
|
|
2512
2556
|
);
|
|
2513
2557
|
}
|
|
2514
2558
|
} catch (e) {
|
|
2515
2559
|
if (!isNoWait && this._connection !== void 0) {
|
|
2516
|
-
this.
|
|
2560
|
+
this.hub.systemCallSender.error(
|
|
2517
2561
|
this._connection,
|
|
2518
|
-
this.
|
|
2562
|
+
this.serializationFormat,
|
|
2519
2563
|
relatedId,
|
|
2520
2564
|
e
|
|
2521
2565
|
);
|
|
@@ -2523,36 +2567,64 @@ var RpcPeer = class {
|
|
|
2523
2567
|
debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
|
|
2524
2568
|
} finally {
|
|
2525
2569
|
if (!isNoWait) {
|
|
2526
|
-
this.
|
|
2570
|
+
this.inboundCalls.remove(relatedId);
|
|
2527
2571
|
}
|
|
2528
2572
|
}
|
|
2529
2573
|
})();
|
|
2530
2574
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2575
|
+
/** Start sending `$sys.KeepAlive` pings and arm the silence watchdog.
|
|
2576
|
+
* Called when the peer transitions into `Connected` (after handshake).
|
|
2577
|
+
* Each inbound `$sys.KeepAlive` re-arms the watchdog; if it fires, the
|
|
2578
|
+
* WebSocket is force-closed so the normal disconnect path runs
|
|
2579
|
+
* (`conn.closed` → `connectionState = Disconnected` → UI).
|
|
2580
|
+
* Mirrors .NET `RpcObjectTrackers.Maintain`'s `KeepAliveTimeout` branch. */
|
|
2581
|
+
_armKeepAliveWatchdog() {
|
|
2582
|
+
this._keepAliveTimer ??= setInterval(() => {
|
|
2533
2583
|
if (this._connection !== void 0) {
|
|
2534
|
-
this.
|
|
2584
|
+
this.hub.systemCallSender.keepAlive(this._connection, this.serializationFormat, [
|
|
2535
2585
|
...this.remoteObjects.keys()
|
|
2536
2586
|
]);
|
|
2537
2587
|
}
|
|
2538
|
-
},
|
|
2588
|
+
}, this.keepAlivePeriodMs);
|
|
2589
|
+
if (this._keepAliveWatchdog !== void 0)
|
|
2590
|
+
clearTimeout(this._keepAliveWatchdog);
|
|
2591
|
+
this._keepAliveWatchdog = setTimeout(() => {
|
|
2592
|
+
this._keepAliveWatchdog = void 0;
|
|
2593
|
+
if (this._connection === void 0) return;
|
|
2594
|
+
warnLog6?.log(
|
|
2595
|
+
`'${this.ref}': no keep-alive for ${this.keepAliveTimeoutMs}ms \u2014 force-closing connection`
|
|
2596
|
+
);
|
|
2597
|
+
this._connection.close();
|
|
2598
|
+
}, this.keepAliveTimeoutMs);
|
|
2539
2599
|
}
|
|
2540
|
-
|
|
2600
|
+
/** Stop sending keep-alives and clear the silence watchdog. Called when
|
|
2601
|
+
* the peer transitions into `Disconnected`. */
|
|
2602
|
+
_disarmKeepAliveWatchdog() {
|
|
2541
2603
|
if (this._keepAliveTimer !== void 0) {
|
|
2542
2604
|
clearInterval(this._keepAliveTimer);
|
|
2543
2605
|
this._keepAliveTimer = void 0;
|
|
2544
2606
|
}
|
|
2607
|
+
if (this._keepAliveWatchdog !== void 0) {
|
|
2608
|
+
clearTimeout(this._keepAliveWatchdog);
|
|
2609
|
+
this._keepAliveWatchdog = void 0;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
/** Called by `RpcSystemCallHandler` when a `$sys.KeepAlive` message
|
|
2613
|
+
* arrives — records the timestamp and re-arms the silence watchdog. */
|
|
2614
|
+
notifyKeepAliveReceived() {
|
|
2615
|
+
this._lastKeepAliveAt = Date.now();
|
|
2616
|
+
if (this._keepAliveWatchdog !== void 0)
|
|
2617
|
+
this._armKeepAliveWatchdog();
|
|
2545
2618
|
}
|
|
2546
2619
|
};
|
|
2547
|
-
var
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
return
|
|
2552
|
-
})(
|
|
2620
|
+
var RpcConnectionState = /* @__PURE__ */ ((RpcConnectionState2) => {
|
|
2621
|
+
RpcConnectionState2[RpcConnectionState2["Disconnected"] = 0] = "Disconnected";
|
|
2622
|
+
RpcConnectionState2[RpcConnectionState2["Connecting"] = 1] = "Connecting";
|
|
2623
|
+
RpcConnectionState2[RpcConnectionState2["Connected"] = 2] = "Connected";
|
|
2624
|
+
return RpcConnectionState2;
|
|
2625
|
+
})(RpcConnectionState || {});
|
|
2553
2626
|
var RpcClientPeer = class extends RpcPeer {
|
|
2554
2627
|
_disposed = false;
|
|
2555
|
-
_connectionKind = 0 /* Disconnected */;
|
|
2556
2628
|
_tryIndex = 0;
|
|
2557
2629
|
_handshakeIndex = 0;
|
|
2558
2630
|
/** Index from the MOST RECENTLY RECEIVED server handshake — sent back
|
|
@@ -2566,19 +2638,21 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2566
2638
|
_reconnectsAt = 0;
|
|
2567
2639
|
/** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
|
|
2568
2640
|
clientId;
|
|
2569
|
-
/**
|
|
2570
|
-
* Serialization format key sent to the server via `f=` query parameter.
|
|
2571
|
-
* TS uses plain JSON.stringify (no polymorphic type wrapping), so `json5np`
|
|
2572
|
-
* (no-polymorphism) is the correct default.
|
|
2573
|
-
*/
|
|
2574
|
-
serializationFormat;
|
|
2575
2641
|
/** Builds the WebSocket URL for this peer. Replace to customize URL construction. */
|
|
2576
2642
|
connectionUrlResolver = defaultConnectionUrlResolver;
|
|
2577
|
-
|
|
2643
|
+
/** Optional WebSocket factory. When set, `run()` uses this instead of the
|
|
2644
|
+
* global `new WebSocket(url)` constructor. Used by tests (to inject mock
|
|
2645
|
+
* sockets) and by the Node load-test harness (to attach `Session` /
|
|
2646
|
+
* `Cookie` headers to the WS upgrade, which the browser `WebSocket`
|
|
2647
|
+
* constructor can't do). Must be set before `start()`. */
|
|
2648
|
+
webSocketFactory;
|
|
2649
|
+
/** Max time to wait for the server's handshake response before aborting
|
|
2650
|
+
* the connection attempt. Mirrors .NET's `Hub.Limits.HandshakeTimeout`. */
|
|
2651
|
+
handshakeTimeoutMs = HANDSHAKE_TIMEOUT_MS;
|
|
2652
|
+
peerChanged = new import_core7.EventHandlerSet();
|
|
2578
2653
|
/** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
|
|
2579
|
-
unsupportedFormat = new
|
|
2580
|
-
|
|
2581
|
-
reconnectsAtChanged = new import_core8.EventHandlerSet();
|
|
2654
|
+
unsupportedFormat = new import_core7.EventHandlerSet();
|
|
2655
|
+
reconnectsAtChanged = new import_core7.EventHandlerSet();
|
|
2582
2656
|
get reconnectsAt() {
|
|
2583
2657
|
return this._reconnectsAt;
|
|
2584
2658
|
}
|
|
@@ -2587,17 +2661,32 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2587
2661
|
this._reconnectsAt = value;
|
|
2588
2662
|
this.reconnectsAtChanged.trigger();
|
|
2589
2663
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2664
|
+
/** The promise returned by the reconnect loop. `undefined` until
|
|
2665
|
+
* `start()` is invoked (which happens automatically when
|
|
2666
|
+
* `mustStart = true` is passed to the ctor). */
|
|
2667
|
+
whenRunning;
|
|
2668
|
+
/**
|
|
2669
|
+
* @param hub RpcHub instance this peer will be tracked by
|
|
2670
|
+
* @param url Full WebSocket URL — the serialization format is taken from
|
|
2671
|
+
* the `f=` query parameter (e.g. `wss://host/rpc/ws?f=msgpack6`);
|
|
2672
|
+
* falls back to the resolver's default when absent. Use
|
|
2673
|
+
* {@link RpcPeerRefBuilder.forClient} to compose URLs with a format.
|
|
2674
|
+
* @param mustStart Kick off the reconnect loop automatically at the end of
|
|
2675
|
+
* construction (default `true`). Pass `false` if you need to
|
|
2676
|
+
* tweak `reconnectDelayer`, `handshakeTimeoutMs`, or similar
|
|
2677
|
+
* options before the loop starts — then call `peer.start()`
|
|
2678
|
+
* manually.
|
|
2679
|
+
*/
|
|
2680
|
+
constructor(hub, url, mustStart = true) {
|
|
2681
|
+
super(hub, url);
|
|
2592
2682
|
this.clientId = guidToBase64Url(this.id);
|
|
2593
2683
|
const resolver = RpcSerializationFormatResolver.Default;
|
|
2594
|
-
this.serializationFormat =
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
get connectionKind() {
|
|
2598
|
-
return this._connectionKind;
|
|
2684
|
+
this.serializationFormat = resolver.get(parseFormatFromUrl(url) ?? resolver.defaultFormatKey);
|
|
2685
|
+
if (mustStart)
|
|
2686
|
+
this.start();
|
|
2599
2687
|
}
|
|
2600
2688
|
/** One-shot connection for tests — no reconnection loop, no handshake.
|
|
2689
|
+
* @param conn Rpc connection
|
|
2601
2690
|
* @param isPeerChanged Whether to treat this as a peer-identity change
|
|
2602
2691
|
* (default `true` — no handshake was exchanged, so stage-3 compute calls
|
|
2603
2692
|
* self-invalidate and remote/shared objects are disposed). Pass `false`
|
|
@@ -2606,97 +2695,137 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2606
2695
|
this.setupConnection(conn);
|
|
2607
2696
|
void this._reconnect(isPeerChanged);
|
|
2608
2697
|
}
|
|
2609
|
-
/**
|
|
2610
|
-
|
|
2698
|
+
/** Kick off the reconnect loop. Idempotent — subsequent calls are
|
|
2699
|
+
* no-ops; await `peer.whenRunning` if you need the loop's completion
|
|
2700
|
+
* promise. Customize `peer.webSocketFactory` first if you need a
|
|
2701
|
+
* non-default WebSocket constructor. */
|
|
2702
|
+
start() {
|
|
2703
|
+
void this.run();
|
|
2704
|
+
}
|
|
2705
|
+
/** The reconnect loop itself. Exposed as `protected` so subclasses (and
|
|
2706
|
+
* `start()`) can drive it; public callers should use {@link start}. */
|
|
2707
|
+
run() {
|
|
2708
|
+
return this.whenRunning ??= this._runImpl();
|
|
2709
|
+
}
|
|
2710
|
+
async _runImpl() {
|
|
2711
|
+
if (this._disposed) return;
|
|
2611
2712
|
const connUrl = this.connectionUrlResolver(this);
|
|
2612
2713
|
infoLog?.log(`'${this.ref}': Started (Client)`);
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
this.setupConnection(conn);
|
|
2632
|
-
this._connection = void 0;
|
|
2633
|
-
const closedRejection = new Promise(
|
|
2634
|
-
(_, reject) => conn.closed.add(
|
|
2635
|
-
() => reject(new Error("Connection failed"))
|
|
2636
|
-
)
|
|
2637
|
-
);
|
|
2638
|
-
closedRejection.catch(() => {
|
|
2639
|
-
});
|
|
2640
|
-
await Promise.race([conn.whenConnected, closedRejection]);
|
|
2714
|
+
try {
|
|
2715
|
+
while (!this._disposed) {
|
|
2716
|
+
this._setConnectionState(1 /* Connecting */);
|
|
2717
|
+
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex);
|
|
2718
|
+
if (retryDelay.isLimitExceeded) {
|
|
2719
|
+
warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
2720
|
+
this._disposed = true;
|
|
2721
|
+
break;
|
|
2722
|
+
}
|
|
2723
|
+
this._setReconnectsAt(retryDelay.endsAt);
|
|
2724
|
+
try {
|
|
2725
|
+
await retryDelay.promise;
|
|
2726
|
+
} finally {
|
|
2727
|
+
this._setReconnectsAt(0);
|
|
2728
|
+
}
|
|
2729
|
+
if (this._disposed) break;
|
|
2730
|
+
let lastCloseCode = 0;
|
|
2731
|
+
let lastCloseReason = "";
|
|
2641
2732
|
try {
|
|
2642
|
-
this.
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
this.
|
|
2647
|
-
|
|
2733
|
+
infoLog?.log(`'${this.ref}': Connecting to ${connUrl}`);
|
|
2734
|
+
const ws = this.webSocketFactory?.(connUrl) ?? new WebSocket(connUrl);
|
|
2735
|
+
const conn = new RpcWebSocketConnection(
|
|
2736
|
+
ws,
|
|
2737
|
+
this.serializationFormat.isBinary,
|
|
2738
|
+
this.serializationFormat,
|
|
2739
|
+
this.hub.registry
|
|
2648
2740
|
);
|
|
2741
|
+
const whenClosed = new import_core7.PromiseSource();
|
|
2742
|
+
const closedController = new AbortController();
|
|
2743
|
+
conn.closed.add((ev) => {
|
|
2744
|
+
lastCloseCode = ev.code;
|
|
2745
|
+
lastCloseReason = ev.reason;
|
|
2746
|
+
whenClosed.resolve(ev);
|
|
2747
|
+
closedController.abort();
|
|
2748
|
+
});
|
|
2749
|
+
const closedRejection = whenClosed.promise.then(() => {
|
|
2750
|
+
throw new Error("Connection failed");
|
|
2751
|
+
});
|
|
2752
|
+
closedRejection.catch(() => {
|
|
2753
|
+
});
|
|
2754
|
+
this._pendingHandshake = new import_core7.PromiseSource();
|
|
2755
|
+
this.setupConnection(conn);
|
|
2756
|
+
this._connection = void 0;
|
|
2757
|
+
await Promise.race([conn.whenConnected, closedRejection]);
|
|
2758
|
+
try {
|
|
2759
|
+
this.hub.systemCallSender.handshake(
|
|
2760
|
+
conn,
|
|
2761
|
+
this.serializationFormat,
|
|
2762
|
+
this.id,
|
|
2763
|
+
this.hub.hubId,
|
|
2764
|
+
++this._handshakeIndex
|
|
2765
|
+
);
|
|
2766
|
+
} catch (e) {
|
|
2767
|
+
warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
2768
|
+
throw e;
|
|
2769
|
+
}
|
|
2770
|
+
const handshakeTimeoutMs = this.handshakeTimeoutMs;
|
|
2771
|
+
let handshakeTimer;
|
|
2772
|
+
const handshakeTimeout = new Promise((_, reject) => {
|
|
2773
|
+
handshakeTimer = setTimeout(() => {
|
|
2774
|
+
reject(new Error("Handshake timeout"));
|
|
2775
|
+
}, handshakeTimeoutMs);
|
|
2776
|
+
});
|
|
2777
|
+
handshakeTimeout.catch(() => {
|
|
2778
|
+
});
|
|
2779
|
+
let remoteHandshake;
|
|
2780
|
+
try {
|
|
2781
|
+
remoteHandshake = await Promise.race([
|
|
2782
|
+
this._pendingHandshake.promise,
|
|
2783
|
+
closedRejection,
|
|
2784
|
+
handshakeTimeout
|
|
2785
|
+
]);
|
|
2786
|
+
} catch (e) {
|
|
2787
|
+
if (e instanceof Error && e.message === "Handshake timeout") {
|
|
2788
|
+
warnLog6?.log(`'${this.ref}': Handshake timed out after ${handshakeTimeoutMs}ms`);
|
|
2789
|
+
conn.close();
|
|
2790
|
+
}
|
|
2791
|
+
throw e;
|
|
2792
|
+
} finally {
|
|
2793
|
+
if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
|
|
2794
|
+
this._pendingHandshake = void 0;
|
|
2795
|
+
}
|
|
2796
|
+
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
2797
|
+
const remoteHubId = remoteHandshake.RemoteHubId;
|
|
2798
|
+
let isPeerChanged = false;
|
|
2799
|
+
if (remoteHubId !== void 0) {
|
|
2800
|
+
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
2801
|
+
if (isPeerChanged) {
|
|
2802
|
+
this.inboundCalls.clear();
|
|
2803
|
+
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
2804
|
+
this.peerChanged.trigger();
|
|
2805
|
+
}
|
|
2806
|
+
this._lastRemoteHubId = remoteHubId;
|
|
2807
|
+
}
|
|
2808
|
+
infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
|
|
2809
|
+
this._connection = conn;
|
|
2810
|
+
this._setConnectionState(2 /* Connected */);
|
|
2811
|
+
this._tryIndex = 0;
|
|
2812
|
+
await this._reconnect(isPeerChanged, closedController.signal);
|
|
2813
|
+
await whenClosed.promise;
|
|
2649
2814
|
} catch (e) {
|
|
2650
|
-
|
|
2651
|
-
throw e;
|
|
2815
|
+
debugLog2?.log(`'${this.ref}': run() iteration failed:`, e);
|
|
2652
2816
|
}
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
let isPeerChanged = false;
|
|
2661
|
-
if (remoteHubId !== void 0) {
|
|
2662
|
-
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
2663
|
-
if (isPeerChanged) {
|
|
2664
|
-
this.inbound.clear();
|
|
2665
|
-
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
2666
|
-
this.peerChanged.trigger();
|
|
2667
|
-
}
|
|
2668
|
-
this._lastRemoteHubId = remoteHubId;
|
|
2817
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2818
|
+
if (this._disposed) break;
|
|
2819
|
+
if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
|
|
2820
|
+
warnLog6?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
|
|
2821
|
+
this.unsupportedFormat.trigger({ reason: lastCloseReason });
|
|
2822
|
+
this._disposed = true;
|
|
2823
|
+
break;
|
|
2669
2824
|
}
|
|
2670
|
-
|
|
2671
|
-
this._connection = conn;
|
|
2672
|
-
this._connectionKind = 2 /* Connected */;
|
|
2673
|
-
this._tryIndex = 0;
|
|
2674
|
-
await this._reconnect(isPeerChanged);
|
|
2675
|
-
await new Promise((r) => conn.closed.add(() => r()));
|
|
2676
|
-
} catch (e) {
|
|
2677
|
-
debugLog2?.log(`'${this.ref}': run() iteration failed:`, e);
|
|
2678
|
-
}
|
|
2679
|
-
this._connectionKind = 0 /* Disconnected */;
|
|
2680
|
-
if (this._disposed) break;
|
|
2681
|
-
if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
|
|
2682
|
-
warnLog6?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
|
|
2683
|
-
this.unsupportedFormat.trigger({ reason: lastCloseReason });
|
|
2684
|
-
this._disposed = true;
|
|
2685
|
-
break;
|
|
2686
|
-
}
|
|
2687
|
-
this._tryIndex++;
|
|
2688
|
-
const delay = this.reconnectDelayer.getDelay(this._tryIndex);
|
|
2689
|
-
if (delay.isLimitExceeded) {
|
|
2690
|
-
warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
2691
|
-
this._disposed = true;
|
|
2692
|
-
break;
|
|
2693
|
-
}
|
|
2694
|
-
this._setReconnectsAt(delay.endsAt);
|
|
2695
|
-
try {
|
|
2696
|
-
await delay.promise;
|
|
2697
|
-
} finally {
|
|
2698
|
-
this._setReconnectsAt(0);
|
|
2825
|
+
this._tryIndex++;
|
|
2699
2826
|
}
|
|
2827
|
+
} finally {
|
|
2828
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2700
2829
|
}
|
|
2701
2830
|
}
|
|
2702
2831
|
/**
|
|
@@ -2718,7 +2847,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2718
2847
|
* are always self-invalidated — TS doesn't track compute-call stages
|
|
2719
2848
|
* across reconnect, so the safest behavior is to force a fresh compute.
|
|
2720
2849
|
*/
|
|
2721
|
-
async _reconnect(isPeerChanged) {
|
|
2850
|
+
async _reconnect(isPeerChanged, closedSignal) {
|
|
2722
2851
|
if (this._connection === void 0) return;
|
|
2723
2852
|
if (isPeerChanged) {
|
|
2724
2853
|
this.remoteObjects.disconnectAll();
|
|
@@ -2727,7 +2856,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2727
2856
|
this.remoteObjects.reconnectAll();
|
|
2728
2857
|
}
|
|
2729
2858
|
const eligible = [];
|
|
2730
|
-
for (const call of [...this.
|
|
2859
|
+
for (const call of [...this.outboundCalls.values()]) {
|
|
2731
2860
|
const mode = call.remoteExecutionMode;
|
|
2732
2861
|
if (!(mode & 2)) {
|
|
2733
2862
|
if (!call.result.isCompleted)
|
|
@@ -2735,7 +2864,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2735
2864
|
"Outbound call failed: disconnected and AllowReconnect is not set."
|
|
2736
2865
|
));
|
|
2737
2866
|
call.onDisconnect();
|
|
2738
|
-
this.
|
|
2867
|
+
this.outboundCalls.remove(call.callId);
|
|
2739
2868
|
continue;
|
|
2740
2869
|
}
|
|
2741
2870
|
if (isPeerChanged && !(mode & 4)) {
|
|
@@ -2744,19 +2873,19 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2744
2873
|
"Outbound call failed: reconnected to a different peer and AllowResend is not set."
|
|
2745
2874
|
));
|
|
2746
2875
|
call.onDisconnect();
|
|
2747
|
-
this.
|
|
2876
|
+
this.outboundCalls.remove(call.callId);
|
|
2748
2877
|
continue;
|
|
2749
2878
|
}
|
|
2750
2879
|
eligible.push(call);
|
|
2751
2880
|
}
|
|
2752
2881
|
let unknownIds = null;
|
|
2753
2882
|
if (!isPeerChanged && eligible.length > 0) {
|
|
2754
|
-
unknownIds = await this._reconcileReconnect(eligible);
|
|
2883
|
+
unknownIds = await this._reconcileReconnect(eligible, closedSignal);
|
|
2755
2884
|
}
|
|
2756
2885
|
for (const call of eligible) {
|
|
2757
2886
|
if (!call.removeOnOk && call.result.isCompleted) {
|
|
2758
2887
|
call.onDisconnect();
|
|
2759
|
-
this.
|
|
2888
|
+
this.outboundCalls.remove(call.callId);
|
|
2760
2889
|
continue;
|
|
2761
2890
|
}
|
|
2762
2891
|
if (unknownIds !== null && !unknownIds.has(call.callId)) {
|
|
@@ -2779,7 +2908,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2779
2908
|
* native msgpack `map<int, bin>`, matching .NET MessagePack-CSharp's
|
|
2780
2909
|
* default for `Dictionary<int, byte[]>`.
|
|
2781
2910
|
*/
|
|
2782
|
-
async _reconcileReconnect(eligible) {
|
|
2911
|
+
async _reconcileReconnect(eligible, closedSignal) {
|
|
2783
2912
|
try {
|
|
2784
2913
|
const byStage = /* @__PURE__ */ new Map();
|
|
2785
2914
|
for (const call of eligible) {
|
|
@@ -2795,7 +2924,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2795
2924
|
if (byStage.size === 0) {
|
|
2796
2925
|
return new Set(eligible.map((c) => c.callId));
|
|
2797
2926
|
}
|
|
2798
|
-
const isBinary = this.
|
|
2927
|
+
const isBinary = this.serializationFormat.isBinary;
|
|
2799
2928
|
const stagesArg = isBinary ? /* @__PURE__ */ new Map() : {};
|
|
2800
2929
|
for (const [stage, ids] of byStage) {
|
|
2801
2930
|
ids.sort((a, b) => a - b);
|
|
@@ -2810,8 +2939,9 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2810
2939
|
RpcSystemCalls.reconnect,
|
|
2811
2940
|
[this._remoteHandshakeIndex, stagesArg],
|
|
2812
2941
|
{
|
|
2813
|
-
remoteExecutionMode: 1
|
|
2942
|
+
remoteExecutionMode: 1,
|
|
2814
2943
|
/* AwaitForConnection only; must not itself reconnect */
|
|
2944
|
+
signal: closedSignal
|
|
2815
2945
|
}
|
|
2816
2946
|
);
|
|
2817
2947
|
const result = await reconnectCall.result.promise;
|
|
@@ -2831,11 +2961,14 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2831
2961
|
}
|
|
2832
2962
|
};
|
|
2833
2963
|
var RpcServerPeer = class extends RpcPeer {
|
|
2964
|
+
// Needed to widen the base `protected constructor` to public.
|
|
2965
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
2834
2966
|
constructor(hub, ref) {
|
|
2835
|
-
super(
|
|
2967
|
+
super(hub, ref);
|
|
2836
2968
|
}
|
|
2837
2969
|
/** Accept an incoming connection — sets up message handling and handshake response. */
|
|
2838
2970
|
accept(conn) {
|
|
2971
|
+
this._setConnectionState(1 /* Connecting */);
|
|
2839
2972
|
this.setupConnection(conn);
|
|
2840
2973
|
conn.closed.add(() => {
|
|
2841
2974
|
this.sharedObjects.disconnectAll();
|
|
@@ -2843,13 +2976,14 @@ var RpcServerPeer = class extends RpcPeer {
|
|
|
2843
2976
|
}
|
|
2844
2977
|
_onHandshakeReceived(_handshake) {
|
|
2845
2978
|
if (this._connection !== void 0) {
|
|
2846
|
-
this.
|
|
2979
|
+
this.hub.systemCallSender.handshake(
|
|
2847
2980
|
this._connection,
|
|
2848
|
-
this.
|
|
2981
|
+
this.serializationFormat,
|
|
2849
2982
|
this.id,
|
|
2850
|
-
this.
|
|
2983
|
+
this.hub.hubId,
|
|
2851
2984
|
0
|
|
2852
2985
|
);
|
|
2986
|
+
this._setConnectionState(2 /* Connected */);
|
|
2853
2987
|
}
|
|
2854
2988
|
}
|
|
2855
2989
|
};
|
|
@@ -2881,6 +3015,15 @@ function _toBytes2(value) {
|
|
|
2881
3015
|
return null;
|
|
2882
3016
|
}
|
|
2883
3017
|
|
|
3018
|
+
// src/rpc-client-peer-reconnect-delayer.ts
|
|
3019
|
+
var import_core8 = require("@actuallab/core");
|
|
3020
|
+
var RpcClientPeerReconnectDelayer = class extends import_core8.RetryDelayer {
|
|
3021
|
+
constructor() {
|
|
3022
|
+
super();
|
|
3023
|
+
this.delays = import_core8.RetryDelaySeq.fixed(100);
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
|
|
2884
3027
|
// src/rpc-service-host.ts
|
|
2885
3028
|
var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
|
|
2886
3029
|
var RpcServiceHost = class {
|
|
@@ -3483,6 +3626,16 @@ var RpcHub = class {
|
|
|
3483
3626
|
/** Method registry for compact format hash ↔ name resolution.
|
|
3484
3627
|
* Created lazily, populated by addService/addClient. */
|
|
3485
3628
|
registry = new RpcMethodRegistry();
|
|
3629
|
+
/** Shared reconnect delayer used by every client peer in this hub. Swap in
|
|
3630
|
+
* a custom subclass (e.g. app-level signal-gated) before peers start. */
|
|
3631
|
+
reconnectDelayer = new RpcClientPeerReconnectDelayer();
|
|
3632
|
+
/** URL used by {@link defaultPeer} to resolve / create the default client
|
|
3633
|
+
* peer. Must be set before the first {@link defaultPeer} access. */
|
|
3634
|
+
defaultPeerUrl;
|
|
3635
|
+
/** Factory used by {@link defaultPeer} when the peer needs to be created.
|
|
3636
|
+
* If left undefined, `getPeer`'s built-in default (RpcClientPeer with
|
|
3637
|
+
* format derived from URL or resolver default) is used. */
|
|
3638
|
+
defaultPeerFactory;
|
|
3486
3639
|
constructor(hubId) {
|
|
3487
3640
|
this.hubId = hubId ?? crypto.randomUUID();
|
|
3488
3641
|
this.serviceHost = new RpcServiceHost();
|
|
@@ -3491,7 +3644,20 @@ var RpcHub = class {
|
|
|
3491
3644
|
this.registry.register(methodName);
|
|
3492
3645
|
}
|
|
3493
3646
|
}
|
|
3647
|
+
/** Shortcut for the hub's default client peer. Fails if
|
|
3648
|
+
* {@link defaultPeerUrl} is not set. */
|
|
3649
|
+
get defaultPeer() {
|
|
3650
|
+
if (this.defaultPeerUrl === void 0)
|
|
3651
|
+
throw new Error("RpcHub.defaultPeerUrl is not set.");
|
|
3652
|
+
return this.getClientPeer(this.defaultPeerUrl, this.defaultPeerFactory);
|
|
3653
|
+
}
|
|
3654
|
+
/** Register a peer under its ref. If a different peer is already registered
|
|
3655
|
+
* at the same ref, close it first — the hub guarantees at most one live
|
|
3656
|
+
* peer per ref, and no peer exists outside the hub. */
|
|
3494
3657
|
addPeer(peer) {
|
|
3658
|
+
const existing = this.peers.get(peer.ref);
|
|
3659
|
+
if (existing !== void 0 && existing !== peer)
|
|
3660
|
+
existing.close();
|
|
3495
3661
|
this.peers.set(peer.ref, peer);
|
|
3496
3662
|
}
|
|
3497
3663
|
removePeer(ref) {
|
|
@@ -3502,21 +3668,27 @@ var RpcHub = class {
|
|
|
3502
3668
|
warnLog9?.log(`'${ref}': peer is removed from RpcHub`);
|
|
3503
3669
|
}
|
|
3504
3670
|
}
|
|
3505
|
-
/** Get or create a peer by ref.
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3671
|
+
/** Get or create a peer by ref. If the peer does not exist:
|
|
3672
|
+
* - with `factory` provided: it is constructed via `factory(this, ref)`;
|
|
3673
|
+
* - otherwise: server peers (ref starts with `server://`) get a plain
|
|
3674
|
+
* RpcServerPeer, anything else gets a default-configured RpcClientPeer.
|
|
3675
|
+
* The created peer is always registered in this hub. */
|
|
3676
|
+
getPeer(ref, factory) {
|
|
3677
|
+
const existing = this.peers.get(ref);
|
|
3678
|
+
if (existing) return existing;
|
|
3679
|
+
const peer = factory ? factory(this, ref) : ref.startsWith("server://") ? new RpcServerPeer(this, ref) : new RpcClientPeer(this, ref);
|
|
3510
3680
|
this.addPeer(peer);
|
|
3511
3681
|
return peer;
|
|
3512
3682
|
}
|
|
3513
|
-
/** Get or create a client peer for the given URL.
|
|
3514
|
-
|
|
3515
|
-
|
|
3683
|
+
/** Get or create a client peer for the given URL. See {@link getPeer} for
|
|
3684
|
+
* the factory semantics. */
|
|
3685
|
+
getClientPeer(ref, factory) {
|
|
3686
|
+
return this.getPeer(ref, factory);
|
|
3516
3687
|
}
|
|
3517
|
-
/** Get or create a server peer for the given ref.
|
|
3518
|
-
|
|
3519
|
-
|
|
3688
|
+
/** Get or create a server peer for the given ref. See {@link getPeer} for
|
|
3689
|
+
* the factory semantics. */
|
|
3690
|
+
getServerPeer(ref, factory) {
|
|
3691
|
+
return this.getPeer(ref, factory);
|
|
3520
3692
|
}
|
|
3521
3693
|
/** Register a service with optional server method wrapping for custom call types. */
|
|
3522
3694
|
addService(defOrContract, impl) {
|
|
@@ -3645,6 +3817,25 @@ var RpcHub = class {
|
|
|
3645
3817
|
}
|
|
3646
3818
|
};
|
|
3647
3819
|
|
|
3820
|
+
// src/rpc-peer-ref-builder.ts
|
|
3821
|
+
var RpcPeerRefBuilder = class {
|
|
3822
|
+
/** Build a client peer ref from a WebSocket URL, optionally baking in the
|
|
3823
|
+
* serialization format as a `f=` query parameter.
|
|
3824
|
+
* @param url Base WebSocket URL, e.g. `wss://host/rpc/ws`.
|
|
3825
|
+
* @param serializationFormat Optional format key, e.g. `msgpack6`. When
|
|
3826
|
+
* provided, replaces any existing `f=` in the URL. */
|
|
3827
|
+
static forClient(url, serializationFormat) {
|
|
3828
|
+
if (serializationFormat === void 0) return url;
|
|
3829
|
+
const u = new URL(url);
|
|
3830
|
+
u.searchParams.set("f", serializationFormat);
|
|
3831
|
+
return u.toString();
|
|
3832
|
+
}
|
|
3833
|
+
/** Build a server peer ref from a peer id — returns `server://{id}`. */
|
|
3834
|
+
static forServer(id) {
|
|
3835
|
+
return `server://${id}`;
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
|
|
3648
3839
|
// src/rpc-client.ts
|
|
3649
3840
|
function createRpcClient(peer, serviceDef) {
|
|
3650
3841
|
const overloads = /* @__PURE__ */ new Map();
|
|
@@ -3746,20 +3937,18 @@ var RpcPeerStateMonitor = class {
|
|
|
3746
3937
|
_state;
|
|
3747
3938
|
_timer;
|
|
3748
3939
|
// Arrow functions to preserve `this` when used as event handlers
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
if (wasConnected) {
|
|
3762
|
-
this._disconnectedAt = Date.now();
|
|
3940
|
+
_onConnectionStateChanged = (state) => {
|
|
3941
|
+
if (state === 2 /* Connected */) {
|
|
3942
|
+
this._rawConnected = true;
|
|
3943
|
+
this._connectedAt = Date.now();
|
|
3944
|
+
this._lastError = void 0;
|
|
3945
|
+
} else {
|
|
3946
|
+
const wasConnected = this._rawConnected;
|
|
3947
|
+
this._rawConnected = false;
|
|
3948
|
+
this._lastError = new Error("Connection closed");
|
|
3949
|
+
if (wasConnected) {
|
|
3950
|
+
this._disconnectedAt = Date.now();
|
|
3951
|
+
}
|
|
3763
3952
|
}
|
|
3764
3953
|
this._recompute();
|
|
3765
3954
|
};
|
|
@@ -3785,10 +3974,9 @@ var RpcPeerStateMonitor = class {
|
|
|
3785
3974
|
reconnectsIn: 0
|
|
3786
3975
|
};
|
|
3787
3976
|
}
|
|
3788
|
-
peer.
|
|
3789
|
-
peer.disconnected.add(this._onDisconnected);
|
|
3977
|
+
peer.connectionStateChanged.add(this._onConnectionStateChanged);
|
|
3790
3978
|
peer.reconnectsAtChanged.add(this._onReconnectsAtChanged);
|
|
3791
|
-
peer.reconnectDelayer.cancelDelaysChanged.add(
|
|
3979
|
+
peer.hub.reconnectDelayer.cancelDelaysChanged.add(
|
|
3792
3980
|
this._onCancelDelaysChanged
|
|
3793
3981
|
);
|
|
3794
3982
|
this._scheduleRecompute(
|
|
@@ -3804,10 +3992,9 @@ var RpcPeerStateMonitor = class {
|
|
|
3804
3992
|
clearTimeout(this._timer);
|
|
3805
3993
|
this._timer = void 0;
|
|
3806
3994
|
}
|
|
3807
|
-
this.peer.
|
|
3808
|
-
this.peer.disconnected.remove(this._onDisconnected);
|
|
3995
|
+
this.peer.connectionStateChanged.remove(this._onConnectionStateChanged);
|
|
3809
3996
|
this.peer.reconnectsAtChanged.remove(this._onReconnectsAtChanged);
|
|
3810
|
-
this.peer.reconnectDelayer.cancelDelaysChanged.remove(
|
|
3997
|
+
this.peer.hub.reconnectDelayer.cancelDelaysChanged.remove(
|
|
3811
3998
|
this._onCancelDelaysChanged
|
|
3812
3999
|
);
|
|
3813
4000
|
infoLog2?.log(`'${this.peer.ref}': monitor stopped`);
|
|
@@ -3890,12 +4077,14 @@ var RpcPeerStateMonitor = class {
|
|
|
3890
4077
|
ARG_DELIMITER,
|
|
3891
4078
|
ENVELOPE_DELIMITER,
|
|
3892
4079
|
FRAME_DELIMITER,
|
|
4080
|
+
HANDSHAKE_TIMEOUT_MS,
|
|
3893
4081
|
IncreasingSeqCompressor,
|
|
3894
4082
|
RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
|
|
3895
4083
|
RpcCallStage,
|
|
3896
4084
|
RpcCallTypeId,
|
|
3897
4085
|
RpcClientPeer,
|
|
3898
4086
|
RpcClientPeerReconnectDelayer,
|
|
4087
|
+
RpcConnectionState,
|
|
3899
4088
|
RpcHub,
|
|
3900
4089
|
RpcInboundCall,
|
|
3901
4090
|
RpcInboundCallTracker,
|
|
@@ -3908,7 +4097,7 @@ var RpcPeerStateMonitor = class {
|
|
|
3908
4097
|
RpcOutboundCall,
|
|
3909
4098
|
RpcOutboundCallTracker,
|
|
3910
4099
|
RpcPeer,
|
|
3911
|
-
|
|
4100
|
+
RpcPeerRefBuilder,
|
|
3912
4101
|
RpcPeerStateKind,
|
|
3913
4102
|
RpcPeerStateMonitor,
|
|
3914
4103
|
RpcRemoteExecutionMode,
|