@relaybus/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,72 +1,14 @@
1
+ import { ConfigurationError, assertEventType, NonRetryableError, LifecycleError, DrainIncompleteError, toErrorInfo, schedule, TransactionRequiredError, validatePayload, keyFromDefinition, assertPositiveInteger, DispatcherStoppedError, AttemptsExhaustedError, LeaseLostError, HandlerTimeoutError, TransactionTimeoutError, InvalidEnvelopeError } from './chunk-WS274DL3.js';
2
+ export { AttemptsExhaustedError, ConfigurationError, DispatcherStoppedError, DrainIncompleteError, EventValidationError, HandlerTimeoutError, InvalidEnvelopeError, LeaseLostError, LifecycleError, NonRetryableError, RelayBusError, TransactionRequiredError, TransactionTimeoutError, assertEventType, defineEvent, toErrorInfo, validatePayload } from './chunk-WS274DL3.js';
1
3
  import { randomBytes } from 'crypto';
2
- import { hostname } from 'os';
3
4
  import { AsyncLocalStorage } from 'async_hooks';
5
+ import { hostname } from 'os';
4
6
 
5
7
  // src/clock.ts
6
8
  var systemClock = {
7
9
  now: () => /* @__PURE__ */ new Date()
8
10
  };
9
11
 
10
- // src/errors.ts
11
- var RelayBusError = class extends Error {
12
- // Explicit names survive minification, unlike `new.target.name`.
13
- name = "RelayBusError";
14
- };
15
- var ConfigurationError = class extends RelayBusError {
16
- name = "ConfigurationError";
17
- };
18
- var TransactionRequiredError = class extends RelayBusError {
19
- name = "TransactionRequiredError";
20
- };
21
- var NonRetryableError = class extends RelayBusError {
22
- name = "NonRetryableError";
23
- };
24
- var EventValidationError = class extends NonRetryableError {
25
- constructor(message, issues) {
26
- super(message);
27
- this.issues = issues;
28
- }
29
- issues;
30
- name = "EventValidationError";
31
- };
32
- var InvalidEnvelopeError = class extends NonRetryableError {
33
- name = "InvalidEnvelopeError";
34
- };
35
- var LeaseLostError = class extends RelayBusError {
36
- name = "LeaseLostError";
37
- /**
38
- * Whether the store said so (a fenced write returned false). A worker that only noticed
39
- * locally that its lease ran out may still own the delivery in the store and has to give it back.
40
- */
41
- storeConfirmed;
42
- constructor(message, options = {}) {
43
- super(message);
44
- this.storeConfirmed = options.storeConfirmed ?? false;
45
- }
46
- };
47
- var HandlerTimeoutError = class extends RelayBusError {
48
- name = "HandlerTimeoutError";
49
- };
50
- var DispatcherStoppedError = class extends RelayBusError {
51
- name = "DispatcherStoppedError";
52
- };
53
- function toErrorInfo(error) {
54
- if (error instanceof Error) {
55
- return { name: error.name, message: error.message, stack: error.stack };
56
- }
57
- return { name: "NonError", message: safeStringify(error) };
58
- }
59
- function safeStringify(value) {
60
- if (typeof value === "string") {
61
- return value;
62
- }
63
- try {
64
- return JSON.stringify(value) ?? String(value);
65
- } catch {
66
- return String(value);
67
- }
68
- }
69
-
70
12
  // src/duration.ts
71
13
  var MILLISECONDS_PER_UNIT = {
72
14
  ms: 1,
@@ -108,46 +50,6 @@ function toTimerMilliseconds(value, name) {
108
50
  return milliseconds;
109
51
  }
110
52
 
111
- // src/event.ts
112
- var EVENT_TYPE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
113
- function assertEventType(type) {
114
- if (!EVENT_TYPE_PATTERN.test(type)) {
115
- throw new ConfigurationError(
116
- `Event type '${type}' must be 1-255 characters of letters, digits, '.', '_', ':', '/' or '-', starting with a letter or digit`
117
- );
118
- }
119
- }
120
- function defineEvent(type, options) {
121
- assertEventType(type);
122
- const schema = options?.schema;
123
- return class DefinedEvent {
124
- constructor(payload) {
125
- this.payload = payload;
126
- }
127
- payload;
128
- static type = type;
129
- static schema = schema;
130
- type = type;
131
- };
132
- }
133
- async function validatePayload(type, schema, payload) {
134
- if (schema === void 0) {
135
- return payload;
136
- }
137
- const result = await schema["~standard"].validate(payload);
138
- if (result.issues !== void 0) {
139
- const issues = result.issues.map((issue) => ({
140
- message: issue.message,
141
- path: issue.path?.map((segment) => typeof segment === "object" ? segment.key : segment)
142
- }));
143
- throw new EventValidationError(
144
- `Payload of '${type}' failed validation: ${issues.map((issue) => issue.message).join("; ")}`,
145
- issues
146
- );
147
- }
148
- return result.value;
149
- }
150
-
151
53
  // src/catalog.ts
152
54
  var EVENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
153
55
  var descriptions = /* @__PURE__ */ new WeakMap();
@@ -232,7 +134,10 @@ function requireCatalogEntry(catalog, event) {
232
134
  }
233
135
  return entry;
234
136
  }
