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