rivetkit 2.0.24-rc.1 → 2.0.24
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/v2.ts +3 -3
- package/dist/schemas/actor-persist/v3.ts +274 -0
- package/dist/schemas/client-protocol/v2.ts +432 -0
- package/dist/schemas/file-system-driver/v2.ts +136 -0
- package/dist/tsup/actor/errors.cjs +2 -4
- package/dist/tsup/actor/errors.cjs.map +1 -1
- package/dist/tsup/actor/errors.d.cts +7 -10
- package/dist/tsup/actor/errors.d.ts +7 -10
- package/dist/tsup/actor/errors.js +9 -11
- package/dist/tsup/{actor-router-consts-B3Lu87yJ.d.cts → actor-router-consts-DzI2szci.d.cts} +5 -9
- package/dist/tsup/{actor-router-consts-B3Lu87yJ.d.ts → actor-router-consts-DzI2szci.d.ts} +5 -9
- package/dist/tsup/{chunk-HHFKKVLR.cjs → chunk-3543NCSN.cjs} +45 -57
- package/dist/tsup/chunk-3543NCSN.cjs.map +1 -0
- package/dist/tsup/chunk-4SHILYS5.cjs +5694 -0
- package/dist/tsup/chunk-4SHILYS5.cjs.map +1 -0
- package/dist/tsup/{chunk-ZTH3KYFH.cjs → chunk-5BZO5XPS.cjs} +3 -3
- package/dist/tsup/{chunk-ZTH3KYFH.cjs.map → chunk-5BZO5XPS.cjs.map} +1 -1
- package/dist/tsup/{chunk-PLUN2NQT.js → chunk-BAIGSF64.js} +189 -187
- package/dist/tsup/chunk-BAIGSF64.js.map +1 -0
- package/dist/tsup/{chunk-SHVX2QUR.cjs → chunk-CHLZBSI2.cjs} +17 -17
- package/dist/tsup/chunk-CHLZBSI2.cjs.map +1 -0
- package/dist/tsup/chunk-D3SLADUD.cjs +512 -0
- package/dist/tsup/chunk-D3SLADUD.cjs.map +1 -0
- package/dist/tsup/{chunk-KSRXX3Z4.cjs → chunk-D6762AOA.cjs} +20 -25
- package/dist/tsup/chunk-D6762AOA.cjs.map +1 -0
- package/dist/tsup/{chunk-7L65NNWP.cjs → chunk-DLK5YCTN.cjs} +187 -185
- package/dist/tsup/chunk-DLK5YCTN.cjs.map +1 -0
- package/dist/tsup/{chunk-YBG6R7LX.js → chunk-DUJQWGYD.js} +3 -7
- package/dist/tsup/chunk-DUJQWGYD.js.map +1 -0
- package/dist/tsup/{chunk-CD33GT6Z.js → chunk-EIPANQMF.js} +2 -2
- package/dist/tsup/{chunk-2JYPS5YM.cjs → chunk-ESMTDP7G.cjs} +6 -6
- package/dist/tsup/chunk-ESMTDP7G.cjs.map +1 -0
- package/dist/tsup/{chunk-VHGY7PU5.cjs → chunk-FVAKREFB.cjs} +1900 -1737
- package/dist/tsup/chunk-FVAKREFB.cjs.map +1 -0
- package/dist/tsup/{chunk-BLK27ES3.js → chunk-I3XT7WOF.js} +44 -56
- package/dist/tsup/chunk-I3XT7WOF.js.map +1 -0
- package/dist/tsup/{chunk-YBHYXIP6.js → chunk-IMDS5T42.js} +3 -3
- package/dist/tsup/chunk-IMDS5T42.js.map +1 -0
- package/dist/tsup/{chunk-INNFK746.cjs → chunk-J3HZJF2P.cjs} +10 -14
- package/dist/tsup/chunk-J3HZJF2P.cjs.map +1 -0
- package/dist/tsup/{chunk-BYMKMOBS.js → chunk-MBBJUHSP.js} +1844 -1681
- package/dist/tsup/chunk-MBBJUHSP.js.map +1 -0
- package/dist/tsup/{chunk-BOMZS2TJ.js → chunk-MO5CB6MD.js} +9 -9
- package/dist/tsup/chunk-MO5CB6MD.js.map +1 -0
- package/dist/tsup/chunk-OFOTPKAH.js +512 -0
- package/dist/tsup/chunk-OFOTPKAH.js.map +1 -0
- package/dist/tsup/{chunk-G64QUEDJ.js → chunk-W6RDS6NW.js} +23 -28
- package/dist/tsup/chunk-W6RDS6NW.js.map +1 -0
- package/dist/tsup/{chunk-36JJ4IQB.cjs → chunk-YC5DUHPM.cjs} +4 -8
- package/dist/tsup/chunk-YC5DUHPM.cjs.map +1 -0
- package/dist/tsup/{chunk-FX7TWFQR.js → chunk-YC7YPM2T.js} +2 -6
- package/dist/tsup/chunk-YC7YPM2T.js.map +1 -0
- package/dist/tsup/{chunk-227FEWMB.js → chunk-ZSPU5R4C.js} +3322 -2251
- package/dist/tsup/chunk-ZSPU5R4C.js.map +1 -0
- package/dist/tsup/client/mod.cjs +9 -9
- package/dist/tsup/client/mod.d.cts +5 -7
- package/dist/tsup/client/mod.d.ts +5 -7
- package/dist/tsup/client/mod.js +8 -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/{conn-B3Vhbgnd.d.ts → config-BRDYDraU.d.cts} +1119 -1047
- package/dist/tsup/{conn-DJWL3nGx.d.cts → config-Bo-blHpJ.d.ts} +1119 -1047
- package/dist/tsup/driver-helpers/mod.cjs +5 -13
- package/dist/tsup/driver-helpers/mod.cjs.map +1 -1
- package/dist/tsup/driver-helpers/mod.d.cts +11 -9
- package/dist/tsup/driver-helpers/mod.d.ts +11 -9
- package/dist/tsup/driver-helpers/mod.js +14 -22
- package/dist/tsup/driver-test-suite/mod.cjs +474 -303
- package/dist/tsup/driver-test-suite/mod.cjs.map +1 -1
- package/dist/tsup/driver-test-suite/mod.d.cts +6 -9
- package/dist/tsup/driver-test-suite/mod.d.ts +6 -9
- package/dist/tsup/driver-test-suite/mod.js +1085 -914
- 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 +5 -7
- package/dist/tsup/inspector/mod.d.ts +5 -7
- package/dist/tsup/inspector/mod.js +5 -5
- package/dist/tsup/mod.cjs +10 -16
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +23 -25
- package/dist/tsup/mod.d.ts +23 -25
- package/dist/tsup/mod.js +17 -23
- package/dist/tsup/test/mod.cjs +11 -11
- package/dist/tsup/test/mod.d.cts +4 -6
- package/dist/tsup/test/mod.d.ts +4 -6
- package/dist/tsup/test/mod.js +10 -10
- package/dist/tsup/utils.cjs +3 -5
- package/dist/tsup/utils.cjs.map +1 -1
- package/dist/tsup/utils.d.cts +1 -2
- package/dist/tsup/utils.d.ts +1 -2
- package/dist/tsup/utils.js +2 -4
- package/package.json +13 -6
- package/src/actor/config.ts +56 -44
- package/src/actor/conn/driver.ts +61 -0
- package/src/actor/conn/drivers/http.ts +17 -0
- package/src/actor/conn/drivers/raw-request.ts +24 -0
- package/src/actor/conn/drivers/raw-websocket.ts +65 -0
- package/src/actor/conn/drivers/websocket.ts +129 -0
- package/src/actor/conn/mod.ts +232 -0
- package/src/actor/conn/persisted.ts +81 -0
- package/src/actor/conn/state-manager.ts +196 -0
- package/src/actor/contexts/action.ts +23 -0
- package/src/actor/{context.ts → contexts/actor.ts} +19 -8
- package/src/actor/contexts/conn-init.ts +31 -0
- package/src/actor/contexts/conn.ts +48 -0
- package/src/actor/contexts/create-conn-state.ts +13 -0
- package/src/actor/contexts/on-before-connect.ts +13 -0
- package/src/actor/contexts/on-connect.ts +22 -0
- package/src/actor/contexts/request.ts +48 -0
- package/src/actor/contexts/websocket.ts +48 -0
- package/src/actor/definition.ts +3 -3
- package/src/actor/driver.ts +36 -5
- package/src/actor/errors.ts +19 -24
- package/src/actor/instance/connection-manager.ts +465 -0
- package/src/actor/instance/event-manager.ts +292 -0
- package/src/actor/instance/kv.ts +15 -0
- package/src/actor/instance/mod.ts +1107 -0
- package/src/actor/instance/persisted.ts +67 -0
- package/src/actor/instance/schedule-manager.ts +349 -0
- package/src/actor/instance/state-manager.ts +502 -0
- package/src/actor/mod.ts +13 -16
- package/src/actor/protocol/old.ts +131 -43
- package/src/actor/protocol/serde.ts +19 -4
- package/src/actor/router-endpoints.ts +61 -586
- package/src/actor/router-websocket-endpoints.ts +408 -0
- package/src/actor/router.ts +63 -197
- package/src/actor/schedule.ts +1 -1
- package/src/client/actor-conn.ts +183 -249
- package/src/client/actor-handle.ts +29 -6
- package/src/client/client.ts +0 -4
- package/src/client/config.ts +1 -4
- package/src/client/mod.ts +0 -1
- package/src/client/raw-utils.ts +3 -3
- package/src/client/utils.ts +85 -39
- package/src/common/actor-router-consts.ts +5 -12
- package/src/common/{inline-websocket-adapter2.ts → inline-websocket-adapter.ts} +26 -48
- package/src/common/log.ts +1 -1
- package/src/common/router.ts +28 -17
- package/src/common/utils.ts +2 -0
- package/src/driver-helpers/mod.ts +7 -10
- package/src/driver-helpers/utils.ts +18 -9
- package/src/driver-test-suite/mod.ts +26 -50
- package/src/driver-test-suite/test-inline-client-driver.ts +27 -51
- package/src/driver-test-suite/tests/actor-conn-hibernation.ts +150 -0
- package/src/driver-test-suite/tests/actor-conn-state.ts +1 -4
- package/src/driver-test-suite/tests/actor-conn.ts +5 -9
- package/src/driver-test-suite/tests/actor-destroy.ts +294 -0
- package/src/driver-test-suite/tests/actor-driver.ts +0 -7
- package/src/driver-test-suite/tests/actor-handle.ts +12 -12
- package/src/driver-test-suite/tests/actor-metadata.ts +1 -1
- package/src/driver-test-suite/tests/manager-driver.ts +1 -1
- package/src/driver-test-suite/tests/raw-http-direct-registry.ts +8 -8
- package/src/driver-test-suite/tests/raw-http-request-properties.ts +6 -5
- package/src/driver-test-suite/tests/raw-http.ts +5 -5
- package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +7 -7
- package/src/driver-test-suite/tests/request-access.ts +4 -4
- package/src/driver-test-suite/utils.ts +6 -10
- package/src/drivers/engine/actor-driver.ts +614 -424
- package/src/drivers/engine/mod.ts +0 -1
- package/src/drivers/file-system/actor.ts +24 -12
- package/src/drivers/file-system/global-state.ts +427 -37
- package/src/drivers/file-system/manager.ts +71 -83
- package/src/drivers/file-system/mod.ts +3 -0
- package/src/drivers/file-system/utils.ts +18 -8
- package/src/engine-process/mod.ts +38 -38
- package/src/inspector/utils.ts +7 -5
- package/src/manager/driver.ts +11 -4
- package/src/manager/gateway.ts +4 -29
- package/src/manager/protocol/mod.ts +0 -2
- package/src/manager/protocol/query.ts +0 -4
- package/src/manager/router.ts +67 -64
- package/src/manager-api/actors.ts +13 -0
- package/src/mod.ts +1 -3
- package/src/registry/mod.ts +20 -20
- package/src/registry/serve.ts +9 -14
- package/src/remote-manager-driver/actor-websocket-client.ts +1 -16
- package/src/remote-manager-driver/api-endpoints.ts +13 -1
- package/src/remote-manager-driver/api-utils.ts +8 -0
- package/src/remote-manager-driver/metadata.ts +58 -0
- package/src/remote-manager-driver/mod.ts +47 -62
- package/src/remote-manager-driver/ws-proxy.ts +1 -1
- package/src/schemas/actor-persist/mod.ts +1 -1
- package/src/schemas/actor-persist/versioned.ts +56 -31
- package/src/schemas/client-protocol/mod.ts +1 -1
- package/src/schemas/client-protocol/versioned.ts +41 -21
- package/src/schemas/client-protocol-zod/mod.ts +103 -0
- package/src/schemas/file-system-driver/mod.ts +1 -1
- package/src/schemas/file-system-driver/versioned.ts +42 -19
- package/src/serde.ts +33 -11
- package/src/test/mod.ts +7 -3
- package/src/utils/node.ts +173 -0
- package/src/utils.ts +0 -4
- package/dist/tsup/chunk-227FEWMB.js.map +0 -1
- package/dist/tsup/chunk-2JYPS5YM.cjs.map +0 -1
- package/dist/tsup/chunk-36JJ4IQB.cjs.map +0 -1
- package/dist/tsup/chunk-7L65NNWP.cjs.map +0 -1
- package/dist/tsup/chunk-BLK27ES3.js.map +0 -1
- package/dist/tsup/chunk-BOMZS2TJ.js.map +0 -1
- package/dist/tsup/chunk-BYMKMOBS.js.map +0 -1
- package/dist/tsup/chunk-FX7TWFQR.js.map +0 -1
- package/dist/tsup/chunk-G64QUEDJ.js.map +0 -1
- package/dist/tsup/chunk-HHFKKVLR.cjs.map +0 -1
- package/dist/tsup/chunk-INNFK746.cjs.map +0 -1
- package/dist/tsup/chunk-KSRXX3Z4.cjs.map +0 -1
- package/dist/tsup/chunk-O44LFKSB.cjs +0 -4623
- package/dist/tsup/chunk-O44LFKSB.cjs.map +0 -1
- package/dist/tsup/chunk-PLUN2NQT.js.map +0 -1
- package/dist/tsup/chunk-S4UJG7ZE.js +0 -1119
- package/dist/tsup/chunk-S4UJG7ZE.js.map +0 -1
- package/dist/tsup/chunk-SHVX2QUR.cjs.map +0 -1
- package/dist/tsup/chunk-VFB23BYZ.cjs +0 -1119
- package/dist/tsup/chunk-VFB23BYZ.cjs.map +0 -1
- package/dist/tsup/chunk-VHGY7PU5.cjs.map +0 -1
- package/dist/tsup/chunk-YBG6R7LX.js.map +0 -1
- package/dist/tsup/chunk-YBHYXIP6.js.map +0 -1
- package/src/actor/action.ts +0 -178
- package/src/actor/conn-drivers.ts +0 -216
- package/src/actor/conn-socket.ts +0 -8
- package/src/actor/conn.ts +0 -272
- package/src/actor/instance.ts +0 -2336
- package/src/actor/persisted.ts +0 -49
- package/src/actor/unstable-react.ts +0 -110
- package/src/driver-test-suite/tests/actor-reconnect.ts +0 -170
- package/src/drivers/engine/kv.ts +0 -3
- package/src/manager/hono-websocket-adapter.ts +0 -393
- /package/dist/tsup/{chunk-CD33GT6Z.js.map → chunk-EIPANQMF.js.map} +0 -0
package/src/actor/instance.ts
DELETED
|
@@ -1,2336 +0,0 @@
|
|
|
1
|
-
import * as cbor from "cbor-x";
|
|
2
|
-
import type { SSEStreamingApi } from "hono/streaming";
|
|
3
|
-
import type { WSContext } from "hono/ws";
|
|
4
|
-
import invariant from "invariant";
|
|
5
|
-
import onChange from "on-change";
|
|
6
|
-
import type { ActorKey, Encoding } from "@/actor/mod";
|
|
7
|
-
import type { Client } from "@/client/client";
|
|
8
|
-
import { getBaseLogger, getIncludeTarget, type Logger } from "@/common/log";
|
|
9
|
-
import { isCborSerializable, stringifyError } from "@/common/utils";
|
|
10
|
-
import type { UniversalWebSocket } from "@/common/websocket-interface";
|
|
11
|
-
import { ActorInspector } from "@/inspector/actor";
|
|
12
|
-
import type { Registry } from "@/mod";
|
|
13
|
-
import type * as bareSchema from "@/schemas/actor-persist/mod";
|
|
14
|
-
import { PERSISTED_ACTOR_VERSIONED } from "@/schemas/actor-persist/versioned";
|
|
15
|
-
import type * as protocol from "@/schemas/client-protocol/mod";
|
|
16
|
-
import { TO_CLIENT_VERSIONED } from "@/schemas/client-protocol/versioned";
|
|
17
|
-
import {
|
|
18
|
-
arrayBuffersEqual,
|
|
19
|
-
bufferToArrayBuffer,
|
|
20
|
-
EXTRA_ERROR_LOG,
|
|
21
|
-
getEnvUniversal,
|
|
22
|
-
idToStr,
|
|
23
|
-
promiseWithResolvers,
|
|
24
|
-
SinglePromiseQueue,
|
|
25
|
-
} from "@/utils";
|
|
26
|
-
import { ActionContext } from "./action";
|
|
27
|
-
import type { ActorConfig, OnConnectOptions } from "./config";
|
|
28
|
-
import {
|
|
29
|
-
Conn,
|
|
30
|
-
type ConnId,
|
|
31
|
-
generateConnId,
|
|
32
|
-
generateConnRequestId,
|
|
33
|
-
generateConnToken,
|
|
34
|
-
} from "./conn";
|
|
35
|
-
import {
|
|
36
|
-
CONN_DRIVERS,
|
|
37
|
-
type ConnDriver,
|
|
38
|
-
ConnDriverKind,
|
|
39
|
-
type ConnDriverState,
|
|
40
|
-
getConnDriverKindFromState,
|
|
41
|
-
} from "./conn-drivers";
|
|
42
|
-
import type { ConnSocket } from "./conn-socket";
|
|
43
|
-
import { ActorContext } from "./context";
|
|
44
|
-
import type { AnyDatabaseProvider, InferDatabaseClient } from "./database";
|
|
45
|
-
import type { ActorDriver } from "./driver";
|
|
46
|
-
import * as errors from "./errors";
|
|
47
|
-
import { serializeActorKey } from "./keys";
|
|
48
|
-
import type {
|
|
49
|
-
PersistedActor,
|
|
50
|
-
PersistedConn,
|
|
51
|
-
PersistedHibernatableWebSocket,
|
|
52
|
-
PersistedScheduleEvent,
|
|
53
|
-
} from "./persisted";
|
|
54
|
-
import { processMessage } from "./protocol/old";
|
|
55
|
-
import { CachedSerializer } from "./protocol/serde";
|
|
56
|
-
import { Schedule } from "./schedule";
|
|
57
|
-
import { DeadlineError, deadline, isConnStatePath, isStatePath } from "./utils";
|
|
58
|
-
|
|
59
|
-
export const PERSIST_SYMBOL = Symbol("persist");
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Options for the `_saveState` method.
|
|
63
|
-
*/
|
|
64
|
-
export interface SaveStateOptions {
|
|
65
|
-
/**
|
|
66
|
-
* Forces the state to be saved immediately. This function will return when the state has saved successfully.
|
|
67
|
-
*/
|
|
68
|
-
immediate?: boolean;
|
|
69
|
-
/** Bypass ready check for stopping. */
|
|
70
|
-
allowStoppingState?: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Actor type alias with all `any` types. Used for `extends` in classes referencing this actor. */
|
|
74
|
-
export type AnyActorInstance = ActorInstance<
|
|
75
|
-
// biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
|
|
76
|
-
any,
|
|
77
|
-
// biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
|
|
78
|
-
any,
|
|
79
|
-
// biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
|
|
80
|
-
any,
|
|
81
|
-
// biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
|
|
82
|
-
any,
|
|
83
|
-
// biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
|
|
84
|
-
any,
|
|
85
|
-
// biome-ignore lint/suspicious/noExplicitAny: Needs to be used in `extends`
|
|
86
|
-
any
|
|
87
|
-
>;
|
|
88
|
-
|
|
89
|
-
export type ExtractActorState<A extends AnyActorInstance> =
|
|
90
|
-
A extends ActorInstance<
|
|
91
|
-
infer State,
|
|
92
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
93
|
-
any,
|
|
94
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
95
|
-
any,
|
|
96
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
97
|
-
any,
|
|
98
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
99
|
-
any,
|
|
100
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
101
|
-
any
|
|
102
|
-
>
|
|
103
|
-
? State
|
|
104
|
-
: never;
|
|
105
|
-
|
|
106
|
-
export type ExtractActorConnParams<A extends AnyActorInstance> =
|
|
107
|
-
A extends ActorInstance<
|
|
108
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
109
|
-
any,
|
|
110
|
-
infer ConnParams,
|
|
111
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
112
|
-
any,
|
|
113
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
114
|
-
any,
|
|
115
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
116
|
-
any,
|
|
117
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
118
|
-
any
|
|
119
|
-
>
|
|
120
|
-
? ConnParams
|
|
121
|
-
: never;
|
|
122
|
-
|
|
123
|
-
export type ExtractActorConnState<A extends AnyActorInstance> =
|
|
124
|
-
A extends ActorInstance<
|
|
125
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
126
|
-
any,
|
|
127
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
128
|
-
any,
|
|
129
|
-
infer ConnState,
|
|
130
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
131
|
-
any,
|
|
132
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
133
|
-
any,
|
|
134
|
-
// biome-ignore lint/suspicious/noExplicitAny: Must be used for `extends`
|
|
135
|
-
any
|
|
136
|
-
>
|
|
137
|
-
? ConnState
|
|
138
|
-
: never;
|
|
139
|
-
|
|
140
|
-
enum CanSleep {
|
|
141
|
-
Yes,
|
|
142
|
-
NotReady,
|
|
143
|
-
ActiveConns,
|
|
144
|
-
ActiveHonoHttpRequests,
|
|
145
|
-
ActiveRawWebSockets,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export class ActorInstance<S, CP, CS, V, I, DB extends AnyDatabaseProvider> {
|
|
149
|
-
// Shared actor context for this instance
|
|
150
|
-
actorContext: ActorContext<S, CP, CS, V, I, DB>;
|
|
151
|
-
|
|
152
|
-
/** Actor log, intended for the user to call */
|
|
153
|
-
#log!: Logger;
|
|
154
|
-
|
|
155
|
-
/** Runtime log, intended for internal actor logs */
|
|
156
|
-
#rLog!: Logger;
|
|
157
|
-
|
|
158
|
-
#sleepCalled = false;
|
|
159
|
-
#stopCalled = false;
|
|
160
|
-
|
|
161
|
-
get isStopping() {
|
|
162
|
-
return this.#stopCalled;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
#persistChanged = false;
|
|
166
|
-
#isInOnStateChange = false;
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* The proxied state that notifies of changes automatically.
|
|
170
|
-
*
|
|
171
|
-
* Any data that should be stored indefinitely should be held within this object.
|
|
172
|
-
*/
|
|
173
|
-
#persist!: PersistedActor<S, CP, CS, I>;
|
|
174
|
-
|
|
175
|
-
get [PERSIST_SYMBOL](): PersistedActor<S, CP, CS, I> {
|
|
176
|
-
return this.#persist;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Raw state without the proxy wrapper */
|
|
180
|
-
#persistRaw!: PersistedActor<S, CP, CS, I>;
|
|
181
|
-
|
|
182
|
-
#persistWriteQueue = new SinglePromiseQueue();
|
|
183
|
-
#alarmWriteQueue = new SinglePromiseQueue();
|
|
184
|
-
|
|
185
|
-
#lastSaveTime = 0;
|
|
186
|
-
#pendingSaveTimeout?: NodeJS.Timeout;
|
|
187
|
-
|
|
188
|
-
#vars?: V;
|
|
189
|
-
|
|
190
|
-
#backgroundPromises: Promise<void>[] = [];
|
|
191
|
-
#abortController = new AbortController();
|
|
192
|
-
#config: ActorConfig<S, CP, CS, V, I, DB>;
|
|
193
|
-
#actorDriver!: ActorDriver;
|
|
194
|
-
#inlineClient!: Client<Registry<any>>;
|
|
195
|
-
#actorId!: string;
|
|
196
|
-
#name!: string;
|
|
197
|
-
#key!: ActorKey;
|
|
198
|
-
#region!: string;
|
|
199
|
-
#ready = false;
|
|
200
|
-
|
|
201
|
-
#connections = new Map<ConnId, Conn<S, CP, CS, V, I, DB>>();
|
|
202
|
-
#subscriptionIndex = new Map<string, Set<Conn<S, CP, CS, V, I, DB>>>();
|
|
203
|
-
#checkConnLivenessInterval?: NodeJS.Timeout;
|
|
204
|
-
|
|
205
|
-
#sleepTimeout?: NodeJS.Timeout;
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Track active HTTP requests through Hono router so sleep logic can
|
|
209
|
-
* account for them. Does not include WebSockets.
|
|
210
|
-
**/
|
|
211
|
-
#activeHonoHttpRequests = 0;
|
|
212
|
-
#activeRawWebSockets = new Set<UniversalWebSocket>();
|
|
213
|
-
|
|
214
|
-
#schedule!: Schedule;
|
|
215
|
-
#db!: InferDatabaseClient<DB>;
|
|
216
|
-
|
|
217
|
-
#inspector = new ActorInspector(() => {
|
|
218
|
-
return {
|
|
219
|
-
isDbEnabled: async () => {
|
|
220
|
-
return this.#db !== undefined;
|
|
221
|
-
},
|
|
222
|
-
getDb: async () => {
|
|
223
|
-
return this.db;
|
|
224
|
-
},
|
|
225
|
-
isStateEnabled: async () => {
|
|
226
|
-
return this.stateEnabled;
|
|
227
|
-
},
|
|
228
|
-
getState: async () => {
|
|
229
|
-
this.#validateStateEnabled();
|
|
230
|
-
|
|
231
|
-
// Must return from `#persistRaw` in order to not return the `onchange` proxy
|
|
232
|
-
return this.#persistRaw.state as Record<string, any> as unknown;
|
|
233
|
-
},
|
|
234
|
-
getRpcs: async () => {
|
|
235
|
-
return Object.keys(this.#config.actions);
|
|
236
|
-
},
|
|
237
|
-
getConnections: async () => {
|
|
238
|
-
return Array.from(this.#connections.entries()).map(
|
|
239
|
-
([id, conn]) => ({
|
|
240
|
-
id,
|
|
241
|
-
params: conn.params as any,
|
|
242
|
-
state: conn.__stateEnabled ? conn.state : undefined,
|
|
243
|
-
status: conn.status,
|
|
244
|
-
subscriptions: conn.subscriptions.size,
|
|
245
|
-
lastSeen: conn.lastSeen,
|
|
246
|
-
stateEnabled: conn.__stateEnabled,
|
|
247
|
-
isHibernatable: conn.isHibernatable,
|
|
248
|
-
hibernatableRequestId: conn.__persist
|
|
249
|
-
.hibernatableRequestId
|
|
250
|
-
? idToStr(conn.__persist.hibernatableRequestId)
|
|
251
|
-
: undefined,
|
|
252
|
-
driver: conn.__driverState
|
|
253
|
-
? getConnDriverKindFromState(conn.__driverState)
|
|
254
|
-
: undefined,
|
|
255
|
-
}),
|
|
256
|
-
);
|
|
257
|
-
},
|
|
258
|
-
setState: async (state: unknown) => {
|
|
259
|
-
this.#validateStateEnabled();
|
|
260
|
-
|
|
261
|
-
// Must set on `#persist` instead of `#persistRaw` in order to ensure that the `Proxy` is correctly configured
|
|
262
|
-
//
|
|
263
|
-
// We have to use `...` so `on-change` recognizes the changes to `state` (i.e. set #persistChanged` to true). This is because:
|
|
264
|
-
// 1. In `getState`, we returned the value from `persistRaw`, which does not have the Proxy to monitor state changes
|
|
265
|
-
// 2. If we were to assign `state` to `#persist.s`, `on-change` would assume nothing changed since `state` is still === `#persist.s` since we returned a reference in `getState`
|
|
266
|
-
this.#persist.state = { ...(state as S) };
|
|
267
|
-
await this.saveState({ immediate: true });
|
|
268
|
-
},
|
|
269
|
-
executeAction: async (name, params) => {
|
|
270
|
-
const requestId = generateConnRequestId();
|
|
271
|
-
const conn = await this.createConn(
|
|
272
|
-
{
|
|
273
|
-
requestId: requestId,
|
|
274
|
-
hibernatable: false,
|
|
275
|
-
driverState: { [ConnDriverKind.HTTP]: {} },
|
|
276
|
-
},
|
|
277
|
-
undefined,
|
|
278
|
-
undefined,
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
try {
|
|
282
|
-
return await this.executeAction(
|
|
283
|
-
new ActionContext(this.actorContext, conn),
|
|
284
|
-
name,
|
|
285
|
-
params || [],
|
|
286
|
-
);
|
|
287
|
-
} finally {
|
|
288
|
-
this.__connDisconnected(conn, true, requestId);
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
};
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
get id() {
|
|
295
|
-
return this.#actorId;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
get inlineClient(): Client<Registry<any>> {
|
|
299
|
-
return this.#inlineClient;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
get inspector() {
|
|
303
|
-
return this.#inspector;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
get #sleepingSupported(): boolean {
|
|
307
|
-
return this.#actorDriver.startSleep !== undefined;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* This constructor should never be used directly.
|
|
312
|
-
*
|
|
313
|
-
* Constructed in {@link ActorInstance.start}.
|
|
314
|
-
*
|
|
315
|
-
* @private
|
|
316
|
-
*/
|
|
317
|
-
constructor(config: ActorConfig<S, CP, CS, V, I, DB>) {
|
|
318
|
-
this.#config = config;
|
|
319
|
-
this.actorContext = new ActorContext(this);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async start(
|
|
323
|
-
actorDriver: ActorDriver,
|
|
324
|
-
inlineClient: Client<Registry<any>>,
|
|
325
|
-
actorId: string,
|
|
326
|
-
name: string,
|
|
327
|
-
key: ActorKey,
|
|
328
|
-
region: string,
|
|
329
|
-
) {
|
|
330
|
-
const logParams = {
|
|
331
|
-
actor: name,
|
|
332
|
-
key: serializeActorKey(key),
|
|
333
|
-
actorId,
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const extraLogParams = actorDriver.getExtraActorLogParams?.();
|
|
337
|
-
if (extraLogParams) Object.assign(logParams, extraLogParams);
|
|
338
|
-
|
|
339
|
-
this.#log = getBaseLogger().child(
|
|
340
|
-
Object.assign(
|
|
341
|
-
getIncludeTarget() ? { target: "actor" } : {},
|
|
342
|
-
logParams,
|
|
343
|
-
),
|
|
344
|
-
);
|
|
345
|
-
this.#rLog = getBaseLogger().child(
|
|
346
|
-
Object.assign(
|
|
347
|
-
getIncludeTarget() ? { target: "actor-runtime" } : {},
|
|
348
|
-
logParams,
|
|
349
|
-
),
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
this.#actorDriver = actorDriver;
|
|
353
|
-
this.#inlineClient = inlineClient;
|
|
354
|
-
this.#actorId = actorId;
|
|
355
|
-
this.#name = name;
|
|
356
|
-
this.#key = key;
|
|
357
|
-
this.#region = region;
|
|
358
|
-
this.#schedule = new Schedule(this);
|
|
359
|
-
|
|
360
|
-
// Initialize server
|
|
361
|
-
//
|
|
362
|
-
// Store the promise so network requests can await initialization
|
|
363
|
-
await this.#initialize();
|
|
364
|
-
|
|
365
|
-
// TODO: Exit process if this errors
|
|
366
|
-
if (this.#varsEnabled) {
|
|
367
|
-
let vars: V | undefined;
|
|
368
|
-
if ("createVars" in this.#config) {
|
|
369
|
-
const dataOrPromise = this.#config.createVars(
|
|
370
|
-
this.actorContext as unknown as ActorContext<
|
|
371
|
-
undefined,
|
|
372
|
-
undefined,
|
|
373
|
-
undefined,
|
|
374
|
-
undefined,
|
|
375
|
-
undefined,
|
|
376
|
-
any
|
|
377
|
-
>,
|
|
378
|
-
this.#actorDriver.getContext(this.#actorId),
|
|
379
|
-
);
|
|
380
|
-
if (dataOrPromise instanceof Promise) {
|
|
381
|
-
vars = await deadline(
|
|
382
|
-
dataOrPromise,
|
|
383
|
-
this.#config.options.createVarsTimeout,
|
|
384
|
-
);
|
|
385
|
-
} else {
|
|
386
|
-
vars = dataOrPromise;
|
|
387
|
-
}
|
|
388
|
-
} else if ("vars" in this.#config) {
|
|
389
|
-
vars = structuredClone(this.#config.vars);
|
|
390
|
-
} else {
|
|
391
|
-
throw new Error(
|
|
392
|
-
"Could not variables from 'createVars' or 'vars'",
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
this.#vars = vars;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// TODO: Exit process if this errors
|
|
399
|
-
this.#rLog.info({ msg: "actor starting" });
|
|
400
|
-
if (this.#config.onStart) {
|
|
401
|
-
const result = this.#config.onStart(this.actorContext);
|
|
402
|
-
if (result instanceof Promise) {
|
|
403
|
-
await result;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Setup Database
|
|
408
|
-
if ("db" in this.#config && this.#config.db) {
|
|
409
|
-
const client = await this.#config.db.createClient({
|
|
410
|
-
getDatabase: () => actorDriver.getDatabase(this.#actorId),
|
|
411
|
-
});
|
|
412
|
-
this.#rLog.info({ msg: "database migration starting" });
|
|
413
|
-
await this.#config.db.onMigrate?.(client);
|
|
414
|
-
this.#rLog.info({ msg: "database migration complete" });
|
|
415
|
-
this.#db = client;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Set alarm for next scheduled event if any exist after finishing initiation sequence
|
|
419
|
-
if (this.#persist.scheduledEvents.length > 0) {
|
|
420
|
-
await this.#queueSetAlarm(
|
|
421
|
-
this.#persist.scheduledEvents[0].timestamp,
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
this.#rLog.info({ msg: "actor ready" });
|
|
426
|
-
this.#ready = true;
|
|
427
|
-
|
|
428
|
-
// Must be called after setting `#ready` or else it will not schedule sleep
|
|
429
|
-
this.#resetSleepTimer();
|
|
430
|
-
|
|
431
|
-
// Start conn liveness interval
|
|
432
|
-
//
|
|
433
|
-
// Check for liveness immediately since we may have connections that
|
|
434
|
-
// were in `reconnecting` state when the actor went to sleep that we
|
|
435
|
-
// need to purge.
|
|
436
|
-
//
|
|
437
|
-
// We don't use alarms for connection liveness since alarms require
|
|
438
|
-
// durability & are expensive. Connection liveness is safe to assume
|
|
439
|
-
// it only needs to be ran while the actor is awake and does not need
|
|
440
|
-
// to manually wake the actor. The only case this is not true is if the
|
|
441
|
-
// connection liveness timeout is greater than the actor sleep timeout
|
|
442
|
-
// OR if the actor is manually put to sleep. In this case, the connections
|
|
443
|
-
// will be stuck in a `reconnecting` state until the actor is awaken again.
|
|
444
|
-
this.#checkConnLivenessInterval = setInterval(
|
|
445
|
-
this.#checkConnectionsLiveness.bind(this),
|
|
446
|
-
this.#config.options.connectionLivenessInterval,
|
|
447
|
-
);
|
|
448
|
-
this.#checkConnectionsLiveness();
|
|
449
|
-
|
|
450
|
-
// Trigger any pending alarms
|
|
451
|
-
await this._onAlarm();
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
async #scheduleEventInner(newEvent: PersistedScheduleEvent) {
|
|
455
|
-
this.actorContext.log.info({ msg: "scheduling event", ...newEvent });
|
|
456
|
-
|
|
457
|
-
// Insert event in to index
|
|
458
|
-
const insertIndex = this.#persist.scheduledEvents.findIndex(
|
|
459
|
-
(x) => x.timestamp > newEvent.timestamp,
|
|
460
|
-
);
|
|
461
|
-
if (insertIndex === -1) {
|
|
462
|
-
this.#persist.scheduledEvents.push(newEvent);
|
|
463
|
-
} else {
|
|
464
|
-
this.#persist.scheduledEvents.splice(insertIndex, 0, newEvent);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Update alarm if:
|
|
468
|
-
// - this is the newest event (i.e. at beginning of array) or
|
|
469
|
-
// - this is the only event (i.e. the only event in the array)
|
|
470
|
-
if (insertIndex === 0 || this.#persist.scheduledEvents.length === 1) {
|
|
471
|
-
this.actorContext.log.info({
|
|
472
|
-
msg: "setting alarm",
|
|
473
|
-
timestamp: newEvent.timestamp,
|
|
474
|
-
eventCount: this.#persist.scheduledEvents.length,
|
|
475
|
-
});
|
|
476
|
-
await this.#queueSetAlarm(newEvent.timestamp);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Triggers any pending alarms.
|
|
482
|
-
*
|
|
483
|
-
* This method is idempotent. It's called automatically when the actor wakes
|
|
484
|
-
* in order to trigger any pending alarms.
|
|
485
|
-
*/
|
|
486
|
-
async _onAlarm() {
|
|
487
|
-
const now = Date.now();
|
|
488
|
-
this.actorContext.log.debug({
|
|
489
|
-
msg: "alarm triggered",
|
|
490
|
-
now,
|
|
491
|
-
events: this.#persist.scheduledEvents.length,
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
// Update sleep
|
|
495
|
-
//
|
|
496
|
-
// Do this before any async logic
|
|
497
|
-
this.#resetSleepTimer();
|
|
498
|
-
|
|
499
|
-
// Remove events from schedule that we're about to run
|
|
500
|
-
const runIndex = this.#persist.scheduledEvents.findIndex(
|
|
501
|
-
(x) => x.timestamp <= now,
|
|
502
|
-
);
|
|
503
|
-
if (runIndex === -1) {
|
|
504
|
-
// This method is idempotent, so this will happen in scenarios like `start` and
|
|
505
|
-
// no events are pending.
|
|
506
|
-
this.#rLog.debug({ msg: "no events are due yet" });
|
|
507
|
-
if (this.#persist.scheduledEvents.length > 0) {
|
|
508
|
-
const nextTs = this.#persist.scheduledEvents[0].timestamp;
|
|
509
|
-
this.actorContext.log.debug({
|
|
510
|
-
msg: "alarm fired early, rescheduling for next event",
|
|
511
|
-
now,
|
|
512
|
-
nextTs,
|
|
513
|
-
delta: nextTs - now,
|
|
514
|
-
});
|
|
515
|
-
await this.#queueSetAlarm(nextTs);
|
|
516
|
-
}
|
|
517
|
-
this.actorContext.log.debug({ msg: "no events to run", now });
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
const scheduleEvents = this.#persist.scheduledEvents.splice(
|
|
521
|
-
0,
|
|
522
|
-
runIndex + 1,
|
|
523
|
-
);
|
|
524
|
-
this.actorContext.log.debug({
|
|
525
|
-
msg: "running events",
|
|
526
|
-
count: scheduleEvents.length,
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Set alarm for next event
|
|
530
|
-
if (this.#persist.scheduledEvents.length > 0) {
|
|
531
|
-
const nextTs = this.#persist.scheduledEvents[0].timestamp;
|
|
532
|
-
this.actorContext.log.info({
|
|
533
|
-
msg: "setting next alarm",
|
|
534
|
-
nextTs,
|
|
535
|
-
remainingEvents: this.#persist.scheduledEvents.length,
|
|
536
|
-
});
|
|
537
|
-
await this.#queueSetAlarm(nextTs);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Iterate by event key in order to ensure we call the events in order
|
|
541
|
-
for (const event of scheduleEvents) {
|
|
542
|
-
try {
|
|
543
|
-
this.actorContext.log.info({
|
|
544
|
-
msg: "running action for event",
|
|
545
|
-
event: event.eventId,
|
|
546
|
-
timestamp: event.timestamp,
|
|
547
|
-
action: event.kind.generic.actionName,
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// Look up function
|
|
551
|
-
const fn: unknown =
|
|
552
|
-
this.#config.actions[event.kind.generic.actionName];
|
|
553
|
-
|
|
554
|
-
if (!fn)
|
|
555
|
-
throw new Error(
|
|
556
|
-
`Missing action for alarm ${event.kind.generic.actionName}`,
|
|
557
|
-
);
|
|
558
|
-
if (typeof fn !== "function")
|
|
559
|
-
throw new Error(
|
|
560
|
-
`Alarm function lookup for ${event.kind.generic.actionName} returned ${typeof fn}`,
|
|
561
|
-
);
|
|
562
|
-
|
|
563
|
-
// Call function
|
|
564
|
-
try {
|
|
565
|
-
const args = event.kind.generic.args
|
|
566
|
-
? cbor.decode(new Uint8Array(event.kind.generic.args))
|
|
567
|
-
: [];
|
|
568
|
-
await fn.call(undefined, this.actorContext, ...args);
|
|
569
|
-
} catch (error) {
|
|
570
|
-
this.actorContext.log.error({
|
|
571
|
-
msg: "error while running event",
|
|
572
|
-
error: stringifyError(error),
|
|
573
|
-
event: event.eventId,
|
|
574
|
-
timestamp: event.timestamp,
|
|
575
|
-
action: event.kind.generic.actionName,
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
} catch (error) {
|
|
579
|
-
this.actorContext.log.error({
|
|
580
|
-
msg: "internal error while running event",
|
|
581
|
-
error: stringifyError(error),
|
|
582
|
-
...event,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async scheduleEvent(
|
|
589
|
-
timestamp: number,
|
|
590
|
-
action: string,
|
|
591
|
-
args: unknown[],
|
|
592
|
-
): Promise<void> {
|
|
593
|
-
return this.#scheduleEventInner({
|
|
594
|
-
eventId: crypto.randomUUID(),
|
|
595
|
-
timestamp,
|
|
596
|
-
kind: {
|
|
597
|
-
generic: {
|
|
598
|
-
actionName: action,
|
|
599
|
-
args: bufferToArrayBuffer(cbor.encode(args)),
|
|
600
|
-
},
|
|
601
|
-
},
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
get stateEnabled() {
|
|
606
|
-
return "createState" in this.#config || "state" in this.#config;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
#validateStateEnabled() {
|
|
610
|
-
if (!this.stateEnabled) {
|
|
611
|
-
throw new errors.StateNotEnabled();
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
get connStateEnabled() {
|
|
616
|
-
return "createConnState" in this.#config || "connState" in this.#config;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
get #varsEnabled() {
|
|
620
|
-
return "createVars" in this.#config || "vars" in this.#config;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
#validateVarsEnabled() {
|
|
624
|
-
if (!this.#varsEnabled) {
|
|
625
|
-
throw new errors.VarsNotEnabled();
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/** Promise used to wait for a save to complete. This is required since you cannot await `#saveStateThrottled`. */
|
|
630
|
-
#onPersistSavedPromise?: ReturnType<typeof promiseWithResolvers<void>>;
|
|
631
|
-
|
|
632
|
-
/** Throttled save state method. Used to write to KV at a reasonable cadence. */
|
|
633
|
-
#savePersistThrottled() {
|
|
634
|
-
const now = Date.now();
|
|
635
|
-
const timeSinceLastSave = now - this.#lastSaveTime;
|
|
636
|
-
const saveInterval = this.#config.options.stateSaveInterval;
|
|
637
|
-
|
|
638
|
-
// If we're within the throttle window and not already scheduled, schedule the next save.
|
|
639
|
-
if (timeSinceLastSave < saveInterval) {
|
|
640
|
-
if (this.#pendingSaveTimeout === undefined) {
|
|
641
|
-
this.#pendingSaveTimeout = setTimeout(() => {
|
|
642
|
-
this.#pendingSaveTimeout = undefined;
|
|
643
|
-
this.#savePersistInner();
|
|
644
|
-
}, saveInterval - timeSinceLastSave);
|
|
645
|
-
}
|
|
646
|
-
} else {
|
|
647
|
-
// If we're outside the throttle window, save immediately
|
|
648
|
-
this.#savePersistInner();
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** Saves the state to KV. You probably want to use #saveStateThrottled instead except for a few edge cases. */
|
|
653
|
-
async #savePersistInner() {
|
|
654
|
-
try {
|
|
655
|
-
this.#lastSaveTime = Date.now();
|
|
656
|
-
|
|
657
|
-
if (this.#persistChanged) {
|
|
658
|
-
const finished = this.#persistWriteQueue.enqueue(async () => {
|
|
659
|
-
this.#rLog.debug({ msg: "saving persist" });
|
|
660
|
-
|
|
661
|
-
// There might be more changes while we're writing, so we set this
|
|
662
|
-
// before writing to KV in order to avoid a race condition.
|
|
663
|
-
this.#persistChanged = false;
|
|
664
|
-
|
|
665
|
-
// Convert to BARE types and write to KV
|
|
666
|
-
const bareData = this.#convertToBarePersisted(
|
|
667
|
-
this.#persistRaw,
|
|
668
|
-
);
|
|
669
|
-
await this.#actorDriver.writePersistedData(
|
|
670
|
-
this.#actorId,
|
|
671
|
-
PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(
|
|
672
|
-
bareData,
|
|
673
|
-
),
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
this.#rLog.debug({ msg: "persist saved" });
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
await finished;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
this.#onPersistSavedPromise?.resolve();
|
|
683
|
-
} catch (error) {
|
|
684
|
-
this.#rLog.error({
|
|
685
|
-
msg: "error saving persist",
|
|
686
|
-
error: stringifyError(error),
|
|
687
|
-
});
|
|
688
|
-
this.#onPersistSavedPromise?.reject(error);
|
|
689
|
-
throw error;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
async #queueSetAlarm(timestamp: number): Promise<void> {
|
|
694
|
-
await this.#alarmWriteQueue.enqueue(async () => {
|
|
695
|
-
await this.#actorDriver.setAlarm(this, timestamp);
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Creates proxy for `#persist` that handles automatically flagging when state needs to be updated.
|
|
701
|
-
*/
|
|
702
|
-
#setPersist(target: PersistedActor<S, CP, CS, I>) {
|
|
703
|
-
// Set raw persist object
|
|
704
|
-
this.#persistRaw = target;
|
|
705
|
-
|
|
706
|
-
// TODO: Allow disabling in production
|
|
707
|
-
// If this can't be proxied, return raw value
|
|
708
|
-
if (target === null || typeof target !== "object") {
|
|
709
|
-
let invalidPath = "";
|
|
710
|
-
if (
|
|
711
|
-
!isCborSerializable(
|
|
712
|
-
target,
|
|
713
|
-
(path) => {
|
|
714
|
-
invalidPath = path;
|
|
715
|
-
},
|
|
716
|
-
"",
|
|
717
|
-
)
|
|
718
|
-
) {
|
|
719
|
-
throw new errors.InvalidStateType({ path: invalidPath });
|
|
720
|
-
}
|
|
721
|
-
return target;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Unsubscribe from old state
|
|
725
|
-
if (this.#persist) {
|
|
726
|
-
onChange.unsubscribe(this.#persist);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Listen for changes to the object in order to automatically write state
|
|
730
|
-
this.#persist = onChange(
|
|
731
|
-
target,
|
|
732
|
-
// biome-ignore lint/suspicious/noExplicitAny: Don't know types in proxy
|
|
733
|
-
(
|
|
734
|
-
path: string,
|
|
735
|
-
value: any,
|
|
736
|
-
_previousValue: any,
|
|
737
|
-
_applyData: any,
|
|
738
|
-
) => {
|
|
739
|
-
const actorStatePath = isStatePath(path);
|
|
740
|
-
const connStatePath = isConnStatePath(path);
|
|
741
|
-
|
|
742
|
-
// Validate CBOR serializability for state changes
|
|
743
|
-
if (actorStatePath || connStatePath) {
|
|
744
|
-
let invalidPath = "";
|
|
745
|
-
if (
|
|
746
|
-
!isCborSerializable(
|
|
747
|
-
value,
|
|
748
|
-
(invalidPathPart) => {
|
|
749
|
-
invalidPath = invalidPathPart;
|
|
750
|
-
},
|
|
751
|
-
"",
|
|
752
|
-
)
|
|
753
|
-
) {
|
|
754
|
-
throw new errors.InvalidStateType({
|
|
755
|
-
path: path + (invalidPath ? `.${invalidPath}` : ""),
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
this.#rLog.debug({
|
|
761
|
-
msg: "onChange triggered, setting persistChanged=true",
|
|
762
|
-
path,
|
|
763
|
-
});
|
|
764
|
-
this.#persistChanged = true;
|
|
765
|
-
|
|
766
|
-
// Inform the inspector about state changes (only for state path)
|
|
767
|
-
if (actorStatePath) {
|
|
768
|
-
this.inspector.emitter.emit(
|
|
769
|
-
"stateUpdated",
|
|
770
|
-
this.#persist.state,
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Call onStateChange if it exists
|
|
775
|
-
//
|
|
776
|
-
// Skip if we're already inside onStateChange to prevent infinite recursion
|
|
777
|
-
if (
|
|
778
|
-
actorStatePath &&
|
|
779
|
-
this.#config.onStateChange &&
|
|
780
|
-
this.#ready &&
|
|
781
|
-
!this.#isInOnStateChange
|
|
782
|
-
) {
|
|
783
|
-
try {
|
|
784
|
-
this.#isInOnStateChange = true;
|
|
785
|
-
this.#config.onStateChange(
|
|
786
|
-
this.actorContext,
|
|
787
|
-
this.#persistRaw.state,
|
|
788
|
-
);
|
|
789
|
-
} catch (error) {
|
|
790
|
-
this.#rLog.error({
|
|
791
|
-
msg: "error in `_onStateChange`",
|
|
792
|
-
error: stringifyError(error),
|
|
793
|
-
});
|
|
794
|
-
} finally {
|
|
795
|
-
this.#isInOnStateChange = false;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// State will be flushed at the end of the action
|
|
800
|
-
},
|
|
801
|
-
{ ignoreDetached: true },
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
async #initialize() {
|
|
806
|
-
// Read initial state
|
|
807
|
-
const persistDataBuffer = await this.#actorDriver.readPersistedData(
|
|
808
|
-
this.#actorId,
|
|
809
|
-
);
|
|
810
|
-
invariant(
|
|
811
|
-
persistDataBuffer !== undefined,
|
|
812
|
-
"persist data has not been set, it should be set when initialized",
|
|
813
|
-
);
|
|
814
|
-
const bareData =
|
|
815
|
-
PERSISTED_ACTOR_VERSIONED.deserializeWithEmbeddedVersion(
|
|
816
|
-
persistDataBuffer,
|
|
817
|
-
);
|
|
818
|
-
const persistData = this.#convertFromBarePersisted(bareData);
|
|
819
|
-
|
|
820
|
-
if (persistData.hasInitiated) {
|
|
821
|
-
this.#rLog.info({
|
|
822
|
-
msg: "actor restoring",
|
|
823
|
-
connections: persistData.connections.length,
|
|
824
|
-
hibernatableWebSockets:
|
|
825
|
-
persistData.hibernatableWebSocket.length,
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
// Set initial state
|
|
829
|
-
this.#setPersist(persistData);
|
|
830
|
-
|
|
831
|
-
// Load connections
|
|
832
|
-
for (const connPersist of this.#persist.connections) {
|
|
833
|
-
// Create connections
|
|
834
|
-
const conn = new Conn<S, CP, CS, V, I, DB>(this, connPersist);
|
|
835
|
-
this.#connections.set(conn.id, conn);
|
|
836
|
-
|
|
837
|
-
// Register event subscriptions
|
|
838
|
-
for (const sub of connPersist.subscriptions) {
|
|
839
|
-
this.#addSubscription(sub.eventName, conn, true);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
} else {
|
|
843
|
-
this.#rLog.info({ msg: "actor creating" });
|
|
844
|
-
|
|
845
|
-
// Initialize actor state
|
|
846
|
-
let stateData: unknown;
|
|
847
|
-
if (this.stateEnabled) {
|
|
848
|
-
this.#rLog.info({ msg: "actor state initializing" });
|
|
849
|
-
|
|
850
|
-
if ("createState" in this.#config) {
|
|
851
|
-
this.#config.createState;
|
|
852
|
-
|
|
853
|
-
// Convert state to undefined since state is not defined yet here
|
|
854
|
-
stateData = await this.#config.createState(
|
|
855
|
-
this.actorContext as unknown as ActorContext<
|
|
856
|
-
undefined,
|
|
857
|
-
undefined,
|
|
858
|
-
undefined,
|
|
859
|
-
undefined,
|
|
860
|
-
undefined,
|
|
861
|
-
undefined
|
|
862
|
-
>,
|
|
863
|
-
persistData.input!,
|
|
864
|
-
);
|
|
865
|
-
} else if ("state" in this.#config) {
|
|
866
|
-
stateData = structuredClone(this.#config.state);
|
|
867
|
-
} else {
|
|
868
|
-
throw new Error(
|
|
869
|
-
"Both 'createState' or 'state' were not defined",
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
} else {
|
|
873
|
-
this.#rLog.debug({ msg: "state not enabled" });
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Save state and mark as initialized
|
|
877
|
-
persistData.state = stateData as S;
|
|
878
|
-
persistData.hasInitiated = true;
|
|
879
|
-
|
|
880
|
-
// Update state
|
|
881
|
-
this.#rLog.debug({ msg: "writing state" });
|
|
882
|
-
const bareData = this.#convertToBarePersisted(persistData);
|
|
883
|
-
await this.#actorDriver.writePersistedData(
|
|
884
|
-
this.#actorId,
|
|
885
|
-
PERSISTED_ACTOR_VERSIONED.serializeWithEmbeddedVersion(
|
|
886
|
-
bareData,
|
|
887
|
-
),
|
|
888
|
-
);
|
|
889
|
-
|
|
890
|
-
this.#setPersist(persistData);
|
|
891
|
-
|
|
892
|
-
// Notify creation
|
|
893
|
-
if (this.#config.onCreate) {
|
|
894
|
-
await this.#config.onCreate(
|
|
895
|
-
this.actorContext,
|
|
896
|
-
persistData.input!,
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
__getConnForId(id: string): Conn<S, CP, CS, V, I, DB> | undefined {
|
|
903
|
-
return this.#connections.get(id);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
/**
|
|
907
|
-
* Call when conn is disconnected. Used by transports.
|
|
908
|
-
*
|
|
909
|
-
* If a clean diconnect, will be removed immediately.
|
|
910
|
-
*
|
|
911
|
-
* If not a clean disconnect, will keep the connection alive for a given interval to wait for reconnect.
|
|
912
|
-
*/
|
|
913
|
-
__connDisconnected(
|
|
914
|
-
conn: Conn<S, CP, CS, V, I, DB>,
|
|
915
|
-
wasClean: boolean,
|
|
916
|
-
requestId: string,
|
|
917
|
-
) {
|
|
918
|
-
// If socket ID is provided, check if it matches the current socket ID
|
|
919
|
-
// If it doesn't match, this is a stale disconnect event from an old socket
|
|
920
|
-
if (
|
|
921
|
-
requestId &&
|
|
922
|
-
conn.__socket &&
|
|
923
|
-
requestId !== conn.__socket.requestId
|
|
924
|
-
) {
|
|
925
|
-
this.#rLog.debug({
|
|
926
|
-
msg: "ignoring stale disconnect event",
|
|
927
|
-
connId: conn.id,
|
|
928
|
-
eventRequestId: requestId,
|
|
929
|
-
currentRequestId: conn.__socket.requestId,
|
|
930
|
-
});
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (wasClean) {
|
|
935
|
-
// Disconnected cleanly, remove the conn
|
|
936
|
-
|
|
937
|
-
this.#removeConn(conn);
|
|
938
|
-
} else {
|
|
939
|
-
// Disconnected uncleanly, allow reconnection
|
|
940
|
-
|
|
941
|
-
if (!conn.__driverState) {
|
|
942
|
-
this.rLog.warn("called conn disconnected without driver state");
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Update last seen so we know when to clean it up
|
|
946
|
-
conn.__persist.lastSeen = Date.now();
|
|
947
|
-
|
|
948
|
-
// Remove socket
|
|
949
|
-
conn.__socket = undefined;
|
|
950
|
-
|
|
951
|
-
// Update sleep
|
|
952
|
-
this.#resetSleepTimer();
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Removes a connection and cleans up its resources.
|
|
958
|
-
*/
|
|
959
|
-
#removeConn(conn: Conn<S, CP, CS, V, I, DB>) {
|
|
960
|
-
// Remove from persist & save immediately
|
|
961
|
-
const connIdx = this.#persist.connections.findIndex(
|
|
962
|
-
(c) => c.connId === conn.id,
|
|
963
|
-
);
|
|
964
|
-
if (connIdx !== -1) {
|
|
965
|
-
this.#persist.connections.splice(connIdx, 1);
|
|
966
|
-
this.saveState({ immediate: true, allowStoppingState: true });
|
|
967
|
-
} else {
|
|
968
|
-
this.#rLog.warn({
|
|
969
|
-
msg: "could not find persisted connection to remove",
|
|
970
|
-
connId: conn.id,
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Remove from state
|
|
975
|
-
this.#connections.delete(conn.id);
|
|
976
|
-
this.#rLog.debug({ msg: "removed conn", connId: conn.id });
|
|
977
|
-
|
|
978
|
-
// Remove subscriptions
|
|
979
|
-
for (const eventName of [...conn.subscriptions.values()]) {
|
|
980
|
-
this.#removeSubscription(eventName, conn, true);
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
this.inspector.emitter.emit("connectionUpdated");
|
|
984
|
-
if (this.#config.onDisconnect) {
|
|
985
|
-
try {
|
|
986
|
-
const result = this.#config.onDisconnect(
|
|
987
|
-
this.actorContext,
|
|
988
|
-
conn,
|
|
989
|
-
);
|
|
990
|
-
if (result instanceof Promise) {
|
|
991
|
-
// Handle promise but don't await it to prevent blocking
|
|
992
|
-
result.catch((error) => {
|
|
993
|
-
this.#rLog.error({
|
|
994
|
-
msg: "error in `onDisconnect`",
|
|
995
|
-
error: stringifyError(error),
|
|
996
|
-
});
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
this.#rLog.error({
|
|
1001
|
-
msg: "error in `onDisconnect`",
|
|
1002
|
-
error: stringifyError(error),
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// Update sleep
|
|
1008
|
-
this.#resetSleepTimer();
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* Called to create a new connection or reconnect an existing one.
|
|
1013
|
-
*/
|
|
1014
|
-
async createConn(
|
|
1015
|
-
socket: ConnSocket,
|
|
1016
|
-
// biome-ignore lint/suspicious/noExplicitAny: TypeScript bug with ExtractActorConnParams<this>,
|
|
1017
|
-
params: any,
|
|
1018
|
-
request?: Request,
|
|
1019
|
-
connectionId?: string,
|
|
1020
|
-
connectionToken?: string,
|
|
1021
|
-
): Promise<Conn<S, CP, CS, V, I, DB>> {
|
|
1022
|
-
this.#assertReady();
|
|
1023
|
-
|
|
1024
|
-
// Check for hibernatable websocket reconnection
|
|
1025
|
-
if (socket.requestIdBuf && socket.hibernatable) {
|
|
1026
|
-
this.rLog.debug({
|
|
1027
|
-
msg: "checking for hibernatable websocket connection",
|
|
1028
|
-
requestId: socket.requestId,
|
|
1029
|
-
existingConnectionsCount: this.#connections.size,
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
// Find existing connection with matching hibernatableRequestId
|
|
1033
|
-
const existingConn = Array.from(this.#connections.values()).find(
|
|
1034
|
-
(conn) =>
|
|
1035
|
-
conn.__persist.hibernatableRequestId &&
|
|
1036
|
-
arrayBuffersEqual(
|
|
1037
|
-
conn.__persist.hibernatableRequestId,
|
|
1038
|
-
socket.requestIdBuf!,
|
|
1039
|
-
),
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
if (existingConn) {
|
|
1043
|
-
this.rLog.debug({
|
|
1044
|
-
msg: "reconnecting hibernatable websocket connection",
|
|
1045
|
-
connectionId: existingConn.id,
|
|
1046
|
-
requestId: socket.requestId,
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
// If there's an existing driver state, clean it up without marking as clean disconnect
|
|
1050
|
-
if (existingConn.__driverState) {
|
|
1051
|
-
this.#rLog.warn({
|
|
1052
|
-
msg: "found existing driver state on hibernatable websocket",
|
|
1053
|
-
connectionId: existingConn.id,
|
|
1054
|
-
requestId: socket.requestId,
|
|
1055
|
-
});
|
|
1056
|
-
const driverKind = getConnDriverKindFromState(
|
|
1057
|
-
existingConn.__driverState,
|
|
1058
|
-
);
|
|
1059
|
-
const driver = CONN_DRIVERS[driverKind];
|
|
1060
|
-
if (driver.disconnect) {
|
|
1061
|
-
// Call driver disconnect to clean up directly. Don't use Conn.disconnect since that will remove the connection entirely.
|
|
1062
|
-
driver.disconnect(
|
|
1063
|
-
this,
|
|
1064
|
-
existingConn,
|
|
1065
|
-
(existingConn.__driverState as any)[driverKind],
|
|
1066
|
-
"Reconnecting hibernatable websocket with new driver state",
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// Update with new driver state
|
|
1072
|
-
existingConn.__socket = socket;
|
|
1073
|
-
existingConn.__persist.lastSeen = Date.now();
|
|
1074
|
-
|
|
1075
|
-
// Update sleep timer since connection is now active
|
|
1076
|
-
this.#resetSleepTimer();
|
|
1077
|
-
|
|
1078
|
-
this.inspector.emitter.emit("connectionUpdated");
|
|
1079
|
-
|
|
1080
|
-
// We don't need to send a new init message since this is a
|
|
1081
|
-
// hibernated request that has already been initialized
|
|
1082
|
-
|
|
1083
|
-
return existingConn;
|
|
1084
|
-
} else {
|
|
1085
|
-
this.rLog.debug({
|
|
1086
|
-
msg: "no existing hibernatable connection found, creating new connection",
|
|
1087
|
-
requestId: socket.requestId,
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// If connection ID and token are provided, try to reconnect
|
|
1093
|
-
if (connectionId && connectionToken) {
|
|
1094
|
-
this.rLog.debug({
|
|
1095
|
-
msg: "checking for existing connection",
|
|
1096
|
-
connectionId,
|
|
1097
|
-
});
|
|
1098
|
-
const existingConn = this.#connections.get(connectionId);
|
|
1099
|
-
if (existingConn && existingConn._token === connectionToken) {
|
|
1100
|
-
// This is a valid reconnection
|
|
1101
|
-
this.rLog.debug({
|
|
1102
|
-
msg: "reconnecting existing connection",
|
|
1103
|
-
connectionId,
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
// If there's an existing driver state, clean it up without marking as clean disconnect
|
|
1107
|
-
if (existingConn.__driverState) {
|
|
1108
|
-
const driverKind = getConnDriverKindFromState(
|
|
1109
|
-
existingConn.__driverState,
|
|
1110
|
-
);
|
|
1111
|
-
const driver = CONN_DRIVERS[driverKind];
|
|
1112
|
-
if (driver.disconnect) {
|
|
1113
|
-
// Call driver disconnect to clean up directly. Don't use Conn.disconnect since that will remove the connection entirely.
|
|
1114
|
-
driver.disconnect(
|
|
1115
|
-
this,
|
|
1116
|
-
existingConn,
|
|
1117
|
-
(existingConn.__driverState as any)[driverKind],
|
|
1118
|
-
"Reconnecting with new driver state",
|
|
1119
|
-
);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Update with new driver state
|
|
1124
|
-
existingConn.__socket = socket;
|
|
1125
|
-
existingConn.__persist.lastSeen = Date.now();
|
|
1126
|
-
|
|
1127
|
-
// Update sleep timer since connection is now active
|
|
1128
|
-
this.#resetSleepTimer();
|
|
1129
|
-
|
|
1130
|
-
this.inspector.emitter.emit("connectionUpdated");
|
|
1131
|
-
|
|
1132
|
-
// Send init message for reconnection
|
|
1133
|
-
existingConn._sendMessage(
|
|
1134
|
-
new CachedSerializer<protocol.ToClient>(
|
|
1135
|
-
{
|
|
1136
|
-
body: {
|
|
1137
|
-
tag: "Init",
|
|
1138
|
-
val: {
|
|
1139
|
-
actorId: this.id,
|
|
1140
|
-
connectionId: existingConn.id,
|
|
1141
|
-
connectionToken: existingConn._token,
|
|
1142
|
-
},
|
|
1143
|
-
},
|
|
1144
|
-
},
|
|
1145
|
-
TO_CLIENT_VERSIONED,
|
|
1146
|
-
),
|
|
1147
|
-
);
|
|
1148
|
-
|
|
1149
|
-
return existingConn;
|
|
1150
|
-
} else {
|
|
1151
|
-
this.rLog.debug({
|
|
1152
|
-
msg: "connection not found or token mismatch, creating new connection",
|
|
1153
|
-
connectionId,
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// Generate new connection ID and token if not provided or if reconnection failed
|
|
1159
|
-
const newConnId = generateConnId();
|
|
1160
|
-
const newConnToken = generateConnToken();
|
|
1161
|
-
|
|
1162
|
-
if (this.#connections.has(newConnId)) {
|
|
1163
|
-
throw new Error(`Connection already exists: ${newConnId}`);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// Prepare connection state
|
|
1167
|
-
let connState: CS | undefined;
|
|
1168
|
-
|
|
1169
|
-
const onBeforeConnectOpts = {
|
|
1170
|
-
request,
|
|
1171
|
-
} satisfies OnConnectOptions;
|
|
1172
|
-
|
|
1173
|
-
if (this.#config.onBeforeConnect) {
|
|
1174
|
-
await this.#config.onBeforeConnect(
|
|
1175
|
-
this.actorContext,
|
|
1176
|
-
onBeforeConnectOpts,
|
|
1177
|
-
params,
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
if (this.connStateEnabled) {
|
|
1182
|
-
if ("createConnState" in this.#config) {
|
|
1183
|
-
const dataOrPromise = this.#config.createConnState(
|
|
1184
|
-
this.actorContext as unknown as ActorContext<
|
|
1185
|
-
undefined,
|
|
1186
|
-
undefined,
|
|
1187
|
-
undefined,
|
|
1188
|
-
undefined,
|
|
1189
|
-
undefined,
|
|
1190
|
-
undefined
|
|
1191
|
-
>,
|
|
1192
|
-
onBeforeConnectOpts,
|
|
1193
|
-
params,
|
|
1194
|
-
);
|
|
1195
|
-
if (dataOrPromise instanceof Promise) {
|
|
1196
|
-
connState = await deadline(
|
|
1197
|
-
dataOrPromise,
|
|
1198
|
-
this.#config.options.createConnStateTimeout,
|
|
1199
|
-
);
|
|
1200
|
-
} else {
|
|
1201
|
-
connState = dataOrPromise;
|
|
1202
|
-
}
|
|
1203
|
-
} else if ("connState" in this.#config) {
|
|
1204
|
-
connState = structuredClone(this.#config.connState);
|
|
1205
|
-
} else {
|
|
1206
|
-
throw new Error(
|
|
1207
|
-
"Could not create connection state from 'createConnState' or 'connState'",
|
|
1208
|
-
);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Create connection
|
|
1213
|
-
const persist: PersistedConn<CP, CS> = {
|
|
1214
|
-
connId: newConnId,
|
|
1215
|
-
token: newConnToken,
|
|
1216
|
-
params: params,
|
|
1217
|
-
state: connState as CS,
|
|
1218
|
-
lastSeen: Date.now(),
|
|
1219
|
-
subscriptions: [],
|
|
1220
|
-
};
|
|
1221
|
-
|
|
1222
|
-
// Check if this connection is for a hibernatable websocket
|
|
1223
|
-
if (socket.requestIdBuf) {
|
|
1224
|
-
const isHibernatable =
|
|
1225
|
-
this.#persist.hibernatableWebSocket.findIndex((ws) =>
|
|
1226
|
-
arrayBuffersEqual(ws.requestId, socket.requestIdBuf!),
|
|
1227
|
-
) !== -1;
|
|
1228
|
-
|
|
1229
|
-
if (isHibernatable) {
|
|
1230
|
-
persist.hibernatableRequestId = socket.requestIdBuf;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
const conn = new Conn<S, CP, CS, V, I, DB>(this, persist);
|
|
1235
|
-
conn.__socket = socket;
|
|
1236
|
-
this.#connections.set(conn.id, conn);
|
|
1237
|
-
|
|
1238
|
-
// Update sleep
|
|
1239
|
-
//
|
|
1240
|
-
// Do this immediately after adding connection & before any async logic in order to avoid race conditions with sleep timeouts
|
|
1241
|
-
this.#resetSleepTimer();
|
|
1242
|
-
|
|
1243
|
-
// Add to persistence & save immediately
|
|
1244
|
-
this.#persist.connections.push(persist);
|
|
1245
|
-
this.saveState({ immediate: true });
|
|
1246
|
-
|
|
1247
|
-
// Handle connection
|
|
1248
|
-
if (this.#config.onConnect) {
|
|
1249
|
-
try {
|
|
1250
|
-
const result = this.#config.onConnect(this.actorContext, conn);
|
|
1251
|
-
if (result instanceof Promise) {
|
|
1252
|
-
deadline(
|
|
1253
|
-
result,
|
|
1254
|
-
this.#config.options.onConnectTimeout,
|
|
1255
|
-
).catch((error) => {
|
|
1256
|
-
this.#rLog.error({
|
|
1257
|
-
msg: "error in `onConnect`, closing socket",
|
|
1258
|
-
error,
|
|
1259
|
-
});
|
|
1260
|
-
conn?.disconnect("`onConnect` failed");
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
} catch (error) {
|
|
1264
|
-
this.#rLog.error({
|
|
1265
|
-
msg: "error in `onConnect`",
|
|
1266
|
-
error: stringifyError(error),
|
|
1267
|
-
});
|
|
1268
|
-
conn?.disconnect("`onConnect` failed");
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
this.inspector.emitter.emit("connectionUpdated");
|
|
1273
|
-
|
|
1274
|
-
// Send init message
|
|
1275
|
-
conn._sendMessage(
|
|
1276
|
-
new CachedSerializer<protocol.ToClient>(
|
|
1277
|
-
{
|
|
1278
|
-
body: {
|
|
1279
|
-
tag: "Init",
|
|
1280
|
-
val: {
|
|
1281
|
-
actorId: this.id,
|
|
1282
|
-
connectionId: conn.id,
|
|
1283
|
-
connectionToken: conn._token,
|
|
1284
|
-
},
|
|
1285
|
-
},
|
|
1286
|
-
},
|
|
1287
|
-
TO_CLIENT_VERSIONED,
|
|
1288
|
-
),
|
|
1289
|
-
);
|
|
1290
|
-
|
|
1291
|
-
return conn;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// MARK: Messages
|
|
1295
|
-
async processMessage(
|
|
1296
|
-
message: protocol.ToServer,
|
|
1297
|
-
conn: Conn<S, CP, CS, V, I, DB>,
|
|
1298
|
-
) {
|
|
1299
|
-
await processMessage(message, this, conn, {
|
|
1300
|
-
onExecuteAction: async (ctx, name, args) => {
|
|
1301
|
-
this.inspector.emitter.emit("eventFired", {
|
|
1302
|
-
type: "action",
|
|
1303
|
-
name,
|
|
1304
|
-
args,
|
|
1305
|
-
connId: conn.id,
|
|
1306
|
-
});
|
|
1307
|
-
return await this.executeAction(ctx, name, args);
|
|
1308
|
-
},
|
|
1309
|
-
onSubscribe: async (eventName, conn) => {
|
|
1310
|
-
this.inspector.emitter.emit("eventFired", {
|
|
1311
|
-
type: "subscribe",
|
|
1312
|
-
eventName,
|
|
1313
|
-
connId: conn.id,
|
|
1314
|
-
});
|
|
1315
|
-
this.#addSubscription(eventName, conn, false);
|
|
1316
|
-
},
|
|
1317
|
-
onUnsubscribe: async (eventName, conn) => {
|
|
1318
|
-
this.inspector.emitter.emit("eventFired", {
|
|
1319
|
-
type: "unsubscribe",
|
|
1320
|
-
eventName,
|
|
1321
|
-
connId: conn.id,
|
|
1322
|
-
});
|
|
1323
|
-
this.#removeSubscription(eventName, conn, false);
|
|
1324
|
-
},
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// MARK: Events
|
|
1329
|
-
#addSubscription(
|
|
1330
|
-
eventName: string,
|
|
1331
|
-
connection: Conn<S, CP, CS, V, I, DB>,
|
|
1332
|
-
fromPersist: boolean,
|
|
1333
|
-
) {
|
|
1334
|
-
if (connection.subscriptions.has(eventName)) {
|
|
1335
|
-
this.#rLog.debug({
|
|
1336
|
-
msg: "connection already has subscription",
|
|
1337
|
-
eventName,
|
|
1338
|
-
});
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
// Persist subscriptions & save immediately
|
|
1343
|
-
//
|
|
1344
|
-
// Don't update persistence if already restoring from persistence
|
|
1345
|
-
if (!fromPersist) {
|
|
1346
|
-
connection.__persist.subscriptions.push({ eventName: eventName });
|
|
1347
|
-
this.saveState({ immediate: true });
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
// Update subscriptions
|
|
1351
|
-
connection.subscriptions.add(eventName);
|
|
1352
|
-
|
|
1353
|
-
// Update subscription index
|
|
1354
|
-
let subscribers = this.#subscriptionIndex.get(eventName);
|
|
1355
|
-
if (!subscribers) {
|
|
1356
|
-
subscribers = new Set();
|
|
1357
|
-
this.#subscriptionIndex.set(eventName, subscribers);
|
|
1358
|
-
}
|
|
1359
|
-
subscribers.add(connection);
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
#removeSubscription(
|
|
1363
|
-
eventName: string,
|
|
1364
|
-
connection: Conn<S, CP, CS, V, I, DB>,
|
|
1365
|
-
fromRemoveConn: boolean,
|
|
1366
|
-
) {
|
|
1367
|
-
if (!connection.subscriptions.has(eventName)) {
|
|
1368
|
-
this.#rLog.warn({
|
|
1369
|
-
msg: "connection does not have subscription",
|
|
1370
|
-
eventName,
|
|
1371
|
-
});
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
// Persist subscriptions & save immediately
|
|
1376
|
-
//
|
|
1377
|
-
// Don't update the connection itself if the connection is already being removed
|
|
1378
|
-
if (!fromRemoveConn) {
|
|
1379
|
-
connection.subscriptions.delete(eventName);
|
|
1380
|
-
|
|
1381
|
-
const subIdx = connection.__persist.subscriptions.findIndex(
|
|
1382
|
-
(s) => s.eventName === eventName,
|
|
1383
|
-
);
|
|
1384
|
-
if (subIdx !== -1) {
|
|
1385
|
-
connection.__persist.subscriptions.splice(subIdx, 1);
|
|
1386
|
-
} else {
|
|
1387
|
-
this.#rLog.warn({
|
|
1388
|
-
msg: "subscription does not exist with name",
|
|
1389
|
-
eventName,
|
|
1390
|
-
});
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
this.saveState({ immediate: true });
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// Update scriptions index
|
|
1397
|
-
const subscribers = this.#subscriptionIndex.get(eventName);
|
|
1398
|
-
if (subscribers) {
|
|
1399
|
-
subscribers.delete(connection);
|
|
1400
|
-
if (subscribers.size === 0) {
|
|
1401
|
-
this.#subscriptionIndex.delete(eventName);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
#assertReady(allowStoppingState: boolean = false) {
|
|
1407
|
-
if (!this.#ready) throw new errors.InternalError("Actor not ready");
|
|
1408
|
-
if (!allowStoppingState && this.#stopCalled)
|
|
1409
|
-
throw new errors.InternalError("Actor is stopping");
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
/**
|
|
1413
|
-
* Check the liveness of all connections.
|
|
1414
|
-
* Sets up a recurring check based on the configured interval.
|
|
1415
|
-
*/
|
|
1416
|
-
#checkConnectionsLiveness() {
|
|
1417
|
-
this.#rLog.debug({ msg: "checking connections liveness" });
|
|
1418
|
-
|
|
1419
|
-
let connected = 0;
|
|
1420
|
-
let reconnecting = 0;
|
|
1421
|
-
let removed = 0;
|
|
1422
|
-
for (const conn of this.#connections.values()) {
|
|
1423
|
-
if (conn.__status === "connected") {
|
|
1424
|
-
connected += 1;
|
|
1425
|
-
this.#rLog.debug({
|
|
1426
|
-
msg: "connection is alive",
|
|
1427
|
-
connId: conn.id,
|
|
1428
|
-
});
|
|
1429
|
-
} else {
|
|
1430
|
-
reconnecting += 1;
|
|
1431
|
-
|
|
1432
|
-
const lastSeen = conn.__persist.lastSeen;
|
|
1433
|
-
const sinceLastSeen = Date.now() - lastSeen;
|
|
1434
|
-
if (
|
|
1435
|
-
sinceLastSeen <
|
|
1436
|
-
this.#config.options.connectionLivenessTimeout
|
|
1437
|
-
) {
|
|
1438
|
-
this.#rLog.debug({
|
|
1439
|
-
msg: "connection might be alive, will check later",
|
|
1440
|
-
connId: conn.id,
|
|
1441
|
-
lastSeen,
|
|
1442
|
-
sinceLastSeen,
|
|
1443
|
-
});
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// Connection is dead, remove it
|
|
1448
|
-
this.#rLog.info({
|
|
1449
|
-
msg: "connection is dead, removing",
|
|
1450
|
-
connId: conn.id,
|
|
1451
|
-
lastSeen,
|
|
1452
|
-
});
|
|
1453
|
-
|
|
1454
|
-
// Assume that the connection is dead here, no need to disconnect anything
|
|
1455
|
-
removed += 1;
|
|
1456
|
-
this.#removeConn(conn);
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
this.#rLog.debug({
|
|
1461
|
-
msg: "checked connection liveness",
|
|
1462
|
-
total: connected + reconnecting,
|
|
1463
|
-
connected,
|
|
1464
|
-
reconnecting,
|
|
1465
|
-
removed,
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
/**
|
|
1470
|
-
* Check if the actor is ready to handle requests.
|
|
1471
|
-
*/
|
|
1472
|
-
isReady(): boolean {
|
|
1473
|
-
return this.#ready;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
/**
|
|
1477
|
-
* Execute an action call from a client.
|
|
1478
|
-
*
|
|
1479
|
-
* This method handles:
|
|
1480
|
-
* 1. Validating the action name
|
|
1481
|
-
* 2. Executing the action function
|
|
1482
|
-
* 3. Processing the result through onBeforeActionResponse (if configured)
|
|
1483
|
-
* 4. Handling timeouts and errors
|
|
1484
|
-
* 5. Saving state changes
|
|
1485
|
-
*
|
|
1486
|
-
* @param ctx The action context
|
|
1487
|
-
* @param actionName The name of the action being called
|
|
1488
|
-
* @param args The arguments passed to the action
|
|
1489
|
-
* @returns The result of the action call
|
|
1490
|
-
* @throws {ActionNotFound} If the action doesn't exist
|
|
1491
|
-
* @throws {ActionTimedOut} If the action times out
|
|
1492
|
-
* @internal
|
|
1493
|
-
*/
|
|
1494
|
-
async executeAction(
|
|
1495
|
-
ctx: ActionContext<S, CP, CS, V, I, DB>,
|
|
1496
|
-
actionName: string,
|
|
1497
|
-
args: unknown[],
|
|
1498
|
-
): Promise<unknown> {
|
|
1499
|
-
invariant(this.#ready, "executing action before ready");
|
|
1500
|
-
|
|
1501
|
-
// Prevent calling private or reserved methods
|
|
1502
|
-
if (!(actionName in this.#config.actions)) {
|
|
1503
|
-
this.#rLog.warn({ msg: "action does not exist", actionName });
|
|
1504
|
-
throw new errors.ActionNotFound(actionName);
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// Check if the method exists on this object
|
|
1508
|
-
const actionFunction = this.#config.actions[actionName];
|
|
1509
|
-
if (typeof actionFunction !== "function") {
|
|
1510
|
-
this.#rLog.warn({
|
|
1511
|
-
msg: "action is not a function",
|
|
1512
|
-
actionName: actionName,
|
|
1513
|
-
type: typeof actionFunction,
|
|
1514
|
-
});
|
|
1515
|
-
throw new errors.ActionNotFound(actionName);
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// TODO: pass abortable to the action to decide when to abort
|
|
1519
|
-
// TODO: Manually call abortable for better error handling
|
|
1520
|
-
// Call the function on this object with those arguments
|
|
1521
|
-
try {
|
|
1522
|
-
// Log when we start executing the action
|
|
1523
|
-
this.#rLog.debug({
|
|
1524
|
-
msg: "executing action",
|
|
1525
|
-
actionName: actionName,
|
|
1526
|
-
args,
|
|
1527
|
-
});
|
|
1528
|
-
|
|
1529
|
-
const outputOrPromise = actionFunction.call(
|
|
1530
|
-
undefined,
|
|
1531
|
-
ctx,
|
|
1532
|
-
...args,
|
|
1533
|
-
);
|
|
1534
|
-
let output: unknown;
|
|
1535
|
-
if (outputOrPromise instanceof Promise) {
|
|
1536
|
-
// Log that we're waiting for an async action
|
|
1537
|
-
this.#rLog.debug({
|
|
1538
|
-
msg: "awaiting async action",
|
|
1539
|
-
actionName: actionName,
|
|
1540
|
-
});
|
|
1541
|
-
|
|
1542
|
-
output = await deadline(
|
|
1543
|
-
outputOrPromise,
|
|
1544
|
-
this.#config.options.actionTimeout,
|
|
1545
|
-
);
|
|
1546
|
-
|
|
1547
|
-
// Log that async action completed
|
|
1548
|
-
this.#rLog.debug({
|
|
1549
|
-
msg: "async action completed",
|
|
1550
|
-
actionName: actionName,
|
|
1551
|
-
});
|
|
1552
|
-
} else {
|
|
1553
|
-
output = outputOrPromise;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// Process the output through onBeforeActionResponse if configured
|
|
1557
|
-
if (this.#config.onBeforeActionResponse) {
|
|
1558
|
-
try {
|
|
1559
|
-
const processedOutput = this.#config.onBeforeActionResponse(
|
|
1560
|
-
this.actorContext,
|
|
1561
|
-
actionName,
|
|
1562
|
-
args,
|
|
1563
|
-
output,
|
|
1564
|
-
);
|
|
1565
|
-
if (processedOutput instanceof Promise) {
|
|
1566
|
-
this.#rLog.debug({
|
|
1567
|
-
msg: "awaiting onBeforeActionResponse",
|
|
1568
|
-
actionName: actionName,
|
|
1569
|
-
});
|
|
1570
|
-
output = await processedOutput;
|
|
1571
|
-
this.#rLog.debug({
|
|
1572
|
-
msg: "onBeforeActionResponse completed",
|
|
1573
|
-
actionName: actionName,
|
|
1574
|
-
});
|
|
1575
|
-
} else {
|
|
1576
|
-
output = processedOutput;
|
|
1577
|
-
}
|
|
1578
|
-
} catch (error) {
|
|
1579
|
-
this.#rLog.error({
|
|
1580
|
-
msg: "error in `onBeforeActionResponse`",
|
|
1581
|
-
error: stringifyError(error),
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
// Log the output before returning
|
|
1587
|
-
this.#rLog.debug({
|
|
1588
|
-
msg: "action completed",
|
|
1589
|
-
actionName: actionName,
|
|
1590
|
-
outputType: typeof output,
|
|
1591
|
-
isPromise: output instanceof Promise,
|
|
1592
|
-
});
|
|
1593
|
-
|
|
1594
|
-
// This output *might* reference a part of the state (using onChange), but
|
|
1595
|
-
// that's OK since this value always gets serialized and sent over the
|
|
1596
|
-
// network.
|
|
1597
|
-
return output;
|
|
1598
|
-
} catch (error) {
|
|
1599
|
-
if (error instanceof DeadlineError) {
|
|
1600
|
-
throw new errors.ActionTimedOut();
|
|
1601
|
-
}
|
|
1602
|
-
this.#rLog.error({
|
|
1603
|
-
msg: "action error",
|
|
1604
|
-
actionName: actionName,
|
|
1605
|
-
error: stringifyError(error),
|
|
1606
|
-
});
|
|
1607
|
-
throw error;
|
|
1608
|
-
} finally {
|
|
1609
|
-
this.#savePersistThrottled();
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
/**
|
|
1614
|
-
* Returns a list of action methods available on this actor.
|
|
1615
|
-
*/
|
|
1616
|
-
get actions(): string[] {
|
|
1617
|
-
return Object.keys(this.#config.actions);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
/**
|
|
1621
|
-
* Handles raw HTTP requests to the actor.
|
|
1622
|
-
*/
|
|
1623
|
-
async handleFetch(
|
|
1624
|
-
request: Request,
|
|
1625
|
-
opts: Record<never, never>,
|
|
1626
|
-
): Promise<Response> {
|
|
1627
|
-
this.#assertReady();
|
|
1628
|
-
|
|
1629
|
-
if (!this.#config.onFetch) {
|
|
1630
|
-
throw new errors.FetchHandlerNotDefined();
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
try {
|
|
1634
|
-
const response = await this.#config.onFetch(
|
|
1635
|
-
this.actorContext,
|
|
1636
|
-
request,
|
|
1637
|
-
opts,
|
|
1638
|
-
);
|
|
1639
|
-
if (!response) {
|
|
1640
|
-
throw new errors.InvalidFetchResponse();
|
|
1641
|
-
}
|
|
1642
|
-
return response;
|
|
1643
|
-
} catch (error) {
|
|
1644
|
-
this.#rLog.error({
|
|
1645
|
-
msg: "onFetch error",
|
|
1646
|
-
error: stringifyError(error),
|
|
1647
|
-
});
|
|
1648
|
-
throw error;
|
|
1649
|
-
} finally {
|
|
1650
|
-
this.#savePersistThrottled();
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
/**
|
|
1655
|
-
* Handles raw WebSocket connections to the actor.
|
|
1656
|
-
*/
|
|
1657
|
-
async handleWebSocket(
|
|
1658
|
-
websocket: UniversalWebSocket,
|
|
1659
|
-
opts: { request: Request },
|
|
1660
|
-
): Promise<void> {
|
|
1661
|
-
this.#assertReady();
|
|
1662
|
-
|
|
1663
|
-
if (!this.#config.onWebSocket) {
|
|
1664
|
-
throw new errors.InternalError("onWebSocket handler not defined");
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
try {
|
|
1668
|
-
// Set up state tracking to detect changes during WebSocket handling
|
|
1669
|
-
const stateBeforeHandler = this.#persistChanged;
|
|
1670
|
-
|
|
1671
|
-
// Track active websocket until it fully closes
|
|
1672
|
-
this.#activeRawWebSockets.add(websocket);
|
|
1673
|
-
this.#resetSleepTimer();
|
|
1674
|
-
|
|
1675
|
-
// Track hibernatable WebSockets
|
|
1676
|
-
let rivetRequestId: ArrayBuffer | undefined;
|
|
1677
|
-
let persistedHibernatableWebSocket:
|
|
1678
|
-
| PersistedHibernatableWebSocket
|
|
1679
|
-
| undefined;
|
|
1680
|
-
|
|
1681
|
-
const onSocketOpened = (event: any) => {
|
|
1682
|
-
rivetRequestId = event?.rivetRequestId;
|
|
1683
|
-
|
|
1684
|
-
// Find hibernatable WS
|
|
1685
|
-
if (rivetRequestId) {
|
|
1686
|
-
const rivetRequestIdLocal = rivetRequestId;
|
|
1687
|
-
persistedHibernatableWebSocket =
|
|
1688
|
-
this.#persist.hibernatableWebSocket.find((ws) =>
|
|
1689
|
-
arrayBuffersEqual(
|
|
1690
|
-
ws.requestId,
|
|
1691
|
-
rivetRequestIdLocal,
|
|
1692
|
-
),
|
|
1693
|
-
);
|
|
1694
|
-
|
|
1695
|
-
if (persistedHibernatableWebSocket) {
|
|
1696
|
-
persistedHibernatableWebSocket.lastSeenTimestamp =
|
|
1697
|
-
BigInt(Date.now());
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
this.#rLog.debug({
|
|
1702
|
-
msg: "actor instance onSocketOpened",
|
|
1703
|
-
rivetRequestId,
|
|
1704
|
-
isHibernatable: !!persistedHibernatableWebSocket,
|
|
1705
|
-
hibernationMsgIndex:
|
|
1706
|
-
persistedHibernatableWebSocket?.msgIndex,
|
|
1707
|
-
});
|
|
1708
|
-
};
|
|
1709
|
-
|
|
1710
|
-
const onSocketMessage = (event: any) => {
|
|
1711
|
-
// Update state of hibernatable WS
|
|
1712
|
-
if (persistedHibernatableWebSocket) {
|
|
1713
|
-
persistedHibernatableWebSocket.lastSeenTimestamp = BigInt(
|
|
1714
|
-
Date.now(),
|
|
1715
|
-
);
|
|
1716
|
-
persistedHibernatableWebSocket.msgIndex = BigInt(
|
|
1717
|
-
event.rivetMessageIndex,
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
this.#rLog.debug({
|
|
1722
|
-
msg: "actor instance onSocketMessage",
|
|
1723
|
-
rivetRequestId,
|
|
1724
|
-
isHibernatable: !!persistedHibernatableWebSocket,
|
|
1725
|
-
hibernationMsgIndex:
|
|
1726
|
-
persistedHibernatableWebSocket?.msgIndex,
|
|
1727
|
-
});
|
|
1728
|
-
};
|
|
1729
|
-
|
|
1730
|
-
const onSocketClosed = (_event: any) => {
|
|
1731
|
-
// Remove hibernatable WS
|
|
1732
|
-
if (rivetRequestId) {
|
|
1733
|
-
const rivetRequestIdLocal = rivetRequestId;
|
|
1734
|
-
const wsIndex =
|
|
1735
|
-
this.#persist.hibernatableWebSocket.findIndex((ws) =>
|
|
1736
|
-
arrayBuffersEqual(
|
|
1737
|
-
ws.requestId,
|
|
1738
|
-
rivetRequestIdLocal,
|
|
1739
|
-
),
|
|
1740
|
-
);
|
|
1741
|
-
|
|
1742
|
-
const removed = this.#persist.hibernatableWebSocket.splice(
|
|
1743
|
-
wsIndex,
|
|
1744
|
-
1,
|
|
1745
|
-
);
|
|
1746
|
-
if (removed.length > 0) {
|
|
1747
|
-
this.#rLog.debug({
|
|
1748
|
-
msg: "removed hibernatable websocket",
|
|
1749
|
-
rivetRequestId,
|
|
1750
|
-
hibernationMsgIndex:
|
|
1751
|
-
persistedHibernatableWebSocket?.msgIndex,
|
|
1752
|
-
});
|
|
1753
|
-
} else {
|
|
1754
|
-
this.#rLog.warn({
|
|
1755
|
-
msg: "could not find hibernatable websocket to remove",
|
|
1756
|
-
rivetRequestId,
|
|
1757
|
-
hibernationMsgIndex:
|
|
1758
|
-
persistedHibernatableWebSocket?.msgIndex,
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
this.#rLog.debug({
|
|
1764
|
-
msg: "actor instance onSocketMessage",
|
|
1765
|
-
rivetRequestId,
|
|
1766
|
-
isHibernatable: !!persistedHibernatableWebSocket,
|
|
1767
|
-
hibernatableWebSocketCount:
|
|
1768
|
-
this.#persist.hibernatableWebSocket.length,
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
// Remove listener and socket from tracking
|
|
1772
|
-
try {
|
|
1773
|
-
websocket.removeEventListener("open", onSocketOpened);
|
|
1774
|
-
websocket.removeEventListener("message", onSocketMessage);
|
|
1775
|
-
websocket.removeEventListener("close", onSocketClosed);
|
|
1776
|
-
websocket.removeEventListener("error", onSocketClosed);
|
|
1777
|
-
} catch {}
|
|
1778
|
-
this.#activeRawWebSockets.delete(websocket);
|
|
1779
|
-
this.#resetSleepTimer();
|
|
1780
|
-
};
|
|
1781
|
-
|
|
1782
|
-
try {
|
|
1783
|
-
websocket.addEventListener("open", onSocketOpened);
|
|
1784
|
-
websocket.addEventListener("message", onSocketMessage);
|
|
1785
|
-
websocket.addEventListener("close", onSocketClosed);
|
|
1786
|
-
websocket.addEventListener("error", onSocketClosed);
|
|
1787
|
-
} catch {}
|
|
1788
|
-
|
|
1789
|
-
// Handle WebSocket
|
|
1790
|
-
await this.#config.onWebSocket(this.actorContext, websocket, opts);
|
|
1791
|
-
|
|
1792
|
-
// If state changed during the handler, save it
|
|
1793
|
-
if (this.#persistChanged && !stateBeforeHandler) {
|
|
1794
|
-
await this.saveState({ immediate: true });
|
|
1795
|
-
}
|
|
1796
|
-
} catch (error) {
|
|
1797
|
-
this.#rLog.error({
|
|
1798
|
-
msg: "onWebSocket error",
|
|
1799
|
-
error: stringifyError(error),
|
|
1800
|
-
});
|
|
1801
|
-
throw error;
|
|
1802
|
-
} finally {
|
|
1803
|
-
this.#savePersistThrottled();
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// MARK: Lifecycle hooks
|
|
1808
|
-
|
|
1809
|
-
// MARK: Exposed methods
|
|
1810
|
-
get log(): Logger {
|
|
1811
|
-
invariant(this.#log, "log not configured");
|
|
1812
|
-
return this.#log;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
get rLog(): Logger {
|
|
1816
|
-
invariant(this.#rLog, "log not configured");
|
|
1817
|
-
return this.#rLog;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
/**
|
|
1821
|
-
* Gets the name.
|
|
1822
|
-
*/
|
|
1823
|
-
get name(): string {
|
|
1824
|
-
return this.#name;
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
/**
|
|
1828
|
-
* Gets the key.
|
|
1829
|
-
*/
|
|
1830
|
-
get key(): ActorKey {
|
|
1831
|
-
return this.#key;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
/**
|
|
1835
|
-
* Gets the region.
|
|
1836
|
-
*/
|
|
1837
|
-
get region(): string {
|
|
1838
|
-
return this.#region;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
/**
|
|
1842
|
-
* Gets the scheduler.
|
|
1843
|
-
*/
|
|
1844
|
-
get schedule(): Schedule {
|
|
1845
|
-
return this.#schedule;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
/**
|
|
1849
|
-
* Gets the map of connections.
|
|
1850
|
-
*/
|
|
1851
|
-
get conns(): Map<ConnId, Conn<S, CP, CS, V, I, DB>> {
|
|
1852
|
-
return this.#connections;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
/**
|
|
1856
|
-
* Gets the current state.
|
|
1857
|
-
*
|
|
1858
|
-
* Changing properties of this value will automatically be persisted.
|
|
1859
|
-
*/
|
|
1860
|
-
get state(): S {
|
|
1861
|
-
this.#validateStateEnabled();
|
|
1862
|
-
return this.#persist.state;
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
/**
|
|
1866
|
-
* Gets the database.
|
|
1867
|
-
* @experimental
|
|
1868
|
-
* @throws {DatabaseNotEnabled} If the database is not enabled.
|
|
1869
|
-
*/
|
|
1870
|
-
get db(): InferDatabaseClient<DB> {
|
|
1871
|
-
if (!this.#db) {
|
|
1872
|
-
throw new errors.DatabaseNotEnabled();
|
|
1873
|
-
}
|
|
1874
|
-
return this.#db;
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
/**
|
|
1878
|
-
* Sets the current state.
|
|
1879
|
-
*
|
|
1880
|
-
* This property will automatically be persisted.
|
|
1881
|
-
*/
|
|
1882
|
-
set state(value: S) {
|
|
1883
|
-
this.#validateStateEnabled();
|
|
1884
|
-
this.#persist.state = value;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
get vars(): V {
|
|
1888
|
-
this.#validateVarsEnabled();
|
|
1889
|
-
invariant(this.#vars !== undefined, "vars not enabled");
|
|
1890
|
-
return this.#vars;
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
/**
|
|
1894
|
-
* Broadcasts an event to all connected clients.
|
|
1895
|
-
* @param name - The name of the event.
|
|
1896
|
-
* @param args - The arguments to send with the event.
|
|
1897
|
-
*/
|
|
1898
|
-
_broadcast<Args extends Array<unknown>>(name: string, ...args: Args) {
|
|
1899
|
-
this.#assertReady();
|
|
1900
|
-
|
|
1901
|
-
this.inspector.emitter.emit("eventFired", {
|
|
1902
|
-
type: "broadcast",
|
|
1903
|
-
eventName: name,
|
|
1904
|
-
args,
|
|
1905
|
-
});
|
|
1906
|
-
|
|
1907
|
-
// Send to all connected clients
|
|
1908
|
-
const subscriptions = this.#subscriptionIndex.get(name);
|
|
1909
|
-
if (!subscriptions) return;
|
|
1910
|
-
|
|
1911
|
-
const toClientSerializer = new CachedSerializer<protocol.ToClient>(
|
|
1912
|
-
{
|
|
1913
|
-
body: {
|
|
1914
|
-
tag: "Event",
|
|
1915
|
-
val: {
|
|
1916
|
-
name,
|
|
1917
|
-
args: bufferToArrayBuffer(cbor.encode(args)),
|
|
1918
|
-
},
|
|
1919
|
-
},
|
|
1920
|
-
},
|
|
1921
|
-
TO_CLIENT_VERSIONED,
|
|
1922
|
-
);
|
|
1923
|
-
|
|
1924
|
-
// Send message to clients
|
|
1925
|
-
for (const connection of subscriptions) {
|
|
1926
|
-
connection._sendMessage(toClientSerializer);
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
/**
|
|
1931
|
-
* Prevents the actor from sleeping until promise is complete.
|
|
1932
|
-
*
|
|
1933
|
-
* This allows the actor runtime to ensure that a promise completes while
|
|
1934
|
-
* returning from an action request early.
|
|
1935
|
-
*
|
|
1936
|
-
* @param promise - The promise to run in the background.
|
|
1937
|
-
*/
|
|
1938
|
-
_waitUntil(promise: Promise<void>) {
|
|
1939
|
-
this.#assertReady();
|
|
1940
|
-
|
|
1941
|
-
// TODO: Should we force save the state?
|
|
1942
|
-
// Add logging to promise and make it non-failable
|
|
1943
|
-
const nonfailablePromise = promise
|
|
1944
|
-
.then(() => {
|
|
1945
|
-
this.#rLog.debug({ msg: "wait until promise complete" });
|
|
1946
|
-
})
|
|
1947
|
-
.catch((error) => {
|
|
1948
|
-
this.#rLog.error({
|
|
1949
|
-
msg: "wait until promise failed",
|
|
1950
|
-
error: stringifyError(error),
|
|
1951
|
-
});
|
|
1952
|
-
});
|
|
1953
|
-
this.#backgroundPromises.push(nonfailablePromise);
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
/**
|
|
1957
|
-
* Forces the state to get saved.
|
|
1958
|
-
*
|
|
1959
|
-
* This is helpful if running a long task that may fail later or when
|
|
1960
|
-
* running a background job that updates the state.
|
|
1961
|
-
*
|
|
1962
|
-
* @param opts - Options for saving the state.
|
|
1963
|
-
*/
|
|
1964
|
-
async saveState(opts: SaveStateOptions) {
|
|
1965
|
-
this.#assertReady(opts.allowStoppingState);
|
|
1966
|
-
|
|
1967
|
-
this.#rLog.debug({
|
|
1968
|
-
msg: "saveState called",
|
|
1969
|
-
persistChanged: this.#persistChanged,
|
|
1970
|
-
allowStoppingState: opts.allowStoppingState,
|
|
1971
|
-
immediate: opts.immediate,
|
|
1972
|
-
});
|
|
1973
|
-
|
|
1974
|
-
if (this.#persistChanged) {
|
|
1975
|
-
if (opts.immediate) {
|
|
1976
|
-
// Save immediately
|
|
1977
|
-
await this.#savePersistInner();
|
|
1978
|
-
} else {
|
|
1979
|
-
// Create callback
|
|
1980
|
-
if (!this.#onPersistSavedPromise) {
|
|
1981
|
-
this.#onPersistSavedPromise = promiseWithResolvers();
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
// Save state throttled
|
|
1985
|
-
this.#savePersistThrottled();
|
|
1986
|
-
|
|
1987
|
-
// Wait for save
|
|
1988
|
-
await this.#onPersistSavedPromise.promise;
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
/**
|
|
1994
|
-
* Called by router middleware when an HTTP request begins.
|
|
1995
|
-
*/
|
|
1996
|
-
__beginHonoHttpRequest() {
|
|
1997
|
-
this.#activeHonoHttpRequests++;
|
|
1998
|
-
this.#resetSleepTimer();
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
/**
|
|
2002
|
-
* Called by router middleware when an HTTP request ends.
|
|
2003
|
-
*/
|
|
2004
|
-
__endHonoHttpRequest() {
|
|
2005
|
-
this.#activeHonoHttpRequests--;
|
|
2006
|
-
if (this.#activeHonoHttpRequests < 0) {
|
|
2007
|
-
this.#activeHonoHttpRequests = 0;
|
|
2008
|
-
this.#rLog.warn({
|
|
2009
|
-
msg: "active hono requests went below 0, this is a RivetKit bug",
|
|
2010
|
-
...EXTRA_ERROR_LOG,
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
this.#resetSleepTimer();
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
// MARK: Sleep
|
|
2017
|
-
/**
|
|
2018
|
-
* Reset timer from the last actor interaction that allows it to be put to sleep.
|
|
2019
|
-
*
|
|
2020
|
-
* This should be called any time a sleep-related event happens:
|
|
2021
|
-
* - Connection opens (will clear timer)
|
|
2022
|
-
* - Connection closes (will schedule timer if there are no open connections)
|
|
2023
|
-
* - Alarm triggers (will reset timer)
|
|
2024
|
-
*
|
|
2025
|
-
* We don't need to call this on events like individual action calls, since there will always be a connection open for these.
|
|
2026
|
-
**/
|
|
2027
|
-
#resetSleepTimer() {
|
|
2028
|
-
if (this.#config.options.noSleep || !this.#sleepingSupported) return;
|
|
2029
|
-
|
|
2030
|
-
// Don't sleep if already stopping
|
|
2031
|
-
if (this.#stopCalled) return;
|
|
2032
|
-
|
|
2033
|
-
const canSleep = this.#canSleep();
|
|
2034
|
-
|
|
2035
|
-
this.#rLog.debug({
|
|
2036
|
-
msg: "resetting sleep timer",
|
|
2037
|
-
canSleep: CanSleep[canSleep],
|
|
2038
|
-
existingTimeout: !!this.#sleepTimeout,
|
|
2039
|
-
timeout: this.#config.options.sleepTimeout,
|
|
2040
|
-
});
|
|
2041
|
-
|
|
2042
|
-
if (this.#sleepTimeout) {
|
|
2043
|
-
clearTimeout(this.#sleepTimeout);
|
|
2044
|
-
this.#sleepTimeout = undefined;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
// Don't set a new timer if already sleeping
|
|
2048
|
-
if (this.#sleepCalled) return;
|
|
2049
|
-
|
|
2050
|
-
if (canSleep === CanSleep.Yes) {
|
|
2051
|
-
this.#sleepTimeout = setTimeout(() => {
|
|
2052
|
-
this._startSleep();
|
|
2053
|
-
}, this.#config.options.sleepTimeout);
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
/** If this actor can be put in a sleeping state. */
|
|
2058
|
-
#canSleep(): CanSleep {
|
|
2059
|
-
if (!this.#ready) return CanSleep.NotReady;
|
|
2060
|
-
|
|
2061
|
-
// Do not sleep if Hono HTTP requests are in-flight
|
|
2062
|
-
if (this.#activeHonoHttpRequests > 0)
|
|
2063
|
-
return CanSleep.ActiveHonoHttpRequests;
|
|
2064
|
-
|
|
2065
|
-
// TODO: When WS hibernation is ready, update this to only count non-hibernatable websockets
|
|
2066
|
-
// Do not sleep if there are raw websockets open
|
|
2067
|
-
if (this.#activeRawWebSockets.size > 0)
|
|
2068
|
-
return CanSleep.ActiveRawWebSockets;
|
|
2069
|
-
|
|
2070
|
-
// Check for active conns. This will also cover active actions, since all actions have a connection.
|
|
2071
|
-
for (const conn of this.#connections.values()) {
|
|
2072
|
-
// TODO: Enable this when hibernation is implemented. We're waiting on support for Guard to not auto-wake the actor if it sleeps.
|
|
2073
|
-
// if (conn.status === "connected" && !conn.isHibernatable)
|
|
2074
|
-
// return false;
|
|
2075
|
-
|
|
2076
|
-
if (conn.status === "connected") return CanSleep.ActiveConns;
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
return CanSleep.Yes;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
/**
|
|
2083
|
-
* Puts an actor to sleep. This should just start the sleep sequence, most shutdown logic should be in _stop (which is called by the ActorDriver when sleeping).
|
|
2084
|
-
*
|
|
2085
|
-
* For the engine, this will:
|
|
2086
|
-
* 1. Publish EventActorIntent with ActorIntentSleep (via driver.startSleep)
|
|
2087
|
-
* 2. Engine runner will wait for CommandStopActor
|
|
2088
|
-
* 3. Engine runner will call _onStop and wait for it to finish
|
|
2089
|
-
* 4. Engine runner will publish EventActorStateUpdate with ActorStateSTop
|
|
2090
|
-
**/
|
|
2091
|
-
_startSleep() {
|
|
2092
|
-
if (this.#stopCalled) {
|
|
2093
|
-
this.#rLog.debug({
|
|
2094
|
-
msg: "cannot call _startSleep if actor already stopping",
|
|
2095
|
-
});
|
|
2096
|
-
return;
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// IMPORTANT: #sleepCalled should have no effect on the actor's
|
|
2100
|
-
// behavior aside from preventing calling _startSleep twice. Wait for
|
|
2101
|
-
// `_onStop` before putting in a stopping state.
|
|
2102
|
-
if (this.#sleepCalled) {
|
|
2103
|
-
this.#rLog.warn({
|
|
2104
|
-
msg: "cannot call _startSleep twice, actor already sleeping",
|
|
2105
|
-
});
|
|
2106
|
-
return;
|
|
2107
|
-
}
|
|
2108
|
-
this.#sleepCalled = true;
|
|
2109
|
-
|
|
2110
|
-
// NOTE: Publishes ActorIntentSleep
|
|
2111
|
-
const sleep = this.#actorDriver.startSleep?.bind(
|
|
2112
|
-
this.#actorDriver,
|
|
2113
|
-
this.#actorId,
|
|
2114
|
-
);
|
|
2115
|
-
invariant(this.#sleepingSupported, "sleeping not supported");
|
|
2116
|
-
invariant(sleep, "no sleep on driver");
|
|
2117
|
-
|
|
2118
|
-
this.#rLog.info({ msg: "actor sleeping" });
|
|
2119
|
-
|
|
2120
|
-
// Schedule sleep to happen on the next tick. This allows for any action that calls _sleep to complete.
|
|
2121
|
-
setImmediate(() => {
|
|
2122
|
-
// The actor driver should call stop when ready to stop
|
|
2123
|
-
//
|
|
2124
|
-
// This will call _stop once Pegboard responds with the new status
|
|
2125
|
-
sleep();
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
// MARK: Stop
|
|
2130
|
-
/**
|
|
2131
|
-
* For the engine:
|
|
2132
|
-
* 1. Engine runner receives CommandStopActor
|
|
2133
|
-
* 2. Engine runner calls _onStop and waits for it to finish
|
|
2134
|
-
* 3. Engine runner publishes EventActorStateUpdate with ActorStateSTop
|
|
2135
|
-
*/
|
|
2136
|
-
async _onStop() {
|
|
2137
|
-
if (this.#stopCalled) {
|
|
2138
|
-
this.#rLog.warn({ msg: "already stopping actor" });
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
this.#stopCalled = true;
|
|
2142
|
-
|
|
2143
|
-
this.#rLog.info({ msg: "actor stopping" });
|
|
2144
|
-
|
|
2145
|
-
if (this.#sleepTimeout) {
|
|
2146
|
-
clearTimeout(this.#sleepTimeout);
|
|
2147
|
-
this.#sleepTimeout = undefined;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
// Abort any listeners waiting for shutdown
|
|
2151
|
-
try {
|
|
2152
|
-
this.#abortController.abort();
|
|
2153
|
-
} catch {}
|
|
2154
|
-
|
|
2155
|
-
// Call onStop lifecycle hook if defined
|
|
2156
|
-
if (this.#config.onStop) {
|
|
2157
|
-
try {
|
|
2158
|
-
this.#rLog.debug({ msg: "calling onStop" });
|
|
2159
|
-
const result = this.#config.onStop(this.actorContext);
|
|
2160
|
-
if (result instanceof Promise) {
|
|
2161
|
-
await deadline(result, this.#config.options.onStopTimeout);
|
|
2162
|
-
}
|
|
2163
|
-
this.#rLog.debug({ msg: "onStop completed" });
|
|
2164
|
-
} catch (error) {
|
|
2165
|
-
if (error instanceof DeadlineError) {
|
|
2166
|
-
this.#rLog.error({ msg: "onStop timed out" });
|
|
2167
|
-
} else {
|
|
2168
|
-
this.#rLog.error({
|
|
2169
|
-
msg: "error in onStop",
|
|
2170
|
-
error: stringifyError(error),
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
const promises: Promise<unknown>[] = [];
|
|
2177
|
-
|
|
2178
|
-
// Disconnect existing non-hibernatable connections
|
|
2179
|
-
for (const connection of this.#connections.values()) {
|
|
2180
|
-
if (!connection.isHibernatable) {
|
|
2181
|
-
this.#rLog.debug({
|
|
2182
|
-
msg: "disconnecting non-hibernatable connection on actor stop",
|
|
2183
|
-
connId: connection.id,
|
|
2184
|
-
});
|
|
2185
|
-
promises.push(connection.disconnect());
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// TODO: Figure out how to abort HTTP requests on shutdown. This
|
|
2189
|
-
// might already be handled by the engine runner tunnel shutdown.
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
// Wait for any background tasks to finish, with timeout
|
|
2193
|
-
await this.#waitBackgroundPromises(
|
|
2194
|
-
this.#config.options.waitUntilTimeout,
|
|
2195
|
-
);
|
|
2196
|
-
|
|
2197
|
-
// Clear timeouts
|
|
2198
|
-
if (this.#pendingSaveTimeout) clearTimeout(this.#pendingSaveTimeout);
|
|
2199
|
-
if (this.#checkConnLivenessInterval)
|
|
2200
|
-
clearInterval(this.#checkConnLivenessInterval);
|
|
2201
|
-
|
|
2202
|
-
// Write state
|
|
2203
|
-
await this.saveState({ immediate: true, allowStoppingState: true });
|
|
2204
|
-
|
|
2205
|
-
// Await all `close` event listeners with 1.5 second timeout
|
|
2206
|
-
const res = Promise.race([
|
|
2207
|
-
Promise.all(promises).then(() => false),
|
|
2208
|
-
new Promise<boolean>((res) =>
|
|
2209
|
-
globalThis.setTimeout(() => res(true), 1500),
|
|
2210
|
-
),
|
|
2211
|
-
]);
|
|
2212
|
-
|
|
2213
|
-
if (await res) {
|
|
2214
|
-
this.#rLog.warn({
|
|
2215
|
-
msg: "timed out waiting for connections to close, shutting down anyway",
|
|
2216
|
-
});
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
// Wait for queues to finish
|
|
2220
|
-
if (this.#persistWriteQueue.runningDrainLoop)
|
|
2221
|
-
await this.#persistWriteQueue.runningDrainLoop;
|
|
2222
|
-
if (this.#alarmWriteQueue.runningDrainLoop)
|
|
2223
|
-
await this.#alarmWriteQueue.runningDrainLoop;
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
/** Abort signal that fires when the actor is stopping. */
|
|
2227
|
-
get abortSignal(): AbortSignal {
|
|
2228
|
-
return this.#abortController.signal;
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
/** Wait for background waitUntil promises with a timeout. */
|
|
2232
|
-
async #waitBackgroundPromises(timeoutMs: number) {
|
|
2233
|
-
const pending = this.#backgroundPromises;
|
|
2234
|
-
if (pending.length === 0) {
|
|
2235
|
-
this.#rLog.debug({ msg: "no background promises" });
|
|
2236
|
-
return;
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
// Race promises with timeout to determine if pending promises settled fast enough
|
|
2240
|
-
const timedOut = await Promise.race([
|
|
2241
|
-
Promise.allSettled(pending).then(() => false),
|
|
2242
|
-
new Promise<true>((resolve) =>
|
|
2243
|
-
setTimeout(() => resolve(true), timeoutMs),
|
|
2244
|
-
),
|
|
2245
|
-
]);
|
|
2246
|
-
|
|
2247
|
-
if (timedOut) {
|
|
2248
|
-
this.#rLog.error({
|
|
2249
|
-
msg: "timed out waiting for background tasks, background promises may have leaked",
|
|
2250
|
-
count: pending.length,
|
|
2251
|
-
timeoutMs,
|
|
2252
|
-
});
|
|
2253
|
-
} else {
|
|
2254
|
-
this.#rLog.debug({ msg: "background promises finished" });
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
// MARK: BARE Conversion Helpers
|
|
2259
|
-
#convertToBarePersisted(
|
|
2260
|
-
persist: PersistedActor<S, CP, CS, I>,
|
|
2261
|
-
): bareSchema.PersistedActor {
|
|
2262
|
-
return {
|
|
2263
|
-
input:
|
|
2264
|
-
persist.input !== undefined
|
|
2265
|
-
? bufferToArrayBuffer(cbor.encode(persist.input))
|
|
2266
|
-
: null,
|
|
2267
|
-
hasInitialized: persist.hasInitiated,
|
|
2268
|
-
state: bufferToArrayBuffer(cbor.encode(persist.state)),
|
|
2269
|
-
connections: persist.connections.map((conn) => ({
|
|
2270
|
-
id: conn.connId,
|
|
2271
|
-
token: conn.token,
|
|
2272
|
-
parameters: bufferToArrayBuffer(cbor.encode(conn.params || {})),
|
|
2273
|
-
state: bufferToArrayBuffer(cbor.encode(conn.state || {})),
|
|
2274
|
-
subscriptions: conn.subscriptions.map((sub) => ({
|
|
2275
|
-
eventName: sub.eventName,
|
|
2276
|
-
})),
|
|
2277
|
-
lastSeen: BigInt(conn.lastSeen),
|
|
2278
|
-
hibernatableRequestId: conn.hibernatableRequestId ?? null,
|
|
2279
|
-
})),
|
|
2280
|
-
scheduledEvents: persist.scheduledEvents.map((event) => ({
|
|
2281
|
-
eventId: event.eventId,
|
|
2282
|
-
timestamp: BigInt(event.timestamp),
|
|
2283
|
-
kind: {
|
|
2284
|
-
tag: "GenericPersistedScheduleEvent" as const,
|
|
2285
|
-
val: {
|
|
2286
|
-
action: event.kind.generic.actionName,
|
|
2287
|
-
args: event.kind.generic.args ?? null,
|
|
2288
|
-
},
|
|
2289
|
-
},
|
|
2290
|
-
})),
|
|
2291
|
-
hibernatableWebSocket: persist.hibernatableWebSocket.map((ws) => ({
|
|
2292
|
-
requestId: ws.requestId,
|
|
2293
|
-
lastSeenTimestamp: ws.lastSeenTimestamp,
|
|
2294
|
-
msgIndex: ws.msgIndex,
|
|
2295
|
-
})),
|
|
2296
|
-
};
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
#convertFromBarePersisted(
|
|
2300
|
-
bareData: bareSchema.PersistedActor,
|
|
2301
|
-
): PersistedActor<S, CP, CS, I> {
|
|
2302
|
-
return {
|
|
2303
|
-
input: bareData.input
|
|
2304
|
-
? cbor.decode(new Uint8Array(bareData.input))
|
|
2305
|
-
: undefined,
|
|
2306
|
-
hasInitiated: bareData.hasInitialized,
|
|
2307
|
-
state: cbor.decode(new Uint8Array(bareData.state)),
|
|
2308
|
-
connections: bareData.connections.map((conn) => ({
|
|
2309
|
-
connId: conn.id,
|
|
2310
|
-
token: conn.token,
|
|
2311
|
-
params: cbor.decode(new Uint8Array(conn.parameters)),
|
|
2312
|
-
state: cbor.decode(new Uint8Array(conn.state)),
|
|
2313
|
-
subscriptions: conn.subscriptions.map((sub) => ({
|
|
2314
|
-
eventName: sub.eventName,
|
|
2315
|
-
})),
|
|
2316
|
-
lastSeen: Number(conn.lastSeen),
|
|
2317
|
-
hibernatableRequestId: conn.hibernatableRequestId ?? undefined,
|
|
2318
|
-
})),
|
|
2319
|
-
scheduledEvents: bareData.scheduledEvents.map((event) => ({
|
|
2320
|
-
eventId: event.eventId,
|
|
2321
|
-
timestamp: Number(event.timestamp),
|
|
2322
|
-
kind: {
|
|
2323
|
-
generic: {
|
|
2324
|
-
actionName: event.kind.val.action,
|
|
2325
|
-
args: event.kind.val.args,
|
|
2326
|
-
},
|
|
2327
|
-
},
|
|
2328
|
-
})),
|
|
2329
|
-
hibernatableWebSocket: bareData.hibernatableWebSocket.map((ws) => ({
|
|
2330
|
-
requestId: ws.requestId,
|
|
2331
|
-
lastSeenTimestamp: ws.lastSeenTimestamp,
|
|
2332
|
-
msgIndex: ws.msgIndex,
|
|
2333
|
-
})),
|
|
2334
|
-
};
|
|
2335
|
-
}
|
|
2336
|
-
}
|