@rivetkit/cloudflare-workers 2.0.2 → 2.0.4-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/README.md +3 -3
- package/dist/mod.cjs +793 -366
- package/dist/mod.cjs.map +1 -1
- package/dist/mod.d.cts +70 -161
- package/dist/mod.d.ts +70 -161
- package/dist/mod.js +804 -377
- package/dist/mod.js.map +1 -1
- package/package.json +9 -5
- package/src/actor-driver.ts +227 -76
- package/src/actor-handler-do.ts +338 -113
- package/src/actor-id.ts +38 -0
- package/src/actor-kv.ts +71 -0
- package/src/config.ts +22 -10
- package/src/global-kv.ts +6 -0
- package/src/handler.ts +103 -52
- package/src/log.ts +2 -4
- package/src/manager-driver.ts +237 -134
- package/src/mod.ts +9 -1
- package/src/websocket.ts +17 -6
package/src/actor-handler-do.ts
CHANGED
|
@@ -1,62 +1,59 @@
|
|
|
1
1
|
import { DurableObject, env } from "cloudflare:workers";
|
|
2
|
-
import type {
|
|
3
|
-
ActorKey,
|
|
4
|
-
ActorRouter,
|
|
5
|
-
Registry,
|
|
6
|
-
RunConfig,
|
|
7
|
-
} from "@rivetkit/core";
|
|
8
|
-
import {
|
|
9
|
-
createActorRouter,
|
|
10
|
-
createClientWithDriver,
|
|
11
|
-
createInlineClientDriver,
|
|
12
|
-
} from "@rivetkit/core";
|
|
13
|
-
import { serializeEmptyPersistData } from "@rivetkit/core/driver-helpers";
|
|
14
2
|
import type { ExecutionContext } from "hono";
|
|
15
3
|
import invariant from "invariant";
|
|
4
|
+
import type { ActorKey, ActorRouter, Registry, RegistryConfig } from "rivetkit";
|
|
5
|
+
import { createActorRouter, createClientWithDriver } from "rivetkit";
|
|
6
|
+
import type { ActorDriver, ManagerDriver } from "rivetkit/driver-helpers";
|
|
7
|
+
import { getInitialActorKvState } from "rivetkit/driver-helpers";
|
|
8
|
+
import type { GetUpgradeWebSocket } from "rivetkit/utils";
|
|
9
|
+
import { stringifyError } from "rivetkit/utils";
|
|
16
10
|
import {
|
|
11
|
+
ActorGlobalState,
|
|
17
12
|
CloudflareDurableObjectGlobalState,
|
|
18
13
|
createCloudflareActorsActorDriverBuilder,
|
|
19
14
|
} from "./actor-driver";
|
|
15
|
+
import { buildActorId, parseActorId } from "./actor-id";
|
|
16
|
+
import { kvGet, kvPut } from "./actor-kv";
|
|
17
|
+
import { GLOBAL_KV_KEYS } from "./global-kv";
|
|
20
18
|
import type { Bindings } from "./handler";
|
|
19
|
+
import { getCloudflareAmbientEnv } from "./handler";
|
|
21
20
|
import { logger } from "./log";
|
|
22
|
-
|
|
23
|
-
export const KEYS = {
|
|
24
|
-
NAME: "rivetkit:name",
|
|
25
|
-
KEY: "rivetkit:key",
|
|
26
|
-
PERSIST_DATA: "rivetkit:data",
|
|
27
|
-
};
|
|
21
|
+
import { CloudflareActorsManagerDriver } from "./manager-driver";
|
|
28
22
|
|
|
29
23
|
export interface ActorHandlerInterface extends DurableObject {
|
|
30
|
-
|
|
24
|
+
create(req: ActorInitRequest): Promise<ActorInitResponse>;
|
|
25
|
+
getMetadata(): Promise<
|
|
26
|
+
| {
|
|
27
|
+
actorId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
key: ActorKey;
|
|
30
|
+
destroying: boolean;
|
|
31
|
+
}
|
|
32
|
+
| undefined
|
|
33
|
+
>;
|
|
34
|
+
managerKvGet(key: Uint8Array): Promise<Uint8Array | null>;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface ActorInitRequest {
|
|
34
38
|
name: string;
|
|
35
39
|
key: ActorKey;
|
|
36
40
|
input?: unknown;
|
|
41
|
+
allowExisting: boolean;
|
|
37
42
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
key: ActorKey;
|
|
42
|
-
}
|
|
43
|
+
export type ActorInitResponse =
|
|
44
|
+
| { success: { actorId: string; created: boolean } }
|
|
45
|
+
| { error: { actorAlreadyExists: true } };
|
|
43
46
|
|
|
44
47
|
export type DurableObjectConstructor = new (
|
|
45
48
|
...args: ConstructorParameters<typeof DurableObject<Bindings>>
|
|
46
49
|
) => DurableObject<Bindings>;
|
|
47
50
|
|
|
48
|
-
interface LoadedActor {
|
|
49
|
-
actorRouter: ActorRouter;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
51
|
export function createActorDurableObject(
|
|
53
52
|
registry: Registry<any>,
|
|
54
|
-
|
|
53
|
+
getUpgradeWebSocket: GetUpgradeWebSocket,
|
|
55
54
|
): DurableObjectConstructor {
|
|
56
55
|
const globalState = new CloudflareDurableObjectGlobalState();
|
|
57
|
-
|
|
58
|
-
// Configure to use the runner role instead of server role
|
|
59
|
-
const runConfig = Object.assign({}, rootRunConfig, { role: "runner" });
|
|
56
|
+
const parsedConfig = registry.parseConfig();
|
|
60
57
|
|
|
61
58
|
/**
|
|
62
59
|
* Startup steps:
|
|
@@ -68,46 +65,104 @@ export function createActorDurableObject(
|
|
|
68
65
|
extends DurableObject<Bindings>
|
|
69
66
|
implements ActorHandlerInterface
|
|
70
67
|
{
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
/**
|
|
69
|
+
* This holds a strong reference to ActorGlobalState.
|
|
70
|
+
* CloudflareDurableObjectGlobalState holds a weak reference so we can
|
|
71
|
+
* access it elsewhere.
|
|
72
|
+
**/
|
|
73
|
+
#state: ActorGlobalState;
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
...args: ConstructorParameters<typeof DurableObject<Bindings>>
|
|
77
|
+
) {
|
|
78
|
+
super(...args);
|
|
79
|
+
|
|
80
|
+
// Initialize SQL table for key-value storage
|
|
81
|
+
//
|
|
82
|
+
// We do this instead of using the native KV storage so we can store blob keys. The native CF KV API only supports string keys.
|
|
83
|
+
this.ctx.storage.sql.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS _rivetkit_kv_storage(
|
|
85
|
+
key BLOB PRIMARY KEY,
|
|
86
|
+
value BLOB
|
|
87
|
+
);
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
// Initialize SQL table for actor metadata
|
|
91
|
+
//
|
|
92
|
+
// id always equals 1 in order to ensure that there's always exactly 1 row in this table
|
|
93
|
+
this.ctx.storage.sql.exec(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS _rivetkit_metadata(
|
|
95
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
96
|
+
name TEXT NOT NULL,
|
|
97
|
+
key TEXT NOT NULL,
|
|
98
|
+
destroyed INTEGER DEFAULT 0,
|
|
99
|
+
generation INTEGER DEFAULT 0
|
|
100
|
+
);
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
// Get or create the actor state from the global WeakMap
|
|
104
|
+
const state = globalState.getActorState(this.ctx);
|
|
105
|
+
if (state) {
|
|
106
|
+
this.#state = state;
|
|
107
|
+
} else {
|
|
108
|
+
this.#state = new ActorGlobalState();
|
|
109
|
+
globalState.setActorState(this.ctx, this.#state);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
75
112
|
|
|
76
|
-
async #loadActor()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
async #loadActor() {
|
|
114
|
+
invariant(this.#state, "State should be initialized");
|
|
115
|
+
|
|
116
|
+
// Check if initialized
|
|
117
|
+
if (!this.#state.initialized) {
|
|
118
|
+
// Query SQL for initialization data
|
|
119
|
+
const cursor = this.ctx.storage.sql.exec(
|
|
120
|
+
"SELECT name, key, destroyed, generation FROM _rivetkit_metadata WHERE id = 1",
|
|
121
|
+
);
|
|
122
|
+
const result = cursor.raw().next();
|
|
123
|
+
|
|
124
|
+
if (!result.done && result.value) {
|
|
125
|
+
const name = result.value[0] as string;
|
|
126
|
+
const key = JSON.parse(
|
|
127
|
+
result.value[1] as string,
|
|
128
|
+
) as ActorKey;
|
|
129
|
+
const destroyed = result.value[2] as number;
|
|
130
|
+
const generation = result.value[3] as number;
|
|
131
|
+
|
|
132
|
+
// Only initialize if not destroyed
|
|
133
|
+
if (!destroyed) {
|
|
134
|
+
logger().debug({
|
|
135
|
+
msg: "already initialized",
|
|
136
|
+
name,
|
|
137
|
+
key,
|
|
138
|
+
generation,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
this.#state.initialized = { name, key, generation };
|
|
99
142
|
} else {
|
|
100
|
-
logger().debug("
|
|
143
|
+
logger().debug("actor is destroyed, cannot load");
|
|
144
|
+
throw new Error("Actor is destroyed");
|
|
101
145
|
}
|
|
146
|
+
} else {
|
|
147
|
+
logger().debug("not initialized");
|
|
148
|
+
throw new Error("Actor is not initialized");
|
|
102
149
|
}
|
|
103
150
|
}
|
|
104
151
|
|
|
105
152
|
// Check if already loaded
|
|
106
|
-
if (this.#actor) {
|
|
107
|
-
|
|
153
|
+
if (this.#state.actor) {
|
|
154
|
+
// Assert that the cached actor has the correct generation
|
|
155
|
+
// This will catch any cases where #state.actor has a stale generation
|
|
156
|
+
invariant(
|
|
157
|
+
!this.#state.initialized ||
|
|
158
|
+
this.#state.actor.generation ===
|
|
159
|
+
this.#state.initialized.generation,
|
|
160
|
+
`Stale actor cached: actor generation ${this.#state.actor.generation} != initialized generation ${this.#state.initialized?.generation}. This should not happen.`,
|
|
161
|
+
);
|
|
162
|
+
return this.#state.actor;
|
|
108
163
|
}
|
|
109
164
|
|
|
110
|
-
if (!this.#initialized) throw new Error("Not initialized");
|
|
165
|
+
if (!this.#state.initialized) throw new Error("Not initialized");
|
|
111
166
|
|
|
112
167
|
// Register DO with global state first
|
|
113
168
|
// HACK: This leaks the DO context, but DO does not provide a native way
|
|
@@ -116,93 +171,253 @@ export function createActorDurableObject(
|
|
|
116
171
|
const actorId = this.ctx.id.toString();
|
|
117
172
|
globalState.setDOState(actorId, { ctx: this.ctx, env: env });
|
|
118
173
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
runConfig.driver.actor =
|
|
122
|
-
createCloudflareActorsActorDriverBuilder(globalState);
|
|
123
|
-
|
|
124
|
-
// Create manager driver (we need this for the actor router)
|
|
125
|
-
const managerDriver = runConfig.driver.manager(
|
|
126
|
-
registry.config,
|
|
127
|
-
runConfig,
|
|
128
|
-
);
|
|
174
|
+
// Create manager driver
|
|
175
|
+
const managerDriver = new CloudflareActorsManagerDriver();
|
|
129
176
|
|
|
130
177
|
// Create inline client
|
|
131
|
-
const inlineClient = createClientWithDriver(
|
|
132
|
-
|
|
133
|
-
|
|
178
|
+
const inlineClient = createClientWithDriver(managerDriver);
|
|
179
|
+
|
|
180
|
+
// Create actor driver builder
|
|
181
|
+
const actorDriverBuilder =
|
|
182
|
+
createCloudflareActorsActorDriverBuilder(globalState);
|
|
134
183
|
|
|
135
184
|
// Create actor driver
|
|
136
|
-
const actorDriver =
|
|
137
|
-
|
|
138
|
-
runConfig,
|
|
185
|
+
const actorDriver = actorDriverBuilder(
|
|
186
|
+
parsedConfig,
|
|
139
187
|
managerDriver,
|
|
140
188
|
inlineClient,
|
|
141
189
|
);
|
|
142
190
|
|
|
143
191
|
// Create actor router
|
|
144
|
-
const actorRouter = createActorRouter(
|
|
192
|
+
const actorRouter = createActorRouter(
|
|
193
|
+
parsedConfig,
|
|
194
|
+
actorDriver,
|
|
195
|
+
getUpgradeWebSocket,
|
|
196
|
+
registry.config.test?.enabled ?? false,
|
|
197
|
+
);
|
|
145
198
|
|
|
146
|
-
// Save actor
|
|
147
|
-
this.#actor = {
|
|
199
|
+
// Save actor with generation
|
|
200
|
+
this.#state.actor = {
|
|
148
201
|
actorRouter,
|
|
202
|
+
actorDriver,
|
|
203
|
+
generation: this.#state.initialized.generation,
|
|
149
204
|
};
|
|
150
205
|
|
|
206
|
+
// Build actor ID with generation for loading
|
|
207
|
+
const actorIdWithGen = buildActorId(
|
|
208
|
+
actorId,
|
|
209
|
+
this.#state.initialized.generation,
|
|
210
|
+
);
|
|
211
|
+
|
|
151
212
|
// Initialize the actor instance with proper metadata
|
|
152
213
|
// This ensures the actor driver knows about this actor
|
|
153
|
-
await actorDriver.loadActor(
|
|
214
|
+
await actorDriver.loadActor(actorIdWithGen);
|
|
154
215
|
|
|
155
|
-
return this.#actor;
|
|
216
|
+
return this.#state.actor;
|
|
156
217
|
}
|
|
157
218
|
|
|
158
|
-
/** RPC called
|
|
159
|
-
async
|
|
160
|
-
|
|
219
|
+
/** RPC called to get actor metadata without creating it */
|
|
220
|
+
async getMetadata(): Promise<
|
|
221
|
+
| {
|
|
222
|
+
actorId: string;
|
|
223
|
+
name: string;
|
|
224
|
+
key: ActorKey;
|
|
225
|
+
destroying: boolean;
|
|
226
|
+
}
|
|
227
|
+
| undefined
|
|
228
|
+
> {
|
|
229
|
+
// Query the metadata
|
|
230
|
+
const cursor = this.ctx.storage.sql.exec(
|
|
231
|
+
"SELECT name, key, destroyed, generation FROM _rivetkit_metadata WHERE id = 1",
|
|
232
|
+
);
|
|
233
|
+
const result = cursor.raw().next();
|
|
234
|
+
|
|
235
|
+
if (!result.done && result.value) {
|
|
236
|
+
const name = result.value[0] as string;
|
|
237
|
+
const key = JSON.parse(result.value[1] as string) as ActorKey;
|
|
238
|
+
const destroyed = result.value[2] as number;
|
|
239
|
+
const generation = result.value[3] as number;
|
|
240
|
+
|
|
241
|
+
// Check if destroyed
|
|
242
|
+
if (destroyed) {
|
|
243
|
+
logger().debug({
|
|
244
|
+
msg: "getMetadata: actor is destroyed",
|
|
245
|
+
name,
|
|
246
|
+
key,
|
|
247
|
+
generation,
|
|
248
|
+
});
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
161
251
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
252
|
+
// Build actor ID with generation
|
|
253
|
+
const doId = this.ctx.id.toString();
|
|
254
|
+
const actorId = buildActorId(doId, generation);
|
|
255
|
+
const destroying =
|
|
256
|
+
globalState.getActorState(this.ctx)?.destroying ?? false;
|
|
257
|
+
|
|
258
|
+
logger().debug({
|
|
259
|
+
msg: "getMetadata: found actor metadata",
|
|
260
|
+
actorId,
|
|
261
|
+
name,
|
|
262
|
+
key,
|
|
263
|
+
generation,
|
|
264
|
+
destroying,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return { actorId, name, key, destroying };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
logger().debug({
|
|
271
|
+
msg: "getMetadata: no metadata found",
|
|
166
272
|
});
|
|
167
|
-
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** RPC called by ManagerDriver.kvGet to read from KV. */
|
|
277
|
+
async managerKvGet(key: Uint8Array): Promise<Uint8Array | null> {
|
|
278
|
+
return kvGet(this.ctx.storage.sql, key);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** RPC called by the manager to create a DO. Can optionally allow existing actors. */
|
|
282
|
+
async create(req: ActorInitRequest): Promise<ActorInitResponse> {
|
|
283
|
+
// Check if actor exists
|
|
284
|
+
const checkCursor = this.ctx.storage.sql.exec(
|
|
285
|
+
"SELECT destroyed, generation FROM _rivetkit_metadata WHERE id = 1",
|
|
286
|
+
);
|
|
287
|
+
const checkResult = checkCursor.raw().next();
|
|
288
|
+
|
|
289
|
+
let created = false;
|
|
290
|
+
let generation = 0;
|
|
291
|
+
|
|
292
|
+
if (!checkResult.done && checkResult.value) {
|
|
293
|
+
const destroyed = checkResult.value[0] as number;
|
|
294
|
+
generation = checkResult.value[1] as number;
|
|
295
|
+
|
|
296
|
+
if (!destroyed) {
|
|
297
|
+
// Actor exists and is not destroyed
|
|
298
|
+
if (!req.allowExisting) {
|
|
299
|
+
// Fail if not allowing existing actors
|
|
300
|
+
logger().debug({
|
|
301
|
+
msg: "create failed: actor already exists",
|
|
302
|
+
name: req.name,
|
|
303
|
+
key: req.key,
|
|
304
|
+
generation,
|
|
305
|
+
});
|
|
306
|
+
return { error: { actorAlreadyExists: true } };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Return existing actor
|
|
310
|
+
logger().debug({
|
|
311
|
+
msg: "actor already exists",
|
|
312
|
+
key: req.key,
|
|
313
|
+
generation,
|
|
314
|
+
});
|
|
315
|
+
const doId = this.ctx.id.toString();
|
|
316
|
+
const actorId = buildActorId(doId, generation);
|
|
317
|
+
return { success: { actorId, created: false } };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Actor exists but is destroyed - resurrect with incremented generation
|
|
321
|
+
generation = generation + 1;
|
|
322
|
+
created = true;
|
|
323
|
+
|
|
324
|
+
// Clear stale actor from previous generation
|
|
325
|
+
// This is necessary because the DO instance may still be in memory
|
|
326
|
+
// with the old #state.actor field from before the destroy
|
|
327
|
+
if (this.#state) {
|
|
328
|
+
this.#state.actor = undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
logger().debug({
|
|
332
|
+
msg: "resurrecting destroyed actor",
|
|
333
|
+
key: req.key,
|
|
334
|
+
oldGeneration: generation - 1,
|
|
335
|
+
newGeneration: generation,
|
|
336
|
+
});
|
|
337
|
+
} else {
|
|
338
|
+
// No actor exists - will create with generation 0
|
|
339
|
+
generation = 0;
|
|
340
|
+
created = true;
|
|
341
|
+
logger().debug({
|
|
342
|
+
msg: "creating new actor",
|
|
343
|
+
key: req.key,
|
|
344
|
+
generation,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Perform upsert - either inserts new or updates destroyed actor
|
|
349
|
+
this.ctx.storage.sql.exec(
|
|
350
|
+
`INSERT INTO _rivetkit_metadata (id, name, key, destroyed, generation)
|
|
351
|
+
VALUES (1, ?, ?, 0, ?)
|
|
352
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
353
|
+
name = excluded.name,
|
|
354
|
+
key = excluded.key,
|
|
355
|
+
destroyed = 0,
|
|
356
|
+
generation = excluded.generation`,
|
|
357
|
+
req.name,
|
|
358
|
+
JSON.stringify(req.key),
|
|
359
|
+
generation,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
this.#state.initialized = {
|
|
168
363
|
name: req.name,
|
|
169
364
|
key: req.key,
|
|
365
|
+
generation,
|
|
170
366
|
};
|
|
171
367
|
|
|
172
|
-
|
|
368
|
+
// Build actor ID with generation
|
|
369
|
+
const doId = this.ctx.id.toString();
|
|
370
|
+
const actorId = buildActorId(doId, generation);
|
|
371
|
+
|
|
372
|
+
// Initialize storage and update KV when created or resurrected
|
|
373
|
+
if (created) {
|
|
374
|
+
// Initialize persist data in KV storage
|
|
375
|
+
initializeActorKvStorage(this.ctx.storage.sql, req.input);
|
|
376
|
+
|
|
377
|
+
// Update metadata in the background
|
|
378
|
+
const env = getCloudflareAmbientEnv();
|
|
379
|
+
const actorData = { name: req.name, key: req.key, generation };
|
|
380
|
+
this.ctx.waitUntil(
|
|
381
|
+
env.ACTOR_KV.put(
|
|
382
|
+
GLOBAL_KV_KEYS.actorMetadata(actorId),
|
|
383
|
+
JSON.stringify(actorData),
|
|
384
|
+
),
|
|
385
|
+
);
|
|
386
|
+
}
|
|
173
387
|
|
|
174
|
-
// Preemptively actor so the lifecycle hooks are called
|
|
388
|
+
// Preemptively load actor so the lifecycle hooks are called
|
|
175
389
|
await this.#loadActor();
|
|
390
|
+
|
|
391
|
+
logger().debug({
|
|
392
|
+
msg: created
|
|
393
|
+
? "actor created/resurrected"
|
|
394
|
+
: "returning existing actor",
|
|
395
|
+
actorId,
|
|
396
|
+
created,
|
|
397
|
+
generation,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
return { success: { actorId, created } };
|
|
176
401
|
}
|
|
177
402
|
|
|
178
403
|
async fetch(request: Request): Promise<Response> {
|
|
179
|
-
const { actorRouter } = await this.#loadActor();
|
|
404
|
+
const { actorRouter, generation } = await this.#loadActor();
|
|
405
|
+
|
|
406
|
+
// Build actor ID with generation
|
|
407
|
+
const doId = this.ctx.id.toString();
|
|
408
|
+
const actorId = buildActorId(doId, generation);
|
|
180
409
|
|
|
181
|
-
const actorId = this.ctx.id.toString();
|
|
182
410
|
return await actorRouter.fetch(request, {
|
|
183
411
|
actorId,
|
|
184
412
|
});
|
|
185
413
|
}
|
|
186
414
|
|
|
187
415
|
async alarm(): Promise<void> {
|
|
188
|
-
await this.#loadActor();
|
|
189
|
-
const actorId = this.ctx.id.toString();
|
|
416
|
+
const { actorDriver, generation } = await this.#loadActor();
|
|
190
417
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
registry.config,
|
|
195
|
-
runConfig,
|
|
196
|
-
);
|
|
197
|
-
const inlineClient = createClientWithDriver(
|
|
198
|
-
createInlineClientDriver(managerDriver),
|
|
199
|
-
);
|
|
200
|
-
const actorDriver = runConfig.driver.actor(
|
|
201
|
-
registry.config,
|
|
202
|
-
runConfig,
|
|
203
|
-
managerDriver,
|
|
204
|
-
inlineClient,
|
|
205
|
-
);
|
|
418
|
+
// Build actor ID with generation
|
|
419
|
+
const doId = this.ctx.id.toString();
|
|
420
|
+
const actorId = buildActorId(doId, generation);
|
|
206
421
|
|
|
207
422
|
// Load the actor instance and trigger alarm
|
|
208
423
|
const actor = await actorDriver.loadActor(actorId);
|
|
@@ -210,3 +425,13 @@ export function createActorDurableObject(
|
|
|
210
425
|
}
|
|
211
426
|
};
|
|
212
427
|
}
|
|
428
|
+
|
|
429
|
+
function initializeActorKvStorage(
|
|
430
|
+
sql: SqlStorage,
|
|
431
|
+
input: unknown | undefined,
|
|
432
|
+
): void {
|
|
433
|
+
const initialKvState = getInitialActorKvState(input);
|
|
434
|
+
for (const [key, value] of initialKvState) {
|
|
435
|
+
kvPut(sql, key, value);
|
|
436
|
+
}
|
|
437
|
+
}
|
package/src/actor-id.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor ID utilities for managing actor IDs with generation tracking.
|
|
3
|
+
*
|
|
4
|
+
* Actor IDs are formatted as: `{doId}:{generation}`
|
|
5
|
+
* This allows tracking actor resurrection and preventing stale references.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build an actor ID from a Durable Object ID and generation number.
|
|
10
|
+
* @param doId The Durable Object ID
|
|
11
|
+
* @param generation The generation number (increments on resurrection)
|
|
12
|
+
* @returns The formatted actor ID
|
|
13
|
+
*/
|
|
14
|
+
export function buildActorId(doId: string, generation: number): string {
|
|
15
|
+
return `${doId}:${generation}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse an actor ID into its components.
|
|
20
|
+
* @param actorId The actor ID to parse
|
|
21
|
+
* @returns A tuple of [doId, generation]
|
|
22
|
+
* @throws Error if the actor ID format is invalid
|
|
23
|
+
*/
|
|
24
|
+
export function parseActorId(actorId: string): [string, number] {
|
|
25
|
+
const parts = actorId.split(":");
|
|
26
|
+
if (parts.length !== 2) {
|
|
27
|
+
throw new Error(`Invalid actor ID format: ${actorId}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const [doId, generationStr] = parts;
|
|
31
|
+
const generation = parseInt(generationStr, 10);
|
|
32
|
+
|
|
33
|
+
if (Number.isNaN(generation)) {
|
|
34
|
+
throw new Error(`Invalid generation number in actor ID: ${actorId}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [doId, generation];
|
|
38
|
+
}
|
package/src/actor-kv.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function kvGet(sql: SqlStorage, key: Uint8Array): Uint8Array | null {
|
|
2
|
+
const cursor = sql.exec(
|
|
3
|
+
"SELECT value FROM _rivetkit_kv_storage WHERE key = ?",
|
|
4
|
+
key,
|
|
5
|
+
);
|
|
6
|
+
const result = cursor.raw().next();
|
|
7
|
+
|
|
8
|
+
if (!result.done && result.value) {
|
|
9
|
+
return toUint8Array(result.value[0]);
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function kvPut(
|
|
15
|
+
sql: SqlStorage,
|
|
16
|
+
key: Uint8Array,
|
|
17
|
+
value: Uint8Array,
|
|
18
|
+
): void {
|
|
19
|
+
sql.exec(
|
|
20
|
+
"INSERT OR REPLACE INTO _rivetkit_kv_storage (key, value) VALUES (?, ?)",
|
|
21
|
+
key,
|
|
22
|
+
value,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function kvDelete(sql: SqlStorage, key: Uint8Array): void {
|
|
27
|
+
sql.exec("DELETE FROM _rivetkit_kv_storage WHERE key = ?", key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function kvListPrefix(
|
|
31
|
+
sql: SqlStorage,
|
|
32
|
+
prefix: Uint8Array,
|
|
33
|
+
): [Uint8Array, Uint8Array][] {
|
|
34
|
+
const cursor = sql.exec("SELECT key, value FROM _rivetkit_kv_storage");
|
|
35
|
+
const entries: [Uint8Array, Uint8Array][] = [];
|
|
36
|
+
|
|
37
|
+
for (const row of cursor.raw()) {
|
|
38
|
+
const key = toUint8Array(row[0]);
|
|
39
|
+
const value = toUint8Array(row[1]);
|
|
40
|
+
|
|
41
|
+
// Check if key starts with prefix
|
|
42
|
+
if (hasPrefix(key, prefix)) {
|
|
43
|
+
entries.push([key, value]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Helper function to convert SqlStorageValue to Uint8Array
|
|
51
|
+
function toUint8Array(
|
|
52
|
+
value: string | number | ArrayBuffer | Uint8Array | null,
|
|
53
|
+
): Uint8Array {
|
|
54
|
+
if (value instanceof Uint8Array) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (value instanceof ArrayBuffer) {
|
|
58
|
+
return new Uint8Array(value);
|
|
59
|
+
}
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Unexpected SQL value type: ${typeof value} (${value?.constructor?.name})`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasPrefix(arr: Uint8Array, prefix: Uint8Array): boolean {
|
|
66
|
+
if (prefix.length > arr.length) return false;
|
|
67
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
68
|
+
if (arr[i] !== prefix[i]) return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import type { Client } from "rivetkit";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
|
|
4
|
+
const ConfigSchemaBase = z.object({
|
|
5
|
+
/** Path that the Rivet manager API will be mounted. */
|
|
6
|
+
managerPath: z.string().optional().default("/api/rivet"),
|
|
7
|
+
|
|
8
|
+
/** Runner key for authentication. */
|
|
9
|
+
runnerKey: z.string().optional(),
|
|
10
|
+
|
|
11
|
+
/** Disable the welcome message. */
|
|
12
|
+
noWelcome: z.boolean().optional().default(false),
|
|
13
|
+
|
|
14
|
+
fetch: z
|
|
15
|
+
.custom<
|
|
16
|
+
ExportedHandlerFetchHandler<{ RIVET: Client<any> }, unknown>
|
|
17
|
+
>()
|
|
18
|
+
.optional(),
|
|
19
|
+
});
|
|
20
|
+
export const ConfigSchema = ConfigSchemaBase.default(() =>
|
|
21
|
+
ConfigSchemaBase.parse({}),
|
|
22
|
+
);
|
|
11
23
|
export type InputConfig = z.input<typeof ConfigSchema>;
|
|
12
24
|
export type Config = z.infer<typeof ConfigSchema>;
|