@effect-agent/storage-cloudflare 0.1.0-beta.38 → 0.1.0-beta.40
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/do-storage-failpoint-9XB9tuif.d.mts +107 -0
- package/dist/do-storage-failpoint-C-Qtchs7.mjs +12 -0
- package/dist/do-storage-failpoint-C-Qtchs7.mjs.map +1 -0
- package/dist/index.d.mts +161 -248
- package/dist/index.mjs +1211 -539
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +33 -0
- package/dist/testing.mjs +35 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +14 -7
- package/src/do-journal.ts +148 -157
- package/src/do-ledger.ts +66 -66
- package/src/do-schedule-store.ts +7 -3
- package/src/do-storage-failpoint-testing.ts +73 -0
- package/src/do-storage-failpoint.ts +1 -68
- package/src/do-subscription-store.ts +1122 -0
- package/src/{do-conversation-store.ts → do-thread-store.ts} +313 -334
- package/src/errors.ts +3 -3
- package/src/index.ts +9 -8
- package/src/migrations.ts +28 -35
- package/src/port-protocol.ts +29 -29
- package/src/routing.ts +146 -188
- package/src/testing.ts +2 -0
|
@@ -6,17 +6,18 @@ import {
|
|
|
6
6
|
CanonicalRecordEnvelope,
|
|
7
7
|
CanonicalSequence,
|
|
8
8
|
CheckpointRejected,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
ThreadCheckpoint,
|
|
10
|
+
ThreadExport,
|
|
11
|
+
ThreadExportRequest,
|
|
12
|
+
ThreadMaterialization,
|
|
13
|
+
ThreadNotMaterialized,
|
|
14
|
+
ThreadObservation,
|
|
15
|
+
ThreadRead,
|
|
16
|
+
ThreadStore,
|
|
17
|
+
type ThreadCheckpoints,
|
|
18
|
+
ThreadStoreError,
|
|
19
|
+
ThreadTail,
|
|
20
|
+
ThreadTailRequest,
|
|
20
21
|
DEFAULT_OWNERSHIP_LEASE_DURATION,
|
|
21
22
|
digestCanonicalBatch,
|
|
22
23
|
Digest,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
LoadCheckpointRequest,
|
|
27
28
|
ObservationOffset,
|
|
28
29
|
SaveCheckpointRequest,
|
|
29
|
-
} from "@effect-agent/
|
|
30
|
+
} from "@effect-agent/thread";
|
|
30
31
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
31
32
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
32
33
|
import {
|
|
@@ -76,7 +77,7 @@ export interface DoStorageOptions {
|
|
|
76
77
|
readonly observationPollInterval?: number | undefined;
|
|
77
78
|
/**
|
|
78
79
|
* Submission ownership lease duration in milliseconds (D5). Defaults to
|
|
79
|
-
* `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/
|
|
80
|
+
* `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/thread`.
|
|
80
81
|
*/
|
|
81
82
|
readonly ownershipLeaseDuration?: number | undefined;
|
|
82
83
|
/**
|
|
@@ -107,27 +108,27 @@ const isDoAppendConflict = Schema.is(DoAppendConflict);
|
|
|
107
108
|
const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
|
|
108
109
|
|
|
109
110
|
const storeError = (operation: string, error: { readonly message: string }) =>
|
|
110
|
-
|
|
111
|
+
ThreadStoreError.make({
|
|
111
112
|
cause: error,
|
|
112
113
|
operation,
|
|
113
114
|
message: error.message,
|
|
114
115
|
});
|
|
115
116
|
|
|
116
117
|
const schemaStoreError = (operation: string, error: { readonly message: string }) =>
|
|
117
|
-
|
|
118
|
+
ThreadStoreError.make({
|
|
118
119
|
cause: error,
|
|
119
120
|
operation,
|
|
120
121
|
message: error.message,
|
|
121
122
|
});
|
|
122
123
|
|
|
123
124
|
const makeOffset = Effect.fn(function* (
|
|
124
|
-
|
|
125
|
+
threadId: ThreadMaterialization["threadId"],
|
|
125
126
|
sequence: number,
|
|
126
|
-
): Effect.fn.Return<ObservationOffset,
|
|
127
|
+
): Effect.fn.Return<ObservationOffset, ThreadStoreError> {
|
|
127
128
|
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(
|
|
128
129
|
Effect.flatMap((validatedSequence) =>
|
|
129
130
|
Schema.decodeUnknownEffect(ObservationOffset)(
|
|
130
|
-
`${DO_OFFSET_PREFIX}${encodeURIComponent(
|
|
131
|
+
`${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`,
|
|
131
132
|
),
|
|
132
133
|
),
|
|
133
134
|
Effect.mapError((error) => schemaStoreError("encode observation offset", error)),
|
|
@@ -135,24 +136,23 @@ const makeOffset = Effect.fn(function* (
|
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
const parseOffset = Effect.fn(function* (
|
|
138
|
-
|
|
139
|
+
threadId: ThreadMaterialization["threadId"],
|
|
139
140
|
offset: ObservationOffset | undefined,
|
|
140
|
-
): Effect.fn.Return<CanonicalSequence,
|
|
141
|
+
): Effect.fn.Return<CanonicalSequence, ThreadStoreError> {
|
|
141
142
|
if (offset === undefined) return ZERO_CANONICAL_SEQUENCE;
|
|
142
143
|
const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(
|
|
143
144
|
Effect.mapError((error) => schemaStoreError("decode observation offset", error)),
|
|
144
145
|
);
|
|
145
|
-
const
|
|
146
|
-
if (!text.startsWith(
|
|
147
|
-
return yield*
|
|
146
|
+
const threadPrefix = `${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
|
|
147
|
+
if (!text.startsWith(threadPrefix)) {
|
|
148
|
+
return yield* ThreadStoreError.make({
|
|
148
149
|
operation: "decode observation offset",
|
|
149
|
-
message:
|
|
150
|
-
"The observation offset belongs to a different adapter, storage version, or Conversation.",
|
|
150
|
+
message: "The observation offset belongs to a different adapter, storage version, or Thread.",
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
|
-
const sequenceText = text.slice(
|
|
153
|
+
const sequenceText = text.slice(threadPrefix.length);
|
|
154
154
|
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) {
|
|
155
|
-
return yield*
|
|
155
|
+
return yield* ThreadStoreError.make({
|
|
156
156
|
operation: "decode observation offset",
|
|
157
157
|
message: "The observation offset is malformed.",
|
|
158
158
|
});
|
|
@@ -162,19 +162,16 @@ const parseOffset = Effect.fn(function* (
|
|
|
162
162
|
);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
const mapFence = (
|
|
166
|
-
conversationId: ConversationMaterialization["conversationId"],
|
|
167
|
-
error: DoFenceRejected,
|
|
168
|
-
) =>
|
|
165
|
+
const mapFence = (threadId: ThreadMaterialization["threadId"], error: DoFenceRejected) =>
|
|
169
166
|
FenceRejected.make({
|
|
170
|
-
|
|
167
|
+
threadId,
|
|
171
168
|
actualEpoch: error.actualEpoch,
|
|
172
169
|
attemptedEpoch: error.producerEpoch,
|
|
173
170
|
});
|
|
174
171
|
|
|
175
172
|
const encodeCanonicalRecord = Effect.fn(function* (
|
|
176
173
|
record: CanonicalRecord,
|
|
177
|
-
): Effect.fn.Return<string,
|
|
174
|
+
): Effect.fn.Return<string, ThreadStoreError> {
|
|
178
175
|
return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(record).pipe(
|
|
179
176
|
Effect.mapError((error) => schemaStoreError("encode canonical record", error)),
|
|
180
177
|
);
|
|
@@ -182,23 +179,23 @@ const encodeCanonicalRecord = Effect.fn(function* (
|
|
|
182
179
|
|
|
183
180
|
const encodeCanonicalBatch = Effect.fn(function* (
|
|
184
181
|
batch: CanonicalBatch,
|
|
185
|
-
): Effect.fn.Return<string,
|
|
182
|
+
): Effect.fn.Return<string, ThreadStoreError> {
|
|
186
183
|
return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(
|
|
187
184
|
Effect.mapError((error) => schemaStoreError("encode canonical batch", error)),
|
|
188
185
|
);
|
|
189
186
|
});
|
|
190
187
|
|
|
191
188
|
const encodeCheckpoint = Effect.fn(function* (
|
|
192
|
-
checkpoint:
|
|
193
|
-
): Effect.fn.Return<string,
|
|
194
|
-
return yield* Schema.encodeEffect(Schema.fromJsonString(
|
|
189
|
+
checkpoint: ThreadCheckpoint,
|
|
190
|
+
): Effect.fn.Return<string, ThreadStoreError> {
|
|
191
|
+
return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(
|
|
195
192
|
Effect.mapError((error) => schemaStoreError("encode checkpoint", error)),
|
|
196
193
|
);
|
|
197
194
|
});
|
|
198
195
|
|
|
199
196
|
const decodeEnvelope = Effect.fn(function* (row: {
|
|
200
197
|
readonly batch_id: string;
|
|
201
|
-
readonly
|
|
198
|
+
readonly thread_id: string;
|
|
202
199
|
readonly record_json: string;
|
|
203
200
|
readonly sequence: CanonicalSequence;
|
|
204
201
|
}) {
|
|
@@ -206,23 +203,21 @@ const decodeEnvelope = Effect.fn(function* (row: {
|
|
|
206
203
|
row.record_json,
|
|
207
204
|
).pipe(
|
|
208
205
|
Effect.mapError((error) =>
|
|
209
|
-
|
|
206
|
+
ThreadStoreError.make({
|
|
210
207
|
operation: "decode canonical record",
|
|
211
208
|
message: error.message,
|
|
212
209
|
}),
|
|
213
210
|
),
|
|
214
211
|
);
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
)(
|
|
218
|
-
|
|
219
|
-
);
|
|
220
|
-
const offset = yield* makeOffset(conversationId, row.sequence);
|
|
212
|
+
const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(
|
|
213
|
+
row.thread_id,
|
|
214
|
+
).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
|
|
215
|
+
const offset = yield* makeOffset(threadId, row.sequence);
|
|
221
216
|
const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(
|
|
222
217
|
row.batch_id,
|
|
223
218
|
).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
|
|
224
219
|
return CanonicalRecordEnvelope.make({
|
|
225
|
-
|
|
220
|
+
threadId,
|
|
226
221
|
batchId,
|
|
227
222
|
sequence: row.sequence,
|
|
228
223
|
offset,
|
|
@@ -232,37 +227,37 @@ const decodeEnvelope = Effect.fn(function* (row: {
|
|
|
232
227
|
|
|
233
228
|
const decodeCheckpoint = Effect.fn(function* (
|
|
234
229
|
checkpointJson: string,
|
|
235
|
-
): Effect.fn.Return<
|
|
236
|
-
return yield* Schema.decodeEffect(Schema.fromJsonString(
|
|
237
|
-
|
|
238
|
-
)
|
|
230
|
+
): Effect.fn.Return<ThreadCheckpoint, ThreadStoreError> {
|
|
231
|
+
return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(
|
|
232
|
+
Effect.mapError((error) => schemaStoreError("decode checkpoint", error)),
|
|
233
|
+
);
|
|
239
234
|
});
|
|
240
235
|
|
|
241
|
-
const
|
|
236
|
+
const requireThread = Effect.fn("DoThreadStore.requireThread")(function* (
|
|
242
237
|
journal: DoJournal,
|
|
243
|
-
|
|
238
|
+
threadId: ThreadMaterialization["threadId"],
|
|
244
239
|
) {
|
|
245
240
|
const rows = yield* journal
|
|
246
|
-
.
|
|
247
|
-
.pipe(Effect.mapError((error) => storeError("read
|
|
241
|
+
.getThread(threadId)
|
|
242
|
+
.pipe(Effect.mapError((error) => storeError("read thread", error)));
|
|
248
243
|
if (rows.length === 0) {
|
|
249
|
-
return yield*
|
|
244
|
+
return yield* ThreadNotMaterialized.make({ threadId });
|
|
250
245
|
}
|
|
251
246
|
return rows[0];
|
|
252
247
|
});
|
|
253
248
|
|
|
254
|
-
const tailDigestAt = Effect.fn("
|
|
249
|
+
const tailDigestAt = Effect.fn("DoThreadStore.tailDigestAt")(function* (
|
|
255
250
|
journal: DoJournal,
|
|
256
|
-
|
|
251
|
+
threadId: ThreadMaterialization["threadId"],
|
|
257
252
|
sequence: CanonicalSequence,
|
|
258
253
|
) {
|
|
259
254
|
if (sequence === 0) return EMPTY_TAIL_DIGEST;
|
|
260
255
|
const digests = yield* journal
|
|
261
|
-
.getTailDigestAt(
|
|
256
|
+
.getTailDigestAt(threadId, sequence)
|
|
262
257
|
.pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
|
|
263
258
|
if (digests.length !== 1) {
|
|
264
259
|
return yield* CheckpointRejected.make({
|
|
265
|
-
|
|
260
|
+
threadId,
|
|
266
261
|
reason: "digest-mismatch",
|
|
267
262
|
});
|
|
268
263
|
}
|
|
@@ -292,7 +287,7 @@ const groupByKey = <A>(
|
|
|
292
287
|
* and re-digested against the canonical chain. Routine opens skip this scan: per-operation
|
|
293
288
|
* Schema decoding plus the digest chain already fail clearly on corrupt rows.
|
|
294
289
|
*/
|
|
295
|
-
const decodeStartupPayloads = Effect.fn("
|
|
290
|
+
const decodeStartupPayloads = Effect.fn("DoThreadStore.decodeStartupPayloads")(function* (
|
|
296
291
|
journal: DoJournal,
|
|
297
292
|
crypto: Crypto.Crypto,
|
|
298
293
|
) {
|
|
@@ -303,7 +298,7 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
303
298
|
Effect.mapError((error) =>
|
|
304
299
|
DoStorageCorruptionError.make({
|
|
305
300
|
table: "effect_agent_canonical_batches",
|
|
306
|
-
rowKey: `${batch.
|
|
301
|
+
rowKey: `${batch.thread_id}/${batch.batch_id}`,
|
|
307
302
|
message: error.message,
|
|
308
303
|
}),
|
|
309
304
|
),
|
|
@@ -315,46 +310,41 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
315
310
|
Effect.mapError((error) =>
|
|
316
311
|
DoStorageCorruptionError.make({
|
|
317
312
|
table: "effect_agent_canonical_records",
|
|
318
|
-
rowKey: `${record.
|
|
313
|
+
rowKey: `${record.thread_id}/${record.sequence}`,
|
|
319
314
|
message: error.message,
|
|
320
315
|
}),
|
|
321
316
|
),
|
|
322
317
|
),
|
|
323
318
|
);
|
|
324
319
|
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) =>
|
|
325
|
-
Schema.decodeEffect(Schema.fromJsonString(
|
|
326
|
-
checkpoint.checkpoint_json,
|
|
327
|
-
).pipe(
|
|
320
|
+
Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(
|
|
328
321
|
Effect.map((decoded) => ({ decoded, row: checkpoint })),
|
|
329
322
|
Effect.mapError((error) =>
|
|
330
323
|
DoStorageCorruptionError.make({
|
|
331
324
|
table: "effect_agent_checkpoints",
|
|
332
|
-
rowKey: `${checkpoint.
|
|
325
|
+
rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
|
|
333
326
|
message: error.message,
|
|
334
327
|
}),
|
|
335
328
|
),
|
|
336
329
|
),
|
|
337
330
|
);
|
|
338
331
|
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
const materializedIds = new Set(
|
|
343
|
-
stored.conversations.map((conversation) => conversation.conversation_id),
|
|
344
|
-
);
|
|
332
|
+
const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
|
|
333
|
+
const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
|
|
334
|
+
const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
|
|
335
|
+
const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
|
|
345
336
|
|
|
346
|
-
for (const
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
const recordsByBatch = groupByKey(conversationRecords, ({ row }) => row.batch_id);
|
|
337
|
+
for (const thread of stored.threads) {
|
|
338
|
+
const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
|
|
339
|
+
const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
|
|
340
|
+
const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
|
|
341
|
+
const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
|
|
352
342
|
let previousDigest = EMPTY_TAIL_DIGEST;
|
|
353
343
|
let expectedSequence = 1;
|
|
354
344
|
const tailDigests = new Map<number, string>([[0, EMPTY_TAIL_DIGEST]]);
|
|
355
345
|
|
|
356
|
-
for (const { decoded: canonicalBatch, row: batchRow } of
|
|
357
|
-
const key = `${batchRow.
|
|
346
|
+
for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
|
|
347
|
+
const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
|
|
358
348
|
if (
|
|
359
349
|
canonicalBatch.batchId !== batchRow.batch_id ||
|
|
360
350
|
batchRow.first_sequence !== expectedSequence ||
|
|
@@ -437,27 +427,27 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
437
427
|
}
|
|
438
428
|
|
|
439
429
|
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
430
|
+
threadRecords.length !== thread.tail_sequence ||
|
|
431
|
+
thread.tail_sequence !== expectedSequence - 1 ||
|
|
432
|
+
thread.tail_digest !== previousDigest
|
|
443
433
|
) {
|
|
444
434
|
return yield* DoStorageCorruptionError.make({
|
|
445
|
-
table: "
|
|
446
|
-
rowKey:
|
|
447
|
-
message: "
|
|
435
|
+
table: "effect_agent_threads",
|
|
436
|
+
rowKey: thread.thread_id,
|
|
437
|
+
message: "Thread tail does not match its canonical batch chain.",
|
|
448
438
|
});
|
|
449
439
|
}
|
|
450
440
|
|
|
451
|
-
for (const checkpoint of
|
|
441
|
+
for (const checkpoint of threadCheckpoints) {
|
|
452
442
|
if (
|
|
453
|
-
checkpoint.decoded.
|
|
443
|
+
checkpoint.decoded.threadId !== thread.thread_id ||
|
|
454
444
|
checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence ||
|
|
455
445
|
checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest ||
|
|
456
446
|
tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest
|
|
457
447
|
) {
|
|
458
448
|
return yield* DoStorageCorruptionError.make({
|
|
459
449
|
table: "effect_agent_checkpoints",
|
|
460
|
-
rowKey: `${
|
|
450
|
+
rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
|
|
461
451
|
message: "Checkpoint identity or digest is not bound to a canonical batch tail.",
|
|
462
452
|
});
|
|
463
453
|
}
|
|
@@ -465,19 +455,19 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
465
455
|
}
|
|
466
456
|
|
|
467
457
|
if (
|
|
468
|
-
batches.some(({ row }) => !materializedIds.has(row.
|
|
469
|
-
records.some(({ row }) => !materializedIds.has(row.
|
|
470
|
-
checkpoints.some(({ row }) => !materializedIds.has(row.
|
|
458
|
+
batches.some(({ row }) => !materializedIds.has(row.thread_id)) ||
|
|
459
|
+
records.some(({ row }) => !materializedIds.has(row.thread_id)) ||
|
|
460
|
+
checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))
|
|
471
461
|
) {
|
|
472
462
|
return yield* DoStorageCorruptionError.make({
|
|
473
|
-
table: "
|
|
463
|
+
table: "effect_agent_threads",
|
|
474
464
|
rowKey: "startup_scan",
|
|
475
|
-
message: "Canonical rows exist without a materialized
|
|
465
|
+
message: "Canonical rows exist without a materialized Thread.",
|
|
476
466
|
});
|
|
477
467
|
}
|
|
478
468
|
});
|
|
479
469
|
|
|
480
|
-
const makeServices = Effect.fn("
|
|
470
|
+
const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
481
471
|
const config = yield* DoStorageConfig;
|
|
482
472
|
const failpoint = yield* DoStorageFailpoint;
|
|
483
473
|
const sql = yield* SqlClientService.SqlClient;
|
|
@@ -490,142 +480,135 @@ const makeServices = Effect.fn("DoConversationStore.makeServices")(function* ()
|
|
|
490
480
|
const provideCrypto = <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) =>
|
|
491
481
|
Effect.provideService(effect, Crypto.Crypto, crypto);
|
|
492
482
|
const hitFailpoint = Effect.fn(
|
|
493
|
-
(location: DoStorageFailpointLocation): Effect.Effect<void,
|
|
483
|
+
(location: DoStorageFailpointLocation): Effect.Effect<void, ThreadStoreError> =>
|
|
494
484
|
failpoint
|
|
495
485
|
.hit(location)
|
|
496
486
|
.pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))),
|
|
497
487
|
);
|
|
498
488
|
|
|
499
|
-
const materialize:
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationMaterialization))(
|
|
503
|
-
request,
|
|
504
|
-
).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
505
|
-
const now = yield* Clock.currentTimeMillis;
|
|
506
|
-
yield* hitFailpoint("materialize:before");
|
|
507
|
-
yield* journal
|
|
508
|
-
.materialize(
|
|
509
|
-
validated.conversationId,
|
|
510
|
-
new Date(now).toISOString(),
|
|
511
|
-
EMPTY_TAIL_DIGEST,
|
|
512
|
-
validated.producerEpoch,
|
|
513
|
-
)
|
|
514
|
-
.pipe(
|
|
515
|
-
Effect.mapError((error) =>
|
|
516
|
-
error._tag === "DoFenceRejected"
|
|
517
|
-
? mapFence(validated.conversationId, error)
|
|
518
|
-
: storeError("materialize conversation", error),
|
|
519
|
-
),
|
|
520
|
-
);
|
|
521
|
-
yield* hitFailpoint("materialize:after");
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
const append: ConversationStore["Service"]["append"] = Effect.fn("DoConversationStore.append")(
|
|
525
|
-
function* (request: FencedAppendRequest) {
|
|
526
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(
|
|
489
|
+
const materialize: ThreadStore["Service"]["materialize"] = Effect.fn("DoThreadStore.materialize")(
|
|
490
|
+
function* (request: ThreadMaterialization) {
|
|
491
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(
|
|
527
492
|
request,
|
|
528
|
-
).pipe(Effect.mapError((error) => schemaStoreError("validate
|
|
529
|
-
yield*
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
conversationId: validated.conversationId,
|
|
544
|
-
batchId: validated.batch.batchId,
|
|
545
|
-
batchDigest: tailDigest,
|
|
546
|
-
batchJson,
|
|
547
|
-
expectedTailSequence: validated.expectedTailSequence,
|
|
548
|
-
expectedTailDigest: validated.expectedTailDigest,
|
|
549
|
-
producerEpoch: validated.producerEpoch,
|
|
550
|
-
records: rawRecords,
|
|
551
|
-
tailDigest,
|
|
552
|
-
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
553
|
-
yield* hitFailpoint("append:before");
|
|
554
|
-
const result = yield* journal.append(rawRequest).pipe(
|
|
555
|
-
Effect.mapError((error) => {
|
|
556
|
-
if (isDoFenceRejected(error)) {
|
|
557
|
-
return mapFence(validated.conversationId, error);
|
|
558
|
-
}
|
|
559
|
-
if (isDoAppendConflict(error)) {
|
|
560
|
-
return error.actualTailSequence !== undefined && isDigest(error.actualTailDigest)
|
|
561
|
-
? AppendConflict.make({
|
|
562
|
-
conversationId: validated.conversationId,
|
|
563
|
-
batchId: validated.batch.batchId,
|
|
564
|
-
reason: error.reason,
|
|
565
|
-
actualTailSequence: error.actualTailSequence,
|
|
566
|
-
actualTailDigest: error.actualTailDigest,
|
|
567
|
-
})
|
|
568
|
-
: AppendConflict.make({
|
|
569
|
-
conversationId: validated.conversationId,
|
|
570
|
-
batchId: validated.batch.batchId,
|
|
571
|
-
reason: error.reason,
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
return storeError("append canonical batch", error);
|
|
575
|
-
}),
|
|
576
|
-
Effect.flatMap((result) =>
|
|
577
|
-
Schema.decodeUnknownEffect(AppendResult)(result).pipe(
|
|
578
|
-
Effect.mapError((error) => schemaStoreError("decode append result", error)),
|
|
493
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
494
|
+
const now = yield* Clock.currentTimeMillis;
|
|
495
|
+
yield* hitFailpoint("materialize:before");
|
|
496
|
+
yield* journal
|
|
497
|
+
.materialize(
|
|
498
|
+
validated.threadId,
|
|
499
|
+
new Date(now).toISOString(),
|
|
500
|
+
EMPTY_TAIL_DIGEST,
|
|
501
|
+
validated.producerEpoch,
|
|
502
|
+
)
|
|
503
|
+
.pipe(
|
|
504
|
+
Effect.mapError((error) =>
|
|
505
|
+
error._tag === "DoFenceRejected"
|
|
506
|
+
? mapFence(validated.threadId, error)
|
|
507
|
+
: storeError("materialize thread", error),
|
|
579
508
|
),
|
|
580
|
-
)
|
|
581
|
-
);
|
|
582
|
-
yield* hitFailpoint("append:after");
|
|
583
|
-
return result;
|
|
509
|
+
);
|
|
510
|
+
yield* hitFailpoint("materialize:after");
|
|
584
511
|
},
|
|
585
512
|
);
|
|
586
513
|
|
|
587
|
-
const
|
|
588
|
-
request:
|
|
514
|
+
const append: ThreadStore["Service"]["append"] = Effect.fn("DoThreadStore.append")(function* (
|
|
515
|
+
request: FencedAppendRequest,
|
|
589
516
|
) {
|
|
517
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(
|
|
518
|
+
request,
|
|
519
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
|
|
520
|
+
yield* requireThread(journal, validated.threadId);
|
|
521
|
+
const tailDigest = yield* provideCrypto(
|
|
522
|
+
digestCanonicalBatch(validated.expectedTailDigest, validated.batch),
|
|
523
|
+
).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
|
|
524
|
+
const batchJson = yield* encodeCanonicalBatch(validated.batch);
|
|
525
|
+
const rawRecords = yield* Effect.forEach(validated.batch.records, (record) =>
|
|
526
|
+
encodeCanonicalRecord(record).pipe(
|
|
527
|
+
Effect.map((recordJson) => ({
|
|
528
|
+
recordId: record.recordId,
|
|
529
|
+
recordJson,
|
|
530
|
+
})),
|
|
531
|
+
),
|
|
532
|
+
);
|
|
533
|
+
const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
|
|
534
|
+
threadId: validated.threadId,
|
|
535
|
+
batchId: validated.batch.batchId,
|
|
536
|
+
batchDigest: tailDigest,
|
|
537
|
+
batchJson,
|
|
538
|
+
expectedTailSequence: validated.expectedTailSequence,
|
|
539
|
+
expectedTailDigest: validated.expectedTailDigest,
|
|
540
|
+
producerEpoch: validated.producerEpoch,
|
|
541
|
+
records: rawRecords,
|
|
542
|
+
tailDigest,
|
|
543
|
+
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
544
|
+
yield* hitFailpoint("append:before");
|
|
545
|
+
const result = yield* journal.append(rawRequest).pipe(
|
|
546
|
+
Effect.mapError((error) => {
|
|
547
|
+
if (isDoFenceRejected(error)) {
|
|
548
|
+
return mapFence(validated.threadId, error);
|
|
549
|
+
}
|
|
550
|
+
if (isDoAppendConflict(error)) {
|
|
551
|
+
return error.actualTailSequence !== undefined && isDigest(error.actualTailDigest)
|
|
552
|
+
? AppendConflict.make({
|
|
553
|
+
threadId: validated.threadId,
|
|
554
|
+
batchId: validated.batch.batchId,
|
|
555
|
+
reason: error.reason,
|
|
556
|
+
actualTailSequence: error.actualTailSequence,
|
|
557
|
+
actualTailDigest: error.actualTailDigest,
|
|
558
|
+
})
|
|
559
|
+
: AppendConflict.make({
|
|
560
|
+
threadId: validated.threadId,
|
|
561
|
+
batchId: validated.batch.batchId,
|
|
562
|
+
reason: error.reason,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return storeError("append canonical batch", error);
|
|
566
|
+
}),
|
|
567
|
+
Effect.flatMap((result) =>
|
|
568
|
+
Schema.decodeUnknownEffect(AppendResult)(result).pipe(
|
|
569
|
+
Effect.mapError((error) => schemaStoreError("decode append result", error)),
|
|
570
|
+
),
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
yield* hitFailpoint("append:after");
|
|
574
|
+
return result;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const loadRecords = Effect.fn("DoThreadStore.loadRecords")(function* (request: RawReadRequest) {
|
|
590
578
|
const rows = yield* journal
|
|
591
579
|
.read(request)
|
|
592
580
|
.pipe(Effect.mapError((error) => storeError("read canonical records", error)));
|
|
593
581
|
return yield* Effect.forEach(rows, decodeEnvelope);
|
|
594
582
|
});
|
|
595
583
|
|
|
596
|
-
const readEffect = Effect.fn("
|
|
597
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
598
|
-
|
|
599
|
-
)
|
|
600
|
-
yield*
|
|
584
|
+
const readEffect = Effect.fn("DoThreadStore.read")(function* (request: ThreadRead) {
|
|
585
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(
|
|
586
|
+
Effect.mapError((error) => schemaStoreError("validate thread read", error)),
|
|
587
|
+
);
|
|
588
|
+
yield* requireThread(journal, validated.threadId);
|
|
601
589
|
const records = yield* loadRecords(
|
|
602
590
|
RawReadRequest.make({
|
|
603
|
-
|
|
591
|
+
threadId: validated.threadId,
|
|
604
592
|
fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
|
|
605
593
|
limit: validated.limit,
|
|
606
594
|
}),
|
|
607
595
|
);
|
|
608
596
|
return Stream.fromIterable(records);
|
|
609
597
|
});
|
|
610
|
-
const read:
|
|
611
|
-
Stream.unwrap(readEffect(request));
|
|
598
|
+
const read: ThreadStore["Service"]["read"] = (request) => Stream.unwrap(readEffect(request));
|
|
612
599
|
|
|
613
|
-
const observeEffect = Effect.fn("
|
|
614
|
-
|
|
615
|
-
) {
|
|
616
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationObservation))(
|
|
600
|
+
const observeEffect = Effect.fn("DoThreadStore.observe")(function* (request: ThreadObservation) {
|
|
601
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(
|
|
617
602
|
request,
|
|
618
|
-
).pipe(
|
|
619
|
-
|
|
620
|
-
);
|
|
621
|
-
yield* requireConversation(journal, validated.conversationId);
|
|
622
|
-
const initialSequence = yield* parseOffset(validated.conversationId, validated.afterOffset);
|
|
603
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
|
|
604
|
+
yield* requireThread(journal, validated.threadId);
|
|
605
|
+
const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
|
|
623
606
|
const cursor = yield* Ref.make(initialSequence);
|
|
624
|
-
const poll = Effect.fn("
|
|
607
|
+
const poll = Effect.fn("DoThreadStore.observePoll")(function* () {
|
|
625
608
|
const fromSequenceExclusive = yield* Ref.get(cursor);
|
|
626
609
|
const records = yield* loadRecords(
|
|
627
610
|
RawReadRequest.make({
|
|
628
|
-
|
|
611
|
+
threadId: validated.threadId,
|
|
629
612
|
fromSequenceExclusive,
|
|
630
613
|
limit: 1_024,
|
|
631
614
|
}),
|
|
@@ -639,163 +622,159 @@ const makeServices = Effect.fn("DoConversationStore.makeServices")(function* ()
|
|
|
639
622
|
});
|
|
640
623
|
return Stream.fromIterableEffectRepeat(poll());
|
|
641
624
|
});
|
|
642
|
-
const observe:
|
|
625
|
+
const observe: ThreadStore["Service"]["observe"] = (request) =>
|
|
643
626
|
Stream.unwrap(observeEffect(request));
|
|
644
627
|
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
628
|
+
const exportThread: ThreadStore["Service"]["export"] = Effect.fn("DoThreadStore.export")(
|
|
629
|
+
function* (request: ThreadExportRequest) {
|
|
630
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(
|
|
631
|
+
request,
|
|
632
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
|
|
633
|
+
yield* requireThread(journal, validated.threadId);
|
|
634
|
+
const exported = yield* journal
|
|
635
|
+
.exportThread(validated.threadId)
|
|
636
|
+
.pipe(Effect.mapError((error) => storeError("export thread", error)));
|
|
637
|
+
const records = yield* Effect.forEach(exported.records, decodeEnvelope);
|
|
638
|
+
if (records.length > 65_536) {
|
|
639
|
+
return yield* ThreadStoreError.make({
|
|
640
|
+
operation: "decode thread export",
|
|
641
|
+
message: "The thread exceeds the current export record limit.",
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(
|
|
645
|
+
exported.thread.tail_digest,
|
|
646
|
+
).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
|
|
647
|
+
return ThreadExport.make({
|
|
648
|
+
format: "effect-agent/thread@1",
|
|
649
|
+
threadId: validated.threadId,
|
|
650
|
+
tailSequence: exported.thread.tail_sequence,
|
|
651
|
+
tailDigest,
|
|
652
|
+
records,
|
|
660
653
|
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
exported.conversation.tail_digest,
|
|
664
|
-
).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
|
|
665
|
-
return ConversationExport.make({
|
|
666
|
-
format: "effect-agent/conversation@1",
|
|
667
|
-
conversationId: validated.conversationId,
|
|
668
|
-
tailSequence: exported.conversation.tail_sequence,
|
|
669
|
-
tailDigest,
|
|
670
|
-
records,
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
const inspectTail: ConversationStore["Service"]["inspectTail"] = Effect.fn(
|
|
675
|
-
"DoConversationStore.inspectTail",
|
|
676
|
-
)(function* (request: ConversationTailRequest) {
|
|
677
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ConversationTailRequest))(
|
|
678
|
-
request,
|
|
679
|
-
).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
|
|
680
|
-
const conversation = yield* requireConversation(journal, validated.conversationId);
|
|
681
|
-
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(conversation.tail_digest).pipe(
|
|
682
|
-
Effect.mapError((error) => schemaStoreError("decode tail digest", error)),
|
|
683
|
-
);
|
|
684
|
-
return ConversationTail.make({
|
|
685
|
-
conversationId: validated.conversationId,
|
|
686
|
-
tailSequence: conversation.tail_sequence,
|
|
687
|
-
tailDigest,
|
|
688
|
-
producerEpoch: conversation.producer_epoch,
|
|
689
|
-
});
|
|
690
|
-
});
|
|
654
|
+
},
|
|
655
|
+
);
|
|
691
656
|
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
journal,
|
|
707
|
-
validated.checkpoint.conversationId,
|
|
708
|
-
validated.checkpoint.throughSequence,
|
|
709
|
-
);
|
|
710
|
-
if (canonicalDigest !== validated.checkpoint.tailDigest) {
|
|
711
|
-
return yield* CheckpointRejected.make({
|
|
712
|
-
conversationId: validated.checkpoint.conversationId,
|
|
713
|
-
reason: "digest-mismatch",
|
|
657
|
+
const inspectTail: ThreadStore["Service"]["inspectTail"] = Effect.fn("DoThreadStore.inspectTail")(
|
|
658
|
+
function* (request: ThreadTailRequest) {
|
|
659
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(
|
|
660
|
+
request,
|
|
661
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
|
|
662
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
663
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(
|
|
664
|
+
Effect.mapError((error) => schemaStoreError("decode tail digest", error)),
|
|
665
|
+
);
|
|
666
|
+
return ThreadTail.make({
|
|
667
|
+
threadId: validated.threadId,
|
|
668
|
+
tailSequence: thread.tail_sequence,
|
|
669
|
+
tailDigest,
|
|
670
|
+
producerEpoch: thread.producer_epoch,
|
|
714
671
|
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const raw = RawCheckpoint.make({
|
|
718
|
-
conversationId: validated.checkpoint.conversationId,
|
|
719
|
-
throughSequence: validated.checkpoint.throughSequence,
|
|
720
|
-
tailDigest: validated.checkpoint.tailDigest,
|
|
721
|
-
checkpointJson,
|
|
722
|
-
});
|
|
723
|
-
yield* hitFailpoint("save-checkpoint:before");
|
|
724
|
-
yield* journal.saveCheckpoint(raw).pipe(
|
|
725
|
-
Effect.mapError((error) =>
|
|
726
|
-
isDoCheckpointConflict(error)
|
|
727
|
-
? CheckpointRejected.make({
|
|
728
|
-
conversationId: validated.checkpoint.conversationId,
|
|
729
|
-
reason: "digest-mismatch",
|
|
730
|
-
})
|
|
731
|
-
: storeError("save checkpoint", error),
|
|
732
|
-
),
|
|
733
|
-
);
|
|
734
|
-
yield* hitFailpoint("save-checkpoint:after");
|
|
735
|
-
});
|
|
672
|
+
},
|
|
673
|
+
);
|
|
736
674
|
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
checkpoint
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
reason: "digest-mismatch",
|
|
675
|
+
const saveCheckpoint: ThreadCheckpoints["save"] = Effect.fn("DoThreadStore.saveCheckpoint")(
|
|
676
|
+
function* (request: SaveCheckpointRequest) {
|
|
677
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(
|
|
678
|
+
request,
|
|
679
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
|
|
680
|
+
const thread = yield* requireThread(journal, validated.checkpoint.threadId);
|
|
681
|
+
if (validated.checkpoint.throughSequence > thread.tail_sequence) {
|
|
682
|
+
return yield* CheckpointRejected.make({
|
|
683
|
+
threadId: validated.checkpoint.threadId,
|
|
684
|
+
reason: "ahead-of-tail",
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
const canonicalDigest = yield* tailDigestAt(
|
|
688
|
+
journal,
|
|
689
|
+
validated.checkpoint.threadId,
|
|
690
|
+
validated.checkpoint.throughSequence,
|
|
691
|
+
);
|
|
692
|
+
if (canonicalDigest !== validated.checkpoint.tailDigest) {
|
|
693
|
+
return yield* CheckpointRejected.make({
|
|
694
|
+
threadId: validated.checkpoint.threadId,
|
|
695
|
+
reason: "digest-mismatch",
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
|
|
699
|
+
const raw = RawCheckpoint.make({
|
|
700
|
+
threadId: validated.checkpoint.threadId,
|
|
701
|
+
throughSequence: validated.checkpoint.throughSequence,
|
|
702
|
+
tailDigest: validated.checkpoint.tailDigest,
|
|
703
|
+
checkpointJson,
|
|
767
704
|
});
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
705
|
+
yield* hitFailpoint("save-checkpoint:before");
|
|
706
|
+
yield* journal.saveCheckpoint(raw).pipe(
|
|
707
|
+
Effect.mapError((error) =>
|
|
708
|
+
isDoCheckpointConflict(error)
|
|
709
|
+
? CheckpointRejected.make({
|
|
710
|
+
threadId: validated.checkpoint.threadId,
|
|
711
|
+
reason: "digest-mismatch",
|
|
712
|
+
})
|
|
713
|
+
: storeError("save checkpoint", error),
|
|
714
|
+
),
|
|
715
|
+
);
|
|
716
|
+
yield* hitFailpoint("save-checkpoint:after");
|
|
717
|
+
},
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
const loadCheckpoint: ThreadCheckpoints["load"] = Effect.fn("DoThreadStore.loadCheckpoint")(
|
|
721
|
+
function* (request: LoadCheckpointRequest) {
|
|
722
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(
|
|
723
|
+
request,
|
|
724
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
|
|
725
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
726
|
+
const rows = yield* journal
|
|
727
|
+
.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
|
|
728
|
+
.pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
|
|
729
|
+
if (rows.length === 0) return Option.none();
|
|
730
|
+
if (rows.length !== 1) {
|
|
731
|
+
return yield* ThreadStoreError.make({
|
|
732
|
+
operation: "load checkpoint",
|
|
733
|
+
message: `Expected at most one checkpoint row but found ${rows.length}.`,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
|
|
737
|
+
const canonicalDigest = yield* tailDigestAt(
|
|
738
|
+
journal,
|
|
739
|
+
checkpoint.threadId,
|
|
740
|
+
checkpoint.throughSequence,
|
|
741
|
+
);
|
|
742
|
+
if (canonicalDigest !== checkpoint.tailDigest) {
|
|
743
|
+
return yield* CheckpointRejected.make({
|
|
744
|
+
threadId: checkpoint.threadId,
|
|
745
|
+
reason: "digest-mismatch",
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return Option.some(checkpoint);
|
|
749
|
+
},
|
|
750
|
+
);
|
|
771
751
|
|
|
772
|
-
const
|
|
752
|
+
const threadStore = ThreadStore.of({
|
|
773
753
|
append,
|
|
774
|
-
export:
|
|
754
|
+
export: exportThread,
|
|
775
755
|
inspectTail,
|
|
776
|
-
loadCheckpoint,
|
|
777
756
|
materialize,
|
|
778
757
|
observe,
|
|
779
758
|
read,
|
|
780
|
-
saveCheckpoint,
|
|
759
|
+
checkpoints: { save: saveCheckpoint, load: loadCheckpoint },
|
|
781
760
|
});
|
|
782
761
|
|
|
783
|
-
return Context.make(
|
|
762
|
+
return Context.make(ThreadStore, threadStore);
|
|
784
763
|
});
|
|
785
764
|
|
|
786
765
|
/**
|
|
787
|
-
* Durable Object
|
|
766
|
+
* Durable Object Thread Store implementation with configuration, failpoint, SQL, and
|
|
788
767
|
* Crypto authority kept visible in its input channel.
|
|
789
768
|
*/
|
|
790
|
-
export const
|
|
791
|
-
|
|
769
|
+
export const threadStoreLayer: Layer.Layer<
|
|
770
|
+
ThreadStore,
|
|
792
771
|
DoStorageInitializationError,
|
|
793
772
|
DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
|
|
794
773
|
> = Layer.effectContext(makeServices());
|
|
795
774
|
|
|
796
775
|
/**
|
|
797
776
|
* Validated Durable Object storage configuration Layer with the documented defaults applied.
|
|
798
|
-
* Shared by the
|
|
777
|
+
* Shared by the ThreadStore and SubmissionLedger convenience layers so their defaults
|
|
799
778
|
* cannot drift.
|
|
800
779
|
*/
|
|
801
780
|
export const storageConfigLayer = (
|
|
@@ -828,17 +807,17 @@ export const storageFailpointLayer = (
|
|
|
828
807
|
: Layer.succeed(DoStorageFailpoint)({ hit: options.failpoint });
|
|
829
808
|
|
|
830
809
|
/**
|
|
831
|
-
* A composition-root convenience Layer for canonical
|
|
810
|
+
* A composition-root convenience Layer for canonical Threads inside one Durable Object,
|
|
832
811
|
* built over `ctx.storage`. Durable accepted work is served by the separate SubmissionLedger
|
|
833
812
|
* port; point both at the SAME `ctx.storage` so claims fence the same producer epochs
|
|
834
813
|
* (ADR-0011 D7's "same file" rule, transposed to one object's private database).
|
|
835
814
|
*/
|
|
836
815
|
export const layer = (
|
|
837
816
|
options: DoStorageOptions,
|
|
838
|
-
): Layer.Layer<
|
|
817
|
+
): Layer.Layer<ThreadStore, DoStorageInitializationError> =>
|
|
839
818
|
Layer.unwrap(
|
|
840
819
|
Effect.map(DoStorageConfig, (config) =>
|
|
841
|
-
|
|
820
|
+
threadStoreLayer.pipe(
|
|
842
821
|
Layer.provide(
|
|
843
822
|
Layer.mergeAll(
|
|
844
823
|
Layer.succeed(DoStorageConfig)(config),
|