@effect-agent/storage-sqlite 0.1.0-beta.42 → 0.1.0-beta.44
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
|
@@ -131,10 +131,13 @@ const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (
|
|
|
131
131
|
offset: ObservationOffset | undefined,
|
|
132
132
|
): Effect.fn.Return<CanonicalSequence, ThreadStoreError> {
|
|
133
133
|
if (offset === undefined) return ZERO_CANONICAL_SEQUENCE;
|
|
134
|
+
|
|
134
135
|
const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(
|
|
135
136
|
Effect.mapError((error) => schemaStoreError("decode observation offset", error)),
|
|
136
137
|
);
|
|
138
|
+
|
|
137
139
|
const threadPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
|
|
140
|
+
|
|
138
141
|
if (!text.startsWith(threadPrefix)) {
|
|
139
142
|
return yield* ThreadStoreError.make({
|
|
140
143
|
operation: "decode observation offset",
|
|
@@ -142,12 +145,14 @@ const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (
|
|
|
142
145
|
});
|
|
143
146
|
}
|
|
144
147
|
const sequenceText = text.slice(threadPrefix.length);
|
|
148
|
+
|
|
145
149
|
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) {
|
|
146
150
|
return yield* ThreadStoreError.make({
|
|
147
151
|
operation: "decode observation offset",
|
|
148
152
|
message: "The observation offset is malformed.",
|
|
149
153
|
});
|
|
150
154
|
}
|
|
155
|
+
|
|
151
156
|
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(
|
|
152
157
|
Effect.mapError((error) => schemaStoreError("decode observation offset", error)),
|
|
153
158
|
);
|
|
@@ -200,13 +205,17 @@ const decodeEnvelope = Effect.fn("SqliteThreadStore.decodeEnvelope")(function* (
|
|
|
200
205
|
}),
|
|
201
206
|
),
|
|
202
207
|
);
|
|
208
|
+
|
|
203
209
|
const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(
|
|
204
210
|
row.thread_id,
|
|
205
211
|
).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
|
|
212
|
+
|
|
206
213
|
const offset = yield* makeOffset(threadId, row.sequence);
|
|
214
|
+
|
|
207
215
|
const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(
|
|
208
216
|
row.batch_id,
|
|
209
217
|
).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
|
|
218
|
+
|
|
210
219
|
return CanonicalRecordEnvelope.make({
|
|
211
220
|
threadId,
|
|
212
221
|
batchId,
|
|
@@ -231,9 +240,11 @@ const requireThread = Effect.fn("SqliteThreadStore.requireThread")(function* (
|
|
|
231
240
|
const rows = yield* journal
|
|
232
241
|
.getThread(threadId)
|
|
233
242
|
.pipe(Effect.mapError((error) => storeError("read thread", error)));
|
|
243
|
+
|
|
234
244
|
if (rows.length === 0) {
|
|
235
245
|
return yield* ThreadNotMaterialized.make({ threadId });
|
|
236
246
|
}
|
|
247
|
+
|
|
237
248
|
return rows[0];
|
|
238
249
|
});
|
|
239
250
|
|
|
@@ -243,15 +254,18 @@ const tailDigestAt = Effect.fn("SqliteThreadStore.tailDigestAt")(function* (
|
|
|
243
254
|
sequence: CanonicalSequence,
|
|
244
255
|
) {
|
|
245
256
|
if (sequence === 0) return EMPTY_TAIL_DIGEST;
|
|
257
|
+
|
|
246
258
|
const digests = yield* journal
|
|
247
259
|
.getTailDigestAt(threadId, sequence)
|
|
248
260
|
.pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
|
|
261
|
+
|
|
249
262
|
if (digests.length !== 1) {
|
|
250
263
|
return yield* CheckpointRejected.make({
|
|
251
264
|
threadId,
|
|
252
265
|
reason: "digest-mismatch",
|
|
253
266
|
});
|
|
254
267
|
}
|
|
268
|
+
|
|
255
269
|
return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(
|
|
256
270
|
Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)),
|
|
257
271
|
);
|
|
@@ -262,14 +276,17 @@ const groupByKey = <A>(
|
|
|
262
276
|
key: (row: A) => string,
|
|
263
277
|
): ReadonlyMap<string, ReadonlyArray<A>> => {
|
|
264
278
|
const grouped = new Map<string, Array<A>>();
|
|
279
|
+
|
|
265
280
|
for (const row of rows) {
|
|
266
281
|
const existing = grouped.get(key(row));
|
|
282
|
+
|
|
267
283
|
if (existing === undefined) {
|
|
268
284
|
grouped.set(key(row), [row]);
|
|
269
285
|
} else {
|
|
270
286
|
existing.push(row);
|
|
271
287
|
}
|
|
272
288
|
}
|
|
289
|
+
|
|
273
290
|
return grouped;
|
|
274
291
|
};
|
|
275
292
|
|
|
@@ -283,6 +300,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
283
300
|
crypto: Crypto.Crypto,
|
|
284
301
|
) {
|
|
285
302
|
const stored = yield* journal.scanStoredPayloads();
|
|
303
|
+
|
|
286
304
|
const batches = yield* Effect.forEach(stored.batches, (batch) =>
|
|
287
305
|
Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(
|
|
288
306
|
Effect.map((decoded) => ({ decoded, row: batch })),
|
|
@@ -295,6 +313,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
295
313
|
),
|
|
296
314
|
),
|
|
297
315
|
);
|
|
316
|
+
|
|
298
317
|
const records = yield* Effect.forEach(stored.records, (record) =>
|
|
299
318
|
Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(
|
|
300
319
|
Effect.map((decoded) => ({ decoded, row: record })),
|
|
@@ -307,6 +326,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
307
326
|
),
|
|
308
327
|
),
|
|
309
328
|
);
|
|
329
|
+
|
|
310
330
|
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) =>
|
|
311
331
|
Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(
|
|
312
332
|
Effect.map((decoded) => ({ decoded, row: checkpoint })),
|
|
@@ -336,6 +356,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
336
356
|
|
|
337
357
|
for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
|
|
338
358
|
const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
|
|
359
|
+
|
|
339
360
|
if (
|
|
340
361
|
canonicalBatch.batchId !== batchRow.batch_id ||
|
|
341
362
|
batchRow.first_sequence !== expectedSequence ||
|
|
@@ -358,6 +379,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
358
379
|
}),
|
|
359
380
|
),
|
|
360
381
|
);
|
|
382
|
+
|
|
361
383
|
if (batchRow.batch_digest !== digest || batchRow.tail_digest !== digest) {
|
|
362
384
|
return yield* SqliteStorageCorruptionError.make({
|
|
363
385
|
table: "effect_agent_canonical_batches",
|
|
@@ -367,6 +389,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
367
389
|
}
|
|
368
390
|
|
|
369
391
|
const batchRecords = recordsByBatch.get(batchRow.batch_id) ?? [];
|
|
392
|
+
|
|
370
393
|
if (batchRecords.length !== canonicalBatch.records.length) {
|
|
371
394
|
return yield* SqliteStorageCorruptionError.make({
|
|
372
395
|
table: "effect_agent_canonical_records",
|
|
@@ -377,6 +400,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
377
400
|
for (let index = 0; index < canonicalBatch.records.length; index++) {
|
|
378
401
|
const expectedRecord = canonicalBatch.records[index];
|
|
379
402
|
const storedRecord = batchRecords[index];
|
|
403
|
+
|
|
380
404
|
const expectedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
|
|
381
405
|
expectedRecord,
|
|
382
406
|
).pipe(
|
|
@@ -388,6 +412,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
388
412
|
}),
|
|
389
413
|
),
|
|
390
414
|
);
|
|
415
|
+
|
|
391
416
|
const storedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
|
|
392
417
|
storedRecord.decoded,
|
|
393
418
|
).pipe(
|
|
@@ -399,6 +424,7 @@ const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads
|
|
|
399
424
|
}),
|
|
400
425
|
),
|
|
401
426
|
);
|
|
427
|
+
|
|
402
428
|
if (
|
|
403
429
|
storedRecord.row.sequence !== batchRow.first_sequence + index ||
|
|
404
430
|
storedRecord.row.record_id !== expectedRecord.recordId ||
|
|
@@ -463,12 +489,14 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
463
489
|
const failpoint = yield* SqliteStorageFailpoint;
|
|
464
490
|
const crypto = yield* Crypto.Crypto;
|
|
465
491
|
const journal = yield* initializeSqliteJournal();
|
|
492
|
+
|
|
466
493
|
if (config.verifyOnOpen) {
|
|
467
494
|
yield* decodeStartupPayloads(journal, crypto);
|
|
468
495
|
}
|
|
469
496
|
|
|
470
497
|
const provideCrypto = <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) =>
|
|
471
498
|
Effect.provideService(effect, Crypto.Crypto, crypto);
|
|
499
|
+
|
|
472
500
|
const hitFailpoint = Effect.fn("SqliteThreadStore.hitFailpoint")(
|
|
473
501
|
(location: SqliteStorageFailpointLocation): Effect.Effect<void, ThreadStoreError> =>
|
|
474
502
|
failpoint
|
|
@@ -482,7 +510,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
482
510
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(
|
|
483
511
|
request,
|
|
484
512
|
).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
513
|
+
|
|
485
514
|
const now = yield* Clock.currentTimeMillis;
|
|
515
|
+
|
|
486
516
|
yield* hitFailpoint("materialize:before");
|
|
487
517
|
yield* journal
|
|
488
518
|
.materialize(
|
|
@@ -507,11 +537,15 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
507
537
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(
|
|
508
538
|
request,
|
|
509
539
|
).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
|
|
540
|
+
|
|
510
541
|
yield* requireThread(journal, validated.threadId);
|
|
542
|
+
|
|
511
543
|
const tailDigest = yield* provideCrypto(
|
|
512
544
|
digestCanonicalBatch(validated.expectedTailDigest, validated.batch),
|
|
513
545
|
).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
|
|
546
|
+
|
|
514
547
|
const batchJson = yield* encodeCanonicalBatch(validated.batch);
|
|
548
|
+
|
|
515
549
|
const rawRecords = yield* Effect.forEach(validated.batch.records, (record) =>
|
|
516
550
|
encodeCanonicalRecord(record).pipe(
|
|
517
551
|
Effect.map((recordJson) => ({
|
|
@@ -520,6 +554,7 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
520
554
|
})),
|
|
521
555
|
),
|
|
522
556
|
);
|
|
557
|
+
|
|
523
558
|
const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
|
|
524
559
|
threadId: validated.threadId,
|
|
525
560
|
batchId: validated.batch.batchId,
|
|
@@ -531,7 +566,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
531
566
|
records: rawRecords,
|
|
532
567
|
tailDigest,
|
|
533
568
|
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
569
|
+
|
|
534
570
|
yield* hitFailpoint("append:before");
|
|
571
|
+
|
|
535
572
|
const result = yield* journal.append(rawRequest).pipe(
|
|
536
573
|
Effect.mapError((error) => {
|
|
537
574
|
if (isSqliteFenceRejected(error)) {
|
|
@@ -552,6 +589,7 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
552
589
|
reason: error.reason,
|
|
553
590
|
});
|
|
554
591
|
}
|
|
592
|
+
|
|
555
593
|
return storeError("append canonical batch", error);
|
|
556
594
|
}),
|
|
557
595
|
Effect.flatMap((result) =>
|
|
@@ -560,7 +598,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
560
598
|
),
|
|
561
599
|
),
|
|
562
600
|
);
|
|
601
|
+
|
|
563
602
|
yield* hitFailpoint("append:after");
|
|
603
|
+
|
|
564
604
|
return result;
|
|
565
605
|
});
|
|
566
606
|
|
|
@@ -570,6 +610,7 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
570
610
|
const rows = yield* journal
|
|
571
611
|
.read(request)
|
|
572
612
|
.pipe(Effect.mapError((error) => storeError("read canonical records", error)));
|
|
613
|
+
|
|
573
614
|
return yield* Effect.forEach(rows, decodeEnvelope);
|
|
574
615
|
});
|
|
575
616
|
|
|
@@ -577,7 +618,9 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
577
618
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(
|
|
578
619
|
Effect.mapError((error) => schemaStoreError("validate thread read", error)),
|
|
579
620
|
);
|
|
621
|
+
|
|
580
622
|
yield* requireThread(journal, validated.threadId);
|
|
623
|
+
|
|
581
624
|
const records = yield* loadRecords(
|
|
582
625
|
RawReadRequest.make({
|
|
583
626
|
threadId: validated.threadId,
|
|
@@ -585,8 +628,10 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
585
628
|
limit: validated.limit,
|
|
586
629
|
}),
|
|
587
630
|
);
|
|
631
|
+
|
|
588
632
|
return Stream.fromIterable(records);
|
|
589
633
|
});
|
|
634
|
+
|
|
590
635
|
const read: ThreadStore["Service"]["read"] = (request) => Stream.unwrap(readEffect(request));
|
|
591
636
|
|
|
592
637
|
const observeEffect = Effect.fn("SqliteThreadStore.observe")(function* (
|
|
@@ -595,11 +640,14 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
595
640
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(
|
|
596
641
|
request,
|
|
597
642
|
).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
|
|
643
|
+
|
|
598
644
|
yield* requireThread(journal, validated.threadId);
|
|
599
645
|
const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
|
|
600
646
|
const cursor = yield* Ref.make(initialSequence);
|
|
647
|
+
|
|
601
648
|
const poll = Effect.fn("SqliteThreadStore.observePoll")(function* () {
|
|
602
649
|
const fromSequenceExclusive = yield* Ref.get(cursor);
|
|
650
|
+
|
|
603
651
|
const records = yield* loadRecords(
|
|
604
652
|
RawReadRequest.make({
|
|
605
653
|
threadId: validated.threadId,
|
|
@@ -607,15 +655,20 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
607
655
|
limit: 1_024,
|
|
608
656
|
}),
|
|
609
657
|
);
|
|
658
|
+
|
|
610
659
|
if (records.length === 0) {
|
|
611
660
|
yield* Effect.sleep(config.observationPollInterval);
|
|
661
|
+
|
|
612
662
|
return [];
|
|
613
663
|
}
|
|
614
664
|
yield* Ref.set(cursor, records[records.length - 1].sequence);
|
|
665
|
+
|
|
615
666
|
return records;
|
|
616
667
|
});
|
|
668
|
+
|
|
617
669
|
return Stream.fromIterableEffectRepeat(poll());
|
|
618
670
|
});
|
|
671
|
+
|
|
619
672
|
const observe: ThreadStore["Service"]["observe"] = (request) =>
|
|
620
673
|
Stream.unwrap(observeEffect(request));
|
|
621
674
|
|
|
@@ -624,20 +677,26 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
624
677
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(
|
|
625
678
|
request,
|
|
626
679
|
).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
|
|
680
|
+
|
|
627
681
|
yield* requireThread(journal, validated.threadId);
|
|
682
|
+
|
|
628
683
|
const exported = yield* journal
|
|
629
684
|
.exportThread(validated.threadId)
|
|
630
685
|
.pipe(Effect.mapError((error) => storeError("export thread", error)));
|
|
686
|
+
|
|
631
687
|
const records = yield* Effect.forEach(exported.records, decodeEnvelope);
|
|
688
|
+
|
|
632
689
|
if (records.length > 65_536) {
|
|
633
690
|
return yield* ThreadStoreError.make({
|
|
634
691
|
operation: "decode thread export",
|
|
635
692
|
message: "The thread exceeds the current export record limit.",
|
|
636
693
|
});
|
|
637
694
|
}
|
|
695
|
+
|
|
638
696
|
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(
|
|
639
697
|
exported.thread.tail_digest,
|
|
640
698
|
).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
|
|
699
|
+
|
|
641
700
|
return ThreadExport.make({
|
|
642
701
|
format: "effect-agent/thread@1",
|
|
643
702
|
threadId: validated.threadId,
|
|
@@ -654,10 +713,13 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
654
713
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(
|
|
655
714
|
request,
|
|
656
715
|
).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
|
|
716
|
+
|
|
657
717
|
const thread = yield* requireThread(journal, validated.threadId);
|
|
718
|
+
|
|
658
719
|
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(
|
|
659
720
|
Effect.mapError((error) => schemaStoreError("decode tail digest", error)),
|
|
660
721
|
);
|
|
722
|
+
|
|
661
723
|
return ThreadTail.make({
|
|
662
724
|
threadId: validated.threadId,
|
|
663
725
|
tailSequence: thread.tail_sequence,
|
|
@@ -671,18 +733,22 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
671
733
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(
|
|
672
734
|
request,
|
|
673
735
|
).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
|
|
736
|
+
|
|
674
737
|
const thread = yield* requireThread(journal, validated.checkpoint.threadId);
|
|
738
|
+
|
|
675
739
|
if (validated.checkpoint.throughSequence > thread.tail_sequence) {
|
|
676
740
|
return yield* CheckpointRejected.make({
|
|
677
741
|
threadId: validated.checkpoint.threadId,
|
|
678
742
|
reason: "ahead-of-tail",
|
|
679
743
|
});
|
|
680
744
|
}
|
|
745
|
+
|
|
681
746
|
const canonicalDigest = yield* tailDigestAt(
|
|
682
747
|
journal,
|
|
683
748
|
validated.checkpoint.threadId,
|
|
684
749
|
validated.checkpoint.throughSequence,
|
|
685
750
|
);
|
|
751
|
+
|
|
686
752
|
if (canonicalDigest !== validated.checkpoint.tailDigest) {
|
|
687
753
|
return yield* CheckpointRejected.make({
|
|
688
754
|
threadId: validated.checkpoint.threadId,
|
|
@@ -690,12 +756,14 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
690
756
|
});
|
|
691
757
|
}
|
|
692
758
|
const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
|
|
759
|
+
|
|
693
760
|
const raw = RawCheckpoint.make({
|
|
694
761
|
threadId: validated.checkpoint.threadId,
|
|
695
762
|
throughSequence: validated.checkpoint.throughSequence,
|
|
696
763
|
tailDigest: validated.checkpoint.tailDigest,
|
|
697
764
|
checkpointJson,
|
|
698
765
|
});
|
|
766
|
+
|
|
699
767
|
yield* hitFailpoint("save-checkpoint:before");
|
|
700
768
|
yield* journal.saveCheckpoint(raw).pipe(
|
|
701
769
|
Effect.mapError((error) =>
|
|
@@ -716,10 +784,13 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
716
784
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(
|
|
717
785
|
request,
|
|
718
786
|
).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
|
|
787
|
+
|
|
719
788
|
const thread = yield* requireThread(journal, validated.threadId);
|
|
789
|
+
|
|
720
790
|
const rows = yield* journal
|
|
721
791
|
.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
|
|
722
792
|
.pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
|
|
793
|
+
|
|
723
794
|
if (rows.length === 0) return Option.none();
|
|
724
795
|
if (rows.length !== 1) {
|
|
725
796
|
return yield* ThreadStoreError.make({
|
|
@@ -728,17 +799,20 @@ const makeServices = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
|
728
799
|
});
|
|
729
800
|
}
|
|
730
801
|
const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
|
|
802
|
+
|
|
731
803
|
const canonicalDigest = yield* tailDigestAt(
|
|
732
804
|
journal,
|
|
733
805
|
checkpoint.threadId,
|
|
734
806
|
checkpoint.throughSequence,
|
|
735
807
|
);
|
|
808
|
+
|
|
736
809
|
if (canonicalDigest !== checkpoint.tailDigest) {
|
|
737
810
|
return yield* CheckpointRejected.make({
|
|
738
811
|
threadId: checkpoint.threadId,
|
|
739
812
|
reason: "digest-mismatch",
|
|
740
813
|
});
|
|
741
814
|
}
|
|
815
|
+
|
|
742
816
|
return Option.some(checkpoint);
|
|
743
817
|
},
|
|
744
818
|
);
|