@qkitt/queue 0.1.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.
Files changed (38) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +723 -0
  3. package/dist/config/config-freeze.util.d.ts +7 -0
  4. package/dist/config/from-config.d.ts +33 -0
  5. package/dist/config/index.d.ts +4 -0
  6. package/dist/config/parse.util.d.ts +12 -0
  7. package/dist/config/store-resolve.util.d.ts +11 -0
  8. package/dist/config/types.d.ts +205 -0
  9. package/dist/config/validate.d.ts +36 -0
  10. package/dist/events/index.d.ts +48 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.js +1552 -0
  13. package/dist/persist/index.d.ts +3 -0
  14. package/dist/persist/json-codec.util.d.ts +13 -0
  15. package/dist/persist/memory.d.ts +14 -0
  16. package/dist/persist/web-storage-access.util.d.ts +10 -0
  17. package/dist/persist/web-storage.d.ts +59 -0
  18. package/dist/queue/core/forward.util.d.ts +21 -0
  19. package/dist/queue/core/layers.util.d.ts +13 -0
  20. package/dist/queue/core/queue.d.ts +66 -0
  21. package/dist/queue/index.d.ts +7 -0
  22. package/dist/queue/persist/create-id.util.d.ts +10 -0
  23. package/dist/queue/persist/hydrate-gate.util.d.ts +9 -0
  24. package/dist/queue/persist/persist.support.d.ts +19 -0
  25. package/dist/queue/persist/persist.types.d.ts +23 -0
  26. package/dist/queue/persist/row-ids.util.d.ts +14 -0
  27. package/dist/queue/persist/with-row-persist.d.ts +72 -0
  28. package/dist/queue/persist/with-snapshot-persist.d.ts +45 -0
  29. package/dist/queue/persist/write-chain.util.d.ts +12 -0
  30. package/dist/queue/worker/with-worker.d.ts +56 -0
  31. package/dist/router/index.d.ts +3 -0
  32. package/dist/router/match.util.d.ts +24 -0
  33. package/dist/router/router.d.ts +107 -0
  34. package/dist/worker/index.d.ts +4 -0
  35. package/dist/worker/pipeline.d.ts +22 -0
  36. package/dist/worker/retry.d.ts +31 -0
  37. package/dist/worker/types.d.ts +5 -0
  38. package/package.json +64 -0
