@relaybus/core 0.3.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/dist/{chunk-3OHMEMZA.cjs → chunk-J76M7XTS.cjs} +127 -9
- package/dist/chunk-J76M7XTS.cjs.map +1 -0
- package/dist/{chunk-WS274DL3.js → chunk-NOR5JHVN.js} +121 -9
- package/dist/chunk-NOR5JHVN.js.map +1 -0
- package/dist/index.cjs +2232 -1898
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -475
- package/dist/index.d.ts +82 -475
- package/dist/index.js +2259 -1931
- package/dist/index.js.map +1 -1
- package/dist/internal/index.cjs +335 -117
- package/dist/internal/index.cjs.map +1 -1
- package/dist/internal/index.d.cts +269 -2
- package/dist/internal/index.d.ts +269 -2
- package/dist/internal/index.js +293 -101
- package/dist/internal/index.js.map +1 -1
- package/dist/transport-BlfzIBZD.d.cts +535 -0
- package/dist/transport-BlfzIBZD.d.ts +535 -0
- package/package.json +1 -1
- package/dist/chunk-3OHMEMZA.cjs.map +0 -1
- package/dist/chunk-WS274DL3.js.map +0 -1
- package/dist/event-DCRTuYKI.d.cts +0 -142
- package/dist/event-DCRTuYKI.d.ts +0 -142
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ConfigurationError, assertEventType,
|
|
2
|
-
export { AttemptsExhaustedError, ConfigurationError, DispatcherStoppedError, DrainIncompleteError, EventValidationError, HandlerTimeoutError, InvalidEnvelopeError, LeaseLostError, LifecycleError, NonRetryableError, RelayBusError, TransactionRequiredError, TransactionTimeoutError, assertEventType, defineEvent, toErrorInfo, validatePayload } from './chunk-
|
|
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';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
5
5
|
import { hostname } from 'os';
|
|
@@ -9,47 +9,6 @@ var systemClock = {
|
|
|
9
9
|
now: () => /* @__PURE__ */ new Date()
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/duration.ts
|
|
13
|
-
var MILLISECONDS_PER_UNIT = {
|
|
14
|
-
ms: 1,
|
|
15
|
-
s: 1e3,
|
|
16
|
-
m: 6e4,
|
|
17
|
-
h: 36e5,
|
|
18
|
-
d: 864e5
|
|
19
|
-
};
|
|
20
|
-
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/;
|
|
21
|
-
function toMilliseconds(value, name) {
|
|
22
|
-
if (typeof value === "number") {
|
|
23
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
24
|
-
throw new ConfigurationError(
|
|
25
|
-
`${name} must be a non-negative finite number of milliseconds, received ${value}`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
const match = DURATION_PATTERN.exec(value);
|
|
31
|
-
if (match === null) {
|
|
32
|
-
throw new ConfigurationError(
|
|
33
|
-
`${name} must look like '500ms', '30s', '5m', '1h' or '7d', received '${value}'`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
const [, amount, unit] = match;
|
|
37
|
-
return Number(amount) * MILLISECONDS_PER_UNIT[unit];
|
|
38
|
-
}
|
|
39
|
-
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
40
|
-
function toTimerMilliseconds(value, name) {
|
|
41
|
-
const milliseconds = toMilliseconds(value, name);
|
|
42
|
-
if (milliseconds === 0) {
|
|
43
|
-
throw new ConfigurationError(`${name} must be greater than zero`);
|
|
44
|
-
}
|
|
45
|
-
if (milliseconds > MAX_TIMER_DELAY_MS) {
|
|
46
|
-
throw new ConfigurationError(
|
|
47
|
-
`${name} must not exceed ${MAX_TIMER_DELAY_MS}ms (about 24.8 days), received ${milliseconds}ms`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
return milliseconds;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
12
|
// src/catalog.ts
|
|
54
13
|
var EVENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
55
14
|
var descriptions = /* @__PURE__ */ new WeakMap();
|
|
@@ -340,6 +299,302 @@ function createUuidV7(timestamp = Date.now()) {
|
|
|
340
299
|
return bytes.toString("hex").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5");
|
|
341
300
|
}
|
|
342
301
|
|
|
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;
|
|
322
|
+
}
|
|
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;
|
|
340
|
+
}
|
|
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;
|
|
364
|
+
}
|
|
365
|
+
if (delivery.status === "dead" && request.deadBefore !== void 0) {
|
|
366
|
+
return delivery.updatedAt < request.deadBefore;
|
|
367
|
+
}
|
|
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(
|
|
384
|
+
(left, right) => left.createdAt.getTime() - right.createdAt.getTime() || compareIds(left.id, right.id)
|
|
385
|
+
).slice(0, limit).map((delivery) => ({
|
|
386
|
+
id: delivery.id,
|
|
387
|
+
handler: delivery.handler,
|
|
388
|
+
status: delivery.status,
|
|
389
|
+
attempts: delivery.attempts,
|
|
390
|
+
availableAt: delivery.availableAt,
|
|
391
|
+
leaseOwner: delivery.leaseOwner,
|
|
392
|
+
leaseExpiresAt: delivery.leaseExpiresAt,
|
|
393
|
+
lastError: delivery.lastError,
|
|
394
|
+
createdAt: delivery.createdAt,
|
|
395
|
+
updatedAt: delivery.updatedAt,
|
|
396
|
+
completedAt: delivery.completedAt,
|
|
397
|
+
event: this.#tables.readEnvelope(delivery.eventId)
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
requeue(deliveryIds, now) {
|
|
401
|
+
let moved = 0;
|
|
402
|
+
for (const deliveryId of new Set(deliveryIds)) {
|
|
403
|
+
const delivery = this.#tables.deliveries.get(deliveryId);
|
|
404
|
+
if (delivery?.status !== "dead") {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
delivery.status = "pending";
|
|
408
|
+
delivery.attempts = 0;
|
|
409
|
+
delivery.availableAt = now;
|
|
410
|
+
delivery.updatedAt = now;
|
|
411
|
+
moved += 1;
|
|
412
|
+
}
|
|
413
|
+
return moved;
|
|
414
|
+
}
|
|
415
|
+
purge(request) {
|
|
416
|
+
let deletedDeliveries = 0;
|
|
417
|
+
const touchedEventIds = /* @__PURE__ */ new Set();
|
|
418
|
+
for (const delivery of [...this.#tables.deliveries.values()]) {
|
|
419
|
+
if (isPurgeable(delivery, request)) {
|
|
420
|
+
this.#tables.deliveries.delete(delivery.id);
|
|
421
|
+
touchedEventIds.add(delivery.eventId);
|
|
422
|
+
deletedDeliveries += 1;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { deletedDeliveries, deletedEvents: this.#purgeEvents(request, touchedEventIds) };
|
|
426
|
+
}
|
|
427
|
+
statistics(now) {
|
|
428
|
+
const counts = {
|
|
429
|
+
pending: 0,
|
|
430
|
+
processing: 0,
|
|
431
|
+
completed: 0,
|
|
432
|
+
dead: 0
|
|
433
|
+
};
|
|
434
|
+
let oldestDueAt = null;
|
|
435
|
+
for (const delivery of this.#tables.deliveries.values()) {
|
|
436
|
+
counts[delivery.status] += 1;
|
|
437
|
+
if (isDue(delivery, now)) {
|
|
438
|
+
const dueAt = dueAtOf(delivery);
|
|
439
|
+
if (oldestDueAt === null || dueAt < oldestDueAt) {
|
|
440
|
+
oldestDueAt = dueAt;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return { ...counts, oldestDueAt };
|
|
445
|
+
}
|
|
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
|
+
}
|
|
476
|
+
/** The events and deliveries the transaction appended so far, which are not applied before it commits. */
|
|
477
|
+
written(transaction) {
|
|
478
|
+
let written = this.#writes.get(transaction);
|
|
479
|
+
if (written === void 0) {
|
|
480
|
+
written = { eventIds: /* @__PURE__ */ new Set(), deliveryKeys: /* @__PURE__ */ new Set() };
|
|
481
|
+
this.#writes.set(transaction, written);
|
|
482
|
+
}
|
|
483
|
+
return written;
|
|
484
|
+
}
|
|
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) {
|
|
491
|
+
for (const name of [...new Set(names)].sort()) {
|
|
492
|
+
let held = this.#locks.get(name);
|
|
493
|
+
while (held !== void 0 && held.owner !== transaction) {
|
|
494
|
+
await held.released;
|
|
495
|
+
held = this.#locks.get(name);
|
|
496
|
+
}
|
|
497
|
+
if (held !== void 0) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
let release;
|
|
501
|
+
const released = new Promise((resolve) => {
|
|
502
|
+
release = resolve;
|
|
503
|
+
});
|
|
504
|
+
this.#locks.set(name, { owner: transaction, released });
|
|
505
|
+
transaction.onFinish(() => {
|
|
506
|
+
this.#locks.delete(name);
|
|
507
|
+
release();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
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) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
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);
|
|
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;
|
|
544
|
+
}
|
|
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() {
|
|
558
|
+
}
|
|
559
|
+
/** Deletes a completed delivery, or marks it completed when a history is kept. */
|
|
560
|
+
#finish(delivery, now) {
|
|
561
|
+
const event = this.#tables.events.get(delivery.eventId);
|
|
562
|
+
if (event.completedAt === null || event.completedAt < now) {
|
|
563
|
+
event.completedAt = now;
|
|
564
|
+
}
|
|
565
|
+
if (!this.#retainCompleted) {
|
|
566
|
+
this.#tables.deliveries.delete(delivery.id);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
delivery.status = "completed";
|
|
570
|
+
delivery.completedAt = now;
|
|
571
|
+
delivery.updatedAt = now;
|
|
572
|
+
clearLease(delivery);
|
|
573
|
+
}
|
|
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;
|
|
585
|
+
}
|
|
586
|
+
const next = (this.#keySequences.get(key) ?? 0) + 1;
|
|
587
|
+
this.#keySequences.set(key, next);
|
|
588
|
+
return next;
|
|
589
|
+
}
|
|
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;
|
|
593
|
+
const copy = JSON.parse(JSON.stringify(serialized));
|
|
594
|
+
return { ...copy, occurredAt: new Date(copy.occurredAt) };
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
|
|
343
598
|
// src/in-memory-transaction.ts
|
|
344
599
|
var InMemoryTransaction = class {
|
|
345
600
|
#operations = [];
|
|
@@ -377,16 +632,12 @@ var InMemoryTransaction = class {
|
|
|
377
632
|
};
|
|
378
633
|
|
|
379
634
|
// src/in-memory-event-store.ts
|
|
380
|
-
var DEFAULT_LIST_LIMIT = 100;
|
|
381
635
|
var InMemoryEventStore = class {
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
|
|
386
|
-
#locks = /* @__PURE__ */ new Map();
|
|
387
|
-
#writes = /* @__PURE__ */ new WeakMap();
|
|
636
|
+
#tables = new InMemoryTables();
|
|
637
|
+
#locks = new InMemoryRowLocks();
|
|
638
|
+
#settlement;
|
|
639
|
+
#maintenance = new InMemoryMaintenance(this.#tables, this.#locks);
|
|
388
640
|
#generateIdentifier;
|
|
389
|
-
#retainCompleted;
|
|
390
641
|
constructor(options = {}) {
|
|
391
642
|
if (process.env["NODE_ENV"] === "production" && options.allowInProduction !== true) {
|
|
392
643
|
throw new ConfigurationError(
|
|
@@ -394,7 +645,7 @@ var InMemoryEventStore = class {
|
|
|
394
645
|
);
|
|
395
646
|
}
|
|
396
647
|
this.#generateIdentifier = options.generateIdentifier ?? (() => createUuidV7());
|
|
397
|
-
this.#
|
|
648
|
+
this.#settlement = new InMemorySettlement(this.#tables, options.retainCompleted ?? false);
|
|
398
649
|
}
|
|
399
650
|
async transaction(work) {
|
|
400
651
|
const transaction = new InMemoryTransaction();
|
|
@@ -409,14 +660,14 @@ var InMemoryEventStore = class {
|
|
|
409
660
|
}
|
|
410
661
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
411
662
|
async append(transaction, entries, now) {
|
|
412
|
-
await this.#lock(
|
|
663
|
+
await this.#locks.lock(
|
|
413
664
|
transaction,
|
|
414
665
|
entries.flatMap((entry) => [
|
|
415
666
|
`event:${entry.event.id}`,
|
|
416
667
|
...entry.event.key === null ? [] : [`key:${entry.event.key}`]
|
|
417
668
|
])
|
|
418
669
|
);
|
|
419
|
-
const written = this.#written(transaction);
|
|
670
|
+
const written = this.#locks.written(transaction);
|
|
420
671
|
const pendingEventIds = written.eventIds;
|
|
421
672
|
const pendingDeliveryKeys = written.deliveryKeys;
|
|
422
673
|
const appendedEventIds = [];
|
|
@@ -425,7 +676,7 @@ var InMemoryEventStore = class {
|
|
|
425
676
|
for (const entry of entries) {
|
|
426
677
|
const eventId = entry.event.id;
|
|
427
678
|
const serialized = serializeEnvelope(entry.event);
|
|
428
|
-
const isDuplicate = this.#events.has(eventId) || pendingEventIds.has(eventId);
|
|
679
|
+
const isDuplicate = this.#tables.events.has(eventId) || pendingEventIds.has(eventId);
|
|
429
680
|
if (isDuplicate) {
|
|
430
681
|
duplicateEventIds.push(eventId);
|
|
431
682
|
} else {
|
|
@@ -447,8 +698,8 @@ var InMemoryEventStore = class {
|
|
|
447
698
|
#newDeliveries(entry, pendingDeliveryKeys) {
|
|
448
699
|
const eventId = entry.event.id;
|
|
449
700
|
return entry.deliveries.filter((delivery) => {
|
|
450
|
-
const deliveryKey =
|
|
451
|
-
if (pendingDeliveryKeys.has(deliveryKey) || this.#events.get(eventId)?.handlers.has(delivery.handler) === true) {
|
|
701
|
+
const deliveryKey = CompositeKey.of(eventId, delivery.handler);
|
|
702
|
+
if (pendingDeliveryKeys.has(deliveryKey) || this.#tables.events.get(eventId)?.handlers.has(delivery.handler) === true) {
|
|
452
703
|
return false;
|
|
453
704
|
}
|
|
454
705
|
pendingDeliveryKeys.add(deliveryKey);
|
|
@@ -458,19 +709,20 @@ var InMemoryEventStore = class {
|
|
|
458
709
|
/** Applies one entry of an append when its transaction commits. */
|
|
459
710
|
#applyAppend(input) {
|
|
460
711
|
const { eventId, serialized, isDuplicate, newDeliveries, now } = input;
|
|
712
|
+
const { events, deliveries } = this.#tables;
|
|
461
713
|
if (!isDuplicate) {
|
|
462
|
-
|
|
714
|
+
events.set(eventId, {
|
|
463
715
|
envelope: serialized,
|
|
464
|
-
sequence: this.#nextSequence(serialized.key),
|
|
716
|
+
sequence: this.#tables.nextSequence(serialized.key),
|
|
465
717
|
createdAt: now,
|
|
466
718
|
handlers: /* @__PURE__ */ new Set(),
|
|
467
719
|
completedAt: null
|
|
468
720
|
});
|
|
469
721
|
}
|
|
470
|
-
const stored =
|
|
722
|
+
const stored = events.get(eventId);
|
|
471
723
|
for (const delivery of newDeliveries) {
|
|
472
724
|
stored.handlers.add(delivery.handler);
|
|
473
|
-
|
|
725
|
+
deliveries.set(delivery.id, {
|
|
474
726
|
id: delivery.id,
|
|
475
727
|
eventId,
|
|
476
728
|
handler: delivery.handler,
|
|
@@ -491,7 +743,7 @@ var InMemoryEventStore = class {
|
|
|
491
743
|
}
|
|
492
744
|
async claim(request) {
|
|
493
745
|
const handlers = new Set(request.handlers);
|
|
494
|
-
const due = [...this.#deliveries.values()].filter((delivery) => handlers.has(delivery.handler) && isDue(delivery, request.now)).sort(compareDue);
|
|
746
|
+
const due = [...this.#tables.deliveries.values()].filter((delivery) => handlers.has(delivery.handler) && isDue(delivery, request.now)).sort(compareDue);
|
|
495
747
|
const claimed = [];
|
|
496
748
|
for (const delivery of due) {
|
|
497
749
|
if (claimed.length >= request.limit) {
|
|
@@ -512,193 +764,50 @@ var InMemoryEventStore = class {
|
|
|
512
764
|
handler: delivery.handler,
|
|
513
765
|
attempt: delivery.attempts,
|
|
514
766
|
leaseExpiresAt: request.leaseExpiresAt,
|
|
515
|
-
event: this.#readEnvelope(delivery.eventId)
|
|
767
|
+
event: this.#tables.readEnvelope(delivery.eventId)
|
|
516
768
|
});
|
|
517
769
|
}
|
|
518
770
|
return claimed;
|
|
519
771
|
}
|
|
520
772
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
521
773
|
async extendLease(lease, leaseExpiresAt, now) {
|
|
522
|
-
return this.#
|
|
523
|
-
delivery.leaseExpiresAt = leaseExpiresAt;
|
|
524
|
-
delivery.updatedAt = now;
|
|
525
|
-
});
|
|
774
|
+
return this.#settlement.extendLease({ lease, leaseExpiresAt, now });
|
|
526
775
|
}
|
|
527
776
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
528
777
|
async complete(lease, now, transaction) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
535
|
-
transaction.enqueue(() => {
|
|
536
|
-
const delivery = this.#findLeased(lease);
|
|
537
|
-
if (delivery !== void 0) {
|
|
538
|
-
this.#finish(delivery, now);
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
return true;
|
|
778
|
+
return this.#settlement.complete(
|
|
779
|
+
lease,
|
|
780
|
+
transaction === void 0 ? { now } : { now, transaction }
|
|
781
|
+
);
|
|
542
782
|
}
|
|
543
783
|
async completeMany(leases, now) {
|
|
544
784
|
return Promise.all(leases.map((lease) => this.complete(lease, now)));
|
|
545
785
|
}
|
|
546
786
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
547
787
|
async reschedule(lease, availableAt, error, now) {
|
|
548
|
-
return this.#
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
delivery.lastError = error;
|
|
552
|
-
delivery.updatedAt = now;
|
|
553
|
-
clearLease(delivery);
|
|
788
|
+
return this.#settlement.changeClaim({
|
|
789
|
+
lease,
|
|
790
|
+
change: { kind: "reschedule", availableAt, error, now }
|
|
554
791
|
});
|
|
555
792
|
}
|
|
556
793
|
async release(lease, now) {
|
|
557
|
-
return this.#
|
|
558
|
-
delivery.status = "pending";
|
|
559
|
-
delivery.attempts -= 1;
|
|
560
|
-
delivery.availableAt = now;
|
|
561
|
-
delivery.updatedAt = now;
|
|
562
|
-
clearLease(delivery);
|
|
563
|
-
});
|
|
794
|
+
return this.#settlement.changeClaim({ lease, change: { kind: "release", now } });
|
|
564
795
|
}
|
|
565
796
|
// eslint-disable-next-line relaybus/max-two-params -- EventStore port signature
|
|
566
797
|
async bury(lease, error, now) {
|
|
567
|
-
return this.#
|
|
568
|
-
delivery.status = "dead";
|
|
569
|
-
delivery.lastError = error;
|
|
570
|
-
delivery.updatedAt = now;
|
|
571
|
-
clearLease(delivery);
|
|
572
|
-
});
|
|
798
|
+
return this.#settlement.changeClaim({ lease, change: { kind: "bury", error, now } });
|
|
573
799
|
}
|
|
574
800
|
async listDeliveries(query) {
|
|
575
|
-
|
|
576
|
-
return [...this.#deliveries.values()].filter(
|
|
577
|
-
(delivery) => matchesQuery(delivery, { event: this.#events.get(delivery.eventId), query })
|
|
578
|
-
).sort(
|
|
579
|
-
(left, right) => left.createdAt.getTime() - right.createdAt.getTime() || compareIds(left.id, right.id)
|
|
580
|
-
).slice(0, limit).map((delivery) => ({
|
|
581
|
-
id: delivery.id,
|
|
582
|
-
handler: delivery.handler,
|
|
583
|
-
status: delivery.status,
|
|
584
|
-
attempts: delivery.attempts,
|
|
585
|
-
availableAt: delivery.availableAt,
|
|
586
|
-
leaseOwner: delivery.leaseOwner,
|
|
587
|
-
leaseExpiresAt: delivery.leaseExpiresAt,
|
|
588
|
-
lastError: delivery.lastError,
|
|
589
|
-
createdAt: delivery.createdAt,
|
|
590
|
-
updatedAt: delivery.updatedAt,
|
|
591
|
-
completedAt: delivery.completedAt,
|
|
592
|
-
event: this.#readEnvelope(delivery.eventId)
|
|
593
|
-
}));
|
|
801
|
+
return this.#maintenance.listDeliveries(query);
|
|
594
802
|
}
|
|
595
803
|
async requeue(deliveryIds, now) {
|
|
596
|
-
|
|
597
|
-
for (const deliveryId of new Set(deliveryIds)) {
|
|
598
|
-
const delivery = this.#deliveries.get(deliveryId);
|
|
599
|
-
if (delivery?.status !== "dead") {
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
delivery.status = "pending";
|
|
603
|
-
delivery.attempts = 0;
|
|
604
|
-
delivery.availableAt = now;
|
|
605
|
-
delivery.updatedAt = now;
|
|
606
|
-
moved += 1;
|
|
607
|
-
}
|
|
608
|
-
return moved;
|
|
804
|
+
return this.#maintenance.requeue(deliveryIds, now);
|
|
609
805
|
}
|
|
610
806
|
async purge(request) {
|
|
611
|
-
|
|
612
|
-
const touchedEventIds = /* @__PURE__ */ new Set();
|
|
613
|
-
for (const delivery of [...this.#deliveries.values()]) {
|
|
614
|
-
if (isPurgeable(delivery, request)) {
|
|
615
|
-
this.#deliveries.delete(delivery.id);
|
|
616
|
-
touchedEventIds.add(delivery.eventId);
|
|
617
|
-
deletedDeliveries += 1;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return { deletedDeliveries, deletedEvents: this.#purgeEvents(request, touchedEventIds) };
|
|
621
|
-
}
|
|
622
|
-
/** Deletes the events no delivery references whose retention ran out; returns how many. */
|
|
623
|
-
#purgeEvents(request, touchedEventIds) {
|
|
624
|
-
const referencedEventIds = new Set(
|
|
625
|
-
[...this.#deliveries.values()].map((delivery) => delivery.eventId)
|
|
626
|
-
);
|
|
627
|
-
const { orphanCutoff, completedCutoff } = eventCutoffs(request);
|
|
628
|
-
let deletedEvents = 0;
|
|
629
|
-
for (const [eventId, event] of [...this.#events]) {
|
|
630
|
-
if (referencedEventIds.has(eventId) || this.#locks.has(`event:${eventId}`)) {
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
const expired = event.completedAt === null ? touchedEventIds.has(eventId) || event.createdAt.getTime() < orphanCutoff : event.completedAt.getTime() < completedCutoff;
|
|
634
|
-
if (expired) {
|
|
635
|
-
this.#events.delete(eventId);
|
|
636
|
-
deletedEvents += 1;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return deletedEvents;
|
|
807
|
+
return this.#maintenance.purge(request);
|
|
640
808
|
}
|
|
641
809
|
async statistics(now) {
|
|
642
|
-
|
|
643
|
-
pending: 0,
|
|
644
|
-
processing: 0,
|
|
645
|
-
completed: 0,
|
|
646
|
-
dead: 0
|
|
647
|
-
};
|
|
648
|
-
let oldestDueAt = null;
|
|
649
|
-
for (const delivery of this.#deliveries.values()) {
|
|
650
|
-
counts[delivery.status] += 1;
|
|
651
|
-
if (isDue(delivery, now)) {
|
|
652
|
-
const dueAt = dueAtOf(delivery);
|
|
653
|
-
if (oldestDueAt === null || dueAt < oldestDueAt) {
|
|
654
|
-
oldestDueAt = dueAt;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
return { ...counts, oldestDueAt };
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Waits for other open transactions holding any of `names`, then holds them
|
|
662
|
-
* until `transaction` finishes, the way a database holds row locks. Sorted
|
|
663
|
-
* acquisition keeps two transactions from waiting on each other.
|
|
664
|
-
*/
|
|
665
|
-
/** The events and deliveries the transaction appended so far, which are not applied before it commits. */
|
|
666
|
-
#written(transaction) {
|
|
667
|
-
let written = this.#writes.get(transaction);
|
|
668
|
-
if (written === void 0) {
|
|
669
|
-
written = { eventIds: /* @__PURE__ */ new Set(), deliveryKeys: /* @__PURE__ */ new Set() };
|
|
670
|
-
this.#writes.set(transaction, written);
|
|
671
|
-
}
|
|
672
|
-
return written;
|
|
673
|
-
}
|
|
674
|
-
async #lock(transaction, names) {
|
|
675
|
-
for (const name of [...new Set(names)].sort()) {
|
|
676
|
-
let held = this.#locks.get(name);
|
|
677
|
-
while (held !== void 0 && held.owner !== transaction) {
|
|
678
|
-
await held.released;
|
|
679
|
-
held = this.#locks.get(name);
|
|
680
|
-
}
|
|
681
|
-
if (held !== void 0) {
|
|
682
|
-
continue;
|
|
683
|
-
}
|
|
684
|
-
let release;
|
|
685
|
-
const released = new Promise((resolve) => {
|
|
686
|
-
release = resolve;
|
|
687
|
-
});
|
|
688
|
-
this.#locks.set(name, { owner: transaction, released });
|
|
689
|
-
transaction.onFinish(() => {
|
|
690
|
-
this.#locks.delete(name);
|
|
691
|
-
release();
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
#nextSequence(key) {
|
|
696
|
-
if (key === null) {
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
const next = (this.#keySequences.get(key) ?? 0) + 1;
|
|
700
|
-
this.#keySequences.set(key, next);
|
|
701
|
-
return next;
|
|
810
|
+
return this.#maintenance.statistics(now);
|
|
702
811
|
}
|
|
703
812
|
/**
|
|
704
813
|
* A keyed delivery waits while an earlier delivery of the same handler and
|
|
@@ -710,121 +819,210 @@ var InMemoryEventStore = class {
|
|
|
710
819
|
if (delivery.key === null) {
|
|
711
820
|
return false;
|
|
712
821
|
}
|
|
713
|
-
for (const other of this.#deliveries.values()) {
|
|
822
|
+
for (const other of this.#tables.deliveries.values()) {
|
|
714
823
|
if (blocksKey(other, { delivery, now })) {
|
|
715
824
|
return true;
|
|
716
825
|
}
|
|
717
826
|
}
|
|
718
827
|
return false;
|
|
719
828
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
+
];
|
|
734
851
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return delivery;
|
|
739
|
-
}
|
|
740
|
-
return void 0;
|
|
852
|
+
/** Aborted when the deadline passes. */
|
|
853
|
+
get signal() {
|
|
854
|
+
return this.#controller.signal;
|
|
741
855
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (delivery === void 0) {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
update(delivery);
|
|
748
|
-
return true;
|
|
856
|
+
get passed() {
|
|
857
|
+
return this.#controller.signal.aborted;
|
|
749
858
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
+
}
|
|
754
872
|
}
|
|
755
873
|
};
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
874
|
+
async function withinDeadline({ clock, timeoutMs }, work) {
|
|
875
|
+
const startedAt = clock.now().getTime();
|
|
876
|
+
const deadline = new ShutdownDeadline(clock, timeoutMs);
|
|
877
|
+
try {
|
|
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();
|
|
759
886
|
}
|
|
760
|
-
const unfinishedPredecessor = other.sequence < delivery.sequence && (other.status === "pending" || other.status === "processing");
|
|
761
|
-
const runningUnderLiveLease = other.status === "processing" && other.leaseExpiresAt > now;
|
|
762
|
-
return unfinishedPredecessor || runningUnderLiveLease;
|
|
763
|
-
}
|
|
764
|
-
function eventCutoffs(request) {
|
|
765
|
-
const cutoffs = [request.completedBefore, request.deadBefore].filter((cutoff) => cutoff !== void 0).map((cutoff) => cutoff.getTime());
|
|
766
|
-
return {
|
|
767
|
-
orphanCutoff: cutoffs.length === 0 ? Number.NEGATIVE_INFINITY : Math.min(...cutoffs),
|
|
768
|
-
completedCutoff: request.completedBefore?.getTime() ?? Number.NEGATIVE_INFINITY
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
function serializeEnvelope(envelope) {
|
|
772
|
-
return JSON.parse(JSON.stringify(envelope));
|
|
773
887
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
888
|
+
|
|
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;
|
|
777
897
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
function dueAtOf(delivery) {
|
|
781
|
-
return delivery.status === "pending" ? delivery.availableAt : delivery.leaseExpiresAt;
|
|
782
|
-
}
|
|
783
|
-
function compareDue(left, right) {
|
|
784
|
-
const byDueTime = dueAtOf(left).getTime() - dueAtOf(right).getTime();
|
|
785
|
-
return byDueTime === 0 ? compareIds(left.id, right.id) : byDueTime;
|
|
786
|
-
}
|
|
787
|
-
function compareIds(left, right) {
|
|
788
|
-
return Number(left > right) - Number(left < right);
|
|
789
|
-
}
|
|
790
|
-
function clearLease(delivery) {
|
|
791
|
-
delivery.leaseOwner = null;
|
|
792
|
-
delivery.leaseToken = null;
|
|
793
|
-
delivery.leaseExpiresAt = null;
|
|
794
|
-
}
|
|
795
|
-
function matchesQuery(delivery, { event, query }) {
|
|
796
|
-
return (query.status === void 0 || delivery.status === query.status) && (query.handler === void 0 || delivery.handler === query.handler) && (query.eventType === void 0 || event.envelope.type === query.eventType) && (query.eventId === void 0 || delivery.eventId === query.eventId);
|
|
797
|
-
}
|
|
798
|
-
function isPurgeable(delivery, request) {
|
|
799
|
-
if (delivery.status === "completed" && request.completedBefore !== void 0) {
|
|
800
|
-
return delivery.completedAt < request.completedBefore;
|
|
898
|
+
get state() {
|
|
899
|
+
return this.#state;
|
|
801
900
|
}
|
|
802
|
-
|
|
803
|
-
|
|
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}`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
804
908
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
var TRACESTATE_HEADER = "tracestate";
|
|
811
|
-
var TRACEPARENT_FIELDS = "(?!0{32})[\\da-f]{32}-(?!0{16})[\\da-f]{16}-[\\da-f]{2}";
|
|
812
|
-
var TRACEPARENT = new RegExp(
|
|
813
|
-
`^(?:00-${TRACEPARENT_FIELDS}|(?!00|ff)[\\da-f]{2}-${TRACEPARENT_FIELDS}(?:-.*)?)$`
|
|
814
|
-
);
|
|
815
|
-
function traceContextOf(headers) {
|
|
816
|
-
const traceparent = headers[TRACEPARENT_HEADER];
|
|
817
|
-
if (traceparent === void 0 || !TRACEPARENT.test(traceparent)) {
|
|
818
|
-
return void 0;
|
|
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;
|
|
819
914
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
915
|
+
/** The start in progress settled. */
|
|
916
|
+
endStart() {
|
|
917
|
+
this.#starting = void 0;
|
|
918
|
+
}
|
|
919
|
+
markRunning() {
|
|
920
|
+
this.#state = "running";
|
|
921
|
+
}
|
|
922
|
+
markStopped() {
|
|
923
|
+
this.#state = "stopped";
|
|
924
|
+
}
|
|
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 };
|
|
929
|
+
}
|
|
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 };
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
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.
|
|
939
|
+
*/
|
|
940
|
+
async #stopOutsideRun(timeoutMs) {
|
|
941
|
+
if (this.#state !== "idle") {
|
|
942
|
+
return { graceful: true, abandonedDeliveries: 0, durationMs: 0 };
|
|
943
|
+
}
|
|
944
|
+
return withinDeadline(
|
|
945
|
+
{ clock: this.#parts.clock, timeoutMs },
|
|
946
|
+
(deadline) => this.#parts.dispatcher.stop(deadline)
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
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.
|
|
953
|
+
*/
|
|
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";
|
|
976
|
+
}
|
|
977
|
+
}
|
|
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
|
+
}
|
|
828
1026
|
var BusObservation = class _BusObservation {
|
|
829
1027
|
#notifier;
|
|
830
1028
|
#tracer;
|
|
@@ -1075,291 +1273,516 @@ var DispatchObservation = class _DispatchObservation {
|
|
|
1075
1273
|
}
|
|
1076
1274
|
};
|
|
1077
1275
|
|
|
1078
|
-
// src/
|
|
1079
|
-
var
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
#
|
|
1084
|
-
#
|
|
1085
|
-
#
|
|
1086
|
-
|
|
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);
|
|
1319
|
+
}
|
|
1087
1320
|
/**
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
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.
|
|
1090
1323
|
*/
|
|
1091
|
-
|
|
1092
|
-
this.#
|
|
1093
|
-
this.#
|
|
1094
|
-
this.#
|
|
1095
|
-
|
|
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
|
|
1338
|
+
});
|
|
1096
1339
|
}
|
|
1097
1340
|
/**
|
|
1098
|
-
*
|
|
1099
|
-
*
|
|
1341
|
+
* Claims until halted. A claim that filled its batch is followed by the next one at once while enough
|
|
1342
|
+
* slots are free. Otherwise the loop sleeps until a wake-up (a publish, a store notification, or enough
|
|
1343
|
+
* deliveries settling to refill a group of slots) or the poll interval. Only the poll interval claims
|
|
1344
|
+
* for fewer free slots than the refill threshold, so a busy dispatcher never claims slot by slot.
|
|
1100
1345
|
*/
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1346
|
+
async run() {
|
|
1347
|
+
let minimumFree = 1;
|
|
1348
|
+
while (this.#running) {
|
|
1349
|
+
let round = { requested: 0, claimed: 0 };
|
|
1350
|
+
if (this.#freeSlots() >= minimumFree) {
|
|
1351
|
+
try {
|
|
1352
|
+
round = await this.claimAndStart();
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
this.#parts.dependencies.notifier.failure("claim", error);
|
|
1355
|
+
}
|
|
1111
1356
|
}
|
|
1112
|
-
|
|
1357
|
+
if (round.requested > 0 && round.claimed === round.requested && this.#freeSlots() >= this.#refillThreshold) {
|
|
1358
|
+
minimumFree = this.#refillThreshold;
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
minimumFree = await this.#waitForWakeUp();
|
|
1362
|
+
if (this.#freeSlots() < minimumFree) {
|
|
1363
|
+
this.#requestClaim();
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1113
1366
|
}
|
|
1114
1367
|
/**
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
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.
|
|
1117
1371
|
*/
|
|
1118
|
-
|
|
1119
|
-
this.#
|
|
1120
|
-
|
|
1121
|
-
this.#flush();
|
|
1372
|
+
onSettled(delivery, succeeded) {
|
|
1373
|
+
if (delivery.event.key !== null || !succeeded || !this.#exhaustedHandlers.has(delivery.handler)) {
|
|
1374
|
+
this.#requestClaim();
|
|
1122
1375
|
}
|
|
1123
1376
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
)
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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 };
|
|
1138
1407
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
this.#clock = clock;
|
|
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
|
+
}
|
|
1148
1416
|
}
|
|
1149
|
-
/**
|
|
1150
|
-
|
|
1151
|
-
|
|
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?.();
|
|
1152
1421
|
}
|
|
1153
|
-
/**
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1422
|
+
/** Waits for a wake-up or the poll interval. Resolves with the free slots the next claim needs. */
|
|
1423
|
+
async #waitForWakeUp() {
|
|
1424
|
+
if (this.#signalledMinimum === void 0) {
|
|
1425
|
+
await new Promise((resolve) => {
|
|
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
|
+
});
|
|
1433
|
+
this.#wakeUp = () => {
|
|
1434
|
+
cancel();
|
|
1435
|
+
resolve();
|
|
1436
|
+
};
|
|
1437
|
+
});
|
|
1438
|
+
this.#wakeUp = void 0;
|
|
1439
|
+
}
|
|
1440
|
+
const minimumFree = this.#signalledMinimum;
|
|
1441
|
+
this.#signalledMinimum = void 0;
|
|
1442
|
+
return minimumFree;
|
|
1158
1443
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1444
|
+
/**
|
|
1445
|
+
* Asks the loop for a claim. Once a group of slots is free the claim waits only for the deliveries
|
|
1446
|
+
* settling in the same tick, so they are claimed for together; fewer free slots are refilled when no
|
|
1447
|
+
* claim started for a short while. With no free slot the next settlement asks again.
|
|
1448
|
+
*/
|
|
1449
|
+
#requestClaim() {
|
|
1450
|
+
const free = this.#freeSlots();
|
|
1451
|
+
if (free >= this.#refillThreshold) {
|
|
1452
|
+
if (!this.#refillScheduled) {
|
|
1453
|
+
this.#refillScheduled = true;
|
|
1454
|
+
setImmediate(() => {
|
|
1455
|
+
this.#refillScheduled = false;
|
|
1456
|
+
this.#signal(this.#refillThreshold);
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
} else if (free > 0) {
|
|
1460
|
+
this.#quietRefillTimer ??= schedule(this.#parts.dependencies.clock, {
|
|
1461
|
+
run: () => {
|
|
1462
|
+
this.#quietRefillTimer = void 0;
|
|
1463
|
+
this.#signal(1);
|
|
1464
|
+
},
|
|
1465
|
+
delayMs: Math.max(0, this.#lastClaimStartedAt + QUIET_REFILL_MS - Date.now())
|
|
1466
|
+
});
|
|
1163
1467
|
}
|
|
1164
1468
|
}
|
|
1165
|
-
#
|
|
1166
|
-
|
|
1167
|
-
cancelTimer();
|
|
1168
|
-
this.#waiting.delete(cancel);
|
|
1169
|
-
};
|
|
1170
|
-
const cancelTimer = schedule(this.#clock, {
|
|
1171
|
-
run: () => {
|
|
1172
|
-
this.#waiting.delete(cancel);
|
|
1173
|
-
task.run();
|
|
1174
|
-
},
|
|
1175
|
-
delayMs: task.delayMs
|
|
1176
|
-
});
|
|
1177
|
-
this.#waiting.set(cancel, onCancel);
|
|
1178
|
-
return cancel;
|
|
1469
|
+
#freeSlots() {
|
|
1470
|
+
return this.#parts.dependencies.settings.concurrency - this.#parts.inFlight.size;
|
|
1179
1471
|
}
|
|
1180
1472
|
};
|
|
1181
1473
|
|
|
1182
|
-
// src/
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
var storage = new AsyncLocalStorage();
|
|
1192
|
-
function runInScope(scope, work) {
|
|
1193
|
-
return storage.run({ ...storage.getStore(), ...scope }, work);
|
|
1194
|
-
}
|
|
1195
|
-
function runInIsolatedScope(scope, work) {
|
|
1196
|
-
return storage.run(scope, work);
|
|
1197
|
-
}
|
|
1198
|
-
function getCurrentScope() {
|
|
1199
|
-
return storage.getStore();
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// src/shutdown-deadline.ts
|
|
1203
|
-
var DRAIN_SHARE = 0.8;
|
|
1204
|
-
var ShutdownDeadline = class {
|
|
1205
|
-
#controller = new AbortController();
|
|
1206
|
-
#drained;
|
|
1207
|
-
#expired;
|
|
1208
|
-
#cancelTimers;
|
|
1209
|
-
constructor(clock, timeoutMs) {
|
|
1210
|
-
let drain;
|
|
1211
|
-
this.#drained = new Promise((resolve) => {
|
|
1212
|
-
drain = () => resolve("timeout");
|
|
1213
|
-
});
|
|
1214
|
-
const signal = this.#controller.signal;
|
|
1215
|
-
this.#expired = new Promise((resolve) => {
|
|
1216
|
-
signal.addEventListener("abort", () => resolve("timeout"), { once: true });
|
|
1217
|
-
});
|
|
1218
|
-
this.#cancelTimers = [
|
|
1219
|
-
schedule(clock, { run: drain, delayMs: Math.floor(timeoutMs * DRAIN_SHARE) }),
|
|
1220
|
-
schedule(clock, { run: () => this.#controller.abort(), delayMs: timeoutMs })
|
|
1221
|
-
];
|
|
1222
|
-
}
|
|
1223
|
-
/** Aborted when the deadline passes. */
|
|
1224
|
-
get signal() {
|
|
1225
|
-
return this.#controller.signal;
|
|
1226
|
-
}
|
|
1227
|
-
get passed() {
|
|
1228
|
-
return this.#controller.signal.aborted;
|
|
1229
|
-
}
|
|
1230
|
-
/** Waits for `work` until the deadline. Rejects when `work` rejects before it. */
|
|
1231
|
-
race(work) {
|
|
1232
|
-
return Promise.race([work.then(() => "done"), this.#expired]);
|
|
1233
|
-
}
|
|
1234
|
-
/** Waits for running handlers until the drain point, when they are aborted. */
|
|
1235
|
-
raceDrain(work) {
|
|
1236
|
-
return Promise.race([work.then(() => "done"), this.#drained]);
|
|
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;
|
|
1237
1481
|
}
|
|
1238
|
-
/**
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1482
|
+
/** One claim for handlers without a concurrency limit, one per limited handler with room. */
|
|
1483
|
+
plan(registrations, freeSlots) {
|
|
1484
|
+
let free = Math.min(freeSlots, this.#batchSize);
|
|
1485
|
+
const plans = [];
|
|
1486
|
+
const unlimited = registrations.filter((registration) => registration.concurrency === null);
|
|
1487
|
+
for (const registration of registrations) {
|
|
1488
|
+
if (registration.concurrency === null || free === 0) {
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
const room = registration.concurrency - this.#inFlight.countOf(registration);
|
|
1492
|
+
const limit = Math.min(room, free);
|
|
1493
|
+
if (limit > 0) {
|
|
1494
|
+
plans.push({ handlers: [...registration.claimIds], limit });
|
|
1495
|
+
free -= limit;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (unlimited.length > 0 && free > 0) {
|
|
1499
|
+
plans.push({
|
|
1500
|
+
handlers: unlimited.flatMap((registration) => registration.claimIds),
|
|
1501
|
+
limit: free
|
|
1502
|
+
});
|
|
1242
1503
|
}
|
|
1504
|
+
return plans;
|
|
1243
1505
|
}
|
|
1244
1506
|
};
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
return
|
|
1251
|
-
graceful: !deadline.passed,
|
|
1252
|
-
abandonedDeliveries,
|
|
1253
|
-
durationMs: clock.now().getTime() - startedAt
|
|
1254
|
-
};
|
|
1255
|
-
} finally {
|
|
1256
|
-
deadline.dispose();
|
|
1507
|
+
|
|
1508
|
+
// src/dispatch/dispatch-context.ts
|
|
1509
|
+
var ShutdownLatch = class {
|
|
1510
|
+
#abandoned = false;
|
|
1511
|
+
get abandoned() {
|
|
1512
|
+
return this.#abandoned;
|
|
1257
1513
|
}
|
|
1258
|
-
|
|
1514
|
+
abandon() {
|
|
1515
|
+
this.#abandoned = true;
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1259
1518
|
|
|
1260
1519
|
// src/lease-heartbeat.ts
|
|
1261
1520
|
var RENEWALS_PER_LEASE = 3;
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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;
|
|
1266
1556
|
controller.abort(
|
|
1267
1557
|
new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, { storeConfirmed })
|
|
1268
1558
|
);
|
|
1269
|
-
}
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
function startLeaseHeartbeat(dependencies, target) {
|
|
1562
|
+
const renewal = new LeaseRenewal(dependencies, target);
|
|
1563
|
+
const { clock, settings } = dependencies;
|
|
1270
1564
|
return schedule(clock, {
|
|
1271
|
-
run: () =>
|
|
1272
|
-
const now = clock.now();
|
|
1273
|
-
if (now.getTime() >= confirmedExpiry) {
|
|
1274
|
-
abortLeaseLost();
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
if (renewing) {
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
renewing = true;
|
|
1281
|
-
const expiry = now.getTime() + settings.leaseDurationMs;
|
|
1282
|
-
store.extendLease(delivery, new Date(expiry), now).then((extended) => {
|
|
1283
|
-
if (extended) {
|
|
1284
|
-
confirmedExpiry = expiry;
|
|
1285
|
-
} else {
|
|
1286
|
-
abortLeaseLost(true);
|
|
1287
|
-
}
|
|
1288
|
-
}).catch((error) => notifier.failure("extend-lease", error)).finally(() => {
|
|
1289
|
-
renewing = false;
|
|
1290
|
-
});
|
|
1291
|
-
},
|
|
1565
|
+
run: () => renewal.beat(),
|
|
1292
1566
|
delayMs: Math.max(1, Math.floor(settings.leaseDurationMs / RENEWALS_PER_LEASE)),
|
|
1293
1567
|
repeat: true,
|
|
1294
1568
|
unref: true
|
|
1295
1569
|
});
|
|
1296
1570
|
}
|
|
1297
1571
|
|
|
1298
|
-
// src/
|
|
1299
|
-
var
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
retryIf: void 0
|
|
1306
|
-
};
|
|
1307
|
-
var DEFAULT_ROUTE_RETRY_POLICY = {
|
|
1308
|
-
attempts: Number.POSITIVE_INFINITY,
|
|
1309
|
-
initialDelayMs: 1e3,
|
|
1310
|
-
factor: 2,
|
|
1311
|
-
maxDelayMs: 3e5,
|
|
1312
|
-
jitter: "full",
|
|
1313
|
-
retryIf: void 0
|
|
1314
|
-
};
|
|
1315
|
-
var JITTER_STRATEGIES = ["full", "equal", "none"];
|
|
1316
|
-
function createRetryPolicy(options, base = DEFAULT_RETRY_POLICY) {
|
|
1317
|
-
const policy = mergeRetryPolicy(options, base);
|
|
1318
|
-
assertValidRetryPolicy(policy);
|
|
1319
|
-
return policy;
|
|
1320
|
-
}
|
|
1321
|
-
function mergeRetryPolicy(options, base) {
|
|
1322
|
-
const overrides = options ?? {};
|
|
1323
|
-
const initialDelayMs = overrides.initialDelay === void 0 ? base.initialDelayMs : toMilliseconds(overrides.initialDelay, "retry.initialDelay");
|
|
1324
|
-
return {
|
|
1325
|
-
attempts: overrides.attempts ?? base.attempts,
|
|
1326
|
-
initialDelayMs,
|
|
1327
|
-
factor: overrides.factor ?? base.factor,
|
|
1328
|
-
maxDelayMs: overrides.maxDelay === void 0 ? (
|
|
1329
|
-
// Raising only the initial delay must not trip the inherited cap.
|
|
1330
|
-
Math.max(base.maxDelayMs, initialDelayMs)
|
|
1331
|
-
) : toMilliseconds(overrides.maxDelay, "retry.maxDelay"),
|
|
1332
|
-
jitter: overrides.jitter ?? base.jitter,
|
|
1333
|
-
retryIf: overrides.retryIf ?? base.retryIf
|
|
1334
|
-
};
|
|
1335
|
-
}
|
|
1336
|
-
function assertValidRetryPolicy(policy) {
|
|
1337
|
-
if (policy.attempts !== Number.POSITIVE_INFINITY && (!Number.isInteger(policy.attempts) || policy.attempts < 1)) {
|
|
1338
|
-
throw new ConfigurationError(
|
|
1339
|
-
`retry.attempts must be a positive integer or Infinity, received ${policy.attempts}`
|
|
1340
|
-
);
|
|
1341
|
-
}
|
|
1342
|
-
if (!Number.isFinite(policy.factor) || policy.factor < 1) {
|
|
1343
|
-
throw new ConfigurationError(`retry.factor must be at least 1, received ${policy.factor}`);
|
|
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;
|
|
1344
1579
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
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;
|
|
1347
1597
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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();
|
|
1352
1604
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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 });
|
|
1612
|
+
const startedAt = clock.now().getTime();
|
|
1613
|
+
if (delivery.attempt > registration.retry.attempts) {
|
|
1614
|
+
await outcomes.bury(
|
|
1615
|
+
delivery,
|
|
1616
|
+
new AttemptsExhaustedError(
|
|
1617
|
+
`Delivery reached attempt ${delivery.attempt} of ${registration.retry.attempts} after its lease expired`
|
|
1618
|
+
)
|
|
1619
|
+
);
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
let stopHeartbeat = () => void 0;
|
|
1623
|
+
try {
|
|
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 });
|
|
1628
|
+
return true;
|
|
1629
|
+
} catch (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;
|
|
1636
|
+
}
|
|
1637
|
+
return false;
|
|
1638
|
+
} finally {
|
|
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
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
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;
|
|
1672
|
+
const payload = await validatePayload(
|
|
1673
|
+
delivery.event.type,
|
|
1674
|
+
registration.schema,
|
|
1675
|
+
delivery.event.payload
|
|
1676
|
+
);
|
|
1677
|
+
const event = { ...delivery.event, payload };
|
|
1678
|
+
if (registration.transactional) {
|
|
1679
|
+
await store.transaction(async (transaction) => {
|
|
1680
|
+
await invocation.invoke({ registration, event, delivery, controller, transaction });
|
|
1681
|
+
if (!await store.complete(delivery, clock.now(), transaction)) {
|
|
1682
|
+
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, {
|
|
1683
|
+
storeConfirmed: true
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
return "completed";
|
|
1688
|
+
}
|
|
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":
|
|
1363
1786
|
return Math.round(capped);
|
|
1364
1787
|
}
|
|
1365
1788
|
}
|
|
@@ -1377,7 +1800,7 @@ function isRetryableError(policy, error) {
|
|
|
1377
1800
|
}
|
|
1378
1801
|
}
|
|
1379
1802
|
|
|
1380
|
-
// src/
|
|
1803
|
+
// src/dispatch/delivery-outcomes.ts
|
|
1381
1804
|
var FIRST_COMPLETION_RETRY_MS = 50;
|
|
1382
1805
|
var SECOND_COMPLETION_RETRY_MS = 250;
|
|
1383
1806
|
var LAST_COMPLETION_RETRY_MS = 1e3;
|
|
@@ -1386,205 +1809,317 @@ var COMPLETION_RETRY_DELAYS_MS = [
|
|
|
1386
1809
|
SECOND_COMPLETION_RETRY_MS,
|
|
1387
1810
|
LAST_COMPLETION_RETRY_MS
|
|
1388
1811
|
];
|
|
1389
|
-
var
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
#dependencies;
|
|
1394
|
-
#sleep;
|
|
1395
|
-
#inFlight = /* @__PURE__ */ new Set();
|
|
1396
|
-
#completions;
|
|
1397
|
-
#pending = /* @__PURE__ */ new Map();
|
|
1398
|
-
#recoveryTimer;
|
|
1399
|
-
#recovery;
|
|
1400
|
-
#running = false;
|
|
1401
|
-
#loop;
|
|
1402
|
-
#wakeUp;
|
|
1403
|
-
/** Free slots the claim the loop was woken for needs at least; undefined while nobody woke it. */
|
|
1404
|
-
#signalledMinimum;
|
|
1405
|
-
/** Free slots a claim needs unless the poll interval or the quiet refill time passed: claims refill in groups. */
|
|
1406
|
-
#refillThreshold;
|
|
1407
|
-
/** Handlers whose last claim got less than it asked for: the store has nothing more for them right now. */
|
|
1408
|
-
#exhaustedHandlers = /* @__PURE__ */ new Set();
|
|
1409
|
-
/** Grows with every wake-up: a claim that started before one must not mark a handler exhausted. */
|
|
1410
|
-
#wakeGeneration = 0;
|
|
1411
|
-
#lastClaimStartedAt = Number.NEGATIVE_INFINITY;
|
|
1412
|
-
#cooldownTimer;
|
|
1413
|
-
#refillScheduled = false;
|
|
1414
|
-
#quietRefillTimer;
|
|
1415
|
-
#unwatch;
|
|
1416
|
-
#watching;
|
|
1417
|
-
/** Set when a stop gave up at its deadline: the store may be closed, so nothing more is started or retried. */
|
|
1418
|
-
#abandoned = false;
|
|
1419
|
-
/** Stops the lease renewal (or transactional lease guard) of each delivery whose handler runs. */
|
|
1420
|
-
#leaseKeepers = /* @__PURE__ */ new Map();
|
|
1421
|
-
#timers;
|
|
1422
|
-
constructor(dependencies) {
|
|
1423
|
-
this.#dependencies = dependencies;
|
|
1424
|
-
this.#timers = new DeliveryTimers(dependencies.clock);
|
|
1425
|
-
this.#sleep = dependencies.sleep ?? ((milliseconds) => this.#timers.sleep(milliseconds));
|
|
1426
|
-
const { store, clock, settings } = dependencies;
|
|
1427
|
-
this.#refillThreshold = Math.ceil(
|
|
1428
|
-
Math.min(settings.concurrency, settings.batchSize) * REFILL_FRACTION
|
|
1429
|
-
);
|
|
1430
|
-
const { completionBatchWindowMs: windowMs = 0, completionBatchSize: maxBatchSize = 1 } = settings;
|
|
1431
|
-
if (store.completeMany !== void 0 && windowMs > 0) {
|
|
1432
|
-
this.#completions = new CompletionBatcher(
|
|
1433
|
-
{ completeMany: (leases, now) => store.completeMany(leases, now) },
|
|
1434
|
-
clock,
|
|
1435
|
-
{ windowMs, maxBatchSize },
|
|
1436
|
-
() => this.#inFlight.size
|
|
1437
|
-
);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
get isRunning() {
|
|
1441
|
-
return this.#running;
|
|
1442
|
-
}
|
|
1443
|
-
get inFlightCount() {
|
|
1444
|
-
return this.#inFlight.size;
|
|
1812
|
+
var DeliveryOutcomes = class {
|
|
1813
|
+
#parts;
|
|
1814
|
+
constructor(parts) {
|
|
1815
|
+
this.#parts = parts;
|
|
1445
1816
|
}
|
|
1446
1817
|
/**
|
|
1447
|
-
*
|
|
1448
|
-
*
|
|
1818
|
+
* Records a completion, retrying transient store errors while the lease is
|
|
1819
|
+
* still held. Gives up to lease expiry, which reruns the handler: at-least-once.
|
|
1449
1820
|
*/
|
|
1450
|
-
|
|
1451
|
-
|
|
1821
|
+
async confirmCompletion(delivery) {
|
|
1822
|
+
const { dependencies, shutdown, sleep } = this.#parts;
|
|
1823
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
1824
|
+
if (shutdown.abandoned) {
|
|
1825
|
+
return "unconfirmed";
|
|
1826
|
+
}
|
|
1827
|
+
let completed;
|
|
1828
|
+
try {
|
|
1829
|
+
completed = await this.complete(delivery);
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
const delay = COMPLETION_RETRY_DELAYS_MS[attempt];
|
|
1832
|
+
if (delay === void 0) {
|
|
1833
|
+
dependencies.notifier.failure("complete", error);
|
|
1834
|
+
return "unconfirmed";
|
|
1835
|
+
}
|
|
1836
|
+
await sleep(delay);
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (!completed) {
|
|
1840
|
+
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, {
|
|
1841
|
+
storeConfirmed: true
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
return "completed";
|
|
1845
|
+
}
|
|
1452
1846
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1847
|
+
/** Records one completion, grouped with its neighbours when the store can complete many at once. */
|
|
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);
|
|
1852
|
+
}
|
|
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);
|
|
1455
1857
|
return;
|
|
1456
1858
|
}
|
|
1457
|
-
this.#
|
|
1458
|
-
this.#completions?.setClosing(false);
|
|
1459
|
-
this.#watching = this.#watchNewDeliveries();
|
|
1460
|
-
this.#loop = this.#runLoop();
|
|
1859
|
+
await this.#reschedule(registration, { delivery, error });
|
|
1461
1860
|
}
|
|
1462
1861
|
/**
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
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.
|
|
1465
1868
|
*/
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
this.#requestClaim();
|
|
1472
|
-
return;
|
|
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 });
|
|
1473
1874
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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 })
|
|
1480
1885
|
});
|
|
1481
1886
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
this.#
|
|
1485
|
-
|
|
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
|
+
});
|
|
1486
1896
|
}
|
|
1487
|
-
/**
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const
|
|
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;
|
|
1493
1920
|
try {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
notifier.failure("watch", error);
|
|
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 });
|
|
1505
1931
|
}
|
|
1506
1932
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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);
|
|
1961
|
+
const scope = transaction === void 0 ? { event } : { event, transaction };
|
|
1962
|
+
if (controller.signal.aborted) {
|
|
1963
|
+
throw controller.signal.reason;
|
|
1964
|
+
}
|
|
1965
|
+
const handle = () => runInIsolatedScope(scope, () => registration.handle(event, context));
|
|
1966
|
+
const { observation } = this.#dependencies;
|
|
1967
|
+
const work = Promise.resolve().then(
|
|
1968
|
+
observation === void 0 ? handle : () => observation.process({ registration, delivery }, handle)
|
|
1969
|
+
);
|
|
1970
|
+
work.catch(() => void 0);
|
|
1971
|
+
const cancelTimeout = this.#startTimeout(registration, controller);
|
|
1512
1972
|
try {
|
|
1513
|
-
|
|
1973
|
+
const settled = Promise.race([work, rejectOnAbort(controller.signal)]);
|
|
1974
|
+
await (observation === void 0 ? settled : observation.observe({ registration, delivery }, settled));
|
|
1514
1975
|
} catch (error) {
|
|
1515
|
-
this.#
|
|
1976
|
+
await this.#awaitAbandonedHandler(work, {
|
|
1977
|
+
reason: error,
|
|
1978
|
+
transactional: transaction !== void 0
|
|
1979
|
+
});
|
|
1980
|
+
throw error;
|
|
1981
|
+
} finally {
|
|
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;
|
|
1516
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
|
+
});
|
|
1517
2001
|
}
|
|
1518
2002
|
/**
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
2003
|
+
* After an abort the handler may still be running. Waiting for it keeps a
|
|
2004
|
+
* retry from overlapping the abandoned run, and keeps a transactional
|
|
2005
|
+
* handler from writing through a transaction that was already released.
|
|
2006
|
+
* Transactional handlers are always awaited; others for one lease duration,
|
|
2007
|
+
* except at shutdown, which already waited.
|
|
1523
2008
|
*/
|
|
1524
|
-
async
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
2009
|
+
async #awaitAbandonedHandler(work, { reason, transactional }) {
|
|
2010
|
+
const settledWork = work.then(
|
|
2011
|
+
() => void 0,
|
|
2012
|
+
() => void 0
|
|
2013
|
+
);
|
|
2014
|
+
if (transactional) {
|
|
2015
|
+
await settledWork;
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
if (reason instanceof DispatcherStoppedError) {
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
const deadline = Date.now() + this.#dependencies.settings.leaseDurationMs;
|
|
2022
|
+
await waitUntil(this.#dependencies.clock, { work: settledWork, deadline });
|
|
2023
|
+
}
|
|
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;
|
|
1532
2067
|
}
|
|
1533
2068
|
}
|
|
2069
|
+
return count;
|
|
1534
2070
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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);
|
|
1539
2078
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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;
|
|
1557
2099
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
+
});
|
|
1561
2107
|
}
|
|
1562
|
-
/**
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
#abandon() {
|
|
1569
|
-
this.#abandoned = true;
|
|
1570
|
-
for (const stopKeeping of this.#leaseKeepers.values()) {
|
|
1571
|
-
stopKeeping();
|
|
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);
|
|
1572
2114
|
}
|
|
1573
|
-
this.#leaseKeepers.clear();
|
|
1574
|
-
this.#timers.cancelAll();
|
|
1575
|
-
this.#recoveryTimer?.();
|
|
1576
|
-
this.#recoveryTimer = void 0;
|
|
1577
|
-
this.#pending.clear();
|
|
1578
|
-
return this.#inFlight.size;
|
|
1579
2115
|
}
|
|
1580
2116
|
/**
|
|
1581
2117
|
* Gives the settlements still pending one last try and forgets them. A pass of the timer may be inside the
|
|
1582
2118
|
* store: it ends before the last pass starts. A store that does not answer before the deadline gets no
|
|
1583
2119
|
* further call, since it may be closed right after.
|
|
1584
2120
|
*/
|
|
1585
|
-
async
|
|
1586
|
-
this.#
|
|
1587
|
-
this.#recoveryTimer = void 0;
|
|
2121
|
+
async close(deadline) {
|
|
2122
|
+
this.#stopTimer();
|
|
1588
2123
|
const lastTry = (async () => {
|
|
1589
2124
|
await this.#recovery;
|
|
1590
2125
|
await this.#recoverSettlements();
|
|
@@ -1592,6 +2127,15 @@ var Dispatcher = class {
|
|
|
1592
2127
|
await deadline.race(lastTry);
|
|
1593
2128
|
this.#pending.clear();
|
|
1594
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
|
+
}
|
|
1595
2139
|
/**
|
|
1596
2140
|
* Retries every pending settlement once; a pass that is already running is the one returned. A store that
|
|
1597
2141
|
* still fails leaves the settlement for the next attempt (it was reported when it first failed).
|
|
@@ -1609,7 +2153,7 @@ var Dispatcher = class {
|
|
|
1609
2153
|
}
|
|
1610
2154
|
try {
|
|
1611
2155
|
const applied = await entry.settle();
|
|
1612
|
-
this.#pending.delete(
|
|
2156
|
+
this.#pending.delete(pendingKeyOf(entry.delivery));
|
|
1613
2157
|
if (applied) {
|
|
1614
2158
|
entry.applied();
|
|
1615
2159
|
} else {
|
|
@@ -1619,671 +2163,888 @@ var Dispatcher = class {
|
|
|
1619
2163
|
}
|
|
1620
2164
|
}
|
|
1621
2165
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
}
|
|
1643
|
-
/** Claims one batch and resolves when every claimed delivery settled. Returns how many ran. */
|
|
1644
|
-
async processBatch() {
|
|
1645
|
-
const { started } = await this.#claimAndStart();
|
|
1646
|
-
await Promise.allSettled(started.map((delivery) => delivery.settled));
|
|
1647
|
-
return started.length;
|
|
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;
|
|
1648
2186
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
});
|
|
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
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
1656
2202
|
});
|
|
1657
|
-
const outcome = await Promise.race([work.then(() => "done"), timedOut]);
|
|
1658
|
-
cancel?.();
|
|
1659
|
-
return outcome;
|
|
1660
2203
|
}
|
|
1661
2204
|
/**
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
* deliveries settling to refill a group of slots) or the poll interval. Only the poll interval claims
|
|
1665
|
-
* for fewer free slots than the refill threshold, so a busy dispatcher never claims slot by slot.
|
|
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.
|
|
1666
2207
|
*/
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
if (this.#freeSlots() >= minimumFree) {
|
|
1672
|
-
try {
|
|
1673
|
-
round = await this.#claimAndStart();
|
|
1674
|
-
} catch (error) {
|
|
1675
|
-
this.#dependencies.notifier.failure("claim", error);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
if (round.requested > 0 && round.claimed === round.requested && this.#freeSlots() >= this.#refillThreshold) {
|
|
1679
|
-
minimumFree = this.#refillThreshold;
|
|
1680
|
-
continue;
|
|
1681
|
-
}
|
|
1682
|
-
minimumFree = await this.#waitForWakeUp();
|
|
1683
|
-
if (this.#freeSlots() < minimumFree) {
|
|
1684
|
-
this.#requestClaim();
|
|
1685
|
-
}
|
|
2208
|
+
setClosing(closing) {
|
|
2209
|
+
this.#closing = closing;
|
|
2210
|
+
if (closing && this.#pending.length > 0) {
|
|
2211
|
+
this.#flush();
|
|
1686
2212
|
}
|
|
1687
2213
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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();
|
|
1705
2253
|
}
|
|
1706
|
-
const minimumFree = this.#signalledMinimum;
|
|
1707
|
-
this.#signalledMinimum = void 0;
|
|
1708
|
-
return minimumFree;
|
|
1709
2254
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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();
|
|
2264
|
+
},
|
|
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;
|
|
1718
2304
|
}
|
|
2305
|
+
return new CompletionBatcher(
|
|
2306
|
+
{ completeMany: (leases, now) => store.completeMany(leases, now) },
|
|
2307
|
+
clock,
|
|
2308
|
+
{ windowMs, maxBatchSize },
|
|
2309
|
+
() => this.#inFlight.size
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
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;
|
|
1719
2337
|
}
|
|
1720
2338
|
/**
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
* claim started for a short while. With no free slot the next settlement asks again.
|
|
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.
|
|
1724
2341
|
*/
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
this.#refillScheduled = false;
|
|
1732
|
-
this.#signal(this.#refillThreshold);
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1735
|
-
} else if (free > 0) {
|
|
1736
|
-
this.#quietRefillTimer ??= schedule(this.#dependencies.clock, {
|
|
1737
|
-
run: () => {
|
|
1738
|
-
this.#quietRefillTimer = void 0;
|
|
1739
|
-
this.#signal(1);
|
|
1740
|
-
},
|
|
1741
|
-
delayMs: Math.max(0, this.#lastClaimStartedAt + QUIET_REFILL_MS - Date.now())
|
|
1742
|
-
});
|
|
2342
|
+
get watchesNewDeliveries() {
|
|
2343
|
+
return this.#unwatch !== void 0;
|
|
2344
|
+
}
|
|
2345
|
+
start() {
|
|
2346
|
+
if (this.#loop.isRunning) {
|
|
2347
|
+
return;
|
|
1743
2348
|
}
|
|
2349
|
+
this.#loop.open();
|
|
2350
|
+
this.#completions?.setClosing(false);
|
|
2351
|
+
this.#watching = this.#watchNewDeliveries();
|
|
2352
|
+
this.#looping = this.#loop.run();
|
|
1744
2353
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
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();
|
|
1747
2360
|
}
|
|
1748
|
-
/**
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
const
|
|
1756
|
-
this.#
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
requested += request.limit;
|
|
1760
|
-
const claim = {
|
|
1761
|
-
handlers: request.handlers,
|
|
1762
|
-
limit: request.limit,
|
|
1763
|
-
leaseOwner: settings.owner,
|
|
1764
|
-
leaseExpiresAt: new Date(now.getTime() + settings.leaseDurationMs),
|
|
1765
|
-
now
|
|
1766
|
-
};
|
|
1767
|
-
const claimed = await (observation === void 0 ? store.claim(claim) : observation.claim(store, claim));
|
|
1768
|
-
for (const handler of request.handlers) {
|
|
1769
|
-
if (claimed.length < request.limit && wakeGeneration === this.#wakeGeneration) {
|
|
1770
|
-
this.#exhaustedHandlers.add(handler);
|
|
1771
|
-
} else {
|
|
1772
|
-
this.#exhaustedHandlers.delete(handler);
|
|
1773
|
-
}
|
|
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;
|
|
1774
2372
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
2373
|
+
this.#unwatch = unwatch;
|
|
2374
|
+
if (unwatch !== void 0) {
|
|
2375
|
+
this.wake();
|
|
1777
2376
|
}
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
notifier.failure("watch", error);
|
|
1778
2379
|
}
|
|
1779
|
-
return { requested, claimed: started.length, started };
|
|
1780
2380
|
}
|
|
1781
|
-
/**
|
|
1782
|
-
#
|
|
1783
|
-
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
const room = registration.concurrency - this.#countInFlight(registration);
|
|
1791
|
-
const limit = Math.min(room, free);
|
|
1792
|
-
if (limit > 0) {
|
|
1793
|
-
plans.push({ handlers: [...registration.claimIds], limit });
|
|
1794
|
-
free -= limit;
|
|
1795
|
-
}
|
|
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);
|
|
1796
2390
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
+
}
|
|
1802
2407
|
}
|
|
1803
|
-
return plans;
|
|
1804
2408
|
}
|
|
1805
|
-
#
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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();
|
|
1810
2424
|
}
|
|
1811
2425
|
}
|
|
1812
|
-
|
|
2426
|
+
await this.#recovery.close(deadline);
|
|
2427
|
+
await deadline.race(this.#relinquish());
|
|
2428
|
+
return 0;
|
|
1813
2429
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
};
|
|
1827
|
-
this.#inFlight.add(entry);
|
|
1828
|
-
return entry;
|
|
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;
|
|
1829
2442
|
}
|
|
1830
|
-
/**
|
|
1831
|
-
async #
|
|
1832
|
-
const {
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
this.#pending.delete(key);
|
|
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);
|
|
1838
2450
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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 '-'`
|
|
1847
2471
|
);
|
|
1848
|
-
return false;
|
|
1849
2472
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
} else {
|
|
1878
|
-
await this.#fail(registration, { delivery, error });
|
|
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`);
|
|
1879
2500
|
}
|
|
1880
|
-
return false;
|
|
1881
|
-
} finally {
|
|
1882
|
-
stopHeartbeat();
|
|
1883
|
-
this.#leaseKeepers.delete(delivery);
|
|
1884
2501
|
}
|
|
1885
2502
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
registration
|
|
1894
|
-
|
|
1895
|
-
);
|
|
1896
|
-
const event = { ...delivery.event, payload };
|
|
1897
|
-
if (registration.transactional) {
|
|
1898
|
-
await store.transaction(async (transaction) => {
|
|
1899
|
-
await this.#invoke({ registration, event, delivery, controller, transaction });
|
|
1900
|
-
if (!await store.complete(delivery, clock.now(), transaction)) {
|
|
1901
|
-
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, {
|
|
1902
|
-
storeConfirmed: true
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
});
|
|
1906
|
-
return "completed";
|
|
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);
|
|
1907
2512
|
}
|
|
1908
|
-
await this.#invoke({ registration, event, delivery, controller, transaction: void 0 });
|
|
1909
|
-
return this.#confirmCompletion(delivery);
|
|
1910
2513
|
}
|
|
1911
|
-
/**
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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) {
|
|
1922
2554
|
try {
|
|
1923
|
-
|
|
2555
|
+
observer[method]?.call(
|
|
2556
|
+
observer,
|
|
2557
|
+
argument
|
|
2558
|
+
);
|
|
1924
2559
|
} catch (error) {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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) {
|
|
1931
2582
|
continue;
|
|
1932
2583
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2584
|
+
try {
|
|
2585
|
+
observer.onFailure?.({ stage: "observer", error: errorInfo });
|
|
2586
|
+
} catch (observerError) {
|
|
2587
|
+
this.#logObserverFailure(observerError);
|
|
1937
2588
|
}
|
|
1938
|
-
return "completed";
|
|
1939
2589
|
}
|
|
1940
2590
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2591
|
+
#logObserverFailure(error) {
|
|
2592
|
+
const errorInfo = toErrorInfo(error);
|
|
2593
|
+
this.#logger.warn(
|
|
2594
|
+
`[Notifier](logObserverFailure) relaybus observer threw | ${errorInfo.name}: ${errorInfo.message}`
|
|
2595
|
+
);
|
|
1945
2596
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
transaction
|
|
1955
|
-
};
|
|
1956
|
-
const scope = transaction === void 0 ? { event } : { event, transaction };
|
|
1957
|
-
if (controller.signal.aborted) {
|
|
1958
|
-
throw controller.signal.reason;
|
|
1959
|
-
}
|
|
1960
|
-
const handle = () => runInIsolatedScope(scope, () => registration.handle(event, context));
|
|
1961
|
-
const { observation } = this.#dependencies;
|
|
1962
|
-
const work = Promise.resolve().then(
|
|
1963
|
-
observation === void 0 ? handle : () => observation.process({ registration, delivery }, handle)
|
|
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
|
+
)
|
|
1964
2605
|
);
|
|
1965
|
-
work.catch(() => void 0);
|
|
1966
|
-
let cancelTimeout;
|
|
1967
|
-
if (registration.timeoutMs !== null) {
|
|
1968
|
-
const timeoutMs = registration.timeoutMs;
|
|
1969
|
-
cancelTimeout = this.#timers.start({
|
|
1970
|
-
run: () => {
|
|
1971
|
-
controller.abort(
|
|
1972
|
-
new HandlerTimeoutError(
|
|
1973
|
-
`Handler '${registration.id}' did not settle within ${timeoutMs}ms`
|
|
1974
|
-
)
|
|
1975
|
-
);
|
|
1976
|
-
},
|
|
1977
|
-
delayMs: timeoutMs
|
|
1978
|
-
});
|
|
1979
|
-
}
|
|
1980
|
-
try {
|
|
1981
|
-
const settled = Promise.race([work, rejectOnAbort(controller.signal)]);
|
|
1982
|
-
await (observation === void 0 ? settled : observation.observe({ registration, delivery }, settled));
|
|
1983
|
-
} catch (error) {
|
|
1984
|
-
await this.#awaitAbandonedHandler(work, {
|
|
1985
|
-
reason: error,
|
|
1986
|
-
transactional: transaction !== void 0
|
|
1987
|
-
});
|
|
1988
|
-
throw error;
|
|
1989
|
-
} finally {
|
|
1990
|
-
cancelTimeout?.();
|
|
1991
|
-
}
|
|
1992
2606
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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)}'`
|
|
2004
2625
|
);
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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 });
|
|
2011
2710
|
}
|
|
2012
|
-
const deadline = Date.now() + this.#dependencies.settings.leaseDurationMs;
|
|
2013
|
-
await this.#waitUntil(settledWork, deadline);
|
|
2014
2711
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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`);
|
|
2035
2795
|
}
|
|
2036
2796
|
}
|
|
2037
|
-
return schedule(clock, {
|
|
2038
|
-
run: () => {
|
|
2039
|
-
controller.abort(new LeaseLostError(`Lease on delivery ${delivery.deliveryId} expired`));
|
|
2040
|
-
},
|
|
2041
|
-
delayMs: Math.max(0, expiry - clock.now().getTime()),
|
|
2042
|
-
unref: true
|
|
2043
|
-
});
|
|
2044
2797
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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
|
+
}
|
|
2050
2804
|
}
|
|
2051
|
-
|
|
2805
|
+
return true;
|
|
2052
2806
|
}
|
|
2053
|
-
/**
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
const
|
|
2060
|
-
|
|
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)
|
|
2061
2820
|
);
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
delivery,
|
|
2069
|
-
settle: () => store.reschedule(delivery, availableAt, errorInfo, now),
|
|
2070
|
-
applied: () => notifier.notify("onDeliveryRetryScheduled", { delivery, error: errorInfo, availableAt })
|
|
2071
|
-
});
|
|
2072
|
-
return;
|
|
2073
|
-
}
|
|
2074
|
-
if (rescheduled) {
|
|
2075
|
-
notifier.notify("onDeliveryRetryScheduled", { delivery, error: errorInfo, availableAt });
|
|
2076
|
-
} else {
|
|
2077
|
-
notifier.notify("onLeaseLost", { delivery });
|
|
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;
|
|
2078
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
|
+
};
|
|
2079
2840
|
}
|
|
2080
|
-
|
|
2081
|
-
const
|
|
2082
|
-
|
|
2083
|
-
let buried;
|
|
2084
|
-
try {
|
|
2085
|
-
buried = await store.bury(delivery, errorInfo, clock.now());
|
|
2086
|
-
} catch (storeError) {
|
|
2087
|
-
notifier.failure("bury", storeError);
|
|
2088
|
-
this.#retain({
|
|
2089
|
-
delivery,
|
|
2090
|
-
settle: () => store.bury(delivery, errorInfo, clock.now()),
|
|
2091
|
-
applied: () => notifier.notify("onDeliveryDead", { delivery, error: errorInfo })
|
|
2092
|
-
});
|
|
2093
|
-
return;
|
|
2841
|
+
#registerRoutes(routes) {
|
|
2842
|
+
for (const route of routes) {
|
|
2843
|
+
this.#requireTransport(route.transport);
|
|
2094
2844
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
} else {
|
|
2098
|
-
notifier.notify("onLeaseLost", { delivery });
|
|
2845
|
+
for (const group of groupRoutes(routes)) {
|
|
2846
|
+
this.#registerRoute(group);
|
|
2099
2847
|
}
|
|
2100
2848
|
}
|
|
2101
|
-
/**
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
+
);
|
|
2115
2868
|
}
|
|
2116
|
-
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
released = await store.release(delivery, clock.now());
|
|
2121
|
-
} catch (storeError) {
|
|
2122
|
-
notifier.failure("release", storeError);
|
|
2123
|
-
this.#retain({
|
|
2124
|
-
delivery,
|
|
2125
|
-
settle: () => store.release(delivery, clock.now()),
|
|
2126
|
-
applied: () => notifier.notify("onDeliveryReleased", { delivery })
|
|
2127
|
-
});
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
if (released) {
|
|
2131
|
-
notifier.notify("onDeliveryReleased", { delivery });
|
|
2132
|
-
} else {
|
|
2133
|
-
notifier.notify("onLeaseLost", { delivery });
|
|
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`);
|
|
2134
2873
|
}
|
|
2874
|
+
return transport;
|
|
2135
2875
|
}
|
|
2136
2876
|
};
|
|
2137
2877
|
|
|
2138
|
-
// src/
|
|
2139
|
-
var
|
|
2140
|
-
|
|
2141
|
-
#
|
|
2142
|
-
#
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
const { options } = input;
|
|
2146
|
-
if (!HANDLER_ID_PATTERN.test(options.id)) {
|
|
2147
|
-
throw new ConfigurationError(
|
|
2148
|
-
`Handler id '${options.id}' must be 1-255 characters of letters, digits, '.', '_', ':', '/' or '-'`
|
|
2149
|
-
);
|
|
2150
|
-
}
|
|
2151
|
-
const claimIds = [options.id, ...input.legacyIds ?? []];
|
|
2152
|
-
this.#assertUnregistered(claimIds);
|
|
2153
|
-
if (options.concurrency !== void 0) {
|
|
2154
|
-
assertPositiveInteger(options.concurrency, `Concurrency of handler '${options.id}'`);
|
|
2155
|
-
}
|
|
2156
|
-
const registration = {
|
|
2157
|
-
id: options.id,
|
|
2158
|
-
eventTypes: input.eventTypes,
|
|
2159
|
-
claimIds,
|
|
2160
|
-
schema: input.schema,
|
|
2161
|
-
handle: input.handle,
|
|
2162
|
-
retry: createRetryPolicy(options.retry, defaults.retry),
|
|
2163
|
-
timeoutMs: resolveTimeout(options.timeout, {
|
|
2164
|
-
fallback: defaults.timeoutMs,
|
|
2165
|
-
handlerId: options.id
|
|
2166
|
-
}),
|
|
2167
|
-
concurrency: options.concurrency ?? null,
|
|
2168
|
-
transactional: options.transactional ?? false,
|
|
2169
|
-
forwardsToTransport: input.forwardsToTransport
|
|
2170
|
-
};
|
|
2171
|
-
this.#index(registration);
|
|
2172
|
-
return registration;
|
|
2878
|
+
// src/bus/bus-maintenance.ts
|
|
2879
|
+
var BusMaintenance = class {
|
|
2880
|
+
#parts;
|
|
2881
|
+
#retentionTimer;
|
|
2882
|
+
#backlogTimer;
|
|
2883
|
+
constructor(parts) {
|
|
2884
|
+
this.#parts = parts;
|
|
2173
2885
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
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;
|
|
2891
|
+
try {
|
|
2892
|
+
const result = await store.purge({
|
|
2893
|
+
...completedMs === null ? {} : { completedBefore: new Date(now - completedMs) },
|
|
2894
|
+
...deadMs === null ? {} : { deadBefore: new Date(now - deadMs) }
|
|
2895
|
+
});
|
|
2896
|
+
notifier.notify("onPurged", { result });
|
|
2897
|
+
return result;
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
notifier.failure("purge", error);
|
|
2900
|
+
throw error;
|
|
2179
2901
|
}
|
|
2180
2902
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
}
|
|
2186
|
-
for (const eventType of registration.eventTypes) {
|
|
2187
|
-
const forType = this.#byEventType.get(eventType) ?? [];
|
|
2188
|
-
forType.push(registration);
|
|
2189
|
-
this.#byEventType.set(eventType, forType);
|
|
2190
|
-
}
|
|
2903
|
+
/** Starts the retention purge and the backlog report, each when it has anything to do. */
|
|
2904
|
+
start() {
|
|
2905
|
+
this.#scheduleRetention();
|
|
2906
|
+
this.#scheduleBacklog();
|
|
2191
2907
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2908
|
+
stop() {
|
|
2909
|
+
this.#retentionTimer?.();
|
|
2910
|
+
this.#backlogTimer?.();
|
|
2195
2911
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
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
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2198
2925
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
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 });
|
|
2931
|
+
}
|
|
2201
2932
|
}
|
|
2202
2933
|
};
|
|
2203
|
-
function resolveTimeout(timeout, { fallback, handlerId }) {
|
|
2204
|
-
if (timeout === void 0) {
|
|
2205
|
-
return fallback;
|
|
2206
|
-
}
|
|
2207
|
-
if (timeout === false) {
|
|
2208
|
-
return null;
|
|
2209
|
-
}
|
|
2210
|
-
return toTimerMilliseconds(timeout, `Timeout of handler '${handlerId}'`);
|
|
2211
|
-
}
|
|
2212
2934
|
|
|
2213
|
-
// src/
|
|
2214
|
-
var
|
|
2215
|
-
#
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
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) })
|
|
2224
2950
|
);
|
|
2225
2951
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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 });
|
|
2964
|
+
}
|
|
2965
|
+
return entries.map((entry) => entry.event);
|
|
2229
2966
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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);
|
|
2239
2982
|
}
|
|
2983
|
+
entries.push({
|
|
2984
|
+
event: envelope,
|
|
2985
|
+
deliveries: registrations.map((registration) => createDelivery(registration, availableAt))
|
|
2986
|
+
});
|
|
2240
2987
|
}
|
|
2988
|
+
return entries;
|
|
2241
2989
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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();
|
|
2252
3005
|
}
|
|
2253
3006
|
}
|
|
2254
3007
|
}
|
|
2255
|
-
#
|
|
2256
|
-
this.#
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
this.#logObserverFailure(observerError);
|
|
2266
|
-
}
|
|
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);
|
|
3013
|
+
}
|
|
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
|
+
);
|
|
2267
3018
|
}
|
|
3019
|
+
return store.transaction((ownTransaction) => store.append(ownTransaction, entries, now));
|
|
2268
3020
|
}
|
|
2269
|
-
#
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
3021
|
+
async #createEnvelope(input) {
|
|
3022
|
+
const { event, options, id, scope, now, correlationId } = input;
|
|
3023
|
+
if (typeof event.type !== "string") {
|
|
3024
|
+
throw new ConfigurationError(
|
|
3025
|
+
"publish expects an instance of a class created with defineEvent"
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
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
|
|
3045
|
+
};
|
|
2274
3046
|
}
|
|
2275
3047
|
};
|
|
2276
|
-
function hookNames(observer) {
|
|
2277
|
-
const names = [];
|
|
2278
|
-
for (let target = observer; target !== null && target !== Object.prototype; target = Object.getPrototypeOf(target)) {
|
|
2279
|
-
names.push(
|
|
2280
|
-
...Object.getOwnPropertyNames(target).filter(
|
|
2281
|
-
(name) => typeof observer[name] === "function"
|
|
2282
|
-
)
|
|
2283
|
-
);
|
|
2284
|
-
}
|
|
2285
|
-
return names;
|
|
2286
|
-
}
|
|
2287
3048
|
|
|
2288
3049
|
// src/received-envelope.ts
|
|
2289
3050
|
function normalizeEnvelope(envelope) {
|
|
@@ -2321,165 +3082,126 @@ function toDate(value) {
|
|
|
2321
3082
|
function optionalText(value) {
|
|
2322
3083
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2323
3084
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
3085
|
+
|
|
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";
|
|
3104
|
+
}
|
|
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)
|
|
2340
3112
|
);
|
|
3113
|
+
const status = result.duplicateEventIds.length > 0 ? "duplicate" : "accepted";
|
|
3114
|
+
notifier.notify("onReceived", { envelope: normalized, status });
|
|
3115
|
+
if (result.appendedDeliveryCount > 0) {
|
|
3116
|
+
scheduleWake();
|
|
3117
|
+
}
|
|
3118
|
+
return status;
|
|
2341
3119
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
);
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
);
|
|
2368
|
-
return {
|
|
2369
|
-
owner: `${hostname()}:${process.pid}:${generateIdentifier()}`,
|
|
2370
|
-
concurrency,
|
|
2371
|
-
batchSize: assertPositiveInteger(dispatcher.batchSize ?? concurrency, "dispatcher.batchSize"),
|
|
2372
|
-
...createDispatcherTimings(dispatcher)
|
|
2373
|
-
};
|
|
2374
|
-
}
|
|
2375
|
-
function createDispatcherTimings(dispatcher) {
|
|
2376
|
-
return {
|
|
2377
|
-
pollIntervalMs: toTimerMilliseconds(
|
|
2378
|
-
dispatcher.pollInterval ?? DEFAULTS.pollInterval,
|
|
2379
|
-
"dispatcher.pollInterval"
|
|
2380
|
-
),
|
|
2381
|
-
leaseDurationMs: toTimerMilliseconds(
|
|
2382
|
-
dispatcher.leaseDuration ?? DEFAULTS.leaseDuration,
|
|
2383
|
-
"dispatcher.leaseDuration"
|
|
2384
|
-
),
|
|
2385
|
-
shutdownTimeoutMs: toTimerMilliseconds(
|
|
2386
|
-
dispatcher.shutdownTimeout ?? DEFAULTS.shutdownTimeout,
|
|
2387
|
-
"dispatcher.shutdownTimeout"
|
|
2388
|
-
),
|
|
2389
|
-
completionBatchWindowMs: toMilliseconds(
|
|
2390
|
-
dispatcher.completionBatchWindow ?? DEFAULTS.completionBatchWindow,
|
|
2391
|
-
"dispatcher.completionBatchWindow"
|
|
2392
|
-
),
|
|
2393
|
-
completionBatchSize: assertPositiveInteger(
|
|
2394
|
-
dispatcher.completionBatchSize ?? DEFAULTS.completionBatchSize,
|
|
2395
|
-
"dispatcher.completionBatchSize"
|
|
2396
|
-
)
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
function createRetentionSettings(options) {
|
|
2400
|
-
const completed = options?.completed ?? DEFAULTS.completedRetention;
|
|
2401
|
-
const dead = options?.dead ?? false;
|
|
3120
|
+
};
|
|
3121
|
+
|
|
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
|
+
});
|
|
2402
3146
|
return {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
3147
|
+
...core,
|
|
3148
|
+
maintenance,
|
|
3149
|
+
connections,
|
|
3150
|
+
catalogRequirements,
|
|
3151
|
+
dispatcherEnabled,
|
|
3152
|
+
shutdownTimeoutMs: settings.shutdownTimeoutMs,
|
|
3153
|
+
dispatcher,
|
|
3154
|
+
...createInboxAndOutbox(core, hooks)
|
|
2409
3155
|
};
|
|
2410
3156
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
}
|
|
2427
|
-
return [...groups.values()];
|
|
2428
|
-
}
|
|
2429
|
-
function createGroup(route, {
|
|
2430
|
-
id,
|
|
2431
|
-
destination,
|
|
2432
|
-
type
|
|
2433
|
-
}) {
|
|
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;
|
|
2434
3172
|
return {
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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
|
|
2442
3187
|
};
|
|
2443
3188
|
}
|
|
2444
|
-
function
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
if (route.transport !== group.transport) {
|
|
2451
|
-
throw new ConfigurationError(
|
|
2452
|
-
`Routes to ${target} and to '${destination}' of transport '${route.transport}' would share the handler id '${group.id}'; rename the transport or the destination`
|
|
2453
|
-
);
|
|
2454
|
-
}
|
|
2455
|
-
if (group.eventTypes.includes(type)) {
|
|
2456
|
-
throw new ConfigurationError(`Event type ${type} is routed to ${target} twice`);
|
|
2457
|
-
}
|
|
2458
|
-
const { retry, timeoutMs } = resolveSettings(route, group.id);
|
|
2459
|
-
if (timeoutMs !== group.timeoutMs || !sameRetryPolicy(retry, group.retry)) {
|
|
2460
|
-
throw new ConfigurationError(
|
|
2461
|
-
`Routes to ${target} must share retry and timeout, because their events are sent in one queue per key | types=${group.eventTypes[0]},${type}`
|
|
2462
|
-
);
|
|
2463
|
-
}
|
|
2464
|
-
group.eventTypes.push(type);
|
|
2465
|
-
group.legacyIds.push(`${group.id}:${type}`);
|
|
2466
|
-
}
|
|
2467
|
-
function resolveSettings(route, id) {
|
|
3189
|
+
function createInboxAndOutbox(core, hooks) {
|
|
3190
|
+
const createDelivery = (registration, availableAt) => ({
|
|
3191
|
+
id: core.generateIdentifier(),
|
|
3192
|
+
handler: registration.id,
|
|
3193
|
+
availableAt
|
|
3194
|
+
});
|
|
2468
3195
|
return {
|
|
2469
|
-
|
|
2470
|
-
|
|
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 })
|
|
2471
3203
|
};
|
|
2472
3204
|
}
|
|
2473
|
-
function sameRetryPolicy(left, right) {
|
|
2474
|
-
return Object.keys(left).every(
|
|
2475
|
-
(field) => left[field] === right[field]
|
|
2476
|
-
);
|
|
2477
|
-
}
|
|
2478
|
-
function routedType(event) {
|
|
2479
|
-
const type = typeof event === "string" ? event : event.type;
|
|
2480
|
-
assertEventType(type);
|
|
2481
|
-
return type;
|
|
2482
|
-
}
|
|
2483
3205
|
|
|
2484
3206
|
// src/transaction-budget.ts
|
|
2485
3207
|
async function runBudgetedTransaction(store, { clock, timeoutMs, work }) {
|
|
@@ -2512,125 +3234,36 @@ function createTimeoutError(timeoutMs, cause) {
|
|
|
2512
3234
|
}
|
|
2513
3235
|
|
|
2514
3236
|
// src/relay-bus.ts
|
|
2515
|
-
var DEFAULT_DRAIN_BATCHES = 1e3;
|
|
2516
|
-
var WAKE_DELAY_MS = 25;
|
|
2517
|
-
var RelayBus = class {
|
|
2518
|
-
#
|
|
2519
|
-
#
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
#clock;
|
|
2531
|
-
#generateIdentifier;
|
|
2532
|
-
#dispatcher;
|
|
2533
|
-
#dispatcherEnabled;
|
|
2534
|
-
#catalogRequirements = [];
|
|
2535
|
-
#setupStore;
|
|
2536
|
-
#retention;
|
|
2537
|
-
#retentionTimer;
|
|
2538
|
-
#observation;
|
|
2539
|
-
#backlogIntervalMs;
|
|
2540
|
-
#backlogTimer;
|
|
2541
|
-
#shutdownTimeoutMs;
|
|
2542
|
-
#state = "idle";
|
|
2543
|
-
#starting;
|
|
2544
|
-
#stopping;
|
|
2545
|
-
constructor(options) {
|
|
2546
|
-
this.#store = options.store;
|
|
2547
|
-
this.#source = options.source ?? "relaybus";
|
|
2548
|
-
this.#transactionMode = oneOf(options.transactionMode ?? "required", {
|
|
2549
|
-
allowed: ["required", "optional"],
|
|
2550
|
-
name: "transactionMode"
|
|
2551
|
-
});
|
|
2552
|
-
this.#unroutedEvents = oneOf(options.unroutedEvents ?? "warn", {
|
|
2553
|
-
allowed: ["warn", "throw"],
|
|
2554
|
-
name: "unroutedEvents"
|
|
2555
|
-
});
|
|
2556
|
-
this.#transactionResolver = options.transactionResolver;
|
|
2557
|
-
this.#transactionTimeoutMs = resolveTransactionTimeout(options);
|
|
2558
|
-
this.#defaults = createHandlerDefaults(options);
|
|
2559
|
-
this.#logger = options.logger ?? console;
|
|
2560
|
-
this.#notifier = new Notifier(options.observers ?? [], this.#logger);
|
|
2561
|
-
this.#clock = options.clock ?? systemClock;
|
|
2562
|
-
this.#observation = BusObservation.create({
|
|
2563
|
-
notifier: this.#notifier,
|
|
2564
|
-
tracer: options.tracer,
|
|
2565
|
-
clock: this.#clock
|
|
2566
|
-
});
|
|
2567
|
-
this.#backlogIntervalMs = resolveBacklogInterval(options);
|
|
2568
|
-
this.#generateIdentifier = options.generateIdentifier ?? (() => createUuidV7());
|
|
2569
|
-
this.#setupStore = options.setupStore ?? true;
|
|
2570
|
-
this.#retention = createRetentionSettings(options.retention);
|
|
2571
|
-
this.#subscriptions = options.subscriptions ?? [];
|
|
2572
|
-
this.#registerConnections(options);
|
|
2573
|
-
this.#dispatcherEnabled = isDispatcherEnabled(options);
|
|
2574
|
-
const settings = createDispatcherSettings(options.dispatcher, this.#generateIdentifier);
|
|
2575
|
-
this.#shutdownTimeoutMs = settings.shutdownTimeoutMs;
|
|
2576
|
-
this.#dispatcher = this.#createDispatcher(settings, options);
|
|
2577
|
-
}
|
|
2578
|
-
#createDispatcher(settings, options) {
|
|
2579
|
-
return new Dispatcher({
|
|
2580
|
-
store: this.#store,
|
|
2581
|
-
registry: this.#registry,
|
|
2582
|
-
clock: this.#clock,
|
|
2583
|
-
random: options.random ?? Math.random,
|
|
2584
|
-
notifier: this.#notifier,
|
|
2585
|
-
settings,
|
|
2586
|
-
observation: DispatchObservation.create({
|
|
2587
|
-
notifier: this.#notifier,
|
|
2588
|
-
tracer: options.tracer,
|
|
2589
|
-
clock: this.#clock
|
|
2590
|
-
})
|
|
2591
|
-
});
|
|
2592
|
-
}
|
|
2593
|
-
/** Registers the transports, then the routes, checks the subscriptions and registers the catalogs. */
|
|
2594
|
-
#registerConnections(options) {
|
|
2595
|
-
for (const transport of options.transports ?? []) {
|
|
2596
|
-
if (this.#transports.has(transport.name)) {
|
|
2597
|
-
throw new ConfigurationError(`Transport name '${transport.name}' is used twice`);
|
|
2598
|
-
}
|
|
2599
|
-
this.#transports.set(transport.name, transport);
|
|
2600
|
-
}
|
|
2601
|
-
this.#registerRoutes(options.routes ?? []);
|
|
2602
|
-
for (const subscription of this.#subscriptions) {
|
|
2603
|
-
const transport = this.#requireTransport(subscription.transport);
|
|
2604
|
-
if (transport.subscribe === void 0) {
|
|
2605
|
-
throw new ConfigurationError(`Transport '${transport.name}' cannot subscribe`);
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
for (const catalog of options.catalogs ?? []) {
|
|
2609
|
-
this.registerCatalog(catalog);
|
|
2610
|
-
}
|
|
3237
|
+
var DEFAULT_DRAIN_BATCHES = 1e3;
|
|
3238
|
+
var WAKE_DELAY_MS = 25;
|
|
3239
|
+
var RelayBus = class {
|
|
3240
|
+
#parts;
|
|
3241
|
+
#lifecycle;
|
|
3242
|
+
constructor(options) {
|
|
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)
|
|
3251
|
+
});
|
|
2611
3252
|
}
|
|
2612
3253
|
get store() {
|
|
2613
|
-
return this.#store;
|
|
3254
|
+
return this.#parts.store;
|
|
2614
3255
|
}
|
|
2615
3256
|
get isRunning() {
|
|
2616
|
-
return this.#state === "running";
|
|
3257
|
+
return this.#lifecycle.state === "running";
|
|
2617
3258
|
}
|
|
2618
3259
|
/** True while the bus runs and every transport reports itself ready. */
|
|
2619
3260
|
isReady() {
|
|
2620
|
-
|
|
2621
|
-
return false;
|
|
2622
|
-
}
|
|
2623
|
-
for (const transport of this.#transports.values()) {
|
|
2624
|
-
if (transport.isReady?.() === false) {
|
|
2625
|
-
return false;
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
return true;
|
|
3261
|
+
return this.#lifecycle.state === "running" && this.#parts.connections.allReady();
|
|
2629
3262
|
}
|
|
2630
3263
|
// eslint-disable-next-line relaybus/max-two-params -- public RelayBus.subscribe signature
|
|
2631
3264
|
subscribe(event, handler, options) {
|
|
2632
3265
|
const { type, schema } = describeEvent(event);
|
|
2633
|
-
this.#registry.register(
|
|
3266
|
+
this.#parts.registry.register(
|
|
2634
3267
|
{
|
|
2635
3268
|
eventTypes: [type],
|
|
2636
3269
|
schema,
|
|
@@ -2638,7 +3271,7 @@ var RelayBus = class {
|
|
|
2638
3271
|
options,
|
|
2639
3272
|
forwardsToTransport: false
|
|
2640
3273
|
},
|
|
2641
|
-
this.#defaults
|
|
3274
|
+
this.#parts.defaults
|
|
2642
3275
|
);
|
|
2643
3276
|
}
|
|
2644
3277
|
// eslint-disable-next-line relaybus/max-two-params -- public RelayBus.subscribeCatalog signature
|
|
@@ -2654,7 +3287,7 @@ var RelayBus = class {
|
|
|
2654
3287
|
* option.
|
|
2655
3288
|
*/
|
|
2656
3289
|
registerCatalog(catalog) {
|
|
2657
|
-
this.#catalogRequirements.push(toCatalogRequirement(catalog));
|
|
3290
|
+
this.#parts.catalogRequirements.push(toCatalogRequirement(catalog));
|
|
2658
3291
|
}
|
|
2659
3292
|
/**
|
|
2660
3293
|
* A view of this bus that publishes only the events of `catalog`, or only
|
|
@@ -2666,7 +3299,7 @@ var RelayBus = class {
|
|
|
2666
3299
|
}
|
|
2667
3300
|
/** Registered handlers and routes, for diagnostics. */
|
|
2668
3301
|
handlers() {
|
|
2669
|
-
return this.#registry.all().flatMap(
|
|
3302
|
+
return this.#parts.registry.all().flatMap(
|
|
2670
3303
|
(registration) => registration.eventTypes.map((eventType) => toDescriptor(registration, eventType))
|
|
2671
3304
|
);
|
|
2672
3305
|
}
|
|
@@ -2676,12 +3309,12 @@ var RelayBus = class {
|
|
|
2676
3309
|
* have been written when the id already exists.
|
|
2677
3310
|
*/
|
|
2678
3311
|
async publish(event, options = {}) {
|
|
2679
|
-
const [envelope] = await this.#
|
|
3312
|
+
const [envelope] = await this.#parts.publisher.publish([event], { options, id: options.id });
|
|
2680
3313
|
return envelope;
|
|
2681
3314
|
}
|
|
2682
3315
|
/** Stores several events atomically, in order, sharing one correlation id. */
|
|
2683
3316
|
async publishAll(events, options = {}) {
|
|
2684
|
-
return this.#
|
|
3317
|
+
return this.#parts.publisher.publish(events, { options, id: void 0 });
|
|
2685
3318
|
}
|
|
2686
3319
|
/**
|
|
2687
3320
|
* Runs `work` in a new store transaction. `publish` calls inside it join the
|
|
@@ -2689,9 +3322,9 @@ var RelayBus = class {
|
|
|
2689
3322
|
* and rolled back with a {@link TransactionTimeoutError} when it runs past it.
|
|
2690
3323
|
*/
|
|
2691
3324
|
async transaction(work) {
|
|
2692
|
-
return runBudgetedTransaction(this.#store, {
|
|
2693
|
-
clock: this.#clock,
|
|
2694
|
-
timeoutMs: this.#transactionTimeoutMs,
|
|
3325
|
+
return runBudgetedTransaction(this.#parts.store, {
|
|
3326
|
+
clock: this.#parts.clock,
|
|
3327
|
+
timeoutMs: this.#parts.transactionTimeoutMs,
|
|
2695
3328
|
work: (transaction) => runInScope({ transaction }, () => work(transaction))
|
|
2696
3329
|
});
|
|
2697
3330
|
}
|
|
@@ -2701,7 +3334,7 @@ var RelayBus = class {
|
|
|
2701
3334
|
* `transactionResolver` finds. Repositories use it to write in the same transaction.
|
|
2702
3335
|
*/
|
|
2703
3336
|
currentTransaction() {
|
|
2704
|
-
return getCurrentScope()?.transaction ?? this.#transactionResolver?.();
|
|
3337
|
+
return getCurrentScope()?.transaction ?? this.#parts.transactionResolver?.();
|
|
2705
3338
|
}
|
|
2706
3339
|
/**
|
|
2707
3340
|
* Stores an event received from a broker and schedules it for local handlers.
|
|
@@ -2710,32 +3343,7 @@ var RelayBus = class {
|
|
|
2710
3343
|
* events, which transports should dead-letter instead of redelivering.
|
|
2711
3344
|
*/
|
|
2712
3345
|
receive(envelope) {
|
|
2713
|
-
return this.#
|
|
2714
|
-
}
|
|
2715
|
-
#observedReceive(envelope, origin) {
|
|
2716
|
-
return this.#observation.receive({ envelope, origin }, () => this.#receive(envelope));
|
|
2717
|
-
}
|
|
2718
|
-
async #receive(envelope) {
|
|
2719
|
-
const normalized = normalizeEnvelope(envelope);
|
|
2720
|
-
const registrations = this.#registry.forEventType(normalized.type).filter((registration) => !registration.forwardsToTransport);
|
|
2721
|
-
if (registrations.length === 0) {
|
|
2722
|
-
this.#notifier.notify("onReceived", { envelope: normalized, status: "ignored" });
|
|
2723
|
-
return "ignored";
|
|
2724
|
-
}
|
|
2725
|
-
const now = this.#clock.now();
|
|
2726
|
-
const entry = {
|
|
2727
|
-
event: normalized,
|
|
2728
|
-
deliveries: registrations.map((registration) => this.#createDelivery(registration, now))
|
|
2729
|
-
};
|
|
2730
|
-
const result = await this.#store.transaction(
|
|
2731
|
-
(transaction) => this.#store.append(transaction, [entry], now)
|
|
2732
|
-
);
|
|
2733
|
-
const status = result.duplicateEventIds.length > 0 ? "duplicate" : "accepted";
|
|
2734
|
-
this.#notifier.notify("onReceived", { envelope: normalized, status });
|
|
2735
|
-
if (result.appendedDeliveryCount > 0) {
|
|
2736
|
-
this.#scheduleWake();
|
|
2737
|
-
}
|
|
2738
|
-
return status;
|
|
3346
|
+
return this.#parts.receiver.receive(envelope);
|
|
2739
3347
|
}
|
|
2740
3348
|
/**
|
|
2741
3349
|
* Creates the store schema, connects transports, subscribes and starts
|
|
@@ -2743,18 +3351,13 @@ var RelayBus = class {
|
|
|
2743
3351
|
* stopped for good, because its transports are closed; create a new instance.
|
|
2744
3352
|
*/
|
|
2745
3353
|
async start() {
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
);
|
|
2750
|
-
}
|
|
2751
|
-
assertCatalogsCovered(this.#catalogRequirements, (type) => this.#registrationsOf(type));
|
|
2752
|
-
this.#state = "starting";
|
|
2753
|
-
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());
|
|
2754
3357
|
try {
|
|
2755
|
-
await
|
|
3358
|
+
await starting;
|
|
2756
3359
|
} finally {
|
|
2757
|
-
this.#
|
|
3360
|
+
this.#lifecycle.endStart();
|
|
2758
3361
|
}
|
|
2759
3362
|
}
|
|
2760
3363
|
/**
|
|
@@ -2767,41 +3370,25 @@ var RelayBus = class {
|
|
|
2767
3370
|
* get the result of the first.
|
|
2768
3371
|
*/
|
|
2769
3372
|
async stop(options = {}) {
|
|
2770
|
-
const timeoutMs = options.timeout === void 0 ? this.#shutdownTimeoutMs :
|
|
2771
|
-
|
|
2772
|
-
|
|
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;
|
|
2773
3377
|
}
|
|
2774
|
-
|
|
2775
|
-
return this.#stopOutsideRun(timeoutMs);
|
|
2776
|
-
}
|
|
2777
|
-
this.#stopping = this.#stopComponents(timeoutMs);
|
|
2778
|
-
const result = await this.#stopping;
|
|
3378
|
+
const result = await stopping;
|
|
2779
3379
|
if (!result.graceful) {
|
|
2780
|
-
this.#logger.warn(
|
|
3380
|
+
this.#parts.logger.warn(
|
|
2781
3381
|
`[RelayBus](stop) Shutdown deadline passed, forced close | abandoned=${result.abandonedDeliveries} | timeoutMs=${timeoutMs}`
|
|
2782
3382
|
);
|
|
2783
3383
|
}
|
|
2784
3384
|
return result;
|
|
2785
3385
|
}
|
|
2786
|
-
/**
|
|
2787
|
-
* A bus that is not running: an idle one only settles what batches run by hand left behind (and
|
|
2788
|
-
* stays startable); a stopped one has nothing left to do.
|
|
2789
|
-
*/
|
|
2790
|
-
async #stopOutsideRun(timeoutMs) {
|
|
2791
|
-
if (this.#state !== "idle") {
|
|
2792
|
-
return { graceful: true, abandonedDeliveries: 0, durationMs: 0 };
|
|
2793
|
-
}
|
|
2794
|
-
return withinDeadline(
|
|
2795
|
-
{ clock: this.#clock, timeoutMs },
|
|
2796
|
-
(deadline) => this.#dispatcher.stop(deadline)
|
|
2797
|
-
);
|
|
2798
|
-
}
|
|
2799
3386
|
/**
|
|
2800
3387
|
* Claims and runs one batch of due deliveries, resolving when they settle.
|
|
2801
3388
|
* Returns how many ran. Intended for tests and scripts.
|
|
2802
3389
|
*/
|
|
2803
3390
|
async runOnce() {
|
|
2804
|
-
return this.#dispatcher.processBatch();
|
|
3391
|
+
return this.#parts.dispatcher.processBatch();
|
|
2805
3392
|
}
|
|
2806
3393
|
/**
|
|
2807
3394
|
* Runs batches until nothing is due. Deliveries scheduled in the future
|
|
@@ -2819,325 +3406,85 @@ var RelayBus = class {
|
|
|
2819
3406
|
throw new DrainIncompleteError(`drain did not finish within ${maxBatches} batches`);
|
|
2820
3407
|
}
|
|
2821
3408
|
listDeliveries(query = {}) {
|
|
2822
|
-
return this.#store.listDeliveries(query);
|
|
3409
|
+
return this.#parts.store.listDeliveries(query);
|
|
2823
3410
|
}
|
|
2824
3411
|
/** Moves dead deliveries back to pending with a fresh attempt budget. */
|
|
2825
3412
|
async requeue(deliveryIds) {
|
|
2826
|
-
const moved = await this.#store.requeue(deliveryIds, this.#clock.now());
|
|
3413
|
+
const moved = await this.#parts.store.requeue(deliveryIds, this.#parts.clock.now());
|
|
2827
3414
|
if (moved > 0) {
|
|
2828
|
-
this.#dispatcher.wake();
|
|
3415
|
+
this.#parts.dispatcher.wake();
|
|
2829
3416
|
}
|
|
2830
3417
|
return moved;
|
|
2831
3418
|
}
|
|
2832
3419
|
statistics() {
|
|
2833
|
-
return this.#store.statistics(this.#clock.now());
|
|
3420
|
+
return this.#parts.store.statistics(this.#parts.clock.now());
|
|
2834
3421
|
}
|
|
2835
3422
|
/** Applies the retention policy once. Runs automatically while the bus is running. */
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
const { completedMs, deadMs } = this.#retention;
|
|
2839
|
-
try {
|
|
2840
|
-
const result = await this.#store.purge({
|
|
2841
|
-
...completedMs === null ? {} : { completedBefore: new Date(now - completedMs) },
|
|
2842
|
-
...deadMs === null ? {} : { deadBefore: new Date(now - deadMs) }
|
|
2843
|
-
});
|
|
2844
|
-
this.#notifier.notify("onPurged", { result });
|
|
2845
|
-
return result;
|
|
2846
|
-
} catch (error) {
|
|
2847
|
-
this.#notifier.failure("purge", error);
|
|
2848
|
-
throw error;
|
|
2849
|
-
}
|
|
3423
|
+
purgeExpired() {
|
|
3424
|
+
return this.#parts.maintenance.purgeExpired();
|
|
2850
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.
|
|
2851
3428
|
async #startComponents() {
|
|
3429
|
+
const { setupStore, store, connections } = this.#parts;
|
|
2852
3430
|
try {
|
|
2853
|
-
if (
|
|
2854
|
-
await
|
|
3431
|
+
if (setupStore) {
|
|
3432
|
+
await store.setup?.();
|
|
2855
3433
|
}
|
|
2856
|
-
for (const transport of
|
|
3434
|
+
for (const transport of connections.transports) {
|
|
2857
3435
|
await transport.connect?.();
|
|
2858
3436
|
}
|
|
2859
|
-
for (const subscription of
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
async (envelope, message) => {
|
|
2864
|
-
await (this.#observation === void 0 ? this.#receive(envelope) : this.#observedReceive(envelope, { transport, destination: message?.destination }));
|
|
2865
|
-
},
|
|
2866
|
-
this.#subscriptionHooks(transport)
|
|
3437
|
+
for (const subscription of connections.subscriptions) {
|
|
3438
|
+
await connections.subscribe(
|
|
3439
|
+
subscription,
|
|
3440
|
+
(envelope, origin) => this.#parts.receiver.receive(envelope, origin)
|
|
2867
3441
|
);
|
|
2868
3442
|
}
|
|
2869
3443
|
} catch (error) {
|
|
2870
|
-
await
|
|
2871
|
-
|
|
2872
|
-
async (deadline) => {
|
|
2873
|
-
await this.#closeTransports(deadline);
|
|
2874
|
-
return 0;
|
|
2875
|
-
}
|
|
2876
|
-
);
|
|
2877
|
-
this.#state = "stopped";
|
|
3444
|
+
await this.#closeAfterFailedStart();
|
|
3445
|
+
this.#lifecycle.markStopped();
|
|
2878
3446
|
throw error;
|
|
2879
3447
|
}
|
|
2880
|
-
if (this.#state !== "starting") {
|
|
3448
|
+
if (this.#lifecycle.state !== "starting") {
|
|
2881
3449
|
throw new LifecycleError("relaybus was stopped before it finished starting");
|
|
2882
3450
|
}
|
|
2883
|
-
if (this.#dispatcherEnabled) {
|
|
2884
|
-
this.#dispatcher.start();
|
|
3451
|
+
if (this.#parts.dispatcherEnabled) {
|
|
3452
|
+
this.#parts.dispatcher.start();
|
|
2885
3453
|
}
|
|
2886
|
-
this.#
|
|
2887
|
-
this.#
|
|
2888
|
-
this.#
|
|
2889
|
-
|
|
2890
|
-
`[RelayBus](startComponents) relaybus started | handlers=${this.#registry.all().length} dispatcher=${this.#dispatcherEnabled}`
|
|
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}`
|
|
2891
3458
|
);
|
|
2892
3459
|
}
|
|
2893
|
-
/**
|
|
2894
|
-
#
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
const errorInfo = toErrorInfo(error);
|
|
2901
|
-
this.#notifier.notify("onInvalidMessage", {
|
|
2902
|
-
transport: transport.name,
|
|
2903
|
-
system: transport.system ?? transport.name,
|
|
2904
|
-
destination,
|
|
2905
|
-
reason: errorInfo.message,
|
|
2906
|
-
error: errorInfo
|
|
2907
|
-
});
|
|
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;
|
|
2908
3467
|
}
|
|
2909
|
-
};
|
|
2910
|
-
}
|
|
2911
|
-
/** Applies the retention policy every `retention.interval` while the bus runs, when it deletes anything. */
|
|
2912
|
-
#scheduleRetention() {
|
|
2913
|
-
if (this.#retention.completedMs !== null || this.#retention.deadMs !== null) {
|
|
2914
|
-
this.#retentionTimer = schedule(this.#clock, {
|
|
2915
|
-
run: () => {
|
|
2916
|
-
this.purgeExpired().catch(() => void 0);
|
|
2917
|
-
},
|
|
2918
|
-
delayMs: this.#retention.intervalMs,
|
|
2919
|
-
repeat: true,
|
|
2920
|
-
unref: true
|
|
2921
|
-
});
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
/** Reports the backlog every `backlogInterval` while the bus runs, when an observer wants it. */
|
|
2925
|
-
#scheduleBacklog() {
|
|
2926
|
-
if (this.#backlogIntervalMs !== void 0) {
|
|
2927
|
-
this.#backlogTimer = this.#observation?.startBacklog({
|
|
2928
|
-
intervalMs: this.#backlogIntervalMs,
|
|
2929
|
-
statistics: () => this.statistics()
|
|
2930
|
-
});
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
|
-
/**
|
|
2934
|
-
* Waits for a start in progress (a failed start leaves nothing to stop), then stops the dispatcher
|
|
2935
|
-
* and closes the transports and the store. A start still running at the deadline is given up: the
|
|
2936
|
-
* transports and the store are force-closed and the start rejects when it resumes.
|
|
2937
|
-
*/
|
|
2938
|
-
async #stopComponents(timeoutMs) {
|
|
2939
|
-
try {
|
|
2940
|
-
return await withinDeadline({ clock: this.#clock, timeoutMs }, async (deadline) => {
|
|
2941
|
-
if (this.#state === "starting") {
|
|
2942
|
-
const started = await deadline.race(this.#starting.catch(() => void 0));
|
|
2943
|
-
if (started === "timeout") {
|
|
2944
|
-
this.#state = "stopping";
|
|
2945
|
-
await this.#closeConnections(deadline);
|
|
2946
|
-
return 0;
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
if (this.#state !== "running") {
|
|
2950
|
-
return 0;
|
|
2951
|
-
}
|
|
2952
|
-
this.#state = "stopping";
|
|
2953
|
-
this.#retentionTimer?.();
|
|
2954
|
-
this.#backlogTimer?.();
|
|
2955
|
-
const abandoned = await this.#dispatcher.stop(deadline);
|
|
2956
|
-
await this.#closeConnections(deadline);
|
|
2957
|
-
return abandoned;
|
|
2958
|
-
});
|
|
2959
|
-
} finally {
|
|
2960
|
-
this.#state = "stopped";
|
|
2961
|
-
}
|
|
2962
|
-
}
|
|
2963
|
-
async #closeConnections(deadline) {
|
|
2964
|
-
await this.#closeTransports(deadline);
|
|
2965
|
-
const options = { signal: deadline.signal };
|
|
2966
|
-
await deadline.race(Promise.resolve(this.#store.close?.(options)));
|
|
2967
|
-
}
|
|
2968
|
-
/** Publishes inside the tracer's span, with its trace headers, when the bus is observed. */
|
|
2969
|
-
#observedPublish(events, input) {
|
|
2970
|
-
const observation = this.#observation;
|
|
2971
|
-
if (observation === void 0) {
|
|
2972
|
-
return this.#publish(events, input);
|
|
2973
|
-
}
|
|
2974
|
-
return observation.publish(
|
|
2975
|
-
{ eventTypes: events.map((event) => String(event.type)) },
|
|
2976
|
-
(headers) => this.#publish(events, { ...input, options: withTraceHeaders(input.options, headers) })
|
|
2977
3468
|
);
|
|
2978
3469
|
}
|
|
2979
|
-
async #publish(events, {
|
|
2980
|
-
options,
|
|
2981
|
-
id
|
|
2982
|
-
}) {
|
|
2983
|
-
if (events.length === 0) {
|
|
2984
|
-
return [];
|
|
2985
|
-
}
|
|
2986
|
-
const now = this.#clock.now();
|
|
2987
|
-
const delayMs = options.delay === void 0 ? 0 : toMilliseconds(options.delay, "delay");
|
|
2988
|
-
const availableAt = new Date(now.getTime() + delayMs);
|
|
2989
|
-
const scope = getCurrentScope();
|
|
2990
|
-
const entries = await this.#createEntries({ events, options, id, scope, now, availableAt });
|
|
2991
|
-
const deliverable = entries.filter((entry) => entry.deliveries.length > 0);
|
|
2992
|
-
if (deliverable.length > 0) {
|
|
2993
|
-
await this.#appendDeliverable({ entries: deliverable, options, scope, now, delayMs });
|
|
2994
|
-
}
|
|
2995
|
-
return entries.map((entry) => entry.event);
|
|
2996
|
-
}
|
|
2997
|
-
/** One entry per event, in order: its envelope and a delivery for every registration of its type. */
|
|
2998
|
-
async #createEntries(input) {
|
|
2999
|
-
const { events, options, id, scope, now, availableAt } = input;
|
|
3000
|
-
const entries = [];
|
|
3001
|
-
let correlationId = options.correlationId ?? scope?.event?.correlationId ?? scope?.event?.id;
|
|
3002
|
-
for (const event of events) {
|
|
3003
|
-
const envelope = await this.#createEnvelope({
|
|
3004
|
-
event,
|
|
3005
|
-
options,
|
|
3006
|
-
id,
|
|
3007
|
-
scope,
|
|
3008
|
-
now,
|
|
3009
|
-
correlationId
|
|
3010
|
-
});
|
|
3011
|
-
correlationId = envelope.correlationId;
|
|
3012
|
-
const registrations = this.#registry.forEventType(envelope.type);
|
|
3013
|
-
if (registrations.length === 0) {
|
|
3014
|
-
this.#reportUnrouted(envelope);
|
|
3015
|
-
}
|
|
3016
|
-
entries.push({
|
|
3017
|
-
event: envelope,
|
|
3018
|
-
deliveries: registrations.map(
|
|
3019
|
-
(registration) => this.#createDelivery(registration, availableAt)
|
|
3020
|
-
)
|
|
3021
|
-
});
|
|
3022
|
-
}
|
|
3023
|
-
return entries;
|
|
3024
|
-
}
|
|
3025
|
-
/** Appends the entries that have deliveries, reports what was new and wakes the dispatcher when it is due now. */
|
|
3026
|
-
async #appendDeliverable(input) {
|
|
3027
|
-
const { entries, options, scope, now, delayMs } = input;
|
|
3028
|
-
const result = await this.#appendInTransaction(entries, { options, scope, now });
|
|
3029
|
-
const appended = new Set(result.appendedEventIds);
|
|
3030
|
-
const published = entries.map((entry) => entry.event).filter((envelope) => appended.has(envelope.id));
|
|
3031
|
-
if (published.length > 0) {
|
|
3032
|
-
this.#notifier.notify("onPublished", {
|
|
3033
|
-
envelopes: published,
|
|
3034
|
-
deliveries: result.appendedDeliveryCount
|
|
3035
|
-
});
|
|
3036
|
-
if (delayMs === 0) {
|
|
3037
|
-
this.#scheduleWake();
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
3470
|
#reportUnrouted(envelope) {
|
|
3042
3471
|
const message = `No handler or route for event type, event not stored | type=${envelope.type} id=${envelope.id}`;
|
|
3043
|
-
if (this.#unroutedEvents === "throw") {
|
|
3472
|
+
if (this.#parts.unroutedEvents === "throw") {
|
|
3044
3473
|
throw new ConfigurationError(message);
|
|
3045
3474
|
}
|
|
3046
|
-
this.#logger.warn(`[RelayBus](reportUnrouted) ${message}`);
|
|
3047
|
-
}
|
|
3048
|
-
async #appendInTransaction(entries, {
|
|
3049
|
-
options,
|
|
3050
|
-
scope,
|
|
3051
|
-
now
|
|
3052
|
-
}) {
|
|
3053
|
-
const transaction = options.transaction ?? scope?.transaction ?? this.#transactionResolver?.();
|
|
3054
|
-
if (transaction !== void 0) {
|
|
3055
|
-
return this.#store.append(transaction, entries, now);
|
|
3056
|
-
}
|
|
3057
|
-
if (this.#transactionMode === "required") {
|
|
3058
|
-
throw new TransactionRequiredError(
|
|
3059
|
-
"publish found no transaction. Pass { transaction }, publish inside bus.transaction(), configure transactionResolver, or set transactionMode to optional"
|
|
3060
|
-
);
|
|
3061
|
-
}
|
|
3062
|
-
return this.#store.transaction(
|
|
3063
|
-
(ownTransaction) => this.#store.append(ownTransaction, entries, now)
|
|
3064
|
-
);
|
|
3065
|
-
}
|
|
3066
|
-
async #createEnvelope(input) {
|
|
3067
|
-
const { event, options, id, scope, now, correlationId } = input;
|
|
3068
|
-
if (typeof event.type !== "string") {
|
|
3069
|
-
throw new ConfigurationError(
|
|
3070
|
-
"publish expects an instance of a class created with defineEvent"
|
|
3071
|
-
);
|
|
3072
|
-
}
|
|
3073
|
-
assertEventType(event.type);
|
|
3074
|
-
const schema = event.constructor.schema;
|
|
3075
|
-
const payload = await validatePayload(event.type, schema, event.payload);
|
|
3076
|
-
if (options.key !== void 0 && options.key.length === 0) {
|
|
3077
|
-
throw new ConfigurationError("key must not be empty");
|
|
3078
|
-
}
|
|
3079
|
-
const eventId = id ?? this.#generateIdentifier();
|
|
3080
|
-
return {
|
|
3081
|
-
id: eventId,
|
|
3082
|
-
type: event.type,
|
|
3083
|
-
source: this.#source,
|
|
3084
|
-
key: options.key ?? keyFromDefinition(event, payload),
|
|
3085
|
-
payload,
|
|
3086
|
-
headers: { ...options.headers },
|
|
3087
|
-
correlationId: correlationId ?? eventId,
|
|
3088
|
-
causationId: scope?.event?.id ?? null,
|
|
3089
|
-
occurredAt: now
|
|
3090
|
-
};
|
|
3091
|
-
}
|
|
3092
|
-
#createDelivery(registration, availableAt) {
|
|
3093
|
-
return { id: this.#generateIdentifier(), handler: registration.id, availableAt };
|
|
3475
|
+
this.#parts.logger.warn(`[RelayBus](reportUnrouted) ${message}`);
|
|
3094
3476
|
}
|
|
3095
3477
|
#registrationsOf(eventType) {
|
|
3096
|
-
const registrations = this.#registry.forEventType(eventType);
|
|
3478
|
+
const registrations = this.#parts.registry.forEventType(eventType);
|
|
3097
3479
|
const routes = registrations.filter((registration) => registration.forwardsToTransport).length;
|
|
3098
3480
|
return { handlers: registrations.length - routes, routes };
|
|
3099
3481
|
}
|
|
3100
|
-
#registerRoutes(routes) {
|
|
3101
|
-
for (const route of routes) {
|
|
3102
|
-
this.#requireTransport(route.transport);
|
|
3103
|
-
}
|
|
3104
|
-
for (const group of groupRoutes(routes)) {
|
|
3105
|
-
this.#registerRoute(group);
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
/** Registers one handler for every route to a destination, so a key's events reach it in commit order. */
|
|
3109
|
-
#registerRoute(group) {
|
|
3110
|
-
const transport = this.#transports.get(group.transport);
|
|
3111
|
-
const { route, destination } = group;
|
|
3112
|
-
this.#registry.register(
|
|
3113
|
-
{
|
|
3114
|
-
eventTypes: group.eventTypes,
|
|
3115
|
-
legacyIds: group.legacyIds,
|
|
3116
|
-
schema: void 0,
|
|
3117
|
-
handle: this.#observation?.sender(transport, destination) ?? ((envelope) => transport.publish(envelope, destination)),
|
|
3118
|
-
options: {
|
|
3119
|
-
id: group.id,
|
|
3120
|
-
...route.retry === void 0 ? {} : { retry: route.retry },
|
|
3121
|
-
...route.timeout === void 0 ? {} : { timeout: route.timeout }
|
|
3122
|
-
},
|
|
3123
|
-
forwardsToTransport: true
|
|
3124
|
-
},
|
|
3125
|
-
{ retry: DEFAULT_ROUTE_RETRY_POLICY, timeoutMs: DEFAULTS.routeTimeoutMs }
|
|
3126
|
-
);
|
|
3127
|
-
}
|
|
3128
|
-
#requireTransport(name) {
|
|
3129
|
-
const transport = this.#transports.get(name);
|
|
3130
|
-
if (transport === void 0) {
|
|
3131
|
-
throw new ConfigurationError(`No transport named '${name}' is configured`);
|
|
3132
|
-
}
|
|
3133
|
-
return transport;
|
|
3134
|
-
}
|
|
3135
3482
|
#scheduleWake() {
|
|
3136
|
-
if (this.#state !== "running" || this.#dispatcher.watchesNewDeliveries) {
|
|
3483
|
+
if (this.#lifecycle.state !== "running" || this.#parts.dispatcher.watchesNewDeliveries) {
|
|
3137
3484
|
return;
|
|
3138
3485
|
}
|
|
3139
|
-
schedule(this.#clock, {
|
|
3140
|
-
run: () => this.#dispatcher.wake(),
|
|
3486
|
+
schedule(this.#parts.clock, {
|
|
3487
|
+
run: () => this.#parts.dispatcher.wake(),
|
|
3141
3488
|
delayMs: WAKE_DELAY_MS,
|
|
3142
3489
|
unref: true
|
|
3143
3490
|
});
|
|
@@ -3145,37 +3492,18 @@ var RelayBus = class {
|
|
|
3145
3492
|
/** Closes every transport, each at most until the deadline; past it they are asked to force. */
|
|
3146
3493
|
async #closeTransports(deadline) {
|
|
3147
3494
|
const options = { signal: deadline.signal };
|
|
3148
|
-
for (const transport of this.#transports
|
|
3495
|
+
for (const transport of this.#parts.connections.transports) {
|
|
3149
3496
|
try {
|
|
3150
3497
|
await deadline.race(transport.close(options));
|
|
3151
3498
|
} catch (error) {
|
|
3152
|
-
this.#logger.error(
|
|
3499
|
+
this.#parts.logger.error(
|
|
3153
3500
|
`[RelayBus](closeTransports) Failed to close transport | transport=${transport.name} error=${toErrorInfo(error).message}`
|
|
3154
3501
|
);
|
|
3155
3502
|
}
|
|
3156
3503
|
}
|
|
3157
3504
|
}
|
|
3158
3505
|
};
|
|
3159
|
-
function toDescriptor(registration, eventType) {
|
|
3160
|
-
return {
|
|
3161
|
-
id: registration.id,
|
|
3162
|
-
eventType,
|
|
3163
|
-
retry: registration.retry,
|
|
3164
|
-
timeoutMs: registration.timeoutMs,
|
|
3165
|
-
concurrency: registration.concurrency,
|
|
3166
|
-
transactional: registration.transactional,
|
|
3167
|
-
forwardsToTransport: registration.forwardsToTransport
|
|
3168
|
-
};
|
|
3169
|
-
}
|
|
3170
|
-
function describeEvent(event) {
|
|
3171
|
-
if (typeof event === "string") {
|
|
3172
|
-
assertEventType(event);
|
|
3173
|
-
return { type: event, schema: void 0 };
|
|
3174
|
-
}
|
|
3175
|
-
assertEventType(event.type);
|
|
3176
|
-
return { type: event.type, schema: event.schema };
|
|
3177
|
-
}
|
|
3178
3506
|
|
|
3179
|
-
export { CatalogPublisher, DEFAULT_RETRY_POLICY, DEFAULT_ROUTE_RETRY_POLICY, DELIVERY_STATUSES, InMemoryEventStore, InMemoryTransaction,
|
|
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 };
|
|
3180
3508
|
//# sourceMappingURL=index.js.map
|
|
3181
3509
|
//# sourceMappingURL=index.js.map
|