@yielded/agent-storage-memory 0.1.0-beta.169 → 0.1.0-beta.171

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.
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { compareScheduleNames } from "@yielded/agent/schedule-transition";
3
3
  import { Clock, Effect, Layer, Ref, Result, Schema } from "effect";
4
- import { AcceptedEvent, DeliveryChange, SourcePartition, SubscriptionChange, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionRetentionPolicy, SubscriptionScanCursors, SubscriptionStore, subscriptionDeliveryKeyString, subscriptionKeyString } from "@yielded/agent/subscription";
4
+ import { AcceptedEvent, DeliveryChange, SourcePartition, SubscriptionChange, SubscriptionDelivery, SubscriptionDeliveryKey, SubscriptionError, SubscriptionFailpoint, SubscriptionKey, SubscriptionLimits, SubscriptionName, SubscriptionRecord, SubscriptionRetentionPolicy, SubscriptionScanCursors, SubscriptionStore, defaultSubscriptionLimits, subscriptionDeliveryKeyString, subscriptionKeyString } from "@yielded/agent/subscription";
5
5
  import { Digest } from "@yielded/agent/records";
6
- import { applySubscriptionChange, applySubscriptionDeliveryChange, sameAcceptedEventIdentity, subscriptionCanSelect, subscriptionDeliveryCanSelect, validateEventRetention } from "@yielded/agent/subscription-transition";
6
+ import { applySubscriptionChange, applySubscriptionDeliveryChange, sameAcceptedEventIdentity, subscriptionCanSelect, subscriptionDeliveryCanSelect, subscriptionDeliveryUsesCapacity, subscriptionUsesCapacity, validateEventRetention } from "@yielded/agent/subscription-transition";
7
7
  //#region src/MemorySubscriptionStore.ts
8
8
  var MemorySubscriptionStore_exports = /* @__PURE__ */ __exportAll({ memorySubscriptionStoreLayer: () => memorySubscriptionStoreLayer });
