@effect-agent/storage-cloudflare 0.1.0-beta.42 → 0.1.0-beta.45

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.
@@ -29,12 +29,15 @@ const CURRENT_SUBSCRIPTION_STORE_VERSION = 2;
29
29
  const StoredJson = Schema.String.check(Schema.isMaxLength(1_900_000));
30
30
  const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
31
31
  const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
32
+
32
33
  const ScanRow = Schema.Struct({
33
34
  event_scan_cursor: Schema.String,
34
35
  delivery_scan_cursor: Schema.String,
35
36
  recovery_scan_cursor: Schema.Natural,
36
37
  });
38
+
37
39
  const JsonRow = Schema.Struct({ record_json: StoredJson });
40
+
38
41
  const RegistrationRow = Schema.Struct({
39
42
  owner_id: Schema.String,
40
43
  subscription_id: Schema.String,
@@ -47,6 +50,7 @@ const RegistrationRow = Schema.Struct({
47
50
  recovery_at_millis: Schema.NullOr(Schema.Number),
48
51
  record_json: StoredJson,
49
52
  });
53
+
50
54
  const EventRow = Schema.Struct({
51
55
  event_id: Schema.String,
52
56
  source_name: Schema.String,
@@ -59,6 +63,7 @@ const EventRow = Schema.Struct({
59
63
  next_attempt_at_millis: Schema.Number,
60
64
  record_json: StoredJson,
61
65
  });
66
+
62
67
  const DeliveryRow = Schema.Struct({
63
68
  owner_id: Schema.String,
64
69
  subscription_id: Schema.String,
@@ -68,6 +73,7 @@ const DeliveryRow = Schema.Struct({
68
73
  next_attempt_at_millis: Schema.Number,
69
74
  record_json: StoredJson,
70
75
  });
76
+
71
77
  const StoreStateRow = Schema.Struct({
72
78
  storage_version: Schema.Int,
73
79
  alarm_generation: Schema.Natural,
@@ -77,6 +83,7 @@ export interface DoSubscriptionAlarmReplacement {
77
83
  readonly deadlineAtMillis: number | null;
78
84
  readonly generation: number;
79
85
  }
86
+
80
87
  export type DoSubscriptionReplaceAlarm = (
81
88
  replacement: DoSubscriptionAlarmReplacement,
82
89
  ) => Effect.Effect<void, SubscriptionError>;
@@ -103,6 +110,7 @@ export class DoSubscriptionAlarmControl extends Context.Service<
103
110
 
104
111
  const error = (reason: SubscriptionError["reason"], code: string) =>
105
112
  SubscriptionError.make({ reason, code });
113
+
106
114
  const unavailable = (operation: string) => error("storage", operation);
107
115
  const corrupt = (operation: string) => error("corrupt", operation);
108
116
  const bytes = (value: unknown) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
@@ -135,14 +143,17 @@ const sameDeliveryIdentity = (left: SubscriptionDelivery, right: SubscriptionDel
135
143
 
136
144
  const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize")(function* () {
137
145
  const sql = yield* SqlClientService.SqlClient;
146
+
138
147
  const names = yield* sql<Record<string, unknown>>`
139
148
  SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'effect_agent_subscription_%'
140
149
  `.pipe(Effect.mapError(() => unavailable("inspect subscription storage")));
150
+
141
151
  const decodedNames = yield* decodeRows(
142
152
  Schema.Struct({ name: Schema.String }),
143
153
  names,
144
154
  "inspect subscription storage",
145
155
  );
156
+
146
157
  const expected = new Set([
147
158
  "effect_agent_subscription_store_state",
148
159
  "effect_agent_subscription_sequences",
@@ -150,9 +161,11 @@ const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize"
150
161
  "effect_agent_subscription_events",
151
162
  "effect_agent_subscription_deliveries",
152
163
  ]);
164
+
153
165
  const hasState = decodedNames.some(
154
166
  ({ name }) => name === "effect_agent_subscription_store_state",
155
167
  );
168
+
156
169
  if (!hasState && decodedNames.length > 0) return yield* corrupt("partial subscription storage");
157
170
  if (!hasState) {
158
171
  yield* sql
@@ -201,16 +214,20 @@ const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize"
201
214
  }),
202
215
  )
203
216
  .pipe(Effect.mapError(() => unavailable("initialize subscription storage")));
217
+
204
218
  return;
205
219
  }
206
220
  if (decodedNames.length !== expected.size || decodedNames.some(({ name }) => !expected.has(name)))
207
221
  return yield* corrupt("subscription storage tables");
222
+
208
223
  const rows = yield* sql<
209
224
  Record<string, unknown>
210
225
  >`SELECT storage_version, alarm_generation FROM effect_agent_subscription_store_state WHERE singleton=1`.pipe(
211
226
  Effect.mapError(() => unavailable("read subscription storage version")),
212
227
  );
228
+
213
229
  const state = yield* decodeRows(StoreStateRow, rows, "read subscription storage version");
230
+
214
231
  if (state.length !== 1 || state[0].storage_version !== CURRENT_SUBSCRIPTION_STORE_VERSION)
215
232
  return yield* corrupt(
216
233
  state.length === 1
@@ -226,6 +243,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
226
243
  const sql = yield* SqlClientService.SqlClient;
227
244
  const failpoint = yield* SubscriptionFailpoint;
228
245
  const transactions = yield* DoSubscriptionTransaction;
246
+
229
247
  yield* initializeDoSubscriptionStore();
230
248
  yield* sql`
231
249
  INSERT INTO effect_agent_subscription_sequences (
@@ -237,6 +255,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
237
255
  effect: Effect.Effect<ReadonlyArray<A>, SqlError>,
238
256
  code: string,
239
257
  ) => effect.pipe(Effect.mapError(() => unavailable(code)));
258
+
240
259
  const readIndexedDeadline = Effect.fn("DoSubscriptionStore.readIndexedDeadline")(function* () {
241
260
  const scanRows = yield* query(
242
261
  sql<Record<string, unknown>>`
@@ -245,7 +264,9 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
245
264
  `,
246
265
  "read scan cursor deadline",
247
266
  );
267
+
248
268
  const scans = yield* decodeRows(ScanRow, scanRows, "read scan cursor deadline");
269
+
249
270
  if (scans.length !== 1) return yield* corrupt("scan cursor deadline");
250
271
  if (
251
272
  scans[0].event_scan_cursor !== "" ||
@@ -253,6 +274,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
253
274
  scans[0].recovery_scan_cursor !== 0
254
275
  )
255
276
  return 0;
277
+
256
278
  const rows = yield* query(
257
279
  sql<Record<string, unknown>>`
258
280
  SELECT MIN(deadline) AS deadline FROM (
@@ -266,14 +288,18 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
266
288
  `,
267
289
  "read subscription deadline",
268
290
  );
291
+
269
292
  const decoded = yield* decodeRows(
270
293
  Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }),
271
294
  rows,
272
295
  "read subscription deadline",
273
296
  );
297
+
274
298
  if (decoded.length !== 1) return yield* corrupt("subscription deadline");
299
+
275
300
  return decoded[0].deadline;
276
301
  });
302
+
277
303
  const replaceAlarm = Effect.fn("DoSubscriptionStore.replaceAlarm")(function* (
278
304
  replace: DoSubscriptionReplaceAlarm,
279
305
  deadlineAtMillis: number | null,
@@ -285,33 +311,43 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
285
311
  `,
286
312
  "advance subscription alarm generation",
287
313
  );
314
+
288
315
  const state = yield* decodeRows(StoreStateRow, rows, "advance subscription alarm generation");
316
+
289
317
  if (state.length !== 1 || state[0].storage_version !== CURRENT_SUBSCRIPTION_STORE_VERSION)
290
318
  return yield* corrupt("subscription alarm state");
291
319
  yield* failpoint.hit("subscription:alarm:before");
292
320
  yield* replace({ deadlineAtMillis, generation: state[0].alarm_generation });
293
321
  yield* failpoint.hit("subscription:alarm:after");
294
322
  });
323
+
295
324
  const transact = <A>(effect: Effect.Effect<A, SubscriptionError | SubscriptionFailpointError>) =>
296
325
  transactions.run((replace) =>
297
326
  Effect.gen(function* () {
298
327
  const value = yield* effect;
328
+
299
329
  yield* replaceAlarm(replace, yield* readIndexedDeadline());
330
+
300
331
  return value;
301
332
  }),
302
333
  );
334
+
303
335
  const requirePartition = (candidate: SourcePartition, code: string) =>
304
336
  sameSourcePartition(candidate, partition)
305
337
  ? Effect.void
306
338
  : Effect.fail(error("validation", code));
339
+
307
340
  const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (
308
341
  input: SubscriptionKey,
309
342
  code: string,
310
343
  ) {
311
344
  const key = yield* validate(SubscriptionKey, input, code);
345
+
312
346
  yield* requirePartition(key.partition, code);
347
+
313
348
  return key;
314
349
  });
350
+
315
351
  const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (
316
352
  key: SubscriptionKey,
317
353
  code: string,
@@ -325,11 +361,15 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
325
361
  `,
326
362
  code,
327
363
  );
364
+
328
365
  const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
366
+
329
367
  if (decodedRows.length > 1) return yield* corrupt(code);
330
368
  const row = decodedRows[0];
369
+
331
370
  if (row === undefined) return null;
332
371
  const record = yield* decode(SubscriptionRecord, row.record_json, code);
372
+
333
373
  if (
334
374
  !sameSourcePartition(record.key.partition, partition) ||
335
375
  record.key.ownerId !== row.owner_id ||
@@ -343,8 +383,10 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
343
383
  (record.recovery?.nextAttemptAtMillis ?? null) !== row.recovery_at_millis
344
384
  )
345
385
  return yield* corrupt(`${code}-projection`);
386
+
346
387
  return record;
347
388
  });
389
+
348
390
  const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (
349
391
  eventId: string,
350
392
  code: string,
@@ -357,11 +399,15 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
357
399
  `,
358
400
  code,
359
401
  );
402
+
360
403
  const decodedRows = yield* decodeRows(EventRow, rows, code);
404
+
361
405
  if (decodedRows.length > 1) return yield* corrupt(code);
362
406
  const row = decodedRows[0];
407
+
363
408
  if (row === undefined) return null;
364
409
  const event = yield* decode(AcceptedEvent, row.record_json, code);
410
+
365
411
  if (
366
412
  !sameSourcePartition(event.partition, partition) ||
367
413
  event.eventId !== row.event_id ||
@@ -375,8 +421,10 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
375
421
  event.nextAttemptAtMillis !== row.next_attempt_at_millis
376
422
  )
377
423
  return yield* corrupt(`${code}-projection`);
424
+
378
425
  return event;
379
426
  });
427
+
380
428
  const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (
381
429
  key: SubscriptionDeliveryKey,
382
430
  code: string,
@@ -391,11 +439,15 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
391
439
  `,
392
440
  code,
393
441
  );
442
+
394
443
  const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
444
+
395
445
  if (decodedRows.length > 1) return yield* corrupt(code);
396
446
  const row = decodedRows[0];
447
+
397
448
  if (row === undefined) return null;
398
449
  const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
450
+
399
451
  if (
400
452
  !sameSourcePartition(delivery.key.subscription.partition, partition) ||
401
453
  delivery.key.subscription.ownerId !== row.owner_id ||
@@ -406,17 +458,22 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
406
458
  delivery.retry.nextAttemptAtMillis !== row.next_attempt_at_millis
407
459
  )
408
460
  return yield* corrupt(`${code}-projection`);
461
+
409
462
  return delivery;
410
463
  });
464
+
411
465
  const count = Effect.fn("SqliteSubscriptionStore.count")(function* (
412
466
  statement: Effect.Effect<ReadonlyArray<Record<string, unknown>>, SqlError>,
413
467
  code: string,
414
468
  ) {
415
469
  const rows = yield* query(statement, code);
416
470
  const decoded = yield* decodeRows(CountRow, rows, code);
471
+
417
472
  if (decoded.length !== 1) return yield* corrupt(code);
473
+
418
474
  return decoded[0].count;
419
475
  });
476
+
420
477
  const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
421
478
  const rows = yield* query(
422
479
  sql<Record<string, unknown>>`
@@ -426,14 +483,19 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
426
483
  `,
427
484
  "advance subscription sequence",
428
485
  );
486
+
429
487
  const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
488
+
430
489
  if (decoded.length !== 1) return yield* corrupt("subscription sequence");
490
+
431
491
  return decoded[0].sequence;
432
492
  });
493
+
433
494
  const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (
434
495
  record: SubscriptionRecord,
435
496
  ) {
436
497
  const json = yield* encode(SubscriptionRecord, record, "encode subscription");
498
+
437
499
  yield* query(
438
500
  sql<Record<string, unknown>>`
439
501
  UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
@@ -444,10 +506,12 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
444
506
  "write subscription",
445
507
  );
446
508
  });
