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