@effect-agent/storage-memory 0.1.0-beta.44 → 0.1.0-beta.46

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.
@@ -0,0 +1,600 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { compareScheduleNames } from "@effect-agent/thread/ScheduleTransition";
3
+ import { Clock, Effect, Layer, Ref, Result, Schema } from "effect";
4
+ import { Digest } from "@effect-agent/thread/Records";
5
+ import { AcceptedEvent, DeliveryChange, SourcePartition, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionScanCursors, SubscriptionStore, subscriptionDeliveryKeyString, subscriptionKeyString } from "@effect-agent/thread/Subscription";
6
+ import { applySubscriptionDeliveryChange, subscriptionCanSelect, subscriptionDeliveryCanSelect } from "@effect-agent/thread/SubscriptionTransition";
7
+ //#region src/MemorySubscriptionStore.ts
8
+ var MemorySubscriptionStore_exports = /* @__PURE__ */ __exportAll({ memorySubscriptionStoreLayer: () => memorySubscriptionStoreLayer });
9
+ const error = (reason, code) => SubscriptionError.make({
10
+ reason,
11
+ code
12
+ });
13
+ const samePartition = (left, right) => left.tenantId === right.tenantId && left.address === right.address;
14
+ const sameSource = (left, right) => left.name === right.name && left.version === right.version;
15
+ const encode = (schema, value, code) => Result.try({
16
+ try: () => Schema.encodeSync(Schema.fromJsonString(schema))(value),
17
+ catch: () => error("corrupt", code)
18
+ });
19
+ const decode = (schema, value, code) => Result.try({
20
+ try: () => Schema.decodeSync(Schema.fromJsonString(schema))(value),
21
+ catch: () => error("corrupt", code)
22
+ });
23
+ const decodeEffect = (schema, value, code) => Effect.fromResult(decode(schema, value, code));
24
+ const validate = (schema, value, code) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
25
+ const jsonBytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
26
+ 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;
27
+ const candidateIndexKey = (record) => JSON.stringify([
28
+ record.configuration.source.name,
29
+ record.configuration.source.version,
30
+ record.configuration.matchingKey
31
+ ]);
32
+ const eventCandidateIndexKey = (event) => JSON.stringify([
33
+ event.source.name,
34
+ event.source.version,
35
+ event.matchingKey
36
+ ]);
37
+ 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;
38
+ const deliveryBelongsTo = (delivery, record, event) => subscriptionKeyString(delivery.key.subscription) === subscriptionKeyString(record.key) && delivery.key.eventId === event.eventId && sameSource(delivery.source, event.source);
39
+ const makeMemorySubscriptionStore = Effect.fn("makeMemorySubscriptionStore")(function* (ownedPartition) {
40
+ const partition = yield* validate(SourcePartition, ownedPartition, "partition");
41
+ const state = yield* Ref.make({
42
+ sequence: 0,
43
+ registrations: /* @__PURE__ */ new Map(),
44
+ events: /* @__PURE__ */ new Map(),
45
+ deliveries: /* @__PURE__ */ new Map(),
46
+ registrationIndex: /* @__PURE__ */ new Map(),
47
+ candidateIndex: /* @__PURE__ */ new Map(),
48
+ ownerRegistrationCounts: /* @__PURE__ */ new Map(),
49
+ eventIndex: /* @__PURE__ */ new Map(),
50
+ deliveryIndex: /* @__PURE__ */ new Map(),
51
+ ownerDeliveryCounts: /* @__PURE__ */ new Map(),
52
+ scanCursors: {
53
+ events: "",
54
+ deliveries: "",
55
+ recovery: 0
56
+ }
57
+ });
58
+ const failpoint = yield* SubscriptionFailpoint;
59
+ const requirePartition = (value, code) => samePartition(value.partition, partition) ? Effect.succeed(value) : Effect.fail(error("validation", code));
60
+ const requireKey = (key, code) => validate(SubscriptionKey, key, code).pipe(Effect.flatMap((decoded) => requirePartition(decoded, code)));
61
+ const register = Effect.fn("MemorySubscriptionStore.register")(function* (input, inputLimits) {
62
+ const record = yield* validate(SubscriptionRecord, input, "register-record");
63
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
64
+ yield* requirePartition(record.key, "register-partition");
65
+ yield* failpoint.hit("subscription:register:before");
66
+ const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
67
+ const key = subscriptionKeyString(record.key);
68
+ const existingText = current.registrations.get(key);
69
+ if (existingText !== void 0) {
70
+ const existing = decode(SubscriptionRecord, existingText, "register-existing");
71
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
72
+ return existing.success.creationFingerprint === record.creationFingerprint ? [Result.succeed(existing.success), current] : [Result.fail(error("conflict", "registration-identity")), current];
73
+ }
74
+ if (jsonBytes(record.configuration.context) > limits.maxContextBytes) return [Result.fail(error("capacity", "context-bytes")), current];
75
+ if (jsonBytes(record.configuration.parameters) > limits.maxPayloadBytes) return [Result.fail(error("capacity", "parameters-bytes")), current];
76
+ if (record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return [Result.fail(error("capacity", "lifetime")), current];
77
+ if (current.registrations.size >= limits.maxRegistrations) return [Result.fail(error("capacity", "registrations")), current];
78
+ const ownerCount = current.ownerRegistrationCounts.get(record.key.ownerId) ?? 0;
79
+ if (ownerCount >= limits.maxRegistrationsPerOwner) return [Result.fail(error("capacity", "owner-registrations")), current];
80
+ const assigned = {
81
+ ...record,
82
+ ordinal: current.sequence + 1
83
+ };
84
+ const encoded = encode(SubscriptionRecord, assigned, "register-encode");
85
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
86
+ const registrations = new Map(current.registrations);
87
+ registrations.set(key, encoded.success);
88
+ const registrationIndex = new Map(current.registrationIndex);
89
+ registrationIndex.set(key, {
90
+ key: assigned.key,
91
+ ordinal: assigned.ordinal,
92
+ state: assigned.state,
93
+ recoveryAt: assigned.recovery?.nextAttemptAtMillis ?? null
94
+ });
95
+ const candidateIndex = new Map(current.candidateIndex);
96
+ const candidateKey = candidateIndexKey(assigned);
97
+ candidateIndex.set(candidateKey, [...candidateIndex.get(candidateKey) ?? [], key]);
98
+ const ownerRegistrationCounts = new Map(current.ownerRegistrationCounts);
99
+ ownerRegistrationCounts.set(assigned.key.ownerId, ownerCount + 1);
100
+ return [Result.succeed(assigned), {
101
+ ...current,
102
+ sequence: assigned.ordinal,
103
+ registrations,
104
+ registrationIndex,
105
+ candidateIndex,
106
+ ownerRegistrationCounts
107
+ }];
108
+ })).pipe(Effect.flatMap(Effect.fromResult));
109
+ yield* failpoint.hit("subscription:register:after");
110
+ return result;
111
+ });
112
+ const get = Effect.fn("MemorySubscriptionStore.get")(function* (input) {
113
+ const key = yield* requireKey(input, "get-key");
114
+ const text = (yield* Ref.get(state)).registrations.get(subscriptionKeyString(key));
115
+ return text === void 0 ? null : yield* decodeEffect(SubscriptionRecord, text, "get-record");
116
+ });
117
+ const list = Effect.fn("MemorySubscriptionStore.list")(function* (ownerId, after, limit) {
118
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit <= 0) return yield* error("validation", "list-page");
119
+ const current = yield* Ref.get(state);
120
+ const records = [];
121
+ for (const [storageKey, indexed] of current.registrationIndex) {
122
+ if (indexed.key.ownerId !== ownerId || indexed.ordinal <= after) continue;
123
+ const text = current.registrations.get(storageKey);
124
+ if (text === void 0) return yield* error("corrupt", "list-index");
125
+ records.push(yield* decodeEffect(SubscriptionRecord, text, "list-record"));
126
+ }
127
+ records.sort((a, b) => a.ordinal - b.ordinal);
128
+ return records.slice(0, limit);
129
+ });
130
+ const cancel = Effect.fn("MemorySubscriptionStore.cancel")(function* (input) {
131
+ const key = yield* requireKey(input, "cancel-key");
132
+ yield* failpoint.hit("subscription:cancel:before");
133
+ const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
134
+ const storageKey = subscriptionKeyString(key);
135
+ const text = current.registrations.get(storageKey);
136
+ if (text === void 0) return [Result.fail(error("not-found", "subscription")), current];
137
+ const decoded = decode(SubscriptionRecord, text, "cancel-record");
138
+ if (Result.isFailure(decoded)) return [decoded, current];
139
+ if (decoded.success.state === "cancelled") return [Result.succeed(decoded.success), current];
140
+ const cancelled = {
141
+ ...decoded.success,
142
+ state: "cancelled",
143
+ recovery: null
144
+ };
145
+ const encoded = encode(SubscriptionRecord, cancelled, "cancel-encode");
146
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
147
+ const registrations = new Map(current.registrations);
148
+ registrations.set(storageKey, encoded.success);
149
+ const registrationIndex = new Map(current.registrationIndex);
150
+ const indexed = registrationIndex.get(storageKey);
151
+ if (indexed === void 0) return [Result.fail(error("corrupt", "cancel-index")), current];
152
+ registrationIndex.set(storageKey, {
153
+ ...indexed,
154
+ state: "cancelled",
155
+ recoveryAt: null
156
+ });
157
+ return [Result.succeed(cancelled), {
158
+ ...current,
159
+ registrations,
160
+ registrationIndex
161
+ }];
162
+ })).pipe(Effect.flatMap(Effect.fromResult));
163
+ yield* failpoint.hit("subscription:cancel:after");
164
+ return result;
165
+ });
166
+ const accept = Effect.fn("MemorySubscriptionStore.accept")(function* (input, inputLimits) {
167
+ const event = yield* validate(AcceptedEvent, input, "accept-event");
168
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
169
+ yield* requirePartition(event, "accept-partition");
170
+ yield* failpoint.hit("subscription:accept:before");
171
+ const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
172
+ const existingText = current.events.get(event.eventId);
173
+ if (existingText !== void 0) {
174
+ const existing = decode(AcceptedEvent, existingText, "accept-existing");
175
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
176
+ return sameEventIdentity(existing.success, event) ? [Result.succeed(existing.success), current] : [Result.fail(error("conflict", "event-identity")), current];
177
+ }
178
+ if (jsonBytes(event.payload) > limits.maxPayloadBytes) return [Result.fail(error("capacity", "payload-bytes")), current];
179
+ if (current.events.size >= limits.maxEvents) return [Result.fail(error("capacity", "events")), current];
180
+ const accepted = {
181
+ ...event,
182
+ cutoff: current.sequence + 1,
183
+ cursor: 0,
184
+ routingComplete: false,
185
+ routingFailure: null
186
+ };
187
+ const encoded = encode(AcceptedEvent, accepted, "accept-encode");
188
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
189
+ const events = new Map(current.events);
190
+ events.set(accepted.eventId, encoded.success);
191
+ const eventIndex = new Map(current.eventIndex);
192
+ eventIndex.set(accepted.eventId, {
193
+ routingComplete: false,
194
+ nextAttemptAtMillis: accepted.nextAttemptAtMillis
195
+ });
196
+ return [Result.succeed(accepted), {
197
+ ...current,
198
+ sequence: accepted.cutoff,
199
+ events,
200
+ eventIndex
201
+ }];
202
+ })).pipe(Effect.flatMap(Effect.fromResult));
203
+ yield* failpoint.hit("subscription:accept:after");
204
+ return result;
205
+ });
206
+ const event = Effect.fn("MemorySubscriptionStore.event")(function* (eventId) {
207
+ const text = (yield* Ref.get(state)).events.get(eventId);
208
+ return text === void 0 ? null : yield* decodeEffect(AcceptedEvent, text, "event-record");
209
+ });
210
+ const pendingEvents = Effect.fn("MemorySubscriptionStore.pendingEvents")(function* (nowMillis, after, limit) {
211
+ const events = [];
212
+ for (const [eventId, indexed] of (yield* Ref.get(state)).eventIndex) if (!indexed.routingComplete && indexed.nextAttemptAtMillis <= nowMillis && compareScheduleNames(eventId, after) > 0) events.push(eventId);
213
+ events.sort(compareScheduleNames);
214
+ return events.slice(0, limit);
215
+ });
216
+ const candidates = Effect.fn("MemorySubscriptionStore.candidates")(function* (input, limit) {
217
+ const accepted = yield* validate(AcceptedEvent, input, "candidates-event");
218
+ yield* requirePartition(accepted, "candidates-partition");
219
+ const stored = yield* event(accepted.eventId);
220
+ if (stored === null || !sameEventIdentity(stored, accepted)) return yield* error(stored === null ? "not-found" : "conflict", "event");
221
+ const current = yield* Ref.get(state);
222
+ const records = [];
223
+ for (const storageKey of current.candidateIndex.get(eventCandidateIndexKey(stored)) ?? []) {
224
+ const indexed = current.registrationIndex.get(storageKey);
225
+ if (indexed === void 0) return yield* error("corrupt", "candidate-index");
226
+ if (indexed.ordinal <= stored.cursor || indexed.ordinal > stored.cutoff) continue;
227
+ const text = current.registrations.get(storageKey);
228
+ if (text === void 0) return yield* error("corrupt", "candidate-record");
229
+ records.push(yield* decodeEffect(SubscriptionRecord, text, "candidate-record"));
230
+ }
231
+ records.sort((a, b) => a.ordinal - b.ordinal);
232
+ return records.slice(0, limit);
233
+ });
234
+ const select = Effect.fn("MemorySubscriptionStore.select")(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
235
+ const suppliedEvent = yield* validate(AcceptedEvent, inputEvent, "select-event");
236
+ const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
237
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
238
+ yield* requirePartition(suppliedEvent, "select-partition");
239
+ yield* failpoint.hit("subscription:select:before");
240
+ const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
241
+ yield* Effect.uninterruptible(Ref.modify(state, (current) => {
242
+ const eventText = current.events.get(suppliedEvent.eventId);
243
+ if (eventText === void 0) return [Result.fail(error("not-found", "event")), current];
244
+ const decodedEvent = decode(AcceptedEvent, eventText, "select-event-record");
245
+ if (Result.isFailure(decodedEvent)) return [Result.fail(decodedEvent.failure), current];
246
+ const accepted = decodedEvent.success;
247
+ if (!sameEventIdentity(accepted, suppliedEvent) || suppliedEvent.cursor !== accepted.cursor) return [Result.fail(error("conflict", "event-cursor")), current];
248
+ if (accepted.routingComplete) return [complete && cursor === accepted.cursor ? Result.void : Result.fail(error("conflict", "routing-complete")), current];
249
+ if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff) return [Result.fail(error("validation", "cursor")), current];
250
+ const additions = [];
251
+ const updates = [];
252
+ const additionIndex = [];
253
+ const registrationUpdates = [];
254
+ const owners = new Map(current.ownerDeliveryCounts);
255
+ for (const delivery of deliveries) {
256
+ const recordText = current.registrations.get(subscriptionKeyString(delivery.key.subscription));
257
+ if (recordText === void 0) return [Result.fail(error("not-found", "subscription")), current];
258
+ const record = decode(SubscriptionRecord, recordText, "select-registration");
259
+ if (Result.isFailure(record)) return [Result.fail(record.failure), current];
260
+ 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];
261
+ if (!subscriptionCanSelect(record.success, accepted, effectiveNowMillis, false)) continue;
262
+ const deliveryKey = subscriptionDeliveryKeyString(delivery.key);
263
+ const existingText = current.deliveries.get(deliveryKey);
264
+ if (existingText !== void 0) {
265
+ const existing = decode(SubscriptionDelivery, existingText, "select-existing");
266
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
267
+ if (!sameDeliveryIdentity(existing.success, delivery)) return [Result.fail(error("conflict", "delivery-identity")), current];
268
+ continue;
269
+ }
270
+ const encodedDelivery = encode(SubscriptionDelivery, delivery, "select-delivery-encode");
271
+ if (Result.isFailure(encodedDelivery)) return [Result.fail(encodedDelivery.failure), current];
272
+ additions.push([deliveryKey, encodedDelivery.success]);
273
+ additionIndex.push([
274
+ deliveryKey,
275
+ {
276
+ key: delivery.key,
277
+ state: delivery.state,
278
+ nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis
279
+ },
280
+ record.success.key.ownerId
281
+ ]);
282
+ const ownerId = record.success.key.ownerId;
283
+ owners.set(ownerId, (owners.get(ownerId) ?? 0) + 1);
284
+ if ((owners.get(ownerId) ?? 0) > limits.maxDeliveriesPerOwner) return [Result.fail(error("capacity", "owner-deliveries")), current];
285
+ if (record.success.configuration.mode === "once") {
286
+ const consumed = {
287
+ ...record.success,
288
+ state: "consumed",
289
+ recovery: null
290
+ };
291
+ const encodedRecord = encode(SubscriptionRecord, consumed, "select-registration-encode");
292
+ if (Result.isFailure(encodedRecord)) return [Result.fail(encodedRecord.failure), current];
293
+ const consumedKey = subscriptionKeyString(consumed.key);
294
+ updates.push([consumedKey, encodedRecord.success]);
295
+ const indexed = current.registrationIndex.get(consumedKey);
296
+ if (indexed === void 0) return [Result.fail(error("corrupt", "selection-index")), current];
297
+ registrationUpdates.push([consumedKey, {
298
+ ...indexed,
299
+ state: "consumed",
300
+ recoveryAt: null
301
+ }]);
302
+ }
303
+ }
304
+ if (current.deliveries.size + additions.length > limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
305
+ const registrations = new Map(current.registrations);
306
+ for (const [key, value] of updates) registrations.set(key, value);
307
+ const nextDeliveries = new Map(current.deliveries);
308
+ for (const [key, value] of additions) nextDeliveries.set(key, value);
309
+ const deliveryIndex = new Map(current.deliveryIndex);
310
+ for (const [key, value] of additionIndex) deliveryIndex.set(key, value);
311
+ const registrationIndex = new Map(current.registrationIndex);
312
+ for (const [key, value] of registrationUpdates) registrationIndex.set(key, value);
313
+ const nextEvent = {
314
+ ...accepted,
315
+ cursor,
316
+ routingComplete: complete,
317
+ routingFailure: null
318
+ };
319
+ const encodedEvent = encode(AcceptedEvent, nextEvent, "select-event-encode");
320
+ if (Result.isFailure(encodedEvent)) return [Result.fail(encodedEvent.failure), current];
321
+ const events = new Map(current.events);
322
+ events.set(nextEvent.eventId, encodedEvent.success);
323
+ const eventIndex = new Map(current.eventIndex);
324
+ eventIndex.set(nextEvent.eventId, {
325
+ routingComplete: nextEvent.routingComplete,
326
+ nextAttemptAtMillis: nextEvent.nextAttemptAtMillis
327
+ });
328
+ return [Result.void, {
329
+ ...current,
330
+ registrations,
331
+ registrationIndex,
332
+ events,
333
+ eventIndex,
334
+ deliveries: nextDeliveries,
335
+ deliveryIndex,
336
+ ownerDeliveryCounts: owners
337
+ }];
338
+ })).pipe(Effect.flatMap(Effect.fromResult));
339
+ yield* failpoint.hit("subscription:select:after");
340
+ });
341
+ const catchUp = Effect.fn("MemorySubscriptionStore.catchUp")(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
342
+ const suppliedEvent = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
343
+ const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
344
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
345
+ yield* requirePartition(suppliedEvent, "catch-up-partition");
346
+ yield* failpoint.hit("subscription:catch-up:before");
347
+ const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
348
+ yield* Effect.uninterruptible(Ref.modify(state, (current) => {
349
+ const eventText = current.events.get(suppliedEvent.eventId);
350
+ const recordText = current.registrations.get(subscriptionKeyString(delivery.key.subscription));
351
+ if (eventText === void 0 || recordText === void 0) return [Result.fail(error("not-found", eventText === void 0 ? "event" : "subscription")), current];
352
+ const accepted = decode(AcceptedEvent, eventText, "catch-up-event-record");
353
+ if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
354
+ const record = decode(SubscriptionRecord, recordText, "catch-up-registration");
355
+ if (Result.isFailure(record)) return [Result.fail(record.failure), current];
356
+ 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];
357
+ const key = subscriptionDeliveryKeyString(delivery.key);
358
+ const existingText = current.deliveries.get(key);
359
+ if (existingText !== void 0) {
360
+ const existing = decode(SubscriptionDelivery, existingText, "catch-up-existing");
361
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
362
+ return sameDeliveryIdentity(existing.success, delivery) ? [Result.void, current] : [Result.fail(error("conflict", "delivery-identity")), current];
363
+ }
364
+ if (!subscriptionCanSelect(record.success, accepted.success, effectiveNowMillis, true)) return [Result.fail(error("conflict", "catch-up-eligibility")), current];
365
+ if (current.deliveries.size >= limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
366
+ const ownerCount = current.ownerDeliveryCounts.get(record.success.key.ownerId) ?? 0;
367
+ if (ownerCount >= limits.maxDeliveriesPerOwner) return [Result.fail(error("capacity", "owner-deliveries")), current];
368
+ const encodedDelivery = encode(SubscriptionDelivery, delivery, "catch-up-delivery-encode");
369
+ if (Result.isFailure(encodedDelivery)) return [Result.fail(encodedDelivery.failure), current];
370
+ const consumed = {
371
+ ...record.success,
372
+ state: "consumed",
373
+ recovery: null
374
+ };
375
+ const encodedRecord = encode(SubscriptionRecord, consumed, "catch-up-registration-encode");
376
+ if (Result.isFailure(encodedRecord)) return [Result.fail(encodedRecord.failure), current];
377
+ const deliveries = new Map(current.deliveries);
378
+ deliveries.set(key, encodedDelivery.success);
379
+ const registrations = new Map(current.registrations);
380
+ const consumedKey = subscriptionKeyString(consumed.key);
381
+ registrations.set(consumedKey, encodedRecord.success);
382
+ const registrationIndex = new Map(current.registrationIndex);
383
+ const indexed = registrationIndex.get(consumedKey);
384
+ if (indexed === void 0) return [Result.fail(error("corrupt", "catch-up-index")), current];
385
+ registrationIndex.set(consumedKey, {
386
+ ...indexed,
387
+ state: "consumed",
388
+ recoveryAt: null
389
+ });
390
+ const deliveryIndex = new Map(current.deliveryIndex);
391
+ deliveryIndex.set(key, {
392
+ key: delivery.key,
393
+ state: delivery.state,
394
+ nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis
395
+ });
396
+ const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
397
+ ownerDeliveryCounts.set(consumed.key.ownerId, ownerCount + 1);
398
+ return [Result.void, {
399
+ ...current,
400
+ deliveries,
401
+ deliveryIndex,
402
+ ownerDeliveryCounts,
403
+ registrations,
404
+ registrationIndex
405
+ }];
406
+ })).pipe(Effect.flatMap(Effect.fromResult));
407
+ yield* failpoint.hit("subscription:catch-up:after");
408
+ });
409
+ const deferEvent = Effect.fn("MemorySubscriptionStore.deferEvent")(function* (eventId, nextAttemptAtMillis, code) {
410
+ const routingFailure = code === void 0 ? "routing-failed" : yield* validate(SubscriptionName, code, "routing-failure");
411
+ yield* failpoint.hit("subscription:defer-event:before");
412
+ yield* Effect.uninterruptible(Ref.modify(state, (current) => {
413
+ const text = current.events.get(eventId);
414
+ if (text === void 0) return [Result.fail(error("not-found", "event")), current];
415
+ const accepted = decode(AcceptedEvent, text, "defer-event-record");
416
+ if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
417
+ const updated = {
418
+ ...accepted.success,
419
+ nextAttemptAtMillis,
420
+ routingFailure
421
+ };
422
+ const encoded = encode(AcceptedEvent, updated, "defer-event-encode");
423
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
424
+ const events = new Map(current.events);
425
+ events.set(eventId, encoded.success);
426
+ const eventIndex = new Map(current.eventIndex);
427
+ const indexed = eventIndex.get(eventId);
428
+ if (indexed === void 0) return [Result.fail(error("corrupt", "defer-event-index")), current];
429
+ eventIndex.set(eventId, {
430
+ ...indexed,
431
+ nextAttemptAtMillis
432
+ });
433
+ return [Result.void, {
434
+ ...current,
435
+ events,
436
+ eventIndex
437
+ }];
438
+ })).pipe(Effect.flatMap(Effect.fromResult));
439
+ yield* failpoint.hit("subscription:defer-event:after");
440
+ });
441
+ const delivery = Effect.fn("MemorySubscriptionStore.delivery")(function* (input) {
442
+ const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
443
+ yield* requirePartition(key.subscription, "delivery-partition");
444
+ const text = (yield* Ref.get(state)).deliveries.get(subscriptionDeliveryKeyString(key));
445
+ return text === void 0 ? null : yield* decodeEffect(SubscriptionDelivery, text, "delivery-record");
446
+ });
447
+ const pendingDeliveries = Effect.fn("MemorySubscriptionStore.pendingDeliveries")(function* (nowMillis, after, limit) {
448
+ const items = [];
449
+ 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);
450
+ items.sort((a, b) => compareScheduleNames(subscriptionDeliveryKeyString(a), subscriptionDeliveryKeyString(b)));
451
+ return items.slice(0, limit);
452
+ });
453
+ const listDeliveries = Effect.fn("MemorySubscriptionStore.listDeliveries")(function* (input, after, limit) {
454
+ const key = yield* requireKey(input, "list-deliveries-key");
455
+ const items = [];
456
+ for (const text of (yield* Ref.get(state)).deliveries.values()) {
457
+ const item = yield* decodeEffect(SubscriptionDelivery, text, "list-delivery");
458
+ const itemKey = subscriptionDeliveryKeyString(item.key);
459
+ if (subscriptionKeyString(item.key.subscription) === subscriptionKeyString(key) && compareScheduleNames(itemKey, after) > 0) items.push(item);
460
+ }
461
+ items.sort((a, b) => compareScheduleNames(subscriptionDeliveryKeyString(a.key), subscriptionDeliveryKeyString(b.key)));
462
+ return items.slice(0, limit);
463
+ });
464
+ const changeDelivery = Effect.fn("MemorySubscriptionStore.changeDelivery")(function* (inputKey, inputDeliveryId, inputChange) {
465
+ const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
466
+ const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
467
+ const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
468
+ yield* requirePartition(key.subscription, "change-delivery-partition");
469
+ yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
470
+ const effectiveChange = change._tag === "Prepare" ? {
471
+ ...change,
472
+ nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis)
473
+ } : change;
474
+ const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
475
+ const storageKey = subscriptionDeliveryKeyString(key);
476
+ const text = current.deliveries.get(storageKey);
477
+ if (text === void 0) return [Result.fail(error("not-found", "delivery")), current];
478
+ const decoded = decode(SubscriptionDelivery, text, "change-delivery-record");
479
+ if (Result.isFailure(decoded)) return [decoded, current];
480
+ const registrationText = current.registrations.get(subscriptionKeyString(key.subscription));
481
+ if (registrationText === void 0) return [Result.fail(error("corrupt", "delivery-registration")), current];
482
+ const registration = decode(SubscriptionRecord, registrationText, "delivery-registration");
483
+ if (Result.isFailure(registration)) return [Result.fail(registration.failure), current];
484
+ const transition = applySubscriptionDeliveryChange(decoded.success, registration.success, deliveryId, effectiveChange);
485
+ if (Result.isFailure(transition)) return [Result.fail(transition.failure), current];
486
+ if (transition.success === decoded.success) return [Result.succeed(decoded.success), current];
487
+ const updated = transition.success;
488
+ const encoded = encode(SubscriptionDelivery, updated, "change-delivery-encode");
489
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
490
+ const deliveries = new Map(current.deliveries);
491
+ deliveries.set(storageKey, encoded.success);
492
+ const deliveryIndex = new Map(current.deliveryIndex);
493
+ const indexed = deliveryIndex.get(storageKey);
494
+ if (indexed === void 0) return [Result.fail(error("corrupt", "delivery-index")), current];
495
+ deliveryIndex.set(storageKey, {
496
+ ...indexed,
497
+ state: updated.state,
498
+ nextAttemptAtMillis: updated.retry.nextAttemptAtMillis
499
+ });
500
+ return [Result.succeed(updated), {
501
+ ...current,
502
+ deliveries,
503
+ deliveryIndex
504
+ }];
505
+ })).pipe(Effect.flatMap(Effect.fromResult));
506
+ yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
507
+ return result;
508
+ });
509
+ const recovering = Effect.fn("MemorySubscriptionStore.recovering")(function* (nowMillis, after, limit) {
510
+ const records = [];
511
+ 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({
512
+ key: item.key,
513
+ ordinal: item.ordinal
514
+ });
515
+ records.sort((a, b) => a.ordinal - b.ordinal);
516
+ return records.slice(0, limit);
517
+ });
518
+ const deferRecovery = Effect.fn("MemorySubscriptionStore.deferRecovery")(function* (input, recovery) {
519
+ const key = yield* requireKey(input, "defer-recovery-key");
520
+ yield* failpoint.hit("subscription:defer-recovery:before");
521
+ yield* Effect.uninterruptible(Ref.modify(state, (current) => {
522
+ const storageKey = subscriptionKeyString(key);
523
+ const text = current.registrations.get(storageKey);
524
+ if (text === void 0) return [Result.fail(error("not-found", "subscription")), current];
525
+ const record = decode(SubscriptionRecord, text, "defer-recovery-record");
526
+ if (Result.isFailure(record)) return [Result.fail(record.failure), current];
527
+ const updated = {
528
+ ...record.success,
529
+ recovery: record.success.state === "active" ? recovery : null
530
+ };
531
+ const encoded = encode(SubscriptionRecord, updated, "defer-recovery-encode");
532
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
533
+ const registrations = new Map(current.registrations);
534
+ registrations.set(storageKey, encoded.success);
535
+ const registrationIndex = new Map(current.registrationIndex);
536
+ const indexed = registrationIndex.get(storageKey);
537
+ if (indexed === void 0) return [Result.fail(error("corrupt", "recovery-index")), current];
538
+ registrationIndex.set(storageKey, {
539
+ ...indexed,
540
+ recoveryAt: updated.recovery?.nextAttemptAtMillis ?? null
541
+ });
542
+ return [Result.void, {
543
+ ...current,
544
+ registrations,
545
+ registrationIndex
546
+ }];
547
+ })).pipe(Effect.flatMap(Effect.fromResult));
548
+ yield* failpoint.hit("subscription:defer-recovery:after");
549
+ });
550
+ const readScanCursors = Ref.get(state).pipe(Effect.map((current) => current.scanCursors));
551
+ const advanceScanCursors = Effect.fn("MemorySubscriptionStore.advanceScanCursors")(function* (input) {
552
+ const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
553
+ yield* failpoint.hit("subscription:advance-scan-cursors:before");
554
+ yield* Effect.uninterruptible(Ref.update(state, (current) => ({
555
+ ...current,
556
+ scanCursors: cursors
557
+ })));
558
+ yield* failpoint.hit("subscription:advance-scan-cursors:after");
559
+ });
560
+ const nextDeadline = Effect.gen(function* () {
561
+ let deadline = null;
562
+ const current = yield* Ref.get(state);
563
+ if (current.scanCursors.events !== "" || current.scanCursors.deliveries !== "" || current.scanCursors.recovery !== 0) return 0;
564
+ const consider = (value) => {
565
+ if (deadline === null || value < deadline) deadline = value;
566
+ };
567
+ for (const accepted of current.eventIndex.values()) if (!accepted.routingComplete) consider(accepted.nextAttemptAtMillis);
568
+ for (const item of current.deliveryIndex.values()) if (item.state !== "delivered" && item.state !== "refused") consider(item.nextAttemptAtMillis);
569
+ for (const record of current.registrationIndex.values()) if (record.state === "active" && record.recoveryAt !== null) consider(record.recoveryAt);
570
+ return deadline;
571
+ }).pipe(Effect.withSpan("MemorySubscriptionStore.nextDeadline"));
572
+ return SubscriptionStore.of({
573
+ partition,
574
+ register,
575
+ get,
576
+ list,
577
+ cancel,
578
+ accept,
579
+ event,
580
+ pendingEvents,
581
+ candidates,
582
+ select,
583
+ catchUp,
584
+ deferEvent,
585
+ delivery,
586
+ pendingDeliveries,
587
+ listDeliveries,
588
+ changeDelivery,
589
+ recovering,
590
+ deferRecovery,
591
+ readScanCursors,
592
+ advanceScanCursors,
593
+ nextDeadline
594
+ });
595
+ });
596
+ const memorySubscriptionStoreLayer = (partition) => Layer.effect(SubscriptionStore, makeMemorySubscriptionStore(partition));
597
+ //#endregion
598
+ export { memorySubscriptionStoreLayer, MemorySubscriptionStore_exports as t };
599
+
600
+ //# sourceMappingURL=MemorySubscriptionStore.mjs.map