509
+
447
510
  const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (
448
511
  event: AcceptedEvent,
449
512
  ) {
450
513
  const json = yield* encode(AcceptedEvent, event, "encode event");
514
+
451
515
  yield* query(
452
516
  sql<Record<string, unknown>>`
453
517
  UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
@@ -457,10 +521,12 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
457
521
  "write event",
458
522
  );
459
523
  });
524
+
460
525
  const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (
461
526
  delivery: SubscriptionDelivery,
462
527
  ) {
463
528
  const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
529
+
464
530
  yield* query(
465
531
  sql<Record<string, unknown>>`
466
532
  UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
@@ -478,13 +544,17 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
478
544
  )(function* (input, inputLimits) {
479
545
  const record = yield* validate(SubscriptionRecord, input, "register-record");
480
546
  const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
547
+
481
548
  yield* requirePartition(record.key.partition, "register-partition");
549
+
482
550
  const result = yield* transact(
483
551
  Effect.gen(function* () {
484
552
  const existing = yield* readRegistration(record.key, "register-existing");
553
+
485
554
  if (existing !== null) {
486
555
  if (existing.creationFingerprint !== record.creationFingerprint)
487
556
  return yield* error("conflict", "registration-identity");
557
+
488
558
  return { value: existing, changed: false } as const;
489
559
  }
490
560
  if (bytes(record.configuration.context) > limits.maxContextBytes)
@@ -516,6 +586,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
516
586
  return yield* error("capacity", "owner-registrations");
517
587
  const assigned = { ...record, ordinal: yield* nextSequence() };
518
588
  const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
589
+
519
590
  yield* failpoint.hit("subscription:register:before");
520
591
  yield* query(
521
592
  sql<Record<string, unknown>>`
@@ -527,10 +598,13 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
527
598
  `,
528
599
  "insert registration",
529
600
  );
601
+
530
602
  return { value: assigned, changed: true } as const;
531
603
  }),
532
604
  );
605
+
533
606
  if (result.changed) yield* failpoint.hit("subscription:register:after");
607
+
534
608
  return result.value;
535
609
  });
536
610
 
@@ -549,6 +623,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
549
623
  `,
550
624
  "list subscriptions",
551
625
  );
626
+
552
627
  const decoded = yield* decodeRows(
553
628
  Schema.Struct({
554
629
  owner_id: Schema.String,
@@ -558,6 +633,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
558
633
  rows,
559
634
  "list subscriptions",
560
635
  );
636
+
561
637
  return yield* Effect.forEach(
562
638
  decoded,
563
639
  Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
@@ -565,8 +641,10 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
565
641
  { partition, ownerId: row.owner_id, subscriptionId: row.subscription_id },
566
642
  "list subscription",
567
643
  );
644
+
568
645
  if (record === null || record.ordinal !== row.ordinal)
569
646
  return yield* corrupt("list subscription projection");
647
+
570
648
  return record;
571
649
  }),
572
650
  );
@@ -577,18 +655,24 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
577
655
  "SqliteSubscriptionStore.cancel",
578
656
  )(function* (input) {
579
657
  const key = yield* requireKey(input, "cancel-key");
658
+
580
659
  const result = yield* transact(
581
660
  Effect.gen(function* () {
582
661
  const current = yield* readRegistration(key, "cancel subscription");
662
+
583
663
  if (current === null) return yield* error("not-found", "subscription");
584
664
  if (current.state === "cancelled") return { value: current, changed: false } as const;
585
665
  const updated = { ...current, state: "cancelled" as const, recovery: null };
666
+
586
667
  yield* failpoint.hit("subscription:cancel:before");
587
668
  yield* writeRegistration(updated);
669
+
588
670
  return { value: updated, changed: true } as const;
589
671
  }),
590
672
  );
673
+
591
674
  if (result.changed) yield* failpoint.hit("subscription:cancel:after");
675
+
592
676
  return result.value;
593
677
  });
594
678
 
@@ -597,13 +681,17 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
597
681
  )(function* (input, inputLimits) {
598
682
  const event = yield* validate(AcceptedEvent, input, "accept-event");
599
683
  const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
684
+
600
685
  yield* requirePartition(event.partition, "accept-partition");
686
+
601
687
  const result = yield* transact(
602
688
  Effect.gen(function* () {
603
689
  const existing = yield* readEvent(event.eventId, "accept event");
690
+
604
691
  if (existing !== null) {
605
692
  if (!sameAcceptedEventIdentity(existing, event))
606
693
  return yield* error("conflict", "event-identity");
694
+
607
695
  return { value: existing, changed: false } as const;
608
696
  }
609
697
  if (bytes(event.payload) > limits.maxPayloadBytes)
@@ -617,6 +705,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
617
705
  )) >= limits.maxEvents
618
706
  )
619
707
  return yield* error("capacity", "events");
708
+
620
709
  const accepted: AcceptedEvent = {
621
710
  ...event,
622
711
  cutoff: yield* nextSequence(),
@@ -624,7 +713,9 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
624
713
  routingComplete: false,
625
714
  routingFailure: null,
626
715
  };
716
+
627
717
  const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
718
+
628
719
  yield* failpoint.hit("subscription:accept:before");
629
720
  yield* query(
630
721
  sql<Record<string, unknown>>`
@@ -635,10 +726,13 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
635
726
  `,
636
727
  "insert event",
637
728
  );
729
+
638
730
  return { value: accepted, changed: true } as const;
639
731
  }),
640
732
  );
733
+
641
734
  if (result.changed) yield* failpoint.hit("subscription:accept:after");
735
+
642
736
  return result.value;
643
737
  });
644
738
 
@@ -656,6 +750,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
656
750
  `,
657
751
  "pending events",
658
752
  );