235
- function resolveCatalogHandlers(catalog, handlers, options) {
137
+ function resolveCatalogHandlers(catalog, {
138
+ handlers,
139
+ options
140
+ }) {
236
141
  const { name } = describeCatalog(catalog);
237
142
  const { idPrefix, events, ...subscribeOptions } = options;
238
143
  const entries = selectCatalogEntries(catalog, events);
@@ -262,7 +167,7 @@ function classNameOf(handlers) {
262
167
  const className = handlers.constructor.name;
263
168
  return className === "Object" || className === "" ? void 0 : className;
264
169
  }
265
- function assertExactHandlers(catalog, entries, handlers) {
170
+ function assertExactHandlers(catalog, { entries, handlers }) {
266
171
  const expected = new Set(entries.map((entry) => entry.handlerName));
267
172
  const present = Object.keys(handlers);
268
173
  const missing = [...expected].filter((handlerName) => !present.includes(handlerName));
@@ -282,7 +187,10 @@ function assertExactHandlers(catalog, entries, handlers) {
282
187
  }
283
188
  }
284
189
  function defineHandlers(catalog, handlers, options = {}) {
285
- assertExactHandlers(catalog, selectCatalogEntries(catalog, options.events), handlers);
190
+ assertExactHandlers(catalog, {
191
+ entries: selectCatalogEntries(catalog, options.events),
192
+ handlers
193
+ });
286
194
  return handlers;
287
195
  }
288
196
 
@@ -336,12 +244,12 @@ var CatalogPublisher = class {
336
244
  // src/catalog-coverage.ts
337
245
  var declarations = /* @__PURE__ */ new WeakSet();
338
246
  function consume(catalog, options = {}) {
339
- return declare(catalog, "consume", options);
247
+ return declare(catalog, { role: "consume", options });
340
248
  }
341
249
  function produce(catalog, options = {}) {
342
- return declare(catalog, "produce", options);
250
+ return declare(catalog, { role: "produce", options });
343
251
  }
344
- function declare(catalog, role, options) {
252
+ function declare(catalog, { role, options }) {
345
253
  const entries = selectCatalogEntries(catalog, options.events);
346
254
  const declaration = Object.freeze({
347
255
  catalog,
@@ -369,7 +277,7 @@ function assertCatalogsCovered(requirements, registrationsOf) {
369
277
  let onlyBareCatalogs = true;
370
278
  for (const { catalogName, entries, need } of requirements) {
371
279
  for (const entry of entries) {
372
- const problem = describeGap(entry, need, registrationsOf(entry.type));
280
+ const problem = describeGap(entry, { need, registrations: registrationsOf(entry.type) });
373
281
  if (problem !== void 0) {
374
282
  problems.add(`${catalogName === void 0 ? "" : `${catalogName}.`}${problem}`);
375
283
  onlyBareCatalogs &&= need === "handler-or-route";
@@ -384,7 +292,10 @@ ${[...problems].map((line) => ` - ${line}`).join("\n")}`
384
292
  );
385
293
  }
386
294
  }
387
- function describeGap(entry, need, { handlers, routes }) {
295
+ function describeGap(entry, {
296
+ need,
297
+ registrations: { handlers, routes }
298
+ }) {
388
299
  const subject = `${entry.name} (${entry.type})`;
389
300
  if (need === "handler-or-route") {
390
301
  return handlers + routes > 0 ? void 0 : `${subject}: expected handler method ${entry.handlerName}`;
@@ -410,20 +321,26 @@ var DELIVERY_STATUSES = [
410
321
  "completed",
411
322
  "dead"
412
323
  ];
324
+ var UUID_BYTES = 16;
325
+ var TIMESTAMP_BYTES = 6;
326
+ var BYTE_VALUES = 256;
327
+ var LOW_NIBBLE = 15;
328
+ var VERSION_7 = 112;
329
+ var VARIANT_PAYLOAD = 63;
330
+ var RFC_9562_VARIANT = 128;
413
331
  function createUuidV7(timestamp = Date.now()) {
414
- const bytes = randomBytes(16);
332
+ const bytes = randomBytes(UUID_BYTES);
415
333
  let remaining = timestamp;
416
- for (let index = 5; index >= 0; index -= 1) {
417
- bytes[index] = remaining % 256;
418
- remaining = Math.floor(remaining / 256);
334
+ for (let index = TIMESTAMP_BYTES - 1; index >= 0; index -= 1) {
335
+ bytes[index] = remaining % BYTE_VALUES;
336
+ remaining = Math.floor(remaining / BYTE_VALUES);
419
337
  }
420
- bytes[6] = bytes[6] & 15 | 112;
421
- bytes[8] = bytes[8] & 63 | 128;
422
- const hex = bytes.toString("hex");
423
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
338
+ bytes[6] = bytes[6] & LOW_NIBBLE | VERSION_7;
339
+ bytes[8] = bytes[8] & VARIANT_PAYLOAD | RFC_9562_VARIANT;
340
+ return bytes.toString("hex").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5");
424
341
  }
425
342
 
426
- // src/in-memory-event-store.ts
343
+ // src/in-memory-transaction.ts
427
344
  var InMemoryTransaction = class {
428
345
  #operations = [];
429
346
  #finishers = [];
@@ -458,6 +375,9 @@ var InMemoryTransaction = class {
458
375
  }
459
376
  }
460
377
  };
378
+
379
+ // src/in-memory-event-store.ts
380
+ var DEFAULT_LIST_LIMIT = 100;
461
381
  var InMemoryEventStore = class {
462
382
  #events = /* @__PURE__ */ new Map();
463
383
  #deliveries = /* @__PURE__ */ new Map();
@@ -487,6 +407,7 @@ var InMemoryEventStore = class {
487
407
  throw error;
488
408
  }
489
409
  }
410
+ // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
490
411
  async append(transaction, entries, now) {
491
412
  await this.#lock(
492
413
  transaction,
@@ -511,50 +432,63 @@ var InMemoryEventStore = class {
511
432
  appendedEventIds.push(eventId);
512
433
  pendingEventIds.add(eventId);
513
434
  }
514
- const newDeliveries = entry.deliveries.filter((delivery) => {
515
- const deliveryKey = `${eventId}\0${delivery.handler}`;
516
- if (pendingDeliveryKeys.has(deliveryKey) || this.#events.get(eventId)?.handlers.has(delivery.handler) === true) {
517
- return false;
518
- }
519
- pendingDeliveryKeys.add(deliveryKey);
520
- return true;
521
- });
435
+ const newDeliveries = this.#newDeliveries(entry, pendingDeliveryKeys);
522
436
  appendedDeliveryCount += newDeliveries.length;
523
437
  transaction.enqueue(() => {
524
- if (!isDuplicate) {
525
- this.#events.set(eventId, {
526
- envelope: serialized,
527
- sequence: this.#nextSequence(serialized.key),
528
- createdAt: now,
529
- handlers: /* @__PURE__ */ new Set(),
530
- completedAt: null
531
- });
532
- }
533
- const stored = this.#events.get(eventId);
534
- for (const delivery of newDeliveries) {
535
- stored.handlers.add(delivery.handler);
536
- this.#deliveries.set(delivery.id, {
537
- id: delivery.id,
538
- eventId,
539
- handler: delivery.handler,
540
- key: stored.envelope.key,
541
- sequence: stored.sequence,
542
- status: "pending",
543
- attempts: 0,
544
- availableAt: delivery.availableAt,
545
- leaseOwner: null,
546
- leaseToken: null,
547
- leaseExpiresAt: null,
548
- lastError: null,
549
- createdAt: now,
550
- updatedAt: now,
551
- completedAt: null
552
- });
553
- }
438
+ this.#applyAppend({ eventId, serialized, isDuplicate, newDeliveries, now });
554
439
  });
555
440
  }
556
441
  return { appendedEventIds, duplicateEventIds, appendedDeliveryCount };
557
442
  }
443
+ /**
444
+ * The deliveries of `entry` whose handler the event neither has nor gets earlier in the
445
+ * transaction; remembers them in `pendingDeliveryKeys`.
446
+ */
447
+ #newDeliveries(entry, pendingDeliveryKeys) {
448
+ const eventId = entry.event.id;
449
+ return entry.deliveries.filter((delivery) => {
450
+ const deliveryKey = `${eventId}\0${delivery.handler}`;
451
+ if (pendingDeliveryKeys.has(deliveryKey) || this.#events.get(eventId)?.handlers.has(delivery.handler) === true) {
452
+ return false;
453
+ }
454
+ pendingDeliveryKeys.add(deliveryKey);
455
+ return true;
456
+ });
457
+ }
458
+ /** Applies one entry of an append when its transaction commits. */
459
+ #applyAppend(input) {
460
+ const { eventId, serialized, isDuplicate, newDeliveries, now } = input;
461
+ if (!isDuplicate) {
462
+ this.#events.set(eventId, {
463
+ envelope: serialized,
464
+ sequence: this.#nextSequence(serialized.key),
465
+ createdAt: now,
466
+ handlers: /* @__PURE__ */ new Set(),
467
+ completedAt: null
468
+ });
469
+ }
470
+ const stored = this.#events.get(eventId);
471
+ for (const delivery of newDeliveries) {
472
+ stored.handlers.add(delivery.handler);
473
+ this.#deliveries.set(delivery.id, {
474
+ id: delivery.id,
475
+ eventId,
476
+ handler: delivery.handler,
477
+ key: stored.envelope.key,
478
+ sequence: stored.sequence,
479
+ status: "pending",
480
+ attempts: 0,
481
+ availableAt: delivery.availableAt,
482
+ leaseOwner: null,
483
+ leaseToken: null,
484
+ leaseExpiresAt: null,
485
+ lastError: null,
486
+ createdAt: now,
487
+ updatedAt: now,
488
+ completedAt: null
489
+ });
490
+ }
491
+ }
558
492
  async claim(request) {
559
493
  const handlers = new Set(request.handlers);
560
494
  const due = [...this.#deliveries.values()].filter((delivery) => handlers.has(delivery.handler) && isDue(delivery, request.now)).sort(compareDue);
@@ -583,12 +517,14 @@ var InMemoryEventStore = class {
583
517
  }
584
518
  return claimed;
585
519
  }
520
+ // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
586
521
  async extendLease(lease, leaseExpiresAt, now) {
587
522
  return this.#updateLeased(lease, (delivery) => {
588
523
  delivery.leaseExpiresAt = leaseExpiresAt;
589
524
  delivery.updatedAt = now;
590
525
  });
591
526
  }
527
+ // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
592
528
  async complete(lease, now, transaction) {
593
529
  if (transaction === void 0) {
594
530
  return this.#updateLeased(lease, (delivery) => this.#finish(delivery, now));
@@ -607,6 +543,7 @@ var InMemoryEventStore = class {
607
543
  async completeMany(leases, now) {
608
544
  return Promise.all(leases.map((lease) => this.complete(lease, now)));
609
545
  }
546
+ // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
610
547
  async reschedule(lease, availableAt, error, now) {
611
548
  return this.#updateLeased(lease, (delivery) => {
612
549
  delivery.status = "pending";
@@ -625,6 +562,7 @@ var InMemoryEventStore = class {
625
562
  clearLease(delivery);
626
563
  });
627
564
  }
565
+ // eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
628
566
  async bury(lease, error, now) {
629
567
  return this.#updateLeased(lease, (delivery) => {
630
568
  delivery.status = "dead";
@@ -634,8 +572,10 @@ var InMemoryEventStore = class {
634
572
  });
635
573
  }
636
574
  async listDeliveries(query) {
637
- const limit = query.limit ?? 100;
638
- return [...this.#deliveries.values()].filter((delivery) => matchesQuery(delivery, this.#events.get(delivery.eventId), query)).sort(
575
+ const limit = query.limit ?? DEFAULT_LIST_LIMIT;
576
+ return [...this.#deliveries.values()].filter(
577
+ (delivery) => matchesQuery(delivery, { event: this.#events.get(delivery.eventId), query })
578
+ ).sort(
639
579
  (left, right) => left.createdAt.getTime() - right.createdAt.getTime() || compareIds(left.id, right.id)
640
580
  ).slice(0, limit).map((delivery) => ({
641
581
  id: delivery.id,
@@ -677,12 +617,14 @@ var InMemoryEventStore = class {
677
617
  deletedDeliveries += 1;
678
618
  }
679
619
  }
620
+ return { deletedDeliveries, deletedEvents: this.#purgeEvents(request, touchedEventIds) };
621
+ }
622
+ /** Deletes the events no delivery references whose retention ran out; returns how many. */
623
+ #purgeEvents(request, touchedEventIds) {
680
624
  const referencedEventIds = new Set(
681
625
  [...this.#deliveries.values()].map((delivery) => delivery.eventId)
682
626
  );
683
- const cutoffs = [request.completedBefore, request.deadBefore].filter((cutoff) => cutoff !== void 0).map((cutoff) => cutoff.getTime());
684
- const orphanCutoff = cutoffs.length === 0 ? Number.NEGATIVE_INFINITY : Math.min(...cutoffs);
685
- const completedCutoff = request.completedBefore?.getTime() ?? Number.NEGATIVE_INFINITY;
627
+ const { orphanCutoff, completedCutoff } = eventCutoffs(request);
686
628
  let deletedEvents = 0;
687
629
  for (const [eventId, event] of [...this.#events]) {
688
630
  if (referencedEventIds.has(eventId) || this.#locks.has(`event:${eventId}`)) {
@@ -694,7 +636,7 @@ var InMemoryEventStore = class {
694
636
  deletedEvents += 1;
695
637
  }
696
638
  }
697
- return { deletedDeliveries, deletedEvents };
639
+ return deletedEvents;
698
640
  }
699
641
  async statistics(now) {
700
642
  const counts = {
@@ -769,12 +711,7 @@ var InMemoryEventStore = class {
769
711
  return false;
770
712
  }
771
713
  for (const other of this.#deliveries.values()) {
772
- if (other === delivery || other.handler !== delivery.handler || other.key !== delivery.key) {
773
- continue;
774
- }
775
- const unfinishedPredecessor = other.sequence < delivery.sequence && (other.status === "pending" || other.status === "processing");
776
- const runningUnderLiveLease = other.status === "processing" && other.leaseExpiresAt > now;
777
- if (unfinishedPredecessor || runningUnderLiveLease) {
714
+ if (blocksKey(other, { delivery, now })) {
778
715
  return true;
779
716
  }
780
717
  }
@@ -816,6 +753,21 @@ var InMemoryEventStore = class {
816
753
  return { ...copy, occurredAt: new Date(copy.occurredAt) };
817
754
  }
818
755
  };
756
+ function blocksKey(other, { delivery, now }) {
757
+ if (other === delivery || other.handler !== delivery.handler || other.key !== delivery.key) {
758
+ return false;
759
+ }
760
+ const unfinishedPredecessor = other.sequence < delivery.sequence && (other.status === "pending" || other.status === "processing");
761
+ const runningUnderLiveLease = other.status === "processing" && other.leaseExpiresAt > now;
762
+ return unfinishedPredecessor || runningUnderLiveLease;
763
+ }
764
+ function eventCutoffs(request) {
765
+ const cutoffs = [request.completedBefore, request.deadBefore].filter((cutoff) => cutoff !== void 0).map((cutoff) => cutoff.getTime());
766
+ return {
767
+ orphanCutoff: cutoffs.length === 0 ? Number.NEGATIVE_INFINITY : Math.min(...cutoffs),
768
+ completedCutoff: request.completedBefore?.getTime() ?? Number.NEGATIVE_INFINITY
769
+ };
770
+ }
819
771
  function serializeEnvelope(envelope) {
820
772
  return JSON.parse(JSON.stringify(envelope));
821
773
  }
@@ -840,7 +792,7 @@ function clearLease(delivery) {
840
792
  delivery.leaseToken = null;
841
793
  delivery.leaseExpiresAt = null;
842
794
  }
843
- function matchesQuery(delivery, event, query) {
795
+ function matchesQuery(delivery, { event, query }) {
844
796
  return (query.status === void 0 || delivery.status === query.status) && (query.handler === void 0 || delivery.handler === query.handler) && (query.eventType === void 0 || event.envelope.type === query.eventType) && (query.eventId === void 0 || delivery.eventId === query.eventId);
845
797
  }
846
798
  function isPurgeable(delivery, request) {
@@ -853,6 +805,276 @@ function isPurgeable(delivery, request) {
853
805
  return false;
854
806
  }
855
807
 
808
+ // src/tracing.ts
809
+ var TRACEPARENT_HEADER = "traceparent";
810
+ var TRACESTATE_HEADER = "tracestate";
811
+ var TRACEPARENT_FIELDS = "(?!0{32})[\\da-f]{32}-(?!0{16})[\\da-f]{16}-[\\da-f]{2}";
812
+ var TRACEPARENT = new RegExp(
813
+ `^(?:00-${TRACEPARENT_FIELDS}|(?!00|ff)[\\da-f]{2}-${TRACEPARENT_FIELDS}(?:-.*)?)$`
814
+ );
815
+ function traceContextOf(headers) {
816
+ const traceparent = headers[TRACEPARENT_HEADER];
817
+ if (traceparent === void 0 || !TRACEPARENT.test(traceparent)) {
818
+ return void 0;
819
+ }
820
+ const tracestate = headers[TRACESTATE_HEADER];
821
+ return tracestate === void 0 ? { traceparent } : { traceparent, tracestate };
822
+ }
823
+
824
+ // src/bus-observation.ts
825
+ function withTraceHeaders(options, headers) {
826
+ return Object.keys(headers).length === 0 ? options : { ...options, headers: { ...headers, ...options.headers } };
827
+ }
828
+ var BusObservation = class _BusObservation {
829
+ #notifier;
830
+ #tracer;
831
+ #clock;
832
+ #publishes;
833
+ #receives;
834
+ #sends;
835
+ constructor(input) {
836
+ this.#notifier = input.notifier;
837
+ this.#tracer = input.tracer;
838
+ this.#clock = input.clock;
839
+ this.#publishes = input.notifier.wants("onPublishFinished");
840
+ this.#receives = input.notifier.wants("onReceiveFinished");
841
+ this.#sends = input.notifier.wants("onTransportSent");
842
+ }
843
+ /** An observation, or undefined when no observer hook and no tracer would see anything. */
844
+ static create(input) {
845
+ const observed = input.tracer !== void 0 || ["onPublishFinished", "onReceiveFinished", "onTransportSent", "onBacklog"].some(
846
+ (method) => input.notifier.wants(method)
847
+ );
848
+ return observed ? new _BusObservation(input) : void 0;
849
+ }
850
+ /** Runs a publish call inside the tracer's publish span, reporting its duration and outcome. */
851
+ async publish(input, run) {
852
+ const startedAt = this.#publishes ? this.#now() : 0;
853
+ const tracer = this.#tracer;
854
+ const traced = tracer?.publish === void 0 ? run({}) : tracer.publish(input, run);
855
+ if (!this.#publishes) {
856
+ return traced;
857
+ }
858
+ const report = (outcome, error) => {
859
+ this.#notifier.notify("onPublishFinished", {
860
+ eventTypes: input.eventTypes,
861
+ durationMs: this.#now() - startedAt,
862
+ outcome,
863
+ ...error === void 0 ? {} : { error: toErrorInfo(error) }
864
+ });
865
+ };
866
+ try {
867
+ const result = await traced;
868
+ report("success");
869
+ return result;
870
+ } catch (error) {
871
+ report("error", error);
872
+ throw error;
873
+ }
874
+ }
875
+ /** Runs `receive` for one event, reporting where it came from and what became of it. */
876
+ async receive(input, run) {
877
+ if (!this.#receives) {
878
+ return run();
879
+ }
880
+ const startedAt = this.#now();
881
+ try {
882
+ const outcome = await run();
883
+ this.#reportReceive(input, { outcome, startedAt, failure: void 0 });
884
+ return outcome;
885
+ } catch (error) {
886
+ const outcome = error instanceof InvalidEnvelopeError ? "invalid" : "error";
887
+ this.#reportReceive(input, { outcome, startedAt, failure: error });
888
+ throw error;
889
+ }
890
+ }
891
+ #reportReceive({ envelope, origin }, report) {
892
+ const { headers } = envelope;
893
+ this.#notifier.notify("onReceiveFinished", {
894
+ transport: origin?.transport.name,
895
+ system: origin === void 0 ? void 0 : systemOf(origin.transport),
896
+ destination: origin?.destination,
897
+ eventType: textOrUndefined(envelope.type),
898
+ source: textOrUndefined(envelope.source),
899
+ traceContext: isObject(headers) ? traceContextOf(headers) : void 0,
900
+ outcome: report.outcome,
901
+ durationMs: this.#now() - report.startedAt,
902
+ ...report.failure === void 0 ? {} : { error: toErrorInfo(report.failure) }
903
+ });
904
+ }
905
+ /** The handler of a route: sends inside the tracer's send span and reports each send. */
906
+ sender(transport, destination) {
907
+ if (this.#tracer?.send === void 0 && !this.#sends) {
908
+ return (envelope) => transport.publish(envelope, destination);
909
+ }
910
+ return (envelope) => this.#send({ envelope, transport, destination });
911
+ }
912
+ async #send({ envelope, transport, destination }) {
913
+ const startedAt = this.#sends ? this.#now() : 0;
914
+ const system = systemOf(transport);
915
+ const run = (headers) => transport.publish(
916
+ Object.keys(headers).length === 0 ? envelope : { ...envelope, headers: { ...envelope.headers, ...headers } },
917
+ destination
918
+ );
919
+ const tracer = this.#tracer;
920
+ const sent = tracer?.send === void 0 ? run({}) : tracer.send({ envelope, transport: transport.name, system, destination }, run);
921
+ if (!this.#sends) {
922
+ return sent;
923
+ }
924
+ const report = (outcome, error) => {
925
+ this.#notifier.notify("onTransportSent", {
926
+ envelope,
927
+ transport: transport.name,
928
+ system,
929
+ destination,
930
+ durationMs: this.#now() - startedAt,
931
+ outcome,
932
+ ...error === void 0 ? {} : { error: toErrorInfo(error) }
933
+ });
934
+ };
935
+ try {
936
+ await sent;
937
+ report("success");
938
+ } catch (error) {
939
+ report("error", error);
940
+ throw error;
941
+ }
942
+ }
943
+ /**
944
+ * Reads the backlog every `intervalMs` and reports it, when an observer wants it. A reading that
945
+ * is still running when the next one is due is not overlapped. Returns the cancel of the timer.
946
+ */
947
+ startBacklog({ intervalMs, statistics }) {
948
+ if (!this.#notifier.wants("onBacklog")) {
949
+ return void 0;
950
+ }
951
+ let reading = false;
952
+ return schedule(this.#clock, {
953
+ run: () => {
954
+ if (reading) {
955
+ return;
956
+ }
957
+ reading = true;
958
+ statistics().then((result) => this.#reportBacklog(result)).catch((error) => this.#notifier.failure("backlog", error)).finally(() => {
959
+ reading = false;
960
+ });
961
+ },
962
+ delayMs: intervalMs,
963
+ repeat: true,
964
+ unref: true
965
+ });
966
+ }
967
+ #reportBacklog(statistics) {
968
+ const oldestDueAt = statistics.oldestDueAt;
969
+ this.#notifier.notify("onBacklog", {
970
+ pending: statistics.pending,
971
+ oldestPendingAgeMs: oldestDueAt === null ? 0 : Math.max(0, this.#now() - oldestDueAt.getTime()),
972
+ statistics
973
+ });
974
+ }
975
+ #now() {
976
+ return this.#clock.now().getTime();
977
+ }
978
+ };
979
+ function systemOf(transport) {
980
+ return transport.system ?? transport.name;
981
+ }
982
+ function textOrUndefined(value) {
983
+ return typeof value === "string" ? value : void 0;
984
+ }
985
+ function isObject(value) {
986
+ return typeof value === "object" && value !== null;
987
+ }
988
+
989
+ // src/dispatch-observation.ts
990
+ var DispatchObservation = class _DispatchObservation {
991
+ #notifier;
992
+ #tracer;
993
+ #clock;
994
+ #claims;
995
+ #runs;
996
+ constructor(input) {
997
+ this.#notifier = input.notifier;
998
+ this.#tracer = input.tracer;
999
+ this.#clock = input.clock;
1000
+ this.#claims = input.notifier.wants("onClaimFinished");
1001
+ this.#runs = input.notifier.wants("onHandlerStarted") || input.notifier.wants("onHandlerFinished");
1002
+ }
1003
+ /** An observation, or undefined when no observer hook and no tracer would see anything. */
1004
+ static create(input) {
1005
+ const observation = new _DispatchObservation(input);
1006
+ return observation.#claims || observation.#runs || input.tracer?.process !== void 0 ? observation : void 0;
1007
+ }
1008
+ /** Claims through `store`, reporting the query's size, lanes and duration. */
1009
+ async claim(store, request) {
1010
+ if (!this.#claims) {
1011
+ return store.claim(request);
1012
+ }
1013
+ const startedAt = this.#clock.now().getTime();
1014
+ const report = { handlers: request.handlers, requested: request.limit };
1015
+ try {
1016
+ const claimed = await store.claim(request);
1017
+ this.#notifier.notify("onClaimFinished", {
1018
+ ...report,
1019
+ claimed: claimed.length,
1020
+ lanes: new Set(claimed.map((delivery) => delivery.leaseToken)).size,
1021
+ durationMs: this.#clock.now().getTime() - startedAt,
1022
+ outcome: "success"
1023
+ });
1024
+ return claimed;
1025
+ } catch (error) {
1026
+ this.#notifier.notify("onClaimFinished", {
1027
+ ...report,
1028
+ claimed: 0,
1029
+ lanes: 0,
1030
+ durationMs: this.#clock.now().getTime() - startedAt,
1031
+ outcome: "error",
1032
+ error: toErrorInfo(error)
1033
+ });
1034
+ throw error;
1035
+ }
1036
+ }
1037
+ /** Runs a local handler inside the tracer's process span; routes are traced as sends instead. */
1038
+ process(run, handle) {
1039
+ const tracer = this.#tracer;
1040
+ if (tracer?.process === void 0 || run.registration.forwardsToTransport) {
1041
+ return handle();
1042
+ }
1043
+ return tracer.process({ delivery: run.delivery }, async () => handle());
1044
+ }
1045
+ /** Reports the start of a handler run now and its end when `settled` settles, passing it through. */
1046
+ async observe(run, settled) {
1047
+ if (!this.#runs) {
1048
+ await settled;
1049
+ return;
1050
+ }
1051
+ const { delivery } = run;
1052
+ const kind = run.registration.forwardsToTransport ? "route" : "handler";
1053
+ const startedAt = this.#clock.now().getTime();
1054
+ this.#notifier.notify("onHandlerStarted", {
1055
+ delivery,
1056
+ kind,
1057
+ endToEndLatencyMs: startedAt - delivery.event.occurredAt.getTime()
1058
+ });
1059
+ try {
1060
+ await settled;
1061
+ } catch (error) {
1062
+ const durationMs2 = this.#clock.now().getTime() - startedAt;
1063
+ const errorInfo = toErrorInfo(error);
1064
+ this.#notifier.notify("onHandlerFinished", {
1065
+ delivery,
1066
+ kind,
1067
+ durationMs: durationMs2,
1068
+ outcome: "error",
1069
+ error: errorInfo
1070
+ });
1071
+ throw error;
1072
+ }
1073
+ const durationMs = this.#clock.now().getTime() - startedAt;
1074
+ this.#notifier.notify("onHandlerFinished", { delivery, kind, durationMs, outcome: "success" });
1075
+ }
1076
+ };
1077
+
856
1078
  // src/completion-batcher.ts
857
1079
  var CompletionBatcher = class {
858
1080
  #store;
@@ -882,7 +1104,10 @@ var CompletionBatcher = class {
882
1104
  if (this.#closing || this.#pending.length >= this.#settings.maxBatchSize || this.#pending.length >= this.#runningCount()) {
883
1105
  this.#flush();
884
1106
  } else {
885
- this.#timer ??= setTimeout(() => this.#flush(), this.#settings.windowMs);
1107
+ this.#timer ??= schedule(this.#clock, {
1108
+ run: () => this.#flush(),
1109
+ delayMs: this.#settings.windowMs
1110
+ });
886
1111
  }
887
1112
  });
888
1113
  }
@@ -897,7 +1122,7 @@ var CompletionBatcher = class {
897
1122
  }
898
1123
  }
899
1124
  #flush() {
900
- clearTimeout(this.#timer);
1125
+ this.#timer?.();
901
1126
  this.#timer = void 0;
902
1127
  const batch = this.#pending;
903
1128
  this.#pending = [];
@@ -912,6 +1137,57 @@ var CompletionBatcher = class {
912
1137
  );
913
1138
  }
914
1139
  };
1140
+
1141
+ // src/delivery-timers.ts
1142
+ var DeliveryTimers = class {
1143
+ #clock;
1144
+ /** Cancel of every timer still waiting, with what its cancellation must also do. */
1145
+ #waiting = /* @__PURE__ */ new Map();
1146
+ constructor(clock) {
1147
+ this.#clock = clock;
1148
+ }
1149
+ /** Runs `task.run` after `task.delayMs`; returns what cancels it. */
1150
+ start(task) {
1151
+ return this.#start(task, () => void 0);
1152
+ }
1153
+ /** Resolves after `milliseconds`, or at once when {@link DeliveryTimers.cancelAll} runs. */
1154
+ sleep(milliseconds) {
1155
+ return new Promise((resolve) => {
1156
+ this.#start({ run: resolve, delayMs: milliseconds }, resolve);
1157
+ });
1158
+ }
1159
+ cancelAll() {
1160
+ for (const [cancel, onCancel] of [...this.#waiting]) {
1161
+ cancel();
1162
+ onCancel();
1163
+ }
1164
+ }
1165
+ #start(task, onCancel) {
1166
+ const cancel = () => {
1167
+ cancelTimer();
1168
+ this.#waiting.delete(cancel);
1169
+ };
1170
+ const cancelTimer = schedule(this.#clock, {
1171
+ run: () => {
1172
+ this.#waiting.delete(cancel);
1173
+ task.run();
1174
+ },
1175
+ delayMs: task.delayMs
1176
+ });
1177
+ this.#waiting.set(cancel, onCancel);
1178
+ return cancel;
1179
+ }
1180
+ };
1181
+
1182
+ // src/dispatcher-support.ts
1183
+ function pendingKey(delivery) {
1184
+ return `${delivery.deliveryId}\0${delivery.leaseToken}`;
1185
+ }
1186
+ function rejectOnAbort(signal) {
1187
+ return new Promise((_, reject) => {
1188
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
1189
+ });
1190
+ }
915
1191
  var storage = new AsyncLocalStorage();
916
1192
  function runInScope(scope, work) {
917
1193
  return storage.run({ ...storage.getStore(), ...scope }, work);
@@ -923,6 +1199,102 @@ function getCurrentScope() {
923
1199
  return storage.getStore();
924
1200
  }
925
1201
 
1202
+ // src/shutdown-deadline.ts
1203
+ var DRAIN_SHARE = 0.8;
1204
+ var ShutdownDeadline = class {
1205
+ #controller = new AbortController();
1206
+ #drained;
1207
+ #expired;
1208
+ #cancelTimers;
1209
+ constructor(clock, timeoutMs) {
1210
+ let drain;
1211
+ this.#drained = new Promise((resolve) => {
1212
+ drain = () => resolve("timeout");
1213
+ });
1214
+ const signal = this.#controller.signal;
1215
+ this.#expired = new Promise((resolve) => {
1216
+ signal.addEventListener("abort", () => resolve("timeout"), { once: true });
1217
+ });
1218
+ this.#cancelTimers = [
1219
+ schedule(clock, { run: drain, delayMs: Math.floor(timeoutMs * DRAIN_SHARE) }),
1220
+ schedule(clock, { run: () => this.#controller.abort(), delayMs: timeoutMs })
1221
+ ];
1222
+ }
1223
+ /** Aborted when the deadline passes. */
1224
+ get signal() {
1225
+ return this.#controller.signal;
1226
+ }
1227
+ get passed() {
1228
+ return this.#controller.signal.aborted;
1229
+ }
1230
+ /** Waits for `work` until the deadline. Rejects when `work` rejects before it. */
1231
+ race(work) {
1232
+ return Promise.race([work.then(() => "done"), this.#expired]);
1233
+ }
1234
+ /** Waits for running handlers until the drain point, when they are aborted. */
1235
+ raceDrain(work) {
1236
+ return Promise.race([work.then(() => "done"), this.#drained]);
1237
+ }
1238
+ /** Cancels the timers once the shutdown is over. */
1239
+ dispose() {
1240
+ for (const cancel of this.#cancelTimers) {
1241
+ cancel();
1242
+ }
1243
+ }
1244
+ };
1245
+ async function withinDeadline({ clock, timeoutMs }, work) {
1246
+ const startedAt = clock.now().getTime();
1247
+ const deadline = new ShutdownDeadline(clock, timeoutMs);
1248
+ try {
1249
+ const abandonedDeliveries = await work(deadline);
1250
+ return {
1251
+ graceful: !deadline.passed,
1252
+ abandonedDeliveries,
1253
+ durationMs: clock.now().getTime() - startedAt
1254
+ };
1255
+ } finally {
1256
+ deadline.dispose();
1257
+ }
1258
+ }
1259
+
1260
+ // src/lease-heartbeat.ts
1261
+ var RENEWALS_PER_LEASE = 3;
1262
+ function startLeaseHeartbeat({ store, clock, notifier, settings }, { delivery, controller }) {
1263
+ let confirmedExpiry = delivery.leaseExpiresAt.getTime();
1264
+ let renewing = false;
1265
+ const abortLeaseLost = (storeConfirmed = false) => {
1266
+ controller.abort(
1267
+ new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, { storeConfirmed })
1268
+ );
1269
+ };
1270
+ return schedule(clock, {
1271
+ run: () => {
1272
+ const now = clock.now();
1273
+ if (now.getTime() >= confirmedExpiry) {
1274
+ abortLeaseLost();
1275
+ return;
1276
+ }
1277
+ if (renewing) {
1278
+ return;
1279
+ }
1280
+ renewing = true;
1281
+ const expiry = now.getTime() + settings.leaseDurationMs;
1282
+ store.extendLease(delivery, new Date(expiry), now).then((extended) => {
1283
+ if (extended) {
1284
+ confirmedExpiry = expiry;
1285
+ } else {
1286
+ abortLeaseLost(true);
1287
+ }
1288
+ }).catch((error) => notifier.failure("extend-lease", error)).finally(() => {
1289
+ renewing = false;
1290
+ });
1291
+ },
1292
+ delayMs: Math.max(1, Math.floor(settings.leaseDurationMs / RENEWALS_PER_LEASE)),
1293
+ repeat: true,
1294
+ unref: true
1295
+ });
1296
+ }
1297
+
926
1298
  // src/retry-policy.ts
927
1299
  var DEFAULT_RETRY_POLICY = {
928
1300
  attempts: 10,
@@ -942,18 +1314,26 @@ var DEFAULT_ROUTE_RETRY_POLICY = {
942
1314
  };
943
1315
  var JITTER_STRATEGIES = ["full", "equal", "none"];
944
1316
  function createRetryPolicy(options, base = DEFAULT_RETRY_POLICY) {
945
- const initialDelayMs = options?.initialDelay === void 0 ? base.initialDelayMs : toMilliseconds(options.initialDelay, "retry.initialDelay");
946
- const policy = {
947
- attempts: options?.attempts ?? base.attempts,
1317
+ const policy = mergeRetryPolicy(options, base);
1318
+ assertValidRetryPolicy(policy);
1319
+ return policy;
1320
+ }
1321
+ function mergeRetryPolicy(options, base) {
1322
+ const overrides = options ?? {};
1323
+ const initialDelayMs = overrides.initialDelay === void 0 ? base.initialDelayMs : toMilliseconds(overrides.initialDelay, "retry.initialDelay");
1324
+ return {
1325
+ attempts: overrides.attempts ?? base.attempts,
948
1326
  initialDelayMs,
949
- factor: options?.factor ?? base.factor,
950
- maxDelayMs: options?.maxDelay === void 0 ? (
1327
+ factor: overrides.factor ?? base.factor,
1328
+ maxDelayMs: overrides.maxDelay === void 0 ? (
951
1329
  // Raising only the initial delay must not trip the inherited cap.
952
1330
  Math.max(base.maxDelayMs, initialDelayMs)
953
- ) : toMilliseconds(options.maxDelay, "retry.maxDelay"),
954
- jitter: options?.jitter ?? base.jitter,
955
- retryIf: options?.retryIf ?? base.retryIf
1331
+ ) : toMilliseconds(overrides.maxDelay, "retry.maxDelay"),
1332
+ jitter: overrides.jitter ?? base.jitter,
1333
+ retryIf: overrides.retryIf ?? base.retryIf
956
1334
  };
1335
+ }
1336
+ function assertValidRetryPolicy(policy) {
957
1337
  if (policy.attempts !== Number.POSITIVE_INFINITY && (!Number.isInteger(policy.attempts) || policy.attempts < 1)) {
958
1338
  throw new ConfigurationError(
959
1339
  `retry.attempts must be a positive integer or Infinity, received ${policy.attempts}`
@@ -970,7 +1350,6 @@ function createRetryPolicy(options, base = DEFAULT_RETRY_POLICY) {
970
1350
  `retry.jitter must be one of ${JITTER_STRATEGIES.join(", ")}, received '${String(policy.jitter)}'`
971
1351
  );
972
1352
  }
973
- return policy;
974
1353
  }
975
1354
  function computeRetryDelay(policy, failedAttempts, random = Math.random) {
976
1355
  const exponential = policy.initialDelayMs * policy.factor ** (failedAttempts - 1);
@@ -999,11 +1378,17 @@ function isRetryableError(policy, error) {
999
1378
  }
1000
1379
 
1001
1380
  // src/dispatcher.ts
1002
- var COMPLETION_RETRY_DELAYS_MS = [50, 250, 1e3];
1381
+ var FIRST_COMPLETION_RETRY_MS = 50;
1382
+ var SECOND_COMPLETION_RETRY_MS = 250;
1383
+ var LAST_COMPLETION_RETRY_MS = 1e3;
1384
+ var COMPLETION_RETRY_DELAYS_MS = [
1385
+ FIRST_COMPLETION_RETRY_MS,
1386
+ SECOND_COMPLETION_RETRY_MS,
1387
+ LAST_COMPLETION_RETRY_MS
1388
+ ];
1003
1389
  var WAKE_COOLDOWN_MS = 5;
1004
1390
  var QUIET_REFILL_MS = 10;
1005
1391
  var REFILL_FRACTION = 0.7;
1006
- var defaultSleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
1007
1392
  var Dispatcher = class {
1008
1393
  #dependencies;
1009
1394
  #sleep;
@@ -1029,9 +1414,15 @@ var Dispatcher = class {
1029
1414
  #quietRefillTimer;
1030
1415
  #unwatch;
1031
1416
  #watching;
1417
+ /** Set when a stop gave up at its deadline: the store may be closed, so nothing more is started or retried. */
1418
+ #abandoned = false;
1419
+ /** Stops the lease renewal (or transactional lease guard) of each delivery whose handler runs. */
1420
+ #leaseKeepers = /* @__PURE__ */ new Map();
1421
+ #timers;
1032
1422
  constructor(dependencies) {
1033
1423
  this.#dependencies = dependencies;
1034
- this.#sleep = dependencies.sleep ?? defaultSleep;
1424
+ this.#timers = new DeliveryTimers(dependencies.clock);
1425
+ this.#sleep = dependencies.sleep ?? ((milliseconds) => this.#timers.sleep(milliseconds));
1035
1426
  const { store, clock, settings } = dependencies;
1036
1427
  this.#refillThreshold = Math.ceil(
1037
1428
  Math.min(settings.concurrency, settings.batchSize) * REFILL_FRACTION
@@ -1080,10 +1471,13 @@ var Dispatcher = class {
1080
1471
  this.#requestClaim();
1081
1472
  return;
1082
1473
  }
1083
- this.#cooldownTimer ??= setTimeout(() => {
1084
- this.#cooldownTimer = void 0;
1085
- this.#requestClaim();
1086
- }, wait);
1474
+ this.#cooldownTimer ??= schedule(this.#dependencies.clock, {
1475
+ run: () => {
1476
+ this.#cooldownTimer = void 0;
1477
+ this.#requestClaim();
1478
+ },
1479
+ delayMs: wait
1480
+ });
1087
1481
  }
1088
1482
  /** Ends the current wait of the claim loop, or the next one at once, for a claim once `minimumFree` slots are free. */
1089
1483
  #signal(minimumFree) {
@@ -1122,33 +1516,66 @@ var Dispatcher = class {
1122
1516
  }
1123
1517
  }
1124
1518
  /**
1125
- * Stops claiming, waits up to the shutdown timeout for running handlers,
1126
- * then aborts the rest and releases their deliveries for another process.
1519
+ * Stops claiming and waits for running handlers until the drain point of `deadline` (default: the
1520
+ * shutdown timeout from now), then aborts the rest and releases their deliveries for another
1521
+ * process. Deliveries still in flight when the deadline itself passes are abandoned: their leases
1522
+ * expire and they run again elsewhere. Resolves with how many were abandoned.
1127
1523
  */
1128
- async stop() {
1524
+ async stop(deadline) {
1525
+ const { clock, settings } = this.#dependencies;
1526
+ const own = deadline ?? new ShutdownDeadline(clock, settings.shutdownTimeoutMs);
1527
+ try {
1528
+ return await this.#stopWithin(own);
1529
+ } finally {
1530
+ if (deadline === void 0) {
1531
+ own.dispose();
1532
+ }
1533
+ }
1534
+ }
1535
+ async #stopWithin(deadline) {
1129
1536
  if (!this.#running) {
1130
- await this.#closeRecovery(Date.now() + this.#dependencies.settings.shutdownTimeoutMs);
1131
- return;
1537
+ await this.#closeRecovery(deadline);
1538
+ return 0;
1132
1539
  }
1133
1540
  this.#running = false;
1134
1541
  this.#completions?.setClosing(true);
1135
- clearTimeout(this.#cooldownTimer);
1542
+ this.#cooldownTimer?.();
1136
1543
  this.#cooldownTimer = void 0;
1137
- clearTimeout(this.#quietRefillTimer);
1544
+ this.#quietRefillTimer?.();
1138
1545
  this.#quietRefillTimer = void 0;
1139
1546
  this.#signal(this.#refillThreshold);
1140
- const deadline = Date.now() + this.#dependencies.settings.shutdownTimeoutMs;
1141
- await this.#waitUntil(this.#stopWatching(), deadline);
1142
- await this.#waitUntil(this.#loop, deadline);
1547
+ await deadline.race(this.#stopWatching());
1548
+ await deadline.race(this.#loop);
1143
1549
  const settled = Promise.allSettled([...this.#inFlight].map((delivery) => delivery.settled));
1144
- if (await this.#waitUntil(settled, deadline) === "timeout") {
1550
+ if (await deadline.raceDrain(settled) === "timeout") {
1145
1551
  for (const delivery of this.#inFlight) {
1146
1552
  delivery.controller.abort(new DispatcherStoppedError("relaybus dispatcher stopped"));
1147
1553
  }
1148
- await settled;
1554
+ if (await deadline.race(settled) === "timeout") {
1555
+ return this.#abandon();
1556
+ }
1149
1557
  }
1150
1558
  await this.#closeRecovery(deadline);
1151
- await this.#waitUntil(this.#relinquish(), deadline);
1559
+ await deadline.race(this.#relinquish());
1560
+ return 0;
1561
+ }
1562
+ /**
1563
+ * The deadline passed with deliveries still in flight (aborted, their release not answered): forgets
1564
+ * them and every settlement still to retry. Whatever they release or complete from now on may reach a
1565
+ * closed store; a delivery whose state change is lost stays leased until the lease expires and then
1566
+ * runs again (at-least-once).
1567
+ */
1568
+ #abandon() {
1569
+ this.#abandoned = true;
1570
+ for (const stopKeeping of this.#leaseKeepers.values()) {
1571
+ stopKeeping();
1572
+ }
1573
+ this.#leaseKeepers.clear();
1574
+ this.#timers.cancelAll();
1575
+ this.#recoveryTimer?.();
1576
+ this.#recoveryTimer = void 0;
1577
+ this.#pending.clear();
1578
+ return this.#inFlight.size;
1152
1579
  }
1153
1580
  /**
1154
1581
  * Gives the settlements still pending one last try and forgets them. A pass of the timer may be inside the
@@ -1156,13 +1583,13 @@ var Dispatcher = class {
1156
1583
  * further call, since it may be closed right after.
1157
1584
  */
1158
1585
  async #closeRecovery(deadline) {
1159
- clearInterval(this.#recoveryTimer);
1586
+ this.#recoveryTimer?.();
1160
1587
  this.#recoveryTimer = void 0;
1161
1588
  const lastTry = (async () => {
1162
1589
  await this.#recovery;
1163
1590
  await this.#recoverSettlements();
1164
1591
  })();
1165
- await this.#waitUntil(lastTry, deadline);
1592
+ await deadline.race(lastTry);
1166
1593
  this.#pending.clear();
1167
1594
  }
1168
1595
  /**
@@ -1193,12 +1620,16 @@ var Dispatcher = class {
1193
1620
  }
1194
1621
  }
1195
1622
  #retain(entry) {
1623
+ if (this.#abandoned) {
1624
+ return;
1625
+ }
1196
1626
  this.#pending.set(pendingKey(entry.delivery), entry);
1197
- this.#recoveryTimer ??= setInterval(
1198
- () => void this.#recoverSettlements(),
1199
- this.#dependencies.settings.pollIntervalMs
1200
- );
1201
- this.#recoveryTimer.unref();
1627
+ this.#recoveryTimer ??= schedule(this.#dependencies.clock, {
1628
+ run: () => void this.#recoverSettlements(),
1629
+ delayMs: this.#dependencies.settings.pollIntervalMs,
1630
+ repeat: true,
1631
+ unref: true
1632
+ });
1202
1633
  }
1203
1634
  /** Gives lease groups the owner no longer works on back to the store, so other workers need not wait for expiry. */
1204
1635
  async #relinquish() {
@@ -1216,12 +1647,15 @@ var Dispatcher = class {
1216
1647
  return started.length;
1217
1648
  }
1218
1649
  async #waitUntil(work, deadline) {
1219
- let timer;
1650
+ let cancel;
1220
1651
  const timedOut = new Promise((resolve) => {
1221
- timer = setTimeout(() => resolve("timeout"), Math.max(0, deadline - Date.now()));
1652
+ cancel = schedule(this.#dependencies.clock, {
1653
+ run: () => resolve("timeout"),
1654
+ delayMs: Math.max(0, deadline - Date.now())
1655
+ });
1222
1656
  });
1223
1657
  const outcome = await Promise.race([work.then(() => "done"), timedOut]);
1224
- clearTimeout(timer);
1658
+ cancel?.();
1225
1659
  return outcome;
1226
1660
  }
1227
1661
  /**
@@ -1255,12 +1689,15 @@ var Dispatcher = class {
1255
1689
  async #waitForWakeUp() {
1256
1690
  if (this.#signalledMinimum === void 0) {
1257
1691
  await new Promise((resolve) => {
1258
- const timer = setTimeout(() => {
1259
- this.#signalledMinimum = 1;
1260
- resolve();
1261
- }, this.#dependencies.settings.pollIntervalMs);
1692
+ const cancel = schedule(this.#dependencies.clock, {
1693
+ run: () => {
1694
+ this.#signalledMinimum = 1;
1695
+ resolve();
1696
+ },
1697
+ delayMs: this.#dependencies.settings.pollIntervalMs
1698
+ });
1262
1699
  this.#wakeUp = () => {
1263
- clearTimeout(timer);
1700
+ cancel();
1264
1701
  resolve();
1265
1702
  };
1266
1703
  });
@@ -1296,13 +1733,13 @@ var Dispatcher = class {
1296
1733
  });
1297
1734
  }
1298
1735
  } else if (free > 0) {
1299
- this.#quietRefillTimer ??= setTimeout(
1300
- () => {
1736
+ this.#quietRefillTimer ??= schedule(this.#dependencies.clock, {
1737
+ run: () => {
1301
1738
  this.#quietRefillTimer = void 0;
1302
1739
  this.#signal(1);
1303
1740
  },
1304
- Math.max(0, this.#lastClaimStartedAt + QUIET_REFILL_MS - Date.now())
1305
- );
1741
+ delayMs: Math.max(0, this.#lastClaimStartedAt + QUIET_REFILL_MS - Date.now())
1742
+ });
1306
1743
  }
1307
1744
  }
1308
1745
  #freeSlots() {
@@ -1310,23 +1747,24 @@ var Dispatcher = class {
1310
1747
  }
1311
1748
  /** Claims for every handler with room and starts what was claimed. */
1312
1749
  async #claimAndStart() {
1313
- const { store, registry, clock, settings } = this.#dependencies;
1750
+ const { store, registry, clock, settings, observation } = this.#dependencies;
1314
1751
  const started = [];
1315
1752
  let requested = 0;
1316
1753
  for (const request of this.#planClaims(registry.all())) {
1317
1754
  const now = clock.now();
1318
1755
  const wakeGeneration = this.#wakeGeneration;
1319
1756
  this.#lastClaimStartedAt = Date.now();
1320
- clearTimeout(this.#quietRefillTimer);
1757
+ this.#quietRefillTimer?.();
1321
1758
  this.#quietRefillTimer = void 0;
1322
1759
  requested += request.limit;
1323
- const claimed = await store.claim({
1760
+ const claim = {
1324
1761
  handlers: request.handlers,
1325
1762
  limit: request.limit,
1326
1763
  leaseOwner: settings.owner,
1327
1764
  leaseExpiresAt: new Date(now.getTime() + settings.leaseDurationMs),
1328
1765
  now
1329
- });
1766
+ };
1767
+ const claimed = await (observation === void 0 ? store.claim(claim) : observation.claim(store, claim));
1330
1768
  for (const handler of request.handlers) {
1331
1769
  if (claimed.length < request.limit && wakeGeneration === this.#wakeGeneration) {
1332
1770
  this.#exhaustedHandlers.add(handler);
@@ -1334,7 +1772,7 @@ var Dispatcher = class {
1334
1772
  this.#exhaustedHandlers.delete(handler);
1335
1773
  }
1336
1774
  }
1337
- for (const delivery of claimed) {
1775
+ for (const delivery of this.#abandoned ? [] : claimed) {
1338
1776
  started.push(this.#start(delivery));
1339
1777
  }
1340
1778
  }
@@ -1349,22 +1787,25 @@ var Dispatcher = class {
1349
1787
  if (registration.concurrency === null || free === 0) {
1350
1788
  continue;
1351
1789
  }
1352
- const room = registration.concurrency - this.#countInFlight(registration.id);
1790
+ const room = registration.concurrency - this.#countInFlight(registration);
1353
1791
  const limit = Math.min(room, free);
1354
1792
  if (limit > 0) {
1355
- plans.push({ handlers: [registration.id], limit });
1793
+ plans.push({ handlers: [...registration.claimIds], limit });
1356
1794
  free -= limit;
1357
1795
  }
1358
1796
  }
1359
1797
  if (unlimited.length > 0 && free > 0) {
1360
- plans.push({ handlers: unlimited.map((registration) => registration.id), limit: free });
1798
+ plans.push({
1799
+ handlers: unlimited.flatMap((registration) => registration.claimIds),
1800
+ limit: free
1801
+ });
1361
1802
  }
1362
1803
  return plans;
1363
1804
  }
1364
- #countInFlight(handler) {
1805
+ #countInFlight(registration) {
1365
1806
  let count = 0;
1366
1807
  for (const delivery of this.#inFlight) {
1367
- if (delivery.handler === handler) {
1808
+ if (registration.claimIds.includes(delivery.handler)) {
1368
1809
  count += 1;
1369
1810
  }
1370
1811
  }
@@ -1400,7 +1841,7 @@ var Dispatcher = class {
1400
1841
  if (delivery.attempt > registration.retry.attempts) {
1401
1842
  await this.#bury(
1402
1843
  delivery,
1403
- new RelayBusError(
1844
+ new AttemptsExhaustedError(
1404
1845
  `Delivery reached attempt ${delivery.attempt} of ${registration.retry.attempts} after its lease expired`
1405
1846
  )
1406
1847
  );
@@ -1408,8 +1849,9 @@ var Dispatcher = class {
1408
1849
  }
1409
1850
  let stopHeartbeat = () => void 0;
1410
1851
  try {
1411
- stopHeartbeat = registration.transactional ? await this.#guardTransactionalLease(registration, delivery, controller) : this.#startHeartbeat(delivery, controller);
1412
- const outcome = await this.#runHandler(registration, delivery, controller);
1852
+ stopHeartbeat = registration.transactional ? await this.#guardTransactionalLease(registration, { delivery, controller }) : startLeaseHeartbeat(this.#dependencies, { delivery, controller });
1853
+ this.#leaseKeepers.set(delivery, stopHeartbeat);
1854
+ const outcome = await this.#runHandler(registration, { delivery, controller });
1413
1855
  const durationMs = clock.now().getTime() - startedAt;
1414
1856
  if (outcome === "completed") {
1415
1857
  notifier.notify("onDeliverySucceeded", { delivery, durationMs });
@@ -1422,22 +1864,29 @@ var Dispatcher = class {
1422
1864
  }
1423
1865
  return true;
1424
1866
  } catch (error) {
1867
+ if (this.#abandoned) {
1868
+ return false;
1869
+ }
1425
1870
  if (error instanceof LeaseLostError) {
1426
1871
  notifier.notify("onLeaseLost", { delivery });
1427
1872
  if (!error.storeConfirmed) {
1428
- await this.#giveBack(registration, delivery, error);
1873
+ await this.#giveBack(registration, { delivery, error });
1429
1874
  }
1430
1875
  } else if (error instanceof DispatcherStoppedError) {
1431
1876
  await this.#release(delivery);
1432
1877
  } else {
1433
- await this.#fail(registration, delivery, error);
1878
+ await this.#fail(registration, { delivery, error });
1434
1879
  }
1435
1880
  return false;
1436
1881
  } finally {
1437
1882
  stopHeartbeat();
1883
+ this.#leaseKeepers.delete(delivery);
1438
1884
  }
1439
1885
  }
1440
- async #runHandler(registration, delivery, controller) {
1886
+ async #runHandler(registration, {
1887
+ delivery,
1888
+ controller
1889
+ }) {
1441
1890
  const { store, clock } = this.#dependencies;
1442
1891
  const payload = await validatePayload(
1443
1892
  delivery.event.type,
@@ -1447,7 +1896,7 @@ var Dispatcher = class {
1447
1896
  const event = { ...delivery.event, payload };
1448
1897
  if (registration.transactional) {
1449
1898
  await store.transaction(async (transaction) => {
1450
- await this.#invoke(registration, event, delivery, controller, transaction);
1899
+ await this.#invoke({ registration, event, delivery, controller, transaction });
1451
1900
  if (!await store.complete(delivery, clock.now(), transaction)) {
1452
1901
  throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, {
1453
1902
  storeConfirmed: true
@@ -1456,7 +1905,7 @@ var Dispatcher = class {
1456
1905
  });
1457
1906
  return "completed";
1458
1907
  }
1459
- await this.#invoke(registration, event, delivery, controller, void 0);
1908
+ await this.#invoke({ registration, event, delivery, controller, transaction: void 0 });
1460
1909
  return this.#confirmCompletion(delivery);
1461
1910
  }
1462
1911
  /**
@@ -1466,6 +1915,9 @@ var Dispatcher = class {
1466
1915
  async #confirmCompletion(delivery) {
1467
1916
  const { notifier } = this.#dependencies;
1468
1917
  for (let attempt = 0; ; attempt += 1) {
1918
+ if (this.#abandoned) {
1919
+ return "unconfirmed";
1920
+ }
1469
1921
  let completed;
1470
1922
  try {
1471
1923
  completed = await this.#complete(delivery);
@@ -1491,7 +1943,8 @@ var Dispatcher = class {
1491
1943
  const { store, clock } = this.#dependencies;
1492
1944
  return this.#completions === void 0 ? store.complete(delivery, clock.now()) : this.#completions.submit(delivery);
1493
1945
  }
1494
- async #invoke(registration, event, delivery, controller, transaction) {
1946
+ async #invoke(input) {
1947
+ const { registration, event, delivery, controller, transaction } = input;
1495
1948
  const context = {
1496
1949
  deliveryId: delivery.deliveryId,
1497
1950
  handler: delivery.handler,
@@ -1504,28 +1957,37 @@ var Dispatcher = class {
1504
1957
  if (controller.signal.aborted) {
1505
1958
  throw controller.signal.reason;
1506
1959
  }
1960
+ const handle = () => runInIsolatedScope(scope, () => registration.handle(event, context));
1961
+ const { observation } = this.#dependencies;
1507
1962
  const work = Promise.resolve().then(
1508
- () => runInIsolatedScope(scope, () => registration.handle(event, context))
1963
+ observation === void 0 ? handle : () => observation.process({ registration, delivery }, handle)
1509
1964
  );
1510
1965
  work.catch(() => void 0);
1511
- let timer;
1966
+ let cancelTimeout;
1512
1967
  if (registration.timeoutMs !== null) {
1513
1968
  const timeoutMs = registration.timeoutMs;
1514
- timer = setTimeout(() => {
1515
- controller.abort(
1516
- new HandlerTimeoutError(
1517
- `Handler '${registration.id}' did not settle within ${timeoutMs}ms`
1518
- )
1519
- );
1520
- }, timeoutMs);
1969
+ cancelTimeout = this.#timers.start({
1970
+ run: () => {
1971
+ controller.abort(
1972
+ new HandlerTimeoutError(
1973
+ `Handler '${registration.id}' did not settle within ${timeoutMs}ms`
1974
+ )
1975
+ );
1976
+ },
1977
+ delayMs: timeoutMs
1978
+ });
1521
1979
  }
1522
1980
  try {
1523
- await Promise.race([work, rejectOnAbort(controller.signal)]);
1981
+ const settled = Promise.race([work, rejectOnAbort(controller.signal)]);
1982
+ await (observation === void 0 ? settled : observation.observe({ registration, delivery }, settled));
1524
1983
  } catch (error) {
1525
- await this.#awaitAbandonedHandler(work, error, transaction !== void 0);
1984
+ await this.#awaitAbandonedHandler(work, {
1985
+ reason: error,
1986
+ transactional: transaction !== void 0
1987
+ });
1526
1988
  throw error;
1527
1989
  } finally {
1528
- clearTimeout(timer);
1990
+ cancelTimeout?.();
1529
1991
  }
1530
1992
  }
1531
1993
  /**
@@ -1535,7 +1997,7 @@ var Dispatcher = class {
1535
1997
  * Transactional handlers are always awaited; others for one lease duration,
1536
1998
  * except at shutdown, which already waited.
1537
1999
  */
1538
- async #awaitAbandonedHandler(work, reason, transactional) {
2000
+ async #awaitAbandonedHandler(work, { reason, transactional }) {
1539
2001
  const settledWork = work.then(
1540
2002
  () => void 0,
1541
2003
  () => void 0
@@ -1557,7 +2019,10 @@ var Dispatcher = class {
1557
2019
  * lease is extended once to cover the handler timeout, and the handler is
1558
2020
  * aborted when that lease ends.
1559
2021
  */
1560
- async #guardTransactionalLease(registration, delivery, controller) {
2022
+ async #guardTransactionalLease(registration, {
2023
+ delivery,
2024
+ controller
2025
+ }) {
1561
2026
  const { store, clock, settings } = this.#dependencies;
1562
2027
  const now = clock.now();
1563
2028
  let expiry = delivery.leaseExpiresAt.getTime();
@@ -1569,66 +2034,24 @@ var Dispatcher = class {
1569
2034
  });
1570
2035
  }
1571
2036
  }
1572
- const timer = setTimeout(
1573
- () => {
2037
+ return schedule(clock, {
2038
+ run: () => {
1574
2039
  controller.abort(new LeaseLostError(`Lease on delivery ${delivery.deliveryId} expired`));
1575
2040
  },
1576
- Math.max(0, expiry - clock.now().getTime())
1577
- );
1578
- timer.unref();
1579
- return () => clearTimeout(timer);
2041
+ delayMs: Math.max(0, expiry - clock.now().getTime()),
2042
+ unref: true
2043
+ });
1580
2044
  }
1581
- /**
1582
- * Renews the lease every third of its duration. A worker that cannot renew
1583
- * aborts its handler once the last confirmed expiry passes, because another
1584
- * worker may then claim the delivery (self-fencing).
1585
- */
1586
- #startHeartbeat(delivery, controller) {
1587
- const { store, clock, notifier, settings } = this.#dependencies;
1588
- let confirmedExpiry = delivery.leaseExpiresAt.getTime();
1589
- let renewing = false;
1590
- const abortLeaseLost = (storeConfirmed = false) => {
1591
- controller.abort(
1592
- new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, { storeConfirmed })
1593
- );
1594
- };
1595
- const timer = setInterval(
1596
- () => {
1597
- const now = clock.now();
1598
- if (now.getTime() >= confirmedExpiry) {
1599
- abortLeaseLost();
1600
- return;
1601
- }
1602
- if (renewing) {
1603
- return;
1604
- }
1605
- renewing = true;
1606
- const expiry = now.getTime() + settings.leaseDurationMs;
1607
- store.extendLease(delivery, new Date(expiry), now).then((extended) => {
1608
- if (extended) {
1609
- confirmedExpiry = expiry;
1610
- } else {
1611
- abortLeaseLost(true);
1612
- }
1613
- }).catch((error) => notifier.failure("extend-lease", error)).finally(() => {
1614
- renewing = false;
1615
- });
1616
- },
1617
- Math.max(1, Math.floor(settings.leaseDurationMs / 3))
1618
- );
1619
- timer.unref();
1620
- return () => clearInterval(timer);
1621
- }
1622
- async #fail(registration, delivery, error) {
2045
+ async #fail(registration, { delivery, error }) {
1623
2046
  const policy = registration.retry;
1624
2047
  if (delivery.attempt >= policy.attempts || !isRetryableError(policy, error)) {
1625
2048
  await this.#bury(delivery, error);
1626
2049
  return;
1627
2050
  }
1628
- await this.#reschedule(registration, delivery, error);
2051
+ await this.#reschedule(registration, { delivery, error });
1629
2052
  }
1630
2053
  /** Schedules the next attempt with the retry policy's backoff. */
1631
- async #reschedule(registration, delivery, error) {
2054
+ async #reschedule(registration, { delivery, error }) {
1632
2055
  const { store, clock, random, notifier } = this.#dependencies;
1633
2056
  const policy = registration.retry;
1634
2057
  const errorInfo = toErrorInfo(error);
@@ -1683,11 +2106,11 @@ var Dispatcher = class {
1683
2106
  * outlives its lease cannot run for ever at attempt 1. If the lease is gone for real the store
1684
2107
  * refuses and nothing changes. The lost lease was reported already.
1685
2108
  */
1686
- async #giveBack(registration, delivery, error) {
2109
+ async #giveBack(registration, { delivery, error }) {
1687
2110
  if (delivery.attempt >= registration.retry.attempts) {
1688
2111
  await this.#bury(delivery, error);
1689
2112
  } else {
1690
- await this.#reschedule(registration, delivery, error);
2113
+ await this.#reschedule(registration, { delivery, error });
1691
2114
  }
1692
2115
  }
1693
2116
  async #release(delivery) {
@@ -1711,19 +2134,12 @@ var Dispatcher = class {
1711
2134
  }
1712
2135
  }
1713
2136
  };
1714
- function pendingKey(delivery) {
1715
- return `${delivery.deliveryId}\0${delivery.leaseToken}`;
1716
- }
1717
- function rejectOnAbort(signal) {
1718
- return new Promise((_, reject) => {
1719
- signal.addEventListener("abort", () => reject(signal.reason), { once: true });
1720
- });
1721
- }
1722
2137
 
1723
2138
  // src/handler-registry.ts
1724
2139
  var HANDLER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
1725
2140
  var HandlerRegistry = class {
1726
2141
  #byId = /* @__PURE__ */ new Map();
2142
+ #byClaimId = /* @__PURE__ */ new Map();
1727
2143
  #byEventType = /* @__PURE__ */ new Map();
1728
2144
  register(input, defaults) {
1729
2145
  const { options } = input;
@@ -1732,33 +2148,50 @@ var HandlerRegistry = class {
1732
2148
  `Handler id '${options.id}' must be 1-255 characters of letters, digits, '.', '_', ':', '/' or '-'`
1733
2149
  );
1734
2150
  }
1735
- if (this.#byId.has(options.id)) {
1736
- throw new ConfigurationError(`Handler id '${options.id}' is already registered`);
1737
- }
1738
- if (options.concurrency !== void 0 && (!Number.isInteger(options.concurrency) || options.concurrency < 1)) {
1739
- throw new ConfigurationError(
1740
- `Concurrency of handler '${options.id}' must be a positive integer, received ${options.concurrency}`
1741
- );
2151
+ const claimIds = [options.id, ...input.legacyIds ?? []];
2152
+ this.#assertUnregistered(claimIds);
2153
+ if (options.concurrency !== void 0) {
2154
+ assertPositiveInteger(options.concurrency, `Concurrency of handler '${options.id}'`);
1742
2155
  }
1743
2156
  const registration = {
1744
2157
  id: options.id,
1745
- eventType: input.eventType,
2158
+ eventTypes: input.eventTypes,
2159
+ claimIds,
1746
2160
  schema: input.schema,
1747
2161
  handle: input.handle,
1748
2162
  retry: createRetryPolicy(options.retry, defaults.retry),
1749
- timeoutMs: resolveTimeout(options.timeout, defaults.timeoutMs, options.id),
2163
+ timeoutMs: resolveTimeout(options.timeout, {
2164
+ fallback: defaults.timeoutMs,
2165
+ handlerId: options.id
2166
+ }),
1750
2167
  concurrency: options.concurrency ?? null,
1751
2168
  transactional: options.transactional ?? false,
1752
2169
  forwardsToTransport: input.forwardsToTransport
1753
2170
  };
1754
- this.#byId.set(registration.id, registration);
1755
- const forType = this.#byEventType.get(registration.eventType) ?? [];
1756
- forType.push(registration);
1757
- this.#byEventType.set(registration.eventType, forType);
2171
+ this.#index(registration);
1758
2172
  return registration;
1759
2173
  }
2174
+ #assertUnregistered(claimIds) {
2175
+ for (const id of claimIds) {
2176
+ if (this.#byClaimId.has(id)) {
2177
+ throw new ConfigurationError(`Handler id '${id}' is already registered`);
2178
+ }
2179
+ }
2180
+ }
2181
+ #index(registration) {
2182
+ this.#byId.set(registration.id, registration);
2183
+ for (const id of registration.claimIds) {
2184
+ this.#byClaimId.set(id, registration);
2185
+ }
2186
+ for (const eventType of registration.eventTypes) {
2187
+ const forType = this.#byEventType.get(eventType) ?? [];
2188
+ forType.push(registration);
2189
+ this.#byEventType.set(eventType, forType);
2190
+ }
2191
+ }
2192
+ /** The registration that runs deliveries stored under `id`, its own id or a legacy one. */
1760
2193
  get(id) {
1761
- return this.#byId.get(id);
2194
+ return this.#byClaimId.get(id);
1762
2195
  }
1763
2196
  forEventType(eventType) {
1764
2197
  return this.#byEventType.get(eventType) ?? [];
@@ -1767,7 +2200,7 @@ var HandlerRegistry = class {
1767
2200
  return [...this.#byId.values()];
1768
2201
  }
1769
2202
  };
1770
- function resolveTimeout(timeout, fallback, handlerId) {
2203
+ function resolveTimeout(timeout, { fallback, handlerId }) {
1771
2204
  if (timeout === void 0) {
1772
2205
  return fallback;
1773
2206
  }
@@ -1781,9 +2214,18 @@ function resolveTimeout(timeout, fallback, handlerId) {
1781
2214
  var Notifier = class {
1782
2215
  #observers;
1783
2216
  #logger;
2217
+ /** Hooks at least one observer implements, so callers skip building events nobody reads. */
2218
+ #implemented;
1784
2219
  constructor(observers, logger) {
1785
2220
  this.#observers = observers;
1786
2221
  this.#logger = logger;
2222
+ this.#implemented = new Set(
2223
+ observers.flatMap((observer) => hookNames(observer))
2224
+ );
2225
+ }
2226
+ /** True when some observer implements `method`. */
2227
+ wants(method) {
2228
+ return this.#implemented.has(method);
1787
2229
  }
1788
2230
  notify(method, argument) {
1789
2231
  for (const observer of this.#observers) {
@@ -1827,12 +2269,58 @@ var Notifier = class {
1827
2269
  #logObserverFailure(error) {
1828
2270
  const errorInfo = toErrorInfo(error);
1829
2271
  this.#logger.warn(
1830
- `[Notifier](notify) relaybus observer threw | ${errorInfo.name}: ${errorInfo.message}`
2272
+ `[Notifier](logObserverFailure) relaybus observer threw | ${errorInfo.name}: ${errorInfo.message}`
1831
2273
  );
1832
2274
  }
1833
2275
  };
2276
+ function hookNames(observer) {
2277
+ const names = [];
2278
+ for (let target = observer; target !== null && target !== Object.prototype; target = Object.getPrototypeOf(target)) {
2279
+ names.push(
2280
+ ...Object.getOwnPropertyNames(target).filter(
2281
+ (name) => typeof observer[name] === "function"
2282
+ )
2283
+ );
2284
+ }
2285
+ return names;
2286
+ }
1834
2287
 
1835
- // src/relay-bus.ts
2288
+ // src/received-envelope.ts
2289
+ function normalizeEnvelope(envelope) {
2290
+ if (typeof envelope.id !== "string" || envelope.id.length === 0) {
2291
+ throw new InvalidEnvelopeError("Received event has no id");
2292
+ }
2293
+ if (typeof envelope.type !== "string") {
2294
+ throw new InvalidEnvelopeError(`Received event ${envelope.id} has no type`);
2295
+ }
2296
+ try {
2297
+ assertEventType(envelope.type);
2298
+ } catch (error) {
2299
+ throw new InvalidEnvelopeError(toErrorInfo(error).message);
2300
+ }
2301
+ const occurredAt = toDate(envelope.occurredAt);
2302
+ if (occurredAt === void 0) {
2303
+ throw new InvalidEnvelopeError(`Received event ${envelope.id} has an invalid occurredAt`);
2304
+ }
2305
+ return {
2306
+ id: envelope.id,
2307
+ type: envelope.type,
2308
+ source: typeof envelope.source === "string" ? envelope.source : "unknown",
2309
+ key: optionalText(envelope.key),
2310
+ payload: envelope.payload,
2311
+ headers: { ...envelope.headers },
2312
+ correlationId: optionalText(envelope.correlationId),
2313
+ causationId: optionalText(envelope.causationId),
2314
+ occurredAt
2315
+ };
2316
+ }
2317
+ function toDate(value) {
2318
+ const date = typeof value === "string" ? new Date(value) : value;
2319
+ return date instanceof Date && !Number.isNaN(date.getTime()) ? date : void 0;
2320
+ }
2321
+ function optionalText(value) {
2322
+ return typeof value === "string" && value.length > 0 ? value : null;
2323
+ }
1836
2324
  var DEFAULTS = {
1837
2325
  concurrency: 10,
1838
2326
  pollInterval: "1s",
@@ -1842,14 +2330,195 @@ var DEFAULTS = {
1842
2330
  completionBatchSize: 200,
1843
2331
  completedRetention: "7d",
1844
2332
  retentionInterval: "1h",
1845
- routeTimeoutMs: 3e4
2333
+ routeTimeoutMs: 3e4,
2334
+ transactionTimeout: "5s"
1846
2335
  };
2336
+ function oneOf(value, { allowed, name }) {
2337
+ if (!allowed.includes(value)) {
2338
+ throw new ConfigurationError(
2339
+ `${name} must be one of ${allowed.map((option) => `'${option}'`).join(", ")}, received '${String(value)}'`
2340
+ );
2341
+ }
2342
+ return value;
2343
+ }
2344
+ function isDispatcherEnabled(options) {
2345
+ return options.dispatcher?.enabled ?? true;
2346
+ }
2347
+ function resolveBacklogInterval(options) {
2348
+ return options.backlogInterval === void 0 ? void 0 : toTimerMilliseconds(options.backlogInterval, "backlogInterval");
2349
+ }
2350
+ function resolveTransactionTimeout(options) {
2351
+ return toTimerMilliseconds(
2352
+ options.transactionTimeout ?? DEFAULTS.transactionTimeout,
2353
+ "transactionTimeout"
2354
+ );
2355
+ }
2356
+ function createHandlerDefaults(options) {
2357
+ return {
2358
+ retry: createRetryPolicy(options.retry),
2359
+ timeoutMs: options.handlerTimeout === void 0 || options.handlerTimeout === false ? null : toTimerMilliseconds(options.handlerTimeout, "handlerTimeout")
2360
+ };
2361
+ }
2362
+ function createDispatcherSettings(options, generateIdentifier) {
2363
+ const dispatcher = options ?? {};
2364
+ const concurrency = assertPositiveInteger(
2365
+ dispatcher.concurrency ?? DEFAULTS.concurrency,
2366
+ "dispatcher.concurrency"
2367
+ );
2368
+ return {
2369
+ owner: `${hostname()}:${process.pid}:${generateIdentifier()}`,
2370
+ concurrency,
2371
+ batchSize: assertPositiveInteger(dispatcher.batchSize ?? concurrency, "dispatcher.batchSize"),
2372
+ ...createDispatcherTimings(dispatcher)
2373
+ };
2374
+ }
2375
+ function createDispatcherTimings(dispatcher) {
2376
+ return {
2377
+ pollIntervalMs: toTimerMilliseconds(
2378
+ dispatcher.pollInterval ?? DEFAULTS.pollInterval,
2379
+ "dispatcher.pollInterval"
2380
+ ),
2381
+ leaseDurationMs: toTimerMilliseconds(
2382
+ dispatcher.leaseDuration ?? DEFAULTS.leaseDuration,
2383
+ "dispatcher.leaseDuration"
2384
+ ),
2385
+ shutdownTimeoutMs: toTimerMilliseconds(
2386
+ dispatcher.shutdownTimeout ?? DEFAULTS.shutdownTimeout,
2387
+ "dispatcher.shutdownTimeout"
2388
+ ),
2389
+ completionBatchWindowMs: toMilliseconds(
2390
+ dispatcher.completionBatchWindow ?? DEFAULTS.completionBatchWindow,
2391
+ "dispatcher.completionBatchWindow"
2392
+ ),
2393
+ completionBatchSize: assertPositiveInteger(
2394
+ dispatcher.completionBatchSize ?? DEFAULTS.completionBatchSize,
2395
+ "dispatcher.completionBatchSize"
2396
+ )
2397
+ };
2398
+ }
2399
+ function createRetentionSettings(options) {
2400
+ const completed = options?.completed ?? DEFAULTS.completedRetention;
2401
+ const dead = options?.dead ?? false;
2402
+ return {
2403
+ completedMs: completed === false ? null : toMilliseconds(completed, "retention.completed"),
2404
+ deadMs: dead === false ? null : toMilliseconds(dead, "retention.dead"),
2405
+ intervalMs: toTimerMilliseconds(
2406
+ options?.interval ?? DEFAULTS.retentionInterval,
2407
+ "retention.interval"
2408
+ )
2409
+ };
2410
+ }
2411
+
2412
+ // src/route-groups.ts
1847
2413
  var TRANSPORT_HANDLER_PREFIX = "relaybus.transport";
2414
+ function groupRoutes(routes) {
2415
+ const groups = /* @__PURE__ */ new Map();
2416
+ for (const route of routes) {
2417
+ const type = routedType(route.event);
2418
+ const destination = route.destination ?? type;
2419
+ const id = `${TRANSPORT_HANDLER_PREFIX}:${route.transport}:${destination}`;
2420
+ const group = groups.get(id);
2421
+ if (group === void 0) {
2422
+ groups.set(id, createGroup(route, { id, destination, type }));
2423
+ } else {
2424
+ addRoute(group, { route, type, destination });
2425
+ }
2426
+ }
2427
+ return [...groups.values()];
2428
+ }
2429
+ function createGroup(route, {
2430
+ id,
2431
+ destination,
2432
+ type
2433
+ }) {
2434
+ return {
2435
+ transport: route.transport,
2436
+ destination,
2437
+ id,
2438
+ eventTypes: [type],
2439
+ legacyIds: [`${id}:${type}`],
2440
+ route,
2441
+ ...resolveSettings(route, id)
2442
+ };
2443
+ }
2444
+ function addRoute(group, {
2445
+ route,
2446
+ type,
2447
+ destination
2448
+ }) {
2449
+ const target = `'${group.destination}' of transport '${group.transport}'`;
2450
+ if (route.transport !== group.transport) {
2451
+ throw new ConfigurationError(
2452
+ `Routes to ${target} and to '${destination}' of transport '${route.transport}' would share the handler id '${group.id}'; rename the transport or the destination`
2453
+ );
2454
+ }
2455
+ if (group.eventTypes.includes(type)) {
2456
+ throw new ConfigurationError(`Event type ${type} is routed to ${target} twice`);
2457
+ }
2458
+ const { retry, timeoutMs } = resolveSettings(route, group.id);
2459
+ if (timeoutMs !== group.timeoutMs || !sameRetryPolicy(retry, group.retry)) {
2460
+ throw new ConfigurationError(
2461
+ `Routes to ${target} must share retry and timeout, because their events are sent in one queue per key | types=${group.eventTypes[0]},${type}`
2462
+ );
2463
+ }
2464
+ group.eventTypes.push(type);
2465
+ group.legacyIds.push(`${group.id}:${type}`);
2466
+ }
2467
+ function resolveSettings(route, id) {
2468
+ return {
2469
+ retry: createRetryPolicy(route.retry, DEFAULT_ROUTE_RETRY_POLICY),
2470
+ timeoutMs: route.timeout === void 0 ? DEFAULTS.routeTimeoutMs : toTimerMilliseconds(route.timeout, `Timeout of handler '${id}'`)
2471
+ };
2472
+ }
2473
+ function sameRetryPolicy(left, right) {
2474
+ return Object.keys(left).every(
2475
+ (field) => left[field] === right[field]
2476
+ );
2477
+ }
2478
+ function routedType(event) {
2479
+ const type = typeof event === "string" ? event : event.type;
2480
+ assertEventType(type);
2481
+ return type;
2482
+ }
2483
+
2484
+ // src/transaction-budget.ts
2485
+ async function runBudgetedTransaction(store, { clock, timeoutMs, work }) {
2486
+ const startedAt = clock.now().getTime();
2487
+ const overrun = () => clock.now().getTime() - startedAt >= timeoutMs;
2488
+ let committing = false;
2489
+ try {
2490
+ return await store.transaction(
2491
+ async (transaction) => {
2492
+ committing = false;
2493
+ const result = await work(transaction);
2494
+ if (overrun()) {
2495
+ throw createTimeoutError(timeoutMs, void 0);
2496
+ }
2497
+ committing = true;
2498
+ return result;
2499
+ },
2500
+ { timeoutMs }
2501
+ );
2502
+ } catch (error) {
2503
+ if (error instanceof TransactionTimeoutError || committing || !overrun()) {
2504
+ throw error;
2505
+ }
2506
+ throw createTimeoutError(timeoutMs, error);
2507
+ }
2508
+ }
2509
+ function createTimeoutError(timeoutMs, cause) {
2510
+ const message = `The transaction ran past its ${timeoutMs} ms budget (transactionTimeout) and was rolled back. The usual cause is a slow call to another service (HTTP, gRPC, a broker, a lock) inside the transaction: make external calls before the transaction starts or after it commits, or raise transactionTimeout`;
2511
+ return cause === void 0 ? new TransactionTimeoutError(message) : new TransactionTimeoutError(`${message} | cause=${toErrorInfo(cause).message}`, { cause });
2512
+ }
2513
+
2514
+ // src/relay-bus.ts
2515
+ var DEFAULT_DRAIN_BATCHES = 1e3;
1848
2516
  var WAKE_DELAY_MS = 25;
1849
2517
  var RelayBus = class {
1850
2518
  #store;
1851
2519
  #source;
1852
2520
  #transactionMode;
2521
+ #transactionTimeoutMs;
1853
2522
  #transactionResolver;
1854
2523
  #unroutedEvents;
1855
2524
  #registry = new HandlerRegistry();
@@ -1866,43 +2535,70 @@ var RelayBus = class {
1866
2535
  #setupStore;
1867
2536
  #retention;
1868
2537
  #retentionTimer;
2538
+ #observation;
2539
+ #backlogIntervalMs;
2540
+ #backlogTimer;
2541
+ #shutdownTimeoutMs;
1869
2542
  #state = "idle";
1870
2543
  #starting;
1871
2544
  #stopping;
1872
2545
  constructor(options) {
1873
2546
  this.#store = options.store;
1874
2547
  this.#source = options.source ?? "relaybus";
1875
- this.#transactionMode = oneOf(
1876
- options.transactionMode ?? "required",
1877
- ["required", "optional"],
1878
- "transactionMode"
1879
- );
1880
- this.#unroutedEvents = oneOf(
1881
- options.unroutedEvents ?? "warn",
1882
- ["warn", "throw"],
1883
- "unroutedEvents"
1884
- );
2548
+ this.#transactionMode = oneOf(options.transactionMode ?? "required", {
2549
+ allowed: ["required", "optional"],
2550
+ name: "transactionMode"
2551
+ });
2552
+ this.#unroutedEvents = oneOf(options.unroutedEvents ?? "warn", {
2553
+ allowed: ["warn", "throw"],
2554
+ name: "unroutedEvents"
2555
+ });
1885
2556
  this.#transactionResolver = options.transactionResolver;
1886
- this.#defaults = {
1887
- retry: createRetryPolicy(options.retry),
1888
- timeoutMs: options.handlerTimeout === void 0 || options.handlerTimeout === false ? null : toTimerMilliseconds(options.handlerTimeout, "handlerTimeout")
1889
- };
2557
+ this.#transactionTimeoutMs = resolveTransactionTimeout(options);
2558
+ this.#defaults = createHandlerDefaults(options);
1890
2559
  this.#logger = options.logger ?? console;
1891
2560
  this.#notifier = new Notifier(options.observers ?? [], this.#logger);
1892
2561
  this.#clock = options.clock ?? systemClock;
2562
+ this.#observation = BusObservation.create({
2563
+ notifier: this.#notifier,
2564
+ tracer: options.tracer,
2565
+ clock: this.#clock
2566
+ });
2567
+ this.#backlogIntervalMs = resolveBacklogInterval(options);
1893
2568
  this.#generateIdentifier = options.generateIdentifier ?? (() => createUuidV7());
1894
2569
  this.#setupStore = options.setupStore ?? true;
1895
2570
  this.#retention = createRetentionSettings(options.retention);
2571
+ this.#subscriptions = options.subscriptions ?? [];
2572
+ this.#registerConnections(options);
2573
+ this.#dispatcherEnabled = isDispatcherEnabled(options);
2574
+ const settings = createDispatcherSettings(options.dispatcher, this.#generateIdentifier);
2575
+ this.#shutdownTimeoutMs = settings.shutdownTimeoutMs;
2576
+ this.#dispatcher = this.#createDispatcher(settings, options);
2577
+ }
2578
+ #createDispatcher(settings, options) {
2579
+ return new Dispatcher({
2580
+ store: this.#store,
2581
+ registry: this.#registry,
2582
+ clock: this.#clock,
2583
+ random: options.random ?? Math.random,
2584
+ notifier: this.#notifier,
2585
+ settings,
2586
+ observation: DispatchObservation.create({
2587
+ notifier: this.#notifier,
2588
+ tracer: options.tracer,
2589
+ clock: this.#clock
2590
+ })
2591
+ });
2592
+ }
2593
+ /** Registers the transports, then the routes, checks the subscriptions and registers the catalogs. */
2594
+ #registerConnections(options) {
1896
2595
  for (const transport of options.transports ?? []) {
1897
2596
  if (this.#transports.has(transport.name)) {
1898
2597
  throw new ConfigurationError(`Transport name '${transport.name}' is used twice`);
1899
2598
  }
1900
2599
  this.#transports.set(transport.name, transport);
1901
2600
  }
1902
- for (const route of options.routes ?? []) {
1903
- this.#registerRoute(route);
1904
- }
1905
- this.#subscriptions = options.subscriptions ?? [];
2601
+ this.#registerRoutes(options.routes ?? []);
1906
2602
  for (const subscription of this.#subscriptions) {
1907
2603
  const transport = this.#requireTransport(subscription.transport);
1908
2604
  if (transport.subscribe === void 0) {
@@ -1912,15 +2608,6 @@ var RelayBus = class {
1912
2608
  for (const catalog of options.catalogs ?? []) {
1913
2609
  this.registerCatalog(catalog);
1914
2610
  }
1915
- this.#dispatcherEnabled = options.dispatcher?.enabled ?? true;
1916
- this.#dispatcher = new Dispatcher({
1917
- store: this.#store,
1918
- registry: this.#registry,
1919
- clock: this.#clock,
1920
- random: options.random ?? Math.random,
1921
- notifier: this.#notifier,
1922
- settings: createDispatcherSettings(options.dispatcher, this.#generateIdentifier)
1923
- });
1924
2611
  }
1925
2612
  get store() {
1926
2613
  return this.#store;
@@ -1940,11 +2627,12 @@ var RelayBus = class {
1940
2627
  }
1941
2628
  return true;
1942
2629
  }
2630
+ // eslint-disable-next-line relaybus/max-two-params -- public RelayBus.subscribe signature
1943
2631
  subscribe(event, handler, options) {
1944
2632
  const { type, schema } = describeEvent(event);
1945
2633
  this.#registry.register(
1946
2634
  {
1947
- eventType: type,
2635
+ eventTypes: [type],
1948
2636
  schema,
1949
2637
  handle: handler,
1950
2638
  options,
@@ -1953,8 +2641,9 @@ var RelayBus = class {
1953
2641
  this.#defaults
1954
2642
  );
1955
2643
  }
2644
+ // eslint-disable-next-line relaybus/max-two-params -- public RelayBus.subscribeCatalog signature
1956
2645
  subscribeCatalog(catalog, handlers, options = {}) {
1957
- for (const binding of resolveCatalogHandlers(catalog, handlers, options)) {
2646
+ for (const binding of resolveCatalogHandlers(catalog, { handlers, options })) {
1958
2647
  this.subscribe(binding.eventClass, binding.handle, binding.options);
1959
2648
  }
1960
2649
  }
@@ -1977,7 +2666,9 @@ var RelayBus = class {
1977
2666
  }
1978
2667
  /** Registered handlers and routes, for diagnostics. */
1979
2668
  handlers() {
1980
- return this.#registry.all().map(toDescriptor);
2669
+ return this.#registry.all().flatMap(
2670
+ (registration) => registration.eventTypes.map((eventType) => toDescriptor(registration, eventType))
2671
+ );
1981
2672
  }
1982
2673
  /**
1983
2674
  * Stores an event in the current transaction. Handlers run after the
@@ -1985,21 +2676,32 @@ var RelayBus = class {
1985
2676
  * have been written when the id already exists.
1986
2677
  */
1987
2678
  async publish(event, options = {}) {
1988
- const [envelope] = await this.#publish([event], options, options.id);
2679
+ const [envelope] = await this.#observedPublish([event], { options, id: options.id });
1989
2680
  return envelope;
1990
2681
  }
1991
2682
  /** Stores several events atomically, in order, sharing one correlation id. */
1992
2683
  async publishAll(events, options = {}) {
1993
- return this.#publish(events, options, void 0);
2684
+ return this.#observedPublish(events, { options, id: void 0 });
1994
2685
  }
1995
2686
  /**
1996
2687
  * Runs `work` in a new store transaction. `publish` calls inside it join the
1997
- * transaction without passing it explicitly.
2688
+ * transaction without passing it explicitly. The transaction is held to `transactionTimeout`
2689
+ * and rolled back with a {@link TransactionTimeoutError} when it runs past it.
1998
2690
  */
1999
2691
  async transaction(work) {
2000
- return this.#store.transaction(
2001
- (transaction) => runInScope({ transaction }, () => work(transaction))
2002
- );
2692
+ return runBudgetedTransaction(this.#store, {
2693
+ clock: this.#clock,
2694
+ timeoutMs: this.#transactionTimeoutMs,
2695
+ work: (transaction) => runInScope({ transaction }, () => work(transaction))
2696
+ });
2697
+ }
2698
+ /**
2699
+ * The transaction `publish` would join right now: the one of the enclosing `bus.transaction`
2700
+ * (or `@RelayBusTransaction()` method, or transactional handler), else the one
2701
+ * `transactionResolver` finds. Repositories use it to write in the same transaction.
2702
+ */
2703
+ currentTransaction() {
2704
+ return getCurrentScope()?.transaction ?? this.#transactionResolver?.();
2003
2705
  }
2004
2706
  /**
2005
2707
  * Stores an event received from a broker and schedules it for local handlers.
@@ -2007,7 +2709,13 @@ var RelayBus = class {
2007
2709
  * only after it resolves. Throws {@link InvalidEnvelopeError} for malformed
2008
2710
  * events, which transports should dead-letter instead of redelivering.
2009
2711
  */
2010
- async receive(envelope) {
2712
+ receive(envelope) {
2713
+ return this.#observation === void 0 ? this.#receive(envelope) : this.#observedReceive(envelope, void 0);
2714
+ }
2715
+ #observedReceive(envelope, origin) {
2716
+ return this.#observation.receive({ envelope, origin }, () => this.#receive(envelope));
2717
+ }
2718
+ async #receive(envelope) {
2011
2719
  const normalized = normalizeEnvelope(envelope);
2012
2720
  const registrations = this.#registry.forEventType(normalized.type).filter((registration) => !registration.forwardsToTransport);
2013
2721
  if (registrations.length === 0) {
@@ -2036,7 +2744,7 @@ var RelayBus = class {
2036
2744
  */
2037
2745
  async start() {
2038
2746
  if (this.#state !== "idle") {
2039
- throw new RelayBusError(
2747
+ throw new LifecycleError(
2040
2748
  this.#state === "stopped" ? "A stopped relaybus cannot be started again; create a new instance" : `Cannot start a relaybus that is ${this.#state}`
2041
2749
  );
2042
2750
  }
@@ -2050,25 +2758,43 @@ var RelayBus = class {
2050
2758
  }
2051
2759
  }
2052
2760
  /**
2053
- * Stops dispatching, waits for running handlers, closes transports and the
2054
- * store. Waits for a start in progress first. Safe to call more than once.
2761
+ * Stops claiming, waits for running handlers, hands back leases, closes transports and the store,
2762
+ * all within `options.timeout` (default `dispatcher.shutdownTimeout`). When the deadline passes it
2763
+ * aborts the handlers still running, force-closes what relaybus opened itself (never pools or
2764
+ * clients the application passed in) and resolves anyway: it never rejects because of the
2765
+ * deadline. Abandoned deliveries stay leased until their lease expires and then run again
2766
+ * (at-least-once). Waits for a start in progress first. Safe to call more than once: later calls
2767
+ * get the result of the first.
2055
2768
  */
2056
- async stop() {
2769
+ async stop(options = {}) {
2770
+ const timeoutMs = options.timeout === void 0 ? this.#shutdownTimeoutMs : toTimerMilliseconds(options.timeout, "timeout");
2057
2771
  if (this.#stopping !== void 0) {
2058
2772
  return this.#stopping;
2059
2773
  }
2060
- if (this.#state === "starting") {
2061
- await this.#starting.catch(() => void 0);
2774
+ if (this.#state !== "running" && this.#state !== "starting") {
2775
+ return this.#stopOutsideRun(timeoutMs);
2062
2776
  }
2063
- if (this.#state === "idle") {
2064
- await this.#dispatcher.stop();
2065
- return;
2777
+ this.#stopping = this.#stopComponents(timeoutMs);
2778
+ const result = await this.#stopping;
2779
+ if (!result.graceful) {
2780
+ this.#logger.warn(
2781
+ `[RelayBus](stop) Shutdown deadline passed, forced close | abandoned=${result.abandonedDeliveries} | timeoutMs=${timeoutMs}`
2782
+ );
2066
2783
  }
2067
- if (this.#state !== "running") {
2068
- return;
2784
+ return result;
2785
+ }
2786
+ /**
2787
+ * A bus that is not running: an idle one only settles what batches run by hand left behind (and
2788
+ * stays startable); a stopped one has nothing left to do.
2789
+ */
2790
+ async #stopOutsideRun(timeoutMs) {
2791
+ if (this.#state !== "idle") {
2792
+ return { graceful: true, abandonedDeliveries: 0, durationMs: 0 };
2069
2793
  }
2070
- this.#stopping = this.#stopComponents();
2071
- return this.#stopping;
2794
+ return withinDeadline(
2795
+ { clock: this.#clock, timeoutMs },
2796
+ (deadline) => this.#dispatcher.stop(deadline)
2797
+ );
2072
2798
  }
2073
2799
  /**
2074
2800
  * Claims and runs one batch of due deliveries, resolving when they settle.
@@ -2081,7 +2807,7 @@ var RelayBus = class {
2081
2807
  * Runs batches until nothing is due. Deliveries scheduled in the future
2082
2808
  * (retries, delays) are not waited for. Intended for tests.
2083
2809
  */
2084
- async drain(maxBatches = 1e3) {
2810
+ async drain(maxBatches = DEFAULT_DRAIN_BATCHES) {
2085
2811
  let total = 0;
2086
2812
  for (let batch = 0; batch < maxBatches; batch += 1) {
2087
2813
  const processed = await this.runOnce();
@@ -2090,7 +2816,7 @@ var RelayBus = class {
2090
2816
  }
2091
2817
  total += processed;
2092
2818
  }
2093
- throw new RelayBusError(`drain did not finish within ${maxBatches} batches`);
2819
+ throw new DrainIncompleteError(`drain did not finish within ${maxBatches} batches`);
2094
2820
  }
2095
2821
  listDeliveries(query = {}) {
2096
2822
  return this.#store.listDeliveries(query);
@@ -2132,41 +2858,128 @@ var RelayBus = class {
2132
2858
  }
2133
2859
  for (const subscription of this.#subscriptions) {
2134
2860
  const transport = this.#transports.get(subscription.transport);
2135
- await transport.subscribe(subscription.destinations, async (envelope) => {
2136
- await this.receive(envelope);
2137
- });
2861
+ await transport.subscribe(
2862
+ subscription.destinations,
2863
+ async (envelope, message) => {
2864
+ await (this.#observation === void 0 ? this.#receive(envelope) : this.#observedReceive(envelope, { transport, destination: message?.destination }));
2865
+ },
2866
+ this.#subscriptionHooks(transport)
2867
+ );
2138
2868
  }
2139
2869
  } catch (error) {
2140
- await this.#closeTransports();
2870
+ await withinDeadline(
2871
+ { clock: this.#clock, timeoutMs: this.#shutdownTimeoutMs },
2872
+ async (deadline) => {
2873
+ await this.#closeTransports(deadline);
2874
+ return 0;
2875
+ }
2876
+ );
2141
2877
  this.#state = "stopped";
2142
2878
  throw error;
2143
2879
  }
2880
+ if (this.#state !== "starting") {
2881
+ throw new LifecycleError("relaybus was stopped before it finished starting");
2882
+ }
2144
2883
  if (this.#dispatcherEnabled) {
2145
2884
  this.#dispatcher.start();
2146
2885
  }
2147
- if (this.#retention.completedMs !== null || this.#retention.deadMs !== null) {
2148
- this.#retentionTimer = setInterval(() => {
2149
- this.purgeExpired().catch(() => void 0);
2150
- }, this.#retention.intervalMs);
2151
- this.#retentionTimer.unref();
2152
- }
2886
+ this.#scheduleRetention();
2887
+ this.#scheduleBacklog();
2153
2888
  this.#state = "running";
2154
2889
  this.#logger.log(
2155
- `[RelayBus](start) relaybus started | handlers=${this.#registry.all().length} dispatcher=${this.#dispatcherEnabled}`
2890
+ `[RelayBus](startComponents) relaybus started | handlers=${this.#registry.all().length} dispatcher=${this.#dispatcherEnabled}`
2156
2891
  );
2157
2892
  }
2158
- async #stopComponents() {
2159
- this.#state = "stopping";
2160
- clearInterval(this.#retentionTimer);
2893
+ /** What a transport may report back while subscribed; undefined when no observer wants it. */
2894
+ #subscriptionHooks(transport) {
2895
+ if (!this.#notifier.wants("onInvalidMessage")) {
2896
+ return void 0;
2897
+ }
2898
+ return {
2899
+ onInvalidMessage: ({ destination, error }) => {
2900
+ const errorInfo = toErrorInfo(error);
2901
+ this.#notifier.notify("onInvalidMessage", {
2902
+ transport: transport.name,
2903
+ system: transport.system ?? transport.name,
2904
+ destination,
2905
+ reason: errorInfo.message,
2906
+ error: errorInfo
2907
+ });
2908
+ }
2909
+ };
2910
+ }
2911
+ /** Applies the retention policy every `retention.interval` while the bus runs, when it deletes anything. */
2912
+ #scheduleRetention() {
2913
+ if (this.#retention.completedMs !== null || this.#retention.deadMs !== null) {
2914
+ this.#retentionTimer = schedule(this.#clock, {
2915
+ run: () => {
2916
+ this.purgeExpired().catch(() => void 0);
2917
+ },
2918
+ delayMs: this.#retention.intervalMs,
2919
+ repeat: true,
2920
+ unref: true
2921
+ });
2922
+ }
2923
+ }
2924
+ /** Reports the backlog every `backlogInterval` while the bus runs, when an observer wants it. */
2925
+ #scheduleBacklog() {
2926
+ if (this.#backlogIntervalMs !== void 0) {
2927
+ this.#backlogTimer = this.#observation?.startBacklog({
2928
+ intervalMs: this.#backlogIntervalMs,
2929
+ statistics: () => this.statistics()
2930
+ });
2931
+ }
2932
+ }
2933
+ /**
2934
+ * Waits for a start in progress (a failed start leaves nothing to stop), then stops the dispatcher
2935
+ * and closes the transports and the store. A start still running at the deadline is given up: the
2936
+ * transports and the store are force-closed and the start rejects when it resumes.
2937
+ */
2938
+ async #stopComponents(timeoutMs) {
2161
2939
  try {
2162
- await this.#dispatcher.stop();
2163
- await this.#closeTransports();
2164
- await this.#store.close?.();
2940
+ return await withinDeadline({ clock: this.#clock, timeoutMs }, async (deadline) => {
2941
+ if (this.#state === "starting") {
2942
+ const started = await deadline.race(this.#starting.catch(() => void 0));
2943
+ if (started === "timeout") {
2944
+ this.#state = "stopping";
2945
+ await this.#closeConnections(deadline);
2946
+ return 0;
2947
+ }
2948
+ }
2949
+ if (this.#state !== "running") {
2950
+ return 0;
2951
+ }
2952
+ this.#state = "stopping";
2953
+ this.#retentionTimer?.();
2954
+ this.#backlogTimer?.();
2955
+ const abandoned = await this.#dispatcher.stop(deadline);
2956
+ await this.#closeConnections(deadline);
2957
+ return abandoned;
2958
+ });
2165
2959
  } finally {
2166
2960
  this.#state = "stopped";
2167
2961
  }
2168
2962
  }
2169
- async #publish(events, options, id) {
2963
+ async #closeConnections(deadline) {
2964
+ await this.#closeTransports(deadline);
2965
+ const options = { signal: deadline.signal };
2966
+ await deadline.race(Promise.resolve(this.#store.close?.(options)));
2967
+ }
2968
+ /** Publishes inside the tracer's span, with its trace headers, when the bus is observed. */
2969
+ #observedPublish(events, input) {
2970
+ const observation = this.#observation;
2971
+ if (observation === void 0) {
2972
+ return this.#publish(events, input);
2973
+ }
2974
+ return observation.publish(
2975
+ { eventTypes: events.map((event) => String(event.type)) },
2976
+ (headers) => this.#publish(events, { ...input, options: withTraceHeaders(input.options, headers) })
2977
+ );
2978
+ }
2979
+ async #publish(events, {
2980
+ options,
2981
+ id
2982
+ }) {
2170
2983
  if (events.length === 0) {
2171
2984
  return [];
2172
2985
  }
@@ -2174,10 +2987,27 @@ var RelayBus = class {
2174
2987
  const delayMs = options.delay === void 0 ? 0 : toMilliseconds(options.delay, "delay");
2175
2988
  const availableAt = new Date(now.getTime() + delayMs);
2176
2989
  const scope = getCurrentScope();
2990
+ const entries = await this.#createEntries({ events, options, id, scope, now, availableAt });
2991
+ const deliverable = entries.filter((entry) => entry.deliveries.length > 0);
2992
+ if (deliverable.length > 0) {
2993
+ await this.#appendDeliverable({ entries: deliverable, options, scope, now, delayMs });
2994
+ }
2995
+ return entries.map((entry) => entry.event);
2996
+ }
2997
+ /** One entry per event, in order: its envelope and a delivery for every registration of its type. */
2998
+ async #createEntries(input) {
2999
+ const { events, options, id, scope, now, availableAt } = input;
2177
3000
  const entries = [];
2178
3001
  let correlationId = options.correlationId ?? scope?.event?.correlationId ?? scope?.event?.id;
2179
3002
  for (const event of events) {
2180
- const envelope = await this.#createEnvelope(event, options, id, scope, now, correlationId);
3003
+ const envelope = await this.#createEnvelope({
3004
+ event,
3005
+ options,
3006
+ id,
3007
+ scope,
3008
+ now,
3009
+ correlationId
3010
+ });
2181
3011
  correlationId = envelope.correlationId;
2182
3012
  const registrations = this.#registry.forEventType(envelope.type);
2183
3013
  if (registrations.length === 0) {
@@ -2190,31 +3020,36 @@ var RelayBus = class {
2190
3020
  )
2191
3021
  });
2192
3022
  }
2193
- const deliverable = entries.filter((entry) => entry.deliveries.length > 0);
2194
- if (deliverable.length > 0) {
2195
- const result = await this.#appendInTransaction(deliverable, options, scope, now);
2196
- const appended = new Set(result.appendedEventIds);
2197
- const published = deliverable.map((entry) => entry.event).filter((envelope) => appended.has(envelope.id));
2198
- if (published.length > 0) {
2199
- this.#notifier.notify("onPublished", {
2200
- envelopes: published,
2201
- deliveries: result.appendedDeliveryCount
2202
- });
2203
- if (delayMs === 0) {
2204
- this.#scheduleWake();
2205
- }
3023
+ return entries;
3024
+ }
3025
+ /** Appends the entries that have deliveries, reports what was new and wakes the dispatcher when it is due now. */
3026
+ async #appendDeliverable(input) {
3027
+ const { entries, options, scope, now, delayMs } = input;
3028
+ const result = await this.#appendInTransaction(entries, { options, scope, now });
3029
+ const appended = new Set(result.appendedEventIds);
3030
+ const published = entries.map((entry) => entry.event).filter((envelope) => appended.has(envelope.id));
3031
+ if (published.length > 0) {
3032
+ this.#notifier.notify("onPublished", {
3033
+ envelopes: published,
3034
+ deliveries: result.appendedDeliveryCount
3035
+ });
3036
+ if (delayMs === 0) {
3037
+ this.#scheduleWake();
2206
3038
  }
