@dotpress/events 0.0.0-stage → 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +707 -2
- package/dist/aws-sqs.d.ts +127 -0
- package/dist/aws-sqs.js +440 -0
- package/dist/aws-sqs.js.map +1 -0
- package/dist/azure-service-bus.d.ts +99 -0
- package/dist/azure-service-bus.js +320 -0
- package/dist/azure-service-bus.js.map +1 -0
- package/dist/bullmq.d.ts +92 -0
- package/dist/bullmq.js +169 -0
- package/dist/bullmq.js.map +1 -0
- package/dist/bus-Bei_kN-e.d.ts +137 -0
- package/dist/chunk-E4NMMZMY.js +24 -0
- package/dist/chunk-E4NMMZMY.js.map +1 -0
- package/dist/chunk-NCQ4ZTNB.js +50 -0
- package/dist/chunk-NCQ4ZTNB.js.map +1 -0
- package/dist/cli.js +202 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloudflare-queues.d.ts +120 -0
- package/dist/cloudflare-queues.js +333 -0
- package/dist/cloudflare-queues.js.map +1 -0
- package/dist/google-pubsub.d.ts +111 -0
- package/dist/google-pubsub.js +420 -0
- package/dist/google-pubsub.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +701 -0
- package/dist/index.js.map +1 -0
- package/dist/pg-boss.d.ts +79 -0
- package/dist/pg-boss.js +189 -0
- package/dist/pg-boss.js.map +1 -0
- package/dist/plugin.d.ts +40 -0
- package/dist/plugin.js +27 -0
- package/dist/plugin.js.map +1 -0
- package/dist/transport-BtNR1ucF.d.ts +273 -0
- package/package.json +139 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkLogger,
|
|
3
|
+
silentLogger
|
|
4
|
+
} from "./chunk-E4NMMZMY.js";
|
|
5
|
+
import {
|
|
6
|
+
EventEmitError,
|
|
7
|
+
EventPayloadError,
|
|
8
|
+
EventsError,
|
|
9
|
+
PartialSendError
|
|
10
|
+
} from "./chunk-NCQ4ZTNB.js";
|
|
11
|
+
|
|
12
|
+
// src/define.ts
|
|
13
|
+
var NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
14
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
var checkName = (prefix, kind, name) => {
|
|
16
|
+
if (typeof name !== "string" || !NAME.test(name)) {
|
|
17
|
+
throw new TypeError(
|
|
18
|
+
`${prefix}: ${kind} must have 1 to 128 letters, digits, ".", "_", ":" or "-", starting with a letter or a digit.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var checkTransport = (prefix, transport) => {
|
|
23
|
+
if (transport !== void 0 && (typeof transport !== "string" || transport === "")) {
|
|
24
|
+
throw new TypeError(`${prefix}: transport must be a non-empty string.`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var checkKeys = (prefix, options, allowed) => {
|
|
28
|
+
if (!isRecord(options)) throw new TypeError(`${prefix}: expected an object.`);
|
|
29
|
+
for (const key of Object.keys(options)) {
|
|
30
|
+
if (!allowed.has(key)) {
|
|
31
|
+
throw new TypeError(`${prefix}: unknown option "${key}".`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return options;
|
|
35
|
+
};
|
|
36
|
+
var events = /* @__PURE__ */ new WeakSet();
|
|
37
|
+
var loaders = /* @__PURE__ */ new WeakMap();
|
|
38
|
+
var isEvent = (value) => (
|
|
39
|
+
// WeakSet.has() returns false for other values.
|
|
40
|
+
events.has(value)
|
|
41
|
+
);
|
|
42
|
+
var loaderOf = (listener) => loaders.get(listener);
|
|
43
|
+
var defineEvent = (options) => {
|
|
44
|
+
const name = options?.name;
|
|
45
|
+
const prefix = typeof name === "string" ? `Invalid event "${name}"` : "Invalid event";
|
|
46
|
+
const values = checkKeys(
|
|
47
|
+
prefix,
|
|
48
|
+
options,
|
|
49
|
+
/* @__PURE__ */ new Set(["name", "schema", "transport"])
|
|
50
|
+
);
|
|
51
|
+
checkName(prefix, "name", name);
|
|
52
|
+
if (typeof values.schema?.safeParseAsync !== "function") {
|
|
53
|
+
throw new TypeError(`${prefix}: schema must be a Zod schema.`);
|
|
54
|
+
}
|
|
55
|
+
checkTransport(prefix, values.transport);
|
|
56
|
+
const event = Object.freeze({
|
|
57
|
+
name,
|
|
58
|
+
schema: values.schema,
|
|
59
|
+
transport: values.transport
|
|
60
|
+
});
|
|
61
|
+
events.add(event);
|
|
62
|
+
return event;
|
|
63
|
+
};
|
|
64
|
+
var defineListener = (event, options) => {
|
|
65
|
+
const name = options?.name;
|
|
66
|
+
const prefix = typeof name === "string" ? `Invalid listener "${name}"` : "Invalid listener";
|
|
67
|
+
if (!isEvent(event)) {
|
|
68
|
+
throw new TypeError(
|
|
69
|
+
`${prefix}: expected an event created by defineEvent().`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const values = checkKeys(
|
|
73
|
+
prefix,
|
|
74
|
+
options,
|
|
75
|
+
/* @__PURE__ */ new Set(["name", "transport", "handle", "load"])
|
|
76
|
+
);
|
|
77
|
+
checkName(prefix, "name", name);
|
|
78
|
+
checkTransport(prefix, values.transport);
|
|
79
|
+
const { handle, load } = values;
|
|
80
|
+
if (handle === void 0 === (load === void 0)) {
|
|
81
|
+
throw new TypeError(`${prefix}: set exactly one of handle and load.`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof (handle ?? load) !== "function") {
|
|
84
|
+
throw new TypeError(
|
|
85
|
+
`${prefix}: ${handle === void 0 ? "load" : "handle"} must be a function.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const listener = Object.freeze({
|
|
89
|
+
name,
|
|
90
|
+
event,
|
|
91
|
+
transport: values.transport
|
|
92
|
+
});
|
|
93
|
+
let loading;
|
|
94
|
+
loaders.set(
|
|
95
|
+
listener,
|
|
96
|
+
handle === void 0 ? () => (loading ??= load().then((module) => {
|
|
97
|
+
const loaded = module?.default;
|
|
98
|
+
if (typeof loaded !== "function") {
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`Listener "${name}": load() must resolve to a module whose default export is the handler.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return loaded;
|
|
104
|
+
})).catch((error) => {
|
|
105
|
+
loading = void 0;
|
|
106
|
+
throw error;
|
|
107
|
+
}) : () => Promise.resolve(handle)
|
|
108
|
+
);
|
|
109
|
+
return listener;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/envelope.ts
|
|
113
|
+
var hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
114
|
+
var messageId = async (emissionId, listener) => {
|
|
115
|
+
const digest = await crypto.subtle.digest(
|
|
116
|
+
"SHA-256",
|
|
117
|
+
new TextEncoder().encode(`${emissionId}
|
|
118
|
+
${listener}`)
|
|
119
|
+
);
|
|
120
|
+
const bytes = new Uint8Array(digest.slice(0, 16));
|
|
121
|
+
bytes[6] = bytes[6] & 15 | 128;
|
|
122
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
123
|
+
const value = hex(bytes);
|
|
124
|
+
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
|
125
|
+
};
|
|
126
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
127
|
+
var STRING_FIELDS = [
|
|
128
|
+
"id",
|
|
129
|
+
"emissionId",
|
|
130
|
+
"event",
|
|
131
|
+
"listener",
|
|
132
|
+
"emittedAt"
|
|
133
|
+
];
|
|
134
|
+
var parseEnvelope = (message) => {
|
|
135
|
+
let value = message;
|
|
136
|
+
if (typeof value === "string") {
|
|
137
|
+
try {
|
|
138
|
+
value = JSON.parse(value);
|
|
139
|
+
} catch {
|
|
140
|
+
return { problem: "the message is not JSON" };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!isRecord2(value)) return { problem: "the message is not an object" };
|
|
144
|
+
if (value.v !== 1) {
|
|
145
|
+
return {
|
|
146
|
+
problem: `unsupported envelope version ${JSON.stringify(value.v)}`
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
for (const field of STRING_FIELDS) {
|
|
150
|
+
if (typeof value[field] !== "string" || value[field] === "") {
|
|
151
|
+
return { problem: `the envelope has no ${field}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!("payload" in value)) return { problem: "the envelope has no payload" };
|
|
155
|
+
const { metadata } = value;
|
|
156
|
+
if (!isRecord2(metadata) || !Object.values(metadata).every((item) => typeof item === "string")) {
|
|
157
|
+
return { problem: "the metadata of the envelope is not a string map" };
|
|
158
|
+
}
|
|
159
|
+
return { envelope: value };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/transport.ts
|
|
163
|
+
var isTransport = (value) => typeof value?.send === "function" && typeof value.kind === "string";
|
|
164
|
+
|
|
165
|
+
// src/worker.ts
|
|
166
|
+
var OPTIONS = /* @__PURE__ */ new Set(["listeners", "logger", "onFailure", "maxAttempts"]);
|
|
167
|
+
var listenersByName = (label, listeners) => {
|
|
168
|
+
if (!Array.isArray(listeners)) {
|
|
169
|
+
throw new TypeError(`${label}: listeners must be a list.`);
|
|
170
|
+
}
|
|
171
|
+
const byName = /* @__PURE__ */ new Map();
|
|
172
|
+
for (const listener of listeners) {
|
|
173
|
+
if (!loaderOf(listener)) {
|
|
174
|
+
throw new TypeError(
|
|
175
|
+
`${label}: listeners must be created by defineListener().`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const { name } = listener;
|
|
179
|
+
if (byName.has(name)) {
|
|
180
|
+
throw new TypeError(`${label}: two listeners are named "${name}".`);
|
|
181
|
+
}
|
|
182
|
+
byName.set(name, listener);
|
|
183
|
+
}
|
|
184
|
+
return byName;
|
|
185
|
+
};
|
|
186
|
+
var describe = (error) => error instanceof Error ? error.message : String(error);
|
|
187
|
+
var createWorker = (options) => {
|
|
188
|
+
const label = "createWorker()";
|
|
189
|
+
if (typeof options !== "object" || options === null) {
|
|
190
|
+
throw new TypeError(`${label}: expected an object of options.`);
|
|
191
|
+
}
|
|
192
|
+
for (const key of Object.keys(options)) {
|
|
193
|
+
if (!OPTIONS.has(key)) {
|
|
194
|
+
throw new TypeError(`${label}: unknown option "${key}".`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const listeners = listenersByName(label, options.listeners);
|
|
198
|
+
const logger = options.logger === void 0 ? silentLogger : checkLogger(label, options.logger);
|
|
199
|
+
const { onFailure, maxAttempts } = options;
|
|
200
|
+
if (onFailure !== void 0 && typeof onFailure !== "function") {
|
|
201
|
+
throw new TypeError(`${label}: onFailure must be a function.`);
|
|
202
|
+
}
|
|
203
|
+
if (maxAttempts !== void 0 && (!Number.isInteger(maxAttempts) || maxAttempts < 1)) {
|
|
204
|
+
throw new TypeError(`${label}: maxAttempts must be a positive integer.`);
|
|
205
|
+
}
|
|
206
|
+
const fail = async (error, info) => {
|
|
207
|
+
const fields = {
|
|
208
|
+
event: info.envelope?.event,
|
|
209
|
+
listener: info.envelope?.listener,
|
|
210
|
+
messageId: info.envelope?.id,
|
|
211
|
+
attempt: info.attempt,
|
|
212
|
+
reason: info.reason,
|
|
213
|
+
error: describe(error)
|
|
214
|
+
};
|
|
215
|
+
if (info.final) {
|
|
216
|
+
logger.error(fields, "Event listener failed, the message is not retried");
|
|
217
|
+
} else {
|
|
218
|
+
logger.warn(fields, "Event listener failed, the message is retried");
|
|
219
|
+
}
|
|
220
|
+
if (onFailure) {
|
|
221
|
+
try {
|
|
222
|
+
await onFailure(error, info);
|
|
223
|
+
} catch (hookError) {
|
|
224
|
+
logger.error(
|
|
225
|
+
{ ...fields, hookError: describe(hookError) },
|
|
226
|
+
"onFailure hook failed"
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return info.reason === "listener-error" ? { status: "retry", error } : { status: "dead", reason: info.reason, error };
|
|
231
|
+
};
|
|
232
|
+
const handle = async (message, handleOptions = {}) => {
|
|
233
|
+
const attempt = handleOptions.attempt ?? 1;
|
|
234
|
+
const parsed = parseEnvelope(message);
|
|
235
|
+
if ("problem" in parsed) {
|
|
236
|
+
return fail(new Error(`Invalid message: ${parsed.problem}.`), {
|
|
237
|
+
envelope: void 0,
|
|
238
|
+
reason: "invalid-envelope",
|
|
239
|
+
attempt,
|
|
240
|
+
final: true
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const { envelope } = parsed;
|
|
244
|
+
const dead = (reason, text) => fail(new Error(text), { envelope, reason, attempt, final: true });
|
|
245
|
+
const listener = listeners.get(envelope.listener);
|
|
246
|
+
if (!listener) {
|
|
247
|
+
return dead(
|
|
248
|
+
"unknown-listener",
|
|
249
|
+
`Unknown listener "${envelope.listener}": this worker runs ${[...listeners.keys()].join(", ") || "no listener"}.`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (listener.event.name !== envelope.event) {
|
|
253
|
+
return dead(
|
|
254
|
+
"invalid-envelope",
|
|
255
|
+
`Listener "${listener.name}" listens to "${listener.event.name}", not "${envelope.event}".`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
let result;
|
|
259
|
+
try {
|
|
260
|
+
result = await listener.event.schema.safeParseAsync(envelope.payload);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return fail(error, {
|
|
263
|
+
envelope,
|
|
264
|
+
reason: "listener-error",
|
|
265
|
+
attempt,
|
|
266
|
+
final: maxAttempts !== void 0 && attempt >= maxAttempts
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (!result.success) {
|
|
270
|
+
const issues = result.error.issues.map(
|
|
271
|
+
({ path, message: text }) => path.length > 0 ? `${path.map(String).join(".")}: ${text}` : text
|
|
272
|
+
).join("; ");
|
|
273
|
+
return dead(
|
|
274
|
+
"invalid-payload",
|
|
275
|
+
`Invalid payload for event "${envelope.event}": ${issues}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const context = {
|
|
279
|
+
payload: result.data,
|
|
280
|
+
event: envelope.event,
|
|
281
|
+
listener: envelope.listener,
|
|
282
|
+
id: envelope.id,
|
|
283
|
+
emissionId: envelope.emissionId,
|
|
284
|
+
attempt,
|
|
285
|
+
emittedAt: new Date(envelope.emittedAt),
|
|
286
|
+
metadata: envelope.metadata,
|
|
287
|
+
logger
|
|
288
|
+
};
|
|
289
|
+
try {
|
|
290
|
+
const handler = await loaderOf(listener)();
|
|
291
|
+
await handler(context);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return fail(error, {
|
|
294
|
+
envelope,
|
|
295
|
+
reason: "listener-error",
|
|
296
|
+
attempt,
|
|
297
|
+
final: maxAttempts !== void 0 && attempt >= maxAttempts
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
logger.debug?.(
|
|
301
|
+
{
|
|
302
|
+
event: envelope.event,
|
|
303
|
+
listener: envelope.listener,
|
|
304
|
+
messageId: envelope.id,
|
|
305
|
+
attempt
|
|
306
|
+
},
|
|
307
|
+
"Event listener succeeded"
|
|
308
|
+
);
|
|
309
|
+
return { status: "success" };
|
|
310
|
+
};
|
|
311
|
+
return Object.freeze({
|
|
312
|
+
listeners: Object.freeze([...listeners.keys()]),
|
|
313
|
+
handle
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/bus.ts
|
|
318
|
+
var OPTIONS2 = /* @__PURE__ */ new Set([
|
|
319
|
+
"listeners",
|
|
320
|
+
"logger",
|
|
321
|
+
"onFailure",
|
|
322
|
+
"transport",
|
|
323
|
+
"transports"
|
|
324
|
+
]);
|
|
325
|
+
var METADATA_KEY = /^[A-Za-z0-9._-]{1,128}$/;
|
|
326
|
+
var LINE_BREAK = /[\r\n]/;
|
|
327
|
+
var checkMetadata = (label, metadata) => {
|
|
328
|
+
if (metadata === void 0) return {};
|
|
329
|
+
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
|
|
330
|
+
throw new TypeError(`${label}: metadata must be an object.`);
|
|
331
|
+
}
|
|
332
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
333
|
+
if (!METADATA_KEY.test(key)) {
|
|
334
|
+
throw new TypeError(
|
|
335
|
+
`${label}: metadata keys must have 1 to 128 letters, digits, ".", "_" or "-": ${JSON.stringify(key)}.`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (typeof value !== "string" || LINE_BREAK.test(value)) {
|
|
339
|
+
throw new TypeError(
|
|
340
|
+
`${label}: metadata.${key} must be a string on one line.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return metadata;
|
|
345
|
+
};
|
|
346
|
+
var routeOf = (listener) => listener.transport ?? listener.event.transport ?? "default";
|
|
347
|
+
var checkEmission = async (event, payload, emitOptions, contextMetadata) => {
|
|
348
|
+
if (!isEvent(event)) {
|
|
349
|
+
throw new TypeError("emit(): expected an event created by defineEvent().");
|
|
350
|
+
}
|
|
351
|
+
const emitLabel = `emit("${event.name}")`;
|
|
352
|
+
const { delay, metadata } = emitOptions ?? {};
|
|
353
|
+
if (delay !== void 0 && (!Number.isInteger(delay) || delay < 0)) {
|
|
354
|
+
throw new TypeError(
|
|
355
|
+
`${emitLabel}: delay must be a non-negative integer (seconds).`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const checked = {
|
|
359
|
+
...contextMetadata,
|
|
360
|
+
...checkMetadata(emitLabel, metadata)
|
|
361
|
+
};
|
|
362
|
+
let json;
|
|
363
|
+
try {
|
|
364
|
+
json = JSON.parse(JSON.stringify(payload) ?? "null");
|
|
365
|
+
} catch (error) {
|
|
366
|
+
throw new EventPayloadError(event.name, [
|
|
367
|
+
{
|
|
368
|
+
path: [],
|
|
369
|
+
message: `the payload cannot be serialized as JSON (${error instanceof Error ? error.message : String(error)})`
|
|
370
|
+
}
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
const result = await event.schema.safeParseAsync(json);
|
|
374
|
+
if (!result.success) {
|
|
375
|
+
throw new EventPayloadError(
|
|
376
|
+
event.name,
|
|
377
|
+
result.error.issues.map(({ path, message }) => ({
|
|
378
|
+
path: path.map((key) => typeof key === "symbol" ? String(key) : key),
|
|
379
|
+
message
|
|
380
|
+
}))
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return { event, json, delay, metadata: checked };
|
|
384
|
+
};
|
|
385
|
+
var createEventBus = (options) => {
|
|
386
|
+
const label = "createEventBus()";
|
|
387
|
+
if (typeof options !== "object" || options === null) {
|
|
388
|
+
throw new TypeError(`${label}: expected an object of options.`);
|
|
389
|
+
}
|
|
390
|
+
for (const key of Object.keys(options)) {
|
|
391
|
+
if (!OPTIONS2.has(key)) {
|
|
392
|
+
throw new TypeError(`${label}: unknown option "${key}".`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const listeners = listenersByName(label, options.listeners);
|
|
396
|
+
const logger = options.logger === void 0 ? silentLogger : checkLogger(label, options.logger);
|
|
397
|
+
const { onFailure } = options;
|
|
398
|
+
if (onFailure !== void 0 && typeof onFailure !== "function") {
|
|
399
|
+
throw new TypeError(`${label}: onFailure must be a function.`);
|
|
400
|
+
}
|
|
401
|
+
if (options.transport === void 0 === (options.transports === void 0)) {
|
|
402
|
+
throw new TypeError(
|
|
403
|
+
`${label}: set exactly one of transport and transports.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const transports = new Map(
|
|
407
|
+
options.transport === void 0 ? Object.entries(options.transports ?? {}).filter(
|
|
408
|
+
([, transport]) => transport !== void 0
|
|
409
|
+
) : [["default", options.transport]]
|
|
410
|
+
);
|
|
411
|
+
if (options.transports !== void 0 && (typeof options.transports !== "object" || options.transports === null)) {
|
|
412
|
+
throw new TypeError(`${label}: transports must be an object.`);
|
|
413
|
+
}
|
|
414
|
+
for (const [name, transport] of transports) {
|
|
415
|
+
if (!isTransport(transport)) {
|
|
416
|
+
throw new TypeError(
|
|
417
|
+
`${label}: the "${name}" transport must have a kind and a send() method.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const routed = /* @__PURE__ */ new Map();
|
|
422
|
+
for (const listener of listeners.values()) {
|
|
423
|
+
const route = routeOf(listener);
|
|
424
|
+
if (!transports.has(route)) {
|
|
425
|
+
throw new TypeError(
|
|
426
|
+
`${label}: listener "${listener.name}" (event "${listener.event.name}") uses the "${route}" transport, which is not configured.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
routed.set(route, [...routed.get(route) ?? [], listener]);
|
|
430
|
+
}
|
|
431
|
+
for (const [name, transport] of transports) {
|
|
432
|
+
transport.bind?.(
|
|
433
|
+
createWorker({
|
|
434
|
+
listeners: routed.get(name) ?? [],
|
|
435
|
+
logger,
|
|
436
|
+
onFailure,
|
|
437
|
+
maxAttempts: transport.maxAttempts
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const listenersOf = (event) => [...listeners.values()].filter((listener) => listener.event.name === event);
|
|
442
|
+
const create = (context) => {
|
|
443
|
+
const emit = async (event, payload, emitOptions = {}) => {
|
|
444
|
+
const { json, delay, metadata } = await checkEmission(
|
|
445
|
+
event,
|
|
446
|
+
payload,
|
|
447
|
+
emitOptions,
|
|
448
|
+
context.metadata
|
|
449
|
+
);
|
|
450
|
+
const emissionId = crypto.randomUUID();
|
|
451
|
+
const targets = listenersOf(event.name);
|
|
452
|
+
if (targets.length === 0) {
|
|
453
|
+
context.logger.debug?.(
|
|
454
|
+
{ event: event.name, emissionId },
|
|
455
|
+
"Event emitted without listeners"
|
|
456
|
+
);
|
|
457
|
+
return { emissionId, messages: [] };
|
|
458
|
+
}
|
|
459
|
+
const base = {
|
|
460
|
+
v: 1,
|
|
461
|
+
emissionId,
|
|
462
|
+
event: event.name,
|
|
463
|
+
payload: json,
|
|
464
|
+
emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
465
|
+
metadata
|
|
466
|
+
};
|
|
467
|
+
const messages = [];
|
|
468
|
+
for (const listener of targets) {
|
|
469
|
+
const id = await messageId(emissionId, listener.name);
|
|
470
|
+
messages.push({
|
|
471
|
+
ref: { id, listener: listener.name, transport: routeOf(listener) },
|
|
472
|
+
envelope: { ...base, id, listener: listener.name }
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const groups = /* @__PURE__ */ new Map();
|
|
476
|
+
for (const message of messages) {
|
|
477
|
+
const { transport } = message.ref;
|
|
478
|
+
groups.set(transport, [...groups.get(transport) ?? [], message]);
|
|
479
|
+
}
|
|
480
|
+
const sent = [];
|
|
481
|
+
const failed = [];
|
|
482
|
+
await Promise.all(
|
|
483
|
+
[...groups].map(async ([name, group]) => {
|
|
484
|
+
try {
|
|
485
|
+
await transports.get(name).send(
|
|
486
|
+
group.map(({ envelope }) => envelope),
|
|
487
|
+
{ delay }
|
|
488
|
+
);
|
|
489
|
+
for (const { ref } of group) sent.push(ref);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
const partial = error instanceof PartialSendError ? new Map(error.failed.map((item) => [item.id, item.error])) : void 0;
|
|
492
|
+
for (const { ref } of group) {
|
|
493
|
+
if (partial && !partial.has(ref.id)) sent.push(ref);
|
|
494
|
+
else failed.push({ ...ref, error: partial?.get(ref.id) ?? error });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
const order = (refs) => refs.sort(
|
|
500
|
+
(a, b) => targets.findIndex(({ name }) => name === a.listener) - targets.findIndex(({ name }) => name === b.listener)
|
|
501
|
+
);
|
|
502
|
+
if (failed.length > 0) {
|
|
503
|
+
throw new EventEmitError(event.name, order(sent), order(failed));
|
|
504
|
+
}
|
|
505
|
+
context.logger.info(
|
|
506
|
+
{ event: event.name, emissionId, messages: sent.length },
|
|
507
|
+
"Event emitted"
|
|
508
|
+
);
|
|
509
|
+
return {
|
|
510
|
+
emissionId,
|
|
511
|
+
messages: order(messages.map(({ ref }) => ref))
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
return Object.freeze({
|
|
515
|
+
emit,
|
|
516
|
+
withContext: ({ logger: other, metadata } = {}) => create({
|
|
517
|
+
logger: other === void 0 ? context.logger : checkLogger("withContext()", other),
|
|
518
|
+
metadata: {
|
|
519
|
+
...context.metadata,
|
|
520
|
+
...checkMetadata("withContext()", metadata)
|
|
521
|
+
}
|
|
522
|
+
})
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
return create({ logger, metadata: {} });
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// src/fake.ts
|
|
529
|
+
var createFakeEventBus = () => {
|
|
530
|
+
const emitted = [];
|
|
531
|
+
const create = (metadata) => {
|
|
532
|
+
const emit = async (event, payload, options) => {
|
|
533
|
+
const checked = await checkEmission(event, payload, options, metadata);
|
|
534
|
+
const emissionId = crypto.randomUUID();
|
|
535
|
+
emitted.push(
|
|
536
|
+
Object.freeze({
|
|
537
|
+
emissionId,
|
|
538
|
+
event: checked.event.name,
|
|
539
|
+
payload: checked.json,
|
|
540
|
+
delay: checked.delay,
|
|
541
|
+
metadata: Object.freeze(checked.metadata),
|
|
542
|
+
emittedAt: /* @__PURE__ */ new Date()
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
return { emissionId, messages: [] };
|
|
546
|
+
};
|
|
547
|
+
return Object.freeze({
|
|
548
|
+
emit,
|
|
549
|
+
withContext: ({ logger, metadata: added } = {}) => {
|
|
550
|
+
if (logger !== void 0) checkLogger("withContext()", logger);
|
|
551
|
+
return create({
|
|
552
|
+
...metadata,
|
|
553
|
+
...checkMetadata("withContext()", added)
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
const root = create({});
|
|
559
|
+
return Object.freeze({
|
|
560
|
+
...root,
|
|
561
|
+
emitted,
|
|
562
|
+
clear: () => {
|
|
563
|
+
emitted.length = 0;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/start-worker.ts
|
|
569
|
+
var startWorker = (worker, source, options = {}) => {
|
|
570
|
+
const label = "startWorker()";
|
|
571
|
+
if (typeof worker?.handle !== "function") {
|
|
572
|
+
throw new TypeError(
|
|
573
|
+
`${label}: expected a worker created by createWorker().`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (typeof source?.consume !== "function") {
|
|
577
|
+
throw new TypeError(`${label}: expected a source with a consume() method.`);
|
|
578
|
+
}
|
|
579
|
+
const logger = options.logger === void 0 ? silentLogger : checkLogger(label, options.logger);
|
|
580
|
+
const controller = new AbortController();
|
|
581
|
+
const stop = () => controller.abort();
|
|
582
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
583
|
+
if (options.signal?.aborted) stop();
|
|
584
|
+
logger.info(
|
|
585
|
+
{ source: source.kind, listeners: worker.listeners },
|
|
586
|
+
"Event worker started"
|
|
587
|
+
);
|
|
588
|
+
const done = source.consume(worker, { signal: controller.signal, logger }).finally(() => {
|
|
589
|
+
options.signal?.removeEventListener("abort", stop);
|
|
590
|
+
logger.info({ source: source.kind }, "Event worker stopped");
|
|
591
|
+
});
|
|
592
|
+
return {
|
|
593
|
+
stop: () => {
|
|
594
|
+
stop();
|
|
595
|
+
return done;
|
|
596
|
+
},
|
|
597
|
+
done
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/transports/memory.ts
|
|
602
|
+
var createMemoryTransport = (options = {}) => {
|
|
603
|
+
const label = "createMemoryTransport()";
|
|
604
|
+
const {
|
|
605
|
+
maxAttempts = 3,
|
|
606
|
+
retryDelay = (attempt) => 100 * 2 ** (attempt - 1)
|
|
607
|
+
} = options ?? {};
|
|
608
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
609
|
+
throw new TypeError(`${label}: maxAttempts must be a positive integer.`);
|
|
610
|
+
}
|
|
611
|
+
if (typeof retryDelay !== "function") {
|
|
612
|
+
throw new TypeError(`${label}: retryDelay must be a function.`);
|
|
613
|
+
}
|
|
614
|
+
const sent = [];
|
|
615
|
+
let worker;
|
|
616
|
+
let pending = 0;
|
|
617
|
+
const idle = [];
|
|
618
|
+
const settle = () => {
|
|
619
|
+
pending -= 1;
|
|
620
|
+
if (pending === 0) {
|
|
621
|
+
for (const resolve of idle.splice(0)) resolve();
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
const schedule = (json, attempt, delayMs) => {
|
|
625
|
+
pending += 1;
|
|
626
|
+
setTimeout(() => {
|
|
627
|
+
void (async () => {
|
|
628
|
+
const result = await worker.handle(json, { attempt });
|
|
629
|
+
if (result.status === "retry" && attempt < maxAttempts) {
|
|
630
|
+
schedule(json, attempt + 1, retryDelay(attempt));
|
|
631
|
+
}
|
|
632
|
+
settle();
|
|
633
|
+
})();
|
|
634
|
+
}, delayMs);
|
|
635
|
+
};
|
|
636
|
+
return {
|
|
637
|
+
kind: "memory",
|
|
638
|
+
maxAttempts,
|
|
639
|
+
sent,
|
|
640
|
+
bind: (bound) => {
|
|
641
|
+
worker = bound;
|
|
642
|
+
},
|
|
643
|
+
send: async (envelopes, { delay }) => {
|
|
644
|
+
if (!worker) {
|
|
645
|
+
throw new Error("The memory transport is not used by an event bus.");
|
|
646
|
+
}
|
|
647
|
+
for (const envelope of envelopes) {
|
|
648
|
+
const json = JSON.stringify(envelope);
|
|
649
|
+
sent.push(JSON.parse(json));
|
|
650
|
+
schedule(json, 1, (delay ?? 0) * 1e3);
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
drain: () => pending === 0 ? Promise.resolve() : new Promise((resolve) => {
|
|
654
|
+
idle.push(resolve);
|
|
655
|
+
}),
|
|
656
|
+
clear: () => {
|
|
657
|
+
sent.length = 0;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// src/transports/sync.ts
|
|
663
|
+
var createSyncTransport = () => {
|
|
664
|
+
let worker;
|
|
665
|
+
return {
|
|
666
|
+
kind: "sync",
|
|
667
|
+
maxAttempts: 1,
|
|
668
|
+
bind: (bound) => {
|
|
669
|
+
worker = bound;
|
|
670
|
+
},
|
|
671
|
+
send: async (envelopes) => {
|
|
672
|
+
if (!worker) {
|
|
673
|
+
throw new Error("The sync transport is not used by an event bus.");
|
|
674
|
+
}
|
|
675
|
+
const failed = [];
|
|
676
|
+
for (const envelope of envelopes) {
|
|
677
|
+
const result = await worker.handle(envelope, { attempt: 1 });
|
|
678
|
+
if (result.status !== "success") {
|
|
679
|
+
failed.push({ id: envelope.id, error: result.error });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (failed.length > 0) throw new PartialSendError(failed);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
};
|
|
686
|
+
export {
|
|
687
|
+
EventEmitError,
|
|
688
|
+
EventPayloadError,
|
|
689
|
+
EventsError,
|
|
690
|
+
PartialSendError,
|
|
691
|
+
createEventBus,
|
|
692
|
+
createFakeEventBus,
|
|
693
|
+
createMemoryTransport,
|
|
694
|
+
createSyncTransport,
|
|
695
|
+
createWorker,
|
|
696
|
+
defineEvent,
|
|
697
|
+
defineListener,
|
|
698
|
+
parseEnvelope,
|
|
699
|
+
startWorker
|
|
700
|
+
};
|
|
701
|
+
//# sourceMappingURL=index.js.map
|