9
9
  const error = (reason, code) => SubscriptionError.make({
@@ -65,6 +65,41 @@ const insertKey = (keys, key) => {
65
65
  ...keys.slice(at)
66
66
  ];
67
67
  };
68
+ const expiryUpperBound = (expiries, nowMillis) => {
69
+ let low = 0;
70
+ let high = expiries.length;
71
+ while (low < high) {
72
+ const middle = Math.floor((low + high) / 2);
73
+ const expiry = expiries[middle];
74
+ if (expiry !== void 0 && expiry <= nowMillis) low = middle + 1;
75
+ else high = middle;
76
+ }
77
+ return low;
78
+ };
79
+ const registrationCount = (current, nowMillis, ownerId) => {
80
+ const expiries = ownerId === void 0 ? current.registrationExpiries : current.ownerRegistrationExpiries.get(ownerId) ?? [];
81
+ return expiries.length - expiryUpperBound(expiries, nowMillis);
82
+ };
83
+ const registrationExpiriesAfter = (current, before, after) => {
84
+ const expiry = (record) => record !== void 0 && (record.state === "active" || record.state === "paused") ? record.configuration.expiresAtMillis ?? Number.POSITIVE_INFINITY : null;
85
+ const previous = expiry(before);
86
+ const next = expiry(after);
87
+ const replace = (expiries) => {
88
+ if (previous === next) return expiries;
89
+ const updated = [...expiries];
90
+ if (previous !== null) updated.splice(expiryUpperBound(updated, previous) - 1, 1);
91
+ if (next !== null) updated.splice(expiryUpperBound(updated, next), 0, next);
92
+ return updated;
93
+ };
94
+ const owners = new Map(current.ownerRegistrationExpiries);
95
+ const expiries = replace(owners.get(after.key.ownerId) ?? []);
96
+ if (expiries.length === 0) owners.delete(after.key.ownerId);
97
+ else owners.set(after.key.ownerId, expiries);
98
+ return {
99
+ registrationExpiries: replace(current.registrationExpiries),
100
+ ownerRegistrationExpiries: owners
101
+ };
102
+ };
68
103
  const recoveryCountsAfter = (current, next, keys) => {
69
104
  const counts = new Map(current.recoveryCounts);
70
105
  for (const key of keys) {
@@ -94,10 +129,13 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
94
129
  deliveries: /* @__PURE__ */ new Map(),
95
130
  registrationIndex: /* @__PURE__ */ new Map(),
96
131
  candidateIndex: /* @__PURE__ */ new Map(),
97
- ownerRegistrationCounts: /* @__PURE__ */ new Map(),
132
+ registrationExpiries: [],
133
+ ownerRegistrationExpiries: /* @__PURE__ */ new Map(),
98
134
  eventIndex: /* @__PURE__ */ new Map(),
99
135
  deliveryIndex: /* @__PURE__ */ new Map(),
100
136
  ownerDeliveryCounts: /* @__PURE__ */ new Map(),
137
+ liveDeliveryCount: 0,
138
+ pendingEventCount: 0,
101
139
  scanCursors: {
102
140
  events: "",
103
141
  deliveries: "",
@@ -112,6 +150,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
112
150
  const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
113
151
  yield* requirePartition(record.key, "register-partition");
114
152
  yield* failpoint.hit("subscription:register:before");
153
+ const nowMillis = yield* Clock.currentTimeMillis;
115
154
  const result = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
116
155
  const key = subscriptionKeyString(record.key);
117
156
  const existingText = current.registrations.get(key);
@@ -123,9 +162,8 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
123
162
  if (jsonBytes(record.configuration.context) > limits.maxContextBytes) return [Result.fail(error("capacity", "context-bytes")), current];
124
163
  if (jsonBytes(record.configuration.parameters) > limits.maxPayloadBytes) return [Result.fail(error("capacity", "parameters-bytes")), current];
125
164
  if (record.configuration.expiresAtMillis !== null && record.configuration.expiresAtMillis - record.createdAtMillis > limits.maxLifetimeMillis) return [Result.fail(error("capacity", "lifetime")), current];
126
- if (current.registrations.size >= limits.maxRegistrations) return [Result.fail(error("capacity", "registrations")), current];
127
- const ownerCount = current.ownerRegistrationCounts.get(record.key.ownerId) ?? 0;
128
- if (ownerCount >= limits.maxRegistrationsPerOwner) return [Result.fail(error("capacity", "owner-registrations")), current];
165
+ if (subscriptionUsesCapacity(record, nowMillis) && registrationCount(current, nowMillis) >= limits.maxRegistrations) return [Result.fail(error("capacity", "registrations")), current];
166
+ if (subscriptionUsesCapacity(record, nowMillis) && registrationCount(current, nowMillis, record.key.ownerId) >= limits.maxRegistrationsPerOwner) return [Result.fail(error("capacity", "owner-registrations")), current];
129
167
  const assigned = {
130
168
  ...record,
131
169
  ordinal: current.sequence + 1
@@ -145,8 +183,6 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
145
183
  const candidateIndex = new Map(current.candidateIndex);
146
184
  const candidateKey = candidateIndexKey(assigned);
147
185
  candidateIndex.set(candidateKey, [...candidateIndex.get(candidateKey) ?? [], key]);
148
- const ownerRegistrationCounts = new Map(current.ownerRegistrationCounts);
149
- ownerRegistrationCounts.set(assigned.key.ownerId, ownerCount + 1);
150
186
  return [Result.succeed(assigned), {
151
187
  ...current,
152
188
  sequence: assigned.ordinal,
@@ -154,7 +190,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
154
190
  registrationIndex,
155
191
  recoveryCounts: recoveryCountsAfter(current, registrationIndex, [key]),
156
192
  candidateIndex,
157
- ownerRegistrationCounts
193
+ ...registrationExpiriesAfter(current, void 0, assigned)
158
194
  }];
159
195
  })).pipe(Effect.flatMap(Effect.fromResult));
160
196
  yield* failpoint.hit("subscription:register:after");
@@ -178,11 +214,13 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
178
214
  records.sort((a, b) => a.ordinal - b.ordinal);
179
215
  return records.slice(0, limit);
180
216
  });
