@yielded/agent-storage-cloudflare 0.1.0-beta.174 → 0.1.0-beta.175
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.mjs +1 -1
- package/dist/DoStorageFailpoint.d.mts +3 -1
- package/dist/DoStorageFailpoint.mjs +7 -2
- package/dist/DoStorageFailpoint.mjs.map +1 -1
- package/dist/{DoStorageVersion-NKnQU3eR.d.mts → DoStorageVersion-Mc7CXMdN.d.mts} +2 -2
- package/dist/DoStorageVersion.d.mts +1 -1
- package/dist/DoStorageVersion.mjs +2 -2
- package/dist/DoSubmissionLedger.mjs +116 -65
- package/dist/DoSubmissionLedger.mjs.map +1 -1
- package/dist/{DoThreadStore-DyzOZDXK.mjs → DoThreadStore-CgIZQUNR.mjs} +24 -19
- package/dist/DoThreadStore-CgIZQUNR.mjs.map +1 -0
- package/dist/DoThreadStore.mjs +1 -1
- package/dist/PortProtocol.d.mts +7063 -7063
- package/dist/PortRouting.mjs +6 -1
- package/dist/PortRouting.mjs.map +1 -1
- package/dist/{do-journal-ZMAzzENZ.mjs → do-journal-7P3DUBz0.mjs} +131 -43
- package/dist/do-journal-7P3DUBz0.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{migrations-C3N9GN6A.mjs → migrations-CAO_akhD.mjs} +45 -22
- package/dist/migrations-CAO_akhD.mjs.map +1 -0
- package/dist/{owned-state-DcFh0YNo.mjs → owned-state-B4QhezMp.mjs} +2 -1
- package/dist/{owned-state-DcFh0YNo.mjs.map → owned-state-B4QhezMp.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/DoStorageFailpoint.ts +4 -0
- package/src/DoSubmissionLedger.ts +262 -149
- package/src/DoThreadStore.ts +18 -44
- package/src/PortRouting.ts +10 -2
- package/src/internal/do-journal.ts +286 -66
- package/src/internal/migrations.ts +52 -10
- package/src/internal/owned-state.ts +9 -1
- package/src/internal/prompt-checkpoint.ts +6 -3
- package/src/internal/storage-span.ts +14 -10
- package/dist/DoThreadStore-DyzOZDXK.mjs.map +0 -1
- package/dist/do-journal-ZMAzzENZ.mjs.map +0 -1
- package/dist/migrations-C3N9GN6A.mjs.map +0 -1
package/src/DoThreadStore.ts
CHANGED
|
@@ -18,7 +18,6 @@ import { ThreadImport } from "@yielded/agent/thread-import";
|
|
|
18
18
|
import {
|
|
19
19
|
type ThreadExportRequest,
|
|
20
20
|
AppendConflict,
|
|
21
|
-
AppendResult,
|
|
22
21
|
CheckpointRejected,
|
|
23
22
|
ThreadCheckpoint,
|
|
24
23
|
ThreadMaterialization,
|
|
@@ -104,6 +103,11 @@ const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
|
|
|
104
103
|
const ThreadCheckpointJson = Schema.fromJsonString(ThreadCheckpoint);
|
|
105
104
|
const decodeCanonicalRecord = decodeExportRecordJson;
|
|
106
105
|
|
|
106
|
+
const decodeThreadMaterializationType = Schema.decodeEffect(Schema.toType(ThreadMaterialization));
|
|
107
|
+
const decodeThreadReadRequestType = Schema.decodeEffect(Schema.toType(ThreadReadRequest));
|
|
108
|
+
const decodeThreadTailRequestType = Schema.decodeEffect(Schema.toType(ThreadTailRequest));
|
|
109
|
+
const decodeStorageConfig = Schema.decodeEffect(DoStorageConfigValue);
|
|
110
|
+
|
|
107
111
|
const storeError = (
|
|
108
112
|
operation: string,
|
|
109
113
|
error: { readonly message: string; readonly diagnostic?: ThreadStoreDiagnostic },
|
|
@@ -126,19 +130,6 @@ const schemaStoreError = (operation: string, error: Schema.SchemaError) =>
|
|
|
126
130
|
}),
|
|
127
131
|
});
|
|
128
132
|
|
|
129
|
-
const makeOffset = (
|
|
130
|
-
threadId: ThreadMaterialization["threadId"],
|
|
131
|
-
sequence: number,
|
|
132
|
-
): Effect.Effect<ObservationOffset, ThreadStoreError> =>
|
|
133
|
-
Schema.decodeEffect(CanonicalSequence)(sequence).pipe(
|
|
134
|
-
Effect.flatMap((validatedSequence) =>
|
|
135
|
-
Schema.decodeEffect(ObservationOffset)(
|
|
136
|
-
`${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`,
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
Effect.mapError((error) => schemaStoreError("encode observation offset", error)),
|
|
140
|
-
);
|
|
141
|
-
|
|
142
133
|
const parseOffset = Effect.fnUntraced(function* (
|
|
143
134
|
threadId: ThreadMaterialization["threadId"],
|
|
144
135
|
offset: ObservationOffset | undefined,
|
|
@@ -179,8 +170,8 @@ const encodeCheckpoint = (checkpoint: ThreadCheckpoint): Effect.Effect<string, T
|
|
|
179
170
|
const envelopes = new WeakMap<object, CanonicalRecordEnvelope>();
|
|
180
171
|
|
|
181
172
|
const decodeEnvelope = Effect.fnUntraced(function* (row: {
|
|
182
|
-
readonly batch_id:
|
|
183
|
-
readonly thread_id:
|
|
173
|
+
readonly batch_id: CanonicalRecordEnvelope["batchId"];
|
|
174
|
+
readonly thread_id: CanonicalRecordEnvelope["threadId"];
|
|
184
175
|
readonly record_json: string;
|
|
185
176
|
readonly sequence: CanonicalSequence;
|
|
186
177
|
}) {
|
|
@@ -213,15 +204,11 @@ const decodeEnvelope = Effect.fnUntraced(function* (row: {
|
|
|
213
204
|
),
|
|
214
205
|
);
|
|
215
206
|
|
|
216
|
-
const threadId =
|
|
217
|
-
row.thread_id,
|
|
218
|
-
).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
|
|
207
|
+
const { thread_id: threadId, batch_id: batchId } = row;
|
|
219
208
|
|
|
220
|
-
const offset =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
row.batch_id,
|
|
224
|
-
).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
|
|
209
|
+
const offset = ObservationOffset.make(
|
|
210
|
+
`${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${row.sequence}`,
|
|
211
|
+
);
|
|
225
212
|
|
|
226
213
|
const envelope = CanonicalRecordEnvelope.make(
|
|
227
214
|
{ threadId, batchId, sequence: row.sequence, offset, record },
|
|
@@ -317,9 +304,9 @@ const makeServices = Effect.fnUntraced(function* () {
|
|
|
317
304
|
const materialize: ThreadStore["Service"]["materialize"] = Effect.fnUntraced(function* (
|
|
318
305
|
request: ThreadMaterialization,
|
|
319
306
|
) {
|
|
320
|
-
const validated = yield*
|
|
321
|
-
|
|
322
|
-
)
|
|
307
|
+
const validated = yield* decodeThreadMaterializationType(request).pipe(
|
|
308
|
+
Effect.mapError((error) => schemaStoreError("validate materialization", error)),
|
|
309
|
+
);
|
|
323
310
|
|
|
324
311
|
const now = yield* Clock.currentTimeMillis;
|
|
325
312
|
|
|
@@ -362,11 +349,6 @@ const makeServices = Effect.fnUntraced(function* () {
|
|
|
362
349
|
|
|
363
350
|
return storeError("append canonical batch", error);
|
|
364
351
|
}),
|
|
365
|
-
Effect.flatMap((result) =>
|
|
366
|
-
Schema.decodeEffect(AppendResult)(result).pipe(
|
|
367
|
-
Effect.mapError((error) => schemaStoreError("decode append result", error)),
|
|
368
|
-
),
|
|
369
|
-
),
|
|
370
352
|
);
|
|
371
353
|
|
|
372
354
|
yield* hitFailpoint("append:after");
|
|
@@ -391,7 +373,7 @@ const makeServices = Effect.fnUntraced(function* () {
|
|
|
391
373
|
});
|
|
392
374
|
|
|
393
375
|
const readEffect = Effect.fnUntraced(function* (request: ThreadReadRequest) {
|
|
394
|
-
const validated = yield*
|
|
376
|
+
const validated = yield* decodeThreadReadRequestType(request).pipe(
|
|
395
377
|
Effect.mapError((error) => schemaStoreError("validate thread read", error)),
|
|
396
378
|
);
|
|
397
379
|
|
|
@@ -503,7 +485,7 @@ const makeServices = Effect.fnUntraced(function* () {
|
|
|
503
485
|
const inspectTail: ThreadStore["Service"]["inspectTail"] = Effect.fnUntraced(function* (
|
|
504
486
|
request: ThreadTailRequest,
|
|
505
487
|
) {
|
|
506
|
-
const validated = yield*
|
|
488
|
+
const validated = yield* decodeThreadTailRequestType(request).pipe(
|
|
507
489
|
Effect.mapError((error) => schemaStoreError("validate tail inspection", error)),
|
|
508
490
|
);
|
|
509
491
|
|
|
@@ -602,15 +584,7 @@ const makeServices = Effect.fnUntraced(function* () {
|
|
|
602
584
|
snapshot: journal.state.read,
|
|
603
585
|
tail: (threadId) =>
|
|
604
586
|
journal.getThread(threadId).pipe(
|
|
605
|
-
Effect.
|
|
606
|
-
Schema.decodeEffect(
|
|
607
|
-
Schema.UndefinedOr(
|
|
608
|
-
Schema.Struct({
|
|
609
|
-
tail_sequence: CanonicalSequence,
|
|
610
|
-
}),
|
|
611
|
-
),
|
|
612
|
-
)(rows[0]),
|
|
613
|
-
),
|
|
587
|
+
Effect.map((rows) => rows[0]),
|
|
614
588
|
Effect.mapError((error) => storeError("native thread tail", error)),
|
|
615
589
|
),
|
|
616
590
|
}),
|
|
@@ -668,7 +642,7 @@ export const storageConfigLayer = (
|
|
|
668
642
|
options: DoStorageOptions,
|
|
669
643
|
): Layer.Layer<DoStorageConfig, DoStorageError> =>
|
|
670
644
|
Layer.effect(DoStorageConfig)(
|
|
671
|
-
|
|
645
|
+
decodeStorageConfig({
|
|
672
646
|
observationPollInterval: options.observationPollInterval ?? 25,
|
|
673
647
|
maxStoredValueBytes: options.maxStoredValueBytes ?? DEFAULT_MAX_STORED_VALUE_BYTES,
|
|
674
648
|
verifyOnOpen: options.verifyOnOpen ?? false,
|
package/src/PortRouting.ts
CHANGED
|
@@ -570,9 +570,9 @@ const makeRoutedLedgerServices = Effect.fnUntraced(function* (options: RoutedPor
|
|
|
570
570
|
const routed = SubmissionLedger.of({
|
|
571
571
|
capabilities: local.capabilities,
|
|
572
572
|
|
|
573
|
-
admit: (request) =>
|
|
573
|
+
admit: (request, admissionOptions) =>
|
|
574
574
|
options.ownsThread(request.threadId)
|
|
575
|
-
? local.admit(request)
|
|
575
|
+
? local.admit(request, admissionOptions)
|
|
576
576
|
: foreignLedgerCall(
|
|
577
577
|
"ledger admit",
|
|
578
578
|
request.threadId,
|
|
@@ -824,6 +824,7 @@ const makeRoutedLedgerServices = Effect.fnUntraced(function* (options: RoutedPor
|
|
|
824
824
|
|
|
825
825
|
// The local scan IS the whole worklist: one Thread per Object (durability §5).
|
|
826
826
|
scanNonterminal: local.scanNonterminal,
|
|
827
|
+
...(local.listNonterminal === undefined ? {} : { listNonterminal: local.listNonterminal }),
|
|
827
828
|
|
|
828
829
|
readAbortIntent: (request) =>
|
|
829
830
|
submissionTarget("ledger read abort intent", request.submissionId).pipe(
|
|
@@ -1046,6 +1047,13 @@ const makeRoutedStoreServices = Effect.fnUntraced(function* (options: RoutedPort
|
|
|
1046
1047
|
).pipe(Effect.map((reply) => reply.range)),
|
|
1047
1048
|
},
|
|
1048
1049
|
work: {
|
|
1050
|
+
isOnlyFreshAdmission: (request) => {
|
|
1051
|
+
const prove = local.work?.isOnlyFreshAdmission;
|
|
1052
|
+
|
|
1053
|
+
return prove !== undefined && options.ownsThread(request.threadId)
|
|
1054
|
+
? prove(request)
|
|
1055
|
+
: Effect.succeed(false);
|
|
1056
|
+
},
|
|
1049
1057
|
threads: (request) =>
|
|
1050
1058
|
Effect.flatMap(CurrentNativeSource, (nativeSource) =>
|
|
1051
1059
|
nativeSource && local.work !== undefined
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
1
2
|
import {
|
|
2
3
|
makeCanonicalStorage,
|
|
3
4
|
recordBatchId,
|
|
@@ -21,16 +22,29 @@ import {
|
|
|
21
22
|
LifecyclePublicationFact,
|
|
22
23
|
LifecyclePublicationError,
|
|
23
24
|
} from "@yielded/agent/lifecycle-publication";
|
|
24
|
-
import { CanonicalRecord, CanonicalSequence } from "@yielded/agent/records";
|
|
25
|
+
import { BatchId, CanonicalRecord, CanonicalSequence } from "@yielded/agent/records";
|
|
25
26
|
import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
|
|
26
27
|
import {
|
|
27
28
|
type AppendConflict,
|
|
29
|
+
AppendResult,
|
|
28
30
|
ThreadStoreDiagnostic,
|
|
29
31
|
ThreadStoreError,
|
|
30
32
|
} from "@yielded/agent/thread-store";
|
|
31
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
Cause,
|
|
35
|
+
Clock,
|
|
36
|
+
Effect,
|
|
37
|
+
Option,
|
|
38
|
+
Predicate,
|
|
39
|
+
References,
|
|
40
|
+
Result,
|
|
41
|
+
Schema,
|
|
42
|
+
Stream,
|
|
43
|
+
Tracer,
|
|
44
|
+
} from "effect";
|
|
32
45
|
import * as SqlClient from "effect/sql/SqlClient";
|
|
33
|
-
import { SqlError } from "effect/sql/SqlError";
|
|
46
|
+
import { classifySqliteError, SqlError } from "effect/sql/SqlError";
|
|
47
|
+
import { CurrentTransformer, type Statement } from "effect/sql/Statement";
|
|
34
48
|
|
|
35
49
|
import {
|
|
36
50
|
type DoStorageFailpointError,
|
|
@@ -41,6 +55,7 @@ import {
|
|
|
41
55
|
DoValueBoundExceeded,
|
|
42
56
|
type DoStorageFailpointLocation,
|
|
43
57
|
} from "../DoStorageError.ts";
|
|
58
|
+
import { isDefaultDoStorageFailpoint } from "../DoStorageFailpoint.ts";
|
|
44
59
|
import { ensureDoStorageLayout } from "./migrations.ts";
|
|
45
60
|
import { ownedState, ownedRows, type OwnedState } from "./owned-state.ts";
|
|
46
61
|
import {
|
|
@@ -68,6 +83,12 @@ const MAX_RECORD_CACHE_ENTRIES = 4_096;
|
|
|
68
83
|
const MAX_BOUND_PARAMETERS = 100;
|
|
69
84
|
const isSqlError = Schema.is(SqlError);
|
|
70
85
|
|
|
86
|
+
const isNativeClient = Schema.is(
|
|
87
|
+
Schema.declare<SqliteClient.SqliteClient>((value): value is SqliteClient.SqliteClient =>
|
|
88
|
+
Predicate.hasProperty(value, SqliteClient.TypeId),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
71
92
|
const storedTextBytes = (value: string): number => new TextEncoder().encode(value).byteLength;
|
|
72
93
|
|
|
73
94
|
const chunked = <A>(values: ReadonlyArray<A>, size: number): Array<ReadonlyArray<A>> => {
|
|
@@ -94,8 +115,8 @@ const threadRows = ownedRows(
|
|
|
94
115
|
);
|
|
95
116
|
|
|
96
117
|
class RecordRow extends Schema.Class<RecordRow>("RecordRow")({
|
|
97
|
-
batch_id: Schema.fromJsonString(
|
|
98
|
-
thread_id:
|
|
118
|
+
batch_id: Schema.fromJsonString(BatchId.check(Schema.isMaxLength(MAX_IDENTIFIER_LENGTH))),
|
|
119
|
+
thread_id: ThreadId.check(Schema.isMaxLength(MAX_IDENTIFIER_LENGTH)),
|
|
99
120
|
record_id: Schema.fromJsonString(BoundedIdentifier),
|
|
100
121
|
record_json: BoundedStoredText,
|
|
101
122
|
sequence: CanonicalSequence,
|
|
@@ -200,14 +221,6 @@ const initializedLifecycleSources = new WeakSet<OwnedState>();
|
|
|
200
221
|
|
|
201
222
|
export type { RawAppendRequest } from "@yielded/agent-storage-sql/sql-journal";
|
|
202
223
|
|
|
203
|
-
export class RawAppendResult extends Schema.Class<RawAppendResult>(
|
|
204
|
-
"@effect-agent/storage-cloudflare/RawAppendResult",
|
|
205
|
-
)({
|
|
206
|
-
firstSequence: CanonicalSequence,
|
|
207
|
-
lastSequence: CanonicalSequence,
|
|
208
|
-
replayed: Schema.Boolean,
|
|
209
|
-
}) {}
|
|
210
|
-
|
|
211
224
|
export class RawReadRequest extends Schema.Class<RawReadRequest>(
|
|
212
225
|
"@effect-agent/storage-cloudflare/RawReadRequest",
|
|
213
226
|
)({
|
|
@@ -317,10 +330,14 @@ const ensureCurrentStorage = Effect.fnUntraced(function* (
|
|
|
317
330
|
failpoint: DoJournalFailpoint = noFailpoint,
|
|
318
331
|
maxStoredValueBytes: number,
|
|
319
332
|
) {
|
|
320
|
-
yield* ensureDoStorageLayout().pipe(Effect.provideService(SqlClient.SqlClient, sql));
|
|
321
|
-
|
|
322
333
|
const state = yield* ownedState(sql);
|
|
323
334
|
const owner = (yield* SqlStorageOwner) ?? state;
|
|
335
|
+
|
|
336
|
+
yield* ensureDoStorageLayout().pipe(
|
|
337
|
+
Effect.provideService(SqlStorageOwner, owner),
|
|
338
|
+
Effect.provideService(SqlClient.SqlClient, sql),
|
|
339
|
+
);
|
|
340
|
+
|
|
324
341
|
let journal: DoJournal | undefined;
|
|
325
342
|
|
|
326
343
|
const lifecycle = yield* makeSqlLifecyclePublication(undefined, maxStoredValueBytes).pipe(
|
|
@@ -366,6 +383,14 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
366
383
|
const owner = (yield* SqlStorageOwner) ?? state;
|
|
367
384
|
const progress = yield* SqlStorageProgress;
|
|
368
385
|
|
|
386
|
+
const nativeDb =
|
|
387
|
+
isNativeClient(sql) &&
|
|
388
|
+
sql.config.transformQueryNames === undefined &&
|
|
389
|
+
sql.config.transformResultNames === undefined &&
|
|
390
|
+
isDefaultDoStorageFailpoint(failpoint)
|
|
391
|
+
? sql.config.storage?.sql
|
|
392
|
+
: undefined;
|
|
393
|
+
|
|
369
394
|
const work = yield* makeSqlThreadWork({
|
|
370
395
|
read: (body) => state.read(body),
|
|
371
396
|
write: (body) =>
|
|
@@ -478,13 +503,29 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
478
503
|
<A, E extends { readonly _tag: string }>(
|
|
479
504
|
effect: Effect.Effect<A, E>,
|
|
480
505
|
): Effect.Effect<A, E | DoStorageError> =>
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
506
|
+
Effect.withFiber((fiber) => {
|
|
507
|
+
const body = state.owner
|
|
508
|
+
.withMutation(owner.transaction(storageResult(effect, expected)))
|
|
509
|
+
.pipe(
|
|
510
|
+
Effect.provideService(SqlClient.SqlClient, sql),
|
|
511
|
+
Effect.mapError((error) =>
|
|
512
|
+
isSqlError(error) ? storageError(operation)(error) : error,
|
|
513
|
+
),
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const traced =
|
|
517
|
+
fiber.getRef(References.TracerEnabled) &&
|
|
518
|
+
fiber.getRef(Tracer.Tracer) !== Tracer.nativeTracer;
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
traced
|
|
522
|
+
? body.pipe(
|
|
523
|
+
Effect.tapError(annotateStorageError),
|
|
524
|
+
Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }),
|
|
525
|
+
)
|
|
526
|
+
: body
|
|
527
|
+
).pipe(Effect.flatMap(Effect.fromResult));
|
|
528
|
+
});
|
|
488
529
|
|
|
489
530
|
const materialize = Effect.fnUntraced(function* (
|
|
490
531
|
threadId: string,
|
|
@@ -620,9 +661,152 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
620
661
|
return request;
|
|
621
662
|
});
|
|
622
663
|
|
|
664
|
+
// Replay and progress validation precede this body in the same owner transaction.
|
|
665
|
+
// A miss makes no native change; the ordinary path reuses any tentative batch.
|
|
666
|
+
// After advancing the frontier, failures must roll back rather than fall back.
|
|
667
|
+
const appendNativeBody = Effect.fnUntraced(function* (
|
|
668
|
+
request: RawAppendRequest,
|
|
669
|
+
first: CanonicalSequence,
|
|
670
|
+
last: CanonicalSequence,
|
|
671
|
+
batchInserted: boolean,
|
|
672
|
+
) {
|
|
673
|
+
const db = nativeDb;
|
|
674
|
+
|
|
675
|
+
// Effectful statement transformers and exported SQL spans retain the ordinary path.
|
|
676
|
+
const ordinaryStatement = yield* Effect.withFiber((fiber) =>
|
|
677
|
+
Effect.succeed(
|
|
678
|
+
fiber.getRef(CurrentTransformer) !== undefined ||
|
|
679
|
+
(fiber.getRef(References.TracerEnabled) &&
|
|
680
|
+
fiber.getRef(Tracer.Tracer) !== Tracer.nativeTracer),
|
|
681
|
+
),
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
if (ordinaryStatement) return false;
|
|
685
|
+
if (
|
|
686
|
+
db === undefined ||
|
|
687
|
+
request.executionWrites.length !== 1 ||
|
|
688
|
+
request.executionWrites[0]?.update.retire ||
|
|
689
|
+
request.executionWrites[0]?.update.facts.length !== 0
|
|
690
|
+
)
|
|
691
|
+
return false;
|
|
692
|
+
|
|
693
|
+
const workPlan = work.prepareClosedPair(
|
|
694
|
+
request.threadId,
|
|
695
|
+
first,
|
|
696
|
+
last,
|
|
697
|
+
request.records.map((record) => record.canonical),
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
if (workPlan === undefined) return false;
|
|
701
|
+
|
|
702
|
+
const native = yield* Effect.suspend(() => {
|
|
703
|
+
const query = (statement: Statement<object>, operation: string) =>
|
|
704
|
+
Result.try({
|
|
705
|
+
try: () => {
|
|
706
|
+
const [text, parameters] = statement.compile();
|
|
707
|
+
|
|
708
|
+
return db.exec(text, ...parameters).toArray();
|
|
709
|
+
},
|
|
710
|
+
catch: (cause) =>
|
|
711
|
+
storageError(operation)(
|
|
712
|
+
SqlError.make({
|
|
713
|
+
reason: classifySqliteError(cause, {
|
|
714
|
+
operation,
|
|
715
|
+
message: "Native canonical statement failed",
|
|
716
|
+
}),
|
|
717
|
+
}),
|
|
718
|
+
),
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
return Effect.fromResult(
|
|
722
|
+
Result.gen(function* () {
|
|
723
|
+
const captured = physical.cachedRecords(request.threadId, first, request.records);
|
|
724
|
+
|
|
725
|
+
if (captured === undefined) return undefined;
|
|
726
|
+
const physicalRecords = yield* captured;
|
|
727
|
+
const rows = yield* query(workPlan.advance, "advance native work frontier");
|
|
728
|
+
|
|
729
|
+
// A zero-row predicate miss has made no change. A successful update
|
|
730
|
+
// must never fall back: every later refusal rolls back this transaction.
|
|
731
|
+
if (rows.length === 0) return undefined;
|
|
732
|
+
const closed = yield* workPlan.fold();
|
|
733
|
+
const executionPlans = executions.prepareCommit(request);
|
|
734
|
+
|
|
735
|
+
if (!batchInserted)
|
|
736
|
+
yield* query(
|
|
737
|
+
sql`INSERT INTO effect_agent_canonical_batches
|
|
738
|
+
(thread_id, batch_id, first_sequence, last_sequence, producer_id_json)
|
|
739
|
+
VALUES (${request.threadId}, ${JSON.stringify(request.batchId)}, ${first}, ${last}, ${request.producerIdJson})`,
|
|
740
|
+
"insert canonical batch",
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
const inserted = yield* query(
|
|
744
|
+
sql`INSERT INTO effect_agent_canonical_records ${sql.insert(physicalRecords.map((row) => row.columns))}
|
|
745
|
+
ON CONFLICT(thread_id, record_id) DO NOTHING RETURNING 1`,
|
|
746
|
+
"insert canonical records",
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
if (inserted.length !== request.records.length)
|
|
750
|
+
return yield* Result.fail(
|
|
751
|
+
DoAppendConflict.make({
|
|
752
|
+
message: "A canonical record ID already exists.",
|
|
753
|
+
reason: "record-identity",
|
|
754
|
+
}),
|
|
755
|
+
);
|
|
756
|
+
const memberships = physicalRecords.flatMap((row) => row.memberships);
|
|
757
|
+
|
|
758
|
+
for (const group of chunked(memberships, Math.floor(MAX_BOUND_PARAMETERS / 3)))
|
|
759
|
+
yield* query(
|
|
760
|
+
sql`INSERT INTO effect_agent_record_runs ${sql.insert(group)}`,
|
|
761
|
+
"index canonical Run membership",
|
|
762
|
+
);
|
|
763
|
+
for (const pointer of closed.pointers) yield* query(pointer, "index canonical pointers");
|
|
764
|
+
|
|
765
|
+
for (const plan of executionPlans) {
|
|
766
|
+
const changed = yield* query(plan.statement, "commit active execution");
|
|
767
|
+
const accepted = plan.accept(changed);
|
|
768
|
+
|
|
769
|
+
if (Result.isFailure(accepted))
|
|
770
|
+
return yield* Result.fail(
|
|
771
|
+
DoAppendConflict.make({
|
|
772
|
+
reason: accepted.failure.reason,
|
|
773
|
+
message: "Active execution conflicts with the captured commit",
|
|
774
|
+
...(accepted.failure.actualTailSequence === undefined
|
|
775
|
+
? {}
|
|
776
|
+
: { actualTailSequence: accepted.failure.actualTailSequence }),
|
|
777
|
+
}),
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
for (let index = 0; index < request.records.length; index++) {
|
|
781
|
+
const record = request.records[index]!;
|
|
782
|
+
|
|
783
|
+
recordCache.put(
|
|
784
|
+
RecordRow.make({
|
|
785
|
+
...physicalRecords[index]!.columns,
|
|
786
|
+
thread_id: request.threadId,
|
|
787
|
+
record_id: record.recordId,
|
|
788
|
+
batch_id: request.batchId,
|
|
789
|
+
record_json: record.recordJson,
|
|
790
|
+
sequence: CanonicalSequence.make(first + index),
|
|
791
|
+
}),
|
|
792
|
+
record.recordBytes,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return rows;
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
if (native === undefined) return false;
|
|
802
|
+
yield* threads.write(Effect.succeed(native));
|
|
803
|
+
|
|
804
|
+
return true;
|
|
805
|
+
});
|
|
806
|
+
|
|
623
807
|
const appendPrepared = Effect.fnUntraced(function* (
|
|
624
808
|
request: RawAppendRequest,
|
|
625
|
-
): Effect.fn.Return<
|
|
809
|
+
): Effect.fn.Return<AppendResult, AppendError> {
|
|
626
810
|
const threadRows = yield* getThread(request.threadId);
|
|
627
811
|
|
|
628
812
|
const thread = threadRows.length === 1 ? threadRows[0] : undefined;
|
|
@@ -634,30 +818,62 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
634
818
|
message: `Expected exactly one row but found ${threadRows.length}.`,
|
|
635
819
|
});
|
|
636
820
|
|
|
637
|
-
|
|
821
|
+
// A fresh canonical batch key and absence of an active receipt prove the two
|
|
822
|
+
// negative replay lookups in the same statement that retains the batch header.
|
|
823
|
+
// Keep the read-first path for stale/overflowed coordinates so exact retries
|
|
824
|
+
// still precede sequence and tail refusals.
|
|
825
|
+
const canInsert =
|
|
826
|
+
request.records.length > 0 &&
|
|
827
|
+
request.expectedTailSequence === thread.tail_sequence &&
|
|
828
|
+
Number.isSafeInteger(thread.tail_sequence + request.records.length);
|
|
829
|
+
|
|
830
|
+
const inserted = canInsert
|
|
831
|
+
? yield* sql`INSERT INTO effect_agent_canonical_batches
|
|
832
|
+
(thread_id, batch_id, first_sequence, last_sequence, producer_id_json)
|
|
833
|
+
SELECT ${request.threadId}, ${JSON.stringify(request.batchId)},
|
|
834
|
+
${thread.tail_sequence + 1}, ${thread.tail_sequence + request.records.length},
|
|
835
|
+
${request.producerIdJson}
|
|
836
|
+
WHERE NOT EXISTS (
|
|
837
|
+
SELECT 1 FROM effect_agent_executions
|
|
838
|
+
WHERE thread_id=${request.threadId} AND last_commit_id=${request.batchId}
|
|
839
|
+
)
|
|
840
|
+
ON CONFLICT(thread_id, batch_id) DO NOTHING RETURNING 1`.pipe(
|
|
841
|
+
Effect.mapError(storageError("insert canonical batch")),
|
|
842
|
+
)
|
|
843
|
+
: [];
|
|
638
844
|
|
|
639
|
-
|
|
845
|
+
if (inserted.length === 0) {
|
|
846
|
+
const replay = yield* executionConflict(executions.replay(request));
|
|
640
847
|
|
|
641
|
-
|
|
848
|
+
if (replay !== undefined) return AppendResult.make(replay);
|
|
642
849
|
|
|
643
|
-
|
|
644
|
-
prefix !== undefined && !prefix.some((row) => row.batch_id === request.batchId)
|
|
645
|
-
? undefined
|
|
646
|
-
: yield* archives.replay(request.threadId, request.batchId);
|
|
850
|
+
const prefix = recordCache.prefix(request.threadId, thread.tail_sequence);
|
|
647
851
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
852
|
+
const existing =
|
|
853
|
+
prefix !== undefined && !prefix.some((row) => row.batch_id === request.batchId)
|
|
854
|
+
? undefined
|
|
855
|
+
: yield* archives.replay(request.threadId, request.batchId);
|
|
856
|
+
|
|
857
|
+
if (existing !== undefined) {
|
|
858
|
+
if (existing.batch_json !== request.batchJson) {
|
|
859
|
+
return yield* DoAppendConflict.make({
|
|
860
|
+
message: `Batch ${request.batchId} already exists with different canonical content.`,
|
|
861
|
+
reason: "batch-content",
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return AppendResult.make({
|
|
866
|
+
firstSequence: existing.first_sequence,
|
|
867
|
+
lastSequence: existing.last_sequence,
|
|
868
|
+
replayed: true,
|
|
653
869
|
});
|
|
654
870
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
871
|
+
if (canInsert)
|
|
872
|
+
return yield* DoStorageCorruptionError.make({
|
|
873
|
+
table: "effect_agent_canonical_batches",
|
|
874
|
+
rowKey: request.threadId,
|
|
875
|
+
message: "Batch insertion conflicted without a durable replay receipt.",
|
|
876
|
+
});
|
|
661
877
|
}
|
|
662
878
|
|
|
663
879
|
yield* executionConflict(executions.rejectSuperseded(request));
|
|
@@ -671,9 +887,11 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
671
887
|
});
|
|
672
888
|
}
|
|
673
889
|
|
|
674
|
-
const
|
|
675
|
-
thread.tail_sequence + (request.records.length === 0 ? 0 : 1),
|
|
676
|
-
|
|
890
|
+
const result = yield* Schema.decodeEffect(AppendResult)({
|
|
891
|
+
firstSequence: thread.tail_sequence + (request.records.length === 0 ? 0 : 1),
|
|
892
|
+
lastSequence: thread.tail_sequence + request.records.length,
|
|
893
|
+
replayed: false,
|
|
894
|
+
}).pipe(
|
|
677
895
|
Effect.mapError((error) =>
|
|
678
896
|
DoStorageError.make({
|
|
679
897
|
cause: error,
|
|
@@ -683,27 +901,29 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
683
901
|
),
|
|
684
902
|
);
|
|
685
903
|
|
|
686
|
-
const lastSequence =
|
|
687
|
-
thread.tail_sequence + request.records.length,
|
|
688
|
-
).pipe(
|
|
689
|
-
Effect.mapError((error) =>
|
|
690
|
-
DoStorageError.make({
|
|
691
|
-
cause: error,
|
|
692
|
-
operation: "append canonical batch",
|
|
693
|
-
message: error.message,
|
|
694
|
-
}),
|
|
695
|
-
),
|
|
696
|
-
);
|
|
904
|
+
const { firstSequence, lastSequence } = result;
|
|
697
905
|
|
|
698
906
|
yield* executionConflict(executions.validate(request));
|
|
699
907
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
908
|
+
if (yield* appendNativeBody(request, firstSequence, lastSequence, inserted.length > 0)) {
|
|
909
|
+
yield* progress.committed("canonical").pipe(
|
|
910
|
+
Effect.catchCause((cause) =>
|
|
911
|
+
Effect.failCause(
|
|
912
|
+
Cause.map(cause, (error) =>
|
|
913
|
+
DoStorageError.make({
|
|
914
|
+
operation: "enroll canonical progress",
|
|
915
|
+
message: error.message,
|
|
916
|
+
cause: error,
|
|
917
|
+
}),
|
|
918
|
+
),
|
|
919
|
+
),
|
|
920
|
+
),
|
|
921
|
+
);
|
|
705
922
|
|
|
706
|
-
|
|
923
|
+
return result;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (request.records.length > 0 && inserted.length === 0) {
|
|
707
927
|
yield* sql`
|
|
708
928
|
INSERT INTO effect_agent_canonical_batches (
|
|
709
929
|
thread_id,
|
|
@@ -760,6 +980,7 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
760
980
|
recordCache.put(
|
|
761
981
|
RecordRow.make({
|
|
762
982
|
...row,
|
|
983
|
+
thread_id: request.threadId,
|
|
763
984
|
record_id: record.recordId,
|
|
764
985
|
batch_id: request.batchId,
|
|
765
986
|
record_json: record.recordJson,
|
|
@@ -874,7 +1095,7 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
874
1095
|
function* (
|
|
875
1096
|
request: RawAppendRequest,
|
|
876
1097
|
observed: ThreadRow,
|
|
877
|
-
): Effect.fn.Return<
|
|
1098
|
+
): Effect.fn.Return<AppendResult, AppendError> {
|
|
878
1099
|
yield* prepareAppend(request);
|
|
879
1100
|
// The facade already read this tail. Reject a known stale writer without opening a
|
|
880
1101
|
// transaction; a matching tail is still checked atomically below. Replays precede tail
|
|
@@ -898,10 +1119,9 @@ const makeJournal = Effect.fnUntraced(function* (
|
|
|
898
1119
|
}
|
|
899
1120
|
}
|
|
900
1121
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
)(appendPrepared(request));
|
|
1122
|
+
// Receipt arbitration may have tentatively inserted a batch. Even typed
|
|
1123
|
+
// contention must roll back before leaving the owner mutation gate.
|
|
1124
|
+
return yield* withWriteTransaction("append transaction")(appendPrepared(request));
|
|
905
1125
|
},
|
|
906
1126
|
withStorageSpan("DoJournal.append", isAppendContention),
|
|
907
1127
|
);
|