@effect-agent/storage-memory 0.1.0-beta.38 → 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 +9 -6
- package/dist/index.mjs +752 -160
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +2 -2
- package/dist/testing.mjs +2 -2
- package/package.json +8 -5
- package/src/index.ts +1 -0
- package/src/memory-ledger.ts +28 -35
- package/src/memory-schedule-store.ts +1 -1
- package/src/memory-storage.ts +336 -366
- package/src/memory-subscription-store.ts +921 -0
- package/src/testing.ts +7 -6
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AttemptId,
|
|
2
|
-
import { AbortCommand, AbortIntent, AdmissionAdmitted, AdmissionConflict, AdmissionIndeterminate, AdmissionNotAdmitted, AdmissionRequest, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildSettledNotification, Claim, ClaimJoiningRequest, ClaimRequest,
|
|
1
|
+
import { AttemptId, ReceiptId, SubmissionId, ThreadId } from "@effect-agent/core";
|
|
2
|
+
import { AbortCommand, AbortIntent, AcceptedEvent, AdmissionAdmitted, AdmissionConflict, AdmissionIndeterminate, AdmissionNotAdmitted, AdmissionRequest, AdmissionResult, AppendConflict, AppendResult, ApprovalConflict, ApprovalDecisionCommand, ApprovalDecisionIntent, AttachChildToReservationRequest, BeginChildBudgetReleaseRequest, CanonicalRecordEnvelope, CanonicalSequence, CheckpointRejected, ChildAttachmentSnapshot, ChildBudgetReservationRequest, ChildBudgetReservationSnapshot, ChildReservationConflict, ChildSettledNotification, Claim, ClaimJoiningRequest, ClaimRequest, DEFAULT_OWNERSHIP_LEASE_DURATION, DeliveryChange, Digest, EMPTY_TAIL_DIGEST, FenceRejected, FencedAppendRequest, InputAppliedMarker, JoinSnapshot, JoinedToHost, JoiningClaim, LedgerCapabilities, LedgerError, LoadCheckpointRequest, MarkInputAppliedRequest, MarkJoinedRequest, MarkReadyRequest, MarkUnknownRequest, ObservationOffset, OwnershipLost, OwnershipRenewal, OwnershipSnapshot, OwnershipToken, PersistedJson, ProducerEpoch, QueueSequence, RecoverySnapshot, RecoverySnapshotRequest, ReleaseChildBudgetRequest, ReleaseOwnershipRequest, RenewOwnershipRequest, ReservedChildBudget, ReservedSettlement, RevertJoiningRequest, SaveCheckpointRequest, ScheduleCapacityError, ScheduleChange, ScheduleConflict, ScheduleDueCursor, ScheduleFailpoint, ScheduleKey, ScheduleNotFound, ScheduleOwner, SchedulePageRequest, ScheduleRecord, ScheduleStorageError, ScheduleStore, Settlement, SettlementConflict, SettlementFinalization, SettlementReservation, SettlementReservationSnapshot, SourcePartition, SubmissionLedger, SubmissionLookup, SubmissionLookupByKey, SubmissionSnapshot, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionScanCursors, SubscriptionStore, SuspendRequest, SuspensionSnapshot, ThreadExport, ThreadExportRequest, ThreadMaterialization, ThreadNotMaterialized, ThreadObservation, ThreadRead, ThreadStore, ThreadStoreError, ThreadTail, ThreadTailRequest, UnknownResolution, UnknownResolutionCommand, UnknownResolutionConflict, UnknownResolutionIntent, applyScheduleChange, applySubscriptionDeliveryChange, compareScheduleKeys, compareScheduleNames, defaultSchedulingLimits, digestCanonicalBatch, scheduleDeadline, scheduleKeyOf, scheduleKeyString, scheduleOwnerKey, scheduleUsesCapacity, settlementFailureFromRecord, subscriptionCanSelect, subscriptionDeliveryCanSelect, subscriptionDeliveryKeyString, subscriptionKeyString } from "@effect-agent/thread";
|
|
3
3
|
import { Clock, Crypto, DateTime, Duration, Effect, Encoding, Layer, Option, PubSub, Ref, Result, Schema, Stream } from "effect";
|
|
4
4
|
//#region src/memory-ledger.ts
|
|
5
5
|
const MAX_SUBMISSIONS = 65536;
|
|
@@ -46,7 +46,7 @@ const ledgerError = (operation, message, cause) => cause === void 0 ? LedgerErro
|
|
|
46
46
|
message,
|
|
47
47
|
cause
|
|
48
48
|
});
|
|
49
|
-
const validate$
|
|
49
|
+
const validate$2 = Effect.fn("MemorySubmissionLedger.validate")((schema, operation, value) => Schema.encodeUnknownEffect(schema)(value).pipe(Effect.flatMap(Schema.decodeUnknownEffect(schema)), Effect.mapError((error) => ledgerError(operation, `Invalid ${operation} request`, error))));
|
|
50
50
|
const decodeSubmissionId = Schema.decodeSync(SubmissionId);
|
|
51
51
|
const decodeReceiptId = Schema.decodeSync(ReceiptId);
|
|
52
52
|
const decodeAttemptId = Schema.decodeSync(AttemptId);
|
|
@@ -56,10 +56,10 @@ const decodeProducerEpoch = Schema.decodeSync(ProducerEpoch);
|
|
|
56
56
|
const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
|
|
57
57
|
const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
|
|
58
58
|
const utc = (millis) => DateTime.toUtc(DateTime.makeUnsafe(millis));
|
|
59
|
-
const admissionKey = (
|
|
59
|
+
const admissionKey = (threadId, principal, idempotencyKey) => `${threadId}\u001f${principal}\u001f${idempotencyKey}`;
|
|
60
60
|
const toSnapshot = (row) => SubmissionSnapshot.make({
|
|
61
61
|
submissionId: row.submissionId,
|
|
62
|
-
|
|
62
|
+
threadId: row.threadId,
|
|
63
63
|
queueSequence: row.queueSequence,
|
|
64
64
|
principal: row.principal,
|
|
65
65
|
idempotencyKey: row.idempotencyKey,
|
|
@@ -90,10 +90,10 @@ const toReservationSnapshot = (row) => ChildBudgetReservationSnapshot.make({
|
|
|
90
90
|
});
|
|
91
91
|
/** Linkage equality: both absent, or both present naming the same parent Tool Call. */
|
|
92
92
|
const sameParentLinkage = (left, right) => left === void 0 ? right === void 0 : right !== void 0 && left.parentSubmissionId === right.parentSubmissionId && left.parentToolCallId === right.parentToolCallId;
|
|
93
|
-
const laneEpoch = (state,
|
|
93
|
+
const laneEpoch = (state, threadId) => state.lanes.get(threadId)?.producerEpoch ?? 0;
|
|
94
94
|
const ownershipLost = (state, stored) => OwnershipLost.make({
|
|
95
95
|
submissionId: stored.row.submissionId,
|
|
96
|
-
actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.
|
|
96
|
+
actualEpoch: decodeProducerEpoch(laneEpoch(state, stored.row.threadId))
|
|
97
97
|
});
|
|
98
98
|
/** The presented token owns the lane only while it matches the live ownership record. */
|
|
99
99
|
const ownsLane = (stored, ownershipToken) => stored.ownership !== void 0 && stored.ownership.ownershipToken === ownershipToken;
|
|
@@ -105,10 +105,10 @@ const withChildReservation = (state, reservation) => ({
|
|
|
105
105
|
...state,
|
|
106
106
|
childReservations: new Map(state.childReservations).set(reservation.reservationId, reservation)
|
|
107
107
|
});
|
|
108
|
-
const findHead = (state,
|
|
108
|
+
const findHead = (state, threadId) => {
|
|
109
109
|
let head;
|
|
110
110
|
for (const stored of state.submissions.values()) {
|
|
111
|
-
if (stored.row.
|
|
111
|
+
if (stored.row.threadId !== threadId || stored.row.state === "settled") continue;
|
|
112
112
|
if (head === void 0 || stored.row.queueSequence < head.row.queueSequence) head = stored;
|
|
113
113
|
}
|
|
114
114
|
return head;
|
|
@@ -158,16 +158,16 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
158
158
|
const leaseMillis = Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION);
|
|
159
159
|
const capabilities = Effect.succeed(LedgerCapabilities.make({ durability: "non-durable" }));
|
|
160
160
|
const admit = Effect.fn("MemorySubmissionLedger.admit")((unvalidated) => Effect.gen(function* () {
|
|
161
|
-
const request = yield* validate$
|
|
161
|
+
const request = yield* validate$2(AdmissionRequest, "admit", unvalidated);
|
|
162
162
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
163
163
|
const decision = yield* Ref.modify(state, (current) => {
|
|
164
|
-
const key = admissionKey(request.
|
|
164
|
+
const key = admissionKey(request.threadId, request.principal, request.idempotencyKey);
|
|
165
165
|
const existingId = current.admissionIndex.get(key);
|
|
166
166
|
if (existingId !== void 0) {
|
|
167
167
|
const existing = current.submissions.get(existingId);
|
|
168
168
|
if (existing === void 0) return [failure(ledgerError("admit", "Admission index references a missing Submission")), current];
|
|
169
169
|
if (existing.row.inputDigest !== request.inputDigest || !sameParentLinkage(existing.row.parentLinkage, request.parentLinkage)) return [failure(AdmissionConflict.make({
|
|
170
|
-
|
|
170
|
+
threadId: request.threadId,
|
|
171
171
|
principal: request.principal,
|
|
172
172
|
idempotencyKey: request.idempotencyKey,
|
|
173
173
|
existingInputDigest: existing.row.inputDigest,
|
|
@@ -182,14 +182,14 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
182
182
|
})), current];
|
|
183
183
|
}
|
|
184
184
|
if (current.submissions.size >= MAX_SUBMISSIONS) return [failure(ledgerError("admit", `In-memory submission limit ${MAX_SUBMISSIONS} exceeded`)), current];
|
|
185
|
-
const lane = current.lanes.get(request.
|
|
185
|
+
const lane = current.lanes.get(request.threadId) ?? {
|
|
186
186
|
nextQueueSequence: 1,
|
|
187
187
|
producerEpoch: 0
|
|
188
188
|
};
|
|
189
189
|
const mintCounter = current.mintCounter + 1;
|
|
190
190
|
const row = {
|
|
191
191
|
submissionId: decodeSubmissionId(`submission-memory-${mintCounter}`),
|
|
192
|
-
|
|
192
|
+
threadId: request.threadId,
|
|
193
193
|
queueSequence: decodeQueueSequence(lane.nextQueueSequence),
|
|
194
194
|
principal: request.principal,
|
|
195
195
|
idempotencyKey: request.idempotencyKey,
|
|
@@ -218,7 +218,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
218
218
|
unknownResolutions: /* @__PURE__ */ new Map()
|
|
219
219
|
});
|
|
220
220
|
const admissionIndex = new Map(current.admissionIndex).set(key, row.submissionId);
|
|
221
|
-
const lanes = new Map(current.lanes).set(request.
|
|
221
|
+
const lanes = new Map(current.lanes).set(request.threadId, {
|
|
222
222
|
nextQueueSequence: lane.nextQueueSequence + 1,
|
|
223
223
|
producerEpoch: lane.producerEpoch
|
|
224
224
|
});
|
|
@@ -240,7 +240,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
240
240
|
return decision.value;
|
|
241
241
|
}));
|
|
242
242
|
const markReady = Effect.fn("MemorySubmissionLedger.markReady")((unvalidated) => Effect.gen(function* () {
|
|
243
|
-
const request = yield* validate$
|
|
243
|
+
const request = yield* validate$2(MarkReadyRequest, "markReady", unvalidated);
|
|
244
244
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
245
245
|
const decision = yield* Ref.modify(state, (current) => {
|
|
246
246
|
const stored = current.submissions.get(request.submissionId);
|
|
@@ -258,33 +258,33 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
258
258
|
if (decision._tag === "failure") return yield* decision.error;
|
|
259
259
|
}));
|
|
260
260
|
const lookup = Effect.fn("MemorySubmissionLedger.lookup")((unvalidated) => Effect.gen(function* () {
|
|
261
|
-
const request = yield* validate$
|
|
261
|
+
const request = yield* validate$2(SubmissionLookup, "lookup", unvalidated);
|
|
262
262
|
const current = yield* Ref.get(state);
|
|
263
|
-
const submissionId = request._tag === "SubmissionLookupById" ? request.submissionId : current.admissionIndex.get(admissionKey(request.
|
|
263
|
+
const submissionId = request._tag === "SubmissionLookupById" ? request.submissionId : current.admissionIndex.get(admissionKey(request.threadId, request.principal, request.idempotencyKey));
|
|
264
264
|
const stored = submissionId === void 0 ? void 0 : current.submissions.get(submissionId);
|
|
265
265
|
return stored === void 0 ? Option.none() : Option.some(toSnapshot(stored.row));
|
|
266
266
|
}));
|
|
267
267
|
const resolveAdmission = Effect.fn("MemorySubmissionLedger.resolveAdmission")((unvalidated) => Effect.gen(function* () {
|
|
268
|
-
const request = yield* validate$
|
|
268
|
+
const request = yield* validate$2(SubmissionLookupByKey, "resolveAdmission", unvalidated);
|
|
269
269
|
if (options.resolveAdmissionFault !== void 0) {
|
|
270
270
|
const fault = yield* options.resolveAdmissionFault;
|
|
271
271
|
if (Option.isSome(fault)) return AdmissionIndeterminate.make({ reason: fault.value });
|
|
272
272
|
}
|
|
273
273
|
const current = yield* Ref.get(state);
|
|
274
|
-
const submissionId = current.admissionIndex.get(admissionKey(request.
|
|
274
|
+
const submissionId = current.admissionIndex.get(admissionKey(request.threadId, request.principal, request.idempotencyKey));
|
|
275
275
|
const stored = submissionId === void 0 ? void 0 : current.submissions.get(submissionId);
|
|
276
276
|
return stored === void 0 ? AdmissionNotAdmitted.make() : AdmissionAdmitted.make({ submission: toSnapshot(stored.row) });
|
|
277
277
|
}));
|
|
278
278
|
const claim = Effect.fn("MemorySubmissionLedger.claim")((unvalidated) => Effect.gen(function* () {
|
|
279
|
-
const request = yield* validate$
|
|
279
|
+
const request = yield* validate$2(ClaimRequest, "claim", unvalidated);
|
|
280
280
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
281
281
|
const decision = yield* Ref.modify(state, (current) => {
|
|
282
|
-
const head = findHead(current, request.
|
|
282
|
+
const head = findHead(current, request.threadId);
|
|
283
283
|
if (head === void 0) return [success(Option.none()), current];
|
|
284
284
|
if (BLOCKED_HEAD_STATES.has(head.row.state) || head.row.state === "unknown" && head.abortIntent === void 0) return [success(Option.none()), current];
|
|
285
285
|
if (head.ownership !== void 0 && head.ownership.leaseExpiresAtMillis > nowMillis && head.ownership.ownerProducerId !== request.producerId) return [success(Option.none()), current];
|
|
286
|
-
const lane = current.lanes.get(request.
|
|
287
|
-
if (lane === void 0) return [failure(ledgerError("claim", "Claimable head without a
|
|
286
|
+
const lane = current.lanes.get(request.threadId);
|
|
287
|
+
if (lane === void 0) return [failure(ledgerError("claim", "Claimable head without a Thread lane")), current];
|
|
288
288
|
const producerEpoch = decodeProducerEpoch(lane.producerEpoch + 1);
|
|
289
289
|
const mintCounter = current.mintCounter + 1;
|
|
290
290
|
const ownership = {
|
|
@@ -303,7 +303,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
303
303
|
row,
|
|
304
304
|
ownership
|
|
305
305
|
});
|
|
306
|
-
const lanes = new Map(next.lanes).set(request.
|
|
306
|
+
const lanes = new Map(next.lanes).set(request.threadId, {
|
|
307
307
|
nextQueueSequence: lane.nextQueueSequence,
|
|
308
308
|
producerEpoch: lane.producerEpoch + 1
|
|
309
309
|
});
|
|
@@ -324,7 +324,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
324
324
|
return decision.value;
|
|
325
325
|
}));
|
|
326
326
|
const renewOwnership = Effect.fn("MemorySubmissionLedger.renewOwnership")((unvalidated) => Effect.gen(function* () {
|
|
327
|
-
const request = yield* validate$
|
|
327
|
+
const request = yield* validate$2(RenewOwnershipRequest, "renewOwnership", unvalidated);
|
|
328
328
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
329
329
|
const decision = yield* Ref.modify(state, (current) => {
|
|
330
330
|
const stored = current.submissions.get(request.submissionId);
|
|
@@ -346,7 +346,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
346
346
|
return decision.value;
|
|
347
347
|
}));
|
|
348
348
|
const releaseOwnership = Effect.fn("MemorySubmissionLedger.releaseOwnership")((unvalidated) => Effect.gen(function* () {
|
|
349
|
-
const request = yield* validate$
|
|
349
|
+
const request = yield* validate$2(ReleaseOwnershipRequest, "releaseOwnership", unvalidated);
|
|
350
350
|
const decision = yield* Ref.modify(state, (current) => {
|
|
351
351
|
const stored = current.submissions.get(request.submissionId);
|
|
352
352
|
if (stored === void 0) return [failure(ledgerError("releaseOwnership", `Unknown Submission ${request.submissionId}`)), current];
|
|
@@ -359,7 +359,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
359
359
|
if (decision._tag === "failure") return yield* decision.error;
|
|
360
360
|
}));
|
|
361
361
|
const markInputApplied = Effect.fn("MemorySubmissionLedger.markInputApplied")((unvalidated) => Effect.gen(function* () {
|
|
362
|
-
const request = yield* validate$
|
|
362
|
+
const request = yield* validate$2(MarkInputAppliedRequest, "markInputApplied", unvalidated);
|
|
363
363
|
const decision = yield* Ref.modify(state, (current) => {
|
|
364
364
|
const stored = current.submissions.get(request.submissionId);
|
|
365
365
|
if (stored === void 0) return [failure(ledgerError("markInputApplied", `Unknown Submission ${request.submissionId}`)), current];
|
|
@@ -381,7 +381,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
381
381
|
if (decision._tag === "failure") return yield* decision.error;
|
|
382
382
|
}));
|
|
383
383
|
const reserveSettlement = Effect.fn("MemorySubmissionLedger.reserveSettlement")((unvalidated) => Effect.gen(function* () {
|
|
384
|
-
const request = yield* validate$
|
|
384
|
+
const request = yield* validate$2(SettlementReservation, "reserveSettlement", unvalidated);
|
|
385
385
|
const decision = yield* Ref.modify(state, (current) => {
|
|
386
386
|
const stored = current.submissions.get(request.submissionId);
|
|
387
387
|
if (stored === void 0) return [failure(ledgerError("reserveSettlement", `Unknown Submission ${request.submissionId}`)), current];
|
|
@@ -431,7 +431,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
431
431
|
return decision.value;
|
|
432
432
|
}));
|
|
433
433
|
const finalizeSettlement = Effect.fn("MemorySubmissionLedger.finalizeSettlement")((unvalidated) => Effect.gen(function* () {
|
|
434
|
-
const request = yield* validate$
|
|
434
|
+
const request = yield* validate$2(SettlementFinalization, "finalizeSettlement", unvalidated);
|
|
435
435
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
436
436
|
const decision = yield* Ref.modify(state, (current) => {
|
|
437
437
|
const stored = current.submissions.get(request.submissionId);
|
|
@@ -478,7 +478,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
478
478
|
return decision.value;
|
|
479
479
|
}));
|
|
480
480
|
const requestAbort = Effect.fn("MemorySubmissionLedger.requestAbort")((unvalidated) => Effect.gen(function* () {
|
|
481
|
-
const request = yield* validate$
|
|
481
|
+
const request = yield* validate$2(AbortCommand, "requestAbort", unvalidated);
|
|
482
482
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
483
483
|
const decision = yield* Ref.modify(state, (current) => {
|
|
484
484
|
const stored = current.submissions.get(request.submissionId);
|
|
@@ -513,13 +513,13 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
513
513
|
return decision.value;
|
|
514
514
|
}));
|
|
515
515
|
const claimJoining = Effect.fn("MemorySubmissionLedger.claimJoining")((unvalidated) => Effect.gen(function* () {
|
|
516
|
-
const request = yield* validate$
|
|
516
|
+
const request = yield* validate$2(ClaimJoiningRequest, "claimJoining", unvalidated);
|
|
517
517
|
const decision = yield* Ref.modify(state, (current) => {
|
|
518
518
|
const host = current.submissions.get(request.hostSubmissionId);
|
|
519
519
|
if (host === void 0) return [failure(ledgerError("claimJoining", `Unknown Submission ${request.hostSubmissionId}`)), current];
|
|
520
|
-
if (host.row.
|
|
520
|
+
if (host.row.threadId !== request.threadId) return [failure(ledgerError("claimJoining", `Host Submission ${request.hostSubmissionId} does not belong to Thread ${request.threadId}`)), current];
|
|
521
521
|
if (!ownsLane(host, request.ownershipToken)) return [failure(ownershipLost(current, host)), current];
|
|
522
|
-
const later = [...current.submissions.values()].filter((stored) => stored.row.
|
|
522
|
+
const later = [...current.submissions.values()].filter((stored) => stored.row.threadId === request.threadId && stored.row.queueSequence > host.row.queueSequence).sort((left, right) => left.row.queueSequence - right.row.queueSequence);
|
|
523
523
|
const claims = [];
|
|
524
524
|
const submissions = new Map(current.submissions);
|
|
525
525
|
for (const stored of later) {
|
|
@@ -550,7 +550,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
550
550
|
return decision.value;
|
|
551
551
|
}));
|
|
552
552
|
const markJoined = Effect.fn("MemorySubmissionLedger.markJoined")((unvalidated) => Effect.gen(function* () {
|
|
553
|
-
const request = yield* validate$
|
|
553
|
+
const request = yield* validate$2(MarkJoinedRequest, "markJoined", unvalidated);
|
|
554
554
|
const decision = yield* Ref.modify(state, (current) => {
|
|
555
555
|
const stored = current.submissions.get(request.submissionId);
|
|
556
556
|
if (stored === void 0) return [failure(ledgerError("markJoined", `Unknown Submission ${request.submissionId}`)), current];
|
|
@@ -579,7 +579,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
579
579
|
if (decision._tag === "failure") return yield* decision.error;
|
|
580
580
|
}));
|
|
581
581
|
const revertJoining = Effect.fn("MemorySubmissionLedger.revertJoining")((unvalidated) => Effect.gen(function* () {
|
|
582
|
-
const request = yield* validate$
|
|
582
|
+
const request = yield* validate$2(RevertJoiningRequest, "revertJoining", unvalidated);
|
|
583
583
|
const decision = yield* Ref.modify(state, (current) => {
|
|
584
584
|
const stored = current.submissions.get(request.submissionId);
|
|
585
585
|
if (stored === void 0) return [failure(ledgerError("revertJoining", `Unknown Submission ${request.submissionId}`)), current];
|
|
@@ -596,7 +596,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
596
596
|
if (decision._tag === "failure") return yield* decision.error;
|
|
597
597
|
}));
|
|
598
598
|
const suspend = Effect.fn("MemorySubmissionLedger.suspend")((unvalidated) => Effect.gen(function* () {
|
|
599
|
-
const request = yield* validate$
|
|
599
|
+
const request = yield* validate$2(SuspendRequest, "suspend", unvalidated);
|
|
600
600
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
601
601
|
const decision = yield* Ref.modify(state, (current) => {
|
|
602
602
|
const stored = current.submissions.get(request.submissionId);
|
|
@@ -631,7 +631,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
631
631
|
return decision.value;
|
|
632
632
|
}));
|
|
633
633
|
const recordApprovalDecision = Effect.fn("MemorySubmissionLedger.recordApprovalDecision")((unvalidated) => Effect.gen(function* () {
|
|
634
|
-
const command = yield* validate$
|
|
634
|
+
const command = yield* validate$2(ApprovalDecisionCommand, "recordApprovalDecision", unvalidated);
|
|
635
635
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
636
636
|
const decision = yield* Ref.modify(state, (current) => {
|
|
637
637
|
const stored = current.submissions.get(command.submissionId);
|
|
@@ -676,7 +676,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
676
676
|
return decision.value;
|
|
677
677
|
}));
|
|
678
678
|
const markUnknown = Effect.fn("MemorySubmissionLedger.markUnknown")((unvalidated) => Effect.gen(function* () {
|
|
679
|
-
const request = yield* validate$
|
|
679
|
+
const request = yield* validate$2(MarkUnknownRequest, "markUnknown", unvalidated);
|
|
680
680
|
const decision = yield* Ref.modify(state, (current) => {
|
|
681
681
|
const stored = current.submissions.get(request.submissionId);
|
|
682
682
|
if (stored === void 0) return [failure(ledgerError("markUnknown", `Unknown Submission ${request.submissionId}`)), current];
|
|
@@ -709,7 +709,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
709
709
|
if (decision._tag === "failure") return yield* decision.error;
|
|
710
710
|
}));
|
|
711
711
|
const recordUnknownResolution = Effect.fn("MemorySubmissionLedger.recordUnknownResolution")((unvalidated) => Effect.gen(function* () {
|
|
712
|
-
const command = yield* validate$
|
|
712
|
+
const command = yield* validate$2(UnknownResolutionCommand, "recordUnknownResolution", unvalidated);
|
|
713
713
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
714
714
|
const decision = yield* Ref.modify(state, (current) => {
|
|
715
715
|
const stored = current.submissions.get(command.submissionId);
|
|
@@ -750,7 +750,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
750
750
|
return decision.value;
|
|
751
751
|
}));
|
|
752
752
|
const recordChildSettled = Effect.fn("MemorySubmissionLedger.recordChildSettled")((unvalidated) => Effect.gen(function* () {
|
|
753
|
-
const request = yield* validate$
|
|
753
|
+
const request = yield* validate$2(ChildSettledNotification, "recordChildSettled", unvalidated);
|
|
754
754
|
const decision = yield* Ref.modify(state, (current) => {
|
|
755
755
|
const parent = current.submissions.get(request.parentSubmissionId);
|
|
756
756
|
if (parent === void 0) return [failure(ledgerError("recordChildSettled", `Unknown Submission ${request.parentSubmissionId}`)), current];
|
|
@@ -776,7 +776,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
776
776
|
return decision.value;
|
|
777
777
|
}));
|
|
778
778
|
const reserveChildBudget = Effect.fn("MemorySubmissionLedger.reserveChildBudget")((unvalidated) => Effect.gen(function* () {
|
|
779
|
-
const request = yield* validate$
|
|
779
|
+
const request = yield* validate$2(ChildBudgetReservationRequest, "reserveChildBudget", unvalidated);
|
|
780
780
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
781
781
|
const decision = yield* Ref.modify(state, (current) => {
|
|
782
782
|
const existing = current.childReservations.get(request.reservationId);
|
|
@@ -821,7 +821,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
821
821
|
return decision.value;
|
|
822
822
|
}));
|
|
823
823
|
const attachChildToReservation = Effect.fn("MemorySubmissionLedger.attachChildToReservation")((unvalidated) => Effect.gen(function* () {
|
|
824
|
-
const request = yield* validate$
|
|
824
|
+
const request = yield* validate$2(AttachChildToReservationRequest, "attachChildToReservation", unvalidated);
|
|
825
825
|
const decision = yield* Ref.modify(state, (current) => {
|
|
826
826
|
const reservation = current.childReservations.get(request.reservationId);
|
|
827
827
|
if (reservation === void 0) return [failure(ledgerError("attachChildToReservation", `Unknown child reservation ${request.reservationId}`)), current];
|
|
@@ -852,7 +852,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
852
852
|
return decision.value;
|
|
853
853
|
}));
|
|
854
854
|
const beginChildBudgetRelease = Effect.fn("MemorySubmissionLedger.beginChildBudgetRelease")((unvalidated) => Effect.gen(function* () {
|
|
855
|
-
const request = yield* validate$
|
|
855
|
+
const request = yield* validate$2(BeginChildBudgetReleaseRequest, "beginChildBudgetRelease", unvalidated);
|
|
856
856
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
857
857
|
const decision = yield* Ref.modify(state, (current) => {
|
|
858
858
|
const reservation = current.childReservations.get(request.reservationId);
|
|
@@ -877,7 +877,7 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
877
877
|
return decision.value;
|
|
878
878
|
}));
|
|
879
879
|
const releaseChildBudget = Effect.fn("MemorySubmissionLedger.releaseChildBudget")((unvalidated) => Effect.gen(function* () {
|
|
880
|
-
const request = yield* validate$
|
|
880
|
+
const request = yield* validate$2(ReleaseChildBudgetRequest, "releaseChildBudget", unvalidated);
|
|
881
881
|
const nowMillis = yield* Clock.currentTimeMillis;
|
|
882
882
|
const decision = yield* Ref.modify(state, (current) => {
|
|
883
883
|
const reservation = current.childReservations.get(request.reservationId);
|
|
@@ -899,11 +899,11 @@ const makeSubmissionLedger = (options = {}) => Effect.gen(function* () {
|
|
|
899
899
|
return decision.value;
|
|
900
900
|
}));
|
|
901
901
|
const scanNonterminal = Stream.unwrap(Ref.get(state).pipe(Effect.map((current) => {
|
|
902
|
-
const snapshots = [...current.submissions.values()].filter((stored) => stored.row.state !== "settled").sort((left, right) => left.row.
|
|
902
|
+
const snapshots = [...current.submissions.values()].filter((stored) => stored.row.state !== "settled").sort((left, right) => left.row.threadId < right.row.threadId ? -1 : left.row.threadId > right.row.threadId ? 1 : left.row.queueSequence - right.row.queueSequence).map((stored) => toSnapshot(stored.row));
|
|
903
903
|
return Stream.fromIterable(snapshots);
|
|
904
904
|
})));
|
|
905
905
|
const loadRecoverySnapshot = Effect.fn("MemorySubmissionLedger.loadRecoverySnapshot")((unvalidated) => Effect.gen(function* () {
|
|
906
|
-
const request = yield* validate$
|
|
906
|
+
const request = yield* validate$2(RecoverySnapshotRequest, "loadRecoverySnapshot", unvalidated);
|
|
907
907
|
const current = yield* Ref.get(state);
|
|
908
908
|
const stored = current.submissions.get(request.submissionId);
|
|
909
909
|
if (stored === void 0) return yield* ledgerError("loadRecoverySnapshot", `Unknown Submission ${request.submissionId}`);
|
|
@@ -1150,77 +1150,667 @@ const makeScheduleStore = Effect.gen(function* () {
|
|
|
1150
1150
|
const memoryScheduleStoreLayer = () => Layer.effect(ScheduleStore, makeScheduleStore);
|
|
1151
1151
|
const MemoryScheduleStoreLive = memoryScheduleStoreLayer();
|
|
1152
1152
|
//#endregion
|
|
1153
|
+
//#region src/memory-subscription-store.ts
|
|
1154
|
+
const error = (reason, code) => SubscriptionError.make({
|
|
1155
|
+
reason,
|
|
1156
|
+
code
|
|
1157
|
+
});
|
|
1158
|
+
const samePartition = (left, right) => left.tenantId === right.tenantId && left.address === right.address;
|
|
1159
|
+
const sameSource = (left, right) => left.name === right.name && left.version === right.version;
|
|
1160
|
+
const encode = (schema, value, code) => Result.try({
|
|
1161
|
+
try: () => Schema.encodeSync(Schema.fromJsonString(schema))(value),
|
|
1162
|
+
catch: () => error("corrupt", code)
|
|
1163
|
+
});
|
|
1164
|
+
const decode = (schema, value, code) => Result.try({
|
|
1165
|
+
try: () => Schema.decodeSync(Schema.fromJsonString(schema))(value),
|
|
1166
|
+
catch: () => error("corrupt", code)
|
|
1167
|
+
});
|
|
1168
|
+
const decodeEffect = (schema, value, code) => Effect.fromResult(decode(schema, value, code));
|
|
1169
|
+
const validate$1 = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
|
|
1170
|
+
const jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
1171
|
+
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;
|
|
1172
|
+
const candidateIndexKey = (record) => JSON.stringify([
|
|
1173
|
+
record.configuration.source.name,
|
|
1174
|
+
record.configuration.source.version,
|
|
1175
|
+
record.configuration.matchingKey
|
|
1176
|
+
]);
|
|
1177
|
+
const eventCandidateIndexKey = (event) => JSON.stringify([
|
|
1178
|
+
event.source.name,
|
|
1179
|
+
event.source.version,
|
|
1180
|
+
event.matchingKey
|
|
1181
|
+
]);
|
|
1182
|
+
const sameEventIdentity = (left, right) => samePartition(left.partition, right.partition) && left.eventId === right.eventId && sameSource(left.source, right.source) && left.matchingKey === right.matchingKey && left.payloadDigest === right.payloadDigest;
|
|
1183
|
+
const deliveryBelongsTo = (delivery, record, event) => subscriptionKeyString(delivery.key.subscription) === subscriptionKeyString(record.key) && delivery.key.eventId === event.eventId && sameSource(delivery.source, event.source);
|
|
1184
|
+
const makeMemorySubscriptionStore = Effect.fn("makeMemorySubscriptionStore")(function* (ownedPartition) {
|
|
1185
|
+
const partition = yield* validate$1(SourcePartition, ownedPartition, "partition");
|
|
1186
|
+
const state = yield* Ref.make({
|
|
1187
|
+
sequence: 0,
|
|
1188
|
+
registrations: /* @__PURE__ */ new Map(),
|
|
1189
|
+
events: /* @__PURE__ */ new Map(),
|
|
1190
|
+
deliveries: /* @__PURE__ */ new Map(),
|
|
1191
|
+
registrationIndex: /* @__PURE__ */ new Map(),
|
|
1192
|
+
candidateIndex: /* @__PURE__ */ new Map(),
|
|
1193
|
+
ownerRegistrationCounts: /* @__PURE__ */ new Map(),
|
|
1194
|
+
eventIndex: /* @__PURE__ */ new Map(),
|
|
1195
|
+
deliveryIndex: /* @__PURE__ */ new Map(),
|
|
1196
|
+
ownerDeliveryCounts: /* @__PURE__ */ new Map(),
|
|
1197
|
+
scanCursors: {
|
|
1198
|
+
events: "",
|
|
1199
|
+
deliveries: "",
|
|
1200
|
+
recovery: 0
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
const failpoint = yield* SubscriptionFailpoint;
|
|
1204
|
+
const requirePartition = (value, code) => samePartition(value.partition, partition) ? Effect.succeed(value) : Effect.fail(error("validation", code));
|
|
1205
|
+
const requireKey = (key, code) => validate$1(SubscriptionKey, key, code).pipe(Effect.flatMap((decoded) => requirePartition(decoded, code)));
|
|
1206
|
+
const register = Effect.fn("MemorySubscriptionStore.register")(function* (input, inputLimits) {
|
|
1207
|
+
const record = yield* validate$1(SubscriptionRecord, input, "register-record");
|
|
1208
|
+
const limits = yield* validate$1(SubscriptionLimits, inputLimits, "register-limits");
|
|
1209
|
+
yield* requirePartition(record.key, "register-partition");
|
|
1210
|
+
yield* failpoint.hit("subscription:register:before");
|
|
1211
|
+
const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1212
|
+
const key = subscriptionKeyString(record.key);
|
|
1213
|
+
const existingText = current.registrations.get(key);
|
|
1214
|
+
if (existingText !== void 0) {
|
|
1215
|
+
const existing = decode(SubscriptionRecord, existingText, "register-existing");
|
|
1216
|
+
if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
|
|
1217
|
+
return existing.success.creationFingerprint === record.creationFingerprint ? [Result.succeed(existing.success), current] : [Result.fail(error("conflict", "registration-identity")), current];
|
|
1218
|
+
}
|
|
1219
|
+
if (jsonBytes(record.configuration.context) > limits.maxContextBytes) return [Result.fail(error("capacity", "context-bytes")), current];
|
|
1220
|
+
if (jsonBytes(record.configuration.parameters) > limits.maxPayloadBytes) return [Result.fail(error("capacity", "parameters-bytes")), current];
|
|
1221
|
+
if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return [Result.fail(error("capacity", "lifetime")), current];
|
|
1222
|
+
if (current.registrations.size >= limits.maxRegistrations) return [Result.fail(error("capacity", "registrations")), current];
|
|
1223
|
+
const ownerCount = current.ownerRegistrationCounts.get(record.key.ownerId) ?? 0;
|
|
1224
|
+
if (ownerCount >= limits.maxRegistrationsPerOwner) return [Result.fail(error("capacity", "owner-registrations")), current];
|
|
1225
|
+
const assigned = {
|
|
1226
|
+
...record,
|
|
1227
|
+
ordinal: current.sequence + 1
|
|
1228
|
+
};
|
|
1229
|
+
const encoded = encode(SubscriptionRecord, assigned, "register-encode");
|
|
1230
|
+
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
1231
|
+
const registrations = new Map(current.registrations);
|
|
1232
|
+
registrations.set(key, encoded.success);
|
|
1233
|
+
const registrationIndex = new Map(current.registrationIndex);
|
|
1234
|
+
registrationIndex.set(key, {
|
|
1235
|
+
key: assigned.key,
|
|
1236
|
+
ordinal: assigned.ordinal,
|
|
1237
|
+
state: assigned.state,
|
|
1238
|
+
recoveryAt: assigned.recovery?.nextAttemptAtMillis ?? null
|
|
1239
|
+
});
|
|
1240
|
+
const candidateIndex = new Map(current.candidateIndex);
|
|
1241
|
+
const candidateKey = candidateIndexKey(assigned);
|
|
1242
|
+
candidateIndex.set(candidateKey, [...candidateIndex.get(candidateKey) ?? [], key]);
|
|
1243
|
+
const ownerRegistrationCounts = new Map(current.ownerRegistrationCounts);
|
|
1244
|
+
ownerRegistrationCounts.set(assigned.key.ownerId, ownerCount + 1);
|
|
1245
|
+
return [Result.succeed(assigned), {
|
|
1246
|
+
...current,
|
|
1247
|
+
sequence: assigned.ordinal,
|
|
1248
|
+
registrations,
|
|
1249
|
+
registrationIndex,
|
|
1250
|
+
candidateIndex,
|
|
1251
|
+
ownerRegistrationCounts
|
|
1252
|
+
}];
|
|
1253
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1254
|
+
yield* failpoint.hit("subscription:register:after");
|
|
1255
|
+
return result;
|
|
1256
|
+
});
|
|
1257
|
+
const get = Effect.fn("MemorySubscriptionStore.get")(function* (input) {
|
|
1258
|
+
const key = yield* requireKey(input, "get-key");
|
|
1259
|
+
const text = (yield* Ref.get(state)).registrations.get(subscriptionKeyString(key));
|
|
1260
|
+
return text === void 0 ? null : yield* decodeEffect(SubscriptionRecord, text, "get-record");
|
|
1261
|
+
});
|
|
1262
|
+
const list = Effect.fn("MemorySubscriptionStore.list")(function* (ownerId, after, limit) {
|
|
1263
|
+
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit <= 0) return yield* error("validation", "list-page");
|
|
1264
|
+
const current = yield* Ref.get(state);
|
|
1265
|
+
const records = [];
|
|
1266
|
+
for (const [storageKey, indexed] of current.registrationIndex) {
|
|
1267
|
+
if (indexed.key.ownerId !== ownerId || indexed.ordinal <= after) continue;
|
|
1268
|
+
const text = current.registrations.get(storageKey);
|
|
1269
|
+
if (text === void 0) return yield* error("corrupt", "list-index");
|
|
1270
|
+
records.push(yield* decodeEffect(SubscriptionRecord, text, "list-record"));
|
|
1271
|
+
}
|
|
1272
|
+
records.sort((a, b) => a.ordinal - b.ordinal);
|
|
1273
|
+
return records.slice(0, limit);
|
|
1274
|
+
});
|
|
1275
|
+
const cancel = Effect.fn("MemorySubscriptionStore.cancel")(function* (input) {
|
|
1276
|
+
const key = yield* requireKey(input, "cancel-key");
|
|
1277
|
+
yield* failpoint.hit("subscription:cancel:before");
|
|
1278
|
+
const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1279
|
+
const storageKey = subscriptionKeyString(key);
|
|
1280
|
+
const text = current.registrations.get(storageKey);
|
|
1281
|
+
if (text === void 0) return [Result.fail(error("not-found", "subscription")), current];
|
|
1282
|
+
const decoded = decode(SubscriptionRecord, text, "cancel-record");
|
|
1283
|
+
if (Result.isFailure(decoded)) return [decoded, current];
|
|
1284
|
+
if (decoded.success.state === "cancelled") return [Result.succeed(decoded.success), current];
|
|
1285
|
+
const cancelled = {
|
|
1286
|
+
...decoded.success,
|
|
1287
|
+
state: "cancelled",
|
|
1288
|
+
recovery: null
|
|
1289
|
+
};
|
|
1290
|
+
const encoded = encode(SubscriptionRecord, cancelled, "cancel-encode");
|
|
1291
|
+
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
1292
|
+
const registrations = new Map(current.registrations);
|
|
1293
|
+
registrations.set(storageKey, encoded.success);
|
|
1294
|
+
const registrationIndex = new Map(current.registrationIndex);
|
|
1295
|
+
const indexed = registrationIndex.get(storageKey);
|
|
1296
|
+
if (indexed === void 0) return [Result.fail(error("corrupt", "cancel-index")), current];
|
|
1297
|
+
registrationIndex.set(storageKey, {
|
|
1298
|
+
...indexed,
|
|
1299
|
+
state: "cancelled",
|
|
1300
|
+
recoveryAt: null
|
|
1301
|
+
});
|
|
1302
|
+
return [Result.succeed(cancelled), {
|
|
1303
|
+
...current,
|
|
1304
|
+
registrations,
|
|
1305
|
+
registrationIndex
|
|
1306
|
+
}];
|
|
1307
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1308
|
+
yield* failpoint.hit("subscription:cancel:after");
|
|
1309
|
+
return result;
|
|
1310
|
+
});
|
|
1311
|
+
const accept = Effect.fn("MemorySubscriptionStore.accept")(function* (input, inputLimits) {
|
|
1312
|
+
const event = yield* validate$1(AcceptedEvent, input, "accept-event");
|
|
1313
|
+
const limits = yield* validate$1(SubscriptionLimits, inputLimits, "accept-limits");
|
|
1314
|
+
yield* requirePartition(event, "accept-partition");
|
|
1315
|
+
yield* failpoint.hit("subscription:accept:before");
|
|
1316
|
+
const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1317
|
+
const existingText = current.events.get(event.eventId);
|
|
1318
|
+
if (existingText !== void 0) {
|
|
1319
|
+
const existing = decode(AcceptedEvent, existingText, "accept-existing");
|
|
1320
|
+
if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
|
|
1321
|
+
return sameEventIdentity(existing.success, event) ? [Result.succeed(existing.success), current] : [Result.fail(error("conflict", "event-identity")), current];
|
|
1322
|
+
}
|
|
1323
|
+
if (jsonBytes(event.payload) > limits.maxPayloadBytes) return [Result.fail(error("capacity", "payload-bytes")), current];
|
|
1324
|
+
if (current.events.size >= limits.maxEvents) return [Result.fail(error("capacity", "events")), current];
|
|
1325
|
+
const accepted = {
|
|
1326
|
+
...event,
|
|
1327
|
+
cutoff: current.sequence + 1,
|
|
1328
|
+
cursor: 0,
|
|
1329
|
+
routingComplete: false,
|
|
1330
|
+
routingFailure: null
|
|
1331
|
+
};
|
|
1332
|
+
const encoded = encode(AcceptedEvent, accepted, "accept-encode");
|
|
1333
|
+
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
1334
|
+
const events = new Map(current.events);
|
|
1335
|
+
events.set(accepted.eventId, encoded.success);
|
|
1336
|
+
const eventIndex = new Map(current.eventIndex);
|
|
1337
|
+
eventIndex.set(accepted.eventId, {
|
|
1338
|
+
routingComplete: false,
|
|
1339
|
+
nextAttemptAtMillis: accepted.nextAttemptAtMillis
|
|
1340
|
+
});
|
|
1341
|
+
return [Result.succeed(accepted), {
|
|
1342
|
+
...current,
|
|
1343
|
+
sequence: accepted.cutoff,
|
|
1344
|
+
events,
|
|
1345
|
+
eventIndex
|
|
1346
|
+
}];
|
|
1347
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1348
|
+
yield* failpoint.hit("subscription:accept:after");
|
|
1349
|
+
return result;
|
|
1350
|
+
});
|
|
1351
|
+
const event = Effect.fn("MemorySubscriptionStore.event")(function* (eventId) {
|
|
1352
|
+
const text = (yield* Ref.get(state)).events.get(eventId);
|
|
1353
|
+
return text === void 0 ? null : yield* decodeEffect(AcceptedEvent, text, "event-record");
|
|
1354
|
+
});
|
|
1355
|
+
const pendingEvents = Effect.fn("MemorySubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
|
|
1356
|
+
const events = [];
|
|
1357
|
+
for (const [eventId, indexed] of (yield* Ref.get(state)).eventIndex) if (!indexed.routingComplete && indexed.nextAttemptAtMillis <= nowMillis && compareScheduleNames(eventId, after) > 0) events.push(eventId);
|
|
1358
|
+
events.sort(compareScheduleNames);
|
|
1359
|
+
return events.slice(0, limit);
|
|
1360
|
+
});
|
|
1361
|
+
const candidates = Effect.fn("MemorySubscriptionStore.candidates")(function* (input, limit) {
|
|
1362
|
+
const accepted = yield* validate$1(AcceptedEvent, input, "candidates-event");
|
|
1363
|
+
yield* requirePartition(accepted, "candidates-partition");
|
|
1364
|
+
const stored = yield* event(accepted.eventId);
|
|
1365
|
+
if (stored === null || !sameEventIdentity(stored, accepted)) return yield* error(stored === null ? "not-found" : "conflict", "event");
|
|
1366
|
+
const current = yield* Ref.get(state);
|
|
1367
|
+
const records = [];
|
|
1368
|
+
for (const storageKey of current.candidateIndex.get(eventCandidateIndexKey(stored)) ?? []) {
|
|
1369
|
+
const indexed = current.registrationIndex.get(storageKey);
|
|
1370
|
+
if (indexed === void 0) return yield* error("corrupt", "candidate-index");
|
|
1371
|
+
if (indexed.ordinal <= stored.cursor || indexed.ordinal > stored.cutoff) continue;
|
|
1372
|
+
const text = current.registrations.get(storageKey);
|
|
1373
|
+
if (text === void 0) return yield* error("corrupt", "candidate-record");
|
|
1374
|
+
records.push(yield* decodeEffect(SubscriptionRecord, text, "candidate-record"));
|
|
1375
|
+
}
|
|
1376
|
+
records.sort((a, b) => a.ordinal - b.ordinal);
|
|
1377
|
+
return records.slice(0, limit);
|
|
1378
|
+
});
|
|
1379
|
+
const select = Effect.fn("MemorySubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
|
|
1380
|
+
const suppliedEvent = yield* validate$1(AcceptedEvent, inputEvent, "select-event");
|
|
1381
|
+
const deliveries = yield* validate$1(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
|
|
1382
|
+
const limits = yield* validate$1(SubscriptionLimits, inputLimits, "select-limits");
|
|
1383
|
+
yield* requirePartition(suppliedEvent, "select-partition");
|
|
1384
|
+
yield* failpoint.hit("subscription:select:before");
|
|
1385
|
+
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
1386
|
+
yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1387
|
+
const eventText = current.events.get(suppliedEvent.eventId);
|
|
1388
|
+
if (eventText === void 0) return [Result.fail(error("not-found", "event")), current];
|
|
1389
|
+
const decodedEvent = decode(AcceptedEvent, eventText, "select-event-record");
|
|
1390
|
+
if (Result.isFailure(decodedEvent)) return [Result.fail(decodedEvent.failure), current];
|
|
1391
|
+
const accepted = decodedEvent.success;
|
|
1392
|
+
if (!sameEventIdentity(accepted, suppliedEvent) || suppliedEvent.cursor !== accepted.cursor) return [Result.fail(error("conflict", "event-cursor")), current];
|
|
1393
|
+
if (accepted.routingComplete) return [complete && cursor === accepted.cursor ? Result.void : Result.fail(error("conflict", "routing-complete")), current];
|
|
1394
|
+
if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return [Result.fail(error("validation", "cursor")), current];
|
|
1395
|
+
const additions = [];
|
|
1396
|
+
const updates = [];
|
|
1397
|
+
const additionIndex = [];
|
|
1398
|
+
const registrationUpdates = [];
|
|
1399
|
+
const owners = new Map(current.ownerDeliveryCounts);
|
|
1400
|
+
for (const delivery of deliveries) {
|
|
1401
|
+
const recordText = current.registrations.get(subscriptionKeyString(delivery.key.subscription));
|
|
1402
|
+
if (recordText === void 0) return [Result.fail(error("not-found", "subscription")), current];
|
|
1403
|
+
const record = decode(SubscriptionRecord, recordText, "select-registration");
|
|
1404
|
+
if (Result.isFailure(record)) return [Result.fail(record.failure), current];
|
|
1405
|
+
if (!deliveryBelongsTo(delivery, record.success, accepted) || !subscriptionDeliveryCanSelect(delivery, record.success, accepted) || record.success.ordinal <= accepted.cursor || record.success.ordinal > cursor) return [Result.fail(error("conflict", "selection")), current];
|
|
1406
|
+
if (!subscriptionCanSelect(record.success, accepted, effectiveNowMillis, false)) continue;
|
|
1407
|
+
const deliveryKey = subscriptionDeliveryKeyString(delivery.key);
|
|
1408
|
+
const existingText = current.deliveries.get(deliveryKey);
|
|
1409
|
+
if (existingText !== void 0) {
|
|
1410
|
+
const existing = decode(SubscriptionDelivery, existingText, "select-existing");
|
|
1411
|
+
if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
|
|
1412
|
+
if (!sameDeliveryIdentity(existing.success, delivery)) return [Result.fail(error("conflict", "delivery-identity")), current];
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
const encodedDelivery = encode(SubscriptionDelivery, delivery, "select-delivery-encode");
|
|
1416
|
+
if (Result.isFailure(encodedDelivery)) return [Result.fail(encodedDelivery.failure), current];
|
|
1417
|
+
additions.push([deliveryKey, encodedDelivery.success]);
|
|
1418
|
+
additionIndex.push([
|
|
1419
|
+
deliveryKey,
|
|
1420
|
+
{
|
|
1421
|
+
key: delivery.key,
|
|
1422
|
+
state: delivery.state,
|
|
1423
|
+
nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis
|
|
1424
|
+
},
|
|
1425
|
+
record.success.key.ownerId
|
|
1426
|
+
]);
|
|
1427
|
+
const ownerId = record.success.key.ownerId;
|
|
1428
|
+
owners.set(ownerId, (owners.get(ownerId) ?? 0) + 1);
|
|
1429
|
+
if ((owners.get(ownerId) ?? 0) > limits.maxDeliveriesPerOwner) return [Result.fail(error("capacity", "owner-deliveries")), current];
|
|
1430
|
+
if (record.success.configuration.mode === "once") {
|
|
1431
|
+
const consumed = {
|
|
1432
|
+
...record.success,
|
|
1433
|
+
state: "consumed",
|
|
1434
|
+
recovery: null
|
|
1435
|
+
};
|
|
1436
|
+
const encodedRecord = encode(SubscriptionRecord, consumed, "select-registration-encode");
|
|
1437
|
+
if (Result.isFailure(encodedRecord)) return [Result.fail(encodedRecord.failure), current];
|
|
1438
|
+
const consumedKey = subscriptionKeyString(consumed.key);
|
|
1439
|
+
updates.push([consumedKey, encodedRecord.success]);
|
|
1440
|
+
const indexed = current.registrationIndex.get(consumedKey);
|
|
1441
|
+
if (indexed === void 0) return [Result.fail(error("corrupt", "selection-index")), current];
|
|
1442
|
+
registrationUpdates.push([consumedKey, {
|
|
1443
|
+
...indexed,
|
|
1444
|
+
state: "consumed",
|
|
1445
|
+
recoveryAt: null
|
|
1446
|
+
}]);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (current.deliveries.size + additions.length > limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
|
|
1450
|
+
const registrations = new Map(current.registrations);
|
|
1451
|
+
for (const [key, value] of updates) registrations.set(key, value);
|
|
1452
|
+
const nextDeliveries = new Map(current.deliveries);
|
|
1453
|
+
for (const [key, value] of additions) nextDeliveries.set(key, value);
|
|
1454
|
+
const deliveryIndex = new Map(current.deliveryIndex);
|
|
1455
|
+
for (const [key, value] of additionIndex) deliveryIndex.set(key, value);
|
|
1456
|
+
const registrationIndex = new Map(current.registrationIndex);
|
|
1457
|
+
for (const [key, value] of registrationUpdates) registrationIndex.set(key, value);
|
|
1458
|
+
const nextEvent = {
|
|
1459
|
+
...accepted,
|
|
1460
|
+
cursor,
|
|
1461
|
+
routingComplete: complete,
|
|
1462
|
+
routingFailure: null
|
|
1463
|
+
};
|
|
1464
|
+
const encodedEvent = encode(AcceptedEvent, nextEvent, "select-event-encode");
|
|
1465
|
+
if (Result.isFailure(encodedEvent)) return [Result.fail(encodedEvent.failure), current];
|
|
1466
|
+
const events = new Map(current.events);
|
|
1467
|
+
events.set(nextEvent.eventId, encodedEvent.success);
|
|
1468
|
+
const eventIndex = new Map(current.eventIndex);
|
|
1469
|
+
eventIndex.set(nextEvent.eventId, {
|
|
1470
|
+
routingComplete: nextEvent.routingComplete,
|
|
1471
|
+
nextAttemptAtMillis: nextEvent.nextAttemptAtMillis
|
|
1472
|
+
});
|
|
1473
|
+
return [Result.void, {
|
|
1474
|
+
...current,
|
|
1475
|
+
registrations,
|
|
1476
|
+
registrationIndex,
|
|
1477
|
+
events,
|
|
1478
|
+
eventIndex,
|
|
1479
|
+
deliveries: nextDeliveries,
|
|
1480
|
+
deliveryIndex,
|
|
1481
|
+
ownerDeliveryCounts: owners
|
|
1482
|
+
}];
|
|
1483
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1484
|
+
yield* failpoint.hit("subscription:select:after");
|
|
1485
|
+
});
|
|
1486
|
+
const catchUp = Effect.fn("MemorySubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
|
|
1487
|
+
const suppliedEvent = yield* validate$1(AcceptedEvent, inputEvent, "catch-up-event");
|
|
1488
|
+
const delivery = yield* validate$1(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
|
|
1489
|
+
const limits = yield* validate$1(SubscriptionLimits, inputLimits, "catch-up-limits");
|
|
1490
|
+
yield* requirePartition(suppliedEvent, "catch-up-partition");
|
|
1491
|
+
yield* failpoint.hit("subscription:catch-up:before");
|
|
1492
|
+
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
1493
|
+
yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1494
|
+
const eventText = current.events.get(suppliedEvent.eventId);
|
|
1495
|
+
const recordText = current.registrations.get(subscriptionKeyString(delivery.key.subscription));
|
|
1496
|
+
if (eventText === void 0 || recordText === void 0) return [Result.fail(error("not-found", eventText === void 0 ? "event" : "subscription")), current];
|
|
1497
|
+
const accepted = decode(AcceptedEvent, eventText, "catch-up-event-record");
|
|
1498
|
+
if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
|
|
1499
|
+
const record = decode(SubscriptionRecord, recordText, "catch-up-registration");
|
|
1500
|
+
if (Result.isFailure(record)) return [Result.fail(record.failure), current];
|
|
1501
|
+
if (!sameEventIdentity(accepted.success, suppliedEvent) || !deliveryBelongsTo(delivery, record.success, accepted.success) || !subscriptionDeliveryCanSelect(delivery, record.success, accepted.success) || record.success.configuration.mode !== "once") return [Result.fail(error("conflict", "catch-up-identity")), current];
|
|
1502
|
+
const key = subscriptionDeliveryKeyString(delivery.key);
|
|
1503
|
+
const existingText = current.deliveries.get(key);
|
|
1504
|
+
if (existingText !== void 0) {
|
|
1505
|
+
const existing = decode(SubscriptionDelivery, existingText, "catch-up-existing");
|
|
1506
|
+
if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
|
|
1507
|
+
return sameDeliveryIdentity(existing.success, delivery) ? [Result.void, current] : [Result.fail(error("conflict", "delivery-identity")), current];
|
|
1508
|
+
}
|
|
1509
|
+
if (!subscriptionCanSelect(record.success, accepted.success, effectiveNowMillis, true)) return [Result.fail(error("conflict", "catch-up-eligibility")), current];
|
|
1510
|
+
if (current.deliveries.size >= limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
|
|
1511
|
+
const ownerCount = current.ownerDeliveryCounts.get(record.success.key.ownerId) ?? 0;
|
|
1512
|
+
if (ownerCount >= limits.maxDeliveriesPerOwner) return [Result.fail(error("capacity", "owner-deliveries")), current];
|
|
1513
|
+
const encodedDelivery = encode(SubscriptionDelivery, delivery, "catch-up-delivery-encode");
|
|
1514
|
+
if (Result.isFailure(encodedDelivery)) return [Result.fail(encodedDelivery.failure), current];
|
|
1515
|
+
const consumed = {
|
|
1516
|
+
...record.success,
|
|
1517
|
+
state: "consumed",
|
|
1518
|
+
recovery: null
|
|
1519
|
+
};
|
|
1520
|
+
const encodedRecord = encode(SubscriptionRecord, consumed, "catch-up-registration-encode");
|
|
1521
|
+
if (Result.isFailure(encodedRecord)) return [Result.fail(encodedRecord.failure), current];
|
|
1522
|
+
const deliveries = new Map(current.deliveries);
|
|
1523
|
+
deliveries.set(key, encodedDelivery.success);
|
|
1524
|
+
const registrations = new Map(current.registrations);
|
|
1525
|
+
const consumedKey = subscriptionKeyString(consumed.key);
|
|
1526
|
+
registrations.set(consumedKey, encodedRecord.success);
|
|
1527
|
+
const registrationIndex = new Map(current.registrationIndex);
|
|
1528
|
+
const indexed = registrationIndex.get(consumedKey);
|
|
1529
|
+
if (indexed === void 0) return [Result.fail(error("corrupt", "catch-up-index")), current];
|
|
1530
|
+
registrationIndex.set(consumedKey, {
|
|
1531
|
+
...indexed,
|
|
1532
|
+
state: "consumed",
|
|
1533
|
+
recoveryAt: null
|
|
1534
|
+
});
|
|
1535
|
+
const deliveryIndex = new Map(current.deliveryIndex);
|
|
1536
|
+
deliveryIndex.set(key, {
|
|
1537
|
+
key: delivery.key,
|
|
1538
|
+
state: delivery.state,
|
|
1539
|
+
nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis
|
|
1540
|
+
});
|
|
1541
|
+
const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
|
|
1542
|
+
ownerDeliveryCounts.set(consumed.key.ownerId, ownerCount + 1);
|
|
1543
|
+
return [Result.void, {
|
|
1544
|
+
...current,
|
|
1545
|
+
deliveries,
|
|
1546
|
+
deliveryIndex,
|
|
1547
|
+
ownerDeliveryCounts,
|
|
1548
|
+
registrations,
|
|
1549
|
+
registrationIndex
|
|
1550
|
+
}];
|
|
1551
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1552
|
+
yield* failpoint.hit("subscription:catch-up:after");
|
|
1553
|
+
});
|
|
1554
|
+
const deferEvent = Effect.fn("MemorySubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
|
|
1555
|
+
const routingFailure = code === void 0 ? "routing-failed" : yield* validate$1(SubscriptionName, code, "routing-failure");
|
|
1556
|
+
yield* failpoint.hit("subscription:defer-event:before");
|
|
1557
|
+
yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1558
|
+
const text = current.events.get(eventId);
|
|
1559
|
+
if (text === void 0) return [Result.fail(error("not-found", "event")), current];
|
|
1560
|
+
const accepted = decode(AcceptedEvent, text, "defer-event-record");
|
|
1561
|
+
if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
|
|
1562
|
+
const updated = {
|
|
1563
|
+
...accepted.success,
|
|
1564
|
+
nextAttemptAtMillis,
|
|
1565
|
+
routingFailure
|
|
1566
|
+
};
|
|
1567
|
+
const encoded = encode(AcceptedEvent, updated, "defer-event-encode");
|
|
1568
|
+
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
1569
|
+
const events = new Map(current.events);
|
|
1570
|
+
events.set(eventId, encoded.success);
|
|
1571
|
+
const eventIndex = new Map(current.eventIndex);
|
|
1572
|
+
const indexed = eventIndex.get(eventId);
|
|
1573
|
+
if (indexed === void 0) return [Result.fail(error("corrupt", "defer-event-index")), current];
|
|
1574
|
+
eventIndex.set(eventId, {
|
|
1575
|
+
...indexed,
|
|
1576
|
+
nextAttemptAtMillis
|
|
1577
|
+
});
|
|
1578
|
+
return [Result.void, {
|
|
1579
|
+
...current,
|
|
1580
|
+
events,
|
|
1581
|
+
eventIndex
|
|
1582
|
+
}];
|
|
1583
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1584
|
+
yield* failpoint.hit("subscription:defer-event:after");
|
|
1585
|
+
});
|
|
1586
|
+
const delivery = Effect.fn("MemorySubscriptionStore.delivery")(function* (input) {
|
|
1587
|
+
const key = yield* validate$1(SubscriptionDeliveryKey, input, "delivery-key");
|
|
1588
|
+
yield* requirePartition(key.subscription, "delivery-partition");
|
|
1589
|
+
const text = (yield* Ref.get(state)).deliveries.get(subscriptionDeliveryKeyString(key));
|
|
1590
|
+
return text === void 0 ? null : yield* decodeEffect(SubscriptionDelivery, text, "delivery-record");
|
|
1591
|
+
});
|
|
1592
|
+
const pendingDeliveries = Effect.fn("MemorySubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
|
|
1593
|
+
const items = [];
|
|
1594
|
+
for (const [storageKey, item] of (yield* Ref.get(state)).deliveryIndex) if (item.state !== "delivered" && item.state !== "refused" && item.nextAttemptAtMillis <= nowMillis && compareScheduleNames(storageKey, after) > 0) items.push(item.key);
|
|
1595
|
+
items.sort((a, b) => compareScheduleNames(subscriptionDeliveryKeyString(a), subscriptionDeliveryKeyString(b)));
|
|
1596
|
+
return items.slice(0, limit);
|
|
1597
|
+
});
|
|
1598
|
+
const listDeliveries = Effect.fn("MemorySubscriptionStore.listDeliveries")(function* (input, after, limit) {
|
|
1599
|
+
const key = yield* requireKey(input, "list-deliveries-key");
|
|
1600
|
+
const items = [];
|
|
1601
|
+
for (const text of (yield* Ref.get(state)).deliveries.values()) {
|
|
1602
|
+
const item = yield* decodeEffect(SubscriptionDelivery, text, "list-delivery");
|
|
1603
|
+
const itemKey = subscriptionDeliveryKeyString(item.key);
|
|
1604
|
+
if (subscriptionKeyString(item.key.subscription) === subscriptionKeyString(key) && compareScheduleNames(itemKey, after) > 0) items.push(item);
|
|
1605
|
+
}
|
|
1606
|
+
items.sort((a, b) => compareScheduleNames(subscriptionDeliveryKeyString(a.key), subscriptionDeliveryKeyString(b.key)));
|
|
1607
|
+
return items.slice(0, limit);
|
|
1608
|
+
});
|
|
1609
|
+
const changeDelivery = Effect.fn("MemorySubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
|
|
1610
|
+
const key = yield* validate$1(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
|
|
1611
|
+
const deliveryId = yield* validate$1(Digest, inputDeliveryId, "change-delivery-id");
|
|
1612
|
+
const change = yield* validate$1(DeliveryChange, inputChange, "change-delivery-change");
|
|
1613
|
+
yield* requirePartition(key.subscription, "change-delivery-partition");
|
|
1614
|
+
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
|
|
1615
|
+
const effectiveChange = change._tag === "Prepare" ? {
|
|
1616
|
+
...change,
|
|
1617
|
+
nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
|
|
1618
|
+
} : change;
|
|
1619
|
+
const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1620
|
+
const storageKey = subscriptionDeliveryKeyString(key);
|
|
1621
|
+
const text = current.deliveries.get(storageKey);
|
|
1622
|
+
if (text === void 0) return [Result.fail(error("not-found", "delivery")), current];
|
|
1623
|
+
const decoded = decode(SubscriptionDelivery, text, "change-delivery-record");
|
|
1624
|
+
if (Result.isFailure(decoded)) return [decoded, current];
|
|
1625
|
+
const registrationText = current.registrations.get(subscriptionKeyString(key.subscription));
|
|
1626
|
+
if (registrationText === void 0) return [Result.fail(error("corrupt", "delivery-registration")), current];
|
|
1627
|
+
const registration = decode(SubscriptionRecord, registrationText, "delivery-registration");
|
|
1628
|
+
if (Result.isFailure(registration)) return [Result.fail(registration.failure), current];
|
|
1629
|
+
const transition = applySubscriptionDeliveryChange(decoded.success, registration.success, deliveryId, effectiveChange);
|
|
1630
|
+
if (Result.isFailure(transition)) return [Result.fail(transition.failure), current];
|
|
1631
|
+
if (transition.success === decoded.success) return [Result.succeed(decoded.success), current];
|
|
1632
|
+
const updated = transition.success;
|
|
1633
|
+
const encoded = encode(SubscriptionDelivery, updated, "change-delivery-encode");
|
|
1634
|
+
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
1635
|
+
const deliveries = new Map(current.deliveries);
|
|
1636
|
+
deliveries.set(storageKey, encoded.success);
|
|
1637
|
+
const deliveryIndex = new Map(current.deliveryIndex);
|
|
1638
|
+
const indexed = deliveryIndex.get(storageKey);
|
|
1639
|
+
if (indexed === void 0) return [Result.fail(error("corrupt", "delivery-index")), current];
|
|
1640
|
+
deliveryIndex.set(storageKey, {
|
|
1641
|
+
...indexed,
|
|
1642
|
+
state: updated.state,
|
|
1643
|
+
nextAttemptAtMillis: updated.retry.nextAttemptAtMillis
|
|
1644
|
+
});
|
|
1645
|
+
return [Result.succeed(updated), {
|
|
1646
|
+
...current,
|
|
1647
|
+
deliveries,
|
|
1648
|
+
deliveryIndex
|
|
1649
|
+
}];
|
|
1650
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1651
|
+
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
|
|
1652
|
+
return result;
|
|
1653
|
+
});
|
|
1654
|
+
const recovering = Effect.fn("MemorySubscriptionStore.recovering")(function* (nowMillis, after, limit) {
|
|
1655
|
+
const records = [];
|
|
1656
|
+
for (const item of (yield* Ref.get(state)).registrationIndex.values()) if (item.ordinal > after && item.state === "active" && item.recoveryAt !== null && item.recoveryAt <= nowMillis) records.push({
|
|
1657
|
+
key: item.key,
|
|
1658
|
+
ordinal: item.ordinal
|
|
1659
|
+
});
|
|
1660
|
+
records.sort((a, b) => a.ordinal - b.ordinal);
|
|
1661
|
+
return records.slice(0, limit);
|
|
1662
|
+
});
|
|
1663
|
+
const deferRecovery = Effect.fn("MemorySubscriptionStore.deferRecovery")(function* (input, recovery) {
|
|
1664
|
+
const key = yield* requireKey(input, "defer-recovery-key");
|
|
1665
|
+
yield* failpoint.hit("subscription:defer-recovery:before");
|
|
1666
|
+
yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1667
|
+
const storageKey = subscriptionKeyString(key);
|
|
1668
|
+
const text = current.registrations.get(storageKey);
|
|
1669
|
+
if (text === void 0) return [Result.fail(error("not-found", "subscription")), current];
|
|
1670
|
+
const record = decode(SubscriptionRecord, text, "defer-recovery-record");
|
|
1671
|
+
if (Result.isFailure(record)) return [Result.fail(record.failure), current];
|
|
1672
|
+
const updated = {
|
|
1673
|
+
...record.success,
|
|
1674
|
+
recovery: record.success.state === "active" ? recovery : null
|
|
1675
|
+
};
|
|
1676
|
+
const encoded = encode(SubscriptionRecord, updated, "defer-recovery-encode");
|
|
1677
|
+
if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
|
|
1678
|
+
const registrations = new Map(current.registrations);
|
|
1679
|
+
registrations.set(storageKey, encoded.success);
|
|
1680
|
+
const registrationIndex = new Map(current.registrationIndex);
|
|
1681
|
+
const indexed = registrationIndex.get(storageKey);
|
|
1682
|
+
if (indexed === void 0) return [Result.fail(error("corrupt", "recovery-index")), current];
|
|
1683
|
+
registrationIndex.set(storageKey, {
|
|
1684
|
+
...indexed,
|
|
1685
|
+
recoveryAt: updated.recovery?.nextAttemptAtMillis ?? null
|
|
1686
|
+
});
|
|
1687
|
+
return [Result.void, {
|
|
1688
|
+
...current,
|
|
1689
|
+
registrations,
|
|
1690
|
+
registrationIndex
|
|
1691
|
+
}];
|
|
1692
|
+
})).pipe(Effect.flatMap(Effect.fromResult));
|
|
1693
|
+
yield* failpoint.hit("subscription:defer-recovery:after");
|
|
1694
|
+
});
|
|
1695
|
+
const readScanCursors = Ref.get(state).pipe(Effect.map((current) => current.scanCursors));
|
|
1696
|
+
const advanceScanCursors = Effect.fn("MemorySubscriptionStore.advanceScanCursors")(function* (input) {
|
|
1697
|
+
const cursors = yield* validate$1(SubscriptionScanCursors, input, "scan-cursors");
|
|
1698
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:before");
|
|
1699
|
+
yield* Effect.uninterruptible(Ref.update(state, (current) => ({
|
|
1700
|
+
...current,
|
|
1701
|
+
scanCursors: cursors
|
|
1702
|
+
})));
|
|
1703
|
+
yield* failpoint.hit("subscription:advance-scan-cursors:after");
|
|
1704
|
+
});
|
|
1705
|
+
const nextDeadline = Effect.gen(function* () {
|
|
1706
|
+
let deadline = null;
|
|
1707
|
+
const current = yield* Ref.get(state);
|
|
1708
|
+
if (current.scanCursors.events !== "" || current.scanCursors.deliveries !== "" || current.scanCursors.recovery !== 0) return 0;
|
|
1709
|
+
const consider = (value) => {
|
|
1710
|
+
if (deadline === null || value < deadline) deadline = value;
|
|
1711
|
+
};
|
|
1712
|
+
for (const accepted of current.eventIndex.values()) if (!accepted.routingComplete) consider(accepted.nextAttemptAtMillis);
|
|
1713
|
+
for (const item of current.deliveryIndex.values()) if (item.state !== "delivered" && item.state !== "refused") consider(item.nextAttemptAtMillis);
|
|
1714
|
+
for (const record of current.registrationIndex.values()) if (record.state === "active" && record.recoveryAt !== null) consider(record.recoveryAt);
|
|
1715
|
+
return deadline;
|
|
1716
|
+
}).pipe(Effect.withSpan("MemorySubscriptionStore.nextDeadline"));
|
|
1717
|
+
return SubscriptionStore.of({
|
|
1718
|
+
partition,
|
|
1719
|
+
register,
|
|
1720
|
+
get,
|
|
1721
|
+
list,
|
|
1722
|
+
cancel,
|
|
1723
|
+
accept,
|
|
1724
|
+
event,
|
|
1725
|
+
pendingEvents,
|
|
1726
|
+
candidates,
|
|
1727
|
+
select,
|
|
1728
|
+
catchUp,
|
|
1729
|
+
deferEvent,
|
|
1730
|
+
delivery,
|
|
1731
|
+
pendingDeliveries,
|
|
1732
|
+
listDeliveries,
|
|
1733
|
+
changeDelivery,
|
|
1734
|
+
recovering,
|
|
1735
|
+
deferRecovery,
|
|
1736
|
+
readScanCursors,
|
|
1737
|
+
advanceScanCursors,
|
|
1738
|
+
nextDeadline
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1741
|
+
const memorySubscriptionStoreLayer = (partition) => Layer.effect(SubscriptionStore, makeMemorySubscriptionStore(partition));
|
|
1742
|
+
//#endregion
|
|
1153
1743
|
//#region src/memory-storage.ts
|
|
1154
|
-
const
|
|
1155
|
-
const
|
|
1156
|
-
const
|
|
1157
|
-
const storeError = (operation, message, cause) => cause === void 0 ?
|
|
1744
|
+
const MAX_THREADS = 256;
|
|
1745
|
+
const MAX_RECORDS_PER_THREAD = 65536;
|
|
1746
|
+
const MAX_CHECKPOINTS_PER_THREAD = 1024;
|
|
1747
|
+
const storeError = (operation, message, cause) => cause === void 0 ? ThreadStoreError.make({
|
|
1158
1748
|
operation,
|
|
1159
1749
|
message
|
|
1160
|
-
}) :
|
|
1750
|
+
}) : ThreadStoreError.make({
|
|
1161
1751
|
operation,
|
|
1162
1752
|
message,
|
|
1163
1753
|
cause
|
|
1164
1754
|
});
|
|
1165
|
-
const validate = Effect.fn("
|
|
1755
|
+
const validate = Effect.fn("MemoryThreadStore.validate")((schema, operation, value) => Schema.encodeUnknownEffect(schema)(value).pipe(Effect.flatMap(Schema.decodeUnknownEffect(schema)), Effect.mapError((error) => storeError(operation, `Invalid ${operation} request`, error))));
|
|
1166
1756
|
const decodeCanonicalSequence = Schema.decodeSync(CanonicalSequence);
|
|
1167
1757
|
const ZERO_CANONICAL_SEQUENCE = decodeCanonicalSequence(0);
|
|
1168
|
-
const offsetSequence = Effect.fn("
|
|
1758
|
+
const offsetSequence = Effect.fn("MemoryThreadStore.offsetSequence")((threadId, offset) => {
|
|
1169
1759
|
if (offset === void 0) return Effect.succeed(ZERO_CANONICAL_SEQUENCE);
|
|
1170
|
-
const prefix = `memory:v1:${Encoding.encodeBase64(
|
|
1760
|
+
const prefix = `memory:v1:${Encoding.encodeBase64(threadId)}:`;
|
|
1171
1761
|
const encodedSequence = offset.startsWith(prefix) ? offset.slice(prefix.length) : "";
|
|
1172
1762
|
if (!/^\d+$/.test(encodedSequence)) return Effect.fail(storeError("observe", "Malformed observation offset"));
|
|
1173
1763
|
const sequence = Number(encodedSequence);
|
|
1174
1764
|
return Number.isSafeInteger(sequence) ? Schema.decodeUnknownEffect(CanonicalSequence)(sequence).pipe(Effect.mapError(() => storeError("observe", "Malformed observation offset"))) : Effect.fail(storeError("observe", "Malformed observation offset"));
|
|
1175
1765
|
});
|
|
1176
|
-
const observationOffset = (
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1179
|
-
return
|
|
1766
|
+
const observationOffset = (threadId, sequence) => Schema.decodeSync(ObservationOffset)(`memory:v1:${Encoding.encodeBase64(threadId)}:${sequence}`);
|
|
1767
|
+
const findThread = Effect.fn("MemoryThreadStore.findThread")((state, threadId) => {
|
|
1768
|
+
const thread = state.threads.get(threadId);
|
|
1769
|
+
return thread === void 0 ? Effect.fail(ThreadNotMaterialized.make({ threadId })) : Effect.succeed(thread);
|
|
1180
1770
|
});
|
|
1181
1771
|
const CheckpointVersionEnvelope = Schema.Struct({ checkpoint: Schema.Struct({
|
|
1182
|
-
|
|
1772
|
+
threadId: ThreadId,
|
|
1183
1773
|
schemaVersion: Schema.Natural
|
|
1184
1774
|
}) });
|
|
1185
|
-
const validateCheckpointVersion = Effect.fn("
|
|
1775
|
+
const validateCheckpointVersion = Effect.fn("MemoryThreadStore.validateCheckpointVersion")(function* (value) {
|
|
1186
1776
|
const envelope = yield* Schema.decodeUnknownEffect(CheckpointVersionEnvelope)(value).pipe(Effect.mapError(() => storeError("saveCheckpoint", "Invalid saveCheckpoint request")));
|
|
1187
1777
|
if (envelope.checkpoint.schemaVersion !== 1) return yield* CheckpointRejected.make({
|
|
1188
|
-
|
|
1778
|
+
threadId: envelope.checkpoint.threadId,
|
|
1189
1779
|
reason: "unsupported-version"
|
|
1190
1780
|
});
|
|
1191
1781
|
});
|
|
1192
|
-
const
|
|
1782
|
+
const makeThreadStore = Effect.gen(function* () {
|
|
1193
1783
|
const crypto = yield* Crypto.Crypto;
|
|
1194
|
-
const state = yield* Ref.make({
|
|
1784
|
+
const state = yield* Ref.make({ threads: /* @__PURE__ */ new Map() });
|
|
1195
1785
|
const updates = yield* PubSub.sliding(1);
|
|
1196
1786
|
yield* Effect.addFinalizer(() => PubSub.shutdown(updates));
|
|
1197
|
-
const materialize = Effect.fn("
|
|
1198
|
-
const request = yield* validate(
|
|
1787
|
+
const materialize = Effect.fn("MemoryThreadStore.materialize")((unvalidated) => Effect.gen(function* () {
|
|
1788
|
+
const request = yield* validate(ThreadMaterialization, "materialize", unvalidated);
|
|
1199
1789
|
const decision = yield* Ref.modify(state, (current) => {
|
|
1200
|
-
const existing = current.
|
|
1790
|
+
const existing = current.threads.get(request.threadId);
|
|
1201
1791
|
if (existing !== void 0) {
|
|
1202
1792
|
if (request.producerEpoch < existing.producerEpoch) return [{
|
|
1203
1793
|
_tag: "failure",
|
|
1204
1794
|
error: FenceRejected.make({
|
|
1205
|
-
|
|
1795
|
+
threadId: request.threadId,
|
|
1206
1796
|
actualEpoch: existing.producerEpoch,
|
|
1207
1797
|
attemptedEpoch: request.producerEpoch
|
|
1208
1798
|
})
|
|
1209
1799
|
}, current];
|
|
1210
1800
|
if (request.producerEpoch === existing.producerEpoch) return [{ _tag: "success" }, current];
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1801
|
+
const threads = new Map(current.threads);
|
|
1802
|
+
threads.set(request.threadId, {
|
|
1213
1803
|
...existing,
|
|
1214
1804
|
producerEpoch: request.producerEpoch
|
|
1215
1805
|
});
|
|
1216
|
-
return [{ _tag: "success" }, {
|
|
1806
|
+
return [{ _tag: "success" }, { threads }];
|
|
1217
1807
|
}
|
|
1218
|
-
if (current.
|
|
1808
|
+
if (current.threads.size >= MAX_THREADS) return [{
|
|
1219
1809
|
_tag: "failure",
|
|
1220
|
-
error: storeError("materialize", `In-memory
|
|
1810
|
+
error: storeError("materialize", `In-memory thread limit ${MAX_THREADS} exceeded`)
|
|
1221
1811
|
}, current];
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1812
|
+
const threads = new Map(current.threads);
|
|
1813
|
+
threads.set(request.threadId, {
|
|
1224
1814
|
producerEpoch: request.producerEpoch,
|
|
1225
1815
|
tailSequence: ZERO_CANONICAL_SEQUENCE,
|
|
1226
1816
|
tailDigest: EMPTY_TAIL_DIGEST,
|
|
@@ -1230,33 +1820,33 @@ const makeConversationStore = Effect.gen(function* () {
|
|
|
1230
1820
|
tailDigests: /* @__PURE__ */ new Map([[ZERO_CANONICAL_SEQUENCE, EMPTY_TAIL_DIGEST]]),
|
|
1231
1821
|
checkpoints: /* @__PURE__ */ new Map()
|
|
1232
1822
|
});
|
|
1233
|
-
return [{ _tag: "success" }, {
|
|
1823
|
+
return [{ _tag: "success" }, { threads }];
|
|
1234
1824
|
});
|
|
1235
1825
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1236
1826
|
}));
|
|
1237
|
-
const append = Effect.fn("
|
|
1827
|
+
const append = Effect.fn("MemoryThreadStore.append")((unvalidated) => Effect.gen(function* () {
|
|
1238
1828
|
const request = yield* validate(FencedAppendRequest, "append", unvalidated);
|
|
1239
1829
|
const digest = yield* digestCanonicalBatch(request.expectedTailDigest, request.batch).pipe(Effect.provideService(Crypto.Crypto, crypto), Effect.mapError((error) => storeError("append", error.message, error)));
|
|
1240
1830
|
const decision = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
|
|
1241
|
-
const
|
|
1242
|
-
if (
|
|
1831
|
+
const thread = current.threads.get(request.threadId);
|
|
1832
|
+
if (thread === void 0) return [{
|
|
1243
1833
|
_tag: "failure",
|
|
1244
|
-
error:
|
|
1834
|
+
error: ThreadNotMaterialized.make({ threadId: request.threadId })
|
|
1245
1835
|
}, current];
|
|
1246
|
-
if (request.producerEpoch !==
|
|
1836
|
+
if (request.producerEpoch !== thread.producerEpoch) return [{
|
|
1247
1837
|
_tag: "failure",
|
|
1248
1838
|
error: FenceRejected.make({
|
|
1249
|
-
|
|
1250
|
-
actualEpoch:
|
|
1839
|
+
threadId: request.threadId,
|
|
1840
|
+
actualEpoch: thread.producerEpoch,
|
|
1251
1841
|
attemptedEpoch: request.producerEpoch
|
|
1252
1842
|
})
|
|
1253
1843
|
}, current];
|
|
1254
|
-
const previous =
|
|
1844
|
+
const previous = thread.batches.get(request.batch.batchId);
|
|
1255
1845
|
if (previous !== void 0) {
|
|
1256
1846
|
if (previous.digest !== digest) return [{
|
|
1257
1847
|
_tag: "failure",
|
|
1258
1848
|
error: AppendConflict.make({
|
|
1259
|
-
|
|
1849
|
+
threadId: request.threadId,
|
|
1260
1850
|
batchId: request.batch.batchId,
|
|
1261
1851
|
reason: "batch-digest"
|
|
1262
1852
|
})
|
|
@@ -1272,26 +1862,26 @@ const makeConversationStore = Effect.gen(function* () {
|
|
|
1272
1862
|
records: []
|
|
1273
1863
|
}, current];
|
|
1274
1864
|
}
|
|
1275
|
-
if (request.expectedTailSequence !==
|
|
1865
|
+
if (request.expectedTailSequence !== thread.tailSequence || request.expectedTailDigest !== thread.tailDigest) return [{
|
|
1276
1866
|
_tag: "failure",
|
|
1277
1867
|
error: AppendConflict.make({
|
|
1278
|
-
|
|
1868
|
+
threadId: request.threadId,
|
|
1279
1869
|
batchId: request.batch.batchId,
|
|
1280
1870
|
reason: "tail",
|
|
1281
|
-
actualTailSequence:
|
|
1282
|
-
actualTailDigest:
|
|
1871
|
+
actualTailSequence: thread.tailSequence,
|
|
1872
|
+
actualTailDigest: thread.tailDigest
|
|
1283
1873
|
})
|
|
1284
1874
|
}, current];
|
|
1285
|
-
if (
|
|
1875
|
+
if (thread.records.length + request.batch.records.length > MAX_RECORDS_PER_THREAD) return [{
|
|
1286
1876
|
_tag: "failure",
|
|
1287
|
-
error: storeError("append", `In-memory record limit ${
|
|
1877
|
+
error: storeError("append", `In-memory record limit ${MAX_RECORDS_PER_THREAD} exceeded`)
|
|
1288
1878
|
}, current];
|
|
1289
1879
|
const batchRecordIds = /* @__PURE__ */ new Set();
|
|
1290
1880
|
for (const record of request.batch.records) {
|
|
1291
|
-
if (
|
|
1881
|
+
if (thread.recordIds.has(record.recordId) || batchRecordIds.has(record.recordId)) return [{
|
|
1292
1882
|
_tag: "failure",
|
|
1293
1883
|
error: AppendConflict.make({
|
|
1294
|
-
|
|
1884
|
+
threadId: request.threadId,
|
|
1295
1885
|
batchId: request.batch.batchId,
|
|
1296
1886
|
reason: "record-identity"
|
|
1297
1887
|
})
|
|
@@ -1299,37 +1889,37 @@ const makeConversationStore = Effect.gen(function* () {
|
|
|
1299
1889
|
batchRecordIds.add(record.recordId);
|
|
1300
1890
|
}
|
|
1301
1891
|
const records = request.batch.records.map((record, index) => {
|
|
1302
|
-
const sequence = decodeCanonicalSequence(
|
|
1892
|
+
const sequence = decodeCanonicalSequence(thread.tailSequence + index + 1);
|
|
1303
1893
|
return CanonicalRecordEnvelope.make({
|
|
1304
|
-
|
|
1894
|
+
threadId: request.threadId,
|
|
1305
1895
|
batchId: request.batch.batchId,
|
|
1306
1896
|
sequence,
|
|
1307
|
-
offset: observationOffset(request.
|
|
1897
|
+
offset: observationOffset(request.threadId, sequence),
|
|
1308
1898
|
record
|
|
1309
1899
|
});
|
|
1310
1900
|
});
|
|
1311
|
-
const lastSequence = decodeCanonicalSequence(
|
|
1901
|
+
const lastSequence = decodeCanonicalSequence(thread.tailSequence + records.length);
|
|
1312
1902
|
const result = AppendResult.make({
|
|
1313
|
-
firstSequence: decodeCanonicalSequence(
|
|
1903
|
+
firstSequence: decodeCanonicalSequence(thread.tailSequence + 1),
|
|
1314
1904
|
lastSequence,
|
|
1315
1905
|
tailDigest: digest,
|
|
1316
1906
|
replayed: false
|
|
1317
1907
|
});
|
|
1318
|
-
const batches = new Map(
|
|
1908
|
+
const batches = new Map(thread.batches);
|
|
1319
1909
|
batches.set(request.batch.batchId, {
|
|
1320
1910
|
digest,
|
|
1321
1911
|
result
|
|
1322
1912
|
});
|
|
1323
|
-
const recordIds = new Set(
|
|
1913
|
+
const recordIds = new Set(thread.recordIds);
|
|
1324
1914
|
for (const recordId of batchRecordIds) recordIds.add(recordId);
|
|
1325
|
-
const tailDigests = new Map(
|
|
1915
|
+
const tailDigests = new Map(thread.tailDigests);
|
|
1326
1916
|
tailDigests.set(lastSequence, digest);
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
...
|
|
1917
|
+
const threads = new Map(current.threads);
|
|
1918
|
+
threads.set(request.threadId, {
|
|
1919
|
+
...thread,
|
|
1330
1920
|
tailSequence: lastSequence,
|
|
1331
1921
|
tailDigest: digest,
|
|
1332
|
-
records: [...
|
|
1922
|
+
records: [...thread.records, ...records],
|
|
1333
1923
|
recordIds,
|
|
1334
1924
|
batches,
|
|
1335
1925
|
tailDigests
|
|
@@ -1338,118 +1928,120 @@ const makeConversationStore = Effect.gen(function* () {
|
|
|
1338
1928
|
_tag: "success",
|
|
1339
1929
|
result,
|
|
1340
1930
|
records
|
|
1341
|
-
}, {
|
|
1931
|
+
}, { threads }];
|
|
1342
1932
|
}).pipe(Effect.tap((decision) => decision._tag === "success" && decision.records.length > 0 ? PubSub.publish(updates, void 0) : Effect.void)));
|
|
1343
1933
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1344
1934
|
return decision.result;
|
|
1345
1935
|
}));
|
|
1346
|
-
const readSnapshot = Effect.fn("
|
|
1936
|
+
const readSnapshot = Effect.fn("MemoryThreadStore.readSnapshot")((threadId, afterSequence, limit) => Ref.get(state).pipe(Effect.flatMap((current) => findThread(current, threadId)), Effect.map((thread) => thread.records.filter((record) => record.sequence > (afterSequence ?? ZERO_CANONICAL_SEQUENCE)).slice(0, limit))));
|
|
1347
1937
|
const read = (unvalidated) => Stream.unwrap(Effect.gen(function* () {
|
|
1348
|
-
const request = yield* validate(
|
|
1349
|
-
const records = yield* readSnapshot(request.
|
|
1938
|
+
const request = yield* validate(ThreadRead, "read", unvalidated);
|
|
1939
|
+
const records = yield* readSnapshot(request.threadId, request.afterSequence, request.limit);
|
|
1350
1940
|
return Stream.fromIterable(records);
|
|
1351
1941
|
}));
|
|
1352
1942
|
const observe = (unvalidated) => Stream.unwrap(Effect.gen(function* () {
|
|
1353
|
-
const request = yield* validate(
|
|
1354
|
-
const afterSequence = yield* offsetSequence(request.
|
|
1943
|
+
const request = yield* validate(ThreadObservation, "observe", unvalidated);
|
|
1944
|
+
const afterSequence = yield* offsetSequence(request.threadId, request.afterOffset);
|
|
1355
1945
|
return Stream.unwrap(Effect.gen(function* () {
|
|
1356
1946
|
const subscription = yield* PubSub.subscribe(updates);
|
|
1357
|
-
const initial = yield* readSnapshot(request.
|
|
1947
|
+
const initial = yield* readSnapshot(request.threadId, afterSequence, MAX_RECORDS_PER_THREAD);
|
|
1358
1948
|
const highWater = initial.length === 0 ? afterSequence : initial.at(-1)?.sequence ?? afterSequence;
|
|
1359
|
-
const live = Stream.fromEffectRepeat(PubSub.take(subscription)).pipe(Stream.mapAccumEffect(() => highWater, (lastSequence) => readSnapshot(request.
|
|
1949
|
+
const live = Stream.fromEffectRepeat(PubSub.take(subscription)).pipe(Stream.mapAccumEffect(() => highWater, (lastSequence) => readSnapshot(request.threadId, lastSequence, MAX_RECORDS_PER_THREAD).pipe(Effect.map((records) => [records.at(-1)?.sequence ?? lastSequence, records]))));
|
|
1360
1950
|
return Stream.fromIterable(initial).pipe(Stream.concat(live));
|
|
1361
1951
|
}));
|
|
1362
1952
|
}));
|
|
1363
|
-
const
|
|
1364
|
-
const request = yield* validate(
|
|
1365
|
-
const
|
|
1366
|
-
return
|
|
1367
|
-
format: "effect-agent/
|
|
1368
|
-
|
|
1369
|
-
tailSequence:
|
|
1370
|
-
tailDigest:
|
|
1371
|
-
records:
|
|
1953
|
+
const exportThread = Effect.fn("MemoryThreadStore.export")((unvalidated) => Effect.gen(function* () {
|
|
1954
|
+
const request = yield* validate(ThreadExportRequest, "export", unvalidated);
|
|
1955
|
+
const thread = yield* Ref.get(state).pipe(Effect.flatMap((current) => findThread(current, request.threadId)));
|
|
1956
|
+
return ThreadExport.make({
|
|
1957
|
+
format: "effect-agent/thread@1",
|
|
1958
|
+
threadId: request.threadId,
|
|
1959
|
+
tailSequence: thread.tailSequence,
|
|
1960
|
+
tailDigest: thread.tailDigest,
|
|
1961
|
+
records: thread.records
|
|
1372
1962
|
});
|
|
1373
1963
|
}));
|
|
1374
|
-
const inspectTail = Effect.fn("
|
|
1375
|
-
const request = yield* validate(
|
|
1376
|
-
const
|
|
1377
|
-
return
|
|
1378
|
-
|
|
1379
|
-
tailSequence:
|
|
1380
|
-
tailDigest:
|
|
1381
|
-
producerEpoch:
|
|
1964
|
+
const inspectTail = Effect.fn("MemoryThreadStore.inspectTail")((unvalidated) => Effect.gen(function* () {
|
|
1965
|
+
const request = yield* validate(ThreadTailRequest, "inspectTail", unvalidated);
|
|
1966
|
+
const thread = yield* Ref.get(state).pipe(Effect.flatMap((current) => findThread(current, request.threadId)));
|
|
1967
|
+
return ThreadTail.make({
|
|
1968
|
+
threadId: request.threadId,
|
|
1969
|
+
tailSequence: thread.tailSequence,
|
|
1970
|
+
tailDigest: thread.tailDigest,
|
|
1971
|
+
producerEpoch: thread.producerEpoch
|
|
1382
1972
|
});
|
|
1383
1973
|
}));
|
|
1384
|
-
const saveCheckpoint = Effect.fn("
|
|
1974
|
+
const saveCheckpoint = Effect.fn("MemoryThreadStore.saveCheckpoint")((unvalidated) => Effect.gen(function* () {
|
|
1385
1975
|
yield* validateCheckpointVersion(unvalidated);
|
|
1386
1976
|
const request = yield* validate(SaveCheckpointRequest, "saveCheckpoint", unvalidated);
|
|
1387
1977
|
const decision = yield* Ref.modify(state, (current) => {
|
|
1388
1978
|
const checkpoint = request.checkpoint;
|
|
1389
|
-
const
|
|
1390
|
-
if (
|
|
1979
|
+
const thread = current.threads.get(checkpoint.threadId);
|
|
1980
|
+
if (thread === void 0) return [{
|
|
1391
1981
|
_tag: "failure",
|
|
1392
|
-
error:
|
|
1982
|
+
error: ThreadNotMaterialized.make({ threadId: checkpoint.threadId })
|
|
1393
1983
|
}, current];
|
|
1394
|
-
if (checkpoint.throughSequence >
|
|
1984
|
+
if (checkpoint.throughSequence > thread.tailSequence) return [{
|
|
1395
1985
|
_tag: "failure",
|
|
1396
1986
|
error: CheckpointRejected.make({
|
|
1397
|
-
|
|
1987
|
+
threadId: checkpoint.threadId,
|
|
1398
1988
|
reason: "ahead-of-tail"
|
|
1399
1989
|
})
|
|
1400
1990
|
}, current];
|
|
1401
|
-
if (
|
|
1991
|
+
if (thread.tailDigests.get(checkpoint.throughSequence) !== checkpoint.tailDigest) return [{
|
|
1402
1992
|
_tag: "failure",
|
|
1403
1993
|
error: CheckpointRejected.make({
|
|
1404
|
-
|
|
1994
|
+
threadId: checkpoint.threadId,
|
|
1405
1995
|
reason: "digest-mismatch"
|
|
1406
1996
|
})
|
|
1407
1997
|
}, current];
|
|
1408
|
-
if (!
|
|
1998
|
+
if (!thread.checkpoints.has(checkpoint.throughSequence) && thread.checkpoints.size >= MAX_CHECKPOINTS_PER_THREAD) return [{
|
|
1409
1999
|
_tag: "failure",
|
|
1410
|
-
error: storeError("saveCheckpoint", `In-memory checkpoint limit ${
|
|
2000
|
+
error: storeError("saveCheckpoint", `In-memory checkpoint limit ${MAX_CHECKPOINTS_PER_THREAD} exceeded`)
|
|
1411
2001
|
}, current];
|
|
1412
|
-
const checkpoints = new Map(
|
|
2002
|
+
const checkpoints = new Map(thread.checkpoints);
|
|
1413
2003
|
checkpoints.set(checkpoint.throughSequence, checkpoint);
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
...
|
|
2004
|
+
const threads = new Map(current.threads);
|
|
2005
|
+
threads.set(checkpoint.threadId, {
|
|
2006
|
+
...thread,
|
|
1417
2007
|
checkpoints
|
|
1418
2008
|
});
|
|
1419
|
-
return [{ _tag: "success" }, {
|
|
2009
|
+
return [{ _tag: "success" }, { threads }];
|
|
1420
2010
|
});
|
|
1421
2011
|
if (decision._tag === "failure") return yield* decision.error;
|
|
1422
2012
|
}));
|
|
1423
|
-
const loadCheckpoint = Effect.fn("
|
|
2013
|
+
const loadCheckpoint = Effect.fn("MemoryThreadStore.loadCheckpoint")((unvalidated) => Effect.gen(function* () {
|
|
1424
2014
|
const request = yield* validate(LoadCheckpointRequest, "loadCheckpoint", unvalidated);
|
|
1425
|
-
const
|
|
1426
|
-
const maximum = request.atOrBeforeSequence ??
|
|
2015
|
+
const thread = yield* Ref.get(state).pipe(Effect.flatMap((current) => findThread(current, request.threadId)));
|
|
2016
|
+
const maximum = request.atOrBeforeSequence ?? thread.tailSequence;
|
|
1427
2017
|
let selected;
|
|
1428
|
-
for (const [sequence, checkpoint] of
|
|
1429
|
-
if (selected !== void 0 &&
|
|
1430
|
-
|
|
2018
|
+
for (const [sequence, checkpoint] of thread.checkpoints) if (sequence <= maximum && (selected === void 0 || sequence > selected.throughSequence)) selected = checkpoint;
|
|
2019
|
+
if (selected !== void 0 && thread.tailDigests.get(selected.throughSequence) !== selected.tailDigest) return yield* CheckpointRejected.make({
|
|
2020
|
+
threadId: request.threadId,
|
|
1431
2021
|
reason: "digest-mismatch"
|
|
1432
2022
|
});
|
|
1433
2023
|
return Option.fromNullishOr(selected);
|
|
1434
2024
|
}));
|
|
1435
|
-
return
|
|
2025
|
+
return ThreadStore.of({
|
|
1436
2026
|
materialize,
|
|
1437
2027
|
append,
|
|
1438
2028
|
read,
|
|
1439
2029
|
observe,
|
|
1440
|
-
export:
|
|
2030
|
+
export: exportThread,
|
|
1441
2031
|
inspectTail,
|
|
1442
|
-
|
|
1443
|
-
|
|
2032
|
+
checkpoints: {
|
|
2033
|
+
save: saveCheckpoint,
|
|
2034
|
+
load: loadCheckpoint
|
|
2035
|
+
}
|
|
1444
2036
|
});
|
|
1445
2037
|
});
|
|
1446
|
-
const
|
|
2038
|
+
const MemoryThreadStoreLive = Layer.effect(ThreadStore, makeThreadStore);
|
|
1447
2039
|
/**
|
|
1448
|
-
* In-memory canonical
|
|
1449
|
-
* SubmissionLedger port; this Layer deliberately provides only the
|
|
2040
|
+
* In-memory canonical Thread persistence. Durable accepted work is served by the separate
|
|
2041
|
+
* SubmissionLedger port; this Layer deliberately provides only the ThreadStore.
|
|
1450
2042
|
*/
|
|
1451
|
-
const MemoryStorageLive =
|
|
2043
|
+
const MemoryStorageLive = MemoryThreadStoreLive;
|
|
1452
2044
|
//#endregion
|
|
1453
|
-
export {
|
|
2045
|
+
export { MemoryScheduleStoreLive, MemoryStorageLive, MemorySubmissionLedgerLive, MemoryThreadStoreLive, memoryScheduleStoreLayer, memorySubmissionLedgerLayer, memorySubscriptionStoreLayer };
|
|
1454
2046
|
|
|
1455
2047
|
//# sourceMappingURL=index.mjs.map
|