rivetkit 2.0.30 → 2.0.32
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/schemas/actor-persist/v1.ts +2 -2
- package/dist/schemas/actor-persist/v2.ts +2 -2
- package/dist/schemas/actor-persist/v3.ts +2 -2
- package/dist/schemas/client-protocol/v1.ts +2 -2
- package/dist/schemas/client-protocol/v2.ts +2 -2
- package/dist/schemas/file-system-driver/v1.ts +2 -2
- package/dist/schemas/file-system-driver/v2.ts +2 -2
- package/dist/schemas/file-system-driver/v3.ts +2 -2
- package/dist/tsup/actor/errors.cjs +4 -2
- package/dist/tsup/actor/errors.cjs.map +1 -1
- package/dist/tsup/actor/errors.d.cts +5 -2
- package/dist/tsup/actor/errors.d.ts +5 -2
- package/dist/tsup/actor/errors.js +5 -3
- package/dist/tsup/{chunk-46DWBVYE.cjs → chunk-24MVWG2B.cjs} +15 -15
- package/dist/tsup/{chunk-46DWBVYE.cjs.map → chunk-24MVWG2B.cjs.map} +1 -1
- package/dist/tsup/{chunk-DGSYEC34.js → chunk-2E6QYC2Y.js} +18 -18
- package/dist/tsup/chunk-2E6QYC2Y.js.map +1 -0
- package/dist/tsup/{chunk-23EJLAOV.cjs → chunk-3YT2J462.cjs} +8 -8
- package/dist/tsup/{chunk-23EJLAOV.cjs.map → chunk-3YT2J462.cjs.map} +1 -1
- package/dist/tsup/{chunk-GOC4GSPT.js → chunk-46RC64TZ.js} +119 -32
- package/dist/tsup/chunk-46RC64TZ.js.map +1 -0
- package/dist/tsup/{chunk-REMOXAIW.cjs → chunk-4WDHG57J.cjs} +21 -10
- package/dist/tsup/chunk-4WDHG57J.cjs.map +1 -0
- package/dist/tsup/{chunk-YBOQOQZB.js → chunk-5MD6ZIUI.js} +172 -115
- package/dist/tsup/chunk-5MD6ZIUI.js.map +1 -0
- package/dist/tsup/{chunk-ZUJRXXQC.cjs → chunk-GDXRMMOY.cjs} +372 -315
- package/dist/tsup/chunk-GDXRMMOY.cjs.map +1 -0
- package/dist/tsup/{chunk-MAQSR26X.cjs → chunk-I2OKFOBV.cjs} +16 -5
- package/dist/tsup/chunk-I2OKFOBV.cjs.map +1 -0
- package/dist/tsup/{chunk-3WG6PXWE.cjs → chunk-JFUFZNBY.cjs} +3 -3
- package/dist/tsup/{chunk-3WG6PXWE.cjs.map → chunk-JFUFZNBY.cjs.map} +1 -1
- package/dist/tsup/{chunk-DI7LJEYL.cjs → chunk-JXZMBI2W.cjs} +6 -6
- package/dist/tsup/{chunk-DI7LJEYL.cjs.map → chunk-JXZMBI2W.cjs.map} +1 -1
- package/dist/tsup/{chunk-VROCBPWT.cjs → chunk-KCP36F62.cjs} +10 -10
- package/dist/tsup/{chunk-VROCBPWT.cjs.map → chunk-KCP36F62.cjs.map} +1 -1
- package/dist/tsup/{chunk-Z33UBLLH.js → chunk-KSKIKS4Q.js} +9 -9
- package/dist/tsup/{chunk-Z33UBLLH.js.map → chunk-KSKIKS4Q.js.map} +1 -1
- package/dist/tsup/{chunk-NYQJHQHK.cjs → chunk-MJX33BZM.cjs} +283 -196
- package/dist/tsup/chunk-MJX33BZM.cjs.map +1 -0
- package/dist/tsup/{chunk-FVSTM7QK.cjs → chunk-MR3O2TFJ.cjs} +3 -3
- package/dist/tsup/{chunk-FVSTM7QK.cjs.map → chunk-MR3O2TFJ.cjs.map} +1 -1
- package/dist/tsup/{chunk-EOXUA7SX.js → chunk-OKZQC52X.js} +2 -2
- package/dist/tsup/{chunk-K2UD42XA.js → chunk-QCVN5ZWE.js} +2 -2
- package/dist/tsup/{chunk-DQH5K5TL.js → chunk-RQEUDCBR.js} +3 -3
- package/dist/tsup/{chunk-DQH5K5TL.js.map → chunk-RQEUDCBR.js.map} +1 -1
- package/dist/tsup/{chunk-HPIRVETT.js → chunk-RSSAT5PN.js} +3 -3
- package/dist/tsup/{chunk-HPIRVETT.js.map → chunk-RSSAT5PN.js.map} +1 -1
- package/dist/tsup/{chunk-F4CRQFYG.cjs → chunk-TEDQEGUV.cjs} +30 -30
- package/dist/tsup/chunk-TEDQEGUV.cjs.map +1 -0
- package/dist/tsup/{chunk-OI6FEIRD.js → chunk-TK7XXGVD.js} +15 -4
- package/dist/tsup/chunk-TK7XXGVD.js.map +1 -0
- package/dist/tsup/{chunk-SLAUR4QB.js → chunk-UMVOVPLU.js} +2 -2
- package/dist/tsup/{chunk-P2RZJPYI.js → chunk-V35I3JSW.js} +16 -5
- package/dist/tsup/chunk-V35I3JSW.js.map +1 -0
- package/dist/tsup/client/mod.cjs +11 -9
- package/dist/tsup/client/mod.cjs.map +1 -1
- package/dist/tsup/client/mod.d.cts +2 -2
- package/dist/tsup/client/mod.d.ts +2 -2
- package/dist/tsup/client/mod.js +10 -8
- package/dist/tsup/common/log.cjs +3 -3
- package/dist/tsup/common/log.js +2 -2
- package/dist/tsup/common/websocket.cjs +4 -4
- package/dist/tsup/common/websocket.js +3 -3
- package/dist/tsup/{config-Dj5nTCrh.d.cts → config-D6nMVDna.d.cts} +36 -1
- package/dist/tsup/{config-Cs3B9xN9.d.ts → config-DN0AurPi.d.ts} +36 -1
- package/dist/tsup/driver-helpers/mod.cjs +5 -5
- package/dist/tsup/driver-helpers/mod.d.cts +1 -1
- package/dist/tsup/driver-helpers/mod.d.ts +1 -1
- package/dist/tsup/driver-helpers/mod.js +4 -4
- package/dist/tsup/driver-test-suite/mod.cjs +172 -74
- package/dist/tsup/driver-test-suite/mod.cjs.map +1 -1
- package/dist/tsup/driver-test-suite/mod.d.cts +1 -1
- package/dist/tsup/driver-test-suite/mod.d.ts +1 -1
- package/dist/tsup/driver-test-suite/mod.js +113 -15
- package/dist/tsup/driver-test-suite/mod.js.map +1 -1
- package/dist/tsup/inspector/mod.cjs +6 -6
- package/dist/tsup/inspector/mod.d.cts +2 -2
- package/dist/tsup/inspector/mod.d.ts +2 -2
- package/dist/tsup/inspector/mod.js +5 -5
- package/dist/tsup/mod.cjs +10 -10
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +2 -2
- package/dist/tsup/mod.d.ts +2 -2
- package/dist/tsup/mod.js +9 -9
- package/dist/tsup/test/mod.cjs +12 -12
- package/dist/tsup/test/mod.d.cts +1 -1
- package/dist/tsup/test/mod.d.ts +1 -1
- package/dist/tsup/test/mod.js +11 -11
- package/dist/tsup/utils.cjs +5 -3
- package/dist/tsup/utils.cjs.map +1 -1
- package/dist/tsup/utils.d.cts +3 -1
- package/dist/tsup/utils.d.ts +3 -1
- package/dist/tsup/utils.js +4 -2
- package/package.json +3 -3
- package/src/actor/config.ts +3 -2
- package/src/actor/conn/drivers/websocket.ts +15 -0
- package/src/actor/errors.ts +14 -3
- package/src/actor/instance/event-manager.ts +7 -0
- package/src/actor/instance/state-manager.ts +1 -1
- package/src/actor/protocol/old.ts +1 -1
- package/src/actor/protocol/serde.ts +1 -1
- package/src/actor/router-endpoints.ts +13 -1
- package/src/actor/router-websocket-endpoints.ts +5 -0
- package/src/client/actor-conn.ts +196 -133
- package/src/client/config.ts +4 -1
- package/src/client/mod.ts +3 -1
- package/src/client/utils.ts +1 -1
- package/src/driver-test-suite/tests/action-features.ts +63 -0
- package/src/driver-test-suite/tests/actor-conn.ts +91 -0
- package/src/drivers/file-system/global-state.ts +4 -5
- package/src/drivers/file-system/manager.ts +12 -4
- package/src/engine-process/mod.ts +16 -10
- package/src/registry/run-config.ts +3 -0
- package/src/registry/serve.ts +96 -7
- package/src/schemas/actor-persist/versioned.ts +3 -4
- package/src/serde.ts +1 -1
- package/src/test/mod.ts +1 -1
- package/src/utils.ts +14 -0
- package/dist/tsup/chunk-DGSYEC34.js.map +0 -1
- package/dist/tsup/chunk-F4CRQFYG.cjs.map +0 -1
- package/dist/tsup/chunk-GOC4GSPT.js.map +0 -1
- package/dist/tsup/chunk-MAQSR26X.cjs.map +0 -1
- package/dist/tsup/chunk-NYQJHQHK.cjs.map +0 -1
- package/dist/tsup/chunk-OI6FEIRD.js.map +0 -1
- package/dist/tsup/chunk-P2RZJPYI.js.map +0 -1
- package/dist/tsup/chunk-REMOXAIW.cjs.map +0 -1
- package/dist/tsup/chunk-YBOQOQZB.js.map +0 -1
- package/dist/tsup/chunk-ZUJRXXQC.cjs.map +0 -1
- /package/dist/tsup/{chunk-EOXUA7SX.js.map → chunk-OKZQC52X.js.map} +0 -0
- /package/dist/tsup/{chunk-K2UD42XA.js.map → chunk-QCVN5ZWE.js.map} +0 -0
- /package/dist/tsup/{chunk-SLAUR4QB.js.map → chunk-UMVOVPLU.js.map} +0 -0
package/src/client/actor-conn.ts
CHANGED
|
@@ -54,6 +54,16 @@ import {
|
|
|
54
54
|
sendHttpRequest,
|
|
55
55
|
} from "./utils";
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Connection status for an actor connection.
|
|
59
|
+
*
|
|
60
|
+
* - `"idle"`: Not connected, no auto-reconnect (initial state, after dispose, or disabled)
|
|
61
|
+
* - `"connecting"`: Attempting to establish connection
|
|
62
|
+
* - `"connected"`: Connection is active
|
|
63
|
+
* - `"disconnected"`: Connection was lost, will auto-reconnect
|
|
64
|
+
*/
|
|
65
|
+
export type ActorConnStatus = "idle" | "connecting" | "connected" | "disconnected";
|
|
66
|
+
|
|
57
67
|
interface ActionInFlight {
|
|
58
68
|
name: string;
|
|
59
69
|
resolve: (response: { id: bigint; output: unknown }) => void;
|
|
@@ -86,6 +96,13 @@ export type ActorErrorCallback = (error: errors.ActorError) => void;
|
|
|
86
96
|
*/
|
|
87
97
|
export type ConnectionStateCallback = () => void;
|
|
88
98
|
|
|
99
|
+
/**
|
|
100
|
+
* A callback for connection status changes.
|
|
101
|
+
*
|
|
102
|
+
* @typedef {Function} StatusChangeCallback
|
|
103
|
+
*/
|
|
104
|
+
export type StatusChangeCallback = (status: ActorConnStatus) => void;
|
|
105
|
+
|
|
89
106
|
export interface SendHttpMessageOpts {
|
|
90
107
|
ephemeral: boolean;
|
|
91
108
|
signal?: AbortSignal;
|
|
@@ -104,10 +121,10 @@ export class ActorConnRaw {
|
|
|
104
121
|
/* Will be aborted on dispose. */
|
|
105
122
|
#abortController = new AbortController();
|
|
106
123
|
|
|
107
|
-
#
|
|
124
|
+
#connStatus: ActorConnStatus = "idle";
|
|
108
125
|
|
|
109
126
|
#actorId?: string;
|
|
110
|
-
#
|
|
127
|
+
#connId?: string;
|
|
111
128
|
|
|
112
129
|
#messageQueue: Array<{
|
|
113
130
|
body:
|
|
@@ -128,8 +145,7 @@ export class ActorConnRaw {
|
|
|
128
145
|
#errorHandlers = new Set<ActorErrorCallback>();
|
|
129
146
|
#openHandlers = new Set<ConnectionStateCallback>();
|
|
130
147
|
#closeHandlers = new Set<ConnectionStateCallback>();
|
|
131
|
-
|
|
132
|
-
#isConnected = false;
|
|
148
|
+
#statusChangeHandlers = new Set<StatusChangeCallback>();
|
|
133
149
|
|
|
134
150
|
#actionIdCounter = 0;
|
|
135
151
|
|
|
@@ -249,39 +265,86 @@ enc
|
|
|
249
265
|
this.#connectWithRetry();
|
|
250
266
|
}
|
|
251
267
|
|
|
252
|
-
|
|
253
|
-
|
|
268
|
+
#setConnStatus(status: ActorConnStatus) {
|
|
269
|
+
const prevStatus = this.#connStatus;
|
|
270
|
+
if (prevStatus === status) return;
|
|
271
|
+
this.#connStatus = status;
|
|
254
272
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
273
|
+
// Notify status change handlers
|
|
274
|
+
for (const handler of [...this.#statusChangeHandlers]) {
|
|
275
|
+
try {
|
|
276
|
+
handler(status);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
logger().error({
|
|
279
|
+
msg: "error in status change handler",
|
|
280
|
+
error: stringifyError(err),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
261
284
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
285
|
+
// Notify open handlers
|
|
286
|
+
if (status === "connected") {
|
|
287
|
+
for (const handler of [...this.#openHandlers]) {
|
|
288
|
+
try {
|
|
289
|
+
handler();
|
|
290
|
+
} catch (err) {
|
|
291
|
+
logger().error({
|
|
292
|
+
msg: "error in open handler",
|
|
293
|
+
error: stringifyError(err),
|
|
267
294
|
});
|
|
268
|
-
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
269
298
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
299
|
+
// Notify close handlers (only if transitioning from Connected to Disconnected or Idle)
|
|
300
|
+
if (
|
|
301
|
+
(status === "disconnected" ||
|
|
302
|
+
status === "idle") &&
|
|
303
|
+
prevStatus === "connected"
|
|
304
|
+
) {
|
|
305
|
+
for (const handler of [...this.#closeHandlers]) {
|
|
306
|
+
try {
|
|
307
|
+
handler();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
logger().error({
|
|
310
|
+
msg: "error in close handler",
|
|
311
|
+
error: stringifyError(err),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#connectWithRetry() {
|
|
319
|
+
this.#setConnStatus("connecting");
|
|
320
|
+
|
|
321
|
+
// Attempt to reconnect indefinitely
|
|
322
|
+
// This is intentionally not awaited - connection happens in background
|
|
323
|
+
pRetry(this.#connectAndWait.bind(this), {
|
|
324
|
+
forever: true,
|
|
325
|
+
minTimeout: 250,
|
|
326
|
+
maxTimeout: 30_000,
|
|
327
|
+
|
|
328
|
+
onFailedAttempt: (error) => {
|
|
329
|
+
logger().warn({
|
|
330
|
+
msg: "failed to reconnect",
|
|
331
|
+
attempt: error.attemptNumber,
|
|
332
|
+
error: stringifyError(error),
|
|
333
|
+
});
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
// Cancel retry if aborted
|
|
337
|
+
signal: this.#abortController.signal,
|
|
338
|
+
}).catch((err) => {
|
|
274
339
|
if ((err as Error).name === "AbortError") {
|
|
275
|
-
// Ignore abortions
|
|
276
340
|
logger().info({ msg: "connection retry aborted" });
|
|
277
|
-
return;
|
|
278
341
|
} else {
|
|
279
|
-
|
|
280
|
-
|
|
342
|
+
logger().error({
|
|
343
|
+
msg: "unexpected error in connection retry",
|
|
344
|
+
error: stringifyError(err),
|
|
345
|
+
});
|
|
281
346
|
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
this.#connecting = false;
|
|
347
|
+
});
|
|
285
348
|
}
|
|
286
349
|
|
|
287
350
|
async #connectAndWait() {
|
|
@@ -315,7 +378,7 @@ enc
|
|
|
315
378
|
);
|
|
316
379
|
logger().debug({
|
|
317
380
|
msg: "opened websocket",
|
|
318
|
-
|
|
381
|
+
connId: this.#connId,
|
|
319
382
|
readyState: ws.readyState,
|
|
320
383
|
messageQueueLength: this.#messageQueue.length,
|
|
321
384
|
});
|
|
@@ -323,7 +386,7 @@ enc
|
|
|
323
386
|
ws.addEventListener("open", () => {
|
|
324
387
|
logger().debug({
|
|
325
388
|
msg: "client websocket open",
|
|
326
|
-
|
|
389
|
+
connId: this.#connId,
|
|
327
390
|
});
|
|
328
391
|
});
|
|
329
392
|
ws.addEventListener("message", async (ev) => {
|
|
@@ -360,14 +423,24 @@ enc
|
|
|
360
423
|
|
|
361
424
|
/** Called by the onopen event from drivers. */
|
|
362
425
|
#handleOnOpen() {
|
|
426
|
+
// Connection was disposed before Init message arrived - close the websocket to avoid leak
|
|
427
|
+
if (this.#disposed) {
|
|
428
|
+
logger().debug({ msg: "handleOnOpen called after dispose, closing websocket" });
|
|
429
|
+
if (this.#websocket) {
|
|
430
|
+
this.#websocket.close(1000, "Disposed");
|
|
431
|
+
this.#websocket = undefined;
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
363
436
|
logger().debug({
|
|
364
437
|
msg: "socket open",
|
|
365
438
|
messageQueueLength: this.#messageQueue.length,
|
|
366
|
-
|
|
439
|
+
connId: this.#connId,
|
|
367
440
|
});
|
|
368
441
|
|
|
369
|
-
// Update connection state
|
|
370
|
-
this.#
|
|
442
|
+
// Update connection state (this also notifies handlers)
|
|
443
|
+
this.#setConnStatus("connected");
|
|
371
444
|
|
|
372
445
|
// Resolve open promise
|
|
373
446
|
if (this.#onOpenPromise) {
|
|
@@ -376,18 +449,6 @@ enc
|
|
|
376
449
|
logger().warn({ msg: "#onOpenPromise is undefined" });
|
|
377
450
|
}
|
|
378
451
|
|
|
379
|
-
// Notify open handlers
|
|
380
|
-
for (const handler of [...this.#openHandlers]) {
|
|
381
|
-
try {
|
|
382
|
-
handler();
|
|
383
|
-
} catch (err) {
|
|
384
|
-
logger().error({
|
|
385
|
-
msg: "error in open handler",
|
|
386
|
-
error: stringifyError(err),
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
452
|
// Resubscribe to all active events
|
|
392
453
|
for (const eventName of this.#eventSubscriptions.keys()) {
|
|
393
454
|
this.#sendSubscription(eventName, true);
|
|
@@ -431,11 +492,11 @@ enc
|
|
|
431
492
|
if (response.body.tag === "Init") {
|
|
432
493
|
// Store connection info
|
|
433
494
|
this.#actorId = response.body.val.actorId;
|
|
434
|
-
this.#
|
|
495
|
+
this.#connId = response.body.val.connectionId;
|
|
435
496
|
logger().trace({
|
|
436
497
|
msg: "received init message",
|
|
437
498
|
actorId: this.#actorId,
|
|
438
|
-
|
|
499
|
+
connId: this.#connId,
|
|
439
500
|
});
|
|
440
501
|
this.#handleOnOpen();
|
|
441
502
|
} else if (response.body.tag === "Error") {
|
|
@@ -520,86 +581,54 @@ enc
|
|
|
520
581
|
|
|
521
582
|
/** Called by the onclose event from drivers. */
|
|
522
583
|
#handleOnClose(event: Event | CloseEvent) {
|
|
523
|
-
// TODO: Handle queue
|
|
524
|
-
// TODO: Reconnect with backoff
|
|
525
|
-
|
|
526
584
|
// We can't use `event instanceof CloseEvent` because it's not defined in NodeJS
|
|
527
|
-
//
|
|
528
|
-
// These properties will be undefined
|
|
529
585
|
const closeEvent = event as CloseEvent;
|
|
530
586
|
const wasClean = closeEvent.wasClean;
|
|
531
|
-
|
|
532
|
-
// Update connection state and notify handlers
|
|
533
|
-
const wasConnected = this.#isConnected;
|
|
534
|
-
this.#isConnected = false;
|
|
535
|
-
|
|
536
|
-
if (wasConnected) {
|
|
537
|
-
for (const handler of [...this.#closeHandlers]) {
|
|
538
|
-
try {
|
|
539
|
-
handler();
|
|
540
|
-
} catch (err) {
|
|
541
|
-
logger().error({
|
|
542
|
-
msg: "error in close handler",
|
|
543
|
-
error: stringifyError(err),
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Reject open promise
|
|
550
|
-
if (this.#onOpenPromise) {
|
|
551
|
-
this.#onOpenPromise.reject(
|
|
552
|
-
new Error(
|
|
553
|
-
`websocket closed with code ${closeEvent.code}: ${closeEvent.reason}`,
|
|
554
|
-
),
|
|
555
|
-
);
|
|
556
|
-
}
|
|
587
|
+
const wasConnected = this.#connStatus === "connected";
|
|
557
588
|
|
|
558
589
|
logger().info({
|
|
559
590
|
msg: "socket closed",
|
|
560
591
|
code: closeEvent.code,
|
|
561
592
|
reason: closeEvent.reason,
|
|
562
|
-
wasClean
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
actionsInFlight: this.#actionsInFlight.size,
|
|
593
|
+
wasClean,
|
|
594
|
+
disposed: this.#disposed,
|
|
595
|
+
connId: this.#connId,
|
|
566
596
|
});
|
|
567
597
|
|
|
568
|
-
|
|
569
|
-
if (this.#actionsInFlight.size > 0) {
|
|
570
|
-
logger().debug({
|
|
571
|
-
msg: "rejecting in-flight actions after disconnect",
|
|
572
|
-
count: this.#actionsInFlight.size,
|
|
573
|
-
connectionId: this.#connectionId,
|
|
574
|
-
wasClean,
|
|
575
|
-
});
|
|
598
|
+
this.#websocket = undefined;
|
|
576
599
|
|
|
577
|
-
|
|
578
|
-
|
|
600
|
+
if (this.#disposed) {
|
|
601
|
+
// Use ActorConnDisposed error and prevent unhandled rejection
|
|
602
|
+
this.#rejectPendingPromises(new errors.ActorConnDisposed(), true);
|
|
603
|
+
} else {
|
|
604
|
+
this.#setConnStatus("disconnected");
|
|
605
|
+
this.#rejectPendingPromises(
|
|
606
|
+
new Error(
|
|
607
|
+
`${wasClean ? "Connection closed" : "Connection lost"} (code: ${closeEvent.code}, reason: ${closeEvent.reason})`,
|
|
608
|
+
),
|
|
609
|
+
false,
|
|
579
610
|
);
|
|
580
611
|
|
|
581
|
-
|
|
582
|
-
|
|
612
|
+
// Automatically reconnect if we were connected
|
|
613
|
+
if (wasConnected) {
|
|
614
|
+
logger().debug({ msg: "triggering reconnect", connId: this.#connId });
|
|
615
|
+
this.#connectWithRetry();
|
|
583
616
|
}
|
|
584
|
-
this.#actionsInFlight.clear();
|
|
585
617
|
}
|
|
618
|
+
}
|
|
586
619
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
messageQueueLength: this.#messageQueue.length,
|
|
595
|
-
});
|
|
596
|
-
// TODO: Fetch actor to check if it's destroyed
|
|
597
|
-
// TODO: Add backoff for reconnect
|
|
598
|
-
// TODO: Add a way of preserving connection ID for connection state
|
|
620
|
+
#rejectPendingPromises(error: Error, suppressUnhandled: boolean) {
|
|
621
|
+
if (this.#onOpenPromise) {
|
|
622
|
+
if (suppressUnhandled) {
|
|
623
|
+
this.#onOpenPromise.promise.catch(() => {});
|
|
624
|
+
}
|
|
625
|
+
this.#onOpenPromise.reject(error);
|
|
626
|
+
}
|
|
599
627
|
|
|
600
|
-
|
|
601
|
-
|
|
628
|
+
for (const actionInfo of this.#actionsInFlight.values()) {
|
|
629
|
+
actionInfo.reject(error);
|
|
602
630
|
}
|
|
631
|
+
this.#actionsInFlight.clear();
|
|
603
632
|
}
|
|
604
633
|
|
|
605
634
|
/** Called by the onerror event from drivers. */
|
|
@@ -751,13 +780,23 @@ enc
|
|
|
751
780
|
};
|
|
752
781
|
}
|
|
753
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Returns the current connection status.
|
|
785
|
+
*
|
|
786
|
+
* @returns {ActorConnStatus} - The current connection status.
|
|
787
|
+
*/
|
|
788
|
+
get connStatus(): ActorConnStatus {
|
|
789
|
+
return this.#connStatus;
|
|
790
|
+
}
|
|
791
|
+
|
|
754
792
|
/**
|
|
755
793
|
* Returns whether the connection is currently open.
|
|
756
794
|
*
|
|
795
|
+
* @deprecated Use `connStatus` instead.
|
|
757
796
|
* @returns {boolean} - True if the connection is open, false otherwise.
|
|
758
797
|
*/
|
|
759
798
|
get isConnected(): boolean {
|
|
760
|
-
return this.#
|
|
799
|
+
return this.#connStatus === "connected";
|
|
761
800
|
}
|
|
762
801
|
|
|
763
802
|
/**
|
|
@@ -795,6 +834,23 @@ enc
|
|
|
795
834
|
};
|
|
796
835
|
}
|
|
797
836
|
|
|
837
|
+
/**
|
|
838
|
+
* Subscribes to connection status changes.
|
|
839
|
+
*
|
|
840
|
+
* This is called whenever the connection status changes between Disconnected, Connecting, and Connected.
|
|
841
|
+
*
|
|
842
|
+
* @param {StatusChangeCallback} callback - The callback function to execute when the status changes.
|
|
843
|
+
* @returns {() => void} - A function to unsubscribe from the status change handler.
|
|
844
|
+
*/
|
|
845
|
+
onStatusChange(callback: StatusChangeCallback): () => void {
|
|
846
|
+
this.#statusChangeHandlers.add(callback);
|
|
847
|
+
|
|
848
|
+
// Return unsubscribe function
|
|
849
|
+
return () => {
|
|
850
|
+
this.#statusChangeHandlers.delete(callback);
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
798
854
|
#sendMessage(
|
|
799
855
|
message: {
|
|
800
856
|
body:
|
|
@@ -810,7 +866,11 @@ enc
|
|
|
810
866
|
opts?: SendHttpMessageOpts,
|
|
811
867
|
) {
|
|
812
868
|
if (this.#disposed) {
|
|
813
|
-
|
|
869
|
+
if (opts?.ephemeral) {
|
|
870
|
+
return;
|
|
871
|
+
} else {
|
|
872
|
+
throw new errors.ActorConnDisposed();
|
|
873
|
+
}
|
|
814
874
|
}
|
|
815
875
|
|
|
816
876
|
let queueMessage = false;
|
|
@@ -827,7 +887,7 @@ enc
|
|
|
827
887
|
: readyState === 2
|
|
828
888
|
? "CLOSING"
|
|
829
889
|
: "CLOSED",
|
|
830
|
-
|
|
890
|
+
connId: this.#connId,
|
|
831
891
|
messageType: (message.body as any).tag,
|
|
832
892
|
actionName: (message.body as any).val?.name,
|
|
833
893
|
});
|
|
@@ -837,7 +897,7 @@ enc
|
|
|
837
897
|
this.#encoding,
|
|
838
898
|
message,
|
|
839
899
|
TO_SERVER_VERSIONED,
|
|
840
|
-
|
|
900
|
+
CLIENT_PROTOCOL_CURRENT_VERSION,
|
|
841
901
|
ToServerSchema,
|
|
842
902
|
// JSON: args is the raw value
|
|
843
903
|
(msg): ToServerJson => msg as ToServerJson,
|
|
@@ -870,7 +930,7 @@ enc
|
|
|
870
930
|
logger().warn({
|
|
871
931
|
msg: "failed to send message, added to queue",
|
|
872
932
|
error,
|
|
873
|
-
|
|
933
|
+
connId: this.#connId,
|
|
874
934
|
});
|
|
875
935
|
|
|
876
936
|
// Assuming the socket is disconnected and will be reconnected soon
|
|
@@ -894,7 +954,7 @@ enc
|
|
|
894
954
|
logger().debug({
|
|
895
955
|
msg: "queued connection message",
|
|
896
956
|
queueLength: this.#messageQueue.length,
|
|
897
|
-
|
|
957
|
+
connId: this.#connId,
|
|
898
958
|
messageType: (message.body as any).tag,
|
|
899
959
|
actionName: (message.body as any).val?.name,
|
|
900
960
|
});
|
|
@@ -993,8 +1053,17 @@ enc
|
|
|
993
1053
|
* Get the connection ID (for testing purposes).
|
|
994
1054
|
* @internal
|
|
995
1055
|
*/
|
|
1056
|
+
get connId(): string | undefined {
|
|
1057
|
+
return this.#connId;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Get the connection ID (for testing purposes).
|
|
1062
|
+
* @internal
|
|
1063
|
+
* @deprecated Use `connId` instead.
|
|
1064
|
+
*/
|
|
996
1065
|
get connectionId(): string | undefined {
|
|
997
|
-
return this.#
|
|
1066
|
+
return this.#connId;
|
|
998
1067
|
}
|
|
999
1068
|
|
|
1000
1069
|
/**
|
|
@@ -1013,35 +1082,29 @@ enc
|
|
|
1013
1082
|
|
|
1014
1083
|
logger().debug({ msg: "disposing actor conn" });
|
|
1015
1084
|
|
|
1085
|
+
// Set status to Idle (intentionally closed, no auto-reconnect)
|
|
1086
|
+
this.#setConnStatus("idle");
|
|
1087
|
+
|
|
1016
1088
|
// Clear interval so NodeJS process can exit
|
|
1017
1089
|
clearInterval(this.#keepNodeAliveInterval);
|
|
1018
1090
|
|
|
1019
|
-
// Abort
|
|
1091
|
+
// Abort retry loop
|
|
1020
1092
|
this.#abortController.abort();
|
|
1021
1093
|
|
|
1022
1094
|
// Remove from registry
|
|
1023
1095
|
this.#client[ACTOR_CONNS_SYMBOL].delete(this);
|
|
1024
1096
|
|
|
1025
|
-
//
|
|
1097
|
+
// Close websocket (#handleOnClose will reject pending promises)
|
|
1026
1098
|
if (this.#websocket) {
|
|
1027
|
-
logger().debug("closing ws");
|
|
1028
|
-
|
|
1029
1099
|
const ws = this.#websocket;
|
|
1030
|
-
|
|
1031
|
-
if (
|
|
1032
|
-
ws.readyState === 2 /* CLOSING */ ||
|
|
1033
|
-
ws.readyState === 3 /* CLOSED */
|
|
1034
|
-
) {
|
|
1035
|
-
logger().debug({ msg: "ws already closed or closing" });
|
|
1036
|
-
} else {
|
|
1100
|
+
if (ws.readyState !== 2 /* CLOSING */ && ws.readyState !== 3 /* CLOSED */) {
|
|
1037
1101
|
const { promise, resolve } = promiseWithResolvers();
|
|
1038
|
-
ws.addEventListener("close", () =>
|
|
1039
|
-
|
|
1040
|
-
resolve(undefined);
|
|
1041
|
-
});
|
|
1042
|
-
ws.close(1000, "Normal closure");
|
|
1102
|
+
ws.addEventListener("close", () => resolve(undefined));
|
|
1103
|
+
ws.close(1000, "Disposed");
|
|
1043
1104
|
await promise;
|
|
1044
1105
|
}
|
|
1106
|
+
} else {
|
|
1107
|
+
this.#rejectPendingPromises(new errors.ActorConnDisposed(), true);
|
|
1045
1108
|
}
|
|
1046
1109
|
this.#websocket = undefined;
|
|
1047
1110
|
}
|
package/src/client/config.ts
CHANGED
|
@@ -32,7 +32,10 @@ export const ClientConfigSchema = z.object({
|
|
|
32
32
|
|
|
33
33
|
encoding: EncodingSchema.default("bare"),
|
|
34
34
|
|
|
35
|
-
headers: z
|
|
35
|
+
headers: z
|
|
36
|
+
.record(z.string(), z.string())
|
|
37
|
+
.optional()
|
|
38
|
+
.default(() => ({})),
|
|
36
39
|
|
|
37
40
|
// See RunConfig.getUpgradeWebSocket
|
|
38
41
|
getUpgradeWebSocket: z.custom<GetUpgradeWebSocket>().optional(),
|
package/src/client/mod.ts
CHANGED
|
@@ -14,6 +14,7 @@ export {
|
|
|
14
14
|
export type { Encoding } from "@/actor/protocol/serde";
|
|
15
15
|
export {
|
|
16
16
|
ActorClientError,
|
|
17
|
+
ActorConnDisposed,
|
|
17
18
|
ActorError,
|
|
18
19
|
InternalError,
|
|
19
20
|
MalformedResponseMessage,
|
|
@@ -25,8 +26,9 @@ export type {
|
|
|
25
26
|
ActorConn,
|
|
26
27
|
ConnectionStateCallback,
|
|
27
28
|
EventUnsubscribe,
|
|
29
|
+
StatusChangeCallback,
|
|
28
30
|
} from "./actor-conn";
|
|
29
|
-
export { ActorConnRaw } from "./actor-conn";
|
|
31
|
+
export { ActorConnRaw, ActorConnStatus } from "./actor-conn";
|
|
30
32
|
export type { ActorHandle } from "./actor-handle";
|
|
31
33
|
export { ActorHandleRaw } from "./actor-handle";
|
|
32
34
|
export type {
|
package/src/client/utils.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as cbor from "cbor-x";
|
|
2
2
|
import invariant from "invariant";
|
|
3
|
+
import type { VersionedDataHandler } from "vbare";
|
|
3
4
|
import type { z } from "zod";
|
|
4
5
|
import type { Encoding } from "@/actor/protocol/serde";
|
|
5
6
|
import { assertUnreachable } from "@/common/utils";
|
|
6
|
-
import type { VersionedDataHandler } from "vbare";
|
|
7
7
|
import type { HttpResponseError } from "@/schemas/client-protocol/mod";
|
|
8
8
|
import { HTTP_RESPONSE_ERROR_VERSIONED } from "@/schemas/client-protocol/versioned";
|
|
9
9
|
import {
|
|
@@ -136,5 +136,68 @@ export function runActionFeaturesTests(driverTestConfig: DriverTestConfig) {
|
|
|
136
136
|
expect(results).toContain("delayed");
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
|
+
|
|
140
|
+
describe("Large Payloads", () => {
|
|
141
|
+
test("should handle large request within size limit", async (c) => {
|
|
142
|
+
const { client } = await setupDriverTest(c, driverTestConfig);
|
|
143
|
+
|
|
144
|
+
const instance = client.largePayloadActor.getOrCreate();
|
|
145
|
+
|
|
146
|
+
// Create a large payload that's under the default 64KB limit
|
|
147
|
+
// Each item is roughly 60 bytes, so 800 items ≈ 48KB
|
|
148
|
+
const items: string[] = [];
|
|
149
|
+
for (let i = 0; i < 800; i++) {
|
|
150
|
+
items.push(`Item ${i} with some additional text to increase size`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await instance.processLargeRequest({ items });
|
|
154
|
+
|
|
155
|
+
expect(result.itemCount).toBe(800);
|
|
156
|
+
expect(result.firstItem).toBe("Item 0 with some additional text to increase size");
|
|
157
|
+
expect(result.lastItem).toBe("Item 799 with some additional text to increase size");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("should reject request exceeding maxIncomingMessageSize", async (c) => {
|
|
161
|
+
const { client } = await setupDriverTest(c, driverTestConfig);
|
|
162
|
+
|
|
163
|
+
const instance = client.largePayloadActor.getOrCreate();
|
|
164
|
+
|
|
165
|
+
// Create a payload that exceeds the default 64KB limit
|
|
166
|
+
// Each item is roughly 60 bytes, so 1500 items ≈ 90KB
|
|
167
|
+
const items: string[] = [];
|
|
168
|
+
for (let i = 0; i < 1500; i++) {
|
|
169
|
+
items.push(`Item ${i} with some additional text to increase size`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await expect(
|
|
173
|
+
instance.processLargeRequest({ items })
|
|
174
|
+
).rejects.toThrow();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("should handle large response", async (c) => {
|
|
178
|
+
const { client } = await setupDriverTest(c, driverTestConfig);
|
|
179
|
+
|
|
180
|
+
const instance = client.largePayloadActor.getOrCreate();
|
|
181
|
+
|
|
182
|
+
// Request a large response (800 items ≈ 48KB)
|
|
183
|
+
const result = await instance.getLargeResponse(800);
|
|
184
|
+
|
|
185
|
+
expect(result.items).toHaveLength(800);
|
|
186
|
+
expect(result.items[0]).toBe("Item 0 with some additional text to increase size");
|
|
187
|
+
expect(result.items[799]).toBe("Item 799 with some additional text to increase size");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("should reject response exceeding maxOutgoingMessageSize", async (c) => {
|
|
191
|
+
const { client } = await setupDriverTest(c, driverTestConfig);
|
|
192
|
+
|
|
193
|
+
const instance = client.largePayloadActor.getOrCreate();
|
|
194
|
+
|
|
195
|
+
// Request a response that exceeds the default 1MB limit
|
|
196
|
+
// Each item is roughly 60 bytes, so 20000 items ≈ 1.2MB
|
|
197
|
+
await expect(
|
|
198
|
+
instance.getLargeResponse(20000)
|
|
199
|
+
).rejects.toThrow();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
139
202
|
});
|
|
140
203
|
}
|