@yielded/agent-storage-cloudflare 0.1.0-beta.167 → 0.1.0-beta.169
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/DoMemoryStore.mjs +1 -1
- package/dist/DoMessageDeliveryStore.d.mts +1 -1
- package/dist/DoMessageDeliveryStore.mjs +3 -2
- package/dist/DoMessageDeliveryStore.mjs.map +1 -1
- package/dist/DoScheduleStore.mjs +16 -37
- package/dist/DoScheduleStore.mjs.map +1 -1
- package/dist/DoStorageConfig.d.mts +2 -10
- package/dist/DoStorageConfig.mjs +2 -11
- package/dist/DoStorageConfig.mjs.map +1 -1
- package/dist/{DoStorageError-CGuAfSwF.d.mts → DoStorageError-CxjMnzUA.d.mts} +7 -18
- package/dist/DoStorageError.d.mts +2 -2
- package/dist/DoStorageError.mjs +14 -30
- package/dist/DoStorageError.mjs.map +1 -1
- package/dist/DoStorageFailpoint.d.mts +1 -1
- package/dist/DoStorageFailpointTesting.d.mts +1 -1
- package/dist/{DoStorageVersion-iNSyi3cJ.d.mts → DoStorageVersion-DTFoLglB.d.mts} +3 -4
- package/dist/DoStorageVersion.d.mts +1 -1
- package/dist/DoStorageVersion.mjs +2 -2
- package/dist/DoStorageVersion.mjs.map +1 -1
- package/dist/DoSubmissionLedger.d.mts +7 -5
- package/dist/DoSubmissionLedger.mjs +378 -528
- package/dist/DoSubmissionLedger.mjs.map +1 -1
- package/dist/DoSubscriptionStore.mjs +5 -24
- package/dist/DoSubscriptionStore.mjs.map +1 -1
- package/dist/{DoThreadStore-D_83OjCH.d.mts → DoThreadStore-Bd4fTns3.d.mts} +17 -18
- package/dist/DoThreadStore-CwBvTLoq.mjs +468 -0
- package/dist/DoThreadStore-CwBvTLoq.mjs.map +1 -0
- package/dist/DoThreadStore.d.mts +2 -2
- package/dist/DoThreadStore.mjs +4 -529
- package/dist/MemoryProtocol.mjs +3 -3
- package/dist/MemoryProtocol.mjs.map +1 -1
- package/dist/PortProtocol.d.mts +11727 -3410
- package/dist/PortProtocol.mjs +103 -15
- package/dist/PortProtocol.mjs.map +1 -1
- package/dist/PortRouting.d.mts +14 -9
- package/dist/PortRouting.mjs +155 -29
- package/dist/PortRouting.mjs.map +1 -1
- package/dist/do-journal-Cawnsetj.mjs +761 -0
- package/dist/do-journal-Cawnsetj.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/migrations-D_EP3KYf.mjs +148 -0
- package/dist/migrations-D_EP3KYf.mjs.map +1 -0
- package/dist/{owned-state-DHm2aRXx.mjs → owned-state-BHzUi0eb.mjs} +50 -19
- package/dist/owned-state-BHzUi0eb.mjs.map +1 -0
- package/package.json +1 -1
- package/src/DoMessageDeliveryStore.ts +2 -1
- package/src/DoScheduleStore.ts +19 -59
- package/src/DoStorageConfig.ts +2 -11
- package/src/DoStorageError.ts +3 -26
- package/src/DoStorageVersion.ts +1 -1
- package/src/DoSubmissionLedger.ts +1806 -2030
- package/src/DoSubscriptionStore.ts +5 -47
- package/src/DoThreadStore.ts +276 -622
- package/src/MemoryProtocol.ts +3 -3
- package/src/PortProtocol.ts +198 -12
- package/src/PortRouting.ts +527 -108
- package/src/internal/canonical-append.ts +15 -0
- package/src/internal/do-journal.ts +548 -1440
- package/src/internal/migrations.ts +184 -287
- package/src/internal/owned-state.ts +81 -30
- package/src/internal/prompt-checkpoint.ts +158 -0
- package/src/internal/storage-span.ts +2 -5
- package/dist/DoThreadStore.mjs.map +0 -1
- package/dist/do-journal-DUhjZkOf.mjs +0 -1352
- package/dist/do-journal-DUhjZkOf.mjs.map +0 -1
- package/dist/migrations-DtKz_Gxr.mjs +0 -306
- package/dist/migrations-DtKz_Gxr.mjs.map +0 -1
- package/dist/owned-state-DHm2aRXx.mjs.map +0 -1
- package/src/internal/message-delivery-schema.ts +0 -24
- package/src/internal/recovery-checkpoint-schema.ts +0 -17
package/src/DoThreadStore.ts
CHANGED
|
@@ -1,58 +1,46 @@
|
|
|
1
1
|
import { BrowserCrypto } from "@effect/platform-browser";
|
|
2
2
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
3
|
+
import { makeSqlThreadImport } from "@yielded/agent-storage-sql/sql-thread-import";
|
|
3
4
|
import {
|
|
4
5
|
makeSelectedReads,
|
|
5
6
|
SelectedReadOwner,
|
|
6
7
|
} from "@yielded/agent-storage-sql/sql-thread-native-reads";
|
|
7
|
-
import {
|
|
8
|
+
import { ExportRecord } from "@yielded/agent/record-format";
|
|
8
9
|
import {
|
|
9
|
-
CanonicalBatch,
|
|
10
|
-
CanonicalRecord,
|
|
11
10
|
CanonicalRecordEnvelope,
|
|
12
11
|
CanonicalSequence,
|
|
13
|
-
Digest,
|
|
14
12
|
ObservationOffset,
|
|
15
13
|
} from "@yielded/agent/records";
|
|
14
|
+
import { StorageOwner } from "@yielded/agent/run-storage";
|
|
16
15
|
import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
|
|
17
|
-
import {
|
|
16
|
+
import { MAX_ARCHIVE_RANGE_PAGE } from "@yielded/agent/thread-archive-range";
|
|
17
|
+
import { ThreadImport } from "@yielded/agent/thread-import";
|
|
18
18
|
import {
|
|
19
|
+
type ThreadExportRequest,
|
|
19
20
|
AppendConflict,
|
|
20
21
|
AppendResult,
|
|
21
22
|
CheckpointRejected,
|
|
22
23
|
ThreadCheckpoint,
|
|
23
|
-
ThreadExport,
|
|
24
|
-
ThreadExportRequest,
|
|
25
24
|
ThreadMaterialization,
|
|
26
25
|
ThreadNotMaterialized,
|
|
27
26
|
ThreadObservation,
|
|
28
27
|
ThreadReadRequest,
|
|
29
28
|
ThreadStore,
|
|
29
|
+
ThreadReader,
|
|
30
30
|
type ThreadCheckpoints,
|
|
31
31
|
ThreadStoreError,
|
|
32
32
|
ThreadStoreDiagnostic,
|
|
33
33
|
ThreadTail,
|
|
34
34
|
ThreadTailRequest,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
type AppendRequest,
|
|
36
|
+
PreparedAppend,
|
|
37
37
|
LoadCheckpointRequest,
|
|
38
38
|
SaveCheckpointRequest,
|
|
39
|
-
SaveRecoveryCheckpointRequest,
|
|
40
|
-
MAX_THREAD_EXPORT_RECORDS,
|
|
41
|
-
type ThreadRecoveryCheckpoints,
|
|
42
39
|
} from "@yielded/agent/thread-store";
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
Context,
|
|
46
|
-
Crypto,
|
|
47
|
-
Duration,
|
|
48
|
-
Effect,
|
|
49
|
-
Layer,
|
|
50
|
-
Option,
|
|
51
|
-
Ref,
|
|
52
|
-
Schema,
|
|
53
|
-
Stream,
|
|
54
|
-
} from "effect";
|
|
40
|
+
import { checkExportSource } from "@yielded/agent/thread-transfer";
|
|
41
|
+
import { Clock, Context, Crypto, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
|
|
55
42
|
import * as SqlClientService from "effect/sql/SqlClient";
|
|
43
|
+
import { isSqlError } from "effect/sql/SqlError";
|
|
56
44
|
|
|
57
45
|
import {
|
|
58
46
|
DEFAULT_MAX_STORED_VALUE_BYTES,
|
|
@@ -63,20 +51,21 @@ import {
|
|
|
63
51
|
type DoStorageCompatibilityError,
|
|
64
52
|
DoAppendConflict,
|
|
65
53
|
DoCheckpointConflict,
|
|
66
|
-
DoFenceRejected,
|
|
67
54
|
type DoStorageFailpointLocation,
|
|
68
55
|
DoStorageCorruptionError,
|
|
69
56
|
DoStorageError,
|
|
70
57
|
} from "./DoStorageError.ts";
|
|
71
58
|
import { DoStorageFailpoint, type DoStorageFailpointHandler } from "./DoStorageFailpoint.ts";
|
|
59
|
+
import { DO_OFFSET_PREFIX, prepareCanonicalAppend } from "./internal/canonical-append.ts";
|
|
72
60
|
import {
|
|
73
61
|
initializeDoJournal,
|
|
74
|
-
RawAppendRequest,
|
|
75
62
|
RawCheckpoint,
|
|
76
63
|
RawReadRequest,
|
|
77
64
|
type DoJournal,
|
|
78
65
|
} from "./internal/do-journal.ts";
|
|
66
|
+
import { readDoStorageHeader } from "./internal/migrations.ts";
|
|
79
67
|
import { invalidateOwnedState, ownedState } from "./internal/owned-state.ts";
|
|
68
|
+
import { makePromptCheckpoints, promptCheckpointPrefix } from "./internal/prompt-checkpoint.ts";
|
|
80
69
|
import { isAppendContention, withStorageSpan } from "./internal/storage-span.ts";
|
|
81
70
|
|
|
82
71
|
/**
|
|
@@ -87,20 +76,15 @@ import { isAppendContention, withStorageSpan } from "./internal/storage-span.ts"
|
|
|
87
76
|
export interface DoStorageOptions {
|
|
88
77
|
readonly storage: DurableObjectStorage;
|
|
89
78
|
readonly observationPollInterval?: number | undefined;
|
|
90
|
-
/**
|
|
91
|
-
* Submission ownership lease duration in milliseconds (D5). Defaults to
|
|
92
|
-
* `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@yielded/agent/submission-ledger`.
|
|
93
|
-
*/
|
|
94
|
-
readonly ownershipLeaseDuration?: number | undefined;
|
|
95
79
|
/**
|
|
96
80
|
* Maximum bytes for any single stored value; must stay under the platform's 2 MB
|
|
97
81
|
* per-value limit. Defaults to `DEFAULT_MAX_STORED_VALUE_BYTES`.
|
|
98
82
|
*/
|
|
99
83
|
readonly maxStoredValueBytes?: number | undefined;
|
|
100
84
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
85
|
+
* Audit journal payloads, ownership, and batch coverage in bounded pages on open, including
|
|
86
|
+
* orphan rows and rows outside declared ranges. Defaults to false. Full reads validate
|
|
87
|
+
* canonical records; readPrompt trusts write-time validation of the omitted fields.
|
|
104
88
|
*/
|
|
105
89
|
readonly verifyOnOpen?: boolean | undefined;
|
|
106
90
|
readonly failpoint?: DoStorageFailpointHandler | undefined;
|
|
@@ -112,16 +96,12 @@ export type DoStorageInitializationError =
|
|
|
112
96
|
| DoStorageError;
|
|
113
97
|
|
|
114
98
|
const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
|
|
115
|
-
const DO_OFFSET_PREFIX = "effect-agent-do@1:";
|
|
116
99
|
const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
|
|
117
|
-
const isDigest = Schema.is(Digest);
|
|
118
|
-
const isDoFenceRejected = Schema.is(DoFenceRejected);
|
|
119
100
|
const isDoAppendConflict = Schema.is(DoAppendConflict);
|
|
120
101
|
const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
|
|
121
102
|
// Reuse the same AST/parser across reads. Reconstructing a JSON codec per row
|
|
122
103
|
// defeats Schema's parser cache and repeatedly walks the canonical union.
|
|
123
|
-
const CanonicalRecordJson = Schema.fromJsonString(
|
|
124
|
-
const CanonicalBatchJson = Schema.fromJsonString(CanonicalBatch);
|
|
104
|
+
const CanonicalRecordJson = Schema.fromJsonString(ExportRecord);
|
|
125
105
|
const ThreadCheckpointJson = Schema.fromJsonString(ThreadCheckpoint);
|
|
126
106
|
const decodeCanonicalRecord = Schema.decodeEffect(CanonicalRecordJson);
|
|
127
107
|
|
|
@@ -147,11 +127,11 @@ const schemaStoreError = (operation: string, error: Schema.SchemaError) =>
|
|
|
147
127
|
}),
|
|
148
128
|
});
|
|
149
129
|
|
|
150
|
-
const makeOffset =
|
|
130
|
+
const makeOffset = (
|
|
151
131
|
threadId: ThreadMaterialization["threadId"],
|
|
152
132
|
sequence: number,
|
|
153
|
-
): Effect.
|
|
154
|
-
|
|
133
|
+
): Effect.Effect<ObservationOffset, ThreadStoreError> =>
|
|
134
|
+
Schema.decodeEffect(CanonicalSequence)(sequence).pipe(
|
|
155
135
|
Effect.flatMap((validatedSequence) =>
|
|
156
136
|
Schema.decodeEffect(ObservationOffset)(
|
|
157
137
|
`${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`,
|
|
@@ -159,9 +139,8 @@ const makeOffset = Effect.fn(function* (
|
|
|
159
139
|
),
|
|
160
140
|
Effect.mapError((error) => schemaStoreError("encode observation offset", error)),
|
|
161
141
|
);
|
|
162
|
-
});
|
|
163
142
|
|
|
164
|
-
const parseOffset = Effect.
|
|
143
|
+
const parseOffset = Effect.fnUntraced(function* (
|
|
165
144
|
threadId: ThreadMaterialization["threadId"],
|
|
166
145
|
offset: ObservationOffset | undefined,
|
|
167
146
|
): Effect.fn.Return<CanonicalSequence, ThreadStoreError> {
|
|
@@ -193,36 +172,10 @@ const parseOffset = Effect.fn(function* (
|
|
|
193
172
|
);
|
|
194
173
|
});
|
|
195
174
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
threadId,
|
|
199
|
-
actualEpoch: error.actualEpoch,
|
|
200
|
-
attemptedEpoch: error.producerEpoch,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const encodeCanonicalRecord = Effect.fn(function* (
|
|
204
|
-
record: CanonicalRecord,
|
|
205
|
-
): Effect.fn.Return<string, ThreadStoreError> {
|
|
206
|
-
return yield* Schema.encodeEffect(CanonicalRecordJson)(record).pipe(
|
|
207
|
-
Effect.mapError((error) => schemaStoreError("encode canonical record", error)),
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const encodeCanonicalBatch = Effect.fn(function* (
|
|
212
|
-
batch: CanonicalBatch,
|
|
213
|
-
): Effect.fn.Return<string, ThreadStoreError> {
|
|
214
|
-
return yield* Schema.encodeEffect(CanonicalBatchJson)(batch).pipe(
|
|
215
|
-
Effect.mapError((error) => schemaStoreError("encode canonical batch", error)),
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const encodeCheckpoint = Effect.fn(function* (
|
|
220
|
-
checkpoint: ThreadCheckpoint,
|
|
221
|
-
): Effect.fn.Return<string, ThreadStoreError> {
|
|
222
|
-
return yield* Schema.encodeEffect(ThreadCheckpointJson)(checkpoint).pipe(
|
|
175
|
+
const encodeCheckpoint = (checkpoint: ThreadCheckpoint): Effect.Effect<string, ThreadStoreError> =>
|
|
176
|
+
Schema.encodeEffect(ThreadCheckpointJson)(checkpoint).pipe(
|
|
223
177
|
Effect.mapError((error) => schemaStoreError("encode checkpoint", error)),
|
|
224
178
|
);
|
|
225
|
-
});
|
|
226
179
|
|
|
227
180
|
const envelopes = new WeakMap<object, CanonicalRecordEnvelope>();
|
|
228
181
|
|
|
@@ -271,28 +224,24 @@ const decodeEnvelope = Effect.fnUntraced(function* (row: {
|
|
|
271
224
|
row.batch_id,
|
|
272
225
|
).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
|
|
273
226
|
|
|
274
|
-
const envelope = CanonicalRecordEnvelope
|
|
275
|
-
threadId,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
offset,
|
|
279
|
-
record,
|
|
280
|
-
});
|
|
227
|
+
const envelope = new CanonicalRecordEnvelope(
|
|
228
|
+
{ threadId, batchId, sequence: row.sequence, offset, record },
|
|
229
|
+
{ disableChecks: true },
|
|
230
|
+
);
|
|
281
231
|
|
|
282
232
|
envelopes.set(row, envelope);
|
|
283
233
|
|
|
284
234
|
return envelope;
|
|
285
235
|
});
|
|
286
236
|
|
|
287
|
-
const decodeCheckpoint =
|
|
237
|
+
const decodeCheckpoint = (
|
|
288
238
|
checkpointJson: string,
|
|
289
|
-
): Effect.
|
|
290
|
-
|
|
239
|
+
): Effect.Effect<ThreadCheckpoint, ThreadStoreError> =>
|
|
240
|
+
Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(
|
|
291
241
|
Effect.mapError((error) => schemaStoreError("decode checkpoint", error)),
|
|
292
242
|
);
|
|
293
|
-
});
|
|
294
243
|
|
|
295
|
-
const requireThread = Effect.
|
|
244
|
+
const requireThread = Effect.fnUntraced(function* (
|
|
296
245
|
journal: DoJournal,
|
|
297
246
|
threadId: ThreadMaterialization["threadId"],
|
|
298
247
|
) {
|
|
@@ -307,243 +256,40 @@ const requireThread = Effect.fn("DoThreadStore.requireThread")(function* (
|
|
|
307
256
|
return rows[0];
|
|
308
257
|
});
|
|
309
258
|
|
|
310
|
-
const
|
|
259
|
+
const requireBatchBoundary = Effect.fnUntraced(function* (
|
|
311
260
|
journal: DoJournal,
|
|
312
261
|
threadId: ThreadMaterialization["threadId"],
|
|
313
262
|
sequence: CanonicalSequence,
|
|
314
263
|
) {
|
|
315
|
-
|
|
264
|
+
const valid = yield* journal
|
|
265
|
+
.isBatchBoundary(threadId, sequence)
|
|
266
|
+
.pipe(Effect.mapError((error) => storeError("read checkpoint boundary", error)));
|
|
316
267
|
|
|
317
|
-
|
|
318
|
-
.getTailDigestAt(threadId, sequence)
|
|
319
|
-
.pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
|
|
320
|
-
|
|
321
|
-
if (digests.length !== 1) {
|
|
268
|
+
if (!valid) {
|
|
322
269
|
return yield* CheckpointRejected.make({
|
|
323
270
|
threadId,
|
|
324
|
-
reason: "
|
|
271
|
+
reason: "not-batch-boundary",
|
|
325
272
|
});
|
|
326
273
|
}
|
|
327
|
-
|
|
328
|
-
return yield* Schema.decodeEffect(Digest)(digests[0]).pipe(
|
|
329
|
-
Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)),
|
|
330
|
-
);
|
|
331
274
|
});
|
|
332
275
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const grouped = new Map<string, Array<A>>();
|
|
276
|
+
/** Audit excluded rows before paging owners and bounded ranges; never collect Thread payloads. */
|
|
277
|
+
const decodeStartupPayloads = Effect.fnUntraced(function* (journal: DoJournal) {
|
|
278
|
+
yield* journal.archiveRanges.verifyCoverage;
|
|
279
|
+
let afterThreadId: string | undefined;
|
|
338
280
|
|
|
339
|
-
|
|
340
|
-
const
|
|
281
|
+
while (true) {
|
|
282
|
+
const owners = yield* journal.archiveRanges.threadPage(afterThreadId);
|
|
341
283
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
existing.push(row);
|
|
284
|
+
for (const owner of owners) {
|
|
285
|
+
yield* journal.archiveRanges.verifyThread(owner.thread_id);
|
|
286
|
+
afterThreadId = owner.thread_id;
|
|
346
287
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return grouped;
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Opt-in integrity audit (`verifyOnOpen`) of canonical payloads, their digest chains and generic
|
|
354
|
-
* projection checkpoints. Disposable recovery checkpoints are validated when loaded. Routine opens
|
|
355
|
-
* skip this scan: per-operation Schema decoding fails clearly on corrupt canonical rows.
|
|
356
|
-
*/
|
|
357
|
-
const decodeStartupPayloads = Effect.fn("DoThreadStore.decodeStartupPayloads")(function* (
|
|
358
|
-
journal: DoJournal,
|
|
359
|
-
crypto: Crypto.Crypto,
|
|
360
|
-
) {
|
|
361
|
-
const stored = yield* journal.scanStoredPayloads();
|
|
362
|
-
|
|
363
|
-
const batches = yield* Effect.forEach(stored.batches, (batch) =>
|
|
364
|
-
Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(
|
|
365
|
-
Effect.map((decoded) => ({ decoded, row: batch })),
|
|
366
|
-
Effect.mapError((error) =>
|
|
367
|
-
DoStorageCorruptionError.make({
|
|
368
|
-
table: "effect_agent_canonical_batches",
|
|
369
|
-
rowKey: `${batch.thread_id}/${batch.batch_id}`,
|
|
370
|
-
message: error.message,
|
|
371
|
-
}),
|
|
372
|
-
),
|
|
373
|
-
),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
const records = yield* Effect.forEach(stored.records, (record) =>
|
|
377
|
-
Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(
|
|
378
|
-
Effect.map((decoded) => ({ decoded, row: record })),
|
|
379
|
-
Effect.mapError((error) =>
|
|
380
|
-
DoStorageCorruptionError.make({
|
|
381
|
-
table: "effect_agent_canonical_records",
|
|
382
|
-
rowKey: `${record.thread_id}/${record.sequence}`,
|
|
383
|
-
message: error.message,
|
|
384
|
-
}),
|
|
385
|
-
),
|
|
386
|
-
),
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) =>
|
|
390
|
-
Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(
|
|
391
|
-
Effect.map((decoded) => ({ decoded, row: checkpoint })),
|
|
392
|
-
Effect.mapError((error) =>
|
|
393
|
-
DoStorageCorruptionError.make({
|
|
394
|
-
table: "effect_agent_checkpoints",
|
|
395
|
-
rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
|
|
396
|
-
message: error.message,
|
|
397
|
-
}),
|
|
398
|
-
),
|
|
399
|
-
),
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
|
|
403
|
-
const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
|
|
404
|
-
const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
|
|
405
|
-
const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
|
|
406
|
-
|
|
407
|
-
for (const thread of stored.threads) {
|
|
408
|
-
const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
|
|
409
|
-
const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
|
|
410
|
-
const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
|
|
411
|
-
const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
|
|
412
|
-
let previousDigest = EMPTY_TAIL_DIGEST;
|
|
413
|
-
let expectedSequence = 1;
|
|
414
|
-
const tailDigests = new Map<number, string>([[0, EMPTY_TAIL_DIGEST]]);
|
|
415
|
-
|
|
416
|
-
for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
|
|
417
|
-
const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
canonicalBatch.batchId !== batchRow.batch_id ||
|
|
421
|
-
batchRow.first_sequence !== expectedSequence ||
|
|
422
|
-
batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1
|
|
423
|
-
) {
|
|
424
|
-
return yield* DoStorageCorruptionError.make({
|
|
425
|
-
table: "effect_agent_canonical_batches",
|
|
426
|
-
rowKey: key,
|
|
427
|
-
message: "Canonical batch identity, sequence, or record count is inconsistent.",
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const digest = yield* digestCanonicalBatch(previousDigest, canonicalBatch).pipe(
|
|
432
|
-
Effect.provideService(Crypto.Crypto, crypto),
|
|
433
|
-
Effect.mapError((error) =>
|
|
434
|
-
DoStorageCorruptionError.make({
|
|
435
|
-
table: "effect_agent_canonical_batches",
|
|
436
|
-
rowKey: key,
|
|
437
|
-
message: error.message,
|
|
438
|
-
}),
|
|
439
|
-
),
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
if (batchRow.batch_digest !== digest || batchRow.tail_digest !== digest) {
|
|
443
|
-
return yield* DoStorageCorruptionError.make({
|
|
444
|
-
table: "effect_agent_canonical_batches",
|
|
445
|
-
rowKey: key,
|
|
446
|
-
message: "Canonical batch digest does not match its decoded content and prior tail.",
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const batchRecords = recordsByBatch.get(batchRow.batch_id) ?? [];
|
|
451
|
-
|
|
452
|
-
if (batchRecords.length !== canonicalBatch.records.length) {
|
|
453
|
-
return yield* DoStorageCorruptionError.make({
|
|
454
|
-
table: "effect_agent_canonical_records",
|
|
455
|
-
rowKey: key,
|
|
456
|
-
message: "Canonical batch and record-table counts differ.",
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
for (let index = 0; index < canonicalBatch.records.length; index++) {
|
|
460
|
-
const expectedRecord = canonicalBatch.records[index];
|
|
461
|
-
const storedRecord = batchRecords[index];
|
|
462
|
-
|
|
463
|
-
const expectedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
|
|
464
|
-
expectedRecord,
|
|
465
|
-
).pipe(
|
|
466
|
-
Effect.mapError((error) =>
|
|
467
|
-
DoStorageCorruptionError.make({
|
|
468
|
-
table: "effect_agent_canonical_batches",
|
|
469
|
-
rowKey: key,
|
|
470
|
-
message: error.message,
|
|
471
|
-
}),
|
|
472
|
-
),
|
|
473
|
-
);
|
|
474
|
-
|
|
475
|
-
const storedJson = yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(
|
|
476
|
-
storedRecord.decoded,
|
|
477
|
-
).pipe(
|
|
478
|
-
Effect.mapError((error) =>
|
|
479
|
-
DoStorageCorruptionError.make({
|
|
480
|
-
table: "effect_agent_canonical_records",
|
|
481
|
-
rowKey: `${key}/${storedRecord.row.sequence}`,
|
|
482
|
-
message: error.message,
|
|
483
|
-
}),
|
|
484
|
-
),
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
storedRecord.row.sequence !== batchRow.first_sequence + index ||
|
|
489
|
-
storedRecord.row.record_id !== expectedRecord.recordId ||
|
|
490
|
-
expectedJson !== storedJson
|
|
491
|
-
) {
|
|
492
|
-
return yield* DoStorageCorruptionError.make({
|
|
493
|
-
table: "effect_agent_canonical_records",
|
|
494
|
-
rowKey: `${key}/${storedRecord.row.sequence}`,
|
|
495
|
-
message: "Canonical record identity, sequence, or payload differs from its batch.",
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
previousDigest = digest;
|
|
501
|
-
expectedSequence = batchRow.last_sequence + 1;
|
|
502
|
-
tailDigests.set(batchRow.last_sequence, digest);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
threadRecords.length !== thread.tail_sequence ||
|
|
507
|
-
thread.tail_sequence !== expectedSequence - 1 ||
|
|
508
|
-
thread.tail_digest !== previousDigest
|
|
509
|
-
) {
|
|
510
|
-
return yield* DoStorageCorruptionError.make({
|
|
511
|
-
table: "effect_agent_threads",
|
|
512
|
-
rowKey: thread.thread_id,
|
|
513
|
-
message: "Thread tail does not match its canonical batch chain.",
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
for (const checkpoint of threadCheckpoints) {
|
|
518
|
-
if (
|
|
519
|
-
checkpoint.decoded.threadId !== thread.thread_id ||
|
|
520
|
-
checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence ||
|
|
521
|
-
checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest ||
|
|
522
|
-
tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest
|
|
523
|
-
) {
|
|
524
|
-
return yield* DoStorageCorruptionError.make({
|
|
525
|
-
table: "effect_agent_checkpoints",
|
|
526
|
-
rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
|
|
527
|
-
message: "Checkpoint identity or digest is not bound to a canonical batch tail.",
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (
|
|
534
|
-
batches.some(({ row }) => !materializedIds.has(row.thread_id)) ||
|
|
535
|
-
records.some(({ row }) => !materializedIds.has(row.thread_id)) ||
|
|
536
|
-
checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))
|
|
537
|
-
) {
|
|
538
|
-
return yield* DoStorageCorruptionError.make({
|
|
539
|
-
table: "effect_agent_threads",
|
|
540
|
-
rowKey: "startup_scan",
|
|
541
|
-
message: "Canonical rows exist without a materialized Thread.",
|
|
542
|
-
});
|
|
288
|
+
if (owners.length < MAX_ARCHIVE_RANGE_PAGE) break;
|
|
543
289
|
}
|
|
544
290
|
});
|
|
545
291
|
|
|
546
|
-
const makeServices = Effect.
|
|
292
|
+
const makeServices = Effect.fnUntraced(function* () {
|
|
547
293
|
const config = yield* DoStorageConfig;
|
|
548
294
|
const failpoint = yield* DoStorageFailpoint;
|
|
549
295
|
const sql = yield* SqlClientService.SqlClient;
|
|
@@ -551,113 +297,78 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
|
551
297
|
const journal = yield* initializeDoJournal(sql, failpoint.hit, config.maxStoredValueBytes);
|
|
552
298
|
|
|
553
299
|
if (config.verifyOnOpen) {
|
|
554
|
-
yield* decodeStartupPayloads(journal
|
|
300
|
+
yield* journal.state.read(decodeStartupPayloads(journal)).pipe(
|
|
301
|
+
Effect.mapError((cause) =>
|
|
302
|
+
DoStorageCorruptionError.make({
|
|
303
|
+
table: "effect_agent_journal_ranges",
|
|
304
|
+
rowKey: "verification",
|
|
305
|
+
message: cause.message,
|
|
306
|
+
}),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
555
309
|
}
|
|
556
310
|
|
|
557
|
-
const
|
|
558
|
-
|
|
311
|
+
const hitFailpoint = (
|
|
312
|
+
location: DoStorageFailpointLocation,
|
|
313
|
+
): Effect.Effect<void, ThreadStoreError> =>
|
|
314
|
+
failpoint
|
|
315
|
+
.hit(location)
|
|
316
|
+
.pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error)));
|
|
559
317
|
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
318
|
+
const materialize: ThreadStore["Service"]["materialize"] = Effect.fnUntraced(function* (
|
|
319
|
+
request: ThreadMaterialization,
|
|
320
|
+
) {
|
|
321
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ThreadMaterialization))(
|
|
322
|
+
request,
|
|
323
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
566
324
|
|
|
567
|
-
|
|
568
|
-
function* (request: ThreadMaterialization) {
|
|
569
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(ThreadMaterialization))(
|
|
570
|
-
request,
|
|
571
|
-
).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
572
|
-
|
|
573
|
-
const now = yield* Clock.currentTimeMillis;
|
|
574
|
-
|
|
575
|
-
yield* hitFailpoint("materialize:before");
|
|
576
|
-
yield* journal
|
|
577
|
-
.materialize(
|
|
578
|
-
validated.threadId,
|
|
579
|
-
new Date(now).toISOString(),
|
|
580
|
-
EMPTY_TAIL_DIGEST,
|
|
581
|
-
validated.producerEpoch,
|
|
582
|
-
)
|
|
583
|
-
.pipe(
|
|
584
|
-
Effect.mapError((error) =>
|
|
585
|
-
error._tag === "DoFenceRejected"
|
|
586
|
-
? mapFence(validated.threadId, error)
|
|
587
|
-
: storeError("materialize thread", error),
|
|
588
|
-
),
|
|
589
|
-
);
|
|
590
|
-
yield* hitFailpoint("materialize:after");
|
|
591
|
-
},
|
|
592
|
-
withStorageSpan("DoThreadStore.materialize", (error) => error._tag === "FenceRejected"),
|
|
593
|
-
);
|
|
325
|
+
const now = yield* Clock.currentTimeMillis;
|
|
594
326
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
327
|
+
yield* hitFailpoint("materialize:before");
|
|
328
|
+
yield* journal
|
|
329
|
+
.materialize(validated.threadId, new Date(now).toISOString())
|
|
330
|
+
.pipe(Effect.mapError((error) => storeError("materialize thread", error)));
|
|
331
|
+
yield* hitFailpoint("materialize:after");
|
|
332
|
+
}, withStorageSpan("DoThreadStore.materialize"));
|
|
600
333
|
|
|
334
|
+
const append: ThreadStore["Service"]["append"] = Effect.fnUntraced(
|
|
335
|
+
function* (request: AppendRequest) {
|
|
336
|
+
const validated = yield* PreparedAppend.capture(request);
|
|
601
337
|
const observed = yield* requireThread(journal, validated.threadId);
|
|
602
338
|
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
|
|
606
|
-
|
|
607
|
-
const batchJson = yield* encodeCanonicalBatch(validated.batch);
|
|
608
|
-
|
|
609
|
-
const rawRecords = yield* Effect.forEach(validated.batch.records, (record) =>
|
|
610
|
-
encodeCanonicalRecord(record).pipe(
|
|
611
|
-
Effect.map((recordJson) => ({
|
|
612
|
-
recordId: record.recordId,
|
|
613
|
-
recordJson,
|
|
614
|
-
})),
|
|
615
|
-
),
|
|
339
|
+
const { raw: rawRequest } = yield* prepareCanonicalAppend(validated).pipe(
|
|
340
|
+
Effect.provideService(Crypto.Crypto, crypto),
|
|
616
341
|
);
|
|
617
342
|
|
|
618
|
-
const rawRequest = yield* Schema.decodeEffect(RawAppendRequest)({
|
|
619
|
-
threadId: validated.threadId,
|
|
620
|
-
batchId: validated.batch.batchId,
|
|
621
|
-
batchDigest: tailDigest,
|
|
622
|
-
batchJson,
|
|
623
|
-
expectedTailSequence: validated.expectedTailSequence,
|
|
624
|
-
expectedTailDigest: validated.expectedTailDigest,
|
|
625
|
-
producerEpoch: validated.producerEpoch,
|
|
626
|
-
records: rawRecords,
|
|
627
|
-
tailDigest,
|
|
628
|
-
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
629
|
-
|
|
630
343
|
yield* hitFailpoint("append:before");
|
|
631
344
|
|
|
632
|
-
const result = yield* journal.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
Effect.mapError((error) => schemaStoreError("decode append result", error)),
|
|
345
|
+
const result = yield* journal.state.owner
|
|
346
|
+
.withProgress(rawRequest.threadId, journal.append(rawRequest, observed))
|
|
347
|
+
.pipe(
|
|
348
|
+
Effect.mapError((error) => {
|
|
349
|
+
if (isDoAppendConflict(error)) {
|
|
350
|
+
return error.actualTailSequence !== undefined
|
|
351
|
+
? AppendConflict.make({
|
|
352
|
+
threadId: validated.threadId,
|
|
353
|
+
batchId: validated.batch.batchId,
|
|
354
|
+
reason: error.reason,
|
|
355
|
+
actualTailSequence: error.actualTailSequence,
|
|
356
|
+
})
|
|
357
|
+
: AppendConflict.make({
|
|
358
|
+
threadId: validated.threadId,
|
|
359
|
+
batchId: validated.batch.batchId,
|
|
360
|
+
reason: error.reason,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return storeError("append canonical batch", error);
|
|
365
|
+
}),
|
|
366
|
+
Effect.flatMap((result) =>
|
|
367
|
+
Schema.decodeEffect(AppendResult)(result).pipe(
|
|
368
|
+
Effect.mapError((error) => schemaStoreError("decode append result", error)),
|
|
369
|
+
),
|
|
658
370
|
),
|
|
659
|
-
)
|
|
660
|
-
);
|
|
371
|
+
);
|
|
661
372
|
|
|
662
373
|
yield* hitFailpoint("append:after");
|
|
663
374
|
|
|
@@ -680,7 +391,7 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
|
680
391
|
};
|
|
681
392
|
});
|
|
682
393
|
|
|
683
|
-
const readEffect = Effect.
|
|
394
|
+
const readEffect = Effect.fnUntraced(function* (request: ThreadReadRequest) {
|
|
684
395
|
const validated = yield* Schema.decodeEffect(Schema.toType(ThreadReadRequest))(request).pipe(
|
|
685
396
|
Effect.mapError((error) => schemaStoreError("validate thread read", error)),
|
|
686
397
|
);
|
|
@@ -758,244 +469,128 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
|
758
469
|
const observe: ThreadStore["Service"]["observe"] = (request) =>
|
|
759
470
|
Stream.unwrap(observeEffect(request)).pipe(Stream.withSpan("DoThreadStore.observe"));
|
|
760
471
|
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
)
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
tailSequence: exported.thread.tail_sequence,
|
|
790
|
-
tailDigest,
|
|
791
|
-
records,
|
|
792
|
-
});
|
|
793
|
-
},
|
|
794
|
-
);
|
|
795
|
-
|
|
796
|
-
const inspectTail: ThreadStore["Service"]["inspectTail"] = Effect.fn("DoThreadStore.inspectTail")(
|
|
797
|
-
function* (request: ThreadTailRequest) {
|
|
798
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(ThreadTailRequest))(request).pipe(
|
|
799
|
-
Effect.mapError((error) => schemaStoreError("validate tail inspection", error)),
|
|
800
|
-
);
|
|
801
|
-
|
|
802
|
-
const thread = yield* requireThread(journal, validated.threadId);
|
|
803
|
-
|
|
804
|
-
const tailDigest = yield* Schema.decodeEffect(Digest)(thread.tail_digest).pipe(
|
|
805
|
-
Effect.mapError((error) => schemaStoreError("decode tail digest", error)),
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
return ThreadTail.make({
|
|
809
|
-
threadId: validated.threadId,
|
|
810
|
-
tailSequence: thread.tail_sequence,
|
|
811
|
-
tailDigest,
|
|
812
|
-
producerEpoch: thread.producer_epoch,
|
|
813
|
-
});
|
|
814
|
-
},
|
|
815
|
-
);
|
|
472
|
+
const transfer = yield* makeSqlThreadImport({
|
|
473
|
+
owner: journal.state.owner,
|
|
474
|
+
offsetPrefix: DO_OFFSET_PREFIX,
|
|
475
|
+
maxValueBytes: config.maxStoredValueBytes,
|
|
476
|
+
// Import establishes a new sequence space; discard derivatives in this same transaction.
|
|
477
|
+
afterImport: ({ result }) =>
|
|
478
|
+
Effect.gen(function* () {
|
|
479
|
+
const prefix = promptCheckpointPrefix(result.threadId);
|
|
480
|
+
|
|
481
|
+
yield* sql`DELETE FROM effect_agent_meta WHERE key >= ${prefix} AND key < ${prefix + "~"}`;
|
|
482
|
+
// Closed imported history must not emit historical lifecycle notifications again.
|
|
483
|
+
if (journal.lifecycle !== undefined)
|
|
484
|
+
yield* sql`INSERT INTO effect_agent_lifecycle_cursors(thread_id, through_sequence) VALUES (${result.threadId}, ${result.tailSequence})`;
|
|
485
|
+
}).pipe(
|
|
486
|
+
Effect.catchTag("SqlError", (cause) => storeError("rebuild imported derivatives", cause)),
|
|
487
|
+
),
|
|
488
|
+
afterThreadRead: hitFailpoint("export:after-thread-read"),
|
|
489
|
+
read: (body) =>
|
|
490
|
+
journal.owner.transaction(body).pipe(
|
|
491
|
+
Effect.provideService(SqlClientService.SqlClient, sql),
|
|
492
|
+
Effect.catchIf(isSqlError, (cause) => storeError("export transaction", cause)),
|
|
493
|
+
),
|
|
494
|
+
write: (body) =>
|
|
495
|
+
journal.owner.transaction(body.pipe(Effect.tap(() => journal.state.invalidate))).pipe(
|
|
496
|
+
Effect.provideService(SqlClientService.SqlClient, sql),
|
|
497
|
+
Effect.catchIf(isSqlError, (cause) => storeError("import transaction", cause)),
|
|
498
|
+
),
|
|
499
|
+
});
|
|
816
500
|
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
501
|
+
const inspectTail: ThreadStore["Service"]["inspectTail"] = Effect.fnUntraced(function* (
|
|
502
|
+
request: ThreadTailRequest,
|
|
503
|
+
) {
|
|
504
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(ThreadTailRequest))(request).pipe(
|
|
505
|
+
Effect.mapError((error) => schemaStoreError("validate tail inspection", error)),
|
|
506
|
+
);
|
|
822
507
|
|
|
823
|
-
|
|
508
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
824
509
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
510
|
+
return ThreadTail.make({
|
|
511
|
+
threadId: validated.threadId,
|
|
512
|
+
tailSequence: thread.tail_sequence,
|
|
513
|
+
});
|
|
514
|
+
});
|
|
831
515
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
516
|
+
const saveCheckpoint: ThreadCheckpoints["save"] = Effect.fnUntraced(function* (
|
|
517
|
+
request: SaveCheckpointRequest,
|
|
518
|
+
) {
|
|
519
|
+
const validated = yield* Schema.decodeEffect(Schema.toType(SaveCheckpointRequest))(
|
|
520
|
+
request,
|
|
521
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
|
|
837
522
|
|
|
838
|
-
|
|
839
|
-
return yield* CheckpointRejected.make({
|
|
840
|
-
threadId: validated.checkpoint.threadId,
|
|
841
|
-
reason: "digest-mismatch",
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
|
|
523
|
+
const thread = yield* requireThread(journal, validated.checkpoint.threadId);
|
|
845
524
|
|
|
846
|
-
|
|
525
|
+
if (validated.checkpoint.throughSequence > thread.tail_sequence) {
|
|
526
|
+
return yield* CheckpointRejected.make({
|
|
847
527
|
threadId: validated.checkpoint.threadId,
|
|
848
|
-
|
|
849
|
-
tailDigest: validated.checkpoint.tailDigest,
|
|
850
|
-
checkpointJson,
|
|
528
|
+
reason: "ahead-of-tail",
|
|
851
529
|
});
|
|
530
|
+
}
|
|
852
531
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
threadId: validated.checkpoint.threadId,
|
|
859
|
-
reason: "digest-mismatch",
|
|
860
|
-
})
|
|
861
|
-
: storeError("save checkpoint", error),
|
|
862
|
-
),
|
|
863
|
-
);
|
|
864
|
-
yield* hitFailpoint("save-checkpoint:after");
|
|
865
|
-
},
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
const loadCheckpoint: ThreadCheckpoints["load"] = Effect.fn("DoThreadStore.loadCheckpoint")(
|
|
869
|
-
function* (request: LoadCheckpointRequest) {
|
|
870
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(LoadCheckpointRequest))(
|
|
871
|
-
request,
|
|
872
|
-
).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
|
|
873
|
-
|
|
874
|
-
const thread = yield* requireThread(journal, validated.threadId);
|
|
875
|
-
|
|
876
|
-
const rows = yield* journal
|
|
877
|
-
.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
|
|
878
|
-
.pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
|
|
879
|
-
|
|
880
|
-
if (rows.length === 0) return Option.none();
|
|
881
|
-
if (rows.length !== 1) {
|
|
882
|
-
return yield* ThreadStoreError.make({
|
|
883
|
-
operation: "load checkpoint",
|
|
884
|
-
message: `Expected at most one checkpoint row but found ${rows.length}.`,
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
const row = rows[0];
|
|
888
|
-
const checkpoint = yield* decodeCheckpoint(row.checkpoint_json);
|
|
889
|
-
|
|
890
|
-
if (
|
|
891
|
-
row.thread_id !== validated.threadId ||
|
|
892
|
-
checkpoint.threadId !== row.thread_id ||
|
|
893
|
-
checkpoint.throughSequence !== row.through_sequence ||
|
|
894
|
-
checkpoint.tailDigest !== row.tail_digest
|
|
895
|
-
) {
|
|
896
|
-
return yield* ThreadStoreError.make({
|
|
897
|
-
operation: "load checkpoint",
|
|
898
|
-
message: "Stored checkpoint metadata does not match its canonical row.",
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const canonicalDigest = yield* tailDigestAt(
|
|
903
|
-
journal,
|
|
904
|
-
checkpoint.threadId,
|
|
905
|
-
checkpoint.throughSequence,
|
|
906
|
-
);
|
|
907
|
-
|
|
908
|
-
if (canonicalDigest !== checkpoint.tailDigest) {
|
|
909
|
-
return yield* CheckpointRejected.make({
|
|
910
|
-
threadId: checkpoint.threadId,
|
|
911
|
-
reason: "digest-mismatch",
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
return Option.some(checkpoint);
|
|
916
|
-
},
|
|
917
|
-
);
|
|
918
|
-
|
|
919
|
-
const saveRecoveryCheckpoint: ThreadRecoveryCheckpoints["save"] = Effect.fn(
|
|
920
|
-
"DoThreadStore.saveRecoveryCheckpoint",
|
|
921
|
-
)(function* (request) {
|
|
922
|
-
const validated = yield* Schema.decodeEffect(Schema.toType(SaveRecoveryCheckpointRequest))(
|
|
923
|
-
request,
|
|
924
|
-
).pipe(Effect.mapError((error) => schemaStoreError("validate recovery checkpoint", error)));
|
|
925
|
-
|
|
532
|
+
yield* requireBatchBoundary(
|
|
533
|
+
journal,
|
|
534
|
+
validated.checkpoint.threadId,
|
|
535
|
+
validated.checkpoint.throughSequence,
|
|
536
|
+
);
|
|
926
537
|
const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
|
|
927
538
|
|
|
928
|
-
|
|
929
|
-
.
|
|
930
|
-
.
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
)
|
|
938
|
-
|
|
539
|
+
const raw = RawCheckpoint.make({
|
|
540
|
+
threadId: validated.checkpoint.threadId,
|
|
541
|
+
throughSequence: validated.checkpoint.throughSequence,
|
|
542
|
+
checkpointJson,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
yield* hitFailpoint("save-checkpoint:before");
|
|
546
|
+
yield* journal.saveCheckpoint(raw).pipe(
|
|
547
|
+
Effect.mapError((error) =>
|
|
548
|
+
isDoCheckpointConflict(error)
|
|
549
|
+
? CheckpointRejected.make({
|
|
550
|
+
threadId: validated.checkpoint.threadId,
|
|
551
|
+
reason: error.reason,
|
|
552
|
+
})
|
|
553
|
+
: storeError("save checkpoint", error),
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
yield* hitFailpoint("save-checkpoint:after");
|
|
939
557
|
});
|
|
940
558
|
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
)
|
|
559
|
+
const loadCheckpoint: ThreadCheckpoints["load"] = Effect.fnUntraced(function* (
|
|
560
|
+
request: LoadCheckpointRequest,
|
|
561
|
+
) {
|
|
944
562
|
const validated = yield* Schema.decodeEffect(Schema.toType(LoadCheckpointRequest))(
|
|
945
563
|
request,
|
|
946
|
-
).pipe(
|
|
947
|
-
Effect.mapError((error) => schemaStoreError("validate recovery checkpoint lookup", error)),
|
|
948
|
-
);
|
|
564
|
+
).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
|
|
949
565
|
|
|
950
566
|
const thread = yield* requireThread(journal, validated.threadId);
|
|
951
567
|
|
|
952
|
-
const corrupt = () =>
|
|
953
|
-
CheckpointRejected.make({ threadId: validated.threadId, reason: "corrupt" });
|
|
954
|
-
|
|
955
568
|
const rows = yield* journal
|
|
956
|
-
.
|
|
957
|
-
.pipe(
|
|
958
|
-
Effect.mapError((error) =>
|
|
959
|
-
error._tag === "DoStorageCorruptionError"
|
|
960
|
-
? corrupt()
|
|
961
|
-
: storeError("load recovery checkpoint", error),
|
|
962
|
-
),
|
|
963
|
-
);
|
|
569
|
+
.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence)
|
|
570
|
+
.pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
|
|
964
571
|
|
|
965
572
|
if (rows.length === 0) return Option.none();
|
|
966
|
-
if (rows.length !== 1)
|
|
573
|
+
if (rows.length !== 1) {
|
|
574
|
+
return yield* ThreadStoreError.make({
|
|
575
|
+
operation: "load checkpoint",
|
|
576
|
+
message: `Expected at most one checkpoint row but found ${rows.length}.`,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
967
579
|
const row = rows[0];
|
|
968
|
-
|
|
969
|
-
const checkpoint = yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(
|
|
970
|
-
row.checkpoint_json,
|
|
971
|
-
).pipe(Effect.mapError(corrupt));
|
|
580
|
+
const checkpoint = yield* decodeCheckpoint(row.checkpoint_json);
|
|
972
581
|
|
|
973
582
|
if (
|
|
974
583
|
row.thread_id !== validated.threadId ||
|
|
975
584
|
checkpoint.threadId !== row.thread_id ||
|
|
976
|
-
checkpoint.throughSequence !== row.through_sequence
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
return yield* CheckpointRejected.make({
|
|
982
|
-
threadId: validated.threadId,
|
|
983
|
-
reason: "ahead-of-tail",
|
|
585
|
+
checkpoint.throughSequence !== row.through_sequence
|
|
586
|
+
) {
|
|
587
|
+
return yield* ThreadStoreError.make({
|
|
588
|
+
operation: "load checkpoint",
|
|
589
|
+
message: "Stored checkpoint metadata does not match its canonical row.",
|
|
984
590
|
});
|
|
985
|
-
|
|
986
|
-
return Option.none();
|
|
987
|
-
|
|
988
|
-
const canonicalDigest = yield* tailDigestAt(
|
|
989
|
-
journal,
|
|
990
|
-
checkpoint.threadId,
|
|
991
|
-
checkpoint.throughSequence,
|
|
992
|
-
);
|
|
591
|
+
}
|
|
993
592
|
|
|
994
|
-
|
|
995
|
-
return yield* CheckpointRejected.make({
|
|
996
|
-
threadId: validated.threadId,
|
|
997
|
-
reason: "digest-mismatch",
|
|
998
|
-
});
|
|
593
|
+
yield* requireBatchBoundary(journal, checkpoint.threadId, checkpoint.throughSequence);
|
|
999
594
|
|
|
1000
595
|
return Option.some(checkpoint);
|
|
1001
596
|
});
|
|
@@ -1010,8 +605,6 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
|
1010
605
|
Schema.UndefinedOr(
|
|
1011
606
|
Schema.Struct({
|
|
1012
607
|
tail_sequence: CanonicalSequence,
|
|
1013
|
-
tail_digest: Digest,
|
|
1014
|
-
producer_epoch: ThreadMaterialization.fields.producerEpoch,
|
|
1015
608
|
}),
|
|
1016
609
|
),
|
|
1017
610
|
)(rows[0]),
|
|
@@ -1022,22 +615,36 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
|
1022
615
|
);
|
|
1023
616
|
|
|
1024
617
|
const threadStore = ThreadStore.of({
|
|
618
|
+
archives: journal.archives,
|
|
619
|
+
work: journal.work.storage,
|
|
1025
620
|
...(journal.lifecycle === undefined
|
|
1026
621
|
? {}
|
|
1027
622
|
: { lifecyclePublications: journal.lifecycle.storage }),
|
|
1028
623
|
readIdentity: selectedReads.readIdentity,
|
|
624
|
+
readExecution: (request) =>
|
|
625
|
+
journal.state.read(
|
|
626
|
+
requireThread(journal, request.threadId).pipe(
|
|
627
|
+
Effect.andThen(journal.readExecution(request)),
|
|
628
|
+
),
|
|
629
|
+
),
|
|
630
|
+
readWorkerCapacity: selectedReads.readWorkerCapacity,
|
|
631
|
+
verification: transfer.verification,
|
|
1029
632
|
countPeerMessages: selectedReads.countPeerMessages,
|
|
1030
633
|
append,
|
|
1031
|
-
export:
|
|
634
|
+
export: transfer.export,
|
|
1032
635
|
inspectTail,
|
|
1033
636
|
materialize,
|
|
1034
637
|
observe,
|
|
1035
638
|
read,
|
|
639
|
+
readPrompt: (request) => Stream.fromIterableEffect(selectedReads.readPrompt(request)),
|
|
1036
640
|
checkpoints: { save: saveCheckpoint, load: loadCheckpoint },
|
|
1037
|
-
|
|
641
|
+
promptCheckpoints: makePromptCheckpoints(sql, journal, config.maxStoredValueBytes),
|
|
1038
642
|
});
|
|
1039
643
|
|
|
1040
|
-
return Context.make(ThreadStore, threadStore)
|
|
644
|
+
return Context.make(ThreadStore, threadStore).pipe(
|
|
645
|
+
Context.add(ThreadImport, transfer.importer),
|
|
646
|
+
Context.add(StorageOwner, journal.state.owner),
|
|
647
|
+
);
|
|
1041
648
|
});
|
|
1042
649
|
|
|
1043
650
|
/**
|
|
@@ -1045,10 +652,10 @@ const makeServices = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
|
1045
652
|
* Crypto authority kept visible in its input channel.
|
|
1046
653
|
*/
|
|
1047
654
|
export const threadStoreLayer: Layer.Layer<
|
|
1048
|
-
ThreadStore,
|
|
655
|
+
ThreadStore | ThreadReader | ThreadImport | StorageOwner,
|
|
1049
656
|
DoStorageInitializationError,
|
|
1050
657
|
DoStorageConfig | DoStorageFailpoint | SqlClientService.SqlClient | Crypto.Crypto
|
|
1051
|
-
> = Layer.effectContext(makeServices());
|
|
658
|
+
> = ThreadReader.layer().pipe(Layer.provideMerge(Layer.effectContext(makeServices())));
|
|
1052
659
|
|
|
1053
660
|
/**
|
|
1054
661
|
* Validated Durable Object storage configuration Layer with the documented defaults applied.
|
|
@@ -1061,8 +668,6 @@ export const storageConfigLayer = (
|
|
|
1061
668
|
Layer.effect(DoStorageConfig)(
|
|
1062
669
|
Schema.decodeEffect(DoStorageConfigValue)({
|
|
1063
670
|
observationPollInterval: options.observationPollInterval ?? 25,
|
|
1064
|
-
ownershipLeaseDuration:
|
|
1065
|
-
options.ownershipLeaseDuration ?? Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION),
|
|
1066
671
|
maxStoredValueBytes: options.maxStoredValueBytes ?? DEFAULT_MAX_STORED_VALUE_BYTES,
|
|
1067
672
|
verifyOnOpen: options.verifyOnOpen ?? false,
|
|
1068
673
|
}).pipe(
|
|
@@ -1087,12 +692,15 @@ export const storageFailpointLayer = (
|
|
|
1087
692
|
/**
|
|
1088
693
|
* A composition-root convenience Layer for canonical Threads inside one Durable Object,
|
|
1089
694
|
* built over `ctx.storage`. Durable accepted work is served by the separate SubmissionLedger
|
|
1090
|
-
* port; point both at the SAME `ctx.storage` so
|
|
695
|
+
* port; point both at the SAME `ctx.storage` so both share the same scoped storage owner
|
|
1091
696
|
* (ADR-0011 D7's "same file" rule, transposed to one object's private database).
|
|
1092
697
|
*/
|
|
1093
698
|
export const layer = (
|
|
1094
699
|
options: DoStorageOptions,
|
|
1095
|
-
): Layer.Layer<
|
|
700
|
+
): Layer.Layer<
|
|
701
|
+
ThreadStore | ThreadReader | ThreadImport | StorageOwner,
|
|
702
|
+
DoStorageInitializationError
|
|
703
|
+
> =>
|
|
1096
704
|
Layer.unwrap(
|
|
1097
705
|
Effect.map(DoStorageConfig, (config) =>
|
|
1098
706
|
threadStoreLayer.pipe(
|
|
@@ -1108,6 +716,48 @@ export const layer = (
|
|
|
1108
716
|
),
|
|
1109
717
|
).pipe(Layer.provide(storageConfigLayer(options)));
|
|
1110
718
|
|
|
719
|
+
/** Read a quiesced current-layout Object without initializing its layout or ownership. */
|
|
720
|
+
export const exportThread = Effect.fn("DoThreadStore.exportThread")(function* (
|
|
721
|
+
options: Pick<DoStorageOptions, "storage">,
|
|
722
|
+
request: ThreadExportRequest,
|
|
723
|
+
) {
|
|
724
|
+
yield* checkExportSource(request.threadId);
|
|
725
|
+
|
|
726
|
+
return yield* Effect.gen(function* () {
|
|
727
|
+
const sql = yield* SqlClientService.SqlClient;
|
|
728
|
+
const state = yield* ownedState(sql);
|
|
729
|
+
|
|
730
|
+
return yield* state
|
|
731
|
+
.transaction(
|
|
732
|
+
Effect.gen(function* () {
|
|
733
|
+
yield* readDoStorageHeader();
|
|
734
|
+
|
|
735
|
+
const transfer = yield* makeSqlThreadImport({
|
|
736
|
+
owner: state.owner,
|
|
737
|
+
offsetPrefix: DO_OFFSET_PREFIX,
|
|
738
|
+
read: (body) => body,
|
|
739
|
+
write: () => Effect.die("A read-only exporter cannot import"),
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
return yield* transfer.export(request);
|
|
743
|
+
}),
|
|
744
|
+
)
|
|
745
|
+
.pipe(
|
|
746
|
+
Effect.catchTag("SqlError", (cause) =>
|
|
747
|
+
DoStorageError.make({
|
|
748
|
+
operation: "export Thread snapshot",
|
|
749
|
+
message: cause.message,
|
|
750
|
+
cause,
|
|
751
|
+
}),
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
}).pipe(
|
|
755
|
+
Effect.provide(
|
|
756
|
+
Layer.merge(SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer),
|
|
757
|
+
),
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
|
|
1111
761
|
/**
|
|
1112
762
|
* Discard the Object's derived thread and ledger state after direct SQL maintenance.
|
|
1113
763
|
* Quiesce port operations during the raw write and invalidation, and enroll the write with
|
|
@@ -1116,6 +766,10 @@ export const layer = (
|
|
|
1116
766
|
export const invalidate = invalidateOwnedState;
|
|
1117
767
|
|
|
1118
768
|
/** Share the Object's transaction gate with owner-local SQL Memory compositions. */
|
|
1119
|
-
export const sqlOwnerLayer = Layer.
|
|
1120
|
-
Effect.flatMap(SqlClientService.SqlClient, ownedState)
|
|
769
|
+
export const sqlOwnerLayer = Layer.effectContext(
|
|
770
|
+
Effect.flatMap(SqlClientService.SqlClient, ownedState).pipe(
|
|
771
|
+
Effect.map((state) =>
|
|
772
|
+
Context.make(SqlStorageOwner, state).pipe(Context.add(StorageOwner, state.owner)),
|
|
773
|
+
),
|
|
774
|
+
),
|
|
1121
775
|
);
|