@effect-agent/storage-sqlite 0.1.0-beta.6 → 0.1.0-beta.60

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 (59) hide show
  1. package/dist/SqliteActivityStore.d.mts +14 -0
  2. package/dist/SqliteActivityStore.mjs +310 -0
  3. package/dist/SqliteActivityStore.mjs.map +1 -0
  4. package/dist/SqliteMessageDeliveryStore.d.mts +14 -0
  5. package/dist/SqliteMessageDeliveryStore.mjs +24 -0
  6. package/dist/SqliteMessageDeliveryStore.mjs.map +1 -0
  7. package/dist/SqliteScheduleStore.d.mts +14 -0
  8. package/dist/SqliteScheduleStore.mjs +255 -0
  9. package/dist/SqliteScheduleStore.mjs.map +1 -0
  10. package/dist/SqliteStorageConfig.d.mts +34 -0
  11. package/dist/SqliteStorageConfig.mjs +39 -0
  12. package/dist/SqliteStorageConfig.mjs.map +1 -0
  13. package/dist/SqliteStorageError-CzKmwCLl.d.mts +86 -0
  14. package/dist/SqliteStorageError.d.mts +2 -0
  15. package/dist/SqliteStorageError.mjs +148 -0
  16. package/dist/SqliteStorageError.mjs.map +1 -0
  17. package/dist/SqliteStorageFailpoint.d.mts +17 -0
  18. package/dist/SqliteStorageFailpoint.mjs +14 -0
  19. package/dist/SqliteStorageFailpoint.mjs.map +1 -0
  20. package/dist/SqliteStorageFailpointTesting.d.mts +15 -0
  21. package/dist/SqliteStorageFailpointTesting.mjs +19 -0
  22. package/dist/SqliteStorageFailpointTesting.mjs.map +1 -0
  23. package/dist/SqliteStorageVersion-gA7_96xM.d.mts +9 -0
  24. package/dist/SqliteStorageVersion.d.mts +2 -0
  25. package/dist/SqliteStorageVersion.mjs +8 -0
  26. package/dist/SqliteStorageVersion.mjs.map +1 -0
  27. package/dist/SqliteSubmissionLedger.d.mts +23 -0
  28. package/dist/SqliteSubmissionLedger.mjs +1765 -0
  29. package/dist/SqliteSubmissionLedger.mjs.map +1 -0
  30. package/dist/SqliteSubscriptionStore.d.mts +13 -0
  31. package/dist/SqliteSubscriptionStore.mjs +28 -0
  32. package/dist/SqliteSubscriptionStore.mjs.map +1 -0
  33. package/dist/SqliteThreadStore.d.mts +49 -0
  34. package/dist/SqliteThreadStore.mjs +427 -0
  35. package/dist/SqliteThreadStore.mjs.map +1 -0
  36. package/dist/index.d.mts +11 -193
  37. package/dist/index.mjs +11 -3082
  38. package/dist/migrations-dnTR0RKq.mjs +326 -0
  39. package/dist/migrations-dnTR0RKq.mjs.map +1 -0
  40. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  41. package/dist/sqlite-journal-lDduEakl.mjs +887 -0
  42. package/dist/sqlite-journal-lDduEakl.mjs.map +1 -0
  43. package/package.json +1 -41
  44. package/src/SqliteActivityStore.ts +556 -0
  45. package/src/SqliteMessageDeliveryStore.ts +42 -0
  46. package/src/SqliteScheduleStore.ts +404 -0
  47. package/src/{sqlite-storage-config.ts → SqliteStorageConfig.ts} +1 -1
  48. package/src/{errors.ts → SqliteStorageError.ts} +8 -3
  49. package/src/SqliteStorageFailpoint.ts +23 -0
  50. package/src/{sqlite-storage-failpoint.ts → SqliteStorageFailpointTesting.ts} +7 -18
  51. package/src/SqliteStorageVersion.ts +2 -0
  52. package/src/{sqlite-ledger.ts → SqliteSubmissionLedger.ts} +753 -118
  53. package/src/SqliteSubscriptionStore.ts +59 -0
  54. package/src/{sqlite-conversation-store.ts → SqliteThreadStore.ts} +372 -302
  55. package/src/index.ts +10 -6
  56. package/src/internal/message-delivery-schema.ts +24 -0
  57. package/src/{migrations.ts → internal/migrations.ts} +145 -79
  58. package/src/{sqlite-journal.ts → internal/sqlite-journal.ts} +534 -222
  59. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,556 @@