2207
3039
  }
2208
- return entries.map((entry) => entry.event);
2209
3040
  }
2210
3041
  #reportUnrouted(envelope) {
2211
3042
  const message = `No handler or route for event type, event not stored | type=${envelope.type} id=${envelope.id}`;
2212
3043
  if (this.#unroutedEvents === "throw") {
2213
3044
  throw new ConfigurationError(message);
2214
3045
  }
2215
- this.#logger.warn(`[RelayBus](publish) ${message}`);
3046
+ this.#logger.warn(`[RelayBus](reportUnrouted) ${message}`);
2216
3047
  }
2217
- async #appendInTransaction(entries, options, scope, now) {
3048
+ async #appendInTransaction(entries, {
3049
+ options,
3050
+ scope,
3051
+ now
3052
+ }) {
2218
3053
  const transaction = options.transaction ?? scope?.transaction ?? this.#transactionResolver?.();
2219
3054
  if (transaction !== void 0) {
2220
3055
  return this.#store.append(transaction, entries, now);
@@ -2228,7 +3063,8 @@ var RelayBus = class {
2228
3063
  (ownTransaction) => this.#store.append(ownTransaction, entries, now)
2229
3064
  );
2230
3065
  }
2231
- async #createEnvelope(event, options, id, scope, now, correlationId) {
3066
+ async #createEnvelope(input) {
3067
+ const { event, options, id, scope, now, correlationId } = input;
2232
3068
  if (typeof event.type !== "string") {
2233
3069
  throw new ConfigurationError(
2234
3070
  "publish expects an instance of a class created with defineEvent"
@@ -2245,7 +3081,7 @@ var RelayBus = class {
2245
3081
  id: eventId,
2246
3082
  type: event.type,
2247
3083
  source: this.#source,
2248
- key: options.key ?? null,
3084
+ key: options.key ?? keyFromDefinition(event, payload),
2249
3085
  payload,
2250
3086
  headers: { ...options.headers },
2251
3087
  correlationId: correlationId ?? eventId,
@@ -2261,17 +3097,26 @@ var RelayBus = class {
2261
3097
  const routes = registrations.filter((registration) => registration.forwardsToTransport).length;
2262
3098
  return { handlers: registrations.length - routes, routes };
2263
3099
  }
2264
- #registerRoute(route) {
2265
- const transport = this.#requireTransport(route.transport);
2266
- const { type } = describeEvent(route.event);
2267
- const destination = route.destination ?? type;
3100
+ #registerRoutes(routes) {
3101
+ for (const route of routes) {
3102
+ this.#requireTransport(route.transport);
3103
+ }
3104
+ for (const group of groupRoutes(routes)) {
3105
+ this.#registerRoute(group);
3106
+ }
3107
+ }
3108
+ /** Registers one handler for every route to a destination, so a key's events reach it in commit order. */
3109
+ #registerRoute(group) {
3110
+ const transport = this.#transports.get(group.transport);
3111
+ const { route, destination } = group;
2268
3112
  this.#registry.register(
2269
3113
  {
2270
- eventType: type,
3114
+ eventTypes: group.eventTypes,
3115
+ legacyIds: group.legacyIds,
2271
3116
  schema: void 0,
2272
- handle: (envelope) => transport.publish(envelope, destination),
3117
+ handle: this.#observation?.sender(transport, destination) ?? ((envelope) => transport.publish(envelope, destination)),
2273
3118
  options: {
2274
- id: `${TRANSPORT_HANDLER_PREFIX}:${transport.name}:${destination}:${type}`,
3119
+ id: group.id,
2275
3120
  ...route.retry === void 0 ? {} : { retry: route.retry },
2276
3121
  ...route.timeout === void 0 ? {} : { timeout: route.timeout }
2277
3122
  },
@@ -2291,12 +3136,18 @@ var RelayBus = class {
2291
3136
  if (this.#state !== "running" || this.#dispatcher.watchesNewDeliveries) {
2292
3137
  return;
2293
3138
  }
2294
- setTimeout(() => this.#dispatcher.wake(), WAKE_DELAY_MS).unref();
3139
+ schedule(this.#clock, {
3140
+ run: () => this.#dispatcher.wake(),
3141
+ delayMs: WAKE_DELAY_MS,
3142
+ unref: true
3143
+ });
2295
3144
  }
2296
- async #closeTransports() {
3145
+ /** Closes every transport, each at most until the deadline; past it they are asked to force. */
3146
+ async #closeTransports(deadline) {
3147
+ const options = { signal: deadline.signal };
2297
3148
  for (const transport of this.#transports.values()) {
2298
3149
  try {
2299
- await transport.close();
3150
+ await deadline.race(transport.close(options));
2300
3151
  } catch (error) {
2301
3152
  this.#logger.error(
2302
3153
  `[RelayBus](closeTransports) Failed to close transport | transport=${transport.name} error=${toErrorInfo(error).message}`
@@ -2305,10 +3156,10 @@ var RelayBus = class {
2305
3156
  }
2306
3157
  }
2307
3158
  };
2308
- function toDescriptor(registration) {
3159
+ function toDescriptor(registration, eventType) {
2309
3160
  return {
2310
3161
  id: registration.id,
2311
- eventType: registration.eventType,
3162
+ eventType,
2312
3163
  retry: registration.retry,
2313
3164
  timeoutMs: registration.timeoutMs,
2314
3165
  concurrency: registration.concurrency,
@@ -2324,99 +3175,7 @@ function describeEvent(event) {
2324
3175
  assertEventType(event.type);
2325
3176
  return { type: event.type, schema: event.schema };
2326
3177
  }
2327
- function normalizeEnvelope(envelope) {
2328
- if (typeof envelope.id !== "string" || envelope.id.length === 0) {
2329
- throw new InvalidEnvelopeError("Received event has no id");
2330
- }
2331
- if (typeof envelope.type !== "string") {
2332
- throw new InvalidEnvelopeError(`Received event ${envelope.id} has no type`);
2333
- }
2334
- try {
2335
- assertEventType(envelope.type);
2336
- } catch (error) {
2337
- throw new InvalidEnvelopeError(toErrorInfo(error).message);
2338
- }
2339
- const occurredAt = toDate(envelope.occurredAt);
2340
- if (occurredAt === void 0) {
2341
- throw new InvalidEnvelopeError(`Received event ${envelope.id} has an invalid occurredAt`);
2342
- }
2343
- return {
2344
- id: envelope.id,
2345
- type: envelope.type,
2346
- source: typeof envelope.source === "string" ? envelope.source : "unknown",
2347
- key: optionalText(envelope.key),
2348
- payload: envelope.payload,
2349
- headers: { ...envelope.headers },
2350
- correlationId: optionalText(envelope.correlationId),
2351
- causationId: optionalText(envelope.causationId),
2352
- occurredAt
2353
- };
2354
- }
2355
- function toDate(value) {
2356
- const date = typeof value === "string" ? new Date(value) : value;
2357
- return date instanceof Date && !Number.isNaN(date.getTime()) ? date : void 0;
2358
- }
2359
- function optionalText(value) {
2360
- return typeof value === "string" && value.length > 0 ? value : null;
2361
- }
2362
- function oneOf(value, allowed, name) {
2363
- if (!allowed.includes(value)) {
2364
- throw new ConfigurationError(
2365
- `${name} must be one of ${allowed.map((option) => `'${option}'`).join(", ")}, received '${String(value)}'`
2366
- );
2367
- }
2368
- return value;
2369
- }
2370
- function positiveInteger(value, name) {
2371
- if (!Number.isInteger(value) || value < 1) {
2372
- throw new ConfigurationError(`${name} must be a positive integer, received ${value}`);
2373
- }
2374
- return value;
2375
- }
2376
- function createDispatcherSettings(options, generateIdentifier) {
2377
- const concurrency = positiveInteger(
2378
- options?.concurrency ?? DEFAULTS.concurrency,
2379
- "dispatcher.concurrency"
2380
- );
2381
- return {
2382
- owner: `${hostname()}:${process.pid}:${generateIdentifier()}`,
2383
- concurrency,
2384
- batchSize: positiveInteger(options?.batchSize ?? concurrency, "dispatcher.batchSize"),
2385
- pollIntervalMs: toTimerMilliseconds(
2386
- options?.pollInterval ?? DEFAULTS.pollInterval,
2387
- "dispatcher.pollInterval"
2388
- ),
2389
- leaseDurationMs: toTimerMilliseconds(
2390
- options?.leaseDuration ?? DEFAULTS.leaseDuration,
2391
- "dispatcher.leaseDuration"
2392
- ),
2393
- shutdownTimeoutMs: toTimerMilliseconds(
2394
- options?.shutdownTimeout ?? DEFAULTS.shutdownTimeout,
2395
- "dispatcher.shutdownTimeout"
2396
- ),
2397
- completionBatchWindowMs: toMilliseconds(
2398
- options?.completionBatchWindow ?? DEFAULTS.completionBatchWindow,
2399
- "dispatcher.completionBatchWindow"
2400
- ),
2401
- completionBatchSize: positiveInteger(
2402
- options?.completionBatchSize ?? DEFAULTS.completionBatchSize,
2403
- "dispatcher.completionBatchSize"
2404
- )
2405
- };
2406
- }
2407
- function createRetentionSettings(options) {
2408
- const completed = options?.completed ?? DEFAULTS.completedRetention;
2409
- const dead = options?.dead ?? false;
2410
- return {
2411
- completedMs: completed === false ? null : toMilliseconds(completed, "retention.completed"),
2412
- deadMs: dead === false ? null : toMilliseconds(dead, "retention.dead"),
2413
- intervalMs: toTimerMilliseconds(
2414
- options?.interval ?? DEFAULTS.retentionInterval,
2415
- "retention.interval"
2416
- )
2417
- };
2418
- }
2419
3178
 
2420
- export { CatalogPublisher, ConfigurationError, DEFAULT_RETRY_POLICY, DEFAULT_ROUTE_RETRY_POLICY, DELIVERY_STATUSES, DispatcherStoppedError, EventValidationError, HandlerTimeoutError, InMemoryEventStore, InMemoryTransaction, InvalidEnvelopeError, LeaseLostError, MAX_TIMER_DELAY_MS, NonRetryableError, RelayBus, RelayBusError, TransactionRequiredError, assertEventType, computeRetryDelay, consume, createRetryPolicy, createUuidV7, defineCatalog, defineEvent, defineHandlers, describeCatalog, isRetryableError, produce, systemClock, toErrorInfo, toMilliseconds, validatePayload };
3179
+ export { CatalogPublisher, DEFAULT_RETRY_POLICY, DEFAULT_ROUTE_RETRY_POLICY, DELIVERY_STATUSES, InMemoryEventStore, InMemoryTransaction, MAX_TIMER_DELAY_MS, RelayBus, TRACEPARENT_HEADER, TRACESTATE_HEADER, computeRetryDelay, consume, createRetryPolicy, createUuidV7, defineCatalog, defineHandlers, describeCatalog, isRetryableError, produce, systemClock, toMilliseconds, traceContextOf };
2421
3180
  //# sourceMappingURL=index.js.map
2422
3181
  //# sourceMappingURL=index.js.map