@effect-agent/storage-cloudflare 0.1.0-beta.38 → 0.1.0-beta.40
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 +161 -248
- package/dist/index.mjs +1211 -539
- 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 +14 -7
- 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 +28 -35
- 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",
|
|
@@ -145,32 +146,25 @@ var DoStorageFailpointError = class extends Schema.TaggedError()("DoStorageFailp
|
|
|
145
146
|
};
|
|
146
147
|
//#endregion
|
|
147
148
|
//#region src/migrations.ts
|
|
149
|
+
/** The current storage version recorded in `effect_agent_meta`. */
|
|
150
|
+
const CurrentDoStorageVersion = 2;
|
|
148
151
|
/**
|
|
149
|
-
* The
|
|
150
|
-
*
|
|
151
|
-
* v1→v4 history to replay (deployment spec §9: no rolling data-version promise during
|
|
152
|
-
* private development).
|
|
153
|
-
*/
|
|
154
|
-
const CurrentDoStorageVersion = 1;
|
|
155
|
-
/**
|
|
156
|
-
* The Conversation Durable Object schema. Table names and columns mirror the Node/SQLite v4
|
|
157
|
-
* schema byte-for-byte (`packages/storage-sqlite/src/migrations.ts`, migrations 1–4 collapsed
|
|
158
|
-
* into their final shape) so the shared conformance suites and crash-matrix rows address
|
|
159
|
-
* identical durable state. Two DC-specific additions:
|
|
152
|
+
* The Thread Durable Object schema shares its thread and ledger tables with Node/SQLite.
|
|
153
|
+
* Schedules and subscriptions use separate Durable Objects. Two DC-specific additions:
|
|
160
154
|
*
|
|
161
155
|
* 1. `effect_agent_meta` replaces `PRAGMA user_version` as the exact-or-fresh version gate —
|
|
162
156
|
* a meta table is portable regardless of which PRAGMAs Durable Object SQL storage allows.
|
|
163
157
|
* 2. `effect_agent_child_settlements` is the durable cross-store notification marker the
|
|
164
158
|
* SubmissionLedger port contract mandates for cross-store adapters (`suspend`'s covering
|
|
165
|
-
* check and `recordChildSettled`'s wake both consult it): parent and child
|
|
159
|
+
* check and `recordChildSettled`'s wake both consult it): parent and child Threads
|
|
166
160
|
* live in different Durable Objects, so a child settlement reported before the parent's
|
|
167
161
|
* suspend commits must be observable from the PARENT's own storage.
|
|
168
162
|
*/
|
|
169
|
-
const doMigrations = SqliteMigrator.fromRecord({ "
|
|
163
|
+
const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_thread_object": Effect.gen(function* () {
|
|
170
164
|
const sql = yield* SqlClient.SqlClient;
|
|
171
165
|
yield* sql`
|
|
172
|
-
CREATE TABLE
|
|
173
|
-
|
|
166
|
+
CREATE TABLE effect_agent_threads (
|
|
167
|
+
thread_id TEXT PRIMARY KEY NOT NULL,
|
|
174
168
|
created_at TEXT NOT NULL,
|
|
175
169
|
tail_sequence INTEGER NOT NULL,
|
|
176
170
|
tail_digest TEXT NOT NULL,
|
|
@@ -179,53 +173,53 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
179
173
|
`.withoutTransform;
|
|
180
174
|
yield* sql`
|
|
181
175
|
CREATE TABLE effect_agent_canonical_batches (
|
|
182
|
-
|
|
176
|
+
thread_id TEXT NOT NULL,
|
|
183
177
|
batch_id TEXT NOT NULL,
|
|
184
178
|
first_sequence INTEGER NOT NULL,
|
|
185
179
|
last_sequence INTEGER NOT NULL,
|
|
186
180
|
batch_digest TEXT NOT NULL,
|
|
187
181
|
tail_digest TEXT NOT NULL,
|
|
188
182
|
batch_json TEXT NOT NULL,
|
|
189
|
-
PRIMARY KEY (
|
|
190
|
-
FOREIGN KEY (
|
|
191
|
-
REFERENCES
|
|
183
|
+
PRIMARY KEY (thread_id, batch_id),
|
|
184
|
+
FOREIGN KEY (thread_id)
|
|
185
|
+
REFERENCES effect_agent_threads(thread_id)
|
|
192
186
|
ON DELETE RESTRICT
|
|
193
187
|
)
|
|
194
188
|
`.withoutTransform;
|
|
195
189
|
yield* sql`
|
|
196
190
|
CREATE TABLE effect_agent_canonical_records (
|
|
197
|
-
|
|
191
|
+
thread_id TEXT NOT NULL,
|
|
198
192
|
sequence INTEGER NOT NULL,
|
|
199
193
|
record_id TEXT NOT NULL,
|
|
200
194
|
batch_id TEXT NOT NULL,
|
|
201
195
|
record_json TEXT NOT NULL,
|
|
202
|
-
PRIMARY KEY (
|
|
203
|
-
UNIQUE (
|
|
204
|
-
FOREIGN KEY (
|
|
205
|
-
REFERENCES effect_agent_canonical_batches(
|
|
196
|
+
PRIMARY KEY (thread_id, sequence),
|
|
197
|
+
UNIQUE (thread_id, record_id),
|
|
198
|
+
FOREIGN KEY (thread_id, batch_id)
|
|
199
|
+
REFERENCES effect_agent_canonical_batches(thread_id, batch_id)
|
|
206
200
|
ON DELETE RESTRICT
|
|
207
201
|
)
|
|
208
202
|
`.withoutTransform;
|
|
209
203
|
yield* sql`
|
|
210
204
|
CREATE INDEX effect_agent_canonical_records_batch
|
|
211
|
-
ON effect_agent_canonical_records (
|
|
205
|
+
ON effect_agent_canonical_records (thread_id, batch_id, sequence)
|
|
212
206
|
`.withoutTransform;
|
|
213
207
|
yield* sql`
|
|
214
208
|
CREATE TABLE effect_agent_checkpoints (
|
|
215
|
-
|
|
209
|
+
thread_id TEXT NOT NULL,
|
|
216
210
|
through_sequence INTEGER NOT NULL,
|
|
217
211
|
tail_digest TEXT NOT NULL,
|
|
218
212
|
checkpoint_json TEXT NOT NULL,
|
|
219
|
-
PRIMARY KEY (
|
|
220
|
-
FOREIGN KEY (
|
|
221
|
-
REFERENCES
|
|
213
|
+
PRIMARY KEY (thread_id, through_sequence),
|
|
214
|
+
FOREIGN KEY (thread_id)
|
|
215
|
+
REFERENCES effect_agent_threads(thread_id)
|
|
222
216
|
ON DELETE RESTRICT
|
|
223
217
|
)
|
|
224
218
|
`.withoutTransform;
|
|
225
219
|
yield* sql`
|
|
226
220
|
CREATE TABLE effect_agent_submissions (
|
|
227
221
|
submission_id TEXT PRIMARY KEY NOT NULL,
|
|
228
|
-
|
|
222
|
+
thread_id TEXT NOT NULL,
|
|
229
223
|
queue_sequence INTEGER NOT NULL,
|
|
230
224
|
principal TEXT NOT NULL,
|
|
231
225
|
idempotency_key TEXT NOT NULL,
|
|
@@ -248,8 +242,8 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
248
242
|
unknown_tool_call_ids_json TEXT,
|
|
249
243
|
parent_submission_id TEXT,
|
|
250
244
|
parent_tool_call_id TEXT,
|
|
251
|
-
UNIQUE (
|
|
252
|
-
UNIQUE (
|
|
245
|
+
UNIQUE (thread_id, principal, idempotency_key),
|
|
246
|
+
UNIQUE (thread_id, queue_sequence)
|
|
253
247
|
)
|
|
254
248
|
`.withoutTransform;
|
|
255
249
|
yield* sql`
|
|
@@ -277,7 +271,7 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
277
271
|
CREATE TABLE effect_agent_attempts (
|
|
278
272
|
attempt_id TEXT PRIMARY KEY NOT NULL,
|
|
279
273
|
submission_id TEXT NOT NULL,
|
|
280
|
-
|
|
274
|
+
thread_id TEXT NOT NULL,
|
|
281
275
|
owner_producer_id TEXT NOT NULL,
|
|
282
276
|
producer_epoch INTEGER NOT NULL,
|
|
283
277
|
claimed_at TEXT NOT NULL,
|
|
@@ -377,7 +371,7 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
377
371
|
`.withoutTransform;
|
|
378
372
|
yield* sql`
|
|
379
373
|
INSERT INTO effect_agent_meta (key, value)
|
|
380
|
-
VALUES ('storage_version', ${String(
|
|
374
|
+
VALUES ('storage_version', ${String(2)})
|
|
381
375
|
`.withoutTransform;
|
|
382
376
|
}) });
|
|
383
377
|
//#endregion
|
|
@@ -390,10 +384,8 @@ const doMigrations = SqliteMigrator.fromRecord({ "1_current_cloudflare_conversat
|
|
|
390
384
|
*/
|
|
391
385
|
const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(2e6));
|
|
392
386
|
const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
393
|
-
const
|
|
387
|
+
const MAX_RECORDS_PER_THREAD = 65536;
|
|
394
388
|
const MAX_IDENTIFIER_LENGTH$1 = 1024;
|
|
395
|
-
/** Durable Object SQL storage allows at most 100 bound parameters per statement. */
|
|
396
|
-
const MAX_BOUND_PARAMETERS = 100;
|
|
397
389
|
const isSqlError = Schema.is(SqlError);
|
|
398
390
|
const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
399
391
|
const chunked = (values, size) => {
|
|
@@ -403,8 +395,8 @@ const chunked = (values, size) => {
|
|
|
403
395
|
};
|
|
404
396
|
var DoMetaRow = class extends Schema.Class("DoMetaRow")({ value: Schema.NonEmptyString.check(Schema.isMaxLength(128)) }) {};
|
|
405
397
|
var DoNameRow = class extends Schema.Class("DoNameRow")({ name: BoundedIdentifier$1 }) {};
|
|
406
|
-
var
|
|
407
|
-
|
|
398
|
+
var ThreadRow = class extends Schema.Class("ThreadRow")({
|
|
399
|
+
thread_id: BoundedIdentifier$1,
|
|
408
400
|
created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
|
|
409
401
|
producer_epoch: ProducerEpoch,
|
|
410
402
|
tail_digest: BoundedStoredText$1,
|
|
@@ -414,21 +406,21 @@ var BatchRow = class extends Schema.Class("BatchRow")({
|
|
|
414
406
|
batch_digest: BoundedStoredText$1,
|
|
415
407
|
batch_id: BoundedIdentifier$1,
|
|
416
408
|
batch_json: BoundedStoredText$1,
|
|
417
|
-
|
|
409
|
+
thread_id: BoundedIdentifier$1,
|
|
418
410
|
first_sequence: CanonicalSequence,
|
|
419
411
|
last_sequence: CanonicalSequence,
|
|
420
412
|
tail_digest: BoundedStoredText$1
|
|
421
413
|
}) {};
|
|
422
414
|
var RecordRow = class extends Schema.Class("RecordRow")({
|
|
423
415
|
batch_id: BoundedIdentifier$1,
|
|
424
|
-
|
|
416
|
+
thread_id: BoundedIdentifier$1,
|
|
425
417
|
record_id: BoundedIdentifier$1,
|
|
426
418
|
record_json: BoundedStoredText$1,
|
|
427
419
|
sequence: CanonicalSequence
|
|
428
420
|
}) {};
|
|
429
421
|
var CheckpointRow = class extends Schema.Class("CheckpointRow")({
|
|
430
422
|
checkpoint_json: BoundedStoredText$1,
|
|
431
|
-
|
|
423
|
+
thread_id: BoundedIdentifier$1,
|
|
432
424
|
tail_digest: BoundedStoredText$1,
|
|
433
425
|
through_sequence: CanonicalSequence
|
|
434
426
|
}) {};
|
|
@@ -440,7 +432,7 @@ var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-cloudfl
|
|
|
440
432
|
batchDigest: BoundedStoredText$1,
|
|
441
433
|
batchId: BoundedIdentifier$1,
|
|
442
434
|
batchJson: BoundedStoredText$1,
|
|
443
|
-
|
|
435
|
+
threadId: BoundedIdentifier$1,
|
|
444
436
|
expectedTailDigest: BoundedStoredText$1,
|
|
445
437
|
expectedTailSequence: CanonicalSequence,
|
|
446
438
|
producerEpoch: ProducerEpoch,
|
|
@@ -454,36 +446,36 @@ var RawAppendResult = class extends Schema.Class("@effect-agent/storage-cloudfla
|
|
|
454
446
|
tailDigest: BoundedStoredText$1
|
|
455
447
|
}) {};
|
|
456
448
|
var RawReadRequest = class extends Schema.Class("@effect-agent/storage-cloudflare/RawReadRequest")({
|
|
457
|
-
|
|
449
|
+
threadId: BoundedIdentifier$1,
|
|
458
450
|
fromSequenceExclusive: CanonicalSequence,
|
|
459
451
|
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
|
|
460
452
|
}) {};
|
|
461
453
|
var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-cloudflare/RawCheckpoint")({
|
|
462
454
|
checkpointJson: BoundedStoredText$1,
|
|
463
|
-
|
|
455
|
+
threadId: BoundedIdentifier$1,
|
|
464
456
|
tailDigest: BoundedStoredText$1,
|
|
465
457
|
throughSequence: CanonicalSequence
|
|
466
458
|
}) {};
|
|
467
|
-
var
|
|
459
|
+
var RawThreadExport = class extends Schema.Class("@effect-agent/storage-cloudflare/RawThreadExport")({
|
|
468
460
|
batches: Schema.Array(BatchRow),
|
|
469
461
|
checkpoints: Schema.Array(CheckpointRow),
|
|
470
|
-
|
|
462
|
+
thread: ThreadRow,
|
|
471
463
|
records: Schema.Array(RecordRow)
|
|
472
464
|
}) {};
|
|
473
|
-
const noFailpoint
|
|
465
|
+
const noFailpoint = () => Effect.void;
|
|
474
466
|
const storageError = (operation) => (error) => DoStorageError.make({
|
|
475
467
|
cause: error,
|
|
476
468
|
operation,
|
|
477
469
|
message: error.message
|
|
478
470
|
});
|
|
479
471
|
/** Decode raw Durable Object SQLite rows against a Schema, reporting failures as typed corruption. */
|
|
480
|
-
const decodeRows$
|
|
472
|
+
const decodeRows$2 = Effect.fn((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
481
473
|
table,
|
|
482
474
|
rowKey,
|
|
483
475
|
message: String(error)
|
|
484
476
|
}))));
|
|
485
477
|
/** Decode exactly one raw row against a Schema, reporting failures as typed corruption. */
|
|
486
|
-
const decodeSingleRow = Effect.fn((schema, table, rowKey, rows) => decodeRows$
|
|
478
|
+
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
479
|
table,
|
|
488
480
|
rowKey,
|
|
489
481
|
message: `Expected exactly one row but found ${decoded.length}.`
|
|
@@ -497,7 +489,7 @@ const REQUIRED_TABLES = [
|
|
|
497
489
|
"effect_agent_checkpoints",
|
|
498
490
|
"effect_agent_child_reservations",
|
|
499
491
|
"effect_agent_child_settlements",
|
|
500
|
-
"
|
|
492
|
+
"effect_agent_threads",
|
|
501
493
|
"effect_agent_meta",
|
|
502
494
|
"effect_agent_settlement_reservations",
|
|
503
495
|
"effect_agent_submission_ownership",
|
|
@@ -511,14 +503,14 @@ const REQUIRED_TABLES = [
|
|
|
511
503
|
* through output gates) and no busy timeout (a Durable Object has exactly one writer): the
|
|
512
504
|
* Node machinery those served has no DC analogue and is deliberately absent.
|
|
513
505
|
*/
|
|
514
|
-
const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint
|
|
506
|
+
const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(function* (sql, failpoint = noFailpoint, maxStoredValueBytes) {
|
|
515
507
|
const metaTableRows = yield* sql`
|
|
516
508
|
SELECT name
|
|
517
509
|
FROM sqlite_master
|
|
518
510
|
WHERE type = 'table'
|
|
519
511
|
AND name = 'effect_agent_meta'
|
|
520
512
|
`.pipe(Effect.mapError(storageError("read storage version table")));
|
|
521
|
-
if ((yield* decodeRows$
|
|
513
|
+
if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_meta", metaTableRows)).length === 0) {
|
|
522
514
|
const existingRows = yield* sql`
|
|
523
515
|
SELECT name
|
|
524
516
|
FROM sqlite_master
|
|
@@ -526,9 +518,9 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
526
518
|
AND name LIKE 'effect_agent_%'
|
|
527
519
|
ORDER BY name
|
|
528
520
|
`.pipe(Effect.mapError(storageError("inspect unversioned storage")));
|
|
529
|
-
if ((yield* decodeRows$
|
|
521
|
+
if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* DoStorageCompatibilityError.make({
|
|
530
522
|
actualVersion: 0,
|
|
531
|
-
supportedVersion:
|
|
523
|
+
supportedVersion: 2,
|
|
532
524
|
message: "The Durable Object contains unversioned Effect Agent tables. Reset the development namespace explicitly; refusing to mutate ambiguous stored data."
|
|
533
525
|
});
|
|
534
526
|
yield* SqliteMigrator.run({ loader: doMigrations }).pipe(Effect.provideService(SqlClient.SqlClient, sql), Effect.mapError((error) => DoStorageError.make({
|
|
@@ -543,12 +535,12 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
543
535
|
WHERE key = 'storage_version'
|
|
544
536
|
`.pipe(Effect.mapError(storageError("read storage version")));
|
|
545
537
|
const version = yield* decodeSingleRow(Schema.Array(DoMetaRow), "effect_agent_meta", "storage_version", versionRows);
|
|
546
|
-
if (version.value !== String(
|
|
538
|
+
if (version.value !== String(2)) {
|
|
547
539
|
const actualVersion = Number.parseInt(version.value, 10);
|
|
548
540
|
return yield* DoStorageCompatibilityError.make({
|
|
549
541
|
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
|
|
542
|
+
supportedVersion: 2,
|
|
543
|
+
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
544
|
});
|
|
553
545
|
}
|
|
554
546
|
}
|
|
@@ -559,9 +551,9 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
559
551
|
AND name IN ${sql.in([...REQUIRED_TABLES])}
|
|
560
552
|
ORDER BY name
|
|
561
553
|
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
562
|
-
if ((yield* decodeRows$
|
|
563
|
-
actualVersion:
|
|
564
|
-
supportedVersion:
|
|
554
|
+
if ((yield* decodeRows$2(Schema.Array(DoNameRow), "sqlite_master", "required_tables", requiredRows)).length !== REQUIRED_TABLES.length) return yield* DoStorageCompatibilityError.make({
|
|
555
|
+
actualVersion: 2,
|
|
556
|
+
supportedVersion: 2,
|
|
565
557
|
message: "The Durable Object claims the current format but is missing required tables. Reset the corrupt private-development data."
|
|
566
558
|
});
|
|
567
559
|
return makeJournal(sql, failpoint, maxStoredValueBytes);
|
|
@@ -589,45 +581,45 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
589
581
|
* transaction.
|
|
590
582
|
*/
|
|
591
583
|
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: "
|
|
584
|
+
const materialize = Effect.fn("DoJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
|
|
585
|
+
if (threadId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
|
|
586
|
+
operation: "materialize thread",
|
|
587
|
+
message: "Thread identity exceeds the Durable Object storage bounds."
|
|
596
588
|
});
|
|
597
|
-
yield* checkValueBound("materialize
|
|
589
|
+
yield* checkValueBound("materialize thread", emptyTailDigest);
|
|
598
590
|
yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
|
|
599
591
|
const existingRows = yield* sql`
|
|
600
592
|
SELECT
|
|
601
|
-
|
|
593
|
+
thread_id,
|
|
602
594
|
created_at,
|
|
603
595
|
tail_sequence,
|
|
604
596
|
tail_digest,
|
|
605
597
|
producer_epoch
|
|
606
|
-
FROM
|
|
607
|
-
WHERE
|
|
608
|
-
`.pipe(Effect.mapError(storageError("read materialized
|
|
609
|
-
const existing = yield* decodeRows$
|
|
598
|
+
FROM effect_agent_threads
|
|
599
|
+
WHERE thread_id = ${threadId}
|
|
600
|
+
`.pipe(Effect.mapError(storageError("read materialized thread")));
|
|
601
|
+
const existing = yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
|
|
610
602
|
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
611
|
-
table: "
|
|
612
|
-
rowKey:
|
|
613
|
-
message: "A
|
|
603
|
+
table: "effect_agent_threads",
|
|
604
|
+
rowKey: threadId,
|
|
605
|
+
message: "A thread primary key returned more than one row."
|
|
614
606
|
});
|
|
615
607
|
if (existing.length === 0) {
|
|
616
608
|
yield* sql`
|
|
617
|
-
INSERT INTO
|
|
618
|
-
|
|
609
|
+
INSERT INTO effect_agent_threads (
|
|
610
|
+
thread_id,
|
|
619
611
|
created_at,
|
|
620
612
|
tail_sequence,
|
|
621
613
|
tail_digest,
|
|
622
614
|
producer_epoch
|
|
623
615
|
) VALUES (
|
|
624
|
-
${
|
|
616
|
+
${threadId},
|
|
625
617
|
${createdAt},
|
|
626
618
|
0,
|
|
627
619
|
${emptyTailDigest},
|
|
628
620
|
${producerEpoch}
|
|
629
621
|
)
|
|
630
|
-
`.pipe(Effect.mapError(storageError("materialize
|
|
622
|
+
`.pipe(Effect.mapError(storageError("materialize thread")));
|
|
631
623
|
return;
|
|
632
624
|
}
|
|
633
625
|
if (producerEpoch < existing[0].producer_epoch) return yield* DoFenceRejected.make({
|
|
@@ -636,27 +628,27 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
636
628
|
message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
|
|
637
629
|
});
|
|
638
630
|
if (producerEpoch > existing[0].producer_epoch) yield* sql`
|
|
639
|
-
UPDATE
|
|
631
|
+
UPDATE effect_agent_threads
|
|
640
632
|
SET producer_epoch = ${producerEpoch}
|
|
641
|
-
WHERE
|
|
633
|
+
WHERE thread_id = ${threadId}
|
|
642
634
|
`.pipe(Effect.mapError(storageError("advance materialization epoch")));
|
|
643
635
|
}));
|
|
644
636
|
});
|
|
645
|
-
const
|
|
637
|
+
const getThread = Effect.fn("DoJournal.getThread")(function* (threadId) {
|
|
646
638
|
const rows = yield* sql`
|
|
647
639
|
SELECT
|
|
648
|
-
|
|
640
|
+
thread_id,
|
|
649
641
|
created_at,
|
|
650
642
|
tail_sequence,
|
|
651
643
|
tail_digest,
|
|
652
644
|
producer_epoch
|
|
653
|
-
FROM
|
|
654
|
-
WHERE
|
|
655
|
-
`.pipe(Effect.mapError(storageError("read
|
|
656
|
-
return yield* decodeRows$
|
|
645
|
+
FROM effect_agent_threads
|
|
646
|
+
WHERE thread_id = ${threadId}
|
|
647
|
+
`.pipe(Effect.mapError(storageError("read thread")));
|
|
648
|
+
return yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
|
|
657
649
|
});
|
|
658
650
|
const append = Effect.fn("DoJournal.append")(function* (request) {
|
|
659
|
-
if (request.
|
|
651
|
+
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
652
|
operation: "append canonical batch",
|
|
661
653
|
message: "Canonical identifiers exceed the Durable Object storage bounds."
|
|
662
654
|
});
|
|
@@ -670,25 +662,25 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
670
662
|
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
671
663
|
reason: "record-identity"
|
|
672
664
|
});
|
|
673
|
-
const
|
|
665
|
+
const threadRows = yield* sql`
|
|
674
666
|
SELECT
|
|
675
|
-
|
|
667
|
+
thread_id,
|
|
676
668
|
created_at,
|
|
677
669
|
tail_sequence,
|
|
678
670
|
tail_digest,
|
|
679
671
|
producer_epoch
|
|
680
|
-
FROM
|
|
681
|
-
WHERE
|
|
672
|
+
FROM effect_agent_threads
|
|
673
|
+
WHERE thread_id = ${request.threadId}
|
|
682
674
|
`.pipe(Effect.mapError(storageError("read append tail")));
|
|
683
|
-
const
|
|
684
|
-
if (request.producerEpoch !==
|
|
675
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
|
|
676
|
+
if (request.producerEpoch !== thread.producer_epoch) return yield* DoFenceRejected.make({
|
|
685
677
|
producerEpoch: request.producerEpoch,
|
|
686
|
-
actualEpoch:
|
|
687
|
-
message: `Producer epoch ${request.producerEpoch} is not the current epoch ${
|
|
678
|
+
actualEpoch: thread.producer_epoch,
|
|
679
|
+
message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
|
|
688
680
|
});
|
|
689
681
|
const batchRows = yield* sql`
|
|
690
682
|
SELECT
|
|
691
|
-
|
|
683
|
+
thread_id,
|
|
692
684
|
batch_id,
|
|
693
685
|
first_sequence,
|
|
694
686
|
last_sequence,
|
|
@@ -696,13 +688,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
696
688
|
tail_digest,
|
|
697
689
|
batch_json
|
|
698
690
|
FROM effect_agent_canonical_batches
|
|
699
|
-
WHERE
|
|
691
|
+
WHERE thread_id = ${request.threadId}
|
|
700
692
|
AND batch_id = ${request.batchId}
|
|
701
693
|
`.pipe(Effect.mapError(storageError("read idempotent batch")));
|
|
702
|
-
const batches = yield* decodeRows$
|
|
694
|
+
const batches = yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
|
|
703
695
|
if (batches.length > 1) return yield* DoStorageCorruptionError.make({
|
|
704
696
|
table: "effect_agent_canonical_batches",
|
|
705
|
-
rowKey: `${request.
|
|
697
|
+
rowKey: `${request.threadId}/${request.batchId}`,
|
|
706
698
|
message: "A canonical batch primary key returned more than one row."
|
|
707
699
|
});
|
|
708
700
|
if (batches.length === 1) {
|
|
@@ -718,37 +710,37 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
718
710
|
tailDigest: existing.tail_digest
|
|
719
711
|
});
|
|
720
712
|
}
|
|
721
|
-
if (request.expectedTailSequence !==
|
|
722
|
-
message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${
|
|
713
|
+
if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* DoAppendConflict.make({
|
|
714
|
+
message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
|
|
723
715
|
reason: "tail",
|
|
724
|
-
actualTailSequence:
|
|
725
|
-
actualTailDigest:
|
|
716
|
+
actualTailSequence: thread.tail_sequence,
|
|
717
|
+
actualTailDigest: thread.tail_digest
|
|
726
718
|
});
|
|
727
|
-
if (
|
|
719
|
+
if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* DoStorageError.make({
|
|
728
720
|
operation: "append canonical batch",
|
|
729
|
-
message: `
|
|
721
|
+
message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
|
|
730
722
|
});
|
|
731
723
|
const existingRecords = [];
|
|
732
|
-
for (const chunk of chunked(recordIds,
|
|
724
|
+
for (const chunk of chunked(recordIds, 90)) {
|
|
733
725
|
const existingRecordRows = yield* sql`
|
|
734
726
|
SELECT
|
|
735
|
-
|
|
727
|
+
thread_id,
|
|
736
728
|
sequence,
|
|
737
729
|
record_id,
|
|
738
730
|
batch_id,
|
|
739
731
|
record_json
|
|
740
732
|
FROM effect_agent_canonical_records
|
|
741
|
-
WHERE
|
|
733
|
+
WHERE thread_id = ${request.threadId}
|
|
742
734
|
AND record_id IN ${sql.in([...chunk])}
|
|
743
735
|
ORDER BY sequence
|
|
744
736
|
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
745
|
-
existingRecords.push(...yield* decodeRows$
|
|
737
|
+
existingRecords.push(...yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows));
|
|
746
738
|
}
|
|
747
739
|
if (existingRecords.length > 0) return yield* DoAppendConflict.make({
|
|
748
740
|
message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
|
|
749
741
|
reason: "record-identity"
|
|
750
742
|
});
|
|
751
|
-
const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
|
|
743
|
+
const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => DoStorageError.make({
|
|
752
744
|
cause: error,
|
|
753
745
|
operation: "append canonical batch",
|
|
754
746
|
message: error.message
|
|
@@ -760,7 +752,7 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
760
752
|
})));
|
|
761
753
|
yield* sql`
|
|
762
754
|
INSERT INTO effect_agent_canonical_batches (
|
|
763
|
-
|
|
755
|
+
thread_id,
|
|
764
756
|
batch_id,
|
|
765
757
|
first_sequence,
|
|
766
758
|
last_sequence,
|
|
@@ -768,7 +760,7 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
768
760
|
tail_digest,
|
|
769
761
|
batch_json
|
|
770
762
|
) VALUES (
|
|
771
|
-
${request.
|
|
763
|
+
${request.threadId},
|
|
772
764
|
${request.batchId},
|
|
773
765
|
${firstSequence},
|
|
774
766
|
${lastSequence},
|
|
@@ -781,13 +773,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
781
773
|
yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
|
|
782
774
|
yield* sql`
|
|
783
775
|
INSERT INTO effect_agent_canonical_records (
|
|
784
|
-
|
|
776
|
+
thread_id,
|
|
785
777
|
sequence,
|
|
786
778
|
record_id,
|
|
787
779
|
batch_id,
|
|
788
780
|
record_json
|
|
789
781
|
) VALUES (
|
|
790
|
-
${request.
|
|
782
|
+
${request.threadId},
|
|
791
783
|
${firstSequence + index},
|
|
792
784
|
${record.recordId},
|
|
793
785
|
${request.batchId},
|
|
@@ -797,13 +789,13 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
797
789
|
yield* failpoint("append:after-record-insert");
|
|
798
790
|
}), { discard: true });
|
|
799
791
|
yield* sql`
|
|
800
|
-
UPDATE
|
|
792
|
+
UPDATE effect_agent_threads
|
|
801
793
|
SET
|
|
802
794
|
tail_sequence = ${lastSequence},
|
|
803
795
|
tail_digest = ${request.tailDigest},
|
|
804
796
|
producer_epoch = ${request.producerEpoch}
|
|
805
|
-
WHERE
|
|
806
|
-
`.pipe(Effect.mapError(storageError("advance
|
|
797
|
+
WHERE thread_id = ${request.threadId}
|
|
798
|
+
`.pipe(Effect.mapError(storageError("advance thread tail")));
|
|
807
799
|
yield* failpoint("append:after-tail-update");
|
|
808
800
|
return RawAppendResult.make({
|
|
809
801
|
firstSequence,
|
|
@@ -816,36 +808,36 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
816
808
|
const read = Effect.fn("DoJournal.read")(function* (request) {
|
|
817
809
|
const rows = yield* sql`
|
|
818
810
|
SELECT
|
|
819
|
-
|
|
811
|
+
thread_id,
|
|
820
812
|
sequence,
|
|
821
813
|
record_id,
|
|
822
814
|
batch_id,
|
|
823
815
|
record_json
|
|
824
816
|
FROM effect_agent_canonical_records
|
|
825
|
-
WHERE
|
|
817
|
+
WHERE thread_id = ${request.threadId}
|
|
826
818
|
AND sequence > ${request.fromSequenceExclusive}
|
|
827
819
|
ORDER BY sequence
|
|
828
820
|
LIMIT ${request.limit}
|
|
829
821
|
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
830
|
-
return yield* decodeRows$
|
|
822
|
+
return yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, rows);
|
|
831
823
|
});
|
|
832
|
-
const
|
|
824
|
+
const exportThread = Effect.fn("DoJournal.exportThread")(function* (threadId) {
|
|
833
825
|
return yield* sql.withTransaction(Effect.gen(function* () {
|
|
834
|
-
const
|
|
826
|
+
const threadRows = yield* sql`
|
|
835
827
|
SELECT
|
|
836
|
-
|
|
828
|
+
thread_id,
|
|
837
829
|
created_at,
|
|
838
830
|
tail_sequence,
|
|
839
831
|
tail_digest,
|
|
840
832
|
producer_epoch
|
|
841
|
-
FROM
|
|
842
|
-
WHERE
|
|
843
|
-
`.pipe(Effect.mapError(storageError("export
|
|
844
|
-
const
|
|
845
|
-
yield* failpoint("export:after-
|
|
833
|
+
FROM effect_agent_threads
|
|
834
|
+
WHERE thread_id = ${threadId}
|
|
835
|
+
`.pipe(Effect.mapError(storageError("export thread")));
|
|
836
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
|
|
837
|
+
yield* failpoint("export:after-thread-read");
|
|
846
838
|
const batchRows = yield* sql`
|
|
847
839
|
SELECT
|
|
848
|
-
|
|
840
|
+
thread_id,
|
|
849
841
|
batch_id,
|
|
850
842
|
first_sequence,
|
|
851
843
|
last_sequence,
|
|
@@ -853,71 +845,71 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
853
845
|
tail_digest,
|
|
854
846
|
batch_json
|
|
855
847
|
FROM effect_agent_canonical_batches
|
|
856
|
-
WHERE
|
|
848
|
+
WHERE thread_id = ${threadId}
|
|
857
849
|
ORDER BY first_sequence
|
|
858
850
|
`.pipe(Effect.mapError(storageError("export canonical batches")));
|
|
859
851
|
const recordRows = yield* sql`
|
|
860
852
|
SELECT
|
|
861
|
-
|
|
853
|
+
thread_id,
|
|
862
854
|
sequence,
|
|
863
855
|
record_id,
|
|
864
856
|
batch_id,
|
|
865
857
|
record_json
|
|
866
858
|
FROM effect_agent_canonical_records
|
|
867
|
-
WHERE
|
|
859
|
+
WHERE thread_id = ${threadId}
|
|
868
860
|
ORDER BY sequence
|
|
869
861
|
`.pipe(Effect.mapError(storageError("export canonical records")));
|
|
870
862
|
const checkpointRows = yield* sql`
|
|
871
863
|
SELECT
|
|
872
|
-
|
|
864
|
+
thread_id,
|
|
873
865
|
through_sequence,
|
|
874
866
|
tail_digest,
|
|
875
867
|
checkpoint_json
|
|
876
868
|
FROM effect_agent_checkpoints
|
|
877
|
-
WHERE
|
|
869
|
+
WHERE thread_id = ${threadId}
|
|
878
870
|
ORDER BY through_sequence
|
|
879
871
|
`.pipe(Effect.mapError(storageError("export checkpoints")));
|
|
880
|
-
return
|
|
881
|
-
|
|
882
|
-
batches: yield* decodeRows$
|
|
883
|
-
records: yield* decodeRows$
|
|
884
|
-
checkpoints: yield* decodeRows$
|
|
872
|
+
return RawThreadExport.make({
|
|
873
|
+
thread,
|
|
874
|
+
batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", threadId, batchRows),
|
|
875
|
+
records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", threadId, recordRows),
|
|
876
|
+
checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", threadId, checkpointRows)
|
|
885
877
|
});
|
|
886
878
|
})).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("export transaction")(error))));
|
|
887
879
|
});
|
|
888
880
|
const saveCheckpoint = Effect.fn("DoJournal.saveCheckpoint")(function* (checkpoint) {
|
|
889
|
-
if (checkpoint.
|
|
881
|
+
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH$1) return yield* DoStorageError.make({
|
|
890
882
|
operation: "save checkpoint",
|
|
891
883
|
message: "Checkpoint identity exceeds the Durable Object storage bounds."
|
|
892
884
|
});
|
|
893
885
|
yield* checkValueBound("save checkpoint", checkpoint.checkpointJson);
|
|
894
886
|
yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
|
|
895
|
-
const
|
|
887
|
+
const threadRows = yield* sql`
|
|
896
888
|
SELECT
|
|
897
|
-
|
|
889
|
+
thread_id,
|
|
898
890
|
created_at,
|
|
899
891
|
tail_sequence,
|
|
900
892
|
tail_digest,
|
|
901
893
|
producer_epoch
|
|
902
|
-
FROM
|
|
903
|
-
WHERE
|
|
894
|
+
FROM effect_agent_threads
|
|
895
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
904
896
|
`.pipe(Effect.mapError(storageError("read checkpoint tail")));
|
|
905
|
-
const
|
|
906
|
-
if (checkpoint.throughSequence >
|
|
897
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
|
|
898
|
+
if (checkpoint.throughSequence > thread.tail_sequence) return yield* DoCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
|
|
907
899
|
const checkpointRows = yield* sql`
|
|
908
900
|
SELECT
|
|
909
|
-
|
|
901
|
+
thread_id,
|
|
910
902
|
through_sequence,
|
|
911
903
|
tail_digest,
|
|
912
904
|
checkpoint_json
|
|
913
905
|
FROM effect_agent_checkpoints
|
|
914
|
-
WHERE
|
|
906
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
915
907
|
AND through_sequence = ${checkpoint.throughSequence}
|
|
916
908
|
`.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
|
|
917
|
-
const existing = yield* decodeRows$
|
|
909
|
+
const existing = yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
|
|
918
910
|
if (existing.length > 1) return yield* DoStorageCorruptionError.make({
|
|
919
911
|
table: "effect_agent_checkpoints",
|
|
920
|
-
rowKey: `${checkpoint.
|
|
912
|
+
rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
|
|
921
913
|
message: "A checkpoint primary key returned more than one row."
|
|
922
914
|
});
|
|
923
915
|
if (existing.length === 1) {
|
|
@@ -926,12 +918,12 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
926
918
|
}
|
|
927
919
|
yield* sql`
|
|
928
920
|
INSERT INTO effect_agent_checkpoints (
|
|
929
|
-
|
|
921
|
+
thread_id,
|
|
930
922
|
through_sequence,
|
|
931
923
|
tail_digest,
|
|
932
924
|
checkpoint_json
|
|
933
925
|
) VALUES (
|
|
934
|
-
${checkpoint.
|
|
926
|
+
${checkpoint.threadId},
|
|
935
927
|
${checkpoint.throughSequence},
|
|
936
928
|
${checkpoint.tailDigest},
|
|
937
929
|
${checkpoint.checkpointJson}
|
|
@@ -939,34 +931,34 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
939
931
|
`.pipe(Effect.mapError(storageError("insert checkpoint")));
|
|
940
932
|
}));
|
|
941
933
|
});
|
|
942
|
-
const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (
|
|
934
|
+
const loadCheckpoint = Effect.fn("DoJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
|
|
943
935
|
const rows = yield* sql`
|
|
944
936
|
SELECT
|
|
945
|
-
|
|
937
|
+
thread_id,
|
|
946
938
|
through_sequence,
|
|
947
939
|
tail_digest,
|
|
948
940
|
checkpoint_json
|
|
949
941
|
FROM effect_agent_checkpoints
|
|
950
|
-
WHERE
|
|
942
|
+
WHERE thread_id = ${threadId}
|
|
951
943
|
AND through_sequence <= ${atOrBeforeSequence}
|
|
952
944
|
ORDER BY through_sequence DESC
|
|
953
945
|
LIMIT 1
|
|
954
946
|
`.pipe(Effect.mapError(storageError("load checkpoint")));
|
|
955
|
-
return yield* decodeRows$
|
|
947
|
+
return yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
|
|
956
948
|
});
|
|
957
949
|
return {
|
|
958
950
|
append,
|
|
959
951
|
checkValueBound,
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
getTailDigestAt: Effect.fn("DoJournal.getTailDigestAt")(function* (
|
|
952
|
+
exportThread,
|
|
953
|
+
getThread,
|
|
954
|
+
getTailDigestAt: Effect.fn("DoJournal.getTailDigestAt")(function* (threadId, sequence) {
|
|
963
955
|
if (sequence === 0) {
|
|
964
|
-
const
|
|
965
|
-
return
|
|
956
|
+
const threads = yield* getThread(threadId);
|
|
957
|
+
return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
|
|
966
958
|
}
|
|
967
959
|
const rows = yield* sql`
|
|
968
960
|
SELECT
|
|
969
|
-
|
|
961
|
+
thread_id,
|
|
970
962
|
batch_id,
|
|
971
963
|
first_sequence,
|
|
972
964
|
last_sequence,
|
|
@@ -974,10 +966,10 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
974
966
|
tail_digest,
|
|
975
967
|
batch_json
|
|
976
968
|
FROM effect_agent_canonical_batches
|
|
977
|
-
WHERE
|
|
969
|
+
WHERE thread_id = ${threadId}
|
|
978
970
|
AND last_sequence = ${sequence}
|
|
979
971
|
`.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
|
|
980
|
-
return (yield* decodeRows$
|
|
972
|
+
return (yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
|
|
981
973
|
}),
|
|
982
974
|
loadCheckpoint,
|
|
983
975
|
materialize,
|
|
@@ -985,19 +977,19 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
985
977
|
saveCheckpoint,
|
|
986
978
|
scanStoredPayloads: Effect.fn("DoJournal.scanStoredPayloads")(function* () {
|
|
987
979
|
return yield* sql.withTransaction(Effect.gen(function* () {
|
|
988
|
-
const
|
|
980
|
+
const threads = yield* sql`
|
|
989
981
|
SELECT
|
|
990
|
-
|
|
982
|
+
thread_id,
|
|
991
983
|
created_at,
|
|
992
984
|
tail_sequence,
|
|
993
985
|
tail_digest,
|
|
994
986
|
producer_epoch
|
|
995
|
-
FROM
|
|
996
|
-
ORDER BY
|
|
997
|
-
`.pipe(Effect.mapError(storageError("scan
|
|
987
|
+
FROM effect_agent_threads
|
|
988
|
+
ORDER BY thread_id
|
|
989
|
+
`.pipe(Effect.mapError(storageError("scan threads")));
|
|
998
990
|
const batches = yield* sql`
|
|
999
991
|
SELECT
|
|
1000
|
-
|
|
992
|
+
thread_id,
|
|
1001
993
|
batch_id,
|
|
1002
994
|
first_sequence,
|
|
1003
995
|
last_sequence,
|
|
@@ -1005,32 +997,32 @@ const makeJournal = (sql, failpoint, maxStoredValueBytes) => {
|
|
|
1005
997
|
tail_digest,
|
|
1006
998
|
batch_json
|
|
1007
999
|
FROM effect_agent_canonical_batches
|
|
1008
|
-
ORDER BY
|
|
1000
|
+
ORDER BY thread_id, first_sequence
|
|
1009
1001
|
`.pipe(Effect.mapError(storageError("scan canonical batches")));
|
|
1010
1002
|
const records = yield* sql`
|
|
1011
1003
|
SELECT
|
|
1012
|
-
|
|
1004
|
+
thread_id,
|
|
1013
1005
|
sequence,
|
|
1014
1006
|
record_id,
|
|
1015
1007
|
batch_id,
|
|
1016
1008
|
record_json
|
|
1017
1009
|
FROM effect_agent_canonical_records
|
|
1018
|
-
ORDER BY
|
|
1010
|
+
ORDER BY thread_id, sequence
|
|
1019
1011
|
`.pipe(Effect.mapError(storageError("scan canonical records")));
|
|
1020
1012
|
const checkpoints = yield* sql`
|
|
1021
1013
|
SELECT
|
|
1022
|
-
|
|
1014
|
+
thread_id,
|
|
1023
1015
|
through_sequence,
|
|
1024
1016
|
tail_digest,
|
|
1025
1017
|
checkpoint_json
|
|
1026
1018
|
FROM effect_agent_checkpoints
|
|
1027
|
-
ORDER BY
|
|
1019
|
+
ORDER BY thread_id, through_sequence
|
|
1028
1020
|
`.pipe(Effect.mapError(storageError("scan checkpoints")));
|
|
1029
1021
|
return {
|
|
1030
|
-
|
|
1031
|
-
batches: yield* decodeRows$
|
|
1032
|
-
records: yield* decodeRows$
|
|
1033
|
-
checkpoints: yield* decodeRows$
|
|
1022
|
+
threads: yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
|
|
1023
|
+
batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
|
|
1024
|
+
records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
|
|
1025
|
+
checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
|
|
1034
1026
|
};
|
|
1035
1027
|
})).pipe(Effect.catchTag("SqlError", (error) => Effect.fail(storageError("startup scan transaction")(error))));
|
|
1036
1028
|
}),
|
|
@@ -1081,78 +1073,44 @@ var DoStorageConfigValue = class extends Schema.Class("@effect-agent/storage-clo
|
|
|
1081
1073
|
/** Explicit Durable Object storage configuration authority. */
|
|
1082
1074
|
var DoStorageConfig = class extends Context.Service()("@effect-agent/storage-cloudflare/DoStorageConfig") {};
|
|
1083
1075
|
//#endregion
|
|
1084
|
-
//#region src/do-
|
|
1085
|
-
const
|
|
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
|
|
1119
|
-
const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
|
|
1076
|
+
//#region src/do-thread-store.ts
|
|
1077
|
+
const OffsetText = Schema.String.check(Schema.isMaxLength(4096));
|
|
1120
1078
|
const DO_OFFSET_PREFIX = "effect-agent-do@1:";
|
|
1121
1079
|
const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
|
|
1122
1080
|
const isDigest = Schema.is(Digest);
|
|
1123
1081
|
const isDoFenceRejected = Schema.is(DoFenceRejected);
|
|
1124
1082
|
const isDoAppendConflict = Schema.is(DoAppendConflict);
|
|
1125
1083
|
const isDoCheckpointConflict = Schema.is(DoCheckpointConflict);
|
|
1126
|
-
const storeError = (operation, error) =>
|
|
1084
|
+
const storeError = (operation, error) => ThreadStoreError.make({
|
|
1127
1085
|
cause: error,
|
|
1128
1086
|
operation,
|
|
1129
1087
|
message: error.message
|
|
1130
1088
|
});
|
|
1131
|
-
const schemaStoreError = (operation, error) =>
|
|
1089
|
+
const schemaStoreError = (operation, error) => ThreadStoreError.make({
|
|
1132
1090
|
cause: error,
|
|
1133
1091
|
operation,
|
|
1134
1092
|
message: error.message
|
|
1135
1093
|
});
|
|
1136
|
-
const makeOffset = Effect.fn(function* (
|
|
1137
|
-
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${DO_OFFSET_PREFIX}${encodeURIComponent(
|
|
1094
|
+
const makeOffset = Effect.fn(function* (threadId, sequence) {
|
|
1095
|
+
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
1096
|
});
|
|
1139
|
-
const parseOffset = Effect.fn(function* (
|
|
1097
|
+
const parseOffset = Effect.fn(function* (threadId, offset) {
|
|
1140
1098
|
if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
|
|
1141
1099
|
const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
|
|
1142
|
-
const
|
|
1143
|
-
if (!text.startsWith(
|
|
1100
|
+
const threadPrefix = `${DO_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
|
|
1101
|
+
if (!text.startsWith(threadPrefix)) return yield* ThreadStoreError.make({
|
|
1144
1102
|
operation: "decode observation offset",
|
|
1145
|
-
message: "The observation offset belongs to a different adapter, storage version, or
|
|
1103
|
+
message: "The observation offset belongs to a different adapter, storage version, or Thread."
|
|
1146
1104
|
});
|
|
1147
|
-
const sequenceText = text.slice(
|
|
1148
|
-
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield*
|
|
1105
|
+
const sequenceText = text.slice(threadPrefix.length);
|
|
1106
|
+
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ThreadStoreError.make({
|
|
1149
1107
|
operation: "decode observation offset",
|
|
1150
1108
|
message: "The observation offset is malformed."
|
|
1151
1109
|
});
|
|
1152
1110
|
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
|
|
1153
1111
|
});
|
|
1154
|
-
const mapFence = (
|
|
1155
|
-
|
|
1112
|
+
const mapFence = (threadId, error) => FenceRejected.make({
|
|
1113
|
+
threadId,
|
|
1156
1114
|
actualEpoch: error.actualEpoch,
|
|
1157
1115
|
attemptedEpoch: error.producerEpoch
|
|
1158
1116
|
});
|
|
@@ -1163,18 +1121,18 @@ const encodeCanonicalBatch = Effect.fn(function* (batch) {
|
|
|
1163
1121
|
return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
|
|
1164
1122
|
});
|
|
1165
1123
|
const encodeCheckpoint = Effect.fn(function* (checkpoint) {
|
|
1166
|
-
return yield* Schema.encodeEffect(Schema.fromJsonString(
|
|
1124
|
+
return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
|
|
1167
1125
|
});
|
|
1168
1126
|
const decodeEnvelope = Effect.fn(function* (row) {
|
|
1169
|
-
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) =>
|
|
1127
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ThreadStoreError.make({
|
|
1170
1128
|
operation: "decode canonical record",
|
|
1171
1129
|
message: error.message
|
|
1172
1130
|
})));
|
|
1173
|
-
const
|
|
1174
|
-
const offset = yield* makeOffset(
|
|
1131
|
+
const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(row.thread_id).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
|
|
1132
|
+
const offset = yield* makeOffset(threadId, row.sequence);
|
|
1175
1133
|
const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
|
|
1176
1134
|
return CanonicalRecordEnvelope.make({
|
|
1177
|
-
|
|
1135
|
+
threadId,
|
|
1178
1136
|
batchId,
|
|
1179
1137
|
sequence: row.sequence,
|
|
1180
1138
|
offset,
|
|
@@ -1182,18 +1140,18 @@ const decodeEnvelope = Effect.fn(function* (row) {
|
|
|
1182
1140
|
});
|
|
1183
1141
|
});
|
|
1184
1142
|
const decodeCheckpoint = Effect.fn(function* (checkpointJson) {
|
|
1185
|
-
return yield* Schema.decodeEffect(Schema.fromJsonString(
|
|
1143
|
+
return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
|
|
1186
1144
|
});
|
|
1187
|
-
const
|
|
1188
|
-
const rows = yield* journal.
|
|
1189
|
-
if (rows.length === 0) return yield*
|
|
1145
|
+
const requireThread = Effect.fn("DoThreadStore.requireThread")(function* (journal, threadId) {
|
|
1146
|
+
const rows = yield* journal.getThread(threadId).pipe(Effect.mapError((error) => storeError("read thread", error)));
|
|
1147
|
+
if (rows.length === 0) return yield* ThreadNotMaterialized.make({ threadId });
|
|
1190
1148
|
return rows[0];
|
|
1191
1149
|
});
|
|
1192
|
-
const tailDigestAt = Effect.fn("
|
|
1150
|
+
const tailDigestAt = Effect.fn("DoThreadStore.tailDigestAt")(function* (journal, threadId, sequence) {
|
|
1193
1151
|
if (sequence === 0) return EMPTY_TAIL_DIGEST;
|
|
1194
|
-
const digests = yield* journal.getTailDigestAt(
|
|
1152
|
+
const digests = yield* journal.getTailDigestAt(threadId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
|
|
1195
1153
|
if (digests.length !== 1) return yield* CheckpointRejected.make({
|
|
1196
|
-
|
|
1154
|
+
threadId,
|
|
1197
1155
|
reason: "digest-mismatch"
|
|
1198
1156
|
});
|
|
1199
1157
|
return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
|
|
@@ -1212,14 +1170,14 @@ const groupByKey = (rows, key) => {
|
|
|
1212
1170
|
* and re-digested against the canonical chain. Routine opens skip this scan: per-operation
|
|
1213
1171
|
* Schema decoding plus the digest chain already fail clearly on corrupt rows.
|
|
1214
1172
|
*/
|
|
1215
|
-
const decodeStartupPayloads = Effect.fn("
|
|
1173
|
+
const decodeStartupPayloads = Effect.fn("DoThreadStore.decodeStartupPayloads")(function* (journal, crypto) {
|
|
1216
1174
|
const stored = yield* journal.scanStoredPayloads();
|
|
1217
1175
|
const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
|
|
1218
1176
|
decoded,
|
|
1219
1177
|
row: batch
|
|
1220
1178
|
})), Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
1221
1179
|
table: "effect_agent_canonical_batches",
|
|
1222
|
-
rowKey: `${batch.
|
|
1180
|
+
rowKey: `${batch.thread_id}/${batch.batch_id}`,
|
|
1223
1181
|
message: error.message
|
|
1224
1182
|
}))));
|
|
1225
1183
|
const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
|
|
@@ -1227,31 +1185,31 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
1227
1185
|
row: record
|
|
1228
1186
|
})), Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
1229
1187
|
table: "effect_agent_canonical_records",
|
|
1230
|
-
rowKey: `${record.
|
|
1188
|
+
rowKey: `${record.thread_id}/${record.sequence}`,
|
|
1231
1189
|
message: error.message
|
|
1232
1190
|
}))));
|
|
1233
|
-
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(
|
|
1191
|
+
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
|
|
1234
1192
|
decoded,
|
|
1235
1193
|
row: checkpoint
|
|
1236
1194
|
})), Effect.mapError((error) => DoStorageCorruptionError.make({
|
|
1237
1195
|
table: "effect_agent_checkpoints",
|
|
1238
|
-
rowKey: `${checkpoint.
|
|
1196
|
+
rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
|
|
1239
1197
|
message: error.message
|
|
1240
1198
|
}))));
|
|
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(
|
|
1199
|
+
const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
|
|
1200
|
+
const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
|
|
1201
|
+
const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
|
|
1202
|
+
const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
|
|
1203
|
+
for (const thread of stored.threads) {
|
|
1204
|
+
const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
|
|
1205
|
+
const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
|
|
1206
|
+
const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
|
|
1207
|
+
const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
|
|
1250
1208
|
let previousDigest = EMPTY_TAIL_DIGEST;
|
|
1251
1209
|
let expectedSequence = 1;
|
|
1252
1210
|
const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
|
|
1253
|
-
for (const { decoded: canonicalBatch, row: batchRow } of
|
|
1254
|
-
const key = `${batchRow.
|
|
1211
|
+
for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
|
|
1212
|
+
const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
|
|
1255
1213
|
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
1214
|
table: "effect_agent_canonical_batches",
|
|
1257
1215
|
rowKey: key,
|
|
@@ -1296,24 +1254,24 @@ const decodeStartupPayloads = Effect.fn("DoConversationStore.decodeStartupPayloa
|
|
|
1296
1254
|
expectedSequence = batchRow.last_sequence + 1;
|
|
1297
1255
|
tailDigests.set(batchRow.last_sequence, digest);
|
|
1298
1256
|
}
|
|
1299
|
-
if (
|
|
1300
|
-
table: "
|
|
1301
|
-
rowKey:
|
|
1302
|
-
message: "
|
|
1257
|
+
if (threadRecords.length !== thread.tail_sequence || thread.tail_sequence !== expectedSequence - 1 || thread.tail_digest !== previousDigest) return yield* DoStorageCorruptionError.make({
|
|
1258
|
+
table: "effect_agent_threads",
|
|
1259
|
+
rowKey: thread.thread_id,
|
|
1260
|
+
message: "Thread tail does not match its canonical batch chain."
|
|
1303
1261
|
});
|
|
1304
|
-
for (const checkpoint of
|
|
1262
|
+
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
1263
|
table: "effect_agent_checkpoints",
|
|
1306
|
-
rowKey: `${
|
|
1264
|
+
rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
|
|
1307
1265
|
message: "Checkpoint identity or digest is not bound to a canonical batch tail."
|
|
1308
1266
|
});
|
|
1309
1267
|
}
|
|
1310
|
-
if (batches.some(({ row }) => !materializedIds.has(row.
|
|
1311
|
-
table: "
|
|
1268
|
+
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({
|
|
1269
|
+
table: "effect_agent_threads",
|
|
1312
1270
|
rowKey: "startup_scan",
|
|
1313
|
-
message: "Canonical rows exist without a materialized
|
|
1271
|
+
message: "Canonical rows exist without a materialized Thread."
|
|
1314
1272
|
});
|
|
1315
1273
|
});
|
|
1316
|
-
const makeServices$2 = Effect.fn("
|
|
1274
|
+
const makeServices$2 = Effect.fn("DoThreadStore.makeServices")(function* () {
|
|
1317
1275
|
const config = yield* DoStorageConfig;
|
|
1318
1276
|
const failpoint = yield* DoStorageFailpoint;
|
|
1319
1277
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -1322,16 +1280,16 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1322
1280
|
if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
|
|
1323
1281
|
const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
|
|
1324
1282
|
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(
|
|
1283
|
+
const materialize = Effect.fn("DoThreadStore.materialize")(function* (request) {
|
|
1284
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
1327
1285
|
const now = yield* Clock.currentTimeMillis;
|
|
1328
1286
|
yield* hitFailpoint("materialize:before");
|
|
1329
|
-
yield* journal.materialize(validated.
|
|
1287
|
+
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
1288
|
yield* hitFailpoint("materialize:after");
|
|
1331
1289
|
});
|
|
1332
|
-
const append = Effect.fn("
|
|
1290
|
+
const append = Effect.fn("DoThreadStore.append")(function* (request) {
|
|
1333
1291
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
|
|
1334
|
-
yield*
|
|
1292
|
+
yield* requireThread(journal, validated.threadId);
|
|
1335
1293
|
const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
|
|
1336
1294
|
const batchJson = yield* encodeCanonicalBatch(validated.batch);
|
|
1337
1295
|
const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
|
|
@@ -1339,7 +1297,7 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1339
1297
|
recordJson
|
|
1340
1298
|
}))));
|
|
1341
1299
|
const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
|
|
1342
|
-
|
|
1300
|
+
threadId: validated.threadId,
|
|
1343
1301
|
batchId: validated.batch.batchId,
|
|
1344
1302
|
batchDigest: tailDigest,
|
|
1345
1303
|
batchJson,
|
|
@@ -1351,15 +1309,15 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1351
1309
|
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
1352
1310
|
yield* hitFailpoint("append:before");
|
|
1353
1311
|
const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
|
|
1354
|
-
if (isDoFenceRejected(error)) return mapFence(validated.
|
|
1312
|
+
if (isDoFenceRejected(error)) return mapFence(validated.threadId, error);
|
|
1355
1313
|
if (isDoAppendConflict(error)) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
|
|
1356
|
-
|
|
1314
|
+
threadId: validated.threadId,
|
|
1357
1315
|
batchId: validated.batch.batchId,
|
|
1358
1316
|
reason: error.reason,
|
|
1359
1317
|
actualTailSequence: error.actualTailSequence,
|
|
1360
1318
|
actualTailDigest: error.actualTailDigest
|
|
1361
1319
|
}) : AppendConflict.make({
|
|
1362
|
-
|
|
1320
|
+
threadId: validated.threadId,
|
|
1363
1321
|
batchId: validated.batch.batchId,
|
|
1364
1322
|
reason: error.reason
|
|
1365
1323
|
});
|
|
@@ -1368,30 +1326,30 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1368
1326
|
yield* hitFailpoint("append:after");
|
|
1369
1327
|
return result;
|
|
1370
1328
|
});
|
|
1371
|
-
const loadRecords = Effect.fn("
|
|
1329
|
+
const loadRecords = Effect.fn("DoThreadStore.loadRecords")(function* (request) {
|
|
1372
1330
|
const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
|
|
1373
1331
|
return yield* Effect.forEach(rows, decodeEnvelope);
|
|
1374
1332
|
});
|
|
1375
|
-
const readEffect = Effect.fn("
|
|
1376
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1377
|
-
yield*
|
|
1333
|
+
const readEffect = Effect.fn("DoThreadStore.read")(function* (request) {
|
|
1334
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread read", error)));
|
|
1335
|
+
yield* requireThread(journal, validated.threadId);
|
|
1378
1336
|
const records = yield* loadRecords(RawReadRequest.make({
|
|
1379
|
-
|
|
1337
|
+
threadId: validated.threadId,
|
|
1380
1338
|
fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
|
|
1381
1339
|
limit: validated.limit
|
|
1382
1340
|
}));
|
|
1383
1341
|
return Stream.fromIterable(records);
|
|
1384
1342
|
});
|
|
1385
1343
|
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.
|
|
1344
|
+
const observeEffect = Effect.fn("DoThreadStore.observe")(function* (request) {
|
|
1345
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
|
|
1346
|
+
yield* requireThread(journal, validated.threadId);
|
|
1347
|
+
const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
|
|
1390
1348
|
const cursor = yield* Ref.make(initialSequence);
|
|
1391
|
-
const poll = Effect.fn("
|
|
1349
|
+
const poll = Effect.fn("DoThreadStore.observePoll")(function* () {
|
|
1392
1350
|
const fromSequenceExclusive = yield* Ref.get(cursor);
|
|
1393
1351
|
const records = yield* loadRecords(RawReadRequest.make({
|
|
1394
|
-
|
|
1352
|
+
threadId: validated.threadId,
|
|
1395
1353
|
fromSequenceExclusive,
|
|
1396
1354
|
limit: 1024
|
|
1397
1355
|
}));
|
|
@@ -1405,96 +1363,98 @@ const makeServices$2 = Effect.fn("DoConversationStore.makeServices")(function* (
|
|
|
1405
1363
|
return Stream.fromIterableEffectRepeat(poll());
|
|
1406
1364
|
});
|
|
1407
1365
|
const observe = (request) => Stream.unwrap(observeEffect(request));
|
|
1408
|
-
const
|
|
1409
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1410
|
-
yield*
|
|
1411
|
-
const exported = yield* journal.
|
|
1366
|
+
const exportThread = Effect.fn("DoThreadStore.export")(function* (request) {
|
|
1367
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
|
|
1368
|
+
yield* requireThread(journal, validated.threadId);
|
|
1369
|
+
const exported = yield* journal.exportThread(validated.threadId).pipe(Effect.mapError((error) => storeError("export thread", error)));
|
|
1412
1370
|
const records = yield* Effect.forEach(exported.records, decodeEnvelope);
|
|
1413
|
-
if (records.length > 65536) return yield*
|
|
1414
|
-
operation: "decode
|
|
1415
|
-
message: "The
|
|
1371
|
+
if (records.length > 65536) return yield* ThreadStoreError.make({
|
|
1372
|
+
operation: "decode thread export",
|
|
1373
|
+
message: "The thread exceeds the current export record limit."
|
|
1416
1374
|
});
|
|
1417
|
-
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.
|
|
1418
|
-
return
|
|
1419
|
-
format: "effect-agent/
|
|
1420
|
-
|
|
1421
|
-
tailSequence: exported.
|
|
1375
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
|
|
1376
|
+
return ThreadExport.make({
|
|
1377
|
+
format: "effect-agent/thread@1",
|
|
1378
|
+
threadId: validated.threadId,
|
|
1379
|
+
tailSequence: exported.thread.tail_sequence,
|
|
1422
1380
|
tailDigest,
|
|
1423
1381
|
records
|
|
1424
1382
|
});
|
|
1425
1383
|
});
|
|
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:
|
|
1384
|
+
const inspectTail = Effect.fn("DoThreadStore.inspectTail")(function* (request) {
|
|
1385
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
|
|
1386
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
1387
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
|
|
1388
|
+
return ThreadTail.make({
|
|
1389
|
+
threadId: validated.threadId,
|
|
1390
|
+
tailSequence: thread.tail_sequence,
|
|
1433
1391
|
tailDigest,
|
|
1434
|
-
producerEpoch:
|
|
1392
|
+
producerEpoch: thread.producer_epoch
|
|
1435
1393
|
});
|
|
1436
1394
|
});
|
|
1437
|
-
const saveCheckpoint = Effect.fn("
|
|
1395
|
+
const saveCheckpoint = Effect.fn("DoThreadStore.saveCheckpoint")(function* (request) {
|
|
1438
1396
|
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
|
-
|
|
1397
|
+
const thread = yield* requireThread(journal, validated.checkpoint.threadId);
|
|
1398
|
+
if (validated.checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
|
|
1399
|
+
threadId: validated.checkpoint.threadId,
|
|
1442
1400
|
reason: "ahead-of-tail"
|
|
1443
1401
|
});
|
|
1444
|
-
if ((yield* tailDigestAt(journal, validated.checkpoint.
|
|
1445
|
-
|
|
1402
|
+
if ((yield* tailDigestAt(journal, validated.checkpoint.threadId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
|
|
1403
|
+
threadId: validated.checkpoint.threadId,
|
|
1446
1404
|
reason: "digest-mismatch"
|
|
1447
1405
|
});
|
|
1448
1406
|
const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
|
|
1449
1407
|
const raw = RawCheckpoint.make({
|
|
1450
|
-
|
|
1408
|
+
threadId: validated.checkpoint.threadId,
|
|
1451
1409
|
throughSequence: validated.checkpoint.throughSequence,
|
|
1452
1410
|
tailDigest: validated.checkpoint.tailDigest,
|
|
1453
1411
|
checkpointJson
|
|
1454
1412
|
});
|
|
1455
1413
|
yield* hitFailpoint("save-checkpoint:before");
|
|
1456
1414
|
yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => isDoCheckpointConflict(error) ? CheckpointRejected.make({
|
|
1457
|
-
|
|
1415
|
+
threadId: validated.checkpoint.threadId,
|
|
1458
1416
|
reason: "digest-mismatch"
|
|
1459
1417
|
}) : storeError("save checkpoint", error)));
|
|
1460
1418
|
yield* hitFailpoint("save-checkpoint:after");
|
|
1461
1419
|
});
|
|
1462
|
-
const loadCheckpoint = Effect.fn("
|
|
1420
|
+
const loadCheckpoint = Effect.fn("DoThreadStore.loadCheckpoint")(function* (request) {
|
|
1463
1421
|
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.
|
|
1422
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
1423
|
+
const rows = yield* journal.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
|
|
1466
1424
|
if (rows.length === 0) return Option.none();
|
|
1467
|
-
if (rows.length !== 1) return yield*
|
|
1425
|
+
if (rows.length !== 1) return yield* ThreadStoreError.make({
|
|
1468
1426
|
operation: "load checkpoint",
|
|
1469
1427
|
message: `Expected at most one checkpoint row but found ${rows.length}.`
|
|
1470
1428
|
});
|
|
1471
1429
|
const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
|
|
1472
|
-
if ((yield* tailDigestAt(journal, checkpoint.
|
|
1473
|
-
|
|
1430
|
+
if ((yield* tailDigestAt(journal, checkpoint.threadId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
|
|
1431
|
+
threadId: checkpoint.threadId,
|
|
1474
1432
|
reason: "digest-mismatch"
|
|
1475
1433
|
});
|
|
1476
1434
|
return Option.some(checkpoint);
|
|
1477
1435
|
});
|
|
1478
|
-
const
|
|
1436
|
+
const threadStore = ThreadStore.of({
|
|
1479
1437
|
append,
|
|
1480
|
-
export:
|
|
1438
|
+
export: exportThread,
|
|
1481
1439
|
inspectTail,
|
|
1482
|
-
loadCheckpoint,
|
|
1483
1440
|
materialize,
|
|
1484
1441
|
observe,
|
|
1485
1442
|
read,
|
|
1486
|
-
|
|
1443
|
+
checkpoints: {
|
|
1444
|
+
save: saveCheckpoint,
|
|
1445
|
+
load: loadCheckpoint
|
|
1446
|
+
}
|
|
1487
1447
|
});
|
|
1488
|
-
return Context.make(
|
|
1448
|
+
return Context.make(ThreadStore, threadStore);
|
|
1489
1449
|
});
|
|
1490
1450
|
/**
|
|
1491
|
-
* Durable Object
|
|
1451
|
+
* Durable Object Thread Store implementation with configuration, failpoint, SQL, and
|
|
1492
1452
|
* Crypto authority kept visible in its input channel.
|
|
1493
1453
|
*/
|
|
1494
|
-
const
|
|
1454
|
+
const threadStoreLayer = Layer.effectContext(makeServices$2());
|
|
1495
1455
|
/**
|
|
1496
1456
|
* Validated Durable Object storage configuration Layer with the documented defaults applied.
|
|
1497
|
-
* Shared by the
|
|
1457
|
+
* Shared by the ThreadStore and SubmissionLedger convenience layers so their defaults
|
|
1498
1458
|
* cannot drift.
|
|
1499
1459
|
*/
|
|
1500
1460
|
const storageConfigLayer = (options) => Layer.effect(DoStorageConfig)(Schema.decodeUnknownEffect(DoStorageConfigValue)({
|
|
@@ -1510,12 +1470,12 @@ const storageConfigLayer = (options) => Layer.effect(DoStorageConfig)(Schema.dec
|
|
|
1510
1470
|
/** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
|
|
1511
1471
|
const storageFailpointLayer = (options) => options.failpoint === void 0 ? DoStorageFailpoint.layer : Layer.succeed(DoStorageFailpoint)({ hit: options.failpoint });
|
|
1512
1472
|
/**
|
|
1513
|
-
* A composition-root convenience Layer for canonical
|
|
1473
|
+
* A composition-root convenience Layer for canonical Threads inside one Durable Object,
|
|
1514
1474
|
* built over `ctx.storage`. Durable accepted work is served by the separate SubmissionLedger
|
|
1515
1475
|
* port; point both at the SAME `ctx.storage` so claims fence the same producer epochs
|
|
1516
1476
|
* (ADR-0011 D7's "same file" rule, transposed to one object's private database).
|
|
1517
1477
|
*/
|
|
1518
|
-
const layer = (options) => Layer.unwrap(Effect.map(DoStorageConfig, (config) =>
|
|
1478
|
+
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
1479
|
/** Create an adapter-owned resumable observation offset for a known canonical sequence. */
|
|
1520
1480
|
const observationOffsetAt = makeOffset;
|
|
1521
1481
|
//#endregion
|
|
@@ -1537,7 +1497,7 @@ const WOKEN = "woken";
|
|
|
1537
1497
|
const MAX_IDENTIFIER_LENGTH = 1024;
|
|
1538
1498
|
var SubmissionRow = class extends Schema.Class("SubmissionRow")({
|
|
1539
1499
|
submission_id: BoundedIdentifier,
|
|
1540
|
-
|
|
1500
|
+
thread_id: BoundedIdentifier,
|
|
1541
1501
|
queue_sequence: QueueSequence,
|
|
1542
1502
|
principal: BoundedIdentifier,
|
|
1543
1503
|
idempotency_key: BoundedIdentifier,
|
|
@@ -1625,7 +1585,7 @@ var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ ma
|
|
|
1625
1585
|
var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
|
|
1626
1586
|
const SUBMISSION_COLUMNS = `
|
|
1627
1587
|
submission_id,
|
|
1628
|
-
|
|
1588
|
+
thread_id,
|
|
1629
1589
|
queue_sequence,
|
|
1630
1590
|
principal,
|
|
1631
1591
|
idempotency_key,
|
|
@@ -1662,7 +1622,7 @@ const CHILD_RESERVATION_COLUMNS = `
|
|
|
1662
1622
|
release_began_at,
|
|
1663
1623
|
released_at
|
|
1664
1624
|
`;
|
|
1665
|
-
/** The branded ToolCallId schema, reached through the
|
|
1625
|
+
/** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
|
|
1666
1626
|
const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
|
|
1667
1627
|
const ToolCallIdList = Schema.Array(ToolCallIdSchema);
|
|
1668
1628
|
const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
|
|
@@ -1736,7 +1696,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1736
1696
|
iso: new Date(millis).toISOString()
|
|
1737
1697
|
}));
|
|
1738
1698
|
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$
|
|
1699
|
+
const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1740
1700
|
const readSubmission = Effect.fn("DoSubmissionLedger.readSubmission")(function* (operation, submissionId) {
|
|
1741
1701
|
const rows = yield* sql`
|
|
1742
1702
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
@@ -1767,23 +1727,23 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1767
1727
|
FROM effect_agent_submission_ownership
|
|
1768
1728
|
WHERE submission_id = ${submissionId}
|
|
1769
1729
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1770
|
-
const decoded = yield* decodeRows$
|
|
1730
|
+
const decoded = yield* decodeRows$2(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1771
1731
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
|
|
1772
1732
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1773
1733
|
});
|
|
1774
|
-
const
|
|
1775
|
-
const
|
|
1776
|
-
return
|
|
1734
|
+
const threadEpoch = Effect.fn("DoSubmissionLedger.threadEpoch")(function* (operation, threadId) {
|
|
1735
|
+
const threads = yield* journal.getThread(threadId).pipe(Effect.mapError(internalFailure(operation)));
|
|
1736
|
+
return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
|
|
1777
1737
|
});
|
|
1778
1738
|
/**
|
|
1779
1739
|
* Verify inside the surrounding write transaction that the presented token still owns the
|
|
1780
1740
|
* Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
|
|
1781
|
-
*
|
|
1741
|
+
* Thread's current producer epoch (DUR-006).
|
|
1782
1742
|
*/
|
|
1783
1743
|
const requireOwnership = Effect.fn("DoSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
|
|
1784
1744
|
const ownership = yield* readOwnership(operation, submission.submission_id);
|
|
1785
1745
|
if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
|
|
1786
|
-
const actualEpoch = yield*
|
|
1746
|
+
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
1787
1747
|
const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
1788
1748
|
return yield* OwnershipLost.make({
|
|
1789
1749
|
submissionId,
|
|
@@ -1798,7 +1758,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1798
1758
|
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
1759
|
return yield* decodeSubmissionSnapshotUnknown({
|
|
1800
1760
|
submissionId: row.submission_id,
|
|
1801
|
-
|
|
1761
|
+
threadId: row.thread_id,
|
|
1802
1762
|
queueSequence: row.queue_sequence,
|
|
1803
1763
|
principal: row.principal,
|
|
1804
1764
|
idempotencyKey: row.idempotency_key,
|
|
@@ -1832,7 +1792,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1832
1792
|
FROM effect_agent_settlement_reservations
|
|
1833
1793
|
WHERE submission_id = ${submissionId}
|
|
1834
1794
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1835
|
-
const decoded = yield* decodeRows$
|
|
1795
|
+
const decoded = yield* decodeRows$2(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1836
1796
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
|
|
1837
1797
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1838
1798
|
});
|
|
@@ -1847,7 +1807,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1847
1807
|
FROM effect_agent_abort_intents
|
|
1848
1808
|
WHERE submission_id = ${submissionId}
|
|
1849
1809
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1850
|
-
const decoded = yield* decodeRows$
|
|
1810
|
+
const decoded = yield* decodeRows$2(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1851
1811
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
|
|
1852
1812
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1853
1813
|
});
|
|
@@ -1867,7 +1827,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1867
1827
|
WHERE parent_submission_id = ${parentSubmissionId}
|
|
1868
1828
|
ORDER BY child_submission_id ASC
|
|
1869
1829
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1870
|
-
return yield* decodeRows$
|
|
1830
|
+
return yield* decodeRows$2(Schema.Array(ChildSettlementMarkerRow), "effect_agent_child_settlements", parentSubmissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1871
1831
|
});
|
|
1872
1832
|
/**
|
|
1873
1833
|
* Whether one listed child is provably settled from THIS store: either its own row lives
|
|
@@ -1880,7 +1840,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1880
1840
|
const childRow = yield* readSubmission(operation, childSubmissionId);
|
|
1881
1841
|
return Option.isSome(childRow) && childRow.value.state === "settled";
|
|
1882
1842
|
});
|
|
1883
|
-
const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$
|
|
1843
|
+
const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1884
1844
|
const readChildReservation = Effect.fn("DoSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
|
|
1885
1845
|
const rows = yield* sql`
|
|
1886
1846
|
SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
|
|
@@ -1933,7 +1893,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1933
1893
|
WHERE submission_id = ${submissionId}
|
|
1934
1894
|
ORDER BY tool_call_id ASC
|
|
1935
1895
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1936
|
-
return yield* decodeRows$
|
|
1896
|
+
return yield* decodeRows$2(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1937
1897
|
});
|
|
1938
1898
|
const approvalIntentFromRow = Effect.fn("DoSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
|
|
1939
1899
|
return yield* decodeApprovalDecisionIntent({
|
|
@@ -1958,7 +1918,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1958
1918
|
WHERE submission_id = ${submissionId}
|
|
1959
1919
|
ORDER BY tool_call_id ASC
|
|
1960
1920
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1961
|
-
return yield* decodeRows$
|
|
1921
|
+
return yield* decodeRows$2(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1962
1922
|
});
|
|
1963
1923
|
const unknownResolutionIntentFromRow = Effect.fn("DoSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
|
|
1964
1924
|
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 +1941,18 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
1981
1941
|
* derived from the shared canonical-records table using the deterministic abort record
|
|
1982
1942
|
* identity, never from a cached ledger marker.
|
|
1983
1943
|
*/
|
|
1984
|
-
const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (operation,
|
|
1944
|
+
const canonicalAbortRecordId = Effect.fn("DoSubmissionLedger.canonicalAbortRecordId")(function* (operation, threadId, submissionId) {
|
|
1985
1945
|
const recordId = submissionAbortRecordId(submissionId);
|
|
1986
1946
|
const rows = yield* sql`
|
|
1987
1947
|
SELECT record_id
|
|
1988
1948
|
FROM effect_agent_canonical_records
|
|
1989
|
-
WHERE
|
|
1949
|
+
WHERE thread_id = ${threadId}
|
|
1990
1950
|
AND record_id = ${recordId}
|
|
1991
1951
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1992
|
-
return (yield* decodeRows$
|
|
1952
|
+
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
1953
|
});
|
|
1994
1954
|
const abortIntentFromRow = Effect.fn("DoSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
|
|
1995
|
-
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.
|
|
1955
|
+
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, submissionId);
|
|
1996
1956
|
return yield* decodeAbortIntent({
|
|
1997
1957
|
submissionId: row.submission_id,
|
|
1998
1958
|
author: row.author,
|
|
@@ -2008,19 +1968,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2008
1968
|
const inputJson = yield* encodePersistedJsonText(validated.inputPayload).pipe(Effect.mapError(internalFailure(operation)));
|
|
2009
1969
|
yield* journal.checkValueBound(operation, inputJson).pipe(Effect.mapError(internalFailure(operation)));
|
|
2010
1970
|
const agentDigestsJson = yield* encodeDefinitionDigestsText(validated.agentDigests).pipe(Effect.mapError(internalFailure(operation)));
|
|
2011
|
-
const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.
|
|
1971
|
+
const mintedSubmissionId = `${yield* mintUuid(operation)}:${validated.threadId}`;
|
|
2012
1972
|
if (mintedSubmissionId.length > MAX_IDENTIFIER_LENGTH) return yield* LedgerError.make({
|
|
2013
1973
|
operation,
|
|
2014
|
-
message: `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the
|
|
1974
|
+
message: `A routable Submission identity of ${mintedSubmissionId.length} characters exceeds the ${MAX_IDENTIFIER_LENGTH}-character ledger row bound; shorten the Thread identity.`
|
|
2015
1975
|
});
|
|
2016
1976
|
const mintedReceiptId = `receipt-${yield* mintUuid(operation)}`;
|
|
2017
1977
|
yield* hitFailpoint("ledger:admit:before", operation);
|
|
2018
1978
|
const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
|
|
2019
|
-
const keyRowKey = `${validated.
|
|
1979
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
2020
1980
|
const existingRows = yield* sql`
|
|
2021
1981
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2022
1982
|
FROM effect_agent_submissions
|
|
2023
|
-
WHERE
|
|
1983
|
+
WHERE thread_id = ${validated.threadId}
|
|
2024
1984
|
AND principal = ${validated.principal}
|
|
2025
1985
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2026
1986
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
@@ -2029,7 +1989,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2029
1989
|
if (existing.length === 1) {
|
|
2030
1990
|
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
1991
|
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
|
|
2032
|
-
|
|
1992
|
+
threadId: validated.threadId,
|
|
2033
1993
|
principal: validated.principal,
|
|
2034
1994
|
idempotencyKey: validated.idempotencyKey,
|
|
2035
1995
|
existingInputDigest: existing[0].input_digest,
|
|
@@ -2046,15 +2006,15 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2046
2006
|
const maxRows = yield* sql`
|
|
2047
2007
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
2048
2008
|
FROM effect_agent_submissions
|
|
2049
|
-
WHERE
|
|
2009
|
+
WHERE thread_id = ${validated.threadId}
|
|
2050
2010
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2051
|
-
const decodedMax = yield* decodeRows$
|
|
2011
|
+
const decodedMax = yield* decodeRows$2(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.threadId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
2052
2012
|
const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
|
|
2053
2013
|
const now = yield* currentInstant;
|
|
2054
2014
|
yield* sql`
|
|
2055
2015
|
INSERT INTO effect_agent_submissions (
|
|
2056
2016
|
submission_id,
|
|
2057
|
-
|
|
2017
|
+
thread_id,
|
|
2058
2018
|
queue_sequence,
|
|
2059
2019
|
principal,
|
|
2060
2020
|
idempotency_key,
|
|
@@ -2070,7 +2030,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2070
2030
|
parent_tool_call_id
|
|
2071
2031
|
) VALUES (
|
|
2072
2032
|
${mintedSubmissionId},
|
|
2073
|
-
${validated.
|
|
2033
|
+
${validated.threadId},
|
|
2074
2034
|
${queueSequence},
|
|
2075
2035
|
${validated.principal},
|
|
2076
2036
|
${validated.idempotencyKey},
|
|
@@ -2123,12 +2083,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2123
2083
|
const rows = yield* sql`
|
|
2124
2084
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2125
2085
|
FROM effect_agent_submissions
|
|
2126
|
-
WHERE
|
|
2086
|
+
WHERE thread_id = ${validated.threadId}
|
|
2127
2087
|
AND principal = ${validated.principal}
|
|
2128
2088
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2129
2089
|
`.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.
|
|
2090
|
+
const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
|
|
2091
|
+
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
2092
|
if (decoded.length === 0) return Option.none();
|
|
2133
2093
|
return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
|
|
2134
2094
|
});
|
|
@@ -2138,12 +2098,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2138
2098
|
const rows = yield* sql`
|
|
2139
2099
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2140
2100
|
FROM effect_agent_submissions
|
|
2141
|
-
WHERE
|
|
2101
|
+
WHERE thread_id = ${validated.threadId}
|
|
2142
2102
|
AND principal = ${validated.principal}
|
|
2143
2103
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2144
2104
|
`.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.
|
|
2105
|
+
const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
|
|
2106
|
+
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
2107
|
if (decoded.length === 0) return AdmissionNotAdmitted.make();
|
|
2148
2108
|
return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
|
|
2149
2109
|
});
|
|
@@ -2157,12 +2117,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2157
2117
|
const headRows = yield* sql`
|
|
2158
2118
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2159
2119
|
FROM effect_agent_submissions
|
|
2160
|
-
WHERE
|
|
2120
|
+
WHERE thread_id = ${validated.threadId}
|
|
2161
2121
|
AND state <> 'settled'
|
|
2162
2122
|
ORDER BY queue_sequence ASC
|
|
2163
2123
|
LIMIT 1
|
|
2164
2124
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2165
|
-
const heads = yield* decodeSubmissionRows(operation, validated.
|
|
2125
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
2166
2126
|
if (heads.length === 0) return Option.none();
|
|
2167
2127
|
const head = heads[0];
|
|
2168
2128
|
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 +2131,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2171
2131
|
if (Option.isSome(ownership)) {
|
|
2172
2132
|
if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
|
|
2173
2133
|
}
|
|
2174
|
-
const
|
|
2134
|
+
const threads = yield* journal.getThread(head.thread_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
2175
2135
|
let producerEpoch;
|
|
2176
|
-
if (
|
|
2136
|
+
if (threads.length === 0) {
|
|
2177
2137
|
producerEpoch = 1;
|
|
2178
2138
|
yield* sql`
|
|
2179
|
-
INSERT INTO
|
|
2180
|
-
|
|
2139
|
+
INSERT INTO effect_agent_threads (
|
|
2140
|
+
thread_id,
|
|
2181
2141
|
created_at,
|
|
2182
2142
|
tail_sequence,
|
|
2183
2143
|
tail_digest,
|
|
2184
2144
|
producer_epoch
|
|
2185
2145
|
) VALUES (
|
|
2186
|
-
${head.
|
|
2146
|
+
${head.thread_id},
|
|
2187
2147
|
${now.iso},
|
|
2188
2148
|
0,
|
|
2189
2149
|
${EMPTY_TAIL_DIGEST},
|
|
@@ -2191,11 +2151,11 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2191
2151
|
)
|
|
2192
2152
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2193
2153
|
} else {
|
|
2194
|
-
producerEpoch =
|
|
2154
|
+
producerEpoch = threads[0].producer_epoch + 1;
|
|
2195
2155
|
yield* sql`
|
|
2196
|
-
UPDATE
|
|
2156
|
+
UPDATE effect_agent_threads
|
|
2197
2157
|
SET producer_epoch = ${producerEpoch}
|
|
2198
|
-
WHERE
|
|
2158
|
+
WHERE thread_id = ${head.thread_id}
|
|
2199
2159
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2200
2160
|
}
|
|
2201
2161
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
@@ -2226,14 +2186,14 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2226
2186
|
INSERT INTO effect_agent_attempts (
|
|
2227
2187
|
attempt_id,
|
|
2228
2188
|
submission_id,
|
|
2229
|
-
|
|
2189
|
+
thread_id,
|
|
2230
2190
|
owner_producer_id,
|
|
2231
2191
|
producer_epoch,
|
|
2232
2192
|
claimed_at
|
|
2233
2193
|
) VALUES (
|
|
2234
2194
|
${attemptId},
|
|
2235
2195
|
${head.submission_id},
|
|
2236
|
-
${head.
|
|
2196
|
+
${head.thread_id},
|
|
2237
2197
|
${validated.producerId},
|
|
2238
2198
|
${producerEpoch},
|
|
2239
2199
|
${now.iso}
|
|
@@ -2416,7 +2376,8 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2416
2376
|
submissionId: validated.submissionId,
|
|
2417
2377
|
existingOutcome: reservation.value.outcome
|
|
2418
2378
|
});
|
|
2419
|
-
const
|
|
2379
|
+
const reservationRecord = yield* decodeRecordEnvelopeText(reservation.value.record_json).pipe(Effect.mapError((error) => corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, error.message)));
|
|
2380
|
+
const settlementFailure = settlementFailureFromRecord(reservationRecord);
|
|
2420
2381
|
if (reservation.value.outcome === "failed" !== (settlementFailure !== void 0)) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", validated.submissionId, "The reserved outcome and canonical failure diagnostic disagree.");
|
|
2421
2382
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2422
2383
|
if (submission.state === "settled") {
|
|
@@ -2494,7 +2455,7 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2494
2455
|
${now.iso}
|
|
2495
2456
|
)
|
|
2496
2457
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2497
|
-
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.
|
|
2458
|
+
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, validated.submissionId);
|
|
2498
2459
|
return yield* decodeAbortIntent({
|
|
2499
2460
|
submissionId: validated.submissionId,
|
|
2500
2461
|
author: validated.author,
|
|
@@ -2512,19 +2473,19 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
2512
2473
|
yield* hitFailpoint("ledger:claim-joining:before", operation);
|
|
2513
2474
|
const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
|
|
2514
2475
|
const host = yield* requireSubmission(operation, validated.hostSubmissionId);
|
|
2515
|
-
if (host.
|
|
2476
|
+
if (host.thread_id !== validated.threadId) return yield* LedgerError.make({
|
|
2516
2477
|
operation,
|
|
2517
|
-
message: `Host submission ${validated.hostSubmissionId} does not belong to
|
|
2478
|
+
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`
|
|
2518
2479
|
});
|
|
2519
2480
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
2520
2481
|
const laterRows = yield* sql`
|
|
2521
2482
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2522
2483
|
FROM effect_agent_submissions
|
|
2523
|
-
WHERE
|
|
2484
|
+
WHERE thread_id = ${validated.threadId}
|
|
2524
2485
|
AND queue_sequence > ${host.queue_sequence}
|
|
2525
2486
|
ORDER BY queue_sequence ASC
|
|
2526
2487
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2527
|
-
const later = yield* decodeSubmissionRows(operation, validated.
|
|
2488
|
+
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
2528
2489
|
const claimed = [];
|
|
2529
2490
|
for (const row of later) {
|
|
2530
2491
|
if (claimed.length >= validated.maxCount) break;
|
|
@@ -3034,27 +2995,27 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
3034
2995
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
3035
2996
|
FROM effect_agent_submissions
|
|
3036
2997
|
WHERE state <> 'settled'
|
|
3037
|
-
ORDER BY
|
|
2998
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
3038
2999
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
3039
3000
|
` : sql`
|
|
3040
3001
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
3041
3002
|
FROM effect_agent_submissions
|
|
3042
3003
|
WHERE state <> 'settled'
|
|
3043
3004
|
AND (
|
|
3044
|
-
|
|
3005
|
+
thread_id > ${cursor.threadId}
|
|
3045
3006
|
OR (
|
|
3046
|
-
|
|
3007
|
+
thread_id = ${cursor.threadId}
|
|
3047
3008
|
AND queue_sequence > ${cursor.queueSequence}
|
|
3048
3009
|
)
|
|
3049
3010
|
)
|
|
3050
|
-
ORDER BY
|
|
3011
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
3051
3012
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
3052
3013
|
`).pipe(Effect.mapError(sqlFailure(operation)));
|
|
3053
3014
|
const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
|
|
3054
3015
|
const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
|
|
3055
3016
|
const last = decoded[decoded.length - 1];
|
|
3056
3017
|
return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
|
|
3057
|
-
|
|
3018
|
+
threadId: last.thread_id,
|
|
3058
3019
|
queueSequence: last.queue_sequence
|
|
3059
3020
|
})];
|
|
3060
3021
|
});
|
|
@@ -3211,12 +3172,12 @@ const makeServices$1 = Effect.fn("DoSubmissionLedger.makeServices")(function* ()
|
|
|
3211
3172
|
const submissionLedgerLayer = Layer.effectContext(makeServices$1());
|
|
3212
3173
|
/**
|
|
3213
3174
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
3214
|
-
* same `ctx.storage` as the
|
|
3175
|
+
* same `ctx.storage` as the ThreadStore so claims fence the same producer epochs.
|
|
3215
3176
|
*/
|
|
3216
3177
|
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
3178
|
//#endregion
|
|
3218
3179
|
//#region src/do-schedule-store.ts
|
|
3219
|
-
const CURRENT_SCHEDULE_STORE_VERSION =
|
|
3180
|
+
const CURRENT_SCHEDULE_STORE_VERSION = 2;
|
|
3220
3181
|
const StoredScheduleJson = Schema.String.check(Schema.isMaxLength(19e5));
|
|
3221
3182
|
const StoredDeadline = Schema.NullOr(ScheduleInstant);
|
|
3222
3183
|
var ScheduleRow = class extends Schema.Class("@effect-agent/storage-cloudflare/ScheduleRow")({
|
|
@@ -3246,26 +3207,26 @@ var ScheduleTableNameRow = class extends Schema.Class("@effect-agent/storage-clo
|
|
|
3246
3207
|
var DoScheduleTransaction = class extends Context.Service()("@effect-agent/storage-cloudflare/DoScheduleTransaction") {};
|
|
3247
3208
|
/** Platform driver operations that use the same schedule-state and alarm transaction. */
|
|
3248
3209
|
var DoScheduleAlarmControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoScheduleAlarmControl") {};
|
|
3249
|
-
const unavailable = (operation) => ScheduleStorageError.make({
|
|
3210
|
+
const unavailable$1 = (operation) => ScheduleStorageError.make({
|
|
3250
3211
|
operation,
|
|
3251
3212
|
reason: "unavailable"
|
|
3252
3213
|
});
|
|
3253
|
-
const corrupt = (operation) => ScheduleStorageError.make({
|
|
3214
|
+
const corrupt$1 = (operation) => ScheduleStorageError.make({
|
|
3254
3215
|
operation,
|
|
3255
3216
|
reason: "corrupt"
|
|
3256
3217
|
});
|
|
3257
|
-
const decodeRows = Effect.fn("DoScheduleStore.decodeRows")(function* (schema, rows, operation) {
|
|
3258
|
-
return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3218
|
+
const decodeRows$1 = Effect.fn("DoScheduleStore.decodeRows")(function* (schema, rows, operation) {
|
|
3219
|
+
return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3259
3220
|
});
|
|
3260
|
-
const decodeBoundary = (schema, value, operation) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3221
|
+
const decodeBoundary = (schema, value, operation) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3261
3222
|
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");
|
|
3223
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt$1("decode schedule")));
|
|
3224
|
+
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
3225
|
return record;
|
|
3265
3226
|
});
|
|
3266
3227
|
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")));
|
|
3228
|
+
const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt$1("encode schedule")));
|
|
3229
|
+
return yield* Schema.decodeUnknownEffect(StoredScheduleJson)(encoded).pipe(Effect.mapError(() => corrupt$1("encode schedule bounds")));
|
|
3269
3230
|
});
|
|
3270
3231
|
const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function* () {
|
|
3271
3232
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -3279,12 +3240,12 @@ const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function
|
|
|
3279
3240
|
'effect_agent_schedules'
|
|
3280
3241
|
)
|
|
3281
3242
|
ORDER BY name
|
|
3282
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3283
|
-
const tables = yield* decodeRows(Schema.Array(ScheduleTableNameRow), rawTables, operation);
|
|
3243
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3244
|
+
const tables = yield* decodeRows$1(Schema.Array(ScheduleTableNameRow), rawTables, operation);
|
|
3284
3245
|
const hasState = tables.some((row) => row.name === "effect_agent_schedule_store_state");
|
|
3285
3246
|
const hasSchedules = tables.some((row) => row.name === "effect_agent_schedules");
|
|
3286
3247
|
if (!hasState) {
|
|
3287
|
-
if (hasSchedules) return yield* corrupt(operation);
|
|
3248
|
+
if (hasSchedules) return yield* corrupt$1(operation);
|
|
3288
3249
|
yield* sql.withTransaction(Effect.gen(function* () {
|
|
3289
3250
|
yield* sql`
|
|
3290
3251
|
CREATE TABLE effect_agent_schedule_store_state (
|
|
@@ -3318,17 +3279,17 @@ const initializeScheduleStore = Effect.fn("DoScheduleStore.initialize")(function
|
|
|
3318
3279
|
singleton, storage_version, alarm_generation
|
|
3319
3280
|
) VALUES (1, ${CURRENT_SCHEDULE_STORE_VERSION}, 0)
|
|
3320
3281
|
`.withoutTransform;
|
|
3321
|
-
})).pipe(Effect.mapError(() => unavailable(operation)));
|
|
3282
|
+
})).pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3322
3283
|
return;
|
|
3323
3284
|
}
|
|
3324
|
-
if (!hasSchedules) return yield* corrupt(operation);
|
|
3285
|
+
if (!hasSchedules) return yield* corrupt$1(operation);
|
|
3325
3286
|
const rawState = yield* sql`
|
|
3326
3287
|
SELECT storage_version, alarm_generation
|
|
3327
3288
|
FROM effect_agent_schedule_store_state
|
|
3328
3289
|
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);
|
|
3290
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3291
|
+
const state = yield* decodeRows$1(Schema.Array(ScheduleStoreStateRow), rawState, operation);
|
|
3292
|
+
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
3293
|
});
|
|
3333
3294
|
const makeServices = Effect.gen(function* () {
|
|
3334
3295
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -3342,13 +3303,13 @@ const makeServices = Effect.gen(function* () {
|
|
|
3342
3303
|
WHERE tenant_id = ${key.owner.tenantId}
|
|
3343
3304
|
AND owner_id = ${key.owner.ownerId}
|
|
3344
3305
|
AND schedule_id = ${key.scheduleId}
|
|
3345
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3346
|
-
return yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
3306
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3307
|
+
return yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
|
|
3347
3308
|
});
|
|
3348
3309
|
const readOne = Effect.fn("DoScheduleStore.readOne")(function* (key, operation) {
|
|
3349
3310
|
const rows = yield* readRows(key, operation);
|
|
3350
3311
|
if (rows.length === 0) return null;
|
|
3351
|
-
if (rows.length !== 1) return yield* corrupt(operation);
|
|
3312
|
+
if (rows.length !== 1) return yield* corrupt$1(operation);
|
|
3352
3313
|
return yield* decodeRecord(rows[0]);
|
|
3353
3314
|
});
|
|
3354
3315
|
const readNextDeadline = Effect.fn("DoScheduleStore.readNextDeadline")(function* (owner, operation) {
|
|
@@ -3356,15 +3317,15 @@ const makeServices = Effect.gen(function* () {
|
|
|
3356
3317
|
SELECT MIN(deadline_at_millis) AS deadline_at_millis
|
|
3357
3318
|
FROM effect_agent_schedules
|
|
3358
3319
|
WHERE deadline_at_millis IS NOT NULL
|
|
3359
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3320
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3360
3321
|
SELECT MIN(deadline_at_millis) AS deadline_at_millis
|
|
3361
3322
|
FROM effect_agent_schedules
|
|
3362
3323
|
WHERE tenant_id = ${owner.tenantId}
|
|
3363
3324
|
AND owner_id = ${owner.ownerId}
|
|
3364
3325
|
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);
|
|
3326
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3327
|
+
const decoded = yield* decodeRows$1(Schema.Array(ScheduleDeadlineRow), rows, operation);
|
|
3328
|
+
if (decoded.length !== 1) return yield* corrupt$1(operation);
|
|
3368
3329
|
return decoded[0].deadline_at_millis;
|
|
3369
3330
|
});
|
|
3370
3331
|
const replaceAlarm = Effect.fn("DoScheduleStore.replaceAlarm")(function* (replace, deadlineAtMillis, operation) {
|
|
@@ -3373,9 +3334,9 @@ const makeServices = Effect.gen(function* () {
|
|
|
3373
3334
|
SET alarm_generation = alarm_generation + 1
|
|
3374
3335
|
WHERE singleton = 1
|
|
3375
3336
|
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);
|
|
3337
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3338
|
+
const state = yield* decodeRows$1(Schema.Array(ScheduleStoreStateRow), rawState, operation);
|
|
3339
|
+
if (state.length !== 1 || state[0].storage_version !== CURRENT_SCHEDULE_STORE_VERSION) return yield* corrupt$1(operation);
|
|
3379
3340
|
yield* scheduleFailpoint.hit("schedule:alarm:before");
|
|
3380
3341
|
yield* replace({
|
|
3381
3342
|
deadlineAtMillis,
|
|
@@ -3406,9 +3367,9 @@ const makeServices = Effect.gen(function* () {
|
|
|
3406
3367
|
AND owner_id = ${canonical.owner.ownerId}
|
|
3407
3368
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
3408
3369
|
(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);
|
|
3370
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3371
|
+
const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3372
|
+
if (counts.length !== 1) return yield* corrupt$1(operation);
|
|
3412
3373
|
if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
3413
3374
|
yield* scheduleFailpoint.hit("schedule:insert:before");
|
|
3414
3375
|
yield* sql`
|
|
@@ -3421,7 +3382,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3421
3382
|
${scheduleDeadline(canonical)},
|
|
3422
3383
|
${recordJson}
|
|
3423
3384
|
)
|
|
3424
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3385
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3425
3386
|
const deadline = yield* readNextDeadline(void 0, operation);
|
|
3426
3387
|
yield* replaceAlarm(replace, deadline, operation);
|
|
3427
3388
|
return {
|
|
@@ -3446,7 +3407,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3446
3407
|
AND owner_id = ${request.owner.ownerId}
|
|
3447
3408
|
ORDER BY schedule_id
|
|
3448
3409
|
LIMIT ${request.limit + 1}
|
|
3449
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3410
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3450
3411
|
SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
|
|
3451
3412
|
FROM effect_agent_schedules
|
|
3452
3413
|
WHERE tenant_id = ${request.owner.tenantId}
|
|
@@ -3454,8 +3415,8 @@ const makeServices = Effect.gen(function* () {
|
|
|
3454
3415
|
AND schedule_id > ${request.after}
|
|
3455
3416
|
ORDER BY schedule_id
|
|
3456
3417
|
LIMIT ${request.limit + 1}
|
|
3457
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3458
|
-
const decoded = yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
3418
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3419
|
+
const decoded = yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
|
|
3459
3420
|
const records = yield* Effect.forEach(decoded, decodeRecord);
|
|
3460
3421
|
const hasNext = records.length > request.limit;
|
|
3461
3422
|
const items = hasNext ? records.slice(0, request.limit) : records;
|
|
@@ -3480,9 +3441,9 @@ const makeServices = Effect.gen(function* () {
|
|
|
3480
3441
|
WHERE tenant_id = ${key.owner.tenantId} AND owner_id = ${key.owner.ownerId}
|
|
3481
3442
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
3482
3443
|
(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);
|
|
3444
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3445
|
+
const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3446
|
+
if (counts.length !== 1) return yield* corrupt$1(operation);
|
|
3486
3447
|
if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
3487
3448
|
}
|
|
3488
3449
|
if (next === current) return {
|
|
@@ -3497,7 +3458,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3497
3458
|
WHERE tenant_id = ${canonicalKey.owner.tenantId}
|
|
3498
3459
|
AND owner_id = ${canonicalKey.owner.ownerId}
|
|
3499
3460
|
AND schedule_id = ${canonicalKey.scheduleId}
|
|
3500
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3461
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3501
3462
|
const deadline = yield* readNextDeadline(void 0, operation);
|
|
3502
3463
|
yield* replaceAlarm(replace, deadline, operation);
|
|
3503
3464
|
return {
|
|
@@ -3510,7 +3471,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3510
3471
|
});
|
|
3511
3472
|
const due = Effect.fn("DoScheduleStore.due")(function* (nowMillis, limit, owner, after) {
|
|
3512
3473
|
const operation = "query due schedules";
|
|
3513
|
-
const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3474
|
+
const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3514
3475
|
const continuation = cursor === void 0 ? sql`1 = 1` : sql`
|
|
3515
3476
|
(deadline_at_millis, tenant_id, owner_id, schedule_id) >
|
|
3516
3477
|
(${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
|
|
@@ -3520,7 +3481,7 @@ const makeServices = Effect.gen(function* () {
|
|
|
3520
3481
|
WHERE deadline_at_millis <= ${nowMillis} AND ${continuation}
|
|
3521
3482
|
ORDER BY deadline_at_millis, tenant_id, owner_id, schedule_id
|
|
3522
3483
|
LIMIT ${limit}
|
|
3523
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3484
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3524
3485
|
SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
|
|
3525
3486
|
FROM effect_agent_schedules
|
|
3526
3487
|
WHERE tenant_id = ${owner.tenantId}
|
|
@@ -3528,8 +3489,8 @@ const makeServices = Effect.gen(function* () {
|
|
|
3528
3489
|
AND deadline_at_millis <= ${nowMillis} AND ${continuation}
|
|
3529
3490
|
ORDER BY deadline_at_millis, schedule_id
|
|
3530
3491
|
LIMIT ${limit}
|
|
3531
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3532
|
-
return (yield* decodeRows(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
|
|
3492
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3493
|
+
return (yield* decodeRows$1(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
|
|
3533
3494
|
owner: {
|
|
3534
3495
|
tenantId: row.tenant_id,
|
|
3535
3496
|
ownerId: row.owner_id
|
|
@@ -3571,18 +3532,724 @@ const makeServices = Effect.gen(function* () {
|
|
|
3571
3532
|
*/
|
|
3572
3533
|
const scheduleStoreLayer = Layer.effectContext(makeServices);
|
|
3573
3534
|
//#endregion
|
|
3535
|
+
//#region src/do-subscription-store.ts
|
|
3536
|
+
const CURRENT_SUBSCRIPTION_STORE_VERSION = 2;
|
|
3537
|
+
const StoredJson = Schema.String.check(Schema.isMaxLength(19e5));
|
|
3538
|
+
const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
|
|
3539
|
+
const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
|
|
3540
|
+
const ScanRow = Schema.Struct({
|
|
3541
|
+
event_scan_cursor: Schema.String,
|
|
3542
|
+
delivery_scan_cursor: Schema.String,
|
|
3543
|
+
recovery_scan_cursor: Schema.Natural
|
|
3544
|
+
});
|
|
3545
|
+
const JsonRow = Schema.Struct({ record_json: StoredJson });
|
|
3546
|
+
const RegistrationRow = Schema.Struct({
|
|
3547
|
+
owner_id: Schema.String,
|
|
3548
|
+
subscription_id: Schema.String,
|
|
3549
|
+
ordinal: Schema.Natural,
|
|
3550
|
+
source_name: Schema.String,
|
|
3551
|
+
source_version: Schema.String,
|
|
3552
|
+
matching_key: Schema.String,
|
|
3553
|
+
state: SubscriptionRecord.fields.state,
|
|
3554
|
+
expires_at_millis: Schema.Number,
|
|
3555
|
+
recovery_at_millis: Schema.NullOr(Schema.Number),
|
|
3556
|
+
record_json: StoredJson
|
|
3557
|
+
});
|
|
3558
|
+
const EventRow = Schema.Struct({
|
|
3559
|
+
event_id: Schema.String,
|
|
3560
|
+
source_name: Schema.String,
|
|
3561
|
+
source_version: Schema.String,
|
|
3562
|
+
matching_key: Schema.String,
|
|
3563
|
+
payload_digest: Digest,
|
|
3564
|
+
cutoff: Schema.Natural,
|
|
3565
|
+
cursor: Schema.Natural,
|
|
3566
|
+
routing_complete: Schema.Number,
|
|
3567
|
+
next_attempt_at_millis: Schema.Number,
|
|
3568
|
+
record_json: StoredJson
|
|
3569
|
+
});
|
|
3570
|
+
const DeliveryRow = Schema.Struct({
|
|
3571
|
+
owner_id: Schema.String,
|
|
3572
|
+
subscription_id: Schema.String,
|
|
3573
|
+
event_id: Schema.String,
|
|
3574
|
+
delivery_key: Schema.String,
|
|
3575
|
+
state: SubscriptionDelivery.fields.state,
|
|
3576
|
+
next_attempt_at_millis: Schema.Number,
|
|
3577
|
+
record_json: StoredJson
|
|
3578
|
+
});
|
|
3579
|
+
const StoreStateRow = Schema.Struct({
|
|
3580
|
+
storage_version: Schema.Int,
|
|
3581
|
+
alarm_generation: Schema.Natural
|
|
3582
|
+
});
|
|
3583
|
+
/** One Durable Object transaction combining subscription SQL and its single native alarm. */
|
|
3584
|
+
var DoSubscriptionTransaction = class extends Context.Service()("@effect-agent/storage-cloudflare/DoSubscriptionTransaction") {};
|
|
3585
|
+
var DoSubscriptionAlarmControl = class extends Context.Service()("@effect-agent/storage-cloudflare/DoSubscriptionAlarmControl") {};
|
|
3586
|
+
const error = (reason, code) => SubscriptionError.make({
|
|
3587
|
+
reason,
|
|
3588
|
+
code
|
|
3589
|
+
});
|
|
3590
|
+
const unavailable = (operation) => error("storage", operation);
|
|
3591
|
+
const corrupt = (operation) => error("corrupt", operation);
|
|
3592
|
+
const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
3593
|
+
const validate = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
|
|
3594
|
+
const encode = (schema, value, code) => Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
|
|
3595
|
+
const decode = (schema, value, code) => Schema.decodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
|
|
3596
|
+
const decodeRows = (schema, rows, code) => Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
|
|
3597
|
+
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;
|
|
3598
|
+
const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize")(function* () {
|
|
3599
|
+
const sql = yield* SqlClient.SqlClient;
|
|
3600
|
+
const names = yield* sql`
|
|
3601
|
+
SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'effect_agent_subscription_%'
|
|
3602
|
+
`.pipe(Effect.mapError(() => unavailable("inspect subscription storage")));
|
|
3603
|
+
const decodedNames = yield* decodeRows(Schema.Struct({ name: Schema.String }), names, "inspect subscription storage");
|
|
3604
|
+
const expected = /* @__PURE__ */ new Set([
|
|
3605
|
+
"effect_agent_subscription_store_state",
|
|
3606
|
+
"effect_agent_subscription_sequences",
|
|
3607
|
+
"effect_agent_subscriptions",
|
|
3608
|
+
"effect_agent_subscription_events",
|
|
3609
|
+
"effect_agent_subscription_deliveries"
|
|
3610
|
+
]);
|
|
3611
|
+
const hasState = decodedNames.some(({ name }) => name === "effect_agent_subscription_store_state");
|
|
3612
|
+
if (!hasState && decodedNames.length > 0) return yield* corrupt("partial subscription storage");
|
|
3613
|
+
if (!hasState) {
|
|
3614
|
+
yield* sql.withTransaction(Effect.gen(function* () {
|
|
3615
|
+
yield* sql`CREATE TABLE effect_agent_subscription_store_state (
|
|
3616
|
+
singleton INTEGER PRIMARY KEY NOT NULL CHECK(singleton=1), storage_version INTEGER NOT NULL, alarm_generation INTEGER NOT NULL
|
|
3617
|
+
)`.withoutTransform;
|
|
3618
|
+
yield* sql`CREATE TABLE effect_agent_subscription_sequences (
|
|
3619
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, sequence INTEGER NOT NULL,
|
|
3620
|
+
event_scan_cursor TEXT NOT NULL, delivery_scan_cursor TEXT NOT NULL, recovery_scan_cursor INTEGER NOT NULL,
|
|
3621
|
+
PRIMARY KEY (tenant_id, source_address)
|
|
3622
|
+
)`.withoutTransform;
|
|
3623
|
+
yield* sql`CREATE TABLE effect_agent_subscriptions (
|
|
3624
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
|
|
3625
|
+
ordinal INTEGER NOT NULL, source_name TEXT NOT NULL, source_version TEXT NOT NULL, matching_key TEXT NOT NULL,
|
|
3626
|
+
state TEXT NOT NULL, expires_at_millis INTEGER NOT NULL, recovery_at_millis INTEGER, record_json TEXT NOT NULL,
|
|
3627
|
+
PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id), UNIQUE (tenant_id, source_address, ordinal)
|
|
3628
|
+
)`.withoutTransform;
|
|
3629
|
+
yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`.withoutTransform;
|
|
3630
|
+
yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`.withoutTransform;
|
|
3631
|
+
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;
|
|
3632
|
+
yield* sql`CREATE TABLE effect_agent_subscription_events (
|
|
3633
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, event_id TEXT NOT NULL, source_name TEXT NOT NULL,
|
|
3634
|
+
source_version TEXT NOT NULL, matching_key TEXT NOT NULL, payload_digest TEXT NOT NULL, cutoff INTEGER NOT NULL,
|
|
3635
|
+
cursor INTEGER NOT NULL, routing_complete INTEGER NOT NULL, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL,
|
|
3636
|
+
PRIMARY KEY (tenant_id, source_address, event_id)
|
|
3637
|
+
)`.withoutTransform;
|
|
3638
|
+
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;
|
|
3639
|
+
yield* sql`CREATE TABLE effect_agent_subscription_deliveries (
|
|
3640
|
+
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
|
|
3641
|
+
event_id TEXT NOT NULL, delivery_key TEXT NOT NULL, state TEXT NOT NULL, next_attempt_at_millis INTEGER NOT NULL,
|
|
3642
|
+
record_json TEXT NOT NULL, PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),
|
|
3643
|
+
UNIQUE (tenant_id, source_address, delivery_key)
|
|
3644
|
+
)`.withoutTransform;
|
|
3645
|
+
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;
|
|
3646
|
+
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;
|
|
3647
|
+
yield* sql`INSERT INTO effect_agent_subscription_store_state (singleton, storage_version, alarm_generation)
|
|
3648
|
+
VALUES (1, ${CURRENT_SUBSCRIPTION_STORE_VERSION}, 0)`.withoutTransform;
|
|
3649
|
+
})).pipe(Effect.mapError(() => unavailable("initialize subscription storage")));
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
if (decodedNames.length !== expected.size || decodedNames.some(({ name }) => !expected.has(name))) return yield* corrupt("subscription storage tables");
|
|
3653
|
+
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")));
|
|
3654
|
+
const state = yield* decodeRows(StoreStateRow, rows, "read subscription storage version");
|
|
3655
|
+
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");
|
|
3656
|
+
});
|
|
3657
|
+
const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function* (owned) {
|
|
3658
|
+
const partition = yield* validate(SourcePartition, owned, "partition");
|
|
3659
|
+
const sql = yield* SqlClient.SqlClient;
|
|
3660
|
+
const failpoint = yield* SubscriptionFailpoint;
|
|
3661
|
+
const transactions = yield* DoSubscriptionTransaction;
|
|
3662
|
+
yield* initializeDoSubscriptionStore();
|
|
3663
|
+
yield* sql`
|
|
3664
|
+
INSERT INTO effect_agent_subscription_sequences (
|
|
3665
|
+
tenant_id, source_address, sequence, event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
3666
|
+
) VALUES (${partition.tenantId}, ${partition.address}, 0, '', '', 0) ON CONFLICT DO NOTHING
|
|
3667
|
+
`.pipe(Effect.mapError(() => unavailable("initialize subscription partition")));
|
|
3668
|
+
const query = (effect, code) => effect.pipe(Effect.mapError(() => unavailable(code)));
|
|
3669
|
+
const readIndexedDeadline = Effect.fn("DoSubscriptionStore.readIndexedDeadline")(function* () {
|
|
3670
|
+
const scanRows = yield* query(sql`
|
|
3671
|
+
SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor FROM effect_agent_subscription_sequences
|
|
3672
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3673
|
+
`, "read scan cursor deadline");
|
|
3674
|
+
const scans = yield* decodeRows(ScanRow, scanRows, "read scan cursor deadline");
|
|
3675
|
+
if (scans.length !== 1) return yield* corrupt("scan cursor deadline");
|
|
3676
|
+
if (scans[0].event_scan_cursor !== "" || scans[0].delivery_scan_cursor !== "" || scans[0].recovery_scan_cursor !== 0) return 0;
|
|
3677
|
+
const rows = yield* query(sql`
|
|
3678
|
+
SELECT MIN(deadline) AS deadline FROM (
|
|
3679
|
+
SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
|
|
3680
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
|
|
3681
|
+
UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
|
|
3682
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
3683
|
+
UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
|
|
3684
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
|
|
3685
|
+
)
|
|
3686
|
+
`, "read subscription deadline");
|
|
3687
|
+
const decoded = yield* decodeRows(Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }), rows, "read subscription deadline");
|
|
3688
|
+
if (decoded.length !== 1) return yield* corrupt("subscription deadline");
|
|
3689
|
+
return decoded[0].deadline;
|
|
3690
|
+
});
|
|
3691
|
+
const replaceAlarm = Effect.fn("DoSubscriptionStore.replaceAlarm")(function* (replace, deadlineAtMillis) {
|
|
3692
|
+
const rows = yield* query(sql`
|
|
3693
|
+
UPDATE effect_agent_subscription_store_state SET alarm_generation=alarm_generation+1
|
|
3694
|
+
WHERE singleton=1 RETURNING storage_version, alarm_generation
|
|
3695
|
+
`, "advance subscription alarm generation");
|
|
3696
|
+
const state = yield* decodeRows(StoreStateRow, rows, "advance subscription alarm generation");
|
|
3697
|
+
if (state.length !== 1 || state[0].storage_version !== CURRENT_SUBSCRIPTION_STORE_VERSION) return yield* corrupt("subscription alarm state");
|
|
3698
|
+
yield* failpoint.hit("subscription:alarm:before");
|
|
3699
|
+
yield* replace({
|
|
3700
|
+
deadlineAtMillis,
|
|
3701
|
+
generation: state[0].alarm_generation
|
|
3702
|
+
});
|
|
3703
|
+
yield* failpoint.hit("subscription:alarm:after");
|
|
3704
|
+
});
|
|
3705
|
+
const transact = (effect) => transactions.run((replace) => Effect.gen(function* () {
|
|
3706
|
+
const value = yield* effect;
|
|
3707
|
+
yield* replaceAlarm(replace, yield* readIndexedDeadline());
|
|
3708
|
+
return value;
|
|
3709
|
+
}));
|
|
3710
|
+
const requirePartition = (candidate, code) => sameSourcePartition(candidate, partition) ? Effect.void : Effect.fail(error("validation", code));
|
|
3711
|
+
const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (input, code) {
|
|
3712
|
+
const key = yield* validate(SubscriptionKey, input, code);
|
|
3713
|
+
yield* requirePartition(key.partition, code);
|
|
3714
|
+
return key;
|
|
3715
|
+
});
|
|
3716
|
+
const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (key, code) {
|
|
3717
|
+
const rows = yield* query(sql`
|
|
3718
|
+
SELECT owner_id, subscription_id, ordinal, source_name, source_version, matching_key, state,
|
|
3719
|
+
expires_at_millis, recovery_at_millis, record_json FROM effect_agent_subscriptions
|
|
3720
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3721
|
+
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId}
|
|
3722
|
+
`, code);
|
|
3723
|
+
const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
|
|
3724
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3725
|
+
const row = decodedRows[0];
|
|
3726
|
+
if (row === void 0) return null;
|
|
3727
|
+
const record = yield* decode(SubscriptionRecord, row.record_json, code);
|
|
3728
|
+
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`);
|
|
3729
|
+
return record;
|
|
3730
|
+
});
|
|
3731
|
+
const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (eventId, code) {
|
|
3732
|
+
const rows = yield* query(sql`
|
|
3733
|
+
SELECT event_id, source_name, source_version, matching_key, payload_digest, cutoff, cursor,
|
|
3734
|
+
routing_complete, next_attempt_at_millis, record_json FROM effect_agent_subscription_events
|
|
3735
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${eventId}
|
|
3736
|
+
`, code);
|
|
3737
|
+
const decodedRows = yield* decodeRows(EventRow, rows, code);
|
|
3738
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3739
|
+
const row = decodedRows[0];
|
|
3740
|
+
if (row === void 0) return null;
|
|
3741
|
+
const event = yield* decode(AcceptedEvent, row.record_json, code);
|
|
3742
|
+
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`);
|
|
3743
|
+
return event;
|
|
3744
|
+
});
|
|
3745
|
+
const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (key, code) {
|
|
3746
|
+
const rows = yield* query(sql`
|
|
3747
|
+
SELECT owner_id, subscription_id, event_id, delivery_key, state, next_attempt_at_millis, record_json
|
|
3748
|
+
FROM effect_agent_subscription_deliveries
|
|
3749
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3750
|
+
AND owner_id=${key.subscription.ownerId} AND subscription_id=${key.subscription.subscriptionId}
|
|
3751
|
+
AND event_id=${key.eventId}
|
|
3752
|
+
`, code);
|
|
3753
|
+
const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
|
|
3754
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3755
|
+
const row = decodedRows[0];
|
|
3756
|
+
if (row === void 0) return null;
|
|
3757
|
+
const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
|
|
3758
|
+
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`);
|
|
3759
|
+
return delivery;
|
|
3760
|
+
});
|
|
3761
|
+
const count = Effect.fn("SqliteSubscriptionStore.count")(function* (statement, code) {
|
|
3762
|
+
const rows = yield* query(statement, code);
|
|
3763
|
+
const decoded = yield* decodeRows(CountRow, rows, code);
|
|
3764
|
+
if (decoded.length !== 1) return yield* corrupt(code);
|
|
3765
|
+
return decoded[0].count;
|
|
3766
|
+
});
|
|
3767
|
+
const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
|
|
3768
|
+
const rows = yield* query(sql`
|
|
3769
|
+
UPDATE effect_agent_subscription_sequences SET sequence=sequence+1
|
|
3770
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3771
|
+
RETURNING sequence
|
|
3772
|
+
`, "advance subscription sequence");
|
|
3773
|
+
const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
|
|
3774
|
+
if (decoded.length !== 1) return yield* corrupt("subscription sequence");
|
|
3775
|
+
return decoded[0].sequence;
|
|
3776
|
+
});
|
|
3777
|
+
const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (record) {
|
|
3778
|
+
const json = yield* encode(SubscriptionRecord, record, "encode subscription");
|
|
3779
|
+
yield* query(sql`
|
|
3780
|
+
UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
|
|
3781
|
+
recovery_at_millis=${record.recovery?.nextAttemptAtMillis ?? null}, record_json=${json}
|
|
3782
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3783
|
+
AND owner_id=${record.key.ownerId} AND subscription_id=${record.key.subscriptionId}
|
|
3784
|
+
`, "write subscription");
|
|
3785
|
+
});
|
|
3786
|
+
const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (event) {
|
|
3787
|
+
const json = yield* encode(AcceptedEvent, event, "encode event");
|
|
3788
|
+
yield* query(sql`
|
|
3789
|
+
UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
|
|
3790
|
+
next_attempt_at_millis=${event.nextAttemptAtMillis}, record_json=${json}
|
|
3791
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${event.eventId}
|
|
3792
|
+
`, "write event");
|
|
3793
|
+
});
|
|
3794
|
+
const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (delivery) {
|
|
3795
|
+
const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
|
|
3796
|
+
yield* query(sql`
|
|
3797
|
+
UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
|
|
3798
|
+
next_attempt_at_millis=${delivery.retry.nextAttemptAtMillis}, record_json=${json}
|
|
3799
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3800
|
+
AND owner_id=${delivery.key.subscription.ownerId} AND subscription_id=${delivery.key.subscription.subscriptionId}
|
|
3801
|
+
AND event_id=${delivery.key.eventId}
|
|
3802
|
+
`, "write delivery");
|
|
3803
|
+
});
|
|
3804
|
+
const register = Effect.fn("SqliteSubscriptionStore.register")(function* (input, inputLimits) {
|
|
3805
|
+
const record = yield* validate(SubscriptionRecord, input, "register-record");
|
|
3806
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
|
|
3807
|
+
yield* requirePartition(record.key.partition, "register-partition");
|
|
3808
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3809
|
+
const existing = yield* readRegistration(record.key, "register-existing");
|
|
3810
|
+
if (existing !== null) {
|
|
3811
|
+
if (existing.creationFingerprint !== record.creationFingerprint) return yield* error("conflict", "registration-identity");
|
|
3812
|
+
return {
|
|
3813
|
+
value: existing,
|
|
3814
|
+
changed: false
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
if (bytes(record.configuration.context) > limits.maxContextBytes) return yield* error("capacity", "context-bytes");
|
|
3818
|
+
if (bytes(record.configuration.parameters) > limits.maxPayloadBytes) return yield* error("capacity", "parameters-bytes");
|
|
3819
|
+
if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return yield* error("capacity", "lifetime");
|
|
3820
|
+
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");
|
|
3821
|
+
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");
|
|
3822
|
+
const assigned = {
|
|
3823
|
+
...record,
|
|
3824
|
+
ordinal: yield* nextSequence()
|
|
3825
|
+
};
|
|
3826
|
+
const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
|
|
3827
|
+
yield* failpoint.hit("subscription:register:before");
|
|
3828
|
+
yield* query(sql`
|
|
3829
|
+
INSERT INTO effect_agent_subscriptions (tenant_id, source_address, owner_id, subscription_id, ordinal,
|
|
3830
|
+
source_name, source_version, matching_key, state, expires_at_millis, recovery_at_millis, record_json)
|
|
3831
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${assigned.key.ownerId}, ${assigned.key.subscriptionId}, ${assigned.ordinal},
|
|
3832
|
+
${assigned.configuration.source.name}, ${assigned.configuration.source.version}, ${assigned.configuration.matchingKey}, ${assigned.state},
|
|
3833
|
+
${assigned.configuration.expiresAtMillis}, ${assigned.recovery?.nextAttemptAtMillis ?? null}, ${json})
|
|
3834
|
+
`, "insert registration");
|
|
3835
|
+
return {
|
|
3836
|
+
value: assigned,
|
|
3837
|
+
changed: true
|
|
3838
|
+
};
|
|
3839
|
+
}));
|
|
3840
|
+
if (result.changed) yield* failpoint.hit("subscription:register:after");
|
|
3841
|
+
return result.value;
|
|
3842
|
+
});
|
|
3843
|
+
const get = Effect.fn("SqliteSubscriptionStore.get")(function* (input) {
|
|
3844
|
+
return yield* readRegistration(yield* requireKey(input, "get-key"), "get subscription");
|
|
3845
|
+
});
|
|
3846
|
+
const list = Effect.fn("SqliteSubscriptionStore.list")(function* (ownerId, after, limit) {
|
|
3847
|
+
const rows = yield* query(sql`
|
|
3848
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3849
|
+
AND owner_id=${ownerId} AND ordinal>${after} ORDER BY ordinal LIMIT ${limit}
|
|
3850
|
+
`, "list subscriptions");
|
|
3851
|
+
const decoded = yield* decodeRows(Schema.Struct({
|
|
3852
|
+
owner_id: Schema.String,
|
|
3853
|
+
subscription_id: Schema.String,
|
|
3854
|
+
ordinal: Schema.Natural
|
|
3855
|
+
}), rows, "list subscriptions");
|
|
3856
|
+
return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
|
|
3857
|
+
const record = yield* readRegistration({
|
|
3858
|
+
partition,
|
|
3859
|
+
ownerId: row.owner_id,
|
|
3860
|
+
subscriptionId: row.subscription_id
|
|
3861
|
+
}, "list subscription");
|
|
3862
|
+
if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("list subscription projection");
|
|
3863
|
+
return record;
|
|
3864
|
+
}));
|
|
3865
|
+
});
|
|
3866
|
+
const cancel = Effect.fn("SqliteSubscriptionStore.cancel")(function* (input) {
|
|
3867
|
+
const key = yield* requireKey(input, "cancel-key");
|
|
3868
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3869
|
+
const current = yield* readRegistration(key, "cancel subscription");
|
|
3870
|
+
if (current === null) return yield* error("not-found", "subscription");
|
|
3871
|
+
if (current.state === "cancelled") return {
|
|
3872
|
+
value: current,
|
|
3873
|
+
changed: false
|
|
3874
|
+
};
|
|
3875
|
+
const updated = {
|
|
3876
|
+
...current,
|
|
3877
|
+
state: "cancelled",
|
|
3878
|
+
recovery: null
|
|
3879
|
+
};
|
|
3880
|
+
yield* failpoint.hit("subscription:cancel:before");
|
|
3881
|
+
yield* writeRegistration(updated);
|
|
3882
|
+
return {
|
|
3883
|
+
value: updated,
|
|
3884
|
+
changed: true
|
|
3885
|
+
};
|
|
3886
|
+
}));
|
|
3887
|
+
if (result.changed) yield* failpoint.hit("subscription:cancel:after");
|
|
3888
|
+
return result.value;
|
|
3889
|
+
});
|
|
3890
|
+
const accept = Effect.fn("SqliteSubscriptionStore.accept")(function* (input, inputLimits) {
|
|
3891
|
+
const event = yield* validate(AcceptedEvent, input, "accept-event");
|
|
3892
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
|
|
3893
|
+
yield* requirePartition(event.partition, "accept-partition");
|
|
3894
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3895
|
+
const existing = yield* readEvent(event.eventId, "accept event");
|
|
3896
|
+
if (existing !== null) {
|
|
3897
|
+
if (!sameAcceptedEventIdentity(existing, event)) return yield* error("conflict", "event-identity");
|
|
3898
|
+
return {
|
|
3899
|
+
value: existing,
|
|
3900
|
+
changed: false
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
if (bytes(event.payload) > limits.maxPayloadBytes) return yield* error("capacity", "payload-bytes");
|
|
3904
|
+
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");
|
|
3905
|
+
const accepted = {
|
|
3906
|
+
...event,
|
|
3907
|
+
cutoff: yield* nextSequence(),
|
|
3908
|
+
cursor: 0,
|
|
3909
|
+
routingComplete: false,
|
|
3910
|
+
routingFailure: null
|
|
3911
|
+
};
|
|
3912
|
+
const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
|
|
3913
|
+
yield* failpoint.hit("subscription:accept:before");
|
|
3914
|
+
yield* query(sql`
|
|
3915
|
+
INSERT INTO effect_agent_subscription_events (tenant_id, source_address, event_id, source_name, source_version,
|
|
3916
|
+
matching_key, payload_digest, cutoff, cursor, routing_complete, next_attempt_at_millis, record_json)
|
|
3917
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${accepted.eventId}, ${accepted.source.name}, ${accepted.source.version},
|
|
3918
|
+
${accepted.matchingKey}, ${accepted.payloadDigest}, ${accepted.cutoff}, ${accepted.cursor}, 0, ${accepted.nextAttemptAtMillis}, ${json})
|
|
3919
|
+
`, "insert event");
|
|
3920
|
+
return {
|
|
3921
|
+
value: accepted,
|
|
3922
|
+
changed: true
|
|
3923
|
+
};
|
|
3924
|
+
}));
|
|
3925
|
+
if (result.changed) yield* failpoint.hit("subscription:accept:after");
|
|
3926
|
+
return result.value;
|
|
3927
|
+
});
|
|
3928
|
+
const event = Effect.fn("SqliteSubscriptionStore.event")((eventId) => readEvent(eventId, "get event"));
|
|
3929
|
+
const pendingEvents = Effect.fn("SqliteSubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
|
|
3930
|
+
const rows = yield* query(sql`
|
|
3931
|
+
SELECT event_id FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3932
|
+
AND routing_complete=0 AND next_attempt_at_millis<=${nowMillis} AND event_id>${after} ORDER BY event_id LIMIT ${limit}
|
|
3933
|
+
`, "pending events");
|
|
3934
|
+
return yield* decodeRows(Schema.Struct({ event_id: Schema.String }), rows, "pending event keys").pipe(Effect.map((items) => items.map((item) => item.event_id)));
|
|
3935
|
+
});
|
|
3936
|
+
const candidates = Effect.fn("SqliteSubscriptionStore.candidates")(function* (input, limit) {
|
|
3937
|
+
const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
|
|
3938
|
+
yield* requirePartition(supplied.partition, "candidates-partition");
|
|
3939
|
+
const stored = yield* readEvent(supplied.eventId, "candidates event");
|
|
3940
|
+
if (stored === null) return yield* error("not-found", "event");
|
|
3941
|
+
if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
|
|
3942
|
+
const rows = yield* query(sql`
|
|
3943
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3944
|
+
AND source_name=${stored.source.name} AND source_version=${stored.source.version} AND matching_key=${stored.matchingKey}
|
|
3945
|
+
AND ordinal>${stored.cursor} AND ordinal<=${stored.cutoff} ORDER BY ordinal LIMIT ${limit}
|
|
3946
|
+
`, "subscription candidates");
|
|
3947
|
+
const decoded = yield* decodeRows(Schema.Struct({
|
|
3948
|
+
owner_id: Schema.String,
|
|
3949
|
+
subscription_id: Schema.String,
|
|
3950
|
+
ordinal: Schema.Natural
|
|
3951
|
+
}), rows, "subscription candidates");
|
|
3952
|
+
return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
|
|
3953
|
+
const record = yield* readRegistration({
|
|
3954
|
+
partition,
|
|
3955
|
+
ownerId: row.owner_id,
|
|
3956
|
+
subscriptionId: row.subscription_id
|
|
3957
|
+
}, "subscription candidate");
|
|
3958
|
+
if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("subscription candidate projection");
|
|
3959
|
+
return record;
|
|
3960
|
+
}));
|
|
3961
|
+
});
|
|
3962
|
+
const insertDelivery = Effect.fn("SqliteSubscriptionStore.insertDelivery")(function* (delivery) {
|
|
3963
|
+
const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
|
|
3964
|
+
yield* query(sql`
|
|
3965
|
+
INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
|
|
3966
|
+
delivery_key, state, next_attempt_at_millis, record_json)
|
|
3967
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${delivery.key.subscription.ownerId}, ${delivery.key.subscription.subscriptionId},
|
|
3968
|
+
${delivery.key.eventId}, ${subscriptionDeliveryKeyString(delivery.key)}, ${delivery.state}, ${delivery.retry.nextAttemptAtMillis}, ${json})
|
|
3969
|
+
`, "insert delivery");
|
|
3970
|
+
});
|
|
3971
|
+
const select = Effect.fn("SqliteSubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
|
|
3972
|
+
const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
|
|
3973
|
+
const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
|
|
3974
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
|
|
3975
|
+
yield* requirePartition(supplied.partition, "select-partition");
|
|
3976
|
+
for (const candidate of deliveries) yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
|
|
3977
|
+
if (yield* transact(Effect.gen(function* () {
|
|
3978
|
+
const accepted = yield* readEvent(supplied.eventId, "select event");
|
|
3979
|
+
if (accepted === null) return yield* error("not-found", "event");
|
|
3980
|
+
if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor) return yield* error("conflict", "event-cursor");
|
|
3981
|
+
if (accepted.routingComplete) return false;
|
|
3982
|
+
if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return yield* error("validation", "cursor");
|
|
3983
|
+
yield* failpoint.hit("subscription:select:before");
|
|
3984
|
+
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
3985
|
+
const additions = [];
|
|
3986
|
+
for (const delivery of deliveries) {
|
|
3987
|
+
const record = yield* readRegistration(delivery.key.subscription, "select registration");
|
|
3988
|
+
if (record === null) return yield* error("not-found", "subscription");
|
|
3989
|
+
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");
|
|
3990
|
+
const existing = yield* readDelivery(delivery.key, "select existing delivery");
|
|
3991
|
+
if (existing !== null) {
|
|
3992
|
+
if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
|
|
3993
|
+
continue;
|
|
3994
|
+
}
|
|
3995
|
+
if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
|
|
3996
|
+
additions.push({
|
|
3997
|
+
delivery,
|
|
3998
|
+
record
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
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");
|
|
4002
|
+
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");
|
|
4003
|
+
for (const addition of additions) {
|
|
4004
|
+
yield* insertDelivery(addition.delivery);
|
|
4005
|
+
if (addition.record.configuration.mode === "once") yield* writeRegistration({
|
|
4006
|
+
...addition.record,
|
|
4007
|
+
state: "consumed",
|
|
4008
|
+
recovery: null
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
yield* writeEvent({
|
|
4012
|
+
...accepted,
|
|
4013
|
+
cursor,
|
|
4014
|
+
routingComplete: complete,
|
|
4015
|
+
routingFailure: null
|
|
4016
|
+
});
|
|
4017
|
+
return true;
|
|
4018
|
+
}))) yield* failpoint.hit("subscription:select:after");
|
|
4019
|
+
});
|
|
4020
|
+
const catchUp = Effect.fn("SqliteSubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
|
|
4021
|
+
const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
|
|
4022
|
+
const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
|
|
4023
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
|
|
4024
|
+
yield* requirePartition(supplied.partition, "catch-up-partition");
|
|
4025
|
+
yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
|
|
4026
|
+
if (yield* transact(Effect.gen(function* () {
|
|
4027
|
+
const accepted = yield* readEvent(supplied.eventId, "catch-up event");
|
|
4028
|
+
const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
|
|
4029
|
+
if (accepted === null || record === null) return yield* error("not-found", accepted === null ? "event" : "subscription");
|
|
4030
|
+
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");
|
|
4031
|
+
const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
|
|
4032
|
+
if (existing !== null) {
|
|
4033
|
+
if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
|
|
4034
|
+
return false;
|
|
4035
|
+
}
|
|
4036
|
+
yield* failpoint.hit("subscription:catch-up:before");
|
|
4037
|
+
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
4038
|
+
if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, true)) return yield* error("conflict", "catch-up-eligibility");
|
|
4039
|
+
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");
|
|
4040
|
+
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");
|
|
4041
|
+
yield* insertDelivery(delivery);
|
|
4042
|
+
yield* writeRegistration({
|
|
4043
|
+
...record,
|
|
4044
|
+
state: "consumed",
|
|
4045
|
+
recovery: null
|
|
4046
|
+
});
|
|
4047
|
+
return true;
|
|
4048
|
+
}))) yield* failpoint.hit("subscription:catch-up:after");
|
|
4049
|
+
});
|
|
4050
|
+
const deferEvent = Effect.fn("SqliteSubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
|
|
4051
|
+
const routingFailure = code === void 0 ? "routing-failed" : yield* validate(SubscriptionName, code, "routing-failure");
|
|
4052
|
+
yield* transact(Effect.gen(function* () {
|
|
4053
|
+
const accepted = yield* readEvent(eventId, "defer event");
|
|
4054
|
+
if (accepted === null) return yield* error("not-found", "event");
|
|
4055
|
+
yield* failpoint.hit("subscription:defer-event:before");
|
|
4056
|
+
yield* writeEvent({
|
|
4057
|
+
...accepted,
|
|
4058
|
+
nextAttemptAtMillis,
|
|
4059
|
+
routingFailure
|
|
4060
|
+
});
|
|
4061
|
+
}));
|
|
4062
|
+
yield* failpoint.hit("subscription:defer-event:after");
|
|
4063
|
+
});
|
|
4064
|
+
const delivery = Effect.fn("SqliteSubscriptionStore.delivery")(function* (input) {
|
|
4065
|
+
const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
|
|
4066
|
+
yield* requirePartition(key.subscription.partition, "delivery-partition");
|
|
4067
|
+
return yield* readDelivery(key, "get delivery");
|
|
4068
|
+
});
|
|
4069
|
+
const pendingDeliveries = Effect.fn("SqliteSubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
|
|
4070
|
+
const rows = yield* query(sql`
|
|
4071
|
+
SELECT owner_id, subscription_id, event_id FROM effect_agent_subscription_deliveries
|
|
4072
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
4073
|
+
AND next_attempt_at_millis<=${nowMillis} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
4074
|
+
`, "pending deliveries");
|
|
4075
|
+
const rowSchema = Schema.Struct({
|
|
4076
|
+
owner_id: Schema.String,
|
|
4077
|
+
subscription_id: Schema.String,
|
|
4078
|
+
event_id: Schema.String
|
|
4079
|
+
});
|
|
4080
|
+
return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(Effect.map((items) => items.map((item) => ({
|
|
4081
|
+
subscription: {
|
|
4082
|
+
partition,
|
|
4083
|
+
ownerId: item.owner_id,
|
|
4084
|
+
subscriptionId: item.subscription_id
|
|
4085
|
+
},
|
|
4086
|
+
eventId: item.event_id
|
|
4087
|
+
}))));
|
|
4088
|
+
});
|
|
4089
|
+
const listDeliveries = Effect.fn("SqliteSubscriptionStore.listDeliveries")(function* (input, after, limit) {
|
|
4090
|
+
const key = yield* requireKey(input, "list-deliveries-key");
|
|
4091
|
+
const rows = yield* query(sql`
|
|
4092
|
+
SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
4093
|
+
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
4094
|
+
`, "list deliveries");
|
|
4095
|
+
const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
|
|
4096
|
+
return yield* Effect.forEach(decoded, (row) => decode(SubscriptionDelivery, row.record_json, "list delivery"));
|
|
4097
|
+
});
|
|
4098
|
+
const changeDelivery = Effect.fn("SqliteSubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
|
|
4099
|
+
const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
|
|
4100
|
+
const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
|
|
4101
|
+
const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
|
|
4102
|
+
yield* requirePartition(key.subscription.partition, "change-delivery-partition");
|
|
4103
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
4104
|
+
const existing = yield* readDelivery(key, "change delivery");
|
|
4105
|
+
const record = yield* readRegistration(key.subscription, "change delivery subscription");
|
|
4106
|
+
if (existing === null || record === null) return yield* error("not-found", existing === null ? "delivery" : "subscription");
|
|
4107
|
+
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
|
|
4108
|
+
const effectiveChange = change._tag === "Prepare" ? {
|
|
4109
|
+
...change,
|
|
4110
|
+
nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
|
|
4111
|
+
} : change;
|
|
4112
|
+
const transition = applySubscriptionDeliveryChange(existing, record, deliveryId, effectiveChange);
|
|
4113
|
+
if (Result.isFailure(transition)) return yield* transition.failure;
|
|
4114
|
+
if (transition.success === existing) return {
|
|
4115
|
+
value: existing,
|
|
4116
|
+
changed: false
|
|
4117
|
+
};
|
|
4118
|
+
yield* writeDelivery(transition.success);
|
|
4119
|
+
return {
|
|
4120
|
+
value: transition.success,
|
|
4121
|
+
changed: true
|
|
4122
|
+
};
|
|
4123
|
+
}));
|
|
4124
|
+
if (result.changed) yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
|
|
4125
|
+
return result.value;
|
|
4126
|
+
});
|
|
4127
|
+
const recovering = Effect.fn("SqliteSubscriptionStore.recovering")(function* (nowMillis, after, limit) {
|
|
4128
|
+
const rows = yield* query(sql`
|
|
4129
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions
|
|
4130
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active'
|
|
4131
|
+
AND recovery_at_millis IS NOT NULL AND recovery_at_millis<=${nowMillis} AND ordinal>${after}
|
|
4132
|
+
ORDER BY ordinal LIMIT ${limit}
|
|
4133
|
+
`, "recovering subscriptions");
|
|
4134
|
+
const rowSchema = Schema.Struct({
|
|
4135
|
+
owner_id: Schema.String,
|
|
4136
|
+
subscription_id: Schema.String,
|
|
4137
|
+
ordinal: Schema.Natural
|
|
4138
|
+
});
|
|
4139
|
+
return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(Effect.map((items) => items.map((item) => ({
|
|
4140
|
+
key: {
|
|
4141
|
+
partition,
|
|
4142
|
+
ownerId: item.owner_id,
|
|
4143
|
+
subscriptionId: item.subscription_id
|
|
4144
|
+
},
|
|
4145
|
+
ordinal: item.ordinal
|
|
4146
|
+
}))));
|
|
4147
|
+
});
|
|
4148
|
+
const deferRecovery = Effect.fn("SqliteSubscriptionStore.deferRecovery")(function* (input, recovery) {
|
|
4149
|
+
const key = yield* requireKey(input, "defer-recovery-key");
|
|
4150
|
+
yield* transact(Effect.gen(function* () {
|
|
4151
|
+
const record = yield* readRegistration(key, "defer recovery");
|
|
4152
|
+
if (record === null) return yield* error("not-found", "subscription");
|
|
4153
|
+
yield* failpoint.hit("subscription:defer-recovery:before");
|
|
4154
|
+
yield* writeRegistration({
|
|
4155
|
+
...record,
|
|
4156
|
+
recovery: record.state === "active" ? recovery : null
|
|
4157
|
+
});
|
|
4158
|
+
}));
|
|
4159
|
+
yield* failpoint.hit("subscription:defer-recovery:after");
|
|
4160
|
+
});
|
|
4161
|
+
const readScanCursors = Effect.gen(function* () {
|
|
4162
|
+
const rows = yield* query(sql`
|
|
4163
|
+
SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
4164
|
+
FROM effect_agent_subscription_sequences
|
|
4165
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
4166
|
+
`, "read subscription scan cursors");
|
|
4167
|
+
const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
|
|
4168
|
+
if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
|
|
4169
|
+
return {
|
|
4170
|
+
events: decoded[0].event_scan_cursor,
|
|
4171
|
+
deliveries: decoded[0].delivery_scan_cursor,
|
|
4172
|
+
recovery: decoded[0].recovery_scan_cursor
|
|
4173
|
+
};
|
|
4174
|
+
});
|
|
4175
|
+
const advanceScanCursors = Effect.fn("SqliteSubscriptionStore.advanceScanCursors")(function* (input) {
|
|
4176
|
+
const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
|
|
4177
|
+
yield* transact(Effect.gen(function* () {
|
|
4178
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:before");
|
|
4179
|
+
yield* query(sql`
|
|
4180
|
+
UPDATE effect_agent_subscription_sequences
|
|
4181
|
+
SET event_scan_cursor=${cursors.events}, delivery_scan_cursor=${cursors.deliveries}, recovery_scan_cursor=${cursors.recovery}
|
|
4182
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
4183
|
+
`, "advance subscription scan cursors");
|
|
4184
|
+
}));
|
|
4185
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:after");
|
|
4186
|
+
});
|
|
4187
|
+
const indexedDeadline = query(sql`
|
|
4188
|
+
SELECT MIN(deadline) AS deadline FROM (
|
|
4189
|
+
SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
|
|
4190
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
|
|
4191
|
+
UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
|
|
4192
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
4193
|
+
UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
|
|
4194
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
|
|
4195
|
+
)
|
|
4196
|
+
`, "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"))));
|
|
4197
|
+
const nextDeadline = Effect.gen(function* () {
|
|
4198
|
+
const cursors = yield* readScanCursors;
|
|
4199
|
+
if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
|
|
4200
|
+
return yield* indexedDeadline;
|
|
4201
|
+
});
|
|
4202
|
+
const prearm = Effect.fn("DoSubscriptionStore.prearm")(function* (deadlineAtMillis) {
|
|
4203
|
+
yield* transactions.run((replace) => replaceAlarm(replace, deadlineAtMillis));
|
|
4204
|
+
yield* failpoint.hit("subscription:prearm:after");
|
|
4205
|
+
});
|
|
4206
|
+
const reconcile = Effect.gen(function* () {
|
|
4207
|
+
yield* transactions.run((replace) => Effect.gen(function* () {
|
|
4208
|
+
yield* replaceAlarm(replace, yield* readIndexedDeadline());
|
|
4209
|
+
}));
|
|
4210
|
+
yield* failpoint.hit("subscription:reconcile:after");
|
|
4211
|
+
});
|
|
4212
|
+
return Context.make(SubscriptionStore, {
|
|
4213
|
+
partition,
|
|
4214
|
+
register,
|
|
4215
|
+
get,
|
|
4216
|
+
list,
|
|
4217
|
+
cancel,
|
|
4218
|
+
accept,
|
|
4219
|
+
event,
|
|
4220
|
+
pendingEvents,
|
|
4221
|
+
candidates,
|
|
4222
|
+
select,
|
|
4223
|
+
catchUp,
|
|
4224
|
+
deferEvent,
|
|
4225
|
+
delivery,
|
|
4226
|
+
pendingDeliveries,
|
|
4227
|
+
listDeliveries,
|
|
4228
|
+
changeDelivery,
|
|
4229
|
+
recovering,
|
|
4230
|
+
deferRecovery,
|
|
4231
|
+
readScanCursors,
|
|
4232
|
+
advanceScanCursors,
|
|
4233
|
+
nextDeadline
|
|
4234
|
+
}).pipe(Context.add(DoSubscriptionAlarmControl, {
|
|
4235
|
+
prearm,
|
|
4236
|
+
reconcile
|
|
4237
|
+
}));
|
|
4238
|
+
});
|
|
4239
|
+
const doSubscriptionStoreLayer = (partition) => Layer.effectContext(makeSubscriptionStore(partition));
|
|
4240
|
+
//#endregion
|
|
3574
4241
|
//#region src/port-protocol.ts
|
|
3575
4242
|
/**
|
|
3576
4243
|
* 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
|
|
4244
|
+
* envelopes for the CLOSED route-capable subset of the thread ports. One Thread's
|
|
4245
|
+
* Durable Object executes another Thread's request against its OWN local facets; the
|
|
3579
4246
|
* envelopes here are the only values that cross the Object boundary, and they are
|
|
3580
4247
|
* transport-agnostic — native Durable Object JS RPC is the shipped carrier, fetch-with-JSON
|
|
3581
4248
|
* the documented fallback, and both move the same Schema-encoded JSON.
|
|
3582
4249
|
*
|
|
3583
4250
|
* The closed subset is exactly the set of operations the durable coordinator performs against
|
|
3584
|
-
* a FOREIGN
|
|
3585
|
-
* child-settlement notification, and the child-
|
|
4251
|
+
* a FOREIGN Thread (parent/child establishment, status checks, abort propagation,
|
|
4252
|
+
* child-settlement notification, and the child-thread store operations used by
|
|
3586
4253
|
* establishment, `verifySettledChild`, and result projection):
|
|
3587
4254
|
*
|
|
3588
4255
|
* - ledger: `admit`, `markReady`, `lookup`, `resolveAdmission`, `requestAbort`,
|
|
@@ -3595,18 +4262,18 @@ const scheduleStoreLayer = Layer.effectContext(makeServices);
|
|
|
3595
4262
|
*
|
|
3596
4263
|
* Failures cross the boundary as the `PortFailure` union and re-decode on the caller side to
|
|
3597
4264
|
* the SAME tagged error types the local facet would have produced, so routed calls keep
|
|
3598
|
-
* error-tag fidelity. `cause` chains inside `LedgerError`/`
|
|
4265
|
+
* error-tag fidelity. `cause` chains inside `LedgerError`/`ThreadStoreError` travel as
|
|
3599
4266
|
* Schema defects and do not claim instance fidelity across Objects (plan §2.8).
|
|
3600
4267
|
*/
|
|
3601
4268
|
/** Ceiling for protocol diagnostic strings; matches `AdmissionIndeterminate.reason`. */
|
|
3602
4269
|
const MAX_PORT_DIAGNOSTIC_LENGTH = 4096;
|
|
3603
4270
|
const BoundedDiagnostic = Schema.String.check(Schema.isMaxLength(MAX_PORT_DIAGNOSTIC_LENGTH));
|
|
3604
4271
|
/** Truncate a diagnostic string to the protocol's bounded diagnostic length. */
|
|
3605
|
-
const boundPortDiagnostic = (value) => value.length > 4096 ? `${value.slice(0,
|
|
4272
|
+
const boundPortDiagnostic = (value) => value.length > 4096 ? `${value.slice(0, 4093)}...` : value;
|
|
3606
4273
|
/**
|
|
3607
4274
|
* The envelope itself could not be honored: the receiving Object could not decode the
|
|
3608
4275
|
* 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`/`
|
|
4276
|
+
* callers fold it into the operation's base error (`LedgerError`/`ThreadStoreError`),
|
|
3610
4277
|
* except `resolveAdmission`, which folds it into `AdmissionIndeterminate` because a
|
|
3611
4278
|
* non-answer is never proof of absence (SUB-031).
|
|
3612
4279
|
*/
|
|
@@ -3623,16 +4290,16 @@ var LedgerResolveAdmissionCall = class extends Schema.TaggedClass("@effect-agent
|
|
|
3623
4290
|
var LedgerRequestAbortCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/LedgerRequestAbortCall")("LedgerRequestAbort", { request: AbortCommand }) {};
|
|
3624
4291
|
/** Routed `SubmissionLedger.recordChildSettled` — the child→parent durable notification. */
|
|
3625
4292
|
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 `
|
|
4293
|
+
/** Routed `ThreadStore.materialize` against the owning Object. */
|
|
4294
|
+
var StoreMaterializeCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreMaterializeCall")("StoreMaterialize", { request: ThreadMaterialization }) {};
|
|
4295
|
+
/** Routed `ThreadStore.append` against the owning Object. */
|
|
3629
4296
|
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:
|
|
4297
|
+
/** Routed one-page `ThreadStore.read`; the page bound is the request's own `limit`. */
|
|
4298
|
+
var StoreReadPageCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreReadPageCall")("StoreReadPage", { request: ThreadRead }) {};
|
|
4299
|
+
/** Routed `ThreadStore.inspectTail` against the owning Object. */
|
|
4300
|
+
var StoreInspectTailCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailCall")("StoreInspectTail", { request: ThreadTailRequest }) {};
|
|
4301
|
+
/** Routed `ThreadStore.export` against the owning Object. */
|
|
4302
|
+
var StoreExportCall = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportCall")("StoreExport", { request: ThreadExportRequest }) {};
|
|
3636
4303
|
/** Every request that may cross a Durable Object boundary — the CLOSED route-capable subset. */
|
|
3637
4304
|
const PortRequest = Schema.Union([
|
|
3638
4305
|
LedgerAdmitCall,
|
|
@@ -3658,8 +4325,8 @@ var StoreMaterializeResult = class extends Schema.TaggedClass("@effect-agent/sto
|
|
|
3658
4325
|
var StoreAppendResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreAppendResult")("StoreAppendResult", { result: AppendResult }) {};
|
|
3659
4326
|
/** One page of canonical records, bounded by the request's `limit` (≤ 1,024). */
|
|
3660
4327
|
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:
|
|
4328
|
+
var StoreInspectTailResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreInspectTailResult")("StoreInspectTailResult", { tail: ThreadTail }) {};
|
|
4329
|
+
var StoreExportResult = class extends Schema.TaggedClass("@effect-agent/storage-cloudflare/StoreExportResult")("StoreExportResult", { export: ThreadExport }) {};
|
|
3663
4330
|
/** Every successful routed result. Callers narrow by the tag their request implies. */
|
|
3664
4331
|
const PortResult = Schema.Union([
|
|
3665
4332
|
LedgerAdmitResult,
|
|
@@ -3677,15 +4344,15 @@ const PortResult = Schema.Union([
|
|
|
3677
4344
|
/**
|
|
3678
4345
|
* Every typed failure a route-capable operation can produce on its owning Object, plus the
|
|
3679
4346
|
* protocol's own `PortProtocolError`. Members re-decode to the SAME tagged classes the
|
|
3680
|
-
*
|
|
4347
|
+
* thread ports declare, so a routed caller observes identical error tags and fields.
|
|
3681
4348
|
*/
|
|
3682
4349
|
const PortFailure = Schema.Union([
|
|
3683
4350
|
AdmissionConflict,
|
|
3684
4351
|
SettlementConflict,
|
|
3685
4352
|
JoinedToHost,
|
|
3686
4353
|
LedgerError,
|
|
3687
|
-
|
|
3688
|
-
|
|
4354
|
+
ThreadStoreError,
|
|
4355
|
+
ThreadNotMaterialized,
|
|
3689
4356
|
AppendConflict,
|
|
3690
4357
|
FenceRejected,
|
|
3691
4358
|
PortProtocolError
|
|
@@ -3702,8 +4369,8 @@ const encodePortResponse = Schema.encodeEffect(PortResponse);
|
|
|
3702
4369
|
const decodePortResponse = Schema.decodeUnknownEffect(PortResponse);
|
|
3703
4370
|
//#endregion
|
|
3704
4371
|
//#region src/routing.ts
|
|
3705
|
-
const
|
|
3706
|
-
const
|
|
4372
|
+
const ThreadIdSchema = ThreadMaterialization.fields.threadId;
|
|
4373
|
+
const decodeThreadId = Schema.decodeUnknownEffect(ThreadIdSchema);
|
|
3707
4374
|
/**
|
|
3708
4375
|
* The ledger row bound routable Submission identities must respect (mirrors the local
|
|
3709
4376
|
* facet's `MAX_IDENTIFIER_LENGTH`; the minting side already refuses longer identities typed
|
|
@@ -3714,7 +4381,7 @@ const MAX_ROUTABLE_SUBMISSION_ID_LENGTH = 1024;
|
|
|
3714
4381
|
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
4382
|
/**
|
|
3716
4383
|
* A transport could not deliver a port request to (or an answer from) the owning
|
|
3717
|
-
*
|
|
4384
|
+
* Thread's Durable Object. `retryable` carries the platform's own stub signal when one
|
|
3718
4385
|
* exists. This error never crosses the wire — it is the CALLER-side evidence that the
|
|
3719
4386
|
* authority was unreachable, which is exactly the case `AdmissionIndeterminate` was
|
|
3720
4387
|
* specified for (SUB-031).
|
|
@@ -3757,24 +4424,24 @@ const portTransportFailure = (target, cause) => {
|
|
|
3757
4424
|
};
|
|
3758
4425
|
/**
|
|
3759
4426
|
* Delivery of Schema-encoded port envelopes to the Durable Object that owns a FOREIGN
|
|
3760
|
-
*
|
|
4427
|
+
* Thread (plan §1.3, D-P6-3). The shipped implementation (platform-cloudflare, WP3)
|
|
3761
4428
|
* calls the owner's `portCall` over native Durable Object JS RPC via
|
|
3762
|
-
* `namespace.idFromName(
|
|
4429
|
+
* `namespace.idFromName(threadId)`; the protocol is transport-agnostic and any carrier
|
|
3763
4430
|
* that moves the encoded envelopes verbatim satisfies this service. Implementations MUST
|
|
3764
4431
|
* surface every delivery problem as `PortTransportError` and must never fabricate an answer.
|
|
3765
4432
|
*/
|
|
3766
|
-
var
|
|
4433
|
+
var ThreadPortTransport = class extends Context.Service()("@effect-agent/storage-cloudflare/ThreadPortTransport") {};
|
|
3767
4434
|
const LOCAL = { _tag: "local" };
|
|
3768
4435
|
/**
|
|
3769
|
-
* Parse a DC-minted routable Submission identity — `{uuidv7}:{
|
|
3770
|
-
* FIRST `:` because the
|
|
4436
|
+
* Parse a DC-minted routable Submission identity — `{uuidv7}:{threadId}`, split at the
|
|
4437
|
+
* FIRST `:` because the Thread tail may itself contain colons (D-P6-5). This adapter
|
|
3771
4438
|
* minted the format at admission and is the ONLY component that parses it; identities that do
|
|
3772
4439
|
* not carry the minted shape (no separator, non-UUID head, empty tail) fall back to the local
|
|
3773
4440
|
* facet, which is the only authority this Object can consult without inventing an owner.
|
|
3774
4441
|
* Identities beyond the ledger's 1,024-character row bound fail typed: the minting side
|
|
3775
4442
|
* refused them at admission, so they cannot name any stored row anywhere.
|
|
3776
4443
|
*/
|
|
3777
|
-
const routableSubmissionTarget = (
|
|
4444
|
+
const routableSubmissionTarget = (localThreadId) => Effect.fn("DoPortRouting.routableSubmissionTarget")(function* (operation, submissionId) {
|
|
3778
4445
|
if (submissionId.length > MAX_ROUTABLE_SUBMISSION_ID_LENGTH) return yield* LedgerError.make({
|
|
3779
4446
|
operation,
|
|
3780
4447
|
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 +4450,16 @@ const routableSubmissionTarget = (localConversationId) => Effect.fn("DoPortRouti
|
|
|
3783
4450
|
if (separator === -1) return LOCAL;
|
|
3784
4451
|
if (!UUID_HEAD_PATTERN.test(submissionId.slice(0, separator))) return LOCAL;
|
|
3785
4452
|
const tail = submissionId.slice(separator + 1);
|
|
3786
|
-
if (tail ===
|
|
3787
|
-
return yield*
|
|
4453
|
+
if (tail === localThreadId) return LOCAL;
|
|
4454
|
+
return yield* decodeThreadId(tail).pipe(Effect.map((threadId) => ({
|
|
3788
4455
|
_tag: "foreign",
|
|
3789
|
-
|
|
4456
|
+
threadId
|
|
3790
4457
|
})), Effect.orElseSucceed(() => LOCAL));
|
|
3791
4458
|
});
|
|
3792
4459
|
const NoAdditionalPortFailure = Schema.Never;
|
|
3793
4460
|
const AbortPortFailure = Schema.Union([SettlementConflict, JoinedToHost]);
|
|
3794
4461
|
const AppendPortFailure = Schema.Union([
|
|
3795
|
-
|
|
4462
|
+
ThreadNotMaterialized,
|
|
3796
4463
|
AppendConflict,
|
|
3797
4464
|
FenceRejected
|
|
3798
4465
|
]);
|
|
@@ -3800,26 +4467,27 @@ const AppendPortFailure = Schema.Union([
|
|
|
3800
4467
|
* The fail-fast refusal for any foreign operation OUTSIDE the closed route-capable subset
|
|
3801
4468
|
* (plan §1.3): honesty over accidental distribution.
|
|
3802
4469
|
*/
|
|
3803
|
-
const
|
|
4470
|
+
const crossThreadLedgerError = (operation, target) => LedgerError.make({
|
|
3804
4471
|
operation,
|
|
3805
|
-
message: `${operation} addressed to foreign
|
|
4472
|
+
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
4473
|
});
|
|
3807
|
-
const
|
|
4474
|
+
const crossThreadStoreError = (operation, target) => ThreadStoreError.make({
|
|
3808
4475
|
operation,
|
|
3809
|
-
message: `${operation} addressed to foreign
|
|
4476
|
+
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
4477
|
});
|
|
3811
4478
|
const makeTransportCall = (transport) => Effect.fn("DoPortRouting.transportCall")(function* (target, call) {
|
|
3812
4479
|
const encoded = yield* encodePortRequest(call).pipe(Effect.mapError((error) => PortProtocolError.make({ message: boundPortDiagnostic(`The port request could not be encoded: ${error.message}`) })));
|
|
3813
|
-
|
|
4480
|
+
const raw = yield* transport.call(target, encoded);
|
|
4481
|
+
return yield* decodePortResponse(raw).pipe(Effect.mapError((error) => PortProtocolError.make({ message: boundPortDiagnostic(`The port response could not be decoded: ${error.message}`) })));
|
|
3814
4482
|
});
|
|
3815
4483
|
const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServices")(function* (options) {
|
|
3816
4484
|
const local = yield* SubmissionLedger;
|
|
3817
|
-
const transport = yield*
|
|
4485
|
+
const transport = yield* ThreadPortTransport;
|
|
3818
4486
|
const transportCall = makeTransportCall(transport);
|
|
3819
|
-
const submissionTarget = routableSubmissionTarget(options.
|
|
4487
|
+
const submissionTarget = routableSubmissionTarget(options.localThreadId);
|
|
3820
4488
|
const routeFailure = (operation, target) => (error) => LedgerError.make({
|
|
3821
4489
|
operation,
|
|
3822
|
-
message: boundPortDiagnostic(`Routed ${operation} to the
|
|
4490
|
+
message: boundPortDiagnostic(`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`),
|
|
3823
4491
|
cause: error
|
|
3824
4492
|
});
|
|
3825
4493
|
/**
|
|
@@ -3838,14 +4506,14 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3838
4506
|
if (failure._tag === "LedgerError") return Effect.fail(failure);
|
|
3839
4507
|
return Effect.fail(LedgerError.make({
|
|
3840
4508
|
operation,
|
|
3841
|
-
message: boundPortDiagnostic(`The
|
|
4509
|
+
message: boundPortDiagnostic(`The Thread Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
|
|
3842
4510
|
cause: failure
|
|
3843
4511
|
}));
|
|
3844
4512
|
}
|
|
3845
4513
|
const result = response.result;
|
|
3846
4514
|
if (!isExpectedResult(result)) return Effect.fail(LedgerError.make({
|
|
3847
4515
|
operation,
|
|
3848
|
-
message: `The
|
|
4516
|
+
message: `The Thread Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
|
|
3849
4517
|
}));
|
|
3850
4518
|
return Effect.succeed(result);
|
|
3851
4519
|
}), Effect.withSpan("DoPortRouting.foreignLedgerCall", { attributes: {
|
|
@@ -3864,13 +4532,13 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3864
4532
|
const resolveForeignAdmission = (target, request) => transportCall(target, LedgerResolveAdmissionCall.make({ request })).pipe(Effect.flatMap((response) => {
|
|
3865
4533
|
if (response._tag === "PortFailed") {
|
|
3866
4534
|
if (response.failure._tag === "LedgerError") return Effect.fail(response.failure);
|
|
3867
|
-
return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The
|
|
4535
|
+
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
4536
|
}
|
|
3869
|
-
if (response.result._tag !== "LedgerResolveAdmissionResult") return Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The
|
|
4537
|
+
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
4538
|
return Effect.succeed(response.result.resolution);
|
|
3871
4539
|
}), 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
|
|
4540
|
+
PortTransportError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The Thread Object owning ${target} is unreachable: ${error.message}`) })),
|
|
4541
|
+
PortProtocolError: (error) => Effect.succeed(AdmissionIndeterminate.make({ reason: boundPortDiagnostic(`The answer of the Thread Object owning ${target} could not be understood: ${error.message}`) }))
|
|
3874
4542
|
}), Effect.withSpan("DoPortRouting.resolveForeignAdmission", { attributes: { target } }));
|
|
3875
4543
|
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
4544
|
/**
|
|
@@ -3889,7 +4557,7 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3889
4557
|
if (childSubmissionId === void 0 || attachments.has(childSubmissionId)) continue;
|
|
3890
4558
|
const target = yield* submissionTarget(operation, childSubmissionId);
|
|
3891
4559
|
if (target._tag !== "foreign") continue;
|
|
3892
|
-
const child = yield* foreignLookupById(operation, target.
|
|
4560
|
+
const child = yield* foreignLookupById(operation, target.threadId, childSubmissionId);
|
|
3893
4561
|
if (Option.isNone(child)) continue;
|
|
3894
4562
|
attachments.set(childSubmissionId, ChildAttachmentSnapshot.make({
|
|
3895
4563
|
toolCallId: reservation.parentToolCallId,
|
|
@@ -3913,44 +4581,45 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
3913
4581
|
});
|
|
3914
4582
|
const routed = SubmissionLedger.of({
|
|
3915
4583
|
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(
|
|
4584
|
+
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)),
|
|
4585
|
+
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))),
|
|
4586
|
+
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))),
|
|
4587
|
+
resolveAdmission: (request) => request.threadId === options.localThreadId ? local.resolveAdmission(request) : resolveForeignAdmission(request.threadId, request),
|
|
4588
|
+
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)))),
|
|
4589
|
+
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)))),
|
|
4590
|
+
claim: (request) => request.threadId === options.localThreadId ? local.claim(request) : Effect.fail(crossThreadLedgerError("ledger claim", request.threadId)),
|
|
4591
|
+
claimJoining: (request) => request.threadId === options.localThreadId ? local.claimJoining(request) : Effect.fail(crossThreadLedgerError("ledger claim joining", request.threadId)),
|
|
4592
|
+
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)))),
|
|
4593
|
+
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)))),
|
|
4594
|
+
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)))),
|
|
4595
|
+
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)))),
|
|
4596
|
+
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)))),
|
|
4597
|
+
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)))),
|
|
4598
|
+
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)))),
|
|
4599
|
+
suspend: (request) => submissionTarget("ledger suspend", request.submissionId).pipe(Effect.flatMap((target) => target._tag === "local" ? local.suspend(request) : Effect.fail(crossThreadLedgerError("ledger suspend", target.threadId)))),
|
|
4600
|
+
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)))),
|
|
4601
|
+
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)))),
|
|
4602
|
+
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)))),
|
|
4603
|
+
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
4604
|
attachChildToReservation: local.attachChildToReservation,
|
|
3937
4605
|
beginChildBudgetRelease: local.beginChildBudgetRelease,
|
|
3938
4606
|
releaseChildBudget: local.releaseChildBudget,
|
|
3939
4607
|
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(
|
|
4608
|
+
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
4609
|
});
|
|
3942
4610
|
return Context.make(SubmissionLedger, routed);
|
|
3943
4611
|
});
|
|
3944
4612
|
const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices")(function* (options) {
|
|
3945
|
-
const local = yield*
|
|
3946
|
-
const
|
|
4613
|
+
const local = yield* ThreadStore;
|
|
4614
|
+
const checkpoints = local.checkpoints;
|
|
4615
|
+
const transport = yield* ThreadPortTransport;
|
|
3947
4616
|
const transportCall = makeTransportCall(transport);
|
|
3948
|
-
const routeFailure = (operation, target) => (error) =>
|
|
4617
|
+
const routeFailure = (operation, target) => (error) => ThreadStoreError.make({
|
|
3949
4618
|
operation,
|
|
3950
|
-
message: boundPortDiagnostic(`Routed ${operation} to the
|
|
4619
|
+
message: boundPortDiagnostic(`Routed ${operation} to the Thread Object owning ${target} failed: ${error.message}`),
|
|
3951
4620
|
cause: error
|
|
3952
4621
|
});
|
|
3953
|
-
/** The store twin of `foreignLedgerCall` with `
|
|
4622
|
+
/** The store twin of `foreignLedgerCall` with `ThreadStoreError` as the base error. */
|
|
3954
4623
|
const foreignStoreCall = (operation, target, call, resultSchema, failureSchema) => {
|
|
3955
4624
|
const isExpectedResult = Schema.is(resultSchema);
|
|
3956
4625
|
const isExpectedFailure = Schema.is(failureSchema);
|
|
@@ -3958,17 +4627,17 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
|
|
|
3958
4627
|
if (response._tag === "PortFailed") {
|
|
3959
4628
|
const failure = response.failure;
|
|
3960
4629
|
if (isExpectedFailure(failure)) return Effect.fail(failure);
|
|
3961
|
-
if (failure._tag === "
|
|
3962
|
-
return Effect.fail(
|
|
4630
|
+
if (failure._tag === "ThreadStoreError") return Effect.fail(failure);
|
|
4631
|
+
return Effect.fail(ThreadStoreError.make({
|
|
3963
4632
|
operation,
|
|
3964
|
-
message: boundPortDiagnostic(`The
|
|
4633
|
+
message: boundPortDiagnostic(`The Thread Object owning ${target} answered ${operation} with the out-of-contract failure ${failure._tag}: ${failure.message}`),
|
|
3965
4634
|
cause: failure
|
|
3966
4635
|
}));
|
|
3967
4636
|
}
|
|
3968
4637
|
const result = response.result;
|
|
3969
|
-
if (!isExpectedResult(result)) return Effect.fail(
|
|
4638
|
+
if (!isExpectedResult(result)) return Effect.fail(ThreadStoreError.make({
|
|
3970
4639
|
operation,
|
|
3971
|
-
message: `The
|
|
4640
|
+
message: `The Thread Object owning ${target} answered ${operation} with the mismatched result ${result._tag}.`
|
|
3972
4641
|
}));
|
|
3973
4642
|
return Effect.succeed(result);
|
|
3974
4643
|
}), Effect.withSpan("DoPortRouting.foreignStoreCall", { attributes: {
|
|
@@ -3976,39 +4645,41 @@ const makeRoutedStoreServices = Effect.fn("DoPortRouting.makeRoutedStoreServices
|
|
|
3976
4645
|
target
|
|
3977
4646
|
} }));
|
|
3978
4647
|
};
|
|
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
|
-
|
|
4648
|
+
const routed = ThreadStore.of({
|
|
4649
|
+
materialize: (request) => request.threadId === options.localThreadId ? local.materialize(request) : foreignStoreCall("thread materialize", request.threadId, StoreMaterializeCall.make({ request }), StoreMaterializeResult, FenceRejected).pipe(Effect.asVoid),
|
|
4650
|
+
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)),
|
|
4651
|
+
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)))),
|
|
4652
|
+
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)),
|
|
4653
|
+
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)),
|
|
4654
|
+
observe: (request) => request.threadId === options.localThreadId ? local.observe(request) : Stream.unwrap(Effect.fail(crossThreadStoreError("thread observe", request.threadId))),
|
|
4655
|
+
...checkpoints === void 0 ? {} : { checkpoints: {
|
|
4656
|
+
save: (request) => request.checkpoint.threadId === options.localThreadId ? checkpoints.save(request) : Effect.fail(crossThreadStoreError("thread save checkpoint", request.checkpoint.threadId)),
|
|
4657
|
+
load: (request) => request.threadId === options.localThreadId ? checkpoints.load(request) : Effect.fail(crossThreadStoreError("thread load checkpoint", request.threadId))
|
|
4658
|
+
} }
|
|
4659
|
+
});
|
|
4660
|
+
return Context.make(ThreadStore, routed);
|
|
3990
4661
|
});
|
|
3991
4662
|
/**
|
|
3992
4663
|
* Routing decorator over the LOCAL `SubmissionLedger` facet (plan §1.3): a request addressing
|
|
3993
|
-
* this Object's
|
|
3994
|
-
*
|
|
4664
|
+
* this Object's Thread executes locally; a route-capable request addressing another
|
|
4665
|
+
* Thread is Schema-encoded onto the `ThreadPortTransport` and executed by the
|
|
3995
4666
|
* owning Object's local facet; any other foreign request fails fast typed. Provide the WP1
|
|
3996
4667
|
* local facet (`submissionLedgerLayer`/`ledgerLayer`) and a transport to close it.
|
|
3997
4668
|
*/
|
|
3998
4669
|
const routedSubmissionLedgerLayer = (options) => Layer.effectContext(makeRoutedLedgerServices(options));
|
|
3999
4670
|
/**
|
|
4000
|
-
* Routing decorator over the LOCAL `
|
|
4671
|
+
* Routing decorator over the LOCAL `ThreadStore` facet (plan §1.3): this-thread
|
|
4001
4672
|
* requests execute locally; foreign materialize/append/read/inspectTail/export travel the
|
|
4002
4673
|
* transport; foreign observation and checkpoints fail fast typed.
|
|
4003
4674
|
*/
|
|
4004
|
-
const
|
|
4675
|
+
const routedThreadStoreLayer = (options) => Layer.effectContext(makeRoutedStoreServices(options));
|
|
4005
4676
|
/** Fold one port operation's typed failures into the uniform response envelope. */
|
|
4006
4677
|
const capture = (effect) => effect.pipe(Effect.map((result) => PortSucceeded.make({ result })), Effect.catch((failure) => Effect.succeed(PortFailed.make({ failure }))));
|
|
4007
4678
|
/**
|
|
4008
4679
|
* Execute one decoded port request against THIS Object's LOCAL facets — the owner-side half
|
|
4009
4680
|
* of the routed ports (plan §1.3). Callers must provide the WP1 local facets, never the
|
|
4010
4681
|
* routed decorators: the routing layer already established that this Object owns the
|
|
4011
|
-
* addressed
|
|
4682
|
+
* addressed Thread, and re-routing here could bounce a request between Objects.
|
|
4012
4683
|
* Failures never escape — every typed port failure becomes a `PortFailed` envelope that
|
|
4013
4684
|
* re-decodes on the caller side.
|
|
4014
4685
|
*/
|
|
@@ -4039,24 +4710,24 @@ const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(functio
|
|
|
4039
4710
|
return yield* capture(ledger.recordChildSettled(request.request).pipe(Effect.map((outcome) => LedgerRecordChildSettledResult.make({ outcome }))));
|
|
4040
4711
|
}
|
|
4041
4712
|
case "StoreMaterialize": {
|
|
4042
|
-
const store = yield*
|
|
4713
|
+
const store = yield* ThreadStore;
|
|
4043
4714
|
return yield* capture(store.materialize(request.request).pipe(Effect.map(() => StoreMaterializeResult.make({}))));
|
|
4044
4715
|
}
|
|
4045
4716
|
case "StoreAppend": {
|
|
4046
|
-
const store = yield*
|
|
4717
|
+
const store = yield* ThreadStore;
|
|
4047
4718
|
return yield* capture(store.append(request.request).pipe(Effect.map((result) => StoreAppendResult.make({ result }))));
|
|
4048
4719
|
}
|
|
4049
4720
|
case "StoreReadPage": {
|
|
4050
|
-
const store = yield*
|
|
4721
|
+
const store = yield* ThreadStore;
|
|
4051
4722
|
return yield* capture(store.read(request.request).pipe(Stream.runCollect, Effect.map((records) => StoreReadPageResult.make({ records: [...records] }))));
|
|
4052
4723
|
}
|
|
4053
4724
|
case "StoreInspectTail": {
|
|
4054
|
-
const store = yield*
|
|
4725
|
+
const store = yield* ThreadStore;
|
|
4055
4726
|
return yield* capture(store.inspectTail(request.request).pipe(Effect.map((tail) => StoreInspectTailResult.make({ tail }))));
|
|
4056
4727
|
}
|
|
4057
4728
|
case "StoreExport": {
|
|
4058
|
-
const store = yield*
|
|
4059
|
-
return yield* capture(store.export(request.request).pipe(Effect.map((
|
|
4729
|
+
const store = yield* ThreadStore;
|
|
4730
|
+
return yield* capture(store.export(request.request).pipe(Effect.map((threadExport) => StoreExportResult.make({ export: threadExport }))));
|
|
4060
4731
|
}
|
|
4061
4732
|
}
|
|
4062
4733
|
});
|
|
@@ -4080,9 +4751,10 @@ const encodedProtocolFailure = (message) => ({
|
|
|
4080
4751
|
* transport never has to interpret exceptions as protocol answers.
|
|
4081
4752
|
*/
|
|
4082
4753
|
const handleEncodedPortRequest = Effect.fn("DoPortRouting.handleEncodedPortRequest")(function* (encoded) {
|
|
4083
|
-
|
|
4754
|
+
const response = 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}`) }) }))));
|
|
4755
|
+
return yield* encodePortResponse(response).pipe(Effect.catch((error) => Effect.succeed(encodedProtocolFailure(`The port response could not be encoded: ${error.message}`))));
|
|
4084
4756
|
});
|
|
4085
4757
|
//#endregion
|
|
4086
|
-
export {
|
|
4758
|
+
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
4759
|
|
|
4088
4760
|
//# sourceMappingURL=index.mjs.map
|