1
+ import {
2
+ ActivityBusy,
3
+ ActivityClaim,
4
+ ActivityClaimRequest,
5
+ ActivityMutationFailpoint,
6
+ type ActivityMutationFailure,
7
+ ActivityOwnershipLost,
8
+ ActivityProcessorKey,
9
+ ActivityProcessorStore,
10
+ ActivityProgress,
11
+ ActivityStoreError,
12
+ ActivityWorkConflict,
13
+ PreparedActivity,
14
+ } from "@effect-agent/thread/ActivityStore";
15
+ import { Digest } from "@effect-agent/thread/Records";
16
+ import { Clock, Effect, Layer, Schema } from "effect";
17
+ import * as SqlClientService from "effect/unstable/sql/SqlClient";
18
+ import type { SqlError } from "effect/unstable/sql/SqlError";
19
+
20
+ const STORAGE_VERSION = 1 as const;
21
+ const METADATA_COMPONENT = "activity";
22
+ const STATE_TABLE = "effect_agent_activity_processor_state_v1";
23
+ const StoredJson = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
24
+ const sameKey = Schema.toEquivalence(ActivityProcessorKey);
25
+ const sameWork = Schema.toEquivalence(PreparedActivity);
26
+
27
+ class ActivityMetadataRow extends Schema.Class<ActivityMetadataRow>(
28
+ "@effect-agent/storage-sqlite/ActivityMetadataRow",
29
+ )({
30
+ version: Schema.Int,
31
+ }) {}
32
+
33
+ class ActivityTableRow extends Schema.Class<ActivityTableRow>(
34
+ "@effect-agent/storage-sqlite/ActivityTableRow",
35
+ )({
36
+ name: Schema.NonEmptyString,
37
+ }) {}
38
+
39
+ class ActivityStateRow extends Schema.Class<ActivityStateRow>(
40
+ "@effect-agent/storage-sqlite/ActivityStateRow",
41
+ )({
42
+ processor_id: ActivityProcessorKey.fields.processorId,
43
+ processor_version: ActivityProcessorKey.fields.processorVersion,
44
+ thread_id: ActivityProcessorKey.fields.threadId,
45
+ format_version: Schema.Int,
46
+ through_sequence: ActivityProgress.fields.throughSequence,
47
+ epoch: ActivityProgress.fields.epoch,
48
+ owner: ActivityProgress.fields.owner,
49
+ lease_expires_at: ActivityProgress.fields.leaseExpiresAt,
50
+ progress_json: StoredJson,
51
+ }) {}
52
+
53
+ class ActivityChangeCountRow extends Schema.Class<ActivityChangeCountRow>(
54
+ "@effect-agent/storage-sqlite/ActivityChangeCountRow",
55
+ )({
56
+ changed: Schema.Int,
57
+ }) {}
58
+
59
+ const StoredVersionHeader = Schema.Struct({ version: Schema.Int });
60
+
61
+ export type SqliteActivityInitializationError = ActivityStoreError | ActivityMutationFailure;
62
+
63
+ const storeError = (
64
+ operation: string,
65
+ reason: ActivityStoreError["reason"] = "unavailable",
66
+ ): ActivityStoreError => ActivityStoreError.make({ operation, reason });
67
+
68
+ const query = <A extends object>(
69
+ effect: Effect.Effect<ReadonlyArray<A>, SqlError>,
70
+ operation: string,
71
+ ) => effect.pipe(Effect.mapError(() => storeError(operation)));
72
+
73
+ const decodeRows = Effect.fn("SqliteActivityStore.decodeRows")(function* <A, I>(
74
+ schema: Schema.Codec<A, I, never>,
75
+ rows: ReadonlyArray<unknown>,
76
+ operation: string,
77
+ ): Effect.fn.Return<ReadonlyArray<A>, ActivityStoreError> {
78
+ return yield* Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(
79
+ Effect.mapError(() => storeError(operation, "corrupt")),
80
+ );
81
+ });
82
+
83
+ const decodeInput = Effect.fn("SqliteActivityStore.decodeInput")(function* <A, I>(
84
+ schema: Schema.Codec<A, I, never>,
85
+ value: unknown,
86
+ operation: string,
87
+ ): Effect.fn.Return<A, ActivityStoreError> {
88
+ return yield* Schema.decodeUnknownEffect(schema)(value).pipe(
89
+ Effect.mapError(() => storeError(operation, "invalid-input")),
90
+ );
91
+ });
92
+
93
+ const encodeProgress = Effect.fn("SqliteActivityStore.encodeProgress")(function* (
94
+ progress: ActivityProgress,
95
+ operation: string,
96
+ ): Effect.fn.Return<string, ActivityStoreError> {
97
+ return yield* Schema.encodeEffect(Schema.fromJsonString(ActivityProgress))(progress).pipe(
98
+ Effect.mapError(() => storeError(operation, "corrupt")),
99
+ Effect.flatMap((encoded) =>
100
+ Schema.decodeEffect(StoredJson)(encoded).pipe(
101
+ Effect.mapError(() => storeError(operation, "invalid-input")),
102
+ ),
103
+ ),
104
+ );
105
+ });
106
+
107
+ const decodeProgress = Effect.fn("SqliteActivityStore.decodeProgress")(function* (
108
+ value: string,
109
+ operation: string,
110
+ ): Effect.fn.Return<ActivityProgress, ActivityStoreError> {
111
+ const header = yield* Schema.decodeEffect(Schema.fromJsonString(StoredVersionHeader))(value).pipe(
112
+ Effect.mapError(() => storeError(operation, "corrupt")),
113
+ );
114
+
115
+ if (header.version !== STORAGE_VERSION) {
116
+ return yield* storeError(operation, "incompatible");
117
+ }
118
+
119
+ const progress = yield* Schema.decodeEffect(Schema.fromJsonString(ActivityProgress))(value).pipe(
120
+ Effect.mapError(() => storeError(operation, "corrupt")),
121
+ );
122
+
123
+ const canonical = yield* encodeProgress(progress, operation);
124
+
125
+ if (canonical !== value) return yield* storeError(operation, "corrupt");
126
+
127
+ return progress;
128
+ });
129
+
130
+ const validateProgress = Effect.fn("SqliteActivityStore.validateProgress")(function* (
131
+ progress: ActivityProgress,
132
+ operation: string,
133
+ ): Effect.fn.Return<ActivityProgress, ActivityStoreError> {
134
+ if (
135
+ progress.epoch < 1 ||
136
+ (progress.owner === null && progress.leaseExpiresAt !== 0) ||
137
+ (progress.pending !== null &&
138
+ (!sameKey(progress.pending.key, progress.key) ||
139
+ progress.pending.sequence !== progress.throughSequence + 1))
140
+ ) {
141
+ return yield* storeError(operation, "corrupt");
142
+ }
143
+
144
+ return progress;
145
+ });
146
+
147
+ const makeClaim = (progress: ActivityProgress): ActivityClaim | null =>
148
+ progress.owner === null
149
+ ? null
150
+ : ActivityClaim.make({
151
+ key: progress.key,
152
+ owner: progress.owner,
153
+ epoch: progress.epoch,
154
+ throughSequence: progress.throughSequence,
155
+ leaseExpiresAt: progress.leaseExpiresAt,
156
+ pending: progress.pending,
157
+ });
158
+
159
+ const ownershipLost = (claim: ActivityClaim) =>
160
+ ActivityOwnershipLost.make({ key: claim.key, owner: claim.owner, epoch: claim.epoch });
161
+
162
+ // The pinned Node SQLite client's writable withTransaction begins with BEGIN IMMEDIATE.
163
+ // Each mutation therefore locks before reading progress, which serializes independent
164
+ // connections without a deferred read-to-write upgrade.
165
+ const makeActivityStore = Effect.fn("SqliteActivityStore.make")(function* () {
166
+ const sql = yield* SqlClientService.SqlClient;
167
+ const failpoint = yield* ActivityMutationFailpoint;
168
+
169
+ yield* failpoint.hit("activity:initialize:before");
170
+ yield* sql
171
+ .withTransaction(
172
+ Effect.gen(function* () {
173
+ yield* sql`
174
+ CREATE TABLE IF NOT EXISTS effect_agent_activity_metadata (
175
+ component TEXT PRIMARY KEY NOT NULL,
176
+ version INTEGER NOT NULL
177
+ )
178
+ `;
179
+
180
+ const metadataRows = yield* sql<Record<string, unknown>>`
181
+ SELECT version FROM effect_agent_activity_metadata
182
+ WHERE component = ${METADATA_COMPONENT}
183
+ `;
184
+
185
+ const metadata = yield* decodeRows(
186
+ ActivityMetadataRow,
187
+ metadataRows,
188
+ "decode activity schema version",
189
+ );
190
+
191
+ if (metadata.length > 1) {
192
+ return yield* storeError("decode activity schema version", "corrupt");
193
+ }
194
+ const currentVersion = metadata[0]?.version;
195
+
196
+ if (currentVersion !== undefined && currentVersion !== STORAGE_VERSION) {
197
+ return yield* storeError("initialize activity schema", "incompatible");
198
+ }
199
+ if (currentVersion === undefined) {
200
+ const tableRows = yield* sql<Record<string, unknown>>`
201
+ SELECT name FROM sqlite_master
202
+ WHERE type = 'table' AND name = ${STATE_TABLE}
203
+ `;
204
+
205
+ const existing = yield* decodeRows(
206
+ ActivityTableRow,
207
+ tableRows,
208
+ "inspect activity schema",
209
+ );
210
+
211
+ if (existing.length > 0) {
212
+ return yield* storeError("initialize activity schema", "incompatible");
213
+ }
214
+ yield* sql`
215
+ CREATE TABLE effect_agent_activity_processor_state_v1 (
216
+ processor_id TEXT NOT NULL,
217
+ processor_version TEXT NOT NULL,
218
+ thread_id TEXT NOT NULL,
219
+ format_version INTEGER NOT NULL,
220
+ through_sequence INTEGER NOT NULL,
221
+ epoch INTEGER NOT NULL,
222
+ owner TEXT,
223
+ lease_expires_at REAL NOT NULL,
224
+ progress_json TEXT NOT NULL,
225
+ PRIMARY KEY (processor_id, processor_version, thread_id)
226
+ )
227
+ `;
228
+ yield* sql`
229
+ INSERT INTO effect_agent_activity_metadata (component, version)
230
+ VALUES (${METADATA_COMPONENT}, ${STORAGE_VERSION})
231
+ `;
232
+ }
233
+ yield* sql`
234
+ SELECT processor_id, processor_version, thread_id, format_version,
235
+ through_sequence, epoch, owner, lease_expires_at, progress_json
236
+ FROM effect_agent_activity_processor_state_v1
237
+ LIMIT 0
238
+ `;
239
+ }),
240
+ )
241
+ .pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError("initialize activity schema"))));
242
+ yield* failpoint.hit("activity:initialize:after");
243
+
244
+ const readProgress = Effect.fn("SqliteActivityStore.readProgress")(function* (
245
+ key: ActivityProcessorKey,
246
+ operation: string,
247
+ ): Effect.fn.Return<ActivityProgress | null, ActivityStoreError> {
248
+ const rawRows = yield* query(
249
+ sql<Record<string, unknown>>`
250
+ SELECT processor_id, processor_version, thread_id, format_version,
251
+ through_sequence, epoch, owner, lease_expires_at, progress_json
252
+ FROM effect_agent_activity_processor_state_v1
253
+ WHERE processor_id = ${key.processorId}
254
+ AND processor_version = ${key.processorVersion}
255
+ AND thread_id = ${key.threadId}
256
+ `,
257
+ operation,
258
+ );
259
+
260
+ const rows = yield* decodeRows(ActivityStateRow, rawRows, operation);
261
+
262
+ if (rows.length === 0) return null;
263
+ if (rows.length !== 1) return yield* storeError(operation, "corrupt");
264
+ const row = rows[0];
265
+
266
+ if (row.format_version !== STORAGE_VERSION) {
267
+ return yield* storeError(operation, "incompatible");
268
+ }
269
+ const progress = yield* decodeProgress(row.progress_json, operation);
270
+
271
+ yield* validateProgress(progress, operation);
272
+ if (
273
+ !sameKey(progress.key, key) ||
274
+ row.processor_id !== key.processorId ||
275
+ row.processor_version !== key.processorVersion ||
276
+ row.thread_id !== key.threadId ||
277
+ row.through_sequence !== progress.throughSequence ||
278
+ row.epoch !== progress.epoch ||
279
+ row.owner !== progress.owner ||
280
+ row.lease_expires_at !== progress.leaseExpiresAt
281
+ ) {
282
+ return yield* storeError(operation, "corrupt");
283
+ }
284
+
285
+ return progress;
286
+ });
287
+
288
+ const checkChanged = Effect.fn("SqliteActivityStore.checkChanged")(function* (
289
+ operation: string,
290
+ ): Effect.fn.Return<void, ActivityStoreError> {
291
+ const rawRows = yield* query(
292
+ sql<Record<string, unknown>>`SELECT changes() AS changed`,
293
+ operation,
294
+ );
295
+
296
+ const rows = yield* decodeRows(ActivityChangeCountRow, rawRows, operation);
297
+
298
+ if (rows.length !== 1 || rows[0].changed !== 1) {
299
+ return yield* storeError(operation, "corrupt");
300
+ }
301
+ });
302
+
303
+ const insertProgress = Effect.fn("SqliteActivityStore.insertProgress")(function* (
304
+ progress: ActivityProgress,
305
+ operation: string,
306
+ ) {
307
+ const progressJson = yield* encodeProgress(progress, operation);
308
+
309
+ yield* sql`
310
+ INSERT INTO effect_agent_activity_processor_state_v1 (
311
+ processor_id, processor_version, thread_id, format_version, through_sequence,
312
+ epoch, owner, lease_expires_at, progress_json
313
+ ) VALUES (
314
+ ${progress.key.processorId}, ${progress.key.processorVersion}, ${progress.key.threadId},
315
+ ${STORAGE_VERSION}, ${progress.throughSequence}, ${progress.epoch}, ${progress.owner},
316
+ ${progress.leaseExpiresAt}, ${progressJson}
317
+ )
318
+ `;
319
+ yield* checkChanged(operation);
320
+ });
321
+
322
+ const updateProgress = Effect.fn("SqliteActivityStore.updateProgress")(function* (
323
+ current: ActivityProgress,
324
+ next: ActivityProgress,
325
+ operation: string,
326
+ ) {
327
+ const progressJson = yield* encodeProgress(next, operation);
328
+
329
+ yield* sql`
330
+ UPDATE effect_agent_activity_processor_state_v1
331
+ SET format_version = ${STORAGE_VERSION},
332
+ through_sequence = ${next.throughSequence},
333
+ epoch = ${next.epoch},
334
+ owner = ${next.owner},
335
+ lease_expires_at = ${next.leaseExpiresAt},
336
+ progress_json = ${progressJson}
337
+ WHERE processor_id = ${current.key.processorId}
338
+ AND processor_version = ${current.key.processorVersion}
339
+ AND thread_id = ${current.key.threadId}
340
+ AND through_sequence = ${current.throughSequence}
341
+ AND epoch = ${current.epoch}
342
+ `;
343
+ yield* checkChanged(operation);
344
+ });
345
+
346
+ const requireLive = Effect.fn("SqliteActivityStore.requireLive")(function* (
347
+ progress: ActivityProgress | null,
348
+ claim: ActivityClaim,
349
+ requireSequence: boolean,
350
+ ): Effect.fn.Return<ActivityProgress, ActivityOwnershipLost> {
351
+ const now = yield* Clock.currentTimeMillis;
352
+
353
+ if (
354
+ progress === null ||
355
+ !sameKey(progress.key, claim.key) ||
356
+ progress.owner !== claim.owner ||
357
+ progress.epoch !== claim.epoch ||
358
+ progress.leaseExpiresAt <= now ||
359
+ (requireSequence && progress.throughSequence !== claim.throughSequence)
360
+ ) {
361
+ return yield* ownershipLost(claim);
362
+ }
363
+
364
+ return progress;
365
+ });
366
+
367
+ const inspect: ActivityProcessorStore["Service"]["inspect"] = Effect.fn(
368
+ "SqliteActivityStore.inspect",
369
+ )(function* (key) {
370
+ const decodedKey = yield* decodeInput(ActivityProcessorKey, key, "inspect activity progress");
371
+
372
+ return yield* readProgress(decodedKey, "inspect activity progress");
373
+ });
374
+
375
+ const claim: ActivityProcessorStore["Service"]["claim"] = Effect.fn("SqliteActivityStore.claim")(
376
+ function* (request) {
377
+ const operation = "claim activity progress";
378
+ const decoded = yield* decodeInput(ActivityClaimRequest, request, operation);
379
+
380
+ yield* failpoint.hit("activity:claim:before");
381
+
382
+ const claimed = yield* sql
383
+ .withTransaction(
384
+ Effect.gen(function* () {
385
+ const current = yield* readProgress(decoded.key, operation);
386
+ const now = yield* Clock.currentTimeMillis;
387
+
388
+ if (current !== null && current.owner !== null && current.leaseExpiresAt > now) {
389
+ return yield* ActivityBusy.make({
390
+ key: decoded.key,
391
+ leaseExpiresAt: current.leaseExpiresAt,
392
+ });
393
+ }
394
+
395
+ const next = yield* Schema.decodeUnknownEffect(ActivityProgress)({
396
+ version: STORAGE_VERSION,
397
+ key: decoded.key,
398
+ throughSequence: current?.throughSequence ?? 0,
399
+ epoch: (current?.epoch ?? 0) + 1,
400
+ owner: decoded.owner,
401
+ leaseExpiresAt: now + decoded.leaseMillis,
402
+ pending: current?.pending ?? null,
403
+ advancedAt: current?.advancedAt ?? null,
404
+ }).pipe(Effect.mapError(() => storeError(operation, "corrupt")));
405
+
406
+ if (current === null) yield* insertProgress(next, operation);
407
+ else yield* updateProgress(current, next, operation);
408
+ yield* failpoint.hit("activity:claim:after-state");
409
+ const result = makeClaim(next);
410
+
411
+ if (result === null) return yield* storeError(operation, "corrupt");
412
+
413
+ return result;
414
+ }),
415
+ )
416
+ .pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError(operation))));
417
+
418
+ yield* failpoint.hit("activity:claim:after");
419
+
420
+ return claimed;
421
+ },
422
+ );
423
+
424
+ const prepare: ActivityProcessorStore["Service"]["prepare"] = Effect.fn(
425
+ "SqliteActivityStore.prepare",
426
+ )(function* (request) {
427
+ const operation = "prepare activity output";
428
+ const claim = yield* decodeInput(ActivityClaim, request.claim, operation);
429
+ const work = yield* decodeInput(PreparedActivity, request.work, operation);
430
+
431
+ yield* failpoint.hit("activity:prepare:before");
432
+
433
+ const result = yield* sql
434
+ .withTransaction(
435
+ Effect.gen(function* () {
436
+ const current = yield* requireLive(
437
+ yield* readProgress(claim.key, operation),
438
+ claim,
439
+ true,
440
+ );
441
+
442
+ if (current.pending !== null) {
443
+ if (sameWork(current.pending, work)) {
444
+ return { work: current.pending, changed: false } as const;
445
+ }
446
+
447
+ return yield* ActivityWorkConflict.make({ key: claim.key, workId: work.workId });
448
+ }
449
+ if (!sameKey(work.key, claim.key) || work.sequence !== current.throughSequence + 1) {
450
+ return yield* ActivityWorkConflict.make({ key: claim.key, workId: work.workId });
451
+ }
452
+ const next = ActivityProgress.make({ ...current, pending: work });
453
+
454
+ yield* updateProgress(current, next, operation);
455
+ yield* failpoint.hit("activity:prepare:after-state");
456
+
457
+ return { work, changed: true } as const;
458
+ }),
459
+ )
460
+ .pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError(operation))));
461
+
462
+ if (result.changed) yield* failpoint.hit("activity:prepare:after");
463
+
464
+ return result.work;
465
+ });
466
+
467
+ const advance: ActivityProcessorStore["Service"]["advance"] = Effect.fn(
468
+ "SqliteActivityStore.advance",
469
+ )(function* (request) {
470
+ const operation = "advance activity progress";
471
+ const claim = yield* decodeInput(ActivityClaim, request.claim, operation);
472
+ const workId = yield* decodeInput(Digest, request.workId, operation);
473
+
474
+ yield* failpoint.hit("activity:advance:before");
475
+
476
+ const nextClaim = yield* sql
477
+ .withTransaction(
478
+ Effect.gen(function* () {
479
+ const current = yield* requireLive(
480
+ yield* readProgress(claim.key, operation),
481
+ claim,
482
+ true,
483
+ );
484
+
485
+ if (current.pending === null || current.pending.workId !== workId) {
486
+ return yield* ActivityWorkConflict.make({ key: claim.key, workId });
487
+ }
488
+
489
+ const next = ActivityProgress.make({
490
+ ...current,
491
+ throughSequence: current.pending.sequence,
492
+ pending: null,
493
+ advancedAt: yield* Clock.currentTimeMillis,
494
+ });
495
+
496
+ yield* updateProgress(current, next, operation);
497
+ yield* failpoint.hit("activity:advance:after-state");
498
+ const result = makeClaim(next);
499
+
500
+ if (result === null) return yield* storeError(operation, "corrupt");
501
+
502
+ return result;
503
+ }),
504
+ )
505
+ .pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError(operation))));
506
+
507
+ yield* failpoint.hit("activity:advance:after");
508
+
509
+ return nextClaim;
510
+ });
511
+
512
+ const release: ActivityProcessorStore["Service"]["release"] = Effect.fn(
513
+ "SqliteActivityStore.release",
514
+ )(function* (claim) {
515
+ const operation = "release activity claim";
516
+ const decoded = yield* decodeInput(ActivityClaim, claim, operation);
517
+
518
+ yield* failpoint.hit("activity:release:before");
519
+ yield* sql
520
+ .withTransaction(
521
+ Effect.gen(function* () {
522
+ const current = yield* readProgress(decoded.key, operation);
523
+
524
+ if (
525
+ current === null ||
526
+ current.owner !== decoded.owner ||
527
+ current.epoch !== decoded.epoch
528
+ ) {
529
+ return yield* ownershipLost(decoded);
530
+ }
531
+ const next = ActivityProgress.make({ ...current, owner: null, leaseExpiresAt: 0 });
532
+
533
+ yield* updateProgress(current, next, operation);
534
+ yield* failpoint.hit("activity:release:after-state");
535
+ }),
536
+ )
537
+ .pipe(Effect.catchTag("SqlError", () => Effect.fail(storeError(operation))));
538
+ yield* failpoint.hit("activity:release:after");
539
+ });
540
+
541
+ return ActivityProcessorStore.of({ inspect, claim, prepare, advance, release });
542
+ });
543
+
544
+ /** SQLite activity progress with mutation failpoints kept injectable for recovery tests. */
545
+ export const activityProcessorStoreLayerWithFailpoints: Layer.Layer<
546
+ ActivityProcessorStore,
547
+ SqliteActivityInitializationError,
548
+ SqlClientService.SqlClient | ActivityMutationFailpoint
549
+ > = Layer.effect(ActivityProcessorStore, makeActivityStore());
550
+
551
+ /** SQLite activity progress with the production no-op mutation failpoint. */
552
+ export const activityProcessorStoreLayer: Layer.Layer<
553
+ ActivityProcessorStore,
554
+ SqliteActivityInitializationError,
555
+ SqlClientService.SqlClient
556
+ > = activityProcessorStoreLayerWithFailpoints.pipe(Layer.provide(ActivityMutationFailpoint.layer));
@@ -0,0 +1,42 @@
1
+ import {
2
+ MessageDeliveryError,
3
+ MessageDeliveryStore,
4
+ type MessageDeliveryStoreLimits,
5
+ } from "@effect-agent/thread/MessageDelivery";
6
+ import {
7
+ makeSqlMessageDeliveryStore,
8
+ SqlMessageDeliveryTransaction,
9
+ } from "@effect-agent/thread/SqlMessageDeliveryStore";
10
+ import { Effect, Layer } from "effect";
11
+ import * as SqlClient from "effect/unstable/sql/SqlClient";
12
+
13
+ import { initializeSqliteJournal } from "./internal/sqlite-journal.ts";
14
+
15
+ const transactions = Layer.effect(
16
+ SqlMessageDeliveryTransaction,
17
+ Effect.gen(function* () {
18
+ const sql = yield* SqlClient.SqlClient;
19
+
20
+ return SqlMessageDeliveryTransaction.of({
21
+ run: (body) =>
22
+ sql
23
+ .withTransaction(body)
24
+ .pipe(
25
+ Effect.catchTag("SqlError", () =>
26
+ MessageDeliveryError.make({ reason: "storage", operation: "transaction" }),
27
+ ),
28
+ ),
29
+ });
30
+ }),
31
+ );
32
+
33
+ /** Source-owned message obligations in the host's existing SQLite database. */
34
+ export const messageDeliveryStoreLayer = (limits?: MessageDeliveryStoreLimits) =>
35
+ Layer.effect(
36
+ MessageDeliveryStore,
37
+ Effect.gen(function* () {
38
+ yield* initializeSqliteJournal();
39
+
40
+ return yield* makeSqlMessageDeliveryStore(limits, { maxStoredValueBytes: 16 * 1024 * 1024 });
41
+ }),
42
+ ).pipe(Layer.provide(transactions));