@effect-agent/storage-cloudflare 0.1.0-beta.37 → 0.1.0-beta.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/do-storage-failpoint-9XB9tuif.d.mts +107 -0
- package/dist/do-storage-failpoint-C-Qtchs7.mjs +12 -0
- package/dist/do-storage-failpoint-C-Qtchs7.mjs.map +1 -0
- package/dist/index.d.mts +160 -242
- package/dist/index.mjs +1199 -522
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +33 -0
- package/dist/testing.mjs +35 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +12 -5
- package/src/do-journal.ts +148 -157
- package/src/do-ledger.ts +66 -66
- package/src/do-schedule-store.ts +7 -3
- package/src/do-storage-failpoint-testing.ts +73 -0
- package/src/do-storage-failpoint.ts +1 -68
- package/src/do-subscription-store.ts +1122 -0
- package/src/{do-conversation-store.ts → do-thread-store.ts} +313 -334
- package/src/errors.ts +3 -3
- package/src/index.ts +9 -8
- package/src/migrations.ts +26 -26
- package/src/port-protocol.ts +29 -29
- package/src/routing.ts +146 -188
- package/src/testing.ts +2 -0
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as DoStorageFailpoint } from "./do-storage-failpoint-C-Qtchs7.mjs";
|
|
2
|
+
import { AbortCommand, AbortIntent, AcceptedEvent, AdmissionAdmitted, AdmissionConflict, AdmissionIndeterminate, AdmissionNotAdmitted, AdmissionRequest, AdmissionResolution, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecision, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalBatch, CanonicalRecord, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildReservationStatus, ChildSettledNotification, ChildSettledOutcome, Claim, ClaimJoiningRequest, ClaimRequest, DEFAULT_OWNERSHIP_LEASE_DURATION, DefinitionDigests, DeliveryChange, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, ParentLinkage, PersistedJson, ProducerEpoch, QueueSequence, RecordEnvelope, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, ScheduleCapacityError, ScheduleChange, ScheduleConflict, ScheduleDueCursor, ScheduleFailpoint, ScheduleId, ScheduleInstant, ScheduleKey, ScheduleNotFound, ScheduleOwner, SchedulePageRequest, ScheduleRecord, ScheduleStorageError, ScheduleStore, Settlement, SettlementConflict, SettlementFinalization, SettlementOutcome, SettlementReservation, SettlementReservationSnapshot, SourcePartition, SubmissionLedger, SubmissionLookup, SubmissionLookupById, SubmissionLookupByKey, SubmissionSnapshot, SubmissionState, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionScanCursors, SubscriptionStore, SuspendRequest, SuspensionReason, SuspensionSnapshot, ThreadCheckpoint, ThreadExport, ThreadExportRequest, ThreadMaterialization, ThreadNotMaterialized, ThreadObservation, ThreadRead, ThreadStore, ThreadStoreError, ThreadTail, ThreadTailRequest, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, applyScheduleChange, applySubscriptionDeliveryChange, defaultSchedulingLimits, digestCanonicalBatch, sameAcceptedEventIdentity, sameSourcePartition, scheduleDeadline, scheduleUsesCapacity, settlementFailureFromRecord, submissionAbortRecordId, subscriptionCanSelect, subscriptionDeliveryCanSelect, subscriptionDeliveryKeyString } from "@effect-agent/thread";
|
|
2
3
|
import { Clock, Context, Crypto, DateTime, Duration, Effect, Layer, Option, Predicate, Ref, Result, Schema, Stream } from "effect";
|
|
3
4
|
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-do";
|
|
4
5
|
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
@@ -64,7 +65,7 @@ var DoAppendConflict = class extends Schema.TaggedError()("DoAppendConflict", {
|
|
|
64
65
|
actualTailDigest: Schema.optionalKey(Schema.String)
|
|
65
66
|
}) {};
|
|
66
67
|
/**
|
|
67
|
-
* A producer epoch does not match the
|
|
68
|
+
* A producer epoch does not match the Thread's current writer registration. Appends
|
|
68
69
|
* require the exact registered epoch, so both older and newer unregistered epochs are fenced;
|
|
69
70
|
* a newer epoch takes over by materializing first.
|
|
70
71
|
*/
|
|
@@ -91,7 +92,7 @@ const DoStorageFailpointLocation = Schema.Literals([
|
|
|
91
92
|
"append:after-record-insert",
|
|
92
93
|
"append:after-tail-update",
|
|
93
94
|
"append:after",
|
|
94
|
-
"export:after-
|
|
95
|
+
"export:after-thread-read",
|
|
95
96
|
"save-checkpoint:before",
|
|
96
97
|
"save-checkpoint:after",
|
|
97
98
|
"ledger:admit:before",
|
|
@@ -151,9 +152,9 @@ var DoStorageFailpointError = class extends Schema.TaggedError()("DoStorageFailp
|
|
|
151
152
|
* v1→v4 history to replay (deployment spec §9: no rolling data-version promise during
|
|
152
153
|
* private development).
|
|
153
154
|
*/
|
|
154
|
-
const CurrentDoStorageVersion =
|
|
155
|
+
const CurrentDoStorageVersion = 2;
|
|
155
156
|
/**
|
|
156
|
-
* The
|
|
157
|
+
* The Thread Durable Object schema. Table names and columns mirror the Node/SQLite v4
|
|
157
158
|
* schema byte-for-byte (`packages/storage-sqlite/src/migrations.ts`, migrations 1–4 collapsed
|
|
158
159
|
* into their final shape) so the shared conformance suites and crash-matrix rows address
|
|
159
160
|
* identical durable state. Two DC-specific additions:
|
|
@@ -162,15 +163,15 @@ const CurrentDoStorageVersion = 1;
|
|
|
162
163
|
* a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.
|
|
163
164
|
* 2. `effect_agent_child_settlements` is the durable cross-store notification marker the
|
|
164
165
|
* SubmissionLedger port contract mandates for cross-store adapters (`suspend`'s covering
|
|
165
|
-
* check and `recordChildSettled`'s wake both consult it): parent and child
|
|
166
|
+
* check and `recordChildSettled`'s wake both consult it): parent and child Threads
|
|
166
167
|
* live in different Durable Objects, so a child settlement reported before the parent's
|
|
167
168
|
* suspend commits must be observable from the PARENT's own storage.
|
|
168
169
|
*/
|
|
169
|
-
const doMigrations = SqliteMigrator.fromRecord({ "
|
|
170
|
+
const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_thread_object": Effect.gen(function* () {
|
|
170
171
|
const sql = yield* SqlClient.SqlClient;
|
|
171
172
|
yield* sql`
|
|
172
|
-
CREATE TABLE
|
|
173
|
-
|
|
173
|
+
CREATE TABLE effect_agent_threads (
|
|
174
|
+
thread_id TEXT PRIMARY KEY NOT NULL,
|
|
174
175
|
created_at TEXT NOT NULL,
|
|
175
176
|
tail_sequence INTEGER NOT NULL,
|
|
176
177
|
tail_digest TEXT NOT NULL,
|
|
@@ -179,53 +180,53 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
179
180
|
`.withoutTransform;
|
|
180
181
|
yield* sql`
|
|
181
182
|
CREATE TABLE effect_agent_canonical_batches (
|
|
182
|
-
|
|
183
|
+
thread_id TEXT NOT NULL,
|
|
183
184
|
batch_id TEXT NOT NULL,
|
|
184
185
|
first_sequence INTEGER NOT NULL,
|
|
185
186
|
last_sequence INTEGER NOT NULL,
|
|
186
187
|
batch_digest TEXT NOT NULL,
|
|
187
188
|
tail_digest TEXT NOT NULL,
|
|
188
189
|
batch_json TEXT NOT NULL,
|
|
189
|
-
PRIMARY KEY (
|
|
190
|
-
FOREIGN KEY (
|
|
191
|
-
REFERENCES
|
|
190
|
+
PRIMARY KEY (thread_id, batch_id),
|
|
191
|
+
FOREIGN KEY (thread_id)
|
|
192
|
+
REFERENCES effect_agent_threads(thread_id)
|
|
192
193
|
ON DELETE RESTRICT
|
|
193
194
|
)
|
|
194
195
|
`.withoutTransform;
|
|
195
196
|
yield* sql`
|
|
196
197
|
CREATE TABLE effect_agent_canonical_records (
|
|
197
|
-
|
|
198
|
+
thread_id TEXT NOT NULL,
|
|
198
199
|
sequence INTEGER NOT NULL,
|
|
199
200
|
record_id TEXT NOT NULL,
|
|
200
201
|
batch_id TEXT NOT NULL,
|
|
201
202
|
record_json TEXT NOT NULL,
|
|
202
|
-
PRIMARY KEY (
|
|
203
|
-
UNIQUE (
|
|
204
|
-
FOREIGN KEY (
|
|
205
|
-
REFERENCES effect_agent_canonical_batches(
|
|
203
|
+
PRIMARY KEY (thread_id, sequence),
|
|
204
|
+
UNIQUE (thread_id, record_id),
|
|
205
|
+
FOREIGN KEY (thread_id, batch_id)
|
|
206
|
+
REFERENCES effect_agent_canonical_batches(thread_id, batch_id)
|
|
206
207
|
ON DELETE RESTRICT
|
|
207
208
|
)
|
|
208
209
|
`.withoutTransform;
|
|
209
210
|
yield* sql`
|
|
210
211
|
CREATE INDEX effect_agent_canonical_records_batch
|
|
211
|
-
ON effect_agent_canonical_records (
|
|
212
|
+
ON effect_agent_canonical_records (thread_id, batch_id, sequence)
|
|
212
213
|
`.withoutTransform;
|
|
213
214
|
yield* sql`
|
|
214
215
|
CREATE TABLE effect_agent_checkpoints (
|
|
215
|
-
|
|
216
|
+
thread_id TEXT NOT NULL,
|
|
216
217
|
through_sequence INTEGER NOT NULL,
|
|
217
218
|
tail_digest TEXT NOT NULL,
|
|
218
219
|
checkpoint_json TEXT NOT NULL,
|
|
219
|
-
PRIMARY KEY (
|
|
220
|
-
FOREIGN KEY (
|
|
221
|
-
REFERENCES
|
|
220
|
+
PRIMARY KEY (thread_id, through_sequence),
|
|
221
|
+
FOREIGN KEY (thread_id)
|
|
222
|
+
REFERENCES effect_agent_threads(thread_id)
|
|
222
223
|
ON DELETE RESTRICT
|
|
223
224
|
)
|
|
224
225
|
`.withoutTransform;
|
|
225
226
|
yield* sql`
|
|
226
227
|
CREATE TABLE effect_agent_submissions (
|
|
227
228
|
submission_id TEXT PRIMARY KEY NOT NULL,
|
|
228
|
-
|
|
229
|
+
thread_id TEXT NOT NULL,
|
|
229
230
|
queue_sequence INTEGER NOT NULL,
|
|
230
231
|
principal TEXT NOT NULL,
|
|
231
232
|
idempotency_key TEXT NOT NULL,
|
|
@@ -248,8 +249,8 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
248
249
|
unknown_tool_call_ids_json TEXT,
|
|
249
250
|
parent_submission_id TEXT,
|
|
250
251
|
parent_tool_call_id TEXT,
|
|
251
|
-
UNIQUE (
|
|
252
|
-
UNIQUE (
|
|
252
|
+
UNIQUE (thread_id, principal, idempotency_key),
|
|
253
|
+
UNIQUE (thread_id, queue_sequence)
|
|
253
254
|
)
|
|
254
255
|
`.withoutTransform;
|
|
255
256
|
yield* sql`
|
|
@@ -277,7 +278,7 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
277
278
|
CREATE TABLE effect_agent_attempts (
|
|
278
279
|
attempt_id TEXT PRIMARY KEY NOT NULL,
|
|
279
280
|
submission_id TEXT NOT NULL,
|
|
280
|
-
|
|
281
|
+
thread_id TEXT NOT NULL,
|
|
281
282
|
owner_producer_id TEXT NOT NULL,
|
|
282
283
|
producer_epoch INTEGER NOT NULL,
|
|
283
284
|
claimed_at TEXT NOT NULL,
|
|
@@ -377,7 +378,7 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
377
378
|
`.withoutTransform;
|
|
378
379
|
yield* sql`
|
|
379
380
|
INSERT INTO effect_agent_meta (key, value)
|
|
380
|
-
VALUES ('storage_version', ${String(
|
|
381
|
+
VALUES ('storage_version', ${String(2)})
|
|
381
382
|
`.withoutTransform;
|
|
382
383
|
}) });
|
|
383
384
|
//#endregion
|
|
@@ -390,7 +391,7 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
390
391
|
*/
|
|
391
392
|
const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(2e6));
|
|
392
393
|
const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
393
|
-
const
|
|
394
|
+
const MAX_RECORDS_PER_THREAD = 65536;
|
|
394
395
|
const MAX_IDENTIFIER_LENGTH$1 = 1024;
|
|
395
396
|
/** Durable Object SQL storage allows at most 100 bound parameters per statement. */
|
|
396
397
|
const MAX_BOUND_PARAMETERS = 100;
|
|
@@ -403,8 +404,8 @@ const chunked = (values, size) => {
|
|
|
403
404
|
};
|
|
404
405
|
var DoMetaRow = class extends Schema.Class("DoMetaRow")({ value: Schema.NonEmptyString.check(Schema.isMaxLength(128)) }) {};
|
|
405
406
|
var DoNameRow = class extends Schema.Class("DoNameRow")({ name: BoundedIdentifier$1 }) {};
|
|
406
|
-
var
|
|
407
|
-
|
|
407
|
+
var ThreadRow = class extends Schema.Class("ThreadRow")({
|
|
408
|
+
thread_id: BoundedIdentifier$1,
|
|
408
409
|
created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
|
|
409
410
|
producer_epoch: ProducerEpoch,
|
|
410
411
|
tail_digest: BoundedStoredText$1,
|
|
@@ -414,21 +415,21 @@ var BatchRow = class extends Schema.Class("BatchRow")({
|
|
|
414
415
|
batch_digest: BoundedStoredText$1,
|
|
415
416
|
batch_id: BoundedIdentifier$1,
|
|
416
417
|
batch_json: BoundedStoredText$1,
|
|
417
|
-
|
|
418
|
+
thread_id: BoundedIdentifier$1,
|
|
418
419
|
first_sequence: CanonicalSequence,
|
|
419
420
|
last_sequence: CanonicalSequence,
|
|
420
421
|
tail_digest: BoundedStoredText$1
|
|
421
422
|
}) {};
|
|
422
423
|
var RecordRow = class extends Schema.Class("RecordRow")({
|
|
423
424
|
batch_id: BoundedIdentifier$1,
|
|
424
|
-
|
|
425
|
+
thread_id: BoundedIdentifier$1,
|
|
425
426
|
record_id: BoundedIdentifier$1,
|
|
426
427
|
record_json: BoundedStoredText$1,
|
|
427
428
|
sequence: CanonicalSequence
|
|
428
429
|
}) {};
|
|
429
430
|
var CheckpointRow = class extends Schema.Class("CheckpointRow")({
|
|
430
431
|
checkpoint_json: BoundedStoredText$1,
|
|
431
|
-
|
|
432
|
+
thread_id: BoundedIdentifier$1,
|
|
432
433
|
tail_digest: BoundedStoredText$1,
|
|
433
434
|
through_sequence: CanonicalSequence
|
|
434
435
|
}) {};
|
|
@@ -440,7 +441,7 @@ var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-cloudfl
|
|
|
440
441
|
batchDigest: BoundedStoredText$1,
|
|
441
442
|
batchId: BoundedIdentifier$1,
|
|
442
443
|
batchJson: BoundedStoredText$1,
|
|
443
|
-
|
|
444
|
+
threadId: BoundedIdentifier$1,
|
|
444
445
|
expectedTailDigest: BoundedStoredText$1,
|
|
445
446
|
expectedTailSequence: CanonicalSequence,
|
|
446
447
|
producerEpoch: ProducerEpoch,
|
|
@@ -454,36 +455,36 @@ var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudfla
|
|
|
454
455
|
tailDigest: BoundedStoredText$1
|
|
455
456
|
}) {};
|
|
456
457
|
var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
|
|
457
|
-
|
|
458
|
+
threadId: BoundedIdentifier$1,
|
|
458
459
|
fromSequenceExclusive: CanonicalSequence,
|
|
459
460
|
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
|
|
460
461
|
}) {};
|
|
461
462
|
var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
|
|
462
463
|
checkpointJson: BoundedStoredText$1,
|
|
463
|
-
|
|
464
|
+
threadId: BoundedIdentifier$1,
|
|
464
465
|
tailDigest: BoundedStoredText$1,
|
|
465
466
|
throughSequence: CanonicalSequence
|
|
466
467
|
}) {};
|
|
467
|
-
var
|
|
468
|
+
var RawThreadExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawThreadExport")({
|
|
468
469
|
batches: Schema.Array(BatchRow),
|
|
469
470
|
checkpoints: Schema.Array(CheckpointRow),
|
|
470
|
-
|
|
471
|
+
thread: ThreadRow,
|
|
471
472
|
records: Schema.Array(RecordRow)
|
|
472
473
|
}) {};
|
|
473
|
-
const noFailpoint
|
|
474
|
+
const noFailpoint = () => Effect.void;
|
|
474
475
|
const storageError = (operation) => (error) => DoStorageError.make({
|
|
475
476
|
cause: error,
|
|
476
477
|
operation,
|
|
477
478
|
message: error.message
|
|
478
479
|
});
|
|
479
480
|
/** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
|
|
480
|
-
const decodeRows$
|
|
481
|
+
const decodeRows$2 = Effect.fn((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
481
482
|
table,
|
|
482
483
|
rowKey,
|
|
483
484
|
message: String(error)
|
|
484
485
|
}))));
|
|
485
486
|
/** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
|
|
486
|
-
const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows$
|
|
487
|
+
const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows$2(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(DoStorageCorruptionError.make({
|
|
487
488
|
table,
|
|
488
489
|
rowKey,
|
|
489
490
|
message: `Expected exactly one row but found ${decoded.length}.`
|
|
@@ -497,7 +498,7 @@ const REQUIRED_TABLES = [
|
|
|
497
498
|
"effect_agent_checkpoints",
|
|
498
499
|
"effect_agent_child_reservations",
|
|
499
500
|
"effect_agent_child_settlements",
|
|
500
|
-
"
|
|
501
|
+
"effect_agent_threads",
|
|
501
502
|
"effect_agent_meta",
|
|
502
503
|
"effect_agent_settlement_reservations",
|
|
503
504
|
"effect_agent_submission_ownership",
|
|
@@ -511,14 +512,14 @@ const REQUIRED_TABLES = [
|
|
|
511
512
|
* through output gates) and no busy timeout (a Durable Object has exactly one writer): the
|
|
512
513
|
* Node machinery those served has no DC analogue and is deliberately absent.
|
|
513
514
|
*/
|
|
514
|
-
const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint
|
|
515
|
+
const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint, maxStoredValueBytes) {
|
|
515
516
|
const metaTableRows = yield* sql`
|
|
516
517
|
SELECT name
|
|
517
518
|
FROM sqlite_master
|
|
518
519
|
WHERE type = 'table'
|
|
519
520
|
AND name = 'effect_agent_meta'
|
|
520
521
|
`.pipe(Effect.mapError(storageError("read storage version table")));
|
|
521
|
-
if ((yield* decodeRows$
|
|
522
|
+
if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
|
|
522
523
|
const existingRows = yield* sql`
|
|
523
524
|
SELECT name
|
|
524
525
|
FROM sqlite_master
|
|
@@ -526,9 +527,9 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
526
527
|
AND name LIKE 'effect_agent_%'
|
|
527
528
|
ORDER BY name
|
|
528
529
|
`.pipe(Effect.mapError(storageError("inspect unversioned storage")));
|
|
529
|
-
if ((yield* decodeRows$
|
|
530
|
+
if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
|
|
530
531
|
actualVersion: 0,
|
|
531
|
-
supportedVersion:
|
|
532
|
+
supportedVersion: 2,
|
|
532
533
|
message: "The Durable Object contains unversioned Effect Agent tables. Reset the development namespace explicitly; refusing to mutate ambiguous stored data."
|
|
533
534
|
});
|
|
534
535
|
yield* SqliteMigrator.run({ loader: doMigrations }).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError((error) => DoStorageError.make({
|
|
@@ -543,12 +544,12 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
543
544
|
WHERE key = 'storage_version'
|
|
544
545
|
`.pipe(Effect.mapError(storageError("read storage version")));
|
|
545
546
|
const version = yield* decodeSingleRow(Schema.Array(DoMetaRow), "effect_agent_meta", "storage_version", versionRows);
|
|
546
|
-
if (version.value !== String(
|
|
547
|
+
if (version.value !== String(2)) {
|
|
547
548
|
const actualVersion = Number.parseInt(version.value, 10);
|
|
548
549
|
return yield* DoStorageCompatibilityError.make({
|
|
549
550
|
actualVersion: Number.isSafeInteger(actualVersion) ? actualVersion : -1,
|
|
550
|
-
supportedVersion:
|
|
551
|
-
message: `The Durable Object uses private-development storage version ${version.value}; this build supports exactly version
|
|
551
|
+
supportedVersion: 2,
|
|
552
|
+
message: `The Durable Object uses private-development storage version ${version.value}; this build supports exactly version 2. Replace the development namespace explicitly; automatic stored-data migrations are not provided during private development.`
|
|
552
553
|
});
|
|
553
554
|
}
|
|
554
555
|
}
|
|
@@ -559,9 +560,9 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
559
560
|
AND name IN ${sql.in([...REQUIRED_TABLES])}
|
|
560
561
|
ORDER BY name
|
|
561
562
|
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
562
|
-
if ((yield* decodeRows$
|
|
563
|
-
actualVersion:
|
|
564
|
-
supportedVersion:
|
|
563
|
+
if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
|
|
564
|
+
actualVersion: 2,
|
|
565
|
+
supportedVersion: 2,
|
|
565
566
|
message: "The Durable Object claims the current format but is missing required tables. Reset the corrupt private-development data."
|
|
566
567
|
});
|
|
567
568
|
return makeJournal(sql, failpoint, maxStoredValueBytes);
|
|
@@ -589,45 +590,45 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
589
590
|
* transaction.
|
|
590
591
|
*/
|
|
591
592
|
const withWriteTransaction = (operation) => (effect) => sql.withTransaction(effect).pipe(Effect.mapError((error) => isSqlError(error) ? storageError(operation)(error) : error), Effect.withSpan("DoJournal.withWriteTransaction", { attributes: { operation } }));
|
|
592
|
-
const materialize = Effect.fn("DoJournal.materialize")(function* (
|
|
593
|
-
if (
|
|
594
|
-
operation: "materialize
|
|
595
|
-
message: "
|
|
593
|
+
const materialize = Effect.fn("DoJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
|
|
594
|
+
if (threadId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
|
|
595
|
+
operation: "materialize thread",
|
|
596
|
+
message: "Thread identity exceeds the Durable Object storage bounds."
|
|
596
597
|
});
|
|
597
|
-
yield* checkValueBound("materialize
|
|
598
|
+
yield* checkValueBound("materialize thread", emptyTailDigest);
|
|
598
599
|
yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
|
|
599
600
|
const existingRows = yield* sql`
|
|
600
601
|
SELECT
|
|
601
|
-
|
|
602
|
+
thread_id,
|
|
602
603
|
created_at,
|
|
603
604
|
tail_sequence,
|
|
604
605
|
tail_digest,
|
|
605
606
|
producer_epoch
|
|
606
|
-
FROM
|
|
607
|
-
WHERE
|
|
608
|
-
`.pipe(Effect.mapError(storageError("read materialized
|
|
609
|
-
const existing = yield* decodeRows$
|
|
607
|
+
FROM effect_agent_threads
|
|
608
|
+
WHERE thread_id = ${threadId}
|
|
609
|
+
`.pipe(Effect.mapError(storageError("read materialized thread")));
|
|
610
|
+
const existing = yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
|
|
610
611
|
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
611
|
-
table: "
|
|
612
|
-
rowKey:
|
|
613
|
-
message: "A
|
|
612
|
+
table: "effect_agent_threads",
|
|
613
|
+
rowKey: threadId,
|
|
614
|
+
message: "A thread primary key returned more than one row."
|
|
614
615
|
});
|
|
615
616
|
if (existing.length === 0) {
|
|
616
617
|
yield* sql`
|
|
617
|
-
INSERT INTO
|
|
618
|
-
|
|
618
|
+
INSERT INTO effect_agent_threads (
|
|
619
|
+
thread_id,
|
|
619
620
|
created_at,
|
|
620
621
|
tail_sequence,
|
|
621
622
|
tail_digest,
|
|
622
623
|
producer_epoch
|
|
623
624
|
) VALUES (
|
|
624
|
-
${
|
|
625
|
+
${threadId},
|
|
625
626
|
${createdAt},
|
|
626
627
|
0,
|
|
627
628
|
${emptyTailDigest},
|
|
628
629
|
${producerEpoch}
|
|
629
630
|
)
|
|
630
|
-
`.pipe(Effect.mapError(storageError("materialize
|
|
631
|
+
`.pipe(Effect.mapError(storageError("materialize thread")));
|
|
631
632
|
return;
|
|
632
633
|
}
|
|
633
634
|
if (producerEpoch < existing[0].producer_epoch) return yield* DoFenceRejected.make({
|
|
@@ -636,27 +637,27 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
636
637
|
message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
|
|
637
638
|
});
|
|
638
639
|
if (producerEpoch > existing[0].producer_epoch) yield* sql`
|
|
639
|
-
UPDATE
|
|
640
|
+
UPDATE effect_agent_threads
|
|
640
641
|
SET producer_epoch = ${producerEpoch}
|
|
641
|
-
WHERE
|
|
642
|
+
WHERE thread_id = ${threadId}
|
|
642
643
|
`.pipe(Effect.mapError(storageError("advance materialization epoch")));
|
|
643
644
|
}));
|
|
644
645
|
});
|
|
645
|
-
const
|
|
646
|
+
const getThread = Effect.fn("DoJournal.getThread")(function* (threadId) {
|
|
646
647
|
const rows = yield* sql`
|
|
647
648
|
SELECT
|
|
648
|
-
|
|
649
|
+
thread_id,
|
|
649
650
|
created_at,
|
|
650
651
|
tail_sequence,
|
|
651
652
|
tail_digest,
|
|
652
653
|
producer_epoch
|
|
653
|
-
FROM
|
|
654
|
-
WHERE
|
|
655
|
-
`.pipe(Effect.mapError(storageError("read
|
|
656
|
-
return yield* decodeRows$
|
|
654
|
+
FROM effect_agent_threads
|
|
655
|
+
WHERE thread_id = ${threadId}
|
|
656
|
+
`.pipe(Effect.mapError(storageError("read thread")));
|
|
657
|
+
return yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
|
|
657
658
|
});
|
|
658
659
|
const append = Effect.fn("DoJournal.append")(function* (request) {
|
|
659
|
-
if (request.
|
|
660
|
+
if (request.threadId.length > MAX_IDENTIFIER_LENGTH$1 || request.batchId.length > MAX_IDENTIFIER_LENGTH$1 || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH$1)) return yield* DoStorageError.make({
|
|
660
661
|
operation: "append canonical batch",
|
|
661
662
|
message: "Canonical identifiers exceed the Durable Object storage bounds."
|
|
662
663
|
});
|
|
@@ -670,25 +671,25 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
670
671
|
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
671
672
|
reason: "record-identity"
|
|
672
673
|
});
|
|
673
|
-
const
|
|
674
|
+
const threadRows = yield* sql`
|
|
674
675
|
SELECT
|
|
675
|
-
|
|
676
|
+
thread_id,
|
|
676
677
|
created_at,
|
|
677
678
|
tail_sequence,
|
|
678
679
|
tail_digest,
|
|
679
680
|
producer_epoch
|
|
680
|
-
FROM
|
|
681
|
-
WHERE
|
|
681
|
+
FROM effect_agent_threads
|
|
682
|
+
WHERE thread_id = ${request.threadId}
|
|
682
683
|
`.pipe(Effect.mapError(storageError("read append tail")));
|
|
683
|
-
const
|
|
684
|
-
if (request.producerEpoch !==
|
|
684
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
|
|
685
|
+
if (request.producerEpoch !== thread.producer_epoch) return yield* DoFenceRejected.make({
|
|
685
686
|
producerEpoch: request.producerEpoch,
|
|
686
|
-
actualEpoch:
|
|
687
|
-
message: `Producer epoch ${request.producerEpoch} is not the current epoch ${
|
|
687
|
+
actualEpoch: thread.producer_epoch,
|
|
688
|
+
message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
|
|
688
689
|
});
|
|
689
690
|
const batchRows = yield* sql`
|
|
690
691
|
SELECT
|
|
691
|
-
|
|
692
|
+
thread_id,
|
|
692
693
|
batch_id,
|
|
693
694
|
first_sequence,
|
|
694
695
|
last_sequence,
|
|
@@ -696,13 +697,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
696
697
|
tail_digest,
|
|
697
698
|
batch_json
|
|
698
699
|
FROM effect_agent_canonical_batches
|
|
699
|
-
WHERE
|
|
700
|
+
WHERE thread_id = ${request.threadId}
|
|
700
701
|
AND batch_id = ${request.batchId}
|
|
701
702
|
`.pipe(Effect.mapError(storageError("read idempotent batch")));
|
|
702
|
-
const batches = yield* decodeRows$
|
|
703
|
+
const batches = yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
|
|
703
704
|
if (batches.length > 1) return yield* DoStorageCorruptionError.make({
|
|
704
705
|
table: "effect_agent_canonical_batches",
|
|
705
|
-
rowKey: `${request.
|
|
706
|
+
rowKey: `${request.threadId}/${request.batchId}`,
|
|
706
707
|
message: "A canonical batch primary key returned more than one row."
|
|
707
708
|
});
|
|
708
709
|
if (batches.length === 1) {
|
|
@@ -718,37 +719,37 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
718
719
|
tailDigest: existing.tail_digest
|
|
719
720
|
});
|
|
720
721
|
}
|
|
721
|
-
if (request.expectedTailSequence !==
|
|
722
|
-
message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${
|
|
722
|
+
if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* DoAppendConflict.make({
|
|
723
|
+
message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
|
|
723
724
|
reason: "tail",
|
|
724
|
-
actualTailSequence:
|
|
725
|
-
actualTailDigest:
|
|
725
|
+
actualTailSequence: thread.tail_sequence,
|
|
726
|
+
actualTailDigest: thread.tail_digest
|
|
726
727
|
});
|
|
727
|
-
if (
|
|
728
|
+
if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
|
|
728
729
|
operation: "append canonical batch",
|
|
729
|
-
message: `
|
|
730
|
+
message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
|
|
730
731
|
});
|
|
731
732
|
const existingRecords = [];
|
|
732
733
|
for (const chunk of chunked(recordIds, MAX_BOUND_PARAMETERS - 10)) {
|
|
733
734
|
const existingRecordRows = yield* sql`
|
|
734
735
|
SELECT
|
|
735
|
-
|
|
736
|
+
thread_id,
|
|
736
737
|
sequence,
|
|
737
738
|
record_id,
|
|
738
739
|
batch_id,
|
|
739
740
|
record_json
|
|
740
741
|
FROM effect_agent_canonical_records
|
|
741
|
-
WHERE
|
|
742
|
+
WHERE thread_id = ${request.threadId}
|
|
742
743
|
AND record_id IN ${sql.in([...chunk])}
|
|
743
744
|
ORDER BY sequence
|
|
744
745
|
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
745
|
-
existingRecords.push(...yield* decodeRows$
|
|
746
|
+
existingRecords.push(...yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows));
|
|
746
747
|
}
|
|
747
748
|
if (existingRecords.length > 0) return yield* DoAppendConflict.make({
|
|
748
749
|
message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
|
|
749
750
|
reason: "record-identity"
|
|
750
751
|
});
|
|
751
|
-
const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
|
|
752
|
+
const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
|
|
752
753
|
cause: error,
|
|
753
754
|
operation: "append canonical batch",
|
|
754
755
|
message: error.message
|
|
@@ -760,7 +761,7 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
760
761
|
})));
|
|
761
762
|
yield* sql`
|
|
762
763
|
INSERT INTO effect_agent_canonical_batches (
|
|
763
|
-
|
|
764
|
+
thread_id,
|
|
764
765
|
batch_id,
|
|
765
766
|
first_sequence,
|
|
766
767
|
last_sequence,
|
|
@@ -768,7 +769,7 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
768
769
|
tail_digest,
|
|
769
770
|
batch_json
|
|
770
771
|
) VALUES (
|
|
771
|
-
${request.
|
|
772
|
+
${request.threadId},
|
|
772
773
|
${request.batchId},
|
|
773
774
|
${firstSequence},
|
|
774
775
|
${lastSequence},
|
|
@@ -781,13 +782,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
781
782
|
yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
|
|
782
783
|
yield* sql`
|
|
783
784
|
INSERT INTO effect_agent_canonical_records (
|
|
784
|
-
|
|
785
|
+
thread_id,
|
|
785
786
|
sequence,
|
|
786
787
|
record_id,
|
|
787
788
|
batch_id,
|
|
788
789
|
record_json
|
|
789
790
|
) VALUES (
|
|
790
|
-
${request.
|
|
791
|
+
${request.threadId},
|
|
791
792
|
${firstSequence + index},
|
|
792
793
|
${record.recordId},
|
|
793
794
|
${request.batchId},
|
|
@@ -797,13 +798,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
797
798
|
yield* failpoint("append:after-record-insert");
|
|
798
799
|
}), { discard: true });
|
|
799
800
|
yield* sql`
|
|
800
|
-
UPDATE
|
|
801
|
+
UPDATE effect_agent_threads
|
|
801
802
|
SET
|
|
802
803
|
tail_sequence = ${lastSequence},
|
|
803
804
|
tail_digest = ${request.tailDigest},
|
|
804
805
|
producer_epoch = ${request.producerEpoch}
|
|
805
|
-
WHERE
|
|
806
|
-
`.pipe(Effect.mapError(storageError("advance
|
|
806
|
+
WHERE thread_id = ${request.threadId}
|
|
807
|
+
`.pipe(Effect.mapError(storageError("advance thread tail")));
|
|
807
808
|
yield* failpoint("append:after-tail-update");
|
|
808
809
|
return RawAppendResult.make({
|
|
809
810
|
firstSequence,
|
|
@@ -816,36 +817,36 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
816
817
|
const read = Effect.fn("DoJournal.read")(function* (request) {
|
|
817
818
|
const rows = yield* sql`
|
|
818
819
|
SELECT
|
|
819
|
-
|
|
820
|
+
thread_id,
|
|
820
821
|
sequence,
|
|
821
822
|
record_id,
|
|
822
823
|
batch_id,
|
|
823
824
|
record_json
|
|
824
825
|
FROM effect_agent_canonical_records
|
|
825
|
-
WHERE
|
|
826
|
+
WHERE thread_id = ${request.threadId}
|
|
826
827
|
AND sequence > ${request.fromSequenceExclusive}
|
|
827
828
|
ORDER BY sequence
|
|
828
829
|
LIMIT ${request.limit}
|
|
829
830
|
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
830
|
-
return yield* decodeRows$
|
|
831
|
+
return yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, rows);
|
|
831
832
|
});
|
|
832
|
-
const
|
|
833
|
+
const exportThread = Effect.fn("DoJournal.exportThread")(function* (threadId) {
|
|
833
834
|
return yield* sql.withTransaction(Effect.gen(function* () {
|
|
834
|
-
const
|
|
835
|
+
const threadRows = yield* sql`
|
|
835
836
|
SELECT
|
|
836
|
-
|
|
837
|
+
thread_id,
|
|
837
838
|
created_at,
|
|
838
839
|
tail_sequence,
|
|
839
840
|
tail_digest,
|
|
840
841
|
producer_epoch
|
|
841
|
-
FROM
|
|
842
|
-
WHERE
|
|
843
|
-
`.pipe(Effect.mapError(storageError("export
|
|
844
|
-
const
|
|
845
|
-
yield* failpoint("export:after-
|
|
842
|
+
FROM effect_agent_threads
|
|
843
|
+
WHERE thread_id = ${threadId}
|
|
844
|
+
`.pipe(Effect.mapError(storageError("export thread")));
|
|
845
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
|
|
846
|
+
yield* failpoint("export:after-thread-read");
|
|
846
847
|
const batchRows = yield* sql`
|
|
847
848
|
SELECT
|
|
848
|
-
|
|
849
|
+
thread_id,
|
|
849
850
|
batch_id,
|
|
850
851
|
first_sequence,
|
|
851
852
|
last_sequence,
|
|
@@ -853,71 +854,71 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
853
854
|
tail_digest,
|
|
854
855
|
batch_json
|
|
855
856
|
FROM effect_agent_canonical_batches
|
|
856
|
-
WHERE
|
|
857
|
+
WHERE thread_id = ${threadId}
|
|
857
858
|
ORDER BY first_sequence
|
|
858
859
|
`.pipe(Effect.mapError(storageError("export canonical batches")));
|
|
859
860
|
const recordRows = yield* sql`
|
|
860
861
|
SELECT
|
|
861
|
-
|
|
862
|
+
thread_id,
|
|
862
863
|
sequence,
|
|
863
864
|
record_id,
|
|
864
865
|
batch_id,
|
|
865
866
|
record_json
|
|
866
867
|
FROM effect_agent_canonical_records
|
|
867
|
-
WHERE
|
|
868
|
+
WHERE thread_id = ${threadId}
|
|
868
869
|
ORDER BY sequence
|
|
869
870
|
`.pipe(Effect.mapError(storageError("export canonical records")));
|
|
870
871
|
const checkpointRows = yield* sql`
|
|
871
872
|
SELECT
|
|
872
|
-
|
|
873
|
+
thread_id,
|
|
873
874
|
through_sequence,
|
|
874
875
|
tail_digest,
|
|
875
876
|
checkpoint_json
|
|
876
877
|
FROM effect_agent_checkpoints
|
|
877
|
-
WHERE
|
|
878
|
+
WHERE thread_id = ${threadId}
|
|
878
879
|
ORDER BY through_sequence
|
|
879
880
|
`.pipe(Effect.mapError(storageError("export checkpoints")));
|
|
880
|
-
return
|
|
881
|
-
|
|
882
|
-
batches: yield* decodeRows$
|
|
883
|
-
records: yield* decodeRows$
|
|
884
|
-
checkpoints: yield* decodeRows$
|
|
881
|
+
return RawThreadExport.make({
|
|
882
|
+
thread,
|
|
883
|
+
batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", threadId, batchRows),
|
|
884
|
+
records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", threadId, recordRows),
|
|
885
|
+
checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", threadId, checkpointRows)
|
|
885
886
|
});
|
|
886
887
|
})).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("export transaction")(error))));
|
|
887
888
|
});
|
|
888
889
|
const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (checkpoint) {
|
|
889
|
-
if (checkpoint.
|
|
890
|
+
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
|
|
890
891
|
operation: "save checkpoint",
|
|
891
892
|
message: "Checkpoint identity exceeds the Durable Object storage bounds."
|
|
892
893
|
});
|
|
893
894
|
yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
|
|
894
895
|
yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
|
|
895
|
-
const
|
|
896
|
+
const threadRows = yield* sql`
|
|
896
897
|
SELECT
|
|
897
|
-
|
|
898
|
+
thread_id,
|
|
898
899
|
created_at,
|
|
899
900
|
tail_sequence,
|
|
900
901
|
tail_digest,
|
|
901
902
|
producer_epoch
|
|
902
|
-
FROM
|
|
903
|
-
WHERE
|
|
903
|
+
FROM effect_agent_threads
|
|
904
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
904
905
|
`.pipe(Effect.mapError(storageError("read checkpoint tail")));
|
|
905
|
-
const
|
|
906
|
-
if (checkpoint.throughSequence >
|
|
906
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
|
|
907
|
+
if (checkpoint.throughSequence > thread.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
|
|
907
908
|
const checkpointRows = yield* sql`
|
|
908
909
|
SELECT
|
|
909
|
-
|
|
910
|
+
thread_id,
|
|
910
911
|
through_sequence,
|
|
911
912
|
tail_digest,
|
|
912
913
|
checkpoint_json
|
|
913
914
|
FROM effect_agent_checkpoints
|
|
914
|
-
WHERE
|
|
915
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
915
916
|
AND through_sequence = ${checkpoint.throughSequence}
|
|
916
917
|
`.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
|
|
917
|
-
const existing = yield* decodeRows$
|
|
918
|
+
const existing = yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
|
|
918
919
|
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
919
920
|
table: "effect_agent_checkpoints",
|
|
920
|
-
rowKey: `${checkpoint.
|
|
921
|
+
rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
|
|
921
922
|
message: "A checkpoint primary key returned more than one row."
|
|
922
923
|
});
|
|
923
924
|
if (existing.length === 1) {
|
|
@@ -926,12 +927,12 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
926
927
|
}
|
|
927
928
|
yield* sql`
|
|
928
929
|
INSERT INTO effect_agent_checkpoints (
|
|
929
|
-
|
|
930
|
+
thread_id,
|
|
930
931
|
through_sequence,
|
|
931
932
|
tail_digest,
|
|
932
933
|
checkpoint_json
|
|
933
934
|
) VALUES (
|
|
934
|
-
${checkpoint.
|
|
935
|
+
${checkpoint.threadId},
|
|
935
936
|
${checkpoint.throughSequence},
|
|
936
937
|
${checkpoint.tailDigest},
|
|
937
938
|
${checkpoint.checkpointJson}
|
|
@@ -939,34 +940,34 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
939
940
|
`.pipe(Effect.mapError(storageError("insert checkpoint")));
|
|
940
941
|
}));
|
|
941
942
|
});
|
|
942
|
-
const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (
|
|
943
|
+
const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
|
|
943
944
|
const rows = yield* sql`
|
|
944
945
|
SELECT
|
|
945
|
-
|
|
946
|
+
thread_id,
|
|
946
947
|
through_sequence,
|
|
947
948
|
tail_digest,
|
|
948
949
|
checkpoint_json
|
|
949
950
|
FROM effect_agent_checkpoints
|
|
950
|
-
WHERE
|
|
951
|
+
WHERE thread_id = ${threadId}
|
|
951
952
|
AND through_sequence <= ${atOrBeforeSequence}
|
|
952
953
|
ORDER BY through_sequence DESC
|
|
953
954
|
LIMIT 1
|
|
954
955
|
`.pipe(Effect.mapError(storageError("load checkpoint")));
|
|
955
|
-
return yield* decodeRows$
|
|
956
|
+
return yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
|
|
956
957
|
});
|
|
957
958
|
return {
|
|
958
959
|
append,
|
|
959
960
|
checkValueBound,
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
getTailDigestAt: Effect.fn("DoJournal.getTailDigestAt")(function* (
|
|
961
|
+
exportThread,
|
|
962
|
+
getThread,
|
|
963
|
+
getTailDigestAt: Effect.fn("DoJournal.getTailDigestAt")(function* (threadId, sequence) {
|
|
963
964
|
if (sequence === 0) {
|
|
964
|
-
const
|
|
965
|
-
return
|
|
965
|
+
const threads = yield* getThread(threadId);
|
|
966
|
+
return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
|
|
966
967
|
}
|
|
967
968
|
const rows = yield* sql`
|
|
968
969
|
SELECT
|
|
969
|
-
|
|
970
|
+
thread_id,
|
|
970
971
|
batch_id,
|
|
971
972
|
first_sequence,
|
|
972
973
|
last_sequence,
|
|
@@ -974,10 +975,10 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
974
975
|
tail_digest,
|
|
975
976
|
batch_json
|
|
976
977
|
FROM effect_agent_canonical_batches
|
|
977
|
-
WHERE
|
|
978
|
+
WHERE thread_id = ${threadId}
|
|
978
979
|
AND last_sequence = ${sequence}
|
|
979
980
|
`.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
|
|
980
|
-
return (yield* decodeRows$
|
|
981
|
+
return (yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
|
|
981
982
|
}),
|
|
982
983
|
loadCheckpoint,
|
|
983
984
|
materialize,
|
|
@@ -985,19 +986,19 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
985
986
|
saveCheckpoint,
|
|
986
987
|
scanStoredPayloads: Effect.fn("DoJournal.scanStoredPayloads")(function* () {
|
|
987
988
|
return yield* sql.withTransaction(Effect.gen(function* () {
|
|
988
|
-
const
|
|
989
|
+
const threads = yield* sql`
|
|
989
990
|
SELECT
|
|
990
|
-
|
|
991
|
+
thread_id,
|
|
991
992
|
created_at,
|
|
992
993
|
tail_sequence,
|
|
993
994
|
tail_digest,
|
|
994
995
|
producer_epoch
|
|
995
|
-
FROM
|
|
996
|
-
ORDER BY
|
|
997
|
-
`.pipe(Effect.mapError(storageError("scan
|
|
996
|
+
FROM effect_agent_threads
|
|
997
|
+
ORDER BY thread_id
|
|
998
|
+
`.pipe(Effect.mapError(storageError("scan threads")));
|
|
998
999
|
const batches = yield* sql`
|
|
999
1000
|
SELECT
|
|
1000
|
-
|
|
1001
|
+
thread_id,
|
|
1001
1002
|
batch_id,
|
|
1002
1003
|
first_sequence,
|
|
1003
1004
|
last_sequence,
|
|
@@ -1005,32 +1006,32 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
1005
1006
|
tail_digest,
|
|
1006
1007
|
batch_json
|
|
1007
1008
|
FROM effect_agent_canonical_batches
|
|
1008
|
-
ORDER BY
|
|
1009
|
+
ORDER BY thread_id, first_sequence
|
|
1009
1010
|
`.pipe(Effect.mapError(storageError("scan canonical batches")));
|
|
1010
1011
|
const records = yield* sql`
|
|
1011
1012
|
SELECT
|
|
1012
|
-
|
|
1013
|
+
thread_id,
|
|
1013
1014
|
sequence,
|
|
1014
1015
|
record_id,
|
|
1015
1016
|
batch_id,
|
|
1016
1017
|
record_json
|
|
1017
1018
|
FROM effect_agent_canonical_records
|
|
1018
|
-
ORDER BY
|
|
1019
|
+
ORDER BY thread_id, sequence
|
|
1019
1020
|
`.pipe(Effect.mapError(storageError("scan canonical records")));
|
|
1020
1021
|
const checkpoints = yield* sql`
|
|
1021
1022
|
SELECT
|
|
1022
|
-
|
|
1023
|
+
thread_id,
|
|
1023
1024
|
through_sequence,
|
|
1024
1025
|
tail_digest,
|
|
1025
1026
|
checkpoint_json
|
|
1026
1027
|
FROM effect_agent_checkpoints
|
|
1027
|
-
ORDER BY
|
|
1028
|
+
ORDER BY thread_id, through_sequence
|
|
1028
1029
|
`.pipe(Effect.mapError(storageError("scan checkpoints")));
|
|
1029
1030
|
return {
|
|
1030
|
-
|
|
1031
|
-
batches: yield* decodeRows$
|
|
1032
|
-
records: yield* decodeRows$
|
|
1033
|
-
checkpoints: yield* decodeRows$
|
|
1031
|
+
threads: yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
|
|
1032
|
+
batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
|
|
1033
|
+
records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
|
|
1034
|
+
checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
|
|
1034
1035
|
};
|
|
1035
1036
|
})).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("startup scan transaction")(error))));
|
|
1036
1037
|
}),
|
|
@@ -1081,41 +1082,7 @@ var DoStorageConfigValue = class extends Schema.Class("@effect-agent/storage-clo
|
|
|
1081
1082
|
/** Explicit Durable Object storage configuration authority. */
|
|
1082
1083
|
var DoStorageConfig = class extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageConfig") {};
|
|
1083
1084
|
//#endregion
|
|
1084
|
-
//#region src/do-
|
|
1085
|
-
const noFailpoint = () => Effect.void;
|
|
1086
|
-
/** Test-only control for replacing the active Durable Object failpoint handler. */
|
|
1087
|
-
var DoStorageFailpointTestControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageFailpointTestControl") {};
|
|
1088
|
-
/** Explicit fault-injection authority used at Durable Object storage operation boundaries. */
|
|
1089
|
-
var DoStorageFailpoint = class DoStorageFailpoint extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageFailpoint") {
|
|
1090
|
-
/** Production default: no fault injection. */
|
|
1091
|
-
static layer = Layer.succeed(this)({ hit: noFailpoint });
|
|
1092
|
-
/** Reusable test Layer with a control service backed by the same handler Ref. */
|
|
1093
|
-
static layerTest = Layer.effectContext(Effect.gen(function* () {
|
|
1094
|
-
const handler = yield* Ref.make(noFailpoint);
|
|
1095
|
-
return Context.make(DoStorageFailpoint, DoStorageFailpoint.of({ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))) })).pipe(Context.add(DoStorageFailpointTestControl, DoStorageFailpointTestControl.of({
|
|
1096
|
-
clear: Ref.set(handler, noFailpoint),
|
|
1097
|
-
setHandler: (next) => Ref.set(handler, next)
|
|
1098
|
-
})));
|
|
1099
|
-
}));
|
|
1100
|
-
};
|
|
1101
|
-
/**
|
|
1102
|
-
* The DC-specific eviction failpoint mode: instead of failing typed, an armed hit evicts the
|
|
1103
|
-
* Durable Object through an injected `evict` thunk — in production-shaped harnesses that thunk
|
|
1104
|
-
* is `() => ctx.abort()`, the platform's real failure mode. `ctx.abort()` never returns (it
|
|
1105
|
-
* throws while destroying the in-memory instance and every in-flight implicit or explicit
|
|
1106
|
-
* storage transaction rolls back), so an armed hit ends the current Attempt exactly like an
|
|
1107
|
-
* unannounced platform eviction; DO storage — the only correctness-critical state — survives
|
|
1108
|
-
* for the next incarnation, which the persisted alarm wakes without any incoming request.
|
|
1109
|
-
*
|
|
1110
|
-
* The handles stay injected: this package never imports `cloudflare:workers`, so the harness
|
|
1111
|
-
* that owns a `DurableObjectState` supplies the thunk.
|
|
1112
|
-
*/
|
|
1113
|
-
const evictionFailpointHandler = (options) => (location) => options.isArmed(location).pipe(Effect.flatMap((armed) => armed ? Effect.sync(() => {
|
|
1114
|
-
options.evict(location);
|
|
1115
|
-
throw new Error(`Durable Object eviction did not interrupt execution at ${location}.`);
|
|
1116
|
-
}) : Effect.void));
|
|
1117
|
-
//#endregion
|
|
1118
|
-
//#region src/do-conversation-store.ts
|
|
1085
|
+
//#region src/do-thread-store.ts
|
|
1119
1086
|
const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
|
|
1120
1087
|
const DO_OFFSET_PREFIX = "effect-agent-do@1:";
|
|
1121
1088
|
const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
|
|
@@ -1123,36 +1090,36 @@ const isDigest = Schema.is(Digest);
|
|
|
1123
1090
|
const isDoFenceRejected = Schema.is(DoFenceRejected);
|
|
1124
1091
|
const isDoAppendConflict = Schema.is(DoAppendConflict);
|
|
1125
1092
|
const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
|
|
1126
|
-
const storeError = (operation, error) =>
|
|
1093
|
+
const storeError = (operation, error) => ThreadStoreError.make({
|
|
1127
1094
|
cause: error,
|
|
1128
1095
|
operation,
|
|
1129
1096
|
message: error.message
|
|
1130
1097
|
});
|
|
1131
|
-
const schemaStoreError = (operation, error) =>
|
|
1098
|
+
const schemaStoreError = (operation, error) => ThreadStoreError.make({
|
|
1132
1099
|
cause: error,
|
|
1133
1100
|
operation,
|
|
1134
1101
|
message: error.message
|
|
1135
1102
|
});
|
|
1136
|
-
const makeOffset = Effect.fn(function* (
|
|
1137
|
-
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${DO_OFFSET_PREFIX}${encodeURIComponent(
|
|
1103
|
+
const makeOffset = Effect.fn(function* (threadId, sequence) {
|
|
1104
|
+
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
|
|
1138
1105
|
});
|
|
1139
|
-
const parseOffset = Effect.fn(function* (
|
|
1106
|
+
const parseOffset = Effect.fn(function* (threadId, offset) {
|
|
1140
1107
|
if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
|
|
1141
1108
|
const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
|
|
1142
|
-
const
|
|
1143
|
-
if (!text.startsWith(
|
|
1109
|
+
const threadPrefix = `${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
|
|
1110
|
+
if (!text.startsWith(threadPrefix)) return yield* ThreadStoreError.make({
|
|
1144
1111
|
operation: "decode observation offset",
|
|
1145
|
-
message: "The observation offset belongs to a different adapter, storage version, or
|
|
1112
|
+
message: "The observation offset belongs to a different adapter, storage version, or Thread."
|
|
1146
1113
|
});
|
|
1147
|
-
const sequenceText = text.slice(
|
|
1148
|
-
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield*
|
|
1114
|
+
const sequenceText = text.slice(threadPrefix.length);
|
|
1115
|
+
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ThreadStoreError.make({
|
|
1149
1116
|
operation: "decode observation offset",
|
|
1150
1117
|
message: "The observation offset is malformed."
|
|
1151
1118
|
});
|
|
1152
1119
|
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
|
|
1153
1120
|
});
|
|
1154
|
-
const mapFence = (
|
|
1155
|
-
|
|
1121
|
+
const mapFence = (threadId, error) => FenceRejected.make({
|
|
1122
|
+
threadId,
|
|
1156
1123
|
actualEpoch: error.actualEpoch,
|
|
1157
1124
|
attemptedEpoch: error.producerEpoch
|
|
1158
1125
|
});
|
|
@@ -1163,18 +1130,18 @@ const encodeCanonicalBatch = Effect.fn(function* (batch) {
|
|
|
1163
1130
|
return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
|
|
1164
1131
|
});
|
|
1165
1132
|
const encodeCheckpoint = Effect.fn(function* (checkpoint) {
|
|
1166
|
-
return yield* Schema.encodeEffect(Schema.fromJsonString(
|
|
1133
|
+
return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
|
|
1167
1134
|
});
|
|
1168
1135
|
const decodeEnvelope = Effect.fn(function* (row) {
|
|
1169
|
-
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) =>
|
|
1136
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ThreadStoreError.make({
|
|
1170
1137
|
operation: "decode canonical record",
|
|
1171
1138
|
message: error.message
|
|
1172
1139
|
})));
|
|
1173
|
-
const
|
|
1174
|
-
const offset = yield* makeOffset(
|
|
1140
|
+
const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(row.thread_id).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
|
|
1141
|
+
const offset = yield* makeOffset(threadId, row.sequence);
|
|
1175
1142
|
const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
|
|
1176
1143
|
return CanonicalRecordEnvelope.make({
|
|
1177
|
-
|
|
1144
|
+
threadId,
|
|
1178
1145
|
batchId,
|
|
1179
1146
|
sequence: row.sequence,
|
|
1180
1147
|
offset,
|
|
@@ -1182,18 +1149,18 @@ const decodeEnvelope = Effect.fn(function* (row) {
|
|
|
1182
1149
|
});
|
|
1183
1150
|
});
|
|
1184
1151
|
const decodeCheckpoint = Effect.fn(function* (checkpointJson) {
|
|
1185
|
-
return yield* Schema.decodeEffect(Schema.fromJsonString(
|
|
1152
|
+
return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
|
|
1186
1153
|
});
|
|
1187
|
-
const
|
|
1188
|
-
const rows = yield* journal.
|
|
1189
|
-
if (rows.length === 0) return yield*
|
|
1154
|
+
const requireThread = Effect.fn("DoThreadStore.requireThread")(function* (journal, threadId) {
|
|
1155
|
+
const rows = yield* journal.getThread(threadId).pipe(Effect.mapError((error) => storeError("read thread", error)));
|
|
1156
|
+
if (rows.length === 0) return yield* ThreadNotMaterialized.make({ threadId });
|
|
1190
1157
|
return rows[0];
|
|
1191
1158
|
});
|
|
1192
|
-
const tailDigestAt = Effect.fn("
|
|
1159
|
+
const tailDigestAt = Effect.fn("DoThreadStore.tailDigestAt")(function* (journal, threadId, sequence) {
|
|
1193
1160
|
if (sequence === 0) return EMPTY_TAIL_DIGEST;
|
|
1194
|
-
const digests = yield* journal.getTailDigestAt(
|
|
1161
|
+
const digests = yield* journal.getTailDigestAt(threadId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
|
|
1195
1162
|
if (digests.length !== 1) return yield* CheckpointRejected.make({
|
|
1196
|
-
|
|
1163
|
+
threadId,
|
|
1197
1164
|
reason: "digest-mismatch"
|
|
1198
1165
|
});
|
|
1199
1166
|
return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
|
|
@@ -1212,14 +1179,14 @@ const groupByKey = (rows, key) => {
|
|
|
1212
1179
|
* and re-digested against the canonical chain. Routine opens skip this scan: per-operation
|
|
1213
1180
|
* Schema decoding plus the digest chain already fail clearly on corrupt rows.
|
|
1214
1181
|
*/
|
|
1215
|
-
const decodeStartupPayloads = Effect.fn("
|
|
1182
|
+
const decodeStartupPayloads = Effect.fn("DoThreadStore.decodeStartupPayloads")(function* (journal, crypto) {
|
|
1216
1183
|
const stored = yield* journal.scanStoredPayloads();
|
|
1217
1184
|
const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
|
|
1218
1185
|
decoded,
|
|
1219
1186
|
row: batch
|
|
1220
1187
|
})), Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
1221
1188
|
table: "effect_agent_canonical_batches",
|
|
1222
|
-
rowKey: `${batch.
|
|
1189
|
+
rowKey: `${batch.thread_id}/${batch.batch_id}`,
|
|
1223
1190
|
message: error.message
|
|
1224
1191
|
}))));
|
|
1225
1192
|
const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
|
|
@@ -1227,31 +1194,31 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
1227
1194
|
row: record
|
|
1228
1195
|
})), Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
1229
1196
|
table: "effect_agent_canonical_records",
|
|
1230
|
-
rowKey: `${record.
|
|
1197
|
+
rowKey: `${record.thread_id}/${record.sequence}`,
|
|
1231
1198
|
message: error.message
|
|
1232
1199
|
}))));
|
|
1233
|
-
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(
|
|
1200
|
+
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
|
|
1234
1201
|
decoded,
|
|
1235
1202
|
row: checkpoint
|
|
1236
1203
|
})), Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
1237
1204
|
table: "effect_agent_checkpoints",
|
|
1238
|
-
rowKey: `${checkpoint.
|
|
1205
|
+
rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
|
|
1239
1206
|
message: error.message
|
|
1240
1207
|
}))));
|
|
1241
|
-
const
|
|
1242
|
-
const
|
|
1243
|
-
const
|
|
1244
|
-
const materializedIds = new Set(stored.
|
|
1245
|
-
for (const
|
|
1246
|
-
const
|
|
1247
|
-
const
|
|
1248
|
-
const
|
|
1249
|
-
const recordsByBatch = groupByKey(
|
|
1208
|
+
const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
|
|
1209
|
+
const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
|
|
1210
|
+
const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
|
|
1211
|
+
const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
|
|
1212
|
+
for (const thread of stored.threads) {
|
|
1213
|
+
const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
|
|
1214
|
+
const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
|
|
1215
|
+
const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
|
|
1216
|
+
const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
|
|
1250
1217
|
let previousDigest = EMPTY_TAIL_DIGEST;
|
|
1251
1218
|
let expectedSequence = 1;
|
|
1252
1219
|
const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
|
|
1253
|
-
for (const { decoded: canonicalBatch, row: batchRow } of
|
|
1254
|
-
const key = `${batchRow.
|
|
1220
|
+
for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
|
|
1221
|
+
const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
|
|
1255
1222
|
if (canonicalBatch.batchId !== batchRow.batch_id || batchRow.first_sequence !== expectedSequence || batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1) return yield* DoStorageCorruptionError.make({
|
|
1256
1223
|
table: "effect_agent_canonical_batches",
|
|
1257
1224
|
rowKey: key,
|
|
@@ -1296,24 +1263,24 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
1296
1263
|
expectedSequence = batchRow.last_sequence + 1;
|
|
1297
1264
|
tailDigests.set(batchRow.last_sequence, digest);
|
|
1298
1265
|
}
|
|
1299
|
-
if (
|
|
1300
|
-
table: "
|
|
1301
|
-
rowKey:
|
|
1302
|
-
message: "
|
|
1266
|
+
if (threadRecords.length !== thread.tail_sequence || thread.tail_sequence !== expectedSequence - 1 || thread.tail_digest !== previousDigest) return yield* DoStorageCorruptionError.make({
|
|
1267
|
+
table: "effect_agent_threads",
|
|
1268
|
+
rowKey: thread.thread_id,
|
|
1269
|
+
message: "Thread tail does not match its canonical batch chain."
|
|
1303
1270
|
});
|
|
1304
|
-
for (const checkpoint of
|
|
1271
|
+
for (const checkpoint of threadCheckpoints) if (checkpoint.decoded.threadId !== thread.thread_id || checkpoint.decoded.throughSequence !== checkpoint.row.through_sequence || checkpoint.decoded.tailDigest !== checkpoint.row.tail_digest || tailDigests.get(checkpoint.row.through_sequence) !== checkpoint.row.tail_digest) return yield* DoStorageCorruptionError.make({
|
|
1305
1272
|
table: "effect_agent_checkpoints",
|
|
1306
|
-
rowKey: `${
|
|
1273
|
+
rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
|
|
1307
1274
|
message: "Checkpoint identity or digest is not bound to a canonical batch tail."
|
|
1308
1275
|
});
|
|
1309
1276
|
}
|
|
1310
|
-
if (batches.some(({ row }) => !materializedIds.has(row.
|
|
1311
|
-
table: "
|
|
1277
|
+
if (batches.some(({ row }) => !materializedIds.has(row.thread_id)) || records.some(({ row }) => !materializedIds.has(row.thread_id)) || checkpoints.some(({ row }) => !materializedIds.has(row.thread_id))) return yield* DoStorageCorruptionError.make({
|
|
1278
|
+
table: "effect_agent_threads",
|
|
1312
1279
|
rowKey: "startup_scan",
|
|
1313
|
-
message: "Canonical rows exist without a materialized
|
|
1280
|
+
message: "Canonical rows exist without a materialized Thread."
|
|
1314
1281
|
});
|
|
1315
1282
|
});
|
|
1316
|
-
const makeServices$2 = Effect.fn("
|
|
1283
|
+
const makeServices$2 = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
1317
1284
|
const config = yield* DoStorageConfig;
|
|
1318
1285
|
const failpoint = yield* DoStorageFailpoint;
|
|
1319
1286
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -1322,16 +1289,16 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1322
1289
|
if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
|
|
1323
1290
|
const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
|
|
1324
1291
|
const hitFailpoint = Effect.fn((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
|
|
1325
|
-
const materialize = Effect.fn("
|
|
1326
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1292
|
+
const materialize = Effect.fn("DoThreadStore.materialize")(function* (request) {
|
|
1293
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
1327
1294
|
const now = yield* Clock.currentTimeMillis;
|
|
1328
1295
|
yield* hitFailpoint("materialize:before");
|
|
1329
|
-
yield* journal.materialize(validated.
|
|
1296
|
+
yield* journal.materialize(validated.threadId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "DoFenceRejected" ? mapFence(validated.threadId, error) : storeError("materialize thread", error)));
|
|
1330
1297
|
yield* hitFailpoint("materialize:after");
|
|
1331
1298
|
});
|
|
1332
|
-
const append = Effect.fn("
|
|
1299
|
+
const append = Effect.fn("DoThreadStore.append")(function* (request) {
|
|
1333
1300
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
|
|
1334
|
-
yield*
|
|
1301
|
+
yield* requireThread(journal, validated.threadId);
|
|
1335
1302
|
const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
|
|
1336
1303
|
const batchJson = yield* encodeCanonicalBatch(validated.batch);
|
|
1337
1304
|
const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
|
|
@@ -1339,7 +1306,7 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1339
1306
|
recordJson
|
|
1340
1307
|
}))));
|
|
1341
1308
|
const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
|
|
1342
|
-
|
|
1309
|
+
threadId: validated.threadId,
|
|
1343
1310
|
batchId: validated.batch.batchId,
|
|
1344
1311
|
batchDigest: tailDigest,
|
|
1345
1312
|
batchJson,
|
|
@@ -1351,15 +1318,15 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1351
1318
|
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
1352
1319
|
yield* hitFailpoint("append:before");
|
|
1353
1320
|
const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
|
|
1354
|
-
if (isDoFenceRejected(error)) return mapFence(validated.
|
|
1321
|
+
if (isDoFenceRejected(error)) return mapFence(validated.threadId, error);
|
|
1355
1322
|
if (isDoAppendConflict(error)) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
|
|
1356
|
-
|
|
1323
|
+
threadId: validated.threadId,
|
|
1357
1324
|
batchId: validated.batch.batchId,
|
|
1358
1325
|
reason: error.reason,
|
|
1359
1326
|
actualTailSequence: error.actualTailSequence,
|
|
1360
1327
|
actualTailDigest: error.actualTailDigest
|
|
1361
1328
|
}) : AppendConflict.make({
|
|
1362
|
-
|
|
1329
|
+
threadId: validated.threadId,
|
|
1363
1330
|
batchId: validated.batch.batchId,
|
|
1364
1331
|
reason: error.reason
|
|
1365
1332
|
});
|
|
@@ -1368,30 +1335,30 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1368
1335
|
yield* hitFailpoint("append:after");
|
|
1369
1336
|
return result;
|
|
1370
1337
|
});
|
|
1371
|
-
const loadRecords = Effect.fn("
|
|
1338
|
+
const loadRecords = Effect.fn("DoThreadStore.loadRecords")(function* (request) {
|
|
1372
1339
|
const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
|
|
1373
1340
|
return yield* Effect.forEach(rows, decodeEnvelope);
|
|
1374
1341
|
});
|
|
1375
|
-
const readEffect = Effect.fn("
|
|
1376
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1377
|
-
yield*
|
|
1342
|
+
const readEffect = Effect.fn("DoThreadStore.read")(function* (request) {
|
|
1343
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread read", error)));
|
|
1344
|
+
yield* requireThread(journal, validated.threadId);
|
|
1378
1345
|
const records = yield* loadRecords(RawReadRequest.make({
|
|
1379
|
-
|
|
1346
|
+
threadId: validated.threadId,
|
|
1380
1347
|
fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
|
|
1381
1348
|
limit: validated.limit
|
|
1382
1349
|
}));
|
|
1383
1350
|
return Stream.fromIterable(records);
|
|
1384
1351
|
});
|
|
1385
1352
|
const read = (request) => Stream.unwrap(readEffect(request));
|
|
1386
|
-
const observeEffect = Effect.fn("
|
|
1387
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1388
|
-
yield*
|
|
1389
|
-
const initialSequence = yield* parseOffset(validated.
|
|
1353
|
+
const observeEffect = Effect.fn("DoThreadStore.observe")(function* (request) {
|
|
1354
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
|
|
1355
|
+
yield* requireThread(journal, validated.threadId);
|
|
1356
|
+
const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
|
|
1390
1357
|
const cursor = yield* Ref.make(initialSequence);
|
|
1391
|
-
const poll = Effect.fn("
|
|
1358
|
+
const poll = Effect.fn("DoThreadStore.observePoll")(function* () {
|
|
1392
1359
|
const fromSequenceExclusive = yield* Ref.get(cursor);
|
|
1393
1360
|
const records = yield* loadRecords(RawReadRequest.make({
|
|
1394
|
-
|
|
1361
|
+
threadId: validated.threadId,
|
|
1395
1362
|
fromSequenceExclusive,
|
|
1396
1363
|
limit: 1024
|
|
1397
1364
|
}));
|
|
@@ -1405,96 +1372,98 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1405
1372
|
return Stream.fromIterableEffectRepeat(poll());
|
|
1406
1373
|
});
|
|
1407
1374
|
const observe = (request) => Stream.unwrap(observeEffect(request));
|
|
1408
|
-
const
|
|
1409
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1410
|
-
yield*
|
|
1411
|
-
const exported = yield* journal.
|
|
1375
|
+
const exportThread = Effect.fn("DoThreadStore.export")(function* (request) {
|
|
1376
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
|
|
1377
|
+
yield* requireThread(journal, validated.threadId);
|
|
1378
|
+
const exported = yield* journal.exportThread(validated.threadId).pipe(Effect.mapError((error) => storeError("export thread", error)));
|
|
1412
1379
|
const records = yield* Effect.forEach(exported.records, decodeEnvelope);
|
|
1413
|
-
if (records.length > 65536) return yield*
|
|
1414
|
-
operation: "decode
|
|
1415
|
-
message: "The
|
|
1380
|
+
if (records.length > 65536) return yield* ThreadStoreError.make({
|
|
1381
|
+
operation: "decode thread export",
|
|
1382
|
+
message: "The thread exceeds the current export record limit."
|
|
1416
1383
|
});
|
|
1417
|
-
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.
|
|
1418
|
-
return
|
|
1419
|
-
format: "effect-agent/
|
|
1420
|
-
|
|
1421
|
-
tailSequence: exported.
|
|
1384
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
|
|
1385
|
+
return ThreadExport.make({
|
|
1386
|
+
format: "effect-agent/thread@1",
|
|
1387
|
+
threadId: validated.threadId,
|
|
1388
|
+
tailSequence: exported.thread.tail_sequence,
|
|
1422
1389
|
tailDigest,
|
|
1423
1390
|
records
|
|
1424
1391
|
});
|
|
1425
1392
|
});
|
|
1426
|
-
const inspectTail = Effect.fn("
|
|
1427
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1428
|
-
const
|
|
1429
|
-
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(
|
|
1430
|
-
return
|
|
1431
|
-
|
|
1432
|
-
tailSequence:
|
|
1393
|
+
const inspectTail = Effect.fn("DoThreadStore.inspectTail")(function* (request) {
|
|
1394
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
|
|
1395
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
1396
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
|
|
1397
|
+
return ThreadTail.make({
|
|
1398
|
+
threadId: validated.threadId,
|
|
1399
|
+
tailSequence: thread.tail_sequence,
|
|
1433
1400
|
tailDigest,
|
|
1434
|
-
producerEpoch:
|
|
1401
|
+
producerEpoch: thread.producer_epoch
|
|
1435
1402
|
});
|
|
1436
1403
|
});
|
|
1437
|
-
const saveCheckpoint = Effect.fn("
|
|
1404
|
+
const saveCheckpoint = Effect.fn("DoThreadStore.saveCheckpoint")(function* (request) {
|
|
1438
1405
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
|
|
1439
|
-
const
|
|
1440
|
-
if (validated.checkpoint.throughSequence >
|
|
1441
|
-
|
|
1406
|
+
const thread = yield* requireThread(journal, validated.checkpoint.threadId);
|
|
1407
|
+
if (validated.checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
|
|
1408
|
+
threadId: validated.checkpoint.threadId,
|
|
1442
1409
|
reason: "ahead-of-tail"
|
|
1443
1410
|
});
|
|
1444
|
-
if ((yield* tailDigestAt(journal, validated.checkpoint.
|
|
1445
|
-
|
|
1411
|
+
if ((yield* tailDigestAt(journal, validated.checkpoint.threadId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
|
|
1412
|
+
threadId: validated.checkpoint.threadId,
|
|
1446
1413
|
reason: "digest-mismatch"
|
|
1447
1414
|
});
|
|
1448
1415
|
const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
|
|
1449
1416
|
const raw = RawCheckpoint.make({
|
|
1450
|
-
|
|
1417
|
+
threadId: validated.checkpoint.threadId,
|
|
1451
1418
|
throughSequence: validated.checkpoint.throughSequence,
|
|
1452
1419
|
tailDigest: validated.checkpoint.tailDigest,
|
|
1453
1420
|
checkpointJson
|
|
1454
1421
|
});
|
|
1455
1422
|
yield* hitFailpoint("save-checkpoint:before");
|
|
1456
1423
|
yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => isDoCheckpointConflict(error) ? CheckpointRejected.make({
|
|
1457
|
-
|
|
1424
|
+
threadId: validated.checkpoint.threadId,
|
|
1458
1425
|
reason: "digest-mismatch"
|
|
1459
1426
|
}) : storeError("save checkpoint", error)));
|
|
1460
1427
|
yield* hitFailpoint("save-checkpoint:after");
|
|
1461
1428
|
});
|
|
1462
|
-
const loadCheckpoint = Effect.fn("
|
|
1429
|
+
const loadCheckpoint = Effect.fn("DoThreadStore.loadCheckpoint")(function* (request) {
|
|
1463
1430
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
|
|
1464
|
-
const
|
|
1465
|
-
const rows = yield* journal.loadCheckpoint(validated.
|
|
1431
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
1432
|
+
const rows = yield* journal.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
|
|
1466
1433
|
if (rows.length === 0) return Option.none();
|
|
1467
|
-
if (rows.length !== 1) return yield*
|
|
1434
|
+
if (rows.length !== 1) return yield* ThreadStoreError.make({
|
|
1468
1435
|
operation: "load checkpoint",
|
|
1469
1436
|
message: `Expected at most one checkpoint row but found ${rows.length}.`
|
|
1470
1437
|
});
|
|
1471
1438
|
const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
|
|
1472
|
-
if ((yield* tailDigestAt(journal, checkpoint.
|
|
1473
|
-
|
|
1439
|
+
if ((yield* tailDigestAt(journal, checkpoint.threadId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
|
|
1440
|
+
threadId: checkpoint.threadId,
|
|
1474
1441
|
reason: "digest-mismatch"
|
|
1475
1442
|
});
|
|
1476
1443
|
return Option.some(checkpoint);
|
|
1477
1444
|
});
|
|
1478
|
-
const
|
|
1445
|
+
const threadStore = ThreadStore.of({
|
|
1479
1446
|
append,
|
|
1480
|
-
export:
|
|
1447
|
+
export: exportThread,
|
|
1481
1448
|
inspectTail,
|
|
1482
|
-
loadCheckpoint,
|
|
1483
1449
|
materialize,
|
|
1484
1450
|
observe,
|
|
1485
1451
|
read,
|
|
1486
|
-
|
|
1452
|
+
checkpoints: {
|
|
1453
|
+
save: saveCheckpoint,
|
|
1454
|
+
load: loadCheckpoint
|
|
1455
|
+
}
|
|
1487
1456
|
});
|
|
1488
|
-
return Context.make(
|
|
1457
|
+
return Context.make(ThreadStore, threadStore);
|
|
1489
1458
|
});
|
|
1490
1459
|
/**
|
|
1491
|
-
* Durable Object
|
|
1460
|
+
* Durable Object Thread Store implementation with configuration, failpoint, SQL, and
|
|
1492
1461
|
* Crypto authority kept visible in its input channel.
|
|
1493
1462
|
*/
|
|
1494
|
-
const
|
|
1463
|
+
const threadStoreLayer = Layer.effectContext(makeServices$2());
|
|
1495
1464
|
/**
|
|
1496
1465
|
* Validated Durable Object storage configuration Layer with the documented defaults applied.
|
|
1497
|
-
* Shared by the
|
|
1466
|
+
* Shared by the ThreadStore and SubmissionLedger convenience layers so their defaults
|
|
1498
1467
|
* cannot drift.
|
|
1499
1468
|
*/
|
|
1500
1469
|
const storageConfigLayer = (options) => Layer.effect(DoStorageConfig)(Schema.decodeUnknownEffect(DoStorageConfigValue)({
|
|
@@ -1510,12 +1479,12 @@ const storageConfigLayer = (options) => Layer.effect(DoStorageConfig)(Schema.dec
|
|
|
1510
1479
|
/** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
|
|
1511
1480
|
const storageFailpointLayer = (options) => options.failpoint === void 0 ? DoStorageFailpoint.layer : Layer.succeed(DoStorageFailpoint)({ hit: options.failpoint });
|
|
1512
1481
|
/**
|
|
1513
|
-
* A composition-root convenience Layer for canonical
|
|
1482
|
+
* A composition-root convenience Layer for canonical Threads inside one Durable Object,
|
|
1514
1483
|
* built over `ctx.storage`. Durable accepted work is served by the separate SubmissionLedger
|
|
1515
1484
|
* port; point both at the SAME `ctx.storage` so claims fence the same producer epochs
|
|
1516
1485
|
* (ADR-0011 D7's "same file" rule, transposed to one object's private database).
|
|
1517
1486
|
*/
|
|
1518
|
-
const layer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) =>
|
|
1487
|
+
const layer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) => threadStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(DoStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
|
|
1519
1488
|
/** Create an adapter-owned resumable observation offset for a known canonical sequence. */
|
|
1520
1489
|
const observationOffsetAt = makeOffset;
|
|
1521
1490
|
//#endregion
|
|
@@ -1537,7 +1506,7 @@ const WOKEN = "woken";
|
|
|
1537
1506
|
const MAX_IDENTIFIER_LENGTH = 1024;
|
|
1538
1507
|
var SubmissionRow = class extends Schema.Class("SubmissionRow")({
|
|
1539
1508
|
submission_id: BoundedIdentifier,
|
|
1540
|
-
|
|
1509
|
+
thread_id: BoundedIdentifier,
|
|
1541
1510
|
queue_sequence: QueueSequence,
|
|
1542
1511
|
principal: BoundedIdentifier,
|
|
1543
1512
|
idempotency_key: BoundedIdentifier,
|
|
@@ -1625,7 +1594,7 @@ var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ ma
|
|
|
1625
1594
|
var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
|
|
1626
1595
|
const SUBMISSION_COLUMNS = `
|
|
1627
1596
|
submission_id,
|
|
1628
|
-
|
|
1597
|
+
thread_id,
|
|
1629
1598
|
queue_sequence,
|
|
1630
1599
|
principal,
|
|
1631
1600
|
idempotency_key,
|
|
@@ -1662,7 +1631,7 @@ const CHILD_RESERVATION_COLUMNS = `
|
|
|
1662
1631
|
release_began_at,
|
|
1663
1632
|
released_at
|
|
1664
1633
|
`;
|
|
1665
|
-
/** The branded ToolCallId schema, reached through the
|
|
1634
|
+
/** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
|
|
1666
1635
|
const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
|
|
1667
1636
|
const ToolCallIdList = Schema.Array(ToolCallIdSchema);
|
|
1668
1637
|
const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
|
|
@@ -1736,7 +1705,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1736
1705
|
iso: new Date(millis).toISOString()
|
|
1737
1706
|
}));
|
|
1738
1707
|
const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
|
|
1739
|
-
const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$
|
|
1708
|
+
const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1740
1709
|
const readSubmission = Effect.fn("DoSubmissionLedger.readSubmission")(function* (operation, submissionId) {
|
|
1741
1710
|
const rows = yield* sql`
|
|
1742
1711
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
@@ -1767,23 +1736,23 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1767
1736
|
FROM effect_agent_submission_ownership
|
|
1768
1737
|
WHERE submission_id = ${submissionId}
|
|
1769
1738
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1770
|
-
const decoded = yield* decodeRows$
|
|
1739
|
+
const decoded = yield* decodeRows$2(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1771
1740
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
|
|
1772
1741
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1773
1742
|
});
|
|
1774
|
-
const
|
|
1775
|
-
const
|
|
1776
|
-
return
|
|
1743
|
+
const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (operation, threadId) {
|
|
1744
|
+
const threads = yield* journal.getThread(threadId).pipe(Effect.mapError(internalFailure(operation)));
|
|
1745
|
+
return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
|
|
1777
1746
|
});
|
|
1778
1747
|
/**
|
|
1779
1748
|
* Verify inside the surrounding write transaction that the presented token still owns the
|
|
1780
1749
|
* Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
|
|
1781
|
-
*
|
|
1750
|
+
* Thread's current producer epoch (DUR-006).
|
|
1782
1751
|
*/
|
|
1783
1752
|
const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
|
|
1784
1753
|
const ownership = yield* readOwnership(operation, submission.submission_id);
|
|
1785
1754
|
if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
|
|
1786
|
-
const actualEpoch = yield*
|
|
1755
|
+
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
1787
1756
|
const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
1788
1757
|
return yield* OwnershipLost.make({
|
|
1789
1758
|
submissionId,
|
|
@@ -1798,7 +1767,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1798
1767
|
if (row.parent_submission_id === null !== (row.parent_tool_call_id === null)) return yield* corruptionFailure(operation, "effect_agent_submissions", row.submission_id, "A parent linkage must record both the parent Submission and the parent Tool Call.");
|
|
1799
1768
|
return yield* decodeSubmissionSnapshotUnknown({
|
|
1800
1769
|
submissionId: row.submission_id,
|
|
1801
|
-
|
|
1770
|
+
threadId: row.thread_id,
|
|
1802
1771
|
queueSequence: row.queue_sequence,
|
|
1803
1772
|
principal: row.principal,
|
|
1804
1773
|
idempotencyKey: row.idempotency_key,
|
|
@@ -1832,7 +1801,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1832
1801
|
FROM effect_agent_settlement_reservations
|
|
1833
1802
|
WHERE submission_id = ${submissionId}
|
|
1834
1803
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1835
|
-
const decoded = yield* decodeRows$
|
|
1804
|
+
const decoded = yield* decodeRows$2(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1836
1805
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
|
|
1837
1806
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1838
1807
|
});
|
|
@@ -1847,7 +1816,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1847
1816
|
FROM effect_agent_abort_intents
|
|
1848
1817
|
WHERE submission_id = ${submissionId}
|
|
1849
1818
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1850
|
-
const decoded = yield* decodeRows$
|
|
1819
|
+
const decoded = yield* decodeRows$2(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1851
1820
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
|
|
1852
1821
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1853
1822
|
});
|
|
@@ -1867,7 +1836,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1867
1836
|
WHERE parent_submission_id = ${parentSubmissionId}
|
|
1868
1837
|
ORDER BY child_submission_id ASC
|
|
1869
1838
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1870
|
-
return yield* decodeRows$
|
|
1839
|
+
return yield* decodeRows$2(Schema.Array(ChildSettlementMarkerRow), "effect_agent_child_settlements", parentSubmissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1871
1840
|
});
|
|
1872
1841
|
/**
|
|
1873
1842
|
* Whether one listed child is provably settled from THIS store: either its own row lives
|
|
@@ -1880,7 +1849,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1880
1849
|
const childRow = yield* readSubmission(operation, childSubmissionId);
|
|
1881
1850
|
return Option.isSome(childRow) && childRow.value.state === "settled";
|
|
1882
1851
|
});
|
|
1883
|
-
const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$
|
|
1852
|
+
const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1884
1853
|
const readChildReservation = Effect.fn("DoSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
|
|
1885
1854
|
const rows = yield* sql`
|
|
1886
1855
|
SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
|
|
@@ -1933,7 +1902,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1933
1902
|
WHERE submission_id = ${submissionId}
|
|
1934
1903
|
ORDER BY tool_call_id ASC
|
|
1935
1904
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1936
|
-
return yield* decodeRows$
|
|
1905
|
+
return yield* decodeRows$2(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1937
1906
|
});
|
|
1938
1907
|
const approvalIntentFromRow = Effect.fn("DoSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
|
|
1939
1908
|
return yield* decodeApprovalDecisionIntent({
|
|
@@ -1958,7 +1927,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1958
1927
|
WHERE submission_id = ${submissionId}
|
|
1959
1928
|
ORDER BY tool_call_id ASC
|
|
1960
1929
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1961
|
-
return yield* decodeRows$
|
|
1930
|
+
return yield* decodeRows$2(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1962
1931
|
});
|
|
1963
1932
|
const unknownResolutionIntentFromRow = Effect.fn("DoSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
|
|
1964
1933
|
const resolution = yield* parseStoredJsonText(row.resolution_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_unknown_resolutions", `${row.submission_id}/${row.tool_call_id}`, error.message)));
|
|
@@ -1981,18 +1950,18 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1981
1950
|
* derived from the shared canonical-records table using the deterministic abort record
|
|
1982
1951
|
* identity, never from a cached ledger marker.
|
|
1983
1952
|
*/
|
|
1984
|
-
const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (operation,
|
|
1953
|
+
const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (operation, threadId, submissionId) {
|
|
1985
1954
|
const recordId = submissionAbortRecordId(submissionId);
|
|
1986
1955
|
const rows = yield* sql`
|
|
1987
1956
|
SELECT record_id
|
|
1988
1957
|
FROM effect_agent_canonical_records
|
|
1989
|
-
WHERE
|
|
1958
|
+
WHERE thread_id = ${threadId}
|
|
1990
1959
|
AND record_id = ${recordId}
|
|
1991
1960
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1992
|
-
return (yield* decodeRows$
|
|
1961
|
+
return (yield* decodeRows$2(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${threadId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
|
|
1993
1962
|
});
|
|
1994
1963
|
const abortIntentFromRow = Effect.fn("DoSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
|
|
1995
|
-
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.
|
|
1964
|
+
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, submissionId);
|
|
1996
1965
|
return yield* decodeAbortIntent({
|
|
1997
1966
|
submissionId: row.submission_id,
|
|
1998
1967
|
author: row.author,
|
|
@@ -2008,19 +1977,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2008
1977
|
const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(Effect.mapError(internalFailure(operation)));
|
|
2009
1978
|
yield* journal.checkValueBound(operation, inputJson).pipe(Effect.mapError(internalFailure(operation)));
|
|
2010
1979
|
const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(Effect.mapError(internalFailure(operation)));
|
|
2011
|
-
const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.
|
|
1980
|
+
const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
|
|
2012
1981
|
if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) return yield* LedgerError.make({
|
|
2013
1982
|
operation,
|
|
2014
|
-
message: `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the
|
|
1983
|
+
message: `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`
|
|
2015
1984
|
});
|
|
2016
1985
|
const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
|
|
2017
1986
|
yield* hitFailpoint("ledger:admit:before", operation);
|
|
2018
1987
|
const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
|
|
2019
|
-
const keyRowKey = `${validated.
|
|
1988
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
2020
1989
|
const existingRows = yield* sql`
|
|
2021
1990
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2022
1991
|
FROM effect_agent_submissions
|
|
2023
|
-
WHERE
|
|
1992
|
+
WHERE thread_id = ${validated.threadId}
|
|
2024
1993
|
AND principal = ${validated.principal}
|
|
2025
1994
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2026
1995
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
@@ -2029,7 +1998,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2029
1998
|
if (existing.length === 1) {
|
|
2030
1999
|
const sameLinkage = validated.parentLinkage === void 0 ? existing[0].parent_submission_id === null && existing[0].parent_tool_call_id === null : existing[0].parent_submission_id === validated.parentLinkage.parentSubmissionId && existing[0].parent_tool_call_id === validated.parentLinkage.parentToolCallId;
|
|
2031
2000
|
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
|
|
2032
|
-
|
|
2001
|
+
threadId: validated.threadId,
|
|
2033
2002
|
principal: validated.principal,
|
|
2034
2003
|
idempotencyKey: validated.idempotencyKey,
|
|
2035
2004
|
existingInputDigest: existing[0].input_digest,
|
|
@@ -2046,15 +2015,15 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2046
2015
|
const maxRows = yield* sql`
|
|
2047
2016
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
2048
2017
|
FROM effect_agent_submissions
|
|
2049
|
-
WHERE
|
|
2018
|
+
WHERE thread_id = ${validated.threadId}
|
|
2050
2019
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2051
|
-
const decodedMax = yield* decodeRows$
|
|
2020
|
+
const decodedMax = yield* decodeRows$2(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.threadId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
2052
2021
|
const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
|
|
2053
2022
|
const now = yield* currentInstant;
|
|
2054
2023
|
yield* sql`
|
|
2055
2024
|
INSERT INTO effect_agent_submissions (
|
|
2056
2025
|
submission_id,
|
|
2057
|
-
|
|
2026
|
+
thread_id,
|
|
2058
2027
|
queue_sequence,
|
|
2059
2028
|
principal,
|
|
2060
2029
|
idempotency_key,
|
|
@@ -2070,7 +2039,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2070
2039
|
parent_tool_call_id
|
|
2071
2040
|
) VALUES (
|
|
2072
2041
|
${mintedSubmissionId},
|
|
2073
|
-
${validated.
|
|
2042
|
+
${validated.threadId},
|
|
2074
2043
|
${queueSequence},
|
|
2075
2044
|
${validated.principal},
|
|
2076
2045
|
${validated.idempotencyKey},
|
|
@@ -2123,12 +2092,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2123
2092
|
const rows = yield* sql`
|
|
2124
2093
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2125
2094
|
FROM effect_agent_submissions
|
|
2126
|
-
WHERE
|
|
2095
|
+
WHERE thread_id = ${validated.threadId}
|
|
2127
2096
|
AND principal = ${validated.principal}
|
|
2128
2097
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2129
2098
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2130
|
-
const decoded = yield* decodeSubmissionRows(operation, `${validated.
|
|
2131
|
-
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.
|
|
2099
|
+
const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
|
|
2100
|
+
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
|
|
2132
2101
|
if (decoded.length === 0) return Option.none();
|
|
2133
2102
|
return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
|
|
2134
2103
|
});
|
|
@@ -2138,12 +2107,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2138
2107
|
const rows = yield* sql`
|
|
2139
2108
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2140
2109
|
FROM effect_agent_submissions
|
|
2141
|
-
WHERE
|
|
2110
|
+
WHERE thread_id = ${validated.threadId}
|
|
2142
2111
|
AND principal = ${validated.principal}
|
|
2143
2112
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2144
2113
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2145
|
-
const decoded = yield* decodeSubmissionRows(operation, `${validated.
|
|
2146
|
-
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.
|
|
2114
|
+
const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
|
|
2115
|
+
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, "An admission idempotency key returned more than one row.");
|
|
2147
2116
|
if (decoded.length === 0) return AdmissionNotAdmitted.make();
|
|
2148
2117
|
return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
|
|
2149
2118
|
});
|
|
@@ -2157,12 +2126,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2157
2126
|
const headRows = yield* sql`
|
|
2158
2127
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2159
2128
|
FROM effect_agent_submissions
|
|
2160
|
-
WHERE
|
|
2129
|
+
WHERE thread_id = ${validated.threadId}
|
|
2161
2130
|
AND state <> 'settled'
|
|
2162
2131
|
ORDER BY queue_sequence ASC
|
|
2163
2132
|
LIMIT 1
|
|
2164
2133
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2165
|
-
const heads = yield* decodeSubmissionRows(operation, validated.
|
|
2134
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
2166
2135
|
if (heads.length === 0) return Option.none();
|
|
2167
2136
|
const head = heads[0];
|
|
2168
2137
|
if (head.state === "joining" || head.state === "joined" || head.state === "suspended" || head.state === "unknown" && Option.isNone(yield* readAbortIntent(operation, head.submission_id))) return Option.none();
|
|
@@ -2171,19 +2140,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2171
2140
|
if (Option.isSome(ownership)) {
|
|
2172
2141
|
if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
|
|
2173
2142
|
}
|
|
2174
|
-
const
|
|
2143
|
+
const threads = yield* journal.getThread(head.thread_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
2175
2144
|
let producerEpoch;
|
|
2176
|
-
if (
|
|
2145
|
+
if (threads.length === 0) {
|
|
2177
2146
|
producerEpoch = 1;
|
|
2178
2147
|
yield* sql`
|
|
2179
|
-
INSERT INTO
|
|
2180
|
-
|
|
2148
|
+
INSERT INTO effect_agent_threads (
|
|
2149
|
+
thread_id,
|
|
2181
2150
|
created_at,
|
|
2182
2151
|
tail_sequence,
|
|
2183
2152
|
tail_digest,
|
|
2184
2153
|
producer_epoch
|
|
2185
2154
|
) VALUES (
|
|
2186
|
-
${head.
|
|
2155
|
+
${head.thread_id},
|
|
2187
2156
|
${now.iso},
|
|
2188
2157
|
0,
|
|
2189
2158
|
${EMPTY_TAIL_DIGEST},
|
|
@@ -2191,11 +2160,11 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2191
2160
|
)
|
|
2192
2161
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2193
2162
|
} else {
|
|
2194
|
-
producerEpoch =
|
|
2163
|
+
producerEpoch = threads[0].producer_epoch + 1;
|
|
2195
2164
|
yield* sql`
|
|
2196
|
-
UPDATE
|
|
2165
|
+
UPDATE effect_agent_threads
|
|
2197
2166
|
SET producer_epoch = ${producerEpoch}
|
|
2198
|
-
WHERE
|
|
2167
|
+
WHERE thread_id = ${head.thread_id}
|
|
2199
2168
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2200
2169
|
}
|
|
2201
2170
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
@@ -2226,14 +2195,14 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2226
2195
|
INSERT INTO effect_agent_attempts (
|
|
2227
2196
|
attempt_id,
|
|
2228
2197
|
submission_id,
|
|
2229
|
-
|
|
2198
|
+
thread_id,
|
|
2230
2199
|
owner_producer_id,
|
|
2231
2200
|
producer_epoch,
|
|
2232
2201
|
claimed_at
|
|
2233
2202
|
) VALUES (
|
|
2234
2203
|
${attemptId},
|
|
2235
2204
|
${head.submission_id},
|
|
2236
|
-
${head.
|
|
2205
|
+
${head.thread_id},
|
|
2237
2206
|
${validated.producerId},
|
|
2238
2207
|
${producerEpoch},
|
|
2239
2208
|
${now.iso}
|
|
@@ -2494,7 +2463,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2494
2463
|
${now.iso}
|
|
2495
2464
|
)
|
|
2496
2465
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2497
|
-
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.
|
|
2466
|
+
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, validated.submissionId);
|
|
2498
2467
|
return yield* decodeAbortIntent({
|
|
2499
2468
|
submissionId: validated.submissionId,
|
|
2500
2469
|
author: validated.author,
|
|
@@ -2512,19 +2481,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2512
2481
|
yield* hitFailpoint("ledger:claim-joining:before", operation);
|
|
2513
2482
|
const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
|
|
2514
2483
|
const host = yield* requireSubmission(operation, validated.hostSubmissionId);
|
|
2515
|
-
if (host.
|
|
2484
|
+
if (host.thread_id !== validated.threadId) return yield* LedgerError.make({
|
|
2516
2485
|
operation,
|
|
2517
|
-
message: `Host submission ${validated.hostSubmissionId} does not belong to
|
|
2486
|
+
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`
|
|
2518
2487
|
});
|
|
2519
2488
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
2520
2489
|
const laterRows = yield* sql`
|
|
2521
2490
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2522
2491
|
FROM effect_agent_submissions
|
|
2523
|
-
WHERE
|
|
2492
|
+
WHERE thread_id = ${validated.threadId}
|
|
2524
2493
|
AND queue_sequence > ${host.queue_sequence}
|
|
2525
2494
|
ORDER BY queue_sequence ASC
|
|
2526
2495
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2527
|
-
const later = yield* decodeSubmissionRows(operation, validated.
|
|
2496
|
+
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
2528
2497
|
const claimed = [];
|
|
2529
2498
|
for (const row of later) {
|
|
2530
2499
|
if (claimed.length >= validated.maxCount) break;
|
|
@@ -3034,27 +3003,27 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
3034
3003
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
3035
3004
|
FROM effect_agent_submissions
|
|
3036
3005
|
WHERE state <> 'settled'
|
|
3037
|
-
ORDER BY
|
|
3006
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
3038
3007
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
3039
3008
|
` : sql`
|
|
3040
3009
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
3041
3010
|
FROM effect_agent_submissions
|
|
3042
3011
|
WHERE state <> 'settled'
|
|
3043
3012
|
AND (
|
|
3044
|
-
|
|
3013
|
+
thread_id > ${cursor.threadId}
|
|
3045
3014
|
OR (
|
|
3046
|
-
|
|
3015
|
+
thread_id = ${cursor.threadId}
|
|
3047
3016
|
AND queue_sequence > ${cursor.queueSequence}
|
|
3048
3017
|
)
|
|
3049
3018
|
)
|
|
3050
|
-
ORDER BY
|
|
3019
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
3051
3020
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
3052
3021
|
`).pipe(Effect.mapError(sqlFailure(operation)));
|
|
3053
3022
|
const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
|
|
3054
3023
|
const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
|
|
3055
3024
|
const last = decoded[decoded.length - 1];
|
|
3056
3025
|
return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
|
|
3057
|
-
|
|
3026
|
+
threadId: last.thread_id,
|
|
3058
3027
|
queueSequence: last.queue_sequence
|
|
3059
3028
|
})];
|
|
3060
3029
|
});
|
|
@@ -3211,12 +3180,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
3211
3180
|
const submissionLedgerLayer = Layer.effectContext(makeServices$1());
|
|
3212
3181
|
/**
|
|
3213
3182
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
3214
|
-
* same `ctx.storage` as the
|
|
3183
|
+
* same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
|
|
3215
3184
|
*/
|
|
3216
3185
|
const ledgerLayer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(DoStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ storage: options.storage }), BrowserCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
|
|
3217
3186
|
//#endregion
|
|
3218
3187
|
//#region src/do-schedule-store.ts
|
|
3219
|
-
const CURRENT_SCHEDULE_STORE_VERSION =
|
|
3188
|
+
const CURRENT_SCHEDULE_STORE_VERSION = 2;
|
|
3220
3189
|
const StoredScheduleJson = Schema.String.check(Schema.isMaxLength(19e5));
|
|
3221
3190
|
const StoredDeadline = Schema.NullOr(ScheduleInstant);
|
|
3222
3191
|
var ScheduleRow = class extends Schema.Class("@effect-agent/storage-cloudflare/ScheduleRow")({
|
|
@@ -3246,26 +3215,26 @@ var ScheduleTableNameRow = class extends Schema.Class("@effect-agent/storage-clo
|
|
|
3246
3215
|
var DoScheduleTransaction = class extends Context.Service()("@effect-agent/storage-cloudflare/DoScheduleTransaction") {};
|
|
3247
3216
|
/** Platform driver operations that use the same schedule-state and alarm transaction. */
|
|
3248
3217
|
var DoScheduleAlarmControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoScheduleAlarmControl") {};
|
|
3249
|
-
const unavailable = (operation) => ScheduleStorageError.make({
|
|
3218
|
+
const unavailable$1 = (operation) => ScheduleStorageError.make({
|
|
3250
3219
|
operation,
|
|
3251
3220
|
reason: "unavailable"
|
|
3252
3221
|
});
|
|
3253
|
-
const corrupt = (operation) => ScheduleStorageError.make({
|
|
3222
|
+
const corrupt$1 = (operation) => ScheduleStorageError.make({
|
|
3254
3223
|
operation,
|
|
3255
3224
|
reason: "corrupt"
|
|
3256
3225
|
});
|
|
3257
|
-
const decodeRows = Effect.fn("DoScheduleStore.decodeRows")(function* (schema, rows, operation) {
|
|
3258
|
-
return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3226
|
+
const decodeRows$1 = Effect.fn("DoScheduleStore.decodeRows")(function* (schema, rows, operation) {
|
|
3227
|
+
return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3259
3228
|
});
|
|
3260
|
-
const decodeBoundary = (schema, value, operation) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3229
|
+
const decodeBoundary = (schema, value, operation) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3261
3230
|
const decodeRecord = Effect.fn("DoScheduleStore.decodeRecord")(function* (row) {
|
|
3262
|
-
const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt("decode schedule")));
|
|
3263
|
-
if (record.owner.tenantId !== row.tenant_id || record.owner.ownerId !== row.owner_id || record.scheduleId !== row.schedule_id || scheduleDeadline(record) !== row.deadline_at_millis) return yield* corrupt("decode schedule index");
|
|
3231
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt$1("decode schedule")));
|
|
3232
|
+
if (record.owner.tenantId !== row.tenant_id || record.owner.ownerId !== row.owner_id || record.scheduleId !== row.schedule_id || scheduleDeadline(record) !== row.deadline_at_millis) return yield* corrupt$1("decode schedule index");
|
|
3264
3233
|
return record;
|
|
3265
3234
|
});
|
|
3266
3235
|
const encodeRecord = Effect.fn("DoScheduleStore.encodeRecord")(function* (record) {
|
|
3267
|
-
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt("encode schedule")));
|
|
3268
|
-
return yield* Schema.decodeUnknownEffect(StoredScheduleJson)(encoded).pipe(Effect.mapError(() => corrupt("encode schedule bounds")));
|
|
3236
|
+
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt$1("encode schedule")));
|
|
3237
|
+
return yield* Schema.decodeUnknownEffect(StoredScheduleJson)(encoded).pipe(Effect.mapError(() => corrupt$1("encode schedule bounds")));
|
|
3269
3238
|
});
|
|
3270
3239
|
const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function* () {
|
|
3271
3240
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -3279,12 +3248,12 @@ const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function
|
|
|
3279
3248
|
'effect_agent_schedules'
|
|
3280
3249
|
)
|
|
3281
3250
|
ORDER BY name
|
|
3282
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3283
|
-
const tables = yield* decodeRows(Schema.Array(ScheduleTableNameRow), rawTables, operation);
|
|
3251
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3252
|
+
const tables = yield* decodeRows$1(Schema.Array(ScheduleTableNameRow), rawTables, operation);
|
|
3284
3253
|
const hasState = tables.some((row) => row.name === "effect_agent_schedule_store_state");
|
|
3285
3254
|
const hasSchedules = tables.some((row) => row.name === "effect_agent_schedules");
|
|
3286
3255
|
if (!hasState) {
|
|
3287
|
-
if (hasSchedules) return yield* corrupt(operation);
|
|
3256
|
+
if (hasSchedules) return yield* corrupt$1(operation);
|
|
3288
3257
|
yield* sql.withTransaction(Effect.gen(function* () {
|
|
3289
3258
|
yield* sql`
|
|
3290
3259
|
CREATE TABLE effect_agent_schedule_store_state (
|
|
@@ -3318,17 +3287,17 @@ const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function
|
|
|
3318
3287
|
singleton, storage_version, alarm_generation
|
|
3319
3288
|
) VALUES (1, ${CURRENT_SCHEDULE_STORE_VERSION}, 0)
|
|
3320
3289
|
`.withoutTransform;
|
|
3321
|
-
})).pipe(Effect.mapError(() => unavailable(operation)));
|
|
3290
|
+
})).pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3322
3291
|
return;
|
|
3323
3292
|
}
|
|
3324
|
-
if (!hasSchedules) return yield* corrupt(operation);
|
|
3293
|
+
if (!hasSchedules) return yield* corrupt$1(operation);
|
|
3325
3294
|
const rawState = yield* sql`
|
|
3326
3295
|
SELECT storage_version, alarm_generation
|
|
3327
3296
|
FROM effect_agent_schedule_store_state
|
|
3328
3297
|
WHERE singleton = 1
|
|
3329
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3330
|
-
const state = yield* decodeRows(Schema.Array(ScheduleStoreStateRow), rawState, operation);
|
|
3331
|
-
if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt(operation);
|
|
3298
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3299
|
+
const state = yield* decodeRows$1(Schema.Array(ScheduleStoreStateRow), rawState, operation);
|
|
3300
|
+
if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt$1(state.length === 1 ? `${operation}: incompatible storage version ${state[0].storage_version}; expected ${CURRENT_SCHEDULE_STORE_VERSION}` : `${operation}: invalid storage version row`);
|
|
3332
3301
|
});
|
|
3333
3302
|
const makeServices = Effect.gen(function* () {
|
|
3334
3303
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -3342,13 +3311,13 @@ const makeServices = Effect.gen(function* () {
|
|
|
3342
3311
|
WHERE tenant_id = ${key.owner.tenantId}
|
|
3343
3312
|
AND owner_id = ${key.owner.ownerId}
|
|
3344
3313
|
AND schedule_id = ${key.scheduleId}
|
|
3345
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3346
|
-
return yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
3314
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3315
|
+
return yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
|
|
3347
3316
|
});
|
|
3348
3317
|
const readOne = Effect.fn("DoScheduleStore.readOne")(function* (key, operation) {
|
|
3349
3318
|
const rows = yield* readRows(key, operation);
|
|
3350
3319
|
if (rows.length === 0) return null;
|
|
3351
|
-
if (rows.length !== 1) return yield* corrupt(operation);
|
|
3320
|
+
if (rows.length !== 1) return yield* corrupt$1(operation);
|
|
3352
3321
|
return yield* decodeRecord(rows[0]);
|
|
3353
3322
|
});
|
|
3354
3323
|
const readNextDeadline = Effect.fn("DoScheduleStore.readNextDeadline")(function* (owner, operation) {
|
|
@@ -3356,15 +3325,15 @@ const makeServices = Effect.gen(function* () {
|
|
|
3356
3325
|
SELECT MIN(deadline_at_millis) AS deadline_at_millis
|
|
3357
3326
|
FROM effect_agent_schedules
|
|
3358
3327
|
WHERE deadline_at_millis IS NOT NULL
|
|
3359
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3328
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3360
3329
|
SELECT MIN(deadline_at_millis) AS deadline_at_millis
|
|
3361
3330
|
FROM effect_agent_schedules
|
|
3362
3331
|
WHERE tenant_id = ${owner.tenantId}
|
|
3363
3332
|
AND owner_id = ${owner.ownerId}
|
|
3364
3333
|
AND deadline_at_millis IS NOT NULL
|
|
3365
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3366
|
-
const decoded = yield* decodeRows(Schema.Array(ScheduleDeadlineRow), rows, operation);
|
|
3367
|
-
if (decoded.length !== 1) return yield* corrupt(operation);
|
|
3334
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3335
|
+
const decoded = yield* decodeRows$1(Schema.Array(ScheduleDeadlineRow), rows, operation);
|
|
3336
|
+
if (decoded.length !== 1) return yield* corrupt$1(operation);
|
|
3368
3337
|
return decoded[0].deadline_at_millis;
|
|
3369
3338
|
});
|
|
3370
3339
|
const replaceAlarm = Effect.fn("DoScheduleStore.replaceAlarm")(function* (replace, deadlineAtMillis, operation) {
|
|
@@ -3373,9 +3342,9 @@ const makeServices = Effect.gen(function* () {
|
|
|
3373
3342
|
SET alarm_generation = alarm_generation + 1
|
|
3374
3343
|
WHERE singleton = 1
|
|
3375
3344
|
RETURNING storage_version, alarm_generation
|
|
3376
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3377
|
-
const state = yield* decodeRows(Schema.Array(ScheduleStoreStateRow), rawState, operation);
|
|
3378
|
-
if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt(operation);
|
|
3345
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3346
|
+
const state = yield* decodeRows$1(Schema.Array(ScheduleStoreStateRow), rawState, operation);
|
|
3347
|
+
if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt$1(operation);
|
|
3379
3348
|
yield* scheduleFailpoint.hit("schedule:alarm:before");
|
|
3380
3349
|
yield* replace({
|
|
3381
3350
|
deadlineAtMillis,
|
|
@@ -3406,9 +3375,9 @@ const makeServices = Effect.gen(function* () {
|
|
|
3406
3375
|
AND owner_id = ${canonical.owner.ownerId}
|
|
3407
3376
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
3408
3377
|
(json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
|
|
3409
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3410
|
-
const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3411
|
-
if (counts.length !== 1) return yield* corrupt(operation);
|
|
3378
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3379
|
+
const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3380
|
+
if (counts.length !== 1) return yield* corrupt$1(operation);
|
|
3412
3381
|
if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
3413
3382
|
yield* scheduleFailpoint.hit("schedule:insert:before");
|
|
3414
3383
|
yield* sql`
|
|
@@ -3421,7 +3390,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3421
3390
|
${scheduleDeadline(canonical)},
|
|
3422
3391
|
${recordJson}
|
|
3423
3392
|
)
|
|
3424
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3393
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3425
3394
|
const deadline = yield* readNextDeadline(void 0, operation);
|
|
3426
3395
|
yield* replaceAlarm(replace, deadline, operation);
|
|
3427
3396
|
return {
|
|
@@ -3446,7 +3415,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3446
3415
|
AND owner_id = ${request.owner.ownerId}
|
|
3447
3416
|
ORDER BY schedule_id
|
|
3448
3417
|
LIMIT ${request.limit + 1}
|
|
3449
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3418
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3450
3419
|
SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
|
|
3451
3420
|
FROM effect_agent_schedules
|
|
3452
3421
|
WHERE tenant_id = ${request.owner.tenantId}
|
|
@@ -3454,8 +3423,8 @@ const makeServices = Effect.gen(function* () {
|
|
|
3454
3423
|
AND schedule_id > ${request.after}
|
|
3455
3424
|
ORDER BY schedule_id
|
|
3456
3425
|
LIMIT ${request.limit + 1}
|
|
3457
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3458
|
-
const decoded = yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
3426
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3427
|
+
const decoded = yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
|
|
3459
3428
|
const records = yield* Effect.forEach(decoded, decodeRecord);
|
|
3460
3429
|
const hasNext = records.length > request.limit;
|
|
3461
3430
|
const items = hasNext ? records.slice(0, request.limit) : records;
|
|
@@ -3480,9 +3449,9 @@ const makeServices = Effect.gen(function* () {
|
|
|
3480
3449
|
WHERE tenant_id = ${key.owner.tenantId} AND owner_id = ${key.owner.ownerId}
|
|
3481
3450
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
3482
3451
|
(json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
|
|
3483
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3484
|
-
const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3485
|
-
if (counts.length !== 1) return yield* corrupt(operation);
|
|
3452
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3453
|
+
const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3454
|
+
if (counts.length !== 1) return yield* corrupt$1(operation);
|
|
3486
3455
|
if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
3487
3456
|
}
|
|
3488
3457
|
if (next === current) return {
|
|
@@ -3497,7 +3466,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3497
3466
|
WHERE tenant_id = ${canonicalKey.owner.tenantId}
|
|
3498
3467
|
AND owner_id = ${canonicalKey.owner.ownerId}
|
|
3499
3468
|
AND schedule_id = ${canonicalKey.scheduleId}
|
|
3500
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3469
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3501
3470
|
const deadline = yield* readNextDeadline(void 0, operation);
|
|
3502
3471
|
yield* replaceAlarm(replace, deadline, operation);
|
|
3503
3472
|
return {
|
|
@@ -3510,7 +3479,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3510
3479
|
});
|
|
3511
3480
|
const due = Effect.fn("DoScheduleStore.due")(function* (nowMillis, limit, owner, after) {
|
|
3512
3481
|
const operation = "query due schedules";
|
|
3513
|
-
const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3482
|
+
const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3514
3483
|
const continuation = cursor === void 0 ? sql`1 = 1` : sql`
|
|
3515
3484
|
(deadline_at_millis, tenant_id, owner_id, schedule_id) >
|
|
3516
3485
|
(${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
|
|
@@ -3520,7 +3489,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3520
3489
|
WHERE deadline_at_millis <= ${nowMillis} AND ${continuation}
|
|
3521
3490
|
ORDER BY deadline_at_millis, tenant_id, owner_id, schedule_id
|
|
3522
3491
|
LIMIT ${limit}
|
|
3523
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3492
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3524
3493
|
SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
|
|
3525
3494
|
FROM effect_agent_schedules
|
|
3526
3495
|
WHERE tenant_id = ${owner.tenantId}
|
|
@@ -3528,8 +3497,8 @@ const makeServices = Effect.gen(function* () {
|
|
|
3528
3497
|
AND deadline_at_millis <= ${nowMillis} AND ${continuation}
|
|
3529
3498
|
ORDER BY deadline_at_millis, schedule_id
|
|
3530
3499
|
LIMIT ${limit}
|
|
3531
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3532
|
-
return (yield* decodeRows(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
|
|
3500
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3501
|
+
return (yield* decodeRows$1(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
|
|
3533
3502
|
owner: {
|
|
3534
3503
|
tenantId: row.tenant_id,
|
|
3535
3504
|
ownerId: row.owner_id
|
|
@@ -3571,18 +3540,723 @@ const makeServices = Effect.gen(function* () {
|
|
|
3571
3540
|
*/
|
|
3572
3541
|
const scheduleStoreLayer = Layer.effectContext(makeServices);
|
|
3573
3542
|
//#endregion
|
|
3543
|
+
//#region src/do-subscription-store.ts
|
|
3544
|
+
const CURRENT_SUBSCRIPTION_STORE_VERSION = 2;
|
|
3545
|
+
const StoredJson = Schema.String.check(Schema.isMaxLength(19e5));
|
|
3546
|
+
const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
|
|
3547
|
+
const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
|
|
3548
|
+
const ScanRow = Schema.Struct({
|
|
3549
|
+
event_scan_cursor: Schema.String,
|
|
3550
|
+
delivery_scan_cursor: Schema.String,
|
|
3551
|
+
recovery_scan_cursor: Schema.Natural
|
|
3552
|
+
});
|
|
3553
|
+
const JsonRow = Schema.Struct({ record_json: StoredJson });
|
|
3554
|
+
const RegistrationRow = Schema.Struct({
|
|
3555
|
+
owner_id: Schema.String,
|
|
3556
|
+
subscription_id: Schema.String,
|
|
3557
|
+
ordinal: Schema.Natural,
|
|
3558
|
+
source_name: Schema.String,
|
|
3559
|
+
source_version: Schema.String,
|
|
3560
|
+
matching_key: Schema.String,
|
|
3561
|
+
state: SubscriptionRecord.fields.state,
|
|
3562
|
+
expires_at_millis: Schema.Number,
|
|
3563
|
+
recovery_at_millis: Schema.NullOr(Schema.Number),
|
|
3564
|
+
record_json: StoredJson
|
|
3565
|
+
});
|
|
3566
|
+
const EventRow = Schema.Struct({
|
|
3567
|
+
event_id: Schema.String,
|
|
3568
|
+
source_name: Schema.String,
|
|
3569
|
+
source_version: Schema.String,
|
|
3570
|
+
matching_key: Schema.String,
|
|
3571
|
+
payload_digest: Digest,
|
|
3572
|
+
cutoff: Schema.Natural,
|
|
3573
|
+
cursor: Schema.Natural,
|
|
3574
|
+
routing_complete: Schema.Number,
|
|
3575
|
+
next_attempt_at_millis: Schema.Number,
|
|
3576
|
+
record_json: StoredJson
|
|
3577
|
+
});
|
|
3578
|
+
const DeliveryRow = Schema.Struct({
|
|
3579
|
+
owner_id: Schema.String,
|
|
3580
|
+
subscription_id: Schema.String,
|
|
3581
|
+
event_id: Schema.String,
|
|
3582
|
+
delivery_key: Schema.String,
|
|
3583
|
+
state: SubscriptionDelivery.fields.state,
|
|
3584
|
+
next_attempt_at_millis: Schema.Number,
|
|
3585
|
+
record_json: StoredJson
|
|
3586
|
+
});
|
|
3587
|
+
const StoreStateRow = Schema.Struct({
|
|
3588
|
+
storage_version: Schema.Int,
|
|
3589
|
+
alarm_generation: Schema.Natural
|
|
3590
|
+
});
|
|
3591
|
+
/** One Durable Object transaction combining subscription SQL and its single native alarm. */
|
|
3592
|
+
var DoSubscriptionTransaction = class extends Context.Service()("@effect-agent/storage-cloudflare/DoSubscriptionTransaction") {};
|
|
3593
|
+
var DoSubscriptionAlarmControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoSubscriptionAlarmControl") {};
|
|
3594
|
+
const error = (reason, code) => SubscriptionError.make({
|
|
3595
|
+
reason,
|
|
3596
|
+
code
|
|
3597
|
+
});
|
|
3598
|
+
const unavailable = (operation) => error("storage", operation);
|
|
3599
|
+
const corrupt = (operation) => error("corrupt", operation);
|
|
3600
|
+
const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
3601
|
+
const validate = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
|
|
3602
|
+
const encode = (schema, value, code) => Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
|
|
3603
|
+
const decode = (schema, value, code) => Schema.decodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
|
|
3604
|
+
const decodeRows = (schema, rows, code) => Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
|
|
3605
|
+
const sameDeliveryIdentity = (left, right) => subscriptionDeliveryKeyString(left.key) === subscriptionDeliveryKeyString(right.key) && left.deliveryId === right.deliveryId && left.source.name === right.source.name && left.source.version === right.source.version && left.threadId === right.threadId && left.admissionKey === right.admissionKey && left.subscriptionFingerprint === right.subscriptionFingerprint && left.eventDigest === right.eventDigest;
|
|
3606
|
+
const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize")(function* () {
|
|
3607
|
+
const sql = yield* SqlClient.SqlClient;
|
|
3608
|
+
const names = yield* sql`
|
|
3609
|
+
SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'effect_agent_subscription_%'
|
|
3610
|
+
`.pipe(Effect.mapError(() => unavailable("inspect subscription storage")));
|
|
3611
|
+
const decodedNames = yield* decodeRows(Schema.Struct({ name: Schema.String }), names, "inspect subscription storage");
|
|
3612
|
+
const expected = /* @__PURE__ */ new Set([
|
|
3613
|
+
"effect_agent_subscription_store_state",
|
|
3614
|
+
"effect_agent_subscription_sequences",
|
|
3615
|
+
"effect_agent_subscriptions",
|
|
3616
|
+
"effect_agent_subscription_events",
|
|
3617
|
+
"effect_agent_subscription_deliveries"
|
|
3618
|
+
]);
|
|
3619
|
+
const hasState = decodedNames.some(({ name }) => name === "effect_agent_subscription_store_state");
|
|
3620
|
+
if (!hasState && decodedNames.length > 0) return yield* corrupt("partial subscription storage");
|
|
3621
|
+
if (!hasState) {
|
|
3622
|
+
yield* sql.withTransaction(Effect.gen(function* () {
|
|
3623
|
+
yield* sql`CREATE TABLE effect_agent_subscription_store_state (
|
|
3624
|
+
singleton INTEGER PRIMARY KEY NOT NULL CHECK(singleton=1), storage_version INTEGER NOT NULL, alarm_generation INTEGER NOT NULL
|
|
3625
|
+
)`.withoutTransform;
|
|
3626
|
+
yield* sql`CREATE TABLE effect_agent_subscription_sequences (
|
|
3627
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, sequence INTEGER NOT NULL,
|
|
3628
|
+
event_scan_cursor TEXT NOT NULL, delivery_scan_cursor TEXT NOT NULL, recovery_scan_cursor INTEGER NOT NULL,
|
|
3629
|
+
PRIMARY KEY (tenant_id, source_address)
|
|
3630
|
+
)`.withoutTransform;
|
|
3631
|
+
yield* sql`CREATE TABLE effect_agent_subscriptions (
|
|
3632
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
|
|
3633
|
+
ordinal INTEGER NOT NULL, source_name TEXT NOT NULL, source_version TEXT NOT NULL, matching_key TEXT NOT NULL,
|
|
3634
|
+
state TEXT NOT NULL, expires_at_millis INTEGER NOT NULL, recovery_at_millis INTEGER, record_json TEXT NOT NULL,
|
|
3635
|
+
PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id), UNIQUE (tenant_id, source_address, ordinal)
|
|
3636
|
+
)`.withoutTransform;
|
|
3637
|
+
yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`.withoutTransform;
|
|
3638
|
+
yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`.withoutTransform;
|
|
3639
|
+
yield* sql`CREATE INDEX effect_agent_subscriptions_recovery ON effect_agent_subscriptions (tenant_id, source_address, recovery_at_millis, ordinal) WHERE recovery_at_millis IS NOT NULL`.withoutTransform;
|
|
3640
|
+
yield* sql`CREATE TABLE effect_agent_subscription_events (
|
|
3641
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, event_id TEXT NOT NULL, source_name TEXT NOT NULL,
|
|
3642
|
+
source_version TEXT NOT NULL, matching_key TEXT NOT NULL, payload_digest TEXT NOT NULL, cutoff INTEGER NOT NULL,
|
|
3643
|
+
cursor INTEGER NOT NULL, routing_complete INTEGER NOT NULL, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL,
|
|
3644
|
+
PRIMARY KEY (tenant_id, source_address, event_id)
|
|
3645
|
+
)`.withoutTransform;
|
|
3646
|
+
yield* sql`CREATE INDEX effect_agent_subscription_events_pending ON effect_agent_subscription_events (tenant_id, source_address, routing_complete, next_attempt_at_millis, event_id)`.withoutTransform;
|
|
3647
|
+
yield* sql`CREATE TABLE effect_agent_subscription_deliveries (
|
|
3648
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
|
|
3649
|
+
event_id TEXT NOT NULL, delivery_key TEXT NOT NULL, state TEXT NOT NULL, next_attempt_at_millis INTEGER NOT NULL,
|
|
3650
|
+
record_json TEXT NOT NULL, PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),
|
|
3651
|
+
UNIQUE (tenant_id, source_address, delivery_key)
|
|
3652
|
+
)`.withoutTransform;
|
|
3653
|
+
yield* sql`CREATE INDEX effect_agent_subscription_deliveries_pending ON effect_agent_subscription_deliveries (tenant_id, source_address, state, next_attempt_at_millis, delivery_key)`.withoutTransform;
|
|
3654
|
+
yield* sql`CREATE INDEX effect_agent_subscription_deliveries_registration ON effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, delivery_key)`.withoutTransform;
|
|
3655
|
+
yield* sql`INSERT INTO effect_agent_subscription_store_state (singleton, storage_version, alarm_generation)
|
|
3656
|
+
VALUES (1, ${CURRENT_SUBSCRIPTION_STORE_VERSION}, 0)`.withoutTransform;
|
|
3657
|
+
})).pipe(Effect.mapError(() => unavailable("initialize subscription storage")));
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (decodedNames.length !== expected.size || decodedNames.some(({ name }) => !expected.has(name))) return yield* corrupt("subscription storage tables");
|
|
3661
|
+
const rows = yield* sql`SELECT storage_version, alarm_generation FROM effect_agent_subscription_store_state WHERE singleton=1`.pipe(Effect.mapError(() => unavailable("read subscription storage version")));
|
|
3662
|
+
const state = yield* decodeRows(StoreStateRow, rows, "read subscription storage version");
|
|
3663
|
+
if (state.length !== 1 || state[0].storage_version !== CURRENT_SUBSCRIPTION_STORE_VERSION) return yield* corrupt(state.length === 1 ? `incompatible subscription storage version ${state[0].storage_version}; expected ${CURRENT_SUBSCRIPTION_STORE_VERSION}` : "invalid subscription storage version row");
|
|
3664
|
+
});
|
|
3665
|
+
const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function* (owned) {
|
|
3666
|
+
const partition = yield* validate(SourcePartition, owned, "partition");
|
|
3667
|
+
const sql = yield* SqlClient.SqlClient;
|
|
3668
|
+
const failpoint = yield* SubscriptionFailpoint;
|
|
3669
|
+
const transactions = yield* DoSubscriptionTransaction;
|
|
3670
|
+
yield* initializeDoSubscriptionStore();
|
|
3671
|
+
yield* sql`
|
|
3672
|
+
INSERT INTO effect_agent_subscription_sequences (
|
|
3673
|
+
tenant_id, source_address, sequence, event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
3674
|
+
) VALUES (${partition.tenantId}, ${partition.address}, 0, '', '', 0) ON CONFLICT DO NOTHING
|
|
3675
|
+
`.pipe(Effect.mapError(() => unavailable("initialize subscription partition")));
|
|
3676
|
+
const query = (effect, code) => effect.pipe(Effect.mapError(() => unavailable(code)));
|
|
3677
|
+
const readIndexedDeadline = Effect.fn("DoSubscriptionStore.readIndexedDeadline")(function* () {
|
|
3678
|
+
const scanRows = yield* query(sql`
|
|
3679
|
+
SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor FROM effect_agent_subscription_sequences
|
|
3680
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3681
|
+
`, "read scan cursor deadline");
|
|
3682
|
+
const scans = yield* decodeRows(ScanRow, scanRows, "read scan cursor deadline");
|
|
3683
|
+
if (scans.length !== 1) return yield* corrupt("scan cursor deadline");
|
|
3684
|
+
if (scans[0].event_scan_cursor !== "" || scans[0].delivery_scan_cursor !== "" || scans[0].recovery_scan_cursor !== 0) return 0;
|
|
3685
|
+
const rows = yield* query(sql`
|
|
3686
|
+
SELECT MIN(deadline) AS deadline FROM (
|
|
3687
|
+
SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
|
|
3688
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
|
|
3689
|
+
UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
|
|
3690
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
3691
|
+
UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
|
|
3692
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
|
|
3693
|
+
)
|
|
3694
|
+
`, "read subscription deadline");
|
|
3695
|
+
const decoded = yield* decodeRows(Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }), rows, "read subscription deadline");
|
|
3696
|
+
if (decoded.length !== 1) return yield* corrupt("subscription deadline");
|
|
3697
|
+
return decoded[0].deadline;
|
|
3698
|
+
});
|
|
3699
|
+
const replaceAlarm = Effect.fn("DoSubscriptionStore.replaceAlarm")(function* (replace, deadlineAtMillis) {
|
|
3700
|
+
const rows = yield* query(sql`
|
|
3701
|
+
UPDATE effect_agent_subscription_store_state SET alarm_generation=alarm_generation+1
|
|
3702
|
+
WHERE singleton=1 RETURNING storage_version, alarm_generation
|
|
3703
|
+
`, "advance subscription alarm generation");
|
|
3704
|
+
const state = yield* decodeRows(StoreStateRow, rows, "advance subscription alarm generation");
|
|
3705
|
+
if (state.length !== 1 || state[0].storage_version !== CURRENT_SUBSCRIPTION_STORE_VERSION) return yield* corrupt("subscription alarm state");
|
|
3706
|
+
yield* failpoint.hit("subscription:alarm:before");
|
|
3707
|
+
yield* replace({
|
|
3708
|
+
deadlineAtMillis,
|
|
3709
|
+
generation: state[0].alarm_generation
|
|
3710
|
+
});
|
|
3711
|
+
yield* failpoint.hit("subscription:alarm:after");
|
|
3712
|
+
});
|
|
3713
|
+
const transact = (effect) => transactions.run((replace) => Effect.gen(function* () {
|
|
3714
|
+
const value = yield* effect;
|
|
3715
|
+
yield* replaceAlarm(replace, yield* readIndexedDeadline());
|
|
3716
|
+
return value;
|
|
3717
|
+
}));
|
|
3718
|
+
const requirePartition = (candidate, code) => sameSourcePartition(candidate, partition) ? Effect.void : Effect.fail(error("validation", code));
|
|
3719
|
+
const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (input, code) {
|
|
3720
|
+
const key = yield* validate(SubscriptionKey, input, code);
|
|
3721
|
+
yield* requirePartition(key.partition, code);
|
|
3722
|
+
return key;
|
|
3723
|
+
});
|
|
3724
|
+
const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (key, code) {
|
|
3725
|
+
const rows = yield* query(sql`
|
|
3726
|
+
SELECT owner_id, subscription_id, ordinal, source_name, source_version, matching_key, state,
|
|
3727
|
+
expires_at_millis, recovery_at_millis, record_json FROM effect_agent_subscriptions
|
|
3728
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3729
|
+
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId}
|
|
3730
|
+
`, code);
|
|
3731
|
+
const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
|
|
3732
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3733
|
+
const row = decodedRows[0];
|
|
3734
|
+
if (row === void 0) return null;
|
|
3735
|
+
const record = yield* decode(SubscriptionRecord, row.record_json, code);
|
|
3736
|
+
if (!sameSourcePartition(record.key.partition, partition) || record.key.ownerId !== row.owner_id || record.key.subscriptionId !== row.subscription_id || record.ordinal !== row.ordinal || record.configuration.source.name !== row.source_name || record.configuration.source.version !== row.source_version || record.configuration.matchingKey !== row.matching_key || record.state !== row.state || record.configuration.expiresAtMillis !== row.expires_at_millis || (record.recovery?.nextAttemptAtMillis ?? null) !== row.recovery_at_millis) return yield* corrupt(`${code}-projection`);
|
|
3737
|
+
return record;
|
|
3738
|
+
});
|
|
3739
|
+
const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (eventId, code) {
|
|
3740
|
+
const rows = yield* query(sql`
|
|
3741
|
+
SELECT event_id, source_name, source_version, matching_key, payload_digest, cutoff, cursor,
|
|
3742
|
+
routing_complete, next_attempt_at_millis, record_json FROM effect_agent_subscription_events
|
|
3743
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${eventId}
|
|
3744
|
+
`, code);
|
|
3745
|
+
const decodedRows = yield* decodeRows(EventRow, rows, code);
|
|
3746
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3747
|
+
const row = decodedRows[0];
|
|
3748
|
+
if (row === void 0) return null;
|
|
3749
|
+
const event = yield* decode(AcceptedEvent, row.record_json, code);
|
|
3750
|
+
if (!sameSourcePartition(event.partition, partition) || event.eventId !== row.event_id || event.source.name !== row.source_name || event.source.version !== row.source_version || event.matchingKey !== row.matching_key || event.payloadDigest !== row.payload_digest || event.cutoff !== row.cutoff || event.cursor !== row.cursor || (event.routingComplete ? 1 : 0) !== row.routing_complete || event.nextAttemptAtMillis !== row.next_attempt_at_millis) return yield* corrupt(`${code}-projection`);
|
|
3751
|
+
return event;
|
|
3752
|
+
});
|
|
3753
|
+
const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (key, code) {
|
|
3754
|
+
const rows = yield* query(sql`
|
|
3755
|
+
SELECT owner_id, subscription_id, event_id, delivery_key, state, next_attempt_at_millis, record_json
|
|
3756
|
+
FROM effect_agent_subscription_deliveries
|
|
3757
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3758
|
+
AND owner_id=${key.subscription.ownerId} AND subscription_id=${key.subscription.subscriptionId}
|
|
3759
|
+
AND event_id=${key.eventId}
|
|
3760
|
+
`, code);
|
|
3761
|
+
const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
|
|
3762
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3763
|
+
const row = decodedRows[0];
|
|
3764
|
+
if (row === void 0) return null;
|
|
3765
|
+
const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
|
|
3766
|
+
if (!sameSourcePartition(delivery.key.subscription.partition, partition) || delivery.key.subscription.ownerId !== row.owner_id || delivery.key.subscription.subscriptionId !== row.subscription_id || delivery.key.eventId !== row.event_id || subscriptionDeliveryKeyString(delivery.key) !== row.delivery_key || delivery.state !== row.state || delivery.retry.nextAttemptAtMillis !== row.next_attempt_at_millis) return yield* corrupt(`${code}-projection`);
|
|
3767
|
+
return delivery;
|
|
3768
|
+
});
|
|
3769
|
+
const count = Effect.fn("SqliteSubscriptionStore.count")(function* (statement, code) {
|
|
3770
|
+
const rows = yield* query(statement, code);
|
|
3771
|
+
const decoded = yield* decodeRows(CountRow, rows, code);
|
|
3772
|
+
if (decoded.length !== 1) return yield* corrupt(code);
|
|
3773
|
+
return decoded[0].count;
|
|
3774
|
+
});
|
|
3775
|
+
const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
|
|
3776
|
+
const rows = yield* query(sql`
|
|
3777
|
+
UPDATE effect_agent_subscription_sequences SET sequence=sequence+1
|
|
3778
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3779
|
+
RETURNING sequence
|
|
3780
|
+
`, "advance subscription sequence");
|
|
3781
|
+
const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
|
|
3782
|
+
if (decoded.length !== 1) return yield* corrupt("subscription sequence");
|
|
3783
|
+
return decoded[0].sequence;
|
|
3784
|
+
});
|
|
3785
|
+
const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (record) {
|
|
3786
|
+
const json = yield* encode(SubscriptionRecord, record, "encode subscription");
|
|
3787
|
+
yield* query(sql`
|
|
3788
|
+
UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
|
|
3789
|
+
recovery_at_millis=${record.recovery?.nextAttemptAtMillis ?? null}, record_json=${json}
|
|
3790
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3791
|
+
AND owner_id=${record.key.ownerId} AND subscription_id=${record.key.subscriptionId}
|
|
3792
|
+
`, "write subscription");
|
|
3793
|
+
});
|
|
3794
|
+
const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (event) {
|
|
3795
|
+
const json = yield* encode(AcceptedEvent, event, "encode event");
|
|
3796
|
+
yield* query(sql`
|
|
3797
|
+
UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
|
|
3798
|
+
next_attempt_at_millis=${event.nextAttemptAtMillis}, record_json=${json}
|
|
3799
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${event.eventId}
|
|
3800
|
+
`, "write event");
|
|
3801
|
+
});
|
|
3802
|
+
const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (delivery) {
|
|
3803
|
+
const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
|
|
3804
|
+
yield* query(sql`
|
|
3805
|
+
UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
|
|
3806
|
+
next_attempt_at_millis=${delivery.retry.nextAttemptAtMillis}, record_json=${json}
|
|
3807
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3808
|
+
AND owner_id=${delivery.key.subscription.ownerId} AND subscription_id=${delivery.key.subscription.subscriptionId}
|
|
3809
|
+
AND event_id=${delivery.key.eventId}
|
|
3810
|
+
`, "write delivery");
|
|
3811
|
+
});
|
|
3812
|
+
const register = Effect.fn("SqliteSubscriptionStore.register")(function* (input, inputLimits) {
|
|
3813
|
+
const record = yield* validate(SubscriptionRecord, input, "register-record");
|
|
3814
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
|
|
3815
|
+
yield* requirePartition(record.key.partition, "register-partition");
|
|
3816
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3817
|
+
const existing = yield* readRegistration(record.key, "register-existing");
|
|
3818
|
+
if (existing !== null) {
|
|
3819
|
+
if (existing.creationFingerprint !== record.creationFingerprint) return yield* error("conflict", "registration-identity");
|
|
3820
|
+
return {
|
|
3821
|
+
value: existing,
|
|
3822
|
+
changed: false
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
if (bytes(record.configuration.context) > limits.maxContextBytes) return yield* error("capacity", "context-bytes");
|
|
3826
|
+
if (bytes(record.configuration.parameters) > limits.maxPayloadBytes) return yield* error("capacity", "parameters-bytes");
|
|
3827
|
+
if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return yield* error("capacity", "lifetime");
|
|
3828
|
+
if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count registrations")) >= limits.maxRegistrations) return yield* error("capacity", "registrations");
|
|
3829
|
+
if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`, "count owner registrations")) >= limits.maxRegistrationsPerOwner) return yield* error("capacity", "owner-registrations");
|
|
3830
|
+
const assigned = {
|
|
3831
|
+
...record,
|
|
3832
|
+
ordinal: yield* nextSequence()
|
|
3833
|
+
};
|
|
3834
|
+
const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
|
|
3835
|
+
yield* failpoint.hit("subscription:register:before");
|
|
3836
|
+
yield* query(sql`
|
|
3837
|
+
INSERT INTO effect_agent_subscriptions (tenant_id, source_address, owner_id, subscription_id, ordinal,
|
|
3838
|
+
source_name, source_version, matching_key, state, expires_at_millis, recovery_at_millis, record_json)
|
|
3839
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${assigned.key.ownerId}, ${assigned.key.subscriptionId}, ${assigned.ordinal},
|
|
3840
|
+
${assigned.configuration.source.name}, ${assigned.configuration.source.version}, ${assigned.configuration.matchingKey}, ${assigned.state},
|
|
3841
|
+
${assigned.configuration.expiresAtMillis}, ${assigned.recovery?.nextAttemptAtMillis ?? null}, ${json})
|
|
3842
|
+
`, "insert registration");
|
|
3843
|
+
return {
|
|
3844
|
+
value: assigned,
|
|
3845
|
+
changed: true
|
|
3846
|
+
};
|
|
3847
|
+
}));
|
|
3848
|
+
if (result.changed) yield* failpoint.hit("subscription:register:after");
|
|
3849
|
+
return result.value;
|
|
3850
|
+
});
|
|
3851
|
+
const get = Effect.fn("SqliteSubscriptionStore.get")(function* (input) {
|
|
3852
|
+
return yield* readRegistration(yield* requireKey(input, "get-key"), "get subscription");
|
|
3853
|
+
});
|
|
3854
|
+
const list = Effect.fn("SqliteSubscriptionStore.list")(function* (ownerId, after, limit) {
|
|
3855
|
+
const rows = yield* query(sql`
|
|
3856
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3857
|
+
AND owner_id=${ownerId} AND ordinal>${after} ORDER BY ordinal LIMIT ${limit}
|
|
3858
|
+
`, "list subscriptions");
|
|
3859
|
+
const decoded = yield* decodeRows(Schema.Struct({
|
|
3860
|
+
owner_id: Schema.String,
|
|
3861
|
+
subscription_id: Schema.String,
|
|
3862
|
+
ordinal: Schema.Natural
|
|
3863
|
+
}), rows, "list subscriptions");
|
|
3864
|
+
return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
|
|
3865
|
+
const record = yield* readRegistration({
|
|
3866
|
+
partition,
|
|
3867
|
+
ownerId: row.owner_id,
|
|
3868
|
+
subscriptionId: row.subscription_id
|
|
3869
|
+
}, "list subscription");
|
|
3870
|
+
if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("list subscription projection");
|
|
3871
|
+
return record;
|
|
3872
|
+
}));
|
|
3873
|
+
});
|
|
3874
|
+
const cancel = Effect.fn("SqliteSubscriptionStore.cancel")(function* (input) {
|
|
3875
|
+
const key = yield* requireKey(input, "cancel-key");
|
|
3876
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3877
|
+
const current = yield* readRegistration(key, "cancel subscription");
|
|
3878
|
+
if (current === null) return yield* error("not-found", "subscription");
|
|
3879
|
+
if (current.state === "cancelled") return {
|
|
3880
|
+
value: current,
|
|
3881
|
+
changed: false
|
|
3882
|
+
};
|
|
3883
|
+
const updated = {
|
|
3884
|
+
...current,
|
|
3885
|
+
state: "cancelled",
|
|
3886
|
+
recovery: null
|
|
3887
|
+
};
|
|
3888
|
+
yield* failpoint.hit("subscription:cancel:before");
|
|
3889
|
+
yield* writeRegistration(updated);
|
|
3890
|
+
return {
|
|
3891
|
+
value: updated,
|
|
3892
|
+
changed: true
|
|
3893
|
+
};
|
|
3894
|
+
}));
|
|
3895
|
+
if (result.changed) yield* failpoint.hit("subscription:cancel:after");
|
|
3896
|
+
return result.value;
|
|
3897
|
+
});
|
|
3898
|
+
const accept = Effect.fn("SqliteSubscriptionStore.accept")(function* (input, inputLimits) {
|
|
3899
|
+
const event = yield* validate(AcceptedEvent, input, "accept-event");
|
|
3900
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
|
|
3901
|
+
yield* requirePartition(event.partition, "accept-partition");
|
|
3902
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3903
|
+
const existing = yield* readEvent(event.eventId, "accept event");
|
|
3904
|
+
if (existing !== null) {
|
|
3905
|
+
if (!sameAcceptedEventIdentity(existing, event)) return yield* error("conflict", "event-identity");
|
|
3906
|
+
return {
|
|
3907
|
+
value: existing,
|
|
3908
|
+
changed: false
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
if (bytes(event.payload) > limits.maxPayloadBytes) return yield* error("capacity", "payload-bytes");
|
|
3912
|
+
if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count events")) >= limits.maxEvents) return yield* error("capacity", "events");
|
|
3913
|
+
const accepted = {
|
|
3914
|
+
...event,
|
|
3915
|
+
cutoff: yield* nextSequence(),
|
|
3916
|
+
cursor: 0,
|
|
3917
|
+
routingComplete: false,
|
|
3918
|
+
routingFailure: null
|
|
3919
|
+
};
|
|
3920
|
+
const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
|
|
3921
|
+
yield* failpoint.hit("subscription:accept:before");
|
|
3922
|
+
yield* query(sql`
|
|
3923
|
+
INSERT INTO effect_agent_subscription_events (tenant_id, source_address, event_id, source_name, source_version,
|
|
3924
|
+
matching_key, payload_digest, cutoff, cursor, routing_complete, next_attempt_at_millis, record_json)
|
|
3925
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${accepted.eventId}, ${accepted.source.name}, ${accepted.source.version},
|
|
3926
|
+
${accepted.matchingKey}, ${accepted.payloadDigest}, ${accepted.cutoff}, ${accepted.cursor}, 0, ${accepted.nextAttemptAtMillis}, ${json})
|
|
3927
|
+
`, "insert event");
|
|
3928
|
+
return {
|
|
3929
|
+
value: accepted,
|
|
3930
|
+
changed: true
|
|
3931
|
+
};
|
|
3932
|
+
}));
|
|
3933
|
+
if (result.changed) yield* failpoint.hit("subscription:accept:after");
|
|
3934
|
+
return result.value;
|
|
3935
|
+
});
|
|
3936
|
+
const event = Effect.fn("SqliteSubscriptionStore.event")((eventId) => readEvent(eventId, "get event"));
|
|
3937
|
+
const pendingEvents = Effect.fn("SqliteSubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
|
|
3938
|
+
const rows = yield* query(sql`
|
|
3939
|
+
SELECT event_id FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3940
|
+
AND routing_complete=0 AND next_attempt_at_millis<=${nowMillis} AND event_id>${after} ORDER BY event_id LIMIT ${limit}
|
|
3941
|
+
`, "pending events");
|
|
3942
|
+
return yield* decodeRows(Schema.Struct({ event_id: Schema.String }), rows, "pending event keys").pipe(Effect.map((items) => items.map((item) => item.event_id)));
|
|
3943
|
+
});
|
|
3944
|
+
const candidates = Effect.fn("SqliteSubscriptionStore.candidates")(function* (input, limit) {
|
|
3945
|
+
const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
|
|
3946
|
+
yield* requirePartition(supplied.partition, "candidates-partition");
|
|
3947
|
+
const stored = yield* readEvent(supplied.eventId, "candidates event");
|
|
3948
|
+
if (stored === null) return yield* error("not-found", "event");
|
|
3949
|
+
if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
|
|
3950
|
+
const rows = yield* query(sql`
|
|
3951
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3952
|
+
AND source_name=${stored.source.name} AND source_version=${stored.source.version} AND matching_key=${stored.matchingKey}
|
|
3953
|
+
AND ordinal>${stored.cursor} AND ordinal<=${stored.cutoff} ORDER BY ordinal LIMIT ${limit}
|
|
3954
|
+
`, "subscription candidates");
|
|
3955
|
+
const decoded = yield* decodeRows(Schema.Struct({
|
|
3956
|
+
owner_id: Schema.String,
|
|
3957
|
+
subscription_id: Schema.String,
|
|
3958
|
+
ordinal: Schema.Natural
|
|
3959
|
+
}), rows, "subscription candidates");
|
|
3960
|
+
return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
|
|
3961
|
+
const record = yield* readRegistration({
|
|
3962
|
+
partition,
|
|
3963
|
+
ownerId: row.owner_id,
|
|
3964
|
+
subscriptionId: row.subscription_id
|
|
3965
|
+
}, "subscription candidate");
|
|
3966
|
+
if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("subscription candidate projection");
|
|
3967
|
+
return record;
|
|
3968
|
+
}));
|
|
3969
|
+
});
|
|
3970
|
+
const insertDelivery = Effect.fn("SqliteSubscriptionStore.insertDelivery")(function* (delivery) {
|
|
3971
|
+
const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
|
|
3972
|
+
yield* query(sql`
|
|
3973
|
+
INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
|
|
3974
|
+
delivery_key, state, next_attempt_at_millis, record_json)
|
|
3975
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${delivery.key.subscription.ownerId}, ${delivery.key.subscription.subscriptionId},
|
|
3976
|
+
${delivery.key.eventId}, ${subscriptionDeliveryKeyString(delivery.key)}, ${delivery.state}, ${delivery.retry.nextAttemptAtMillis}, ${json})
|
|
3977
|
+
`, "insert delivery");
|
|
3978
|
+
});
|
|
3979
|
+
const select = Effect.fn("SqliteSubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
|
|
3980
|
+
const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
|
|
3981
|
+
const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
|
|
3982
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
|
|
3983
|
+
yield* requirePartition(supplied.partition, "select-partition");
|
|
3984
|
+
for (const candidate of deliveries) yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
|
|
3985
|
+
if (yield* transact(Effect.gen(function* () {
|
|
3986
|
+
const accepted = yield* readEvent(supplied.eventId, "select event");
|
|
3987
|
+
if (accepted === null) return yield* error("not-found", "event");
|
|
3988
|
+
if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor) return yield* error("conflict", "event-cursor");
|
|
3989
|
+
if (accepted.routingComplete) return false;
|
|
3990
|
+
if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return yield* error("validation", "cursor");
|
|
3991
|
+
yield* failpoint.hit("subscription:select:before");
|
|
3992
|
+
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
3993
|
+
const additions = [];
|
|
3994
|
+
for (const delivery of deliveries) {
|
|
3995
|
+
const record = yield* readRegistration(delivery.key.subscription, "select registration");
|
|
3996
|
+
if (record === null) return yield* error("not-found", "subscription");
|
|
3997
|
+
if (!subscriptionDeliveryCanSelect(delivery, record, accepted) || delivery.key.eventId !== accepted.eventId || delivery.source.name !== accepted.source.name || delivery.source.version !== accepted.source.version || record.ordinal <= accepted.cursor || record.ordinal > cursor) return yield* error("conflict", "selection");
|
|
3998
|
+
const existing = yield* readDelivery(delivery.key, "select existing delivery");
|
|
3999
|
+
if (existing !== null) {
|
|
4000
|
+
if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
|
|
4001
|
+
continue;
|
|
4002
|
+
}
|
|
4003
|
+
if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
|
|
4004
|
+
additions.push({
|
|
4005
|
+
delivery,
|
|
4006
|
+
record
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count deliveries")) + additions.length > limits.maxDeliveries) return yield* error("capacity", "deliveries");
|
|
4010
|
+
for (const ownerId of new Set(additions.map(({ record }) => record.key.ownerId))) if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${ownerId}`, "count owner deliveries")) + additions.filter(({ record }) => record.key.ownerId === ownerId).length > limits.maxDeliveriesPerOwner) return yield* error("capacity", "owner-deliveries");
|
|
4011
|
+
for (const addition of additions) {
|
|
4012
|
+
yield* insertDelivery(addition.delivery);
|
|
4013
|
+
if (addition.record.configuration.mode === "once") yield* writeRegistration({
|
|
4014
|
+
...addition.record,
|
|
4015
|
+
state: "consumed",
|
|
4016
|
+
recovery: null
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
yield* writeEvent({
|
|
4020
|
+
...accepted,
|
|
4021
|
+
cursor,
|
|
4022
|
+
routingComplete: complete,
|
|
4023
|
+
routingFailure: null
|
|
4024
|
+
});
|
|
4025
|
+
return true;
|
|
4026
|
+
}))) yield* failpoint.hit("subscription:select:after");
|
|
4027
|
+
});
|
|
4028
|
+
const catchUp = Effect.fn("SqliteSubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
|
|
4029
|
+
const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
|
|
4030
|
+
const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
|
|
4031
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
|
|
4032
|
+
yield* requirePartition(supplied.partition, "catch-up-partition");
|
|
4033
|
+
yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
|
|
4034
|
+
if (yield* transact(Effect.gen(function* () {
|
|
4035
|
+
const accepted = yield* readEvent(supplied.eventId, "catch-up event");
|
|
4036
|
+
const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
|
|
4037
|
+
if (accepted === null || record === null) return yield* error("not-found", accepted === null ? "event" : "subscription");
|
|
4038
|
+
if (!sameAcceptedEventIdentity(accepted, supplied) || !subscriptionDeliveryCanSelect(delivery, record, accepted) || delivery.key.eventId !== accepted.eventId || delivery.source.name !== accepted.source.name || delivery.source.version !== accepted.source.version || record.configuration.mode !== "once") return yield* error("conflict", "catch-up-identity");
|
|
4039
|
+
const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
|
|
4040
|
+
if (existing !== null) {
|
|
4041
|
+
if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
|
|
4042
|
+
return false;
|
|
4043
|
+
}
|
|
4044
|
+
yield* failpoint.hit("subscription:catch-up:before");
|
|
4045
|
+
if (!subscriptionCanSelect(record, accepted, Math.max(nowMillis, yield* Clock.currentTimeMillis), true)) return yield* error("conflict", "catch-up-eligibility");
|
|
4046
|
+
if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`, "count deliveries")) >= limits.maxDeliveries) return yield* error("capacity", "deliveries");
|
|
4047
|
+
if ((yield* count(sql`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`, "count owner deliveries")) >= limits.maxDeliveriesPerOwner) return yield* error("capacity", "owner-deliveries");
|
|
4048
|
+
yield* insertDelivery(delivery);
|
|
4049
|
+
yield* writeRegistration({
|
|
4050
|
+
...record,
|
|
4051
|
+
state: "consumed",
|
|
4052
|
+
recovery: null
|
|
4053
|
+
});
|
|
4054
|
+
return true;
|
|
4055
|
+
}))) yield* failpoint.hit("subscription:catch-up:after");
|
|
4056
|
+
});
|
|
4057
|
+
const deferEvent = Effect.fn("SqliteSubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
|
|
4058
|
+
const routingFailure = code === void 0 ? "routing-failed" : yield* validate(SubscriptionName, code, "routing-failure");
|
|
4059
|
+
yield* transact(Effect.gen(function* () {
|
|
4060
|
+
const accepted = yield* readEvent(eventId, "defer event");
|
|
4061
|
+
if (accepted === null) return yield* error("not-found", "event");
|
|
4062
|
+
yield* failpoint.hit("subscription:defer-event:before");
|
|
4063
|
+
yield* writeEvent({
|
|
4064
|
+
...accepted,
|
|
4065
|
+
nextAttemptAtMillis,
|
|
4066
|
+
routingFailure
|
|
4067
|
+
});
|
|
4068
|
+
}));
|
|
4069
|
+
yield* failpoint.hit("subscription:defer-event:after");
|
|
4070
|
+
});
|
|
4071
|
+
const delivery = Effect.fn("SqliteSubscriptionStore.delivery")(function* (input) {
|
|
4072
|
+
const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
|
|
4073
|
+
yield* requirePartition(key.subscription.partition, "delivery-partition");
|
|
4074
|
+
return yield* readDelivery(key, "get delivery");
|
|
4075
|
+
});
|
|
4076
|
+
const pendingDeliveries = Effect.fn("SqliteSubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
|
|
4077
|
+
const rows = yield* query(sql`
|
|
4078
|
+
SELECT owner_id, subscription_id, event_id FROM effect_agent_subscription_deliveries
|
|
4079
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
4080
|
+
AND next_attempt_at_millis<=${nowMillis} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
4081
|
+
`, "pending deliveries");
|
|
4082
|
+
const rowSchema = Schema.Struct({
|
|
4083
|
+
owner_id: Schema.String,
|
|
4084
|
+
subscription_id: Schema.String,
|
|
4085
|
+
event_id: Schema.String
|
|
4086
|
+
});
|
|
4087
|
+
return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(Effect.map((items) => items.map((item) => ({
|
|
4088
|
+
subscription: {
|
|
4089
|
+
partition,
|
|
4090
|
+
ownerId: item.owner_id,
|
|
4091
|
+
subscriptionId: item.subscription_id
|
|
4092
|
+
},
|
|
4093
|
+
eventId: item.event_id
|
|
4094
|
+
}))));
|
|
4095
|
+
});
|
|
4096
|
+
const listDeliveries = Effect.fn("SqliteSubscriptionStore.listDeliveries")(function* (input, after, limit) {
|
|
4097
|
+
const key = yield* requireKey(input, "list-deliveries-key");
|
|
4098
|
+
const rows = yield* query(sql`
|
|
4099
|
+
SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
4100
|
+
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
4101
|
+
`, "list deliveries");
|
|
4102
|
+
const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
|
|
4103
|
+
return yield* Effect.forEach(decoded, (row) => decode(SubscriptionDelivery, row.record_json, "list delivery"));
|
|
4104
|
+
});
|
|
4105
|
+
const changeDelivery = Effect.fn("SqliteSubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
|
|
4106
|
+
const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
|
|
4107
|
+
const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
|
|
4108
|
+
const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
|
|
4109
|
+
yield* requirePartition(key.subscription.partition, "change-delivery-partition");
|
|
4110
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
4111
|
+
const existing = yield* readDelivery(key, "change delivery");
|
|
4112
|
+
const record = yield* readRegistration(key.subscription, "change delivery subscription");
|
|
4113
|
+
if (existing === null || record === null) return yield* error("not-found", existing === null ? "delivery" : "subscription");
|
|
4114
|
+
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
|
|
4115
|
+
const effectiveChange = change._tag === "Prepare" ? {
|
|
4116
|
+
...change,
|
|
4117
|
+
nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
|
|
4118
|
+
} : change;
|
|
4119
|
+
const transition = applySubscriptionDeliveryChange(existing, record, deliveryId, effectiveChange);
|
|
4120
|
+
if (Result.isFailure(transition)) return yield* transition.failure;
|
|
4121
|
+
if (transition.success === existing) return {
|
|
4122
|
+
value: existing,
|
|
4123
|
+
changed: false
|
|
4124
|
+
};
|
|
4125
|
+
yield* writeDelivery(transition.success);
|
|
4126
|
+
return {
|
|
4127
|
+
value: transition.success,
|
|
4128
|
+
changed: true
|
|
4129
|
+
};
|
|
4130
|
+
}));
|
|
4131
|
+
if (result.changed) yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
|
|
4132
|
+
return result.value;
|
|
4133
|
+
});
|
|
4134
|
+
const recovering = Effect.fn("SqliteSubscriptionStore.recovering")(function* (nowMillis, after, limit) {
|
|
4135
|
+
const rows = yield* query(sql`
|
|
4136
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions
|
|
4137
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active'
|
|
4138
|
+
AND recovery_at_millis IS NOT NULL AND recovery_at_millis<=${nowMillis} AND ordinal>${after}
|
|
4139
|
+
ORDER BY ordinal LIMIT ${limit}
|
|
4140
|
+
`, "recovering subscriptions");
|
|
4141
|
+
const rowSchema = Schema.Struct({
|
|
4142
|
+
owner_id: Schema.String,
|
|
4143
|
+
subscription_id: Schema.String,
|
|
4144
|
+
ordinal: Schema.Natural
|
|
4145
|
+
});
|
|
4146
|
+
return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(Effect.map((items) => items.map((item) => ({
|
|
4147
|
+
key: {
|
|
4148
|
+
partition,
|
|
4149
|
+
ownerId: item.owner_id,
|
|
4150
|
+
subscriptionId: item.subscription_id
|
|
4151
|
+
},
|
|
4152
|
+
ordinal: item.ordinal
|
|
4153
|
+
}))));
|
|
4154
|
+
});
|
|
4155
|
+
const deferRecovery = Effect.fn("SqliteSubscriptionStore.deferRecovery")(function* (input, recovery) {
|
|
4156
|
+
const key = yield* requireKey(input, "defer-recovery-key");
|
|
4157
|
+
yield* transact(Effect.gen(function* () {
|
|
4158
|
+
const record = yield* readRegistration(key, "defer recovery");
|
|
4159
|
+
if (record === null) return yield* error("not-found", "subscription");
|
|
4160
|
+
yield* failpoint.hit("subscription:defer-recovery:before");
|
|
4161
|
+
yield* writeRegistration({
|
|
4162
|
+
...record,
|
|
4163
|
+
recovery: record.state === "active" ? recovery : null
|
|
4164
|
+
});
|
|
4165
|
+
}));
|
|
4166
|
+
yield* failpoint.hit("subscription:defer-recovery:after");
|
|
4167
|
+
});
|
|
4168
|
+
const readScanCursors = Effect.gen(function* () {
|
|
4169
|
+
const rows = yield* query(sql`
|
|
4170
|
+
SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
4171
|
+
FROM effect_agent_subscription_sequences
|
|
4172
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
4173
|
+
`, "read subscription scan cursors");
|
|
4174
|
+
const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
|
|
4175
|
+
if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
|
|
4176
|
+
return {
|
|
4177
|
+
events: decoded[0].event_scan_cursor,
|
|
4178
|
+
deliveries: decoded[0].delivery_scan_cursor,
|
|
4179
|
+
recovery: decoded[0].recovery_scan_cursor
|
|
4180
|
+
};
|
|
4181
|
+
});
|
|
4182
|
+
const advanceScanCursors = Effect.fn("SqliteSubscriptionStore.advanceScanCursors")(function* (input) {
|
|
4183
|
+
const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
|
|
4184
|
+
yield* transact(Effect.gen(function* () {
|
|
4185
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:before");
|
|
4186
|
+
yield* query(sql`
|
|
4187
|
+
UPDATE effect_agent_subscription_sequences
|
|
4188
|
+
SET event_scan_cursor=${cursors.events}, delivery_scan_cursor=${cursors.deliveries}, recovery_scan_cursor=${cursors.recovery}
|
|
4189
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
4190
|
+
`, "advance subscription scan cursors");
|
|
4191
|
+
}));
|
|
4192
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:after");
|
|
4193
|
+
});
|
|
4194
|
+
const indexedDeadline = query(sql`
|
|
4195
|
+
SELECT MIN(deadline) AS deadline FROM (
|
|
4196
|
+
SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
|
|
4197
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
|
|
4198
|
+
UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
|
|
4199
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
4200
|
+
UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
|
|
4201
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
|
|
4202
|
+
)
|
|
4203
|
+
`, "next subscription deadline").pipe(Effect.flatMap((rows) => decodeRows(Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }), rows, "next subscription deadline")), Effect.flatMap((rows) => rows.length === 1 ? Effect.succeed(rows[0].deadline) : Effect.fail(corrupt("next subscription deadline"))));
|
|
4204
|
+
const nextDeadline = Effect.gen(function* () {
|
|
4205
|
+
const cursors = yield* readScanCursors;
|
|
4206
|
+
if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
|
|
4207
|
+
return yield* indexedDeadline;
|
|
4208
|
+
});
|
|
4209
|
+
const prearm = Effect.fn("DoSubscriptionStore.prearm")(function* (deadlineAtMillis) {
|
|
4210
|
+
yield* transactions.run((replace) => replaceAlarm(replace, deadlineAtMillis));
|
|
4211
|
+
yield* failpoint.hit("subscription:prearm:after");
|
|
4212
|
+
});
|
|
4213
|
+
const reconcile = Effect.gen(function* () {
|
|
4214
|
+
yield* transactions.run((replace) => Effect.gen(function* () {
|
|
4215
|
+
yield* replaceAlarm(replace, yield* readIndexedDeadline());
|
|
4216
|
+
}));
|
|
4217
|
+
yield* failpoint.hit("subscription:reconcile:after");
|
|
4218
|
+
});
|
|
4219
|
+
return Context.make(SubscriptionStore, {
|
|
4220
|
+
partition,
|
|
4221
|
+
register,
|
|
4222
|
+
get,
|
|
4223
|
+
list,
|
|
4224
|
+
cancel,
|
|
4225
|
+
accept,
|
|
4226
|
+
event,
|
|
4227
|
+
pendingEvents,
|
|
4228
|
+
candidates,
|
|
4229
|
+
select,
|
|
4230
|
+
catchUp,
|
|
4231
|
+
deferEvent,
|
|
4232
|
+
delivery,
|
|
4233
|
+
pendingDeliveries,
|
|
4234
|
+
listDeliveries,
|
|
4235
|
+
changeDelivery,
|
|
4236
|
+
recovering,
|
|
4237
|
+
deferRecovery,
|
|
4238
|
+
readScanCursors,
|
|
4239
|
+
advanceScanCursors,
|
|
4240
|
+
nextDeadline
|
|
4241
|
+
}).pipe(Context.add(DoSubscriptionAlarmControl, {
|
|
4242
|
+
prearm,
|
|
4243
|
+
reconcile
|
|
4244
|
+
}));
|
|
4245
|
+
});
|
|
4246
|
+
const doSubscriptionStoreLayer = (partition) => Layer.effectContext(makeSubscriptionStore(partition));
|
|
4247
|
+
//#endregion
|
|
3574
4248
|
//#region src/port-protocol.ts
|
|
3575
4249
|
/**
|
|
3576
4250
|
* The cross-Durable-Object port protocol (plan §1.3, D-P6-3): Schema request/response/error
|
|
3577
|
-
* envelopes for the CLOSED route-capable subset of the
|
|
3578
|
-
* Durable Object executes another
|
|
4251
|
+
* envelopes for the CLOSED route-capable subset of the thread ports. One Thread's
|
|
4252
|
+
* Durable Object executes another Thread's request against its OWN local facets; the
|
|
3579
4253
|
* envelopes here are the only values that cross the Object boundary, and they are
|
|
3580
4254
|
* transport-agnostic — native Durable Object JS RPC is the shipped carrier, fetch-with-JSON
|
|
3581
4255
|
* the documented fallback, and both move the same Schema-encoded JSON.
|
|
3582
4256
|
*
|
|
3583
4257
|
* The closed subset is exactly the set of operations the durable coordinator performs against
|
|
3584
|
-
* a FOREIGN
|
|
3585
|
-
* child-settlement notification, and the child-
|
|
4258
|
+
* a FOREIGN Thread (parent/child establishment, status checks, abort propagation,
|
|
4259
|
+
* child-settlement notification, and the child-thread store operations used by
|
|
3586
4260
|
* establishment, `verifySettledChild`, and result projection):
|
|
3587
4261
|
*
|
|
3588
4262
|
* - ledger: `admit`, `markReady`, `lookup`, `resolveAdmission`, `requestAbort`,
|
|
@@ -3595,7 +4269,7 @@ const scheduleStoreLayer = Layer.effectContext(makeServices);
|
|
|
3595
4269
|
*
|
|
3596
4270
|
* Failures cross the boundary as the `PortFailure` union and re-decode on the caller side to
|
|
3597
4271
|
* the SAME tagged error types the local facet would have produced, so routed calls keep
|
|
3598
|
-
* error-tag fidelity. `cause` chains inside `LedgerError`/`
|
|
4272
|
+
* error-tag fidelity. `cause` chains inside `LedgerError`/`ThreadStoreError` travel as
|
|
3599
4273
|
* Schema defects and do not claim instance fidelity across Objects (plan §2.8).
|
|
3600
4274
|
*/
|
|
3601
4275
|
/** Ceiling for protocol diagnostic strings; matches `AdmissionIndeterminate.reason`. */
|
|
@@ -3606,7 +4280,7 @@ const boundPortDiagnostic = (value) => value.length > 4096 ? `${value.slice(0, M
|
|
|
3606
4280
|
/**
|
|
3607
4281
|
* The envelope itself could not be honored: the receiving Object could not decode the
|
|
3608
4282
|
* request, or a response could not be encoded/decoded. It never carries port semantics —
|
|
3609
|
-
* callers fold it into the operation's base error (`LedgerError`/`
|
|
4283
|
+
* callers fold it into the operation's base error (`LedgerError`/`ThreadStoreError`),
|
|
3610
4284
|
* except `resolveAdmission`, which folds it into `AdmissionIndeterminate` because a
|
|
3611
4285
|
* non-answer is never proof of absence (SUB-031).
|
|
3612
4286
|
*/
|
|
@@ -3623,16 +4297,16 @@ var LedgerResolveAdmissionCall = class extends Schema.TaggedClass("@effect-agent
|
|
|
3623
4297
|
var LedgerRequestAbortCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRequestAbortCall")("LedgerRequestAbort", { request: AbortCommand }) {};
|
|
3624
4298
|
/** Routed `SubmissionLedger.recordChildSettled` — the child→parent durable notification. */
|
|
3625
4299
|
var LedgerRecordChildSettledCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRecordChildSettledCall")("LedgerRecordChildSettled", { request: ChildSettledNotification }) {};
|
|
3626
|
-
/** Routed `
|
|
3627
|
-
var StoreMaterializeCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreMaterializeCall")("StoreMaterialize", { request:
|
|
3628
|
-
/** Routed `
|
|
4300
|
+
/** Routed `ThreadStore.materialize` against the owning Object. */
|
|
4301
|
+
var StoreMaterializeCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreMaterializeCall")("StoreMaterialize", { request: ThreadMaterialization }) {};
|
|
4302
|
+
/** Routed `ThreadStore.append` against the owning Object. */
|
|
3629
4303
|
var StoreAppendCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreAppendCall")("StoreAppend", { request: FencedAppendRequest }) {};
|
|
3630
|
-
/** Routed one-page `
|
|
3631
|
-
var StoreReadPageCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageCall")("StoreReadPage", { request:
|
|
3632
|
-
/** Routed `
|
|
3633
|
-
var StoreInspectTailCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailCall")("StoreInspectTail", { request:
|
|
3634
|
-
/** Routed `
|
|
3635
|
-
var StoreExportCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportCall")("StoreExport", { request:
|
|
4304
|
+
/** Routed one-page `ThreadStore.read`; the page bound is the request's own `limit`. */
|
|
4305
|
+
var StoreReadPageCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageCall")("StoreReadPage", { request: ThreadRead }) {};
|
|
4306
|
+
/** Routed `ThreadStore.inspectTail` against the owning Object. */
|
|
4307
|
+
var StoreInspectTailCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailCall")("StoreInspectTail", { request: ThreadTailRequest }) {};
|
|
4308
|
+
/** Routed `ThreadStore.export` against the owning Object. */
|
|
4309
|
+
var StoreExportCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportCall")("StoreExport", { request: ThreadExportRequest }) {};
|
|
3636
4310
|
/** Every request that may cross a Durable Object boundary — the CLOSED route-capable subset. */
|
|
3637
4311
|
const PortRequest = Schema.Union([
|
|
3638
4312
|
LedgerAdmitCall,
|
|
@@ -3658,8 +4332,8 @@ var StoreMaterializeResult = class extends Schema.TaggedClass("@effect-agent/sto
|
|
|
3658
4332
|
var StoreAppendResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreAppendResult")("StoreAppendResult", { result: AppendResult }) {};
|
|
3659
4333
|
/** One page of canonical records, bounded by the request's `limit` (≤ 1,024). */
|
|
3660
4334
|
var StoreReadPageResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageResult")("StoreReadPageResult", { records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(1024)) }) {};
|
|
3661
|
-
var StoreInspectTailResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailResult")("StoreInspectTailResult", { tail:
|
|
3662
|
-
var StoreExportResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportResult")("StoreExportResult", { export:
|
|
4335
|
+
var StoreInspectTailResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailResult")("StoreInspectTailResult", { tail: ThreadTail }) {};
|
|
4336
|
+
var StoreExportResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportResult")("StoreExportResult", { export: ThreadExport }) {};
|
|
3663
4337
|
/** Every successful routed result. Callers narrow by the tag their request implies. */
|
|
3664
4338
|
const PortResult = Schema.Union([
|
|
3665
4339
|
LedgerAdmitResult,
|
|
@@ -3677,15 +4351,15 @@ const PortResult = Schema.Union([
|
|
|
3677
4351
|
/**
|
|
3678
4352
|
* Every typed failure a route-capable operation can produce on its owning Object, plus the
|
|
3679
4353
|
* protocol's own `PortProtocolError`. Members re-decode to the SAME tagged classes the
|
|
3680
|
-
*
|
|
4354
|
+
* thread ports declare, so a routed caller observes identical error tags and fields.
|
|
3681
4355
|
*/
|
|
3682
4356
|
const PortFailure = Schema.Union([
|
|
3683
4357
|
AdmissionConflict,
|
|
3684
4358
|
SettlementConflict,
|
|
3685
4359
|
JoinedToHost,
|
|
3686
4360
|
LedgerError,
|
|
3687
|
-
|
|
3688
|
-
|
|
4361
|
+
ThreadStoreError,
|
|
4362
|
+
ThreadNotMaterialized,
|
|
3689
4363
|
AppendConflict,
|
|
3690
4364
|
FenceRejected,
|
|
3691
4365
|
PortProtocolError
|
|
@@ -3702,8 +4376,8 @@ const encodePortResponse = Schema.encodeEffect(PortResponse);
|
|
|
3702
4376
|
const decodePortResponse = Schema.decodeUnknownEffect(PortResponse);
|
|
3703
4377
|
//#endregion
|
|
3704
4378
|
//#region src/routing.ts
|
|
3705
|
-
const
|
|
3706
|
-
const
|
|
4379
|
+
const ThreadIdSchema = ThreadMaterialization.fields.threadId;
|
|
4380
|
+
const decodeThreadId = Schema.decodeUnknownEffect(ThreadIdSchema);
|
|
3707
4381
|
/**
|
|
3708
4382
|
* The ledger row bound routable Submission identities must respect (mirrors the local
|
|
3709
4383
|
* facet's `MAX_IDENTIFIER_LENGTH`; the minting side already refuses longer identities typed
|
|
@@ -3714,7 +4388,7 @@ const MAX_ROUTABLE_SUBMISSION_ID_LENGTH = 1024;
|
|
|
3714
4388
|
const UUID_HEAD_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
3715
4389
|
/**
|
|
3716
4390
|
* A transport could not deliver a port request to (or an answer from) the owning
|
|
3717
|
-
*
|
|
4391
|
+
* Thread's Durable Object. `retryable` carries the platform's own stub signal when one
|
|
3718
4392
|
* exists. This error never crosses the wire — it is the CALLER-side evidence that the
|
|
3719
4393
|
* authority was unreachable, which is exactly the case `AdmissionIndeterminate` was
|
|
3720
4394
|
* specified for (SUB-031).
|
|
@@ -3757,24 +4431,24 @@ const portTransportFailure = (target, cause) => {
|
|
|
3757
4431
|
};
|
|
3758
4432
|
/**
|
|
3759
4433
|
* Delivery of Schema-encoded port envelopes to the Durable Object that owns a FOREIGN
|
|
3760
|
-
*
|
|
4434
|
+
* Thread (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
|
|
3761
4435
|
* calls the owner's `portCall` over native Durable Object JS RPC via
|
|
3762
|
-
* `namespace.idFromName(
|
|
4436
|
+
* `namespace.idFromName(threadId)`; the protocol is transport-agnostic and any carrier
|
|
3763
4437
|
* that moves the encoded envelopes verbatim satisfies this service. Implementations MUST
|
|
3764
4438
|
* surface every delivery problem as `PortTransportError` and must never fabricate an answer.
|
|
3765
4439
|
*/
|
|
3766
|
-
var
|
|
4440
|
+
var ThreadPortTransport = class extends Context.Service()("@effect-agent/storage-cloudflare/ThreadPortTransport") {};
|
|
3767
4441
|
const LOCAL = { _tag: "local" };
|
|
3768
4442
|
/**
|
|
3769
|
-
* Parse a DC-minted routable Submission identity — `{uuidv7}:{
|
|
3770
|
-
* FIRST `:` because the
|
|
4443
|
+
* Parse a DC-minted routable Submission identity — `{uuidv7}:{threadId}`, split at the
|
|
4444
|
+
* FIRST `:` because the Thread tail may itself contain colons (D-P6-5). This adapter
|
|
3771
4445
|
* minted the format at admission and is the ONLY component that parses it; identities that do
|
|
3772
4446
|
* not carry the minted shape (no separator, non-UUID head, empty tail) fall back to the local
|
|
3773
4447
|
* facet, which is the only authority this Object can consult without inventing an owner.
|
|
3774
4448
|
* Identities beyond the ledger's 1,024-character row bound fail typed: the minting side
|
|
3775
4449
|
* refused them at admission, so they cannot name any stored row anywhere.
|
|
3776
4450
|
*/
|
|
3777
|
-
const routableSubmissionTarget = (
|
|
4451
|
+
const routableSubmissionTarget = (localThreadId) => Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (operation, submissionId) {
|
|
3778
4452
|
if (submissionId.length > MAX_ROUTABLE_SUBMISSION_ID_LENGTH) return yield* LedgerError.make({
|
|
3779
4453
|
operation,
|
|
3780
4454
|
message: `A Submission identity of ${submissionId.length} characters exceeds the ${MAX_ROUTABLE_SUBMISSION_ID_LENGTH}-character routable identity bound; admission refuses such identities, so it cannot name any stored row.`
|
|
@@ -3783,16 +4457,16 @@ const routableSubmissionTarget = (localConversationId) => Effect.fn("DoPortRouti
|
|
|
3783
4457
|
if (separator === -1) return LOCAL;
|
|
3784
4458
|
if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
|
|
3785
4459
|
const tail = submissionId.slice(separator + 1);
|
|
3786
|
-
if (tail ===
|
|
3787
|
-
return yield*
|
|
4460
|
+
if (tail === localThreadId) return LOCAL;
|
|
4461
|
+
return yield* decodeThreadId(tail).pipe(Effect.map((threadId) => ({
|
|
3788
4462
|
_tag: "foreign",
|
|
3789
|
-
|
|
4463
|
+
threadId
|
|
3790
4464
|
})), Effect.orElseSucceed(() => LOCAL));
|
|
3791
4465
|
});
|
|
3792
4466
|
const NoAdditionalPortFailure = Schema.Never;
|
|
3793
4467
|
const AbortPortFailure = Schema.Union([SettlementConflict, JoinedToHost]);
|
|
3794
4468
|
const AppendPortFailure = Schema.Union([
|
|
3795
|
-
|
|
4469
|
+
ThreadNotMaterialized,
|
|
3796
4470
|
AppendConflict,
|
|
3797
4471
|
FenceRejected
|
|
3798
4472
|
]);
|
|
@@ -3800,13 +4474,13 @@ const AppendPortFailure = Schema.Union([
|
|
|
3800
4474
|
* The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
|
|
3801
4475
|
* (plan §1.3): honesty over accidental distribution.
|
|
3802
4476
|
*/
|
|
3803
|
-
const
|
|
4477
|
+
const crossThreadLedgerError = (operation, target) => LedgerError.make({
|
|
3804
4478
|
operation,
|
|
3805
|
-
message: `${operation} addressed to foreign
|
|
4479
|
+
message: `${operation} addressed to foreign Thread ${target} is not route-capable; the closed cross-Object subset is admit, markReady, lookup, resolveAdmission, requestAbort, and recordChildSettled. Every other ledger operation is lane-local by construction and must execute inside the owning Thread's Durable Object.`
|
|
3806
4480
|
});
|
|
3807
|
-
const
|
|
4481
|
+
const crossThreadStoreError = (operation, target) => ThreadStoreError.make({
|
|
3808
4482
|
operation,
|
|
3809
|
-
message: `${operation} addressed to foreign
|
|
4483
|
+
message: `${operation} addressed to foreign Thread ${target} is not route-capable; the closed cross-Object subset is materialize, append, read (paged), inspectTail, and export. Observation and checkpoints are lane-local by construction and must execute inside the owning Thread's Durable Object.`
|
|
3810
4484
|
});
|
|
3811
4485
|
const makeTransportCall = (transport) => Effect.fn("DoPortRouting.transportCall")(function* (target, call) {
|
|
3812
4486
|
const encoded = yield* encodePortRequest(call).pipe(Effect.mapError((error) => PortProtocolError.make({ message: boundPortDiagnostic(`The port request could not be encoded: ${error.message}`) })));
|
|
@@ -3814,12 +4488,12 @@ const makeTransportCall = (transport) => Effect.fn("DoPortRouting.transportCall"
|
|
|
3814
4488
|
});
|
|
3815
4489
|
const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServices")(function* (options) {
|
|
3816
4490
|
const local = yield* SubmissionLedger;
|
|
3817
|
-
const transport = yield*
|
|
4491
|
+
const transport = yield* ThreadPortTransport;
|
|
3818
4492
|
const transportCall = makeTransportCall(transport);
|
|
3819
|
-
const submissionTarget = routableSubmissionTarget(options.
|
|
4493
|
+
const submissionTarget = routableSubmissionTarget(options.localThreadId);
|
|
3820
4494
|
const routeFailure = (operation, target) => (error) => LedgerError.make({
|
|
3821
4495
|
operation,
|
|
3822
|
-
message: boundPortDiagnostic(`Routed ${operation} to the
|
|
4496
|
+
message: boundPortDiagnostic(`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`),
|
|
3823
4497
|
cause: error
|
|
3824
4498
|
});
|
|
3825
4499
|
/**
|
|
@@ -3838,14 +4512,14 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3838
4512
|
if (failure._tag === "LedgerError") return Effect.fail(failure);
|
|
3839
4513
|
return Effect.fail(LedgerError.make({
|
|
3840
4514
|
operation,
|
|
3841
|
-
message: boundPortDiagnostic(`The
|
|
4515
|
+
message: boundPortDiagnostic(`The Thread Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
|
|
3842
4516
|
cause: failure
|
|
3843
4517
|
}));
|
|
3844
4518
|
}
|
|
3845
4519
|
const result = response.result;
|
|
3846
4520
|
if (!isExpectedResult(result)) return Effect.fail(LedgerError.make({
|
|
3847
4521
|
operation,
|
|
3848
|
-
message: `The
|
|
4522
|
+
message: `The Thread Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
|
|
3849
4523
|
}));
|
|
3850
4524
|
return Effect.succeed(result);
|
|
3851
4525
|
}), Effect.withSpan("DoPortRouting.foreignLedgerCall", { attributes: {
|
|
@@ -3864,13 +4538,13 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3864
4538
|
const resolveForeignAdmission = (target, request) => transportCall(target, LedgerResolveAdmissionCall.make({ request })).pipe(Effect.flatMap((response) => {
|
|
3865
4539
|
if (response._tag === "PortFailed") {
|
|
3866
4540
|
if (response.failure._tag === "LedgerError") return Effect.fail(response.failure);
|
|
3867
|
-
return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The
|
|
4541
|
+
return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Thread Object owning ${target} answered resolveAdmission with the out-of-contract failure ${response.failure._tag}: ${response.failure.message}`) }));
|
|
3868
4542
|
}
|
|
3869
|
-
if (response.result._tag !== "LedgerResolveAdmissionResult") return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The
|
|
4543
|
+
if (response.result._tag !== "LedgerResolveAdmissionResult") return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Thread Object owning ${target} answered resolveAdmission with the mismatched result ${response.result._tag}.`) }));
|
|
3870
4544
|
return Effect.succeed(response.result.resolution);
|
|
3871
4545
|
}), Effect.catchTags({
|
|
3872
|
-
PortTransportError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The
|
|
3873
|
-
PortProtocolError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The answer of the
|
|
4546
|
+
PortTransportError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Thread Object owning ${target} is unreachable: ${error.message}`) })),
|
|
4547
|
+
PortProtocolError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The answer of the Thread Object owning ${target} could not be understood: ${error.message}`) }))
|
|
3874
4548
|
}), Effect.withSpan("DoPortRouting.resolveForeignAdmission", { attributes: { target } }));
|
|
3875
4549
|
const foreignLookupById = (operation, target, submissionId) => foreignLedgerCall(operation, target, LedgerLookupCall.make({ request: SubmissionLookupById.make({ submissionId }) }), LedgerLookupResult, NoAdditionalPortFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission)));
|
|
3876
4550
|
/**
|
|
@@ -3889,7 +4563,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3889
4563
|
if (childSubmissionId === void 0 || attachments.has(childSubmissionId)) continue;
|
|
3890
4564
|
const target = yield* submissionTarget(operation, childSubmissionId);
|
|
3891
4565
|
if (target._tag !== "foreign") continue;
|
|
3892
|
-
const child = yield* foreignLookupById(operation, target.
|
|
4566
|
+
const child = yield* foreignLookupById(operation, target.threadId, childSubmissionId);
|
|
3893
4567
|
if (Option.isNone(child)) continue;
|
|
3894
4568
|
attachments.set(childSubmissionId, ChildAttachmentSnapshot.make({
|
|
3895
4569
|
toolCallId: reservation.parentToolCallId,
|
|
@@ -3913,44 +4587,45 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3913
4587
|
});
|
|
3914
4588
|
const routed = SubmissionLedger.of({
|
|
3915
4589
|
capabilities: local.capabilities,
|
|
3916
|
-
admit: (request) => request.
|
|
3917
|
-
markReady: (request) => submissionTarget("ledger mark ready", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markReady(request) : foreignLedgerCall("ledger mark ready", target.
|
|
3918
|
-
lookup: (request) => request._tag === "SubmissionLookupById" ? submissionTarget("ledger lookup", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.lookup(request) : foreignLookupById("ledger lookup", target.
|
|
3919
|
-
resolveAdmission: (request) => request.
|
|
3920
|
-
requestAbort: (request) => submissionTarget("ledger request abort", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.requestAbort(request) : foreignLedgerCall("ledger request abort", target.
|
|
3921
|
-
recordChildSettled: (request) => submissionTarget("ledger record child settled", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordChildSettled(request) : foreignLedgerCall("ledger record child settled", target.
|
|
3922
|
-
claim: (request) => request.
|
|
3923
|
-
claimJoining: (request) => request.
|
|
3924
|
-
renewOwnership: (request) => submissionTarget("ledger renew ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.renewOwnership(request) : Effect.fail(
|
|
3925
|
-
releaseOwnership: (request) => submissionTarget("ledger release ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.releaseOwnership(request) : Effect.fail(
|
|
3926
|
-
markInputApplied: (request) => submissionTarget("ledger mark input applied", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markInputApplied(request) : Effect.fail(
|
|
3927
|
-
reserveSettlement: (request) => submissionTarget("ledger reserve settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveSettlement(request) : Effect.fail(
|
|
3928
|
-
finalizeSettlement: (request) => submissionTarget("ledger finalize settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.finalizeSettlement(request) : Effect.fail(
|
|
3929
|
-
markJoined: (request) => submissionTarget("ledger mark joined", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markJoined(request) : Effect.fail(
|
|
3930
|
-
revertJoining: (request) => submissionTarget("ledger revert joining", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.revertJoining(request) : Effect.fail(
|
|
3931
|
-
suspend: (request) => submissionTarget("ledger suspend", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.suspend(request) : Effect.fail(
|
|
3932
|
-
recordApprovalDecision: (command) => submissionTarget("ledger record approval decision", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordApprovalDecision(command) : Effect.fail(
|
|
3933
|
-
markUnknown: (request) => submissionTarget("ledger mark unknown", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markUnknown(request) : Effect.fail(
|
|
3934
|
-
recordUnknownResolution: (command) => submissionTarget("ledger record unknown resolution", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordUnknownResolution(command) : Effect.fail(
|
|
3935
|
-
reserveChildBudget: (request) => submissionTarget("ledger reserve child budget", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveChildBudget(request) : Effect.fail(
|
|
4590
|
+
admit: (request) => request.threadId === options.localThreadId ? local.admit(request) : foreignLedgerCall("ledger admit", request.threadId, LedgerAdmitCall.make({ request }), LedgerAdmitResult, AdmissionConflict).pipe(Effect.map((reply) => reply.result)),
|
|
4591
|
+
markReady: (request) => submissionTarget("ledger mark ready", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markReady(request) : foreignLedgerCall("ledger mark ready", target.threadId, LedgerMarkReadyCall.make({ request }), LedgerMarkReadyResult, NoAdditionalPortFailure).pipe(Effect.asVoid))),
|
|
4592
|
+
lookup: (request) => request._tag === "SubmissionLookupById" ? submissionTarget("ledger lookup", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.lookup(request) : foreignLookupById("ledger lookup", target.threadId, request.submissionId))) : request.threadId === options.localThreadId ? local.lookup(request) : foreignLedgerCall("ledger lookup", request.threadId, LedgerLookupCall.make({ request }), LedgerLookupResult, NoAdditionalPortFailure).pipe(Effect.map((result) => result.submission === void 0 ? Option.none() : Option.some(result.submission))),
|
|
4593
|
+
resolveAdmission: (request) => request.threadId === options.localThreadId ? local.resolveAdmission(request) : resolveForeignAdmission(request.threadId, request),
|
|
4594
|
+
requestAbort: (request) => submissionTarget("ledger request abort", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.requestAbort(request) : foreignLedgerCall("ledger request abort", target.threadId, LedgerRequestAbortCall.make({ request }), LedgerRequestAbortResult, AbortPortFailure).pipe(Effect.map((reply) => reply.intent)))),
|
|
4595
|
+
recordChildSettled: (request) => submissionTarget("ledger record child settled", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordChildSettled(request) : foreignLedgerCall("ledger record child settled", target.threadId, LedgerRecordChildSettledCall.make({ request }), LedgerRecordChildSettledResult, NoAdditionalPortFailure).pipe(Effect.map((reply) => reply.outcome)))),
|
|
4596
|
+
claim: (request) => request.threadId === options.localThreadId ? local.claim(request) : Effect.fail(crossThreadLedgerError("ledger claim", request.threadId)),
|
|
4597
|
+
claimJoining: (request) => request.threadId === options.localThreadId ? local.claimJoining(request) : Effect.fail(crossThreadLedgerError("ledger claim joining", request.threadId)),
|
|
4598
|
+
renewOwnership: (request) => submissionTarget("ledger renew ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.renewOwnership(request) : Effect.fail(crossThreadLedgerError("ledger renew ownership", target.threadId)))),
|
|
4599
|
+
releaseOwnership: (request) => submissionTarget("ledger release ownership", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.releaseOwnership(request) : Effect.fail(crossThreadLedgerError("ledger release ownership", target.threadId)))),
|
|
4600
|
+
markInputApplied: (request) => submissionTarget("ledger mark input applied", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markInputApplied(request) : Effect.fail(crossThreadLedgerError("ledger mark input applied", target.threadId)))),
|
|
4601
|
+
reserveSettlement: (request) => submissionTarget("ledger reserve settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveSettlement(request) : Effect.fail(crossThreadLedgerError("ledger reserve settlement", target.threadId)))),
|
|
4602
|
+
finalizeSettlement: (request) => submissionTarget("ledger finalize settlement", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.finalizeSettlement(request) : Effect.fail(crossThreadLedgerError("ledger finalize settlement", target.threadId)))),
|
|
4603
|
+
markJoined: (request) => submissionTarget("ledger mark joined", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markJoined(request) : Effect.fail(crossThreadLedgerError("ledger mark joined", target.threadId)))),
|
|
4604
|
+
revertJoining: (request) => submissionTarget("ledger revert joining", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.revertJoining(request) : Effect.fail(crossThreadLedgerError("ledger revert joining", target.threadId)))),
|
|
4605
|
+
suspend: (request) => submissionTarget("ledger suspend", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.suspend(request) : Effect.fail(crossThreadLedgerError("ledger suspend", target.threadId)))),
|
|
4606
|
+
recordApprovalDecision: (command) => submissionTarget("ledger record approval decision", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordApprovalDecision(command) : Effect.fail(crossThreadLedgerError("ledger record approval decision", target.threadId)))),
|
|
4607
|
+
markUnknown: (request) => submissionTarget("ledger mark unknown", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.markUnknown(request) : Effect.fail(crossThreadLedgerError("ledger mark unknown", target.threadId)))),
|
|
4608
|
+
recordUnknownResolution: (command) => submissionTarget("ledger record unknown resolution", command.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.recordUnknownResolution(command) : Effect.fail(crossThreadLedgerError("ledger record unknown resolution", target.threadId)))),
|
|
4609
|
+
reserveChildBudget: (request) => submissionTarget("ledger reserve child budget", request.parentSubmissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.reserveChildBudget(request) : Effect.fail(crossThreadLedgerError("ledger reserve child budget", target.threadId)))),
|
|
3936
4610
|
attachChildToReservation: local.attachChildToReservation,
|
|
3937
4611
|
beginChildBudgetRelease: local.beginChildBudgetRelease,
|
|
3938
4612
|
releaseChildBudget: local.releaseChildBudget,
|
|
3939
4613
|
scanNonterminal: local.scanNonterminal,
|
|
3940
|
-
loadRecoverySnapshot: (request) => submissionTarget("ledger load recovery snapshot", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.loadRecoverySnapshot(request).pipe(Effect.flatMap(enrichChildAttachments)) : Effect.fail(
|
|
4614
|
+
loadRecoverySnapshot: (request) => submissionTarget("ledger load recovery snapshot", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.loadRecoverySnapshot(request).pipe(Effect.flatMap(enrichChildAttachments)) : Effect.fail(crossThreadLedgerError("ledger load recovery snapshot", target.threadId))))
|
|
3941
4615
|
});
|
|
3942
4616
|
return Context.make(SubmissionLedger, routed);
|
|
3943
4617
|
});
|
|
3944
4618
|
const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices")(function* (options) {
|
|
3945
|
-
const local = yield*
|
|
3946
|
-
const
|
|
4619
|
+
const local = yield* ThreadStore;
|
|
4620
|
+
const checkpoints = local.checkpoints;
|
|
4621
|
+
const transport = yield* ThreadPortTransport;
|
|
3947
4622
|
const transportCall = makeTransportCall(transport);
|
|
3948
|
-
const routeFailure = (operation, target) => (error) =>
|
|
4623
|
+
const routeFailure = (operation, target) => (error) => ThreadStoreError.make({
|
|
3949
4624
|
operation,
|
|
3950
|
-
message: boundPortDiagnostic(`Routed ${operation} to the
|
|
4625
|
+
message: boundPortDiagnostic(`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`),
|
|
3951
4626
|
cause: error
|
|
3952
4627
|
});
|
|
3953
|
-
/** The store twin of `foreignLedgerCall` with `
|
|
4628
|
+
/** The store twin of `foreignLedgerCall` with `ThreadStoreError` as the base error. */
|
|
3954
4629
|
const foreignStoreCall = (operation, target, call, resultSchema, failureSchema) => {
|
|
3955
4630
|
const isExpectedResult = Schema.is(resultSchema);
|
|
3956
4631
|
const isExpectedFailure = Schema.is(failureSchema);
|
|
@@ -3958,17 +4633,17 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
|
|
|
3958
4633
|
if (response._tag === "PortFailed") {
|
|
3959
4634
|
const failure = response.failure;
|
|
3960
4635
|
if (isExpectedFailure(failure)) return Effect.fail(failure);
|
|
3961
|
-
if (failure._tag === "
|
|
3962
|
-
return Effect.fail(
|
|
4636
|
+
if (failure._tag === "ThreadStoreError") return Effect.fail(failure);
|
|
4637
|
+
return Effect.fail(ThreadStoreError.make({
|
|
3963
4638
|
operation,
|
|
3964
|
-
message: boundPortDiagnostic(`The
|
|
4639
|
+
message: boundPortDiagnostic(`The Thread Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
|
|
3965
4640
|
cause: failure
|
|
3966
4641
|
}));
|
|
3967
4642
|
}
|
|
3968
4643
|
const result = response.result;
|
|
3969
|
-
if (!isExpectedResult(result)) return Effect.fail(
|
|
4644
|
+
if (!isExpectedResult(result)) return Effect.fail(ThreadStoreError.make({
|
|
3970
4645
|
operation,
|
|
3971
|
-
message: `The
|
|
4646
|
+
message: `The Thread Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
|
|
3972
4647
|
}));
|
|
3973
4648
|
return Effect.succeed(result);
|
|
3974
4649
|
}), Effect.withSpan("DoPortRouting.foreignStoreCall", { attributes: {
|
|
@@ -3976,39 +4651,41 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
|
|
|
3976
4651
|
target
|
|
3977
4652
|
} }));
|
|
3978
4653
|
};
|
|
3979
|
-
const routed =
|
|
3980
|
-
materialize: (request) => request.
|
|
3981
|
-
append: (request) => request.
|
|
3982
|
-
read: (request) => request.
|
|
3983
|
-
inspectTail: (request) => request.
|
|
3984
|
-
export: (request) => request.
|
|
3985
|
-
observe: (request) => request.
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
4654
|
+
const routed = ThreadStore.of({
|
|
4655
|
+
materialize: (request) => request.threadId === options.localThreadId ? local.materialize(request) : foreignStoreCall("thread materialize", request.threadId, StoreMaterializeCall.make({ request }), StoreMaterializeResult, FenceRejected).pipe(Effect.asVoid),
|
|
4656
|
+
append: (request) => request.threadId === options.localThreadId ? local.append(request) : foreignStoreCall("thread append", request.threadId, StoreAppendCall.make({ request }), StoreAppendResult, AppendPortFailure).pipe(Effect.map((reply) => reply.result)),
|
|
4657
|
+
read: (request) => request.threadId === options.localThreadId ? local.read(request) : Stream.unwrap(foreignStoreCall("thread read", request.threadId, StoreReadPageCall.make({ request }), StoreReadPageResult, ThreadNotMaterialized).pipe(Effect.map((reply) => Stream.fromIterable(reply.records)))),
|
|
4658
|
+
inspectTail: (request) => request.threadId === options.localThreadId ? local.inspectTail(request) : foreignStoreCall("thread inspect tail", request.threadId, StoreInspectTailCall.make({ request }), StoreInspectTailResult, ThreadNotMaterialized).pipe(Effect.map((reply) => reply.tail)),
|
|
4659
|
+
export: (request) => request.threadId === options.localThreadId ? local.export(request) : foreignStoreCall("thread export", request.threadId, StoreExportCall.make({ request }), StoreExportResult, ThreadNotMaterialized).pipe(Effect.map((reply) => reply.export)),
|
|
4660
|
+
observe: (request) => request.threadId === options.localThreadId ? local.observe(request) : Stream.unwrap(Effect.fail(crossThreadStoreError("thread observe", request.threadId))),
|
|
4661
|
+
...checkpoints === void 0 ? {} : { checkpoints: {
|
|
4662
|
+
save: (request) => request.checkpoint.threadId === options.localThreadId ? checkpoints.save(request) : Effect.fail(crossThreadStoreError("thread save checkpoint", request.checkpoint.threadId)),
|
|
4663
|
+
load: (request) => request.threadId === options.localThreadId ? checkpoints.load(request) : Effect.fail(crossThreadStoreError("thread load checkpoint", request.threadId))
|
|
4664
|
+
} }
|
|
4665
|
+
});
|
|
4666
|
+
return Context.make(ThreadStore, routed);
|
|
3990
4667
|
});
|
|
3991
4668
|
/**
|
|
3992
4669
|
* Routing decorator over the LOCAL `SubmissionLedger` facet (plan §1.3): a request addressing
|
|
3993
|
-
* this Object's
|
|
3994
|
-
*
|
|
4670
|
+
* this Object's Thread executes locally; a route-capable request addressing another
|
|
4671
|
+
* Thread is Schema-encoded onto the `ThreadPortTransport` and executed by the
|
|
3995
4672
|
* owning Object's local facet; any other foreign request fails fast typed. Provide the WP1
|
|
3996
4673
|
* local facet (`submissionLedgerLayer`/`ledgerLayer`) and a transport to close it.
|
|
3997
4674
|
*/
|
|
3998
4675
|
const routedSubmissionLedgerLayer = (options) => Layer.effectContext(makeRoutedLedgerServices(options));
|
|
3999
4676
|
/**
|
|
4000
|
-
* Routing decorator over the LOCAL `
|
|
4677
|
+
* Routing decorator over the LOCAL `ThreadStore` facet (plan §1.3): this-thread
|
|
4001
4678
|
* requests execute locally; foreign materialize/append/read/inspectTail/export travel the
|
|
4002
4679
|
* transport; foreign observation and checkpoints fail fast typed.
|
|
4003
4680
|
*/
|
|
4004
|
-
const
|
|
4681
|
+
const routedThreadStoreLayer = (options) => Layer.effectContext(makeRoutedStoreServices(options));
|
|
4005
4682
|
/** Fold one port operation's typed failures into the uniform response envelope. */
|
|
4006
4683
|
const capture = (effect) => effect.pipe(Effect.map((result) => PortSucceeded.make({ result })), Effect.catch((failure) => Effect.succeed(PortFailed.make({ failure }))));
|
|
4007
4684
|
/**
|
|
4008
4685
|
* Execute one decoded port request against THIS Object's LOCAL facets — the owner-side half
|
|
4009
4686
|
* of the routed ports (plan §1.3). Callers must provide the WP1 local facets, never the
|
|
4010
4687
|
* routed decorators: the routing layer already established that this Object owns the
|
|
4011
|
-
* addressed
|
|
4688
|
+
* addressed Thread, and re-routing here could bounce a request between Objects.
|
|
4012
4689
|
* Failures never escape — every typed port failure becomes a `PortFailed` envelope that
|
|
4013
4690
|
* re-decodes on the caller side.
|
|
4014
4691
|
*/
|
|
@@ -4039,24 +4716,24 @@ const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(functio
|
|
|
4039
4716
|
return yield* capture(ledger.recordChildSettled(request.request).pipe(Effect.map((outcome) => LedgerRecordChildSettledResult.make({ outcome }))));
|
|
4040
4717
|
}
|
|
4041
4718
|
case "StoreMaterialize": {
|
|
4042
|
-
const store = yield*
|
|
4719
|
+
const store = yield* ThreadStore;
|
|
4043
4720
|
return yield* capture(store.materialize(request.request).pipe(Effect.map(() => StoreMaterializeResult.make({}))));
|
|
4044
4721
|
}
|
|
4045
4722
|
case "StoreAppend": {
|
|
4046
|
-
const store = yield*
|
|
4723
|
+
const store = yield* ThreadStore;
|
|
4047
4724
|
return yield* capture(store.append(request.request).pipe(Effect.map((result) => StoreAppendResult.make({ result }))));
|
|
4048
4725
|
}
|
|
4049
4726
|
case "StoreReadPage": {
|
|
4050
|
-
const store = yield*
|
|
4727
|
+
const store = yield* ThreadStore;
|
|
4051
4728
|
return yield* capture(store.read(request.request).pipe(Stream.runCollect, Effect.map((records) => StoreReadPageResult.make({ records: [...records] }))));
|
|
4052
4729
|
}
|
|
4053
4730
|
case "StoreInspectTail": {
|
|
4054
|
-
const store = yield*
|
|
4731
|
+
const store = yield* ThreadStore;
|
|
4055
4732
|
return yield* capture(store.inspectTail(request.request).pipe(Effect.map((tail) => StoreInspectTailResult.make({ tail }))));
|
|
4056
4733
|
}
|
|
4057
4734
|
case "StoreExport": {
|
|
4058
|
-
const store = yield*
|
|
4059
|
-
return yield* capture(store.export(request.request).pipe(Effect.map((
|
|
4735
|
+
const store = yield* ThreadStore;
|
|
4736
|
+
return yield* capture(store.export(request.request).pipe(Effect.map((threadExport) => StoreExportResult.make({ export: threadExport }))));
|
|
4060
4737
|
}
|
|
4061
4738
|
}
|
|
4062
4739
|
});
|
|
@@ -4083,6 +4760,6 @@ const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPortReque
|
|
|
4083
4760
|
return yield* encodePortResponse(yield* decodePortRequest(encoded).pipe(Effect.flatMap(executePortRequest), Effect.catch((error) => Effect.succeed(PortFailed.make({ failure: PortProtocolError.make({ message: boundPortDiagnostic(`The port request could not be decoded: ${error.message}`) }) }))))).pipe(Effect.catch((error) => Effect.succeed(encodedProtocolFailure(`The port response could not be encoded: ${error.message}`))));
|
|
4084
4761
|
});
|
|
4085
4762
|
//#endregion
|
|
4086
|
-
export {
|
|
4763
|
+
export { CurrentDoStorageVersion, DEFAULT_MAX_STORED_VALUE_BYTES, DoAppendConflict, DoCheckpointConflict, DoFenceRejected, DoLedgerError, DoScheduleAlarmControl, DoScheduleTransaction, DoStorageCompatibilityError, DoStorageConfig, DoStorageConfigValue, DoStorageCorruptionError, DoStorageError, DoStorageFailpoint, DoStorageFailpointError, DoStorageFailpointLocation, DoSubscriptionAlarmControl, DoSubscriptionTransaction, DoValueBoundExceeded, LedgerAdmitCall, LedgerAdmitResult, LedgerLookupCall, LedgerLookupResult, LedgerMarkReadyCall, LedgerMarkReadyResult, LedgerRecordChildSettledCall, LedgerRecordChildSettledResult, LedgerRequestAbortCall, LedgerRequestAbortResult, LedgerResolveAdmissionCall, LedgerResolveAdmissionResult, MAX_PORT_DIAGNOSTIC_LENGTH, PortFailed, PortFailure, PortProtocolError, PortRequest, PortResponse, PortResult, PortSucceeded, PortTransportError, StoreAppendCall, StoreAppendResult, StoreExportCall, StoreExportResult, StoreInspectTailCall, StoreInspectTailResult, StoreMaterializeCall, StoreMaterializeResult, StoreReadPageCall, StoreReadPageResult, ThreadPortTransport, boundPortDiagnostic, decodePortRequest, decodePortResponse, doSubscriptionStoreLayer, encodePortRequest, encodePortResponse, executePortRequest, handleEncodedPortRequest, layer, ledgerLayer, observationOffsetAt, portTransportFailure, routedSubmissionLedgerLayer, routedThreadStoreLayer, scheduleStoreLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer, threadStoreLayer };
|
|
4087
4764
|
|
|
4088
4765
|
//# sourceMappingURL=index.mjs.map
|