@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.js
CHANGED
|
@@ -955,7 +955,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
955
955
|
if (conn) {
|
|
956
956
|
this.peer.hub.systemCallSender.item(
|
|
957
957
|
conn,
|
|
958
|
-
this.peer.
|
|
958
|
+
this.peer.serializationFormat,
|
|
959
959
|
this.id.localId,
|
|
960
960
|
this._nextIndex,
|
|
961
961
|
item
|
|
@@ -970,7 +970,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
970
970
|
if (conn) {
|
|
971
971
|
this.peer.hub.systemCallSender.batch(
|
|
972
972
|
conn,
|
|
973
|
-
this.peer.
|
|
973
|
+
this.peer.serializationFormat,
|
|
974
974
|
this.id.localId,
|
|
975
975
|
this._nextIndex,
|
|
976
976
|
items
|
|
@@ -987,7 +987,7 @@ var RpcStreamSender = class _RpcStreamSender {
|
|
|
987
987
|
const errorInfo = error ? { TypeRef: "System.Exception", Message: error.message } : { TypeRef: "", Message: "" };
|
|
988
988
|
this.peer.hub.systemCallSender.end(
|
|
989
989
|
conn,
|
|
990
|
-
this.peer.
|
|
990
|
+
this.peer.serializationFormat,
|
|
991
991
|
this.id.localId,
|
|
992
992
|
this._nextIndex,
|
|
993
993
|
errorInfo
|
|
@@ -1442,7 +1442,7 @@ var RpcStream = class _RpcStream {
|
|
|
1442
1442
|
this._sender = sender;
|
|
1443
1443
|
const source = isFactory ? this.localSource(sender.abortSignal) : this.localSource;
|
|
1444
1444
|
this._whenSent = sender.writeFrom(source);
|
|
1445
|
-
this._toRefValue = peer.
|
|
1445
|
+
this._toRefValue = peer.serializationFormat.isBinary ? {
|
|
1446
1446
|
SerializedId: [sender.id.hostId, sender.id.localId],
|
|
1447
1447
|
AckPeriod: sender.ackPeriod,
|
|
1448
1448
|
AckAdvance: sender.ackAdvance,
|
|
@@ -1592,7 +1592,7 @@ var RpcStream = class _RpcStream {
|
|
|
1592
1592
|
const hostId = mustReset ? this.id.hostId : _RpcStream._emptyGuid;
|
|
1593
1593
|
this.peer.hub.systemCallSender.ack(
|
|
1594
1594
|
conn,
|
|
1595
|
-
this.peer.
|
|
1595
|
+
this.peer.serializationFormat,
|
|
1596
1596
|
this.id.localId,
|
|
1597
1597
|
nextIndex,
|
|
1598
1598
|
hostId
|
|
@@ -1604,7 +1604,7 @@ var RpcStream = class _RpcStream {
|
|
|
1604
1604
|
if (conn) {
|
|
1605
1605
|
this.peer.hub.systemCallSender.ackEnd(
|
|
1606
1606
|
conn,
|
|
1607
|
-
this.peer.
|
|
1607
|
+
this.peer.serializationFormat,
|
|
1608
1608
|
this.id.localId,
|
|
1609
1609
|
this.id.hostId
|
|
1610
1610
|
);
|
|
@@ -1651,9 +1651,8 @@ function base64Encode(bytes) {
|
|
|
1651
1651
|
const chunkSize = 32768;
|
|
1652
1652
|
let binary = "";
|
|
1653
1653
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
1654
|
-
binary += String.fromCharCode
|
|
1655
|
-
|
|
1656
|
-
bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
|
|
1654
|
+
binary += String.fromCharCode(
|
|
1655
|
+
...bytes.subarray(i, Math.min(bytes.length, i + chunkSize))
|
|
1657
1656
|
);
|
|
1658
1657
|
}
|
|
1659
1658
|
return btoa(binary);
|
|
@@ -1674,11 +1673,11 @@ var RpcSystemCallHandler = class {
|
|
|
1674
1673
|
debugLog?.log(`'${peer.ref}': handle ${method}#${relatedId}`);
|
|
1675
1674
|
switch (method) {
|
|
1676
1675
|
case RpcSystemCalls.ok: {
|
|
1677
|
-
const call = peer.
|
|
1676
|
+
const call = peer.outboundCalls.get(relatedId);
|
|
1678
1677
|
if (call !== void 0) {
|
|
1679
1678
|
call.completedStage |= RpcCallStage.ResultReady;
|
|
1680
1679
|
if (call.removeOnOk) {
|
|
1681
|
-
peer.
|
|
1680
|
+
peer.outboundCalls.remove(relatedId);
|
|
1682
1681
|
}
|
|
1683
1682
|
call.result.resolve(args[0]);
|
|
1684
1683
|
}
|
|
@@ -1689,7 +1688,7 @@ var RpcSystemCallHandler = class {
|
|
|
1689
1688
|
break;
|
|
1690
1689
|
}
|
|
1691
1690
|
case RpcSystemCalls.error: {
|
|
1692
|
-
const call = peer.
|
|
1691
|
+
const call = peer.outboundCalls.remove(relatedId);
|
|
1693
1692
|
if (call !== void 0) {
|
|
1694
1693
|
const errorInfo = args[0];
|
|
1695
1694
|
const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
|
|
@@ -1704,10 +1703,11 @@ var RpcSystemCallHandler = class {
|
|
|
1704
1703
|
break;
|
|
1705
1704
|
}
|
|
1706
1705
|
case RpcSystemCalls.cancel: {
|
|
1707
|
-
peer.
|
|
1706
|
+
peer.inboundCalls.remove(relatedId);
|
|
1708
1707
|
break;
|
|
1709
1708
|
}
|
|
1710
1709
|
case RpcSystemCalls.keepAlive: {
|
|
1710
|
+
peer.notifyKeepAliveReceived();
|
|
1711
1711
|
break;
|
|
1712
1712
|
}
|
|
1713
1713
|
case RpcSystemCalls.item: {
|
|
@@ -1798,7 +1798,7 @@ var RpcSystemCallHandler = class {
|
|
|
1798
1798
|
if (bytes === null) continue;
|
|
1799
1799
|
const callIds = IncreasingSeqCompressor.deserialize(bytes);
|
|
1800
1800
|
for (const callId of callIds) {
|
|
1801
|
-
if (peer.
|
|
1801
|
+
if (peer.inboundCalls.get(callId) === void 0) {
|
|
1802
1802
|
unknownSet.add(callId);
|
|
1803
1803
|
}
|
|
1804
1804
|
}
|
|
@@ -1808,8 +1808,8 @@ var RpcSystemCallHandler = class {
|
|
|
1808
1808
|
} catch {
|
|
1809
1809
|
}
|
|
1810
1810
|
const responseBytes = IncreasingSeqCompressor.serialize(unknownIds);
|
|
1811
|
-
const responseValue = peer.
|
|
1812
|
-
peer.hub.systemCallSender.ok(peer.connection, peer.
|
|
1811
|
+
const responseValue = peer.serializationFormat.isBinary ? responseBytes : base64Encode(responseBytes);
|
|
1812
|
+
peer.hub.systemCallSender.ok(peer.connection, peer.serializationFormat, relatedId, responseValue);
|
|
1813
1813
|
}
|
|
1814
1814
|
};
|
|
1815
1815
|
function _toBytes(value) {
|
|
@@ -2006,15 +2006,6 @@ var RpcSharedObjectTracker = class {
|
|
|
2006
2006
|
// src/rpc-peer.ts
|
|
2007
2007
|
import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource5 } from "@actuallab/core";
|
|
2008
2008
|
|
|
2009
|
-
// src/rpc-client-peer-reconnect-delayer.ts
|
|
2010
|
-
import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";
|
|
2011
|
-
var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
2012
|
-
constructor() {
|
|
2013
|
-
super();
|
|
2014
|
-
this.delays = RetryDelaySeq.exp(1e3, 6e4);
|
|
2015
|
-
}
|
|
2016
|
-
};
|
|
2017
|
-
|
|
2018
2009
|
// src/rpc-serialization-format.ts
|
|
2019
2010
|
import { Decoder as Decoder3 } from "@msgpack/msgpack";
|
|
2020
2011
|
var RpcSerializationFormat = class {
|
|
@@ -2174,55 +2165,106 @@ var RpcSerializationFormatResolver = class _RpcSerializationFormatResolver {
|
|
|
2174
2165
|
|
|
2175
2166
|
// src/rpc-peer.ts
|
|
2176
2167
|
var RPC_CLOSE_CODE_UNSUPPORTED_FORMAT = 4001;
|
|
2168
|
+
var HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
2169
|
+
var KEEP_ALIVE_PERIOD_MS = 15e3;
|
|
2170
|
+
var KEEP_ALIVE_TIMEOUT_MS = 55e3;
|
|
2177
2171
|
var { debugLog: debugLog2, infoLog, warnLog: warnLog6, errorLog: errorLog2 } = getLogs("RpcPeer");
|
|
2178
2172
|
var defaultConnectionUrlResolver = (peer) => {
|
|
2179
|
-
const
|
|
2180
|
-
|
|
2173
|
+
const formatKey = peer.serializationFormat.key;
|
|
2174
|
+
try {
|
|
2175
|
+
const url = new URL(peer.ref);
|
|
2176
|
+
url.searchParams.set("clientId", peer.clientId);
|
|
2177
|
+
url.searchParams.set("f", formatKey);
|
|
2178
|
+
return url.toString();
|
|
2179
|
+
} catch {
|
|
2180
|
+
const sep = peer.ref.includes("?") ? "&" : "?";
|
|
2181
|
+
return peer.ref + sep + `clientId=${peer.clientId}&f=${formatKey}`;
|
|
2182
|
+
}
|
|
2181
2183
|
};
|
|
2184
|
+
function parseFormatFromUrl(url) {
|
|
2185
|
+
try {
|
|
2186
|
+
const f = new URL(url).searchParams.get("f");
|
|
2187
|
+
return f ?? void 0;
|
|
2188
|
+
} catch {
|
|
2189
|
+
return void 0;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2182
2192
|
var RpcPeer = class {
|
|
2193
|
+
hub;
|
|
2183
2194
|
/** Routing/addressing key — used as key in hub.peers (URL for clients, "server://{uuid}" for servers). */
|
|
2184
2195
|
ref;
|
|
2185
2196
|
/** Auto-generated GUID — used in handshakes only. */
|
|
2186
2197
|
id = crypto.randomUUID();
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
outbound = new RpcOutboundCallTracker();
|
|
2190
|
-
inbound = new RpcInboundCallTracker();
|
|
2198
|
+
outboundCalls = new RpcOutboundCallTracker();
|
|
2199
|
+
inboundCalls = new RpcInboundCallTracker();
|
|
2191
2200
|
remoteObjects = new RpcRemoteObjectTracker();
|
|
2192
2201
|
sharedObjects = new RpcSharedObjectTracker();
|
|
2193
|
-
|
|
2194
|
-
|
|
2202
|
+
/** Fired on every state transition with the new value. Initial state is
|
|
2203
|
+
* `Disconnected`; `Connecting` is entered when the peer starts an
|
|
2204
|
+
* attempt (the reconnect delay is part of this phase); `Connected` is
|
|
2205
|
+
* entered only after the handshake has succeeded. */
|
|
2206
|
+
connectionStateChanged = new EventHandlerSet3();
|
|
2207
|
+
_connection;
|
|
2208
|
+
_connectionState = 0 /* Disconnected */;
|
|
2195
2209
|
_pendingSends = [];
|
|
2196
2210
|
_keepAliveTimer;
|
|
2197
|
-
|
|
2211
|
+
_keepAliveWatchdog;
|
|
2212
|
+
/** Wall-clock time of the last inbound `$sys.KeepAlive`, or 0 if none yet
|
|
2213
|
+
* for the current connection. Useful for diagnostics. */
|
|
2214
|
+
_lastKeepAliveAt = 0;
|
|
2215
|
+
/** Outbound keep-alive send period (per-peer override for tests). */
|
|
2216
|
+
keepAlivePeriodMs = KEEP_ALIVE_PERIOD_MS;
|
|
2217
|
+
/** Inbound keep-alive silence tolerance (per-peer override for tests). */
|
|
2218
|
+
keepAliveTimeoutMs = KEEP_ALIVE_TIMEOUT_MS;
|
|
2219
|
+
constructor(hub, ref) {
|
|
2220
|
+
this.hub = hub;
|
|
2198
2221
|
this.ref = ref;
|
|
2199
|
-
this._hub = hub;
|
|
2200
|
-
}
|
|
2201
|
-
get hub() {
|
|
2202
|
-
return this._hub;
|
|
2203
2222
|
}
|
|
2204
2223
|
get connection() {
|
|
2205
2224
|
return this._connection;
|
|
2206
2225
|
}
|
|
2226
|
+
get connectionState() {
|
|
2227
|
+
return this._connectionState;
|
|
2228
|
+
}
|
|
2207
2229
|
get isConnected() {
|
|
2208
2230
|
return this._connection?.isOpen ?? false;
|
|
2209
2231
|
}
|
|
2232
|
+
_setConnectionState(value) {
|
|
2233
|
+
if (this._connectionState === value) return;
|
|
2234
|
+
this._connectionState = value;
|
|
2235
|
+
if (value === 2 /* Connected */)
|
|
2236
|
+
this._armKeepAliveWatchdog();
|
|
2237
|
+
else if (value === 0 /* Disconnected */)
|
|
2238
|
+
this._disarmKeepAliveWatchdog();
|
|
2239
|
+
this.connectionStateChanged.trigger(value);
|
|
2240
|
+
}
|
|
2241
|
+
/** Resolves on the next transition into `Connected` — or immediately if
|
|
2242
|
+
* already in that state. */
|
|
2243
|
+
whenConnected() {
|
|
2244
|
+
if (this._connectionState === 2 /* Connected */)
|
|
2245
|
+
return Promise.resolve();
|
|
2246
|
+
return new Promise((resolve) => {
|
|
2247
|
+
const handler = (state) => {
|
|
2248
|
+
if (state !== 2 /* Connected */) return;
|
|
2249
|
+
this.connectionStateChanged.remove(handler);
|
|
2250
|
+
resolve();
|
|
2251
|
+
};
|
|
2252
|
+
this.connectionStateChanged.add(handler);
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2210
2255
|
setupConnection(conn) {
|
|
2211
2256
|
this._connection = conn;
|
|
2212
2257
|
conn.messageReceived.add((raw) => this._handleMessage(raw));
|
|
2213
2258
|
conn.closed.add((ev) => {
|
|
2214
2259
|
this._connection = void 0;
|
|
2215
|
-
this._stopKeepAlive();
|
|
2216
2260
|
if (ev.reason)
|
|
2217
2261
|
infoLog?.log(`'${this.ref}': Disconnected: ${ev.reason} (code=${ev.code})`);
|
|
2218
2262
|
else
|
|
2219
2263
|
infoLog?.log(`'${this.ref}': Disconnected`);
|
|
2220
|
-
this.
|
|
2264
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2221
2265
|
});
|
|
2222
2266
|
void conn.whenConnected.then(() => {
|
|
2223
|
-
infoLog?.log(`'${this.ref}':
|
|
2224
|
-
this.connected.trigger();
|
|
2225
|
-
this._startKeepAlive();
|
|
2267
|
+
infoLog?.log(`'${this.ref}': WS open`);
|
|
2226
2268
|
});
|
|
2227
2269
|
}
|
|
2228
2270
|
/**
|
|
@@ -2230,29 +2272,29 @@ var RpcPeer = class {
|
|
|
2230
2272
|
* If not set explicitly, defaults to the resolver's default (JSON /
|
|
2231
2273
|
* json5np). Subclasses can pre-assign in their constructor.
|
|
2232
2274
|
*/
|
|
2233
|
-
|
|
2234
|
-
get
|
|
2235
|
-
return this.
|
|
2275
|
+
_serializationFormat;
|
|
2276
|
+
get serializationFormat() {
|
|
2277
|
+
return this._serializationFormat ?? RpcSerializationFormatResolver.Default.defaultFormat;
|
|
2236
2278
|
}
|
|
2237
|
-
set
|
|
2238
|
-
this.
|
|
2279
|
+
set serializationFormat(value) {
|
|
2280
|
+
this._serializationFormat = value;
|
|
2239
2281
|
}
|
|
2240
2282
|
call(method, args, options) {
|
|
2241
|
-
const callId = this.
|
|
2283
|
+
const callId = this.outboundCalls.nextId();
|
|
2242
2284
|
const outboundCall = options?.outboundCallFactory ? options.outboundCallFactory(callId, method) : new RpcOutboundCall(callId, method, options?.remoteExecutionMode ?? 7);
|
|
2243
2285
|
const envelope = {
|
|
2244
2286
|
Method: method,
|
|
2245
2287
|
RelatedId: callId,
|
|
2246
2288
|
CallType: options?.callTypeId ?? 0
|
|
2247
2289
|
};
|
|
2248
|
-
outboundCall.serializedWireData = this.
|
|
2290
|
+
outboundCall.serializedWireData = this.serializationFormat.serializeMessage(
|
|
2249
2291
|
envelope,
|
|
2250
2292
|
args,
|
|
2251
2293
|
this._connection?.encoder,
|
|
2252
|
-
this.
|
|
2294
|
+
this.hub.registry
|
|
2253
2295
|
);
|
|
2254
2296
|
if (this._connection !== void 0) {
|
|
2255
|
-
this.
|
|
2297
|
+
this.outboundCalls.register(outboundCall);
|
|
2256
2298
|
this._sendWireData(outboundCall.serializedWireData);
|
|
2257
2299
|
} else {
|
|
2258
2300
|
this._pendingSends.push(outboundCall);
|
|
@@ -2260,11 +2302,11 @@ var RpcPeer = class {
|
|
|
2260
2302
|
const signal = options?.signal;
|
|
2261
2303
|
if (signal !== void 0) {
|
|
2262
2304
|
const onAbort = () => {
|
|
2263
|
-
if (this.
|
|
2305
|
+
if (this.outboundCalls.remove(callId) !== void 0) {
|
|
2264
2306
|
outboundCall.result.reject(new Error("Call cancelled."));
|
|
2265
2307
|
outboundCall.onDisconnect();
|
|
2266
2308
|
if (this._connection !== void 0)
|
|
2267
|
-
this.
|
|
2309
|
+
this.hub.systemCallSender.cancel(this._connection, this.serializationFormat, callId);
|
|
2268
2310
|
} else {
|
|
2269
2311
|
const idx = this._pendingSends.findIndex(
|
|
2270
2312
|
(c) => c.callId === callId
|
|
@@ -2286,17 +2328,16 @@ var RpcPeer = class {
|
|
|
2286
2328
|
callNoWait(method, args) {
|
|
2287
2329
|
if (this._connection === void 0) return;
|
|
2288
2330
|
const envelope = { Method: method, RelatedId: 0 };
|
|
2289
|
-
const wireData = this.
|
|
2331
|
+
const wireData = this.serializationFormat.serializeMessage(
|
|
2290
2332
|
envelope,
|
|
2291
2333
|
args,
|
|
2292
2334
|
this._connection.encoder,
|
|
2293
|
-
this.
|
|
2335
|
+
this.hub.registry
|
|
2294
2336
|
);
|
|
2295
2337
|
this._sendWireData(wireData);
|
|
2296
2338
|
}
|
|
2297
2339
|
close() {
|
|
2298
2340
|
infoLog?.log(`'${this.ref}': Stopping`);
|
|
2299
|
-
this._stopKeepAlive();
|
|
2300
2341
|
this.remoteObjects.disconnectAll();
|
|
2301
2342
|
this.sharedObjects.disconnectAll();
|
|
2302
2343
|
for (const call of this._pendingSends) {
|
|
@@ -2305,10 +2346,11 @@ var RpcPeer = class {
|
|
|
2305
2346
|
call.onDisconnect();
|
|
2306
2347
|
}
|
|
2307
2348
|
this._pendingSends.length = 0;
|
|
2308
|
-
this.
|
|
2309
|
-
this.
|
|
2349
|
+
this.outboundCalls.rejectAll(new Error("Peer closed."));
|
|
2350
|
+
this.outboundCalls.invalidateAll();
|
|
2310
2351
|
this._connection?.close();
|
|
2311
|
-
this.
|
|
2352
|
+
this.hub.peers.delete(this.ref);
|
|
2353
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2312
2354
|
infoLog?.log(`'${this.ref}': Stopped`);
|
|
2313
2355
|
}
|
|
2314
2356
|
/** Send any messages buffered while disconnected. Call after connection + handshake are ready. */
|
|
@@ -2316,7 +2358,7 @@ var RpcPeer = class {
|
|
|
2316
2358
|
if (this._pendingSends.length === 0 || this._connection === void 0)
|
|
2317
2359
|
return;
|
|
2318
2360
|
for (const call of this._pendingSends) {
|
|
2319
|
-
this.
|
|
2361
|
+
this.outboundCalls.register(call);
|
|
2320
2362
|
this._sendWireData(call.serializedWireData);
|
|
2321
2363
|
}
|
|
2322
2364
|
this._pendingSends.length = 0;
|
|
@@ -2365,7 +2407,7 @@ var RpcPeer = class {
|
|
|
2365
2407
|
return;
|
|
2366
2408
|
}
|
|
2367
2409
|
if (method.startsWith("$sys")) {
|
|
2368
|
-
this.
|
|
2410
|
+
this.hub.systemCallHandler.handle(message, args, this);
|
|
2369
2411
|
return;
|
|
2370
2412
|
}
|
|
2371
2413
|
this._handleInbound(message, args);
|
|
@@ -2374,12 +2416,12 @@ var RpcPeer = class {
|
|
|
2374
2416
|
const method = message.Method ?? "";
|
|
2375
2417
|
const relatedId = message.RelatedId ?? 0;
|
|
2376
2418
|
const call = new RpcInboundCall(relatedId, method, args);
|
|
2377
|
-
const methodDef = this.
|
|
2419
|
+
const methodDef = this.hub.serviceHost.getMethodDef(method);
|
|
2378
2420
|
const isNoWait = methodDef?.noWait === true;
|
|
2379
2421
|
if (!isNoWait) {
|
|
2380
|
-
this.
|
|
2422
|
+
this.inboundCalls.register(call);
|
|
2381
2423
|
}
|
|
2382
|
-
const serviceHost = this.
|
|
2424
|
+
const serviceHost = this.hub.serviceHost;
|
|
2383
2425
|
void (async () => {
|
|
2384
2426
|
try {
|
|
2385
2427
|
const context = this._connection !== void 0 ? {
|
|
@@ -2394,25 +2436,25 @@ var RpcPeer = class {
|
|
|
2394
2436
|
);
|
|
2395
2437
|
if (methodDef?.stream === true && this._connection !== void 0) {
|
|
2396
2438
|
const stream = result instanceof RpcStream ? result : new RpcStream(result);
|
|
2397
|
-
this.
|
|
2439
|
+
this.hub.systemCallSender.ok(
|
|
2398
2440
|
this._connection,
|
|
2399
|
-
this.
|
|
2441
|
+
this.serializationFormat,
|
|
2400
2442
|
relatedId,
|
|
2401
2443
|
stream.toRef(this)
|
|
2402
2444
|
);
|
|
2403
2445
|
} else if (!isNoWait && this._connection !== void 0) {
|
|
2404
|
-
this.
|
|
2446
|
+
this.hub.systemCallSender.ok(
|
|
2405
2447
|
this._connection,
|
|
2406
|
-
this.
|
|
2448
|
+
this.serializationFormat,
|
|
2407
2449
|
relatedId,
|
|
2408
2450
|
result
|
|
2409
2451
|
);
|
|
2410
2452
|
}
|
|
2411
2453
|
} catch (e) {
|
|
2412
2454
|
if (!isNoWait && this._connection !== void 0) {
|
|
2413
|
-
this.
|
|
2455
|
+
this.hub.systemCallSender.error(
|
|
2414
2456
|
this._connection,
|
|
2415
|
-
this.
|
|
2457
|
+
this.serializationFormat,
|
|
2416
2458
|
relatedId,
|
|
2417
2459
|
e
|
|
2418
2460
|
);
|
|
@@ -2420,36 +2462,64 @@ var RpcPeer = class {
|
|
|
2420
2462
|
debugLog2?.log(`'${this.ref}': Inbound call failed: ${method}#${relatedId}`, e);
|
|
2421
2463
|
} finally {
|
|
2422
2464
|
if (!isNoWait) {
|
|
2423
|
-
this.
|
|
2465
|
+
this.inboundCalls.remove(relatedId);
|
|
2424
2466
|
}
|
|
2425
2467
|
}
|
|
2426
2468
|
})();
|
|
2427
2469
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2470
|
+
/** Start sending `$sys.KeepAlive` pings and arm the silence watchdog.
|
|
2471
|
+
* Called when the peer transitions into `Connected` (after handshake).
|
|
2472
|
+
* Each inbound `$sys.KeepAlive` re-arms the watchdog; if it fires, the
|
|
2473
|
+
* WebSocket is force-closed so the normal disconnect path runs
|
|
2474
|
+
* (`conn.closed` → `connectionState = Disconnected` → UI).
|
|
2475
|
+
* Mirrors .NET `RpcObjectTrackers.Maintain`'s `KeepAliveTimeout` branch. */
|
|
2476
|
+
_armKeepAliveWatchdog() {
|
|
2477
|
+
this._keepAliveTimer ??= setInterval(() => {
|
|
2430
2478
|
if (this._connection !== void 0) {
|
|
2431
|
-
this.
|
|
2479
|
+
this.hub.systemCallSender.keepAlive(this._connection, this.serializationFormat, [
|
|
2432
2480
|
...this.remoteObjects.keys()
|
|
2433
2481
|
]);
|
|
2434
2482
|
}
|
|
2435
|
-
},
|
|
2483
|
+
}, this.keepAlivePeriodMs);
|
|
2484
|
+
if (this._keepAliveWatchdog !== void 0)
|
|
2485
|
+
clearTimeout(this._keepAliveWatchdog);
|
|
2486
|
+
this._keepAliveWatchdog = setTimeout(() => {
|
|
2487
|
+
this._keepAliveWatchdog = void 0;
|
|
2488
|
+
if (this._connection === void 0) return;
|
|
2489
|
+
warnLog6?.log(
|
|
2490
|
+
`'${this.ref}': no keep-alive for ${this.keepAliveTimeoutMs}ms \u2014 force-closing connection`
|
|
2491
|
+
);
|
|
2492
|
+
this._connection.close();
|
|
2493
|
+
}, this.keepAliveTimeoutMs);
|
|
2436
2494
|
}
|
|
2437
|
-
|
|
2495
|
+
/** Stop sending keep-alives and clear the silence watchdog. Called when
|
|
2496
|
+
* the peer transitions into `Disconnected`. */
|
|
2497
|
+
_disarmKeepAliveWatchdog() {
|
|
2438
2498
|
if (this._keepAliveTimer !== void 0) {
|
|
2439
2499
|
clearInterval(this._keepAliveTimer);
|
|
2440
2500
|
this._keepAliveTimer = void 0;
|
|
2441
2501
|
}
|
|
2502
|
+
if (this._keepAliveWatchdog !== void 0) {
|
|
2503
|
+
clearTimeout(this._keepAliveWatchdog);
|
|
2504
|
+
this._keepAliveWatchdog = void 0;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
/** Called by `RpcSystemCallHandler` when a `$sys.KeepAlive` message
|
|
2508
|
+
* arrives — records the timestamp and re-arms the silence watchdog. */
|
|
2509
|
+
notifyKeepAliveReceived() {
|
|
2510
|
+
this._lastKeepAliveAt = Date.now();
|
|
2511
|
+
if (this._keepAliveWatchdog !== void 0)
|
|
2512
|
+
this._armKeepAliveWatchdog();
|
|
2442
2513
|
}
|
|
2443
2514
|
};
|
|
2444
|
-
var
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
return
|
|
2449
|
-
})(
|
|
2515
|
+
var RpcConnectionState = /* @__PURE__ */ ((RpcConnectionState2) => {
|
|
2516
|
+
RpcConnectionState2[RpcConnectionState2["Disconnected"] = 0] = "Disconnected";
|
|
2517
|
+
RpcConnectionState2[RpcConnectionState2["Connecting"] = 1] = "Connecting";
|
|
2518
|
+
RpcConnectionState2[RpcConnectionState2["Connected"] = 2] = "Connected";
|
|
2519
|
+
return RpcConnectionState2;
|
|
2520
|
+
})(RpcConnectionState || {});
|
|
2450
2521
|
var RpcClientPeer = class extends RpcPeer {
|
|
2451
2522
|
_disposed = false;
|
|
2452
|
-
_connectionKind = 0 /* Disconnected */;
|
|
2453
2523
|
_tryIndex = 0;
|
|
2454
2524
|
_handshakeIndex = 0;
|
|
2455
2525
|
/** Index from the MOST RECENTLY RECEIVED server handshake — sent back
|
|
@@ -2463,18 +2533,20 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2463
2533
|
_reconnectsAt = 0;
|
|
2464
2534
|
/** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
|
|
2465
2535
|
clientId;
|
|
2466
|
-
/**
|
|
2467
|
-
* Serialization format key sent to the server via `f=` query parameter.
|
|
2468
|
-
* TS uses plain JSON.stringify (no polymorphic type wrapping), so `json5np`
|
|
2469
|
-
* (no-polymorphism) is the correct default.
|
|
2470
|
-
*/
|
|
2471
|
-
serializationFormat;
|
|
2472
2536
|
/** Builds the WebSocket URL for this peer. Replace to customize URL construction. */
|
|
2473
2537
|
connectionUrlResolver = defaultConnectionUrlResolver;
|
|
2538
|
+
/** Optional WebSocket factory. When set, `run()` uses this instead of the
|
|
2539
|
+
* global `new WebSocket(url)` constructor. Used by tests (to inject mock
|
|
2540
|
+
* sockets) and by the Node load-test harness (to attach `Session` /
|
|
2541
|
+
* `Cookie` headers to the WS upgrade, which the browser `WebSocket`
|
|
2542
|
+
* constructor can't do). Must be set before `start()`. */
|
|
2543
|
+
webSocketFactory;
|
|
2544
|
+
/** Max time to wait for the server's handshake response before aborting
|
|
2545
|
+
* the connection attempt. Mirrors .NET's `Hub.Limits.HandshakeTimeout`. */
|
|
2546
|
+
handshakeTimeoutMs = HANDSHAKE_TIMEOUT_MS;
|
|
2474
2547
|
peerChanged = new EventHandlerSet3();
|
|
2475
2548
|
/** Fired when the server rejects the connection due to an unsupported serialization format (close code 4010). */
|
|
2476
2549
|
unsupportedFormat = new EventHandlerSet3();
|
|
2477
|
-
reconnectDelayer = new RpcClientPeerReconnectDelayer();
|
|
2478
2550
|
reconnectsAtChanged = new EventHandlerSet3();
|
|
2479
2551
|
get reconnectsAt() {
|
|
2480
2552
|
return this._reconnectsAt;
|
|
@@ -2484,17 +2556,32 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2484
2556
|
this._reconnectsAt = value;
|
|
2485
2557
|
this.reconnectsAtChanged.trigger();
|
|
2486
2558
|
}
|
|
2487
|
-
|
|
2488
|
-
|
|
2559
|
+
/** The promise returned by the reconnect loop. `undefined` until
|
|
2560
|
+
* `start()` is invoked (which happens automatically when
|
|
2561
|
+
* `mustStart = true` is passed to the ctor). */
|
|
2562
|
+
whenRunning;
|
|
2563
|
+
/**
|
|
2564
|
+
* @param hub RpcHub instance this peer will be tracked by
|
|
2565
|
+
* @param url Full WebSocket URL — the serialization format is taken from
|
|
2566
|
+
* the `f=` query parameter (e.g. `wss://host/rpc/ws?f=msgpack6`);
|
|
2567
|
+
* falls back to the resolver's default when absent. Use
|
|
2568
|
+
* {@link RpcPeerRefBuilder.forClient} to compose URLs with a format.
|
|
2569
|
+
* @param mustStart Kick off the reconnect loop automatically at the end of
|
|
2570
|
+
* construction (default `true`). Pass `false` if you need to
|
|
2571
|
+
* tweak `reconnectDelayer`, `handshakeTimeoutMs`, or similar
|
|
2572
|
+
* options before the loop starts — then call `peer.start()`
|
|
2573
|
+
* manually.
|
|
2574
|
+
*/
|
|
2575
|
+
constructor(hub, url, mustStart = true) {
|
|
2576
|
+
super(hub, url);
|
|
2489
2577
|
this.clientId = guidToBase64Url(this.id);
|
|
2490
2578
|
const resolver = RpcSerializationFormatResolver.Default;
|
|
2491
|
-
this.serializationFormat =
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
get connectionKind() {
|
|
2495
|
-
return this._connectionKind;
|
|
2579
|
+
this.serializationFormat = resolver.get(parseFormatFromUrl(url) ?? resolver.defaultFormatKey);
|
|
2580
|
+
if (mustStart)
|
|
2581
|
+
this.start();
|
|
2496
2582
|
}
|
|
2497
2583
|
/** One-shot connection for tests — no reconnection loop, no handshake.
|
|
2584
|
+
* @param conn Rpc connection
|
|
2498
2585
|
* @param isPeerChanged Whether to treat this as a peer-identity change
|
|
2499
2586
|
* (default `true` — no handshake was exchanged, so stage-3 compute calls
|
|
2500
2587
|
* self-invalidate and remote/shared objects are disposed). Pass `false`
|
|
@@ -2503,97 +2590,137 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2503
2590
|
this.setupConnection(conn);
|
|
2504
2591
|
void this._reconnect(isPeerChanged);
|
|
2505
2592
|
}
|
|
2506
|
-
/**
|
|
2507
|
-
|
|
2593
|
+
/** Kick off the reconnect loop. Idempotent — subsequent calls are
|
|
2594
|
+
* no-ops; await `peer.whenRunning` if you need the loop's completion
|
|
2595
|
+
* promise. Customize `peer.webSocketFactory` first if you need a
|
|
2596
|
+
* non-default WebSocket constructor. */
|
|
2597
|
+
start() {
|
|
2598
|
+
void this.run();
|
|
2599
|
+
}
|
|
2600
|
+
/** The reconnect loop itself. Exposed as `protected` so subclasses (and
|
|
2601
|
+
* `start()`) can drive it; public callers should use {@link start}. */
|
|
2602
|
+
run() {
|
|
2603
|
+
return this.whenRunning ??= this._runImpl();
|
|
2604
|
+
}
|
|
2605
|
+
async _runImpl() {
|
|
2606
|
+
if (this._disposed) return;
|
|
2508
2607
|
const connUrl = this.connectionUrlResolver(this);
|
|
2509
2608
|
infoLog?.log(`'${this.ref}': Started (Client)`);
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
this.setupConnection(conn);
|
|
2529
|
-
this._connection = void 0;
|
|
2530
|
-
const closedRejection = new Promise(
|
|
2531
|
-
(_, reject) => conn.closed.add(
|
|
2532
|
-
() => reject(new Error("Connection failed"))
|
|
2533
|
-
)
|
|
2534
|
-
);
|
|
2535
|
-
closedRejection.catch(() => {
|
|
2536
|
-
});
|
|
2537
|
-
await Promise.race([conn.whenConnected, closedRejection]);
|
|
2609
|
+
try {
|
|
2610
|
+
while (!this._disposed) {
|
|
2611
|
+
this._setConnectionState(1 /* Connecting */);
|
|
2612
|
+
const retryDelay = this.hub.reconnectDelayer.getDelay(this._tryIndex);
|
|
2613
|
+
if (retryDelay.isLimitExceeded) {
|
|
2614
|
+
warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
2615
|
+
this._disposed = true;
|
|
2616
|
+
break;
|
|
2617
|
+
}
|
|
2618
|
+
this._setReconnectsAt(retryDelay.endsAt);
|
|
2619
|
+
try {
|
|
2620
|
+
await retryDelay.promise;
|
|
2621
|
+
} finally {
|
|
2622
|
+
this._setReconnectsAt(0);
|
|
2623
|
+
}
|
|
2624
|
+
if (this._disposed) break;
|
|
2625
|
+
let lastCloseCode = 0;
|
|
2626
|
+
let lastCloseReason = "";
|
|
2538
2627
|
try {
|
|
2539
|
-
this.
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
this.
|
|
2544
|
-
|
|
2628
|
+
infoLog?.log(`'${this.ref}': Connecting to ${connUrl}`);
|
|
2629
|
+
const ws = this.webSocketFactory?.(connUrl) ?? new WebSocket(connUrl);
|
|
2630
|
+
const conn = new RpcWebSocketConnection(
|
|
2631
|
+
ws,
|
|
2632
|
+
this.serializationFormat.isBinary,
|
|
2633
|
+
this.serializationFormat,
|
|
2634
|
+
this.hub.registry
|
|
2545
2635
|
);
|
|
2636
|
+
const whenClosed = new PromiseSource5();
|
|
2637
|
+
const closedController = new AbortController();
|
|
2638
|
+
conn.closed.add((ev) => {
|
|
2639
|
+
lastCloseCode = ev.code;
|
|
2640
|
+
lastCloseReason = ev.reason;
|
|
2641
|
+
whenClosed.resolve(ev);
|
|
2642
|
+
closedController.abort();
|
|
2643
|
+
});
|
|
2644
|
+
const closedRejection = whenClosed.promise.then(() => {
|
|
2645
|
+
throw new Error("Connection failed");
|
|
2646
|
+
});
|
|
2647
|
+
closedRejection.catch(() => {
|
|
2648
|
+
});
|
|
2649
|
+
this._pendingHandshake = new PromiseSource5();
|
|
2650
|
+
this.setupConnection(conn);
|
|
2651
|
+
this._connection = void 0;
|
|
2652
|
+
await Promise.race([conn.whenConnected, closedRejection]);
|
|
2653
|
+
try {
|
|
2654
|
+
this.hub.systemCallSender.handshake(
|
|
2655
|
+
conn,
|
|
2656
|
+
this.serializationFormat,
|
|
2657
|
+
this.id,
|
|
2658
|
+
this.hub.hubId,
|
|
2659
|
+
++this._handshakeIndex
|
|
2660
|
+
);
|
|
2661
|
+
} catch (e) {
|
|
2662
|
+
warnLog6?.log(`'${this.ref}': Failed to send handshake`, e);
|
|
2663
|
+
throw e;
|
|
2664
|
+
}
|
|
2665
|
+
const handshakeTimeoutMs = this.handshakeTimeoutMs;
|
|
2666
|
+
let handshakeTimer;
|
|
2667
|
+
const handshakeTimeout = new Promise((_, reject) => {
|
|
2668
|
+
handshakeTimer = setTimeout(() => {
|
|
2669
|
+
reject(new Error("Handshake timeout"));
|
|
2670
|
+
}, handshakeTimeoutMs);
|
|
2671
|
+
});
|
|
2672
|
+
handshakeTimeout.catch(() => {
|
|
2673
|
+
});
|
|
2674
|
+
let remoteHandshake;
|
|
2675
|
+
try {
|
|
2676
|
+
remoteHandshake = await Promise.race([
|
|
2677
|
+
this._pendingHandshake.promise,
|
|
2678
|
+
closedRejection,
|
|
2679
|
+
handshakeTimeout
|
|
2680
|
+
]);
|
|
2681
|
+
} catch (e) {
|
|
2682
|
+
if (e instanceof Error && e.message === "Handshake timeout") {
|
|
2683
|
+
warnLog6?.log(`'${this.ref}': Handshake timed out after ${handshakeTimeoutMs}ms`);
|
|
2684
|
+
conn.close();
|
|
2685
|
+
}
|
|
2686
|
+
throw e;
|
|
2687
|
+
} finally {
|
|
2688
|
+
if (handshakeTimer !== void 0) clearTimeout(handshakeTimer);
|
|
2689
|
+
this._pendingHandshake = void 0;
|
|
2690
|
+
}
|
|
2691
|
+
this._remoteHandshakeIndex = remoteHandshake.Index ?? 0;
|
|
2692
|
+
const remoteHubId = remoteHandshake.RemoteHubId;
|
|
2693
|
+
let isPeerChanged = false;
|
|
2694
|
+
if (remoteHubId !== void 0) {
|
|
2695
|
+
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
2696
|
+
if (isPeerChanged) {
|
|
2697
|
+
this.inboundCalls.clear();
|
|
2698
|
+
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
2699
|
+
this.peerChanged.trigger();
|
|
2700
|
+
}
|
|
2701
|
+
this._lastRemoteHubId = remoteHubId;
|
|
2702
|
+
}
|
|
2703
|
+
infoLog?.log(`'${this.ref}': Handshake succeeded, peerChanged=${isPeerChanged}`);
|
|
2704
|
+
this._connection = conn;
|
|
2705
|
+
this._setConnectionState(2 /* Connected */);
|
|
2706
|
+
this._tryIndex = 0;
|
|
2707
|
+
await this._reconnect(isPeerChanged, closedController.signal);
|
|
2708
|
+
await whenClosed.promise;
|
|
2546
2709
|
} catch (e) {
|
|
2547
|
-
|
|
2548
|
-
throw e;
|
|
2710
|
+
debugLog2?.log(`'${this.ref}': run() iteration failed:`, e);
|
|
2549
2711
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
let isPeerChanged = false;
|
|
2558
|
-
if (remoteHubId !== void 0) {
|
|
2559
|
-
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
2560
|
-
if (isPeerChanged) {
|
|
2561
|
-
this.inbound.clear();
|
|
2562
|
-
infoLog?.log(`'${this.ref}': Peer changed`);
|
|
2563
|
-
this.peerChanged.trigger();
|
|
2564
|
-
}
|
|
2565
|
-
this._lastRemoteHubId = remoteHubId;
|
|
2712
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2713
|
+
if (this._disposed) break;
|
|
2714
|
+
if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
|
|
2715
|
+
warnLog6?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
|
|
2716
|
+
this.unsupportedFormat.trigger({ reason: lastCloseReason });
|
|
2717
|
+
this._disposed = true;
|
|
2718
|
+
break;
|
|
2566
2719
|
}
|
|
2567
|
-
|
|
2568
|
-
this._connection = conn;
|
|
2569
|
-
this._connectionKind = 2 /* Connected */;
|
|
2570
|
-
this._tryIndex = 0;
|
|
2571
|
-
await this._reconnect(isPeerChanged);
|
|
2572
|
-
await new Promise((r) => conn.closed.add(() => r()));
|
|
2573
|
-
} catch (e) {
|
|
2574
|
-
debugLog2?.log(`'${this.ref}': run() iteration failed:`, e);
|
|
2575
|
-
}
|
|
2576
|
-
this._connectionKind = 0 /* Disconnected */;
|
|
2577
|
-
if (this._disposed) break;
|
|
2578
|
-
if (lastCloseCode === RPC_CLOSE_CODE_UNSUPPORTED_FORMAT) {
|
|
2579
|
-
warnLog6?.log(`'${this.ref}': Unsupported serialization format - shutting down`);
|
|
2580
|
-
this.unsupportedFormat.trigger({ reason: lastCloseReason });
|
|
2581
|
-
this._disposed = true;
|
|
2582
|
-
break;
|
|
2583
|
-
}
|
|
2584
|
-
this._tryIndex++;
|
|
2585
|
-
const delay = this.reconnectDelayer.getDelay(this._tryIndex);
|
|
2586
|
-
if (delay.isLimitExceeded) {
|
|
2587
|
-
warnLog6?.log(`'${this.ref}': Reconnect limit exceeded - shutting down`);
|
|
2588
|
-
this._disposed = true;
|
|
2589
|
-
break;
|
|
2590
|
-
}
|
|
2591
|
-
this._setReconnectsAt(delay.endsAt);
|
|
2592
|
-
try {
|
|
2593
|
-
await delay.promise;
|
|
2594
|
-
} finally {
|
|
2595
|
-
this._setReconnectsAt(0);
|
|
2720
|
+
this._tryIndex++;
|
|
2596
2721
|
}
|
|
2722
|
+
} finally {
|
|
2723
|
+
this._setConnectionState(0 /* Disconnected */);
|
|
2597
2724
|
}
|
|
2598
2725
|
}
|
|
2599
2726
|
/**
|
|
@@ -2615,7 +2742,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2615
2742
|
* are always self-invalidated — TS doesn't track compute-call stages
|
|
2616
2743
|
* across reconnect, so the safest behavior is to force a fresh compute.
|
|
2617
2744
|
*/
|
|
2618
|
-
async _reconnect(isPeerChanged) {
|
|
2745
|
+
async _reconnect(isPeerChanged, closedSignal) {
|
|
2619
2746
|
if (this._connection === void 0) return;
|
|
2620
2747
|
if (isPeerChanged) {
|
|
2621
2748
|
this.remoteObjects.disconnectAll();
|
|
@@ -2624,7 +2751,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2624
2751
|
this.remoteObjects.reconnectAll();
|
|
2625
2752
|
}
|
|
2626
2753
|
const eligible = [];
|
|
2627
|
-
for (const call of [...this.
|
|
2754
|
+
for (const call of [...this.outboundCalls.values()]) {
|
|
2628
2755
|
const mode = call.remoteExecutionMode;
|
|
2629
2756
|
if (!(mode & 2)) {
|
|
2630
2757
|
if (!call.result.isCompleted)
|
|
@@ -2632,7 +2759,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2632
2759
|
"Outbound call failed: disconnected and AllowReconnect is not set."
|
|
2633
2760
|
));
|
|
2634
2761
|
call.onDisconnect();
|
|
2635
|
-
this.
|
|
2762
|
+
this.outboundCalls.remove(call.callId);
|
|
2636
2763
|
continue;
|
|
2637
2764
|
}
|
|
2638
2765
|
if (isPeerChanged && !(mode & 4)) {
|
|
@@ -2641,19 +2768,19 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2641
2768
|
"Outbound call failed: reconnected to a different peer and AllowResend is not set."
|
|
2642
2769
|
));
|
|
2643
2770
|
call.onDisconnect();
|
|
2644
|
-
this.
|
|
2771
|
+
this.outboundCalls.remove(call.callId);
|
|
2645
2772
|
continue;
|
|
2646
2773
|
}
|
|
2647
2774
|
eligible.push(call);
|
|
2648
2775
|
}
|
|
2649
2776
|
let unknownIds = null;
|
|
2650
2777
|
if (!isPeerChanged && eligible.length > 0) {
|
|
2651
|
-
unknownIds = await this._reconcileReconnect(eligible);
|
|
2778
|
+
unknownIds = await this._reconcileReconnect(eligible, closedSignal);
|
|
2652
2779
|
}
|
|
2653
2780
|
for (const call of eligible) {
|
|
2654
2781
|
if (!call.removeOnOk && call.result.isCompleted) {
|
|
2655
2782
|
call.onDisconnect();
|
|
2656
|
-
this.
|
|
2783
|
+
this.outboundCalls.remove(call.callId);
|
|
2657
2784
|
continue;
|
|
2658
2785
|
}
|
|
2659
2786
|
if (unknownIds !== null && !unknownIds.has(call.callId)) {
|
|
@@ -2676,7 +2803,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2676
2803
|
* native msgpack `map<int, bin>`, matching .NET MessagePack-CSharp's
|
|
2677
2804
|
* default for `Dictionary<int, byte[]>`.
|
|
2678
2805
|
*/
|
|
2679
|
-
async _reconcileReconnect(eligible) {
|
|
2806
|
+
async _reconcileReconnect(eligible, closedSignal) {
|
|
2680
2807
|
try {
|
|
2681
2808
|
const byStage = /* @__PURE__ */ new Map();
|
|
2682
2809
|
for (const call of eligible) {
|
|
@@ -2692,7 +2819,7 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2692
2819
|
if (byStage.size === 0) {
|
|
2693
2820
|
return new Set(eligible.map((c) => c.callId));
|
|
2694
2821
|
}
|
|
2695
|
-
const isBinary = this.
|
|
2822
|
+
const isBinary = this.serializationFormat.isBinary;
|
|
2696
2823
|
const stagesArg = isBinary ? /* @__PURE__ */ new Map() : {};
|
|
2697
2824
|
for (const [stage, ids] of byStage) {
|
|
2698
2825
|
ids.sort((a, b) => a - b);
|
|
@@ -2707,8 +2834,9 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2707
2834
|
RpcSystemCalls.reconnect,
|
|
2708
2835
|
[this._remoteHandshakeIndex, stagesArg],
|
|
2709
2836
|
{
|
|
2710
|
-
remoteExecutionMode: 1
|
|
2837
|
+
remoteExecutionMode: 1,
|
|
2711
2838
|
/* AwaitForConnection only; must not itself reconnect */
|
|
2839
|
+
signal: closedSignal
|
|
2712
2840
|
}
|
|
2713
2841
|
);
|
|
2714
2842
|
const result = await reconnectCall.result.promise;
|
|
@@ -2728,11 +2856,14 @@ var RpcClientPeer = class extends RpcPeer {
|
|
|
2728
2856
|
}
|
|
2729
2857
|
};
|
|
2730
2858
|
var RpcServerPeer = class extends RpcPeer {
|
|
2859
|
+
// Needed to widen the base `protected constructor` to public.
|
|
2860
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
2731
2861
|
constructor(hub, ref) {
|
|
2732
|
-
super(
|
|
2862
|
+
super(hub, ref);
|
|
2733
2863
|
}
|
|
2734
2864
|
/** Accept an incoming connection — sets up message handling and handshake response. */
|
|
2735
2865
|
accept(conn) {
|
|
2866
|
+
this._setConnectionState(1 /* Connecting */);
|
|
2736
2867
|
this.setupConnection(conn);
|
|
2737
2868
|
conn.closed.add(() => {
|
|
2738
2869
|
this.sharedObjects.disconnectAll();
|
|
@@ -2740,13 +2871,14 @@ var RpcServerPeer = class extends RpcPeer {
|
|
|
2740
2871
|
}
|
|
2741
2872
|
_onHandshakeReceived(_handshake) {
|
|
2742
2873
|
if (this._connection !== void 0) {
|
|
2743
|
-
this.
|
|
2874
|
+
this.hub.systemCallSender.handshake(
|
|
2744
2875
|
this._connection,
|
|
2745
|
-
this.
|
|
2876
|
+
this.serializationFormat,
|
|
2746
2877
|
this.id,
|
|
2747
|
-
this.
|
|
2878
|
+
this.hub.hubId,
|
|
2748
2879
|
0
|
|
2749
2880
|
);
|
|
2881
|
+
this._setConnectionState(2 /* Connected */);
|
|
2750
2882
|
}
|
|
2751
2883
|
}
|
|
2752
2884
|
};
|
|
@@ -2778,6 +2910,15 @@ function _toBytes2(value) {
|
|
|
2778
2910
|
return null;
|
|
2779
2911
|
}
|
|
2780
2912
|
|
|
2913
|
+
// src/rpc-client-peer-reconnect-delayer.ts
|
|
2914
|
+
import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";
|
|
2915
|
+
var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
2916
|
+
constructor() {
|
|
2917
|
+
super();
|
|
2918
|
+
this.delays = RetryDelaySeq.fixed(100);
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
|
|
2781
2922
|
// src/rpc-service-host.ts
|
|
2782
2923
|
var { warnLog: warnLog7 } = getLogs("RpcServiceHost");
|
|
2783
2924
|
var RpcServiceHost = class {
|
|
@@ -3380,6 +3521,16 @@ var RpcHub = class {
|
|
|
3380
3521
|
/** Method registry for compact format hash ↔ name resolution.
|
|
3381
3522
|
* Created lazily, populated by addService/addClient. */
|
|
3382
3523
|
registry = new RpcMethodRegistry();
|
|
3524
|
+
/** Shared reconnect delayer used by every client peer in this hub. Swap in
|
|
3525
|
+
* a custom subclass (e.g. app-level signal-gated) before peers start. */
|
|
3526
|
+
reconnectDelayer = new RpcClientPeerReconnectDelayer();
|
|
3527
|
+
/** URL used by {@link defaultPeer} to resolve / create the default client
|
|
3528
|
+
* peer. Must be set before the first {@link defaultPeer} access. */
|
|
3529
|
+
defaultPeerUrl;
|
|
3530
|
+
/** Factory used by {@link defaultPeer} when the peer needs to be created.
|
|
3531
|
+
* If left undefined, `getPeer`'s built-in default (RpcClientPeer with
|
|
3532
|
+
* format derived from URL or resolver default) is used. */
|
|
3533
|
+
defaultPeerFactory;
|
|
3383
3534
|
constructor(hubId) {
|
|
3384
3535
|
this.hubId = hubId ?? crypto.randomUUID();
|
|
3385
3536
|
this.serviceHost = new RpcServiceHost();
|
|
@@ -3388,7 +3539,20 @@ var RpcHub = class {
|
|
|
3388
3539
|
this.registry.register(methodName);
|
|
3389
3540
|
}
|
|
3390
3541
|
}
|
|
3542
|
+
/** Shortcut for the hub's default client peer. Fails if
|
|
3543
|
+
* {@link defaultPeerUrl} is not set. */
|
|
3544
|
+
get defaultPeer() {
|
|
3545
|
+
if (this.defaultPeerUrl === void 0)
|
|
3546
|
+
throw new Error("RpcHub.defaultPeerUrl is not set.");
|
|
3547
|
+
return this.getClientPeer(this.defaultPeerUrl, this.defaultPeerFactory);
|
|
3548
|
+
}
|
|
3549
|
+
/** Register a peer under its ref. If a different peer is already registered
|
|
3550
|
+
* at the same ref, close it first — the hub guarantees at most one live
|
|
3551
|
+
* peer per ref, and no peer exists outside the hub. */
|
|
3391
3552
|
addPeer(peer) {
|
|
3553
|
+
const existing = this.peers.get(peer.ref);
|
|
3554
|
+
if (existing !== void 0 && existing !== peer)
|
|
3555
|
+
existing.close();
|
|
3392
3556
|
this.peers.set(peer.ref, peer);
|
|
3393
3557
|
}
|
|
3394
3558
|
removePeer(ref) {
|
|
@@ -3399,21 +3563,27 @@ var RpcHub = class {
|
|
|
3399
3563
|
warnLog9?.log(`'${ref}': peer is removed from RpcHub`);
|
|
3400
3564
|
}
|
|
3401
3565
|
}
|
|
3402
|
-
/** Get or create a peer by ref.
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3566
|
+
/** Get or create a peer by ref. If the peer does not exist:
|
|
3567
|
+
* - with `factory` provided: it is constructed via `factory(this, ref)`;
|
|
3568
|
+
* - otherwise: server peers (ref starts with `server://`) get a plain
|
|
3569
|
+
* RpcServerPeer, anything else gets a default-configured RpcClientPeer.
|
|
3570
|
+
* The created peer is always registered in this hub. */
|
|
3571
|
+
getPeer(ref, factory) {
|
|
3572
|
+
const existing = this.peers.get(ref);
|
|
3573
|
+
if (existing) return existing;
|
|
3574
|
+
const peer = factory ? factory(this, ref) : ref.startsWith("server://") ? new RpcServerPeer(this, ref) : new RpcClientPeer(this, ref);
|
|
3407
3575
|
this.addPeer(peer);
|
|
3408
3576
|
return peer;
|
|
3409
3577
|
}
|
|
3410
|
-
/** Get or create a client peer for the given URL.
|
|
3411
|
-
|
|
3412
|
-
|
|
3578
|
+
/** Get or create a client peer for the given URL. See {@link getPeer} for
|
|
3579
|
+
* the factory semantics. */
|
|
3580
|
+
getClientPeer(ref, factory) {
|
|
3581
|
+
return this.getPeer(ref, factory);
|
|
3413
3582
|
}
|
|
3414
|
-
/** Get or create a server peer for the given ref.
|
|
3415
|
-
|
|
3416
|
-
|
|
3583
|
+
/** Get or create a server peer for the given ref. See {@link getPeer} for
|
|
3584
|
+
* the factory semantics. */
|
|
3585
|
+
getServerPeer(ref, factory) {
|
|
3586
|
+
return this.getPeer(ref, factory);
|
|
3417
3587
|
}
|
|
3418
3588
|
/** Register a service with optional server method wrapping for custom call types. */
|
|
3419
3589
|
addService(defOrContract, impl) {
|
|
@@ -3542,6 +3712,25 @@ var RpcHub = class {
|
|
|
3542
3712
|
}
|
|
3543
3713
|
};
|
|
3544
3714
|
|
|
3715
|
+
// src/rpc-peer-ref-builder.ts
|
|
3716
|
+
var RpcPeerRefBuilder = class {
|
|
3717
|
+
/** Build a client peer ref from a WebSocket URL, optionally baking in the
|
|
3718
|
+
* serialization format as a `f=` query parameter.
|
|
3719
|
+
* @param url Base WebSocket URL, e.g. `wss://host/rpc/ws`.
|
|
3720
|
+
* @param serializationFormat Optional format key, e.g. `msgpack6`. When
|
|
3721
|
+
* provided, replaces any existing `f=` in the URL. */
|
|
3722
|
+
static forClient(url, serializationFormat) {
|
|
3723
|
+
if (serializationFormat === void 0) return url;
|
|
3724
|
+
const u = new URL(url);
|
|
3725
|
+
u.searchParams.set("f", serializationFormat);
|
|
3726
|
+
return u.toString();
|
|
3727
|
+
}
|
|
3728
|
+
/** Build a server peer ref from a peer id — returns `server://{id}`. */
|
|
3729
|
+
static forServer(id) {
|
|
3730
|
+
return `server://${id}`;
|
|
3731
|
+
}
|
|
3732
|
+
};
|
|
3733
|
+
|
|
3545
3734
|
// src/rpc-client.ts
|
|
3546
3735
|
function createRpcClient(peer, serviceDef) {
|
|
3547
3736
|
const overloads = /* @__PURE__ */ new Map();
|
|
@@ -3643,20 +3832,18 @@ var RpcPeerStateMonitor = class {
|
|
|
3643
3832
|
_state;
|
|
3644
3833
|
_timer;
|
|
3645
3834
|
// Arrow functions to preserve `this` when used as event handlers
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
if (wasConnected) {
|
|
3659
|
-
this._disconnectedAt = Date.now();
|
|
3835
|
+
_onConnectionStateChanged = (state) => {
|
|
3836
|
+
if (state === 2 /* Connected */) {
|
|
3837
|
+
this._rawConnected = true;
|
|
3838
|
+
this._connectedAt = Date.now();
|
|
3839
|
+
this._lastError = void 0;
|
|
3840
|
+
} else {
|
|
3841
|
+
const wasConnected = this._rawConnected;
|
|
3842
|
+
this._rawConnected = false;
|
|
3843
|
+
this._lastError = new Error("Connection closed");
|
|
3844
|
+
if (wasConnected) {
|
|
3845
|
+
this._disconnectedAt = Date.now();
|
|
3846
|
+
}
|
|
3660
3847
|
}
|
|
3661
3848
|
this._recompute();
|
|
3662
3849
|
};
|
|
@@ -3682,10 +3869,9 @@ var RpcPeerStateMonitor = class {
|
|
|
3682
3869
|
reconnectsIn: 0
|
|
3683
3870
|
};
|
|
3684
3871
|
}
|
|
3685
|
-
peer.
|
|
3686
|
-
peer.disconnected.add(this._onDisconnected);
|
|
3872
|
+
peer.connectionStateChanged.add(this._onConnectionStateChanged);
|
|
3687
3873
|
peer.reconnectsAtChanged.add(this._onReconnectsAtChanged);
|
|
3688
|
-
peer.reconnectDelayer.cancelDelaysChanged.add(
|
|
3874
|
+
peer.hub.reconnectDelayer.cancelDelaysChanged.add(
|
|
3689
3875
|
this._onCancelDelaysChanged
|
|
3690
3876
|
);
|
|
3691
3877
|
this._scheduleRecompute(
|
|
@@ -3701,10 +3887,9 @@ var RpcPeerStateMonitor = class {
|
|
|
3701
3887
|
clearTimeout(this._timer);
|
|
3702
3888
|
this._timer = void 0;
|
|
3703
3889
|
}
|
|
3704
|
-
this.peer.
|
|
3705
|
-
this.peer.disconnected.remove(this._onDisconnected);
|
|
3890
|
+
this.peer.connectionStateChanged.remove(this._onConnectionStateChanged);
|
|
3706
3891
|
this.peer.reconnectsAtChanged.remove(this._onReconnectsAtChanged);
|
|
3707
|
-
this.peer.reconnectDelayer.cancelDelaysChanged.remove(
|
|
3892
|
+
this.peer.hub.reconnectDelayer.cancelDelaysChanged.remove(
|
|
3708
3893
|
this._onCancelDelaysChanged
|
|
3709
3894
|
);
|
|
3710
3895
|
infoLog2?.log(`'${this.peer.ref}': monitor stopped`);
|
|
@@ -3786,12 +3971,14 @@ export {
|
|
|
3786
3971
|
ARG_DELIMITER,
|
|
3787
3972
|
ENVELOPE_DELIMITER,
|
|
3788
3973
|
FRAME_DELIMITER,
|
|
3974
|
+
HANDSHAKE_TIMEOUT_MS,
|
|
3789
3975
|
IncreasingSeqCompressor,
|
|
3790
3976
|
RPC_CLOSE_CODE_UNSUPPORTED_FORMAT,
|
|
3791
3977
|
RpcCallStage,
|
|
3792
3978
|
RpcCallTypeId,
|
|
3793
3979
|
RpcClientPeer,
|
|
3794
3980
|
RpcClientPeerReconnectDelayer,
|
|
3981
|
+
RpcConnectionState,
|
|
3795
3982
|
RpcHub,
|
|
3796
3983
|
RpcInboundCall,
|
|
3797
3984
|
RpcInboundCallTracker,
|
|
@@ -3804,7 +3991,7 @@ export {
|
|
|
3804
3991
|
RpcOutboundCall,
|
|
3805
3992
|
RpcOutboundCallTracker,
|
|
3806
3993
|
RpcPeer,
|
|
3807
|
-
|
|
3994
|
+
RpcPeerRefBuilder,
|
|
3808
3995
|
RpcPeerStateKind,
|
|
3809
3996
|
RpcPeerStateMonitor,
|
|
3810
3997
|
RpcRemoteExecutionMode,
|