@relaybus/core 0.1.0 → 0.3.0

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