181
- const change = Effect.fnUntraced(function* (input, expectedRevision, inputChange) {
217
+ const change = Effect.fnUntraced(function* (input, expectedRevision, inputChange, inputLimits = defaultSubscriptionLimits) {
182
218
  const key = yield* requireKey(input, "change-key");
183
219
  const change = yield* validate(SubscriptionChange, inputChange, "change");
220
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "change-limits");
184
221
  yield* validate(Schema.Int.check(Schema.isGreaterThan(0)), expectedRevision, "revision");
185
222
  yield* failpoint.hit("subscription:change:before");
223
+ const nowMillis = yield* Clock.currentTimeMillis;
186
224
  const updated = yield* Effect.uninterruptible(Ref.modify(state, (current) => {
187
225
  const storageKey = subscriptionKeyString(key);
188
226
  const text = current.registrations.get(storageKey);
@@ -191,6 +229,10 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
191
229
  if (Result.isFailure(existing)) return [existing, current];
192
230
  const updated = applySubscriptionChange(existing.success, expectedRevision, change);
193
231
  if (Result.isFailure(updated)) return [updated, current];
232
+ if (!subscriptionUsesCapacity(existing.success, nowMillis) && subscriptionUsesCapacity(updated.success, nowMillis)) {
233
+ if (registrationCount(current, nowMillis) >= limits.maxRegistrations) return [Result.fail(error("capacity", "registrations")), current];
234
+ if (registrationCount(current, nowMillis, key.ownerId) >= limits.maxRegistrationsPerOwner) return [Result.fail(error("capacity", "owner-registrations")), current];
235
+ }
194
236
  const revised = {
195
237
  ...updated.success,
196
238
  ordinal: current.sequence + 1
@@ -218,6 +260,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
218
260
  registrations,
219
261
  registrationIndex,
220
262
  candidateIndex,
263
+ ...registrationExpiriesAfter(current, existing.success, revised),
221
264
  recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey])
222
265
  }];
223
266
  })).pipe(Effect.flatMap(Effect.fromResult));
@@ -263,6 +306,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
263
306
  ...current,
264
307
  registrations,
265
308
  registrationIndex,
309
+ ...registrationExpiriesAfter(current, decoded.success, cancelled),
266
310
  recoveryCounts: recoveryCountsAfter(current, registrationIndex, [storageKey])
267
311
  }];
268
312
  })).pipe(Effect.flatMap(Effect.fromResult));
@@ -286,7 +330,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
286
330
  const horizon = validateEventRetention(event, limits, currentTimeMillis);
287
331
  if (Result.isFailure(horizon)) return [Result.fail(horizon.failure), current];
288
332
  if (jsonBytes(event.payload) > limits.maxPayloadBytes) return [Result.fail(error("capacity", "payload-bytes")), current];
289
- if (current.events.size - current.tombstoneCount >= limits.maxEvents) return [Result.fail(error("capacity", "events")), current];
333
+ if (current.pendingEventCount >= limits.maxEvents) return [Result.fail(error("capacity", "events")), current];
290
334
  const accepted = {
291
335
  ...event,
292
336
  cutoff: current.sequence + 1,
@@ -308,6 +352,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
308
352
  sequence: accepted.cutoff,
309
353
  events,
310
354
  eventIndex,
355
+ pendingEventCount: current.pendingEventCount + 1,
311
356
  eventKeys: insertKey(current.eventKeys, accepted.eventId),
312
357
  retentionHorizon: limits.retention?.replayHorizonMillis ?? current.retentionHorizon,
313
358
  retentionDeadline: limits.retention === void 0 ? current.retentionDeadline : accepted.acceptedAtMillis
@@ -347,6 +392,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
347
392
  const select = Effect.fnUntraced(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
348
393
  const suppliedEvent = yield* validate(AcceptedEvent, inputEvent, "select-event");
349
394
  const deliveries = yield* validate(Schema.Array(SubscriptionDelivery), inputDeliveries, "select-deliveries");
395
+ if (new Set(deliveries.map((delivery) => subscriptionDeliveryKeyString(delivery.key))).size !== deliveries.length) return yield* error("validation", "duplicate-delivery");
350
396
  const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
351
397
  yield* requirePartition(suppliedEvent, "select-partition");
352
398
  yield* failpoint.hit("subscription:select:before");
@@ -365,6 +411,10 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
365
411
  const additionIndex = [];
366
412
  const registrationUpdates = [];
367
413
  const owners = new Map(current.ownerDeliveryCounts);
414
+ let registrationCapacity = {
415
+ registrationExpiries: current.registrationExpiries,
416
+ ownerRegistrationExpiries: current.ownerRegistrationExpiries
417
+ };
368
418
  for (const delivery of deliveries) {
369
419
  const recordText = current.registrations.get(subscriptionKeyString(delivery.key.subscription));
370
420
  if (recordText === void 0) return [Result.fail(error("not-found", "subscription")), current];
@@ -413,9 +463,10 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
413
463
  recoveryAt: null,
414
464
  recoveryKey: null
415
465
  }]);
