@yielded/agent-storage-cloudflare 0.1.0-beta.166
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/LICENSE +21 -0
- package/dist/DoMemoryStore.d.mts +30 -0
- package/dist/DoMemoryStore.mjs +66 -0
- package/dist/DoMemoryStore.mjs.map +1 -0
- package/dist/DoMessageDeliveryStore.d.mts +17 -0
- package/dist/DoMessageDeliveryStore.mjs +30 -0
- package/dist/DoMessageDeliveryStore.mjs.map +1 -0
- package/dist/DoScheduleStore.d.mts +36 -0
- package/dist/DoScheduleStore.mjs +388 -0
- package/dist/DoScheduleStore.mjs.map +1 -0
- package/dist/DoStorageConfig.d.mts +45 -0
- package/dist/DoStorageConfig.mjs +52 -0
- package/dist/DoStorageConfig.mjs.map +1 -0
- package/dist/DoStorageError-CGuAfSwF.d.mts +101 -0
- package/dist/DoStorageError.d.mts +2 -0
- package/dist/DoStorageError.mjs +167 -0
- package/dist/DoStorageError.mjs.map +1 -0
- package/dist/DoStorageFailpoint.d.mts +17 -0
- package/dist/DoStorageFailpoint.mjs +14 -0
- package/dist/DoStorageFailpoint.mjs.map +1 -0
- package/dist/DoStorageFailpointTesting.d.mts +24 -0
- package/dist/DoStorageFailpointTesting.mjs +22 -0
- package/dist/DoStorageFailpointTesting.mjs.map +1 -0
- package/dist/DoStorageVersion-iNSyi3cJ.d.mts +12 -0
- package/dist/DoStorageVersion.d.mts +2 -0
- package/dist/DoStorageVersion.mjs +8 -0
- package/dist/DoStorageVersion.mjs.map +1 -0
- package/dist/DoSubmissionLedger.d.mts +23 -0
- package/dist/DoSubmissionLedger.mjs +1979 -0
- package/dist/DoSubmissionLedger.mjs.map +1 -0
- package/dist/DoSubscriptionStore.d.mts +25 -0
- package/dist/DoSubscriptionStore.mjs +186 -0
- package/dist/DoSubscriptionStore.mjs.map +1 -0
- package/dist/DoThreadStore-D_83OjCH.d.mts +72 -0
- package/dist/DoThreadStore.d.mts +2 -0
- package/dist/DoThreadStore.mjs +529 -0
- package/dist/DoThreadStore.mjs.map +1 -0
- package/dist/MemoryProtocol.d.mts +38160 -0
- package/dist/MemoryProtocol.mjs +219 -0
- package/dist/MemoryProtocol.mjs.map +1 -0
- package/dist/PortProtocol.d.mts +7726 -0
- package/dist/PortProtocol.mjs +234 -0
- package/dist/PortProtocol.mjs.map +1 -0
- package/dist/PortRouting.d.mts +97 -0
- package/dist/PortRouting.mjs +544 -0
- package/dist/PortRouting.mjs.map +1 -0
- package/dist/do-journal-DUhjZkOf.mjs +1352 -0
- package/dist/do-journal-DUhjZkOf.mjs.map +1 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +14 -0
- package/dist/migrations-DtKz_Gxr.mjs +306 -0
- package/dist/migrations-DtKz_Gxr.mjs.map +1 -0
- package/dist/owned-state-DHm2aRXx.mjs +119 -0
- package/dist/owned-state-DHm2aRXx.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -0
- package/src/DoMemoryStore.ts +73 -0
- package/src/DoMessageDeliveryStore.ts +39 -0
- package/src/DoScheduleStore.ts +622 -0
- package/src/DoStorageConfig.ts +53 -0
- package/src/DoStorageError.ts +180 -0
- package/src/DoStorageFailpoint.ts +20 -0
- package/src/DoStorageFailpointTesting.ts +41 -0
- package/src/DoStorageVersion.ts +2 -0
- package/src/DoSubmissionLedger.ts +4027 -0
- package/src/DoSubscriptionStore.ts +327 -0
- package/src/DoThreadStore.ts +1121 -0
- package/src/MemoryProtocol.ts +355 -0
- package/src/PortProtocol.ts +436 -0
- package/src/PortRouting.ts +1464 -0
- package/src/index.ts +13 -0
- package/src/internal/do-journal.ts +2282 -0
- package/src/internal/message-delivery-schema.ts +24 -0
- package/src/internal/migrations.ts +304 -0
- package/src/internal/owned-state.ts +228 -0
- package/src/internal/recovery-checkpoint-schema.ts +17 -0
- package/src/internal/storage-span.ts +44 -0
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
import { DoAppendConflict, DoCheckpointConflict, DoFenceRejected, DoStorageCompatibilityError, DoStorageCorruptionError, DoStorageError, DoValueBoundExceeded } from "./DoStorageError.mjs";
|
|
2
|
+
import { a as createRecoveryCheckpointTable, i as doMigrations, n as createNonterminalIndex, o as createMessageDeliveryTables, r as createWorkerStops } from "./migrations-DtKz_Gxr.mjs";
|
|
3
|
+
import { n as ownedRows, r as ownedState } from "./owned-state-DHm2aRXx.mjs";
|
|
4
|
+
import { SqlLifecycleRetainer, SqlLifecycleSource, makeSqlLifecyclePublication } from "@yielded/agent-storage-sql/sql-lifecycle-publication";
|
|
5
|
+
import { createMessageDeliveryPendingIndex } from "@yielded/agent-storage-sql/sql-message-delivery-store";
|
|
6
|
+
import { SqlStorageOwner } from "@yielded/agent/sql-memory-store";
|
|
7
|
+
import { Cause, Clock, Effect, Option, Predicate, Result, Schema, Stream } from "effect";
|
|
8
|
+
import * as SqlClient$1 from "effect/sql/SqlClient";
|
|
9
|
+
import { SqliteMigrator } from "@effect/sql-sqlite-do";
|
|
10
|
+
import { SqlStorageProgress } from "@yielded/agent-storage-sql/sql-storage-progress";
|
|
11
|
+
import { checkV2ThreadLayout } from "@yielded/agent-storage-sql/sql-storage-v2-upgrade";
|
|
12
|
+
import { canonicalRecordOutstanding, createNativeReadIndexes, indexCanonicalRecord, seedNativeReadIndexes } from "@yielded/agent-storage-sql/sql-thread-native-reads";
|
|
13
|
+
import { EMPTY_TAIL_DIGEST } from "@yielded/agent/digest";
|
|
14
|
+
import { ThreadId } from "@yielded/agent/identifiers";
|
|
15
|
+
import { LifecyclePublicationError, LifecyclePublicationFact } from "@yielded/agent/lifecycle-publication";
|
|
16
|
+
import { CanonicalRecord, CanonicalSequence, ProducerEpoch } from "@yielded/agent/records";
|
|
17
|
+
import { CheckpointRejected, FenceRejected, MAX_THREAD_EXPORT_RECORDS, ThreadNotMaterialized, ThreadStoreDiagnostic } from "@yielded/agent/thread-store";
|
|
18
|
+
import { SqlError } from "effect/sql/SqlError";
|
|
19
|
+
//#region src/internal/storage-span.ts
|
|
20
|
+
const isAppendContention = (error) => error._tag === "DoFenceRejected" || error._tag === "FenceRejected" || (error._tag === "DoAppendConflict" || error._tag === "AppendConflict") && error.reason === "tail";
|
|
21
|
+
/** Expected refusals remain typed at the port, but are outcomes of the storage operation. */
|
|
22
|
+
const storageResult = (effect, expected) => effect.pipe(Effect.map(Result.succeed), Effect.catchIf(expected, (error) => Effect.succeed(Result.fail(error))));
|
|
23
|
+
/** Only error discriminants enter telemetry; messages, SQL, identities and payloads do not. */
|
|
24
|
+
const annotateStorageError = (error) => Effect.annotateCurrentSpan({
|
|
25
|
+
"error.type": error._tag,
|
|
26
|
+
...Predicate.hasProperty(error, "cause") && Predicate.hasProperty(error.cause, "_tag") && typeof error.cause._tag === "string" ? { "error.cause.type": error.cause._tag } : {}
|
|
27
|
+
});
|
|
28
|
+
const withStorageSpan = (name, expected) => (effect) => storageResult(effect, expected).pipe(Effect.tap((result) => Result.isFailure(result) ? Effect.annotateCurrentSpan({ "storage.outcome": result.failure._tag }) : Effect.void), Effect.tapError(annotateStorageError), Effect.withSpan(name), Effect.flatMap(Effect.fromResult));
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/internal/do-journal.ts
|
|
31
|
+
/**
|
|
32
|
+
* Static schema ceiling for stored text columns. Writes are bounded in BYTES by the
|
|
33
|
+
* configured `maxStoredValueBytes` (always ≤ 2,000,000); UTF-8 byte length is never smaller
|
|
34
|
+
* than UTF-16 string length, so any value that passed the byte bound also passes this
|
|
35
|
+
* decode-side character ceiling.
|
|
36
|
+
*/
|
|
37
|
+
const BoundedStoredText = Schema.String.check(Schema.isMaxLength(2e6));
|
|
38
|
+
const BoundedIdentifier = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
39
|
+
const MAX_RECORDS_PER_THREAD = MAX_THREAD_EXPORT_RECORDS;
|
|
40
|
+
const ZERO_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
|
|
41
|
+
const MAX_IDENTIFIER_LENGTH = 1024;
|
|
42
|
+
const MAX_READ_PAGE_JSON_BYTES = 4194304;
|
|
43
|
+
const MAX_RECORD_CACHE_JSON_BYTES = 8388608;
|
|
44
|
+
const MAX_RECORD_CACHE_ENTRIES = 4096;
|
|
45
|
+
/** Durable Object SQL storage allows at most 100 bound parameters per statement. */
|
|
46
|
+
const MAX_BOUND_PARAMETERS = 100;
|
|
47
|
+
const isSqlError = Schema.is(SqlError);
|
|
48
|
+
const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
49
|
+
const chunked = (values, size) => {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
|
|
52
|
+
return chunks;
|
|
53
|
+
};
|
|
54
|
+
var DoMetaRow = class extends Schema.Class("DoMetaRow")({ value: Schema.NonEmptyString.check(Schema.isMaxLength(128)) }) {};
|
|
55
|
+
var DoNameRow = class extends Schema.Class("DoNameRow")({ name: BoundedIdentifier }) {};
|
|
56
|
+
var ThreadRow = class extends Schema.Class("ThreadRow")({
|
|
57
|
+
thread_id: BoundedIdentifier,
|
|
58
|
+
created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
|
|
59
|
+
producer_epoch: ProducerEpoch,
|
|
60
|
+
tail_digest: BoundedStoredText,
|
|
61
|
+
tail_sequence: CanonicalSequence
|
|
62
|
+
}) {};
|
|
63
|
+
const threadRows = ownedRows(ThreadRow, "effect_agent_threads", (row) => row.thread_id, "thread_id");
|
|
64
|
+
var BatchRow = class extends Schema.Class("BatchRow")({
|
|
65
|
+
batch_digest: BoundedStoredText,
|
|
66
|
+
batch_id: BoundedIdentifier,
|
|
67
|
+
batch_json: BoundedStoredText,
|
|
68
|
+
thread_id: BoundedIdentifier,
|
|
69
|
+
first_sequence: CanonicalSequence,
|
|
70
|
+
last_sequence: CanonicalSequence,
|
|
71
|
+
tail_digest: BoundedStoredText
|
|
72
|
+
}) {};
|
|
73
|
+
var RecordRow = class extends Schema.Class("RecordRow")({
|
|
74
|
+
batch_id: BoundedIdentifier,
|
|
75
|
+
thread_id: BoundedIdentifier,
|
|
76
|
+
record_id: BoundedIdentifier,
|
|
77
|
+
record_json: BoundedStoredText,
|
|
78
|
+
sequence: CanonicalSequence
|
|
79
|
+
}) {};
|
|
80
|
+
const isolateRecords = /* @__PURE__ */ new Map();
|
|
81
|
+
let isolateRecordBytes = 0;
|
|
82
|
+
const releaseRecord = (entry) => {
|
|
83
|
+
if (!isolateRecords.delete(entry)) return;
|
|
84
|
+
isolateRecordBytes -= entry.bytes;
|
|
85
|
+
entry.evict();
|
|
86
|
+
};
|
|
87
|
+
const recordCaches = /* @__PURE__ */ new WeakMap();
|
|
88
|
+
const makeRecordCache = () => {
|
|
89
|
+
const records = /* @__PURE__ */ new Map();
|
|
90
|
+
const key = (thread, sequence) => JSON.stringify([thread, sequence]);
|
|
91
|
+
return {
|
|
92
|
+
clear: () => {
|
|
93
|
+
for (const entry of records.values()) releaseRecord(entry);
|
|
94
|
+
},
|
|
95
|
+
get: (thread, sequence) => records.get(key(thread, sequence))?.row,
|
|
96
|
+
prefix: (thread, through) => {
|
|
97
|
+
const prefix = [];
|
|
98
|
+
for (let sequence = 1; sequence <= through; sequence++) {
|
|
99
|
+
const row = records.get(key(thread, sequence));
|
|
100
|
+
if (row === void 0) return void 0;
|
|
101
|
+
prefix.push(row.row);
|
|
102
|
+
}
|
|
103
|
+
return prefix;
|
|
104
|
+
},
|
|
105
|
+
put: (row) => {
|
|
106
|
+
const id = key(row.thread_id, row.sequence);
|
|
107
|
+
const prior = records.get(id);
|
|
108
|
+
if (prior?.row === row) return;
|
|
109
|
+
const size = storedTextBytes(row.record_json);
|
|
110
|
+
if (prior !== void 0) releaseRecord(prior);
|
|
111
|
+
const entry = {
|
|
112
|
+
row,
|
|
113
|
+
bytes: size,
|
|
114
|
+
evict: () => records.delete(id)
|
|
115
|
+
};
|
|
116
|
+
records.set(id, entry);
|
|
117
|
+
isolateRecords.set(entry, true);
|
|
118
|
+
isolateRecordBytes += size;
|
|
119
|
+
while (isolateRecordBytes > MAX_RECORD_CACHE_JSON_BYTES || isolateRecords.size > MAX_RECORD_CACHE_ENTRIES) {
|
|
120
|
+
const oldest = isolateRecords.keys().next().value;
|
|
121
|
+
if (oldest === void 0) break;
|
|
122
|
+
releaseRecord(oldest);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const ReadPlanRow = Schema.Struct({
|
|
128
|
+
sequence: CanonicalSequence,
|
|
129
|
+
record_json_bytes: Schema.Natural.check(Schema.isLessThanOrEqualTo(MAX_READ_PAGE_JSON_BYTES))
|
|
130
|
+
});
|
|
131
|
+
var CheckpointRow = class extends Schema.Class("CheckpointRow")({
|
|
132
|
+
checkpoint_json: BoundedStoredText,
|
|
133
|
+
thread_id: BoundedIdentifier,
|
|
134
|
+
tail_digest: BoundedStoredText,
|
|
135
|
+
through_sequence: CanonicalSequence
|
|
136
|
+
}) {};
|
|
137
|
+
const recoveryRows = ownedRows(CheckpointRow, "effect_agent_recovery_checkpoints", (row) => row.thread_id, "thread_id");
|
|
138
|
+
const LifecycleCursorRow = Schema.Struct({
|
|
139
|
+
thread_id: BoundedIdentifier,
|
|
140
|
+
through_sequence: CanonicalSequence
|
|
141
|
+
});
|
|
142
|
+
const lifecycleCursorRows = ownedRows(LifecycleCursorRow, "effect_agent_lifecycle_cursors", (row) => row.thread_id, "thread_id");
|
|
143
|
+
const initializedLifecycleSources = /* @__PURE__ */ new WeakSet();
|
|
144
|
+
var RawRecord = class extends Schema.Class("@effect-agent/storage-cloudflare/RawRecord")({
|
|
145
|
+
recordId: BoundedIdentifier,
|
|
146
|
+
recordJson: BoundedStoredText
|
|
147
|
+
}) {};
|
|
148
|
+
var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendRequest")({
|
|
149
|
+
batchDigest: BoundedStoredText,
|
|
150
|
+
batchId: BoundedIdentifier,
|
|
151
|
+
batchJson: BoundedStoredText,
|
|
152
|
+
threadId: BoundedIdentifier,
|
|
153
|
+
expectedTailDigest: BoundedStoredText,
|
|
154
|
+
expectedTailSequence: CanonicalSequence,
|
|
155
|
+
producerEpoch: ProducerEpoch,
|
|
156
|
+
records: Schema.NonEmptyArray(RawRecord).check(Schema.isMaxLength(256)),
|
|
157
|
+
tailDigest: BoundedStoredText
|
|
158
|
+
}) {};
|
|
159
|
+
var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudflare/RawAppendResult")({
|
|
160
|
+
firstSequence: CanonicalSequence,
|
|
161
|
+
lastSequence: CanonicalSequence,
|
|
162
|
+
replayed: Schema.Boolean,
|
|
163
|
+
tailDigest: BoundedStoredText
|
|
164
|
+
}) {};
|
|
165
|
+
var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
|
|
166
|
+
threadId: BoundedIdentifier,
|
|
167
|
+
fromSequenceExclusive: CanonicalSequence,
|
|
168
|
+
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
|
|
169
|
+
}) {};
|
|
170
|
+
var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
|
|
171
|
+
checkpointJson: BoundedStoredText,
|
|
172
|
+
threadId: BoundedIdentifier,
|
|
173
|
+
tailDigest: BoundedStoredText,
|
|
174
|
+
throughSequence: CanonicalSequence
|
|
175
|
+
}) {};
|
|
176
|
+
var RawThreadExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawThreadExport")({
|
|
177
|
+
thread: ThreadRow,
|
|
178
|
+
records: Schema.Array(RecordRow)
|
|
179
|
+
}) {};
|
|
180
|
+
const noFailpoint = () => Effect.void;
|
|
181
|
+
const storageError = (operation) => (error) => DoStorageError.make({
|
|
182
|
+
cause: error,
|
|
183
|
+
operation,
|
|
184
|
+
message: error.message,
|
|
185
|
+
diagnostic: ThreadStoreDiagnostic.make({
|
|
186
|
+
causeTag: error._tag,
|
|
187
|
+
operation,
|
|
188
|
+
issueTag: error.reason._tag
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
/** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
|
|
192
|
+
const decodeRows = Effect.fn((schema, table, rowKey, rows, decoder = table) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
193
|
+
table,
|
|
194
|
+
rowKey,
|
|
195
|
+
message: "Stored rows do not satisfy the storage schema",
|
|
196
|
+
diagnostic: ThreadStoreDiagnostic.make({
|
|
197
|
+
causeTag: error._tag,
|
|
198
|
+
operation: "decode storage rows",
|
|
199
|
+
decoder,
|
|
200
|
+
issueTag: error.issue._tag
|
|
201
|
+
})
|
|
202
|
+
})), Effect.tapError((error) => Effect.annotateCurrentSpan({
|
|
203
|
+
"storage.failure.operation": error.diagnostic?.operation,
|
|
204
|
+
"storage.failure.cause": error.diagnostic?.causeTag,
|
|
205
|
+
"storage.failure.decoder": decoder,
|
|
206
|
+
"storage.failure.issue": error.diagnostic?.issueTag
|
|
207
|
+
}))));
|
|
208
|
+
/** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
|
|
209
|
+
const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
|
|
210
|
+
table,
|
|
211
|
+
rowKey,
|
|
212
|
+
message: `Expected exactly one row but found ${decoded.length}.`
|
|
213
|
+
})))));
|
|
214
|
+
/** Column inventory of the supported v3 predecessor, independent of physical column order. */
|
|
215
|
+
const predecessorColumns = {
|
|
216
|
+
effect_agent_threads: [
|
|
217
|
+
"thread_id",
|
|
218
|
+
"created_at",
|
|
219
|
+
"tail_sequence",
|
|
220
|
+
"tail_digest",
|
|
221
|
+
"producer_epoch"
|
|
222
|
+
],
|
|
223
|
+
effect_agent_canonical_batches: [
|
|
224
|
+
"thread_id",
|
|
225
|
+
"batch_id",
|
|
226
|
+
"first_sequence",
|
|
227
|
+
"last_sequence",
|
|
228
|
+
"batch_digest",
|
|
229
|
+
"tail_digest",
|
|
230
|
+
"batch_json"
|
|
231
|
+
],
|
|
232
|
+
effect_agent_canonical_records: [
|
|
233
|
+
"thread_id",
|
|
234
|
+
"sequence",
|
|
235
|
+
"record_id",
|
|
236
|
+
"batch_id",
|
|
237
|
+
"record_json"
|
|
238
|
+
],
|
|
239
|
+
effect_agent_checkpoints: [
|
|
240
|
+
"thread_id",
|
|
241
|
+
"through_sequence",
|
|
242
|
+
"tail_digest",
|
|
243
|
+
"checkpoint_json"
|
|
244
|
+
],
|
|
245
|
+
effect_agent_submissions: [
|
|
246
|
+
"submission_id",
|
|
247
|
+
"thread_id",
|
|
248
|
+
"queue_sequence",
|
|
249
|
+
"principal",
|
|
250
|
+
"idempotency_key",
|
|
251
|
+
"agent_id",
|
|
252
|
+
"agent_digests_json",
|
|
253
|
+
"deployment_id",
|
|
254
|
+
"input_json",
|
|
255
|
+
"input_digest",
|
|
256
|
+
"receipt_id",
|
|
257
|
+
"state",
|
|
258
|
+
"settled_outcome",
|
|
259
|
+
"created_at",
|
|
260
|
+
"ready_at",
|
|
261
|
+
"input_applied_record_id",
|
|
262
|
+
"input_applied_sequence",
|
|
263
|
+
"joined_host_submission_id",
|
|
264
|
+
"suspended_reason_json",
|
|
265
|
+
"suspended_at",
|
|
266
|
+
"unknown_reason",
|
|
267
|
+
"unknown_tool_call_ids_json",
|
|
268
|
+
"parent_submission_id",
|
|
269
|
+
"parent_tool_call_id",
|
|
270
|
+
"admission_group",
|
|
271
|
+
"admission_fence_json"
|
|
272
|
+
],
|
|
273
|
+
effect_agent_submission_ownership: [
|
|
274
|
+
"submission_id",
|
|
275
|
+
"attempt_id",
|
|
276
|
+
"ownership_token",
|
|
277
|
+
"producer_epoch",
|
|
278
|
+
"owner_producer_id",
|
|
279
|
+
"lease_expires_at"
|
|
280
|
+
],
|
|
281
|
+
effect_agent_attempts: [
|
|
282
|
+
"attempt_id",
|
|
283
|
+
"submission_id",
|
|
284
|
+
"thread_id",
|
|
285
|
+
"owner_producer_id",
|
|
286
|
+
"producer_epoch",
|
|
287
|
+
"claimed_at"
|
|
288
|
+
],
|
|
289
|
+
effect_agent_settlement_reservations: [
|
|
290
|
+
"submission_id",
|
|
291
|
+
"settlement_id",
|
|
292
|
+
"outcome",
|
|
293
|
+
"record_id",
|
|
294
|
+
"record_json",
|
|
295
|
+
"record_digest",
|
|
296
|
+
"reserved_at",
|
|
297
|
+
"finalized_at"
|
|
298
|
+
],
|
|
299
|
+
effect_agent_abort_intents: [
|
|
300
|
+
"submission_id",
|
|
301
|
+
"author",
|
|
302
|
+
"reason",
|
|
303
|
+
"requested_at",
|
|
304
|
+
"canonical_record_id"
|
|
305
|
+
],
|
|
306
|
+
effect_agent_approval_decisions: [
|
|
307
|
+
"submission_id",
|
|
308
|
+
"tool_call_id",
|
|
309
|
+
"decision",
|
|
310
|
+
"resolver",
|
|
311
|
+
"reason",
|
|
312
|
+
"decided_at"
|
|
313
|
+
],
|
|
314
|
+
effect_agent_unknown_resolutions: [
|
|
315
|
+
"submission_id",
|
|
316
|
+
"tool_call_id",
|
|
317
|
+
"author",
|
|
318
|
+
"reason",
|
|
319
|
+
"resolution_json",
|
|
320
|
+
"resolved_at"
|
|
321
|
+
],
|
|
322
|
+
effect_agent_child_reservations: [
|
|
323
|
+
"reservation_id",
|
|
324
|
+
"parent_submission_id",
|
|
325
|
+
"parent_tool_call_id",
|
|
326
|
+
"child_submission_id",
|
|
327
|
+
"status",
|
|
328
|
+
"allocation_json",
|
|
329
|
+
"allocation_digest",
|
|
330
|
+
"accounting_json",
|
|
331
|
+
"reserved_at",
|
|
332
|
+
"release_began_at",
|
|
333
|
+
"released_at"
|
|
334
|
+
],
|
|
335
|
+
effect_agent_meta: ["key", "value"],
|
|
336
|
+
effect_agent_child_settlements: [
|
|
337
|
+
"parent_submission_id",
|
|
338
|
+
"child_submission_id",
|
|
339
|
+
"child_outcome",
|
|
340
|
+
"recorded_at"
|
|
341
|
+
]
|
|
342
|
+
};
|
|
343
|
+
const checkPredecessorLayout = Effect.fn("DoJournal.checkPredecessorLayout")(function* (version) {
|
|
344
|
+
const sql = (yield* SqlClient$1.SqlClient).withoutTransforms();
|
|
345
|
+
const expectedColumns = {
|
|
346
|
+
...version === 3 ? predecessorColumns : {
|
|
347
|
+
...predecessorColumns,
|
|
348
|
+
effect_agent_submissions: [
|
|
349
|
+
...predecessorColumns.effect_agent_submissions,
|
|
350
|
+
"worker_admission_json",
|
|
351
|
+
"message_admission_json"
|
|
352
|
+
],
|
|
353
|
+
effect_agent_message_deliveries: [
|
|
354
|
+
"owner_thread_id",
|
|
355
|
+
"message_id",
|
|
356
|
+
"version",
|
|
357
|
+
"state",
|
|
358
|
+
"deadline_at_millis",
|
|
359
|
+
"record_json"
|
|
360
|
+
]
|
|
361
|
+
},
|
|
362
|
+
...version === 7 ? { effect_agent_canonical_records: [...predecessorColumns.effect_agent_canonical_records, "outstanding"] } : {},
|
|
363
|
+
...version >= 5 ? { effect_agent_recovery_checkpoints: [
|
|
364
|
+
"thread_id",
|
|
365
|
+
"through_sequence",
|
|
366
|
+
"tail_digest",
|
|
367
|
+
"checkpoint_json"
|
|
368
|
+
] } : {}
|
|
369
|
+
};
|
|
370
|
+
for (const [table, expected] of Object.entries(expectedColumns)) {
|
|
371
|
+
const columns = yield* decodeRows(Schema.Array(Schema.Struct({ name: BoundedIdentifier })), table, "schema", yield* sql.unsafe(`PRAGMA table_info(${table})`));
|
|
372
|
+
const names = new Set(expected);
|
|
373
|
+
if (columns.length !== names.size || columns.some((column) => !names.has(column.name))) return yield* DoStorageCompatibilityError.make({
|
|
374
|
+
actualVersion: version,
|
|
375
|
+
supportedVersion: 9,
|
|
376
|
+
message: `The v${version} ${table} columns do not match the supported predecessor; no upgrade was committed.`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
const REQUIRED_TABLES = [
|
|
381
|
+
"effect_agent_abort_intents",
|
|
382
|
+
"effect_agent_approval_decisions",
|
|
383
|
+
"effect_agent_attempts",
|
|
384
|
+
"effect_agent_canonical_batches",
|
|
385
|
+
"effect_agent_canonical_records",
|
|
386
|
+
"effect_agent_checkpoints",
|
|
387
|
+
"effect_agent_child_reservations",
|
|
388
|
+
"effect_agent_child_settlements",
|
|
389
|
+
"effect_agent_threads",
|
|
390
|
+
"effect_agent_meta",
|
|
391
|
+
"effect_agent_settlement_reservations",
|
|
392
|
+
"effect_agent_submission_ownership",
|
|
393
|
+
"effect_agent_submissions",
|
|
394
|
+
"effect_agent_unknown_resolutions"
|
|
395
|
+
];
|
|
396
|
+
/**
|
|
397
|
+
* Supported-predecessor or fresh storage gate (DEPLOY-008) over `effect_agent_meta` instead of
|
|
398
|
+
* `PRAGMA user_version` (unverified on Durable Object SQL storage; a meta table is portable
|
|
399
|
+
* regardless). No WAL check (Durable Object storage owns durability and confirms writes
|
|
400
|
+
* through output gates) and no busy timeout (a Durable Object has exactly one writer): the
|
|
401
|
+
* Node machinery those served has no DC analogue and is deliberately absent.
|
|
402
|
+
*/
|
|
403
|
+
const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint, maxStoredValueBytes) {
|
|
404
|
+
const verifyWorkerPredecessor = Effect.fnUntraced(function* (workerContract) {
|
|
405
|
+
const requiredRows = yield* sql`
|
|
406
|
+
SELECT name
|
|
407
|
+
FROM sqlite_master
|
|
408
|
+
WHERE (type = 'table'
|
|
409
|
+
AND name IN ${sql.in([
|
|
410
|
+
...REQUIRED_TABLES,
|
|
411
|
+
"effect_agent_message_deliveries",
|
|
412
|
+
"effect_agent_recovery_checkpoints"
|
|
413
|
+
])}
|
|
414
|
+
) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
|
|
415
|
+
OR (${workerContract ? 1 : 0} = 1 AND name IN ('effect_agent_worker_stops', 'effect_agent_worker_starts', 'effect_agent_worker_pending', 'effect_agent_worker_execution'))
|
|
416
|
+
ORDER BY name
|
|
417
|
+
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
418
|
+
if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length + 9 + (workerContract ? 4 : 0)) return yield* DoStorageCompatibilityError.make({
|
|
419
|
+
actualVersion: 9,
|
|
420
|
+
supportedVersion: 9,
|
|
421
|
+
message: "The Durable Object claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection."
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
const metaTableRows = yield* sql`
|
|
425
|
+
SELECT name
|
|
426
|
+
FROM sqlite_master
|
|
427
|
+
WHERE type = 'table'
|
|
428
|
+
AND name = 'effect_agent_meta'
|
|
429
|
+
`.pipe(Effect.mapError(storageError("read storage version table")));
|
|
430
|
+
if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
|
|
431
|
+
const existingRows = yield* sql`
|
|
432
|
+
SELECT name
|
|
433
|
+
FROM sqlite_master
|
|
434
|
+
WHERE type = 'table'
|
|
435
|
+
AND name LIKE 'effect_agent_%'
|
|
436
|
+
ORDER BY name
|
|
437
|
+
`.pipe(Effect.mapError(storageError("inspect unversioned storage")));
|
|
438
|
+
if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
|
|
439
|
+
actualVersion: 0,
|
|
440
|
+
supportedVersion: 9,
|
|
441
|
+
message: "The Durable Object contains unversioned Effect Agent tables. Refusing to mutate ambiguous stored data; retain it for inspection with its original writer."
|
|
442
|
+
});
|
|
443
|
+
yield* SqliteMigrator.run({
|
|
444
|
+
loader: doMigrations,
|
|
445
|
+
table: "effect_sql_migrations_agent_threads"
|
|
446
|
+
}).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((error) => DoStorageError.make({
|
|
447
|
+
cause: error,
|
|
448
|
+
operation: "initialize current storage",
|
|
449
|
+
message: error.message
|
|
450
|
+
})));
|
|
451
|
+
} else {
|
|
452
|
+
const versionRows = yield* sql`
|
|
453
|
+
SELECT value
|
|
454
|
+
FROM effect_agent_meta
|
|
455
|
+
WHERE key = 'storage_version'
|
|
456
|
+
`.pipe(Effect.mapError(storageError("read storage version")));
|
|
457
|
+
const version = yield* decodeSingleRow(Schema.Array(DoMetaRow), "effect_agent_meta", "storage_version", versionRows);
|
|
458
|
+
if (version.value === "2" || version.value === "3" || version.value === "4" || version.value === "5") yield* sql.withTransaction(Effect.gen(function* () {
|
|
459
|
+
const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key='storage_version'`;
|
|
460
|
+
if (current.length === 1 && current[0].value === String(9)) return;
|
|
461
|
+
if (current.length !== 1 || current[0].value !== "2" && current[0].value !== "3" && current[0].value !== "4" && current[0].value !== "5") return yield* DoStorageCompatibilityError.make({
|
|
462
|
+
actualVersion: -1,
|
|
463
|
+
supportedVersion: 9,
|
|
464
|
+
message: "Storage version changed while acquiring the upgrade transaction."
|
|
465
|
+
});
|
|
466
|
+
if ((yield* decodeRows(Schema.Array(DoNameRow), "sqlite_master", "required_tables", yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name IN ${sql.in([...REQUIRED_TABLES])}`)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
|
|
467
|
+
actualVersion: Number(current[0].value),
|
|
468
|
+
supportedVersion: 9,
|
|
469
|
+
message: "The predecessor store is missing required tables. Retain the original store for inspection; no upgrade was committed."
|
|
470
|
+
});
|
|
471
|
+
if ((yield* sql`SELECT name FROM sqlite_master WHERE type='table' AND name='effect_agent_recovery_checkpoints'`).length !== (current[0].value === "5" ? 1 : 0)) return yield* DoStorageCompatibilityError.make({
|
|
472
|
+
actualVersion: Number(current[0].value),
|
|
473
|
+
supportedVersion: 9,
|
|
474
|
+
message: "The predecessor recovery checkpoint storage does not match its version; refusing ambiguous data without mutation."
|
|
475
|
+
});
|
|
476
|
+
if ((yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_submissions_nonterminal'`).length !== 0) return yield* DoStorageCompatibilityError.make({
|
|
477
|
+
actualVersion: Number(current[0].value),
|
|
478
|
+
supportedVersion: 9,
|
|
479
|
+
message: "The predecessor already contains the nonterminal index; refusing ambiguous storage without mutation."
|
|
480
|
+
});
|
|
481
|
+
if (current[0].value === "2") {
|
|
482
|
+
yield* checkV2ThreadLayout();
|
|
483
|
+
for (const statement of [
|
|
484
|
+
sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_group TEXT`,
|
|
485
|
+
sql`ALTER TABLE effect_agent_submissions ADD COLUMN admission_fence_json TEXT`,
|
|
486
|
+
sql`CREATE INDEX effect_agent_submissions_group ON effect_agent_submissions (thread_id, admission_group, state)`
|
|
487
|
+
]) {
|
|
488
|
+
yield* failpoint("upgrade:before-mutation");
|
|
489
|
+
yield* statement;
|
|
490
|
+
yield* failpoint("upgrade:after-mutation");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (current[0].value === "3") yield* checkPredecessorLayout(3);
|
|
494
|
+
if (current[0].value === "4") yield* checkPredecessorLayout(4);
|
|
495
|
+
if (current[0].value === "5") yield* checkPredecessorLayout(5);
|
|
496
|
+
if (current[0].value === "2" || current[0].value === "3") {
|
|
497
|
+
yield* failpoint("upgrade:before-mutation");
|
|
498
|
+
yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN worker_admission_json TEXT`;
|
|
499
|
+
yield* failpoint("upgrade:after-mutation");
|
|
500
|
+
yield* failpoint("upgrade:before-mutation");
|
|
501
|
+
yield* sql`ALTER TABLE effect_agent_submissions ADD COLUMN message_admission_json TEXT`;
|
|
502
|
+
yield* failpoint("upgrade:after-mutation");
|
|
503
|
+
yield* failpoint("upgrade:before-mutation");
|
|
504
|
+
yield* createMessageDeliveryTables;
|
|
505
|
+
yield* failpoint("upgrade:after-mutation");
|
|
506
|
+
}
|
|
507
|
+
if (current[0].value !== "5") {
|
|
508
|
+
yield* failpoint("upgrade:before-mutation");
|
|
509
|
+
yield* createRecoveryCheckpointTable;
|
|
510
|
+
yield* failpoint("upgrade:after-mutation");
|
|
511
|
+
}
|
|
512
|
+
yield* failpoint("upgrade:before-mutation");
|
|
513
|
+
yield* createNonterminalIndex;
|
|
514
|
+
yield* failpoint("upgrade:after-mutation");
|
|
515
|
+
yield* failpoint("upgrade:before-version");
|
|
516
|
+
yield* createNativeReadIndexes();
|
|
517
|
+
yield* createMessageDeliveryPendingIndex();
|
|
518
|
+
yield* seedNativeReadIndexes.pipe(Effect.catchTag("ThreadStoreError", (error) => DoStorageCorruptionError.make({
|
|
519
|
+
table: "effect_agent_canonical_records",
|
|
520
|
+
rowKey: "upgrade",
|
|
521
|
+
message: error.message
|
|
522
|
+
})));
|
|
523
|
+
yield* createWorkerStops;
|
|
524
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
525
|
+
yield* failpoint("upgrade:after-version");
|
|
526
|
+
})).pipe(Effect.catchTag("DoStorageFailpointError", (error) => DoStorageError.make({
|
|
527
|
+
cause: error,
|
|
528
|
+
operation: "upgrade storage",
|
|
529
|
+
message: error.message
|
|
530
|
+
})), Effect.catchTag("StorageUpgradeError", (error) => DoStorageCorruptionError.make({
|
|
531
|
+
table: error.table,
|
|
532
|
+
rowKey: error.rowKey,
|
|
533
|
+
message: error.message
|
|
534
|
+
})), Effect.catchTag("SchemaError", (error) => DoStorageCorruptionError.make({
|
|
535
|
+
table: "effect_agent_canonical_records",
|
|
536
|
+
rowKey: "upgrade",
|
|
537
|
+
message: error.message
|
|
538
|
+
})), Effect.catchTag("SqlError", storageError("upgrade supported thread storage")));
|
|
539
|
+
else if (version.value === "6") yield* sql.withTransaction(Effect.gen(function* () {
|
|
540
|
+
const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
541
|
+
if (current[0]?.value === "9") return;
|
|
542
|
+
if (current[0]?.value !== "6") return yield* DoStorageCompatibilityError.make({
|
|
543
|
+
actualVersion: -1,
|
|
544
|
+
supportedVersion: 9,
|
|
545
|
+
message: "Storage version changed during native index upgrade"
|
|
546
|
+
});
|
|
547
|
+
yield* checkPredecessorLayout(5);
|
|
548
|
+
if ((yield* sql`SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'effect_agent_submissions_nonterminal'`).length !== 1) return yield* DoStorageCompatibilityError.make({
|
|
549
|
+
actualVersion: 6,
|
|
550
|
+
supportedVersion: 9,
|
|
551
|
+
message: "Predecessor storage is missing its required nonterminal index"
|
|
552
|
+
});
|
|
553
|
+
yield* failpoint("upgrade:before-mutation");
|
|
554
|
+
yield* createNativeReadIndexes();
|
|
555
|
+
yield* createMessageDeliveryPendingIndex();
|
|
556
|
+
yield* seedNativeReadIndexes.pipe(Effect.catchTag("ThreadStoreError", (error) => DoStorageCorruptionError.make({
|
|
557
|
+
table: "effect_agent_canonical_records",
|
|
558
|
+
rowKey: "upgrade",
|
|
559
|
+
message: error.message
|
|
560
|
+
})));
|
|
561
|
+
yield* failpoint("upgrade:after-mutation");
|
|
562
|
+
yield* failpoint("upgrade:before-version");
|
|
563
|
+
yield* createWorkerStops;
|
|
564
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
565
|
+
yield* failpoint("upgrade:after-version");
|
|
566
|
+
})).pipe(Effect.mapError((error) => DoStorageError.make({
|
|
567
|
+
operation: "upgrade native indexes",
|
|
568
|
+
message: error.message,
|
|
569
|
+
cause: error
|
|
570
|
+
})));
|
|
571
|
+
else if (version.value === "7") yield* sql.withTransaction(Effect.gen(function* () {
|
|
572
|
+
const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
573
|
+
if (current[0]?.value === "9") return;
|
|
574
|
+
if (current[0]?.value !== "7") return yield* DoStorageCompatibilityError.make({
|
|
575
|
+
actualVersion: -1,
|
|
576
|
+
supportedVersion: 9,
|
|
577
|
+
message: "Storage version changed during worker stop upgrade"
|
|
578
|
+
});
|
|
579
|
+
yield* checkPredecessorLayout(7);
|
|
580
|
+
yield* verifyWorkerPredecessor(false);
|
|
581
|
+
yield* failpoint("upgrade:before-mutation");
|
|
582
|
+
yield* createWorkerStops;
|
|
583
|
+
yield* failpoint("upgrade:after-mutation");
|
|
584
|
+
yield* failpoint("upgrade:before-version");
|
|
585
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
586
|
+
yield* failpoint("upgrade:after-version");
|
|
587
|
+
})).pipe(Effect.mapError((cause) => DoStorageError.make({
|
|
588
|
+
operation: "upgrade worker stop",
|
|
589
|
+
message: "Worker stop upgrade failed",
|
|
590
|
+
cause
|
|
591
|
+
})));
|
|
592
|
+
else if (version.value === "8") yield* sql.withTransaction(Effect.gen(function* () {
|
|
593
|
+
const current = yield* sql`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
594
|
+
if (current[0]?.value === "9") return;
|
|
595
|
+
if (current[0]?.value !== "8") return yield* DoStorageCompatibilityError.make({
|
|
596
|
+
actualVersion: -1,
|
|
597
|
+
supportedVersion: 9,
|
|
598
|
+
message: "Storage version changed during assignment seal upgrade"
|
|
599
|
+
});
|
|
600
|
+
yield* checkPredecessorLayout(7);
|
|
601
|
+
yield* verifyWorkerPredecessor(true);
|
|
602
|
+
const columns = yield* sql`PRAGMA table_info(effect_agent_worker_stops)`;
|
|
603
|
+
yield* Schema.decodeUnknownEffect(Schema.Tuple([Schema.Struct({
|
|
604
|
+
cid: Schema.Literal(0),
|
|
605
|
+
name: Schema.Literal("thread_id"),
|
|
606
|
+
type: Schema.Literal("TEXT"),
|
|
607
|
+
notnull: Schema.Literal(1),
|
|
608
|
+
dflt_value: Schema.Null,
|
|
609
|
+
pk: Schema.Literal(1)
|
|
610
|
+
})]))(columns).pipe(Effect.mapError(() => DoStorageCompatibilityError.make({
|
|
611
|
+
actualVersion: 8,
|
|
612
|
+
supportedVersion: 9,
|
|
613
|
+
message: "Unsupported worker seal layout; no upgrade was committed"
|
|
614
|
+
})));
|
|
615
|
+
yield* failpoint("upgrade:before-mutation");
|
|
616
|
+
yield* sql`ALTER TABLE effect_agent_worker_stops ADD COLUMN terminal TEXT`;
|
|
617
|
+
yield* failpoint("upgrade:after-mutation");
|
|
618
|
+
yield* failpoint("upgrade:before-version");
|
|
619
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
620
|
+
yield* failpoint("upgrade:after-version");
|
|
621
|
+
})).pipe(Effect.mapError((cause) => DoStorageError.make({
|
|
622
|
+
operation: "upgrade assignment seals",
|
|
623
|
+
message: "Assignment seal upgrade failed",
|
|
624
|
+
cause
|
|
625
|
+
})));
|
|
626
|
+
else if (version.value !== String(9)) {
|
|
627
|
+
const actualVersion = Number.parseInt(version.value, 10);
|
|
628
|
+
return yield* DoStorageCompatibilityError.make({
|
|
629
|
+
actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
|
|
630
|
+
supportedVersion: 9,
|
|
631
|
+
message: `The Durable Object uses unsupported storage version ${version.value}; this build supports exactly version 9. Only supported v2, v3, v4, v5, v6, v7 and v8 can be upgraded automatically. Keep the original store and use a compatible library version.`
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
yield* verifyWorkerPredecessor(true);
|
|
636
|
+
const state = yield* ownedState(sql);
|
|
637
|
+
const owner = (yield* SqlStorageOwner) ?? state;
|
|
638
|
+
let journal;
|
|
639
|
+
const lifecycle = yield* makeSqlLifecyclePublication(void 0, maxStoredValueBytes).pipe(Effect.provideService(SqlStorageOwner, owner), Effect.provideService(SqlLifecycleSource, {
|
|
640
|
+
beforeRetain: (threadId) => Effect.suspend(() => journal === void 0 ? Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })) : journal.flushCanonical(threadId)),
|
|
641
|
+
beforePending: Effect.suspend(() => journal === void 0 ? Effect.fail(LifecyclePublicationError.make({ reason: "unavailable" })) : journal.flushPublications())
|
|
642
|
+
}), Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((cause) => DoStorageError.make({
|
|
643
|
+
operation: "initialize lifecycle publication",
|
|
644
|
+
message: "Native publication storage unavailable",
|
|
645
|
+
cause
|
|
646
|
+
})));
|
|
647
|
+
journal = yield* makeJournal(sql, failpoint, maxStoredValueBytes, lifecycle, state).pipe(Effect.provideService(SqlStorageOwner, owner));
|
|
648
|
+
if (lifecycle !== void 0) yield* journal.initializeLifecycleSource();
|
|
649
|
+
return journal;
|
|
650
|
+
});
|
|
651
|
+
const makeJournal = (sql, failpoint, maxStoredValueBytes, lifecycle, state) => Effect.gen(function* () {
|
|
652
|
+
const owner = (yield* SqlStorageOwner) ?? state;
|
|
653
|
+
const progress = yield* SqlStorageProgress;
|
|
654
|
+
const threads = threadRows(state, sql);
|
|
655
|
+
const recovery = recoveryRows(state, sql);
|
|
656
|
+
const cursors = lifecycleCursorRows(state, sql);
|
|
657
|
+
let records = recordCaches.get(state);
|
|
658
|
+
if (records === void 0) {
|
|
659
|
+
records = makeRecordCache();
|
|
660
|
+
recordCaches.set(state, records);
|
|
661
|
+
state.invalidators.add(records.clear);
|
|
662
|
+
}
|
|
663
|
+
const recordCache = records;
|
|
664
|
+
/** Typed pre-write refusal for any single value over the configured byte bound. */
|
|
665
|
+
const checkValueBound = (operation, value) => {
|
|
666
|
+
const actualBytes = storedTextBytes(value);
|
|
667
|
+
return actualBytes > maxStoredValueBytes ? Effect.fail(DoValueBoundExceeded.make({
|
|
668
|
+
actualBytes,
|
|
669
|
+
maxBytes: maxStoredValueBytes,
|
|
670
|
+
operation
|
|
671
|
+
})) : Effect.void;
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* Runs one journal write transaction on the Durable Object storage-backed
|
|
675
|
+
* `withTransaction` (`ctx.storage.transaction()` under the hood). Within one Durable
|
|
676
|
+
* Object there is exactly ONE writer, so the Node `BEGIN IMMEDIATE` + busy-retry +
|
|
677
|
+
* `SqliteWriteContention` machinery has no analogue here and is deliberately absent.
|
|
678
|
+
* Ownership-token and epoch checks still run INSIDE the transaction, so fencing atomicity
|
|
679
|
+
* (DUR-006) is preserved identically.
|
|
680
|
+
*
|
|
681
|
+
* Journal write transactions are always top level: the Durable Object client rejects
|
|
682
|
+
* nested transactions, so new journal operations must not wrap this helper inside another
|
|
683
|
+
* transaction. Expected outcomes may be captured only when every such refusal precedes
|
|
684
|
+
* mutation; real faults still roll back. Successful preflight reads never authorize a write.
|
|
685
|
+
*/
|
|
686
|
+
const withWriteTransaction = (operation, expected = () => false) => (effect) => owner.transaction(storageResult(effect, expected)).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((error) => isSqlError(error) ? storageError(operation)(error) : error), Effect.tapError(annotateStorageError), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }), Effect.flatMap(Effect.fromResult));
|
|
687
|
+
const materialize = Effect.fnUntraced(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
|
|
688
|
+
if (threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
|
|
689
|
+
operation: "materialize thread",
|
|
690
|
+
message: "Thread identity exceeds the Durable Object storage bounds."
|
|
691
|
+
});
|
|
692
|
+
yield* checkValueBound("materialize thread", emptyTailDigest);
|
|
693
|
+
const current = yield* getThread(threadId);
|
|
694
|
+
if (current.length === 1 && producerEpoch <= current[0].producer_epoch) {
|
|
695
|
+
if (producerEpoch === current[0].producer_epoch) return;
|
|
696
|
+
return yield* DoFenceRejected.make({
|
|
697
|
+
producerEpoch,
|
|
698
|
+
actualEpoch: current[0].producer_epoch,
|
|
699
|
+
message: "The materialization producer epoch is stale."
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
yield* withWriteTransaction("materialize transaction", (error) => error._tag === "DoFenceRejected")(Effect.gen(function* () {
|
|
703
|
+
const existingRows = yield* getThread(threadId);
|
|
704
|
+
const existing = yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
|
|
705
|
+
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
706
|
+
table: "effect_agent_threads",
|
|
707
|
+
rowKey: threadId,
|
|
708
|
+
message: "A thread primary key returned more than one row."
|
|
709
|
+
});
|
|
710
|
+
if (existing.length === 0) {
|
|
711
|
+
yield* sql`
|
|
712
|
+
INSERT INTO effect_agent_threads (
|
|
713
|
+
thread_id,
|
|
714
|
+
created_at,
|
|
715
|
+
tail_sequence,
|
|
716
|
+
tail_digest,
|
|
717
|
+
producer_epoch
|
|
718
|
+
) VALUES (
|
|
719
|
+
${threadId},
|
|
720
|
+
${createdAt},
|
|
721
|
+
0,
|
|
722
|
+
${emptyTailDigest},
|
|
723
|
+
${producerEpoch}
|
|
724
|
+
)
|
|
725
|
+
RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("materialize thread")(error) : error));
|
|
726
|
+
if (lifecycle !== void 0) yield* sql`INSERT INTO effect_agent_lifecycle_cursors (thread_id, through_sequence)
|
|
727
|
+
VALUES (${threadId}, 0) RETURNING *`.pipe(cursors.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("initialize lifecycle source")(error) : error));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (producerEpoch < existing[0].producer_epoch) return yield* DoFenceRejected.make({
|
|
731
|
+
producerEpoch,
|
|
732
|
+
actualEpoch: existing[0].producer_epoch,
|
|
733
|
+
message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
|
|
734
|
+
});
|
|
735
|
+
if (producerEpoch > existing[0].producer_epoch) yield* sql`
|
|
736
|
+
UPDATE effect_agent_threads
|
|
737
|
+
SET producer_epoch = ${producerEpoch}
|
|
738
|
+
WHERE thread_id = ${threadId}
|
|
739
|
+
RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("advance materialization epoch")(error) : error));
|
|
740
|
+
}));
|
|
741
|
+
}, withStorageSpan("DoJournal.materialize", (error) => error._tag === "DoFenceRejected"));
|
|
742
|
+
const getThread = Effect.fn("DoJournal.getThread")(function* (threadId) {
|
|
743
|
+
return (yield* threads.by("thread_id", threadId).pipe(Effect.mapError((error) => error._tag === "SqlError" ? storageError("read thread")(error) : error))).filter((row) => row.thread_id === threadId);
|
|
744
|
+
});
|
|
745
|
+
const hasRecord = Effect.fnUntraced(function* (threadId, recordId) {
|
|
746
|
+
const thread = (yield* getThread(threadId))[0];
|
|
747
|
+
const prefix = recordCache.prefix(threadId, thread?.tail_sequence ?? 0);
|
|
748
|
+
if (prefix !== void 0) return prefix.some((row) => row.record_id === recordId);
|
|
749
|
+
return (yield* sql`SELECT record_id FROM effect_agent_canonical_records
|
|
750
|
+
WHERE thread_id = ${threadId} AND record_id = ${recordId}`.pipe(Effect.mapError(storageError("read canonical record identity")))).length > 0;
|
|
751
|
+
});
|
|
752
|
+
const append = Effect.fnUntraced(function* (request, observed) {
|
|
753
|
+
if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH)) return yield* DoStorageError.make({
|
|
754
|
+
operation: "append canonical batch",
|
|
755
|
+
message: "Canonical identifiers exceed the Durable Object storage bounds."
|
|
756
|
+
});
|
|
757
|
+
yield* checkValueBound("append canonical batch", request.batchJson);
|
|
758
|
+
yield* checkValueBound("append canonical batch", request.batchDigest);
|
|
759
|
+
yield* checkValueBound("append canonical batch", request.tailDigest);
|
|
760
|
+
yield* Effect.forEach(request.records, (record) => checkValueBound("append canonical record", record.recordJson), { discard: true });
|
|
761
|
+
const recordIds = request.records.map((record) => record.recordId);
|
|
762
|
+
if (new Set(recordIds).size !== recordIds.length) return yield* DoAppendConflict.make({
|
|
763
|
+
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
764
|
+
reason: "record-identity"
|
|
765
|
+
});
|
|
766
|
+
if (request.producerEpoch !== observed.producer_epoch) return yield* DoFenceRejected.make({
|
|
767
|
+
producerEpoch: request.producerEpoch,
|
|
768
|
+
actualEpoch: observed.producer_epoch,
|
|
769
|
+
message: "The append producer epoch is not current."
|
|
770
|
+
});
|
|
771
|
+
if (request.expectedTailSequence !== observed.tail_sequence || request.expectedTailDigest !== observed.tail_digest) {
|
|
772
|
+
if ((yield* sql`
|
|
773
|
+
SELECT batch_id FROM effect_agent_canonical_batches
|
|
774
|
+
WHERE thread_id = ${request.threadId} AND batch_id = ${request.batchId}
|
|
775
|
+
`.pipe(Effect.mapError(storageError("preflight append replay")))).length === 0) return yield* DoAppendConflict.make({
|
|
776
|
+
reason: "tail",
|
|
777
|
+
message: "The canonical tail has advanced.",
|
|
778
|
+
actualTailSequence: observed.tail_sequence,
|
|
779
|
+
actualTailDigest: observed.tail_digest
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return yield* withWriteTransaction("append transaction", isAppendContention)(Effect.gen(function* () {
|
|
783
|
+
const threadRows = yield* getThread(request.threadId);
|
|
784
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
|
|
785
|
+
if (request.producerEpoch !== thread.producer_epoch) return yield* DoFenceRejected.make({
|
|
786
|
+
producerEpoch: request.producerEpoch,
|
|
787
|
+
actualEpoch: thread.producer_epoch,
|
|
788
|
+
message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
|
|
789
|
+
});
|
|
790
|
+
const prefix = recordCache.prefix(request.threadId, thread.tail_sequence);
|
|
791
|
+
const batchRows = prefix !== void 0 && !prefix.some((row) => row.batch_id === request.batchId) ? [] : yield* sql`
|
|
792
|
+
SELECT
|
|
793
|
+
thread_id,
|
|
794
|
+
batch_id,
|
|
795
|
+
first_sequence,
|
|
796
|
+
last_sequence,
|
|
797
|
+
batch_digest,
|
|
798
|
+
tail_digest,
|
|
799
|
+
batch_json
|
|
800
|
+
FROM effect_agent_canonical_batches
|
|
801
|
+
WHERE thread_id = ${request.threadId}
|
|
802
|
+
AND batch_id = ${request.batchId}
|
|
803
|
+
`.pipe(Effect.mapError(storageError("read idempotent batch")));
|
|
804
|
+
const batches = yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
|
|
805
|
+
if (batches.length > 1) return yield* DoStorageCorruptionError.make({
|
|
806
|
+
table: "effect_agent_canonical_batches",
|
|
807
|
+
rowKey: `${request.threadId}/${request.batchId}`,
|
|
808
|
+
message: "A canonical batch primary key returned more than one row."
|
|
809
|
+
});
|
|
810
|
+
if (batches.length === 1) {
|
|
811
|
+
const existing = batches[0];
|
|
812
|
+
if (existing.batch_digest !== request.batchDigest) return yield* DoAppendConflict.make({
|
|
813
|
+
message: `Batch ${request.batchId} already exists with different canonical content.`,
|
|
814
|
+
reason: "batch-digest"
|
|
815
|
+
});
|
|
816
|
+
return RawAppendResult.make({
|
|
817
|
+
firstSequence: existing.first_sequence,
|
|
818
|
+
lastSequence: existing.last_sequence,
|
|
819
|
+
replayed: true,
|
|
820
|
+
tailDigest: existing.tail_digest
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* DoAppendConflict.make({
|
|
824
|
+
message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
|
|
825
|
+
reason: "tail",
|
|
826
|
+
actualTailSequence: thread.tail_sequence,
|
|
827
|
+
actualTailDigest: thread.tail_digest
|
|
828
|
+
});
|
|
829
|
+
if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
|
|
830
|
+
operation: "append canonical batch",
|
|
831
|
+
message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
|
|
832
|
+
});
|
|
833
|
+
const existingRecords = prefix === void 0 ? [] : prefix.filter((row) => recordIds.includes(row.record_id));
|
|
834
|
+
for (const chunk of prefix === void 0 ? chunked(recordIds, 90) : []) {
|
|
835
|
+
const existingRecordRows = yield* sql`
|
|
836
|
+
SELECT
|
|
837
|
+
thread_id,
|
|
838
|
+
sequence,
|
|
839
|
+
record_id,
|
|
840
|
+
batch_id,
|
|
841
|
+
record_json
|
|
842
|
+
FROM effect_agent_canonical_records
|
|
843
|
+
WHERE thread_id = ${request.threadId}
|
|
844
|
+
AND record_id IN ${sql.in([...chunk])}
|
|
845
|
+
ORDER BY sequence
|
|
846
|
+
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
847
|
+
existingRecords.push(...yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows));
|
|
848
|
+
}
|
|
849
|
+
if (existingRecords.length > 0) return yield* DoAppendConflict.make({
|
|
850
|
+
message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
|
|
851
|
+
reason: "record-identity"
|
|
852
|
+
});
|
|
853
|
+
const firstSequence = yield* Schema.decodeEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
|
|
854
|
+
cause: error,
|
|
855
|
+
operation: "append canonical batch",
|
|
856
|
+
message: error.message
|
|
857
|
+
})));
|
|
858
|
+
const lastSequence = yield* Schema.decodeEffect(CanonicalSequence)(firstSequence + request.records.length - 1).pipe(Effect.mapError((error) => DoStorageError.make({
|
|
859
|
+
cause: error,
|
|
860
|
+
operation: "append canonical batch",
|
|
861
|
+
message: error.message
|
|
862
|
+
})));
|
|
863
|
+
yield* sql`
|
|
864
|
+
INSERT INTO effect_agent_canonical_batches (
|
|
865
|
+
thread_id,
|
|
866
|
+
batch_id,
|
|
867
|
+
first_sequence,
|
|
868
|
+
last_sequence,
|
|
869
|
+
batch_digest,
|
|
870
|
+
tail_digest,
|
|
871
|
+
batch_json
|
|
872
|
+
) VALUES (
|
|
873
|
+
${request.threadId},
|
|
874
|
+
${request.batchId},
|
|
875
|
+
${firstSequence},
|
|
876
|
+
${lastSequence},
|
|
877
|
+
${request.batchDigest},
|
|
878
|
+
${request.tailDigest},
|
|
879
|
+
${request.batchJson}
|
|
880
|
+
)
|
|
881
|
+
`.pipe(Effect.mapError(storageError("insert canonical batch")));
|
|
882
|
+
yield* failpoint("append:after-batch-insert");
|
|
883
|
+
const records = yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
|
|
884
|
+
const canonical = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.recordJson).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
885
|
+
table: "effect_agent_canonical_records",
|
|
886
|
+
rowKey: record.recordId,
|
|
887
|
+
message: error.message
|
|
888
|
+
})));
|
|
889
|
+
return {
|
|
890
|
+
record,
|
|
891
|
+
canonical,
|
|
892
|
+
row: {
|
|
893
|
+
thread_id: request.threadId,
|
|
894
|
+
sequence: firstSequence + index,
|
|
895
|
+
record_id: record.recordId,
|
|
896
|
+
batch_id: request.batchId,
|
|
897
|
+
record_json: record.recordJson,
|
|
898
|
+
outstanding: canonicalRecordOutstanding(canonical)
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}));
|
|
902
|
+
for (const group of chunked(records, Math.floor(MAX_BOUND_PARAMETERS / 6))) {
|
|
903
|
+
yield* sql`INSERT INTO effect_agent_canonical_records ${sql.insert(group.map(({ row }) => row))}`.pipe(Effect.mapError(storageError("insert canonical records")));
|
|
904
|
+
for (const { canonical, row } of group) {
|
|
905
|
+
yield* indexCanonicalRecord(request.threadId, canonical, void 0, { sequence: row.sequence }).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError(storageError("index canonical record")));
|
|
906
|
+
recordCache.put(RecordRow.make({
|
|
907
|
+
...row,
|
|
908
|
+
sequence: Schema.decodeSync(CanonicalSequence)(row.sequence)
|
|
909
|
+
}));
|
|
910
|
+
yield* failpoint("append:after-record-insert");
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
yield* sql`
|
|
914
|
+
UPDATE effect_agent_threads
|
|
915
|
+
SET
|
|
916
|
+
tail_sequence = ${lastSequence},
|
|
917
|
+
tail_digest = ${request.tailDigest},
|
|
918
|
+
producer_epoch = ${request.producerEpoch}
|
|
919
|
+
WHERE thread_id = ${request.threadId}
|
|
920
|
+
RETURNING *`.pipe(threads.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("advance thread tail")(error) : error));
|
|
921
|
+
yield* failpoint("append:after-tail-update");
|
|
922
|
+
if (lifecycle !== void 0 && records.some(({ canonical: { payload } }) => payload._tag === "RunStarted" || payload._tag === "SubagentStarted")) yield* flushCanonical(request.threadId).pipe(Effect.provideService(SqlLifecycleRetainer, { retainMany: lifecycle.retainMany }), Effect.mapError((cause) => DoStorageError.make({
|
|
923
|
+
operation: "retain lifecycle start prefix",
|
|
924
|
+
message: "Lifecycle start intent could not be retained",
|
|
925
|
+
cause
|
|
926
|
+
})));
|
|
927
|
+
yield* progress.committed("canonical").pipe(Effect.catchCause((cause) => Effect.failCause(Cause.map(cause, (error) => DoStorageError.make({
|
|
928
|
+
operation: "enroll canonical progress",
|
|
929
|
+
message: error.message,
|
|
930
|
+
cause: error
|
|
931
|
+
})))));
|
|
932
|
+
return RawAppendResult.make({
|
|
933
|
+
firstSequence,
|
|
934
|
+
lastSequence,
|
|
935
|
+
replayed: false,
|
|
936
|
+
tailDigest: request.tailDigest
|
|
937
|
+
});
|
|
938
|
+
}));
|
|
939
|
+
}, withStorageSpan("DoJournal.append", isAppendContention));
|
|
940
|
+
const read = Effect.fnUntraced(function* (request) {
|
|
941
|
+
const thread = (yield* getThread(request.threadId))[0];
|
|
942
|
+
const through = Math.min(thread?.tail_sequence ?? 0, request.fromSequenceExclusive + request.limit);
|
|
943
|
+
const cached = [];
|
|
944
|
+
for (let sequence = request.fromSequenceExclusive + 1; sequence <= through; sequence++) {
|
|
945
|
+
const record = recordCache.get(request.threadId, sequence);
|
|
946
|
+
if (record === void 0) break;
|
|
947
|
+
cached.push(record);
|
|
948
|
+
}
|
|
949
|
+
if (cached.length === Math.max(0, through - request.fromSequenceExclusive)) return {
|
|
950
|
+
count: cached.length,
|
|
951
|
+
records: Stream.fromIterable(cached)
|
|
952
|
+
};
|
|
953
|
+
const planRows = yield* sql`
|
|
954
|
+
SELECT
|
|
955
|
+
sequence,
|
|
956
|
+
length(CAST(record_json AS BLOB)) AS record_json_bytes
|
|
957
|
+
FROM effect_agent_canonical_records
|
|
958
|
+
WHERE thread_id = ${request.threadId}
|
|
959
|
+
AND sequence > ${request.fromSequenceExclusive}
|
|
960
|
+
ORDER BY sequence
|
|
961
|
+
LIMIT ${request.limit}
|
|
962
|
+
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
963
|
+
const plan = yield* decodeRows(Schema.Array(ReadPlanRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, planRows, "ReadPlanRow");
|
|
964
|
+
const mismatch = () => DoStorageCorruptionError.make({
|
|
965
|
+
table: "effect_agent_canonical_records",
|
|
966
|
+
rowKey: `${request.threadId}>${request.fromSequenceExclusive}`,
|
|
967
|
+
message: "Canonical read membership, sequence, or payload size changed from its read plan."
|
|
968
|
+
});
|
|
969
|
+
if (plan.length > request.limit) return yield* mismatch();
|
|
970
|
+
const pages = [];
|
|
971
|
+
let page;
|
|
972
|
+
let pageBytes = 0;
|
|
973
|
+
let previousSequence = request.fromSequenceExclusive;
|
|
974
|
+
for (const row of plan) {
|
|
975
|
+
if (row.sequence !== previousSequence + 1) return yield* mismatch();
|
|
976
|
+
previousSequence = row.sequence;
|
|
977
|
+
if (page === void 0 || pageBytes + row.record_json_bytes > MAX_READ_PAGE_JSON_BYTES) {
|
|
978
|
+
page = [row];
|
|
979
|
+
pages.push(page);
|
|
980
|
+
pageBytes = row.record_json_bytes;
|
|
981
|
+
} else {
|
|
982
|
+
page.push(row);
|
|
983
|
+
pageBytes += row.record_json_bytes;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const readPage = Effect.fn("DoJournal.readPage")(function* (page) {
|
|
987
|
+
const rows = yield* sql`
|
|
988
|
+
SELECT thread_id, sequence, record_id, batch_id, record_json
|
|
989
|
+
FROM effect_agent_canonical_records
|
|
990
|
+
WHERE thread_id = ${request.threadId}
|
|
991
|
+
AND sequence >= ${page[0].sequence}
|
|
992
|
+
AND sequence <= ${page[page.length - 1].sequence}
|
|
993
|
+
ORDER BY sequence
|
|
994
|
+
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
995
|
+
const decoded = yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/${page[0].sequence}`, rows, "RecordRow");
|
|
996
|
+
if (decoded.length !== page.length || decoded.some((row, index) => row.thread_id !== request.threadId || row.sequence !== page[index].sequence || storedTextBytes(row.record_json) !== page[index].record_json_bytes)) return yield* mismatch();
|
|
997
|
+
for (const row of decoded) recordCache.put(row);
|
|
998
|
+
return decoded;
|
|
999
|
+
});
|
|
1000
|
+
return {
|
|
1001
|
+
count: plan.length,
|
|
1002
|
+
records: Stream.fromIterable(pages).pipe(Stream.flatMap((page) => Stream.fromIterableEffect(state.read(readPage(page)))))
|
|
1003
|
+
};
|
|
1004
|
+
});
|
|
1005
|
+
const exportThread = Effect.fn("DoJournal.exportThread")(function* (threadId) {
|
|
1006
|
+
return yield* state.transaction(Effect.gen(function* () {
|
|
1007
|
+
const threadRows = yield* getThread(threadId);
|
|
1008
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
|
|
1009
|
+
yield* failpoint("export:after-thread-read");
|
|
1010
|
+
if (thread.tail_sequence > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
|
|
1011
|
+
operation: "export thread",
|
|
1012
|
+
message: "The thread exceeds the current export record limit."
|
|
1013
|
+
});
|
|
1014
|
+
const records = [];
|
|
1015
|
+
let afterSequence = ZERO_SEQUENCE;
|
|
1016
|
+
while (afterSequence < thread.tail_sequence) {
|
|
1017
|
+
const limit = Math.min(1024, thread.tail_sequence - afterSequence);
|
|
1018
|
+
const request = RawReadRequest.make({
|
|
1019
|
+
threadId,
|
|
1020
|
+
fromSequenceExclusive: afterSequence,
|
|
1021
|
+
limit
|
|
1022
|
+
});
|
|
1023
|
+
const plan = yield* read(request);
|
|
1024
|
+
const page = yield* Stream.runCollect(plan.records);
|
|
1025
|
+
if (page.length !== limit || page.some((record, index) => record.sequence !== afterSequence + index + 1)) return yield* DoStorageCorruptionError.make({
|
|
1026
|
+
table: "effect_agent_canonical_records",
|
|
1027
|
+
rowKey: threadId,
|
|
1028
|
+
message: "The exported canonical prefix is not contiguous through its captured tail."
|
|
1029
|
+
});
|
|
1030
|
+
records.push(...page);
|
|
1031
|
+
afterSequence = page[page.length - 1].sequence;
|
|
1032
|
+
}
|
|
1033
|
+
if ((yield* sql`SELECT sequence FROM effect_agent_canonical_records WHERE thread_id=${threadId} AND sequence > ${thread.tail_sequence} LIMIT 1`.pipe(Effect.mapError(storageError("verify export tail")))).length !== 0) return yield* DoStorageCorruptionError.make({
|
|
1034
|
+
table: "effect_agent_canonical_records",
|
|
1035
|
+
rowKey: threadId,
|
|
1036
|
+
message: "Canonical records exist beyond the captured thread tail."
|
|
1037
|
+
});
|
|
1038
|
+
return RawThreadExport.make({
|
|
1039
|
+
thread,
|
|
1040
|
+
records
|
|
1041
|
+
});
|
|
1042
|
+
})).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.catchTag("SqlError", (error) => Effect.fail(storageError("export transaction")(error))));
|
|
1043
|
+
});
|
|
1044
|
+
const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (checkpoint) {
|
|
1045
|
+
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
|
|
1046
|
+
operation: "save checkpoint",
|
|
1047
|
+
message: "Checkpoint identity exceeds the Durable Object storage bounds."
|
|
1048
|
+
});
|
|
1049
|
+
yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
|
|
1050
|
+
yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
|
|
1051
|
+
const threadRows = yield* getThread(checkpoint.threadId);
|
|
1052
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
|
|
1053
|
+
if (checkpoint.throughSequence > thread.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
|
|
1054
|
+
const checkpointRows = yield* sql`
|
|
1055
|
+
SELECT
|
|
1056
|
+
thread_id,
|
|
1057
|
+
through_sequence,
|
|
1058
|
+
tail_digest,
|
|
1059
|
+
checkpoint_json
|
|
1060
|
+
FROM effect_agent_checkpoints
|
|
1061
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
1062
|
+
AND through_sequence = ${checkpoint.throughSequence}
|
|
1063
|
+
`.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
|
|
1064
|
+
const existing = yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
|
|
1065
|
+
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
1066
|
+
table: "effect_agent_checkpoints",
|
|
1067
|
+
rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
|
|
1068
|
+
message: "A checkpoint primary key returned more than one row."
|
|
1069
|
+
});
|
|
1070
|
+
if (existing.length === 1) {
|
|
1071
|
+
if (existing[0].tail_digest !== checkpoint.tailDigest || existing[0].checkpoint_json !== checkpoint.checkpointJson) return yield* DoCheckpointConflict.make({ message: "A different checkpoint already exists at this canonical sequence." });
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
yield* sql`
|
|
1075
|
+
INSERT INTO effect_agent_checkpoints (
|
|
1076
|
+
thread_id,
|
|
1077
|
+
through_sequence,
|
|
1078
|
+
tail_digest,
|
|
1079
|
+
checkpoint_json
|
|
1080
|
+
) VALUES (
|
|
1081
|
+
${checkpoint.threadId},
|
|
1082
|
+
${checkpoint.throughSequence},
|
|
1083
|
+
${checkpoint.tailDigest},
|
|
1084
|
+
${checkpoint.checkpointJson}
|
|
1085
|
+
)
|
|
1086
|
+
`.pipe(Effect.mapError(storageError("insert checkpoint")));
|
|
1087
|
+
}));
|
|
1088
|
+
});
|
|
1089
|
+
const saveRecoveryCheckpoint = Effect.fn("DoJournal.saveRecoveryCheckpoint")(function* (request, checkpointJson) {
|
|
1090
|
+
const { checkpoint } = request;
|
|
1091
|
+
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH) return yield* DoStorageError.make({
|
|
1092
|
+
operation: "save recovery checkpoint",
|
|
1093
|
+
message: "Checkpoint identity exceeds the Durable Object storage bounds."
|
|
1094
|
+
});
|
|
1095
|
+
yield* checkValueBound("save recovery checkpoint", checkpointJson);
|
|
1096
|
+
yield* failpoint("save-recovery-checkpoint:before");
|
|
1097
|
+
yield* withWriteTransaction("recovery checkpoint transaction")(Effect.gen(function* () {
|
|
1098
|
+
const thread = (yield* getThread(checkpoint.threadId))[0];
|
|
1099
|
+
if (thread === void 0) return yield* ThreadNotMaterialized.make({ threadId: checkpoint.threadId });
|
|
1100
|
+
if (request.producerEpoch !== thread.producer_epoch) return yield* FenceRejected.make({
|
|
1101
|
+
threadId: checkpoint.threadId,
|
|
1102
|
+
actualEpoch: thread.producer_epoch,
|
|
1103
|
+
attemptedEpoch: request.producerEpoch
|
|
1104
|
+
});
|
|
1105
|
+
if (checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
|
|
1106
|
+
threadId: checkpoint.threadId,
|
|
1107
|
+
reason: "ahead-of-tail"
|
|
1108
|
+
});
|
|
1109
|
+
const digests = checkpoint.throughSequence === 0 ? [EMPTY_TAIL_DIGEST] : yield* getTailDigestAt(checkpoint.threadId, checkpoint.throughSequence);
|
|
1110
|
+
if (digests.length !== 1 || digests[0] !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
|
|
1111
|
+
threadId: checkpoint.threadId,
|
|
1112
|
+
reason: "digest-mismatch"
|
|
1113
|
+
});
|
|
1114
|
+
yield* sql`
|
|
1115
|
+
INSERT INTO effect_agent_recovery_checkpoints (thread_id, through_sequence, tail_digest, checkpoint_json)
|
|
1116
|
+
VALUES (${checkpoint.threadId}, ${checkpoint.throughSequence}, ${checkpoint.tailDigest}, ${checkpointJson})
|
|
1117
|
+
ON CONFLICT (thread_id) DO UPDATE SET
|
|
1118
|
+
through_sequence = excluded.through_sequence,
|
|
1119
|
+
tail_digest = excluded.tail_digest,
|
|
1120
|
+
checkpoint_json = excluded.checkpoint_json
|
|
1121
|
+
WHERE excluded.through_sequence >= effect_agent_recovery_checkpoints.through_sequence
|
|
1122
|
+
RETURNING *
|
|
1123
|
+
`.pipe(recovery.write, Effect.mapError((error) => error._tag === "SqlError" ? storageError("save recovery checkpoint")(error) : error));
|
|
1124
|
+
}));
|
|
1125
|
+
yield* failpoint("save-recovery-checkpoint:after");
|
|
1126
|
+
});
|
|
1127
|
+
const loadRecoveryCheckpoint = Effect.fn("DoJournal.loadRecoveryCheckpoint")(function* (threadId) {
|
|
1128
|
+
return yield* recovery.by("thread_id", threadId).pipe(Effect.mapError((error) => error._tag === "SqlError" ? storageError("load recovery checkpoint")(error) : error));
|
|
1129
|
+
});
|
|
1130
|
+
const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
|
|
1131
|
+
const rows = yield* sql`
|
|
1132
|
+
SELECT
|
|
1133
|
+
thread_id,
|
|
1134
|
+
through_sequence,
|
|
1135
|
+
tail_digest,
|
|
1136
|
+
checkpoint_json
|
|
1137
|
+
FROM effect_agent_checkpoints
|
|
1138
|
+
WHERE thread_id = ${threadId}
|
|
1139
|
+
AND through_sequence <= ${atOrBeforeSequence}
|
|
1140
|
+
ORDER BY through_sequence DESC
|
|
1141
|
+
LIMIT 1
|
|
1142
|
+
`.pipe(Effect.mapError(storageError("load checkpoint")));
|
|
1143
|
+
return yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
|
|
1144
|
+
});
|
|
1145
|
+
const getTailDigestAt = Effect.fn("DoJournal.getTailDigestAt")(function* (threadId, sequence) {
|
|
1146
|
+
const thread = (yield* getThread(threadId))[0];
|
|
1147
|
+
if (thread !== void 0 && sequence === thread.tail_sequence) return [thread.tail_digest];
|
|
1148
|
+
if (sequence === 0) {
|
|
1149
|
+
const threads = yield* getThread(threadId);
|
|
1150
|
+
return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
|
|
1151
|
+
}
|
|
1152
|
+
const rows = yield* sql`
|
|
1153
|
+
SELECT
|
|
1154
|
+
thread_id,
|
|
1155
|
+
batch_id,
|
|
1156
|
+
first_sequence,
|
|
1157
|
+
last_sequence,
|
|
1158
|
+
batch_digest,
|
|
1159
|
+
tail_digest,
|
|
1160
|
+
batch_json
|
|
1161
|
+
FROM effect_agent_canonical_batches
|
|
1162
|
+
WHERE thread_id = ${threadId}
|
|
1163
|
+
AND batch_id = (
|
|
1164
|
+
SELECT batch_id FROM effect_agent_canonical_records
|
|
1165
|
+
WHERE thread_id = ${threadId} AND sequence = ${sequence}
|
|
1166
|
+
)
|
|
1167
|
+
AND last_sequence = ${sequence}
|
|
1168
|
+
`.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
|
|
1169
|
+
return (yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
|
|
1170
|
+
});
|
|
1171
|
+
const scanStoredPayloads = Effect.fn("DoJournal.scanStoredPayloads")(function* () {
|
|
1172
|
+
return yield* state.transaction(Effect.gen(function* () {
|
|
1173
|
+
const threads = yield* sql`
|
|
1174
|
+
SELECT
|
|
1175
|
+
thread_id,
|
|
1176
|
+
created_at,
|
|
1177
|
+
tail_sequence,
|
|
1178
|
+
tail_digest,
|
|
1179
|
+
producer_epoch
|
|
1180
|
+
FROM effect_agent_threads
|
|
1181
|
+
ORDER BY thread_id
|
|
1182
|
+
`.pipe(Effect.mapError(storageError("scan threads")));
|
|
1183
|
+
const batches = yield* sql`
|
|
1184
|
+
SELECT
|
|
1185
|
+
thread_id,
|
|
1186
|
+
batch_id,
|
|
1187
|
+
first_sequence,
|
|
1188
|
+
last_sequence,
|
|
1189
|
+
batch_digest,
|
|
1190
|
+
tail_digest,
|
|
1191
|
+
batch_json
|
|
1192
|
+
FROM effect_agent_canonical_batches
|
|
1193
|
+
ORDER BY thread_id, first_sequence
|
|
1194
|
+
`.pipe(Effect.mapError(storageError("scan canonical batches")));
|
|
1195
|
+
const records = yield* sql`
|
|
1196
|
+
SELECT
|
|
1197
|
+
thread_id,
|
|
1198
|
+
sequence,
|
|
1199
|
+
record_id,
|
|
1200
|
+
batch_id,
|
|
1201
|
+
record_json
|
|
1202
|
+
FROM effect_agent_canonical_records
|
|
1203
|
+
ORDER BY thread_id, sequence
|
|
1204
|
+
`.pipe(Effect.mapError(storageError("scan canonical records")));
|
|
1205
|
+
const checkpoints = yield* sql`
|
|
1206
|
+
SELECT
|
|
1207
|
+
thread_id,
|
|
1208
|
+
through_sequence,
|
|
1209
|
+
tail_digest,
|
|
1210
|
+
checkpoint_json
|
|
1211
|
+
FROM effect_agent_checkpoints
|
|
1212
|
+
ORDER BY thread_id, through_sequence
|
|
1213
|
+
`.pipe(Effect.mapError(storageError("scan checkpoints")));
|
|
1214
|
+
return {
|
|
1215
|
+
threads: yield* decodeRows(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
|
|
1216
|
+
batches: yield* decodeRows(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
|
|
1217
|
+
records: yield* decodeRows(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
|
|
1218
|
+
checkpoints: yield* decodeRows(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
|
|
1219
|
+
};
|
|
1220
|
+
})).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.catchTag("SqlError", (error) => Effect.fail(storageError("startup scan transaction")(error))));
|
|
1221
|
+
});
|
|
1222
|
+
const sourceFailure = (cause) => LifecyclePublicationError.make({
|
|
1223
|
+
reason: "unavailable",
|
|
1224
|
+
cause
|
|
1225
|
+
});
|
|
1226
|
+
const sourceThreads = Effect.fnUntraced(function* () {
|
|
1227
|
+
const all = yield* threads.matching("lifecycle-owners", () => true, sql`SELECT * FROM effect_agent_threads LIMIT 129`, 128);
|
|
1228
|
+
if (all.length <= 128) return all;
|
|
1229
|
+
return yield* Schema.decodeUnknownEffect(Schema.Array(ThreadRow))(yield* sql`SELECT t.* FROM effect_agent_threads t LEFT JOIN effect_agent_lifecycle_cursors c ON c.thread_id=t.thread_id
|
|
1230
|
+
WHERE c.thread_id IS NULL OR t.tail_sequence > c.through_sequence ORDER BY t.thread_id LIMIT 128`);
|
|
1231
|
+
});
|
|
1232
|
+
const initializeLifecycleSource = Effect.fnUntraced(function* () {
|
|
1233
|
+
if (initializedLifecycleSources.has(state)) return;
|
|
1234
|
+
yield* state.transaction(Effect.gen(function* () {
|
|
1235
|
+
if ((yield* sql`SELECT name FROM sqlite_master WHERE name='effect_agent_lifecycle_cursors'`).length === 0) yield* sql`CREATE TABLE effect_agent_lifecycle_cursors (
|
|
1236
|
+
thread_id TEXT PRIMARY KEY, through_sequence INTEGER NOT NULL
|
|
1237
|
+
)`;
|
|
1238
|
+
yield* sql`INSERT INTO effect_agent_lifecycle_cursors(thread_id, through_sequence)
|
|
1239
|
+
SELECT thread_id, tail_sequence FROM effect_agent_threads WHERE true
|
|
1240
|
+
ON CONFLICT(thread_id) DO NOTHING`;
|
|
1241
|
+
yield* sourceThreads();
|
|
1242
|
+
})).pipe(Effect.provideService(SqlClient$1.SqlClient, sql), Effect.mapError((cause) => DoStorageError.make({
|
|
1243
|
+
operation: "initialize lifecycle source",
|
|
1244
|
+
message: "Lifecycle source cursor unavailable",
|
|
1245
|
+
cause
|
|
1246
|
+
})));
|
|
1247
|
+
initializedLifecycleSources.add(state);
|
|
1248
|
+
});
|
|
1249
|
+
/** The journal is durable intent; only the bounded publication wave materializes a write set. */
|
|
1250
|
+
const flushCanonical = Effect.fnUntraced(function* (threadId, maxRecords = Infinity) {
|
|
1251
|
+
const { retainMany } = yield* SqlLifecycleRetainer;
|
|
1252
|
+
const thread = (yield* getThread(threadId))[0];
|
|
1253
|
+
if (thread === void 0) return;
|
|
1254
|
+
const cursor = (yield* cursors.by("thread_id", threadId))[0];
|
|
1255
|
+
if (cursor === void 0 || cursor.through_sequence > thread.tail_sequence) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
1256
|
+
let through = cursor.through_sequence;
|
|
1257
|
+
const bound = Math.min(thread.tail_sequence, through + maxRecords);
|
|
1258
|
+
while (through < bound) {
|
|
1259
|
+
const page = yield* read(RawReadRequest.make({
|
|
1260
|
+
threadId,
|
|
1261
|
+
fromSequenceExclusive: through,
|
|
1262
|
+
limit: Math.min(128, bound - through)
|
|
1263
|
+
}));
|
|
1264
|
+
const facts = [];
|
|
1265
|
+
let bytes = 0;
|
|
1266
|
+
yield* Stream.runForEach(page.records, (row) => Effect.gen(function* () {
|
|
1267
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json);
|
|
1268
|
+
if (record.recordId !== row.record_id || row.sequence !== through + 1) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
1269
|
+
if (Schema.is(Schema.toType(LifecyclePublicationFact))(record.payload)) {
|
|
1270
|
+
if (bytes + storedTextBytes(row.record_json) > MAX_READ_PAGE_JSON_BYTES && facts.length > 0) {
|
|
1271
|
+
yield* retainMany(facts);
|
|
1272
|
+
facts.length = 0;
|
|
1273
|
+
bytes = 0;
|
|
1274
|
+
}
|
|
1275
|
+
facts.push({
|
|
1276
|
+
id: JSON.stringify([
|
|
1277
|
+
threadId,
|
|
1278
|
+
"record",
|
|
1279
|
+
record.recordId
|
|
1280
|
+
]),
|
|
1281
|
+
ownerThreadId: yield* Schema.decodeEffect(ThreadId)(threadId),
|
|
1282
|
+
canonicalSequence: row.sequence,
|
|
1283
|
+
createdAt: record.createdAt,
|
|
1284
|
+
fact: record.payload
|
|
1285
|
+
});
|
|
1286
|
+
bytes += storedTextBytes(row.record_json);
|
|
1287
|
+
}
|
|
1288
|
+
through = row.sequence;
|
|
1289
|
+
}));
|
|
1290
|
+
if (page.count === 0) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
1291
|
+
yield* retainMany(facts);
|
|
1292
|
+
yield* sql`UPDATE effect_agent_lifecycle_cursors SET through_sequence=${through} WHERE thread_id=${threadId} RETURNING *`.pipe(cursors.write);
|
|
1293
|
+
}
|
|
1294
|
+
}, Effect.mapError(sourceFailure));
|
|
1295
|
+
const flushPublications = Effect.fnUntraced(function* () {
|
|
1296
|
+
let owners = 0;
|
|
1297
|
+
for (const thread of yield* sourceThreads()) {
|
|
1298
|
+
const cursor = (yield* cursors.by("thread_id", thread.thread_id))[0];
|
|
1299
|
+
if (cursor !== void 0 && cursor.through_sequence === thread.tail_sequence) continue;
|
|
1300
|
+
yield* flushCanonical(thread.thread_id, 1024);
|
|
1301
|
+
if (++owners === 4) break;
|
|
1302
|
+
}
|
|
1303
|
+
}, Effect.mapError(sourceFailure));
|
|
1304
|
+
const sourcePending = Effect.gen(function* () {
|
|
1305
|
+
for (const thread of yield* sourceThreads()) {
|
|
1306
|
+
const cursor = (yield* cursors.by("thread_id", thread.thread_id))[0];
|
|
1307
|
+
if (cursor === void 0 || cursor.through_sequence > thread.tail_sequence) return yield* LifecyclePublicationError.make({ reason: "corrupt" });
|
|
1308
|
+
if (cursor.through_sequence < thread.tail_sequence) return true;
|
|
1309
|
+
}
|
|
1310
|
+
return false;
|
|
1311
|
+
}).pipe(Effect.mapError(sourceFailure));
|
|
1312
|
+
return {
|
|
1313
|
+
state,
|
|
1314
|
+
owner,
|
|
1315
|
+
threads,
|
|
1316
|
+
lifecycle: lifecycle === void 0 ? void 0 : {
|
|
1317
|
+
...lifecycle,
|
|
1318
|
+
storage: {
|
|
1319
|
+
...lifecycle.storage,
|
|
1320
|
+
retainedPendingDeadline: lifecycle.storage.pendingDeadline,
|
|
1321
|
+
pendingDeadline: state.read(Effect.gen(function* () {
|
|
1322
|
+
const deadline = yield* lifecycle.storage.pendingDeadline;
|
|
1323
|
+
if (!(yield* sourcePending)) return deadline;
|
|
1324
|
+
const now = yield* Clock.currentTimeMillis;
|
|
1325
|
+
return Option.some(Option.isSome(deadline) ? Math.min(now, deadline.value) : now);
|
|
1326
|
+
}))
|
|
1327
|
+
}
|
|
1328
|
+
},
|
|
1329
|
+
initializeLifecycleSource,
|
|
1330
|
+
flushCanonical,
|
|
1331
|
+
flushPublications,
|
|
1332
|
+
append,
|
|
1333
|
+
checkValueBound,
|
|
1334
|
+
exportThread,
|
|
1335
|
+
getThread,
|
|
1336
|
+
hasRecord,
|
|
1337
|
+
getTailDigestAt,
|
|
1338
|
+
loadCheckpoint,
|
|
1339
|
+
loadRecoveryCheckpoint,
|
|
1340
|
+
saveRecoveryCheckpoint,
|
|
1341
|
+
materialize,
|
|
1342
|
+
read: (request) => state.read(read(request)),
|
|
1343
|
+
saveCheckpoint,
|
|
1344
|
+
scanStoredPayloads,
|
|
1345
|
+
withWriteTransaction
|
|
1346
|
+
};
|
|
1347
|
+
});
|
|
1348
|
+
const initializeDoJournal = ensureCurrentStorage;
|
|
1349
|
+
//#endregion
|
|
1350
|
+
export { initializeDoJournal as a, decodeRows as i, RawCheckpoint as n, isAppendContention as o, RawReadRequest as r, withStorageSpan as s, RawAppendRequest as t };
|
|
1351
|
+
|
|
1352
|
+
//# sourceMappingURL=do-journal-DUhjZkOf.mjs.map
|