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