package/dist/index.js ADDED
@@ -0,0 +1,1552 @@
1
+ // src/events/index.ts
2
+ var createTypedEmit = (emit) => {
3
+ return (eventName, data) => {
4
+ emit(eventName, data);
5
+ };
6
+ };
7
+ var buildEventEmitter = () => {
8
+ const listenersByEvent = /* @__PURE__ */ new Map();
9
+ const on = (eventName, callback) => {
10
+ const listeners = listenersByEvent.get(eventName);
11
+ if (listeners) {
12
+ listeners.push(callback);
13
+ } else {
14
+ listenersByEvent.set(eventName, [
15
+ callback
16
+ ]);
17
+ }
18
+ return () => off(eventName, callback);
19
+ };
20
+ const once = (eventName, callback) => {
21
+ const wrapper = (data) => {
22
+ off(eventName, wrapper);
23
+ callback(data);
24
+ };
25
+ return on(eventName, wrapper);
26
+ };
27
+ const off = (eventName, callback) => {
28
+ const listeners = listenersByEvent.get(eventName);
29
+ if (!listeners) return;
30
+ if (!callback) {
31
+ listenersByEvent.delete(eventName);
32
+ return;
33
+ }
34
+ const next = listeners.filter((cb) => cb !== callback);
35
+ if (next.length === 0) {
36
+ listenersByEvent.delete(eventName);
37
+ } else {
38
+ listenersByEvent.set(eventName, next);
39
+ }
40
+ };
41
+ const emit = (eventName, data) => {
42
+ const listeners = listenersByEvent.get(eventName);
43
+ if (!listeners?.length) return;
44
+ for (const callback of [...listeners]) {
45
+ try {
46
+ callback(data);
47
+ } catch {
48
+ }
49
+ }
50
+ };
51
+ const clear = () => {
52
+ listenersByEvent.clear();
53
+ };
54
+ const listenerCount = (eventName) => {
55
+ return listenersByEvent.get(eventName)?.length ?? 0;
56
+ };
57
+ const eventNames = () => {
58
+ return [...listenersByEvent.keys()];
59
+ };
60
+ const api = {
61
+ on,
62
+ once,
63
+ off,
64
+ emit,
65
+ clear,
66
+ listenerCount,
67
+ eventNames,
68
+ expand: () => api
69
+ };
70
+ return api;
71
+ };
72
+
73
+ // src/queue/core/queue.ts
74
+ var QueueFullError = class extends Error {
75
+ name = "QueueFullError";
76
+ maxSize;
77
+ constructor(maxSize) {
78
+ super(`Queue is full (maxSize=${maxSize})`);
79
+ this.maxSize = maxSize;
80
+ }
81
+ };
82
+ var COMPACT_HEAD_THRESHOLD = 64;
83
+ var buildQueue = (options = {}) => {
84
+ const maxSize = options.maxSize;
85
+ if (maxSize !== void 0 && (!Number.isFinite(maxSize) || maxSize < 1)) {
86
+ throw new Error("maxSize must be a finite number >= 1");
87
+ }
88
+ const items = [];
89
+ let head = 0;
90
+ const emitter = buildEventEmitter();
91
+ const liveSize = () => items.length - head;
92
+ const compactIfNeeded = () => {
93
+ if (head === 0) return;
94
+ if (head < COMPACT_HEAD_THRESHOLD && head * 2 < items.length) return;
95
+ items.splice(0, head);
96
+ head = 0;
97
+ };
98
+ const assertCapacity = (nextSize) => {
99
+ if (maxSize !== void 0 && nextSize > maxSize) {
100
+ throw new QueueFullError(maxSize);
101
+ }
102
+ };
103
+ const enqueue = (item) => {
104
+ assertCapacity(liveSize() + 1);
105
+ items.push(item);
106
+ emitter.emit("queue:enqueued", { item, size: liveSize() });
107
+ };
108
+ const dequeue = () => {
109
+ if (head >= items.length) return void 0;
110
+ const item = items[head];
111
+ head += 1;
112
+ compactIfNeeded();
113
+ const size2 = liveSize();
114
+ emitter.emit("queue:dequeued", { item, size: size2 });
115
+ if (size2 === 0) {
116
+ emitter.emit("queue:emptied", void 0);
117
+ }
118
+ return item;
119
+ };
120
+ const peek = () => {
121
+ if (head >= items.length) return void 0;
122
+ return items[head];
123
+ };
124
+ const size = () => liveSize();
125
+ const isEmpty = () => head >= items.length;
126
+ const clear = () => {
127
+ const removed = liveSize();
128
+ if (removed === 0) return;
129
+ items.length = 0;
130
+ head = 0;
131
+ emitter.emit("queue:cleared", { removed });
132
+ };
133
+ const replaceAll = (next) => {
134
+ assertCapacity(next.length);
135
+ items.length = 0;
136
+ head = 0;
137
+ for (const item of next) {
138
+ items.push(item);
139
+ }
140
+ };
141
+ const toArray = () => items.slice(head);
142
+ const api = {
143
+ enqueue,
144
+ dequeue,
145
+ peek,
146
+ size,
147
+ isEmpty,
148
+ clear,
149
+ replaceAll,
150
+ toArray,
151
+ on: emitter.on,
152
+ once: emitter.once,
153
+ off: emitter.off,
154
+ emit: emitter.emit,
155
+ expand: () => api
156
+ };
157
+ return api;
158
+ };
159
+
160
+ // src/queue/core/layers.util.ts
161
+ var WORKER_LAYER = /* @__PURE__ */ Symbol.for("qkitt:worker-layer");
162
+ var PERSIST_LAYER = /* @__PURE__ */ Symbol.for("qkitt:persist-layer");
163
+ var markQueueLayer = (queue, layer) => {
164
+ if (hasQueueLayer(queue, layer)) return queue;
165
+ Object.defineProperty(queue, layer, {
166
+ value: true,
167
+ enumerable: false,
168
+ configurable: false,
169
+ writable: false
170
+ });
171
+ return queue;
172
+ };
173
+ var hasQueueLayer = (queue, layer) => queue[layer] === true;
174
+ var copyQueueLayers = (from, to) => {
175
+ if (hasQueueLayer(from, WORKER_LAYER)) {
176
+ markQueueLayer(to, WORKER_LAYER);
177
+ }
178
+ if (hasQueueLayer(from, PERSIST_LAYER)) {
179
+ markQueueLayer(to, PERSIST_LAYER);
180
+ }
181
+ return to;
182
+ };
183
+
184
+ // src/queue/core/forward.util.ts
185
+ var forwardQueue = (queue, extra) => {
186
+ const next = {
187
+ ...queue,
188
+ ...extra
189
+ };
190
+ return copyQueueLayers(queue, next);
191
+ };
192
+
193
+ // src/queue/persist/create-id.util.ts
194
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
195
+ var DEFAULT_SIZE = 21;
196
+ var fillRandom = (bytes) => {
197
+ const cryptoObj = globalThis.crypto;
198
+ if (typeof cryptoObj?.getRandomValues === "function") {
199
+ cryptoObj.getRandomValues(bytes);
200
+ return;
201
+ }
202
+ for (let i = 0; i < bytes.length; i += 1) {
203
+ bytes[i] = Math.floor(Math.random() * 256);
204
+ }
205
+ };
206
+ var createId = (size = DEFAULT_SIZE) => {
207
+ if (!Number.isInteger(size) || size <= 0) {
208
+ throw new RangeError(`createId size must be a positive integer, got ${size}`);
209
+ }
210
+ const bytes = new Uint8Array(size);
211
+ fillRandom(bytes);
212
+ let id = "";
213
+ for (let i = 0; i < size; i += 1) {
214
+ id += ALPHABET[bytes[i] & 63];
215
+ }
216
+ return id;
217
+ };
218
+
219
+ // src/queue/persist/hydrate-gate.util.ts
220
+ var createHydrateGate = () => {
221
+ let suppressing = false;
222
+ return {
223
+ isSuppressing: () => suppressing,
224
+ run: async (fn) => {
225
+ suppressing = true;
226
+ try {
227
+ return await fn();
228
+ } finally {
229
+ suppressing = false;
230
+ }
231
+ }
232
+ };
233
+ };
234
+ var assertNotHydrating = (gate) => {
235
+ if (gate.isSuppressing()) {
236
+ throw new Error(
237
+ "Cannot mutate queue while hydrate() is in progress; await hydrate() first"
238
+ );
239
+ }
240
+ };
241
+
242
+ // src/queue/persist/persist.support.ts
243
+ var assertBareQueueForPersist = (queue, wrapperName) => {
244
+ if (hasQueueLayer(queue, WORKER_LAYER)) {
245
+ throw new Error(
246
+ `${wrapperName} must wrap the bare queue before withWorker: withWorker(${wrapperName}(queue, store), worker)`
247
+ );
248
+ }
249
+ if (hasQueueLayer(queue, PERSIST_LAYER)) {
250
+ throw new Error(
251
+ `${wrapperName} cannot wrap an already-persisted queue; use a single persist layer on the bare queue`
252
+ );
253
+ }
254
+ };
255
+ var notifyQueueRestored = (queue) => {
256
+ const size = queue.size();
257
+ if (size === 0) return;
258
+ const item = queue.peek();
259
+ if (item === void 0) return;
260
+ queue.emit("queue:enqueued", { item, size });
261
+ };
262
+
263
+ // src/queue/persist/row-ids.util.ts
264
+ var COMPACT_HEAD_THRESHOLD2 = 64;
265
+ var createRowIdList = () => {
266
+ const ids = [];
267
+ let idHead = 0;
268
+ const compactIfNeeded = () => {
269
+ if (idHead === 0) return;
270
+ if (idHead < COMPACT_HEAD_THRESHOLD2 && idHead * 2 < ids.length) return;
271
+ ids.splice(0, idHead);
272
+ idHead = 0;
273
+ };
274
+ return {
275
+ push: (id) => {
276
+ ids.push(id);
277
+ },
278
+ shift: () => {
279
+ if (idHead >= ids.length) return void 0;
280
+ const id = ids[idHead];
281
+ idHead += 1;
282
+ compactIfNeeded();
283
+ return id;
284
+ },
285
+ reset: (next) => {
286
+ ids.length = 0;
287
+ idHead = 0;
288
+ for (const id of next) {
289
+ ids.push(id);
290
+ }
291
+ },
292
+ live: () => ids.slice(idHead),
293
+ liveCount: () => ids.length - idHead
294
+ };
295
+ };
296
+
297
+ // src/queue/persist/write-chain.util.ts
298
+ var createWriteChain = () => {
299
+ let chain = Promise.resolve();
300
+ const push = (op) => {
301
+ const run = chain.then(op, op);
302
+ chain = run.then(
303
+ () => void 0,
304
+ () => void 0
305
+ );
306
+ return run;
307
+ };
308
+ const flush = () => chain;
309
+ return { push, flush };
310
+ };
311
+
312
+ // src/queue/persist/with-row-persist.ts
313
+ var withRowPersist = (queue, store, options = {}) => {
314
+ assertBareQueueForPersist(queue, "withRowPersist");
315
+ const nextId = options.createId ?? createId;
316
+ const inner = queue.expand();
317
+ const emitPersist = createTypedEmit(
318
+ inner.emit
319
+ );
320
+ const gate = createHydrateGate();
321
+ const writes = createWriteChain();
322
+ const rowIdsList = createRowIdList();
323
+ let localSuppress = false;
324
+ const isSuppressing = () => gate.isSuppressing() || localSuppress;
325
+ const trackError = (operation, error, id) => {
326
+ emitPersist("persist:error", { operation, error, id });
327
+ };
328
+ const rollbackLocalById = (id) => {
329
+ const liveIds = rowIdsList.live();
330
+ const liveIndex = liveIds.indexOf(id);
331
+ if (liveIndex < 0) return;
332
+ const remainingItems = inner.toArray();
333
+ remainingItems.splice(liveIndex, 1);
334
+ liveIds.splice(liveIndex, 1);
335
+ localSuppress = true;
336
+ try {
337
+ rowIdsList.reset(liveIds);
338
+ inner.replaceAll(remainingItems);
339
+ } finally {
340
+ localSuppress = false;
341
+ }
342
+ };
343
+ const scheduleStore = (op) => {
344
+ if (isSuppressing()) return;
345
+ void writes.push(op).catch(() => {
346
+ });
347
+ };
348
+ const enqueue = (item) => {
349
+ assertNotHydrating(gate);
350
+ const id = nextId();
351
+ rowIdsList.push(id);
352
+ if (!isSuppressing()) {
353
+ scheduleStore(async () => {
354
+ try {
355
+ await store.insert({ id, item });
356
+ emitPersist("persist:inserted", { id, item });
357
+ } catch (error) {
358
+ rollbackLocalById(id);
359
+ trackError("insert", error, id);
360
+ }
361
+ });
362
+ }
363
+ inner.enqueue(item);
364
+ };
365
+ const dequeue = () => {
366
+ assertNotHydrating(gate);
367
+ const item = inner.dequeue();
368
+ if (item === void 0) return void 0;
369
+ const id = rowIdsList.shift();
370
+ if (id === void 0) return item;
371
+ if (isSuppressing()) return item;
372
+ scheduleStore(async () => {
373
+ try {
374
+ await store.remove(id);
375
+ emitPersist("persist:removed", { id, item });
376
+ } catch (error) {
377
+ trackError("remove", error, id);
378
+ }
379
+ });
380
+ return item;
381
+ };
382
+ const clear = () => {
383
+ assertNotHydrating(gate);
384
+ const removed = rowIdsList.liveCount();
385
+ if (removed === 0 && inner.isEmpty()) return;
386
+ rowIdsList.reset([]);
387
+ inner.clear();
388
+ if (isSuppressing()) return;
389
+ scheduleStore(async () => {
390
+ try {
391
+ await store.clear();
392
+ emitPersist("persist:cleared", { removed });
393
+ } catch (error) {
394
+ trackError("clear", error);
395
+ }
396
+ });
397
+ };
398
+ const replaceAll = (_items) => {
399
+ throw new Error(
400
+ "replaceAll is not supported on row-persisted queues; use hydrate() to restore from the store, or enqueue/dequeue/clear"
401
+ );
402
+ };
403
+ const hydrate = async () => {
404
+ await gate.run(async () => {
405
+ try {
406
+ await writes.flush();
407
+ const rows = await store.loadAll();
408
+ rowIdsList.reset(rows.map((row) => row.id));
409
+ inner.replaceAll(rows.map((row) => row.item));
410
+ emitPersist("persist:loaded", { size: inner.size() });
411
+ } catch (error) {
412
+ trackError("load", error);
413
+ throw error;
414
+ }
415
+ });
416
+ notifyQueueRestored({
417
+ size: inner.size,
418
+ peek: inner.peek,
419
+ emit: inner.emit
420
+ });
421
+ };
422
+ const api = markQueueLayer(
423
+ forwardQueue(inner, {
424
+ enqueue,
425
+ dequeue,
426
+ clear,
427
+ replaceAll,
428
+ expand: () => api,
429
+ hydrate,
430
+ rowIds: () => rowIdsList.live(),
431
+ flush: writes.flush
432
+ }),
433
+ PERSIST_LAYER
434
+ );
435
+ return api;
436
+ };
437
+
438
+ // src/queue/persist/with-snapshot-persist.ts
439
+ var withSnapshotPersist = (queue, store, options = {}) => {
440
+ assertBareQueueForPersist(queue, "withSnapshotPersist");
441
+ const autoSave = options.autoSave ?? true;
442
+ const inner = queue.expand();
443
+ const emitPersist = createTypedEmit(
444
+ inner.emit
445
+ );
446
+ const gate = createHydrateGate();
447
+ const writes = createWriteChain();
448
+ const persist = () => writes.push(async () => {
449
+ try {
450
+ const items = inner.toArray();
451
+ await store.save(items);
452
+ emitPersist("persist:saved", { size: items.length });
453
+ } catch (error) {
454
+ emitPersist("persist:error", { operation: "save", error });
455
+ throw error;
456
+ }
457
+ });
458
+ const scheduleSave = () => {
459
+ if (gate.isSuppressing() || !autoSave) return;
460
+ void persist().catch(() => {
461
+ });
462
+ };
463
+ const enqueue = (item) => {
464
+ assertNotHydrating(gate);
465
+ inner.enqueue(item);
466
+ scheduleSave();
467
+ };
468
+ const dequeue = () => {
469
+ assertNotHydrating(gate);
470
+ const item = inner.dequeue();
471
+ if (item !== void 0) {
472
+ scheduleSave();
473
+ }
474
+ return item;
475
+ };
476
+ const clear = () => {
477
+ assertNotHydrating(gate);
478
+ if (inner.isEmpty()) return;
479
+ inner.clear();
480
+ scheduleSave();
481
+ };
482
+ const replaceAll = (items) => {
483
+ assertNotHydrating(gate);
484
+ inner.replaceAll(items);
485
+ scheduleSave();
486
+ };
487
+ const hydrate = async () => {
488
+ await gate.run(async () => {
489
+ try {
490
+ await writes.flush();
491
+ const items = await store.load();
492
+ inner.replaceAll(items);
493
+ emitPersist("persist:loaded", { size: inner.size() });
494
+ } catch (error) {
495
+ emitPersist("persist:error", { operation: "load", error });
496
+ throw error;
497
+ }
498
+ });
499
+ notifyQueueRestored({
500
+ size: inner.size,
501
+ peek: inner.peek,
502
+ emit: inner.emit
503
+ });
504
+ };
505
+ const api = markQueueLayer(
506
+ forwardQueue(inner, {
507
+ enqueue,
508
+ dequeue,
509
+ clear,
510
+ replaceAll,
511
+ expand: () => api,
512
+ hydrate,
513
+ persist,
514
+ flush: writes.flush
515
+ }),
516
+ PERSIST_LAYER
517
+ );
518
+ return api;
519
+ };
520
+
521
+ // src/queue/worker/with-worker.ts
522
+ var resolveConcurrency = (value) => {
523
+ const concurrency = value ?? 1;
524
+ if (!Number.isFinite(concurrency) || concurrency < 1) {
525
+ throw new Error("concurrency must be a finite number >= 1");
526
+ }
527
+ return Math.floor(concurrency);
528
+ };
529
+ var withWorker = (queue, worker, options = {}) => {
530
+ const concurrency = resolveConcurrency(options.concurrency);
531
+ const autoStart = options.autoStart ?? true;
532
+ const inner = queue.expand();
533
+ const emitWorker = createTypedEmit(
534
+ inner.emit
535
+ );
536
+ const onQueue = inner.on;
537
+ let running = false;
538
+ let active = 0;
539
+ const processItem = async (item) => {
540
+ emitWorker("worker:started", { item });
541
+ try {
542
+ const result = await worker(item);
543
+ emitWorker("worker:completed", { item, result });
544
+ } catch (error) {
545
+ emitWorker("worker:failed", { item, error });
546
+ } finally {
547
+ active -= 1;
548
+ if (active === 0 && inner.isEmpty()) {
549
+ emitWorker("worker:idle", void 0);
550
+ }
551
+ pump();
552
+ }
553
+ };
554
+ const pump = () => {
555
+ try {
556
+ while (running && active < concurrency) {
557
+ const item = inner.dequeue();
558
+ if (item === void 0) break;
559
+ active += 1;
560
+ void processItem(item);
561
+ }
562
+ } catch {
563
+ }
564
+ };
565
+ const start = () => {
566
+ if (running) return;
567
+ running = true;
568
+ pump();
569
+ };
570
+ const stop = () => {
571
+ running = false;
572
+ };
573
+ onQueue("queue:enqueued", () => {
574
+ pump();
575
+ });
576
+ if (autoStart) {
577
+ start();
578
+ }
579
+ const api = markQueueLayer(
580
+ forwardQueue(inner, {
581
+ expand: () => api,
582
+ start,
583
+ stop,
584
+ isRunning: () => running,
585
+ isProcessing: () => active > 0,
586
+ activeCount: () => active
587
+ }),
588
+ WORKER_LAYER
589
+ );
590
+ return api;
591
+ };
592
+
593
+ // src/router/match.util.ts
594
+ var TOPIC_SEPARATOR = ".";
595
+ var SINGLE_WILDCARD = "*";
596
+ var MULTI_WILDCARD = "#";
597
+ var isEmptySegment = (segment) => segment.length === 0;
598
+ var isValidTopic = (topic) => {
599
+ if (topic.length === 0) return false;
600
+ if (topic.includes(SINGLE_WILDCARD) || topic.includes(MULTI_WILDCARD)) {
601
+ return false;
602
+ }
603
+ const segments = topic.split(TOPIC_SEPARATOR);
604
+ return segments.length > 0 && !segments.some(isEmptySegment);
605
+ };
606
+ var isValidPattern = (pattern) => {
607
+ if (pattern.length === 0) return false;
608
+ const segments = pattern.split(TOPIC_SEPARATOR);
609
+ if (segments.some(isEmptySegment)) return false;
610
+ for (let i = 0; i < segments.length; i += 1) {
611
+ const segment = segments[i];
612
+ if (segment === MULTI_WILDCARD) {
613
+ return i === segments.length - 1;
614
+ }
615
+ }
616
+ return true;
617
+ };
618
+ var matchTopic = (pattern, topic) => {
619
+ if (!isValidPattern(pattern) || !isValidTopic(topic)) return false;
620
+ const patternParts = pattern.split(TOPIC_SEPARATOR);
621
+ const topicParts = topic.split(TOPIC_SEPARATOR);
622
+ let pi = 0;
623
+ let ti = 0;
624
+ while (pi < patternParts.length && ti < topicParts.length) {
625
+ const token = patternParts[pi];
626
+ if (token === MULTI_WILDCARD) {
627
+ return true;
628
+ }
629
+ if (token === SINGLE_WILDCARD || token === topicParts[ti]) {
630
+ pi += 1;
631
+ ti += 1;
632
+ continue;
633
+ }
634
+ return false;
635
+ }
636
+ if (pi === patternParts.length - 1 && patternParts[pi] === MULTI_WILDCARD) {
637
+ return true;
638
+ }
639
+ return pi === patternParts.length && ti === topicParts.length;
640
+ };
641
+
642
+ // src/router/router.ts
643
+ var buildRouter = (options = {}) => {
644
+ const events = buildEventEmitter();
645
+ const emitRouter = createTypedEmit(
646
+ events.emit
647
+ );
648
+ const routes = [];
649
+ let unmatchedTarget = options.unmatchedTarget;
650
+ let unmatchedTotal = 0;
651
+ let lastUnmatchedRecord;
652
+ const bind = (pattern, target) => {
653
+ if (!isValidPattern(pattern)) {
654
+ const error = new Error(`Invalid route pattern: ${pattern}`);
655
+ emitRouter("router:error", { operation: "bind", error, pattern });
656
+ throw error;
657
+ }
658
+ const binding = {
659
+ pattern,
660
+ target
661
+ };
662
+ routes.push(binding);
663
+ emitRouter("router:bound", { pattern });
664
+ return () => {
665
+ unbind(pattern, target);
666
+ };
667
+ };
668
+ const unbind = (pattern, target) => {
669
+ let removed = 0;
670
+ for (let i = routes.length - 1; i >= 0; i -= 1) {
671
+ const route = routes[i];
672
+ if (route.pattern !== pattern) continue;
673
+ if (target !== void 0 && route.target !== target) continue;
674
+ routes.splice(i, 1);
675
+ removed += 1;
676
+ }
677
+ if (removed > 0) {
678
+ emitRouter("router:unbound", { pattern, removed });
679
+ }
680
+ };
681
+ const deliverUnmatched = (topic, data) => {
682
+ if (unmatchedTarget === void 0) {
683
+ return false;
684
+ }
685
+ try {
686
+ unmatchedTarget.enqueue({ topic, data });
687
+ return true;
688
+ } catch (error) {
689
+ emitRouter("router:error", {
690
+ operation: "unmatched",
691
+ error,
692
+ topic
693
+ });
694
+ return false;
695
+ }
696
+ };
697
+ const publish = (topic, data) => {
698
+ if (!isValidTopic(topic)) {
699
+ const error = new Error(`Invalid publish topic: ${topic}`);
700
+ emitRouter("router:error", { operation: "publish", error, topic });
701
+ throw error;
702
+ }
703
+ const message = { topic, data };
704
+ let matched = 0;
705
+ for (const route of [...routes]) {
706
+ if (!matchTopic(route.pattern, topic)) continue;
707
+ try {
708
+ route.target.enqueue(message);
709
+ matched += 1;
710
+ } catch (error) {
711
+ emitRouter("router:error", {
712
+ operation: "publish",
713
+ error,
714
+ topic,
715
+ pattern: route.pattern
716
+ });
717
+ }
718
+ }
719
+ if (matched === 0) {
720
+ unmatchedTotal += 1;
721
+ lastUnmatchedRecord = { topic, data };
722
+ const delivered = deliverUnmatched(topic, data);
723
+ emitRouter("router:unmatched", { topic, data, delivered });
724
+ } else {
725
+ emitRouter("router:published", { topic, data, matched });
726
+ }
727
+ return matched;
728
+ };
729
+ const bindings = () => routes.map((route) => ({
730
+ pattern: route.pattern,
731
+ target: route.target
732
+ }));
733
+ const clear = () => {
734
+ routes.length = 0;
735
+ };
736
+ const setUnmatchedTarget = (target) => {
737
+ unmatchedTarget = target;
738
+ };
739
+ const getUnmatchedTarget = () => unmatchedTarget;
740
+ const unmatchedCount = () => unmatchedTotal;
741
+ const lastUnmatched = () => lastUnmatchedRecord;
742
+ const clearUnmatched = () => {
743
+ unmatchedTotal = 0;
744
+ lastUnmatchedRecord = void 0;
745
+ };
746
+ const api = {
747
+ bind,
748
+ unbind,
749
+ publish,
750
+ bindings,
751
+ clear,
752
+ setUnmatchedTarget,
753
+ getUnmatchedTarget,
754
+ unmatchedCount,
755
+ lastUnmatched,
756
+ clearUnmatched,
757
+ on: events.on,
758
+ once: events.once,
759
+ off: events.off,
760
+ emit: events.emit,
761
+ expand: () => api
762
+ };
763
+ return api;
764
+ };
765
+
766
+ // src/config/config-freeze.util.ts
767
+ var freezeConfig = (config) => {
768
+ const queues = {};
769
+ for (const [name, queue] of Object.entries(config.queues)) {
770
+ queues[name] = Object.freeze({ ...queue });
771
+ }
772
+ const stores = {};
773
+ if (config.stores) {
774
+ for (const [name, store] of Object.entries(config.stores)) {
775
+ stores[name] = Object.freeze({ ...store });
776
+ }
777
+ }
778
+ const frozen = {
779
+ ...config,
780
+ ...Object.keys(stores).length > 0 ? { stores: Object.freeze(stores) } : {},
781
+ queues: Object.freeze(queues),
782
+ ...config.router ? {
783
+ router: Object.freeze({
784
+ ...config.router,
785
+ ...config.router.bindings ? {
786
+ bindings: Object.freeze(
787
+ config.router.bindings.map(
788
+ (b) => Object.freeze({ ...b })
789
+ )
790
+ )
791
+ } : {},
792
+ ...config.router.unmatchedQueue !== void 0 ? { unmatchedQueue: config.router.unmatchedQueue } : {}
793
+ })
794
+ } : {}
795
+ };
796
+ return Object.freeze(frozen);
797
+ };
798
+
799
+ // src/persist/memory.ts
800
+ var createMemorySnapshotStore = (initial = []) => {
801
+ const data = [...initial];
802
+ return {
803
+ get data() {
804
+ return data;
805
+ },
806
+ load: () => data.slice(),
807
+ save: (items) => {
808
+ data.length = 0;
809
+ data.push(...items);
810
+ }
811
+ };
812
+ };
813
+ var createMemoryRowStore = (initial = []) => {
814
+ const rows = initial.map((row) => ({
815
+ id: row.id,
816
+ item: row.item
817
+ }));
818
+ return {
819
+ get rows() {
820
+ return rows;
821
+ },
822
+ loadAll: () => rows.map((row) => ({ id: row.id, item: row.item })),
823
+ insert: (record) => {
824
+ const index = rows.findIndex((row) => row.id === record.id);
825
+ const next = { id: record.id, item: record.item };
826
+ if (index >= 0) {
827
+ rows[index] = next;
828
+ } else {
829
+ rows.push(next);
830
+ }
831
+ },
832
+ remove: (id) => {
833
+ const index = rows.findIndex((row) => row.id === id);
834
+ if (index >= 0) {
835
+ rows.splice(index, 1);
836
+ }
837
+ },
838
+ clear: () => {
839
+ rows.length = 0;
840
+ }
841
+ };
842
+ };
843
+
844
+ // src/persist/json-codec.util.ts
845
+ var StorageCodecError = class extends Error {
846
+ name = "StorageCodecError";
847
+ cause;
848
+ constructor(message, cause) {
849
+ super(message, cause !== void 0 ? { cause } : void 0);
850
+ this.cause = cause;
851
+ }
852
+ };
853
+ var defaultJsonCodec = () => ({
854
+ serialize: (value) => JSON.stringify(value),
855
+ deserialize: (raw) => JSON.parse(raw)
856
+ });
857
+ var decodeWithCodec = (label, raw, deserialize) => {
858
+ try {
859
+ return deserialize(raw);
860
+ } catch (error) {
861
+ throw new StorageCodecError(
862
+ `Failed to deserialize ${label}: ${error instanceof Error ? error.message : String(error)}`,
863
+ error
864
+ );
865
+ }
866
+ };
867
+
868
+ // src/persist/web-storage-access.util.ts
869
+ var getGlobalStorage = (name) => {
870
+ const storage = globalThis[name];
871
+ if (!storage) {
872
+ throw new Error(
873
+ `${name} is not available; pass an explicit \`storage\` option`
874
+ );
875
+ }
876
+ return storage;
877
+ };
878
+ var lazyGlobalStorage = (name) => ({
879
+ getItem: (key) => getGlobalStorage(name).getItem(key),
880
+ setItem: (key, value) => getGlobalStorage(name).setItem(key, value),
881
+ removeItem: (key) => getGlobalStorage(name).removeItem(key)
882
+ });
883
+ var resolveStorage = (storage) => {
884
+ if (storage) return storage;
885
+ return lazyGlobalStorage("localStorage");
886
+ };
887
+
888
+ // src/persist/web-storage.ts
889
+ var createWebSnapshotStore = (options) => {
890
+ const storage = () => resolveStorage(options.storage);
891
+ const codec = options.codec ?? defaultJsonCodec();
892
+ return {
893
+ load: () => {
894
+ const raw = storage().getItem(options.key);
895
+ if (raw === null || raw === "") return [];
896
+ const items = decodeWithCodec(
897
+ `snapshot "${options.key}"`,
898
+ raw,
899
+ codec.deserialize
900
+ );
901
+ return Array.isArray(items) ? items : [];
902
+ },
903
+ save: (items) => {
904
+ storage().setItem(options.key, codec.serialize([...items]));
905
+ }
906
+ };
907
+ };
908
+ var orderCodec = {
909
+ serialize: (ids) => JSON.stringify(ids),
910
+ deserialize: (raw) => {
911
+ const ids = JSON.parse(raw);
912
+ if (!Array.isArray(ids)) return [];
913
+ return ids.filter((id) => typeof id === "string");
914
+ }
915
+ };
916
+ var createWebRowStore = (options) => {
917
+ const storage = () => resolveStorage(options.storage);
918
+ const itemCodec = options.itemCodec ?? defaultJsonCodec();
919
+ const orderKey = `${options.key}:order`;
920
+ const rowKey = (id) => `${options.key}:row:${id}`;
921
+ const readOrder = () => {
922
+ const raw = storage().getItem(orderKey);
923
+ if (raw === null || raw === "") return [];
924
+ return decodeWithCodec(
925
+ `row order "${orderKey}"`,
926
+ raw,
927
+ orderCodec.deserialize
928
+ );
929
+ };
930
+ const writeOrder = (ids) => {
931
+ if (ids.length === 0) {
932
+ storage().removeItem(orderKey);
933
+ return;
934
+ }
935
+ storage().setItem(orderKey, orderCodec.serialize(ids));
936
+ };
937
+ return {
938
+ loadAll: () => {
939
+ const ids = readOrder();
940
+ const rows = [];
941
+ for (const id of ids) {
942
+ const raw = storage().getItem(rowKey(id));
943
+ if (raw === null) continue;
944
+ rows.push({
945
+ id,
946
+ item: decodeWithCodec(
947
+ `row "${rowKey(id)}"`,
948
+ raw,
949
+ itemCodec.deserialize
950
+ )
951
+ });
952
+ }
953
+ return rows;
954
+ },
955
+ insert: (record) => {
956
+ const store = storage();
957
+ store.setItem(rowKey(record.id), itemCodec.serialize(record.item));
958
+ const ids = readOrder();
959
+ if (!ids.includes(record.id)) {
960
+ ids.push(record.id);
961
+ writeOrder(ids);
962
+ }
963
+ },
964
+ remove: (id) => {
965
+ const store = storage();
966
+ store.removeItem(rowKey(id));
967
+ writeOrder(readOrder().filter((entry) => entry !== id));
968
+ },
969
+ clear: () => {
970
+ const store = storage();
971
+ for (const id of readOrder()) {
972
+ store.removeItem(rowKey(id));
973
+ }
974
+ store.removeItem(orderKey);
975
+ }
976
+ };
977
+ };
978
+ var createLocalStorageSnapshotStore = (key, options = {}) => createWebSnapshotStore({
979
+ ...options,
980
+ key,
981
+ storage: lazyGlobalStorage("localStorage")
982
+ });
983
+ var createLocalStorageRowStore = (key, options = {}) => createWebRowStore({
984
+ ...options,
985
+ key,
986
+ storage: lazyGlobalStorage("localStorage")
987
+ });
988
+ var createSessionStorageSnapshotStore = (key, options = {}) => createWebSnapshotStore({
989
+ ...options,
990
+ key,
991
+ storage: lazyGlobalStorage("sessionStorage")
992
+ });
993
+ var createSessionStorageRowStore = (key, options = {}) => createWebRowStore({
994
+ ...options,
995
+ key,
996
+ storage: lazyGlobalStorage("sessionStorage")
997
+ });
998
+
999
+ // src/config/store-resolve.util.ts
1000
+ var isWebAdapter = (adapter) => adapter === "localStorage" || adapter === "sessionStorage";
1001
+ var isSnapshotStore = (value) => typeof value.load === "function" && typeof value.save === "function";
1002
+ var isRowStore = (value) => {
1003
+ const store = value;
1004
+ return typeof store.loadAll === "function" && typeof store.insert === "function" && typeof store.remove === "function" && typeof store.clear === "function";
1005
+ };
1006
+ var createBuiltinSnapshot = (storeName, adapter, key, options) => {
1007
+ if (adapter === "memory") {
1008
+ return createMemorySnapshotStore();
1009
+ }
1010
+ if (key === void 0 || key.length === 0) {
1011
+ throw new Error(
1012
+ `config.stores.${storeName}.key is required when adapter is "${adapter}"`
1013
+ );
1014
+ }
1015
+ if (options.storage) {
1016
+ return createWebSnapshotStore({
1017
+ key,
1018
+ storage: options.storage
1019
+ });
1020
+ }
1021
+ if (adapter === "localStorage") {
1022
+ return createLocalStorageSnapshotStore(key);
1023
+ }
1024
+ return createSessionStorageSnapshotStore(key);
1025
+ };
1026
+ var createBuiltinRow = (storeName, adapter, key, options) => {
1027
+ if (adapter === "memory") {
1028
+ return createMemoryRowStore();
1029
+ }
1030
+ if (key === void 0 || key.length === 0) {
1031
+ throw new Error(
1032
+ `config.stores.${storeName}.key is required when adapter is "${adapter}"`
1033
+ );
1034
+ }
1035
+ if (options.storage) {
1036
+ return createWebRowStore({
1037
+ key,
1038
+ storage: options.storage
1039
+ });
1040
+ }
1041
+ if (adapter === "localStorage") {
1042
+ return createLocalStorageRowStore(key);
1043
+ }
1044
+ return createSessionStorageRowStore(key);
1045
+ };
1046
+ var resolveStore = (storeName, definition, options) => {
1047
+ if ("impl" in definition) {
1048
+ const impl = definition.impl;
1049
+ if (definition.strategy === "snapshot" && !isSnapshotStore(impl)) {
1050
+ throw new Error(
1051
+ `config.stores.${storeName}.impl must be a SnapshotStore (strategy is "snapshot")`
1052
+ );
1053
+ }
1054
+ if (definition.strategy === "row" && !isRowStore(impl)) {
1055
+ throw new Error(
1056
+ `config.stores.${storeName}.impl must be a RowStore (strategy is "row")`
1057
+ );
1058
+ }
1059
+ return impl;
1060
+ }
1061
+ const { adapter, strategy } = definition;
1062
+ const key = definition.key;
1063
+ if (isWebAdapter(adapter) && (key === void 0 || key.length === 0)) {
1064
+ throw new Error(
1065
+ `config.stores.${storeName}.key is required when adapter is "${adapter}"`
1066
+ );
1067
+ }
1068
+ if (strategy === "snapshot") {
1069
+ return createBuiltinSnapshot(storeName, adapter, key, options);
1070
+ }
1071
+ return createBuiltinRow(storeName, adapter, key, options);
1072
+ };
1073
+ var resolveAllStores = (stores, options) => {
1074
+ const resolved = {};
1075
+ if (!stores) return resolved;
1076
+ for (const [name, definition] of Object.entries(stores)) {
1077
+ resolved[name] = resolveStore(name, definition, options);
1078
+ }
1079
+ return resolved;
1080
+ };
1081
+
1082
+ // src/config/parse.util.ts
1083
+ var BUILTIN_ADAPTERS = /* @__PURE__ */ new Set([
1084
+ "memory",
1085
+ "localStorage",
1086
+ "sessionStorage"
1087
+ ]);
1088
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1089
+ var expectString = (value, path) => {
1090
+ if (typeof value !== "string" || value.length === 0) {
1091
+ throw new Error(`${path} must be a non-empty string`);
1092
+ }
1093
+ return value;
1094
+ };
1095
+ var expectBoolean = (value, path) => {
1096
+ if (typeof value !== "boolean") {
1097
+ throw new Error(`${path} must be a boolean`);
1098
+ }
1099
+ return value;
1100
+ };
1101
+ var expectPositiveFinite = (value, path) => {
1102
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
1103
+ throw new Error(`${path} must be a finite number >= 1`);
1104
+ }
1105
+ return value;
1106
+ };
1107
+ var parseAdapter = (value, path) => {
1108
+ if (typeof value !== "string" || !BUILTIN_ADAPTERS.has(value)) {
1109
+ throw new Error(
1110
+ `${path} must be one of: memory, localStorage, sessionStorage`
1111
+ );
1112
+ }
1113
+ return value;
1114
+ };
1115
+ var isSnapshotStoreLike = (value) => isPlainObject(value) && typeof value.load === "function" && typeof value.save === "function";
1116
+ var isRowStoreLike = (value) => isPlainObject(value) && typeof value.loadAll === "function" && typeof value.insert === "function" && typeof value.remove === "function" && typeof value.clear === "function";
1117
+ var parseStrategy = (value, path) => {
1118
+ if (value !== "snapshot" && value !== "row") {
1119
+ throw new Error(`${path} must be "snapshot" or "row"`);
1120
+ }
1121
+ return value;
1122
+ };
1123
+
1124
+ // src/config/validate.ts
1125
+ var parseStoreDefinition = (value, path, { allowJs }) => {
1126
+ if (!isPlainObject(value)) {
1127
+ throw new Error(`${path} must be an object`);
1128
+ }
1129
+ const strategy = parseStrategy(value.strategy, `${path}.strategy`);
1130
+ if (value.impl !== void 0) {
1131
+ if (!allowJs) {
1132
+ throw new Error(
1133
+ `${path}.impl is only valid in JS config (not JSON); implement SnapshotStore/RowStore and pass the instance from a module`
1134
+ );
1135
+ }
1136
+ if (value.adapter !== void 0) {
1137
+ throw new Error(
1138
+ `${path} cannot set both "adapter" and "impl"`
1139
+ );
1140
+ }
1141
+ if (strategy === "snapshot") {
1142
+ if (!isSnapshotStoreLike(value.impl)) {
1143
+ throw new Error(
1144
+ `${path}.impl must be a SnapshotStore (load + save)`
1145
+ );
1146
+ }
1147
+ return {
1148
+ strategy: "snapshot",
1149
+ impl: value.impl
1150
+ };
1151
+ }
1152
+ if (!isRowStoreLike(value.impl)) {
1153
+ throw new Error(
1154
+ `${path}.impl must be a RowStore (loadAll + insert + remove + clear)`
1155
+ );
1156
+ }
1157
+ return {
1158
+ strategy: "row",
1159
+ impl: value.impl
1160
+ };
1161
+ }
1162
+ if (value.adapter === void 0) {
1163
+ throw new Error(
1164
+ `${path} must define "adapter" (built-in) or "impl" (custom store)`
1165
+ );
1166
+ }
1167
+ const adapter = parseAdapter(value.adapter, `${path}.adapter`);
1168
+ const key = value.key === void 0 ? void 0 : expectString(value.key, `${path}.key`);
1169
+ if ((adapter === "localStorage" || adapter === "sessionStorage") && (key === void 0 || key.length === 0)) {
1170
+ throw new Error(
1171
+ `${path}.key is required when adapter is "${adapter}"`
1172
+ );
1173
+ }
1174
+ if (strategy === "snapshot") {
1175
+ return {
1176
+ strategy: "snapshot",
1177
+ adapter,
1178
+ ...key !== void 0 ? { key } : {}
1179
+ };
1180
+ }
1181
+ return {
1182
+ strategy: "row",
1183
+ adapter,
1184
+ ...key !== void 0 ? { key } : {}
1185
+ };
1186
+ };
1187
+ var parsePersistConfig = (value, path, storeNames) => {
1188
+ if (!isPlainObject(value)) {
1189
+ throw new Error(`${path} must be an object`);
1190
+ }
1191
+ const store = expectString(value.store, `${path}.store`);
1192
+ if (!storeNames.has(store)) {
1193
+ throw new Error(
1194
+ `${path}.store "${store}" is not defined in config.stores`
1195
+ );
1196
+ }
1197
+ const autoSave = value.autoSave === void 0 ? void 0 : expectBoolean(value.autoSave, `${path}.autoSave`);
1198
+ return {
1199
+ store,
1200
+ ...autoSave !== void 0 ? { autoSave } : {}
1201
+ };
1202
+ };
1203
+ var parseWorkerConfig = (value, path) => {
1204
+ if (typeof value === "function") {
1205
+ return value;
1206
+ }
1207
+ if (!isPlainObject(value)) {
1208
+ throw new Error(
1209
+ `${path} must be a function or { run, concurrency?, autoStart? }`
1210
+ );
1211
+ }
1212
+ if (typeof value.run !== "function") {
1213
+ throw new Error(`${path}.run must be a function`);
1214
+ }
1215
+ const concurrency = value.concurrency === void 0 ? void 0 : expectPositiveFinite(value.concurrency, `${path}.concurrency`);
1216
+ const autoStart = value.autoStart === void 0 ? void 0 : expectBoolean(value.autoStart, `${path}.autoStart`);
1217
+ return {
1218
+ run: value.run,
1219
+ ...concurrency !== void 0 ? { concurrency } : {},
1220
+ ...autoStart !== void 0 ? { autoStart } : {}
1221
+ };
1222
+ };
1223
+ var parseQueueConfig = (value, path, storeNames, { allowJs }) => {
1224
+ if (!isPlainObject(value)) {
1225
+ throw new Error(`${path} must be an object`);
1226
+ }
1227
+ const queue = {};
1228
+ if (value.maxSize !== void 0) {
1229
+ queue.maxSize = expectPositiveFinite(value.maxSize, `${path}.maxSize`);
1230
+ }
1231
+ if (value.persist !== void 0) {
1232
+ queue.persist = parsePersistConfig(
1233
+ value.persist,
1234
+ `${path}.persist`,
1235
+ storeNames
1236
+ );
1237
+ }
1238
+ if (value.worker !== void 0) {
1239
+ if (!allowJs) {
1240
+ throw new Error(
1241
+ `${path}.worker is only valid in JS config (functions cannot be expressed in JSON)`
1242
+ );
1243
+ }
1244
+ queue.worker = parseWorkerConfig(value.worker, `${path}.worker`);
1245
+ }
1246
+ return queue;
1247
+ };
1248
+ var parseBindingConfig = (value, path) => {
1249
+ if (!isPlainObject(value)) {
1250
+ throw new Error(`${path} must be an object`);
1251
+ }
1252
+ return {
1253
+ pattern: expectString(value.pattern, `${path}.pattern`),
1254
+ queue: expectString(value.queue, `${path}.queue`)
1255
+ };
1256
+ };
1257
+ var parseRouterConfig = (value, path) => {
1258
+ if (!isPlainObject(value)) {
1259
+ throw new Error(`${path} must be an object`);
1260
+ }
1261
+ const router = {};
1262
+ if (value.bindings !== void 0) {
1263
+ if (!Array.isArray(value.bindings)) {
1264
+ throw new Error(`${path}.bindings must be an array`);
1265
+ }
1266
+ router.bindings = value.bindings.map(
1267
+ (binding, index) => parseBindingConfig(binding, `${path}.bindings[${index}]`)
1268
+ );
1269
+ }
1270
+ if (value.unmatchedQueue !== void 0) {
1271
+ router.unmatchedQueue = expectString(
1272
+ value.unmatchedQueue,
1273
+ `${path}.unmatchedQueue`
1274
+ );
1275
+ }
1276
+ return router;
1277
+ };
1278
+ var parseSystemConfigValue = (value, options) => {
1279
+ if (!isPlainObject(value)) {
1280
+ throw new Error("config must be an object");
1281
+ }
1282
+ if (!isPlainObject(value.queues)) {
1283
+ throw new Error("config.queues must be an object");
1284
+ }
1285
+ const queueNames = Object.keys(value.queues);
1286
+ if (queueNames.length === 0) {
1287
+ throw new Error("config.queues must define at least one queue");
1288
+ }
1289
+ const stores = {};
1290
+ if (value.stores !== void 0) {
1291
+ if (!isPlainObject(value.stores)) {
1292
+ throw new Error("config.stores must be an object");
1293
+ }
1294
+ for (const name of Object.keys(value.stores)) {
1295
+ if (name.length === 0) {
1296
+ throw new Error("config.stores keys must be non-empty strings");
1297
+ }
1298
+ stores[name] = parseStoreDefinition(
1299
+ value.stores[name],
1300
+ `config.stores.${name}`,
1301
+ { allowJs: options.allowJs }
1302
+ );
1303
+ }
1304
+ }
1305
+ const storeNames = new Set(Object.keys(stores));
1306
+ const queues = {};
1307
+ for (const name of queueNames) {
1308
+ if (name.length === 0) {
1309
+ throw new Error("config.queues keys must be non-empty strings");
1310
+ }
1311
+ queues[name] = parseQueueConfig(
1312
+ value.queues[name],
1313
+ `config.queues.${name}`,
1314
+ storeNames,
1315
+ { allowJs: options.allowJs }
1316
+ );
1317
+ }
1318
+ const config = { queues };
1319
+ if (Object.keys(stores).length > 0) {
1320
+ config.stores = stores;
1321
+ }
1322
+ if (value.router !== void 0) {
1323
+ config.router = parseRouterConfig(value.router, "config.router");
1324
+ }
1325
+ if (value.hydrate !== void 0) {
1326
+ config.hydrate = expectBoolean(value.hydrate, "config.hydrate");
1327
+ }
1328
+ if (config.router?.bindings) {
1329
+ for (const [index, binding] of config.router.bindings.entries()) {
1330
+ if (!(binding.queue in queues)) {
1331
+ throw new Error(
1332
+ `config.router.bindings[${index}].queue "${binding.queue}" is not defined in config.queues`
1333
+ );
1334
+ }
1335
+ }
1336
+ }
1337
+ if (config.router?.unmatchedQueue !== void 0) {
1338
+ if (!(config.router.unmatchedQueue in queues)) {
1339
+ throw new Error(
1340
+ `config.router.unmatchedQueue "${config.router.unmatchedQueue}" is not defined in config.queues`
1341
+ );
1342
+ }
1343
+ }
1344
+ return config;
1345
+ };
1346
+ var validateSystemConfig = (value) => parseSystemConfigValue(value, { allowJs: false });
1347
+ var validateJsConfig = (value) => parseSystemConfigValue(value, { allowJs: true });
1348
+ var defineConfig = (config) => validateJsConfig(config);
1349
+ var parseSystemConfig = (json) => {
1350
+ let parsed;
1351
+ try {
1352
+ parsed = JSON.parse(json);
1353
+ } catch (error) {
1354
+ const message = error instanceof Error ? error.message : "invalid JSON";
1355
+ throw new Error(`config JSON is invalid: ${message}`);
1356
+ }
1357
+ return validateSystemConfig(parsed);
1358
+ };
1359
+
1360
+ // src/config/from-config.ts
1361
+ var resolveWorker = (worker) => {
1362
+ if (typeof worker === "function") {
1363
+ return { run: worker, options: {} };
1364
+ }
1365
+ const { run, concurrency, autoStart } = worker;
1366
+ return {
1367
+ run,
1368
+ options: {
1369
+ ...concurrency !== void 0 ? { concurrency } : {},
1370
+ ...autoStart !== void 0 ? { autoStart } : {}
1371
+ }
1372
+ };
1373
+ };
1374
+ var buildQueueFromConfig = (queueName, queueConfig, storeDefs, resolvedStores) => {
1375
+ let queue = buildQueue(
1376
+ queueConfig.maxSize !== void 0 ? { maxSize: queueConfig.maxSize } : {}
1377
+ );
1378
+ if (queueConfig.persist) {
1379
+ const storeName = queueConfig.persist.store;
1380
+ const definition = storeDefs?.[storeName];
1381
+ const store = resolvedStores[storeName];
1382
+ if (!definition || !store) {
1383
+ throw new Error(
1384
+ `config.queues.${queueName}.persist.store "${storeName}" is not defined in config.stores`
1385
+ );
1386
+ }
1387
+ if (definition.strategy === "snapshot") {
1388
+ if (!isSnapshotStore(store)) {
1389
+ throw new Error(
1390
+ `config.stores.${storeName} is not a SnapshotStore`
1391
+ );
1392
+ }
1393
+ queue = withSnapshotPersist(queue, store, {
1394
+ autoSave: queueConfig.persist.autoSave
1395
+ });
1396
+ } else {
1397
+ if (!isRowStore(store)) {
1398
+ throw new Error(
1399
+ `config.stores.${storeName} is not a RowStore`
1400
+ );
1401
+ }
1402
+ queue = withRowPersist(queue, store);
1403
+ }
1404
+ }
1405
+ if (queueConfig.worker) {
1406
+ const { run, options: workerOptions } = resolveWorker(queueConfig.worker);
1407
+ queue = withWorker(queue, run, workerOptions);
1408
+ }
1409
+ return queue;
1410
+ };
1411
+ var buildFromConfig = async (config, options = {}) => {
1412
+ const validated = validateJsConfig(config);
1413
+ const resolvedStores = resolveAllStores(validated.stores, options);
1414
+ const queues = {};
1415
+ for (const [name, queueConfig] of Object.entries(validated.queues)) {
1416
+ queues[name] = buildQueueFromConfig(
1417
+ name,
1418
+ queueConfig,
1419
+ validated.stores,
1420
+ resolvedStores
1421
+ );
1422
+ }
1423
+ let router;
1424
+ if (validated.router) {
1425
+ const queueMap = queues;
1426
+ let unmatchedTarget;
1427
+ if (validated.router.unmatchedQueue !== void 0) {
1428
+ const sink = queueMap[validated.router.unmatchedQueue];
1429
+ if (!sink) {
1430
+ throw new Error(
1431
+ `router unmatchedQueue "${validated.router.unmatchedQueue}" is not defined`
1432
+ );
1433
+ }
1434
+ unmatchedTarget = sink;
1435
+ }
1436
+ const built = buildRouter(
1437
+ unmatchedTarget !== void 0 ? { unmatchedTarget } : {}
1438
+ );
1439
+ for (const binding of validated.router.bindings ?? []) {
1440
+ const target = queueMap[binding.queue];
1441
+ if (!target) {
1442
+ throw new Error(
1443
+ `router binding queue "${binding.queue}" is not defined`
1444
+ );
1445
+ }
1446
+ built.bind(binding.pattern, target);
1447
+ }
1448
+ router = built;
1449
+ }
1450
+ const hydrateAll = async () => {
1451
+ const tasks = [];
1452
+ for (const queue of Object.values(
1453
+ queues
1454
+ )) {
1455
+ if (typeof queue.hydrate === "function") {
1456
+ tasks.push(queue.hydrate());
1457
+ }
1458
+ }
1459
+ await Promise.all(tasks);
1460
+ };
1461
+ const flushAll = async () => {
1462
+ const tasks = [];
1463
+ for (const queue of Object.values(
1464
+ queues
1465
+ )) {
1466
+ if (typeof queue.flush === "function") {
1467
+ tasks.push(queue.flush());
1468
+ }
1469
+ }
1470
+ await Promise.all(tasks);
1471
+ };
1472
+ const shouldHydrate = validated.hydrate ?? Object.values(validated.queues).some((q) => q.persist !== void 0);
1473
+ if (shouldHydrate) {
1474
+ await hydrateAll();
1475
+ }
1476
+ return {
1477
+ queues,
1478
+ stores: resolvedStores,
1479
+ router,
1480
+ hydrateAll,
1481
+ flushAll,
1482
+ config: freezeConfig(validated)
1483
+ };
1484
+ };
1485
+ var buildFromJson = async (json, options = {}) => {
1486
+ const config = parseSystemConfig(json);
1487
+ return buildFromConfig(config, options);
1488
+ };
1489
+
1490
+ // src/worker/retry.ts
1491
+ var RetryExhaustedError = class extends Error {
1492
+ name = "RetryExhaustedError";
1493
+ attempts;
1494
+ cause;
1495
+ constructor(attempts, cause) {
1496
+ super(`Retry exhausted after ${attempts} attempt(s)`, { cause });
1497
+ this.attempts = attempts;
1498
+ this.cause = cause;
1499
+ }
1500
+ };
1501
+ var sleep = (ms) => new Promise((resolve) => {
1502
+ const schedule = globalThis.setTimeout;
1503
+ schedule(() => resolve(), ms);
1504
+ });
1505
+ var resolveDelay = (delay, failedAttempt, error) => {
1506
+ if (delay === void 0) return 0;
1507
+ if (typeof delay === "function") return Math.max(0, delay(failedAttempt, error));
1508
+ return Math.max(0, delay);
1509
+ };
1510
+ var withRetry = (worker, options) => {
1511
+ const opts = typeof options === "number" ? { retries: options } : options;
1512
+ const maxRetries = Math.max(0, opts.retries);
1513
+ const shouldRetry = opts.shouldRetry ?? (() => true);
1514
+ return async (item) => {
1515
+ let lastError;
1516
+ for (let attempt = 1; attempt <= maxRetries + 1; attempt += 1) {
1517
+ try {
1518
+ return await worker(item);
1519
+ } catch (error) {
1520
+ lastError = error;
1521
+ const failedAttempt = attempt;
1522
+ const retriesLeft = maxRetries + 1 - attempt;
1523
+ if (retriesLeft <= 0 || !shouldRetry(error, failedAttempt)) {
1524
+ throw new RetryExhaustedError(failedAttempt, error);
1525
+ }
1526
+ const wait = resolveDelay(opts.delay, failedAttempt, error);
1527
+ if (wait > 0) {
1528
+ await sleep(wait);
1529
+ }
1530
+ }
1531
+ }
1532
+ throw lastError;
1533
+ };
1534
+ };
1535
+
1536
+ // src/worker/pipeline.ts
1537
+ function pipeline(...steps) {
1538
+ if (steps.length === 0) {
1539
+ throw new Error("pipeline requires at least one step");
1540
+ }
1541
+ return async (input) => {
1542
+ let value = input;
1543
+ for (const step of steps) {
1544
+ value = await step(value);
1545
+ }
1546
+ return value;
1547
+ };
1548
+ }
1549
+
1550
+ export { MULTI_WILDCARD, QueueFullError, RetryExhaustedError, SINGLE_WILDCARD, StorageCodecError, TOPIC_SEPARATOR, buildEventEmitter, buildFromConfig, buildFromJson, buildQueue, buildRouter, createId, createLocalStorageRowStore, createLocalStorageSnapshotStore, createMemoryRowStore, createMemorySnapshotStore, createSessionStorageRowStore, createSessionStorageSnapshotStore, createTypedEmit, createWebRowStore, createWebSnapshotStore, defineConfig, isValidPattern, isValidTopic, matchTopic, parseSystemConfig, pipeline, validateJsConfig, validateSystemConfig, withRetry, withRowPersist, withSnapshotPersist, withWorker };
1551
+ //# sourceMappingURL=index.js.map
1552
+ //# sourceMappingURL=index.js.map