@effect-agent/storage-sqlite 0.1.0-beta.37 → 0.1.0-beta.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +17 -114
- package/dist/index.mjs +1036 -393
- package/dist/index.mjs.map +1 -1
- package/dist/sqlite-storage-failpoint-Cr0SXflP.d.mts +95 -0
- package/dist/sqlite-storage-failpoint-DIwWk5dw.mjs +12 -0
- package/dist/sqlite-storage-failpoint-DIwWk5dw.mjs.map +1 -0
- package/dist/testing.d.mts +15 -0
- package/dist/testing.mjs +19 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +12 -5
- package/src/errors.ts +3 -3
- package/src/index.ts +3 -2
- package/src/migrations.ts +105 -24
- package/src/sqlite-journal.ts +147 -156
- package/src/sqlite-ledger.ts +60 -60
- package/src/sqlite-schedule-store.ts +2 -2
- package/src/sqlite-storage-config.ts +1 -1
- package/src/sqlite-storage-failpoint-testing.ts +38 -0
- package/src/sqlite-storage-failpoint.ts +1 -31
- package/src/sqlite-subscription-store.ts +935 -0
- package/src/{sqlite-conversation-store.ts → sqlite-thread-store.ts} +248 -267
- package/src/testing.ts +2 -0
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as SqliteStorageFailpoint } from "./sqlite-storage-failpoint-DIwWk5dw.mjs";
|
|
2
|
+
import { AbortCommand, AbortIntent, AcceptedEvent, AdmissionAdmitted, AdmissionConflict, AdmissionNotAdmitted, AdmissionRequest, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecision, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalBatch, CanonicalRecord, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildReservationStatus, ChildSettledNotification, 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, 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, Exit, Layer, Option, Ref, Result, Schema, Stream } from "effect";
|
|
3
4
|
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
|
|
4
5
|
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
@@ -47,7 +48,7 @@ var SqliteAppendConflict = class extends Schema.TaggedError()("SqliteAppendConfl
|
|
|
47
48
|
actualTailDigest: Schema.optionalKey(Schema.String)
|
|
48
49
|
}) {};
|
|
49
50
|
/**
|
|
50
|
-
* A producer epoch does not match the
|
|
51
|
+
* A producer epoch does not match the Thread's current writer registration. Appends
|
|
51
52
|
* require the exact registered epoch, so both older and newer unregistered epochs are fenced;
|
|
52
53
|
* a newer epoch takes over by materializing first.
|
|
53
54
|
*/
|
|
@@ -76,7 +77,7 @@ const SqliteStorageFailpointLocation = Schema.Literals([
|
|
|
76
77
|
"append:after-record-insert",
|
|
77
78
|
"append:after-tail-update",
|
|
78
79
|
"append:after",
|
|
79
|
-
"export:after-
|
|
80
|
+
"export:after-thread-read",
|
|
80
81
|
"save-checkpoint:before",
|
|
81
82
|
"save-checkpoint:after",
|
|
82
83
|
"ledger:admit:before",
|
|
@@ -130,13 +131,13 @@ var SqliteStorageFailpointError = class extends Schema.TaggedError()("SqliteStor
|
|
|
130
131
|
};
|
|
131
132
|
//#endregion
|
|
132
133
|
//#region src/migrations.ts
|
|
133
|
-
const CurrentSqliteStorageVersion =
|
|
134
|
+
const CurrentSqliteStorageVersion = 7;
|
|
134
135
|
const sqliteMigrations = SqliteMigrator.fromRecord({
|
|
135
|
-
"
|
|
136
|
+
"1_current_persistent_thread_foundation": Effect.gen(function* () {
|
|
136
137
|
const sql = yield* SqlClient.SqlClient;
|
|
137
138
|
yield* sql`
|
|
138
|
-
CREATE TABLE
|
|
139
|
-
|
|
139
|
+
CREATE TABLE effect_agent_threads (
|
|
140
|
+
thread_id TEXT PRIMARY KEY NOT NULL,
|
|
140
141
|
created_at TEXT NOT NULL,
|
|
141
142
|
tail_sequence INTEGER NOT NULL,
|
|
142
143
|
tail_digest TEXT NOT NULL,
|
|
@@ -145,46 +146,46 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
|
|
|
145
146
|
`.withoutTransform;
|
|
146
147
|
yield* sql`
|
|
147
148
|
CREATE TABLE effect_agent_canonical_batches (
|
|
148
|
-
|
|
149
|
+
thread_id TEXT NOT NULL,
|
|
149
150
|
batch_id TEXT NOT NULL,
|
|
150
151
|
first_sequence INTEGER NOT NULL,
|
|
151
152
|
last_sequence INTEGER NOT NULL,
|
|
152
153
|
batch_digest TEXT NOT NULL,
|
|
153
154
|
tail_digest TEXT NOT NULL,
|
|
154
155
|
batch_json TEXT NOT NULL,
|
|
155
|
-
PRIMARY KEY (
|
|
156
|
-
FOREIGN KEY (
|
|
157
|
-
REFERENCES
|
|
156
|
+
PRIMARY KEY (thread_id, batch_id),
|
|
157
|
+
FOREIGN KEY (thread_id)
|
|
158
|
+
REFERENCES effect_agent_threads(thread_id)
|
|
158
159
|
ON DELETE RESTRICT
|
|
159
160
|
)
|
|
160
161
|
`.withoutTransform;
|
|
161
162
|
yield* sql`
|
|
162
163
|
CREATE TABLE effect_agent_canonical_records (
|
|
163
|
-
|
|
164
|
+
thread_id TEXT NOT NULL,
|
|
164
165
|
sequence INTEGER NOT NULL,
|
|
165
166
|
record_id TEXT NOT NULL,
|
|
166
167
|
batch_id TEXT NOT NULL,
|
|
167
168
|
record_json TEXT NOT NULL,
|
|
168
|
-
PRIMARY KEY (
|
|
169
|
-
UNIQUE (
|
|
170
|
-
FOREIGN KEY (
|
|
171
|
-
REFERENCES effect_agent_canonical_batches(
|
|
169
|
+
PRIMARY KEY (thread_id, sequence),
|
|
170
|
+
UNIQUE (thread_id, record_id),
|
|
171
|
+
FOREIGN KEY (thread_id, batch_id)
|
|
172
|
+
REFERENCES effect_agent_canonical_batches(thread_id, batch_id)
|
|
172
173
|
ON DELETE RESTRICT
|
|
173
174
|
)
|
|
174
175
|
`.withoutTransform;
|
|
175
176
|
yield* sql`
|
|
176
177
|
CREATE INDEX effect_agent_canonical_records_batch
|
|
177
|
-
ON effect_agent_canonical_records (
|
|
178
|
+
ON effect_agent_canonical_records (thread_id, batch_id, sequence)
|
|
178
179
|
`.withoutTransform;
|
|
179
180
|
yield* sql`
|
|
180
181
|
CREATE TABLE effect_agent_checkpoints (
|
|
181
|
-
|
|
182
|
+
thread_id TEXT NOT NULL,
|
|
182
183
|
through_sequence INTEGER NOT NULL,
|
|
183
184
|
tail_digest TEXT NOT NULL,
|
|
184
185
|
checkpoint_json TEXT NOT NULL,
|
|
185
|
-
PRIMARY KEY (
|
|
186
|
-
FOREIGN KEY (
|
|
187
|
-
REFERENCES
|
|
186
|
+
PRIMARY KEY (thread_id, through_sequence),
|
|
187
|
+
FOREIGN KEY (thread_id)
|
|
188
|
+
REFERENCES effect_agent_threads(thread_id)
|
|
188
189
|
ON DELETE RESTRICT
|
|
189
190
|
)
|
|
190
191
|
`.withoutTransform;
|
|
@@ -195,7 +196,7 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
|
|
|
195
196
|
yield* sql`
|
|
196
197
|
CREATE TABLE effect_agent_submissions (
|
|
197
198
|
submission_id TEXT PRIMARY KEY NOT NULL,
|
|
198
|
-
|
|
199
|
+
thread_id TEXT NOT NULL,
|
|
199
200
|
queue_sequence INTEGER NOT NULL,
|
|
200
201
|
principal TEXT NOT NULL,
|
|
201
202
|
idempotency_key TEXT NOT NULL,
|
|
@@ -211,8 +212,8 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
|
|
|
211
212
|
ready_at TEXT,
|
|
212
213
|
input_applied_record_id TEXT,
|
|
213
214
|
input_applied_sequence INTEGER,
|
|
214
|
-
UNIQUE (
|
|
215
|
-
UNIQUE (
|
|
215
|
+
UNIQUE (thread_id, principal, idempotency_key),
|
|
216
|
+
UNIQUE (thread_id, queue_sequence)
|
|
216
217
|
)
|
|
217
218
|
`.withoutTransform;
|
|
218
219
|
yield* sql`
|
|
@@ -232,7 +233,7 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
|
|
|
232
233
|
CREATE TABLE effect_agent_attempts (
|
|
233
234
|
attempt_id TEXT PRIMARY KEY NOT NULL,
|
|
234
235
|
submission_id TEXT NOT NULL,
|
|
235
|
-
|
|
236
|
+
thread_id TEXT NOT NULL,
|
|
236
237
|
owner_producer_id TEXT NOT NULL,
|
|
237
238
|
producer_epoch INTEGER NOT NULL,
|
|
238
239
|
claimed_at TEXT NOT NULL,
|
|
@@ -384,6 +385,80 @@ const sqliteMigrations = SqliteMigrator.fromRecord({
|
|
|
384
385
|
WHERE deadline_at_millis IS NOT NULL
|
|
385
386
|
`.withoutTransform;
|
|
386
387
|
yield* sql`PRAGMA user_version = 5`.withoutTransform;
|
|
388
|
+
}),
|
|
389
|
+
"6_durable_subscriptions": Effect.gen(function* () {
|
|
390
|
+
const sql = yield* SqlClient.SqlClient;
|
|
391
|
+
yield* sql`
|
|
392
|
+
CREATE TABLE effect_agent_subscription_sequences (
|
|
393
|
+
tenant_id TEXT NOT NULL,
|
|
394
|
+
source_address TEXT NOT NULL,
|
|
395
|
+
sequence INTEGER NOT NULL,
|
|
396
|
+
event_scan_cursor TEXT NOT NULL,
|
|
397
|
+
delivery_scan_cursor TEXT NOT NULL,
|
|
398
|
+
recovery_scan_cursor INTEGER NOT NULL,
|
|
399
|
+
PRIMARY KEY (tenant_id, source_address)
|
|
400
|
+
)
|
|
401
|
+
`.withoutTransform;
|
|
402
|
+
yield* sql`
|
|
403
|
+
CREATE TABLE effect_agent_subscriptions (
|
|
404
|
+
tenant_id TEXT NOT NULL,
|
|
405
|
+
source_address TEXT NOT NULL,
|
|
406
|
+
owner_id TEXT NOT NULL,
|
|
407
|
+
subscription_id TEXT NOT NULL,
|
|
408
|
+
ordinal INTEGER NOT NULL,
|
|
409
|
+
source_name TEXT NOT NULL,
|
|
410
|
+
source_version TEXT NOT NULL,
|
|
411
|
+
matching_key TEXT NOT NULL,
|
|
412
|
+
state TEXT NOT NULL,
|
|
413
|
+
expires_at_millis INTEGER NOT NULL,
|
|
414
|
+
recovery_at_millis INTEGER,
|
|
415
|
+
record_json TEXT NOT NULL,
|
|
416
|
+
PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id),
|
|
417
|
+
UNIQUE (tenant_id, source_address, ordinal)
|
|
418
|
+
)
|
|
419
|
+
`.withoutTransform;
|
|
420
|
+
yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`.withoutTransform;
|
|
421
|
+
yield* sql`CREATE INDEX effect_agent_subscriptions_candidates ON effect_agent_subscriptions (tenant_id, source_address, source_name, source_version, matching_key, ordinal)`.withoutTransform;
|
|
422
|
+
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;
|
|
423
|
+
yield* sql`
|
|
424
|
+
CREATE TABLE effect_agent_subscription_events (
|
|
425
|
+
tenant_id TEXT NOT NULL,
|
|
426
|
+
source_address TEXT NOT NULL,
|
|
427
|
+
event_id TEXT NOT NULL,
|
|
428
|
+
source_name TEXT NOT NULL,
|
|
429
|
+
source_version TEXT NOT NULL,
|
|
430
|
+
matching_key TEXT NOT NULL,
|
|
431
|
+
payload_digest TEXT NOT NULL,
|
|
432
|
+
cutoff INTEGER NOT NULL,
|
|
433
|
+
cursor INTEGER NOT NULL,
|
|
434
|
+
routing_complete INTEGER NOT NULL,
|
|
435
|
+
next_attempt_at_millis INTEGER NOT NULL,
|
|
436
|
+
record_json TEXT NOT NULL,
|
|
437
|
+
PRIMARY KEY (tenant_id, source_address, event_id)
|
|
438
|
+
)
|
|
439
|
+
`.withoutTransform;
|
|
440
|
+
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;
|
|
441
|
+
yield* sql`
|
|
442
|
+
CREATE TABLE effect_agent_subscription_deliveries (
|
|
443
|
+
tenant_id TEXT NOT NULL,
|
|
444
|
+
source_address TEXT NOT NULL,
|
|
445
|
+
owner_id TEXT NOT NULL,
|
|
446
|
+
subscription_id TEXT NOT NULL,
|
|
447
|
+
event_id TEXT NOT NULL,
|
|
448
|
+
delivery_key TEXT NOT NULL,
|
|
449
|
+
state TEXT NOT NULL,
|
|
450
|
+
next_attempt_at_millis INTEGER NOT NULL,
|
|
451
|
+
record_json TEXT NOT NULL,
|
|
452
|
+
PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id, event_id),
|
|
453
|
+
UNIQUE (tenant_id, source_address, delivery_key)
|
|
454
|
+
)
|
|
455
|
+
`.withoutTransform;
|
|
456
|
+
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;
|
|
457
|
+
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;
|
|
458
|
+
yield* sql`PRAGMA user_version = 6`.withoutTransform;
|
|
459
|
+
}),
|
|
460
|
+
"7_thread_terminology_cutover": Effect.gen(function* () {
|
|
461
|
+
yield* (yield* SqlClient.SqlClient)`PRAGMA user_version = 7`.withoutTransform;
|
|
387
462
|
})
|
|
388
463
|
});
|
|
389
464
|
//#endregion
|
|
@@ -404,7 +479,7 @@ var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage
|
|
|
404
479
|
* Submission ownership lease duration in milliseconds (D5). The lease is a liveness hint
|
|
405
480
|
* that makes an abandoned claim reclaimable; correctness never depends on it because every
|
|
406
481
|
* canonical append is fenced by producer epoch. Convenience layers default this to
|
|
407
|
-
* `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/
|
|
482
|
+
* `DEFAULT_OWNERSHIP_LEASE_DURATION` from `@effect-agent/thread`.
|
|
408
483
|
*/
|
|
409
484
|
ownershipLeaseDuration: OwnershipLeaseMillis,
|
|
410
485
|
/**
|
|
@@ -417,37 +492,19 @@ var SqliteStorageConfigValue = class extends Schema.Class("@effect-agent/storage
|
|
|
417
492
|
/** Explicit SQLite storage configuration authority. */
|
|
418
493
|
var SqliteStorageConfig = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageConfig") {};
|
|
419
494
|
//#endregion
|
|
420
|
-
//#region src/sqlite-storage-failpoint.ts
|
|
421
|
-
const noFailpoint = () => Effect.void;
|
|
422
|
-
/** Test-only control for replacing the active SQLite failpoint handler. */
|
|
423
|
-
var SqliteStorageFailpointTestControl = class extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpointTestControl") {};
|
|
424
|
-
/** Explicit fault-injection authority used at SQLite operation boundaries. */
|
|
425
|
-
var SqliteStorageFailpoint = class SqliteStorageFailpoint extends Context.Service()("@effect-agent/storage-sqlite/SqliteStorageFailpoint") {
|
|
426
|
-
/** Production default: no fault injection. */
|
|
427
|
-
static layer = Layer.succeed(this)({ hit: noFailpoint });
|
|
428
|
-
/** Reusable test Layer with a control service backed by the same handler Ref. */
|
|
429
|
-
static layerTest = Layer.effectContext(Effect.gen(function* () {
|
|
430
|
-
const handler = yield* Ref.make(noFailpoint);
|
|
431
|
-
return Context.make(SqliteStorageFailpoint, SqliteStorageFailpoint.of({ hit: (location) => Ref.get(handler).pipe(Effect.flatMap((current) => current(location))) })).pipe(Context.add(SqliteStorageFailpointTestControl, SqliteStorageFailpointTestControl.of({
|
|
432
|
-
clear: Ref.set(handler, noFailpoint),
|
|
433
|
-
setHandler: (next) => Ref.set(handler, next)
|
|
434
|
-
})));
|
|
435
|
-
}));
|
|
436
|
-
};
|
|
437
|
-
//#endregion
|
|
438
495
|
//#region src/sqlite-journal.ts
|
|
439
496
|
const BoundedStoredText$1 = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
|
|
440
497
|
const BoundedIdentifier$1 = Schema.NonEmptyString.check(Schema.isMaxLength(1024));
|
|
441
498
|
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
442
|
-
const
|
|
499
|
+
const MAX_RECORDS_PER_THREAD = 65536;
|
|
443
500
|
const MAX_STORED_TEXT_BYTES = 16 * 1024 * 1024;
|
|
444
501
|
const MAX_IDENTIFIER_LENGTH = 1024;
|
|
445
502
|
const storedTextBytes = (value) => new TextEncoder().encode(value).byteLength;
|
|
446
503
|
var SqliteVersionRow = class extends Schema.Class("SqliteVersionRow")({ user_version: NonNegativeInt }) {};
|
|
447
504
|
var SqliteJournalModeRow = class extends Schema.Class("SqliteJournalModeRow")({ journal_mode: Schema.NonEmptyString.check(Schema.isMaxLength(32)) }) {};
|
|
448
505
|
var SqliteNameRow = class extends Schema.Class("SqliteNameRow")({ name: BoundedIdentifier$1 }) {};
|
|
449
|
-
var
|
|
450
|
-
|
|
506
|
+
var ThreadRow = class extends Schema.Class("ThreadRow")({
|
|
507
|
+
thread_id: BoundedIdentifier$1,
|
|
451
508
|
created_at: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
|
|
452
509
|
producer_epoch: ProducerEpoch,
|
|
453
510
|
tail_digest: BoundedStoredText$1,
|
|
@@ -457,21 +514,21 @@ var BatchRow = class extends Schema.Class("BatchRow")({
|
|
|
457
514
|
batch_digest: BoundedStoredText$1,
|
|
458
515
|
batch_id: BoundedIdentifier$1,
|
|
459
516
|
batch_json: BoundedStoredText$1,
|
|
460
|
-
|
|
517
|
+
thread_id: BoundedIdentifier$1,
|
|
461
518
|
first_sequence: CanonicalSequence,
|
|
462
519
|
last_sequence: CanonicalSequence,
|
|
463
520
|
tail_digest: BoundedStoredText$1
|
|
464
521
|
}) {};
|
|
465
522
|
var RecordRow = class extends Schema.Class("RecordRow")({
|
|
466
523
|
batch_id: BoundedIdentifier$1,
|
|
467
|
-
|
|
524
|
+
thread_id: BoundedIdentifier$1,
|
|
468
525
|
record_id: BoundedIdentifier$1,
|
|
469
526
|
record_json: BoundedStoredText$1,
|
|
470
527
|
sequence: CanonicalSequence
|
|
471
528
|
}) {};
|
|
472
529
|
var CheckpointRow = class extends Schema.Class("CheckpointRow")({
|
|
473
530
|
checkpoint_json: BoundedStoredText$1,
|
|
474
|
-
|
|
531
|
+
thread_id: BoundedIdentifier$1,
|
|
475
532
|
tail_digest: BoundedStoredText$1,
|
|
476
533
|
through_sequence: CanonicalSequence
|
|
477
534
|
}) {};
|
|
@@ -483,7 +540,7 @@ var RawAppendRequest = class extends Schema.Class("@effect-agent/storage-sqlite/
|
|
|
483
540
|
batchDigest: BoundedStoredText$1,
|
|
484
541
|
batchId: BoundedIdentifier$1,
|
|
485
542
|
batchJson: BoundedStoredText$1,
|
|
486
|
-
|
|
543
|
+
threadId: BoundedIdentifier$1,
|
|
487
544
|
expectedTailDigest: BoundedStoredText$1,
|
|
488
545
|
expectedTailSequence: CanonicalSequence,
|
|
489
546
|
producerEpoch: ProducerEpoch,
|
|
@@ -497,20 +554,20 @@ var RawAppendResult = class extends Schema.Class("@effect-agent/storage-sqlite/R
|
|
|
497
554
|
tailDigest: BoundedStoredText$1
|
|
498
555
|
}) {};
|
|
499
556
|
var RawReadRequest = class extends Schema.Class("@effect-agent/storage-sqlite/RawReadRequest")({
|
|
500
|
-
|
|
557
|
+
threadId: BoundedIdentifier$1,
|
|
501
558
|
fromSequenceExclusive: CanonicalSequence,
|
|
502
559
|
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(1024))
|
|
503
560
|
}) {};
|
|
504
561
|
var RawCheckpoint = class extends Schema.Class("@effect-agent/storage-sqlite/RawCheckpoint")({
|
|
505
562
|
checkpointJson: BoundedStoredText$1,
|
|
506
|
-
|
|
563
|
+
threadId: BoundedIdentifier$1,
|
|
507
564
|
tailDigest: BoundedStoredText$1,
|
|
508
565
|
throughSequence: CanonicalSequence
|
|
509
566
|
}) {};
|
|
510
|
-
var
|
|
567
|
+
var RawThreadExport = class extends Schema.Class("@effect-agent/storage-sqlite/RawThreadExport")({
|
|
511
568
|
batches: Schema.Array(BatchRow),
|
|
512
569
|
checkpoints: Schema.Array(CheckpointRow),
|
|
513
|
-
|
|
570
|
+
thread: ThreadRow,
|
|
514
571
|
records: Schema.Array(RecordRow)
|
|
515
572
|
}) {};
|
|
516
573
|
const storageError = (operation) => (error) => SqliteStorageError.make({
|
|
@@ -519,13 +576,13 @@ const storageError = (operation) => (error) => SqliteStorageError.make({
|
|
|
519
576
|
message: error.message
|
|
520
577
|
});
|
|
521
578
|
/** Decode raw SQLite rows against a Schema, reporting failures as typed corruption. */
|
|
522
|
-
const decodeRows$
|
|
579
|
+
const decodeRows$2 = Effect.fn("SqliteJournal.decodeRows")((schema, table, rowKey, rows) => Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError((error) => SqliteStorageCorruptionError.make({
|
|
523
580
|
table,
|
|
524
581
|
rowKey,
|
|
525
582
|
message: String(error)
|
|
526
583
|
}))));
|
|
527
584
|
/** Decode exactly one raw SQLite row against a Schema, reporting failures as typed corruption. */
|
|
528
|
-
const decodeSingleRow = Effect.fn("SqliteJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows$
|
|
585
|
+
const decodeSingleRow = Effect.fn("SqliteJournal.decodeSingleRow")((schema, table, rowKey, rows) => decodeRows$2(schema, table, rowKey, rows).pipe(Effect.flatMap((decoded) => decoded.length === 1 ? Effect.succeed(decoded[0]) : Effect.fail(SqliteStorageCorruptionError.make({
|
|
529
586
|
table,
|
|
530
587
|
rowKey,
|
|
531
588
|
message: `Expected exactly one row but found ${decoded.length}.`
|
|
@@ -540,15 +597,15 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
540
597
|
const journalMode = yield* decodeSingleRow(Schema.Array(SqliteJournalModeRow), "pragma_journal_mode", "singleton", journalModeRows);
|
|
541
598
|
if (journalMode.journal_mode.toLowerCase() !== "wal") return yield* SqliteStorageCompatibilityError.make({
|
|
542
599
|
actualVersion: 0,
|
|
543
|
-
supportedVersion:
|
|
600
|
+
supportedVersion: 7,
|
|
544
601
|
message: `SQLite WAL mode is required; the database reported ${journalMode.journal_mode}.`
|
|
545
602
|
});
|
|
546
603
|
const versionRows = yield* sql`PRAGMA user_version`.pipe(Effect.mapError(storageError("read storage version")));
|
|
547
604
|
const version = yield* decodeSingleRow(Schema.Array(SqliteVersionRow), "pragma_user_version", "singleton", versionRows);
|
|
548
|
-
if (version.user_version !== 0 && version.user_version !==
|
|
605
|
+
if (version.user_version !== 0 && version.user_version !== 7) return yield* SqliteStorageCompatibilityError.make({
|
|
549
606
|
actualVersion: version.user_version,
|
|
550
|
-
supportedVersion:
|
|
551
|
-
message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version
|
|
607
|
+
supportedVersion: 7,
|
|
608
|
+
message: `The SQLite file uses private-development storage version ${version.user_version}; this build supports exactly version 7. Reset the database file explicitly; automatic stored-data migrations are not provided during private development.`
|
|
552
609
|
});
|
|
553
610
|
if (version.user_version === 0) {
|
|
554
611
|
const existingRows = yield* sql`
|
|
@@ -558,9 +615,9 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
558
615
|
AND name LIKE 'effect_agent_%'
|
|
559
616
|
ORDER BY name
|
|
560
617
|
`.pipe(Effect.mapError(storageError("inspect unversioned storage")));
|
|
561
|
-
if ((yield* decodeRows$
|
|
618
|
+
if ((yield* decodeRows$2(Schema.Array(SqliteNameRow), "sqlite_master", "effect_agent_%", existingRows)).length > 0) return yield* SqliteStorageCompatibilityError.make({
|
|
562
619
|
actualVersion: 0,
|
|
563
|
-
supportedVersion:
|
|
620
|
+
supportedVersion: 7,
|
|
564
621
|
message: "The SQLite file contains unversioned Effect Agent tables. Reset it explicitly; refusing to mutate ambiguous stored data."
|
|
565
622
|
});
|
|
566
623
|
yield* SqliteMigrator.run({ loader: sqliteMigrations }).pipe(Effect.mapError((error) => SqliteStorageError.make({
|
|
@@ -574,7 +631,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
574
631
|
FROM sqlite_master
|
|
575
632
|
WHERE type = 'table'
|
|
576
633
|
AND name IN (
|
|
577
|
-
'
|
|
634
|
+
'effect_agent_threads',
|
|
578
635
|
'effect_agent_canonical_batches',
|
|
579
636
|
'effect_agent_canonical_records',
|
|
580
637
|
'effect_agent_checkpoints',
|
|
@@ -589,9 +646,9 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
589
646
|
)
|
|
590
647
|
ORDER BY name
|
|
591
648
|
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
592
|
-
if ((yield* decodeRows$
|
|
593
|
-
actualVersion:
|
|
594
|
-
supportedVersion:
|
|
649
|
+
if ((yield* decodeRows$2(Schema.Array(SqliteNameRow), "sqlite_master", "required_tables", requiredRows)).length !== 12) return yield* SqliteStorageCompatibilityError.make({
|
|
650
|
+
actualVersion: 7,
|
|
651
|
+
supportedVersion: 7,
|
|
595
652
|
message: "The SQLite file claims the current format but is missing required tables. Reset the corrupt private-development data."
|
|
596
653
|
});
|
|
597
654
|
const classifyWriteFailure = (operation) => (error) => error.reason._tag === "LockTimeoutError" ? SqliteWriteContention.make({
|
|
@@ -641,44 +698,44 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
641
698
|
yield* Effect.orDie(connection.executeUnprepared("ROLLBACK", [], void 0));
|
|
642
699
|
return yield* exit;
|
|
643
700
|
})).pipe(Effect.withSpan("SqliteJournal.withReadTransaction", { attributes: { operation } })));
|
|
644
|
-
const materialize = Effect.fn("SqliteJournal.materialize")(function* (
|
|
645
|
-
if (
|
|
646
|
-
operation: "materialize
|
|
647
|
-
message: "
|
|
701
|
+
const materialize = Effect.fn("SqliteJournal.materialize")(function* (threadId, createdAt, emptyTailDigest, producerEpoch) {
|
|
702
|
+
if (threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(emptyTailDigest) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
|
|
703
|
+
operation: "materialize thread",
|
|
704
|
+
message: "Thread identity or initial digest exceeds the SQLite storage bounds."
|
|
648
705
|
});
|
|
649
706
|
yield* withWriteTransaction("materialize transaction")(Effect.gen(function* () {
|
|
650
707
|
const existingRows = yield* sql`
|
|
651
708
|
SELECT
|
|
652
|
-
|
|
709
|
+
thread_id,
|
|
653
710
|
created_at,
|
|
654
711
|
tail_sequence,
|
|
655
712
|
tail_digest,
|
|
656
713
|
producer_epoch
|
|
657
|
-
FROM
|
|
658
|
-
WHERE
|
|
659
|
-
`.pipe(Effect.mapError(storageError("read materialized
|
|
660
|
-
const existing = yield* decodeRows$
|
|
714
|
+
FROM effect_agent_threads
|
|
715
|
+
WHERE thread_id = ${threadId}
|
|
716
|
+
`.pipe(Effect.mapError(storageError("read materialized thread")));
|
|
717
|
+
const existing = yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, existingRows);
|
|
661
718
|
if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
|
|
662
|
-
table: "
|
|
663
|
-
rowKey:
|
|
664
|
-
message: "A
|
|
719
|
+
table: "effect_agent_threads",
|
|
720
|
+
rowKey: threadId,
|
|
721
|
+
message: "A thread primary key returned more than one row."
|
|
665
722
|
});
|
|
666
723
|
if (existing.length === 0) {
|
|
667
724
|
yield* sql`
|
|
668
|
-
INSERT INTO
|
|
669
|
-
|
|
725
|
+
INSERT INTO effect_agent_threads (
|
|
726
|
+
thread_id,
|
|
670
727
|
created_at,
|
|
671
728
|
tail_sequence,
|
|
672
729
|
tail_digest,
|
|
673
730
|
producer_epoch
|
|
674
731
|
) VALUES (
|
|
675
|
-
${
|
|
732
|
+
${threadId},
|
|
676
733
|
${createdAt},
|
|
677
734
|
0,
|
|
678
735
|
${emptyTailDigest},
|
|
679
736
|
${producerEpoch}
|
|
680
737
|
)
|
|
681
|
-
`.pipe(Effect.mapError(storageError("materialize
|
|
738
|
+
`.pipe(Effect.mapError(storageError("materialize thread")));
|
|
682
739
|
return;
|
|
683
740
|
}
|
|
684
741
|
if (producerEpoch < existing[0].producer_epoch) return yield* SqliteFenceRejected.make({
|
|
@@ -687,27 +744,27 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
687
744
|
message: `Producer epoch ${producerEpoch} is stale; current epoch is ${existing[0].producer_epoch}.`
|
|
688
745
|
});
|
|
689
746
|
if (producerEpoch > existing[0].producer_epoch) yield* sql`
|
|
690
|
-
UPDATE
|
|
747
|
+
UPDATE effect_agent_threads
|
|
691
748
|
SET producer_epoch = ${producerEpoch}
|
|
692
|
-
WHERE
|
|
749
|
+
WHERE thread_id = ${threadId}
|
|
693
750
|
`.pipe(Effect.mapError(storageError("advance materialization epoch")));
|
|
694
751
|
}));
|
|
695
752
|
});
|
|
696
|
-
const
|
|
753
|
+
const getThread = Effect.fn("SqliteJournal.getThread")(function* (threadId) {
|
|
697
754
|
const rows = yield* sql`
|
|
698
755
|
SELECT
|
|
699
|
-
|
|
756
|
+
thread_id,
|
|
700
757
|
created_at,
|
|
701
758
|
tail_sequence,
|
|
702
759
|
tail_digest,
|
|
703
760
|
producer_epoch
|
|
704
|
-
FROM
|
|
705
|
-
WHERE
|
|
706
|
-
`.pipe(Effect.mapError(storageError("read
|
|
707
|
-
return yield* decodeRows$
|
|
761
|
+
FROM effect_agent_threads
|
|
762
|
+
WHERE thread_id = ${threadId}
|
|
763
|
+
`.pipe(Effect.mapError(storageError("read thread")));
|
|
764
|
+
return yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", threadId, rows);
|
|
708
765
|
});
|
|
709
766
|
const append = Effect.fn("SqliteJournal.append")(function* (request) {
|
|
710
|
-
if (request.
|
|
767
|
+
if (request.threadId.length > MAX_IDENTIFIER_LENGTH || request.batchId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(request.batchJson) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.batchDigest) > MAX_STORED_TEXT_BYTES || storedTextBytes(request.tailDigest) > MAX_STORED_TEXT_BYTES || request.records.some((record) => record.recordId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(record.recordJson) > MAX_STORED_TEXT_BYTES)) return yield* SqliteStorageError.make({
|
|
711
768
|
operation: "append canonical batch",
|
|
712
769
|
message: "Canonical identifiers or encoded JSON exceed the SQLite storage bounds."
|
|
713
770
|
});
|
|
@@ -717,25 +774,25 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
717
774
|
message: `Batch ${request.batchId} contains duplicate canonical record IDs.`,
|
|
718
775
|
reason: "record-identity"
|
|
719
776
|
});
|
|
720
|
-
const
|
|
777
|
+
const threadRows = yield* sql`
|
|
721
778
|
SELECT
|
|
722
|
-
|
|
779
|
+
thread_id,
|
|
723
780
|
created_at,
|
|
724
781
|
tail_sequence,
|
|
725
782
|
tail_digest,
|
|
726
783
|
producer_epoch
|
|
727
|
-
FROM
|
|
728
|
-
WHERE
|
|
784
|
+
FROM effect_agent_threads
|
|
785
|
+
WHERE thread_id = ${request.threadId}
|
|
729
786
|
`.pipe(Effect.mapError(storageError("read append tail")));
|
|
730
|
-
const
|
|
731
|
-
if (request.producerEpoch !==
|
|
787
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", request.threadId, threadRows);
|
|
788
|
+
if (request.producerEpoch !== thread.producer_epoch) return yield* SqliteFenceRejected.make({
|
|
732
789
|
producerEpoch: request.producerEpoch,
|
|
733
|
-
actualEpoch:
|
|
734
|
-
message: `Producer epoch ${request.producerEpoch} is not the current epoch ${
|
|
790
|
+
actualEpoch: thread.producer_epoch,
|
|
791
|
+
message: `Producer epoch ${request.producerEpoch} is not the current epoch ${thread.producer_epoch}.`
|
|
735
792
|
});
|
|
736
793
|
const batchRows = yield* sql`
|
|
737
794
|
SELECT
|
|
738
|
-
|
|
795
|
+
thread_id,
|
|
739
796
|
batch_id,
|
|
740
797
|
first_sequence,
|
|
741
798
|
last_sequence,
|
|
@@ -743,13 +800,13 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
743
800
|
tail_digest,
|
|
744
801
|
batch_json
|
|
745
802
|
FROM effect_agent_canonical_batches
|
|
746
|
-
WHERE
|
|
803
|
+
WHERE thread_id = ${request.threadId}
|
|
747
804
|
AND batch_id = ${request.batchId}
|
|
748
805
|
`.pipe(Effect.mapError(storageError("read idempotent batch")));
|
|
749
|
-
const batches = yield* decodeRows$
|
|
806
|
+
const batches = yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${request.threadId}/${request.batchId}`, batchRows);
|
|
750
807
|
if (batches.length > 1) return yield* SqliteStorageCorruptionError.make({
|
|
751
808
|
table: "effect_agent_canonical_batches",
|
|
752
|
-
rowKey: `${request.
|
|
809
|
+
rowKey: `${request.threadId}/${request.batchId}`,
|
|
753
810
|
message: "A canonical batch primary key returned more than one row."
|
|
754
811
|
});
|
|
755
812
|
if (batches.length === 1) {
|
|
@@ -765,34 +822,34 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
765
822
|
tailDigest: existing.tail_digest
|
|
766
823
|
});
|
|
767
824
|
}
|
|
768
|
-
if (request.expectedTailSequence !==
|
|
769
|
-
message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${
|
|
825
|
+
if (request.expectedTailSequence !== thread.tail_sequence || request.expectedTailDigest !== thread.tail_digest) return yield* SqliteAppendConflict.make({
|
|
826
|
+
message: `Expected tail ${request.expectedTailSequence}/${request.expectedTailDigest} but found ${thread.tail_sequence}/${thread.tail_digest}.`,
|
|
770
827
|
reason: "tail",
|
|
771
|
-
actualTailSequence:
|
|
772
|
-
actualTailDigest:
|
|
828
|
+
actualTailSequence: thread.tail_sequence,
|
|
829
|
+
actualTailDigest: thread.tail_digest
|
|
773
830
|
});
|
|
774
|
-
if (
|
|
831
|
+
if (thread.tail_sequence + request.records.length > MAX_RECORDS_PER_THREAD) return yield* SqliteStorageError.make({
|
|
775
832
|
operation: "append canonical batch",
|
|
776
|
-
message: `
|
|
833
|
+
message: `Thread record limit ${MAX_RECORDS_PER_THREAD} would be exceeded.`
|
|
777
834
|
});
|
|
778
835
|
const existingRecordRows = yield* sql`
|
|
779
836
|
SELECT
|
|
780
|
-
|
|
837
|
+
thread_id,
|
|
781
838
|
sequence,
|
|
782
839
|
record_id,
|
|
783
840
|
batch_id,
|
|
784
841
|
record_json
|
|
785
842
|
FROM effect_agent_canonical_records
|
|
786
|
-
WHERE
|
|
843
|
+
WHERE thread_id = ${request.threadId}
|
|
787
844
|
AND record_id IN ${sql.in(recordIds)}
|
|
788
845
|
ORDER BY sequence
|
|
789
846
|
`.pipe(Effect.mapError(storageError("check canonical record identities")));
|
|
790
|
-
const existingRecords = yield* decodeRows$
|
|
847
|
+
const existingRecords = yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}/record_ids`, existingRecordRows);
|
|
791
848
|
if (existingRecords.length > 0) return yield* SqliteAppendConflict.make({
|
|
792
849
|
message: `Canonical record ID ${existingRecords[0].record_id} already exists.`,
|
|
793
850
|
reason: "record-identity"
|
|
794
851
|
});
|
|
795
|
-
const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(
|
|
852
|
+
const firstSequence = yield* Schema.decodeUnknownEffect(CanonicalSequence)(thread.tail_sequence + 1).pipe(Effect.mapError((error) => SqliteStorageError.make({
|
|
796
853
|
cause: error,
|
|
797
854
|
operation: "append canonical batch",
|
|
798
855
|
message: error.message
|
|
@@ -804,7 +861,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
804
861
|
})));
|
|
805
862
|
yield* sql`
|
|
806
863
|
INSERT INTO effect_agent_canonical_batches (
|
|
807
|
-
|
|
864
|
+
thread_id,
|
|
808
865
|
batch_id,
|
|
809
866
|
first_sequence,
|
|
810
867
|
last_sequence,
|
|
@@ -812,7 +869,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
812
869
|
tail_digest,
|
|
813
870
|
batch_json
|
|
814
871
|
) VALUES (
|
|
815
|
-
${request.
|
|
872
|
+
${request.threadId},
|
|
816
873
|
${request.batchId},
|
|
817
874
|
${firstSequence},
|
|
818
875
|
${lastSequence},
|
|
@@ -825,13 +882,13 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
825
882
|
yield* Effect.forEach(request.records, (record, index) => Effect.gen(function* () {
|
|
826
883
|
yield* sql`
|
|
827
884
|
INSERT INTO effect_agent_canonical_records (
|
|
828
|
-
|
|
885
|
+
thread_id,
|
|
829
886
|
sequence,
|
|
830
887
|
record_id,
|
|
831
888
|
batch_id,
|
|
832
889
|
record_json
|
|
833
890
|
) VALUES (
|
|
834
|
-
${request.
|
|
891
|
+
${request.threadId},
|
|
835
892
|
${firstSequence + index},
|
|
836
893
|
${record.recordId},
|
|
837
894
|
${request.batchId},
|
|
@@ -841,13 +898,13 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
841
898
|
yield* failpoint("append:after-record-insert");
|
|
842
899
|
}), { discard: true });
|
|
843
900
|
yield* sql`
|
|
844
|
-
UPDATE
|
|
901
|
+
UPDATE effect_agent_threads
|
|
845
902
|
SET
|
|
846
903
|
tail_sequence = ${lastSequence},
|
|
847
904
|
tail_digest = ${request.tailDigest},
|
|
848
905
|
producer_epoch = ${request.producerEpoch}
|
|
849
|
-
WHERE
|
|
850
|
-
`.pipe(Effect.mapError(storageError("advance
|
|
906
|
+
WHERE thread_id = ${request.threadId}
|
|
907
|
+
`.pipe(Effect.mapError(storageError("advance thread tail")));
|
|
851
908
|
yield* failpoint("append:after-tail-update");
|
|
852
909
|
return RawAppendResult.make({
|
|
853
910
|
firstSequence,
|
|
@@ -860,36 +917,36 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
860
917
|
const read = Effect.fn("SqliteJournal.read")(function* (request) {
|
|
861
918
|
const rows = yield* sql`
|
|
862
919
|
SELECT
|
|
863
|
-
|
|
920
|
+
thread_id,
|
|
864
921
|
sequence,
|
|
865
922
|
record_id,
|
|
866
923
|
batch_id,
|
|
867
924
|
record_json
|
|
868
925
|
FROM effect_agent_canonical_records
|
|
869
|
-
WHERE
|
|
926
|
+
WHERE thread_id = ${request.threadId}
|
|
870
927
|
AND sequence > ${request.fromSequenceExclusive}
|
|
871
928
|
ORDER BY sequence
|
|
872
929
|
LIMIT ${request.limit}
|
|
873
930
|
`.pipe(Effect.mapError(storageError("read canonical records")));
|
|
874
|
-
return yield* decodeRows$
|
|
931
|
+
return yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", `${request.threadId}>${request.fromSequenceExclusive}`, rows);
|
|
875
932
|
});
|
|
876
|
-
const
|
|
933
|
+
const exportThread = Effect.fn("SqliteJournal.exportThread")(function* (threadId) {
|
|
877
934
|
return yield* withReadTransaction("export transaction")(Effect.gen(function* () {
|
|
878
|
-
const
|
|
935
|
+
const threadRows = yield* sql`
|
|
879
936
|
SELECT
|
|
880
|
-
|
|
937
|
+
thread_id,
|
|
881
938
|
created_at,
|
|
882
939
|
tail_sequence,
|
|
883
940
|
tail_digest,
|
|
884
941
|
producer_epoch
|
|
885
|
-
FROM
|
|
886
|
-
WHERE
|
|
887
|
-
`.pipe(Effect.mapError(storageError("export
|
|
888
|
-
const
|
|
889
|
-
yield* failpoint("export:after-
|
|
942
|
+
FROM effect_agent_threads
|
|
943
|
+
WHERE thread_id = ${threadId}
|
|
944
|
+
`.pipe(Effect.mapError(storageError("export thread")));
|
|
945
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", threadId, threadRows);
|
|
946
|
+
yield* failpoint("export:after-thread-read");
|
|
890
947
|
const batchRows = yield* sql`
|
|
891
948
|
SELECT
|
|
892
|
-
|
|
949
|
+
thread_id,
|
|
893
950
|
batch_id,
|
|
894
951
|
first_sequence,
|
|
895
952
|
last_sequence,
|
|
@@ -897,70 +954,70 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
897
954
|
tail_digest,
|
|
898
955
|
batch_json
|
|
899
956
|
FROM effect_agent_canonical_batches
|
|
900
|
-
WHERE
|
|
957
|
+
WHERE thread_id = ${threadId}
|
|
901
958
|
ORDER BY first_sequence
|
|
902
959
|
`.pipe(Effect.mapError(storageError("export canonical batches")));
|
|
903
960
|
const recordRows = yield* sql`
|
|
904
961
|
SELECT
|
|
905
|
-
|
|
962
|
+
thread_id,
|
|
906
963
|
sequence,
|
|
907
964
|
record_id,
|
|
908
965
|
batch_id,
|
|
909
966
|
record_json
|
|
910
967
|
FROM effect_agent_canonical_records
|
|
911
|
-
WHERE
|
|
968
|
+
WHERE thread_id = ${threadId}
|
|
912
969
|
ORDER BY sequence
|
|
913
970
|
`.pipe(Effect.mapError(storageError("export canonical records")));
|
|
914
971
|
const checkpointRows = yield* sql`
|
|
915
972
|
SELECT
|
|
916
|
-
|
|
973
|
+
thread_id,
|
|
917
974
|
through_sequence,
|
|
918
975
|
tail_digest,
|
|
919
976
|
checkpoint_json
|
|
920
977
|
FROM effect_agent_checkpoints
|
|
921
|
-
WHERE
|
|
978
|
+
WHERE thread_id = ${threadId}
|
|
922
979
|
ORDER BY through_sequence
|
|
923
980
|
`.pipe(Effect.mapError(storageError("export checkpoints")));
|
|
924
|
-
return
|
|
925
|
-
|
|
926
|
-
batches: yield* decodeRows$
|
|
927
|
-
records: yield* decodeRows$
|
|
928
|
-
checkpoints: yield* decodeRows$
|
|
981
|
+
return RawThreadExport.make({
|
|
982
|
+
thread,
|
|
983
|
+
batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", threadId, batchRows),
|
|
984
|
+
records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", threadId, recordRows),
|
|
985
|
+
checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", threadId, checkpointRows)
|
|
929
986
|
});
|
|
930
987
|
}));
|
|
931
988
|
});
|
|
932
989
|
const saveCheckpoint = Effect.fn("SqliteJournal.saveCheckpoint")(function* (checkpoint) {
|
|
933
|
-
if (checkpoint.
|
|
990
|
+
if (checkpoint.threadId.length > MAX_IDENTIFIER_LENGTH || storedTextBytes(checkpoint.checkpointJson) > MAX_STORED_TEXT_BYTES) return yield* SqliteStorageError.make({
|
|
934
991
|
operation: "save checkpoint",
|
|
935
992
|
message: "Checkpoint identity or encoded JSON exceeds the SQLite storage bounds."
|
|
936
993
|
});
|
|
937
994
|
yield* withWriteTransaction("checkpoint transaction")(Effect.gen(function* () {
|
|
938
|
-
const
|
|
995
|
+
const threadRows = yield* sql`
|
|
939
996
|
SELECT
|
|
940
|
-
|
|
997
|
+
thread_id,
|
|
941
998
|
created_at,
|
|
942
999
|
tail_sequence,
|
|
943
1000
|
tail_digest,
|
|
944
1001
|
producer_epoch
|
|
945
|
-
FROM
|
|
946
|
-
WHERE
|
|
1002
|
+
FROM effect_agent_threads
|
|
1003
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
947
1004
|
`.pipe(Effect.mapError(storageError("read checkpoint tail")));
|
|
948
|
-
const
|
|
949
|
-
if (checkpoint.throughSequence >
|
|
1005
|
+
const thread = yield* decodeSingleRow(Schema.Array(ThreadRow), "effect_agent_threads", checkpoint.threadId, threadRows);
|
|
1006
|
+
if (checkpoint.throughSequence > thread.tail_sequence) return yield* SqliteCheckpointConflict.make({ message: `Checkpoint sequence ${checkpoint.throughSequence} is after canonical tail ${thread.tail_sequence}.` });
|
|
950
1007
|
const checkpointRows = yield* sql`
|
|
951
1008
|
SELECT
|
|
952
|
-
|
|
1009
|
+
thread_id,
|
|
953
1010
|
through_sequence,
|
|
954
1011
|
tail_digest,
|
|
955
1012
|
checkpoint_json
|
|
956
1013
|
FROM effect_agent_checkpoints
|
|
957
|
-
WHERE
|
|
1014
|
+
WHERE thread_id = ${checkpoint.threadId}
|
|
958
1015
|
AND through_sequence = ${checkpoint.throughSequence}
|
|
959
1016
|
`.pipe(Effect.mapError(storageError("read idempotent checkpoint")));
|
|
960
|
-
const existing = yield* decodeRows$
|
|
1017
|
+
const existing = yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${checkpoint.threadId}/${checkpoint.throughSequence}`, checkpointRows);
|
|
961
1018
|
if (existing.length > 1) return yield* SqliteStorageCorruptionError.make({
|
|
962
1019
|
table: "effect_agent_checkpoints",
|
|
963
|
-
rowKey: `${checkpoint.
|
|
1020
|
+
rowKey: `${checkpoint.threadId}/${checkpoint.throughSequence}`,
|
|
964
1021
|
message: "A checkpoint primary key returned more than one row."
|
|
965
1022
|
});
|
|
966
1023
|
if (existing.length === 1) {
|
|
@@ -969,12 +1026,12 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
969
1026
|
}
|
|
970
1027
|
yield* sql`
|
|
971
1028
|
INSERT INTO effect_agent_checkpoints (
|
|
972
|
-
|
|
1029
|
+
thread_id,
|
|
973
1030
|
through_sequence,
|
|
974
1031
|
tail_digest,
|
|
975
1032
|
checkpoint_json
|
|
976
1033
|
) VALUES (
|
|
977
|
-
${checkpoint.
|
|
1034
|
+
${checkpoint.threadId},
|
|
978
1035
|
${checkpoint.throughSequence},
|
|
979
1036
|
${checkpoint.tailDigest},
|
|
980
1037
|
${checkpoint.checkpointJson}
|
|
@@ -982,33 +1039,33 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
982
1039
|
`.pipe(Effect.mapError(storageError("insert checkpoint")));
|
|
983
1040
|
}));
|
|
984
1041
|
});
|
|
985
|
-
const loadCheckpoint = Effect.fn("SqliteJournal.loadCheckpoint")(function* (
|
|
1042
|
+
const loadCheckpoint = Effect.fn("SqliteJournal.loadCheckpoint")(function* (threadId, atOrBeforeSequence) {
|
|
986
1043
|
const rows = yield* sql`
|
|
987
1044
|
SELECT
|
|
988
|
-
|
|
1045
|
+
thread_id,
|
|
989
1046
|
through_sequence,
|
|
990
1047
|
tail_digest,
|
|
991
1048
|
checkpoint_json
|
|
992
1049
|
FROM effect_agent_checkpoints
|
|
993
|
-
WHERE
|
|
1050
|
+
WHERE thread_id = ${threadId}
|
|
994
1051
|
AND through_sequence <= ${atOrBeforeSequence}
|
|
995
1052
|
ORDER BY through_sequence DESC
|
|
996
1053
|
LIMIT 1
|
|
997
1054
|
`.pipe(Effect.mapError(storageError("load checkpoint")));
|
|
998
|
-
return yield* decodeRows$
|
|
1055
|
+
return yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", `${threadId}<=${atOrBeforeSequence}`, rows);
|
|
999
1056
|
});
|
|
1000
1057
|
return {
|
|
1001
1058
|
append,
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
getTailDigestAt: Effect.fn("SqliteJournal.getTailDigestAt")(function* (
|
|
1059
|
+
exportThread,
|
|
1060
|
+
getThread,
|
|
1061
|
+
getTailDigestAt: Effect.fn("SqliteJournal.getTailDigestAt")(function* (threadId, sequence) {
|
|
1005
1062
|
if (sequence === 0) {
|
|
1006
|
-
const
|
|
1007
|
-
return
|
|
1063
|
+
const threads = yield* getThread(threadId);
|
|
1064
|
+
return threads.length === 0 ? [] : [threads[0].tail_sequence === 0 ? threads[0].tail_digest : void 0].filter((value) => value !== void 0);
|
|
1008
1065
|
}
|
|
1009
1066
|
const rows = yield* sql`
|
|
1010
1067
|
SELECT
|
|
1011
|
-
|
|
1068
|
+
thread_id,
|
|
1012
1069
|
batch_id,
|
|
1013
1070
|
first_sequence,
|
|
1014
1071
|
last_sequence,
|
|
@@ -1016,10 +1073,10 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
1016
1073
|
tail_digest,
|
|
1017
1074
|
batch_json
|
|
1018
1075
|
FROM effect_agent_canonical_batches
|
|
1019
|
-
WHERE
|
|
1076
|
+
WHERE thread_id = ${threadId}
|
|
1020
1077
|
AND last_sequence = ${sequence}
|
|
1021
1078
|
`.pipe(Effect.mapError(storageError("read canonical digest at sequence")));
|
|
1022
|
-
return (yield* decodeRows$
|
|
1079
|
+
return (yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", `${threadId}/${sequence}`, rows)).map((batch) => batch.tail_digest);
|
|
1023
1080
|
}),
|
|
1024
1081
|
loadCheckpoint,
|
|
1025
1082
|
materialize,
|
|
@@ -1027,19 +1084,19 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
1027
1084
|
saveCheckpoint,
|
|
1028
1085
|
scanStoredPayloads: Effect.fn("SqliteJournal.scanStoredPayloads")(function* () {
|
|
1029
1086
|
return yield* withReadTransaction("startup scan transaction")(Effect.gen(function* () {
|
|
1030
|
-
const
|
|
1087
|
+
const threads = yield* sql`
|
|
1031
1088
|
SELECT
|
|
1032
|
-
|
|
1089
|
+
thread_id,
|
|
1033
1090
|
created_at,
|
|
1034
1091
|
tail_sequence,
|
|
1035
1092
|
tail_digest,
|
|
1036
1093
|
producer_epoch
|
|
1037
|
-
FROM
|
|
1038
|
-
ORDER BY
|
|
1039
|
-
`.pipe(Effect.mapError(storageError("scan
|
|
1094
|
+
FROM effect_agent_threads
|
|
1095
|
+
ORDER BY thread_id
|
|
1096
|
+
`.pipe(Effect.mapError(storageError("scan threads")));
|
|
1040
1097
|
const batches = yield* sql`
|
|
1041
1098
|
SELECT
|
|
1042
|
-
|
|
1099
|
+
thread_id,
|
|
1043
1100
|
batch_id,
|
|
1044
1101
|
first_sequence,
|
|
1045
1102
|
last_sequence,
|
|
@@ -1047,32 +1104,32 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
1047
1104
|
tail_digest,
|
|
1048
1105
|
batch_json
|
|
1049
1106
|
FROM effect_agent_canonical_batches
|
|
1050
|
-
ORDER BY
|
|
1107
|
+
ORDER BY thread_id, first_sequence
|
|
1051
1108
|
`.pipe(Effect.mapError(storageError("scan canonical batches")));
|
|
1052
1109
|
const records = yield* sql`
|
|
1053
1110
|
SELECT
|
|
1054
|
-
|
|
1111
|
+
thread_id,
|
|
1055
1112
|
sequence,
|
|
1056
1113
|
record_id,
|
|
1057
1114
|
batch_id,
|
|
1058
1115
|
record_json
|
|
1059
1116
|
FROM effect_agent_canonical_records
|
|
1060
|
-
ORDER BY
|
|
1117
|
+
ORDER BY thread_id, sequence
|
|
1061
1118
|
`.pipe(Effect.mapError(storageError("scan canonical records")));
|
|
1062
1119
|
const checkpoints = yield* sql`
|
|
1063
1120
|
SELECT
|
|
1064
|
-
|
|
1121
|
+
thread_id,
|
|
1065
1122
|
through_sequence,
|
|
1066
1123
|
tail_digest,
|
|
1067
1124
|
checkpoint_json
|
|
1068
1125
|
FROM effect_agent_checkpoints
|
|
1069
|
-
ORDER BY
|
|
1126
|
+
ORDER BY thread_id, through_sequence
|
|
1070
1127
|
`.pipe(Effect.mapError(storageError("scan checkpoints")));
|
|
1071
1128
|
return {
|
|
1072
|
-
|
|
1073
|
-
batches: yield* decodeRows$
|
|
1074
|
-
records: yield* decodeRows$
|
|
1075
|
-
checkpoints: yield* decodeRows$
|
|
1129
|
+
threads: yield* decodeRows$2(Schema.Array(ThreadRow), "effect_agent_threads", "startup_scan", threads),
|
|
1130
|
+
batches: yield* decodeRows$2(Schema.Array(BatchRow), "effect_agent_canonical_batches", "startup_scan", batches),
|
|
1131
|
+
records: yield* decodeRows$2(Schema.Array(RecordRow), "effect_agent_canonical_records", "startup_scan", records),
|
|
1132
|
+
checkpoints: yield* decodeRows$2(Schema.Array(CheckpointRow), "effect_agent_checkpoints", "startup_scan", checkpoints)
|
|
1076
1133
|
};
|
|
1077
1134
|
}));
|
|
1078
1135
|
}),
|
|
@@ -1080,7 +1137,7 @@ const initializeSqliteJournal = Effect.fn("SqliteJournal.initialize")(function*
|
|
|
1080
1137
|
};
|
|
1081
1138
|
});
|
|
1082
1139
|
//#endregion
|
|
1083
|
-
//#region src/sqlite-
|
|
1140
|
+
//#region src/sqlite-thread-store.ts
|
|
1084
1141
|
const OffsetText = Schema.String.check(Schema.isMaxLength(4 * 1024));
|
|
1085
1142
|
const SQLITE_OFFSET_PREFIX = "effect-agent-sqlite@1:";
|
|
1086
1143
|
const ZERO_CANONICAL_SEQUENCE = Schema.decodeSync(CanonicalSequence)(0);
|
|
@@ -1088,77 +1145,77 @@ const isDigest = Schema.is(Digest);
|
|
|
1088
1145
|
const isSqliteFenceRejected = Schema.is(SqliteFenceRejected);
|
|
1089
1146
|
const isSqliteAppendConflict = Schema.is(SqliteAppendConflict);
|
|
1090
1147
|
const isSqliteCheckpointConflict = Schema.is(SqliteCheckpointConflict);
|
|
1091
|
-
const storeError = (operation, error) =>
|
|
1148
|
+
const storeError = (operation, error) => ThreadStoreError.make({
|
|
1092
1149
|
cause: error,
|
|
1093
1150
|
operation,
|
|
1094
1151
|
message: error.message
|
|
1095
1152
|
});
|
|
1096
|
-
const schemaStoreError = (operation, error) =>
|
|
1153
|
+
const schemaStoreError = (operation, error) => ThreadStoreError.make({
|
|
1097
1154
|
cause: error,
|
|
1098
1155
|
operation,
|
|
1099
1156
|
message: error.message
|
|
1100
1157
|
});
|
|
1101
|
-
const makeOffset = Effect.fn("
|
|
1102
|
-
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${SQLITE_OFFSET_PREFIX}${encodeURIComponent(
|
|
1158
|
+
const makeOffset = Effect.fn("SqliteThreadStore.makeOffset")(function* (threadId, sequence) {
|
|
1159
|
+
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.flatMap((validatedSequence) => Schema.decodeUnknownEffect(ObservationOffset)(`${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:${validatedSequence}`)), Effect.mapError((error) => schemaStoreError("encode observation offset", error)));
|
|
1103
1160
|
});
|
|
1104
|
-
const parseOffset = Effect.fn("
|
|
1161
|
+
const parseOffset = Effect.fn("SqliteThreadStore.parseOffset")(function* (threadId, offset) {
|
|
1105
1162
|
if (offset === void 0) return ZERO_CANONICAL_SEQUENCE;
|
|
1106
1163
|
const text = yield* Schema.decodeUnknownEffect(OffsetText)(offset).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
|
|
1107
|
-
const
|
|
1108
|
-
if (!text.startsWith(
|
|
1164
|
+
const threadPrefix = `${SQLITE_OFFSET_PREFIX}${encodeURIComponent(threadId)}:`;
|
|
1165
|
+
if (!text.startsWith(threadPrefix)) return yield* ThreadStoreError.make({
|
|
1109
1166
|
operation: "decode observation offset",
|
|
1110
|
-
message: "The observation offset belongs to a different adapter, storage version, or
|
|
1167
|
+
message: "The observation offset belongs to a different adapter, storage version, or Thread."
|
|
1111
1168
|
});
|
|
1112
|
-
const sequenceText = text.slice(
|
|
1113
|
-
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield*
|
|
1169
|
+
const sequenceText = text.slice(threadPrefix.length);
|
|
1170
|
+
if (!/^(0|[1-9][0-9]*)$/.test(sequenceText)) return yield* ThreadStoreError.make({
|
|
1114
1171
|
operation: "decode observation offset",
|
|
1115
1172
|
message: "The observation offset is malformed."
|
|
1116
1173
|
});
|
|
1117
1174
|
return yield* Schema.decodeUnknownEffect(CanonicalSequence)(Number(sequenceText)).pipe(Effect.mapError((error) => schemaStoreError("decode observation offset", error)));
|
|
1118
1175
|
});
|
|
1119
|
-
const mapFence = (
|
|
1120
|
-
|
|
1176
|
+
const mapFence = (threadId, error) => FenceRejected.make({
|
|
1177
|
+
threadId,
|
|
1121
1178
|
actualEpoch: error.actualEpoch,
|
|
1122
1179
|
attemptedEpoch: error.producerEpoch
|
|
1123
1180
|
});
|
|
1124
|
-
const encodeCanonicalRecord = Effect.fn("
|
|
1181
|
+
const encodeCanonicalRecord = Effect.fn("SqliteThreadStore.encodeCanonicalRecord")(function* (record) {
|
|
1125
1182
|
return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalRecord))(record).pipe(Effect.mapError((error) => schemaStoreError("encode canonical record", error)));
|
|
1126
1183
|
});
|
|
1127
|
-
const encodeCanonicalBatch = Effect.fn("
|
|
1184
|
+
const encodeCanonicalBatch = Effect.fn("SqliteThreadStore.encodeCanonicalBatch")(function* (batch) {
|
|
1128
1185
|
return yield* Schema.encodeEffect(Schema.fromJsonString(CanonicalBatch))(batch).pipe(Effect.mapError((error) => schemaStoreError("encode canonical batch", error)));
|
|
1129
1186
|
});
|
|
1130
|
-
const encodeCheckpoint = Effect.fn("
|
|
1131
|
-
return yield* Schema.encodeEffect(Schema.fromJsonString(
|
|
1187
|
+
const encodeCheckpoint = Effect.fn("SqliteThreadStore.encodeCheckpoint")(function* (checkpoint) {
|
|
1188
|
+
return yield* Schema.encodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint).pipe(Effect.mapError((error) => schemaStoreError("encode checkpoint", error)));
|
|
1132
1189
|
});
|
|
1133
|
-
const decodeEnvelope = Effect.fn("
|
|
1134
|
-
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) =>
|
|
1190
|
+
const decodeEnvelope = Effect.fn("SqliteThreadStore.decodeEnvelope")(function* (row) {
|
|
1191
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(row.record_json).pipe(Effect.mapError((error) => ThreadStoreError.make({
|
|
1135
1192
|
operation: "decode canonical record",
|
|
1136
1193
|
message: error.message
|
|
1137
1194
|
})));
|
|
1138
|
-
const
|
|
1139
|
-
const offset = yield* makeOffset(
|
|
1195
|
+
const threadId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.threadId)(row.thread_id).pipe(Effect.mapError((error) => schemaStoreError("decode thread identity", error)));
|
|
1196
|
+
const offset = yield* makeOffset(threadId, row.sequence);
|
|
1140
1197
|
const batchId = yield* Schema.decodeUnknownEffect(CanonicalRecordEnvelope.fields.batchId)(row.batch_id).pipe(Effect.mapError((error) => schemaStoreError("decode batch identity", error)));
|
|
1141
1198
|
return CanonicalRecordEnvelope.make({
|
|
1142
|
-
|
|
1199
|
+
threadId,
|
|
1143
1200
|
batchId,
|
|
1144
1201
|
sequence: row.sequence,
|
|
1145
1202
|
offset,
|
|
1146
1203
|
record
|
|
1147
1204
|
});
|
|
1148
1205
|
});
|
|
1149
|
-
const decodeCheckpoint = Effect.fn("
|
|
1150
|
-
return yield* Schema.decodeEffect(Schema.fromJsonString(
|
|
1206
|
+
const decodeCheckpoint = Effect.fn("SqliteThreadStore.decodeCheckpoint")(function* (checkpointJson) {
|
|
1207
|
+
return yield* Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpointJson).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint", error)));
|
|
1151
1208
|
});
|
|
1152
|
-
const
|
|
1153
|
-
const rows = yield* journal.
|
|
1154
|
-
if (rows.length === 0) return yield*
|
|
1209
|
+
const requireThread = Effect.fn("SqliteThreadStore.requireThread")(function* (journal, threadId) {
|
|
1210
|
+
const rows = yield* journal.getThread(threadId).pipe(Effect.mapError((error) => storeError("read thread", error)));
|
|
1211
|
+
if (rows.length === 0) return yield* ThreadNotMaterialized.make({ threadId });
|
|
1155
1212
|
return rows[0];
|
|
1156
1213
|
});
|
|
1157
|
-
const tailDigestAt = Effect.fn("
|
|
1214
|
+
const tailDigestAt = Effect.fn("SqliteThreadStore.tailDigestAt")(function* (journal, threadId, sequence) {
|
|
1158
1215
|
if (sequence === 0) return EMPTY_TAIL_DIGEST;
|
|
1159
|
-
const digests = yield* journal.getTailDigestAt(
|
|
1216
|
+
const digests = yield* journal.getTailDigestAt(threadId, sequence).pipe(Effect.mapError((error) => storeError("read checkpoint digest", error)));
|
|
1160
1217
|
if (digests.length !== 1) return yield* CheckpointRejected.make({
|
|
1161
|
-
|
|
1218
|
+
threadId,
|
|
1162
1219
|
reason: "digest-mismatch"
|
|
1163
1220
|
});
|
|
1164
1221
|
return yield* Schema.decodeUnknownEffect(Digest)(digests[0]).pipe(Effect.mapError((error) => schemaStoreError("decode checkpoint digest", error)));
|
|
@@ -1177,14 +1234,14 @@ const groupByKey = (rows, key) => {
|
|
|
1177
1234
|
* and re-digested against the canonical chain. Routine opens skip this scan: per-operation
|
|
1178
1235
|
* Schema decoding plus the digest chain already fail clearly on corrupt rows.
|
|
1179
1236
|
*/
|
|
1180
|
-
const decodeStartupPayloads = Effect.fn("
|
|
1237
|
+
const decodeStartupPayloads = Effect.fn("SqliteThreadStore.decodeStartupPayloads")(function* (journal, crypto) {
|
|
1181
1238
|
const stored = yield* journal.scanStoredPayloads();
|
|
1182
1239
|
const batches = yield* Effect.forEach(stored.batches, (batch) => Schema.decodeEffect(Schema.fromJsonString(CanonicalBatch))(batch.batch_json).pipe(Effect.map((decoded) => ({
|
|
1183
1240
|
decoded,
|
|
1184
1241
|
row: batch
|
|
1185
1242
|
})), Effect.mapError((error) => SqliteStorageCorruptionError.make({
|
|
1186
1243
|
table: "effect_agent_canonical_batches",
|
|
1187
|
-
rowKey: `${batch.
|
|
1244
|
+
rowKey: `${batch.thread_id}/${batch.batch_id}`,
|
|
1188
1245
|
message: error.message
|
|
1189
1246
|
}))));
|
|
1190
1247
|
const records = yield* Effect.forEach(stored.records, (record) => Schema.decodeEffect(Schema.fromJsonString(CanonicalRecord))(record.record_json).pipe(Effect.map((decoded) => ({
|
|
@@ -1192,31 +1249,31 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
|
|
|
1192
1249
|
row: record
|
|
1193
1250
|
})), Effect.mapError((error) => SqliteStorageCorruptionError.make({
|
|
1194
1251
|
table: "effect_agent_canonical_records",
|
|
1195
|
-
rowKey: `${record.
|
|
1252
|
+
rowKey: `${record.thread_id}/${record.sequence}`,
|
|
1196
1253
|
message: error.message
|
|
1197
1254
|
}))));
|
|
1198
|
-
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(
|
|
1255
|
+
const checkpoints = yield* Effect.forEach(stored.checkpoints, (checkpoint) => Schema.decodeEffect(Schema.fromJsonString(ThreadCheckpoint))(checkpoint.checkpoint_json).pipe(Effect.map((decoded) => ({
|
|
1199
1256
|
decoded,
|
|
1200
1257
|
row: checkpoint
|
|
1201
1258
|
})), Effect.mapError((error) => SqliteStorageCorruptionError.make({
|
|
1202
1259
|
table: "effect_agent_checkpoints",
|
|
1203
|
-
rowKey: `${checkpoint.
|
|
1260
|
+
rowKey: `${checkpoint.thread_id}/${checkpoint.through_sequence}`,
|
|
1204
1261
|
message: error.message
|
|
1205
1262
|
}))));
|
|
1206
|
-
const
|
|
1207
|
-
const
|
|
1208
|
-
const
|
|
1209
|
-
const materializedIds = new Set(stored.
|
|
1210
|
-
for (const
|
|
1211
|
-
const
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1214
|
-
const recordsByBatch = groupByKey(
|
|
1263
|
+
const batchesByThread = groupByKey(batches, ({ row }) => row.thread_id);
|
|
1264
|
+
const recordsByThread = groupByKey(records, ({ row }) => row.thread_id);
|
|
1265
|
+
const checkpointsByThread = groupByKey(checkpoints, ({ row }) => row.thread_id);
|
|
1266
|
+
const materializedIds = new Set(stored.threads.map((thread) => thread.thread_id));
|
|
1267
|
+
for (const thread of stored.threads) {
|
|
1268
|
+
const threadBatches = batchesByThread.get(thread.thread_id) ?? [];
|
|
1269
|
+
const threadRecords = recordsByThread.get(thread.thread_id) ?? [];
|
|
1270
|
+
const threadCheckpoints = checkpointsByThread.get(thread.thread_id) ?? [];
|
|
1271
|
+
const recordsByBatch = groupByKey(threadRecords, ({ row }) => row.batch_id);
|
|
1215
1272
|
let previousDigest = EMPTY_TAIL_DIGEST;
|
|
1216
1273
|
let expectedSequence = 1;
|
|
1217
1274
|
const tailDigests = /* @__PURE__ */ new Map([[0, EMPTY_TAIL_DIGEST]]);
|
|
1218
|
-
for (const { decoded: canonicalBatch, row: batchRow } of
|
|
1219
|
-
const key = `${batchRow.
|
|
1275
|
+
for (const { decoded: canonicalBatch, row: batchRow } of threadBatches) {
|
|
1276
|
+
const key = `${batchRow.thread_id}/${batchRow.batch_id}`;
|
|
1220
1277
|
if (canonicalBatch.batchId !== batchRow.batch_id || batchRow.first_sequence !== expectedSequence || batchRow.last_sequence !== batchRow.first_sequence + canonicalBatch.records.length - 1) return yield* SqliteStorageCorruptionError.make({
|
|
1221
1278
|
table: "effect_agent_canonical_batches",
|
|
1222
1279
|
rowKey: key,
|
|
@@ -1261,41 +1318,41 @@ const decodeStartupPayloads = Effect.fn("SqliteConversationStore.decodeStartupPa
|
|
|
1261
1318
|
expectedSequence = batchRow.last_sequence + 1;
|
|
1262
1319
|
tailDigests.set(batchRow.last_sequence, digest);
|
|
1263
1320
|
}
|
|
1264
|
-
if (
|
|
1265
|
-
table: "
|
|
1266
|
-
rowKey:
|
|
1267
|
-
message: "
|
|
1321
|
+
if (threadRecords.length !== thread.tail_sequence || thread.tail_sequence !== expectedSequence - 1 || thread.tail_digest !== previousDigest) return yield* SqliteStorageCorruptionError.make({
|
|
1322
|
+
table: "effect_agent_threads",
|
|
1323
|
+
rowKey: thread.thread_id,
|
|
1324
|
+
message: "Thread tail does not match its canonical batch chain."
|
|
1268
1325
|
});
|
|
1269
|
-
for (const checkpoint of
|
|
1326
|
+
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* SqliteStorageCorruptionError.make({
|
|
1270
1327
|
table: "effect_agent_checkpoints",
|
|
1271
|
-
rowKey: `${
|
|
1328
|
+
rowKey: `${thread.thread_id}/${checkpoint.row.through_sequence}`,
|
|
1272
1329
|
message: "Checkpoint identity or digest is not bound to a canonical batch tail."
|
|
1273
1330
|
});
|
|
1274
1331
|
}
|
|
1275
|
-
if (batches.some(({ row }) => !materializedIds.has(row.
|
|
1276
|
-
table: "
|
|
1332
|
+
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* SqliteStorageCorruptionError.make({
|
|
1333
|
+
table: "effect_agent_threads",
|
|
1277
1334
|
rowKey: "startup_scan",
|
|
1278
|
-
message: "Canonical rows exist without a materialized
|
|
1335
|
+
message: "Canonical rows exist without a materialized Thread."
|
|
1279
1336
|
});
|
|
1280
1337
|
});
|
|
1281
|
-
const makeServices$1 = Effect.fn("
|
|
1338
|
+
const makeServices$1 = Effect.fn("SqliteThreadStore.makeServices")(function* () {
|
|
1282
1339
|
const config = yield* SqliteStorageConfig;
|
|
1283
1340
|
const failpoint = yield* SqliteStorageFailpoint;
|
|
1284
1341
|
const crypto = yield* Crypto.Crypto;
|
|
1285
1342
|
const journal = yield* initializeSqliteJournal();
|
|
1286
1343
|
if (config.verifyOnOpen) yield* decodeStartupPayloads(journal, crypto);
|
|
1287
1344
|
const provideCrypto = (effect) => Effect.provideService(effect, Crypto.Crypto, crypto);
|
|
1288
|
-
const hitFailpoint = Effect.fn("
|
|
1289
|
-
const materialize = Effect.fn("
|
|
1290
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1345
|
+
const hitFailpoint = Effect.fn("SqliteThreadStore.hitFailpoint")((location) => failpoint.hit(location).pipe(Effect.mapError((error) => storeError(`storage failpoint ${location}`, error))));
|
|
1346
|
+
const materialize = Effect.fn("SqliteThreadStore.materialize")(function* (request) {
|
|
1347
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadMaterialization))(request).pipe(Effect.mapError((error) => schemaStoreError("validate materialization", error)));
|
|
1291
1348
|
const now = yield* Clock.currentTimeMillis;
|
|
1292
1349
|
yield* hitFailpoint("materialize:before");
|
|
1293
|
-
yield* journal.materialize(validated.
|
|
1350
|
+
yield* journal.materialize(validated.threadId, new Date(now).toISOString(), EMPTY_TAIL_DIGEST, validated.producerEpoch).pipe(Effect.mapError((error) => error._tag === "SqliteFenceRejected" ? mapFence(validated.threadId, error) : storeError("materialize thread", error)));
|
|
1294
1351
|
yield* hitFailpoint("materialize:after");
|
|
1295
1352
|
});
|
|
1296
|
-
const append = Effect.fn("
|
|
1353
|
+
const append = Effect.fn("SqliteThreadStore.append")(function* (request) {
|
|
1297
1354
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(FencedAppendRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate canonical append", error)));
|
|
1298
|
-
yield*
|
|
1355
|
+
yield* requireThread(journal, validated.threadId);
|
|
1299
1356
|
const tailDigest = yield* provideCrypto(digestCanonicalBatch(validated.expectedTailDigest, validated.batch)).pipe(Effect.mapError((error) => storeError("digest canonical append", error)));
|
|
1300
1357
|
const batchJson = yield* encodeCanonicalBatch(validated.batch);
|
|
1301
1358
|
const rawRecords = yield* Effect.forEach(validated.batch.records, (record) => encodeCanonicalRecord(record).pipe(Effect.map((recordJson) => ({
|
|
@@ -1303,7 +1360,7 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
|
|
|
1303
1360
|
recordJson
|
|
1304
1361
|
}))));
|
|
1305
1362
|
const rawRequest = yield* Schema.decodeUnknownEffect(RawAppendRequest)({
|
|
1306
|
-
|
|
1363
|
+
threadId: validated.threadId,
|
|
1307
1364
|
batchId: validated.batch.batchId,
|
|
1308
1365
|
batchDigest: tailDigest,
|
|
1309
1366
|
batchJson,
|
|
@@ -1315,15 +1372,15 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
|
|
|
1315
1372
|
}).pipe(Effect.mapError((error) => schemaStoreError("encode canonical append", error)));
|
|
1316
1373
|
yield* hitFailpoint("append:before");
|
|
1317
1374
|
const result = yield* journal.append(rawRequest).pipe(Effect.mapError((error) => {
|
|
1318
|
-
if (isSqliteFenceRejected(error)) return mapFence(validated.
|
|
1375
|
+
if (isSqliteFenceRejected(error)) return mapFence(validated.threadId, error);
|
|
1319
1376
|
if (isSqliteAppendConflict(error)) return error.actualTailSequence !== void 0 && isDigest(error.actualTailDigest) ? AppendConflict.make({
|
|
1320
|
-
|
|
1377
|
+
threadId: validated.threadId,
|
|
1321
1378
|
batchId: validated.batch.batchId,
|
|
1322
1379
|
reason: error.reason,
|
|
1323
1380
|
actualTailSequence: error.actualTailSequence,
|
|
1324
1381
|
actualTailDigest: error.actualTailDigest
|
|
1325
1382
|
}) : AppendConflict.make({
|
|
1326
|
-
|
|
1383
|
+
threadId: validated.threadId,
|
|
1327
1384
|
batchId: validated.batch.batchId,
|
|
1328
1385
|
reason: error.reason
|
|
1329
1386
|
});
|
|
@@ -1332,30 +1389,30 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
|
|
|
1332
1389
|
yield* hitFailpoint("append:after");
|
|
1333
1390
|
return result;
|
|
1334
1391
|
});
|
|
1335
|
-
const loadRecords = Effect.fn("
|
|
1392
|
+
const loadRecords = Effect.fn("SqliteThreadStore.loadRecords")(function* (request) {
|
|
1336
1393
|
const rows = yield* journal.read(request).pipe(Effect.mapError((error) => storeError("read canonical records", error)));
|
|
1337
1394
|
return yield* Effect.forEach(rows, decodeEnvelope);
|
|
1338
1395
|
});
|
|
1339
|
-
const readEffect = Effect.fn("
|
|
1340
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1341
|
-
yield*
|
|
1396
|
+
const readEffect = Effect.fn("SqliteThreadStore.read")(function* (request) {
|
|
1397
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadRead))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread read", error)));
|
|
1398
|
+
yield* requireThread(journal, validated.threadId);
|
|
1342
1399
|
const records = yield* loadRecords(RawReadRequest.make({
|
|
1343
|
-
|
|
1400
|
+
threadId: validated.threadId,
|
|
1344
1401
|
fromSequenceExclusive: validated.afterSequence ?? ZERO_CANONICAL_SEQUENCE,
|
|
1345
1402
|
limit: validated.limit
|
|
1346
1403
|
}));
|
|
1347
1404
|
return Stream.fromIterable(records);
|
|
1348
1405
|
});
|
|
1349
1406
|
const read = (request) => Stream.unwrap(readEffect(request));
|
|
1350
|
-
const observeEffect = Effect.fn("
|
|
1351
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1352
|
-
yield*
|
|
1353
|
-
const initialSequence = yield* parseOffset(validated.
|
|
1407
|
+
const observeEffect = Effect.fn("SqliteThreadStore.observe")(function* (request) {
|
|
1408
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadObservation))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread observation", error)));
|
|
1409
|
+
yield* requireThread(journal, validated.threadId);
|
|
1410
|
+
const initialSequence = yield* parseOffset(validated.threadId, validated.afterOffset);
|
|
1354
1411
|
const cursor = yield* Ref.make(initialSequence);
|
|
1355
|
-
const poll = Effect.fn("
|
|
1412
|
+
const poll = Effect.fn("SqliteThreadStore.observePoll")(function* () {
|
|
1356
1413
|
const fromSequenceExclusive = yield* Ref.get(cursor);
|
|
1357
1414
|
const records = yield* loadRecords(RawReadRequest.make({
|
|
1358
|
-
|
|
1415
|
+
threadId: validated.threadId,
|
|
1359
1416
|
fromSequenceExclusive,
|
|
1360
1417
|
limit: 1024
|
|
1361
1418
|
}));
|
|
@@ -1369,96 +1426,98 @@ const makeServices$1 = Effect.fn("SqliteConversationStore.makeServices")(functio
|
|
|
1369
1426
|
return Stream.fromIterableEffectRepeat(poll());
|
|
1370
1427
|
});
|
|
1371
1428
|
const observe = (request) => Stream.unwrap(observeEffect(request));
|
|
1372
|
-
const
|
|
1373
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1374
|
-
yield*
|
|
1375
|
-
const exported = yield* journal.
|
|
1429
|
+
const exportThread = Effect.fn("SqliteThreadStore.export")(function* (request) {
|
|
1430
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadExportRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate thread export", error)));
|
|
1431
|
+
yield* requireThread(journal, validated.threadId);
|
|
1432
|
+
const exported = yield* journal.exportThread(validated.threadId).pipe(Effect.mapError((error) => storeError("export thread", error)));
|
|
1376
1433
|
const records = yield* Effect.forEach(exported.records, decodeEnvelope);
|
|
1377
|
-
if (records.length > 65536) return yield*
|
|
1378
|
-
operation: "decode
|
|
1379
|
-
message: "The
|
|
1434
|
+
if (records.length > 65536) return yield* ThreadStoreError.make({
|
|
1435
|
+
operation: "decode thread export",
|
|
1436
|
+
message: "The thread exceeds the current export record limit."
|
|
1380
1437
|
});
|
|
1381
|
-
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.
|
|
1382
|
-
return
|
|
1383
|
-
format: "effect-agent/
|
|
1384
|
-
|
|
1385
|
-
tailSequence: exported.
|
|
1438
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(exported.thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode export tail digest", error)));
|
|
1439
|
+
return ThreadExport.make({
|
|
1440
|
+
format: "effect-agent/thread@1",
|
|
1441
|
+
threadId: validated.threadId,
|
|
1442
|
+
tailSequence: exported.thread.tail_sequence,
|
|
1386
1443
|
tailDigest,
|
|
1387
1444
|
records
|
|
1388
1445
|
});
|
|
1389
1446
|
});
|
|
1390
|
-
const inspectTail = Effect.fn("
|
|
1391
|
-
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(
|
|
1392
|
-
const
|
|
1393
|
-
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(
|
|
1394
|
-
return
|
|
1395
|
-
|
|
1396
|
-
tailSequence:
|
|
1447
|
+
const inspectTail = Effect.fn("SqliteThreadStore.inspectTail")(function* (request) {
|
|
1448
|
+
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(ThreadTailRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate tail inspection", error)));
|
|
1449
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
1450
|
+
const tailDigest = yield* Schema.decodeUnknownEffect(Digest)(thread.tail_digest).pipe(Effect.mapError((error) => schemaStoreError("decode tail digest", error)));
|
|
1451
|
+
return ThreadTail.make({
|
|
1452
|
+
threadId: validated.threadId,
|
|
1453
|
+
tailSequence: thread.tail_sequence,
|
|
1397
1454
|
tailDigest,
|
|
1398
|
-
producerEpoch:
|
|
1455
|
+
producerEpoch: thread.producer_epoch
|
|
1399
1456
|
});
|
|
1400
1457
|
});
|
|
1401
|
-
const saveCheckpoint = Effect.fn("
|
|
1458
|
+
const saveCheckpoint = Effect.fn("SqliteThreadStore.saveCheckpoint")(function* (request) {
|
|
1402
1459
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(SaveCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint", error)));
|
|
1403
|
-
const
|
|
1404
|
-
if (validated.checkpoint.throughSequence >
|
|
1405
|
-
|
|
1460
|
+
const thread = yield* requireThread(journal, validated.checkpoint.threadId);
|
|
1461
|
+
if (validated.checkpoint.throughSequence > thread.tail_sequence) return yield* CheckpointRejected.make({
|
|
1462
|
+
threadId: validated.checkpoint.threadId,
|
|
1406
1463
|
reason: "ahead-of-tail"
|
|
1407
1464
|
});
|
|
1408
|
-
if ((yield* tailDigestAt(journal, validated.checkpoint.
|
|
1409
|
-
|
|
1465
|
+
if ((yield* tailDigestAt(journal, validated.checkpoint.threadId, validated.checkpoint.throughSequence)) !== validated.checkpoint.tailDigest) return yield* CheckpointRejected.make({
|
|
1466
|
+
threadId: validated.checkpoint.threadId,
|
|
1410
1467
|
reason: "digest-mismatch"
|
|
1411
1468
|
});
|
|
1412
1469
|
const checkpointJson = yield* encodeCheckpoint(validated.checkpoint);
|
|
1413
1470
|
const raw = RawCheckpoint.make({
|
|
1414
|
-
|
|
1471
|
+
threadId: validated.checkpoint.threadId,
|
|
1415
1472
|
throughSequence: validated.checkpoint.throughSequence,
|
|
1416
1473
|
tailDigest: validated.checkpoint.tailDigest,
|
|
1417
1474
|
checkpointJson
|
|
1418
1475
|
});
|
|
1419
1476
|
yield* hitFailpoint("save-checkpoint:before");
|
|
1420
1477
|
yield* journal.saveCheckpoint(raw).pipe(Effect.mapError((error) => isSqliteCheckpointConflict(error) ? CheckpointRejected.make({
|
|
1421
|
-
|
|
1478
|
+
threadId: validated.checkpoint.threadId,
|
|
1422
1479
|
reason: "digest-mismatch"
|
|
1423
1480
|
}) : storeError("save checkpoint", error)));
|
|
1424
1481
|
yield* hitFailpoint("save-checkpoint:after");
|
|
1425
1482
|
});
|
|
1426
|
-
const loadCheckpoint = Effect.fn("
|
|
1483
|
+
const loadCheckpoint = Effect.fn("SqliteThreadStore.loadCheckpoint")(function* (request) {
|
|
1427
1484
|
const validated = yield* Schema.decodeUnknownEffect(Schema.toType(LoadCheckpointRequest))(request).pipe(Effect.mapError((error) => schemaStoreError("validate checkpoint lookup", error)));
|
|
1428
|
-
const
|
|
1429
|
-
const rows = yield* journal.loadCheckpoint(validated.
|
|
1485
|
+
const thread = yield* requireThread(journal, validated.threadId);
|
|
1486
|
+
const rows = yield* journal.loadCheckpoint(validated.threadId, validated.atOrBeforeSequence ?? thread.tail_sequence).pipe(Effect.mapError((error) => storeError("load checkpoint", error)));
|
|
1430
1487
|
if (rows.length === 0) return Option.none();
|
|
1431
|
-
if (rows.length !== 1) return yield*
|
|
1488
|
+
if (rows.length !== 1) return yield* ThreadStoreError.make({
|
|
1432
1489
|
operation: "load checkpoint",
|
|
1433
1490
|
message: `Expected at most one checkpoint row but found ${rows.length}.`
|
|
1434
1491
|
});
|
|
1435
1492
|
const checkpoint = yield* decodeCheckpoint(rows[0].checkpoint_json);
|
|
1436
|
-
if ((yield* tailDigestAt(journal, checkpoint.
|
|
1437
|
-
|
|
1493
|
+
if ((yield* tailDigestAt(journal, checkpoint.threadId, checkpoint.throughSequence)) !== checkpoint.tailDigest) return yield* CheckpointRejected.make({
|
|
1494
|
+
threadId: checkpoint.threadId,
|
|
1438
1495
|
reason: "digest-mismatch"
|
|
1439
1496
|
});
|
|
1440
1497
|
return Option.some(checkpoint);
|
|
1441
1498
|
});
|
|
1442
|
-
const
|
|
1499
|
+
const threadStore = ThreadStore.of({
|
|
1443
1500
|
append,
|
|
1444
|
-
export:
|
|
1501
|
+
export: exportThread,
|
|
1445
1502
|
inspectTail,
|
|
1446
|
-
loadCheckpoint,
|
|
1447
1503
|
materialize,
|
|
1448
1504
|
observe,
|
|
1449
1505
|
read,
|
|
1450
|
-
|
|
1506
|
+
checkpoints: {
|
|
1507
|
+
save: saveCheckpoint,
|
|
1508
|
+
load: loadCheckpoint
|
|
1509
|
+
}
|
|
1451
1510
|
});
|
|
1452
|
-
return Context.make(
|
|
1511
|
+
return Context.make(ThreadStore, threadStore);
|
|
1453
1512
|
});
|
|
1454
1513
|
/**
|
|
1455
|
-
* SQLite
|
|
1514
|
+
* SQLite Thread Store implementation with configuration, failpoint, SQL, and Crypto
|
|
1456
1515
|
* authority kept visible in its input channel.
|
|
1457
1516
|
*/
|
|
1458
|
-
const
|
|
1517
|
+
const threadStoreLayer = Layer.effectContext(makeServices$1());
|
|
1459
1518
|
/**
|
|
1460
1519
|
* Validated SQLite storage configuration Layer with the documented defaults applied. Shared
|
|
1461
|
-
* by the
|
|
1520
|
+
* by the ThreadStore and SubmissionLedger convenience layers so their defaults cannot
|
|
1462
1521
|
* drift.
|
|
1463
1522
|
*/
|
|
1464
1523
|
const storageConfigLayer = (options) => Layer.effect(SqliteStorageConfig)(Schema.decodeUnknownEffect(SqliteStorageConfigValue)({
|
|
@@ -1474,10 +1533,10 @@ const storageConfigLayer = (options) => Layer.effect(SqliteStorageConfig)(Schema
|
|
|
1474
1533
|
/** The failpoint Layer selected by convenience options: explicit handler or the no-op default. */
|
|
1475
1534
|
const storageFailpointLayer = (options) => options.failpoint === void 0 ? SqliteStorageFailpoint.layer : Layer.succeed(SqliteStorageFailpoint)({ hit: options.failpoint });
|
|
1476
1535
|
/**
|
|
1477
|
-
* A composition-root convenience Layer for canonical
|
|
1536
|
+
* A composition-root convenience Layer for canonical Threads. Durable accepted work is
|
|
1478
1537
|
* served by the separate SubmissionLedger port.
|
|
1479
1538
|
*/
|
|
1480
|
-
const layer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) =>
|
|
1539
|
+
const layer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) => threadStoreLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(SqliteStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
|
|
1481
1540
|
/** Create an adapter-owned resumable observation offset for a known canonical sequence. */
|
|
1482
1541
|
const observationOffsetAt = makeOffset;
|
|
1483
1542
|
//#endregion
|
|
@@ -1494,7 +1553,7 @@ const STILL_WAITING = "still-waiting";
|
|
|
1494
1553
|
const WOKEN = "woken";
|
|
1495
1554
|
var SubmissionRow = class extends Schema.Class("SubmissionRow")({
|
|
1496
1555
|
submission_id: BoundedIdentifier,
|
|
1497
|
-
|
|
1556
|
+
thread_id: BoundedIdentifier,
|
|
1498
1557
|
queue_sequence: QueueSequence,
|
|
1499
1558
|
principal: BoundedIdentifier,
|
|
1500
1559
|
idempotency_key: BoundedIdentifier,
|
|
@@ -1576,7 +1635,7 @@ var MaxQueueSequenceRow = class extends Schema.Class("MaxQueueSequenceRow")({ ma
|
|
|
1576
1635
|
var CanonicalRecordIdRow = class extends Schema.Class("CanonicalRecordIdRow")({ record_id: BoundedIdentifier }) {};
|
|
1577
1636
|
const SUBMISSION_COLUMNS = `
|
|
1578
1637
|
submission_id,
|
|
1579
|
-
|
|
1638
|
+
thread_id,
|
|
1580
1639
|
queue_sequence,
|
|
1581
1640
|
principal,
|
|
1582
1641
|
idempotency_key,
|
|
@@ -1613,7 +1672,7 @@ const CHILD_RESERVATION_COLUMNS = `
|
|
|
1613
1672
|
release_began_at,
|
|
1614
1673
|
released_at
|
|
1615
1674
|
`;
|
|
1616
|
-
/** The branded ToolCallId schema, reached through the
|
|
1675
|
+
/** The branded ToolCallId schema, reached through the thread port so no core import is needed. */
|
|
1617
1676
|
const ToolCallIdSchema = ApprovalDecisionCommand.fields.toolCallId;
|
|
1618
1677
|
const ToolCallIdList = Schema.Array(ToolCallIdSchema);
|
|
1619
1678
|
const encodePersistedJsonText = Schema.encodeEffect(Schema.fromJsonString(PersistedJson));
|
|
@@ -1691,7 +1750,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1691
1750
|
iso: new Date(millis).toISOString()
|
|
1692
1751
|
}));
|
|
1693
1752
|
const timestampMillis = (operation, rowKey) => (timestamp) => decodeUtcInstant(timestamp).pipe(Effect.map(DateTime.toEpochMillis), Effect.mapError((error) => corruptionFailure(operation, "effect_agent_submission_ownership", rowKey, error.message)));
|
|
1694
|
-
const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$
|
|
1753
|
+
const decodeSubmissionRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1695
1754
|
const readSubmission = Effect.fn("SqliteSubmissionLedger.readSubmission")(function* (operation, submissionId) {
|
|
1696
1755
|
const rows = yield* sql`
|
|
1697
1756
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
@@ -1722,23 +1781,23 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1722
1781
|
FROM effect_agent_submission_ownership
|
|
1723
1782
|
WHERE submission_id = ${submissionId}
|
|
1724
1783
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1725
|
-
const decoded = yield* decodeRows$
|
|
1784
|
+
const decoded = yield* decodeRows$2(Schema.Array(OwnershipRow), "effect_agent_submission_ownership", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1726
1785
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submission_ownership", submissionId, "An ownership primary key returned more than one row.");
|
|
1727
1786
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1728
1787
|
});
|
|
1729
|
-
const
|
|
1730
|
-
const
|
|
1731
|
-
return
|
|
1788
|
+
const threadEpoch = Effect.fn("SqliteSubmissionLedger.threadEpoch")(function* (operation, threadId) {
|
|
1789
|
+
const threads = yield* journal.getThread(threadId).pipe(Effect.mapError(internalFailure(operation)));
|
|
1790
|
+
return threads.length === 0 ? EPOCH_ZERO : threads[0].producer_epoch;
|
|
1732
1791
|
});
|
|
1733
1792
|
/**
|
|
1734
1793
|
* Verify inside the surrounding write transaction that the presented token still owns the
|
|
1735
1794
|
* Submission's lane; a superseded or missing token fails with OwnershipLost carrying the
|
|
1736
|
-
*
|
|
1795
|
+
* Thread's current producer epoch (DUR-006).
|
|
1737
1796
|
*/
|
|
1738
1797
|
const requireOwnership = Effect.fn("SqliteSubmissionLedger.requireOwnership")(function* (operation, submission, ownershipToken) {
|
|
1739
1798
|
const ownership = yield* readOwnership(operation, submission.submission_id);
|
|
1740
1799
|
if (Option.isNone(ownership) || ownership.value.ownership_token !== ownershipToken) {
|
|
1741
|
-
const actualEpoch = yield*
|
|
1800
|
+
const actualEpoch = yield* threadEpoch(operation, submission.thread_id);
|
|
1742
1801
|
const submissionId = yield* Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId)(submission.submission_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
1743
1802
|
return yield* OwnershipLost.make({
|
|
1744
1803
|
submissionId,
|
|
@@ -1753,7 +1812,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1753
1812
|
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.");
|
|
1754
1813
|
return yield* decodeSubmissionSnapshotUnknown({
|
|
1755
1814
|
submissionId: row.submission_id,
|
|
1756
|
-
|
|
1815
|
+
threadId: row.thread_id,
|
|
1757
1816
|
queueSequence: row.queue_sequence,
|
|
1758
1817
|
principal: row.principal,
|
|
1759
1818
|
idempotencyKey: row.idempotency_key,
|
|
@@ -1787,7 +1846,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1787
1846
|
FROM effect_agent_settlement_reservations
|
|
1788
1847
|
WHERE submission_id = ${submissionId}
|
|
1789
1848
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1790
|
-
const decoded = yield* decodeRows$
|
|
1849
|
+
const decoded = yield* decodeRows$2(Schema.Array(ReservationRow), "effect_agent_settlement_reservations", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1791
1850
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_settlement_reservations", submissionId, "A settlement reservation primary key returned more than one row.");
|
|
1792
1851
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1793
1852
|
});
|
|
@@ -1802,11 +1861,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1802
1861
|
FROM effect_agent_abort_intents
|
|
1803
1862
|
WHERE submission_id = ${submissionId}
|
|
1804
1863
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1805
|
-
const decoded = yield* decodeRows$
|
|
1864
|
+
const decoded = yield* decodeRows$2(Schema.Array(AbortIntentRow), "effect_agent_abort_intents", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1806
1865
|
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_abort_intents", submissionId, "An abort intent primary key returned more than one row.");
|
|
1807
1866
|
return decoded.length === 0 ? Option.none() : Option.some(decoded[0]);
|
|
1808
1867
|
});
|
|
1809
|
-
const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$
|
|
1868
|
+
const decodeChildReservationRows = (operation, rowKey, rows) => decodeRows$2(Schema.Array(ChildReservationRow), "effect_agent_child_reservations", rowKey, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1810
1869
|
const readChildReservation = Effect.fn("SqliteSubmissionLedger.readChildReservation")(function* (operation, reservationId) {
|
|
1811
1870
|
const rows = yield* sql`
|
|
1812
1871
|
SELECT ${sql.literal(CHILD_RESERVATION_COLUMNS)}
|
|
@@ -1859,7 +1918,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1859
1918
|
WHERE submission_id = ${submissionId}
|
|
1860
1919
|
ORDER BY tool_call_id ASC
|
|
1861
1920
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1862
|
-
return yield* decodeRows$
|
|
1921
|
+
return yield* decodeRows$2(Schema.Array(ApprovalDecisionRow), "effect_agent_approval_decisions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1863
1922
|
});
|
|
1864
1923
|
const approvalIntentFromRow = Effect.fn("SqliteSubmissionLedger.approvalIntentFromRow")(function* (operation, row) {
|
|
1865
1924
|
return yield* decodeApprovalDecisionIntent({
|
|
@@ -1884,7 +1943,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1884
1943
|
WHERE submission_id = ${submissionId}
|
|
1885
1944
|
ORDER BY tool_call_id ASC
|
|
1886
1945
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1887
|
-
return yield* decodeRows$
|
|
1946
|
+
return yield* decodeRows$2(Schema.Array(UnknownResolutionRow), "effect_agent_unknown_resolutions", submissionId, rows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1888
1947
|
});
|
|
1889
1948
|
const unknownResolutionIntentFromRow = Effect.fn("SqliteSubmissionLedger.unknownResolutionIntentFromRow")(function* (operation, row) {
|
|
1890
1949
|
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)));
|
|
@@ -1907,18 +1966,18 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1907
1966
|
* derived from the shared canonical-records table using the deterministic abort record
|
|
1908
1967
|
* identity, never from a cached ledger marker.
|
|
1909
1968
|
*/
|
|
1910
|
-
const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(function* (operation,
|
|
1969
|
+
const canonicalAbortRecordId = Effect.fn("SqliteSubmissionLedger.canonicalAbortRecordId")(function* (operation, threadId, submissionId) {
|
|
1911
1970
|
const recordId = submissionAbortRecordId(submissionId);
|
|
1912
1971
|
const rows = yield* sql`
|
|
1913
1972
|
SELECT record_id
|
|
1914
1973
|
FROM effect_agent_canonical_records
|
|
1915
|
-
WHERE
|
|
1974
|
+
WHERE thread_id = ${threadId}
|
|
1916
1975
|
AND record_id = ${recordId}
|
|
1917
1976
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1918
|
-
return (yield* decodeRows$
|
|
1977
|
+
return (yield* decodeRows$2(Schema.Array(CanonicalRecordIdRow), "effect_agent_canonical_records", `${threadId}/${recordId}`, rows).pipe(Effect.mapError(internalFailure(operation)))).length === 0 ? void 0 : recordId;
|
|
1919
1978
|
});
|
|
1920
1979
|
const abortIntentFromRow = Effect.fn("SqliteSubmissionLedger.abortIntentFromRow")(function* (operation, submission, submissionId, row) {
|
|
1921
|
-
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.
|
|
1980
|
+
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, submissionId);
|
|
1922
1981
|
return yield* decodeAbortIntent({
|
|
1923
1982
|
submissionId: row.submission_id,
|
|
1924
1983
|
author: row.author,
|
|
@@ -1937,11 +1996,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1937
1996
|
const mintedReceiptId = yield* mintIdentifier("receipt", operation);
|
|
1938
1997
|
yield* hitFailpoint("ledger:admit:before", operation);
|
|
1939
1998
|
const result = yield* inWriteTransaction(operation, Effect.gen(function* () {
|
|
1940
|
-
const keyRowKey = `${validated.
|
|
1999
|
+
const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
|
|
1941
2000
|
const existingRows = yield* sql`
|
|
1942
2001
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
1943
2002
|
FROM effect_agent_submissions
|
|
1944
|
-
WHERE
|
|
2003
|
+
WHERE thread_id = ${validated.threadId}
|
|
1945
2004
|
AND principal = ${validated.principal}
|
|
1946
2005
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
1947
2006
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
@@ -1950,7 +2009,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1950
2009
|
if (existing.length === 1) {
|
|
1951
2010
|
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;
|
|
1952
2011
|
if (existing[0].input_digest !== validated.inputDigest || !sameLinkage) return yield* AdmissionConflict.make({
|
|
1953
|
-
|
|
2012
|
+
threadId: validated.threadId,
|
|
1954
2013
|
principal: validated.principal,
|
|
1955
2014
|
idempotencyKey: validated.idempotencyKey,
|
|
1956
2015
|
existingInputDigest: existing[0].input_digest,
|
|
@@ -1967,15 +2026,15 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1967
2026
|
const maxRows = yield* sql`
|
|
1968
2027
|
SELECT COALESCE(MAX(queue_sequence), 0) AS max_queue_sequence
|
|
1969
2028
|
FROM effect_agent_submissions
|
|
1970
|
-
WHERE
|
|
2029
|
+
WHERE thread_id = ${validated.threadId}
|
|
1971
2030
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
1972
|
-
const decodedMax = yield* decodeRows$
|
|
2031
|
+
const decodedMax = yield* decodeRows$2(Schema.Array(MaxQueueSequenceRow), "effect_agent_submissions", validated.threadId, maxRows).pipe(Effect.mapError(internalFailure(operation)));
|
|
1973
2032
|
const queueSequence = yield* decodeQueueSequence((decodedMax[0]?.max_queue_sequence ?? 0) + 1).pipe(Effect.mapError(internalFailure(operation)));
|
|
1974
2033
|
const now = yield* currentInstant;
|
|
1975
2034
|
yield* sql`
|
|
1976
2035
|
INSERT INTO effect_agent_submissions (
|
|
1977
2036
|
submission_id,
|
|
1978
|
-
|
|
2037
|
+
thread_id,
|
|
1979
2038
|
queue_sequence,
|
|
1980
2039
|
principal,
|
|
1981
2040
|
idempotency_key,
|
|
@@ -1991,7 +2050,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
1991
2050
|
parent_tool_call_id
|
|
1992
2051
|
) VALUES (
|
|
1993
2052
|
${mintedSubmissionId},
|
|
1994
|
-
${validated.
|
|
2053
|
+
${validated.threadId},
|
|
1995
2054
|
${queueSequence},
|
|
1996
2055
|
${validated.principal},
|
|
1997
2056
|
${validated.idempotencyKey},
|
|
@@ -2044,12 +2103,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2044
2103
|
const rows = yield* sql`
|
|
2045
2104
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2046
2105
|
FROM effect_agent_submissions
|
|
2047
|
-
WHERE
|
|
2106
|
+
WHERE thread_id = ${validated.threadId}
|
|
2048
2107
|
AND principal = ${validated.principal}
|
|
2049
2108
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2050
2109
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2051
|
-
const decoded = yield* decodeSubmissionRows(operation, `${validated.
|
|
2052
|
-
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.
|
|
2110
|
+
const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
|
|
2111
|
+
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.");
|
|
2053
2112
|
if (decoded.length === 0) return Option.none();
|
|
2054
2113
|
return Option.some(yield* decodeSubmissionSnapshot(operation, decoded[0]));
|
|
2055
2114
|
});
|
|
@@ -2059,12 +2118,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2059
2118
|
const rows = yield* sql`
|
|
2060
2119
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2061
2120
|
FROM effect_agent_submissions
|
|
2062
|
-
WHERE
|
|
2121
|
+
WHERE thread_id = ${validated.threadId}
|
|
2063
2122
|
AND principal = ${validated.principal}
|
|
2064
2123
|
AND idempotency_key = ${validated.idempotencyKey}
|
|
2065
2124
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2066
|
-
const decoded = yield* decodeSubmissionRows(operation, `${validated.
|
|
2067
|
-
if (decoded.length > 1) return yield* corruptionFailure(operation, "effect_agent_submissions", `${validated.
|
|
2125
|
+
const decoded = yield* decodeSubmissionRows(operation, `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`, rows);
|
|
2126
|
+
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.");
|
|
2068
2127
|
if (decoded.length === 0) return AdmissionNotAdmitted.make();
|
|
2069
2128
|
return AdmissionAdmitted.make({ submission: yield* decodeSubmissionSnapshot(operation, decoded[0]) });
|
|
2070
2129
|
});
|
|
@@ -2078,12 +2137,12 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2078
2137
|
const headRows = yield* sql`
|
|
2079
2138
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2080
2139
|
FROM effect_agent_submissions
|
|
2081
|
-
WHERE
|
|
2140
|
+
WHERE thread_id = ${validated.threadId}
|
|
2082
2141
|
AND state <> 'settled'
|
|
2083
2142
|
ORDER BY queue_sequence ASC
|
|
2084
2143
|
LIMIT 1
|
|
2085
2144
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2086
|
-
const heads = yield* decodeSubmissionRows(operation, validated.
|
|
2145
|
+
const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
|
|
2087
2146
|
if (heads.length === 0) return Option.none();
|
|
2088
2147
|
const head = heads[0];
|
|
2089
2148
|
if (head.state === "joining" || head.state === "joined" || head.state === "suspended" || head.state === "unknown" && Option.isNone(yield* readAbortIntent(operation, head.submission_id))) return Option.none();
|
|
@@ -2092,19 +2151,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2092
2151
|
if (Option.isSome(ownership)) {
|
|
2093
2152
|
if ((yield* timestampMillis(operation, head.submission_id)(ownership.value.lease_expires_at)) > now.millis) return Option.none();
|
|
2094
2153
|
}
|
|
2095
|
-
const
|
|
2154
|
+
const threads = yield* journal.getThread(head.thread_id).pipe(Effect.mapError(internalFailure(operation)));
|
|
2096
2155
|
let producerEpoch;
|
|
2097
|
-
if (
|
|
2156
|
+
if (threads.length === 0) {
|
|
2098
2157
|
producerEpoch = 1;
|
|
2099
2158
|
yield* sql`
|
|
2100
|
-
INSERT INTO
|
|
2101
|
-
|
|
2159
|
+
INSERT INTO effect_agent_threads (
|
|
2160
|
+
thread_id,
|
|
2102
2161
|
created_at,
|
|
2103
2162
|
tail_sequence,
|
|
2104
2163
|
tail_digest,
|
|
2105
2164
|
producer_epoch
|
|
2106
2165
|
) VALUES (
|
|
2107
|
-
${head.
|
|
2166
|
+
${head.thread_id},
|
|
2108
2167
|
${now.iso},
|
|
2109
2168
|
0,
|
|
2110
2169
|
${EMPTY_TAIL_DIGEST},
|
|
@@ -2112,11 +2171,11 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2112
2171
|
)
|
|
2113
2172
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2114
2173
|
} else {
|
|
2115
|
-
producerEpoch =
|
|
2174
|
+
producerEpoch = threads[0].producer_epoch + 1;
|
|
2116
2175
|
yield* sql`
|
|
2117
|
-
UPDATE
|
|
2176
|
+
UPDATE effect_agent_threads
|
|
2118
2177
|
SET producer_epoch = ${producerEpoch}
|
|
2119
|
-
WHERE
|
|
2178
|
+
WHERE thread_id = ${head.thread_id}
|
|
2120
2179
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2121
2180
|
}
|
|
2122
2181
|
const leaseExpiresAt = new Date(now.millis + config.ownershipLeaseDuration).toISOString();
|
|
@@ -2147,14 +2206,14 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2147
2206
|
INSERT INTO effect_agent_attempts (
|
|
2148
2207
|
attempt_id,
|
|
2149
2208
|
submission_id,
|
|
2150
|
-
|
|
2209
|
+
thread_id,
|
|
2151
2210
|
owner_producer_id,
|
|
2152
2211
|
producer_epoch,
|
|
2153
2212
|
claimed_at
|
|
2154
2213
|
) VALUES (
|
|
2155
2214
|
${attemptId},
|
|
2156
2215
|
${head.submission_id},
|
|
2157
|
-
${head.
|
|
2216
|
+
${head.thread_id},
|
|
2158
2217
|
${validated.producerId},
|
|
2159
2218
|
${producerEpoch},
|
|
2160
2219
|
${now.iso}
|
|
@@ -2414,7 +2473,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2414
2473
|
${now.iso}
|
|
2415
2474
|
)
|
|
2416
2475
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2417
|
-
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.
|
|
2476
|
+
const canonicalRecordId = yield* canonicalAbortRecordId(operation, submission.thread_id, validated.submissionId);
|
|
2418
2477
|
return yield* decodeAbortIntent({
|
|
2419
2478
|
submissionId: validated.submissionId,
|
|
2420
2479
|
author: validated.author,
|
|
@@ -2432,19 +2491,19 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2432
2491
|
yield* hitFailpoint("ledger:claim-joining:before", operation);
|
|
2433
2492
|
const claims = yield* inWriteTransaction(operation, Effect.gen(function* () {
|
|
2434
2493
|
const host = yield* requireSubmission(operation, validated.hostSubmissionId);
|
|
2435
|
-
if (host.
|
|
2494
|
+
if (host.thread_id !== validated.threadId) return yield* LedgerError.make({
|
|
2436
2495
|
operation,
|
|
2437
|
-
message: `Host submission ${validated.hostSubmissionId} does not belong to
|
|
2496
|
+
message: `Host submission ${validated.hostSubmissionId} does not belong to thread ${validated.threadId}.`
|
|
2438
2497
|
});
|
|
2439
2498
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
2440
2499
|
const laterRows = yield* sql`
|
|
2441
2500
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2442
2501
|
FROM effect_agent_submissions
|
|
2443
|
-
WHERE
|
|
2502
|
+
WHERE thread_id = ${validated.threadId}
|
|
2444
2503
|
AND queue_sequence > ${host.queue_sequence}
|
|
2445
2504
|
ORDER BY queue_sequence ASC
|
|
2446
2505
|
`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2447
|
-
const later = yield* decodeSubmissionRows(operation, validated.
|
|
2506
|
+
const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
|
|
2448
2507
|
const claimed = [];
|
|
2449
2508
|
for (const row of later) {
|
|
2450
2509
|
if (claimed.length >= validated.maxCount) break;
|
|
@@ -2947,27 +3006,27 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
2947
3006
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2948
3007
|
FROM effect_agent_submissions
|
|
2949
3008
|
WHERE state <> 'settled'
|
|
2950
|
-
ORDER BY
|
|
3009
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2951
3010
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2952
3011
|
` : sql`
|
|
2953
3012
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2954
3013
|
FROM effect_agent_submissions
|
|
2955
3014
|
WHERE state <> 'settled'
|
|
2956
3015
|
AND (
|
|
2957
|
-
|
|
3016
|
+
thread_id > ${cursor.threadId}
|
|
2958
3017
|
OR (
|
|
2959
|
-
|
|
3018
|
+
thread_id = ${cursor.threadId}
|
|
2960
3019
|
AND queue_sequence > ${cursor.queueSequence}
|
|
2961
3020
|
)
|
|
2962
3021
|
)
|
|
2963
|
-
ORDER BY
|
|
3022
|
+
ORDER BY thread_id ASC, queue_sequence ASC
|
|
2964
3023
|
LIMIT ${SCAN_PAGE_SIZE}
|
|
2965
3024
|
`).pipe(Effect.mapError(sqlFailure(operation)));
|
|
2966
3025
|
const decoded = yield* decodeSubmissionRows(operation, "nonterminal_scan", rows);
|
|
2967
3026
|
const snapshots = yield* Effect.forEach(decoded, (row) => decodeSubmissionSnapshot(operation, row));
|
|
2968
3027
|
const last = decoded[decoded.length - 1];
|
|
2969
3028
|
return [snapshots, last === void 0 || decoded.length < SCAN_PAGE_SIZE ? Option.none() : Option.some({
|
|
2970
|
-
|
|
3029
|
+
threadId: last.thread_id,
|
|
2971
3030
|
queueSequence: last.queue_sequence
|
|
2972
3031
|
})];
|
|
2973
3032
|
});
|
|
@@ -3112,7 +3171,7 @@ const makeServices = Effect.fn("SqliteSubmissionLedger.makeServices")(function*
|
|
|
3112
3171
|
const submissionLedgerLayer = Layer.effectContext(makeServices());
|
|
3113
3172
|
/**
|
|
3114
3173
|
* A composition-root convenience Layer for the durable Submission Ledger. Point it at the
|
|
3115
|
-
* same database file as the
|
|
3174
|
+
* same database file as the ThreadStore so claims fence the same producer epochs.
|
|
3116
3175
|
*/
|
|
3117
3176
|
const ledgerLayer = (options) => Layer.unwrap(Effect.map(SqliteStorageConfig, (config) => submissionLedgerLayer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(SqliteStorageConfig)(config), storageFailpointLayer(options), SqliteClient.layer({ filename: options.filename }), NodeCrypto.layer))))).pipe(Layer.provide(storageConfigLayer(options)));
|
|
3118
3177
|
//#endregion
|
|
@@ -3134,27 +3193,27 @@ const ScheduleDueRow = Schema.Struct({
|
|
|
3134
3193
|
});
|
|
3135
3194
|
var ScheduleCountRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleCountRow")({ schedule_count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) }) {};
|
|
3136
3195
|
var ScheduleDeadlineRow = class extends Schema.Class("@effect-agent/storage-sqlite/ScheduleDeadlineRow")({ deadline_at_millis: StoredDeadline }) {};
|
|
3137
|
-
const unavailable = (operation) => ScheduleStorageError.make({
|
|
3196
|
+
const unavailable$1 = (operation) => ScheduleStorageError.make({
|
|
3138
3197
|
operation,
|
|
3139
3198
|
reason: "unavailable"
|
|
3140
3199
|
});
|
|
3141
|
-
const corrupt = (operation) => ScheduleStorageError.make({
|
|
3200
|
+
const corrupt$1 = (operation) => ScheduleStorageError.make({
|
|
3142
3201
|
operation,
|
|
3143
3202
|
reason: "corrupt"
|
|
3144
3203
|
});
|
|
3145
|
-
const decodeRows = Effect.fn("SqliteScheduleStore.decodeRows")(function* (schema, rows, operation) {
|
|
3146
|
-
return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3204
|
+
const decodeRows$1 = Effect.fn("SqliteScheduleStore.decodeRows")(function* (schema, rows, operation) {
|
|
3205
|
+
return yield* Schema.decodeUnknownEffect(schema)(rows).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3147
3206
|
});
|
|
3148
3207
|
const decodeRecord = Effect.fn("SqliteScheduleStore.decodeRecord")(function* (row) {
|
|
3149
|
-
const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt("decode schedule")));
|
|
3150
|
-
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 identity");
|
|
3208
|
+
const record = yield* Schema.decodeEffect(Schema.fromJsonString(ScheduleRecord))(row.record_json).pipe(Effect.mapError(() => corrupt$1("decode schedule")));
|
|
3209
|
+
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 identity");
|
|
3151
3210
|
return record;
|
|
3152
3211
|
});
|
|
3153
3212
|
const encodeRecord = Effect.fn("SqliteScheduleStore.encodeRecord")(function* (record) {
|
|
3154
|
-
return yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt("encode schedule")));
|
|
3213
|
+
return yield* Schema.encodeEffect(Schema.fromJsonString(ScheduleRecord))(record).pipe(Effect.mapError(() => corrupt$1("encode schedule")));
|
|
3155
3214
|
});
|
|
3156
3215
|
const decodeInput = Effect.fn("SqliteScheduleStore.decodeInput")(function* (operation, schema, value) {
|
|
3157
|
-
return yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3216
|
+
return yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3158
3217
|
});
|
|
3159
3218
|
const makeScheduleStore = Effect.gen(function* () {
|
|
3160
3219
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -3167,13 +3226,13 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3167
3226
|
WHERE tenant_id = ${key.owner.tenantId}
|
|
3168
3227
|
AND owner_id = ${key.owner.ownerId}
|
|
3169
3228
|
AND schedule_id = ${key.scheduleId}
|
|
3170
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3171
|
-
return yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
3229
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3230
|
+
return yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
|
|
3172
3231
|
});
|
|
3173
3232
|
const readOne = Effect.fn("SqliteScheduleStore.readOne")(function* (key, operation) {
|
|
3174
3233
|
const rows = yield* readRows(key, operation);
|
|
3175
3234
|
if (rows.length === 0) return null;
|
|
3176
|
-
if (rows.length !== 1) return yield* corrupt(operation);
|
|
3235
|
+
if (rows.length !== 1) return yield* corrupt$1(operation);
|
|
3177
3236
|
return yield* decodeRecord(rows[0]);
|
|
3178
3237
|
});
|
|
3179
3238
|
const insert = Effect.fn("SqliteScheduleStore.insert")(function* (record, ownerLimit) {
|
|
@@ -3202,9 +3261,9 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3202
3261
|
AND owner_id = ${canonical.owner.ownerId}
|
|
3203
3262
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
3204
3263
|
(json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
|
|
3205
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3206
|
-
const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3207
|
-
if (counts.length !== 1) return yield* corrupt(operation);
|
|
3264
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3265
|
+
const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3266
|
+
if (counts.length !== 1) return yield* corrupt$1(operation);
|
|
3208
3267
|
if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
3209
3268
|
yield* scheduleFailpoint.hit("schedule:insert:before");
|
|
3210
3269
|
yield* sql`
|
|
@@ -3217,12 +3276,12 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3217
3276
|
${scheduleDeadline(canonical)},
|
|
3218
3277
|
${recordJson}
|
|
3219
3278
|
)
|
|
3220
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3279
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3221
3280
|
return {
|
|
3222
3281
|
record: canonical,
|
|
3223
3282
|
inserted: true
|
|
3224
3283
|
};
|
|
3225
|
-
})).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
|
|
3284
|
+
})).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable$1(operation))));
|
|
3226
3285
|
if (result.inserted) yield* scheduleFailpoint.hit("schedule:insert:after");
|
|
3227
3286
|
return result.record;
|
|
3228
3287
|
});
|
|
@@ -3240,7 +3299,7 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3240
3299
|
AND owner_id = ${decodedRequest.owner.ownerId}
|
|
3241
3300
|
ORDER BY schedule_id
|
|
3242
3301
|
LIMIT ${decodedRequest.limit + 1}
|
|
3243
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3302
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3244
3303
|
SELECT tenant_id, owner_id, schedule_id, deadline_at_millis, record_json
|
|
3245
3304
|
FROM effect_agent_schedules
|
|
3246
3305
|
WHERE tenant_id = ${decodedRequest.owner.tenantId}
|
|
@@ -3248,8 +3307,8 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3248
3307
|
AND schedule_id > ${decodedRequest.after}
|
|
3249
3308
|
ORDER BY schedule_id
|
|
3250
3309
|
LIMIT ${decodedRequest.limit + 1}
|
|
3251
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3252
|
-
const decoded = yield* decodeRows(Schema.Array(ScheduleRow), rows, operation);
|
|
3310
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3311
|
+
const decoded = yield* decodeRows$1(Schema.Array(ScheduleRow), rows, operation);
|
|
3253
3312
|
const records = yield* Effect.forEach(decoded, decodeRecord);
|
|
3254
3313
|
const hasNext = records.length > decodedRequest.limit;
|
|
3255
3314
|
const items = hasNext ? records.slice(0, decodedRequest.limit) : records;
|
|
@@ -3274,9 +3333,9 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3274
3333
|
WHERE tenant_id = ${key.owner.tenantId} AND owner_id = ${key.owner.ownerId}
|
|
3275
3334
|
AND (json_extract(record_json, '$.pending') IS NOT NULL OR
|
|
3276
3335
|
(json_extract(record_json, '$.state') != 'cancelled' AND json_extract(record_json, '$.nextAtMillis') IS NOT NULL))
|
|
3277
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3278
|
-
const counts = yield* decodeRows(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3279
|
-
if (counts.length !== 1) return yield* corrupt(operation);
|
|
3336
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3337
|
+
const counts = yield* decodeRows$1(Schema.Array(ScheduleCountRow), rawCounts, operation);
|
|
3338
|
+
if (counts.length !== 1) return yield* corrupt$1(operation);
|
|
3280
3339
|
if (counts[0].schedule_count >= ownerLimit) return yield* ScheduleCapacityError.make({ limit: ownerLimit });
|
|
3281
3340
|
}
|
|
3282
3341
|
if (next === current) return {
|
|
@@ -3291,19 +3350,19 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3291
3350
|
WHERE tenant_id = ${decodedKey.owner.tenantId}
|
|
3292
3351
|
AND owner_id = ${decodedKey.owner.ownerId}
|
|
3293
3352
|
AND schedule_id = ${decodedKey.scheduleId}
|
|
3294
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3353
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3295
3354
|
return {
|
|
3296
3355
|
record: next,
|
|
3297
3356
|
changed: true
|
|
3298
3357
|
};
|
|
3299
|
-
})).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable(operation))));
|
|
3358
|
+
})).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable$1(operation))));
|
|
3300
3359
|
if (result.changed) yield* scheduleFailpoint.hit(`schedule:${decodedChange._tag.toLowerCase()}:after`);
|
|
3301
3360
|
return result.record;
|
|
3302
3361
|
});
|
|
3303
3362
|
const due = Effect.fn("SqliteScheduleStore.due")(function* (nowMillis, limit, owner, after) {
|
|
3304
3363
|
const operation = "query due schedules";
|
|
3305
3364
|
const decodedOwner = owner === void 0 ? void 0 : yield* decodeInput(operation, ScheduleOwner, owner);
|
|
3306
|
-
const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt(operation)));
|
|
3365
|
+
const cursor = after === void 0 ? void 0 : yield* Schema.decodeUnknownEffect(ScheduleDueCursor)(after).pipe(Effect.mapError(() => corrupt$1(operation)));
|
|
3307
3366
|
const continuation = cursor === void 0 ? sql`1 = 1` : sql`
|
|
3308
3367
|
(deadline_at_millis, tenant_id, owner_id, schedule_id) >
|
|
3309
3368
|
(${cursor.deadlineAtMillis}, ${cursor.owner.tenantId}, ${cursor.owner.ownerId}, ${cursor.scheduleId})`;
|
|
@@ -3313,7 +3372,7 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3313
3372
|
WHERE deadline_at_millis <= ${nowMillis} AND ${continuation}
|
|
3314
3373
|
ORDER BY deadline_at_millis, tenant_id, owner_id, schedule_id
|
|
3315
3374
|
LIMIT ${limit}
|
|
3316
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3375
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3317
3376
|
SELECT tenant_id, owner_id, schedule_id, deadline_at_millis
|
|
3318
3377
|
FROM effect_agent_schedules
|
|
3319
3378
|
WHERE tenant_id = ${decodedOwner.tenantId}
|
|
@@ -3321,8 +3380,8 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3321
3380
|
AND deadline_at_millis <= ${nowMillis} AND ${continuation}
|
|
3322
3381
|
ORDER BY deadline_at_millis, schedule_id
|
|
3323
3382
|
LIMIT ${limit}
|
|
3324
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3325
|
-
return (yield* decodeRows(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
|
|
3383
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3384
|
+
return (yield* decodeRows$1(Schema.Array(ScheduleDueRow), rows, operation)).map((row) => ({
|
|
3326
3385
|
owner: {
|
|
3327
3386
|
tenantId: row.tenant_id,
|
|
3328
3387
|
ownerId: row.owner_id
|
|
@@ -3338,15 +3397,15 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3338
3397
|
SELECT MIN(deadline_at_millis) AS deadline_at_millis
|
|
3339
3398
|
FROM effect_agent_schedules
|
|
3340
3399
|
WHERE deadline_at_millis IS NOT NULL
|
|
3341
|
-
`.pipe(Effect.mapError(() => unavailable(operation))) : yield* sql`
|
|
3400
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation))) : yield* sql`
|
|
3342
3401
|
SELECT MIN(deadline_at_millis) AS deadline_at_millis
|
|
3343
3402
|
FROM effect_agent_schedules
|
|
3344
3403
|
WHERE tenant_id = ${decodedOwner.tenantId}
|
|
3345
3404
|
AND owner_id = ${decodedOwner.ownerId}
|
|
3346
3405
|
AND deadline_at_millis IS NOT NULL
|
|
3347
|
-
`.pipe(Effect.mapError(() => unavailable(operation)));
|
|
3348
|
-
const decoded = yield* decodeRows(Schema.Array(ScheduleDeadlineRow), rows, operation);
|
|
3349
|
-
if (decoded.length !== 1) return yield* corrupt(operation);
|
|
3406
|
+
`.pipe(Effect.mapError(() => unavailable$1(operation)));
|
|
3407
|
+
const decoded = yield* decodeRows$1(Schema.Array(ScheduleDeadlineRow), rows, operation);
|
|
3408
|
+
if (decoded.length !== 1) return yield* corrupt$1(operation);
|
|
3350
3409
|
return decoded[0].deadline_at_millis;
|
|
3351
3410
|
});
|
|
3352
3411
|
return ScheduleStore.of({
|
|
@@ -3361,6 +3420,590 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
3361
3420
|
/** SQLite implementation of the atomic ScheduleStore port. */
|
|
3362
3421
|
const scheduleStoreLayer = Layer.effect(ScheduleStore)(makeScheduleStore);
|
|
3363
3422
|
//#endregion
|
|
3364
|
-
|
|
3423
|
+
//#region src/sqlite-subscription-store.ts
|
|
3424
|
+
const StoredJson = Schema.String.check(Schema.isMaxLength(16 * 1024 * 1024));
|
|
3425
|
+
const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
|
|
3426
|
+
const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
|
|
3427
|
+
const ScanRow = Schema.Struct({
|
|
3428
|
+
event_scan_cursor: Schema.String,
|
|
3429
|
+
delivery_scan_cursor: Schema.String,
|
|
3430
|
+
recovery_scan_cursor: Schema.Natural
|
|
3431
|
+
});
|
|
3432
|
+
const JsonRow = Schema.Struct({ record_json: StoredJson });
|
|
3433
|
+
const RegistrationRow = Schema.Struct({
|
|
3434
|
+
owner_id: Schema.String,
|
|
3435
|
+
subscription_id: Schema.String,
|
|
3436
|
+
ordinal: Schema.Natural,
|
|
3437
|
+
source_name: Schema.String,
|
|
3438
|
+
source_version: Schema.String,
|
|
3439
|
+
matching_key: Schema.String,
|
|
3440
|
+
state: SubscriptionRecord.fields.state,
|
|
3441
|
+
expires_at_millis: Schema.Number,
|
|
3442
|
+
recovery_at_millis: Schema.NullOr(Schema.Number),
|
|
3443
|
+
record_json: StoredJson
|
|
3444
|
+
});
|
|
3445
|
+
const EventRow = Schema.Struct({
|
|
3446
|
+
event_id: Schema.String,
|
|
3447
|
+
source_name: Schema.String,
|
|
3448
|
+
source_version: Schema.String,
|
|
3449
|
+
matching_key: Schema.String,
|
|
3450
|
+
payload_digest: Digest,
|
|
3451
|
+
cutoff: Schema.Natural,
|
|
3452
|
+
cursor: Schema.Natural,
|
|
3453
|
+
routing_complete: Schema.Number,
|
|
3454
|
+
next_attempt_at_millis: Schema.Number,
|
|
3455
|
+
record_json: StoredJson
|
|
3456
|
+
});
|
|
3457
|
+
const DeliveryRow = Schema.Struct({
|
|
3458
|
+
owner_id: Schema.String,
|
|
3459
|
+
subscription_id: Schema.String,
|
|
3460
|
+
event_id: Schema.String,
|
|
3461
|
+
delivery_key: Schema.String,
|
|
3462
|
+
state: SubscriptionDelivery.fields.state,
|
|
3463
|
+
next_attempt_at_millis: Schema.Number,
|
|
3464
|
+
record_json: StoredJson
|
|
3465
|
+
});
|
|
3466
|
+
const error = (reason, code) => SubscriptionError.make({
|
|
3467
|
+
reason,
|
|
3468
|
+
code
|
|
3469
|
+
});
|
|
3470
|
+
const unavailable = (operation) => error("storage", operation);
|
|
3471
|
+
const corrupt = (operation) => error("corrupt", operation);
|
|
3472
|
+
const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
3473
|
+
const validate = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
|
|
3474
|
+
const encode = (schema, value, code) => Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
|
|
3475
|
+
const decode = (schema, value, code) => Schema.decodeEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError(() => corrupt(code)));
|
|
3476
|
+
const decodeRows = (schema, rows, code) => Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
|
|
3477
|
+
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;
|
|
3478
|
+
const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function* (owned) {
|
|
3479
|
+
const partition = yield* validate(SourcePartition, owned, "partition");
|
|
3480
|
+
const sql = yield* SqlClient.SqlClient;
|
|
3481
|
+
const failpoint = yield* SubscriptionFailpoint;
|
|
3482
|
+
yield* initializeSqliteJournal();
|
|
3483
|
+
yield* sql`
|
|
3484
|
+
INSERT INTO effect_agent_subscription_sequences (
|
|
3485
|
+
tenant_id, source_address, sequence, event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
3486
|
+
) VALUES (${partition.tenantId}, ${partition.address}, 0, '', '', 0) ON CONFLICT DO NOTHING
|
|
3487
|
+
`.pipe(Effect.mapError(() => unavailable("initialize subscription partition")));
|
|
3488
|
+
const transact = (effect) => sql.withTransaction(effect).pipe(Effect.catchTag("SqlError", () => Effect.fail(unavailable("transaction"))));
|
|
3489
|
+
const query = (effect, code) => effect.pipe(Effect.mapError(() => unavailable(code)));
|
|
3490
|
+
const requirePartition = (candidate, code) => sameSourcePartition(candidate, partition) ? Effect.void : Effect.fail(error("validation", code));
|
|
3491
|
+
const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (input, code) {
|
|
3492
|
+
const key = yield* validate(SubscriptionKey, input, code);
|
|
3493
|
+
yield* requirePartition(key.partition, code);
|
|
3494
|
+
return key;
|
|
3495
|
+
});
|
|
3496
|
+
const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (key, code) {
|
|
3497
|
+
const rows = yield* query(sql`
|
|
3498
|
+
SELECT owner_id, subscription_id, ordinal, source_name, source_version, matching_key, state,
|
|
3499
|
+
expires_at_millis, recovery_at_millis, record_json FROM effect_agent_subscriptions
|
|
3500
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3501
|
+
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId}
|
|
3502
|
+
`, code);
|
|
3503
|
+
const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
|
|
3504
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3505
|
+
const row = decodedRows[0];
|
|
3506
|
+
if (row === void 0) return null;
|
|
3507
|
+
const record = yield* decode(SubscriptionRecord, row.record_json, code);
|
|
3508
|
+
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`);
|
|
3509
|
+
return record;
|
|
3510
|
+
});
|
|
3511
|
+
const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (eventId, code) {
|
|
3512
|
+
const rows = yield* query(sql`
|
|
3513
|
+
SELECT event_id, source_name, source_version, matching_key, payload_digest, cutoff, cursor,
|
|
3514
|
+
routing_complete, next_attempt_at_millis, record_json FROM effect_agent_subscription_events
|
|
3515
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${eventId}
|
|
3516
|
+
`, code);
|
|
3517
|
+
const decodedRows = yield* decodeRows(EventRow, rows, code);
|
|
3518
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3519
|
+
const row = decodedRows[0];
|
|
3520
|
+
if (row === void 0) return null;
|
|
3521
|
+
const event = yield* decode(AcceptedEvent, row.record_json, code);
|
|
3522
|
+
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`);
|
|
3523
|
+
return event;
|
|
3524
|
+
});
|
|
3525
|
+
const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (key, code) {
|
|
3526
|
+
const rows = yield* query(sql`
|
|
3527
|
+
SELECT owner_id, subscription_id, event_id, delivery_key, state, next_attempt_at_millis, record_json
|
|
3528
|
+
FROM effect_agent_subscription_deliveries
|
|
3529
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3530
|
+
AND owner_id=${key.subscription.ownerId} AND subscription_id=${key.subscription.subscriptionId}
|
|
3531
|
+
AND event_id=${key.eventId}
|
|
3532
|
+
`, code);
|
|
3533
|
+
const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
|
|
3534
|
+
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
3535
|
+
const row = decodedRows[0];
|
|
3536
|
+
if (row === void 0) return null;
|
|
3537
|
+
const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
|
|
3538
|
+
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`);
|
|
3539
|
+
return delivery;
|
|
3540
|
+
});
|
|
3541
|
+
const count = Effect.fn("SqliteSubscriptionStore.count")(function* (statement, code) {
|
|
3542
|
+
const rows = yield* query(statement, code);
|
|
3543
|
+
const decoded = yield* decodeRows(CountRow, rows, code);
|
|
3544
|
+
if (decoded.length !== 1) return yield* corrupt(code);
|
|
3545
|
+
return decoded[0].count;
|
|
3546
|
+
});
|
|
3547
|
+
const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
|
|
3548
|
+
const rows = yield* query(sql`
|
|
3549
|
+
UPDATE effect_agent_subscription_sequences SET sequence=sequence+1
|
|
3550
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3551
|
+
RETURNING sequence
|
|
3552
|
+
`, "advance subscription sequence");
|
|
3553
|
+
const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
|
|
3554
|
+
if (decoded.length !== 1) return yield* corrupt("subscription sequence");
|
|
3555
|
+
return decoded[0].sequence;
|
|
3556
|
+
});
|
|
3557
|
+
const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (record) {
|
|
3558
|
+
const json = yield* encode(SubscriptionRecord, record, "encode subscription");
|
|
3559
|
+
yield* query(sql`
|
|
3560
|
+
UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
|
|
3561
|
+
recovery_at_millis=${record.recovery?.nextAttemptAtMillis ?? null}, record_json=${json}
|
|
3562
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3563
|
+
AND owner_id=${record.key.ownerId} AND subscription_id=${record.key.subscriptionId}
|
|
3564
|
+
`, "write subscription");
|
|
3565
|
+
});
|
|
3566
|
+
const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (event) {
|
|
3567
|
+
const json = yield* encode(AcceptedEvent, event, "encode event");
|
|
3568
|
+
yield* query(sql`
|
|
3569
|
+
UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
|
|
3570
|
+
next_attempt_at_millis=${event.nextAttemptAtMillis}, record_json=${json}
|
|
3571
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${event.eventId}
|
|
3572
|
+
`, "write event");
|
|
3573
|
+
});
|
|
3574
|
+
const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (delivery) {
|
|
3575
|
+
const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
|
|
3576
|
+
yield* query(sql`
|
|
3577
|
+
UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
|
|
3578
|
+
next_attempt_at_millis=${delivery.retry.nextAttemptAtMillis}, record_json=${json}
|
|
3579
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3580
|
+
AND owner_id=${delivery.key.subscription.ownerId} AND subscription_id=${delivery.key.subscription.subscriptionId}
|
|
3581
|
+
AND event_id=${delivery.key.eventId}
|
|
3582
|
+
`, "write delivery");
|
|
3583
|
+
});
|
|
3584
|
+
const register = Effect.fn("SqliteSubscriptionStore.register")(function* (input, inputLimits) {
|
|
3585
|
+
const record = yield* validate(SubscriptionRecord, input, "register-record");
|
|
3586
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
|
|
3587
|
+
yield* requirePartition(record.key.partition, "register-partition");
|
|
3588
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3589
|
+
const existing = yield* readRegistration(record.key, "register-existing");
|
|
3590
|
+
if (existing !== null) {
|
|
3591
|
+
if (existing.creationFingerprint !== record.creationFingerprint) return yield* error("conflict", "registration-identity");
|
|
3592
|
+
return {
|
|
3593
|
+
value: existing,
|
|
3594
|
+
changed: false
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
if (bytes(record.configuration.context) > limits.maxContextBytes) return yield* error("capacity", "context-bytes");
|
|
3598
|
+
if (bytes(record.configuration.parameters) > limits.maxPayloadBytes) return yield* error("capacity", "parameters-bytes");
|
|
3599
|
+
if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return yield* error("capacity", "lifetime");
|
|
3600
|
+
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");
|
|
3601
|
+
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");
|
|
3602
|
+
const assigned = {
|
|
3603
|
+
...record,
|
|
3604
|
+
ordinal: yield* nextSequence()
|
|
3605
|
+
};
|
|
3606
|
+
const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
|
|
3607
|
+
yield* failpoint.hit("subscription:register:before");
|
|
3608
|
+
yield* query(sql`
|
|
3609
|
+
INSERT INTO effect_agent_subscriptions (tenant_id, source_address, owner_id, subscription_id, ordinal,
|
|
3610
|
+
source_name, source_version, matching_key, state, expires_at_millis, recovery_at_millis, record_json)
|
|
3611
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${assigned.key.ownerId}, ${assigned.key.subscriptionId}, ${assigned.ordinal},
|
|
3612
|
+
${assigned.configuration.source.name}, ${assigned.configuration.source.version}, ${assigned.configuration.matchingKey}, ${assigned.state},
|
|
3613
|
+
${assigned.configuration.expiresAtMillis}, ${assigned.recovery?.nextAttemptAtMillis ?? null}, ${json})
|
|
3614
|
+
`, "insert registration");
|
|
3615
|
+
return {
|
|
3616
|
+
value: assigned,
|
|
3617
|
+
changed: true
|
|
3618
|
+
};
|
|
3619
|
+
}));
|
|
3620
|
+
if (result.changed) yield* failpoint.hit("subscription:register:after");
|
|
3621
|
+
return result.value;
|
|
3622
|
+
});
|
|
3623
|
+
const get = Effect.fn("SqliteSubscriptionStore.get")(function* (input) {
|
|
3624
|
+
return yield* readRegistration(yield* requireKey(input, "get-key"), "get subscription");
|
|
3625
|
+
});
|
|
3626
|
+
const list = Effect.fn("SqliteSubscriptionStore.list")(function* (ownerId, after, limit) {
|
|
3627
|
+
const rows = yield* query(sql`
|
|
3628
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3629
|
+
AND owner_id=${ownerId} AND ordinal>${after} ORDER BY ordinal LIMIT ${limit}
|
|
3630
|
+
`, "list subscriptions");
|
|
3631
|
+
const decoded = yield* decodeRows(Schema.Struct({
|
|
3632
|
+
owner_id: Schema.String,
|
|
3633
|
+
subscription_id: Schema.String,
|
|
3634
|
+
ordinal: Schema.Natural
|
|
3635
|
+
}), rows, "list subscriptions");
|
|
3636
|
+
return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
|
|
3637
|
+
const record = yield* readRegistration({
|
|
3638
|
+
partition,
|
|
3639
|
+
ownerId: row.owner_id,
|
|
3640
|
+
subscriptionId: row.subscription_id
|
|
3641
|
+
}, "list subscription");
|
|
3642
|
+
if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("list subscription projection");
|
|
3643
|
+
return record;
|
|
3644
|
+
}));
|
|
3645
|
+
});
|
|
3646
|
+
const cancel = Effect.fn("SqliteSubscriptionStore.cancel")(function* (input) {
|
|
3647
|
+
const key = yield* requireKey(input, "cancel-key");
|
|
3648
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3649
|
+
const current = yield* readRegistration(key, "cancel subscription");
|
|
3650
|
+
if (current === null) return yield* error("not-found", "subscription");
|
|
3651
|
+
if (current.state === "cancelled") return {
|
|
3652
|
+
value: current,
|
|
3653
|
+
changed: false
|
|
3654
|
+
};
|
|
3655
|
+
const updated = {
|
|
3656
|
+
...current,
|
|
3657
|
+
state: "cancelled",
|
|
3658
|
+
recovery: null
|
|
3659
|
+
};
|
|
3660
|
+
yield* failpoint.hit("subscription:cancel:before");
|
|
3661
|
+
yield* writeRegistration(updated);
|
|
3662
|
+
return {
|
|
3663
|
+
value: updated,
|
|
3664
|
+
changed: true
|
|
3665
|
+
};
|
|
3666
|
+
}));
|
|
3667
|
+
if (result.changed) yield* failpoint.hit("subscription:cancel:after");
|
|
3668
|
+
return result.value;
|
|
3669
|
+
});
|
|
3670
|
+
const accept = Effect.fn("SqliteSubscriptionStore.accept")(function* (input, inputLimits) {
|
|
3671
|
+
const event = yield* validate(AcceptedEvent, input, "accept-event");
|
|
3672
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
|
|
3673
|
+
yield* requirePartition(event.partition, "accept-partition");
|
|
3674
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3675
|
+
const existing = yield* readEvent(event.eventId, "accept event");
|
|
3676
|
+
if (existing !== null) {
|
|
3677
|
+
if (!sameAcceptedEventIdentity(existing, event)) return yield* error("conflict", "event-identity");
|
|
3678
|
+
return {
|
|
3679
|
+
value: existing,
|
|
3680
|
+
changed: false
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
if (bytes(event.payload) > limits.maxPayloadBytes) return yield* error("capacity", "payload-bytes");
|
|
3684
|
+
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");
|
|
3685
|
+
const accepted = {
|
|
3686
|
+
...event,
|
|
3687
|
+
cutoff: yield* nextSequence(),
|
|
3688
|
+
cursor: 0,
|
|
3689
|
+
routingComplete: false,
|
|
3690
|
+
routingFailure: null
|
|
3691
|
+
};
|
|
3692
|
+
const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
|
|
3693
|
+
yield* failpoint.hit("subscription:accept:before");
|
|
3694
|
+
yield* query(sql`
|
|
3695
|
+
INSERT INTO effect_agent_subscription_events (tenant_id, source_address, event_id, source_name, source_version,
|
|
3696
|
+
matching_key, payload_digest, cutoff, cursor, routing_complete, next_attempt_at_millis, record_json)
|
|
3697
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${accepted.eventId}, ${accepted.source.name}, ${accepted.source.version},
|
|
3698
|
+
${accepted.matchingKey}, ${accepted.payloadDigest}, ${accepted.cutoff}, ${accepted.cursor}, 0, ${accepted.nextAttemptAtMillis}, ${json})
|
|
3699
|
+
`, "insert event");
|
|
3700
|
+
return {
|
|
3701
|
+
value: accepted,
|
|
3702
|
+
changed: true
|
|
3703
|
+
};
|
|
3704
|
+
}));
|
|
3705
|
+
if (result.changed) yield* failpoint.hit("subscription:accept:after");
|
|
3706
|
+
return result.value;
|
|
3707
|
+
});
|
|
3708
|
+
const event = Effect.fn("SqliteSubscriptionStore.event")((eventId) => readEvent(eventId, "get event"));
|
|
3709
|
+
const pendingEvents = Effect.fn("SqliteSubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
|
|
3710
|
+
const rows = yield* query(sql`
|
|
3711
|
+
SELECT event_id FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3712
|
+
AND routing_complete=0 AND next_attempt_at_millis<=${nowMillis} AND event_id>${after} ORDER BY event_id LIMIT ${limit}
|
|
3713
|
+
`, "pending events");
|
|
3714
|
+
return yield* decodeRows(Schema.Struct({ event_id: Schema.String }), rows, "pending event keys").pipe(Effect.map((items) => items.map((item) => item.event_id)));
|
|
3715
|
+
});
|
|
3716
|
+
const candidates = Effect.fn("SqliteSubscriptionStore.candidates")(function* (input, limit) {
|
|
3717
|
+
const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
|
|
3718
|
+
yield* requirePartition(supplied.partition, "candidates-partition");
|
|
3719
|
+
const stored = yield* readEvent(supplied.eventId, "candidates event");
|
|
3720
|
+
if (stored === null) return yield* error("not-found", "event");
|
|
3721
|
+
if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
|
|
3722
|
+
const rows = yield* query(sql`
|
|
3723
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3724
|
+
AND source_name=${stored.source.name} AND source_version=${stored.source.version} AND matching_key=${stored.matchingKey}
|
|
3725
|
+
AND ordinal>${stored.cursor} AND ordinal<=${stored.cutoff} ORDER BY ordinal LIMIT ${limit}
|
|
3726
|
+
`, "subscription candidates");
|
|
3727
|
+
const decoded = yield* decodeRows(Schema.Struct({
|
|
3728
|
+
owner_id: Schema.String,
|
|
3729
|
+
subscription_id: Schema.String,
|
|
3730
|
+
ordinal: Schema.Natural
|
|
3731
|
+
}), rows, "subscription candidates");
|
|
3732
|
+
return yield* Effect.forEach(decoded, Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
|
|
3733
|
+
const record = yield* readRegistration({
|
|
3734
|
+
partition,
|
|
3735
|
+
ownerId: row.owner_id,
|
|
3736
|
+
subscriptionId: row.subscription_id
|
|
3737
|
+
}, "subscription candidate");
|
|
3738
|
+
if (record === null || record.ordinal !== row.ordinal) return yield* corrupt("subscription candidate projection");
|
|
3739
|
+
return record;
|
|
3740
|
+
}));
|
|
3741
|
+
});
|
|
3742
|
+
const insertDelivery = Effect.fn("SqliteSubscriptionStore.insertDelivery")(function* (delivery) {
|
|
3743
|
+
const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
|
|
3744
|
+
yield* query(sql`
|
|
3745
|
+
INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
|
|
3746
|
+
delivery_key, state, next_attempt_at_millis, record_json)
|
|
3747
|
+
VALUES (${partition.tenantId}, ${partition.address}, ${delivery.key.subscription.ownerId}, ${delivery.key.subscription.subscriptionId},
|
|
3748
|
+
${delivery.key.eventId}, ${subscriptionDeliveryKeyString(delivery.key)}, ${delivery.state}, ${delivery.retry.nextAttemptAtMillis}, ${json})
|
|
3749
|
+
`, "insert delivery");
|
|
3750
|
+
});
|
|
3751
|
+
const select = Effect.fn("SqliteSubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
|
|
3752
|
+
const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
|
|
3753
|
+
const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
|
|
3754
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
|
|
3755
|
+
yield* requirePartition(supplied.partition, "select-partition");
|
|
3756
|
+
for (const candidate of deliveries) yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
|
|
3757
|
+
if (yield* transact(Effect.gen(function* () {
|
|
3758
|
+
const accepted = yield* readEvent(supplied.eventId, "select event");
|
|
3759
|
+
if (accepted === null) return yield* error("not-found", "event");
|
|
3760
|
+
if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor) return yield* error("conflict", "event-cursor");
|
|
3761
|
+
if (accepted.routingComplete) return false;
|
|
3762
|
+
if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return yield* error("validation", "cursor");
|
|
3763
|
+
yield* failpoint.hit("subscription:select:before");
|
|
3764
|
+
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
3765
|
+
const additions = [];
|
|
3766
|
+
for (const delivery of deliveries) {
|
|
3767
|
+
const record = yield* readRegistration(delivery.key.subscription, "select registration");
|
|
3768
|
+
if (record === null) return yield* error("not-found", "subscription");
|
|
3769
|
+
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");
|
|
3770
|
+
const existing = yield* readDelivery(delivery.key, "select existing delivery");
|
|
3771
|
+
if (existing !== null) {
|
|
3772
|
+
if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
|
|
3773
|
+
continue;
|
|
3774
|
+
}
|
|
3775
|
+
if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
|
|
3776
|
+
additions.push({
|
|
3777
|
+
delivery,
|
|
3778
|
+
record
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
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");
|
|
3782
|
+
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");
|
|
3783
|
+
for (const addition of additions) {
|
|
3784
|
+
yield* insertDelivery(addition.delivery);
|
|
3785
|
+
if (addition.record.configuration.mode === "once") yield* writeRegistration({
|
|
3786
|
+
...addition.record,
|
|
3787
|
+
state: "consumed",
|
|
3788
|
+
recovery: null
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
yield* writeEvent({
|
|
3792
|
+
...accepted,
|
|
3793
|
+
cursor,
|
|
3794
|
+
routingComplete: complete,
|
|
3795
|
+
routingFailure: null
|
|
3796
|
+
});
|
|
3797
|
+
return true;
|
|
3798
|
+
}))) yield* failpoint.hit("subscription:select:after");
|
|
3799
|
+
});
|
|
3800
|
+
const catchUp = Effect.fn("SqliteSubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
|
|
3801
|
+
const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
|
|
3802
|
+
const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
|
|
3803
|
+
const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
|
|
3804
|
+
yield* requirePartition(supplied.partition, "catch-up-partition");
|
|
3805
|
+
yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
|
|
3806
|
+
if (yield* transact(Effect.gen(function* () {
|
|
3807
|
+
const accepted = yield* readEvent(supplied.eventId, "catch-up event");
|
|
3808
|
+
const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
|
|
3809
|
+
if (accepted === null || record === null) return yield* error("not-found", accepted === null ? "event" : "subscription");
|
|
3810
|
+
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");
|
|
3811
|
+
const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
|
|
3812
|
+
if (existing !== null) {
|
|
3813
|
+
if (!sameDeliveryIdentity(existing, delivery)) return yield* error("conflict", "delivery-identity");
|
|
3814
|
+
return false;
|
|
3815
|
+
}
|
|
3816
|
+
yield* failpoint.hit("subscription:catch-up:before");
|
|
3817
|
+
if (!subscriptionCanSelect(record, accepted, Math.max(nowMillis, yield* Clock.currentTimeMillis), true)) return yield* error("conflict", "catch-up-eligibility");
|
|
3818
|
+
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");
|
|
3819
|
+
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");
|
|
3820
|
+
yield* insertDelivery(delivery);
|
|
3821
|
+
yield* writeRegistration({
|
|
3822
|
+
...record,
|
|
3823
|
+
state: "consumed",
|
|
3824
|
+
recovery: null
|
|
3825
|
+
});
|
|
3826
|
+
return true;
|
|
3827
|
+
}))) yield* failpoint.hit("subscription:catch-up:after");
|
|
3828
|
+
});
|
|
3829
|
+
const deferEvent = Effect.fn("SqliteSubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
|
|
3830
|
+
const routingFailure = code === void 0 ? "routing-failed" : yield* validate(SubscriptionName, code, "routing-failure");
|
|
3831
|
+
yield* transact(Effect.gen(function* () {
|
|
3832
|
+
const accepted = yield* readEvent(eventId, "defer event");
|
|
3833
|
+
if (accepted === null) return yield* error("not-found", "event");
|
|
3834
|
+
yield* failpoint.hit("subscription:defer-event:before");
|
|
3835
|
+
yield* writeEvent({
|
|
3836
|
+
...accepted,
|
|
3837
|
+
nextAttemptAtMillis,
|
|
3838
|
+
routingFailure
|
|
3839
|
+
});
|
|
3840
|
+
}));
|
|
3841
|
+
yield* failpoint.hit("subscription:defer-event:after");
|
|
3842
|
+
});
|
|
3843
|
+
const delivery = Effect.fn("SqliteSubscriptionStore.delivery")(function* (input) {
|
|
3844
|
+
const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
|
|
3845
|
+
yield* requirePartition(key.subscription.partition, "delivery-partition");
|
|
3846
|
+
return yield* readDelivery(key, "get delivery");
|
|
3847
|
+
});
|
|
3848
|
+
const pendingDeliveries = Effect.fn("SqliteSubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
|
|
3849
|
+
const rows = yield* query(sql`
|
|
3850
|
+
SELECT owner_id, subscription_id, event_id FROM effect_agent_subscription_deliveries
|
|
3851
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
3852
|
+
AND next_attempt_at_millis<=${nowMillis} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
3853
|
+
`, "pending deliveries");
|
|
3854
|
+
const rowSchema = Schema.Struct({
|
|
3855
|
+
owner_id: Schema.String,
|
|
3856
|
+
subscription_id: Schema.String,
|
|
3857
|
+
event_id: Schema.String
|
|
3858
|
+
});
|
|
3859
|
+
return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(Effect.map((items) => items.map((item) => ({
|
|
3860
|
+
subscription: {
|
|
3861
|
+
partition,
|
|
3862
|
+
ownerId: item.owner_id,
|
|
3863
|
+
subscriptionId: item.subscription_id
|
|
3864
|
+
},
|
|
3865
|
+
eventId: item.event_id
|
|
3866
|
+
}))));
|
|
3867
|
+
});
|
|
3868
|
+
const listDeliveries = Effect.fn("SqliteSubscriptionStore.listDeliveries")(function* (input, after, limit) {
|
|
3869
|
+
const key = yield* requireKey(input, "list-deliveries-key");
|
|
3870
|
+
const rows = yield* query(sql`
|
|
3871
|
+
SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3872
|
+
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
3873
|
+
`, "list deliveries");
|
|
3874
|
+
const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
|
|
3875
|
+
return yield* Effect.forEach(decoded, (row) => decode(SubscriptionDelivery, row.record_json, "list delivery"));
|
|
3876
|
+
});
|
|
3877
|
+
const changeDelivery = Effect.fn("SqliteSubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
|
|
3878
|
+
const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
|
|
3879
|
+
const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
|
|
3880
|
+
const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
|
|
3881
|
+
yield* requirePartition(key.subscription.partition, "change-delivery-partition");
|
|
3882
|
+
const result = yield* transact(Effect.gen(function* () {
|
|
3883
|
+
const existing = yield* readDelivery(key, "change delivery");
|
|
3884
|
+
const record = yield* readRegistration(key.subscription, "change delivery subscription");
|
|
3885
|
+
if (existing === null || record === null) return yield* error("not-found", existing === null ? "delivery" : "subscription");
|
|
3886
|
+
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
|
|
3887
|
+
const effectiveChange = change._tag === "Prepare" ? {
|
|
3888
|
+
...change,
|
|
3889
|
+
nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
|
|
3890
|
+
} : change;
|
|
3891
|
+
const transition = applySubscriptionDeliveryChange(existing, record, deliveryId, effectiveChange);
|
|
3892
|
+
if (Result.isFailure(transition)) return yield* transition.failure;
|
|
3893
|
+
if (transition.success === existing) return {
|
|
3894
|
+
value: existing,
|
|
3895
|
+
changed: false
|
|
3896
|
+
};
|
|
3897
|
+
yield* writeDelivery(transition.success);
|
|
3898
|
+
return {
|
|
3899
|
+
value: transition.success,
|
|
3900
|
+
changed: true
|
|
3901
|
+
};
|
|
3902
|
+
}));
|
|
3903
|
+
if (result.changed) yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
|
|
3904
|
+
return result.value;
|
|
3905
|
+
});
|
|
3906
|
+
const recovering = Effect.fn("SqliteSubscriptionStore.recovering")(function* (nowMillis, after, limit) {
|
|
3907
|
+
const rows = yield* query(sql`
|
|
3908
|
+
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions
|
|
3909
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active'
|
|
3910
|
+
AND recovery_at_millis IS NOT NULL AND recovery_at_millis<=${nowMillis} AND ordinal>${after}
|
|
3911
|
+
ORDER BY ordinal LIMIT ${limit}
|
|
3912
|
+
`, "recovering subscriptions");
|
|
3913
|
+
const rowSchema = Schema.Struct({
|
|
3914
|
+
owner_id: Schema.String,
|
|
3915
|
+
subscription_id: Schema.String,
|
|
3916
|
+
ordinal: Schema.Natural
|
|
3917
|
+
});
|
|
3918
|
+
return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(Effect.map((items) => items.map((item) => ({
|
|
3919
|
+
key: {
|
|
3920
|
+
partition,
|
|
3921
|
+
ownerId: item.owner_id,
|
|
3922
|
+
subscriptionId: item.subscription_id
|
|
3923
|
+
},
|
|
3924
|
+
ordinal: item.ordinal
|
|
3925
|
+
}))));
|
|
3926
|
+
});
|
|
3927
|
+
const deferRecovery = Effect.fn("SqliteSubscriptionStore.deferRecovery")(function* (input, recovery) {
|
|
3928
|
+
const key = yield* requireKey(input, "defer-recovery-key");
|
|
3929
|
+
yield* transact(Effect.gen(function* () {
|
|
3930
|
+
const record = yield* readRegistration(key, "defer recovery");
|
|
3931
|
+
if (record === null) return yield* error("not-found", "subscription");
|
|
3932
|
+
yield* failpoint.hit("subscription:defer-recovery:before");
|
|
3933
|
+
yield* writeRegistration({
|
|
3934
|
+
...record,
|
|
3935
|
+
recovery: record.state === "active" ? recovery : null
|
|
3936
|
+
});
|
|
3937
|
+
}));
|
|
3938
|
+
yield* failpoint.hit("subscription:defer-recovery:after");
|
|
3939
|
+
});
|
|
3940
|
+
const readScanCursors = Effect.gen(function* () {
|
|
3941
|
+
const rows = yield* query(sql`
|
|
3942
|
+
SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
3943
|
+
FROM effect_agent_subscription_sequences
|
|
3944
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3945
|
+
`, "read subscription scan cursors");
|
|
3946
|
+
const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
|
|
3947
|
+
if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
|
|
3948
|
+
return {
|
|
3949
|
+
events: decoded[0].event_scan_cursor,
|
|
3950
|
+
deliveries: decoded[0].delivery_scan_cursor,
|
|
3951
|
+
recovery: decoded[0].recovery_scan_cursor
|
|
3952
|
+
};
|
|
3953
|
+
});
|
|
3954
|
+
const advanceScanCursors = Effect.fn("SqliteSubscriptionStore.advanceScanCursors")(function* (input) {
|
|
3955
|
+
const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
|
|
3956
|
+
yield* transact(Effect.gen(function* () {
|
|
3957
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:before");
|
|
3958
|
+
yield* query(sql`
|
|
3959
|
+
UPDATE effect_agent_subscription_sequences
|
|
3960
|
+
SET event_scan_cursor=${cursors.events}, delivery_scan_cursor=${cursors.deliveries}, recovery_scan_cursor=${cursors.recovery}
|
|
3961
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
3962
|
+
`, "advance subscription scan cursors");
|
|
3963
|
+
}));
|
|
3964
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:after");
|
|
3965
|
+
});
|
|
3966
|
+
const indexedDeadline = query(sql`
|
|
3967
|
+
SELECT MIN(deadline) AS deadline FROM (
|
|
3968
|
+
SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
|
|
3969
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
|
|
3970
|
+
UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
|
|
3971
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
3972
|
+
UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
|
|
3973
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
|
|
3974
|
+
)
|
|
3975
|
+
`, "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"))));
|
|
3976
|
+
const nextDeadline = Effect.gen(function* () {
|
|
3977
|
+
const cursors = yield* readScanCursors;
|
|
3978
|
+
if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
|
|
3979
|
+
return yield* indexedDeadline;
|
|
3980
|
+
});
|
|
3981
|
+
return SubscriptionStore.of({
|
|
3982
|
+
partition,
|
|
3983
|
+
register,
|
|
3984
|
+
get,
|
|
3985
|
+
list,
|
|
3986
|
+
cancel,
|
|
3987
|
+
accept,
|
|
3988
|
+
event,
|
|
3989
|
+
pendingEvents,
|
|
3990
|
+
candidates,
|
|
3991
|
+
select,
|
|
3992
|
+
catchUp,
|
|
3993
|
+
deferEvent,
|
|
3994
|
+
delivery,
|
|
3995
|
+
pendingDeliveries,
|
|
3996
|
+
listDeliveries,
|
|
3997
|
+
changeDelivery,
|
|
3998
|
+
recovering,
|
|
3999
|
+
deferRecovery,
|
|
4000
|
+
readScanCursors,
|
|
4001
|
+
advanceScanCursors,
|
|
4002
|
+
nextDeadline
|
|
4003
|
+
});
|
|
4004
|
+
});
|
|
4005
|
+
const subscriptionStoreLayer = (partition) => Layer.effect(SubscriptionStore, makeSubscriptionStore(partition));
|
|
4006
|
+
//#endregion
|
|
4007
|
+
export { CurrentSqliteStorageVersion, SqliteAppendConflict, SqliteCheckpointConflict, SqliteFenceRejected, SqliteLedgerError, SqliteStorageCompatibilityError, SqliteStorageConfig, SqliteStorageConfigValue, SqliteStorageCorruptionError, SqliteStorageError, SqliteStorageFailpoint, SqliteStorageFailpointError, SqliteStorageFailpointLocation, SqliteWriteContention, layer, ledgerLayer, observationOffsetAt, scheduleStoreLayer, storageConfigLayer, storageFailpointLayer, submissionLedgerLayer, subscriptionStoreLayer, threadStoreLayer };
|
|
3365
4008
|
|
|
3366
4009
|
//# sourceMappingURL=index.mjs.map
|