466
+ registrationCapacity = registrationExpiriesAfter(registrationCapacity, record.success, consumed);
416
467
  }
417
468
  }
418
- if (current.deliveries.size + additions.length > limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
469
+ if (current.liveDeliveryCount + additions.length > limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
419
470
  const registrations = new Map(current.registrations);
420
471
  for (const [key, value] of updates) registrations.set(key, value);
421
472
  const nextDeliveries = new Map(current.deliveries);
@@ -450,9 +501,12 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
450
501
  events,
451
502
  eventIndex,
452
503
  deliveries: nextDeliveries,
504
+ liveDeliveryCount: current.liveDeliveryCount + additions.length,
505
+ pendingEventCount: current.pendingEventCount - Number(complete),
453
506
  deliveryKeys: additions.reduce((keys, [key]) => insertKey(keys, key), current.deliveryKeys),
454
507
  deliveryIndex,
455
- ownerDeliveryCounts: owners
508
+ ownerDeliveryCounts: owners,
509
+ ...registrationCapacity
456
510
  }];
457
511
  })).pipe(Effect.flatMap(Effect.fromResult));
458
512
  yield* failpoint.hit("subscription:select:after");
@@ -481,7 +535,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
481
535
  return sameDeliveryIdentity(existing.success, delivery) ? [Result.void, current] : [Result.fail(error("conflict", "delivery-identity")), current];
482
536
  }
483
537
  if (!subscriptionCanSelect(record.success, accepted.success, effectiveNowMillis, true)) return [Result.fail(error("conflict", "catch-up-eligibility")), current];
484
- if (current.deliveries.size >= limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
538
+ if (current.liveDeliveryCount >= limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
485
539
  const ownerCount = current.ownerDeliveryCounts.get(record.success.key.ownerId) ?? 0;
486
540
  if (ownerCount >= limits.maxDeliveriesPerOwner) return [Result.fail(error("capacity", "owner-deliveries")), current];
487
541
  const encodedDelivery = encode(SubscriptionDelivery, delivery, "catch-up-delivery-encode");
@@ -523,6 +577,8 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
523
577
  deliveryIndex,
524
578
  deliveryKeys: insertKey(current.deliveryKeys, key),
525
579
  ownerDeliveryCounts,
580
+ ...registrationExpiriesAfter(current, record.success, consumed),
581
+ liveDeliveryCount: current.liveDeliveryCount + 1,
526
582
  registrations,
527
583
  registrationIndex,
528
584
  recoveryCounts: recoveryCountsAfter(current, registrationIndex, [consumedKey]),
@@ -586,10 +642,11 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
586
642
  items.sort((a, b) => compareScheduleNames(subscriptionDeliveryKeyString(a.key), subscriptionDeliveryKeyString(b.key)));
587
643
  return items.slice(0, limit);
588
644
  });
