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