queue-jobs-worker 1.0.0 → 1.0.2

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/CHANGELOG.md CHANGED
@@ -1,76 +1,128 @@
1
- # Changelog
2
-
3
- All notable changes to **queue-jobs-worker** will be documented in this file.
4
-
5
- The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ---
9
-
10
- ## [1.0.0] — 2026-08-29
11
-
12
- ### Added
13
-
14
- **Core**
15
-
16
- - `QueueClient` — primary entry point; configures storage, exposes queues, and coordinates lifecycle.
17
- - `Queue` independent job stream with per-queue configuration overrides.
18
- - `Job` rich wrapper around the raw job data record; passed to user processors.
19
- - `Worker` claims and executes jobs with configurable concurrency and graceful shutdown.
20
- - `QueueEventEmitter` strongly-typed lifecycle event bus shared across all components.
21
-
22
- **Job processing**
23
-
24
- - At-least-once delivery guarantee.
25
- - Stable job identity preserved across all retries (no duplicate job IDs).
26
- - Full attempt history stored per job.
27
- - Per-job processor timeout enforcement.
28
- - Priority-ordered job claiming (higher priority = claimed first).
29
- - Delayed and scheduled job support via `schedule.delay` and `schedule.runAt`.
30
- - Recurring job support via `schedule.cron` field (cron expression stored; recurrence integration point provided).
31
-
32
- **Reliability**
33
-
34
- - Configurable retry with `attempts`, `retryDelay`, and `backoff` strategy.
35
- - Three backoff strategies: `fixed`, `linear`, `exponential` (capped at 10 minutes).
36
- - Dead Letter Queue (DLQ): jobs moved to `dead` status after exhausting all attempts.
37
- - Stalled-job recovery: expired locks on `active` jobs are detected and re-queued automatically.
38
- - Configurable lock duration (`lockDuration`) per queue.
39
-
40
- **Concurrency**
41
-
42
- - Per-worker concurrency limit (`concurrency`).
43
- - Atomic job claiming inside `InMemoryStorageAdapter` (single event-loop tick).
44
-
45
- **Rate limiting**
46
-
47
- - Sliding-window rate limiter configurable per queue (`rateLimit.max` / `rateLimit.duration`).
48
-
49
- **Storage**
50
-
51
- - `StorageAdapter` interface — clean abstraction; all core logic talks through this interface.
52
- - `InMemoryStorageAdapter` — full-featured in-process adapter for development and testing.
53
-
54
- **Configuration**
55
-
56
- - Layered configuration: Client defaults Queue options Worker options Job options.
57
- - `QueueClient.withAdapter()` static factory for supplying a custom storage adapter.
58
-
59
- **TypeScript**
60
-
61
- - Full strict TypeScript types with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`.
62
- - All public types exported from the top-level `index.ts`.
63
-
64
- **Build**
65
-
66
- - Dual ESM + CJS output via `tsup`.
67
- - Declaration files (`.d.ts` + `.d.ts.map`) generated via `tsc`.
68
-
69
- **Tests**
70
-
71
- - 31 tests across backoff strategies, storage adapter, queue client, and worker behaviour.
72
- - Coverage: job lifecycle, retry, DLQ, concurrency, stalled recovery, rate limiting, graceful shutdown.
73
-
74
- ---
75
-
76
- [1.0.0]: https://github.com/rafidahmed870/queue-jobs-worker/releases/tag/v1.0.0
1
+ # Changelog
2
+
3
+ All notable changes to **queue-jobs-worker** will be documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
+ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+ ## [1.0.2] — 2026-09-05
10
+
11
+ ### Core
12
+
13
+ ### Fixed
14
+
15
+ - **`Worker` — croner added as a required dependency; invalid expressions no longer fall back to a 1-minute interval** ([#5](https://github.com/rafidahmed870/queue-jobs-worker/issues/5))
16
+
17
+ `enqueueCronNext()` previously attempted a dynamic `import("croner")` inside
18
+ a try/catch. If the import failed or if the resolved `Cron` class was not a
19
+ functionthe code silently fell back to `Date.now() + 60_000`, scheduling
20
+ the next run 60 seconds later regardless of the configured cron expression.
21
+ The same silent fallback was also triggered for invalid cron expressions that
22
+ caused the `Cron` constructor to throw.
23
+
24
+ After the fix:
25
+
26
+ - `croner` is now declared as a proper `dependency` in `package.json`
27
+ (`^10.0.1`) and imported statically, so it is always available without any
28
+ dynamic-import dance.
29
+ - If the `Cron` constructor throws (invalid expression), a descriptive
30
+ `worker:error` event is emitted and re-enqueue is skipped. The worker
31
+ remains running.
32
+ - If `cronInstance.nextRun()` returns `null` (the schedule has no future
33
+ occurrences), a `worker:error` is emitted and re-enqueue is skipped. Again,
34
+ the worker keeps running.
35
+ - The 1-minute fallback path has been removed entirely there is no silent
36
+ fallback under any failure condition.
37
+
38
+ - **`Worker` rate-limit quota no longer consumed on empty-queue polls** ([#4](https://github.com/rafidahmed870/queue-jobs-worker/issues/4))
39
+
40
+ `claimNext()` previously called `checkAndIncrementRateLimit()` before
41
+ attempting to claim a job. This meant every poll cycle against an empty queue
42
+ burned a quota slot, potentially exhausting the configured window budget
43
+ before any real work was done. After the fix, the storage `claim()` call
44
+ happens first; the rate-limit counter is only incremented when a job is
45
+ actually claimed for processing. If the rate limit is reached at that point
46
+ the lock is immediately released via `releaseLock()` so the job remains
47
+ reclaimable on the next window.
48
+
49
+ ---
50
+
51
+ ### Events
52
+
53
+ ### Added
54
+
55
+ - `QueueEventEmitter` — strongly-typed lifecycle event bus shared across all components.
56
+ - Emits events for the full job lifecycle: enqueued, started, completed, failed, retrying, dead, stalled.
57
+ - All event payloads fully typed via `events.types.ts`.
58
+
59
+ ---
60
+
61
+ <!-- Links -->
62
+ [1.0.0]: https://github.com/rafidahmed870/queue-jobs-worker/releases/tag/v1.0.0
63
+
64
+ ### Storage
65
+
66
+ ### Fixed
67
+
68
+ - **`recoverStalledJobs()` race condition — stale recovery overwrites a live job** ([#6](https://github.com/rafidahmed870/queue-jobs-worker/issues/6))
69
+
70
+ The previous implementation used a two-phase read-then-write pattern:
71
+
72
+ 1. A fetch pipeline read `lockExpiresAt` and `priority` for all active jobs.
73
+ 2. A separate write pipeline recovered every job whose lock appeared expired.
74
+
75
+ Between those two phases a worker could complete the job, fail it, or renew
76
+ its lock. The write pipeline had no knowledge of that change and would
77
+ unconditionally overwrite the job back to `"waiting"`, causing duplicate
78
+ processing or data loss.
79
+
80
+ **`RedisStorageAdapter`** — the write pipeline has been replaced with a
81
+ per-job Lua script (`RECOVER_STALLED_LUA`) that implements a
82
+ **compare-and-swap (CAS)** guard. The script atomically re-reads
83
+ `lockExpiresAt`, `lockId`, and `status` from the hash and aborts if any of
84
+ the three values differ from what the caller observed in the read phase.
85
+ Because Redis executes Lua scripts as a single indivisible command, no
86
+ concurrent write can slip between the re-read and the state update. The
87
+ pre-filter (skip jobs whose lock has not yet expired) is preserved as an
88
+ optimisation to avoid unnecessary Lua round-trips.
89
+
90
+ **`InMemoryStorageAdapter`** — all operations run within a single event-loop
91
+ tick so the race is theoretical, but an equivalent CAS guard has been added
92
+ for consistency: `lockId` and `lockExpiresAt` are snapshotted at decision
93
+ time and re-validated immediately before the write. Any interleaving that
94
+ mutated those fields will cause the recovery to be skipped.
95
+
96
+ ---
97
+
98
+ ## [1.0.1] — 2026-08-31
99
+
100
+ ### Core
101
+
102
+ ### Fixed
103
+
104
+ - **`Worker.stop()` — clarified `releaseLock()` behavior in shutdown comment** ([#1](https://github.com/rafidahmed870/queue-jobs-worker/issues/1))
105
+
106
+ The inline comment in `worker.ts` now correctly explains that `releaseLock()`
107
+ sets `lockExpiresAt` to an already-expired timestamp (not null/empty), so
108
+ `recoverStalledJobs()` on any worker will immediately reclaim the job on the
109
+ next stall-check cycle.
110
+
111
+ ---
112
+
113
+ ### Events
114
+
115
+ ### Added
116
+
117
+ - `QueueEventEmitter` — strongly-typed lifecycle event bus shared across all components.
118
+ - Emits events for the full job lifecycle: enqueued, started, completed, failed, retrying, dead, stalled.
119
+ - All event payloads fully typed via `events.types.ts`.
120
+
121
+ ---
122
+
123
+ <!-- Links -->
124
+
125
+ [1.0.2]: https://github.com/rafidahmed870/queue-jobs-worker/compare/v1.0.0...v1.0.2
126
+ [1.0.0]: https://github.com/rafidahmed870/queue-jobs-worker/releases/tag/v1.0.0
127
+ [1.0.1]: https://github.com/rafidahmed870/queue-jobs-worker/compare/v1.0.0...v1.0.1
128
+ [1.0.0]: https://github.com/rafidahmed870/queue-jobs-worker/releases/tag/v1.0.0
package/README.md CHANGED
@@ -12,6 +12,8 @@ A production-ready background job queue for Node.js — persistent, reliable, an
12
12
 
13
13
  `queue-jobs-worker` lets you push work into a persistent queue and process it in the background — outside the main request lifecycle. You define the processor; the library handles everything else: queueing, persistence, retries, scheduling, concurrency, and failure recovery.
14
14
 
15
+ For a full breakdown of every feature, see [FEATURES.md](./FEATURES.md).
16
+
15
17
  **Supports:**
16
18
  - In-memory (dev / testing)
17
19
  - Redis (node-redis v4+)
@@ -374,29 +376,7 @@ queue.process("send-push", async (job) => {
374
376
  });
375
377
  ```
