@yielded/agent-storage-memory 0.1.0-beta.167 → 0.1.0-beta.168

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.
@@ -199,9 +199,7 @@ const recoveryCountsAfter = (
199
199
  return counts;
200
200
  };
201
201
 
202
- const makeMemorySubscriptionStore = Effect.fn("makeMemorySubscriptionStore")(function* (
203
- ownedPartition: SourcePartition,
204
- ) {
202
+ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition: SourcePartition) {
205
203
  const partition = yield* validate(SourcePartition, ownedPartition, "partition");
206
204
 
207
205
  const state = yield* Ref.make<MemorySubscriptionState>({
@@ -242,108 +240,105 @@ const makeMemorySubscriptionStore = Effect.fn("makeMemorySubscriptionStore")(fun
242
240
  Effect.flatMap((decoded) => requirePartition(decoded, code)),
243
241
  );
244
242
 
245
- const register: SubscriptionStore["Service"]["register"] = Effect.fn(
246
- "MemorySubscriptionStore.register",
247
- )(function* (input, inputLimits) {
248
- const record = yield* validate(SubscriptionRecord, input, "register-record");
249
- const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
250
-
251
- yield* requirePartition(record.key, "register-partition");
252
- yield* failpoint.hit("subscription:register:before");
253
-
254
- const result = yield* Effect.uninterruptible(
255
- Ref.modify(
256
- state,
257
- (
258
- current,
259
- ): readonly [
260
- Result.Result<SubscriptionRecord, SubscriptionError>,
261
- MemorySubscriptionState,
262
- ] => {
263
- const key = subscriptionKeyString(record.key);
264
- const existingText = current.registrations.get(key);
265
-
266
- if (existingText !== undefined) {
267
- const existing = decode(SubscriptionRecord, existingText, "register-existing");
268
-
269
- if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
270
-
271
- return existing.success.creationFingerprint === record.creationFingerprint
272
- ? [Result.succeed(existing.success), current]
273
- : [Result.fail(error("conflict", "registration-identity")), current];
274
- }
275
- if (jsonBytes(record.configuration.context) > limits.maxContextBytes)
276
- return [Result.fail(error("capacity", "context-bytes")), current];
277
- if (jsonBytes(record.configuration.parameters) > limits.maxPayloadBytes)
278
- return [Result.fail(error("capacity", "parameters-bytes")), current];
279
- if (
280
- record.configuration.expiresAtMillis !== null &&
281
- record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis
282
- )
283
- return [Result.fail(error("capacity", "lifetime")), current];
284
- if (current.registrations.size >= limits.maxRegistrations)
285
- return [Result.fail(error("capacity", "registrations")), current];
286
- const ownerCount = current.ownerRegistrationCounts.get(record.key.ownerId) ?? 0;
287
-
288
- if (ownerCount >= limits.maxRegistrationsPerOwner)
289
- return [Result.fail(error("capacity", "owner-registrations")), current];
290
- const assigned = { ...record, ordinal: current.sequence + 1 };
291
- const encoded = encode(SubscriptionRecord, assigned, "register-encode");
292
-
293
- if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
294
- const registrations = new Map(current.registrations);
295
-
296
- registrations.set(key, encoded.success);
297
- const registrationIndex = new Map(current.registrationIndex);
298
-
299
- registrationIndex.set(key, {
300
- key: assigned.key,
301
- ordinal: assigned.ordinal,
302
- state: assigned.state,
303
- recoveryKey: assigned.recovery === null ? null : candidateIndexKey(assigned),
304
- recoveryAt: assigned.recovery?.nextAttemptAtMillis ?? null,
305
- });
306
- const candidateIndex = new Map(current.candidateIndex);
307
- const candidateKey = candidateIndexKey(assigned);
308
-
309
- candidateIndex.set(candidateKey, [...(candidateIndex.get(candidateKey) ?? []), key]);
310
- const ownerRegistrationCounts = new Map(current.ownerRegistrationCounts);
311
-
312
- ownerRegistrationCounts.set(assigned.key.ownerId, ownerCount + 1);
313
-
314
- return [
315
- Result.succeed(assigned),
316
- {
317
- ...current,
318
- sequence: assigned.ordinal,
319
- registrations,
320
- registrationIndex,
321
- recoveryCounts: recoveryCountsAfter(current, registrationIndex, [key]),
322
- candidateIndex,
323
- ownerRegistrationCounts,
324
- },
325
- ];
326
- },
327
- ),
328
- ).pipe(Effect.flatMap(Effect.fromResult));
329
-
330
- yield* failpoint.hit("subscription:register:after");
331
-
332
- return result;
333
- });
243
+ const register: SubscriptionStore["Service"]["register"] = Effect.fnUntraced(
244
+ function* (input, inputLimits) {
245
+ const record = yield* validate(SubscriptionRecord, input, "register-record");
246
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
247
+
248
+ yield* requirePartition(record.key, "register-partition");
249
+ yield* failpoint.hit("subscription:register:before");
250
+
251
+ const result = yield* Effect.uninterruptible(
252
+ Ref.modify(
253
+ state,
254
+ (
255
+ current,
256
+ ): readonly [
257
+ Result.Result<SubscriptionRecord, SubscriptionError>,
258
+ MemorySubscriptionState,
259
+ ] => {
260
+ const key = subscriptionKeyString(record.key);
261
+ const existingText = current.registrations.get(key);
334
262
 
335
- const get: SubscriptionStore["Service"]["get"] = Effect.fn("MemorySubscriptionStore.get")(
336
- function* (input) {
337
- const key = yield* requireKey(input, "get-key");
338
- const text = (yield* Ref.get(state)).registrations.get(subscriptionKeyString(key));
263
+ if (existingText !== undefined) {
264
+ const existing = decode(SubscriptionRecord, existingText, "register-existing");
265
+
266
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
267
+
268
+ return existing.success.creationFingerprint === record.creationFingerprint
269
+ ? [Result.succeed(existing.success), current]
270
+ : [Result.fail(error("conflict", "registration-identity")), current];
271
+ }
272
+ if (jsonBytes(record.configuration.context) > limits.maxContextBytes)
273
+ return [Result.fail(error("capacity", "context-bytes")), current];
274
+ if (jsonBytes(record.configuration.parameters) > limits.maxPayloadBytes)
275
+ return [Result.fail(error("capacity", "parameters-bytes")), current];
276
+ if (
277
+ record.configuration.expiresAtMillis !== null &&
278
+ record.configuration.expiresAtMillis - record.createdAtMillis >
279
+ limits.maxLifetimeMillis
280
+ )
281
+ return [Result.fail(error("capacity", "lifetime")), current];
282
+ if (current.registrations.size >= limits.maxRegistrations)
283
+ return [Result.fail(error("capacity", "registrations")), current];
284
+ const ownerCount = current.ownerRegistrationCounts.get(record.key.ownerId) ?? 0;
285
+
286
+ if (ownerCount >= limits.maxRegistrationsPerOwner)
287
+ return [Result.fail(error("capacity", "owner-registrations")), current];
288
+ const assigned = { ...record, ordinal: current.sequence + 1 };
289
+ const encoded = encode(SubscriptionRecord, assigned, "register-encode");
290
+
291
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
292
+ const registrations = new Map(current.registrations);
293
+
294
+ registrations.set(key, encoded.success);
295
+ const registrationIndex = new Map(current.registrationIndex);
296
+
297
+ registrationIndex.set(key, {
298
+ key: assigned.key,
299
+ ordinal: assigned.ordinal,
300
+ state: assigned.state,
301
+ recoveryKey: assigned.recovery === null ? null : candidateIndexKey(assigned),
302
+ recoveryAt: assigned.recovery?.nextAttemptAtMillis ?? null,
303
+ });
304
+ const candidateIndex = new Map(current.candidateIndex);
305
+ const candidateKey = candidateIndexKey(assigned);
306
+
307
+ candidateIndex.set(candidateKey, [...(candidateIndex.get(candidateKey) ?? []), key]);
308
+ const ownerRegistrationCounts = new Map(current.ownerRegistrationCounts);
309
+
310
+ ownerRegistrationCounts.set(assigned.key.ownerId, ownerCount + 1);
311
+
312
+ return [
313
+ Result.succeed(assigned),
314
+ {
315
+ ...current,
316
+ sequence: assigned.ordinal,
317
+ registrations,
318
+ registrationIndex,
319
+ recoveryCounts: recoveryCountsAfter(current, registrationIndex, [key]),
320
+ candidateIndex,
321
+ ownerRegistrationCounts,
322
+ },
323
+ ];
324
+ },
325
+ ),
326
+ ).pipe(Effect.flatMap(Effect.fromResult));
339
327
 
340
- return text === undefined
341
- ? null
342
- : yield* decodeEffect(SubscriptionRecord, text, "get-record");
328
+ yield* failpoint.hit("subscription:register:after");
329
+
330
+ return result;
343
331
  },
344
332
  );
345
333
 
346
- const list: SubscriptionStore["Service"]["list"] = Effect.fn("MemorySubscriptionStore.list")(
334
+ const get: SubscriptionStore["Service"]["get"] = Effect.fnUntraced(function* (input) {
335
+ const key = yield* requireKey(input, "get-key");
336
+ const text = (yield* Ref.get(state)).registrations.get(subscriptionKeyString(key));
337
+
338
+ return text === undefined ? null : yield* decodeEffect(SubscriptionRecord, text, "get-record");
339
+ });
340
+
341
+ const list: SubscriptionStore["Service"]["list"] = Effect.fnUntraced(
347
342
  function* (ownerId, after, limit) {
348
343
  if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit <= 0)
349
344
  return yield* error("validation", "list-page");
@@ -363,690 +358,696 @@ const makeMemorySubscriptionStore = Effect.fn("makeMemorySubscriptionStore")(fun
363
358
  },
364
359
  );
365
360
 
366
- const change: SubscriptionStore["Service"]["change"] = Effect.fn(
367
- "MemorySubscriptionStore.change",
368
- )(function* (input, expectedRevision, inputChange) {
369
- const key = yield* requireKey(input, "change-key");
370
- const change = yield* validate(SubscriptionChange, inputChange, "change");
371
-
372
- yield* validate(Schema.Int.check(Schema.isGreaterThan(0)), expectedRevision, "revision");
373
- yield* failpoint.hit("subscription:change:before");
374
-
375
- const updated = yield* Effect.uninterruptible(
376
- Ref.modify(
377
- state,
378
- (
379
- current,
380
- ): readonly [
381
- Result.Result<SubscriptionRecord, SubscriptionError>,
382
- MemorySubscriptionState,
383
- ] => {
384
- const storageKey = subscriptionKeyString(key);
385
- const text = current.registrations.get(storageKey);
386
-
387
- if (text === undefined) return [Result.fail(error("not-found", "subscription")), current];
388
- const existing = decode(SubscriptionRecord, text, "change-record");
389
-
390
- if (Result.isFailure(existing)) return [existing, current];
391
- const updated = applySubscriptionChange(existing.success, expectedRevision, change);
392
-
393
- if (Result.isFailure(updated)) return [updated, current];
394
- const revised = { ...updated.success, ordinal: current.sequence + 1 };
395
- const encoded = encode(SubscriptionRecord, revised, "change-encode");
396
-
397
- if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
398
- const registrations = new Map(current.registrations);
399
-
400
- registrations.set(storageKey, encoded.success);
401
- const registrationIndex = new Map(current.registrationIndex);
402
-
403
- registrationIndex.set(storageKey, {
404
- key,
405
- ordinal: revised.ordinal,
406
- state: revised.state,
407
- recoveryKey: revised.recovery === null ? null : candidateIndexKey(revised),
408
- recoveryAt: revised.recovery?.nextAttemptAtMillis ?? null,
409
- });
410
- const candidateIndex = new Map(current.candidateIndex);
411
- const oldKey = candidateIndexKey(existing.success);
412
- const nextKey = candidateIndexKey(revised);
413
-
414
- {
415
- candidateIndex.set(
416
- oldKey,
417
- (candidateIndex.get(oldKey) ?? []).filter((key) => key !== storageKey),
418
- );
419
- candidateIndex.set(
420
- nextKey,
421
- [...(candidateIndex.get(nextKey) ?? []), storageKey].sort(
422
- (a, b) =>
423
- (registrationIndex.get(a)?.ordinal ?? 0) -
424
- (registrationIndex.get(b)?.ordinal ?? 0),
425
- ),
426
- );
427
- }
361
+ const change: SubscriptionStore["Service"]["change"] = Effect.fnUntraced(
362
+ function* (input, expectedRevision, inputChange) {
363
+ const key = yield* requireKey(input, "change-key");
364
+ const change = yield* validate(SubscriptionChange, inputChange, "change");
365
+
366
+ yield* validate(Schema.Int.check(Schema.isGreaterThan(0)), expectedRevision, "revision");
367
+ yield* failpoint.hit("subscription:change:before");
368
+
369
+ const updated = yield* Effect.uninterruptible(
370
+ Ref.modify(
371
+ state,
372
+ (
373
+ current,
374
+ ): readonly [
375
+ Result.Result<SubscriptionRecord, SubscriptionError>,
376
+ MemorySubscriptionState,
377
+ ] => {
378
+ const storageKey = subscriptionKeyString(key);
379
+ const text = current.registrations.get(storageKey);
380
+
381
+ if (text === undefined)
382
+ return [Result.fail(error("not-found", "subscription")), current];
383
+ const existing = decode(SubscriptionRecord, text, "change-record");
384
+
385
+ if (Result.isFailure(existing)) return [existing, current];
386
+ const updated = applySubscriptionChange(existing.success, expectedRevision, change);
387
+
388
+ if (Result.isFailure(updated)) return [updated, current];
389
+ const revised = { ...updated.success, ordinal: current.sequence + 1 };
390
+ const encoded = encode(SubscriptionRecord, revised, "change-encode");
391
+
392
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
393
+ const registrations = new Map(current.registrations);
394
+
395
+ registrations.set(storageKey, encoded.success);
396
+ const registrationIndex = new Map(current.registrationIndex);
397
+
398
+ registrationIndex.set(storageKey, {
399
+ key,
400
+ ordinal: revised.ordinal,
401
+ state: revised.state,
402
+ recoveryKey: revised.recovery === null ? null : candidateIndexKey(revised),
403
+ recoveryAt: revised.recovery?.nextAttemptAtMillis ?? null,
404
+ });
405
+ const candidateIndex = new Map(current.candidateIndex);
406
+ const oldKey = candidateIndexKey(existing.success);
407
+ const nextKey = candidateIndexKey(revised);
428
408
 
429
- return [
430
- Result.succeed(revised),
431
409
  {
432
- ...current,
433
- sequence: revised.ordinal,
434
- registrations,
435
- registrationIndex,
436
- candidateIndex,
437
- recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
438
- },
439
- ];
440
- },
441
- ),
442
- ).pipe(Effect.flatMap(Effect.fromResult));
443
-
444
- yield* failpoint.hit("subscription:change:after");
445
-
446
- return updated;
447
- });
410
+ candidateIndex.set(
411
+ oldKey,
412
+ (candidateIndex.get(oldKey) ?? []).filter((key) => key !== storageKey),
413
+ );
414
+ candidateIndex.set(
415
+ nextKey,
416
+ [...(candidateIndex.get(nextKey) ?? []), storageKey].sort(
417
+ (a, b) =>
418
+ (registrationIndex.get(a)?.ordinal ?? 0) -
419
+ (registrationIndex.get(b)?.ordinal ?? 0),
420
+ ),
421
+ );
422
+ }
448
423
 
449
- const cancel: SubscriptionStore["Service"]["cancel"] = Effect.fn(
450
- "MemorySubscriptionStore.cancel",
451
- )(function* (input, expectedRevision) {
452
- const key = yield* requireKey(input, "cancel-key");
453
-
454
- yield* failpoint.hit("subscription:cancel:before");
455
-
456
- const result = yield* Effect.uninterruptible(
457
- Ref.modify(
458
- state,
459
- (
460
- current,
461
- ): readonly [
462
- Result.Result<SubscriptionRecord, SubscriptionError>,
463
- MemorySubscriptionState,
464
- ] => {
465
- const storageKey = subscriptionKeyString(key);
466
- const text = current.registrations.get(storageKey);
467
-
468
- if (text === undefined) return [Result.fail(error("not-found", "subscription")), current];
469
- const decoded = decode(SubscriptionRecord, text, "cancel-record");
470
-
471
- if (Result.isFailure(decoded)) return [decoded, current];
472
- if (
473
- expectedRevision !== undefined &&
474
- expectedRevision !== decoded.success.configurationRevision &&
475
- !(
476
- decoded.success.state === "cancelled" &&
477
- expectedRevision + 1 === decoded.success.configurationRevision
478
- )
479
- )
480
424
  return [
481
- Result.fail(
482
- SubscriptionError.make({
483
- reason: "conflict",
484
- code: "configuration-revision",
485
- currentRevision: decoded.success.configurationRevision,
486
- currentState: decoded.success.state,
487
- }),
488
- ),
489
- current,
425
+ Result.succeed(revised),
426
+ {
427
+ ...current,
428
+ sequence: revised.ordinal,
429
+ registrations,
430
+ registrationIndex,
431
+ candidateIndex,
432
+ recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
433
+ },
490
434
  ];
491
- if (decoded.success.state === "cancelled")
492
- return [Result.succeed(decoded.success), current];
493
-
494
- const cancelled = {
495
- ...decoded.success,
496
- configurationRevision: decoded.success.configurationRevision + 1,
497
- state: "cancelled" as const,
498
- recovery: null,
499
- };
500
-
501
- const encoded = encode(SubscriptionRecord, cancelled, "cancel-encode");
502
-
503
- if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
504
- const registrations = new Map(current.registrations);
505
-
506
- registrations.set(storageKey, encoded.success);
507
- const registrationIndex = new Map(current.registrationIndex);
508
- const indexed = registrationIndex.get(storageKey);
509
-
510
- if (indexed === undefined)
511
- return [Result.fail(error("corrupt", "cancel-index")), current];
512
- registrationIndex.set(storageKey, {
513
- ...indexed,
514
- state: "cancelled",
515
- recoveryAt: null,
516
- recoveryKey: null,
517
- });
518
-
519
- return [
520
- Result.succeed(cancelled),
521
- {
522
- ...current,
523
- registrations,
524
- registrationIndex,
525
- recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
526
- },
527
- ];
528
- },
529
- ),
530
- ).pipe(Effect.flatMap(Effect.fromResult));
531
-
532
- yield* failpoint.hit("subscription:cancel:after");
533
-
534
- return result;
535
- });
435
+ },
436
+ ),
437
+ ).pipe(Effect.flatMap(Effect.fromResult));
536
438
 
537
- const accept: SubscriptionStore["Service"]["accept"] = Effect.fn(
538
- "MemorySubscriptionStore.accept",
539
- )(function* (input, inputLimits) {
540
- const event = yield* validate(AcceptedEvent, input, "accept-event");
541
- const currentTimeMillis = yield* Clock.currentTimeMillis;
542
- const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
543
-
544
- yield* requirePartition(event, "accept-partition");
545
- yield* failpoint.hit("subscription:accept:before");
546
-
547
- const result = yield* Effect.uninterruptible(
548
- Ref.modify(
549
- state,
550
- (
551
- current,
552
- ): readonly [Result.Result<AcceptedEvent, SubscriptionError>, MemorySubscriptionState] => {
553
- const existingText = current.events.get(event.eventId);
554
-
555
- if (existingText !== undefined) {
556
- const existing = decode(AcceptedEvent, existingText, "accept-existing");
557
-
558
- if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
559
-
560
- return sameEventIdentity(existing.success, event)
561
- ? [Result.succeed(existing.success), current]
562
- : [Result.fail(error("conflict", "event-identity")), current];
563
- }
564
- if (
565
- current.retentionHorizon !== null &&
566
- current.retentionHorizon !== limits.retention?.replayHorizonMillis
567
- )
568
- return [Result.fail(error("conflict", "retention-horizon")), current];
569
- const horizon = validateEventRetention(event, limits, currentTimeMillis);
570
-
571
- if (Result.isFailure(horizon)) return [Result.fail(horizon.failure), current];
572
- if (jsonBytes(event.payload) > limits.maxPayloadBytes)
573
- return [Result.fail(error("capacity", "payload-bytes")), current];
574
- if (current.events.size - current.tombstoneCount >= limits.maxEvents)
575
- return [Result.fail(error("capacity", "events")), current];
576
-
577
- const accepted: AcceptedEvent = {
578
- ...event,
579
- cutoff: current.sequence + 1,
580
- cursor: 0,
581
- routingComplete: false,
582
- routingFailure: null,
583
- };
584
-
585
- const encoded = encode(AcceptedEvent, accepted, "accept-encode");
586
-
587
- if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
588
- const events = new Map(current.events);
589
-
590
- events.set(accepted.eventId, encoded.success);
591
- const eventIndex = new Map(current.eventIndex);
592
-
593
- eventIndex.set(accepted.eventId, {
594
- routingComplete: false,
595
- nextAttemptAtMillis: accepted.nextAttemptAtMillis,
596
- });
597
-
598
- return [
599
- Result.succeed(accepted),
600
- {
601
- ...current,
602
- sequence: accepted.cutoff,
603
- events,
604
- eventIndex,
605
- eventKeys: insertKey(current.eventKeys, accepted.eventId),
606
- retentionHorizon: limits.retention?.replayHorizonMillis ?? current.retentionHorizon,
607
- retentionDeadline:
608
- limits.retention === undefined
609
- ? current.retentionDeadline
610
- : accepted.acceptedAtMillis,
611
- },
612
- ];
613
- },
614
- ),
615
- ).pipe(Effect.flatMap(Effect.fromResult));
616
-
617
- yield* failpoint.hit("subscription:accept:after");
618
-
619
- return result;
620
- });
439
+ yield* failpoint.hit("subscription:change:after");
440
+
441
+ return updated;
442
+ },
443
+ );
621
444
 
622
- const event: SubscriptionStore["Service"]["event"] = Effect.fn("MemorySubscriptionStore.event")(
623
- function* (eventId) {
624
- const text = (yield* Ref.get(state)).events.get(eventId);
445
+ const cancel: SubscriptionStore["Service"]["cancel"] = Effect.fnUntraced(
446
+ function* (input, expectedRevision) {
447
+ const key = yield* requireKey(input, "cancel-key");
448
+
449
+ yield* failpoint.hit("subscription:cancel:before");
450
+
451
+ const result = yield* Effect.uninterruptible(
452
+ Ref.modify(
453
+ state,
454
+ (
455
+ current,
456
+ ): readonly [
457
+ Result.Result<SubscriptionRecord, SubscriptionError>,
458
+ MemorySubscriptionState,
459
+ ] => {
460
+ const storageKey = subscriptionKeyString(key);
461
+ const text = current.registrations.get(storageKey);
462
+
463
+ if (text === undefined)
464
+ return [Result.fail(error("not-found", "subscription")), current];
465
+ const decoded = decode(SubscriptionRecord, text, "cancel-record");
625
466
 
626
- return text === undefined ? null : yield* decodeEffect(AcceptedEvent, text, "event-record");
467
+ if (Result.isFailure(decoded)) return [decoded, current];
468
+ if (
469
+ expectedRevision !== undefined &&
470
+ expectedRevision !== decoded.success.configurationRevision &&
471
+ !(
472
+ decoded.success.state === "cancelled" &&
473
+ expectedRevision + 1 === decoded.success.configurationRevision
474
+ )
475
+ )
476
+ return [
477
+ Result.fail(
478
+ SubscriptionError.make({
479
+ reason: "conflict",
480
+ code: "configuration-revision",
481
+ currentRevision: decoded.success.configurationRevision,
482
+ currentState: decoded.success.state,
483
+ }),
484
+ ),
485
+ current,
486
+ ];
487
+ if (decoded.success.state === "cancelled")
488
+ return [Result.succeed(decoded.success), current];
489
+
490
+ const cancelled = {
491
+ ...decoded.success,
492
+ configurationRevision: decoded.success.configurationRevision + 1,
493
+ state: "cancelled" as const,
494
+ recovery: null,
495
+ };
496
+
497
+ const encoded = encode(SubscriptionRecord, cancelled, "cancel-encode");
498
+
499
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
500
+ const registrations = new Map(current.registrations);
501
+
502
+ registrations.set(storageKey, encoded.success);
503
+ const registrationIndex = new Map(current.registrationIndex);
504
+ const indexed = registrationIndex.get(storageKey);
505
+
506
+ if (indexed === undefined)
507
+ return [Result.fail(error("corrupt", "cancel-index")), current];
508
+ registrationIndex.set(storageKey, {
509
+ ...indexed,
510
+ state: "cancelled",
511
+ recoveryAt: null,
512
+ recoveryKey: null,
513
+ });
514
+
515
+ return [
516
+ Result.succeed(cancelled),
517
+ {
518
+ ...current,
519
+ registrations,
520
+ registrationIndex,
521
+ recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
522
+ },
523
+ ];
524
+ },
525
+ ),
526
+ ).pipe(Effect.flatMap(Effect.fromResult));
527
+
528
+ yield* failpoint.hit("subscription:cancel:after");
529
+
530
+ return result;
627
531
  },
628
532
  );
629
533
 
630
- const pendingEvents: SubscriptionStore["Service"]["pendingEvents"] = Effect.fn(
631
- "MemorySubscriptionStore.pendingEvents",
632
- )(function* (nowMillis, after, limit) {
633
- const events: Array<string> = [];
534
+ const accept: SubscriptionStore["Service"]["accept"] = Effect.fnUntraced(
535
+ function* (input, inputLimits) {
536
+ const event = yield* validate(AcceptedEvent, input, "accept-event");
537
+ const currentTimeMillis = yield* Clock.currentTimeMillis;
538
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
539
+
540
+ yield* requirePartition(event, "accept-partition");
541
+ yield* failpoint.hit("subscription:accept:before");
542
+
543
+ const result = yield* Effect.uninterruptible(
544
+ Ref.modify(
545
+ state,
546
+ (
547
+ current,
548
+ ): readonly [
549
+ Result.Result<AcceptedEvent, SubscriptionError>,
550
+ MemorySubscriptionState,
551
+ ] => {
552
+ const existingText = current.events.get(event.eventId);
634
553
 
635
- for (const [eventId, indexed] of (yield* Ref.get(state)).eventIndex) {
636
- if (
637
- !indexed.routingComplete &&
638
- indexed.nextAttemptAtMillis <= nowMillis &&
639
- compareScheduleNames(eventId, after) > 0
640
- )
641
- events.push(eventId);
642
- }
643
- events.sort(compareScheduleNames);
554
+ if (existingText !== undefined) {
555
+ const existing = decode(AcceptedEvent, existingText, "accept-existing");
644
556
 
645
- return events.slice(0, limit);
646
- });
557
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
647
558
 
648
- const candidates: SubscriptionStore["Service"]["candidates"] = Effect.fn(
649
- "MemorySubscriptionStore.candidates",
650
- )(function* (input, limit) {
651
- const accepted = yield* validate(AcceptedEvent, input, "candidates-event");
559
+ return sameEventIdentity(existing.success, event)
560
+ ? [Result.succeed(existing.success), current]
561
+ : [Result.fail(error("conflict", "event-identity")), current];
562
+ }
563
+ if (
564
+ current.retentionHorizon !== null &&
565
+ current.retentionHorizon !== limits.retention?.replayHorizonMillis
566
+ )
567
+ return [Result.fail(error("conflict", "retention-horizon")), current];
568
+ const horizon = validateEventRetention(event, limits, currentTimeMillis);
652
569
 
653
- yield* requirePartition(accepted, "candidates-partition");
654
- const stored = yield* event(accepted.eventId);
570
+ if (Result.isFailure(horizon)) return [Result.fail(horizon.failure), current];
571
+ if (jsonBytes(event.payload) > limits.maxPayloadBytes)
572
+ return [Result.fail(error("capacity", "payload-bytes")), current];
573
+ if (current.events.size - current.tombstoneCount >= limits.maxEvents)
574
+ return [Result.fail(error("capacity", "events")), current];
655
575
 
656
- if (stored === null || !sameEventIdentity(stored, accepted))
657
- return yield* error(stored === null ? "not-found" : "conflict", "event");
658
- const current = yield* Ref.get(state);
659
- const records: Array<SubscriptionRecord> = [];
576
+ const accepted: AcceptedEvent = {
577
+ ...event,
578
+ cutoff: current.sequence + 1,
579
+ cursor: 0,
580
+ routingComplete: false,
581
+ routingFailure: null,
582
+ };
583
+
584
+ const encoded = encode(AcceptedEvent, accepted, "accept-encode");
585
+
586
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
587
+ const events = new Map(current.events);
660
588
 
661
- for (const storageKey of current.candidateIndex.get(eventCandidateIndexKey(stored)) ?? []) {
662
- const indexed = current.registrationIndex.get(storageKey);
589
+ events.set(accepted.eventId, encoded.success);
590
+ const eventIndex = new Map(current.eventIndex);
663
591
 
664
- if (indexed === undefined) return yield* error("corrupt", "candidate-index");
665
- if (indexed.ordinal <= stored.cursor || indexed.ordinal > stored.cutoff) continue;
666
- const text = current.registrations.get(storageKey);
592
+ eventIndex.set(accepted.eventId, {
593
+ routingComplete: false,
594
+ nextAttemptAtMillis: accepted.nextAttemptAtMillis,
595
+ });
667
596
 
668
- if (text === undefined) return yield* error("corrupt", "candidate-record");
669
- records.push(yield* decodeEffect(SubscriptionRecord, text, "candidate-record"));
670
- }
671
- records.sort((a, b) => a.ordinal - b.ordinal);
597
+ return [
598
+ Result.succeed(accepted),
599
+ {
600
+ ...current,
601
+ sequence: accepted.cutoff,
602
+ events,
603
+ eventIndex,
604
+ eventKeys: insertKey(current.eventKeys, accepted.eventId),
605
+ retentionHorizon: limits.retention?.replayHorizonMillis ?? current.retentionHorizon,
606
+ retentionDeadline:
607
+ limits.retention === undefined
608
+ ? current.retentionDeadline
609
+ : accepted.acceptedAtMillis,
610
+ },
611
+ ];
612
+ },
613
+ ),
614
+ ).pipe(Effect.flatMap(Effect.fromResult));
672
615
 
673
- return records.slice(0, limit);
616
+ yield* failpoint.hit("subscription:accept:after");
617
+
618
+ return result;
619
+ },
620
+ );
621
+
622
+ const event: SubscriptionStore["Service"]["event"] = Effect.fnUntraced(function* (eventId) {
623
+ const text = (yield* Ref.get(state)).events.get(eventId);
624
+
625
+ return text === undefined ? null : yield* decodeEffect(AcceptedEvent, text, "event-record");
674
626
  });
675
627
 
676
- const select: SubscriptionStore["Service"]["select"] = Effect.fn(
677
- "MemorySubscriptionStore.select",
678
- )(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
679
- const suppliedEvent = yield* validate(AcceptedEvent, inputEvent, "select-event");
628
+ const pendingEvents: SubscriptionStore["Service"]["pendingEvents"] = Effect.fnUntraced(
629
+ function* (nowMillis, after, limit) {
630
+ const events: Array<string> = [];
631
+
632
+ for (const [eventId, indexed] of (yield* Ref.get(state)).eventIndex) {
633
+ if (
634
+ !indexed.routingComplete &&
635
+ indexed.nextAttemptAtMillis <= nowMillis &&
636
+ compareScheduleNames(eventId, after) > 0
637
+ )
638
+ events.push(eventId);
639
+ }
640
+ events.sort(compareScheduleNames);
680
641
 
681
- const deliveries = yield* validate(
682
- Schema.Array(SubscriptionDelivery),
683
- inputDeliveries,
684
- "select-deliveries",
685
- );
642
+ return events.slice(0, limit);
643
+ },
644
+ );
686
645
 
687
- const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
646
+ const candidates: SubscriptionStore["Service"]["candidates"] = Effect.fnUntraced(
647
+ function* (input, limit) {
648
+ const accepted = yield* validate(AcceptedEvent, input, "candidates-event");
688
649
 
689
- yield* requirePartition(suppliedEvent, "select-partition");
690
- yield* failpoint.hit("subscription:select:before");
691
- const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
650
+ yield* requirePartition(accepted, "candidates-partition");
651
+ const stored = yield* event(accepted.eventId);
692
652
 
693
- yield* Effect.uninterruptible(
694
- Ref.modify(
695
- state,
696
- (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
697
- const eventText = current.events.get(suppliedEvent.eventId);
653
+ if (stored === null || !sameEventIdentity(stored, accepted))
654
+ return yield* error(stored === null ? "not-found" : "conflict", "event");
655
+ const current = yield* Ref.get(state);
656
+ const records: Array<SubscriptionRecord> = [];
698
657
 
699
- if (eventText === undefined) return [Result.fail(error("not-found", "event")), current];
700
- const decodedEvent = decode(AcceptedEvent, eventText, "select-event-record");
658
+ for (const storageKey of current.candidateIndex.get(eventCandidateIndexKey(stored)) ?? []) {
659
+ const indexed = current.registrationIndex.get(storageKey);
701
660
 
702
- if (Result.isFailure(decodedEvent)) return [Result.fail(decodedEvent.failure), current];
703
- const accepted = decodedEvent.success;
661
+ if (indexed === undefined) return yield* error("corrupt", "candidate-index");
662
+ if (indexed.ordinal <= stored.cursor || indexed.ordinal > stored.cutoff) continue;
663
+ const text = current.registrations.get(storageKey);
704
664
 
705
- if (
706
- !sameEventIdentity(accepted, suppliedEvent) ||
707
- suppliedEvent.cursor !== accepted.cursor
708
- )
709
- return [Result.fail(error("conflict", "event-cursor")), current];
710
- if (accepted.routingComplete)
711
- return [
712
- complete && cursor === accepted.cursor
713
- ? Result.void
714
- : Result.fail(error("conflict", "routing-complete")),
715
- current,
716
- ];
717
- if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff)
718
- return [Result.fail(error("validation", "cursor")), current];
665
+ if (text === undefined) return yield* error("corrupt", "candidate-record");
666
+ records.push(yield* decodeEffect(SubscriptionRecord, text, "candidate-record"));
667
+ }
668
+ records.sort((a, b) => a.ordinal - b.ordinal);
719
669
 
720
- const additions: Array<readonly [string, string]> = [];
721
- const updates: Array<readonly [string, string]> = [];
722
- const additionIndex: Array<readonly [string, DeliveryIndex, string]> = [];
723
- const registrationUpdates: Array<readonly [string, RegistrationIndex]> = [];
724
- const owners = new Map(current.ownerDeliveryCounts);
670
+ return records.slice(0, limit);
671
+ },
672
+ );
725
673
 
726
- for (const delivery of deliveries) {
727
- const recordText = current.registrations.get(
728
- subscriptionKeyString(delivery.key.subscription),
729
- );
674
+ const select: SubscriptionStore["Service"]["select"] = Effect.fnUntraced(
675
+ function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
676
+ const suppliedEvent = yield* validate(AcceptedEvent, inputEvent, "select-event");
730
677
 
731
- if (recordText === undefined)
732
- return [Result.fail(error("not-found", "subscription")), current];
733
- const record = decode(SubscriptionRecord, recordText, "select-registration");
678
+ const deliveries = yield* validate(
679
+ Schema.Array(SubscriptionDelivery),
680
+ inputDeliveries,
681
+ "select-deliveries",
682
+ );
683
+
684
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
685
+
686
+ yield* requirePartition(suppliedEvent, "select-partition");
687
+ yield* failpoint.hit("subscription:select:before");
688
+ const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
689
+
690
+ yield* Effect.uninterruptible(
691
+ Ref.modify(
692
+ state,
693
+ (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
694
+ const eventText = current.events.get(suppliedEvent.eventId);
695
+
696
+ if (eventText === undefined) return [Result.fail(error("not-found", "event")), current];
697
+ const decodedEvent = decode(AcceptedEvent, eventText, "select-event-record");
698
+
699
+ if (Result.isFailure(decodedEvent)) return [Result.fail(decodedEvent.failure), current];
700
+ const accepted = decodedEvent.success;
734
701
 
735
- if (Result.isFailure(record)) return [Result.fail(record.failure), current];
736
702
  if (
737
- !deliveryBelongsTo(delivery, record.success, accepted) ||
738
- !subscriptionDeliveryCanSelect(delivery, record.success, accepted) ||
739
- record.success.ordinal <= accepted.cursor ||
740
- record.success.ordinal > cursor
703
+ !sameEventIdentity(accepted, suppliedEvent) ||
704
+ suppliedEvent.cursor !== accepted.cursor
741
705
  )
742
- return [Result.fail(error("conflict", "selection")), current];
743
- if (!subscriptionCanSelect(record.success, accepted, effectiveNowMillis, false))
744
- continue;
745
- const deliveryKey = subscriptionDeliveryKeyString(delivery.key);
746
- const existingText = current.deliveries.get(deliveryKey);
706
+ return [Result.fail(error("conflict", "event-cursor")), current];
707
+ if (accepted.routingComplete)
708
+ return [
709
+ complete && cursor === accepted.cursor
710
+ ? Result.void
711
+ : Result.fail(error("conflict", "routing-complete")),
712
+ current,
713
+ ];
714
+ if (
715
+ !Number.isSafeInteger(cursor) ||
716
+ cursor < accepted.cursor ||
717
+ cursor > accepted.cutoff
718
+ )
719
+ return [Result.fail(error("validation", "cursor")), current];
747
720
 
748
- if (existingText !== undefined) {
749
- const existing = decode(SubscriptionDelivery, existingText, "select-existing");
721
+ const additions: Array<readonly [string, string]> = [];
722
+ const updates: Array<readonly [string, string]> = [];
723
+ const additionIndex: Array<readonly [string, DeliveryIndex, string]> = [];
724
+ const registrationUpdates: Array<readonly [string, RegistrationIndex]> = [];
725
+ const owners = new Map(current.ownerDeliveryCounts);
750
726
 
751
- if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
752
- if (!sameDeliveryIdentity(existing.success, delivery))
753
- return [Result.fail(error("conflict", "delivery-identity")), current];
754
- continue;
727
+ for (const delivery of deliveries) {
728
+ const recordText = current.registrations.get(
729
+ subscriptionKeyString(delivery.key.subscription),
730
+ );
731
+
732
+ if (recordText === undefined)
733
+ return [Result.fail(error("not-found", "subscription")), current];
734
+ const record = decode(SubscriptionRecord, recordText, "select-registration");
735
+
736
+ if (Result.isFailure(record)) return [Result.fail(record.failure), current];
737
+ if (
738
+ !deliveryBelongsTo(delivery, record.success, accepted) ||
739
+ !subscriptionDeliveryCanSelect(delivery, record.success, accepted) ||
740
+ record.success.ordinal <= accepted.cursor ||
741
+ record.success.ordinal > cursor
742
+ )
743
+ return [Result.fail(error("conflict", "selection")), current];
744
+ if (!subscriptionCanSelect(record.success, accepted, effectiveNowMillis, false))
745
+ continue;
746
+ const deliveryKey = subscriptionDeliveryKeyString(delivery.key);
747
+ const existingText = current.deliveries.get(deliveryKey);
748
+
749
+ if (existingText !== undefined) {
750
+ const existing = decode(SubscriptionDelivery, existingText, "select-existing");
751
+
752
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
753
+ if (!sameDeliveryIdentity(existing.success, delivery))
754
+ return [Result.fail(error("conflict", "delivery-identity")), current];
755
+ continue;
756
+ }
757
+
758
+ const encodedDelivery = encode(
759
+ SubscriptionDelivery,
760
+ delivery,
761
+ "select-delivery-encode",
762
+ );
763
+
764
+ if (Result.isFailure(encodedDelivery))
765
+ return [Result.fail(encodedDelivery.failure), current];
766
+ additions.push([deliveryKey, encodedDelivery.success]);
767
+ additionIndex.push([
768
+ deliveryKey,
769
+ {
770
+ key: delivery.key,
771
+ state: delivery.state,
772
+ nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis,
773
+ },
774
+ record.success.key.ownerId,
775
+ ]);
776
+ const ownerId = record.success.key.ownerId;
777
+
778
+ owners.set(ownerId, (owners.get(ownerId) ?? 0) + 1);
779
+ if ((owners.get(ownerId) ?? 0) > limits.maxDeliveriesPerOwner)
780
+ return [Result.fail(error("capacity", "owner-deliveries")), current];
781
+ if (record.success.configuration.mode === "once") {
782
+ const consumed = { ...record.success, state: "consumed" as const, recovery: null };
783
+
784
+ const encodedRecord = encode(
785
+ SubscriptionRecord,
786
+ consumed,
787
+ "select-registration-encode",
788
+ );
789
+
790
+ if (Result.isFailure(encodedRecord))
791
+ return [Result.fail(encodedRecord.failure), current];
792
+ const consumedKey = subscriptionKeyString(consumed.key);
793
+
794
+ updates.push([consumedKey, encodedRecord.success]);
795
+ const indexed = current.registrationIndex.get(consumedKey);
796
+
797
+ if (indexed === undefined)
798
+ return [Result.fail(error("corrupt", "selection-index")), current];
799
+ registrationUpdates.push([
800
+ consumedKey,
801
+ { ...indexed, state: "consumed", recoveryAt: null, recoveryKey: null },
802
+ ]);
803
+ }
755
804
  }
805
+ if (current.deliveries.size + additions.length > limits.maxDeliveries)
806
+ return [Result.fail(error("capacity", "deliveries")), current];
807
+ const registrations = new Map(current.registrations);
756
808
 
757
- const encodedDelivery = encode(
758
- SubscriptionDelivery,
759
- delivery,
760
- "select-delivery-encode",
809
+ for (const [key, value] of updates) registrations.set(key, value);
810
+ const nextDeliveries = new Map(current.deliveries);
811
+
812
+ for (const [key, value] of additions) nextDeliveries.set(key, value);
813
+ const deliveryIndex = new Map(current.deliveryIndex);
814
+
815
+ for (const [key, value] of additionIndex) deliveryIndex.set(key, value);
816
+ const registrationIndex = new Map(current.registrationIndex);
817
+
818
+ for (const [key, value] of registrationUpdates) registrationIndex.set(key, value);
819
+
820
+ const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
821
+
822
+ eventDeliveryCounts.set(
823
+ accepted.eventId,
824
+ (eventDeliveryCounts.get(accepted.eventId) ?? 0) + additions.length,
761
825
  );
762
826
 
763
- if (Result.isFailure(encodedDelivery))
764
- return [Result.fail(encodedDelivery.failure), current];
765
- additions.push([deliveryKey, encodedDelivery.success]);
766
- additionIndex.push([
767
- deliveryKey,
768
- {
769
- key: delivery.key,
770
- state: delivery.state,
771
- nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis,
772
- },
773
- record.success.key.ownerId,
774
- ]);
775
- const ownerId = record.success.key.ownerId;
827
+ const nextEvent: AcceptedEvent = {
828
+ ...accepted,
829
+ cursor,
830
+ routingComplete: complete,
831
+ routingFailure: null,
832
+ };
776
833
 
777
- owners.set(ownerId, (owners.get(ownerId) ?? 0) + 1);
778
- if ((owners.get(ownerId) ?? 0) > limits.maxDeliveriesPerOwner)
779
- return [Result.fail(error("capacity", "owner-deliveries")), current];
780
- if (record.success.configuration.mode === "once") {
781
- const consumed = { ...record.success, state: "consumed" as const, recovery: null };
834
+ const encodedEvent = encode(AcceptedEvent, nextEvent, "select-event-encode");
782
835
 
783
- const encodedRecord = encode(
784
- SubscriptionRecord,
785
- consumed,
786
- "select-registration-encode",
787
- );
836
+ if (Result.isFailure(encodedEvent)) return [Result.fail(encodedEvent.failure), current];
837
+ const events = new Map(current.events);
788
838
 
789
- if (Result.isFailure(encodedRecord))
790
- return [Result.fail(encodedRecord.failure), current];
791
- const consumedKey = subscriptionKeyString(consumed.key);
839
+ events.set(nextEvent.eventId, encodedEvent.success);
840
+ const eventIndex = new Map(current.eventIndex);
792
841
 
793
- updates.push([consumedKey, encodedRecord.success]);
794
- const indexed = current.registrationIndex.get(consumedKey);
842
+ eventIndex.set(nextEvent.eventId, {
843
+ routingComplete: nextEvent.routingComplete,
844
+ nextAttemptAtMillis: nextEvent.nextAttemptAtMillis,
845
+ });
795
846
 
796
- if (indexed === undefined)
797
- return [Result.fail(error("corrupt", "selection-index")), current];
798
- registrationUpdates.push([
799
- consumedKey,
800
- { ...indexed, state: "consumed", recoveryAt: null, recoveryKey: null },
801
- ]);
802
- }
803
- }
804
- if (current.deliveries.size + additions.length > limits.maxDeliveries)
805
- return [Result.fail(error("capacity", "deliveries")), current];
806
- const registrations = new Map(current.registrations);
847
+ return [
848
+ Result.void,
849
+ {
850
+ ...current,
851
+ registrations,
852
+ registrationIndex,
853
+ recoveryCounts: recoveryCountsAfter(
854
+ current,
855
+ registrationIndex,
856
+ registrationUpdates.map(([key]) => key),
857
+ ),
858
+ eventDeliveryCounts,
859
+ events,
860
+ eventIndex,
861
+ deliveries: nextDeliveries,
862
+ deliveryKeys: additions.reduce(
863
+ (keys, [key]) => insertKey(keys, key),
864
+ current.deliveryKeys,
865
+ ),
866
+ deliveryIndex,
867
+ ownerDeliveryCounts: owners,
868
+ },
869
+ ];
870
+ },
871
+ ),
872
+ ).pipe(Effect.flatMap(Effect.fromResult));
873
+ yield* failpoint.hit("subscription:select:after");
874
+ },
875
+ );
807
876
 
808
- for (const [key, value] of updates) registrations.set(key, value);
809
- const nextDeliveries = new Map(current.deliveries);
877
+ const catchUp: SubscriptionStore["Service"]["catchUp"] = Effect.fnUntraced(
878
+ function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
879
+ const suppliedEvent = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
880
+ const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
881
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
810
882
 
811
- for (const [key, value] of additions) nextDeliveries.set(key, value);
812
- const deliveryIndex = new Map(current.deliveryIndex);
883
+ yield* requirePartition(suppliedEvent, "catch-up-partition");
884
+ yield* failpoint.hit("subscription:catch-up:before");
885
+ const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
813
886
 
814
- for (const [key, value] of additionIndex) deliveryIndex.set(key, value);
815
- const registrationIndex = new Map(current.registrationIndex);
887
+ yield* Effect.uninterruptible(
888
+ Ref.modify(
889
+ state,
890
+ (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
891
+ const eventText = current.events.get(suppliedEvent.eventId);
816
892
 
817
- for (const [key, value] of registrationUpdates) registrationIndex.set(key, value);
893
+ const recordText = current.registrations.get(
894
+ subscriptionKeyString(delivery.key.subscription),
895
+ );
818
896
 
819
- const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
897
+ if (eventText === undefined || recordText === undefined)
898
+ return [
899
+ Result.fail(error("not-found", eventText === undefined ? "event" : "subscription")),
900
+ current,
901
+ ];
902
+ const accepted = decode(AcceptedEvent, eventText, "catch-up-event-record");
903
+
904
+ if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
905
+ const record = decode(SubscriptionRecord, recordText, "catch-up-registration");
820
906
 
821
- eventDeliveryCounts.set(
822
- accepted.eventId,
823
- (eventDeliveryCounts.get(accepted.eventId) ?? 0) + additions.length,
824
- );
907
+ if (Result.isFailure(record)) return [Result.fail(record.failure), current];
908
+ if (
909
+ !sameEventIdentity(accepted.success, suppliedEvent) ||
910
+ !deliveryBelongsTo(delivery, record.success, accepted.success) ||
911
+ !subscriptionDeliveryCanSelect(delivery, record.success, accepted.success) ||
912
+ record.success.configuration.mode !== "once"
913
+ )
914
+ return [Result.fail(error("conflict", "catch-up-identity")), current];
915
+ const key = subscriptionDeliveryKeyString(delivery.key);
916
+ const existingText = current.deliveries.get(key);
825
917
 
826
- const nextEvent: AcceptedEvent = {
827
- ...accepted,
828
- cursor,
829
- routingComplete: complete,
830
- routingFailure: null,
831
- };
918
+ if (existingText !== undefined) {
919
+ const existing = decode(SubscriptionDelivery, existingText, "catch-up-existing");
832
920
 
833
- const encodedEvent = encode(AcceptedEvent, nextEvent, "select-event-encode");
921
+ if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
834
922
 
835
- if (Result.isFailure(encodedEvent)) return [Result.fail(encodedEvent.failure), current];
836
- const events = new Map(current.events);
923
+ return sameDeliveryIdentity(existing.success, delivery)
924
+ ? [Result.void, current]
925
+ : [Result.fail(error("conflict", "delivery-identity")), current];
926
+ }
927
+ if (!subscriptionCanSelect(record.success, accepted.success, effectiveNowMillis, true))
928
+ return [Result.fail(error("conflict", "catch-up-eligibility")), current];
929
+ if (current.deliveries.size >= limits.maxDeliveries)
930
+ return [Result.fail(error("capacity", "deliveries")), current];
931
+ const ownerCount = current.ownerDeliveryCounts.get(record.success.key.ownerId) ?? 0;
837
932
 
838
- events.set(nextEvent.eventId, encodedEvent.success);
839
- const eventIndex = new Map(current.eventIndex);
933
+ if (ownerCount >= limits.maxDeliveriesPerOwner)
934
+ return [Result.fail(error("capacity", "owner-deliveries")), current];
840
935
 
841
- eventIndex.set(nextEvent.eventId, {
842
- routingComplete: nextEvent.routingComplete,
843
- nextAttemptAtMillis: nextEvent.nextAttemptAtMillis,
844
- });
936
+ const encodedDelivery = encode(
937
+ SubscriptionDelivery,
938
+ delivery,
939
+ "catch-up-delivery-encode",
940
+ );
845
941
 
846
- return [
847
- Result.void,
848
- {
849
- ...current,
850
- registrations,
851
- registrationIndex,
852
- recoveryCounts: recoveryCountsAfter(
853
- current,
854
- registrationIndex,
855
- registrationUpdates.map(([key]) => key),
856
- ),
857
- eventDeliveryCounts,
858
- events,
859
- eventIndex,
860
- deliveries: nextDeliveries,
861
- deliveryKeys: additions.reduce(
862
- (keys, [key]) => insertKey(keys, key),
863
- current.deliveryKeys,
864
- ),
865
- deliveryIndex,
866
- ownerDeliveryCounts: owners,
867
- },
868
- ];
869
- },
870
- ),
871
- ).pipe(Effect.flatMap(Effect.fromResult));
872
- yield* failpoint.hit("subscription:select:after");
873
- });
942
+ if (Result.isFailure(encodedDelivery))
943
+ return [Result.fail(encodedDelivery.failure), current];
944
+ const consumed = { ...record.success, state: "consumed" as const, recovery: null };
874
945
 
875
- const catchUp: SubscriptionStore["Service"]["catchUp"] = Effect.fn(
876
- "MemorySubscriptionStore.catchUp",
877
- )(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
878
- const suppliedEvent = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
879
- const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
880
- const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
946
+ const encodedRecord = encode(
947
+ SubscriptionRecord,
948
+ consumed,
949
+ "catch-up-registration-encode",
950
+ );
881
951
 
882
- yield* requirePartition(suppliedEvent, "catch-up-partition");
883
- yield* failpoint.hit("subscription:catch-up:before");
884
- const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
952
+ if (Result.isFailure(encodedRecord))
953
+ return [Result.fail(encodedRecord.failure), current];
954
+ const deliveries = new Map(current.deliveries);
955
+
956
+ deliveries.set(key, encodedDelivery.success);
957
+ const registrations = new Map(current.registrations);
958
+ const consumedKey = subscriptionKeyString(consumed.key);
959
+
960
+ registrations.set(consumedKey, encodedRecord.success);
961
+ const registrationIndex = new Map(current.registrationIndex);
962
+ const indexed = registrationIndex.get(consumedKey);
963
+
964
+ if (indexed === undefined)
965
+ return [Result.fail(error("corrupt", "catch-up-index")), current];
966
+ registrationIndex.set(consumedKey, {
967
+ ...indexed,
968
+ state: "consumed",
969
+ recoveryAt: null,
970
+ recoveryKey: null,
971
+ });
972
+ const deliveryIndex = new Map(current.deliveryIndex);
973
+
974
+ deliveryIndex.set(key, {
975
+ key: delivery.key,
976
+ state: delivery.state,
977
+ nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis,
978
+ });
979
+ const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
980
+ const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
885
981
 
886
- yield* Effect.uninterruptible(
887
- Ref.modify(
888
- state,
889
- (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
890
- const eventText = current.events.get(suppliedEvent.eventId);
982
+ eventDeliveryCounts.set(
983
+ accepted.success.eventId,
984
+ (eventDeliveryCounts.get(accepted.success.eventId) ?? 0) + 1,
985
+ );
891
986
 
892
- const recordText = current.registrations.get(
893
- subscriptionKeyString(delivery.key.subscription),
894
- );
987
+ ownerDeliveryCounts.set(consumed.key.ownerId, ownerCount + 1);
895
988
 
896
- if (eventText === undefined || recordText === undefined)
897
989
  return [
898
- Result.fail(error("not-found", eventText === undefined ? "event" : "subscription")),
899
- current,
990
+ Result.void,
991
+ {
992
+ ...current,
993
+ deliveries,
994
+ deliveryIndex,
995
+ deliveryKeys: insertKey(current.deliveryKeys, key),
996
+ ownerDeliveryCounts,
997
+ registrations,
998
+ registrationIndex,
999
+ recoveryCounts: recoveryCountsAfter(current, registrationIndex, [consumedKey]),
1000
+ eventDeliveryCounts,
1001
+ },
900
1002
  ];
901
- const accepted = decode(AcceptedEvent, eventText, "catch-up-event-record");
902
-
903
- if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
904
- const record = decode(SubscriptionRecord, recordText, "catch-up-registration");
905
-
906
- if (Result.isFailure(record)) return [Result.fail(record.failure), current];
907
- if (
908
- !sameEventIdentity(accepted.success, suppliedEvent) ||
909
- !deliveryBelongsTo(delivery, record.success, accepted.success) ||
910
- !subscriptionDeliveryCanSelect(delivery, record.success, accepted.success) ||
911
- record.success.configuration.mode !== "once"
912
- )
913
- return [Result.fail(error("conflict", "catch-up-identity")), current];
914
- const key = subscriptionDeliveryKeyString(delivery.key);
915
- const existingText = current.deliveries.get(key);
916
-
917
- if (existingText !== undefined) {
918
- const existing = decode(SubscriptionDelivery, existingText, "catch-up-existing");
919
-
920
- if (Result.isFailure(existing)) return [Result.fail(existing.failure), current];
921
-
922
- return sameDeliveryIdentity(existing.success, delivery)
923
- ? [Result.void, current]
924
- : [Result.fail(error("conflict", "delivery-identity")), current];
925
- }
926
- if (!subscriptionCanSelect(record.success, accepted.success, effectiveNowMillis, true))
927
- return [Result.fail(error("conflict", "catch-up-eligibility")), current];
928
- if (current.deliveries.size >= limits.maxDeliveries)
929
- return [Result.fail(error("capacity", "deliveries")), current];
930
- const ownerCount = current.ownerDeliveryCounts.get(record.success.key.ownerId) ?? 0;
931
-
932
- if (ownerCount >= limits.maxDeliveriesPerOwner)
933
- return [Result.fail(error("capacity", "owner-deliveries")), current];
934
-
935
- const encodedDelivery = encode(
936
- SubscriptionDelivery,
937
- delivery,
938
- "catch-up-delivery-encode",
939
- );
940
-
941
- if (Result.isFailure(encodedDelivery))
942
- return [Result.fail(encodedDelivery.failure), current];
943
- const consumed = { ...record.success, state: "consumed" as const, recovery: null };
944
-
945
- const encodedRecord = encode(
946
- SubscriptionRecord,
947
- consumed,
948
- "catch-up-registration-encode",
949
- );
950
-
951
- if (Result.isFailure(encodedRecord)) return [Result.fail(encodedRecord.failure), current];
952
- const deliveries = new Map(current.deliveries);
953
-
954
- deliveries.set(key, encodedDelivery.success);
955
- const registrations = new Map(current.registrations);
956
- const consumedKey = subscriptionKeyString(consumed.key);
957
-
958
- registrations.set(consumedKey, encodedRecord.success);
959
- const registrationIndex = new Map(current.registrationIndex);
960
- const indexed = registrationIndex.get(consumedKey);
961
-
962
- if (indexed === undefined)
963
- return [Result.fail(error("corrupt", "catch-up-index")), current];
964
- registrationIndex.set(consumedKey, {
965
- ...indexed,
966
- state: "consumed",
967
- recoveryAt: null,
968
- recoveryKey: null,
969
- });
970
- const deliveryIndex = new Map(current.deliveryIndex);
971
-
972
- deliveryIndex.set(key, {
973
- key: delivery.key,
974
- state: delivery.state,
975
- nextAttemptAtMillis: delivery.retry.nextAttemptAtMillis,
976
- });
977
- const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
978
- const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
979
-
980
- eventDeliveryCounts.set(
981
- accepted.success.eventId,
982
- (eventDeliveryCounts.get(accepted.success.eventId) ?? 0) + 1,
983
- );
984
-
985
- ownerDeliveryCounts.set(consumed.key.ownerId, ownerCount + 1);
986
-
987
- return [
988
- Result.void,
989
- {
990
- ...current,
991
- deliveries,
992
- deliveryIndex,
993
- deliveryKeys: insertKey(current.deliveryKeys, key),
994
- ownerDeliveryCounts,
995
- registrations,
996
- registrationIndex,
997
- recoveryCounts: recoveryCountsAfter(current, registrationIndex, [consumedKey]),
998
- eventDeliveryCounts,
999
- },
1000
- ];
1001
- },
1002
- ),
1003
- ).pipe(Effect.flatMap(Effect.fromResult));
1004
- yield* failpoint.hit("subscription:catch-up:after");
1005
- });
1003
+ },
1004
+ ),
1005
+ ).pipe(Effect.flatMap(Effect.fromResult));
1006
+ yield* failpoint.hit("subscription:catch-up:after");
1007
+ },
1008
+ );
1006
1009
 
1007
- const deferEvent: SubscriptionStore["Service"]["deferEvent"] = Effect.fn(
1008
- "MemorySubscriptionStore.deferEvent",
1009
- )(function* (eventId, nextAttemptAtMillis, code) {
1010
- const routingFailure =
1011
- code === undefined
1012
- ? "routing-failed"
1013
- : yield* validate(SubscriptionName, code, "routing-failure");
1014
-
1015
- yield* failpoint.hit("subscription:defer-event:before");
1016
- yield* Effect.uninterruptible(
1017
- Ref.modify(
1018
- state,
1019
- (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
1020
- const text = current.events.get(eventId);
1021
-
1022
- if (text === undefined) return [Result.fail(error("not-found", "event")), current];
1023
- const accepted = decode(AcceptedEvent, text, "defer-event-record");
1024
-
1025
- if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
1026
- const updated = { ...accepted.success, nextAttemptAtMillis, routingFailure };
1027
- const encoded = encode(AcceptedEvent, updated, "defer-event-encode");
1028
-
1029
- if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
1030
- const events = new Map(current.events);
1031
-
1032
- events.set(eventId, encoded.success);
1033
- const eventIndex = new Map(current.eventIndex);
1034
- const indexed = eventIndex.get(eventId);
1035
-
1036
- if (indexed === undefined)
1037
- return [Result.fail(error("corrupt", "defer-event-index")), current];
1038
- eventIndex.set(eventId, { ...indexed, nextAttemptAtMillis });
1039
-
1040
- return [Result.void, { ...current, events, eventIndex }];
1041
- },
1042
- ),
1043
- ).pipe(Effect.flatMap(Effect.fromResult));
1044
- yield* failpoint.hit("subscription:defer-event:after");
1045
- });
1010
+ const deferEvent: SubscriptionStore["Service"]["deferEvent"] = Effect.fnUntraced(
1011
+ function* (eventId, nextAttemptAtMillis, code) {
1012
+ const routingFailure =
1013
+ code === undefined
1014
+ ? "routing-failed"
1015
+ : yield* validate(SubscriptionName, code, "routing-failure");
1016
+
1017
+ yield* failpoint.hit("subscription:defer-event:before");
1018
+ yield* Effect.uninterruptible(
1019
+ Ref.modify(
1020
+ state,
1021
+ (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
1022
+ const text = current.events.get(eventId);
1023
+
1024
+ if (text === undefined) return [Result.fail(error("not-found", "event")), current];
1025
+ const accepted = decode(AcceptedEvent, text, "defer-event-record");
1026
+
1027
+ if (Result.isFailure(accepted)) return [Result.fail(accepted.failure), current];
1028
+ const updated = { ...accepted.success, nextAttemptAtMillis, routingFailure };
1029
+ const encoded = encode(AcceptedEvent, updated, "defer-event-encode");
1030
+
1031
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
1032
+ const events = new Map(current.events);
1033
+
1034
+ events.set(eventId, encoded.success);
1035
+ const eventIndex = new Map(current.eventIndex);
1036
+ const indexed = eventIndex.get(eventId);
1037
+
1038
+ if (indexed === undefined)
1039
+ return [Result.fail(error("corrupt", "defer-event-index")), current];
1040
+ eventIndex.set(eventId, { ...indexed, nextAttemptAtMillis });
1041
+
1042
+ return [Result.void, { ...current, events, eventIndex }];
1043
+ },
1044
+ ),
1045
+ ).pipe(Effect.flatMap(Effect.fromResult));
1046
+ yield* failpoint.hit("subscription:defer-event:after");
1047
+ },
1048
+ );
1046
1049
 
1047
- const delivery: SubscriptionStore["Service"]["delivery"] = Effect.fn(
1048
- "MemorySubscriptionStore.delivery",
1049
- )(function* (input) {
1050
+ const delivery: SubscriptionStore["Service"]["delivery"] = Effect.fnUntraced(function* (input) {
1050
1051
  const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
1051
1052
 
1052
1053
  yield* requirePartition(key.subscription, "delivery-partition");
@@ -1057,235 +1058,236 @@ const makeMemorySubscriptionStore = Effect.fn("makeMemorySubscriptionStore")(fun
1057
1058
  : yield* decodeEffect(SubscriptionDelivery, text, "delivery-record");
1058
1059
  });
1059
1060
 
1060
- const pendingDeliveries: SubscriptionStore["Service"]["pendingDeliveries"] = Effect.fn(
1061
- "MemorySubscriptionStore.pendingDeliveries",
1062
- )(function* (nowMillis, after, limit) {
1063
- const items: Array<SubscriptionDeliveryKey> = [];
1061
+ const pendingDeliveries: SubscriptionStore["Service"]["pendingDeliveries"] = Effect.fnUntraced(
1062
+ function* (nowMillis, after, limit) {
1063
+ const items: Array<SubscriptionDeliveryKey> = [];
1064
+
1065
+ for (const [storageKey, item] of (yield* Ref.get(state)).deliveryIndex) {
1066
+ if (
1067
+ ((item.state !== "delivered" && item.state !== "refused" && item.parked !== true) ||
1068
+ (item.state === "delivered" && item.observeSettlement === true)) &&
1069
+ item.nextAttemptAtMillis <= nowMillis &&
1070
+ compareScheduleNames(storageKey, after) > 0
1071
+ )
1072
+ items.push(item.key);
1073
+ }
1074
+ items.sort((a, b) =>
1075
+ compareScheduleNames(subscriptionDeliveryKeyString(a), subscriptionDeliveryKeyString(b)),
1076
+ );
1064
1077
 
1065
- for (const [storageKey, item] of (yield* Ref.get(state)).deliveryIndex) {
1066
- if (
1067
- ((item.state !== "delivered" && item.state !== "refused" && item.parked !== true) ||
1068
- (item.state === "delivered" && item.observeSettlement === true)) &&
1069
- item.nextAttemptAtMillis <= nowMillis &&
1070
- compareScheduleNames(storageKey, after) > 0
1071
- )
1072
- items.push(item.key);
1073
- }
1074
- items.sort((a, b) =>
1075
- compareScheduleNames(subscriptionDeliveryKeyString(a), subscriptionDeliveryKeyString(b)),
1076
- );
1078
+ return items.slice(0, limit);
1079
+ },
1080
+ );
1077
1081
 
1078
- return items.slice(0, limit);
1079
- });
1082
+ const listDeliveries: SubscriptionStore["Service"]["listDeliveries"] = Effect.fnUntraced(
1083
+ function* (input, after, limit) {
1084
+ const key = yield* requireKey(input, "list-deliveries-key");
1085
+ const items: Array<SubscriptionDelivery> = [];
1080
1086
 
1081
- const listDeliveries: SubscriptionStore["Service"]["listDeliveries"] = Effect.fn(
1082
- "MemorySubscriptionStore.listDeliveries",
1083
- )(function* (input, after, limit) {
1084
- const key = yield* requireKey(input, "list-deliveries-key");
1085
- const items: Array<SubscriptionDelivery> = [];
1087
+ for (const text of (yield* Ref.get(state)).deliveries.values()) {
1088
+ const item = yield* decodeEffect(SubscriptionDelivery, text, "list-delivery");
1089
+ const itemKey = subscriptionDeliveryKeyString(item.key);
1086
1090
 
1087
- for (const text of (yield* Ref.get(state)).deliveries.values()) {
1088
- const item = yield* decodeEffect(SubscriptionDelivery, text, "list-delivery");
1089
- const itemKey = subscriptionDeliveryKeyString(item.key);
1091
+ if (
1092
+ subscriptionKeyString(item.key.subscription) === subscriptionKeyString(key) &&
1093
+ compareScheduleNames(itemKey, after) > 0
1094
+ )
1095
+ items.push(item);
1096
+ }
1097
+ items.sort((a, b) =>
1098
+ compareScheduleNames(
1099
+ subscriptionDeliveryKeyString(a.key),
1100
+ subscriptionDeliveryKeyString(b.key),
1101
+ ),
1102
+ );
1103
+
1104
+ return items.slice(0, limit);
1105
+ },
1106
+ );
1090
1107
 
1091
- if (
1092
- subscriptionKeyString(item.key.subscription) === subscriptionKeyString(key) &&
1093
- compareScheduleNames(itemKey, after) > 0
1094
- )
1095
- items.push(item);
1096
- }
1097
- items.sort((a, b) =>
1098
- compareScheduleNames(
1099
- subscriptionDeliveryKeyString(a.key),
1100
- subscriptionDeliveryKeyString(b.key),
1101
- ),
1102
- );
1108
+ const changeDelivery: SubscriptionStore["Service"]["changeDelivery"] = Effect.fnUntraced(
1109
+ function* (inputKey, inputDeliveryId, inputChange) {
1110
+ const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
1111
+ const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
1112
+ const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
1113
+
1114
+ yield* requirePartition(key.subscription, "change-delivery-partition");
1115
+ yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
1116
+
1117
+ const effectiveChange =
1118
+ change._tag === "Prepare"
1119
+ ? { ...change, nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis) }
1120
+ : change;
1121
+
1122
+ const result = yield* Effect.uninterruptible(
1123
+ Ref.modify(
1124
+ state,
1125
+ (
1126
+ current,
1127
+ ): readonly [
1128
+ Result.Result<SubscriptionDelivery, SubscriptionError>,
1129
+ MemorySubscriptionState,
1130
+ ] => {
1131
+ const storageKey = subscriptionDeliveryKeyString(key);
1132
+ const text = current.deliveries.get(storageKey);
1133
+
1134
+ if (text === undefined) return [Result.fail(error("not-found", "delivery")), current];
1135
+ const decoded = decode(SubscriptionDelivery, text, "change-delivery-record");
1136
+
1137
+ if (Result.isFailure(decoded)) return [decoded, current];
1138
+
1139
+ const registrationText = current.registrations.get(
1140
+ subscriptionKeyString(key.subscription),
1141
+ );
1103
1142
 
1104
- return items.slice(0, limit);
1105
- });
1143
+ if (registrationText === undefined)
1144
+ return [Result.fail(error("corrupt", "delivery-registration")), current];
1106
1145
 
1107
- const changeDelivery: SubscriptionStore["Service"]["changeDelivery"] = Effect.fn(
1108
- "MemorySubscriptionStore.changeDelivery",
1109
- )(function* (inputKey, inputDeliveryId, inputChange) {
1110
- const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
1111
- const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
1112
- const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
1113
-
1114
- yield* requirePartition(key.subscription, "change-delivery-partition");
1115
- yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
1116
-
1117
- const effectiveChange =
1118
- change._tag === "Prepare"
1119
- ? { ...change, nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis) }
1120
- : change;
1121
-
1122
- const result = yield* Effect.uninterruptible(
1123
- Ref.modify(
1124
- state,
1125
- (
1126
- current,
1127
- ): readonly [
1128
- Result.Result<SubscriptionDelivery, SubscriptionError>,
1129
- MemorySubscriptionState,
1130
- ] => {
1131
- const storageKey = subscriptionDeliveryKeyString(key);
1132
- const text = current.deliveries.get(storageKey);
1133
-
1134
- if (text === undefined) return [Result.fail(error("not-found", "delivery")), current];
1135
- const decoded = decode(SubscriptionDelivery, text, "change-delivery-record");
1136
-
1137
- if (Result.isFailure(decoded)) return [decoded, current];
1138
-
1139
- const registrationText = current.registrations.get(
1140
- subscriptionKeyString(key.subscription),
1141
- );
1142
-
1143
- if (registrationText === undefined)
1144
- return [Result.fail(error("corrupt", "delivery-registration")), current];
1145
-
1146
- const registration = decode(
1147
- SubscriptionRecord,
1148
- registrationText,
1149
- "delivery-registration",
1150
- );
1151
-
1152
- if (Result.isFailure(registration)) return [Result.fail(registration.failure), current];
1153
-
1154
- const transition = applySubscriptionDeliveryChange(
1155
- decoded.success,
1156
- registration.success,
1157
- deliveryId,
1158
- effectiveChange,
1159
- );
1160
-
1161
- if (Result.isFailure(transition)) return [Result.fail(transition.failure), current];
1162
- if (transition.success === decoded.success)
1163
- return [Result.succeed(decoded.success), current];
1164
- const updated = transition.success;
1165
- const encoded = encode(SubscriptionDelivery, updated, "change-delivery-encode");
1166
-
1167
- if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
1168
- const deliveries = new Map(current.deliveries);
1169
-
1170
- deliveries.set(storageKey, encoded.success);
1171
- const deliveryIndex = new Map(current.deliveryIndex);
1172
- const indexed = deliveryIndex.get(storageKey);
1173
-
1174
- if (indexed === undefined)
1175
- return [Result.fail(error("corrupt", "delivery-index")), current];
1176
- deliveryIndex.set(storageKey, {
1177
- ...indexed,
1178
- state: updated.state,
1179
- parked: updated.retry.parked ?? false,
1180
- observeSettlement: updated.observeSettlement ?? false,
1181
- nextAttemptAtMillis: updated.retry.nextAttemptAtMillis,
1182
- });
1183
-
1184
- return [Result.succeed(updated), { ...current, deliveries, deliveryIndex }];
1185
- },
1186
- ),
1187
- ).pipe(Effect.flatMap(Effect.fromResult));
1188
-
1189
- yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
1190
-
1191
- return result;
1192
- });
1146
+ const registration = decode(
1147
+ SubscriptionRecord,
1148
+ registrationText,
1149
+ "delivery-registration",
1150
+ );
1193
1151
 
1194
- const recovering: SubscriptionStore["Service"]["recovering"] = Effect.fn(
1195
- "MemorySubscriptionStore.recovering",
1196
- )(function* (nowMillis, after, limit) {
1197
- const records: Array<{ readonly key: SubscriptionKey; readonly ordinal: number }> = [];
1152
+ if (Result.isFailure(registration)) return [Result.fail(registration.failure), current];
1198
1153
 
1199
- for (const item of (yield* Ref.get(state)).registrationIndex.values()) {
1200
- if (
1201
- item.ordinal > after &&
1202
- item.state === "active" &&
1203
- item.recoveryAt !== null &&
1204
- item.recoveryAt <= nowMillis
1205
- )
1206
- records.push({ key: item.key, ordinal: item.ordinal });
1207
- }
1208
- records.sort((a, b) => a.ordinal - b.ordinal);
1154
+ const transition = applySubscriptionDeliveryChange(
1155
+ decoded.success,
1156
+ registration.success,
1157
+ deliveryId,
1158
+ effectiveChange,
1159
+ );
1209
1160
 
1210
- return records.slice(0, limit);
1211
- });
1161
+ if (Result.isFailure(transition)) return [Result.fail(transition.failure), current];
1162
+ if (transition.success === decoded.success)
1163
+ return [Result.succeed(decoded.success), current];
1164
+ const updated = transition.success;
1165
+ const encoded = encode(SubscriptionDelivery, updated, "change-delivery-encode");
1166
+
1167
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
1168
+ const deliveries = new Map(current.deliveries);
1169
+
1170
+ deliveries.set(storageKey, encoded.success);
1171
+ const deliveryIndex = new Map(current.deliveryIndex);
1172
+ const indexed = deliveryIndex.get(storageKey);
1173
+
1174
+ if (indexed === undefined)
1175
+ return [Result.fail(error("corrupt", "delivery-index")), current];
1176
+ deliveryIndex.set(storageKey, {
1177
+ ...indexed,
1178
+ state: updated.state,
1179
+ parked: updated.retry.parked ?? false,
1180
+ observeSettlement: updated.observeSettlement ?? false,
1181
+ nextAttemptAtMillis: updated.retry.nextAttemptAtMillis,
1182
+ });
1183
+
1184
+ return [Result.succeed(updated), { ...current, deliveries, deliveryIndex }];
1185
+ },
1186
+ ),
1187
+ ).pipe(Effect.flatMap(Effect.fromResult));
1188
+
1189
+ yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
1190
+
1191
+ return result;
1192
+ },
1193
+ );
1212
1194
 
1213
- const deferRecovery: SubscriptionStore["Service"]["deferRecovery"] = Effect.fn(
1214
- "MemorySubscriptionStore.deferRecovery",
1215
- )(function* (input, expectedRevision, recovery) {
1216
- const key = yield* requireKey(input, "defer-recovery-key");
1217
-
1218
- yield* failpoint.hit("subscription:defer-recovery:before");
1219
- yield* Effect.uninterruptible(
1220
- Ref.modify(
1221
- state,
1222
- (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
1223
- const storageKey = subscriptionKeyString(key);
1224
- const text = current.registrations.get(storageKey);
1225
-
1226
- if (text === undefined) return [Result.fail(error("not-found", "subscription")), current];
1227
- const record = decode(SubscriptionRecord, text, "defer-recovery-record");
1228
-
1229
- if (Result.isFailure(record)) return [Result.fail(record.failure), current];
1230
-
1231
- if (record.success.configurationRevision !== expectedRevision)
1232
- return [Result.void, current];
1233
-
1234
- const updated = {
1235
- ...record.success,
1236
- recovery:
1237
- record.success.state === "active" || record.success.state === "paused"
1238
- ? recovery
1239
- : null,
1240
- };
1241
-
1242
- const encoded = encode(SubscriptionRecord, updated, "defer-recovery-encode");
1243
-
1244
- if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
1245
- const registrations = new Map(current.registrations);
1246
-
1247
- registrations.set(storageKey, encoded.success);
1248
- const registrationIndex = new Map(current.registrationIndex);
1249
- const indexed = registrationIndex.get(storageKey);
1250
-
1251
- if (indexed === undefined)
1252
- return [Result.fail(error("corrupt", "recovery-index")), current];
1253
- registrationIndex.set(storageKey, {
1254
- ...indexed,
1255
- recoveryKey: updated.recovery === null ? null : candidateIndexKey(updated),
1256
- recoveryAt: updated.recovery?.nextAttemptAtMillis ?? null,
1257
- });
1258
-
1259
- return [
1260
- Result.void,
1261
- {
1262
- ...current,
1263
- registrations,
1264
- registrationIndex,
1265
- recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
1266
- },
1267
- ];
1268
- },
1269
- ),
1270
- ).pipe(Effect.flatMap(Effect.fromResult));
1271
- yield* failpoint.hit("subscription:defer-recovery:after");
1272
- });
1195
+ const recovering: SubscriptionStore["Service"]["recovering"] = Effect.fnUntraced(
1196
+ function* (nowMillis, after, limit) {
1197
+ const records: Array<{ readonly key: SubscriptionKey; readonly ordinal: number }> = [];
1198
+
1199
+ for (const item of (yield* Ref.get(state)).registrationIndex.values()) {
1200
+ if (
1201
+ item.ordinal > after &&
1202
+ item.state === "active" &&
1203
+ item.recoveryAt !== null &&
1204
+ item.recoveryAt <= nowMillis
1205
+ )
1206
+ records.push({ key: item.key, ordinal: item.ordinal });
1207
+ }
1208
+ records.sort((a, b) => a.ordinal - b.ordinal);
1209
+
1210
+ return records.slice(0, limit);
1211
+ },
1212
+ );
1213
+
1214
+ const deferRecovery: SubscriptionStore["Service"]["deferRecovery"] = Effect.fnUntraced(
1215
+ function* (input, expectedRevision, recovery) {
1216
+ const key = yield* requireKey(input, "defer-recovery-key");
1217
+
1218
+ yield* failpoint.hit("subscription:defer-recovery:before");
1219
+ yield* Effect.uninterruptible(
1220
+ Ref.modify(
1221
+ state,
1222
+ (current): readonly [Result.Result<void, SubscriptionError>, MemorySubscriptionState] => {
1223
+ const storageKey = subscriptionKeyString(key);
1224
+ const text = current.registrations.get(storageKey);
1225
+
1226
+ if (text === undefined)
1227
+ return [Result.fail(error("not-found", "subscription")), current];
1228
+ const record = decode(SubscriptionRecord, text, "defer-recovery-record");
1229
+
1230
+ if (Result.isFailure(record)) return [Result.fail(record.failure), current];
1231
+
1232
+ if (record.success.configurationRevision !== expectedRevision)
1233
+ return [Result.void, current];
1234
+
1235
+ const updated = {
1236
+ ...record.success,
1237
+ recovery:
1238
+ record.success.state === "active" || record.success.state === "paused"
1239
+ ? recovery
1240
+ : null,
1241
+ };
1242
+
1243
+ const encoded = encode(SubscriptionRecord, updated, "defer-recovery-encode");
1244
+
1245
+ if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
1246
+ const registrations = new Map(current.registrations);
1247
+
1248
+ registrations.set(storageKey, encoded.success);
1249
+ const registrationIndex = new Map(current.registrationIndex);
1250
+ const indexed = registrationIndex.get(storageKey);
1251
+
1252
+ if (indexed === undefined)
1253
+ return [Result.fail(error("corrupt", "recovery-index")), current];
1254
+ registrationIndex.set(storageKey, {
1255
+ ...indexed,
1256
+ recoveryKey: updated.recovery === null ? null : candidateIndexKey(updated),
1257
+ recoveryAt: updated.recovery?.nextAttemptAtMillis ?? null,
1258
+ });
1259
+
1260
+ return [
1261
+ Result.void,
1262
+ {
1263
+ ...current,
1264
+ registrations,
1265
+ registrationIndex,
1266
+ recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey]),
1267
+ },
1268
+ ];
1269
+ },
1270
+ ),
1271
+ ).pipe(Effect.flatMap(Effect.fromResult));
1272
+ yield* failpoint.hit("subscription:defer-recovery:after");
1273
+ },
1274
+ );
1273
1275
 
1274
1276
  const readScanCursors: SubscriptionStore["Service"]["readScanCursors"] = Ref.get(state).pipe(
1275
1277
  Effect.map((current) => current.scanCursors),
1276
1278
  );
1277
1279
 
1278
- const advanceScanCursors: SubscriptionStore["Service"]["advanceScanCursors"] = Effect.fn(
1279
- "MemorySubscriptionStore.advanceScanCursors",
1280
- )(function* (input) {
1281
- const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
1280
+ const advanceScanCursors: SubscriptionStore["Service"]["advanceScanCursors"] = Effect.fnUntraced(
1281
+ function* (input) {
1282
+ const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
1282
1283
 
1283
- yield* failpoint.hit("subscription:advance-scan-cursors:before");
1284
- yield* Effect.uninterruptible(
1285
- Ref.update(state, (current) => ({ ...current, scanCursors: cursors })),
1286
- );
1287
- yield* failpoint.hit("subscription:advance-scan-cursors:after");
1288
- });
1284
+ yield* failpoint.hit("subscription:advance-scan-cursors:before");
1285
+ yield* Effect.uninterruptible(
1286
+ Ref.update(state, (current) => ({ ...current, scanCursors: cursors })),
1287
+ );
1288
+ yield* failpoint.hit("subscription:advance-scan-cursors:after");
1289
+ },
1290
+ );
1289
1291
 
1290
1292
  const nextDeadline = Effect.gen(function* () {
1291
1293
  let deadline: number | null = null;
@@ -1315,206 +1317,209 @@ const makeMemorySubscriptionStore = Effect.fn("makeMemorySubscriptionStore")(fun
1315
1317
  if (record.state === "active" && record.recoveryAt !== null) consider(record.recoveryAt);
1316
1318
 
1317
1319
  return deadline;
1318
- }).pipe(Effect.withSpan("MemorySubscriptionStore.nextDeadline"));
1319
-
1320
- const compact: SubscriptionStore["Service"]["compact"] = Effect.fn(
1321
- "MemorySubscriptionStore.compact",
1322
- )(function* (nowMillis, inputPolicy, requestedLimit) {
1323
- nowMillis = Math.min(nowMillis, yield* Clock.currentTimeMillis);
1324
- const policy = yield* validate(SubscriptionRetentionPolicy, inputPolicy, "retention-policy");
1325
-
1326
- const limit = yield* validate(
1327
- Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })),
1328
- requestedLimit,
1329
- "maintenance-limit",
1330
- );
1331
-
1332
- yield* failpoint.hit("subscription:compact:before");
1333
- let corruptCandidates = 0;
1334
-
1335
- const result = yield* Effect.uninterruptible(
1336
- Ref.modify(
1337
- state,
1338
- (current): readonly [Result.Result<number, SubscriptionError>, MemorySubscriptionState] => {
1339
- if (
1340
- current.retentionHorizon !== null &&
1341
- current.retentionHorizon !== policy.replayHorizonMillis
1342
- )
1343
- return [Result.fail(error("conflict", "retention-horizon")), current];
1344
- const events = new Map(current.events);
1345
- const eventIndex = new Map(current.eventIndex);
1346
- const deliveries = new Map(current.deliveries);
1347
- const deliveryIndex = new Map(current.deliveryIndex);
1348
- const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
1349
- const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
1350
-
1351
- // Page indexes before decoding. Each pass reads at most 2 * limit event values and
1352
- // limit delivery values; relationship checks use maintained reference counts.
1353
- const deliveryPage = current.deliveryKeys.slice(
1354
- upperBound(current.deliveryKeys, current.maintenanceDeliveries),
1355
- upperBound(current.deliveryKeys, current.maintenanceDeliveries) + limit,
1356
- );
1357
-
1358
- const eventPage = current.eventKeys.slice(
1359
- upperBound(current.eventKeys, current.maintenanceEvents),
1360
- upperBound(current.eventKeys, current.maintenanceEvents) + limit,
1361
- );
1362
-
1363
- const cutoff = nowMillis - policy.completedRetentionMillis;
1364
- let removed = 0;
1365
- const removedEvents = new Set<string>();
1366
- const removedDeliveries = new Set<string>();
1367
-
1368
- for (const key of deliveryPage) {
1369
- const text = deliveries.get(key);
1370
-
1371
- if (text === undefined) continue;
1372
- const decoded = decode(SubscriptionDelivery, text, "compact-delivery");
1373
-
1374
- if (Result.isFailure(decoded)) {
1375
- corruptCandidates++;
1376
- continue;
1377
- }
1378
- const delivery = decoded.success;
1379
-
1380
- if (
1381
- subscriptionDeliveryKeyString(delivery.key) !== key ||
1382
- !samePartition(delivery.key.subscription.partition, partition)
1383
- ) {
1384
- corruptCandidates++;
1385
- continue;
1386
- }
1387
-
1388
- if (
1389
- delivery.state !== "refused" &&
1390
- (delivery.state !== "delivered" || delivery.settledAtMillis === undefined)
1391
- )
1392
- continue;
1393
- if (
1394
- (delivery.settledAtMillis ??
1395
- delivery.completedAtMillis ??
1396
- delivery.selectedAtMillis) > cutoff
1397
- )
1398
- continue;
1399
- const eventText = events.get(delivery.key.eventId);
1400
-
1401
- if (eventText === undefined) continue;
1402
- const accepted = decode(AcceptedEvent, eventText, "compact-delivery-event");
1403
-
1404
- if (Result.isFailure(accepted)) {
1405
- corruptCandidates++;
1406
- continue;
1407
- }
1408
- const event = accepted.success;
1409
-
1410
- if (
1411
- event.eventId !== delivery.key.eventId ||
1412
- !samePartition(event.partition, partition)
1413
- ) {
1414
- corruptCandidates++;
1415
- continue;
1416
- }
1320
+ });
1417
1321
 
1322
+ const compact: SubscriptionStore["Service"]["compact"] = Effect.fnUntraced(
1323
+ function* (nowMillis, inputPolicy, requestedLimit) {
1324
+ nowMillis = Math.min(nowMillis, yield* Clock.currentTimeMillis);
1325
+ const policy = yield* validate(SubscriptionRetentionPolicy, inputPolicy, "retention-policy");
1326
+
1327
+ const limit = yield* validate(
1328
+ Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 })),
1329
+ requestedLimit,
1330
+ "maintenance-limit",
1331
+ );
1332
+
1333
+ yield* failpoint.hit("subscription:compact:before");
1334
+ let corruptCandidates = 0;
1335
+
1336
+ const result = yield* Effect.uninterruptible(
1337
+ Ref.modify(
1338
+ state,
1339
+ (
1340
+ current,
1341
+ ): readonly [Result.Result<number, SubscriptionError>, MemorySubscriptionState] => {
1418
1342
  if (
1419
- !event.routingComplete ||
1420
- event.occurredAtMillis === undefined ||
1421
- event.acceptedAtMillis > cutoff ||
1422
- (current.recoveryCounts.get(eventCandidateIndexKey(event)) ?? 0) > 0
1343
+ current.retentionHorizon !== null &&
1344
+ current.retentionHorizon !== policy.replayHorizonMillis
1423
1345
  )
1424
- continue;
1425
- deliveries.delete(key);
1426
- removedDeliveries.add(key);
1427
- deliveryIndex.delete(key);
1428
- eventDeliveryCounts.set(
1429
- event.eventId,
1430
- (eventDeliveryCounts.get(event.eventId) ?? 1) - 1,
1346
+ return [Result.fail(error("conflict", "retention-horizon")), current];
1347
+ const events = new Map(current.events);
1348
+ const eventIndex = new Map(current.eventIndex);
1349
+ const deliveries = new Map(current.deliveries);
1350
+ const deliveryIndex = new Map(current.deliveryIndex);
1351
+ const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
1352
+ const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
1353
+
1354
+ // Page indexes before decoding. Each pass reads at most 2 * limit event values and
1355
+ // limit delivery values; relationship checks use maintained reference counts.
1356
+ const deliveryPage = current.deliveryKeys.slice(
1357
+ upperBound(current.deliveryKeys, current.maintenanceDeliveries),
1358
+ upperBound(current.deliveryKeys, current.maintenanceDeliveries) + limit,
1431
1359
  );
1432
- const owner = delivery.key.subscription.ownerId;
1433
-
1434
- ownerDeliveryCounts.set(owner, (ownerDeliveryCounts.get(owner) ?? 1) - 1);
1435
- }
1436
- let tombstones = current.tombstoneCount;
1437
1360
 
1438
- for (const key of eventPage) {
1439
- const text = events.get(key);
1361
+ const eventPage = current.eventKeys.slice(
1362
+ upperBound(current.eventKeys, current.maintenanceEvents),
1363
+ upperBound(current.eventKeys, current.maintenanceEvents) + limit,
1364
+ );
1440
1365
 
1441
- if (text === undefined) continue;
1442
- const decoded = decode(AcceptedEvent, text, "compact-event");
1366
+ const cutoff = nowMillis - policy.completedRetentionMillis;
1367
+ let removed = 0;
1368
+ const removedEvents = new Set<string>();
1369
+ const removedDeliveries = new Set<string>();
1370
+
1371
+ for (const key of deliveryPage) {
1372
+ const text = deliveries.get(key);
1373
+
1374
+ if (text === undefined) continue;
1375
+ const decoded = decode(SubscriptionDelivery, text, "compact-delivery");
1376
+
1377
+ if (Result.isFailure(decoded)) {
1378
+ corruptCandidates++;
1379
+ continue;
1380
+ }
1381
+ const delivery = decoded.success;
1382
+
1383
+ if (
1384
+ subscriptionDeliveryKeyString(delivery.key) !== key ||
1385
+ !samePartition(delivery.key.subscription.partition, partition)
1386
+ ) {
1387
+ corruptCandidates++;
1388
+ continue;
1389
+ }
1390
+
1391
+ if (
1392
+ delivery.state !== "refused" &&
1393
+ (delivery.state !== "delivered" || delivery.settledAtMillis === undefined)
1394
+ )
1395
+ continue;
1396
+ if (
1397
+ (delivery.settledAtMillis ??
1398
+ delivery.completedAtMillis ??
1399
+ delivery.selectedAtMillis) > cutoff
1400
+ )
1401
+ continue;
1402
+ const eventText = events.get(delivery.key.eventId);
1403
+
1404
+ if (eventText === undefined) continue;
1405
+ const accepted = decode(AcceptedEvent, eventText, "compact-delivery-event");
1406
+
1407
+ if (Result.isFailure(accepted)) {
1408
+ corruptCandidates++;
1409
+ continue;
1410
+ }
1411
+ const event = accepted.success;
1412
+
1413
+ if (
1414
+ event.eventId !== delivery.key.eventId ||
1415
+ !samePartition(event.partition, partition)
1416
+ ) {
1417
+ corruptCandidates++;
1418
+ continue;
1419
+ }
1420
+
1421
+ if (
1422
+ !event.routingComplete ||
1423
+ event.occurredAtMillis === undefined ||
1424
+ event.acceptedAtMillis > cutoff ||
1425
+ (current.recoveryCounts.get(eventCandidateIndexKey(event)) ?? 0) > 0
1426
+ )
1427
+ continue;
1428
+ deliveries.delete(key);
1429
+ removedDeliveries.add(key);
1430
+ deliveryIndex.delete(key);
1431
+ eventDeliveryCounts.set(
1432
+ event.eventId,
1433
+ (eventDeliveryCounts.get(event.eventId) ?? 1) - 1,
1434
+ );
1435
+ const owner = delivery.key.subscription.ownerId;
1443
1436
 
1444
- if (Result.isFailure(decoded)) {
1445
- corruptCandidates++;
1446
- continue;
1437
+ ownerDeliveryCounts.set(owner, (ownerDeliveryCounts.get(owner) ?? 1) - 1);
1447
1438
  }
1448
- const event = decoded.success;
1449
-
1450
- if (event.eventId !== key || !samePartition(event.partition, partition)) {
1451
- corruptCandidates++;
1452
- continue;
1439
+ let tombstones = current.tombstoneCount;
1440
+
1441
+ for (const key of eventPage) {
1442
+ const text = events.get(key);
1443
+
1444
+ if (text === undefined) continue;
1445
+ const decoded = decode(AcceptedEvent, text, "compact-event");
1446
+
1447
+ if (Result.isFailure(decoded)) {
1448
+ corruptCandidates++;
1449
+ continue;
1450
+ }
1451
+ const event = decoded.success;
1452
+
1453
+ if (event.eventId !== key || !samePartition(event.partition, partition)) {
1454
+ corruptCandidates++;
1455
+ continue;
1456
+ }
1457
+
1458
+ if (!event.routingComplete || event.occurredAtMillis === undefined) continue;
1459
+ const expired = event.occurredAtMillis <= nowMillis - policy.replayHorizonMillis;
1460
+
1461
+ if (event.tombstone === true && !expired) continue;
1462
+ if (
1463
+ event.acceptedAtMillis > cutoff ||
1464
+ (eventDeliveryCounts.get(key) ?? 0) > 0 ||
1465
+ (current.recoveryCounts.get(eventCandidateIndexKey(event)) ?? 0) > 0
1466
+ )
1467
+ continue;
1468
+ if (expired) {
1469
+ events.delete(key);
1470
+ removedEvents.add(key);
1471
+ eventIndex.delete(key);
1472
+ eventDeliveryCounts.delete(key);
1473
+ if (event.tombstone === true) tombstones--;
1474
+ } else {
1475
+ if (tombstones >= policy.maxTombstones) continue;
1476
+
1477
+ const encoded = encode(
1478
+ AcceptedEvent,
1479
+ { ...event, payload: null, tombstone: true },
1480
+ "compact-tombstone",
1481
+ );
1482
+
1483
+ if (Result.isFailure(encoded)) continue;
1484
+ events.set(key, encoded.success);
1485
+ tombstones++;
1486
+ }
1487
+ removed++;
1453
1488
  }
1454
1489
 
1455
- if (!event.routingComplete || event.occurredAtMillis === undefined) continue;
1456
- const expired = event.occurredAtMillis <= nowMillis - policy.replayHorizonMillis;
1457
-
1458
- if (event.tombstone === true && !expired) continue;
1459
- if (
1460
- event.acceptedAtMillis > cutoff ||
1461
- (eventDeliveryCounts.get(key) ?? 0) > 0 ||
1462
- (current.recoveryCounts.get(eventCandidateIndexKey(event)) ?? 0) > 0
1463
- )
1464
- continue;
1465
- if (expired) {
1466
- events.delete(key);
1467
- removedEvents.add(key);
1468
- eventIndex.delete(key);
1469
- eventDeliveryCounts.delete(key);
1470
- if (event.tombstone === true) tombstones--;
1471
- } else {
1472
- if (tombstones >= policy.maxTombstones) continue;
1473
-
1474
- const encoded = encode(
1475
- AcceptedEvent,
1476
- { ...event, payload: null, tombstone: true },
1477
- "compact-tombstone",
1478
- );
1490
+ return [
1491
+ Result.succeed(removed),
1492
+ {
1493
+ ...current,
1494
+ events,
1495
+ eventIndex,
1496
+ deliveries,
1497
+ deliveryIndex,
1498
+ eventDeliveryCounts,
1499
+ ownerDeliveryCounts,
1500
+ eventKeys: removeKeys(current.eventKeys, removedEvents),
1501
+ deliveryKeys: removeKeys(current.deliveryKeys, removedDeliveries),
1502
+ tombstoneCount: tombstones,
1503
+ maintenanceEvents: eventPage.length < limit ? "" : (eventPage.at(-1) ?? ""),
1504
+ maintenanceDeliveries:
1505
+ deliveryPage.length < limit ? "" : (deliveryPage.at(-1) ?? ""),
1506
+ retentionHorizon: policy.replayHorizonMillis,
1507
+ retentionDeadline: events.size === 0 ? null : nowMillis + 60_000,
1508
+ },
1509
+ ];
1510
+ },
1511
+ ),
1512
+ ).pipe(Effect.flatMap(Effect.fromResult));
1479
1513
 
1480
- if (Result.isFailure(encoded)) continue;
1481
- events.set(key, encoded.success);
1482
- tombstones++;
1483
- }
1484
- removed++;
1485
- }
1514
+ if (corruptCandidates > 0)
1515
+ yield* Effect.logWarning("Subscription retention preserved corrupt candidates", {
1516
+ count: corruptCandidates,
1517
+ });
1518
+ yield* failpoint.hit("subscription:compact:after");
1486
1519
 
1487
- return [
1488
- Result.succeed(removed),
1489
- {
1490
- ...current,
1491
- events,
1492
- eventIndex,
1493
- deliveries,
1494
- deliveryIndex,
1495
- eventDeliveryCounts,
1496
- ownerDeliveryCounts,
1497
- eventKeys: removeKeys(current.eventKeys, removedEvents),
1498
- deliveryKeys: removeKeys(current.deliveryKeys, removedDeliveries),
1499
- tombstoneCount: tombstones,
1500
- maintenanceEvents: eventPage.length < limit ? "" : (eventPage.at(-1) ?? ""),
1501
- maintenanceDeliveries: deliveryPage.length < limit ? "" : (deliveryPage.at(-1) ?? ""),
1502
- retentionHorizon: policy.replayHorizonMillis,
1503
- retentionDeadline: events.size === 0 ? null : nowMillis + 60_000,
1504
- },
1505
- ];
1506
- },
1507
- ),
1508
- ).pipe(Effect.flatMap(Effect.fromResult));
1509
-
1510
- if (corruptCandidates > 0)
1511
- yield* Effect.logWarning("Subscription retention preserved corrupt candidates", {
1512
- count: corruptCandidates,
1513
- });
1514
- yield* failpoint.hit("subscription:compact:after");
1515
-
1516
- return result;
1517
- });
1520
+ return result;
1521
+ },
1522
+ );
1518
1523
 
1519
1524
  return SubscriptionStore.of({
1520
1525
  partition,