753
+
659
754
  return yield* decodeRows(
660
755
  Schema.Struct({ event_id: Schema.String }),
661
756
  rows,
@@ -667,10 +762,13 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
667
762
  "SqliteSubscriptionStore.candidates",
668
763
  )(function* (input, limit) {
669
764
  const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
765
+
670
766
  yield* requirePartition(supplied.partition, "candidates-partition");
671
767
  const stored = yield* readEvent(supplied.eventId, "candidates event");
768
+
672
769
  if (stored === null) return yield* error("not-found", "event");
673
770
  if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
771
+
674
772
  const rows = yield* query(
675
773
  sql<Record<string, unknown>>`
676
774
  SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
@@ -679,6 +777,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
679
777
  `,
680
778
  "subscription candidates",
681
779
  );
780
+
682
781
  const decoded = yield* decodeRows(
683
782
  Schema.Struct({
684
783
  owner_id: Schema.String,
@@ -688,6 +787,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
688
787
  rows,
689
788
  "subscription candidates",
690
789
  );
790
+
691
791
  return yield* Effect.forEach(
692
792
  decoded,
693
793
  Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
@@ -695,8 +795,10 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
695
795
  { partition, ownerId: row.owner_id, subscriptionId: row.subscription_id },
696
796
  "subscription candidate",
697
797
  );
798
+
698
799
  if (record === null || record.ordinal !== row.ordinal)
699
800
  return yield* corrupt("subscription candidate projection");
801
+
700
802
  return record;
701
803
  }),
702
804
  );
@@ -706,6 +808,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
706
808
  delivery: SubscriptionDelivery,
707
809
  ) {
708
810
  const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
811
+
709
812
  yield* query(
710
813
  sql<Record<string, unknown>>`
711
814
  INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
@@ -721,18 +824,23 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
721
824
  "SqliteSubscriptionStore.select",
722
825
  )(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
723
826
  const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
827
+
724
828
  const deliveries = yield* validate(
725
829
  Schema.Array(SubscriptionDelivery),
726
830
  inputDeliveries,
727
831
  "select-deliveries",
728
832
  );
833
+
729
834
  const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
835
+
730
836
  yield* requirePartition(supplied.partition, "select-partition");
731
837
  for (const candidate of deliveries)
732
838
  yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
839
+
733
840
  const changed = yield* transact(
734
841
  Effect.gen(function* () {
735
842
  const accepted = yield* readEvent(supplied.eventId, "select event");
843
+
736
844
  if (accepted === null) return yield* error("not-found", "event");
737
845
  if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor)
738
846
  return yield* error("conflict", "event-cursor");
@@ -742,8 +850,10 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
742
850
  yield* failpoint.hit("subscription:select:before");
743
851
  const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
744
852
  const additions: Array<{ delivery: SubscriptionDelivery; record: SubscriptionRecord }> = [];
853
+
745
854
  for (const delivery of deliveries) {
746
855
  const record = yield* readRegistration(delivery.key.subscription, "select registration");
856
+
747
857
  if (record === null) return yield* error("not-found", "subscription");
748
858
  if (
749
859
  !subscriptionDeliveryCanSelect(delivery, record, accepted) ||
@@ -755,6 +865,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
755
865
  )
756
866
  return yield* error("conflict", "selection");
757
867
  const existing = yield* readDelivery(delivery.key, "select existing delivery");
868
+
758
869
  if (existing !== null) {
759
870
  if (!sameDeliveryIdentity(existing, delivery))
760
871
  return yield* error("conflict", "delivery-identity");
@@ -763,12 +874,14 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
763
874
  if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
764
875
  additions.push({ delivery, record });
765
876
  }
877
+
766
878
  const total = yield* count(
767
879
  sql<
768
880
  Record<string, unknown>
769
881
  >`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`,
770
882
  "count deliveries",
771
883
  );
884
+
772
885
  if (total + additions.length > limits.maxDeliveries)
773
886
  return yield* error("capacity", "deliveries");
774
887
  for (const ownerId of new Set(additions.map(({ record }) => record.key.ownerId))) {
@@ -778,6 +891,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
778
891
  >`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${ownerId}`,
779
892
  "count owner deliveries",
780
893
  );
894
+
781
895
  if (
782
896
  existing + additions.filter(({ record }) => record.key.ownerId === ownerId).length >
783
897
  limits.maxDeliveriesPerOwner
@@ -790,9 +904,11 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
790
904
  yield* writeRegistration({ ...addition.record, state: "consumed", recovery: null });
791
905
  }
792
906
  yield* writeEvent({ ...accepted, cursor, routingComplete: complete, routingFailure: null });
907
+
793
908
  return true;
794
909
  }),
795
910
  );
911
+
796
912
  if (changed) yield* failpoint.hit("subscription:select:after");
797
913
  });
798
914
 
@@ -802,12 +918,15 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
802
918
  const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
803
919
  const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
804
920
  const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
921
+
805
922
  yield* requirePartition(supplied.partition, "catch-up-partition");
806
923
  yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
924
+
807
925
  const changed = yield* transact(
808
926
  Effect.gen(function* () {
809
927
  const accepted = yield* readEvent(supplied.eventId, "catch-up event");
810
928
  const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
929
+
811
930
  if (accepted === null || record === null)
812
931
  return yield* error("not-found", accepted === null ? "event" : "subscription");
813
932
  if (
@@ -820,13 +939,16 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
820
939
  )
821
940
  return yield* error("conflict", "catch-up-identity");
822
941
  const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
942
+
823
943
  if (existing !== null) {
824
944
  if (!sameDeliveryIdentity(existing, delivery))
825
945
  return yield* error("conflict", "delivery-identity");
946
+
826
947
  return false;
827
948
  }
828
949
  yield* failpoint.hit("subscription:catch-up:before");
829
950
  const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
951
+
830
952
  if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, true))
831
953
  return yield* error("conflict", "catch-up-eligibility");
832
954
  if (
@@ -849,9 +971,11 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
849
971
  return yield* error("capacity", "owner-deliveries");
850
972
  yield* insertDelivery(delivery);
851
973
  yield* writeRegistration({ ...record, state: "consumed", recovery: null });
974
+
852
975
  return true;
853
976
  }),
854
977
  );
978
+
855
979
  if (changed) yield* failpoint.hit("subscription:catch-up:after");
856
980
  });
857
981
 
@@ -862,9 +986,11 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
862
986
  code === undefined
863
987
  ? "routing-failed"
864
988
  : yield* validate(SubscriptionName, code, "routing-failure");
989
+
865
990
  yield* transact(
866
991
  Effect.gen(function* () {
867
992
  const accepted = yield* readEvent(eventId, "defer event");
993
+
868
994
  if (accepted === null) return yield* error("not-found", "event");
869
995
  yield* failpoint.hit("subscription:defer-event:before");
870
996
  yield* writeEvent({ ...accepted, nextAttemptAtMillis, routingFailure });
@@ -877,7 +1003,9 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
877
1003
  "SqliteSubscriptionStore.delivery",
878
1004
  )(function* (input) {
879
1005
  const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
1006
+
880
1007
  yield* requirePartition(key.subscription.partition, "delivery-partition");
1008
+
881
1009
  return yield* readDelivery(key, "get delivery");
882
1010
  });
883
1011
 
@@ -892,11 +1020,13 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
892
1020
  `,
893
1021
  "pending deliveries",
894
1022
  );
1023
+
895
1024
  const rowSchema = Schema.Struct({
896
1025
  owner_id: Schema.String,
897
1026
  subscription_id: Schema.String,
898
1027
  event_id: Schema.String,
899
1028
  });
1029
+
900
1030
  return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(
901
1031
  Effect.map((items) =>
902
1032
  items.map((item) => ({
@@ -911,6 +1041,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
911
1041
  "SqliteSubscriptionStore.listDeliveries",
912
1042
  )(function* (input, after, limit) {
913
1043
  const key = yield* requireKey(input, "list-deliveries-key");
1044
+
914
1045
  const rows = yield* query(
915
1046
  sql<Record<string, unknown>>`
916
1047
  SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
@@ -918,7 +1049,9 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
918
1049
  `,
919
1050
  "list deliveries",
920
1051
  );
1052
+
921
1053
  const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
1054
+
922
1055
  return yield* Effect.forEach(decoded, (row) =>
923
1056
  decode(SubscriptionDelivery, row.record_json, "list delivery"),
924
1057
  );
@@ -930,32 +1063,41 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
930
1063
  const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
931
1064
  const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
932
1065
  const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
1066
+
933
1067
  yield* requirePartition(key.subscription.partition, "change-delivery-partition");
1068
+
934
1069
  const result = yield* transact(
935
1070
  Effect.gen(function* () {
936
1071
  const existing = yield* readDelivery(key, "change delivery");
937
1072
  const record = yield* readRegistration(key.subscription, "change delivery subscription");
1073
+
938
1074
  if (existing === null || record === null)
939
1075
  return yield* error("not-found", existing === null ? "delivery" : "subscription");
940
1076
  yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
1077
+
941
1078
  const effectiveChange =
942
1079
  change._tag === "Prepare"
943
1080
  ? { ...change, nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis) }
944
1081
  : change;
1082
+
945
1083
  const transition = applySubscriptionDeliveryChange(
946
1084
  existing,
947
1085
  record,
948
1086
  deliveryId,
949
1087
  effectiveChange,
950
1088
  );
1089
+
951
1090
  if (Result.isFailure(transition)) return yield* transition.failure;
952
1091
  if (transition.success === existing) return { value: existing, changed: false } as const;
953
1092
  yield* writeDelivery(transition.success);
1093
+
954
1094
  return { value: transition.success, changed: true } as const;
955
1095
  }),
956
1096
  );
1097
+
957
1098
  if (result.changed)
958
1099
  yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
1100
+
959
1101
  return result.value;
960
1102
  });
961
1103
 
@@ -971,11 +1113,13 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
971
1113
  `,
972
1114
  "recovering subscriptions",
973
1115
  );
1116
+
974
1117
  const rowSchema = Schema.Struct({
975
1118
  owner_id: Schema.String,
976
1119
  subscription_id: Schema.String,
977
1120
  ordinal: Schema.Natural,
978
1121
  });
1122
+
979
1123
  return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(
980
1124
  Effect.map((items) =>
981
1125
  items.map((item) => ({
@@ -990,9 +1134,11 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
990
1134
  "SqliteSubscriptionStore.deferRecovery",
991
1135
  )(function* (input, recovery) {
992
1136
  const key = yield* requireKey(input, "defer-recovery-key");
1137
+
993
1138
  yield* transact(
994
1139
  Effect.gen(function* () {
995
1140
  const record = yield* readRegistration(key, "defer recovery");
1141
+
996
1142
  if (record === null) return yield* error("not-found", "subscription");
997
1143
  yield* failpoint.hit("subscription:defer-recovery:before");
998
1144
  yield* writeRegistration({
@@ -1013,8 +1159,11 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
1013
1159
  `,
1014
1160
  "read subscription scan cursors",
1015
1161
  );
1162
+
1016
1163
  const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
1164
+
1017
1165
  if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
1166
+
1018
1167
  return {
1019
1168
  events: decoded[0].event_scan_cursor,
1020
1169
  deliveries: decoded[0].delivery_scan_cursor,
@@ -1026,6 +1175,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
1026
1175
  "SqliteSubscriptionStore.advanceScanCursors",
1027
1176
  )(function* (input) {
1028
1177
  const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
1178
+
1029
1179
  yield* transact(
1030
1180
  Effect.gen(function* () {
1031
1181
  yield* failpoint.hit("subscription:advance-scan-cursors:before");
@@ -1071,7 +1221,9 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
1071
1221
 
1072
1222
  const nextDeadline = Effect.gen(function* () {
1073
1223
  const cursors = yield* readScanCursors;
1224
+
1074
1225
  if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
1226
+
1075
1227
  return yield* indexedDeadline;
1076
1228
  });
1077
1229
 
@@ -1079,6 +1231,7 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
1079
1231
  yield* transactions.run((replace) => replaceAlarm(replace, deadlineAtMillis));
1080
1232
  yield* failpoint.hit("subscription:prearm:after");
1081
1233
  });
1234
+
1082
1235
  const reconcile = Effect.gen(function* () {
1083
1236
  yield* transactions.run((replace) =>
1084
1237
  Effect.gen(function* () {