376
378
 
377
- The `job` object exposes:
378
-
379
- ```js
380
- job.id // unique stable ID
381
- job.type // job type string
382
- job.data // your payload (never logged)
383
- job.attemptsMade // attempts already executed
384
- job.maxAttempts // maximum allowed
385
- job.attemptsRemaining
386
- job.attemptHistory // array of past attempt records
387
- job.status // current status
388
- job.priority
389
- job.runAt
390
- job.createdAt
391
- job.updatedAt
392
-
393
- // helpers
394
- job.isActive()
395
- job.isCompleted()
396
- job.isWaiting()
397
- job.isDelayed()
398
- job.isDead()
399
- ```
379
+ For the full list of `job` fields and helper methods, see [FEATURES.md → Job Identity & Metadata](./FEATURES.md#job-identity--metadata).
400
380
 
401
381
  ---
402
382
 
@@ -429,35 +409,17 @@ const w2 = queue.createWorker({ concurrency: 5 });
429
409
  All lifecycle events are emitted on the client. Subscribe before creating queues/workers:
430
410
 
431
411
  ```js
432
- // Job events
433
- client.on("job:enqueued", (job) => console.log("Enqueued:", job.id));
434
- client.on("job:started", (job) => console.log("Started:", job.id));
435
412
  client.on("job:completed", (job) => console.log("Done:", job.id));
436
413
  client.on("job:failed", (job, err) => console.error("Failed:", job.id, err.message));
437
- client.on("job:retrying", (job, err, nextRunAt) => {
438
- console.log(`Retrying ${job.id} at ${nextRunAt}`);
439
- });
440
414
  client.on("job:dead", (job, err) => console.error("DLQ:", job.id, err.message));
441
- client.on("job:stalled", (jobId) => console.warn("Stalled:", jobId));
442
- client.on("job:recovered", (jobId) => console.log("Recovered:", jobId));
443
-
444
- // Worker events
445
- client.on("worker:started", (workerId) => console.log("Worker started:", workerId));
446
- client.on("worker:stopped", (workerId) => console.log("Worker stopped:", workerId));
447
- client.on("worker:status", (workerId, status) => console.log(workerId, "→", status));
448
- client.on("worker:error", (workerId, err) => console.error("Worker error:", err));
415
+ client.on("worker:error", (workerId, err) => console.error("Worker error:", err));
449
416
 
450
- // Queue / system errors
451
- client.on("queue:error", (queueName, err) => console.error("Queue error:", queueName, err));
452
- client.on("error", (err) => console.error("Error:", err));
453
-
454
- // Remove a listener
455
- client.off("job:completed", myListener);
456
-
457
- // One-time listener
458
- client.once("job:dead", (job, err) => alertTeam(job, err));
417
+ client.off("job:completed", myListener); // remove a listener
418
+ client.once("job:dead", (job, err) => alertTeam(job, err)); // one-time listener
459
419
  ```
460
420
 
421
+ For the full event reference (all job, worker, and system events), see [FEATURES.md → Event System](./FEATURES.md#event-system).
422
+
461
423
  ---
462
424
 
463
425
  ## Querying Jobs
@@ -507,44 +469,24 @@ await queue.enqueue("task", payload, {
507
469
  });
508
470
  ```
509
471
 
510
- **Backoff strategies:**
511
-
512
- | Strategy | Formula | Example (base = 1 s) |
513
- |---|---|---|
514
- | `fixed` | `baseDelay` | 1 s, 1 s, 1 s |
515
- | `linear` | `baseDelay × attempt` | 1 s, 2 s, 3 s |
516
- | `exponential` | `baseDelay × 2^(attempt-1)` (max 10 min) | 1 s, 2 s, 4 s, 8 s |
517
-
518
- Each failed attempt is recorded in `job.attemptHistory`:
519
-
520
- ```js
521
- job.attemptHistory.forEach((attempt) => {
522
- console.log(`Attempt ${attempt.attempt}: ${attempt.error}`);
523
- });
524
- ```
472
+ Three strategies are available: `fixed`, `linear`, and `exponential`. Each failed attempt is recorded in `job.attemptHistory`. See [FEATURES.md → Retry & Backoff](./FEATURES.md#retry--backoff) for strategy formulas and details.
525
473
 
526
474
  ---
527
475
 
528
476
  ## Scheduling
529
477
 
530
478
  ```js
531
- // Run 30 seconds from now
532
- await queue.enqueue("reminder", payload, {
533
- schedule: { delay: 30_000 },
534
- });
479
+ // Relative delay
480
+ await queue.enqueue("reminder", payload, { schedule: { delay: 30_000 } });
535
481
 
536
- // Run at a specific time
537
- await queue.enqueue("report", payload, {
538
- schedule: { runAt: "2026-09-01T09:00:00Z" },
539
- });
482
+ // Absolute timestamp
483
+ await queue.enqueue("report", payload, { schedule: { runAt: "2026-09-01T09:00:00Z" } });
540
484
 
541
- // Store a cron expression (integration point for recurring jobs)
542
- await queue.enqueue("cleanup", payload, {
543
- schedule: { cron: "0 3 * * *" },
544
- });
485
+ // Cron expression (stored for recurring jobs)
486
+ await queue.enqueue("cleanup", payload, { schedule: { cron: "0 3 * * *" } });
545
487
  ```
546
488
 
547
- Delayed jobs are not eligible until their `runAt` time. The Redis adapter promotes them automatically inside the Lua claim script; SQL adapters check `run_at` in the `WHERE` clause.
489
+ Delayed jobs are not eligible until their `runAt` time. See [FEATURES.md Scheduling](./FEATURES.md#scheduling) for details on how each adapter handles promotion.
548
490
 
549
491
  ---
550
492
 
@@ -556,34 +498,22 @@ Higher values are processed first. Default is `0`.
556
498
  await queue.enqueue("urgent-task", payload, { priority: 100 });
557
499
  await queue.enqueue("normal-task", payload, { priority: 0 });
558
500
  await queue.enqueue("low-task", payload, { priority: -10 });
559
-
560
501
  // Processing order: urgent → normal → low
561
502
  ```
562
503
 
504
+ See [FEATURES.md → Priority](./FEATURES.md#priority).
505
+
563
506
  ---
564
507
 
565
508
  ## Rate Limiting
566
509
 
567
- Limit how many jobs are processed per time window:
568
-
569
510
  ```js
570
- // Queue-level rate limit
571
511
  const queue = client.createQueue("webhooks", {
572
- rateLimit: {
573
- max: 50, // max 50 jobs
574
- duration: 60_000, // per 60 seconds
575
- },
576
- });
577
-
578
- // Or as a client default
579
- const client = new QueueClient({
580
- defaults: {
581
- rateLimit: { max: 100, duration: 60_000 },
582
- },
512
+ rateLimit: { max: 50, duration: 60_000 }, // 50 jobs per minute
583
513
  });
584
514
  ```
585
515
 
586
- When the limit is reached, workers skip claiming until the window resets. Jobs stay in the queue and are not lost.
516
+ When the limit is reached, workers skip claiming until the window resets jobs are never discarded. See [FEATURES.md Rate Limiting](./FEATURES.md#rate-limiting).
587
517
 
588
518
  ---
589
519
 
@@ -593,26 +523,14 @@ When a job exhausts all retry attempts it is moved to the DLQ (status: `"dead"`)
593
523
 
594
524
  ```js
595
525
  client.on("job:dead", async (job, error) => {
596
- await alertOncall({
597
- jobId: job.id,
598
- type: job.type,
599
- error: error.message,
600
- attempts: job.attemptsMade,
601
- });
526
+ await alertOncall({ jobId: job.id, type: job.type, error: error.message });
602
527
  });
603
528
 
604
- // Query dead jobs
605
529
  const deadJobs = await queue.getJobs("dead");
606
-
607
- // Inspect a dead job's full attempt history
608
- const job = await queue.getJob(jobId);
609
- if (job) {
610
- job.attemptHistory.forEach((a) => {
611
- console.log(`Attempt ${a.attempt} failed: ${a.error}`);
612
- });
613
- }
614
530
  ```
615
531
 
532
+ Full attempt history is preserved on the job. See [FEATURES.md → Dead Letter Queue](./FEATURES.md#dead-letter-queue).
533
+
616
534
  ---
617
535
 
618
536
  ## Graceful Shutdown
@@ -620,21 +538,17 @@ if (job) {
620
538
  Always call `client.close()` before your process exits:
621
539
 
622
540
  ```js
623
- async function shutdown() {
624
- await client.close(); // stops workers, releases locks, closes DB connections
541
+ process.on("SIGTERM", async () => {
542
+ await client.close(); // stops workers, releases locks, closes connections
625
543
  process.exit(0);
626
- }
627
-
628
- process.on("SIGTERM", shutdown);
629
- process.on("SIGINT", shutdown);
544
+ });
545
+ process.on("SIGINT", async () => {
546
+ await client.close();
547
+ process.exit(0);
548
+ });
630
549
  ```
631
550
 
632
- During shutdown each worker:
633
- 1. Stops polling for new jobs
634
- 2. Waits up to `shutdownTimeout` (default 30 s) for active jobs to finish
635
- 3. Releases DB/Redis connections
636
-
637
- Interrupted jobs remain recoverable — the stalled-job recovery mechanism will reclaim them on the next startup.
551
+ Interrupted jobs remain recoverable via the stalled-job recovery mechanism. See [FEATURES.md → Graceful Shutdown](./FEATURES.md#graceful-shutdown).
638
552
 
639
553
  ---
640
554
 
@@ -818,4 +732,10 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
818
732
 
819
733
  ## License
820
734
 
821
- MIT — [LICENSE](./LICENSE)
735
+ MIT — [LICENSE](./LICENSE)
736
+
737
+ ## Donation
738
+
739
+ If you find this project useful, you can support me with a coffee.
740
+
741
+ **BTC:** `12dxgVQ3sRFhc4g7M6oydsN2tTMMthJJqS`
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/core/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,KAAK,gBAAgB,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;AAyBjD,qBAAa,MAAM;IACjB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,WAAW,CAAK;IAExB;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAElD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,YAAY,CAA+B;IAEnD,4DAA4D;IAC5D,OAAO,CAAC,YAAY,CAA6B;gBAG/C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,EAC3C,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,gBAAgB;IAc5B,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,8BAA8B;IAC9B,KAAK,IAAI,IAAI;IAab;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiD3B,OAAO,CAAC,YAAY;YAQN,IAAI;YAsBJ,SAAS;YAiCT,UAAU;YAwDV,eAAe;YAwDf,aAAa;IA0C3B,OAAO,CAAC,kBAAkB;YAQZ,kBAAkB;CAkBjC"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/core/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,KAAK,gBAAgB,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;AAyBjD,qBAAa,MAAM;IACjB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,WAAW,CAAK;IAExB;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAElD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,YAAY,CAA+B;IAEnD,4DAA4D;IAC5D,OAAO,CAAC,YAAY,CAA6B;gBAG/C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,EAC3C,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,gBAAgB;IAc5B,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,8BAA8B;IAC9B,KAAK,IAAI,IAAI;IAab;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqD3B,OAAO,CAAC,YAAY;YAQN,IAAI;YAsBJ,SAAS;YAyCT,UAAU;YAwDV,eAAe;YA4Df,aAAa;IA0C3B,OAAO,CAAC,kBAAkB;YAQZ,kBAAkB;CAkBjC"}