@methodacting/actor-kit 0.47.0

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.
Files changed (79) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +2042 -0
  3. package/dist/browser.d.ts +384 -0
  4. package/dist/browser.js +2 -0
  5. package/dist/browser.js.map +1 -0
  6. package/dist/index.d.ts +644 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/react.d.ts +416 -0
  10. package/dist/react.js +2 -0
  11. package/dist/react.js.map +1 -0
  12. package/dist/src/alarms.d.ts +47 -0
  13. package/dist/src/alarms.d.ts.map +1 -0
  14. package/dist/src/browser.d.ts +2 -0
  15. package/dist/src/browser.d.ts.map +1 -0
  16. package/dist/src/constants.d.ts +12 -0
  17. package/dist/src/constants.d.ts.map +1 -0
  18. package/dist/src/createAccessToken.d.ts +9 -0
  19. package/dist/src/createAccessToken.d.ts.map +1 -0
  20. package/dist/src/createActorFetch.d.ts +18 -0
  21. package/dist/src/createActorFetch.d.ts.map +1 -0
  22. package/dist/src/createActorKitClient.d.ts +13 -0
  23. package/dist/src/createActorKitClient.d.ts.map +1 -0
  24. package/dist/src/createActorKitContext.d.ts +29 -0
  25. package/dist/src/createActorKitContext.d.ts.map +1 -0
  26. package/dist/src/createActorKitMockClient.d.ts +11 -0
  27. package/dist/src/createActorKitMockClient.d.ts.map +1 -0
  28. package/dist/src/createActorKitRouter.d.ts +4 -0
  29. package/dist/src/createActorKitRouter.d.ts.map +1 -0
  30. package/dist/src/createMachineServer.d.ts +20 -0
  31. package/dist/src/createMachineServer.d.ts.map +1 -0
  32. package/dist/src/durable-object-system.d.ts +36 -0
  33. package/dist/src/durable-object-system.d.ts.map +1 -0
  34. package/dist/src/index.d.ts +7 -0
  35. package/dist/src/index.d.ts.map +1 -0
  36. package/dist/src/react.d.ts +2 -0
  37. package/dist/src/react.d.ts.map +1 -0
  38. package/dist/src/schemas.d.ts +312 -0
  39. package/dist/src/schemas.d.ts.map +1 -0
  40. package/dist/src/server.d.ts +3 -0
  41. package/dist/src/server.d.ts.map +1 -0
  42. package/dist/src/storage.d.ts +64 -0
  43. package/dist/src/storage.d.ts.map +1 -0
  44. package/dist/src/storybook.d.ts +13 -0
  45. package/dist/src/storybook.d.ts.map +1 -0
  46. package/dist/src/test.d.ts +2 -0
  47. package/dist/src/test.d.ts.map +1 -0
  48. package/dist/src/types.d.ts +181 -0
  49. package/dist/src/types.d.ts.map +1 -0
  50. package/dist/src/utils.d.ts +30 -0
  51. package/dist/src/utils.d.ts.map +1 -0
  52. package/dist/src/withActorKit.d.ts +9 -0
  53. package/dist/src/withActorKit.d.ts.map +1 -0
  54. package/dist/src/worker.d.ts +3 -0
  55. package/dist/src/worker.d.ts.map +1 -0
  56. package/package.json +87 -0
  57. package/src/alarms.ts +237 -0
  58. package/src/browser.ts +1 -0
  59. package/src/constants.ts +31 -0
  60. package/src/createAccessToken.ts +29 -0
  61. package/src/createActorFetch.ts +111 -0
  62. package/src/createActorKitClient.ts +224 -0
  63. package/src/createActorKitContext.tsx +228 -0
  64. package/src/createActorKitMockClient.ts +138 -0
  65. package/src/createActorKitRouter.ts +149 -0
  66. package/src/createMachineServer.ts +844 -0
  67. package/src/durable-object-system.ts +212 -0
  68. package/src/global.d.ts +7 -0
  69. package/src/index.ts +6 -0
  70. package/src/react.ts +1 -0
  71. package/src/schemas.ts +95 -0
  72. package/src/server.ts +3 -0
  73. package/src/storage.ts +404 -0
  74. package/src/storybook.ts +42 -0
  75. package/src/test.ts +1 -0
  76. package/src/types.ts +334 -0
  77. package/src/utils.ts +171 -0
  78. package/src/withActorKit.tsx +103 -0
  79. package/src/worker.ts +2 -0
