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