@effect-agent/storage-sqlite 0.1.0-beta.42 → 0.1.0-beta.45
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/index.mjs.map +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -49
- package/src/errors.ts +1 -0
- package/src/sqlite-activity-store.ts +53 -0
- package/src/sqlite-journal.ts +52 -0
- package/src/sqlite-ledger.ts +307 -0
- package/src/sqlite-schedule-store.ts +39 -0
- package/src/sqlite-storage-failpoint-testing.ts +1 -0
- package/src/sqlite-subscription-store.ts +129 -0
- package/src/sqlite-thread-store.ts +74 -0
package/src/sqlite-journal.ts
CHANGED
|
@@ -139,6 +139,7 @@ type CheckpointError =
|
|
|
139
139
|
| SqliteStorageCorruptionError
|
|
140
140
|
| SqliteStorageError
|
|
141
141
|
| SqliteWriteContention;
|
|
142
|
+
|
|
142
143
|
const storageError =
|
|
143
144
|
(operation: string) =>
|
|
144
145
|
(error: SqlError): SqliteStorageError =>
|
|
@@ -194,21 +195,25 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
194
195
|
const sql = yield* SqlClient.SqlClient;
|
|
195
196
|
const { hit: failpoint } = yield* SqliteStorageFailpoint;
|
|
196
197
|
const { busyTimeout } = yield* SqliteStorageConfig;
|
|
198
|
+
|
|
197
199
|
yield* sql`PRAGMA foreign_keys = ON`.pipe(Effect.mapError(storageError("enable foreign keys")));
|
|
198
200
|
// PRAGMA statements do not accept bound parameters; the value is a schema-validated
|
|
199
201
|
// non-negative integer, never caller-controlled text.
|
|
200
202
|
yield* sql
|
|
201
203
|
.unsafe(`PRAGMA busy_timeout = ${busyTimeout}`)
|
|
202
204
|
.pipe(Effect.mapError(storageError("configure busy timeout")));
|
|
205
|
+
|
|
203
206
|
const journalModeRows = yield* sql<Record<string, unknown>>`PRAGMA journal_mode`.pipe(
|
|
204
207
|
Effect.mapError(storageError("read journal mode")),
|
|
205
208
|
);
|
|
209
|
+
|
|
206
210
|
const journalMode = yield* decodeSingleRow(
|
|
207
211
|
Schema.Array(SqliteJournalModeRow),
|
|
208
212
|
"pragma_journal_mode",
|
|
209
213
|
"singleton",
|
|
210
214
|
journalModeRows,
|
|
211
215
|
);
|
|
216
|
+
|
|
212
217
|
if (journalMode.journal_mode.toLowerCase() !== "wal") {
|
|
213
218
|
return yield* SqliteStorageCompatibilityError.make({
|
|
214
219
|
actualVersion: 0,
|
|
@@ -220,6 +225,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
220
225
|
const versionRows = yield* sql<Record<string, unknown>>`PRAGMA user_version`.pipe(
|
|
221
226
|
Effect.mapError(storageError("read storage version")),
|
|
222
227
|
);
|
|
228
|
+
|
|
223
229
|
const version = yield* decodeSingleRow(
|
|
224
230
|
Schema.Array(SqliteVersionRow),
|
|
225
231
|
"pragma_user_version",
|
|
@@ -249,6 +255,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
249
255
|
AND name LIKE 'effect_agent_%'
|
|
250
256
|
ORDER BY name
|
|
251
257
|
`.pipe(Effect.mapError(storageError("inspect unversioned storage")));
|
|
258
|
+
|
|
252
259
|
const existing = yield* decodeRows(
|
|
253
260
|
Schema.Array(SqliteNameRow),
|
|
254
261
|
"sqlite_master",
|
|
@@ -296,12 +303,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
296
303
|
)
|
|
297
304
|
ORDER BY name
|
|
298
305
|
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
306
|
+
|
|
299
307
|
const required = yield* decodeRows(
|
|
300
308
|
Schema.Array(SqliteNameRow),
|
|
301
309
|
"sqlite_master",
|
|
302
310
|
"required_tables",
|
|
303
311
|
requiredRows,
|
|
304
312
|
);
|
|
313
|
+
|
|
305
314
|
if (required.length !== 12) {
|
|
306
315
|
return yield* SqliteStorageCompatibilityError.make({
|
|
307
316
|
actualVersion: CurrentSqliteStorageVersion,
|
|
@@ -345,19 +354,24 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
345
354
|
const connection = yield* sql.reserve.pipe(
|
|
346
355
|
Effect.mapError(classifyWriteFailure(operation)),
|
|
347
356
|
);
|
|
357
|
+
|
|
348
358
|
yield* connection
|
|
349
359
|
.executeUnprepared("BEGIN IMMEDIATE", [], undefined)
|
|
350
360
|
.pipe(Effect.mapError(classifyWriteFailure(operation)));
|
|
361
|
+
|
|
351
362
|
const exit = yield* restore(
|
|
352
363
|
Effect.provideService(effect, sql.transactionService, [connection, 0] as const),
|
|
353
364
|
).pipe(Effect.exit);
|
|
365
|
+
|
|
354
366
|
if (Exit.isSuccess(exit)) {
|
|
355
367
|
yield* connection
|
|
356
368
|
.executeUnprepared("COMMIT", [], undefined)
|
|
357
369
|
.pipe(Effect.mapError(classifyWriteFailure(operation)));
|
|
370
|
+
|
|
358
371
|
return exit.value;
|
|
359
372
|
}
|
|
360
373
|
yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], undefined));
|
|
374
|
+
|
|
361
375
|
return yield* exit;
|
|
362
376
|
}),
|
|
363
377
|
).pipe(
|
|
@@ -380,19 +394,24 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
380
394
|
Effect.scoped(
|
|
381
395
|
Effect.gen(function* () {
|
|
382
396
|
const connection = yield* sql.reserve.pipe(Effect.mapError(storageError(operation)));
|
|
397
|
+
|
|
383
398
|
yield* connection
|
|
384
399
|
.executeUnprepared("BEGIN", [], undefined)
|
|
385
400
|
.pipe(Effect.mapError(storageError(operation)));
|
|
401
|
+
|
|
386
402
|
const exit = yield* restore(
|
|
387
403
|
Effect.provideService(effect, sql.transactionService, [connection, 0] as const),
|
|
388
404
|
).pipe(Effect.exit);
|
|
405
|
+
|
|
389
406
|
if (Exit.isSuccess(exit)) {
|
|
390
407
|
yield* connection
|
|
391
408
|
.executeUnprepared("COMMIT", [], undefined)
|
|
392
409
|
.pipe(Effect.mapError(storageError(operation)));
|
|
410
|
+
|
|
393
411
|
return exit.value;
|
|
394
412
|
}
|
|
395
413
|
yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], undefined));
|
|
414
|
+
|
|
396
415
|
return yield* exit;
|
|
397
416
|
}),
|
|
398
417
|
).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })),
|
|
@@ -428,12 +447,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
428
447
|
FROM effect_agent_threads
|
|
429
448
|
WHERE thread_id = ${threadId}
|
|
430
449
|
`.pipe(Effect.mapError(storageError("read materialized thread")));
|
|
450
|
+
|
|
431
451
|
const existing = yield* decodeRows(
|
|
432
452
|
Schema.Array(ThreadRow),
|
|
433
453
|
"effect_agent_threads",
|
|
434
454
|
threadId,
|
|
435
455
|
existingRows,
|
|
436
456
|
);
|
|
457
|
+
|
|
437
458
|
if (existing.length > 1) {
|
|
438
459
|
return yield* SqliteStorageCorruptionError.make({
|
|
439
460
|
table: "effect_agent_threads",
|
|
@@ -457,6 +478,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
457
478
|
${producerEpoch}
|
|
458
479
|
)
|
|
459
480
|
`.pipe(Effect.mapError(storageError("materialize thread")));
|
|
481
|
+
|
|
460
482
|
return;
|
|
461
483
|
}
|
|
462
484
|
if (producerEpoch < existing[0].producer_epoch) {
|
|
@@ -488,6 +510,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
488
510
|
FROM effect_agent_threads
|
|
489
511
|
WHERE thread_id = ${threadId}
|
|
490
512
|
`.pipe(Effect.mapError(storageError("read thread")));
|
|
513
|
+
|
|
491
514
|
return yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
|
|
492
515
|
});
|
|
493
516
|
|
|
@@ -511,9 +534,11 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
511
534
|
message: "Canonical identifiers or encoded JSON exceed the SQLite storage bounds.",
|
|
512
535
|
});
|
|
513
536
|
}
|
|
537
|
+
|
|
514
538
|
return yield* withWriteTransaction("append transaction")(
|
|
515
539
|
Effect.gen(function* () {
|
|
516
540
|
const recordIds = request.records.map((record) => record.recordId);
|
|
541
|
+
|
|
517
542
|
if (new Set(recordIds).size !== recordIds.length) {
|
|
518
543
|
return yield* SqliteAppendConflict.make({
|
|
519
544
|
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
@@ -531,6 +556,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
531
556
|
FROM effect_agent_threads
|
|
532
557
|
WHERE thread_id = ${request.threadId}
|
|
533
558
|
`.pipe(Effect.mapError(storageError("read append tail")));
|
|
559
|
+
|
|
534
560
|
const thread = yield* decodeSingleRow(
|
|
535
561
|
Schema.Array(ThreadRow),
|
|
536
562
|
"effect_agent_threads",
|
|
@@ -559,6 +585,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
559
585
|
WHERE thread_id = ${request.threadId}
|
|
560
586
|
AND batch_id = ${request.batchId}
|
|
561
587
|
`.pipe(Effect.mapError(storageError("read idempotent batch")));
|
|
588
|
+
|
|
562
589
|
const batches = yield* decodeRows(
|
|
563
590
|
Schema.Array(BatchRow),
|
|
564
591
|
"effect_agent_canonical_batches",
|
|
@@ -575,12 +602,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
575
602
|
}
|
|
576
603
|
if (batches.length === 1) {
|
|
577
604
|
const existing = batches[0];
|
|
605
|
+
|
|
578
606
|
if (existing.batch_digest !== request.batchDigest) {
|
|
579
607
|
return yield* SqliteAppendConflict.make({
|
|
580
608
|
message: `Batch ${request.batchId} already exists with different canonical content.`,
|
|
581
609
|
reason: "batch-digest",
|
|
582
610
|
});
|
|
583
611
|
}
|
|
612
|
+
|
|
584
613
|
return RawAppendResult.make({
|
|
585
614
|
firstSequence: existing.first_sequence,
|
|
586
615
|
lastSequence: existing.last_sequence,
|
|
@@ -621,12 +650,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
621
650
|
AND record_id IN ${sql.in(recordIds)}
|
|
622
651
|
ORDER BY sequence
|
|
623
652
|
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
653
|
+
|
|
624
654
|
const existingRecords = yield* decodeRows(
|
|
625
655
|
Schema.Array(RecordRow),
|
|
626
656
|
"effect_agent_canonical_records",
|
|
627
657
|
`${request.threadId}/record_ids`,
|
|
628
658
|
existingRecordRows,
|
|
629
659
|
);
|
|
660
|
+
|
|
630
661
|
if (existingRecords.length > 0) {
|
|
631
662
|
return yield* SqliteAppendConflict.make({
|
|
632
663
|
message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
|
|
@@ -645,6 +676,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
645
676
|
}),
|
|
646
677
|
),
|
|
647
678
|
);
|
|
679
|
+
|
|
648
680
|
const lastSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
|
|
649
681
|
firstSequence + request.records.length - 1,
|
|
650
682
|
).pipe(
|
|
@@ -736,6 +768,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
736
768
|
ORDER BY sequence
|
|
737
769
|
LIMIT ${request.limit}
|
|
738
770
|
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
771
|
+
|
|
739
772
|
return yield* decodeRows(
|
|
740
773
|
Schema.Array(RecordRow),
|
|
741
774
|
"effect_agent_canonical_records",
|
|
@@ -757,13 +790,16 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
757
790
|
FROM effect_agent_threads
|
|
758
791
|
WHERE thread_id = ${threadId}
|
|
759
792
|
`.pipe(Effect.mapError(storageError("export thread")));
|
|
793
|
+
|
|
760
794
|
const thread = yield* decodeSingleRow(
|
|
761
795
|
Schema.Array(ThreadRow),
|
|
762
796
|
"effect_agent_threads",
|
|
763
797
|
threadId,
|
|
764
798
|
threadRows,
|
|
765
799
|
);
|
|
800
|
+
|
|
766
801
|
yield* failpoint("export:after-thread-read");
|
|
802
|
+
|
|
767
803
|
const batchRows = yield* sql<Record<string, unknown>>`
|
|
768
804
|
SELECT
|
|
769
805
|
thread_id,
|
|
@@ -777,6 +813,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
777
813
|
WHERE thread_id = ${threadId}
|
|
778
814
|
ORDER BY first_sequence
|
|
779
815
|
`.pipe(Effect.mapError(storageError("export canonical batches")));
|
|
816
|
+
|
|
780
817
|
const recordRows = yield* sql<Record<string, unknown>>`
|
|
781
818
|
SELECT
|
|
782
819
|
thread_id,
|
|
@@ -788,6 +825,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
788
825
|
WHERE thread_id = ${threadId}
|
|
789
826
|
ORDER BY sequence
|
|
790
827
|
`.pipe(Effect.mapError(storageError("export canonical records")));
|
|
828
|
+
|
|
791
829
|
const checkpointRows = yield* sql<Record<string, unknown>>`
|
|
792
830
|
SELECT
|
|
793
831
|
thread_id,
|
|
@@ -848,12 +886,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
848
886
|
FROM effect_agent_threads
|
|
849
887
|
WHERE thread_id = ${checkpoint.threadId}
|
|
850
888
|
`.pipe(Effect.mapError(storageError("read checkpoint tail")));
|
|
889
|
+
|
|
851
890
|
const thread = yield* decodeSingleRow(
|
|
852
891
|
Schema.Array(ThreadRow),
|
|
853
892
|
"effect_agent_threads",
|
|
854
893
|
checkpoint.threadId,
|
|
855
894
|
threadRows,
|
|
856
895
|
);
|
|
896
|
+
|
|
857
897
|
if (checkpoint.throughSequence > thread.tail_sequence) {
|
|
858
898
|
return yield* SqliteCheckpointConflict.make({
|
|
859
899
|
message:
|
|
@@ -872,12 +912,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
872
912
|
WHERE thread_id = ${checkpoint.threadId}
|
|
873
913
|
AND through_sequence = ${checkpoint.throughSequence}
|
|
874
914
|
`.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
|
|
915
|
+
|
|
875
916
|
const existing = yield* decodeRows(
|
|
876
917
|
Schema.Array(CheckpointRow),
|
|
877
918
|
"effect_agent_checkpoints",
|
|
878
919
|
`${checkpoint.threadId}/${checkpoint.throughSequence}`,
|
|
879
920
|
checkpointRows,
|
|
880
921
|
);
|
|
922
|
+
|
|
881
923
|
if (existing.length > 1) {
|
|
882
924
|
return yield* SqliteStorageCorruptionError.make({
|
|
883
925
|
table: "effect_agent_checkpoints",
|
|
@@ -894,6 +936,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
894
936
|
message: "A different checkpoint already exists at this canonical sequence.",
|
|
895
937
|
});
|
|
896
938
|
}
|
|
939
|
+
|
|
897
940
|
return;
|
|
898
941
|
}
|
|
899
942
|
|
|
@@ -930,6 +973,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
930
973
|
ORDER BY through_sequence DESC
|
|
931
974
|
LIMIT 1
|
|
932
975
|
`.pipe(Effect.mapError(storageError("load checkpoint")));
|
|
976
|
+
|
|
933
977
|
return yield* decodeRows(
|
|
934
978
|
Schema.Array(CheckpointRow),
|
|
935
979
|
"effect_agent_checkpoints",
|
|
@@ -944,12 +988,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
944
988
|
) {
|
|
945
989
|
if (sequence === 0) {
|
|
946
990
|
const threads = yield* getThread(threadId);
|
|
991
|
+
|
|
947
992
|
return threads.length === 0
|
|
948
993
|
? []
|
|
949
994
|
: [threads[0].tail_sequence === 0 ? threads[0].tail_digest : undefined].filter(
|
|
950
995
|
(value): value is string => value !== undefined,
|
|
951
996
|
);
|
|
952
997
|
}
|
|
998
|
+
|
|
953
999
|
const rows = yield* sql<Record<string, unknown>>`
|
|
954
1000
|
SELECT
|
|
955
1001
|
thread_id,
|
|
@@ -963,12 +1009,14 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
963
1009
|
WHERE thread_id = ${threadId}
|
|
964
1010
|
AND last_sequence = ${sequence}
|
|
965
1011
|
`.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
|
|
1012
|
+
|
|
966
1013
|
const batches = yield* decodeRows(
|
|
967
1014
|
Schema.Array(BatchRow),
|
|
968
1015
|
"effect_agent_canonical_batches",
|
|
969
1016
|
`${threadId}/${sequence}`,
|
|
970
1017
|
rows,
|
|
971
1018
|
);
|
|
1019
|
+
|
|
972
1020
|
return batches.map((batch) => batch.tail_digest);
|
|
973
1021
|
});
|
|
974
1022
|
|
|
@@ -985,6 +1033,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
985
1033
|
FROM effect_agent_threads
|
|
986
1034
|
ORDER BY thread_id
|
|
987
1035
|
`.pipe(Effect.mapError(storageError("scan threads")));
|
|
1036
|
+
|
|
988
1037
|
const batches = yield* sql<Record<string, unknown>>`
|
|
989
1038
|
SELECT
|
|
990
1039
|
thread_id,
|
|
@@ -997,6 +1046,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
997
1046
|
FROM effect_agent_canonical_batches
|
|
998
1047
|
ORDER BY thread_id, first_sequence
|
|
999
1048
|
`.pipe(Effect.mapError(storageError("scan canonical batches")));
|
|
1049
|
+
|
|
1000
1050
|
const records = yield* sql<Record<string, unknown>>`
|
|
1001
1051
|
SELECT
|
|
1002
1052
|
thread_id,
|
|
@@ -1007,6 +1057,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
1007
1057
|
FROM effect_agent_canonical_records
|
|
1008
1058
|
ORDER BY thread_id, sequence
|
|
1009
1059
|
`.pipe(Effect.mapError(storageError("scan canonical records")));
|
|
1060
|
+
|
|
1010
1061
|
const checkpoints = yield* sql<Record<string, unknown>>`
|
|
1011
1062
|
SELECT
|
|
1012
1063
|
thread_id,
|
|
@@ -1016,6 +1067,7 @@ export const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(fun
|
|
|
1016
1067
|
FROM effect_agent_checkpoints
|
|
1017
1068
|
ORDER BY thread_id, through_sequence
|
|
1018
1069
|
`.pipe(Effect.mapError(storageError("scan checkpoints")));
|
|
1070
|
+
|
|
1019
1071
|
return {
|
|
1020
1072
|
threads: yield* decodeRows(
|
|
1021
1073
|
Schema.Array(ThreadRow),
|