@relaybus/core 0.0.0 → 0.2.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 +230 -26
- package/dist/index.cjs +759 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +608 -227
- package/dist/index.d.ts +608 -227
- package/dist/index.js +754 -64
- package/dist/index.js.map +1 -1
- package/package.json +20 -8
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,15 @@ var InvalidEnvelopeError = class extends NonRetryableError {
|
|
|
36
36
|
};
|
|
37
37
|
var LeaseLostError = class extends RelayBusError {
|
|
38
38
|
name = "LeaseLostError";
|
|
39
|
+
/**
|
|
40
|
+
* Whether the store said so (a fenced write returned false). A worker that only noticed
|
|
41
|
+
* locally that its lease ran out may still own the delivery in the store and has to give it back.
|
|
42
|
+
*/
|
|
43
|
+
storeConfirmed;
|
|
44
|
+
constructor(message, options = {}) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.storeConfirmed = options.storeConfirmed ?? false;
|
|
47
|
+
}
|
|
39
48
|
};
|
|
40
49
|
var HandlerTimeoutError = class extends RelayBusError {
|
|
41
50
|
name = "HandlerTimeoutError";
|
|
@@ -141,6 +150,261 @@ async function validatePayload(type, schema, payload) {
|
|
|
141
150
|
return result.value;
|
|
142
151
|
}
|
|
143
152
|
|
|
153
|
+
// src/catalog.ts
|
|
154
|
+
var EVENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
155
|
+
var descriptions = /* @__PURE__ */ new WeakMap();
|
|
156
|
+
function defineCatalog(events, options = {}) {
|
|
157
|
+
const label = options.name === void 0 ? "Catalog" : `Catalog '${options.name}'`;
|
|
158
|
+
const names = Object.keys(events);
|
|
159
|
+
if (names.length === 0) {
|
|
160
|
+
throw new ConfigurationError(`${label} must contain at least one event`);
|
|
161
|
+
}
|
|
162
|
+
const owners = /* @__PURE__ */ new Map();
|
|
163
|
+
const entries = names.map((name) => {
|
|
164
|
+
const eventClass = events[name];
|
|
165
|
+
if (!EVENT_NAME_PATTERN.test(name)) {
|
|
166
|
+
throw new ConfigurationError(
|
|
167
|
+
`${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`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (typeof eventClass !== "function" || typeof eventClass.type !== "string") {
|
|
171
|
+
throw new ConfigurationError(
|
|
172
|
+
`${label} entry '${name}' must be an event class created with defineEvent`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
assertEventType(eventClass.type);
|
|
176
|
+
const owner = owners.get(eventClass.type);
|
|
177
|
+
if (owner !== void 0) {
|
|
178
|
+
throw new ConfigurationError(
|
|
179
|
+
`${label} entries '${owner}' and '${name}' share the event type '${eventClass.type}'`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
owners.set(eventClass.type, name);
|
|
183
|
+
return Object.freeze({ name, eventClass, type: eventClass.type, handlerName: `on${name}` });
|
|
184
|
+
});
|
|
185
|
+
const catalog = Object.freeze({ ...events });
|
|
186
|
+
descriptions.set(catalog, Object.freeze({ name: options.name, entries: Object.freeze(entries) }));
|
|
187
|
+
return catalog;
|
|
188
|
+
}
|
|
189
|
+
function describeCatalog(catalog) {
|
|
190
|
+
const description = descriptions.get(catalog);
|
|
191
|
+
if (description === void 0) {
|
|
192
|
+
throw new ConfigurationError("Expected a catalog created with defineCatalog");
|
|
193
|
+
}
|
|
194
|
+
return description;
|
|
195
|
+
}
|
|
196
|
+
function catalogLabel(catalog) {
|
|
197
|
+
const { name } = describeCatalog(catalog);
|
|
198
|
+
return name === void 0 ? "the catalog" : `catalog '${name}'`;
|
|
199
|
+
}
|
|
200
|
+
function selectCatalogEntries(catalog, events) {
|
|
201
|
+
const { entries } = describeCatalog(catalog);
|
|
202
|
+
if (events === void 0) {
|
|
203
|
+
return entries;
|
|
204
|
+
}
|
|
205
|
+
const label = catalogLabel(catalog);
|
|
206
|
+
if (events.length === 0) {
|
|
207
|
+
throw new ConfigurationError(
|
|
208
|
+
`The events selected from ${label} must name at least one event; omit events to select all of them`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
const selected = /* @__PURE__ */ new Set();
|
|
212
|
+
for (const event of events) {
|
|
213
|
+
if (!entries.some((entry) => entry.name === event)) {
|
|
214
|
+
throw new ConfigurationError(
|
|
215
|
+
`${capitalize(label)} has no event named '${String(event)}'; its events are ${entries.map((entry) => entry.name).join(", ")}`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (selected.has(event)) {
|
|
219
|
+
throw new ConfigurationError(`Event '${String(event)}' of ${label} is selected twice`);
|
|
220
|
+
}
|
|
221
|
+
selected.add(event);
|
|
222
|
+
}
|
|
223
|
+
return entries.filter((entry) => selected.has(entry.name));
|
|
224
|
+
}
|
|
225
|
+
function capitalize(text) {
|
|
226
|
+
return `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
|
|
227
|
+
}
|
|
228
|
+
function requireCatalogEntry(catalog, event) {
|
|
229
|
+
const entry = describeCatalog(catalog).entries.find((candidate) => candidate.type === event.type);
|
|
230
|
+
if (entry === void 0) {
|
|
231
|
+
throw new ConfigurationError(
|
|
232
|
+
`Event type '${String(event.type)}' is not part of ${catalogLabel(catalog)}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return entry;
|
|
236
|
+
}
|
|
237
|
+
function resolveCatalogHandlers(catalog, handlers, options) {
|
|
238
|
+
const { name } = describeCatalog(catalog);
|
|
239
|
+
const { idPrefix, events, ...subscribeOptions } = options;
|
|
240
|
+
const entries = selectCatalogEntries(catalog, events);
|
|
241
|
+
const lookup = handlers;
|
|
242
|
+
const missing = entries.filter((entry) => typeof lookup[entry.handlerName] !== "function");
|
|
243
|
+
if (missing.length > 0) {
|
|
244
|
+
throw new ConfigurationError(
|
|
245
|
+
`Handlers for catalog ${name === void 0 ? "events" : `'${name}'`} lack methods: ${missing.map((entry) => entry.handlerName).join(", ")}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const prefix = idPrefix ?? name ?? classNameOf(handlers);
|
|
249
|
+
if (prefix === void 0) {
|
|
250
|
+
throw new ConfigurationError(
|
|
251
|
+
"Cannot derive handler ids: name the catalog, pass an idPrefix or use a class instance as handlers"
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return entries.map((entry) => {
|
|
255
|
+
const method = lookup[entry.handlerName];
|
|
256
|
+
return {
|
|
257
|
+
eventClass: entry.eventClass,
|
|
258
|
+
handle: (envelope, context) => method.call(handlers, envelope, context),
|
|
259
|
+
options: { ...subscribeOptions, id: `${prefix}.${entry.handlerName}` }
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function classNameOf(handlers) {
|
|
264
|
+
const className = handlers.constructor.name;
|
|
265
|
+
return className === "Object" || className === "" ? void 0 : className;
|
|
266
|
+
}
|
|
267
|
+
function assertExactHandlers(catalog, entries, handlers) {
|
|
268
|
+
const expected = new Set(entries.map((entry) => entry.handlerName));
|
|
269
|
+
const present = Object.keys(handlers);
|
|
270
|
+
const missing = [...expected].filter((handlerName) => !present.includes(handlerName));
|
|
271
|
+
const extra = present.filter((key) => !expected.has(key));
|
|
272
|
+
const notFunctions = present.filter(
|
|
273
|
+
(key) => expected.has(key) && typeof handlers[key] !== "function"
|
|
274
|
+
);
|
|
275
|
+
if (missing.length + extra.length + notFunctions.length > 0) {
|
|
276
|
+
const problems = [
|
|
277
|
+
...missing.map((key) => `missing ${key}`),
|
|
278
|
+
...extra.map((key) => `unexpected ${key}`),
|
|
279
|
+
...notFunctions.map((key) => `${key} is not a function`)
|
|
280
|
+
];
|
|
281
|
+
throw new ConfigurationError(
|
|
282
|
+
`Handlers do not match ${catalogLabel(catalog)}: ${problems.join(", ")}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function defineHandlers(catalog, handlers, options = {}) {
|
|
287
|
+
assertExactHandlers(catalog, selectCatalogEntries(catalog, options.events), handlers);
|
|
288
|
+
return handlers;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/catalog-publisher.ts
|
|
292
|
+
var CatalogPublisher = class {
|
|
293
|
+
#catalog;
|
|
294
|
+
#target;
|
|
295
|
+
#entries;
|
|
296
|
+
/**
|
|
297
|
+
* Whether the view publishes the event named `eventName`. Its parameter
|
|
298
|
+
* makes a view of some events unassignable to a view of more events, which
|
|
299
|
+
* the bivariant method parameters alone would allow.
|
|
300
|
+
*/
|
|
301
|
+
#publishes;
|
|
302
|
+
/**
|
|
303
|
+
* Throws {@link ConfigurationError} when `catalog` was not created with
|
|
304
|
+
* `defineCatalog` or `options.events` names no event, an unknown event or
|
|
305
|
+
* the same event twice.
|
|
306
|
+
*/
|
|
307
|
+
constructor(catalog, target, options = {}) {
|
|
308
|
+
this.#entries = selectCatalogEntries(catalog, options.events);
|
|
309
|
+
const names = new Set(this.#entries.map((entry) => entry.name));
|
|
310
|
+
this.#publishes = (eventName) => names.has(eventName);
|
|
311
|
+
this.#catalog = catalog;
|
|
312
|
+
this.#target = target;
|
|
313
|
+
}
|
|
314
|
+
/** Same as `bus.publish`, limited to the events of the view. */
|
|
315
|
+
async publish(event, options) {
|
|
316
|
+
this.#assertPublishable(event);
|
|
317
|
+
return this.#target.publish(event, options);
|
|
318
|
+
}
|
|
319
|
+
/** Same as `bus.publishAll`, limited to the events of the view. Nothing is stored unless every event belongs to it. */
|
|
320
|
+
async publishAll(events, options) {
|
|
321
|
+
for (const event of events) {
|
|
322
|
+
this.#assertPublishable(event);
|
|
323
|
+
}
|
|
324
|
+
return this.#target.publishAll(events, options);
|
|
325
|
+
}
|
|
326
|
+
#assertPublishable(event) {
|
|
327
|
+
const { name } = requireCatalogEntry(this.#catalog, event);
|
|
328
|
+
if (!this.#publishes(name)) {
|
|
329
|
+
throw new ConfigurationError(
|
|
330
|
+
`Event type '${event.type}' is not one of the events this view of ${catalogLabel(
|
|
331
|
+
this.#catalog
|
|
332
|
+
)} publishes (${this.#entries.map((entry) => entry.name).join(", ")})`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// src/catalog-coverage.ts
|
|
339
|
+
var declarations = /* @__PURE__ */ new WeakSet();
|
|
340
|
+
function consume(catalog, options = {}) {
|
|
341
|
+
return declare(catalog, "consume", options);
|
|
342
|
+
}
|
|
343
|
+
function produce(catalog, options = {}) {
|
|
344
|
+
return declare(catalog, "produce", options);
|
|
345
|
+
}
|
|
346
|
+
function declare(catalog, role, options) {
|
|
347
|
+
const entries = selectCatalogEntries(catalog, options.events);
|
|
348
|
+
const declaration = Object.freeze({
|
|
349
|
+
catalog,
|
|
350
|
+
role,
|
|
351
|
+
events: Object.freeze(entries.map((entry) => entry.name)),
|
|
352
|
+
entries
|
|
353
|
+
});
|
|
354
|
+
declarations.add(declaration);
|
|
355
|
+
return declaration;
|
|
356
|
+
}
|
|
357
|
+
function toCatalogRequirement(value) {
|
|
358
|
+
if (declarations.has(value)) {
|
|
359
|
+
const declaration = value;
|
|
360
|
+
return {
|
|
361
|
+
catalogName: describeCatalog(declaration.catalog).name,
|
|
362
|
+
entries: declaration.entries,
|
|
363
|
+
need: declaration.role === "consume" ? "handler" : "route"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const { name, entries } = describeCatalog(value);
|
|
367
|
+
return { catalogName: name, entries, need: "handler-or-route" };
|
|
368
|
+
}
|
|
369
|
+
function assertCatalogsCovered(requirements, registrationsOf) {
|
|
370
|
+
const problems = /* @__PURE__ */ new Set();
|
|
371
|
+
let onlyBareCatalogs = true;
|
|
372
|
+
for (const { catalogName, entries, need } of requirements) {
|
|
373
|
+
for (const entry of entries) {
|
|
374
|
+
const problem = describeGap(entry, need, registrationsOf(entry.type));
|
|
375
|
+
if (problem !== void 0) {
|
|
376
|
+
problems.add(`${catalogName === void 0 ? "" : `${catalogName}.`}${problem}`);
|
|
377
|
+
onlyBareCatalogs &&= need === "handler-or-route";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (problems.size > 0) {
|
|
382
|
+
const heading = onlyBareCatalogs ? "Catalog events without a handler or route:" : "Catalog events this service does not cover:";
|
|
383
|
+
throw new ConfigurationError(
|
|
384
|
+
`${heading}
|
|
385
|
+
${[...problems].map((line) => ` - ${line}`).join("\n")}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function describeGap(entry, need, { handlers, routes }) {
|
|
390
|
+
const subject = `${entry.name} (${entry.type})`;
|
|
391
|
+
if (need === "handler-or-route") {
|
|
392
|
+
return handlers + routes > 0 ? void 0 : `${subject}: expected handler method ${entry.handlerName}`;
|
|
393
|
+
}
|
|
394
|
+
if (need === "handler") {
|
|
395
|
+
if (handlers > 0) {
|
|
396
|
+
return void 0;
|
|
397
|
+
}
|
|
398
|
+
const routeNote = routes > 0 ? "; its route to a transport does not handle it here" : "";
|
|
399
|
+
return `${subject}: consumed here, needs handler method ${entry.handlerName}${routeNote}`;
|
|
400
|
+
}
|
|
401
|
+
if (routes > 0) {
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
const handlerNote = handlers > 0 ? "; a local handler does not deliver it to other services" : "";
|
|
405
|
+
return `${subject}: produced here, needs a route to a transport${handlerNote}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
144
408
|
// src/event-store.ts
|
|
145
409
|
var DELIVERY_STATUSES = [
|
|
146
410
|
"pending",
|
|
@@ -202,7 +466,9 @@ var InMemoryEventStore = class {
|
|
|
202
466
|
#keySequences = /* @__PURE__ */ new Map();
|
|
203
467
|
/** Row-lock emulation: event ids and ordering keys written by open transactions. */
|
|
204
468
|
#locks = /* @__PURE__ */ new Map();
|
|
469
|
+
#writes = /* @__PURE__ */ new WeakMap();
|
|
205
470
|
#generateIdentifier;
|
|
471
|
+
#retainCompleted;
|
|
206
472
|
constructor(options = {}) {
|
|
207
473
|
if (process.env["NODE_ENV"] === "production" && options.allowInProduction !== true) {
|
|
208
474
|
throw new ConfigurationError(
|
|
@@ -210,6 +476,7 @@ var InMemoryEventStore = class {
|
|
|
210
476
|
);
|
|
211
477
|
}
|
|
212
478
|
this.#generateIdentifier = options.generateIdentifier ?? (() => createUuidV7());
|
|
479
|
+
this.#retainCompleted = options.retainCompleted ?? false;
|
|
213
480
|
}
|
|
214
481
|
async transaction(work) {
|
|
215
482
|
const transaction = new InMemoryTransaction();
|
|
@@ -230,13 +497,15 @@ var InMemoryEventStore = class {
|
|
|
230
497
|
...entry.event.key === null ? [] : [`key:${entry.event.key}`]
|
|
231
498
|
])
|
|
232
499
|
);
|
|
233
|
-
const
|
|
234
|
-
const
|
|
500
|
+
const written = this.#written(transaction);
|
|
501
|
+
const pendingEventIds = written.eventIds;
|
|
502
|
+
const pendingDeliveryKeys = written.deliveryKeys;
|
|
235
503
|
const appendedEventIds = [];
|
|
236
504
|
const duplicateEventIds = [];
|
|
237
505
|
let appendedDeliveryCount = 0;
|
|
238
506
|
for (const entry of entries) {
|
|
239
507
|
const eventId = entry.event.id;
|
|
508
|
+
const serialized = serializeEnvelope(entry.event);
|
|
240
509
|
const isDuplicate = this.#events.has(eventId) || pendingEventIds.has(eventId);
|
|
241
510
|
if (isDuplicate) {
|
|
242
511
|
duplicateEventIds.push(eventId);
|
|
@@ -246,23 +515,26 @@ var InMemoryEventStore = class {
|
|
|
246
515
|
}
|
|
247
516
|
const newDeliveries = entry.deliveries.filter((delivery) => {
|
|
248
517
|
const deliveryKey = `${eventId}\0${delivery.handler}`;
|
|
249
|
-
if (pendingDeliveryKeys.has(deliveryKey) || this.#
|
|
518
|
+
if (pendingDeliveryKeys.has(deliveryKey) || this.#events.get(eventId)?.handlers.has(delivery.handler) === true) {
|
|
250
519
|
return false;
|
|
251
520
|
}
|
|
252
521
|
pendingDeliveryKeys.add(deliveryKey);
|
|
253
522
|
return true;
|
|
254
523
|
});
|
|
255
524
|
appendedDeliveryCount += newDeliveries.length;
|
|
256
|
-
const serialized = serializeEnvelope(entry.event);
|
|
257
525
|
transaction.enqueue(() => {
|
|
258
526
|
if (!isDuplicate) {
|
|
259
527
|
this.#events.set(eventId, {
|
|
260
528
|
envelope: serialized,
|
|
261
|
-
sequence: this.#nextSequence(serialized.key)
|
|
529
|
+
sequence: this.#nextSequence(serialized.key),
|
|
530
|
+
createdAt: now,
|
|
531
|
+
handlers: /* @__PURE__ */ new Set(),
|
|
532
|
+
completedAt: null
|
|
262
533
|
});
|
|
263
534
|
}
|
|
264
535
|
const stored = this.#events.get(eventId);
|
|
265
536
|
for (const delivery of newDeliveries) {
|
|
537
|
+
stored.handlers.add(delivery.handler);
|
|
266
538
|
this.#deliveries.set(delivery.id, {
|
|
267
539
|
id: delivery.id,
|
|
268
540
|
eventId,
|
|
@@ -320,14 +592,8 @@ var InMemoryEventStore = class {
|
|
|
320
592
|
});
|
|
321
593
|
}
|
|
322
594
|
async complete(lease, now, transaction) {
|
|
323
|
-
const finish = (delivery) => {
|
|
324
|
-
delivery.status = "completed";
|
|
325
|
-
delivery.completedAt = now;
|
|
326
|
-
delivery.updatedAt = now;
|
|
327
|
-
clearLease(delivery);
|
|
328
|
-
};
|
|
329
595
|
if (transaction === void 0) {
|
|
330
|
-
return this.#updateLeased(lease, finish);
|
|
596
|
+
return this.#updateLeased(lease, (delivery) => this.#finish(delivery, now));
|
|
331
597
|
}
|
|
332
598
|
if (this.#findLeased(lease) === void 0) {
|
|
333
599
|
return false;
|
|
@@ -335,11 +601,14 @@ var InMemoryEventStore = class {
|
|
|
335
601
|
transaction.enqueue(() => {
|
|
336
602
|
const delivery = this.#findLeased(lease);
|
|
337
603
|
if (delivery !== void 0) {
|
|
338
|
-
finish(delivery);
|
|
604
|
+
this.#finish(delivery, now);
|
|
339
605
|
}
|
|
340
606
|
});
|
|
341
607
|
return true;
|
|
342
608
|
}
|
|
609
|
+
async completeMany(leases, now) {
|
|
610
|
+
return Promise.all(leases.map((lease) => this.complete(lease, now)));
|
|
611
|
+
}
|
|
343
612
|
async reschedule(lease, availableAt, error, now) {
|
|
344
613
|
return this.#updateLeased(lease, (delivery) => {
|
|
345
614
|
delivery.status = "pending";
|
|
@@ -413,9 +682,16 @@ var InMemoryEventStore = class {
|
|
|
413
682
|
const referencedEventIds = new Set(
|
|
414
683
|
[...this.#deliveries.values()].map((delivery) => delivery.eventId)
|
|
415
684
|
);
|
|
685
|
+
const cutoffs = [request.completedBefore, request.deadBefore].filter((cutoff) => cutoff !== void 0).map((cutoff) => cutoff.getTime());
|
|
686
|
+
const orphanCutoff = cutoffs.length === 0 ? Number.NEGATIVE_INFINITY : Math.min(...cutoffs);
|
|
687
|
+
const completedCutoff = request.completedBefore?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
416
688
|
let deletedEvents = 0;
|
|
417
|
-
for (const eventId of
|
|
418
|
-
if (
|
|
689
|
+
for (const [eventId, event] of [...this.#events]) {
|
|
690
|
+
if (referencedEventIds.has(eventId) || this.#locks.has(`event:${eventId}`)) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const expired = event.completedAt === null ? touchedEventIds.has(eventId) || event.createdAt.getTime() < orphanCutoff : event.completedAt.getTime() < completedCutoff;
|
|
694
|
+
if (expired) {
|
|
419
695
|
this.#events.delete(eventId);
|
|
420
696
|
deletedEvents += 1;
|
|
421
697
|
}
|
|
@@ -446,6 +722,15 @@ var InMemoryEventStore = class {
|
|
|
446
722
|
* until `transaction` finishes, the way a database holds row locks. Sorted
|
|
447
723
|
* acquisition keeps two transactions from waiting on each other.
|
|
448
724
|
*/
|
|
725
|
+
/** The events and deliveries the transaction appended so far, which are not applied before it commits. */
|
|
726
|
+
#written(transaction) {
|
|
727
|
+
let written = this.#writes.get(transaction);
|
|
728
|
+
if (written === void 0) {
|
|
729
|
+
written = { eventIds: /* @__PURE__ */ new Set(), deliveryKeys: /* @__PURE__ */ new Set() };
|
|
730
|
+
this.#writes.set(transaction, written);
|
|
731
|
+
}
|
|
732
|
+
return written;
|
|
733
|
+
}
|
|
449
734
|
async #lock(transaction, names) {
|
|
450
735
|
for (const name of [...new Set(names)].sort()) {
|
|
451
736
|
let held = this.#locks.get(name);
|
|
@@ -467,14 +752,6 @@ var InMemoryEventStore = class {
|
|
|
467
752
|
});
|
|
468
753
|
}
|
|
469
754
|
}
|
|
470
|
-
#hasDelivery(eventId, handler) {
|
|
471
|
-
for (const delivery of this.#deliveries.values()) {
|
|
472
|
-
if (delivery.eventId === eventId && delivery.handler === handler) {
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
755
|
#nextSequence(key) {
|
|
479
756
|
if (key === null) {
|
|
480
757
|
return null;
|
|
@@ -505,6 +782,21 @@ var InMemoryEventStore = class {
|
|
|
505
782
|
}
|
|
506
783
|
return false;
|
|
507
784
|
}
|
|
785
|
+
/** Deletes a completed delivery, or marks it completed when a history is kept. */
|
|
786
|
+
#finish(delivery, now) {
|
|
787
|
+
const event = this.#events.get(delivery.eventId);
|
|
788
|
+
if (event.completedAt === null || event.completedAt < now) {
|
|
789
|
+
event.completedAt = now;
|
|
790
|
+
}
|
|
791
|
+
if (!this.#retainCompleted) {
|
|
792
|
+
this.#deliveries.delete(delivery.id);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
delivery.status = "completed";
|
|
796
|
+
delivery.completedAt = now;
|
|
797
|
+
delivery.updatedAt = now;
|
|
798
|
+
clearLease(delivery);
|
|
799
|
+
}
|
|
508
800
|
#findLeased(lease) {
|
|
509
801
|
const delivery = this.#deliveries.get(lease.deliveryId);
|
|
510
802
|
if (delivery?.status === "processing" && delivery.leaseToken === lease.leaseToken) {
|
|
@@ -562,6 +854,66 @@ function isPurgeable(delivery, request) {
|
|
|
562
854
|
}
|
|
563
855
|
return false;
|
|
564
856
|
}
|
|
857
|
+
|
|
858
|
+
// src/completion-batcher.ts
|
|
859
|
+
var CompletionBatcher = class {
|
|
860
|
+
#store;
|
|
861
|
+
#clock;
|
|
862
|
+
#settings;
|
|
863
|
+
#runningCount;
|
|
864
|
+
#pending = [];
|
|
865
|
+
#timer;
|
|
866
|
+
#closing = false;
|
|
867
|
+
/**
|
|
868
|
+
* @param runningCount Number of deliveries the dispatcher is running right
|
|
869
|
+
* now, including the ones waiting in this batcher.
|
|
870
|
+
*/
|
|
871
|
+
constructor(store, clock, settings, runningCount) {
|
|
872
|
+
this.#store = store;
|
|
873
|
+
this.#clock = clock;
|
|
874
|
+
this.#settings = settings;
|
|
875
|
+
this.#runningCount = runningCount;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Queues a completion. Resolves with whether the lease was still held;
|
|
879
|
+
* rejects with the store error when the batch could not be written.
|
|
880
|
+
*/
|
|
881
|
+
submit(lease) {
|
|
882
|
+
return new Promise((resolve, reject) => {
|
|
883
|
+
this.#pending.push({ lease, resolve, reject });
|
|
884
|
+
if (this.#closing || this.#pending.length >= this.#settings.maxBatchSize || this.#pending.length >= this.#runningCount()) {
|
|
885
|
+
this.#flush();
|
|
886
|
+
} else {
|
|
887
|
+
this.#timer ??= setTimeout(() => this.#flush(), this.#settings.windowMs);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Shutdown has begun (or ended): while closing, nothing waits for company any more, so the completions
|
|
893
|
+
* already queued are written now and later ones at once.
|
|
894
|
+
*/
|
|
895
|
+
setClosing(closing) {
|
|
896
|
+
this.#closing = closing;
|
|
897
|
+
if (closing && this.#pending.length > 0) {
|
|
898
|
+
this.#flush();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
#flush() {
|
|
902
|
+
clearTimeout(this.#timer);
|
|
903
|
+
this.#timer = void 0;
|
|
904
|
+
const batch = this.#pending;
|
|
905
|
+
this.#pending = [];
|
|
906
|
+
Promise.resolve().then(
|
|
907
|
+
() => this.#store.completeMany(
|
|
908
|
+
batch.map((entry) => entry.lease),
|
|
909
|
+
this.#clock.now()
|
|
910
|
+
)
|
|
911
|
+
).then(
|
|
912
|
+
(outcomes) => batch.forEach((entry, index) => entry.resolve(outcomes[index])),
|
|
913
|
+
(error) => batch.forEach((entry) => entry.reject(error))
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
};
|
|
565
917
|
var storage = new async_hooks.AsyncLocalStorage();
|
|
566
918
|
function runInScope(scope, work) {
|
|
567
919
|
return storage.run({ ...storage.getStore(), ...scope }, work);
|
|
@@ -650,18 +1002,51 @@ function isRetryableError(policy, error) {
|
|
|
650
1002
|
|
|
651
1003
|
// src/dispatcher.ts
|
|
652
1004
|
var COMPLETION_RETRY_DELAYS_MS = [50, 250, 1e3];
|
|
1005
|
+
var WAKE_COOLDOWN_MS = 5;
|
|
1006
|
+
var QUIET_REFILL_MS = 10;
|
|
1007
|
+
var REFILL_FRACTION = 0.7;
|
|
653
1008
|
var defaultSleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
654
1009
|
var Dispatcher = class {
|
|
655
1010
|
#dependencies;
|
|
656
1011
|
#sleep;
|
|
657
1012
|
#inFlight = /* @__PURE__ */ new Set();
|
|
1013
|
+
#completions;
|
|
1014
|
+
#pending = /* @__PURE__ */ new Map();
|
|
1015
|
+
#recoveryTimer;
|
|
1016
|
+
#recovery;
|
|
658
1017
|
#running = false;
|
|
659
1018
|
#loop;
|
|
660
1019
|
#wakeUp;
|
|
661
|
-
|
|
1020
|
+
/** Free slots the claim the loop was woken for needs at least; undefined while nobody woke it. */
|
|
1021
|
+
#signalledMinimum;
|
|
1022
|
+
/** Free slots a claim needs unless the poll interval or the quiet refill time passed: claims refill in groups. */
|
|
1023
|
+
#refillThreshold;
|
|
1024
|
+
/** Handlers whose last claim got less than it asked for: the store has nothing more for them right now. */
|
|
1025
|
+
#exhaustedHandlers = /* @__PURE__ */ new Set();
|
|
1026
|
+
/** Grows with every wake-up: a claim that started before one must not mark a handler exhausted. */
|
|
1027
|
+
#wakeGeneration = 0;
|
|
1028
|
+
#lastClaimStartedAt = Number.NEGATIVE_INFINITY;
|
|
1029
|
+
#cooldownTimer;
|
|
1030
|
+
#refillScheduled = false;
|
|
1031
|
+
#quietRefillTimer;
|
|
1032
|
+
#unwatch;
|
|
1033
|
+
#watching;
|
|
662
1034
|
constructor(dependencies) {
|
|
663
1035
|
this.#dependencies = dependencies;
|
|
664
1036
|
this.#sleep = dependencies.sleep ?? defaultSleep;
|
|
1037
|
+
const { store, clock, settings } = dependencies;
|
|
1038
|
+
this.#refillThreshold = Math.ceil(
|
|
1039
|
+
Math.min(settings.concurrency, settings.batchSize) * REFILL_FRACTION
|
|
1040
|
+
);
|
|
1041
|
+
const { completionBatchWindowMs: windowMs = 0, completionBatchSize: maxBatchSize = 1 } = settings;
|
|
1042
|
+
if (store.completeMany !== void 0 && windowMs > 0) {
|
|
1043
|
+
this.#completions = new CompletionBatcher(
|
|
1044
|
+
{ completeMany: (leases, now) => store.completeMany(leases, now) },
|
|
1045
|
+
clock,
|
|
1046
|
+
{ windowMs, maxBatchSize },
|
|
1047
|
+
() => this.#inFlight.size
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
665
1050
|
}
|
|
666
1051
|
get isRunning() {
|
|
667
1052
|
return this.#running;
|
|
@@ -669,29 +1054,93 @@ var Dispatcher = class {
|
|
|
669
1054
|
get inFlightCount() {
|
|
670
1055
|
return this.#inFlight.size;
|
|
671
1056
|
}
|
|
1057
|
+
/**
|
|
1058
|
+
* True while the store reports new deliveries to this dispatcher by itself (see
|
|
1059
|
+
* {@link EventStore.watchNewDeliveries}), from every process: a publisher then has no reason to wake it.
|
|
1060
|
+
*/
|
|
1061
|
+
get watchesNewDeliveries() {
|
|
1062
|
+
return this.#unwatch !== void 0;
|
|
1063
|
+
}
|
|
672
1064
|
start() {
|
|
673
1065
|
if (this.#running) {
|
|
674
1066
|
return;
|
|
675
1067
|
}
|
|
676
1068
|
this.#running = true;
|
|
1069
|
+
this.#completions?.setClosing(false);
|
|
1070
|
+
this.#watching = this.#watchNewDeliveries();
|
|
677
1071
|
this.#loop = this.#runLoop();
|
|
678
1072
|
}
|
|
679
|
-
/**
|
|
1073
|
+
/**
|
|
1074
|
+
* Asks for a claim soon, for example right after a publish: at once after a quiet period, otherwise
|
|
1075
|
+
* at the end of a short cooldown that merges the wake-ups arriving meanwhile into one claim.
|
|
1076
|
+
*/
|
|
680
1077
|
wake() {
|
|
681
|
-
this.#
|
|
1078
|
+
this.#exhaustedHandlers.clear();
|
|
1079
|
+
this.#wakeGeneration += 1;
|
|
1080
|
+
const wait = this.#lastClaimStartedAt + WAKE_COOLDOWN_MS - Date.now();
|
|
1081
|
+
if (wait <= 0) {
|
|
1082
|
+
this.#requestClaim();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
this.#cooldownTimer ??= setTimeout(() => {
|
|
1086
|
+
this.#cooldownTimer = void 0;
|
|
1087
|
+
this.#requestClaim();
|
|
1088
|
+
}, wait);
|
|
1089
|
+
}
|
|
1090
|
+
/** Ends the current wait of the claim loop, or the next one at once, for a claim once `minimumFree` slots are free. */
|
|
1091
|
+
#signal(minimumFree) {
|
|
1092
|
+
this.#signalledMinimum = Math.min(this.#signalledMinimum ?? minimumFree, minimumFree);
|
|
682
1093
|
this.#wakeUp?.();
|
|
683
1094
|
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Starts listening to the store's reports of new deliveries. Polling stays the safety net: a store
|
|
1097
|
+
* that cannot listen, or fails to, only costs latency, so a failure is reported and otherwise ignored.
|
|
1098
|
+
*/
|
|
1099
|
+
async #watchNewDeliveries() {
|
|
1100
|
+
const { store, notifier } = this.#dependencies;
|
|
1101
|
+
try {
|
|
1102
|
+
const unwatch = await store.watchNewDeliveries?.(() => this.wake());
|
|
1103
|
+
if (unwatch !== void 0 && !this.#running) {
|
|
1104
|
+
await unwatch();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
this.#unwatch = unwatch;
|
|
1108
|
+
if (unwatch !== void 0) {
|
|
1109
|
+
this.wake();
|
|
1110
|
+
}
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
notifier.failure("watch", error);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
/** Stops listening to the store's reports of new deliveries. */
|
|
1116
|
+
async #stopWatching() {
|
|
1117
|
+
await this.#watching;
|
|
1118
|
+
const unwatch = this.#unwatch;
|
|
1119
|
+
this.#unwatch = void 0;
|
|
1120
|
+
try {
|
|
1121
|
+
await unwatch?.();
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
this.#dependencies.notifier.failure("watch", error);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
684
1126
|
/**
|
|
685
1127
|
* Stops claiming, waits up to the shutdown timeout for running handlers,
|
|
686
1128
|
* then aborts the rest and releases their deliveries for another process.
|
|
687
1129
|
*/
|
|
688
1130
|
async stop() {
|
|
689
1131
|
if (!this.#running) {
|
|
1132
|
+
await this.#closeRecovery(Date.now() + this.#dependencies.settings.shutdownTimeoutMs);
|
|
690
1133
|
return;
|
|
691
1134
|
}
|
|
692
1135
|
this.#running = false;
|
|
693
|
-
this
|
|
1136
|
+
this.#completions?.setClosing(true);
|
|
1137
|
+
clearTimeout(this.#cooldownTimer);
|
|
1138
|
+
this.#cooldownTimer = void 0;
|
|
1139
|
+
clearTimeout(this.#quietRefillTimer);
|
|
1140
|
+
this.#quietRefillTimer = void 0;
|
|
1141
|
+
this.#signal(this.#refillThreshold);
|
|
694
1142
|
const deadline = Date.now() + this.#dependencies.settings.shutdownTimeoutMs;
|
|
1143
|
+
await this.#waitUntil(this.#stopWatching(), deadline);
|
|
695
1144
|
await this.#waitUntil(this.#loop, deadline);
|
|
696
1145
|
const settled = Promise.allSettled([...this.#inFlight].map((delivery) => delivery.settled));
|
|
697
1146
|
if (await this.#waitUntil(settled, deadline) === "timeout") {
|
|
@@ -700,10 +1149,71 @@ var Dispatcher = class {
|
|
|
700
1149
|
}
|
|
701
1150
|
await settled;
|
|
702
1151
|
}
|
|
1152
|
+
await this.#closeRecovery(deadline);
|
|
1153
|
+
await this.#waitUntil(this.#relinquish(), deadline);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Gives the settlements still pending one last try and forgets them. A pass of the timer may be inside the
|
|
1157
|
+
* store: it ends before the last pass starts. A store that does not answer before the deadline gets no
|
|
1158
|
+
* further call, since it may be closed right after.
|
|
1159
|
+
*/
|
|
1160
|
+
async #closeRecovery(deadline) {
|
|
1161
|
+
clearInterval(this.#recoveryTimer);
|
|
1162
|
+
this.#recoveryTimer = void 0;
|
|
1163
|
+
const lastTry = (async () => {
|
|
1164
|
+
await this.#recovery;
|
|
1165
|
+
await this.#recoverSettlements();
|
|
1166
|
+
})();
|
|
1167
|
+
await this.#waitUntil(lastTry, deadline);
|
|
1168
|
+
this.#pending.clear();
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Retries every pending settlement once; a pass that is already running is the one returned. A store that
|
|
1172
|
+
* still fails leaves the settlement for the next attempt (it was reported when it first failed).
|
|
1173
|
+
*/
|
|
1174
|
+
#recoverSettlements() {
|
|
1175
|
+
this.#recovery ??= this.#recoverAll().finally(() => {
|
|
1176
|
+
this.#recovery = void 0;
|
|
1177
|
+
});
|
|
1178
|
+
return this.#recovery;
|
|
1179
|
+
}
|
|
1180
|
+
async #recoverAll() {
|
|
1181
|
+
for (const [key, entry] of [...this.#pending]) {
|
|
1182
|
+
if (this.#pending.get(key) !== entry) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
const applied = await entry.settle();
|
|
1187
|
+
this.#pending.delete(pendingKey(entry.delivery));
|
|
1188
|
+
if (applied) {
|
|
1189
|
+
entry.applied();
|
|
1190
|
+
} else {
|
|
1191
|
+
this.#dependencies.notifier.notify("onLeaseLost", { delivery: entry.delivery });
|
|
1192
|
+
}
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
#retain(entry) {
|
|
1198
|
+
this.#pending.set(pendingKey(entry.delivery), entry);
|
|
1199
|
+
this.#recoveryTimer ??= setInterval(
|
|
1200
|
+
() => void this.#recoverSettlements(),
|
|
1201
|
+
this.#dependencies.settings.pollIntervalMs
|
|
1202
|
+
);
|
|
1203
|
+
this.#recoveryTimer.unref();
|
|
1204
|
+
}
|
|
1205
|
+
/** Gives lease groups the owner no longer works on back to the store, so other workers need not wait for expiry. */
|
|
1206
|
+
async #relinquish() {
|
|
1207
|
+
const { store, clock, notifier, settings } = this.#dependencies;
|
|
1208
|
+
try {
|
|
1209
|
+
await store.relinquish?.(settings.owner, clock.now());
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
notifier.failure("relinquish", error);
|
|
1212
|
+
}
|
|
703
1213
|
}
|
|
704
1214
|
/** Claims one batch and resolves when every claimed delivery settled. Returns how many ran. */
|
|
705
1215
|
async processBatch() {
|
|
706
|
-
const started = await this.#claimAndStart();
|
|
1216
|
+
const { started } = await this.#claimAndStart();
|
|
707
1217
|
await Promise.allSettled(started.map((delivery) => delivery.settled));
|
|
708
1218
|
return started.length;
|
|
709
1219
|
}
|
|
@@ -716,44 +1226,102 @@ var Dispatcher = class {
|
|
|
716
1226
|
clearTimeout(timer);
|
|
717
1227
|
return outcome;
|
|
718
1228
|
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Claims until stopped. A claim that filled its batch is followed by the next one at once while enough
|
|
1231
|
+
* slots are free. Otherwise the loop sleeps until a wake-up (a publish, a store notification, or enough
|
|
1232
|
+
* deliveries settling to refill a group of slots) or the poll interval. Only the poll interval claims
|
|
1233
|
+
* for fewer free slots than the refill threshold, so a busy dispatcher never claims slot by slot.
|
|
1234
|
+
*/
|
|
719
1235
|
async #runLoop() {
|
|
1236
|
+
let minimumFree = 1;
|
|
720
1237
|
while (this.#running) {
|
|
721
|
-
let
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1238
|
+
let round = { requested: 0, claimed: 0 };
|
|
1239
|
+
if (this.#freeSlots() >= minimumFree) {
|
|
1240
|
+
try {
|
|
1241
|
+
round = await this.#claimAndStart();
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
this.#dependencies.notifier.failure("claim", error);
|
|
1244
|
+
}
|
|
726
1245
|
}
|
|
727
|
-
if (
|
|
1246
|
+
if (round.requested > 0 && round.claimed === round.requested && this.#freeSlots() >= this.#refillThreshold) {
|
|
1247
|
+
minimumFree = this.#refillThreshold;
|
|
728
1248
|
continue;
|
|
729
1249
|
}
|
|
730
|
-
await this.#waitForWakeUp();
|
|
1250
|
+
minimumFree = await this.#waitForWakeUp();
|
|
1251
|
+
if (this.#freeSlots() < minimumFree) {
|
|
1252
|
+
this.#requestClaim();
|
|
1253
|
+
}
|
|
731
1254
|
}
|
|
732
1255
|
}
|
|
1256
|
+
/** Waits for a wake-up or the poll interval. Resolves with the free slots the next claim needs. */
|
|
733
1257
|
async #waitForWakeUp() {
|
|
734
|
-
if (this.#
|
|
735
|
-
|
|
736
|
-
|
|
1258
|
+
if (this.#signalledMinimum === void 0) {
|
|
1259
|
+
await new Promise((resolve) => {
|
|
1260
|
+
const timer = setTimeout(() => {
|
|
1261
|
+
this.#signalledMinimum = 1;
|
|
1262
|
+
resolve();
|
|
1263
|
+
}, this.#dependencies.settings.pollIntervalMs);
|
|
1264
|
+
this.#wakeUp = () => {
|
|
1265
|
+
clearTimeout(timer);
|
|
1266
|
+
resolve();
|
|
1267
|
+
};
|
|
1268
|
+
});
|
|
1269
|
+
this.#wakeUp = void 0;
|
|
1270
|
+
}
|
|
1271
|
+
const minimumFree = this.#signalledMinimum;
|
|
1272
|
+
this.#signalledMinimum = void 0;
|
|
1273
|
+
return minimumFree;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* A delivery settled. Its slot is worth refilling when the store may have more for its handler: a keyed
|
|
1277
|
+
* delivery unblocks the next one of its key, a delivery that did not succeed may be due again at once
|
|
1278
|
+
* (a retry without delay, a release), and a handler whose last claim was full may have a backlog.
|
|
1279
|
+
*/
|
|
1280
|
+
#onSettled(delivery, succeeded) {
|
|
1281
|
+
if (delivery.event.key !== null || !succeeded || !this.#exhaustedHandlers.has(delivery.handler)) {
|
|
1282
|
+
this.#requestClaim();
|
|
737
1283
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Asks the loop for a claim. Once a group of slots is free the claim waits only for the deliveries
|
|
1287
|
+
* settling in the same tick, so they are claimed for together; fewer free slots are refilled when no
|
|
1288
|
+
* claim started for a short while. With no free slot the next settlement asks again.
|
|
1289
|
+
*/
|
|
1290
|
+
#requestClaim() {
|
|
1291
|
+
const free = this.#freeSlots();
|
|
1292
|
+
if (free >= this.#refillThreshold) {
|
|
1293
|
+
if (!this.#refillScheduled) {
|
|
1294
|
+
this.#refillScheduled = true;
|
|
1295
|
+
setImmediate(() => {
|
|
1296
|
+
this.#refillScheduled = false;
|
|
1297
|
+
this.#signal(this.#refillThreshold);
|
|
1298
|
+
});
|
|
743
1299
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1300
|
+
} else if (free > 0) {
|
|
1301
|
+
this.#quietRefillTimer ??= setTimeout(
|
|
1302
|
+
() => {
|
|
1303
|
+
this.#quietRefillTimer = void 0;
|
|
1304
|
+
this.#signal(1);
|
|
1305
|
+
},
|
|
1306
|
+
Math.max(0, this.#lastClaimStartedAt + QUIET_REFILL_MS - Date.now())
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
748
1309
|
}
|
|
749
1310
|
#freeSlots() {
|
|
750
1311
|
return this.#dependencies.settings.concurrency - this.#inFlight.size;
|
|
751
1312
|
}
|
|
1313
|
+
/** Claims for every handler with room and starts what was claimed. */
|
|
752
1314
|
async #claimAndStart() {
|
|
753
1315
|
const { store, registry, clock, settings } = this.#dependencies;
|
|
754
1316
|
const started = [];
|
|
1317
|
+
let requested = 0;
|
|
755
1318
|
for (const request of this.#planClaims(registry.all())) {
|
|
756
1319
|
const now = clock.now();
|
|
1320
|
+
const wakeGeneration = this.#wakeGeneration;
|
|
1321
|
+
this.#lastClaimStartedAt = Date.now();
|
|
1322
|
+
clearTimeout(this.#quietRefillTimer);
|
|
1323
|
+
this.#quietRefillTimer = void 0;
|
|
1324
|
+
requested += request.limit;
|
|
757
1325
|
const claimed = await store.claim({
|
|
758
1326
|
handlers: request.handlers,
|
|
759
1327
|
limit: request.limit,
|
|
@@ -761,11 +1329,18 @@ var Dispatcher = class {
|
|
|
761
1329
|
leaseExpiresAt: new Date(now.getTime() + settings.leaseDurationMs),
|
|
762
1330
|
now
|
|
763
1331
|
});
|
|
1332
|
+
for (const handler of request.handlers) {
|
|
1333
|
+
if (claimed.length < request.limit && wakeGeneration === this.#wakeGeneration) {
|
|
1334
|
+
this.#exhaustedHandlers.add(handler);
|
|
1335
|
+
} else {
|
|
1336
|
+
this.#exhaustedHandlers.delete(handler);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
764
1339
|
for (const delivery of claimed) {
|
|
765
1340
|
started.push(this.#start(delivery));
|
|
766
1341
|
}
|
|
767
1342
|
}
|
|
768
|
-
return started;
|
|
1343
|
+
return { requested, claimed: started.length, started };
|
|
769
1344
|
}
|
|
770
1345
|
/** One claim for handlers without a concurrency limit, one per limited handler with room. */
|
|
771
1346
|
#planClaims(registrations) {
|
|
@@ -799,20 +1374,29 @@ var Dispatcher = class {
|
|
|
799
1374
|
}
|
|
800
1375
|
#start(delivery) {
|
|
801
1376
|
const controller = new AbortController();
|
|
1377
|
+
let succeeded = false;
|
|
802
1378
|
const entry = {
|
|
803
1379
|
handler: delivery.handler,
|
|
804
1380
|
controller,
|
|
805
|
-
settled: Promise.resolve().then(
|
|
1381
|
+
settled: Promise.resolve().then(async () => {
|
|
1382
|
+
succeeded = await this.#execute(delivery, controller);
|
|
1383
|
+
}).catch((error) => this.#dependencies.notifier.failure("dispatch", error)).finally(() => {
|
|
806
1384
|
this.#inFlight.delete(entry);
|
|
807
|
-
this
|
|
1385
|
+
this.#onSettled(delivery, succeeded);
|
|
808
1386
|
})
|
|
809
1387
|
};
|
|
810
1388
|
this.#inFlight.add(entry);
|
|
811
1389
|
return entry;
|
|
812
1390
|
}
|
|
1391
|
+
/** Runs one delivery. Resolves true when its handler succeeded, so the delivery is done for good. */
|
|
813
1392
|
async #execute(delivery, controller) {
|
|
814
1393
|
const { registry, notifier, clock } = this.#dependencies;
|
|
815
1394
|
const registration = registry.get(delivery.handler);
|
|
1395
|
+
for (const key of [...this.#pending.keys()].filter(
|
|
1396
|
+
(pending) => pending.startsWith(`${delivery.deliveryId}\0`)
|
|
1397
|
+
)) {
|
|
1398
|
+
this.#pending.delete(key);
|
|
1399
|
+
}
|
|
816
1400
|
notifier.notify("onDeliveryStarted", { delivery });
|
|
817
1401
|
const startedAt = clock.now().getTime();
|
|
818
1402
|
if (delivery.attempt > registration.retry.attempts) {
|
|
@@ -822,26 +1406,35 @@ var Dispatcher = class {
|
|
|
822
1406
|
`Delivery reached attempt ${delivery.attempt} of ${registration.retry.attempts} after its lease expired`
|
|
823
1407
|
)
|
|
824
1408
|
);
|
|
825
|
-
return;
|
|
1409
|
+
return false;
|
|
826
1410
|
}
|
|
827
1411
|
let stopHeartbeat = () => void 0;
|
|
828
1412
|
try {
|
|
829
1413
|
stopHeartbeat = registration.transactional ? await this.#guardTransactionalLease(registration, delivery, controller) : this.#startHeartbeat(delivery, controller);
|
|
830
1414
|
const outcome = await this.#runHandler(registration, delivery, controller);
|
|
1415
|
+
const durationMs = clock.now().getTime() - startedAt;
|
|
831
1416
|
if (outcome === "completed") {
|
|
832
|
-
notifier.notify("onDeliverySucceeded", {
|
|
1417
|
+
notifier.notify("onDeliverySucceeded", { delivery, durationMs });
|
|
1418
|
+
} else {
|
|
1419
|
+
this.#retain({
|
|
833
1420
|
delivery,
|
|
834
|
-
|
|
1421
|
+
settle: () => this.#complete(delivery),
|
|
1422
|
+
applied: () => notifier.notify("onDeliverySucceeded", { delivery, durationMs })
|
|
835
1423
|
});
|
|
836
1424
|
}
|
|
1425
|
+
return true;
|
|
837
1426
|
} catch (error) {
|
|
838
1427
|
if (error instanceof LeaseLostError) {
|
|
839
1428
|
notifier.notify("onLeaseLost", { delivery });
|
|
1429
|
+
if (!error.storeConfirmed) {
|
|
1430
|
+
await this.#giveBack(registration, delivery, error);
|
|
1431
|
+
}
|
|
840
1432
|
} else if (error instanceof DispatcherStoppedError) {
|
|
841
1433
|
await this.#release(delivery);
|
|
842
1434
|
} else {
|
|
843
1435
|
await this.#fail(registration, delivery, error);
|
|
844
1436
|
}
|
|
1437
|
+
return false;
|
|
845
1438
|
} finally {
|
|
846
1439
|
stopHeartbeat();
|
|
847
1440
|
}
|
|
@@ -858,7 +1451,9 @@ var Dispatcher = class {
|
|
|
858
1451
|
await store.transaction(async (transaction) => {
|
|
859
1452
|
await this.#invoke(registration, event, delivery, controller, transaction);
|
|
860
1453
|
if (!await store.complete(delivery, clock.now(), transaction)) {
|
|
861
|
-
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost
|
|
1454
|
+
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, {
|
|
1455
|
+
storeConfirmed: true
|
|
1456
|
+
});
|
|
862
1457
|
}
|
|
863
1458
|
});
|
|
864
1459
|
return "completed";
|
|
@@ -871,11 +1466,11 @@ var Dispatcher = class {
|
|
|
871
1466
|
* still held. Gives up to lease expiry, which reruns the handler: at-least-once.
|
|
872
1467
|
*/
|
|
873
1468
|
async #confirmCompletion(delivery) {
|
|
874
|
-
const {
|
|
1469
|
+
const { notifier } = this.#dependencies;
|
|
875
1470
|
for (let attempt = 0; ; attempt += 1) {
|
|
876
1471
|
let completed;
|
|
877
1472
|
try {
|
|
878
|
-
completed = await
|
|
1473
|
+
completed = await this.#complete(delivery);
|
|
879
1474
|
} catch (error) {
|
|
880
1475
|
const delay = COMPLETION_RETRY_DELAYS_MS[attempt];
|
|
881
1476
|
if (delay === void 0) {
|
|
@@ -886,11 +1481,18 @@ var Dispatcher = class {
|
|
|
886
1481
|
continue;
|
|
887
1482
|
}
|
|
888
1483
|
if (!completed) {
|
|
889
|
-
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost
|
|
1484
|
+
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, {
|
|
1485
|
+
storeConfirmed: true
|
|
1486
|
+
});
|
|
890
1487
|
}
|
|
891
1488
|
return "completed";
|
|
892
1489
|
}
|
|
893
1490
|
}
|
|
1491
|
+
/** Records one completion, grouped with its neighbours when the store can complete many at once. */
|
|
1492
|
+
#complete(delivery) {
|
|
1493
|
+
const { store, clock } = this.#dependencies;
|
|
1494
|
+
return this.#completions === void 0 ? store.complete(delivery, clock.now()) : this.#completions.submit(delivery);
|
|
1495
|
+
}
|
|
894
1496
|
async #invoke(registration, event, delivery, controller, transaction) {
|
|
895
1497
|
const context = {
|
|
896
1498
|
deliveryId: delivery.deliveryId,
|
|
@@ -964,7 +1566,9 @@ var Dispatcher = class {
|
|
|
964
1566
|
if (registration.timeoutMs !== null && now.getTime() + registration.timeoutMs >= expiry) {
|
|
965
1567
|
expiry = now.getTime() + registration.timeoutMs + settings.leaseDurationMs;
|
|
966
1568
|
if (!await store.extendLease(delivery, new Date(expiry), now)) {
|
|
967
|
-
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost
|
|
1569
|
+
throw new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, {
|
|
1570
|
+
storeConfirmed: true
|
|
1571
|
+
});
|
|
968
1572
|
}
|
|
969
1573
|
}
|
|
970
1574
|
const timer = setTimeout(
|
|
@@ -985,8 +1589,10 @@ var Dispatcher = class {
|
|
|
985
1589
|
const { store, clock, notifier, settings } = this.#dependencies;
|
|
986
1590
|
let confirmedExpiry = delivery.leaseExpiresAt.getTime();
|
|
987
1591
|
let renewing = false;
|
|
988
|
-
const abortLeaseLost = () => {
|
|
989
|
-
controller.abort(
|
|
1592
|
+
const abortLeaseLost = (storeConfirmed = false) => {
|
|
1593
|
+
controller.abort(
|
|
1594
|
+
new LeaseLostError(`Lease on delivery ${delivery.deliveryId} was lost`, { storeConfirmed })
|
|
1595
|
+
);
|
|
990
1596
|
};
|
|
991
1597
|
const timer = setInterval(
|
|
992
1598
|
() => {
|
|
@@ -1004,7 +1610,7 @@ var Dispatcher = class {
|
|
|
1004
1610
|
if (extended) {
|
|
1005
1611
|
confirmedExpiry = expiry;
|
|
1006
1612
|
} else {
|
|
1007
|
-
abortLeaseLost();
|
|
1613
|
+
abortLeaseLost(true);
|
|
1008
1614
|
}
|
|
1009
1615
|
}).catch((error) => notifier.failure("extend-lease", error)).finally(() => {
|
|
1010
1616
|
renewing = false;
|
|
@@ -1016,12 +1622,17 @@ var Dispatcher = class {
|
|
|
1016
1622
|
return () => clearInterval(timer);
|
|
1017
1623
|
}
|
|
1018
1624
|
async #fail(registration, delivery, error) {
|
|
1019
|
-
const { store, clock, random, notifier } = this.#dependencies;
|
|
1020
1625
|
const policy = registration.retry;
|
|
1021
1626
|
if (delivery.attempt >= policy.attempts || !isRetryableError(policy, error)) {
|
|
1022
1627
|
await this.#bury(delivery, error);
|
|
1023
1628
|
return;
|
|
1024
1629
|
}
|
|
1630
|
+
await this.#reschedule(registration, delivery, error);
|
|
1631
|
+
}
|
|
1632
|
+
/** Schedules the next attempt with the retry policy's backoff. */
|
|
1633
|
+
async #reschedule(registration, delivery, error) {
|
|
1634
|
+
const { store, clock, random, notifier } = this.#dependencies;
|
|
1635
|
+
const policy = registration.retry;
|
|
1025
1636
|
const errorInfo = toErrorInfo(error);
|
|
1026
1637
|
const now = clock.now();
|
|
1027
1638
|
const availableAt = new Date(
|
|
@@ -1032,6 +1643,11 @@ var Dispatcher = class {
|
|
|
1032
1643
|
rescheduled = await store.reschedule(delivery, availableAt, errorInfo, now);
|
|
1033
1644
|
} catch (storeError) {
|
|
1034
1645
|
notifier.failure("reschedule", storeError);
|
|
1646
|
+
this.#retain({
|
|
1647
|
+
delivery,
|
|
1648
|
+
settle: () => store.reschedule(delivery, availableAt, errorInfo, now),
|
|
1649
|
+
applied: () => notifier.notify("onDeliveryRetryScheduled", { delivery, error: errorInfo, availableAt })
|
|
1650
|
+
});
|
|
1035
1651
|
return;
|
|
1036
1652
|
}
|
|
1037
1653
|
if (rescheduled) {
|
|
@@ -1048,6 +1664,11 @@ var Dispatcher = class {
|
|
|
1048
1664
|
buried = await store.bury(delivery, errorInfo, clock.now());
|
|
1049
1665
|
} catch (storeError) {
|
|
1050
1666
|
notifier.failure("bury", storeError);
|
|
1667
|
+
this.#retain({
|
|
1668
|
+
delivery,
|
|
1669
|
+
settle: () => store.bury(delivery, errorInfo, clock.now()),
|
|
1670
|
+
applied: () => notifier.notify("onDeliveryDead", { delivery, error: errorInfo })
|
|
1671
|
+
});
|
|
1051
1672
|
return;
|
|
1052
1673
|
}
|
|
1053
1674
|
if (buried) {
|
|
@@ -1056,6 +1677,21 @@ var Dispatcher = class {
|
|
|
1056
1677
|
notifier.notify("onLeaseLost", { delivery });
|
|
1057
1678
|
}
|
|
1058
1679
|
}
|
|
1680
|
+
/**
|
|
1681
|
+
* A worker that only noticed locally that its lease ran out may still own the delivery in the
|
|
1682
|
+
* store, where other deliveries of its lane keep renewing the lane: it must not stay in flight
|
|
1683
|
+
* for ever. The attempt took longer than its lease, so it counts as a failed attempt (an
|
|
1684
|
+
* exhausted delivery is dead-lettered, whatever a custom `retryIf` says): a handler that always
|
|
1685
|
+
* outlives its lease cannot run for ever at attempt 1. If the lease is gone for real the store
|
|
1686
|
+
* refuses and nothing changes. The lost lease was reported already.
|
|
1687
|
+
*/
|
|
1688
|
+
async #giveBack(registration, delivery, error) {
|
|
1689
|
+
if (delivery.attempt >= registration.retry.attempts) {
|
|
1690
|
+
await this.#bury(delivery, error);
|
|
1691
|
+
} else {
|
|
1692
|
+
await this.#reschedule(registration, delivery, error);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1059
1695
|
async #release(delivery) {
|
|
1060
1696
|
const { store, clock, notifier } = this.#dependencies;
|
|
1061
1697
|
let released;
|
|
@@ -1063,6 +1699,11 @@ var Dispatcher = class {
|
|
|
1063
1699
|
released = await store.release(delivery, clock.now());
|
|
1064
1700
|
} catch (storeError) {
|
|
1065
1701
|
notifier.failure("release", storeError);
|
|
1702
|
+
this.#retain({
|
|
1703
|
+
delivery,
|
|
1704
|
+
settle: () => store.release(delivery, clock.now()),
|
|
1705
|
+
applied: () => notifier.notify("onDeliveryReleased", { delivery })
|
|
1706
|
+
});
|
|
1066
1707
|
return;
|
|
1067
1708
|
}
|
|
1068
1709
|
if (released) {
|
|
@@ -1072,6 +1713,9 @@ var Dispatcher = class {
|
|
|
1072
1713
|
}
|
|
1073
1714
|
}
|
|
1074
1715
|
};
|
|
1716
|
+
function pendingKey(delivery) {
|
|
1717
|
+
return `${delivery.deliveryId}\0${delivery.leaseToken}`;
|
|
1718
|
+
}
|
|
1075
1719
|
function rejectOnAbort(signal) {
|
|
1076
1720
|
return new Promise((_, reject) => {
|
|
1077
1721
|
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
@@ -1196,6 +1840,8 @@ var DEFAULTS = {
|
|
|
1196
1840
|
pollInterval: "1s",
|
|
1197
1841
|
leaseDuration: "30s",
|
|
1198
1842
|
shutdownTimeout: "30s",
|
|
1843
|
+
completionBatchWindow: "2ms",
|
|
1844
|
+
completionBatchSize: 200,
|
|
1199
1845
|
completedRetention: "7d",
|
|
1200
1846
|
retentionInterval: "1h",
|
|
1201
1847
|
routeTimeoutMs: 3e4
|
|
@@ -1218,6 +1864,7 @@ var RelayBus = class {
|
|
|
1218
1864
|
#generateIdentifier;
|
|
1219
1865
|
#dispatcher;
|
|
1220
1866
|
#dispatcherEnabled;
|
|
1867
|
+
#catalogRequirements = [];
|
|
1221
1868
|
#setupStore;
|
|
1222
1869
|
#retention;
|
|
1223
1870
|
#retentionTimer;
|
|
@@ -1264,6 +1911,9 @@ var RelayBus = class {
|
|
|
1264
1911
|
throw new ConfigurationError(`Transport '${transport.name}' cannot subscribe`);
|
|
1265
1912
|
}
|
|
1266
1913
|
}
|
|
1914
|
+
for (const catalog of options.catalogs ?? []) {
|
|
1915
|
+
this.registerCatalog(catalog);
|
|
1916
|
+
}
|
|
1267
1917
|
this.#dispatcherEnabled = options.dispatcher?.enabled ?? true;
|
|
1268
1918
|
this.#dispatcher = new Dispatcher({
|
|
1269
1919
|
store: this.#store,
|
|
@@ -1305,6 +1955,28 @@ var RelayBus = class {
|
|
|
1305
1955
|
this.#defaults
|
|
1306
1956
|
);
|
|
1307
1957
|
}
|
|
1958
|
+
subscribeCatalog(catalog, handlers, options = {}) {
|
|
1959
|
+
for (const binding of resolveCatalogHandlers(catalog, handlers, options)) {
|
|
1960
|
+
this.subscribe(binding.eventClass, binding.handle, binding.options);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Adds a catalog, or a `consume` / `produce` declaration, to the checks
|
|
1965
|
+
* `start()` runs; `start()` throws a {@link ConfigurationError} naming each
|
|
1966
|
+
* event that is not covered and what it lacks. Equivalent to the `catalogs`
|
|
1967
|
+
* option.
|
|
1968
|
+
*/
|
|
1969
|
+
registerCatalog(catalog) {
|
|
1970
|
+
this.#catalogRequirements.push(toCatalogRequirement(catalog));
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* A view of this bus that publishes only the events of `catalog`, or only
|
|
1974
|
+
* the events named in `options.events` (for example the events this service
|
|
1975
|
+
* produces). Other events fail to compile and are rejected at runtime.
|
|
1976
|
+
*/
|
|
1977
|
+
forCatalog(catalog, options = {}) {
|
|
1978
|
+
return new CatalogPublisher(catalog, this, options);
|
|
1979
|
+
}
|
|
1308
1980
|
/** Registered handlers and routes, for diagnostics. */
|
|
1309
1981
|
handlers() {
|
|
1310
1982
|
return this.#registry.all().map(toDescriptor);
|
|
@@ -1370,6 +2042,7 @@ var RelayBus = class {
|
|
|
1370
2042
|
this.#state === "stopped" ? "A stopped relaybus cannot be started again; create a new instance" : `Cannot start a relaybus that is ${this.#state}`
|
|
1371
2043
|
);
|
|
1372
2044
|
}
|
|
2045
|
+
assertCatalogsCovered(this.#catalogRequirements, (type) => this.#registrationsOf(type));
|
|
1373
2046
|
this.#state = "starting";
|
|
1374
2047
|
this.#starting = this.#startComponents();
|
|
1375
2048
|
try {
|
|
@@ -1389,6 +2062,10 @@ var RelayBus = class {
|
|
|
1389
2062
|
if (this.#state === "starting") {
|
|
1390
2063
|
await this.#starting.catch(() => void 0);
|
|
1391
2064
|
}
|
|
2065
|
+
if (this.#state === "idle") {
|
|
2066
|
+
await this.#dispatcher.stop();
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
1392
2069
|
if (this.#state !== "running") {
|
|
1393
2070
|
return;
|
|
1394
2071
|
}
|
|
@@ -1581,6 +2258,11 @@ var RelayBus = class {
|
|
|
1581
2258
|
#createDelivery(registration, availableAt) {
|
|
1582
2259
|
return { id: this.#generateIdentifier(), handler: registration.id, availableAt };
|
|
1583
2260
|
}
|
|
2261
|
+
#registrationsOf(eventType) {
|
|
2262
|
+
const registrations = this.#registry.forEventType(eventType);
|
|
2263
|
+
const routes = registrations.filter((registration) => registration.forwardsToTransport).length;
|
|
2264
|
+
return { handlers: registrations.length - routes, routes };
|
|
2265
|
+
}
|
|
1584
2266
|
#registerRoute(route) {
|
|
1585
2267
|
const transport = this.#requireTransport(route.transport);
|
|
1586
2268
|
const { type } = describeEvent(route.event);
|
|
@@ -1608,7 +2290,7 @@ var RelayBus = class {
|
|
|
1608
2290
|
return transport;
|
|
1609
2291
|
}
|
|
1610
2292
|
#scheduleWake() {
|
|
1611
|
-
if (this.#state !== "running") {
|
|
2293
|
+
if (this.#state !== "running" || this.#dispatcher.watchesNewDeliveries) {
|
|
1612
2294
|
return;
|
|
1613
2295
|
}
|
|
1614
2296
|
setTimeout(() => this.#dispatcher.wake(), WAKE_DELAY_MS).unref();
|
|
@@ -1713,6 +2395,14 @@ function createDispatcherSettings(options, generateIdentifier) {
|
|
|
1713
2395
|
shutdownTimeoutMs: toTimerMilliseconds(
|
|
1714
2396
|
options?.shutdownTimeout ?? DEFAULTS.shutdownTimeout,
|
|
1715
2397
|
"dispatcher.shutdownTimeout"
|
|
2398
|
+
),
|
|
2399
|
+
completionBatchWindowMs: toMilliseconds(
|
|
2400
|
+
options?.completionBatchWindow ?? DEFAULTS.completionBatchWindow,
|
|
2401
|
+
"dispatcher.completionBatchWindow"
|
|
2402
|
+
),
|
|
2403
|
+
completionBatchSize: positiveInteger(
|
|
2404
|
+
options?.completionBatchSize ?? DEFAULTS.completionBatchSize,
|
|
2405
|
+
"dispatcher.completionBatchSize"
|
|
1716
2406
|
)
|
|
1717
2407
|
};
|
|
1718
2408
|
}
|
|
@@ -1729,6 +2419,7 @@ function createRetentionSettings(options) {
|
|
|
1729
2419
|
};
|
|
1730
2420
|
}
|
|
1731
2421
|
|
|
2422
|
+
exports.CatalogPublisher = CatalogPublisher;
|
|
1732
2423
|
exports.ConfigurationError = ConfigurationError;
|
|
1733
2424
|
exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
|
|
1734
2425
|
exports.DEFAULT_ROUTE_RETRY_POLICY = DEFAULT_ROUTE_RETRY_POLICY;
|
|
@@ -1747,10 +2438,15 @@ exports.RelayBusError = RelayBusError;
|
|
|
1747
2438
|
exports.TransactionRequiredError = TransactionRequiredError;
|
|
1748
2439
|
exports.assertEventType = assertEventType;
|
|
1749
2440
|
exports.computeRetryDelay = computeRetryDelay;
|
|
2441
|
+
exports.consume = consume;
|
|
1750
2442
|
exports.createRetryPolicy = createRetryPolicy;
|
|
1751
2443
|
exports.createUuidV7 = createUuidV7;
|
|
2444
|
+
exports.defineCatalog = defineCatalog;
|
|
1752
2445
|
exports.defineEvent = defineEvent;
|
|
2446
|
+
exports.defineHandlers = defineHandlers;
|
|
2447
|
+
exports.describeCatalog = describeCatalog;
|
|
1753
2448
|
exports.isRetryableError = isRetryableError;
|
|
2449
|
+
exports.produce = produce;
|
|
1754
2450
|
exports.systemClock = systemClock;
|
|
1755
2451
|
exports.toErrorInfo = toErrorInfo;
|
|
1756
2452
|
exports.toMilliseconds = toMilliseconds;
|