589
- const changeDelivery = Effect.fnUntraced(function* (inputKey, inputDeliveryId, inputChange) {
645
+ const changeDelivery = Effect.fnUntraced(function* (inputKey, inputDeliveryId, inputChange, inputLimits = defaultSubscriptionLimits) {
590
646
  const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
591
647
  const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
592
648
  const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
649
+ const limits = yield* validate(SubscriptionLimits, inputLimits, "change-delivery-limits");
593
650
  yield* requirePartition(key.subscription, "change-delivery-partition");
594
651
  yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
595
652
  const effectiveChange = change._tag === "Prepare" ? {
@@ -610,6 +667,13 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
610
667
  if (Result.isFailure(transition)) return [Result.fail(transition.failure), current];
611
668
  if (transition.success === decoded.success) return [Result.succeed(decoded.success), current];
612
669
  const updated = transition.success;
670
+ const capacityDelta = Number(subscriptionDeliveryUsesCapacity(updated)) - Number(subscriptionDeliveryUsesCapacity(decoded.success));
671
+ const ownerId = key.subscription.ownerId;
672
+ const ownerCount = current.ownerDeliveryCounts.get(ownerId) ?? 0;
673
+ if (capacityDelta > 0) {
674
+ if (current.liveDeliveryCount >= limits.maxDeliveries) return [Result.fail(error("capacity", "deliveries")), current];
675
+ if (ownerCount >= limits.maxDeliveriesPerOwner) return [Result.fail(error("capacity", "owner-deliveries")), current];
676
+ }
613
677
  const encoded = encode(SubscriptionDelivery, updated, "change-delivery-encode");
614
678
  if (Result.isFailure(encoded)) return [Result.fail(encoded.failure), current];
615
679
  const deliveries = new Map(current.deliveries);
@@ -624,10 +688,15 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
624
688
  observeSettlement: updated.observeSettlement ?? false,
625
689
  nextAttemptAtMillis: updated.retry.nextAttemptAtMillis
626
690
  });
691
+ const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
692
+ if (ownerCount + capacityDelta === 0) ownerDeliveryCounts.delete(ownerId);
693
+ else ownerDeliveryCounts.set(ownerId, ownerCount + capacityDelta);
627
694
  return [Result.succeed(updated), {
628
695
  ...current,
629
696
  deliveries,
630
- deliveryIndex
697
+ deliveryIndex,
698
+ ownerDeliveryCounts,
699
+ liveDeliveryCount: current.liveDeliveryCount + capacityDelta
631
700
  }];
632
701
  })).pipe(Effect.flatMap(Effect.fromResult));
633
702
  yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
@@ -717,6 +786,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
717
786
  const deliveryIndex = new Map(current.deliveryIndex);
718
787
  const eventDeliveryCounts = new Map(current.eventDeliveryCounts);
719
788
  const ownerDeliveryCounts = new Map(current.ownerDeliveryCounts);
789
+ let liveDeliveryCount = current.liveDeliveryCount;
720
790
  const deliveryPage = current.deliveryKeys.slice(upperBound(current.deliveryKeys, current.maintenanceDeliveries), upperBound(current.deliveryKeys, current.maintenanceDeliveries) + limit);
721
791
  const eventPage = current.eventKeys.slice(upperBound(current.eventKeys, current.maintenanceEvents), upperBound(current.eventKeys, current.maintenanceEvents) + limit);
722
792
  const cutoff = nowMillis - policy.completedRetentionMillis;
@@ -755,8 +825,13 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
755
825
  removedDeliveries.add(key);
756
826
  deliveryIndex.delete(key);
757
827
  eventDeliveryCounts.set(event.eventId, (eventDeliveryCounts.get(event.eventId) ?? 1) - 1);
758
- const owner = delivery.key.subscription.ownerId;
759
- ownerDeliveryCounts.set(owner, (ownerDeliveryCounts.get(owner) ?? 1) - 1);
828
+ if (subscriptionDeliveryUsesCapacity(delivery)) {
829
+ liveDeliveryCount--;
830
+ const owner = delivery.key.subscription.ownerId;
831
+ const count = (ownerDeliveryCounts.get(owner) ?? 1) - 1;
832
+ if (count === 0) ownerDeliveryCounts.delete(owner);
833
+ else ownerDeliveryCounts.set(owner, count);
834
+ }
760
835
  }
761
836
  let tombstones = current.tombstoneCount;
762
837
  for (const key of eventPage) {
@@ -803,6 +878,7 @@ const makeMemorySubscriptionStore = Effect.fnUntraced(function* (ownedPartition)
803
878
  deliveryIndex,
804
879
  eventDeliveryCounts,
805
880
  ownerDeliveryCounts,
881
+ liveDeliveryCount,
806
882
  eventKeys: removeKeys(current.eventKeys, removedEvents),
807
883
  deliveryKeys: removeKeys(current.deliveryKeys, removedDeliveries),
808
884
  tombstoneCount: tombstones,