rivetkit 2.3.3 → 2.3.5-rc.1
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/browser/client.d.ts +157 -9
- package/dist/browser/client.js +518 -542
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/inspector/client.d.ts +101 -2
- package/dist/browser/inspector/client.js +1245 -227
- package/dist/browser/inspector/client.js.map +1 -1
- package/dist/tsup/agent-os/index.cjs +103 -36
- package/dist/tsup/agent-os/index.cjs.map +1 -1
- package/dist/tsup/agent-os/index.d.cts +159 -11
- package/dist/tsup/agent-os/index.d.ts +159 -11
- package/dist/tsup/agent-os/index.js +103 -36
- package/dist/tsup/agent-os/index.js.map +1 -1
- package/dist/tsup/{chunk-34P567PO.cjs → chunk-4XKED5P5.cjs} +145 -176
- package/dist/tsup/chunk-4XKED5P5.cjs.map +1 -0
- package/dist/tsup/{chunk-PYO2U53V.cjs → chunk-6HQCLWAW.cjs} +384 -4
- package/dist/tsup/chunk-6HQCLWAW.cjs.map +1 -0
- package/dist/tsup/{chunk-JZ7TWV65.js → chunk-7AFZMFPQ.js} +15 -1
- package/dist/tsup/chunk-7AFZMFPQ.js.map +1 -0
- package/dist/tsup/{chunk-BLDO2FKO.js → chunk-B4IZU537.js} +382 -2
- package/dist/tsup/chunk-B4IZU537.js.map +1 -0
- package/dist/tsup/{chunk-3MHDOUD7.cjs → chunk-G3CCRCTK.cjs} +14 -9
- package/dist/tsup/chunk-G3CCRCTK.cjs.map +1 -0
- package/dist/tsup/{chunk-CXRPENO3.js → chunk-GAV4PKFT.js} +15 -46
- package/dist/tsup/{chunk-CXRPENO3.js.map → chunk-GAV4PKFT.js.map} +1 -1
- package/dist/tsup/{chunk-HXUEHHJF.js → chunk-JI6GZ2C2.js} +2 -11
- package/dist/tsup/{chunk-HXUEHHJF.js.map → chunk-JI6GZ2C2.js.map} +1 -1
- package/dist/tsup/{chunk-YUEC5SEY.cjs → chunk-M3FCXL3Q.cjs} +3 -3
- package/dist/tsup/{chunk-YUEC5SEY.cjs.map → chunk-M3FCXL3Q.cjs.map} +1 -1
- package/dist/tsup/{chunk-KEKWJECX.js → chunk-MGTSVSUC.js} +2 -2
- package/dist/tsup/{chunk-VE2X4KMG.cjs → chunk-MYWZCDML.cjs} +2 -11
- package/dist/tsup/chunk-MYWZCDML.cjs.map +1 -0
- package/dist/tsup/{chunk-DFA7SYSW.js → chunk-NIC3QLKL.js} +3 -3
- package/dist/tsup/{chunk-BZM4PMFS.js → chunk-P4OGQ4JL.js} +5 -7
- package/dist/tsup/{chunk-BZM4PMFS.js.map → chunk-P4OGQ4JL.js.map} +1 -1
- package/dist/tsup/{chunk-NT2NK3VT.js → chunk-PAG6G46L.js} +2 -2
- package/dist/tsup/{chunk-XIX5DOZN.js → chunk-SHBE77IT.js} +14 -9
- package/dist/tsup/chunk-SHBE77IT.js.map +1 -0
- package/dist/tsup/{chunk-TYLP3U4Y.cjs → chunk-TNIWIUNV.cjs} +9 -11
- package/dist/tsup/chunk-TNIWIUNV.cjs.map +1 -0
- package/dist/tsup/{chunk-S644FL73.cjs → chunk-TR5NLR5V.cjs} +4 -4
- package/dist/tsup/{chunk-S644FL73.cjs.map → chunk-TR5NLR5V.cjs.map} +1 -1
- package/dist/tsup/{chunk-ZA7FLHKH.cjs → chunk-UEULFXUI.cjs} +16 -2
- package/dist/tsup/chunk-UEULFXUI.cjs.map +1 -0
- package/dist/tsup/{chunk-X63ROSOG.cjs → chunk-UJ2FQY63.cjs} +7 -7
- package/dist/tsup/{chunk-X63ROSOG.cjs.map → chunk-UJ2FQY63.cjs.map} +1 -1
- package/dist/tsup/client/mod.cjs +7 -8
- package/dist/tsup/client/mod.cjs.map +1 -1
- package/dist/tsup/client/mod.d.cts +5 -4
- package/dist/tsup/client/mod.d.ts +5 -4
- package/dist/tsup/client/mod.js +6 -7
- package/dist/tsup/common/log.cjs +2 -2
- package/dist/tsup/common/log.js +1 -1
- package/dist/tsup/common/websocket.cjs +3 -3
- package/dist/tsup/common/websocket.js +2 -2
- package/dist/tsup/{config-D49x8NpL.d.cts → config-BxOimoGX.d.cts} +16 -0
- package/dist/tsup/{config-D49x8NpL.d.ts → config-BxOimoGX.d.ts} +16 -0
- package/dist/tsup/{config-ZkIsRFsN.d.ts → config-BzdqbLAP.d.ts} +164 -27
- package/dist/tsup/{config-BfBhjEbk.d.cts → config-CPPEwn_e.d.cts} +164 -27
- package/dist/tsup/{context-sZ9GlaBK.d.cts → context-DSA9gFXB.d.cts} +2 -2
- package/dist/tsup/{context-B7-84u9K.d.ts → context-wWv-K2K_.d.ts} +2 -2
- package/dist/tsup/db/drizzle.cjs +120 -68
- package/dist/tsup/db/drizzle.cjs.map +1 -1
- package/dist/tsup/db/drizzle.d.cts +8 -3
- package/dist/tsup/db/drizzle.d.ts +8 -3
- package/dist/tsup/db/drizzle.js +120 -68
- package/dist/tsup/db/drizzle.js.map +1 -1
- package/dist/tsup/db/mod.cjs +74 -22
- package/dist/tsup/db/mod.cjs.map +1 -1
- package/dist/tsup/db/mod.d.cts +4 -3
- package/dist/tsup/db/mod.d.ts +4 -3
- package/dist/tsup/db/mod.js +74 -22
- package/dist/tsup/db/mod.js.map +1 -1
- package/dist/tsup/dynamic/mod.cjs +2 -2
- package/dist/tsup/dynamic/mod.d.cts +3 -3
- package/dist/tsup/dynamic/mod.d.ts +3 -3
- package/dist/tsup/dynamic/mod.js +1 -1
- package/dist/tsup/inspector/mod.cjs +4 -5
- package/dist/tsup/inspector/mod.cjs.map +1 -1
- package/dist/tsup/inspector/mod.js +3 -4
- package/dist/tsup/inspector/mod.js.map +1 -1
- package/dist/tsup/inspector-tab/mod.cjs +2 -2
- package/dist/tsup/inspector-tab/mod.cjs.map +1 -1
- package/dist/tsup/inspector-tab/mod.d.cts +5 -5
- package/dist/tsup/inspector-tab/mod.d.ts +5 -5
- package/dist/tsup/inspector-tab/mod.js +1 -1
- package/dist/tsup/inspector-tab/mod.js.map +1 -1
- package/dist/tsup/mod.cjs +605 -158
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +5 -5
- package/dist/tsup/mod.d.ts +5 -5
- package/dist/tsup/mod.js +528 -81
- package/dist/tsup/mod.js.map +1 -1
- package/dist/tsup/test/mod.cjs +10 -11
- package/dist/tsup/test/mod.cjs.map +1 -1
- package/dist/tsup/test/mod.d.cts +3 -3
- package/dist/tsup/test/mod.d.ts +3 -3
- package/dist/tsup/test/mod.js +6 -7
- package/dist/tsup/test/mod.js.map +1 -1
- package/dist/tsup/utils-CV0akuDD.d.ts +162 -0
- package/dist/tsup/utils-DQDCphrl.d.cts +162 -0
- package/dist/tsup/utils.cjs +6 -2
- package/dist/tsup/utils.cjs.map +1 -1
- package/dist/tsup/utils.d.cts +3 -146
- package/dist/tsup/utils.d.ts +3 -146
- package/dist/tsup/utils.js +5 -1
- package/dist/tsup/workflow/mod.cjs +198 -151
- package/dist/tsup/workflow/mod.cjs.map +1 -1
- package/dist/tsup/workflow/mod.d.cts +5 -5
- package/dist/tsup/workflow/mod.d.ts +5 -5
- package/dist/tsup/workflow/mod.js +187 -140
- package/dist/tsup/workflow/mod.js.map +1 -1
- package/package.json +8 -8
- package/src/actor/config.test.ts +14 -0
- package/src/actor/config.ts +104 -16
- package/src/actor/definition.ts +18 -7
- package/src/agent-os/actor/index.ts +2 -2
- package/src/common/bare/generated/actor-runtime-socket-protocol/v1.ts +848 -0
- package/src/common/bare/generated/inspector/v6.ts +1367 -0
- package/src/common/database/config.ts +24 -0
- package/src/common/database/mod.test.ts +148 -59
- package/src/common/database/mod.ts +93 -23
- package/src/common/database/native-database.test.ts +9 -0
- package/src/common/database/native-database.ts +151 -0
- package/src/common/database/shared.ts +15 -0
- package/src/common/encoding.ts +28 -21
- package/src/db/drizzle.test.ts +157 -0
- package/src/db/drizzle.ts +170 -79
- package/src/dynamic/isolate-runtime.ts +0 -2
- package/src/engine-client/driver.ts +0 -15
- package/src/engine-client/mod.ts +0 -41
- package/src/inspector/client.browser.ts +69 -8
- package/src/inspector-tab/mod.ts +1 -0
- package/src/registry/config/index.ts +1 -35
- package/src/registry/napi-runtime.ts +204 -16
- package/src/registry/native.ts +200 -30
- package/src/registry/runtime.ts +146 -3
- package/src/registry/wasm-runtime.ts +217 -6
- package/src/utils.ts +3 -0
- package/src/workflow/context.ts +5 -27
- package/src/workflow/driver.test.ts +62 -0
- package/src/workflow/driver.ts +231 -113
- package/src/workflow/mod.ts +3 -3
- package/dist/tsup/chunk-34P567PO.cjs.map +0 -1
- package/dist/tsup/chunk-3MHDOUD7.cjs.map +0 -1
- package/dist/tsup/chunk-APW73HCH.cjs +0 -382
- package/dist/tsup/chunk-APW73HCH.cjs.map +0 -1
- package/dist/tsup/chunk-BLDO2FKO.js.map +0 -1
- package/dist/tsup/chunk-DHXWD4DF.js +0 -382
- package/dist/tsup/chunk-DHXWD4DF.js.map +0 -1
- package/dist/tsup/chunk-JZ7TWV65.js.map +0 -1
- package/dist/tsup/chunk-PYO2U53V.cjs.map +0 -1
- package/dist/tsup/chunk-TYLP3U4Y.cjs.map +0 -1
- package/dist/tsup/chunk-VE2X4KMG.cjs.map +0 -1
- package/dist/tsup/chunk-XIX5DOZN.js.map +0 -1
- package/dist/tsup/chunk-ZA7FLHKH.cjs.map +0 -1
- package/src/actor/driver.ts +0 -141
- package/src/actor/instance/mod.ts +0 -2484
- package/src/drivers/engine/actor-driver.ts +0 -2473
- /package/dist/tsup/{chunk-KEKWJECX.js.map → chunk-MGTSVSUC.js.map} +0 -0
- /package/dist/tsup/{chunk-DFA7SYSW.js.map → chunk-NIC3QLKL.js.map} +0 -0
- /package/dist/tsup/{chunk-NT2NK3VT.js.map → chunk-PAG6G46L.js.map} +0 -0
|
@@ -1,2484 +0,0 @@
|
|
|
1
|
-
// TODO(sleep-cleanup): investigate whether ActorInstance / drivers/engine
|
|
2
|
-
// path is still needed after native path maturation. preventSleep tracking
|
|
3
|
-
// below is retained for legacy driver compatibility but no longer drives core.
|
|
4
|
-
import type { OtlpExportTraceServiceRequestJson } from "@rivetkit/traces";
|
|
5
|
-
import {
|
|
6
|
-
createNoopTraces,
|
|
7
|
-
createTraces,
|
|
8
|
-
type SpanHandle,
|
|
9
|
-
type SpanStatusInput,
|
|
10
|
-
type Traces,
|
|
11
|
-
} from "@rivetkit/traces";
|
|
12
|
-
import invariant from "invariant";
|
|
13
|
-
import { ActorMetrics, type StartupTimingKey } from "@/actor/metrics";
|
|
14
|
-
import type { Client } from "@/client/client";
|
|
15
|
-
import type { ActorKey } from "@/client/query";
|
|
16
|
-
import { getBaseLogger, getIncludeTarget, type Logger } from "@/common/log";
|
|
17
|
-
import { stringifyError } from "@/common/utils";
|
|
18
|
-
import type { UniversalWebSocket } from "@/common/websocket-interface";
|
|
19
|
-
import { ActorInspector } from "@/inspector/actor-inspector";
|
|
20
|
-
import type { Registry } from "@/mod";
|
|
21
|
-
import {
|
|
22
|
-
ACTOR_VERSIONED,
|
|
23
|
-
CONN_VERSIONED,
|
|
24
|
-
} from "@/schemas/actor-persist/versioned";
|
|
25
|
-
import { EXTRA_ERROR_LOG, promiseWithResolvers } from "@/utils";
|
|
26
|
-
import { getRivetExperimentalOtel } from "@/utils/env-vars";
|
|
27
|
-
import {
|
|
28
|
-
type Actions,
|
|
29
|
-
type ActorConfig,
|
|
30
|
-
type ActorConfigInput,
|
|
31
|
-
ActorConfigSchema,
|
|
32
|
-
getRunFunction,
|
|
33
|
-
} from "../config";
|
|
34
|
-
import {
|
|
35
|
-
handleInboundHibernatableWebSocketMessage as applyInboundHibernatableWebSocketMessage,
|
|
36
|
-
HibernatableWebSocketAckState,
|
|
37
|
-
} from "../conn/hibernatable-websocket-ack-state";
|
|
38
|
-
import {
|
|
39
|
-
type AnyConn,
|
|
40
|
-
CONN_STATE_MANAGER_SYMBOL,
|
|
41
|
-
type Conn,
|
|
42
|
-
type ConnId,
|
|
43
|
-
} from "../conn/mod";
|
|
44
|
-
import {
|
|
45
|
-
convertConnFromBarePersistedConn,
|
|
46
|
-
type PersistedConn,
|
|
47
|
-
} from "../conn/persisted";
|
|
48
|
-
import {
|
|
49
|
-
type ActionContext,
|
|
50
|
-
ActorContext,
|
|
51
|
-
RequestContext,
|
|
52
|
-
WebSocketContext,
|
|
53
|
-
} from "../contexts";
|
|
54
|
-
|
|
55
|
-
import type { AnyDatabaseProvider, InferDatabaseClient } from "../database";
|
|
56
|
-
import { ActorDefinition } from "../definition";
|
|
57
|
-
import type { ActorDriver } from "../driver";
|
|
58
|
-
import * as errors from "../errors";
|
|
59
|
-
import { serializeActorKey } from "../keys";
|
|
60
|
-
import { getValueLength, processMessage } from "../protocol/old";
|
|
61
|
-
import type { InputData } from "../protocol/serde";
|
|
62
|
-
import { Schedule } from "../schedule";
|
|
63
|
-
import {
|
|
64
|
-
type EventSchemaConfig,
|
|
65
|
-
getEventCanSubscribe,
|
|
66
|
-
getQueueCanPublish,
|
|
67
|
-
type QueueSchemaConfig,
|
|
68
|
-
} from "../schema";
|
|
69
|
-
import {
|
|
70
|
-
assertUnreachable,
|
|
71
|
-
DeadlineError,
|
|
72
|
-
deadline,
|
|
73
|
-
generateSecureToken,
|
|
74
|
-
} from "../utils";
|
|
75
|
-
import { ConnectionManager } from "./connection-manager";
|
|
76
|
-
import { EventManager } from "./event-manager";
|
|
77
|
-
import { KEYS, workflowStoragePrefix } from "./keys";
|
|
78
|
-
import {
|
|
79
|
-
convertActorFromBarePersisted,
|
|
80
|
-
type PersistedActor,
|
|
81
|
-
} from "./persisted";
|
|
82
|
-
import type { PreloadedEntries, PreloadMap } from "./preload-map";
|
|
83
|
-
import { QueueManager } from "./queue-manager";
|
|
84
|
-
import { ScheduleManager } from "./schedule-manager";
|
|
85
|
-
import { type SaveStateOptions, StateManager } from "./state-manager";
|
|
86
|
-
import { ActorTracesDriver } from "./traces-driver";
|
|
87
|
-
import { TrackedWebSocket } from "./tracked-websocket";
|
|
88
|
-
import { WriteCollector } from "./write-collector";
|
|
89
|
-
|
|
90
|
-
export type { SaveStateOptions };
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Symbol used by subsystems (e.g., queue-manager) to access the
|
|
94
|
-
* unexpected KV round-trip warning without exposing it as a public method.
|
|
95
|
-
*/
|
|
96
|
-
export const WARN_UNEXPECTED_KV_ROUND_TRIP = Symbol(
|
|
97
|
-
"warnUnexpectedKvRoundTrip",
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
export function actor<
|
|
101
|
-
TState,
|
|
102
|
-
TConnParams,
|
|
103
|
-
TConnState,
|
|
104
|
-
TVars,
|
|
105
|
-
TInput,
|
|
106
|
-
TDatabase extends AnyDatabaseProvider,
|
|
107
|
-
TEvents extends EventSchemaConfig = Record<never, never>,
|
|
108
|
-
TQueues extends QueueSchemaConfig = Record<never, never>,
|
|
109
|
-
TActions extends Actions<
|
|
110
|
-
TState,
|
|
111
|
-
TConnParams,
|
|
112
|
-
TConnState,
|
|
113
|
-
TVars,
|
|
114
|
-
TInput,
|
|
115
|
-
TDatabase,
|
|
116
|
-
TEvents,
|
|
117
|
-
TQueues
|
|
118
|
-
> = Actions<
|
|
119
|
-
TState,
|
|
120
|
-
TConnParams,
|
|
121
|
-
TConnState,
|
|
122
|
-
TVars,
|
|
123
|
-
TInput,
|
|
124
|
-
TDatabase,
|
|
125
|
-
TEvents,
|
|
126
|
-
TQueues
|
|
127
|
-
>,
|
|
128
|
-
>(
|
|
129
|
-
input: ActorConfigInput<
|
|
130
|
-
TState,
|
|
131
|
-
TConnParams,
|
|
132
|
-
TConnState,
|
|
133
|
-
TVars,
|
|
134
|
-
TInput,
|
|
135
|
-
TDatabase,
|
|
136
|
-
TEvents,
|
|
137
|
-
TQueues,
|
|
138
|
-
TActions
|
|
139
|
-
>,
|
|
140
|
-
): ActorDefinition<
|
|
141
|
-
TState,
|
|
142
|
-
TConnParams,
|
|
143
|
-
TConnState,
|
|
144
|
-
TVars,
|
|
145
|
-
TInput,
|
|
146
|
-
TDatabase,
|
|
147
|
-
TEvents,
|
|
148
|
-
TQueues,
|
|
149
|
-
TActions
|
|
150
|
-
> {
|
|
151
|
-
const config = ActorConfigSchema.parse(input) as ActorConfig<
|
|
152
|
-
TState,
|
|
153
|
-
TConnParams,
|
|
154
|
-
TConnState,
|
|
155
|
-
TVars,
|
|
156
|
-
TInput,
|
|
157
|
-
TDatabase,
|
|
158
|
-
TEvents,
|
|
159
|
-
TQueues
|
|
160
|
-
>;
|
|
161
|
-
return new ActorDefinition(config);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
enum CanSleep {
|
|
165
|
-
Yes,
|
|
166
|
-
NotReady,
|
|
167
|
-
NotStarted,
|
|
168
|
-
PreventSleep,
|
|
169
|
-
ActiveConns,
|
|
170
|
-
ActiveDisconnectCallbacks,
|
|
171
|
-
ActiveHonoHttpRequests,
|
|
172
|
-
ActiveKeepAwake,
|
|
173
|
-
ActiveInternalKeepAwake,
|
|
174
|
-
ActiveRun,
|
|
175
|
-
ActiveWebSocketCallbacks,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Names of actor-managed async regions that should keep the actor awake while
|
|
180
|
-
* work is still running.
|
|
181
|
-
*/
|
|
182
|
-
interface ActiveAsyncRegionCounts {
|
|
183
|
-
keepAwake: number;
|
|
184
|
-
internalKeepAwake: number;
|
|
185
|
-
websocketCallbacks: number;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Error messages for the async-region counters. These are used when a counter
|
|
190
|
-
* underflows, which indicates mismatched begin/end bookkeeping.
|
|
191
|
-
*/
|
|
192
|
-
const ACTIVE_ASYNC_REGION_ERROR_MESSAGES: Record<
|
|
193
|
-
keyof ActiveAsyncRegionCounts,
|
|
194
|
-
string
|
|
195
|
-
> = {
|
|
196
|
-
keepAwake: "active keep awake count went below 0, this is a RivetKit bug",
|
|
197
|
-
internalKeepAwake:
|
|
198
|
-
"active internal keep awake count went below 0, this is a RivetKit bug",
|
|
199
|
-
websocketCallbacks:
|
|
200
|
-
"active websocket callback count went below 0, this is a RivetKit bug",
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Minimal lifecycle contract shared by static and dynamic actor instances.
|
|
205
|
-
*
|
|
206
|
-
* Runtime internals (connections, inspector, queue manager, etc) are exposed
|
|
207
|
-
* only on `ActorInstance`.
|
|
208
|
-
*/
|
|
209
|
-
export interface BaseActorInstance<
|
|
210
|
-
_S = any,
|
|
211
|
-
_CP = any,
|
|
212
|
-
_CS = any,
|
|
213
|
-
_V = any,
|
|
214
|
-
_I = any,
|
|
215
|
-
_DB extends AnyDatabaseProvider = AnyDatabaseProvider,
|
|
216
|
-
_E extends EventSchemaConfig = Record<never, never>,
|
|
217
|
-
_Q extends QueueSchemaConfig = Record<never, never>,
|
|
218
|
-
> {
|
|
219
|
-
readonly id: string;
|
|
220
|
-
readonly isStopping: boolean;
|
|
221
|
-
onStop(mode: "sleep" | "destroy"): Promise<void>;
|
|
222
|
-
onAlarm(): Promise<void>;
|
|
223
|
-
cleanupPersistedConnections?(reason?: string): Promise<number>;
|
|
224
|
-
getHibernatingWebSocketMetadata?(): Array<{
|
|
225
|
-
gatewayId: ArrayBuffer;
|
|
226
|
-
requestId: ArrayBuffer;
|
|
227
|
-
serverMessageIndex: number;
|
|
228
|
-
clientMessageIndex: number;
|
|
229
|
-
path: string;
|
|
230
|
-
headers: Record<string, string>;
|
|
231
|
-
}>;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** Actor type alias with all `any` types. */
|
|
235
|
-
export type AnyActorInstance = BaseActorInstance<
|
|
236
|
-
any,
|
|
237
|
-
any,
|
|
238
|
-
any,
|
|
239
|
-
any,
|
|
240
|
-
any,
|
|
241
|
-
any,
|
|
242
|
-
any,
|
|
243
|
-
any
|
|
244
|
-
>;
|
|
245
|
-
|
|
246
|
-
/** Static actor type alias with all `any` types. */
|
|
247
|
-
export type AnyStaticActorInstance = ActorInstance<
|
|
248
|
-
any,
|
|
249
|
-
any,
|
|
250
|
-
any,
|
|
251
|
-
any,
|
|
252
|
-
any,
|
|
253
|
-
any,
|
|
254
|
-
any,
|
|
255
|
-
any
|
|
256
|
-
>;
|
|
257
|
-
|
|
258
|
-
export function isStaticActorInstance(
|
|
259
|
-
actor: AnyActorInstance,
|
|
260
|
-
): actor is AnyStaticActorInstance {
|
|
261
|
-
if (actor instanceof ActorInstance) {
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (!actor || typeof actor !== "object") {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const candidate = actor as Partial<AnyStaticActorInstance>;
|
|
270
|
-
return (
|
|
271
|
-
typeof candidate.executeAction === "function" &&
|
|
272
|
-
typeof candidate.beginHonoHttpRequest === "function" &&
|
|
273
|
-
typeof candidate.endHonoHttpRequest === "function" &&
|
|
274
|
-
typeof candidate.connectionManager === "object" &&
|
|
275
|
-
candidate.connectionManager !== null
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export type ExtractActorState<A extends AnyActorInstance> =
|
|
280
|
-
A extends ActorInstance<infer State, any, any, any, any, any, any, any>
|
|
281
|
-
? State
|
|
282
|
-
: never;
|
|
283
|
-
|
|
284
|
-
export type ExtractActorConnParams<A extends AnyActorInstance> =
|
|
285
|
-
A extends ActorInstance<any, infer ConnParams, any, any, any, any, any, any>
|
|
286
|
-
? ConnParams
|
|
287
|
-
: never;
|
|
288
|
-
|
|
289
|
-
export type ExtractActorConnState<A extends AnyActorInstance> =
|
|
290
|
-
A extends ActorInstance<any, any, infer ConnState, any, any, any, any, any>
|
|
291
|
-
? ConnState
|
|
292
|
-
: never;
|
|
293
|
-
|
|
294
|
-
// MARK: - Main ActorInstance Class
|
|
295
|
-
export class ActorInstance<
|
|
296
|
-
S,
|
|
297
|
-
CP,
|
|
298
|
-
CS,
|
|
299
|
-
V,
|
|
300
|
-
I,
|
|
301
|
-
DB extends AnyDatabaseProvider,
|
|
302
|
-
E extends EventSchemaConfig = Record<never, never>,
|
|
303
|
-
Q extends QueueSchemaConfig = Record<never, never>,
|
|
304
|
-
> implements BaseActorInstance<S, CP, CS, V, I, DB, E, Q>
|
|
305
|
-
{
|
|
306
|
-
// MARK: - Core Properties
|
|
307
|
-
actorContext: ActorContext<S, CP, CS, V, I, DB, E, Q>;
|
|
308
|
-
#config: ActorConfig<S, CP, CS, V, I, DB, E, Q>;
|
|
309
|
-
driver!: ActorDriver;
|
|
310
|
-
#inlineClient!: Client<Registry<any>>;
|
|
311
|
-
#actorId!: string;
|
|
312
|
-
#name!: string;
|
|
313
|
-
#key!: ActorKey;
|
|
314
|
-
#actorKeyString!: string;
|
|
315
|
-
#region!: string;
|
|
316
|
-
|
|
317
|
-
// MARK: - Managers
|
|
318
|
-
connectionManager!: ConnectionManager<S, CP, CS, V, I, DB, E, Q>;
|
|
319
|
-
|
|
320
|
-
stateManager!: StateManager<S, CP, CS, I, E, Q>;
|
|
321
|
-
|
|
322
|
-
eventManager!: EventManager<S, CP, CS, V, I, DB, E, Q>;
|
|
323
|
-
|
|
324
|
-
#scheduleManager!: ScheduleManager<S, CP, CS, V, I, DB, E, Q>;
|
|
325
|
-
|
|
326
|
-
queueManager!: QueueManager<S, CP, CS, V, I, DB, E, Q>;
|
|
327
|
-
|
|
328
|
-
// MARK: - Logging
|
|
329
|
-
#log!: Logger;
|
|
330
|
-
#rLog!: Logger;
|
|
331
|
-
|
|
332
|
-
// MARK: - Lifecycle State
|
|
333
|
-
/**
|
|
334
|
-
* If the core actor initiation has set up.
|
|
335
|
-
*
|
|
336
|
-
* Almost all actions on this actor will throw an error if false.
|
|
337
|
-
**/
|
|
338
|
-
#ready = false;
|
|
339
|
-
/**
|
|
340
|
-
* If the actor has fully started.
|
|
341
|
-
*
|
|
342
|
-
* The only purpose of this is to prevent sleeping until started.
|
|
343
|
-
*/
|
|
344
|
-
#started = false;
|
|
345
|
-
#sleepCalled = false;
|
|
346
|
-
#destroyCalled = false;
|
|
347
|
-
#stopCalled = false;
|
|
348
|
-
#shutdownComplete = false;
|
|
349
|
-
#sleepTimeout?: NodeJS.Timeout;
|
|
350
|
-
#abortController = new AbortController();
|
|
351
|
-
|
|
352
|
-
// MARK: - Variables & Database
|
|
353
|
-
#vars?: V;
|
|
354
|
-
#db?: InferDatabaseClient<DB>;
|
|
355
|
-
#metrics = new ActorMetrics();
|
|
356
|
-
|
|
357
|
-
// MARK: - Preload
|
|
358
|
-
#workflowPreloadEntries?: PreloadedEntries;
|
|
359
|
-
#expectNoKvRoundTrips = false;
|
|
360
|
-
|
|
361
|
-
// MARK: - Background Tasks
|
|
362
|
-
#backgroundPromises: Promise<void>[] = [];
|
|
363
|
-
#websocketCallbackPromises: Promise<void>[] = [];
|
|
364
|
-
#preventSleepClearedPromise?: ReturnType<typeof promiseWithResolvers<void>>;
|
|
365
|
-
#runPromise?: Promise<void>;
|
|
366
|
-
#runHandlerActive = false;
|
|
367
|
-
#activeQueueWaitCount = 0;
|
|
368
|
-
|
|
369
|
-
// MARK: - HTTP/WebSocket Tracking
|
|
370
|
-
#activeHonoHttpRequests = 0;
|
|
371
|
-
#activeAsyncRegionCounts: ActiveAsyncRegionCounts = {
|
|
372
|
-
keepAwake: 0,
|
|
373
|
-
internalKeepAwake: 0,
|
|
374
|
-
websocketCallbacks: 0,
|
|
375
|
-
};
|
|
376
|
-
#preventSleep = false;
|
|
377
|
-
|
|
378
|
-
// MARK: - Deprecated (kept for compatibility)
|
|
379
|
-
#schedule!: Schedule;
|
|
380
|
-
|
|
381
|
-
// MARK: - Hibernatable WebSocket State
|
|
382
|
-
#hibernatableWebSocketAckState = new HibernatableWebSocketAckState();
|
|
383
|
-
|
|
384
|
-
// MARK: - Inspector
|
|
385
|
-
#inspectorToken?: string;
|
|
386
|
-
#inspector: ActorInspector;
|
|
387
|
-
|
|
388
|
-
// MARK: - Tracing
|
|
389
|
-
#traces!: Traces<OtlpExportTraceServiceRequestJson>;
|
|
390
|
-
|
|
391
|
-
// MARK: - Driver Overrides
|
|
392
|
-
/**
|
|
393
|
-
* Per-instance config option overrides applied by the driver after creation.
|
|
394
|
-
* When set, the effective option value is the minimum of the base config
|
|
395
|
-
* value and the override value.
|
|
396
|
-
*/
|
|
397
|
-
overrides: {
|
|
398
|
-
sleepGracePeriod?: number;
|
|
399
|
-
} = {};
|
|
400
|
-
|
|
401
|
-
// MARK: - Constructor
|
|
402
|
-
constructor(config: ActorConfig<S, CP, CS, V, I, DB, E, Q>) {
|
|
403
|
-
this.#config = config;
|
|
404
|
-
this.actorContext = new ActorContext(this);
|
|
405
|
-
this.#inspector = new ActorInspector(this);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// MARK: - Public Getters
|
|
409
|
-
get log(): Logger {
|
|
410
|
-
invariant(this.#log, "log not configured");
|
|
411
|
-
return this.#log;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
get rLog(): Logger {
|
|
415
|
-
invariant(this.#rLog, "log not configured");
|
|
416
|
-
return this.#rLog;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
get isStopping(): boolean {
|
|
420
|
-
return this.#stopCalled;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
get id(): string {
|
|
424
|
-
return this.#actorId;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
get name(): string {
|
|
428
|
-
return this.#name;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
get key(): ActorKey {
|
|
432
|
-
return this.#key;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
get region(): string {
|
|
436
|
-
return this.#region;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
get inlineClient(): Client<Registry<any>> {
|
|
440
|
-
return this.#inlineClient;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
get inspector(): ActorInspector {
|
|
444
|
-
return this.#inspector;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
get traces(): Traces<OtlpExportTraceServiceRequestJson> {
|
|
448
|
-
return this.#traces;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
get inspectorToken(): string | undefined {
|
|
452
|
-
return this.#inspectorToken;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
get metrics(): ActorMetrics {
|
|
456
|
-
return this.#metrics;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// MARK: - Tracing
|
|
460
|
-
getCurrentTraceSpan(): SpanHandle | null {
|
|
461
|
-
return this.#traces.getCurrentSpan();
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
startTraceSpan(
|
|
465
|
-
name: string,
|
|
466
|
-
attributes?: Record<string, unknown>,
|
|
467
|
-
): SpanHandle {
|
|
468
|
-
return this.#traces.startSpan(name, {
|
|
469
|
-
parent: this.#traces.getCurrentSpan() ?? undefined,
|
|
470
|
-
attributes: this.#traceAttributes(attributes),
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
endTraceSpan(handle: SpanHandle, status?: SpanStatusInput): void {
|
|
475
|
-
this.#traces.endSpan(handle, status ? { status } : undefined);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
async runInTraceSpan<T>(
|
|
479
|
-
name: string,
|
|
480
|
-
attributes: Record<string, unknown> | undefined,
|
|
481
|
-
fn: () => T | Promise<T>,
|
|
482
|
-
): Promise<T> {
|
|
483
|
-
const span = this.startTraceSpan(name, attributes);
|
|
484
|
-
try {
|
|
485
|
-
const result = this.#traces.withSpan(span, fn);
|
|
486
|
-
const resolved = result instanceof Promise ? await result : result;
|
|
487
|
-
this.#traces.endSpan(span, {
|
|
488
|
-
status: { code: "OK" },
|
|
489
|
-
});
|
|
490
|
-
return resolved;
|
|
491
|
-
} catch (error) {
|
|
492
|
-
this.#traces.endSpan(span, {
|
|
493
|
-
status: {
|
|
494
|
-
code: "ERROR",
|
|
495
|
-
message: stringifyError(error),
|
|
496
|
-
},
|
|
497
|
-
});
|
|
498
|
-
throw error;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
emitTraceEvent(
|
|
503
|
-
name: string,
|
|
504
|
-
attributes?: Record<string, unknown>,
|
|
505
|
-
handle?: SpanHandle,
|
|
506
|
-
): void {
|
|
507
|
-
const span = handle ?? this.#traces.getCurrentSpan();
|
|
508
|
-
if (!span) {
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
this.#traces.emitEvent(span, name, {
|
|
512
|
-
attributes: this.#traceAttributes(attributes),
|
|
513
|
-
timeUnixMs: Date.now(),
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
get workflowPreloadEntries(): PreloadedEntries | undefined {
|
|
518
|
-
return this.#workflowPreloadEntries;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
[WARN_UNEXPECTED_KV_ROUND_TRIP](method: string): void {
|
|
522
|
-
if (this.#expectNoKvRoundTrips) {
|
|
523
|
-
this.#rLog.warn({
|
|
524
|
-
msg: "unexpected KV round-trip during startup",
|
|
525
|
-
method,
|
|
526
|
-
});
|
|
527
|
-
this.#expectNoKvRoundTrips = false;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
static #userStartupKeys: Set<StartupTimingKey> = new Set([
|
|
532
|
-
"createStateMs",
|
|
533
|
-
"onCreateMs",
|
|
534
|
-
"onWakeMs",
|
|
535
|
-
"createVarsMs",
|
|
536
|
-
"dbMigrateMs",
|
|
537
|
-
]);
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Measure the duration of an async startup step. Logs at debug level
|
|
541
|
-
* and records the duration on the startup metrics object.
|
|
542
|
-
*
|
|
543
|
-
* When `pauseKvGuard` is true, the unexpected KV round-trip guard is
|
|
544
|
-
* suspended for the duration of the callback (used for user code
|
|
545
|
-
* callbacks that may legitimately issue KV reads).
|
|
546
|
-
*/
|
|
547
|
-
async #measureStartup<T>(
|
|
548
|
-
name: StartupTimingKey,
|
|
549
|
-
fn: () => Promise<T> | T,
|
|
550
|
-
opts?: { pauseKvGuard?: boolean },
|
|
551
|
-
): Promise<T> {
|
|
552
|
-
const savedGuard = this.#expectNoKvRoundTrips;
|
|
553
|
-
if (opts?.pauseKvGuard) {
|
|
554
|
-
this.#expectNoKvRoundTrips = false;
|
|
555
|
-
}
|
|
556
|
-
const start = performance.now();
|
|
557
|
-
try {
|
|
558
|
-
const result = await fn();
|
|
559
|
-
return result;
|
|
560
|
-
} finally {
|
|
561
|
-
const durationMs = performance.now() - start;
|
|
562
|
-
this.#metrics.startup[name] = durationMs;
|
|
563
|
-
const prefix = ActorInstance.#userStartupKeys.has(name)
|
|
564
|
-
? "perf user"
|
|
565
|
-
: "perf internal";
|
|
566
|
-
this.#rLog.debug({ msg: `${prefix}: ${name}`, durationMs });
|
|
567
|
-
if (opts?.pauseKvGuard) {
|
|
568
|
-
this.#expectNoKvRoundTrips = savedGuard;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
get conns(): Map<ConnId, Conn<S, CP, CS, V, I, DB, E, Q>> {
|
|
574
|
-
return this.connectionManager.connections;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Records delivery of an inbound indexed hibernatable websocket message and
|
|
579
|
-
* schedules persistence so the index is only acked after a durable write.
|
|
580
|
-
*/
|
|
581
|
-
handleInboundHibernatableWebSocketMessage(
|
|
582
|
-
conn: AnyConn | undefined,
|
|
583
|
-
payload: InputData,
|
|
584
|
-
rivetMessageIndex: number | undefined,
|
|
585
|
-
): void {
|
|
586
|
-
if (!conn?.isHibernatable) {
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const connStateManager = conn[CONN_STATE_MANAGER_SYMBOL];
|
|
591
|
-
const hibernatable = connStateManager.hibernatableData;
|
|
592
|
-
if (!hibernatable) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
invariant(
|
|
597
|
-
typeof rivetMessageIndex === "number",
|
|
598
|
-
"missing rivetMessageIndex for hibernatable websocket message",
|
|
599
|
-
);
|
|
600
|
-
|
|
601
|
-
applyInboundHibernatableWebSocketMessage({
|
|
602
|
-
connId: conn.id,
|
|
603
|
-
hibernatable,
|
|
604
|
-
messageLength: getValueLength(payload),
|
|
605
|
-
rivetMessageIndex,
|
|
606
|
-
ackState: this.#hibernatableWebSocketAckState,
|
|
607
|
-
saveState: (opts) => {
|
|
608
|
-
void this.stateManager.saveState(opts).catch((error) => {
|
|
609
|
-
this.#rLog.error({
|
|
610
|
-
msg: "failed to schedule hibernatable websocket persistence",
|
|
611
|
-
connId: conn.id,
|
|
612
|
-
error: stringifyError(error),
|
|
613
|
-
});
|
|
614
|
-
});
|
|
615
|
-
},
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
onCreateHibernatableConn(conn: AnyConn): void {
|
|
620
|
-
const hibernatable = conn[CONN_STATE_MANAGER_SYMBOL].hibernatableData;
|
|
621
|
-
if (!hibernatable) {
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
this.#hibernatableWebSocketAckState.createConnEntry(
|
|
626
|
-
conn.id,
|
|
627
|
-
hibernatable.serverMessageIndex,
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
onDestroyHibernatableConn(conn: AnyConn): void {
|
|
632
|
-
this.#hibernatableWebSocketAckState.deleteConnEntry(conn.id);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
onBeforePersistHibernatableConn(conn: AnyConn): void {
|
|
636
|
-
const hibernatable =
|
|
637
|
-
conn[CONN_STATE_MANAGER_SYMBOL].hibernatableDataOrError();
|
|
638
|
-
this.#hibernatableWebSocketAckState.onBeforePersist(
|
|
639
|
-
conn.id,
|
|
640
|
-
hibernatable.serverMessageIndex,
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
onAfterPersistHibernatableConn(conn: AnyConn): void {
|
|
645
|
-
const hibernatable =
|
|
646
|
-
conn[CONN_STATE_MANAGER_SYMBOL].hibernatableDataOrError();
|
|
647
|
-
const ackServerMessageIndex =
|
|
648
|
-
this.#hibernatableWebSocketAckState.consumeAck(conn.id);
|
|
649
|
-
if (ackServerMessageIndex === undefined) {
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
this.driver.ackHibernatableWebSocketMessage?.(
|
|
654
|
-
hibernatable.gatewayId,
|
|
655
|
-
hibernatable.requestId,
|
|
656
|
-
ackServerMessageIndex,
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
getHibernatingWebSocketMetadata(): Array<{
|
|
661
|
-
gatewayId: ArrayBuffer;
|
|
662
|
-
requestId: ArrayBuffer;
|
|
663
|
-
serverMessageIndex: number;
|
|
664
|
-
clientMessageIndex: number;
|
|
665
|
-
path: string;
|
|
666
|
-
headers: Record<string, string>;
|
|
667
|
-
}> {
|
|
668
|
-
return Array.from(this.conns.values(), (conn) => {
|
|
669
|
-
const hibernatable =
|
|
670
|
-
conn[CONN_STATE_MANAGER_SYMBOL].hibernatableData;
|
|
671
|
-
if (!hibernatable) {
|
|
672
|
-
return undefined;
|
|
673
|
-
}
|
|
674
|
-
return {
|
|
675
|
-
gatewayId: hibernatable.gatewayId.slice(0),
|
|
676
|
-
requestId: hibernatable.requestId.slice(0),
|
|
677
|
-
serverMessageIndex: hibernatable.serverMessageIndex,
|
|
678
|
-
clientMessageIndex: hibernatable.clientMessageIndex,
|
|
679
|
-
path: hibernatable.requestPath,
|
|
680
|
-
headers: { ...hibernatable.requestHeaders },
|
|
681
|
-
};
|
|
682
|
-
}).filter((entry) => entry !== undefined);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
get schedule(): Schedule {
|
|
686
|
-
return this.#schedule;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
get abortSignal(): AbortSignal {
|
|
690
|
-
return this.#abortController.signal;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
get preventSleep(): boolean {
|
|
694
|
-
return this.#preventSleep;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
get actions(): string[] {
|
|
698
|
-
return Object.keys(this.#config.actions ?? {});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
get config(): ActorConfig<S, CP, CS, V, I, DB, E, Q> {
|
|
702
|
-
return this.#config;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// MARK: - State Access
|
|
706
|
-
get persist(): PersistedActor<S, I> {
|
|
707
|
-
return this.stateManager.persist;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
get state(): S {
|
|
711
|
-
return this.stateManager.state;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
set state(value: S) {
|
|
715
|
-
this.stateManager.state = value;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
get stateEnabled(): boolean {
|
|
719
|
-
return this.stateManager.stateEnabled;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
get connStateEnabled(): boolean {
|
|
723
|
-
return "createConnState" in this.#config || "connState" in this.#config;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// MARK: - Variables & Database
|
|
727
|
-
get vars(): V {
|
|
728
|
-
this.#validateVarsEnabled();
|
|
729
|
-
invariant(this.#vars !== undefined, "vars not enabled");
|
|
730
|
-
return this.#vars;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
get db(): InferDatabaseClient<DB> {
|
|
734
|
-
if (!this.#db) {
|
|
735
|
-
if (this.#shutdownComplete && "db" in this.#config) {
|
|
736
|
-
throw new errors.ActorStopping(
|
|
737
|
-
"database accessed after actor stopped. If you are using setInterval or other background timers, clean them up with c.abortSignal.",
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
throw new errors.DatabaseNotEnabled();
|
|
741
|
-
}
|
|
742
|
-
return this.#db;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// MARK: - Initialization
|
|
746
|
-
async start(
|
|
747
|
-
actorDriver: ActorDriver,
|
|
748
|
-
inlineClient: Client<Registry<any>>,
|
|
749
|
-
actorId: string,
|
|
750
|
-
name: string,
|
|
751
|
-
key: ActorKey,
|
|
752
|
-
region: string,
|
|
753
|
-
preload?: PreloadMap,
|
|
754
|
-
) {
|
|
755
|
-
const startupStart = performance.now();
|
|
756
|
-
|
|
757
|
-
// Initialize properties
|
|
758
|
-
this.driver = actorDriver;
|
|
759
|
-
this.#inlineClient = inlineClient;
|
|
760
|
-
this.#actorId = actorId;
|
|
761
|
-
this.#name = name;
|
|
762
|
-
this.#key = key;
|
|
763
|
-
this.#actorKeyString = serializeActorKey(this.#key);
|
|
764
|
-
this.#region = region;
|
|
765
|
-
|
|
766
|
-
// Initialize tracing
|
|
767
|
-
this.#initializeTraces();
|
|
768
|
-
|
|
769
|
-
// Initialize logging
|
|
770
|
-
this.#initializeLogging();
|
|
771
|
-
|
|
772
|
-
// Initialize managers
|
|
773
|
-
this.connectionManager = new ConnectionManager(this);
|
|
774
|
-
this.stateManager = new StateManager(this, actorDriver, this.#config);
|
|
775
|
-
this.eventManager = new EventManager(this);
|
|
776
|
-
this.queueManager = new QueueManager(this, actorDriver);
|
|
777
|
-
this.#scheduleManager = new ScheduleManager(
|
|
778
|
-
this,
|
|
779
|
-
actorDriver,
|
|
780
|
-
this.#config,
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
// Legacy schedule object (for compatibility)
|
|
784
|
-
this.#schedule = new Schedule(this);
|
|
785
|
-
|
|
786
|
-
// Enable unexpected KV round-trip detection when preload data was
|
|
787
|
-
// provided.
|
|
788
|
-
if (preload) {
|
|
789
|
-
this.#expectNoKvRoundTrips = true;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Extract workflow preload data for lazy consumption by workflow engine.
|
|
793
|
-
if (preload) {
|
|
794
|
-
const workflowEntries = preload.listPrefix(workflowStoragePrefix());
|
|
795
|
-
if (workflowEntries !== undefined) {
|
|
796
|
-
this.#workflowPreloadEntries = workflowEntries;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Setup database before lifecycle hooks so c.db is available in
|
|
801
|
-
// createState, onCreate, createVars, and onWake.
|
|
802
|
-
await this.#setupDatabase(preload);
|
|
803
|
-
|
|
804
|
-
// Create a write collector to batch new-actor init writes into a
|
|
805
|
-
// single kvBatchPut.
|
|
806
|
-
const writeCollector = new WriteCollector(actorDriver, actorId);
|
|
807
|
-
|
|
808
|
-
// Load state
|
|
809
|
-
await this.#measureStartup("loadStateMs", () =>
|
|
810
|
-
this.#loadState(preload, writeCollector),
|
|
811
|
-
);
|
|
812
|
-
|
|
813
|
-
await this.#measureStartup("initQueueMs", () =>
|
|
814
|
-
this.queueManager.initialize(preload, writeCollector),
|
|
815
|
-
);
|
|
816
|
-
|
|
817
|
-
await this.#measureStartup("initInspectorTokenMs", () =>
|
|
818
|
-
this.#initializeInspectorToken(preload, writeCollector),
|
|
819
|
-
);
|
|
820
|
-
|
|
821
|
-
// Flush any batched writes from new actor initialization.
|
|
822
|
-
await this.#measureStartup("flushWritesMs", async () => {
|
|
823
|
-
this.#metrics.startup.flushWritesEntries = writeCollector.size;
|
|
824
|
-
await writeCollector.flush();
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
// Initialize variables.
|
|
828
|
-
await this.#measureStartup(
|
|
829
|
-
"createVarsMs",
|
|
830
|
-
async () => {
|
|
831
|
-
if (this.#varsEnabled) {
|
|
832
|
-
await this.#initializeVars();
|
|
833
|
-
}
|
|
834
|
-
},
|
|
835
|
-
{ pauseKvGuard: true },
|
|
836
|
-
);
|
|
837
|
-
|
|
838
|
-
// Call onStart lifecycle.
|
|
839
|
-
await this.#measureStartup("onWakeMs", () => this.#callOnStart(), {
|
|
840
|
-
pauseKvGuard: true,
|
|
841
|
-
});
|
|
842
|
-
// Initialize alarms
|
|
843
|
-
await this.#measureStartup("initAlarmsMs", () =>
|
|
844
|
-
this.#scheduleManager.initializeAlarms(),
|
|
845
|
-
);
|
|
846
|
-
|
|
847
|
-
// Mark as ready
|
|
848
|
-
this.#ready = true;
|
|
849
|
-
|
|
850
|
-
// Finish up any remaining initiation
|
|
851
|
-
//
|
|
852
|
-
// Do this after #ready = true since this can call any actor callbacks
|
|
853
|
-
// (which require #assertReady)
|
|
854
|
-
await this.#measureStartup("onBeforeActorStartMs", async () => {
|
|
855
|
-
await this.driver.onBeforeActorStart?.(this);
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
// Mark as started
|
|
859
|
-
//
|
|
860
|
-
// We do this after onBeforeActorStart to prevent the actor from going
|
|
861
|
-
// to sleep before finishing setup
|
|
862
|
-
this.#started = true;
|
|
863
|
-
|
|
864
|
-
// Clear KV round-trip detection after startup completes.
|
|
865
|
-
this.#expectNoKvRoundTrips = false;
|
|
866
|
-
|
|
867
|
-
// Release workflow preload data after startup completes.
|
|
868
|
-
this.#workflowPreloadEntries = undefined;
|
|
869
|
-
|
|
870
|
-
// Record total startup time.
|
|
871
|
-
this.#metrics.startup.totalMs = performance.now() - startupStart;
|
|
872
|
-
this.#rLog.info({
|
|
873
|
-
msg: "actor started",
|
|
874
|
-
startupMs: this.#metrics.startup.totalMs,
|
|
875
|
-
kvRoundTrips: this.#metrics.startup.kvRoundTrips,
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
// Start sleep timer after setting #started since this affects the
|
|
879
|
-
// timer
|
|
880
|
-
this.resetSleepTimer();
|
|
881
|
-
|
|
882
|
-
// Start run handler in background (does not block startup)
|
|
883
|
-
this.#startRunHandler();
|
|
884
|
-
|
|
885
|
-
// Trigger any pending alarms
|
|
886
|
-
await this.onAlarm();
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// MARK: - Ready Check
|
|
890
|
-
isReady(): boolean {
|
|
891
|
-
return this.#ready;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
assertReady() {
|
|
895
|
-
if (!this.#ready) throw new errors.InternalError("Actor not ready");
|
|
896
|
-
this.assertNotShutdown();
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
assertNotShutdown() {
|
|
900
|
-
if (this.#shutdownComplete)
|
|
901
|
-
throw new errors.ActorStopping("Actor has shut down");
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
async cleanupPersistedConnections(reason?: string): Promise<number> {
|
|
905
|
-
this.assertReady();
|
|
906
|
-
return await this.connectionManager.cleanupPersistedHibernatableConnections(
|
|
907
|
-
reason,
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
async restartRunHandler(): Promise<void> {
|
|
912
|
-
this.assertReady();
|
|
913
|
-
if (this.#stopCalled)
|
|
914
|
-
throw errors.actorRestarting({ phase: "stopping" });
|
|
915
|
-
if (this.#runHandlerActive && this.#runPromise) {
|
|
916
|
-
await this.#runPromise;
|
|
917
|
-
}
|
|
918
|
-
if (this.#runHandlerActive) {
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
this.#startRunHandler();
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
isRunHandlerActive(): boolean {
|
|
926
|
-
return this.#runHandlerActive;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// MARK: - Stop
|
|
930
|
-
async onStop(mode: "sleep" | "destroy") {
|
|
931
|
-
if (this.#stopCalled) {
|
|
932
|
-
this.#rLog.warn({ msg: "already stopping actor" });
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
this.#stopCalled = true;
|
|
936
|
-
this.#rLog.info({
|
|
937
|
-
msg: "setting stopCalled=true",
|
|
938
|
-
mode,
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
try {
|
|
942
|
-
// Clear sleep timeout
|
|
943
|
-
if (this.#sleepTimeout) {
|
|
944
|
-
clearTimeout(this.#sleepTimeout);
|
|
945
|
-
this.#sleepTimeout = undefined;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// Cancel alarm timeouts so they cannot fire during shutdown.
|
|
949
|
-
// Scheduled events are persisted and will be re-initialized
|
|
950
|
-
// on wake via initializeAlarms().
|
|
951
|
-
this.driver.cancelAlarm?.(this.#actorId);
|
|
952
|
-
this.#abortActorSignal();
|
|
953
|
-
|
|
954
|
-
// The run-handler join, lifecycle hooks, and remaining shutdown
|
|
955
|
-
// tasks all share the single sleepGracePeriod budget.
|
|
956
|
-
const shutdownTaskDeadlineTs =
|
|
957
|
-
Date.now() + this.#getEffectiveSleepGracePeriod();
|
|
958
|
-
|
|
959
|
-
await this.#waitForRunHandler(shutdownTaskDeadlineTs - Date.now());
|
|
960
|
-
|
|
961
|
-
// Call onStop lifecycle
|
|
962
|
-
if (mode === "sleep") {
|
|
963
|
-
await this.#waitForIdleSleepWindow(shutdownTaskDeadlineTs);
|
|
964
|
-
await this.#callOnSleep(shutdownTaskDeadlineTs);
|
|
965
|
-
} else if (mode === "destroy") {
|
|
966
|
-
await this.#callOnDestroy(shutdownTaskDeadlineTs);
|
|
967
|
-
} else {
|
|
968
|
-
assertUnreachable(mode);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Wait for shutdown tasks that were already in flight before
|
|
972
|
-
// connection teardown starts.
|
|
973
|
-
await this.#waitShutdownTasks(shutdownTaskDeadlineTs);
|
|
974
|
-
|
|
975
|
-
// Disconnect non-hibernatable connections
|
|
976
|
-
await this.#disconnectConnections();
|
|
977
|
-
|
|
978
|
-
// Drain async WebSocket close handlers and any waitUntil work they
|
|
979
|
-
// enqueue before persisting final state.
|
|
980
|
-
await this.#waitShutdownTasks(shutdownTaskDeadlineTs);
|
|
981
|
-
|
|
982
|
-
// Clear timeouts and save state
|
|
983
|
-
this.#rLog.info({ msg: "clearing pending save timeouts" });
|
|
984
|
-
this.stateManager.clearPendingSaveTimeout();
|
|
985
|
-
this.#rLog.info({ msg: "saving state immediately" });
|
|
986
|
-
await this.stateManager.saveState({
|
|
987
|
-
immediate: true,
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
// Wait for write queues
|
|
991
|
-
await this.stateManager.waitForPendingWrites();
|
|
992
|
-
await this.#scheduleManager.waitForPendingAlarmWrites();
|
|
993
|
-
} finally {
|
|
994
|
-
this.#shutdownComplete = true;
|
|
995
|
-
await this.#cleanupDatabase();
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
async debugForceCrash() {
|
|
1000
|
-
if (this.#shutdownComplete) {
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
if (this.#stopCalled) {
|
|
1004
|
-
this.#rLog.warn({
|
|
1005
|
-
msg: "already stopping actor during hard crash",
|
|
1006
|
-
});
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
this.#stopCalled = true;
|
|
1010
|
-
|
|
1011
|
-
try {
|
|
1012
|
-
if (this.#sleepTimeout) {
|
|
1013
|
-
clearTimeout(this.#sleepTimeout);
|
|
1014
|
-
this.#sleepTimeout = undefined;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
this.driver.cancelAlarm?.(this.#actorId);
|
|
1018
|
-
this.#abortActorSignal();
|
|
1019
|
-
this.stateManager.clearPendingSaveTimeout();
|
|
1020
|
-
} finally {
|
|
1021
|
-
this.#shutdownComplete = true;
|
|
1022
|
-
await this.#cleanupDatabase();
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// MARK: - Sleep
|
|
1027
|
-
startSleep() {
|
|
1028
|
-
if (this.#stopCalled || this.#destroyCalled) {
|
|
1029
|
-
this.#rLog.debug({
|
|
1030
|
-
msg: "cannot call startSleep if actor already stopping",
|
|
1031
|
-
});
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (this.#sleepCalled) {
|
|
1036
|
-
this.#rLog.warn({
|
|
1037
|
-
msg: "cannot call startSleep twice, actor already sleeping",
|
|
1038
|
-
});
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
this.#sleepCalled = true;
|
|
1042
|
-
|
|
1043
|
-
const sleep = this.driver.startSleep?.bind(this.driver, this.#actorId);
|
|
1044
|
-
invariant(this.#sleepingSupported, "sleeping not supported");
|
|
1045
|
-
invariant(sleep, "no sleep on driver");
|
|
1046
|
-
|
|
1047
|
-
this.#rLog.info({ msg: "actor sleeping" });
|
|
1048
|
-
|
|
1049
|
-
// Start sleep on next tick so call site of startSleep can exit
|
|
1050
|
-
setImmediate(() => {
|
|
1051
|
-
sleep();
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// MARK: - Destroy
|
|
1056
|
-
startDestroy() {
|
|
1057
|
-
if (this.#stopCalled || this.#sleepCalled) {
|
|
1058
|
-
this.#rLog.debug({
|
|
1059
|
-
msg: "cannot call startDestroy if actor already stopping or sleeping",
|
|
1060
|
-
});
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (this.#destroyCalled) {
|
|
1065
|
-
this.#rLog.warn({
|
|
1066
|
-
msg: "cannot call startDestroy twice, actor already destroying",
|
|
1067
|
-
});
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
this.#destroyCalled = true;
|
|
1071
|
-
|
|
1072
|
-
const destroy = this.driver.startDestroy.bind(
|
|
1073
|
-
this.driver,
|
|
1074
|
-
this.#actorId,
|
|
1075
|
-
);
|
|
1076
|
-
|
|
1077
|
-
this.#rLog.info({ msg: "actor destroying" });
|
|
1078
|
-
|
|
1079
|
-
// Start destroy on next tick so call site of startDestroy can exit
|
|
1080
|
-
setImmediate(() => {
|
|
1081
|
-
destroy();
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
#abortActorSignal() {
|
|
1086
|
-
if (this.#abortController.signal.aborted) {
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
try {
|
|
1090
|
-
this.#abortController.abort();
|
|
1091
|
-
} catch {}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// MARK: - HTTP Request Tracking
|
|
1095
|
-
beginHonoHttpRequest() {
|
|
1096
|
-
this.#activeHonoHttpRequests++;
|
|
1097
|
-
this.resetSleepTimer();
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
endHonoHttpRequest() {
|
|
1101
|
-
this.#activeHonoHttpRequests--;
|
|
1102
|
-
if (this.#activeHonoHttpRequests < 0) {
|
|
1103
|
-
this.#activeHonoHttpRequests = 0;
|
|
1104
|
-
this.#rLog.warn({
|
|
1105
|
-
msg: "active hono requests went below 0, this is a RivetKit bug",
|
|
1106
|
-
...EXTRA_ERROR_LOG,
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
this.resetSleepTimer();
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
// MARK: - Message Processing
|
|
1113
|
-
async processMessage(
|
|
1114
|
-
message: {
|
|
1115
|
-
body:
|
|
1116
|
-
| {
|
|
1117
|
-
tag: "ActionRequest";
|
|
1118
|
-
val: { id: bigint; name: string; args: unknown };
|
|
1119
|
-
}
|
|
1120
|
-
| {
|
|
1121
|
-
tag: "SubscriptionRequest";
|
|
1122
|
-
val: { eventName: string; subscribe: boolean };
|
|
1123
|
-
};
|
|
1124
|
-
},
|
|
1125
|
-
conn: Conn<S, CP, CS, V, I, DB, E, Q>,
|
|
1126
|
-
) {
|
|
1127
|
-
// Hibernating WebSocket connections intentionally do not keep the
|
|
1128
|
-
// actor alive so the actor can sleep while connections are idle.
|
|
1129
|
-
// Reset the sleep timer on each message so the actor stays awake
|
|
1130
|
-
// while clients are actively communicating.
|
|
1131
|
-
this.resetSleepTimer();
|
|
1132
|
-
|
|
1133
|
-
await processMessage(message, this, conn, {
|
|
1134
|
-
onExecuteAction: async (ctx, name, args) => {
|
|
1135
|
-
return await this.executeAction(ctx, name, args);
|
|
1136
|
-
},
|
|
1137
|
-
onSubscribe: async (eventName, conn) => {
|
|
1138
|
-
this.eventManager.addSubscription(eventName, conn, false);
|
|
1139
|
-
},
|
|
1140
|
-
onUnsubscribe: async (eventName, conn) => {
|
|
1141
|
-
this.eventManager.removeSubscription(eventName, conn, false);
|
|
1142
|
-
},
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
async assertCanSubscribe(
|
|
1147
|
-
ctx: ActionContext<S, CP, CS, V, I, DB, E, Q>,
|
|
1148
|
-
eventName: string,
|
|
1149
|
-
): Promise<void> {
|
|
1150
|
-
const canSubscribe = getEventCanSubscribe(
|
|
1151
|
-
this.#config.events,
|
|
1152
|
-
eventName,
|
|
1153
|
-
);
|
|
1154
|
-
if (!canSubscribe) {
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
const result = await canSubscribe(ctx);
|
|
1159
|
-
if (typeof result !== "boolean") {
|
|
1160
|
-
throw new errors.InvalidCanSubscribeResponse();
|
|
1161
|
-
}
|
|
1162
|
-
if (!result) {
|
|
1163
|
-
throw new errors.Forbidden();
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
async assertCanPublish(
|
|
1168
|
-
ctx: ActionContext<S, CP, CS, V, I, DB, E, Q>,
|
|
1169
|
-
queueName: string,
|
|
1170
|
-
): Promise<void> {
|
|
1171
|
-
const canPublish = getQueueCanPublish<
|
|
1172
|
-
ActionContext<S, CP, CS, V, I, DB, E, Q>
|
|
1173
|
-
>(this.#config.queues, queueName);
|
|
1174
|
-
if (!canPublish) {
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
const result = await canPublish(ctx);
|
|
1179
|
-
if (typeof result !== "boolean") {
|
|
1180
|
-
throw new errors.InvalidCanPublishResponse();
|
|
1181
|
-
}
|
|
1182
|
-
if (!result) {
|
|
1183
|
-
throw new errors.Forbidden();
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// MARK: - Action Execution
|
|
1188
|
-
async invokeActionByName(
|
|
1189
|
-
ctx: ActorContext<S, CP, CS, V, I, DB, E, Q>,
|
|
1190
|
-
actionName: string,
|
|
1191
|
-
args: unknown[],
|
|
1192
|
-
timeoutMs?: number,
|
|
1193
|
-
): Promise<unknown> {
|
|
1194
|
-
this.assertReady();
|
|
1195
|
-
|
|
1196
|
-
const actions = this.#config.actions ?? {};
|
|
1197
|
-
if (!(actionName in actions)) {
|
|
1198
|
-
this.#rLog.warn({ msg: "action does not exist", actionName });
|
|
1199
|
-
throw new errors.ActionNotFound(actionName);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
const actionFunction = actions[actionName];
|
|
1203
|
-
if (typeof actionFunction !== "function") {
|
|
1204
|
-
this.#rLog.warn({
|
|
1205
|
-
msg: "action is not a function",
|
|
1206
|
-
actionName,
|
|
1207
|
-
type: typeof actionFunction,
|
|
1208
|
-
});
|
|
1209
|
-
throw new errors.ActionNotFound(actionName);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const outputOrPromise = actionFunction.call(
|
|
1213
|
-
undefined,
|
|
1214
|
-
// TODO: Replace this cast after scheduled actions and direct actions
|
|
1215
|
-
// share a properly typed internal action invocation context.
|
|
1216
|
-
ctx as any,
|
|
1217
|
-
...args,
|
|
1218
|
-
);
|
|
1219
|
-
const maybeThenable = outputOrPromise as {
|
|
1220
|
-
then?: (onfulfilled?: unknown, onrejected?: unknown) => unknown;
|
|
1221
|
-
};
|
|
1222
|
-
if (maybeThenable && typeof maybeThenable.then === "function") {
|
|
1223
|
-
const promise = Promise.resolve(outputOrPromise);
|
|
1224
|
-
return await (timeoutMs === undefined
|
|
1225
|
-
? promise
|
|
1226
|
-
: deadline(promise, timeoutMs));
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
return outputOrPromise;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
async executeAction(
|
|
1233
|
-
ctx: ActionContext<S, CP, CS, V, I, DB, E, Q>,
|
|
1234
|
-
actionName: string,
|
|
1235
|
-
args: unknown[],
|
|
1236
|
-
): Promise<unknown> {
|
|
1237
|
-
this.assertReady();
|
|
1238
|
-
|
|
1239
|
-
this.#beginActiveAsyncRegion("internalKeepAwake");
|
|
1240
|
-
this.#metrics.actionCalls++;
|
|
1241
|
-
const actionStart = performance.now();
|
|
1242
|
-
const actionSpan = this.startTraceSpan(`actor.action.${actionName}`, {
|
|
1243
|
-
"rivet.action.name": actionName,
|
|
1244
|
-
});
|
|
1245
|
-
let spanEnded = false;
|
|
1246
|
-
|
|
1247
|
-
try {
|
|
1248
|
-
const output = await this.#traces.withSpan(actionSpan, async () => {
|
|
1249
|
-
this.#rLog.debug({
|
|
1250
|
-
msg: "executing action",
|
|
1251
|
-
actionName,
|
|
1252
|
-
args,
|
|
1253
|
-
});
|
|
1254
|
-
|
|
1255
|
-
let output = await this.invokeActionByName(
|
|
1256
|
-
ctx,
|
|
1257
|
-
actionName,
|
|
1258
|
-
args,
|
|
1259
|
-
this.#config.options.actionTimeout,
|
|
1260
|
-
);
|
|
1261
|
-
|
|
1262
|
-
// Process through onBeforeActionResponse if configured
|
|
1263
|
-
if (this.#config.onBeforeActionResponse) {
|
|
1264
|
-
try {
|
|
1265
|
-
output = await this.runInTraceSpan(
|
|
1266
|
-
"actor.onBeforeActionResponse",
|
|
1267
|
-
{ "rivet.action.name": actionName },
|
|
1268
|
-
() =>
|
|
1269
|
-
this.#config.onBeforeActionResponse?.(
|
|
1270
|
-
this.actorContext,
|
|
1271
|
-
actionName,
|
|
1272
|
-
args,
|
|
1273
|
-
output,
|
|
1274
|
-
),
|
|
1275
|
-
);
|
|
1276
|
-
} catch (error) {
|
|
1277
|
-
this.#rLog.error({
|
|
1278
|
-
msg: "error in `onBeforeActionResponse`",
|
|
1279
|
-
error: stringifyError(error),
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
return output;
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
return output;
|
|
1288
|
-
} catch (error) {
|
|
1289
|
-
this.#metrics.actionErrors++;
|
|
1290
|
-
const isTimeout = error instanceof DeadlineError;
|
|
1291
|
-
const message = isTimeout
|
|
1292
|
-
? "ActionTimedOut"
|
|
1293
|
-
: stringifyError(error);
|
|
1294
|
-
this.#traces.setAttributes(actionSpan, {
|
|
1295
|
-
"error.message": message,
|
|
1296
|
-
"error.type":
|
|
1297
|
-
error instanceof Error ? error.name : typeof error,
|
|
1298
|
-
});
|
|
1299
|
-
this.#traces.endSpan(actionSpan, {
|
|
1300
|
-
status: { code: "ERROR", message },
|
|
1301
|
-
});
|
|
1302
|
-
spanEnded = true;
|
|
1303
|
-
if (isTimeout) {
|
|
1304
|
-
throw new errors.ActionTimedOut();
|
|
1305
|
-
}
|
|
1306
|
-
this.#rLog.error({
|
|
1307
|
-
msg: "action error",
|
|
1308
|
-
actionName,
|
|
1309
|
-
error: stringifyError(error),
|
|
1310
|
-
});
|
|
1311
|
-
throw error;
|
|
1312
|
-
} finally {
|
|
1313
|
-
this.#metrics.actionTotalMs += performance.now() - actionStart;
|
|
1314
|
-
if (!spanEnded && actionSpan.isActive()) {
|
|
1315
|
-
this.#traces.endSpan(actionSpan, {
|
|
1316
|
-
status: { code: "OK" },
|
|
1317
|
-
});
|
|
1318
|
-
}
|
|
1319
|
-
this.#endActiveAsyncRegion("internalKeepAwake");
|
|
1320
|
-
this.stateManager.savePersistThrottled();
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
// MARK: - HTTP/WebSocket Handlers
|
|
1325
|
-
//
|
|
1326
|
-
// handleRawRequest intentionally has no isStopping guard (unlike
|
|
1327
|
-
// handleRawWebSocket). In-flight HTTP requests from pre-existing
|
|
1328
|
-
// connections are allowed during the graceful shutdown window.
|
|
1329
|
-
// New external requests cannot reach a stopping actor because the
|
|
1330
|
-
// driver layer blocks them.
|
|
1331
|
-
async handleRawRequest(
|
|
1332
|
-
conn: Conn<S, CP, CS, V, I, DB, E, Q>,
|
|
1333
|
-
request: Request,
|
|
1334
|
-
): Promise<Response> {
|
|
1335
|
-
this.assertReady();
|
|
1336
|
-
|
|
1337
|
-
if (!this.#config.onRequest) {
|
|
1338
|
-
throw new errors.RequestHandlerNotDefined();
|
|
1339
|
-
}
|
|
1340
|
-
const onRequest = this.#config.onRequest;
|
|
1341
|
-
|
|
1342
|
-
return await this.runInTraceSpan(
|
|
1343
|
-
"actor.onRequest",
|
|
1344
|
-
{
|
|
1345
|
-
"http.method": request.method,
|
|
1346
|
-
"http.url": request.url,
|
|
1347
|
-
"rivet.conn.id": conn.id,
|
|
1348
|
-
},
|
|
1349
|
-
async () => {
|
|
1350
|
-
const ctx = new RequestContext(this, conn, request);
|
|
1351
|
-
try {
|
|
1352
|
-
const response = await onRequest(ctx, request);
|
|
1353
|
-
if (!response) {
|
|
1354
|
-
throw new errors.InvalidRequestHandlerResponse();
|
|
1355
|
-
}
|
|
1356
|
-
return response;
|
|
1357
|
-
} catch (error) {
|
|
1358
|
-
this.#rLog.error({
|
|
1359
|
-
msg: "onRequest error",
|
|
1360
|
-
error: stringifyError(error),
|
|
1361
|
-
});
|
|
1362
|
-
throw error;
|
|
1363
|
-
} finally {
|
|
1364
|
-
this.stateManager.savePersistThrottled();
|
|
1365
|
-
}
|
|
1366
|
-
},
|
|
1367
|
-
);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
handleRawWebSocket(
|
|
1371
|
-
conn: Conn<S, CP, CS, V, I, DB, E, Q>,
|
|
1372
|
-
websocket: UniversalWebSocket,
|
|
1373
|
-
request?: Request,
|
|
1374
|
-
) {
|
|
1375
|
-
// NOTE: All code before `onWebSocket` must be synchronous in order to ensure the order of `open` events happen in the correct order.
|
|
1376
|
-
|
|
1377
|
-
this.assertReady();
|
|
1378
|
-
if (this.#stopCalled)
|
|
1379
|
-
throw new errors.InternalError("Actor is stopping");
|
|
1380
|
-
|
|
1381
|
-
if (!this.#config.onWebSocket) {
|
|
1382
|
-
throw new errors.InternalError("onWebSocket handler not defined");
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
const span = this.startTraceSpan("actor.onWebSocket", {
|
|
1386
|
-
"http.url": request?.url,
|
|
1387
|
-
"rivet.conn.id": conn.id,
|
|
1388
|
-
});
|
|
1389
|
-
let spanEnded = false;
|
|
1390
|
-
|
|
1391
|
-
try {
|
|
1392
|
-
// Reset sleep timer when handling WebSocket
|
|
1393
|
-
this.resetSleepTimer();
|
|
1394
|
-
|
|
1395
|
-
// Handle WebSocket
|
|
1396
|
-
const ctx = new WebSocketContext(this, conn, request);
|
|
1397
|
-
const trackedWebSocket = this.#createTrackedWebSocket(websocket);
|
|
1398
|
-
|
|
1399
|
-
// NOTE: This is async and will run in the background
|
|
1400
|
-
const voidOrPromise = this.#traces.withSpan(span, () =>
|
|
1401
|
-
this.#config.onWebSocket?.(ctx, trackedWebSocket),
|
|
1402
|
-
);
|
|
1403
|
-
|
|
1404
|
-
// Save changes from the WebSocket open
|
|
1405
|
-
if (voidOrPromise instanceof Promise) {
|
|
1406
|
-
voidOrPromise
|
|
1407
|
-
.then(() => {
|
|
1408
|
-
if (!spanEnded) {
|
|
1409
|
-
this.endTraceSpan(span, { code: "OK" });
|
|
1410
|
-
spanEnded = true;
|
|
1411
|
-
}
|
|
1412
|
-
})
|
|
1413
|
-
.catch((error) => {
|
|
1414
|
-
if (!spanEnded) {
|
|
1415
|
-
this.endTraceSpan(span, {
|
|
1416
|
-
code: "ERROR",
|
|
1417
|
-
message: stringifyError(error),
|
|
1418
|
-
});
|
|
1419
|
-
spanEnded = true;
|
|
1420
|
-
}
|
|
1421
|
-
this.#rLog.error({
|
|
1422
|
-
msg: "onWebSocket error",
|
|
1423
|
-
error: stringifyError(error),
|
|
1424
|
-
});
|
|
1425
|
-
})
|
|
1426
|
-
.finally(() => {
|
|
1427
|
-
this.stateManager.savePersistThrottled();
|
|
1428
|
-
});
|
|
1429
|
-
} else {
|
|
1430
|
-
if (!spanEnded) {
|
|
1431
|
-
this.endTraceSpan(span, { code: "OK" });
|
|
1432
|
-
spanEnded = true;
|
|
1433
|
-
}
|
|
1434
|
-
this.stateManager.savePersistThrottled();
|
|
1435
|
-
}
|
|
1436
|
-
} catch (error) {
|
|
1437
|
-
if (!spanEnded) {
|
|
1438
|
-
this.endTraceSpan(span, {
|
|
1439
|
-
code: "ERROR",
|
|
1440
|
-
message: stringifyError(error),
|
|
1441
|
-
});
|
|
1442
|
-
spanEnded = true;
|
|
1443
|
-
}
|
|
1444
|
-
this.#rLog.error({
|
|
1445
|
-
msg: "onWebSocket error",
|
|
1446
|
-
error: stringifyError(error),
|
|
1447
|
-
});
|
|
1448
|
-
throw error;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
// MARK: - Scheduling
|
|
1453
|
-
async scheduleEvent(
|
|
1454
|
-
timestamp: number,
|
|
1455
|
-
action: string,
|
|
1456
|
-
args: unknown[],
|
|
1457
|
-
): Promise<void> {
|
|
1458
|
-
await this.#scheduleManager.scheduleEvent(timestamp, action, args);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
async onAlarm() {
|
|
1462
|
-
if (this.#stopCalled) return;
|
|
1463
|
-
this.resetSleepTimer();
|
|
1464
|
-
await this.#scheduleManager.onAlarm();
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// MARK: - Background Tasks
|
|
1468
|
-
waitUntil(promise: Promise<void>) {
|
|
1469
|
-
this.assertNotShutdown();
|
|
1470
|
-
|
|
1471
|
-
const nonfailablePromise = promise
|
|
1472
|
-
.then(() => {
|
|
1473
|
-
this.#rLog.debug({ msg: "wait until promise complete" });
|
|
1474
|
-
})
|
|
1475
|
-
.catch((error) => {
|
|
1476
|
-
this.#rLog.error({
|
|
1477
|
-
msg: "wait until promise failed",
|
|
1478
|
-
error: stringifyError(error),
|
|
1479
|
-
});
|
|
1480
|
-
});
|
|
1481
|
-
this.#backgroundPromises.push(nonfailablePromise);
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
#getEffectiveSleepGracePeriod(): number {
|
|
1485
|
-
if (this.overrides.sleepGracePeriod !== undefined) {
|
|
1486
|
-
return Math.min(
|
|
1487
|
-
this.#config.options.sleepGracePeriod,
|
|
1488
|
-
this.overrides.sleepGracePeriod,
|
|
1489
|
-
);
|
|
1490
|
-
}
|
|
1491
|
-
return this.#config.options.sleepGracePeriod;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
#beginActiveAsyncRegion(region: keyof ActiveAsyncRegionCounts) {
|
|
1495
|
-
this.#activeAsyncRegionCounts[region]++;
|
|
1496
|
-
this.resetSleepTimer();
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
#endActiveAsyncRegion(region: keyof ActiveAsyncRegionCounts) {
|
|
1500
|
-
this.#activeAsyncRegionCounts[region]--;
|
|
1501
|
-
if (this.#activeAsyncRegionCounts[region] < 0) {
|
|
1502
|
-
this.#activeAsyncRegionCounts[region] = 0;
|
|
1503
|
-
this.#rLog.warn({
|
|
1504
|
-
msg: ACTIVE_ASYNC_REGION_ERROR_MESSAGES[region],
|
|
1505
|
-
...EXTRA_ERROR_LOG,
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
this.resetSleepTimer();
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
#trackWebSocketCallback(eventType: string, promise: Promise<void>) {
|
|
1513
|
-
this.#beginActiveAsyncRegion("websocketCallbacks");
|
|
1514
|
-
|
|
1515
|
-
const trackedPromise = promise
|
|
1516
|
-
.then(() => {
|
|
1517
|
-
this.#rLog.debug({
|
|
1518
|
-
msg: "websocket callback complete",
|
|
1519
|
-
eventType,
|
|
1520
|
-
});
|
|
1521
|
-
})
|
|
1522
|
-
.catch((error) => {
|
|
1523
|
-
this.#rLog.error({
|
|
1524
|
-
msg: "websocket callback failed",
|
|
1525
|
-
eventType,
|
|
1526
|
-
error: stringifyError(error),
|
|
1527
|
-
});
|
|
1528
|
-
})
|
|
1529
|
-
.finally(() => {
|
|
1530
|
-
this.#endActiveAsyncRegion("websocketCallbacks");
|
|
1531
|
-
});
|
|
1532
|
-
|
|
1533
|
-
this.#websocketCallbackPromises.push(trackedPromise);
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
/**
|
|
1537
|
-
* Prevents the actor from sleeping while the given promise is running.
|
|
1538
|
-
*
|
|
1539
|
-
* Use this when performing async operations in the `run` handler or other
|
|
1540
|
-
* background contexts where you need to ensure the actor stays awake.
|
|
1541
|
-
*
|
|
1542
|
-
* Returns the resolved value and resets the sleep timer on completion.
|
|
1543
|
-
* Errors are propagated to the caller.
|
|
1544
|
-
*
|
|
1545
|
-
* @deprecated Use `setPreventSleep(true)` while work is active, or move
|
|
1546
|
-
* shutdown and flush work to `onSleep` if it can wait until the actor is
|
|
1547
|
-
* sleeping.
|
|
1548
|
-
*/
|
|
1549
|
-
async keepAwake<T>(promise: Promise<T>): Promise<T> {
|
|
1550
|
-
this.assertNotShutdown();
|
|
1551
|
-
|
|
1552
|
-
this.#beginActiveAsyncRegion("keepAwake");
|
|
1553
|
-
|
|
1554
|
-
try {
|
|
1555
|
-
return await promise;
|
|
1556
|
-
} finally {
|
|
1557
|
-
this.#endActiveAsyncRegion("keepAwake");
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
/**
|
|
1562
|
-
* Internal sleep blocker used by runtime subsystems.
|
|
1563
|
-
*
|
|
1564
|
-
* Accepts either a promise or a thunk. The thunk form exists so the actor
|
|
1565
|
-
* can enter the sleep-blocking region before user code starts running. This
|
|
1566
|
-
* avoids a race where work begins, but the actor is not yet marked active,
|
|
1567
|
-
* which can allow the sleep timer to fire underneath that work.
|
|
1568
|
-
*/
|
|
1569
|
-
internalKeepAwake<T>(promise: Promise<T>): Promise<T>;
|
|
1570
|
-
internalKeepAwake<T>(run: () => T | Promise<T>): Promise<T>;
|
|
1571
|
-
async internalKeepAwake<T>(
|
|
1572
|
-
promiseOrRun: Promise<T> | (() => T | Promise<T>),
|
|
1573
|
-
): Promise<T> {
|
|
1574
|
-
this.assertNotShutdown();
|
|
1575
|
-
|
|
1576
|
-
this.#beginActiveAsyncRegion("internalKeepAwake");
|
|
1577
|
-
|
|
1578
|
-
try {
|
|
1579
|
-
if (typeof promiseOrRun === "function") {
|
|
1580
|
-
return await promiseOrRun();
|
|
1581
|
-
}
|
|
1582
|
-
return await promiseOrRun;
|
|
1583
|
-
} finally {
|
|
1584
|
-
this.#endActiveAsyncRegion("internalKeepAwake");
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
setPreventSleep(prevent: boolean) {
|
|
1589
|
-
if (this.#preventSleep === prevent) return;
|
|
1590
|
-
|
|
1591
|
-
this.#preventSleep = prevent;
|
|
1592
|
-
if (!prevent) {
|
|
1593
|
-
this.#preventSleepClearedPromise?.resolve();
|
|
1594
|
-
this.#preventSleepClearedPromise = undefined;
|
|
1595
|
-
}
|
|
1596
|
-
this.#rLog.debug({
|
|
1597
|
-
msg: "updated prevent sleep state",
|
|
1598
|
-
prevent,
|
|
1599
|
-
});
|
|
1600
|
-
this.resetSleepTimer();
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
beginQueueWait() {
|
|
1604
|
-
this.assertReady();
|
|
1605
|
-
this.#activeQueueWaitCount++;
|
|
1606
|
-
this.resetSleepTimer();
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
endQueueWait() {
|
|
1610
|
-
this.#activeQueueWaitCount--;
|
|
1611
|
-
if (this.#activeQueueWaitCount < 0) {
|
|
1612
|
-
this.#activeQueueWaitCount = 0;
|
|
1613
|
-
this.#rLog.warn({
|
|
1614
|
-
msg: "active queue wait count went below 0, this is a RivetKit bug",
|
|
1615
|
-
...EXTRA_ERROR_LOG,
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
this.resetSleepTimer();
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
// MARK: - Private Helper Methods
|
|
1622
|
-
#initializeTraces() {
|
|
1623
|
-
if (getRivetExperimentalOtel()) {
|
|
1624
|
-
// Experimental mode persists trace data to actor storage so inspector
|
|
1625
|
-
// queries can return OTel payloads.
|
|
1626
|
-
this.#traces = createTraces({
|
|
1627
|
-
driver: new ActorTracesDriver(this.driver, this.#actorId),
|
|
1628
|
-
});
|
|
1629
|
-
} else {
|
|
1630
|
-
// Keep the tracing API calls active while disabling trace persistence
|
|
1631
|
-
// until the experimental flag is enabled.
|
|
1632
|
-
this.#traces = createNoopTraces();
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
#traceAttributes(
|
|
1637
|
-
attributes?: Record<string, unknown>,
|
|
1638
|
-
): Record<string, unknown> {
|
|
1639
|
-
return {
|
|
1640
|
-
"rivet.actors.actor.id": this.#actorId,
|
|
1641
|
-
"rivet.actors.actor.name": this.#name,
|
|
1642
|
-
"rivet.actors.actor.key": this.#actorKeyString,
|
|
1643
|
-
"rivet.actors.actor.region": this.#region,
|
|
1644
|
-
...(attributes ?? {}),
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
#patchLoggerForTraces(logger: Logger) {
|
|
1649
|
-
const levels: Array<
|
|
1650
|
-
"trace" | "debug" | "info" | "warn" | "error" | "fatal"
|
|
1651
|
-
> = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
1652
|
-
for (const level of levels) {
|
|
1653
|
-
const original = logger[level].bind(logger) as (
|
|
1654
|
-
...args: any[]
|
|
1655
|
-
) => unknown;
|
|
1656
|
-
logger[level] = ((...args: unknown[]) => {
|
|
1657
|
-
this.#emitLogEvent(level, args);
|
|
1658
|
-
return original(...(args as any[]));
|
|
1659
|
-
}) as Logger[typeof level];
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
#emitLogEvent(level: string, args: unknown[]) {
|
|
1664
|
-
const span = this.#traces.getCurrentSpan();
|
|
1665
|
-
if (!span || !span.isActive()) {
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
let message: string | undefined;
|
|
1670
|
-
if (args.length >= 2) {
|
|
1671
|
-
message = String(args[1]);
|
|
1672
|
-
} else if (args.length === 1) {
|
|
1673
|
-
const [value] = args;
|
|
1674
|
-
if (typeof value === "string") {
|
|
1675
|
-
message = value;
|
|
1676
|
-
} else if (
|
|
1677
|
-
typeof value === "number" ||
|
|
1678
|
-
typeof value === "boolean"
|
|
1679
|
-
) {
|
|
1680
|
-
message = String(value);
|
|
1681
|
-
} else if (value && typeof value === "object") {
|
|
1682
|
-
const maybeMsg = (value as { msg?: unknown }).msg;
|
|
1683
|
-
if (maybeMsg !== undefined) {
|
|
1684
|
-
message = String(maybeMsg);
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
this.#traces.emitEvent(span, "log", {
|
|
1690
|
-
attributes: this.#traceAttributes({
|
|
1691
|
-
"log.level": level,
|
|
1692
|
-
...(message ? { "log.message": message } : {}),
|
|
1693
|
-
}),
|
|
1694
|
-
timeUnixMs: Date.now(),
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
#initializeLogging() {
|
|
1699
|
-
const logParams = {
|
|
1700
|
-
actor: this.#name,
|
|
1701
|
-
key: this.#actorKeyString,
|
|
1702
|
-
actorId: this.#actorId,
|
|
1703
|
-
};
|
|
1704
|
-
|
|
1705
|
-
const extraLogParams = this.driver.getExtraActorLogParams?.();
|
|
1706
|
-
if (extraLogParams) Object.assign(logParams, extraLogParams);
|
|
1707
|
-
|
|
1708
|
-
this.#log = getBaseLogger().child(
|
|
1709
|
-
Object.assign(
|
|
1710
|
-
getIncludeTarget() ? { target: "actor" } : {},
|
|
1711
|
-
logParams,
|
|
1712
|
-
),
|
|
1713
|
-
);
|
|
1714
|
-
this.#rLog = getBaseLogger().child(
|
|
1715
|
-
Object.assign(
|
|
1716
|
-
getIncludeTarget() ? { target: "actor-runtime" } : {},
|
|
1717
|
-
logParams,
|
|
1718
|
-
),
|
|
1719
|
-
);
|
|
1720
|
-
|
|
1721
|
-
this.#patchLoggerForTraces(this.#log);
|
|
1722
|
-
this.#patchLoggerForTraces(this.#rLog);
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
async #loadState(preload?: PreloadMap, writeCollector?: WriteCollector) {
|
|
1726
|
-
let persistDataBuffer: Uint8Array | null;
|
|
1727
|
-
const preloaded = preload?.get(KEYS.PERSIST_DATA);
|
|
1728
|
-
if (preloaded) {
|
|
1729
|
-
persistDataBuffer = preloaded.value;
|
|
1730
|
-
} else {
|
|
1731
|
-
this[WARN_UNEXPECTED_KV_ROUND_TRIP]("kvBatchGet");
|
|
1732
|
-
this.#metrics.startup.kvRoundTrips++;
|
|
1733
|
-
const [buf] = await this.driver.kvBatchGet(this.#actorId, [
|
|
1734
|
-
KEYS.PERSIST_DATA,
|
|
1735
|
-
]);
|
|
1736
|
-
persistDataBuffer = buf;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
invariant(
|
|
1740
|
-
persistDataBuffer !== null,
|
|
1741
|
-
"persist data has not been set, it should be set when initialized",
|
|
1742
|
-
);
|
|
1743
|
-
|
|
1744
|
-
const bareData =
|
|
1745
|
-
ACTOR_VERSIONED.deserializeWithEmbeddedVersion(persistDataBuffer);
|
|
1746
|
-
const persistData = convertActorFromBarePersisted<S, I>(bareData);
|
|
1747
|
-
|
|
1748
|
-
if (persistData.hasInitialized) {
|
|
1749
|
-
await this.#measureStartup("restoreConnectionsMs", () =>
|
|
1750
|
-
this.#restoreExistingActor(persistData, preload),
|
|
1751
|
-
);
|
|
1752
|
-
} else {
|
|
1753
|
-
this.#metrics.startup.isNew = true;
|
|
1754
|
-
await this.#createNewActor(persistData, writeCollector);
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
// Pass persist reference to schedule manager
|
|
1758
|
-
this.#scheduleManager.setPersist(this.stateManager.persist);
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
async #createNewActor(
|
|
1762
|
-
persistData: PersistedActor<S, I>,
|
|
1763
|
-
writeCollector?: WriteCollector,
|
|
1764
|
-
) {
|
|
1765
|
-
this.#rLog.info({ msg: "actor creating" });
|
|
1766
|
-
|
|
1767
|
-
// Initialize state
|
|
1768
|
-
await this.#measureStartup("createStateMs", () =>
|
|
1769
|
-
this.stateManager.initializeState(persistData, writeCollector),
|
|
1770
|
-
);
|
|
1771
|
-
|
|
1772
|
-
// Call onCreate lifecycle
|
|
1773
|
-
if (this.#config.onCreate) {
|
|
1774
|
-
const onCreate = this.#config.onCreate;
|
|
1775
|
-
await this.#measureStartup(
|
|
1776
|
-
"onCreateMs",
|
|
1777
|
-
() =>
|
|
1778
|
-
this.runInTraceSpan("actor.onCreate", undefined, () =>
|
|
1779
|
-
onCreate(this.actorContext as any, persistData.input!),
|
|
1780
|
-
),
|
|
1781
|
-
{ pauseKvGuard: true },
|
|
1782
|
-
);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
async #restoreExistingActor(
|
|
1787
|
-
persistData: PersistedActor<S, I>,
|
|
1788
|
-
preload?: PreloadMap,
|
|
1789
|
-
) {
|
|
1790
|
-
let connEntries: [Uint8Array, Uint8Array][];
|
|
1791
|
-
const preloadedConns = preload?.listPrefix(KEYS.CONN_PREFIX);
|
|
1792
|
-
if (preloadedConns !== undefined) {
|
|
1793
|
-
connEntries = preloadedConns;
|
|
1794
|
-
} else {
|
|
1795
|
-
this[WARN_UNEXPECTED_KV_ROUND_TRIP]("kvListPrefix");
|
|
1796
|
-
this.#metrics.startup.kvRoundTrips++;
|
|
1797
|
-
connEntries = await this.driver.kvListPrefix(
|
|
1798
|
-
this.#actorId,
|
|
1799
|
-
KEYS.CONN_PREFIX,
|
|
1800
|
-
);
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
// Decode connections
|
|
1804
|
-
const connections: PersistedConn<CP, CS>[] = [];
|
|
1805
|
-
for (const [_key, value] of connEntries) {
|
|
1806
|
-
try {
|
|
1807
|
-
const bareData = CONN_VERSIONED.deserializeWithEmbeddedVersion(
|
|
1808
|
-
new Uint8Array(value),
|
|
1809
|
-
);
|
|
1810
|
-
const conn = convertConnFromBarePersistedConn<CP, CS>(bareData);
|
|
1811
|
-
connections.push(conn);
|
|
1812
|
-
} catch (error) {
|
|
1813
|
-
this.#rLog.error({
|
|
1814
|
-
msg: "failed to decode connection",
|
|
1815
|
-
error: stringifyError(error),
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
this.#metrics.startup.restoreConnectionsCount = connections.length;
|
|
1821
|
-
this.#rLog.info({
|
|
1822
|
-
msg: "actor restoring",
|
|
1823
|
-
connections: connections.length,
|
|
1824
|
-
});
|
|
1825
|
-
|
|
1826
|
-
// Initialize state
|
|
1827
|
-
this.stateManager.initPersistProxy(persistData);
|
|
1828
|
-
|
|
1829
|
-
// Restore connections
|
|
1830
|
-
this.connectionManager.restoreConnections(connections);
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
async #initializeInspectorToken(
|
|
1834
|
-
preload?: PreloadMap,
|
|
1835
|
-
writeCollector?: WriteCollector,
|
|
1836
|
-
) {
|
|
1837
|
-
let tokenBuffer: Uint8Array | null;
|
|
1838
|
-
const preloaded = preload?.get(KEYS.INSPECTOR_TOKEN);
|
|
1839
|
-
if (preloaded) {
|
|
1840
|
-
tokenBuffer = preloaded.value;
|
|
1841
|
-
} else {
|
|
1842
|
-
this[WARN_UNEXPECTED_KV_ROUND_TRIP]("kvBatchGet");
|
|
1843
|
-
this.#metrics.startup.kvRoundTrips++;
|
|
1844
|
-
const [buf] = await this.driver.kvBatchGet(this.#actorId, [
|
|
1845
|
-
KEYS.INSPECTOR_TOKEN,
|
|
1846
|
-
]);
|
|
1847
|
-
tokenBuffer = buf;
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
if (tokenBuffer !== null) {
|
|
1851
|
-
const decoder = new TextDecoder();
|
|
1852
|
-
this.#inspectorToken = decoder.decode(tokenBuffer);
|
|
1853
|
-
this.#rLog.debug({ msg: "loaded existing inspector token" });
|
|
1854
|
-
} else {
|
|
1855
|
-
this.#inspectorToken = generateSecureToken();
|
|
1856
|
-
const tokenBytes = new TextEncoder().encode(this.#inspectorToken);
|
|
1857
|
-
if (writeCollector) {
|
|
1858
|
-
writeCollector.add(KEYS.INSPECTOR_TOKEN, tokenBytes);
|
|
1859
|
-
} else {
|
|
1860
|
-
this.#metrics.startup.kvRoundTrips++;
|
|
1861
|
-
await this.driver.kvBatchPut(this.#actorId, [
|
|
1862
|
-
[KEYS.INSPECTOR_TOKEN, tokenBytes],
|
|
1863
|
-
]);
|
|
1864
|
-
}
|
|
1865
|
-
this.#rLog.debug({ msg: "generated new inspector token" });
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
async #initializeVars() {
|
|
1870
|
-
let vars: V | undefined;
|
|
1871
|
-
if ("createVars" in this.#config) {
|
|
1872
|
-
const createVars = this.#config.createVars;
|
|
1873
|
-
vars = await this.runInTraceSpan(
|
|
1874
|
-
"actor.createVars",
|
|
1875
|
-
undefined,
|
|
1876
|
-
() => {
|
|
1877
|
-
const dataOrPromise = createVars?.(
|
|
1878
|
-
this.actorContext as any,
|
|
1879
|
-
this.driver.getContext(this.#actorId),
|
|
1880
|
-
);
|
|
1881
|
-
if (dataOrPromise instanceof Promise) {
|
|
1882
|
-
return deadline(
|
|
1883
|
-
dataOrPromise,
|
|
1884
|
-
this.#config.options.createVarsTimeout,
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
return dataOrPromise;
|
|
1888
|
-
},
|
|
1889
|
-
);
|
|
1890
|
-
} else if ("vars" in this.#config) {
|
|
1891
|
-
vars = structuredClone(this.#config.vars);
|
|
1892
|
-
} else {
|
|
1893
|
-
throw new Error(
|
|
1894
|
-
"Could not create variables from 'createVars' or 'vars'",
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
this.#vars = vars;
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
async #callOnStart() {
|
|
1901
|
-
this.#rLog.info({ msg: "actor starting" });
|
|
1902
|
-
if (this.#config.onWake) {
|
|
1903
|
-
const onWake = this.#config.onWake;
|
|
1904
|
-
await this.runInTraceSpan("actor.onWake", undefined, () =>
|
|
1905
|
-
onWake(this.actorContext),
|
|
1906
|
-
);
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
async #callOnSleep(deadlineTs: number) {
|
|
1911
|
-
if (this.#config.onSleep) {
|
|
1912
|
-
const onSleep = this.#config.onSleep;
|
|
1913
|
-
try {
|
|
1914
|
-
this.#rLog.debug({ msg: "calling onSleep" });
|
|
1915
|
-
await this.runInTraceSpan(
|
|
1916
|
-
"actor.onSleep",
|
|
1917
|
-
undefined,
|
|
1918
|
-
async () => {
|
|
1919
|
-
const result = onSleep(this.actorContext);
|
|
1920
|
-
if (result instanceof Promise) {
|
|
1921
|
-
const remaining = deadlineTs - Date.now();
|
|
1922
|
-
if (remaining <= 0) {
|
|
1923
|
-
throw new DeadlineError();
|
|
1924
|
-
}
|
|
1925
|
-
await deadline(result, remaining);
|
|
1926
|
-
}
|
|
1927
|
-
},
|
|
1928
|
-
);
|
|
1929
|
-
this.#rLog.debug({ msg: "onSleep completed" });
|
|
1930
|
-
} catch (error) {
|
|
1931
|
-
if (error instanceof DeadlineError) {
|
|
1932
|
-
this.#rLog.error({ msg: "onSleep timed out" });
|
|
1933
|
-
} else {
|
|
1934
|
-
this.#rLog.error({
|
|
1935
|
-
msg: "error in onSleep",
|
|
1936
|
-
error: stringifyError(error),
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
async #callOnDestroy(deadlineTs: number) {
|
|
1944
|
-
if (this.#config.onDestroy) {
|
|
1945
|
-
const onDestroy = this.#config.onDestroy;
|
|
1946
|
-
try {
|
|
1947
|
-
this.#rLog.debug({ msg: "calling onDestroy" });
|
|
1948
|
-
await this.runInTraceSpan(
|
|
1949
|
-
"actor.onDestroy",
|
|
1950
|
-
undefined,
|
|
1951
|
-
async () => {
|
|
1952
|
-
const result = onDestroy(this.actorContext);
|
|
1953
|
-
if (result instanceof Promise) {
|
|
1954
|
-
const remaining = deadlineTs - Date.now();
|
|
1955
|
-
if (remaining <= 0) {
|
|
1956
|
-
throw new DeadlineError();
|
|
1957
|
-
}
|
|
1958
|
-
await deadline(result, remaining);
|
|
1959
|
-
}
|
|
1960
|
-
},
|
|
1961
|
-
);
|
|
1962
|
-
this.#rLog.debug({ msg: "onDestroy completed" });
|
|
1963
|
-
} catch (error) {
|
|
1964
|
-
if (error instanceof DeadlineError) {
|
|
1965
|
-
this.#rLog.error({ msg: "onDestroy timed out" });
|
|
1966
|
-
} else {
|
|
1967
|
-
this.#rLog.error({
|
|
1968
|
-
msg: "error in onDestroy",
|
|
1969
|
-
error: stringifyError(error),
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
#startRunHandler() {
|
|
1977
|
-
const runFn = getRunFunction(this.#config.run);
|
|
1978
|
-
if (!runFn) return;
|
|
1979
|
-
|
|
1980
|
-
this.#rLog.debug({ msg: "starting run handler" });
|
|
1981
|
-
this.#runHandlerActive = true;
|
|
1982
|
-
this.resetSleepTimer();
|
|
1983
|
-
|
|
1984
|
-
const runSpan = this.startTraceSpan("actor.run");
|
|
1985
|
-
const runResult = this.#traces.withSpan(runSpan, () =>
|
|
1986
|
-
runFn(this.actorContext),
|
|
1987
|
-
);
|
|
1988
|
-
|
|
1989
|
-
// Do not destroy or immediately sleep the actor when run exits. Finished
|
|
1990
|
-
// workflows must stay inspectable when something goes wrong, and callers
|
|
1991
|
-
// may still need to invoke actions after the run handler has completed.
|
|
1992
|
-
if (runResult instanceof Promise) {
|
|
1993
|
-
this.#runPromise = runResult
|
|
1994
|
-
.then(() => {
|
|
1995
|
-
if (this.#stopCalled) {
|
|
1996
|
-
if (runSpan.isActive()) {
|
|
1997
|
-
this.endTraceSpan(runSpan, { code: "OK" });
|
|
1998
|
-
}
|
|
1999
|
-
this.#rLog.debug({
|
|
2000
|
-
msg: "run handler exited during actor stop",
|
|
2001
|
-
});
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
if (runSpan.isActive()) {
|
|
2006
|
-
this.endTraceSpan(runSpan, { code: "OK" });
|
|
2007
|
-
}
|
|
2008
|
-
this.#rLog.info({
|
|
2009
|
-
msg: "run handler exited",
|
|
2010
|
-
});
|
|
2011
|
-
})
|
|
2012
|
-
.catch((error) => {
|
|
2013
|
-
if (this.#stopCalled) {
|
|
2014
|
-
if (runSpan.isActive()) {
|
|
2015
|
-
this.endTraceSpan(runSpan, { code: "OK" });
|
|
2016
|
-
}
|
|
2017
|
-
this.#rLog.debug({
|
|
2018
|
-
msg: "run handler threw during actor stop",
|
|
2019
|
-
error: stringifyError(error),
|
|
2020
|
-
});
|
|
2021
|
-
return;
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
this.endTraceSpan(runSpan, {
|
|
2025
|
-
code: "ERROR",
|
|
2026
|
-
message: stringifyError(error),
|
|
2027
|
-
});
|
|
2028
|
-
this.#rLog.error({
|
|
2029
|
-
msg: "run handler threw error",
|
|
2030
|
-
error: stringifyError(error),
|
|
2031
|
-
});
|
|
2032
|
-
})
|
|
2033
|
-
.finally(() => {
|
|
2034
|
-
this.#runHandlerActive = false;
|
|
2035
|
-
this.resetSleepTimer();
|
|
2036
|
-
});
|
|
2037
|
-
} else if (runSpan.isActive()) {
|
|
2038
|
-
this.endTraceSpan(runSpan, { code: "OK" });
|
|
2039
|
-
this.#rLog.info({
|
|
2040
|
-
msg: "run handler exited",
|
|
2041
|
-
});
|
|
2042
|
-
this.#runHandlerActive = false;
|
|
2043
|
-
this.resetSleepTimer();
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
async #waitForRunHandler(timeoutMs: number) {
|
|
2048
|
-
if (!this.#runPromise) {
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
this.#rLog.debug({ msg: "waiting for run handler to complete" });
|
|
2053
|
-
|
|
2054
|
-
if (timeoutMs <= 0) {
|
|
2055
|
-
this.#rLog.warn({
|
|
2056
|
-
msg: "run handler did not complete in time, it may have leaked - ensure long-running work settles before shutdown finalizes",
|
|
2057
|
-
timeoutMs,
|
|
2058
|
-
});
|
|
2059
|
-
return;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
const timedOut = await Promise.race([
|
|
2063
|
-
this.#runPromise.then(() => false).catch(() => false),
|
|
2064
|
-
new Promise<true>((resolve) =>
|
|
2065
|
-
setTimeout(() => resolve(true), timeoutMs),
|
|
2066
|
-
),
|
|
2067
|
-
]);
|
|
2068
|
-
|
|
2069
|
-
if (timedOut) {
|
|
2070
|
-
this.#rLog.warn({
|
|
2071
|
-
msg: "run handler did not complete in time, it may have leaked - ensure long-running work settles before shutdown finalizes",
|
|
2072
|
-
timeoutMs,
|
|
2073
|
-
});
|
|
2074
|
-
} else {
|
|
2075
|
-
this.#rLog.debug({ msg: "run handler completed" });
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
async #setupDatabase(_preload?: PreloadMap) {
|
|
2080
|
-
if (!("db" in this.#config) || !this.#config.db) {
|
|
2081
|
-
return;
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
const dbProvider = this.#config.db;
|
|
2085
|
-
|
|
2086
|
-
let client: InferDatabaseClient<DB> | undefined;
|
|
2087
|
-
try {
|
|
2088
|
-
client = await this.#measureStartup("setupDatabaseClientMs", () =>
|
|
2089
|
-
dbProvider.createClient({
|
|
2090
|
-
actorId: this.#actorId,
|
|
2091
|
-
overrideRawDatabaseClient: this.driver
|
|
2092
|
-
.overrideRawDatabaseClient
|
|
2093
|
-
? () =>
|
|
2094
|
-
this.driver.overrideRawDatabaseClient?.(
|
|
2095
|
-
this.#actorId,
|
|
2096
|
-
)
|
|
2097
|
-
: undefined,
|
|
2098
|
-
overrideDrizzleDatabaseClient: this.driver
|
|
2099
|
-
.overrideDrizzleDatabaseClient
|
|
2100
|
-
? () =>
|
|
2101
|
-
this.driver.overrideDrizzleDatabaseClient?.(
|
|
2102
|
-
this.#actorId,
|
|
2103
|
-
)
|
|
2104
|
-
: undefined,
|
|
2105
|
-
kv: {
|
|
2106
|
-
batchPut: (entries: [Uint8Array, Uint8Array][]) =>
|
|
2107
|
-
this.driver.kvBatchPut(this.#actorId, entries),
|
|
2108
|
-
batchGet: (keys: Uint8Array[]) =>
|
|
2109
|
-
this.driver.kvBatchGet(this.#actorId, keys),
|
|
2110
|
-
batchDelete: (keys: Uint8Array[]) =>
|
|
2111
|
-
this.driver.kvBatchDelete(this.#actorId, keys),
|
|
2112
|
-
deleteRange: (start: Uint8Array, end: Uint8Array) =>
|
|
2113
|
-
this.driver.kvDeleteRange(
|
|
2114
|
-
this.#actorId,
|
|
2115
|
-
start,
|
|
2116
|
-
end,
|
|
2117
|
-
),
|
|
2118
|
-
},
|
|
2119
|
-
metrics: this.#metrics,
|
|
2120
|
-
log: this.#rLog,
|
|
2121
|
-
nativeDatabaseProvider:
|
|
2122
|
-
this.driver.getNativeDatabaseProvider?.(),
|
|
2123
|
-
}),
|
|
2124
|
-
);
|
|
2125
|
-
this.#rLog.info({ msg: "database migration starting" });
|
|
2126
|
-
await this.#measureStartup("dbMigrateMs", async () => {
|
|
2127
|
-
await dbProvider.onMigrate?.(client!);
|
|
2128
|
-
});
|
|
2129
|
-
this.#rLog.info({ msg: "database migration complete" });
|
|
2130
|
-
this.#db = client;
|
|
2131
|
-
} catch (error) {
|
|
2132
|
-
if (client) {
|
|
2133
|
-
try {
|
|
2134
|
-
await client.close();
|
|
2135
|
-
} catch (cleanupError) {
|
|
2136
|
-
this.#rLog.error({
|
|
2137
|
-
msg: "database setup cleanup failed",
|
|
2138
|
-
error: stringifyError(cleanupError),
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
if (error instanceof Error) {
|
|
2143
|
-
this.#rLog.error({
|
|
2144
|
-
msg: "database setup failed",
|
|
2145
|
-
error: stringifyError(error),
|
|
2146
|
-
});
|
|
2147
|
-
throw error;
|
|
2148
|
-
}
|
|
2149
|
-
const wrappedError = new Error(
|
|
2150
|
-
`Database setup failed: ${String(error)}`,
|
|
2151
|
-
);
|
|
2152
|
-
this.#rLog.error({
|
|
2153
|
-
msg: "database setup failed with non-Error object",
|
|
2154
|
-
error: String(error),
|
|
2155
|
-
errorType: typeof error,
|
|
2156
|
-
});
|
|
2157
|
-
throw wrappedError;
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
async #cleanupDatabase() {
|
|
2162
|
-
const client = this.#db;
|
|
2163
|
-
const dbConfig = "db" in this.#config ? this.#config.db : undefined;
|
|
2164
|
-
this.#db = undefined;
|
|
2165
|
-
|
|
2166
|
-
if (client && dbConfig) {
|
|
2167
|
-
try {
|
|
2168
|
-
await client.close();
|
|
2169
|
-
} catch (error) {
|
|
2170
|
-
this.#rLog.error({
|
|
2171
|
-
msg: "database cleanup failed",
|
|
2172
|
-
error: stringifyError(error),
|
|
2173
|
-
});
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
async #disconnectConnections() {
|
|
2179
|
-
const promises: Promise<unknown>[] = [];
|
|
2180
|
-
this.#rLog.debug({
|
|
2181
|
-
msg: "disconnecting connections on actor stop",
|
|
2182
|
-
totalConns: this.connectionManager.connections.size,
|
|
2183
|
-
});
|
|
2184
|
-
for (const connection of this.connectionManager.connections.values()) {
|
|
2185
|
-
this.#rLog.debug({
|
|
2186
|
-
msg: "checking connection for disconnect",
|
|
2187
|
-
connId: connection.id,
|
|
2188
|
-
isHibernatable: connection.isHibernatable,
|
|
2189
|
-
});
|
|
2190
|
-
if (!connection.isHibernatable) {
|
|
2191
|
-
this.#rLog.debug({
|
|
2192
|
-
msg: "disconnecting non-hibernatable connection on actor stop",
|
|
2193
|
-
connId: connection.id,
|
|
2194
|
-
});
|
|
2195
|
-
promises.push(connection.disconnect());
|
|
2196
|
-
} else {
|
|
2197
|
-
this.#rLog.debug({
|
|
2198
|
-
msg: "preserving hibernatable connection on actor stop",
|
|
2199
|
-
connId: connection.id,
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
// Wait with timeout
|
|
2205
|
-
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
2206
|
-
const res = await Promise.race([
|
|
2207
|
-
Promise.all(promises).then(() => {
|
|
2208
|
-
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
|
|
2209
|
-
return false;
|
|
2210
|
-
}),
|
|
2211
|
-
new Promise<boolean>((res) => {
|
|
2212
|
-
timeoutHandle = globalThis.setTimeout(() => res(true), 1500);
|
|
2213
|
-
}),
|
|
2214
|
-
]);
|
|
2215
|
-
|
|
2216
|
-
if (res) {
|
|
2217
|
-
this.#rLog.warn({
|
|
2218
|
-
msg: "timed out waiting for connections to close, shutting down anyway",
|
|
2219
|
-
});
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
/**
|
|
2224
|
-
* Drain shutdown blockers within the shared shutdown deadline.
|
|
2225
|
-
*
|
|
2226
|
-
* This method is intentionally called multiple times during shutdown so
|
|
2227
|
-
* work created by earlier shutdown phases, such as async WebSocket close
|
|
2228
|
-
* handlers or waitUntil calls they enqueue, is also drained before final
|
|
2229
|
-
* persistence.
|
|
2230
|
-
*/
|
|
2231
|
-
async #waitShutdownTasks(deadlineTs: number) {
|
|
2232
|
-
while (
|
|
2233
|
-
this.#backgroundPromises.length > 0 ||
|
|
2234
|
-
this.#websocketCallbackPromises.length > 0 ||
|
|
2235
|
-
this.#preventSleep
|
|
2236
|
-
) {
|
|
2237
|
-
await this.#drainPromiseQueue(
|
|
2238
|
-
this.#backgroundPromises,
|
|
2239
|
-
"background tasks",
|
|
2240
|
-
deadlineTs,
|
|
2241
|
-
);
|
|
2242
|
-
await this.#drainPromiseQueue(
|
|
2243
|
-
this.#websocketCallbackPromises,
|
|
2244
|
-
"websocket callbacks",
|
|
2245
|
-
deadlineTs,
|
|
2246
|
-
);
|
|
2247
|
-
await this.#waitForPreventSleepClear(deadlineTs);
|
|
2248
|
-
|
|
2249
|
-
if (deadlineTs - Date.now() <= 0) {
|
|
2250
|
-
break;
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
#sleepWindowBlocker():
|
|
2256
|
-
| "activeHonoHttpRequests"
|
|
2257
|
-
| "keepAwake"
|
|
2258
|
-
| "internalKeepAwake"
|
|
2259
|
-
| "pendingDisconnectCallbacks"
|
|
2260
|
-
| null {
|
|
2261
|
-
if (this.#activeHonoHttpRequests > 0) {
|
|
2262
|
-
return "activeHonoHttpRequests";
|
|
2263
|
-
}
|
|
2264
|
-
if (this.#activeAsyncRegionCounts.keepAwake > 0) {
|
|
2265
|
-
return "keepAwake";
|
|
2266
|
-
}
|
|
2267
|
-
if (this.#activeAsyncRegionCounts.internalKeepAwake > 0) {
|
|
2268
|
-
return "internalKeepAwake";
|
|
2269
|
-
}
|
|
2270
|
-
if (this.connectionManager.pendingDisconnectCount > 0) {
|
|
2271
|
-
return "pendingDisconnectCallbacks";
|
|
2272
|
-
}
|
|
2273
|
-
return null;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
async #waitForIdleSleepWindow(deadlineTs: number) {
|
|
2277
|
-
while (true) {
|
|
2278
|
-
const blocker = this.#sleepWindowBlocker();
|
|
2279
|
-
if (!blocker) {
|
|
2280
|
-
return;
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
const remaining = deadlineTs - Date.now();
|
|
2284
|
-
if (remaining <= 0) {
|
|
2285
|
-
this.#rLog.warn({
|
|
2286
|
-
msg: "timed out waiting for actor to become idle before onSleep",
|
|
2287
|
-
blocker,
|
|
2288
|
-
});
|
|
2289
|
-
return;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
await new Promise<void>((resolve) =>
|
|
2293
|
-
setTimeout(resolve, Math.min(25, remaining)),
|
|
2294
|
-
);
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
async #drainPromiseQueue(
|
|
2299
|
-
promises: Promise<void>[],
|
|
2300
|
-
label: string,
|
|
2301
|
-
deadlineTs: number,
|
|
2302
|
-
) {
|
|
2303
|
-
// Drain in a loop so that work scheduled from earlier callbacks is also
|
|
2304
|
-
// awaited within the same deadline.
|
|
2305
|
-
while (promises.length > 0) {
|
|
2306
|
-
const remaining = deadlineTs - Date.now();
|
|
2307
|
-
if (remaining <= 0) {
|
|
2308
|
-
this.#rLog.error({
|
|
2309
|
-
msg: `timed out waiting for ${label}`,
|
|
2310
|
-
count: promises.length,
|
|
2311
|
-
});
|
|
2312
|
-
break;
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
const batch = promises.length;
|
|
2316
|
-
|
|
2317
|
-
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
2318
|
-
const timedOut = await Promise.race([
|
|
2319
|
-
Promise.allSettled(promises.slice(0, batch)).then(() => {
|
|
2320
|
-
if (timeoutHandle !== undefined)
|
|
2321
|
-
clearTimeout(timeoutHandle);
|
|
2322
|
-
return false;
|
|
2323
|
-
}),
|
|
2324
|
-
new Promise<true>((resolve) => {
|
|
2325
|
-
timeoutHandle = setTimeout(() => resolve(true), remaining);
|
|
2326
|
-
}),
|
|
2327
|
-
]);
|
|
2328
|
-
|
|
2329
|
-
if (timedOut) {
|
|
2330
|
-
this.#rLog.error({
|
|
2331
|
-
msg: `timed out waiting for ${label}`,
|
|
2332
|
-
count: promises.length,
|
|
2333
|
-
});
|
|
2334
|
-
break;
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
promises.splice(0, batch);
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
if (promises.length === 0) {
|
|
2341
|
-
this.#rLog.debug({ msg: `${label} finished` });
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
async #waitForPreventSleepClear(deadlineTs: number) {
|
|
2346
|
-
while (this.#preventSleep) {
|
|
2347
|
-
const remaining = deadlineTs - Date.now();
|
|
2348
|
-
if (remaining <= 0) {
|
|
2349
|
-
this.#rLog.error({
|
|
2350
|
-
msg: "timed out waiting for preventSleep to clear during shutdown",
|
|
2351
|
-
});
|
|
2352
|
-
break;
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
if (!this.#preventSleepClearedPromise) {
|
|
2356
|
-
this.#preventSleepClearedPromise = promiseWithResolvers<void>(
|
|
2357
|
-
(reason: unknown) =>
|
|
2358
|
-
this.#rLog.warn({
|
|
2359
|
-
msg: "preventSleep clear waiter rejected unexpectedly",
|
|
2360
|
-
reason: stringifyError(reason),
|
|
2361
|
-
...EXTRA_ERROR_LOG,
|
|
2362
|
-
}),
|
|
2363
|
-
);
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
2367
|
-
const timedOut = await Promise.race([
|
|
2368
|
-
this.#preventSleepClearedPromise.promise.then(() => {
|
|
2369
|
-
if (timeoutHandle !== undefined)
|
|
2370
|
-
clearTimeout(timeoutHandle);
|
|
2371
|
-
return false;
|
|
2372
|
-
}),
|
|
2373
|
-
new Promise<true>((resolve) => {
|
|
2374
|
-
timeoutHandle = setTimeout(() => resolve(true), remaining);
|
|
2375
|
-
}),
|
|
2376
|
-
]);
|
|
2377
|
-
|
|
2378
|
-
if (timedOut) {
|
|
2379
|
-
this.#rLog.error({
|
|
2380
|
-
msg: "timed out waiting for preventSleep to clear during shutdown",
|
|
2381
|
-
});
|
|
2382
|
-
break;
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
#createTrackedWebSocket(websocket: UniversalWebSocket): TrackedWebSocket {
|
|
2388
|
-
return new TrackedWebSocket(websocket, {
|
|
2389
|
-
onPromise: (eventType, promise) => {
|
|
2390
|
-
this.#trackWebSocketCallback(eventType, promise);
|
|
2391
|
-
},
|
|
2392
|
-
onError: (eventType, error) => {
|
|
2393
|
-
this.#rLog.error({
|
|
2394
|
-
msg: "error in websocket event handler",
|
|
2395
|
-
eventType,
|
|
2396
|
-
error: stringifyError(error),
|
|
2397
|
-
});
|
|
2398
|
-
},
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
resetSleepTimer() {
|
|
2403
|
-
if (this.#config.options.noSleep || !this.#sleepingSupported) return;
|
|
2404
|
-
if (this.#stopCalled) return;
|
|
2405
|
-
|
|
2406
|
-
const canSleep = this.#canSleep();
|
|
2407
|
-
let timeoutMs: number | undefined;
|
|
2408
|
-
|
|
2409
|
-
if (canSleep === CanSleep.Yes) {
|
|
2410
|
-
timeoutMs = this.#config.options.sleepTimeout;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
this.#rLog.debug({
|
|
2414
|
-
msg: "resetting sleep timer",
|
|
2415
|
-
canSleep: CanSleep[canSleep],
|
|
2416
|
-
existingTimeout: !!this.#sleepTimeout,
|
|
2417
|
-
timeout: timeoutMs,
|
|
2418
|
-
});
|
|
2419
|
-
|
|
2420
|
-
if (this.#sleepTimeout) {
|
|
2421
|
-
clearTimeout(this.#sleepTimeout);
|
|
2422
|
-
this.#sleepTimeout = undefined;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
if (this.#sleepCalled) return;
|
|
2426
|
-
|
|
2427
|
-
if (timeoutMs !== undefined) {
|
|
2428
|
-
this.#sleepTimeout = setTimeout(() => {
|
|
2429
|
-
if (this.#canSleep() !== CanSleep.Yes) {
|
|
2430
|
-
this.resetSleepTimer();
|
|
2431
|
-
return;
|
|
2432
|
-
}
|
|
2433
|
-
this.startSleep();
|
|
2434
|
-
}, timeoutMs);
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
#canSleep(): CanSleep {
|
|
2439
|
-
if (!this.#ready) return CanSleep.NotReady;
|
|
2440
|
-
if (!this.#started) return CanSleep.NotReady;
|
|
2441
|
-
if (this.#preventSleep) return CanSleep.PreventSleep;
|
|
2442
|
-
if (this.#activeHonoHttpRequests > 0)
|
|
2443
|
-
return CanSleep.ActiveHonoHttpRequests;
|
|
2444
|
-
if (this.#activeAsyncRegionCounts.keepAwake > 0) {
|
|
2445
|
-
return CanSleep.ActiveKeepAwake;
|
|
2446
|
-
}
|
|
2447
|
-
if (this.#activeAsyncRegionCounts.internalKeepAwake > 0) {
|
|
2448
|
-
return CanSleep.ActiveInternalKeepAwake;
|
|
2449
|
-
}
|
|
2450
|
-
if (this.#runHandlerActive && this.#activeQueueWaitCount === 0) {
|
|
2451
|
-
return CanSleep.ActiveRun;
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
|
-
for (const _conn of this.connectionManager.connections.values()) {
|
|
2455
|
-
if (!_conn.isHibernatable) {
|
|
2456
|
-
return CanSleep.ActiveConns;
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
if (this.connectionManager.pendingDisconnectCount > 0) {
|
|
2461
|
-
return CanSleep.ActiveDisconnectCallbacks;
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
if (this.#activeAsyncRegionCounts.websocketCallbacks > 0) {
|
|
2465
|
-
return CanSleep.ActiveWebSocketCallbacks;
|
|
2466
|
-
}
|
|
2467
|
-
|
|
2468
|
-
return CanSleep.Yes;
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
get #sleepingSupported(): boolean {
|
|
2472
|
-
return this.driver.startSleep !== undefined;
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
get #varsEnabled(): boolean {
|
|
2476
|
-
return "createVars" in this.#config || "vars" in this.#config;
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
#validateVarsEnabled() {
|
|
2480
|
-
if (!this.#varsEnabled) {
|
|
2481
|
-
throw new errors.VarsNotEnabled();
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
}
|