@@ -0,0 +1,844 @@
1
+ // Import necessary dependencies and types
2
+ import { DurableObject } from "cloudflare:workers";
3
+ import { compare } from "fast-json-patch";
4
+ import {
5
+ Actor,
6
+ AnyEventObject,
7
+ createActor,
8
+ InputFrom,
9
+ matchesState,
10
+ SnapshotFrom,
11
+ StateValueFrom,
12
+ Subscription,
13
+ } from "xstate";
14
+ import { xstateMigrate } from "xstate-migrate";
15
+ import { z } from "zod";
16
+ import { AlarmTypes, PERSISTED_SNAPSHOT_KEY } from "./constants";
17
+ import { CallerSchema } from "./schemas";
18
+ import {
19
+ ActorKitInputProps,
20
+ ActorKitStateMachine,
21
+ ActorKitSystemEvent,
22
+ ActorServer,
23
+ Caller,
24
+ CallerSnapshotFrom,
25
+ ClientEventFrom,
26
+ EnvFromMachine,
27
+ MachineServerOptions,
28
+ ServiceEventFrom,
29
+ WithActorKitContext,
30
+ WithActorKitEvent,
31
+ } from "./types";
32
+ import { ActorKitStorage } from "./storage";
33
+ import { AlarmManager, generateAlarmId } from "./alarms";
34
+ import {
35
+ createAlarmScheduler,
36
+ handleXStateAlarm,
37
+ restoreScheduledEvents,
38
+ } from "./durable-object-system";
39
+ import { assert, getCallerFromRequest } from "./utils";
40
+
41
+ // Define schemas for storage and WebSocket attachments
42
+ const StorageSchema = z.object({
43
+ actorType: z.string(),
44
+ actorId: z.string(),
45
+ initialCaller: CallerSchema,
46
+ input: z.record(z.unknown()),
47
+ });
48
+
49
+ const WebSocketAttachmentSchema = z.object({
50
+ caller: CallerSchema,
51
+ lastSentChecksum: z.string().optional(),
52
+ });
53
+ type WebSocketAttachment = z.infer<typeof WebSocketAttachmentSchema>;
54
+
55
+ /**
56
+ * Creates a MachineServer class that extends DurableObject and implements ActorServer.
57
+ * This function is the main entry point for creating a machine server.
58
+ */
59
+ export const createMachineServer = <
60
+ TClientEvent extends AnyEventObject,
61
+ TServiceEvent extends AnyEventObject,
62
+ TInputSchema extends z.ZodObject<z.ZodRawShape>,
63
+ TMachine extends ActorKitStateMachine<
64
+ (
65
+ | WithActorKitEvent<TClientEvent, "client">
66
+ | WithActorKitEvent<TServiceEvent, "service">
67
+ | ActorKitSystemEvent
68
+ ) & {
69
+ storage: DurableObjectStorage;
70
+ env: EnvFromMachine<TMachine>;
71
+ },
72
+ z.infer<TInputSchema> & {
73
+ id: string;
74
+ caller: Caller;
75
+ storage: DurableObjectStorage;
76
+ },
77
+ WithActorKitContext<any, any, any>
78
+ >
79
+ >({
80
+ machine,
81
+ schemas,
82
+ options,
83
+ }: {
84
+ machine: TMachine;
85
+ schemas: {
86
+ clientEvent: z.ZodSchema<TClientEvent>;
87
+ serviceEvent: z.ZodSchema<TServiceEvent>;
88
+ inputProps: TInputSchema;
89
+ };
90
+ options?: MachineServerOptions;
91
+ }): new (
92
+ state: DurableObjectState,
93
+ env: EnvFromMachine<TMachine>,
94
+ ctx: ExecutionContext
95
+ ) => ActorServer<TMachine> =>
96
+ class MachineServerImpl
97
+ extends DurableObject
98
+ implements ActorServer<TMachine>
99
+ {
100
+ // Class properties
101
+ actor: Actor<TMachine> | undefined;
102
+ actorType: string | undefined;
103
+ actorId: string | undefined;
104
+ input: Record<string, unknown> | undefined;
105
+ initialCaller: Caller | undefined;
106
+ lastPersistedSnapshot: SnapshotFrom<TMachine> | null = null;
107
+ lastSnapshotChecksum: string | null = null;
108
+ snapshotCache: Map<
109
+ string,
110
+ { snapshot: SnapshotFrom<TMachine>; timestamp: number }
111
+ > = new Map();
112
+ state: DurableObjectState;
113
+ storage: ActorKitStorage; // Now uses SQLite storage wrapper
114
+ kvStorage: DurableObjectStorage; // Keep reference for backward compatibility
115
+ attachments: Map<WebSocket, WebSocketAttachment>;
116
+ subscriptions: Map<WebSocket, Subscription>;
117
+ env: EnvFromMachine<TMachine>;
118
+ currentChecksum: string | null = null;
119
+ alarmManager: AlarmManager | null = null; // Alarm manager for delayed events
120
+ private enableAlarms: boolean;
121
+
122
+ /**
123
+ * Constructor for the MachineServerImpl class.
124
+ * Initializes the server and sets up WebSocket connections.
125
+ */
126
+ constructor(
127
+ state: DurableObjectState,
128
+ env: EnvFromMachine<TMachine>,
129
+ ctx: ExecutionContext
130
+ ) {
131
+ super(state, env);
132
+ this.state = state;
133
+ this.kvStorage = state.storage;
134
+ this.storage = new ActorKitStorage(state.storage);
135
+ this.env = env;
136
+ this.attachments = new Map();
137
+ this.subscriptions = new Map();
138
+ this.enableAlarms = options?.enableAlarms !== false;
139
+
140
+ // Initialize alarm manager if alarms are enabled
141
+ if (this.enableAlarms) {
142
+ this.alarmManager = new AlarmManager(this.storage, state);
143
+ }
144
+
145
+ // Initialize actor data from storage
146
+ this.state.blockConcurrencyWhile(async () => {
147
+ // Try to load from SQLite first (new format)
148
+ const actorMeta = await this.storage.getActorMeta();
149
+
150
+ if (actorMeta) {
151
+ // Loaded from SQLite
152
+ this.actorType = actorMeta.actorType;
153
+ this.actorId = actorMeta.actorId;
154
+ this.initialCaller = actorMeta.initialCaller;
155
+ this.input = actorMeta.input;
156
+ } else {
157
+ // Try legacy KV format for migration
158
+ const [actorType, actorId, initialCallerString, inputString] =
159
+ await Promise.all([
160
+ this.kvStorage.get("actorType"),
161
+ this.kvStorage.get("actorId"),
162
+ this.kvStorage.get("initialCaller"),
163
+ this.kvStorage.get("input"),
164
+ ]);
165
+
166
+ if (actorType && actorId && initialCallerString && inputString) {
167
+ try {
168
+ const parsedData = StorageSchema.parse({
169
+ actorType,
170
+ actorId,
171
+ initialCaller: JSON.parse(
172
+ initialCallerString as string
173
+ ) as Caller,
174
+ input: JSON.parse(inputString as string),
175
+ });
176
+
177
+ this.actorType = parsedData.actorType;
178
+ this.actorId = parsedData.actorId;
179
+ this.initialCaller = parsedData.initialCaller;
180
+ this.input = parsedData.input;
181
+
182
+ // Migrate to SQLite
183
+ await this.storage.setActorMeta({
184
+ actorId: this.actorId,
185
+ actorType: this.actorType,
186
+ initialCaller: this.initialCaller,
187
+ input: this.input,
188
+ });
189
+ } catch (error) {
190
+ console.error("Failed to parse stored data:", error);
191
+ }
192
+ }
193
+ }
194
+
195
+ if (this.actorId) {
196
+ console.debug(
197
+ `[${this.actorId}] Attempting to load actor data from storage`
198
+ );
199
+
200
+ if (options?.persisted) {
201
+ // Try SQLite first, then fall back to KV
202
+ let persistedSnapshot = await this.loadPersistedSnapshotFromSQLite();
203
+ if (!persistedSnapshot) {
204
+ persistedSnapshot = await this.loadPersistedSnapshotFromKV();
205
+ }
206
+
207
+ if (persistedSnapshot) {
208
+ await this.restorePersistedActor(persistedSnapshot);
209
+ } else {
210
+ this.#ensureActorRunning();
211
+ }
212
+ } else {
213
+ this.#ensureActorRunning();
214
+ }
215
+ }
216
+
217
+ // Resume all existing WebSockets
218
+ this.state.getWebSockets().forEach((ws) => {
219
+ this.#subscribeSocketToActor(ws);
220
+ });
221
+
222
+ // Schedule cache cleanup alarm if alarms are enabled
223
+ if (this.alarmManager) {
224
+ await this.#scheduleCacheCleanupAlarm();
225
+ }
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Ensures that the actor is running. If not, it creates and initializes the actor.
231
+ * @private
232
+ */
233
+ #ensureActorRunning() {
234
+ assert(this.actorId, "actorId is not set");
235
+ assert(this.actorType, "actorType is not set");
236
+ assert(this.input, "input is not set");
237
+ assert(this.initialCaller, "initialCaller is not set");
238
+
239
+ if (!this.actor) {
240
+ console.debug(`[${this.actorId}] Creating new actor`);
241
+ const input = {
242
+ id: this.actorId,
243
+ caller: this.initialCaller,
244
+ env: this.env,
245
+ storage: this.kvStorage, // Use KV storage for machine compatibility
246
+ ...this.input,
247
+ } satisfies ActorKitInputProps;
248
+
249
+ // Create actor with durable object system if alarms are enabled
250
+ const actorOptions: any = { input };
251
+
252
+ this.actor = createActor(machine, actorOptions);
253
+
254
+ // Monkey patch the scheduler to use alarms if alarm manager is available
255
+ if (this.alarmManager && this.actor.system) {
256
+ console.debug(`[${this.actorId}] Replacing scheduler with alarm-based scheduler`);
257
+ this.actor.system.scheduler = createAlarmScheduler(
258
+ this.alarmManager,
259
+ this.actor.system
260
+ );
261
+ }
262
+
263
+ if (options?.persisted) {
264
+ console.debug(
265
+ `[${this.actorId}] Setting up persistence for new actor`
266
+ );
267
+ this.#setupStatePersistence(this.actor);
268
+ }
269
+
270
+ this.actor.start();
271
+ console.debug(`[${this.actorId}] New actor started`);
272
+ }
273
+ return this.actor;
274
+ }
275
+
276
+ #subscribeSocketToActor(ws: WebSocket) {
277
+ try {
278
+ const attachment = WebSocketAttachmentSchema.parse(
279
+ ws.deserializeAttachment()
280
+ );
281
+ this.attachments.set(ws, attachment);
282
+
283
+ // Send initial state update
284
+ this.#sendStateUpdate(ws);
285
+
286
+ // Set up subscription for this WebSocket
287
+ const sub = this.actor!.subscribe((snapshot) => {
288
+ this.#sendStateUpdate(ws);
289
+ });
290
+ this.subscriptions.set(ws, sub);
291
+ } catch (error) {
292
+ console.error("Failed to subscribe WebSocket to actor:", error);
293
+ // Optionally, handle the error (e.g., close the WebSocket)
294
+ }
295
+ }
296
+
297
+ #sendStateUpdate(ws: WebSocket) {
298
+ assert(this.actor, "actor is not running");
299
+ const attachment = this.attachments.get(ws);
300
+ assert(attachment, "Attachment missing for WebSocket");
301
+
302
+ const fullSnapshot = this.actor.getSnapshot();
303
+ const currentChecksum = this.#calculateChecksum(fullSnapshot);
304
+
305
+ // Store snapshot in cache with timestamp
306
+ this.snapshotCache.set(currentChecksum, {
307
+ snapshot: fullSnapshot,
308
+ timestamp: Date.now(),
309
+ });
310
+
311
+ // Update current checksum
312
+ this.currentChecksum = currentChecksum;
313
+
314
+ // Only send updates if the checksum has changed
315
+ if (attachment.lastSentChecksum !== currentChecksum) {
316
+ const nextSnapshot = this.#createCallerSnapshot(
317
+ fullSnapshot,
318
+ attachment.caller.id
319
+ );
320
+ let lastSnapshot = {};
321
+ if (attachment.lastSentChecksum) {
322
+ const cachedData = this.snapshotCache.get(
323
+ attachment.lastSentChecksum
324
+ );
325
+ if (cachedData) {
326
+ lastSnapshot = this.#createCallerSnapshot(
327
+ cachedData.snapshot,
328
+ attachment.caller.id
329
+ );
330
+ }
331
+ }
332
+
333
+ const operations = compare(lastSnapshot, nextSnapshot);
334
+
335
+ if (operations.length) {
336
+ ws.send(JSON.stringify({ operations, checksum: currentChecksum }));
337
+ attachment.lastSentChecksum = currentChecksum;
338
+ ws.serializeAttachment(attachment);
339
+ }
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Sets up state persistence for the actor if the persisted option is enabled.
345
+ * @private
346
+ */
347
+ #setupStatePersistence(actor: Actor<TMachine>) {
348
+ console.debug(`[${this.actorId}] Setting up state persistence`);
349
+ actor.subscribe((state) => {
350
+ const fullSnapshot = actor.getSnapshot();
351
+ if (fullSnapshot) {
352
+ this.#persistSnapshot(fullSnapshot);
353
+ }
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Persists the given snapshot if it's different from the last persisted snapshot.
359
+ * @private
360
+ */
361
+ async #persistSnapshot(snapshot: SnapshotFrom<TMachine>) {
362
+ try {
363
+ if (
364
+ !this.lastPersistedSnapshot ||
365
+ compare(this.lastPersistedSnapshot, snapshot).length > 0
366
+ ) {
367
+ console.debug(`[${this.actorId}] Persisting new snapshot`);
368
+
369
+ // Calculate checksum
370
+ const checksum = this.#calculateChecksum(snapshot);
371
+
372
+ // Save to SQLite (new format)
373
+ if (this.actorId) {
374
+ await this.storage.setSnapshot(this.actorId, snapshot, checksum);
375
+ }
376
+
377
+ // Also save to KV for backward compatibility during migration
378
+ await this.kvStorage.put(
379
+ PERSISTED_SNAPSHOT_KEY,
380
+ JSON.stringify(snapshot)
381
+ );
382
+
383
+ this.lastPersistedSnapshot = snapshot;
384
+ } else {
385
+ console.debug(
386
+ `[${this.actorId}] No changes in snapshot, skipping persistence`
387
+ );
388
+ }
389
+ } catch (error) {
390
+ console.error(`[${this.actorId}] Error persisting snapshot:`, error);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Handles incoming HTTP requests and sets up WebSocket connections.
396
+ */
397
+ async fetch(request: Request): Promise<Response> {
398
+ const actor = this.#ensureActorRunning();
399
+ assert(this.actorType, "actorType is not set");
400
+ assert(this.actorId, "actorId is not set");
401
+
402
+ const webSocketPair = new WebSocketPair();
403
+ const [client, server] = Object.values(webSocketPair);
404
+
405
+ let caller: Caller | undefined;
406
+ try {
407
+ caller = await getCallerFromRequest(
408
+ request,
409
+ this.actorType,
410
+ this.actorId,
411
+ this.env.ACTOR_KIT_SECRET
412
+ );
413
+ } catch (error: any) {
414
+ return new Response(`Error: ${error.message}`, { status: 401 });
415
+ }
416
+
417
+ if (!caller) {
418
+ return new Response("Unauthorized", { status: 401 });
419
+ }
420
+
421
+ // Parse the checksum from the request, if provided
422
+ const url = new URL(request.url);
423
+ const clientChecksum = url.searchParams.get("checksum");
424
+
425
+ this.state.acceptWebSocket(server);
426
+ const initialAttachment = {
427
+ caller,
428
+ lastSentChecksum: clientChecksum ?? undefined,
429
+ };
430
+ server.serializeAttachment(initialAttachment);
431
+
432
+ // Subscribe the new WebSocket to the actor
433
+ this.#subscribeSocketToActor(server);
434
+
435
+ return new Response(null, {
436
+ status: 101,
437
+ webSocket: client,
438
+ });
439
+ }
440
+
441
+ /**
442
+ * Handles incoming WebSocket messages.
443
+ */
444
+ async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
445
+ const attachment = this.attachments.get(ws);
446
+ assert(attachment, "Attachment missing for WebSocket");
447
+
448
+ let event: ClientEventFrom<TMachine> | ServiceEventFrom<TMachine>;
449
+
450
+ const { caller } = attachment;
451
+ if (caller.type === "client") {
452
+ const clientEvent = schemas.clientEvent.parse(
453
+ JSON.parse(message as string)
454
+ );
455
+ event = {
456
+ ...clientEvent,
457
+ caller,
458
+ } as ClientEventFrom<TMachine>;
459
+ } else if (caller.type === "service") {
460
+ const serviceEvent = schemas.serviceEvent.parse(
461
+ JSON.parse(message as string)
462
+ );
463
+ event = {
464
+ ...serviceEvent,
465
+ caller,
466
+ } as ServiceEventFrom<TMachine>;
467
+ } else {
468
+ throw new Error(`Unknown caller type: ${caller.type}`);
469
+ }
470
+
471
+ this.send(event);
472
+ }
473
+
474
+ /**
475
+ * Handles WebSocket errors.
476
+ */
477
+ async webSocketError(ws: WebSocket, error: Error) {
478
+ console.error(
479
+ "[MachineServerImpl] WebSocket error:",
480
+ error.message,
481
+ error.stack
482
+ );
483
+ }
484
+
485
+ /**
486
+ * Handles WebSocket closure.
487
+ */
488
+ async webSocketClose(
489
+ ws: WebSocket,
490
+ code: number,
491
+ reason: string,
492
+ wasClean: boolean
493
+ ) {
494
+ ws.close(code, "Durable Object is closing WebSocket");
495
+ // Remove the subscription for the socket
496
+ const subscription = this.subscriptions.get(ws);
497
+ if (subscription) {
498
+ subscription.unsubscribe();
499
+ this.subscriptions.delete(ws);
500
+ }
501
+ // Remove the attachment for the socket
502
+ this.attachments.delete(ws);
503
+ }
504
+
505
+ /**
506
+ * Sends an event to the actor.
507
+ */
508
+ send(event: ClientEventFrom<TMachine> | ServiceEventFrom<TMachine>): void {
509
+ assert(this.actor, "Actor is not running");
510
+ this.actor.send({
511
+ ...event,
512
+ env: this.env,
513
+ storage: this.storage,
514
+ });
515
+ }
516
+
517
+ /**
518
+ * Retrieves a snapshot of the actor's state for a specific caller.
519
+ * @param caller The caller requesting the snapshot.
520
+ * @returns An object containing the caller-specific snapshot and a checksum for the full snapshot.
521
+ */
522
+ async getSnapshot(
523
+ caller: Caller,
524
+ options?: {
525
+ waitForEvent?: ClientEventFrom<TMachine>;
526
+ waitForState?: StateValueFrom<TMachine>;
527
+ timeout?: number;
528
+ errorOnWaitTimeout?: boolean;
529
+ }
530
+ ): Promise<{
531
+ checksum: string;
532
+ snapshot: CallerSnapshotFrom<TMachine>;
533
+ }> {
534
+ this.#ensureActorRunning();
535
+
536
+ if (options?.waitForEvent || options?.waitForState) {
537
+ const timeoutPromise = new Promise((resolve, reject) => {
538
+ setTimeout(() => {
539
+ if (options.errorOnWaitTimeout !== false) {
540
+ reject(new Error("Timeout waiting for event or state"));
541
+ } else {
542
+ resolve(this.#getCurrentSnapshot(caller));
543
+ }
544
+ }, options.timeout || 5000);
545
+ });
546
+
547
+ const waitPromise: Promise<{
548
+ checksum: string;
549
+ snapshot: CallerSnapshotFrom<TMachine>;
550
+ }> = new Promise((resolve) => {
551
+ const sub = this.actor!.subscribe((state) => {
552
+ if (
553
+ (options.waitForEvent &&
554
+ this.#matchesEvent(state, options.waitForEvent)) ||
555
+ (options.waitForState &&
556
+ this.#matchesState(state, options.waitForState))
557
+ ) {
558
+ sub && sub.unsubscribe();
559
+ resolve(this.#getCurrentSnapshot(caller));
560
+ }
561
+ });
562
+ });
563
+
564
+ return Promise.race([waitPromise, timeoutPromise]) as Promise<{
565
+ checksum: string;
566
+ snapshot: CallerSnapshotFrom<TMachine>;
567
+ }>;
568
+ }
569
+
570
+ // const checksum =
571
+ return this.#getCurrentSnapshot(caller);
572
+ }
573
+
574
+ #getCurrentSnapshot(caller: Caller) {
575
+ const fullSnapshot = this.actor!.getSnapshot();
576
+ const callerSnapshot = this.#createCallerSnapshot(
577
+ fullSnapshot,
578
+ caller.id
579
+ );
580
+ const checksum = this.#calculateChecksum(fullSnapshot);
581
+ return { snapshot: callerSnapshot, checksum };
582
+ }
583
+
584
+ #matchesEvent(
585
+ snapshot: SnapshotFrom<TMachine>,
586
+ event: ClientEventFrom<TMachine>
587
+ ): boolean {
588
+ // todo implement later
589
+ return true;
590
+ }
591
+
592
+ #matchesState(
593
+ snapshot: SnapshotFrom<TMachine>,
594
+ stateValue: StateValueFrom<TMachine>
595
+ ): boolean {
596
+ return matchesState(stateValue, snapshot);
597
+ }
598
+
599
+ /**
600
+ * Calculates a checksum for the given snapshot.
601
+ * @private
602
+ */
603
+ #calculateChecksum(snapshot: SnapshotFrom<TMachine>): string {
604
+ const snapshotString = JSON.stringify(snapshot);
605
+ return this.#hashString(snapshotString);
606
+ }
607
+
608
+ /**
609
+ * Generates a simple hash for a given string.
610
+ * @private
611
+ */
612
+ #hashString(str: string): string {
613
+ let hash = 0;
614
+ for (let i = 0; i < str.length; i++) {
615
+ const char = str.charCodeAt(i);
616
+ hash = (hash << 5) - hash + char;
617
+ hash = hash & hash; // Convert to 32-bit integer
618
+ }
619
+ return hash.toString(16); // Convert to hexadecimal
620
+ }
621
+
622
+ /**
623
+ * Creates a caller-specific snapshot from the full snapshot.
624
+ * @private
625
+ */
626
+ #createCallerSnapshot(
627
+ fullSnapshot: SnapshotFrom<TMachine>,
628
+ callerId: string
629
+ ): CallerSnapshotFrom<TMachine> {
630
+ const snap = fullSnapshot as any;
631
+ assert(snap.value, "expected value");
632
+ assert(snap.context.public, "expected public key in context");
633
+ assert(snap.context.private, "expected private key in context");
634
+ return {
635
+ public: snap.context.public,
636
+ private: snap.context.private[callerId] || {},
637
+ value: snap.value,
638
+ };
639
+ }
640
+
641
+ /**
642
+ * Spawns a new actor with the given properties.
643
+ */
644
+ async spawn(props: {
645
+ actorType: string;
646
+ actorId: string;
647
+ caller: Caller;
648
+ input: Record<string, unknown>;
649
+ }) {
650
+ if (!this.actorType && !this.actorId && !this.initialCaller) {
651
+ // Store actor data in SQLite storage
652
+ await this.storage.setActorMeta({
653
+ actorId: props.actorId,
654
+ actorType: props.actorType,
655
+ initialCaller: props.caller,
656
+ input: props.input,
657
+ }).catch((error) => {
658
+ console.error("Error storing actor data:", error);
659
+ });
660
+
661
+ // Update the instance properties
662
+ this.actorType = props.actorType;
663
+ this.actorId = props.actorId;
664
+ this.initialCaller = props.caller;
665
+ this.input = props.input;
666
+
667
+ this.#ensureActorRunning();
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Schedule the cache cleanup alarm
673
+ * @private
674
+ */
675
+ async #scheduleCacheCleanupAlarm() {
676
+ if (!this.alarmManager) return;
677
+
678
+ const CLEANUP_INTERVAL = 300000; // 5 minutes
679
+ const alarmId = generateAlarmId();
680
+
681
+ await this.alarmManager.schedule({
682
+ id: alarmId,
683
+ type: AlarmTypes["cache-cleanup"],
684
+ scheduledAt: Date.now() + CLEANUP_INTERVAL,
685
+ repeatInterval: CLEANUP_INTERVAL, // Recurring
686
+ payload: {},
687
+ });
688
+ }
689
+
690
+ /**
691
+ * Handle cache cleanup alarm
692
+ * @private
693
+ */
694
+ async #handleCacheCleanupAlarm() {
695
+ const now = Date.now();
696
+ const CLEANUP_AGE = 300000; // 5 minutes
697
+
698
+ for (const [checksum, { timestamp }] of this.snapshotCache.entries()) {
699
+ if (now - timestamp > CLEANUP_AGE) {
700
+ this.snapshotCache.delete(checksum);
701
+ }
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Load persisted snapshot from SQLite storage
707
+ * @private
708
+ */
709
+ async loadPersistedSnapshotFromSQLite(): Promise<SnapshotFrom<TMachine> | null> {
710
+ if (!this.actorId) return null;
711
+
712
+ const snapshotData = await this.storage.getSnapshot(this.actorId);
713
+ if (snapshotData) {
714
+ console.debug(`[${this.actorId}] Loaded persisted snapshot from SQLite`);
715
+ return snapshotData.snapshot as SnapshotFrom<TMachine>;
716
+ }
717
+ return null;
718
+ }
719
+
720
+ /**
721
+ * Load persisted snapshot from KV storage (legacy)
722
+ * @private
723
+ */
724
+ async loadPersistedSnapshotFromKV(): Promise<SnapshotFrom<TMachine> | null> {
725
+ const snapshotString = await this.kvStorage.get(PERSISTED_SNAPSHOT_KEY);
726
+ if (snapshotString) {
727
+ console.debug(`[${this.actorId}] Loaded persisted snapshot from KV`);
728
+ return JSON.parse(snapshotString as string);
729
+ }
730
+ return null;
731
+ }
732
+
733
+ // Add this method to restore the persisted actor
734
+ async restorePersistedActor(persistedSnapshot: SnapshotFrom<TMachine>) {
735
+ console.debug(
736
+ `[${this.actorId}] Restoring persisted actor from `,
737
+ persistedSnapshot
738
+ );
739
+ assert(this.actorId, "actorId is not set");
740
+ assert(this.actorType, "actorType is not set");
741
+ assert(this.initialCaller, "initialCaller is not set");
742
+ assert(this.input, "input is not set");
743
+
744
+ const input = {
745
+ id: this.actorId,
746
+ caller: this.initialCaller,
747
+ storage: this.kvStorage, // Use KV storage for machine compatibility
748
+ env: this.env,
749
+ ...this.input,
750
+ } as InputFrom<TMachine>;
751
+
752
+ const migrations = xstateMigrate.generateMigrations(
753
+ machine,
754
+ persistedSnapshot,
755
+ input
756
+ );
757
+ const restoredSnapshot = xstateMigrate.applyMigrations(
758
+ persistedSnapshot,
759
+ migrations
760
+ );
761
+
762
+ // Create actor options with system provider if alarms are enabled
763
+ const actorOptions: any = {
764
+ snapshot: restoredSnapshot,
765
+ input,
766
+ };
767
+
768
+ this.actor = createActor(machine, actorOptions);
769
+
770
+ // Monkey patch the scheduler to use alarms if alarm manager is available
771
+ if (this.alarmManager && this.actor.system) {
772
+ console.debug(`[${this.actorId}] Replacing scheduler with alarm-based scheduler (restored)`);
773
+ this.actor.system.scheduler = createAlarmScheduler(
774
+ this.alarmManager,
775
+ this.actor.system
776
+ );
777
+
778
+ // Restore any scheduled events from alarm storage
779
+ const pendingAlarms = await this.alarmManager.getPendingAlarms();
780
+ const xstateAlarms = pendingAlarms.filter((a) => a.type === "xstate-delay");
781
+ if (xstateAlarms.length > 0) {
782
+ restoreScheduledEvents(xstateAlarms.map((a) => ({
783
+ payload: a.payload as any,
784
+ scheduledAt: a.scheduledAt,
785
+ })));
786
+ }
787
+ }
788
+
789
+ if (options?.persisted) {
790
+ console.debug(
791
+ `[${this.actorId}] Setting up persistence for restored actor`
792
+ );
793
+ this.#setupStatePersistence(this.actor);
794
+ }
795
+
796
+ this.actor.start();
797
+ console.debug(`[${this.actorId}] Restored actor started`);
798
+
799
+ this.actor.send({
800
+ type: "RESUME",
801
+ caller: { id: this.actorId, type: "system" },
802
+ env: this.env,
803
+ storage: this.kvStorage,
804
+ } as any);
805
+ console.debug(`[${this.actorId}] Sent RESUME event to restored actor`);
806
+
807
+ this.lastPersistedSnapshot = restoredSnapshot as any;
808
+ }
809
+
810
+ /**
811
+ * Durable Object alarm handler
812
+ * Called when a scheduled alarm fires
813
+ */
814
+ async alarm(): Promise<void> {
815
+ if (!this.alarmManager) {
816
+ console.warn("Alarm fired but alarmManager is not initialized");
817
+ return;
818
+ }
819
+
820
+ console.debug(`[${this.actorId}] Alarm fired, processing due alarms`);
821
+
822
+ await this.alarmManager.handleDueAlarms(async (alarm) => {
823
+ switch (alarm.type) {
824
+ case AlarmTypes["cache-cleanup"]:
825
+ await this.#handleCacheCleanupAlarm();
826
+ return true;
827
+ case AlarmTypes["xstate-delay"]:
828
+ // Handle XState delayed events
829
+ if (this.actor) {
830
+ const eventData = alarm.payload as any;
831
+ console.debug(
832
+ `[${this.actorId}] Processing XState delayed event: ${eventData.scheduledEventId}`
833
+ );
834
+ // Use the handleXStateAlarm function to deliver the event
835
+ await handleXStateAlarm(eventData, this.actor);
836
+ }
837
+ return true;
838
+ default:
839
+ console.warn(`[${this.actorId}] Unknown alarm type: ${alarm.type}`);
840
+ return true;
841
+ }
842
+ });
843
+ }
844
+ };