@actuallab/rpc 12.1.62
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 +1200 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +440 -0
- package/dist/index.d.ts +440 -0
- package/dist/index.js +1133 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
// src/rpc-message.ts
|
|
2
|
+
var RpcCallTypeId = /* @__PURE__ */ ((RpcCallTypeId2) => {
|
|
3
|
+
RpcCallTypeId2[RpcCallTypeId2["Regular"] = 0] = "Regular";
|
|
4
|
+
RpcCallTypeId2[RpcCallTypeId2["Reliable"] = 1] = "Reliable";
|
|
5
|
+
return RpcCallTypeId2;
|
|
6
|
+
})(RpcCallTypeId || {});
|
|
7
|
+
var RpcSystemCalls = {
|
|
8
|
+
handshake: "$sys.Handshake:1",
|
|
9
|
+
ok: "$sys.Ok:1",
|
|
10
|
+
error: "$sys.Error:1",
|
|
11
|
+
cancel: "$sys.Cancel:0",
|
|
12
|
+
keepAlive: "$sys.KeepAlive:1",
|
|
13
|
+
reconnect: "$sys.Reconnect:3"
|
|
14
|
+
};
|
|
15
|
+
var ENVELOPE_DELIMITER = "\n";
|
|
16
|
+
var ARG_DELIMITER = "";
|
|
17
|
+
var FRAME_DELIMITER = "\n";
|
|
18
|
+
|
|
19
|
+
// src/rpc-serialization.ts
|
|
20
|
+
function serializeMessage(message, args) {
|
|
21
|
+
const envelope = JSON.stringify(message);
|
|
22
|
+
if (args === void 0 || args.length === 0) {
|
|
23
|
+
return envelope + ENVELOPE_DELIMITER;
|
|
24
|
+
}
|
|
25
|
+
const argsStr = args.map((a) => JSON.stringify(a)).join(ARG_DELIMITER);
|
|
26
|
+
return envelope + ENVELOPE_DELIMITER + argsStr;
|
|
27
|
+
}
|
|
28
|
+
function serializeFrame(messages) {
|
|
29
|
+
return messages.join(FRAME_DELIMITER);
|
|
30
|
+
}
|
|
31
|
+
function splitFrame(frame) {
|
|
32
|
+
return frame.split(FRAME_DELIMITER);
|
|
33
|
+
}
|
|
34
|
+
function deserializeMessage(raw) {
|
|
35
|
+
const nlIndex = raw.indexOf(ENVELOPE_DELIMITER);
|
|
36
|
+
if (nlIndex === -1) {
|
|
37
|
+
return { message: JSON.parse(raw), args: [] };
|
|
38
|
+
}
|
|
39
|
+
const envelopeStr = raw.substring(0, nlIndex);
|
|
40
|
+
const argsStr = raw.substring(nlIndex + 1);
|
|
41
|
+
const message = JSON.parse(envelopeStr);
|
|
42
|
+
if (argsStr.length === 0) {
|
|
43
|
+
return { message, args: [] };
|
|
44
|
+
}
|
|
45
|
+
const args = argsStr.split(ARG_DELIMITER).map((s) => {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(s);
|
|
48
|
+
} catch {
|
|
49
|
+
return s;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return { message, args };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/rpc-connection.ts
|
|
56
|
+
import { PromiseSource, EventHandlerSet } from "@actuallab/core";
|
|
57
|
+
var WebSocketState = {
|
|
58
|
+
CONNECTING: 0,
|
|
59
|
+
OPEN: 1,
|
|
60
|
+
CLOSING: 2,
|
|
61
|
+
CLOSED: 3
|
|
62
|
+
};
|
|
63
|
+
var RpcWebSocketConnection = class {
|
|
64
|
+
_ws;
|
|
65
|
+
_sendBuffer = [];
|
|
66
|
+
_connected = new PromiseSource();
|
|
67
|
+
messageReceived = new EventHandlerSet();
|
|
68
|
+
closed = new EventHandlerSet();
|
|
69
|
+
error = new EventHandlerSet();
|
|
70
|
+
constructor(ws) {
|
|
71
|
+
this._ws = ws;
|
|
72
|
+
if (ws.readyState === WebSocketState.OPEN) {
|
|
73
|
+
this._connected.resolve();
|
|
74
|
+
this._flush();
|
|
75
|
+
}
|
|
76
|
+
ws.onopen = () => {
|
|
77
|
+
this._connected.resolve();
|
|
78
|
+
this._flush();
|
|
79
|
+
};
|
|
80
|
+
ws.onmessage = (ev) => {
|
|
81
|
+
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
82
|
+
const messages = splitFrame(data);
|
|
83
|
+
for (const msg of messages) {
|
|
84
|
+
if (msg.length > 0) this.messageReceived.trigger(msg);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
ws.onclose = (ev) => {
|
|
88
|
+
this.closed.trigger({ code: ev.code, reason: ev.reason });
|
|
89
|
+
};
|
|
90
|
+
ws.onerror = (ev) => {
|
|
91
|
+
this.error.trigger(ev);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
get isOpen() {
|
|
95
|
+
return this._ws.readyState === WebSocketState.OPEN;
|
|
96
|
+
}
|
|
97
|
+
get whenConnected() {
|
|
98
|
+
return this._connected.promise;
|
|
99
|
+
}
|
|
100
|
+
send(serializedMessage) {
|
|
101
|
+
try {
|
|
102
|
+
if (this._ws.readyState === WebSocketState.OPEN)
|
|
103
|
+
this._ws.send(serializedMessage);
|
|
104
|
+
else if (this._ws.readyState === WebSocketState.CONNECTING)
|
|
105
|
+
this._sendBuffer.push(serializedMessage);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
sendBatch(messages) {
|
|
110
|
+
const frame = serializeFrame(messages);
|
|
111
|
+
this.send(frame);
|
|
112
|
+
}
|
|
113
|
+
close(code, reason) {
|
|
114
|
+
this._ws.close(code, reason);
|
|
115
|
+
}
|
|
116
|
+
_flush() {
|
|
117
|
+
if (this._sendBuffer.length === 0) return;
|
|
118
|
+
const frame = serializeFrame(this._sendBuffer);
|
|
119
|
+
this._sendBuffer = [];
|
|
120
|
+
try {
|
|
121
|
+
this._ws.send(frame);
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/rpc-message-channel-connection.ts
|
|
128
|
+
import { EventHandlerSet as EventHandlerSet2 } from "@actuallab/core";
|
|
129
|
+
var RpcMessageChannelConnection = class {
|
|
130
|
+
_port;
|
|
131
|
+
_open = true;
|
|
132
|
+
messageReceived = new EventHandlerSet2();
|
|
133
|
+
closed = new EventHandlerSet2();
|
|
134
|
+
whenConnected = Promise.resolve();
|
|
135
|
+
// immediately connected
|
|
136
|
+
constructor(port) {
|
|
137
|
+
this._port = port;
|
|
138
|
+
port.onmessage = (ev) => {
|
|
139
|
+
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
140
|
+
for (const msg of splitFrame(data))
|
|
141
|
+
if (msg.length > 0) this.messageReceived.trigger(msg);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
get isOpen() {
|
|
145
|
+
return this._open;
|
|
146
|
+
}
|
|
147
|
+
send(serializedMessage) {
|
|
148
|
+
if (!this._open) return;
|
|
149
|
+
try {
|
|
150
|
+
this._port.postMessage(serializedMessage);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
close(code, reason) {
|
|
155
|
+
if (!this._open) return;
|
|
156
|
+
this._open = false;
|
|
157
|
+
this._port.close();
|
|
158
|
+
this.closed.trigger({ code: code ?? 1e3, reason: reason ?? "" });
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function createMessageChannelPair() {
|
|
162
|
+
const channel = new MessageChannel();
|
|
163
|
+
return [
|
|
164
|
+
new RpcMessageChannelConnection(channel.port1),
|
|
165
|
+
new RpcMessageChannelConnection(channel.port2)
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/rpc-service-def.ts
|
|
170
|
+
var RpcType = {
|
|
171
|
+
object: /* @__PURE__ */ Symbol("object"),
|
|
172
|
+
stream: /* @__PURE__ */ Symbol("stream"),
|
|
173
|
+
void: /* @__PURE__ */ Symbol("void")
|
|
174
|
+
};
|
|
175
|
+
function wireMethodName(def) {
|
|
176
|
+
return `${def.serviceName}.${def.name}:${def.wireArgCount}`;
|
|
177
|
+
}
|
|
178
|
+
function defineRpcService(name, methods, options) {
|
|
179
|
+
const ctOffset = options?.ctOffset ?? 0;
|
|
180
|
+
const map = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const [key, input] of Object.entries(methods)) {
|
|
182
|
+
const colonIndex = key.indexOf(":");
|
|
183
|
+
const cleanName = colonIndex >= 0 ? key.substring(0, colonIndex) : key;
|
|
184
|
+
const wireArgCount = input.args.length + ctOffset;
|
|
185
|
+
const mapKey = `${cleanName}:${wireArgCount}`;
|
|
186
|
+
map.set(mapKey, {
|
|
187
|
+
name: cleanName,
|
|
188
|
+
serviceName: name,
|
|
189
|
+
argCount: input.args.length,
|
|
190
|
+
wireArgCount,
|
|
191
|
+
callTypeId: input.callTypeId ?? 0,
|
|
192
|
+
stream: input.stream ?? input.returns === RpcType.stream,
|
|
193
|
+
noWait: input.noWait ?? false
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return { name, methods: map };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/rpc-call-tracker.ts
|
|
200
|
+
import { PromiseSource as PromiseSource2 } from "@actuallab/core";
|
|
201
|
+
var RpcOutboundCall = class {
|
|
202
|
+
callId;
|
|
203
|
+
method;
|
|
204
|
+
result = new PromiseSource2();
|
|
205
|
+
/** Whether to remove this call from the tracker on $sys.Ok. Default: true.
|
|
206
|
+
* Subclasses (e.g. compute calls) override to false to stay in tracker for invalidation. */
|
|
207
|
+
removeOnOk = true;
|
|
208
|
+
/** The serialized wire message — stored for re-sending on reconnect. */
|
|
209
|
+
serializedMessage = "";
|
|
210
|
+
constructor(callId, method) {
|
|
211
|
+
this.callId = callId;
|
|
212
|
+
this.method = method;
|
|
213
|
+
}
|
|
214
|
+
/** Called when the connection is lost. Subclasses can override to resolve invalidation promises. */
|
|
215
|
+
onDisconnect() {
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var RpcOutboundCallTracker = class {
|
|
219
|
+
_calls = /* @__PURE__ */ new Map();
|
|
220
|
+
_nextId = 1;
|
|
221
|
+
get size() {
|
|
222
|
+
return this._calls.size;
|
|
223
|
+
}
|
|
224
|
+
nextId() {
|
|
225
|
+
return this._nextId++;
|
|
226
|
+
}
|
|
227
|
+
register(call) {
|
|
228
|
+
this._calls.set(call.callId, call);
|
|
229
|
+
}
|
|
230
|
+
get(callId) {
|
|
231
|
+
return this._calls.get(callId);
|
|
232
|
+
}
|
|
233
|
+
remove(callId) {
|
|
234
|
+
const call = this._calls.get(callId);
|
|
235
|
+
if (call !== void 0) this._calls.delete(callId);
|
|
236
|
+
return call;
|
|
237
|
+
}
|
|
238
|
+
values() {
|
|
239
|
+
return this._calls.values();
|
|
240
|
+
}
|
|
241
|
+
activeCallIds() {
|
|
242
|
+
return [...this._calls.keys()];
|
|
243
|
+
}
|
|
244
|
+
/** Reject all pending calls with the given error.
|
|
245
|
+
* Stage-3 compute calls (result resolved, awaiting invalidation) are kept in the tracker. */
|
|
246
|
+
rejectAll(error) {
|
|
247
|
+
for (const [id, call] of this._calls) {
|
|
248
|
+
if (!call.removeOnOk && call.result.isCompleted) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
call.result.reject(error);
|
|
252
|
+
call.onDisconnect();
|
|
253
|
+
this._calls.delete(id);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Invalidate all remaining stage-3 calls (on reconnect or peer stop). */
|
|
257
|
+
invalidateAll() {
|
|
258
|
+
for (const call of this._calls.values()) {
|
|
259
|
+
call.onDisconnect();
|
|
260
|
+
}
|
|
261
|
+
this._calls.clear();
|
|
262
|
+
}
|
|
263
|
+
clear() {
|
|
264
|
+
this._calls.clear();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
var RpcInboundCall = class {
|
|
268
|
+
callId;
|
|
269
|
+
method;
|
|
270
|
+
args;
|
|
271
|
+
constructor(callId, method, args) {
|
|
272
|
+
this.callId = callId;
|
|
273
|
+
this.method = method;
|
|
274
|
+
this.args = args;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
var RpcInboundCallTracker = class {
|
|
278
|
+
_calls = /* @__PURE__ */ new Map();
|
|
279
|
+
get size() {
|
|
280
|
+
return this._calls.size;
|
|
281
|
+
}
|
|
282
|
+
register(call) {
|
|
283
|
+
this._calls.set(call.callId, call);
|
|
284
|
+
}
|
|
285
|
+
get(callId) {
|
|
286
|
+
return this._calls.get(callId);
|
|
287
|
+
}
|
|
288
|
+
remove(callId) {
|
|
289
|
+
const call = this._calls.get(callId);
|
|
290
|
+
if (call !== void 0) this._calls.delete(callId);
|
|
291
|
+
return call;
|
|
292
|
+
}
|
|
293
|
+
clear() {
|
|
294
|
+
this._calls.clear();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// src/rpc-system-call-handler.ts
|
|
299
|
+
function handleSystemCall(message, args, outboundTracker, inboundTracker) {
|
|
300
|
+
const method = message.Method;
|
|
301
|
+
const relatedId = message.RelatedId ?? 0;
|
|
302
|
+
switch (method) {
|
|
303
|
+
case RpcSystemCalls.ok: {
|
|
304
|
+
const call = outboundTracker.get(relatedId);
|
|
305
|
+
if (call !== void 0) {
|
|
306
|
+
if (call.removeOnOk) {
|
|
307
|
+
outboundTracker.remove(relatedId);
|
|
308
|
+
}
|
|
309
|
+
call.result.resolve(args[0]);
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case RpcSystemCalls.error: {
|
|
314
|
+
const call = outboundTracker.remove(relatedId);
|
|
315
|
+
if (call !== void 0) {
|
|
316
|
+
const errorInfo = args[0];
|
|
317
|
+
const msg = errorInfo?.Message ?? errorInfo?.message ?? "RPC error";
|
|
318
|
+
call.result.reject(new Error(msg));
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case RpcSystemCalls.cancel: {
|
|
323
|
+
inboundTracker.remove(relatedId);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case RpcSystemCalls.keepAlive: {
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/rpc-system-call-sender.ts
|
|
333
|
+
var RpcSystemCallSender = class {
|
|
334
|
+
handshake(conn, peerId, hubId, index) {
|
|
335
|
+
const msg = serializeMessage({ Method: RpcSystemCalls.handshake }, [{
|
|
336
|
+
RemotePeerId: peerId,
|
|
337
|
+
RemoteApiVersionSet: null,
|
|
338
|
+
RemoteHubId: hubId,
|
|
339
|
+
ProtocolVersion: 2,
|
|
340
|
+
Index: index
|
|
341
|
+
}]);
|
|
342
|
+
conn.send(msg);
|
|
343
|
+
}
|
|
344
|
+
ok(conn, relatedId, result) {
|
|
345
|
+
const msg = serializeMessage({ Method: RpcSystemCalls.ok, RelatedId: relatedId }, [result]);
|
|
346
|
+
conn.send(msg);
|
|
347
|
+
}
|
|
348
|
+
error(conn, relatedId, error) {
|
|
349
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
350
|
+
const msg = serializeMessage(
|
|
351
|
+
{ Method: RpcSystemCalls.error, RelatedId: relatedId },
|
|
352
|
+
[{ Message: message }]
|
|
353
|
+
);
|
|
354
|
+
conn.send(msg);
|
|
355
|
+
}
|
|
356
|
+
cancel(conn, relatedId) {
|
|
357
|
+
const msg = serializeMessage({ Method: RpcSystemCalls.cancel, RelatedId: relatedId });
|
|
358
|
+
conn.send(msg);
|
|
359
|
+
}
|
|
360
|
+
keepAlive(conn, activeCallIds) {
|
|
361
|
+
const msg = serializeMessage({ Method: RpcSystemCalls.keepAlive }, [activeCallIds]);
|
|
362
|
+
conn.send(msg);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/rpc-peer.ts
|
|
367
|
+
import { EventHandlerSet as EventHandlerSet3, PromiseSource as PromiseSource3 } from "@actuallab/core";
|
|
368
|
+
|
|
369
|
+
// src/rpc-client-peer-reconnect-delayer.ts
|
|
370
|
+
import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";
|
|
371
|
+
var RpcClientPeerReconnectDelayer = class extends RetryDelayer {
|
|
372
|
+
constructor() {
|
|
373
|
+
super();
|
|
374
|
+
this.delays = RetryDelaySeq.exp(1e3, 6e4);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/rpc-peer.ts
|
|
379
|
+
var DEFAULT_SERIALIZATION_FORMAT = "json5np";
|
|
380
|
+
var defaultConnectionUrlResolver = (peer) => {
|
|
381
|
+
const sep = peer.ref.includes("?") ? "&" : "?";
|
|
382
|
+
return peer.ref + sep + `clientId=${peer.clientId}&f=${peer.serializationFormat}`;
|
|
383
|
+
};
|
|
384
|
+
var RpcPeer = class {
|
|
385
|
+
/** Routing/addressing key — used as key in hub.peers (URL for clients, "server://{uuid}" for servers). */
|
|
386
|
+
ref;
|
|
387
|
+
/** Auto-generated GUID — used in handshakes only. */
|
|
388
|
+
id = crypto.randomUUID();
|
|
389
|
+
_hub;
|
|
390
|
+
_connection;
|
|
391
|
+
outbound = new RpcOutboundCallTracker();
|
|
392
|
+
inbound = new RpcInboundCallTracker();
|
|
393
|
+
connected = new EventHandlerSet3();
|
|
394
|
+
disconnected = new EventHandlerSet3();
|
|
395
|
+
_pendingSends = [];
|
|
396
|
+
_keepAliveTimer;
|
|
397
|
+
constructor(ref, hub) {
|
|
398
|
+
this.ref = ref;
|
|
399
|
+
this._hub = hub;
|
|
400
|
+
}
|
|
401
|
+
get connection() {
|
|
402
|
+
return this._connection;
|
|
403
|
+
}
|
|
404
|
+
get isConnected() {
|
|
405
|
+
return this._connection?.isOpen ?? false;
|
|
406
|
+
}
|
|
407
|
+
setupConnection(conn) {
|
|
408
|
+
this._connection = conn;
|
|
409
|
+
conn.messageReceived.add((raw) => this._handleMessage(raw));
|
|
410
|
+
conn.closed.add((ev) => {
|
|
411
|
+
this._connection = void 0;
|
|
412
|
+
this._stopKeepAlive();
|
|
413
|
+
this.disconnected.trigger(ev);
|
|
414
|
+
});
|
|
415
|
+
void conn.whenConnected.then(() => {
|
|
416
|
+
this.connected.trigger();
|
|
417
|
+
this._startKeepAlive();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
call(method, args, options) {
|
|
421
|
+
const callId = this.outbound.nextId();
|
|
422
|
+
const outboundCall = options?.outboundCallFactory ? options.outboundCallFactory(callId, method) : new RpcOutboundCall(callId, method);
|
|
423
|
+
const msg = serializeMessage(
|
|
424
|
+
{ Method: method, RelatedId: callId, CallType: options?.callTypeId ?? 0 },
|
|
425
|
+
args
|
|
426
|
+
);
|
|
427
|
+
outboundCall.serializedMessage = msg;
|
|
428
|
+
if (this._connection !== void 0) {
|
|
429
|
+
this.outbound.register(outboundCall);
|
|
430
|
+
this._connection.send(msg);
|
|
431
|
+
} else {
|
|
432
|
+
this._pendingSends.push(outboundCall);
|
|
433
|
+
}
|
|
434
|
+
const signal = options?.signal;
|
|
435
|
+
if (signal !== void 0) {
|
|
436
|
+
const peer = this;
|
|
437
|
+
const hub = this._hub;
|
|
438
|
+
const tracker = this.outbound;
|
|
439
|
+
const onAbort = () => {
|
|
440
|
+
if (tracker.remove(callId) !== void 0) {
|
|
441
|
+
outboundCall.result.reject(new Error("Call cancelled."));
|
|
442
|
+
outboundCall.onDisconnect();
|
|
443
|
+
if (peer._connection !== void 0)
|
|
444
|
+
hub.systemCallSender.cancel(peer._connection, callId);
|
|
445
|
+
} else {
|
|
446
|
+
const idx = peer._pendingSends.findIndex((c) => c.callId === callId);
|
|
447
|
+
if (idx !== -1) {
|
|
448
|
+
peer._pendingSends.splice(idx, 1);
|
|
449
|
+
outboundCall.result.reject(new Error("Call cancelled."));
|
|
450
|
+
outboundCall.onDisconnect();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
455
|
+
outboundCall.result.promise.then(() => signal.removeEventListener("abort", onAbort)).catch(() => signal.removeEventListener("abort", onAbort));
|
|
456
|
+
}
|
|
457
|
+
return outboundCall;
|
|
458
|
+
}
|
|
459
|
+
callNoWait(method, args) {
|
|
460
|
+
if (this._connection === void 0) return;
|
|
461
|
+
const msg = serializeMessage({ Method: method, RelatedId: 0 }, args);
|
|
462
|
+
this._connection.send(msg);
|
|
463
|
+
}
|
|
464
|
+
close() {
|
|
465
|
+
this._stopKeepAlive();
|
|
466
|
+
for (const call of this._pendingSends) {
|
|
467
|
+
if (!call.result.isCompleted)
|
|
468
|
+
call.result.reject(new Error("Peer closed."));
|
|
469
|
+
call.onDisconnect();
|
|
470
|
+
}
|
|
471
|
+
this._pendingSends.length = 0;
|
|
472
|
+
this.outbound.rejectAll(new Error("Peer closed."));
|
|
473
|
+
this.outbound.invalidateAll();
|
|
474
|
+
this._connection?.close();
|
|
475
|
+
this._hub.peers.delete(this.ref);
|
|
476
|
+
}
|
|
477
|
+
/** Send any messages buffered while disconnected. Call after connection + handshake are ready. */
|
|
478
|
+
_flushPendingSends() {
|
|
479
|
+
if (this._pendingSends.length === 0 || this._connection === void 0) return;
|
|
480
|
+
for (const call of this._pendingSends) {
|
|
481
|
+
this.outbound.register(call);
|
|
482
|
+
this._connection.send(call.serializedMessage);
|
|
483
|
+
}
|
|
484
|
+
this._pendingSends.length = 0;
|
|
485
|
+
}
|
|
486
|
+
/** Override in subclasses to handle the remote peer's handshake response. */
|
|
487
|
+
_onHandshakeReceived(_handshake) {
|
|
488
|
+
}
|
|
489
|
+
_handleMessage(raw) {
|
|
490
|
+
const { message, args } = deserializeMessage(raw);
|
|
491
|
+
const method = message.Method ?? "";
|
|
492
|
+
if (method === RpcSystemCalls.handshake) {
|
|
493
|
+
const handshake = args[0];
|
|
494
|
+
if (handshake !== void 0)
|
|
495
|
+
this._onHandshakeReceived(handshake);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (method.startsWith("$sys")) {
|
|
499
|
+
this._hub.handleSystemCall(message, args, this.outbound, this.inbound);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this._handleInbound(message, args);
|
|
503
|
+
}
|
|
504
|
+
_handleInbound(message, args) {
|
|
505
|
+
const method = message.Method ?? "";
|
|
506
|
+
const relatedId = message.RelatedId ?? 0;
|
|
507
|
+
const call = new RpcInboundCall(relatedId, method, args);
|
|
508
|
+
const methodDef = this._hub.serviceHost.getMethodDef(method);
|
|
509
|
+
const isNoWait = methodDef?.noWait === true;
|
|
510
|
+
if (!isNoWait) {
|
|
511
|
+
this.inbound.register(call);
|
|
512
|
+
}
|
|
513
|
+
const serviceHost = this._hub.serviceHost;
|
|
514
|
+
if (serviceHost !== void 0) {
|
|
515
|
+
void (async () => {
|
|
516
|
+
try {
|
|
517
|
+
const context = this._connection !== void 0 ? { __rpcDispatch: true, callId: relatedId, connection: this._connection } : void 0;
|
|
518
|
+
const result = await serviceHost.dispatch(method, args, context);
|
|
519
|
+
if (!isNoWait && this._connection !== void 0) {
|
|
520
|
+
this._hub.systemCallSender.ok(this._connection, relatedId, result);
|
|
521
|
+
}
|
|
522
|
+
} catch (e) {
|
|
523
|
+
if (!isNoWait && this._connection !== void 0) {
|
|
524
|
+
this._hub.systemCallSender.error(this._connection, relatedId, e);
|
|
525
|
+
}
|
|
526
|
+
} finally {
|
|
527
|
+
if (!isNoWait) {
|
|
528
|
+
this.inbound.remove(relatedId);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
})();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
_startKeepAlive() {
|
|
535
|
+
this._keepAliveTimer = setInterval(() => {
|
|
536
|
+
if (this._connection !== void 0) {
|
|
537
|
+
this._hub.systemCallSender.keepAlive(this._connection, this.outbound.activeCallIds());
|
|
538
|
+
}
|
|
539
|
+
}, 15e3);
|
|
540
|
+
}
|
|
541
|
+
_stopKeepAlive() {
|
|
542
|
+
if (this._keepAliveTimer !== void 0) {
|
|
543
|
+
clearInterval(this._keepAliveTimer);
|
|
544
|
+
this._keepAliveTimer = void 0;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var RpcPeerConnectionKind = /* @__PURE__ */ ((RpcPeerConnectionKind2) => {
|
|
549
|
+
RpcPeerConnectionKind2[RpcPeerConnectionKind2["Disconnected"] = 0] = "Disconnected";
|
|
550
|
+
RpcPeerConnectionKind2[RpcPeerConnectionKind2["Connecting"] = 1] = "Connecting";
|
|
551
|
+
RpcPeerConnectionKind2[RpcPeerConnectionKind2["Connected"] = 2] = "Connected";
|
|
552
|
+
return RpcPeerConnectionKind2;
|
|
553
|
+
})(RpcPeerConnectionKind || {});
|
|
554
|
+
var RpcClientPeer = class extends RpcPeer {
|
|
555
|
+
_disposed = false;
|
|
556
|
+
_connectionKind = 0 /* Disconnected */;
|
|
557
|
+
_tryIndex = 0;
|
|
558
|
+
_handshakeIndex = 0;
|
|
559
|
+
_lastRemoteHubId;
|
|
560
|
+
_pendingHandshake;
|
|
561
|
+
_reconnectsAt = 0;
|
|
562
|
+
/** Base64url-encoded peer ID — matches .NET's RpcClientPeer.ClientId (Guid.ToBase64Url). */
|
|
563
|
+
clientId;
|
|
564
|
+
/**
|
|
565
|
+
* Serialization format key sent to the server via `f=` query parameter.
|
|
566
|
+
* TS uses plain JSON.stringify (no polymorphic type wrapping), so `json5np`
|
|
567
|
+
* (no-polymorphism) is the correct default.
|
|
568
|
+
*/
|
|
569
|
+
serializationFormat;
|
|
570
|
+
/** Builds the WebSocket URL for this peer. Replace to customize URL construction. */
|
|
571
|
+
connectionUrlResolver = defaultConnectionUrlResolver;
|
|
572
|
+
peerChanged = new EventHandlerSet3();
|
|
573
|
+
reconnectDelayer = new RpcClientPeerReconnectDelayer();
|
|
574
|
+
reconnectsAtChanged = new EventHandlerSet3();
|
|
575
|
+
get reconnectsAt() {
|
|
576
|
+
return this._reconnectsAt;
|
|
577
|
+
}
|
|
578
|
+
_setReconnectsAt(value) {
|
|
579
|
+
if (this._reconnectsAt === value) return;
|
|
580
|
+
this._reconnectsAt = value;
|
|
581
|
+
this.reconnectsAtChanged.trigger();
|
|
582
|
+
}
|
|
583
|
+
constructor(hub, url, serializationFormat) {
|
|
584
|
+
super(url, hub);
|
|
585
|
+
this.clientId = guidToBase64Url(this.id);
|
|
586
|
+
this.serializationFormat = serializationFormat ?? DEFAULT_SERIALIZATION_FORMAT;
|
|
587
|
+
}
|
|
588
|
+
get connectionKind() {
|
|
589
|
+
return this._connectionKind;
|
|
590
|
+
}
|
|
591
|
+
/** One-shot connection for tests — no reconnection loop, no handshake. */
|
|
592
|
+
connectWith(conn) {
|
|
593
|
+
this.setupConnection(conn);
|
|
594
|
+
this._reconnect(true);
|
|
595
|
+
}
|
|
596
|
+
/** Start the reconnection loop — runs until disposed. */
|
|
597
|
+
async run(wsFactory) {
|
|
598
|
+
const connUrl = this.connectionUrlResolver(this);
|
|
599
|
+
while (!this._disposed) {
|
|
600
|
+
try {
|
|
601
|
+
const ws = wsFactory?.(connUrl) ?? new WebSocket(connUrl);
|
|
602
|
+
const conn = new RpcWebSocketConnection(ws);
|
|
603
|
+
this._connectionKind = 1 /* Connecting */;
|
|
604
|
+
this._pendingHandshake = new PromiseSource3();
|
|
605
|
+
this.setupConnection(conn);
|
|
606
|
+
this._connection = void 0;
|
|
607
|
+
const closedRejection = new Promise((_, reject) => conn.closed.add(() => reject(new Error("Connection failed"))));
|
|
608
|
+
closedRejection.catch(() => {
|
|
609
|
+
});
|
|
610
|
+
await Promise.race([conn.whenConnected, closedRejection]);
|
|
611
|
+
this._hub.systemCallSender.handshake(conn, this.id, this._hub.hubId, ++this._handshakeIndex);
|
|
612
|
+
const remoteHandshake = await Promise.race([
|
|
613
|
+
this._pendingHandshake.promise,
|
|
614
|
+
closedRejection
|
|
615
|
+
]);
|
|
616
|
+
this._pendingHandshake = void 0;
|
|
617
|
+
const remoteHubId = remoteHandshake.RemoteHubId;
|
|
618
|
+
let isPeerChanged = false;
|
|
619
|
+
if (remoteHubId !== void 0) {
|
|
620
|
+
isPeerChanged = this._lastRemoteHubId !== void 0 && this._lastRemoteHubId !== remoteHubId;
|
|
621
|
+
if (isPeerChanged) {
|
|
622
|
+
this.inbound.clear();
|
|
623
|
+
this.peerChanged.trigger();
|
|
624
|
+
}
|
|
625
|
+
this._lastRemoteHubId = remoteHubId;
|
|
626
|
+
}
|
|
627
|
+
this._connection = conn;
|
|
628
|
+
this._connectionKind = 2 /* Connected */;
|
|
629
|
+
this._tryIndex = 0;
|
|
630
|
+
this._reconnect(isPeerChanged);
|
|
631
|
+
await new Promise((r) => conn.closed.add(() => r()));
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
this._connectionKind = 0 /* Disconnected */;
|
|
635
|
+
if (this._disposed) break;
|
|
636
|
+
this._tryIndex++;
|
|
637
|
+
const delay = this.reconnectDelayer.getDelay(this._tryIndex);
|
|
638
|
+
if (delay.isLimitExceeded) {
|
|
639
|
+
this._disposed = true;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
this._setReconnectsAt(delay.endsAt);
|
|
643
|
+
try {
|
|
644
|
+
await delay.promise;
|
|
645
|
+
} finally {
|
|
646
|
+
this._setReconnectsAt(0);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** Re-send outbound calls after reconnection + flush pending sends.
|
|
651
|
+
* Stage-3 compute calls are always self-invalidated: without $sys.Reconnect
|
|
652
|
+
* protocol, the server's invalidation tracking is lost on disconnect, and
|
|
653
|
+
* re-sending would get a duplicate $sys.Ok that is ignored (PromiseSource
|
|
654
|
+
* already resolved). Self-invalidation forces a fresh recompute that
|
|
655
|
+
* establishes new invalidation tracking on the new connection.
|
|
656
|
+
* Regular in-flight calls are re-sent transparently. */
|
|
657
|
+
_reconnect(_isPeerChanged) {
|
|
658
|
+
if (this._connection === void 0) return;
|
|
659
|
+
const conn = this._connection;
|
|
660
|
+
const trackerCalls = [...this.outbound.values()];
|
|
661
|
+
for (const call of trackerCalls) {
|
|
662
|
+
if (!call.removeOnOk && call.result.isCompleted) {
|
|
663
|
+
call.onDisconnect();
|
|
664
|
+
this.outbound.remove(call.callId);
|
|
665
|
+
} else {
|
|
666
|
+
conn.send(call.serializedMessage);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
this._flushPendingSends();
|
|
670
|
+
}
|
|
671
|
+
_onHandshakeReceived(handshake) {
|
|
672
|
+
this._pendingHandshake?.resolve(handshake);
|
|
673
|
+
}
|
|
674
|
+
close() {
|
|
675
|
+
this._disposed = true;
|
|
676
|
+
super.close();
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
var RpcServerPeer = class extends RpcPeer {
|
|
680
|
+
constructor(hub, ref) {
|
|
681
|
+
super(ref, hub);
|
|
682
|
+
}
|
|
683
|
+
/** Accept an incoming connection — sets up message handling and handshake response. */
|
|
684
|
+
accept(conn) {
|
|
685
|
+
this.setupConnection(conn);
|
|
686
|
+
}
|
|
687
|
+
_onHandshakeReceived(_handshake) {
|
|
688
|
+
if (this._connection !== void 0) {
|
|
689
|
+
this._hub.systemCallSender.handshake(this._connection, this.id, this._hub.hubId, 0);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function guidToBase64Url(uuid) {
|
|
694
|
+
const hex = uuid.replace(/-/g, "");
|
|
695
|
+
const bytes = new Uint8Array(16);
|
|
696
|
+
bytes[0] = parseInt(hex.slice(6, 8), 16);
|
|
697
|
+
bytes[1] = parseInt(hex.slice(4, 6), 16);
|
|
698
|
+
bytes[2] = parseInt(hex.slice(2, 4), 16);
|
|
699
|
+
bytes[3] = parseInt(hex.slice(0, 2), 16);
|
|
700
|
+
bytes[4] = parseInt(hex.slice(10, 12), 16);
|
|
701
|
+
bytes[5] = parseInt(hex.slice(8, 10), 16);
|
|
702
|
+
bytes[6] = parseInt(hex.slice(14, 16), 16);
|
|
703
|
+
bytes[7] = parseInt(hex.slice(12, 14), 16);
|
|
704
|
+
for (let i = 8; i < 16; i++)
|
|
705
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
706
|
+
const binary = String.fromCharCode(...bytes);
|
|
707
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/rpc-service-host.ts
|
|
711
|
+
var RpcServiceHost = class {
|
|
712
|
+
_methods = /* @__PURE__ */ new Map();
|
|
713
|
+
register(def, impl) {
|
|
714
|
+
for (const methodDef of def.methods.values()) {
|
|
715
|
+
const fn = impl[methodDef.name];
|
|
716
|
+
if (!fn) continue;
|
|
717
|
+
this._methods.set(wireMethodName(methodDef), { def: methodDef, fn });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async dispatch(wireMethod, args, context) {
|
|
721
|
+
const entry = this._methods.get(wireMethod);
|
|
722
|
+
if (entry === void 0) throw new Error(`Method not found: ${wireMethod}`);
|
|
723
|
+
if (context !== void 0 && entry.def.callTypeId !== 0) {
|
|
724
|
+
return await entry.fn(...args, context);
|
|
725
|
+
}
|
|
726
|
+
return await entry.fn(...args);
|
|
727
|
+
}
|
|
728
|
+
getMethodDef(wireMethod) {
|
|
729
|
+
return this._methods.get(wireMethod)?.def;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/rpc-decorators.ts
|
|
734
|
+
var METHODS_META = /* @__PURE__ */ Symbol.for("actuallab.methods");
|
|
735
|
+
var SERVICE_META = /* @__PURE__ */ Symbol.for("actuallab.service");
|
|
736
|
+
function getMethodsMeta(cls) {
|
|
737
|
+
return cls[Symbol.metadata]?.[METHODS_META];
|
|
738
|
+
}
|
|
739
|
+
function getServiceMeta(cls) {
|
|
740
|
+
return cls[Symbol.metadata]?.[SERVICE_META];
|
|
741
|
+
}
|
|
742
|
+
function rpcService(serviceName, options) {
|
|
743
|
+
return function(_target, context) {
|
|
744
|
+
const meta = context.metadata[SERVICE_META] ??= {};
|
|
745
|
+
meta.name = serviceName;
|
|
746
|
+
if (options?.ctOffset !== void 0)
|
|
747
|
+
meta.ctOffset = options.ctOffset;
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function rpcMethod(options) {
|
|
751
|
+
return function(target, context) {
|
|
752
|
+
const methodName = String(context.name);
|
|
753
|
+
const methods = context.metadata[METHODS_META] ??= {};
|
|
754
|
+
methods[methodName] = {
|
|
755
|
+
...methods[methodName],
|
|
756
|
+
argCount: target.length,
|
|
757
|
+
stream: options?.stream ?? false,
|
|
758
|
+
noWait: options?.noWait ?? false
|
|
759
|
+
};
|
|
760
|
+
return target;
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/rpc-hub.ts
|
|
765
|
+
var RpcHub = class {
|
|
766
|
+
hubId;
|
|
767
|
+
peers = /* @__PURE__ */ new Map();
|
|
768
|
+
serviceHost;
|
|
769
|
+
systemCallSender = new RpcSystemCallSender();
|
|
770
|
+
constructor(hubId) {
|
|
771
|
+
this.hubId = hubId ?? crypto.randomUUID();
|
|
772
|
+
this.serviceHost = new RpcServiceHost();
|
|
773
|
+
}
|
|
774
|
+
addPeer(peer) {
|
|
775
|
+
this.peers.set(peer.ref, peer);
|
|
776
|
+
}
|
|
777
|
+
removePeer(ref) {
|
|
778
|
+
const peer = this.peers.get(ref);
|
|
779
|
+
if (peer !== void 0) {
|
|
780
|
+
peer.close();
|
|
781
|
+
this.peers.delete(ref);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/** Get or create a peer by ref. Server peers have "server://" prefix, everything else is a client peer. */
|
|
785
|
+
getPeer(ref) {
|
|
786
|
+
let peer = this.peers.get(ref);
|
|
787
|
+
if (peer) return peer;
|
|
788
|
+
peer = ref.startsWith("server://") ? new RpcServerPeer(this, ref) : new RpcClientPeer(this, ref);
|
|
789
|
+
this.addPeer(peer);
|
|
790
|
+
return peer;
|
|
791
|
+
}
|
|
792
|
+
/** Get or create a client peer for the given URL. */
|
|
793
|
+
getClientPeer(ref) {
|
|
794
|
+
return this.getPeer(ref);
|
|
795
|
+
}
|
|
796
|
+
/** Get or create a server peer for the given ref. */
|
|
797
|
+
getServerPeer(ref) {
|
|
798
|
+
return this.getPeer(ref);
|
|
799
|
+
}
|
|
800
|
+
/** Register a service with optional server method wrapping for custom call types. */
|
|
801
|
+
addService(defOrContract, impl) {
|
|
802
|
+
const def = this._resolveServiceDef(defOrContract);
|
|
803
|
+
const wrappedImpl = {};
|
|
804
|
+
for (const methodDef of def.methods.values()) {
|
|
805
|
+
const fn = impl[methodDef.name];
|
|
806
|
+
if (!fn) continue;
|
|
807
|
+
wrappedImpl[methodDef.name] = methodDef.callTypeId !== 0 ? this._wrapServerMethod(methodDef, fn, impl) : fn;
|
|
808
|
+
}
|
|
809
|
+
this.serviceHost.register(def, wrappedImpl);
|
|
810
|
+
}
|
|
811
|
+
/** Create a typed client proxy for a service on a remote peer. */
|
|
812
|
+
addClient(peer, defOrContract) {
|
|
813
|
+
const def = this._resolveServiceDef(defOrContract);
|
|
814
|
+
const overloads = /* @__PURE__ */ new Map();
|
|
815
|
+
for (const methodDef of def.methods.values()) {
|
|
816
|
+
let byArgCount = overloads.get(methodDef.name);
|
|
817
|
+
if (!byArgCount) {
|
|
818
|
+
byArgCount = /* @__PURE__ */ new Map();
|
|
819
|
+
overloads.set(methodDef.name, byArgCount);
|
|
820
|
+
}
|
|
821
|
+
byArgCount.set(methodDef.argCount, this._createClientMethod(peer, methodDef));
|
|
822
|
+
}
|
|
823
|
+
const methods = /* @__PURE__ */ new Map();
|
|
824
|
+
for (const [name, byArgCount] of overloads) {
|
|
825
|
+
if (byArgCount.size === 1) {
|
|
826
|
+
methods.set(name, byArgCount.values().next().value);
|
|
827
|
+
} else {
|
|
828
|
+
methods.set(name, (...args) => {
|
|
829
|
+
const fn = byArgCount.get(args.length);
|
|
830
|
+
if (!fn) throw new Error(`No overload of ${name} accepts ${args.length} arguments`);
|
|
831
|
+
return fn(...args);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return new Proxy({}, {
|
|
836
|
+
get: (_target, prop) => typeof prop === "string" ? methods.get(prop) : void 0
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
close() {
|
|
840
|
+
for (const peer of this.peers.values()) peer.close();
|
|
841
|
+
this.peers.clear();
|
|
842
|
+
}
|
|
843
|
+
/** Dispatch an inbound system call. Override in FusionHub to handle compute-specific system calls. */
|
|
844
|
+
handleSystemCall(message, args, outbound, inbound) {
|
|
845
|
+
handleSystemCall(message, args, outbound, inbound);
|
|
846
|
+
}
|
|
847
|
+
/** Build an RpcServiceDef from decorator metadata on a contract class. Override in FusionHub for compute support. */
|
|
848
|
+
_buildServiceDef(cls) {
|
|
849
|
+
const svcMeta = getServiceMeta(cls);
|
|
850
|
+
if (svcMeta === void 0) throw new Error("Contract class missing @rpcService metadata");
|
|
851
|
+
const methodsMeta = getMethodsMeta(cls) ?? {};
|
|
852
|
+
const ctOffset = svcMeta.ctOffset ?? 0;
|
|
853
|
+
const methods = /* @__PURE__ */ new Map();
|
|
854
|
+
for (const [name, meta] of Object.entries(methodsMeta)) {
|
|
855
|
+
const wireArgCount = meta.argCount + ctOffset;
|
|
856
|
+
const mapKey = `${name}:${wireArgCount}`;
|
|
857
|
+
methods.set(mapKey, {
|
|
858
|
+
name,
|
|
859
|
+
serviceName: svcMeta.name,
|
|
860
|
+
argCount: meta.argCount,
|
|
861
|
+
wireArgCount,
|
|
862
|
+
callTypeId: 0,
|
|
863
|
+
stream: meta.stream ?? false,
|
|
864
|
+
noWait: meta.noWait ?? false
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
return { name: svcMeta.name, methods };
|
|
868
|
+
}
|
|
869
|
+
/** Resolve a service def from either a plain RpcServiceDef or a contract class. */
|
|
870
|
+
_resolveServiceDef(defOrContract) {
|
|
871
|
+
if (typeof defOrContract === "function")
|
|
872
|
+
return this._buildServiceDef(defOrContract);
|
|
873
|
+
return defOrContract;
|
|
874
|
+
}
|
|
875
|
+
/** Override in FusionHub — default is pass-through (for methods with callTypeId !== 0). */
|
|
876
|
+
_wrapServerMethod(_methodDef, fn, _impl) {
|
|
877
|
+
return fn;
|
|
878
|
+
}
|
|
879
|
+
/** Create a client method for the given methodDef. Override in FusionHub for custom call types. */
|
|
880
|
+
_createClientMethod(peer, methodDef) {
|
|
881
|
+
const wireName = wireMethodName(methodDef);
|
|
882
|
+
if (methodDef.noWait) {
|
|
883
|
+
return (...args) => {
|
|
884
|
+
const callArgs = args.slice(0, methodDef.argCount);
|
|
885
|
+
peer.callNoWait(wireName, callArgs);
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
const callOptions = methodDef.callTypeId !== 0 ? { callTypeId: methodDef.callTypeId } : void 0;
|
|
889
|
+
return async (...args) => {
|
|
890
|
+
const callArgs = args.slice(0, methodDef.argCount);
|
|
891
|
+
const outboundCall = peer.call(wireName, callArgs, callOptions);
|
|
892
|
+
return outboundCall.result.promise;
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// src/rpc-client.ts
|
|
898
|
+
function createRpcClient(peer, serviceDef) {
|
|
899
|
+
const overloads = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const methodDef of serviceDef.methods.values()) {
|
|
901
|
+
let byArgCount = overloads.get(methodDef.name);
|
|
902
|
+
if (!byArgCount) {
|
|
903
|
+
byArgCount = /* @__PURE__ */ new Map();
|
|
904
|
+
overloads.set(methodDef.name, byArgCount);
|
|
905
|
+
}
|
|
906
|
+
byArgCount.set(methodDef.argCount, methodDef);
|
|
907
|
+
}
|
|
908
|
+
const methods = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const [name, byArgCount] of overloads) {
|
|
910
|
+
if (byArgCount.size === 1) {
|
|
911
|
+
const methodDef = byArgCount.values().next().value;
|
|
912
|
+
methods.set(name, createMethod(peer, methodDef));
|
|
913
|
+
} else {
|
|
914
|
+
const fns = /* @__PURE__ */ new Map();
|
|
915
|
+
for (const [argCount, methodDef] of byArgCount) {
|
|
916
|
+
fns.set(argCount, createMethod(peer, methodDef));
|
|
917
|
+
}
|
|
918
|
+
methods.set(name, (...args) => {
|
|
919
|
+
const fn = fns.get(args.length);
|
|
920
|
+
if (!fn) throw new Error(`No overload of ${name} accepts ${args.length} arguments`);
|
|
921
|
+
return fn(...args);
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return new Proxy({}, {
|
|
926
|
+
get(_target, prop) {
|
|
927
|
+
if (typeof prop !== "string") return void 0;
|
|
928
|
+
return methods.get(prop);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
function createMethod(peer, methodDef) {
|
|
933
|
+
return async (...args) => {
|
|
934
|
+
const callArgs = args.slice(0, methodDef.argCount);
|
|
935
|
+
const wireName = wireMethodName(methodDef);
|
|
936
|
+
const options = methodDef.callTypeId !== 0 ? { callTypeId: methodDef.callTypeId } : void 0;
|
|
937
|
+
const outboundCall = peer.call(wireName, callArgs, options);
|
|
938
|
+
return outboundCall.result.promise;
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/rpc-peer-state.ts
|
|
943
|
+
var RpcPeerStateKind = /* @__PURE__ */ ((RpcPeerStateKind2) => {
|
|
944
|
+
RpcPeerStateKind2[RpcPeerStateKind2["Connected"] = 0] = "Connected";
|
|
945
|
+
RpcPeerStateKind2[RpcPeerStateKind2["JustDisconnected"] = 1] = "JustDisconnected";
|
|
946
|
+
RpcPeerStateKind2[RpcPeerStateKind2["Disconnected"] = 2] = "Disconnected";
|
|
947
|
+
RpcPeerStateKind2[RpcPeerStateKind2["JustConnected"] = 3] = "JustConnected";
|
|
948
|
+
return RpcPeerStateKind2;
|
|
949
|
+
})(RpcPeerStateKind || {});
|
|
950
|
+
function isConnected(state) {
|
|
951
|
+
return state.kind === 0 /* Connected */ || state.kind === 3 /* JustConnected */;
|
|
952
|
+
}
|
|
953
|
+
function likelyConnected(state) {
|
|
954
|
+
return state.kind !== 2 /* Disconnected */;
|
|
955
|
+
}
|
|
956
|
+
function getStateDescription(state, useLastError = false) {
|
|
957
|
+
switch (state.kind) {
|
|
958
|
+
case 3 /* JustConnected */:
|
|
959
|
+
return "Just connected.";
|
|
960
|
+
case 0 /* Connected */:
|
|
961
|
+
return "Connected.";
|
|
962
|
+
case 1 /* JustDisconnected */:
|
|
963
|
+
return "Just disconnected, reconnecting...";
|
|
964
|
+
case 2 /* Disconnected */:
|
|
965
|
+
if (state.reconnectsIn > 0)
|
|
966
|
+
break;
|
|
967
|
+
return "Reconnecting...";
|
|
968
|
+
}
|
|
969
|
+
if (state.lastError == null || !useLastError)
|
|
970
|
+
return "Disconnected.";
|
|
971
|
+
let message = state.lastError.message.trim();
|
|
972
|
+
if (!(message.endsWith(".") || message.endsWith("!") || message.endsWith("?")))
|
|
973
|
+
message += ".";
|
|
974
|
+
return "Disconnected: " + message;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// src/rpc-peer-state-monitor.ts
|
|
978
|
+
import { EventHandlerSet as EventHandlerSet4 } from "@actuallab/core";
|
|
979
|
+
var JustConnectedPeriodMs = 1500;
|
|
980
|
+
var JustDisconnectedPeriodMs = 3e3;
|
|
981
|
+
var MinReconnectsInMs = 1e3;
|
|
982
|
+
var RpcPeerStateMonitor = class {
|
|
983
|
+
peer;
|
|
984
|
+
stateChanged = new EventHandlerSet4();
|
|
985
|
+
_rawConnected;
|
|
986
|
+
_connectedAt = 0;
|
|
987
|
+
_disconnectedAt = 0;
|
|
988
|
+
_lastError;
|
|
989
|
+
_state;
|
|
990
|
+
_timer;
|
|
991
|
+
// Arrow functions to preserve `this` when used as event handlers
|
|
992
|
+
_onConnected = () => {
|
|
993
|
+
this._rawConnected = true;
|
|
994
|
+
this._connectedAt = Date.now();
|
|
995
|
+
this._lastError = void 0;
|
|
996
|
+
this._recompute();
|
|
997
|
+
};
|
|
998
|
+
_onDisconnected = (ev) => {
|
|
999
|
+
const wasConnected = this._rawConnected;
|
|
1000
|
+
this._rawConnected = false;
|
|
1001
|
+
this._lastError = new Error(`Connection closed: ${ev.reason} (code ${ev.code})`);
|
|
1002
|
+
if (wasConnected) {
|
|
1003
|
+
this._disconnectedAt = Date.now();
|
|
1004
|
+
}
|
|
1005
|
+
this._recompute();
|
|
1006
|
+
};
|
|
1007
|
+
_onReconnectsAtChanged = () => {
|
|
1008
|
+
this._recompute();
|
|
1009
|
+
};
|
|
1010
|
+
_onCancelDelaysChanged = () => {
|
|
1011
|
+
this._recompute();
|
|
1012
|
+
};
|
|
1013
|
+
constructor(peer) {
|
|
1014
|
+
this.peer = peer;
|
|
1015
|
+
this._rawConnected = peer.isConnected;
|
|
1016
|
+
if (this._rawConnected) {
|
|
1017
|
+
this._connectedAt = Date.now();
|
|
1018
|
+
this._state = { kind: 3 /* JustConnected */, reconnectsIn: 0 };
|
|
1019
|
+
} else {
|
|
1020
|
+
this._disconnectedAt = Date.now();
|
|
1021
|
+
this._state = { kind: 1 /* JustDisconnected */, reconnectsIn: 0 };
|
|
1022
|
+
}
|
|
1023
|
+
peer.connected.add(this._onConnected);
|
|
1024
|
+
peer.disconnected.add(this._onDisconnected);
|
|
1025
|
+
peer.reconnectsAtChanged.add(this._onReconnectsAtChanged);
|
|
1026
|
+
peer.reconnectDelayer.cancelDelaysChanged.add(this._onCancelDelaysChanged);
|
|
1027
|
+
this._scheduleRecompute(this._rawConnected ? JustConnectedPeriodMs : JustDisconnectedPeriodMs);
|
|
1028
|
+
}
|
|
1029
|
+
get state() {
|
|
1030
|
+
return this._state;
|
|
1031
|
+
}
|
|
1032
|
+
dispose() {
|
|
1033
|
+
if (this._timer !== void 0) {
|
|
1034
|
+
clearTimeout(this._timer);
|
|
1035
|
+
this._timer = void 0;
|
|
1036
|
+
}
|
|
1037
|
+
this.peer.connected.remove(this._onConnected);
|
|
1038
|
+
this.peer.disconnected.remove(this._onDisconnected);
|
|
1039
|
+
this.peer.reconnectsAtChanged.remove(this._onReconnectsAtChanged);
|
|
1040
|
+
this.peer.reconnectDelayer.cancelDelaysChanged.remove(this._onCancelDelaysChanged);
|
|
1041
|
+
}
|
|
1042
|
+
_recompute() {
|
|
1043
|
+
if (this._timer !== void 0) {
|
|
1044
|
+
clearTimeout(this._timer);
|
|
1045
|
+
this._timer = void 0;
|
|
1046
|
+
}
|
|
1047
|
+
const now = Date.now();
|
|
1048
|
+
let newState;
|
|
1049
|
+
if (this._rawConnected) {
|
|
1050
|
+
const connectedFor = now - this._connectedAt;
|
|
1051
|
+
if (connectedFor < JustConnectedPeriodMs) {
|
|
1052
|
+
newState = { kind: 3 /* JustConnected */, reconnectsIn: 0 };
|
|
1053
|
+
this._scheduleRecompute(JustConnectedPeriodMs - connectedFor);
|
|
1054
|
+
} else {
|
|
1055
|
+
newState = { kind: 0 /* Connected */, reconnectsIn: 0 };
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
const disconnectedFor = now - this._disconnectedAt;
|
|
1059
|
+
if (disconnectedFor < JustDisconnectedPeriodMs) {
|
|
1060
|
+
newState = { kind: 1 /* JustDisconnected */, lastError: this._lastError, reconnectsIn: 0 };
|
|
1061
|
+
this._scheduleRecompute(JustDisconnectedPeriodMs - disconnectedFor);
|
|
1062
|
+
} else {
|
|
1063
|
+
const reconnectsAt = this.peer.reconnectsAt;
|
|
1064
|
+
const reconnectsIn = reconnectsAt > 0 ? Math.max(0, reconnectsAt - now) : 0;
|
|
1065
|
+
if (reconnectsIn < MinReconnectsInMs) {
|
|
1066
|
+
newState = { kind: 2 /* Disconnected */, lastError: this._lastError, reconnectsIn: 0 };
|
|
1067
|
+
} else {
|
|
1068
|
+
newState = { kind: 2 /* Disconnected */, lastError: this._lastError, reconnectsIn };
|
|
1069
|
+
this._scheduleRecompute(Math.min(1e3, reconnectsIn - MinReconnectsInMs));
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
this._setState(newState);
|
|
1074
|
+
}
|
|
1075
|
+
_setState(newState) {
|
|
1076
|
+
const old = this._state;
|
|
1077
|
+
if (old.kind === newState.kind && old.lastError === newState.lastError && old.reconnectsIn === newState.reconnectsIn)
|
|
1078
|
+
return;
|
|
1079
|
+
this._state = newState;
|
|
1080
|
+
this.stateChanged.trigger(newState);
|
|
1081
|
+
}
|
|
1082
|
+
_scheduleRecompute(delayMs) {
|
|
1083
|
+
if (this._timer !== void 0) clearTimeout(this._timer);
|
|
1084
|
+
this._timer = setTimeout(() => {
|
|
1085
|
+
this._timer = void 0;
|
|
1086
|
+
this._recompute();
|
|
1087
|
+
}, Math.max(0, delayMs));
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
export {
|
|
1091
|
+
ARG_DELIMITER,
|
|
1092
|
+
DEFAULT_SERIALIZATION_FORMAT,
|
|
1093
|
+
ENVELOPE_DELIMITER,
|
|
1094
|
+
FRAME_DELIMITER,
|
|
1095
|
+
RpcCallTypeId,
|
|
1096
|
+
RpcClientPeer,
|
|
1097
|
+
RpcClientPeerReconnectDelayer,
|
|
1098
|
+
RpcHub,
|
|
1099
|
+
RpcInboundCall,
|
|
1100
|
+
RpcInboundCallTracker,
|
|
1101
|
+
RpcMessageChannelConnection,
|
|
1102
|
+
RpcOutboundCall,
|
|
1103
|
+
RpcOutboundCallTracker,
|
|
1104
|
+
RpcPeer,
|
|
1105
|
+
RpcPeerConnectionKind,
|
|
1106
|
+
RpcPeerStateKind,
|
|
1107
|
+
RpcPeerStateMonitor,
|
|
1108
|
+
RpcServerPeer,
|
|
1109
|
+
RpcServiceHost,
|
|
1110
|
+
RpcSystemCallSender,
|
|
1111
|
+
RpcSystemCalls,
|
|
1112
|
+
RpcType,
|
|
1113
|
+
RpcWebSocketConnection,
|
|
1114
|
+
WebSocketState,
|
|
1115
|
+
createMessageChannelPair,
|
|
1116
|
+
createRpcClient,
|
|
1117
|
+
defaultConnectionUrlResolver,
|
|
1118
|
+
defineRpcService,
|
|
1119
|
+
deserializeMessage,
|
|
1120
|
+
getMethodsMeta,
|
|
1121
|
+
getServiceMeta,
|
|
1122
|
+
getStateDescription,
|
|
1123
|
+
handleSystemCall,
|
|
1124
|
+
isConnected,
|
|
1125
|
+
likelyConnected,
|
|
1126
|
+
rpcMethod,
|
|
1127
|
+
rpcService,
|
|
1128
|
+
serializeFrame,
|
|
1129
|
+
serializeMessage,
|
|
1130
|
+
splitFrame,
|
|
1131
|
+
wireMethodName
|
|
1132
|
+
};
|
|
1133
|
+
//# sourceMappingURL=index.js.map
|