@platformatic/job-queue 0.0.1 → 0.1.0-alpha.3

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 (58) hide show
  1. package/README.md +511 -0
  2. package/dist/consumer.d.ts +43 -0
  3. package/dist/consumer.d.ts.map +1 -0
  4. package/dist/consumer.js +214 -0
  5. package/dist/consumer.js.map +1 -0
  6. package/dist/errors.d.ts +60 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/errors.js +141 -0
  9. package/dist/errors.js.map +1 -0
  10. package/dist/index.d.ts +12 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +13 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/producer.d.ts +39 -0
  15. package/dist/producer.d.ts.map +1 -0
  16. package/dist/producer.js +163 -0
  17. package/dist/producer.js.map +1 -0
  18. package/dist/queue.d.ts +42 -0
  19. package/dist/queue.d.ts.map +1 -0
  20. package/dist/queue.js +153 -0
  21. package/dist/queue.js.map +1 -0
  22. package/dist/reaper.d.ts +52 -0
  23. package/dist/reaper.d.ts.map +1 -0
  24. package/dist/reaper.js +376 -0
  25. package/dist/reaper.js.map +1 -0
  26. package/dist/serde/index.d.ts +19 -0
  27. package/dist/serde/index.d.ts.map +1 -0
  28. package/dist/serde/index.js +18 -0
  29. package/dist/serde/index.js.map +1 -0
  30. package/dist/storage/file.d.ts +44 -0
  31. package/dist/storage/file.d.ts.map +1 -0
  32. package/dist/storage/file.js +601 -0
  33. package/dist/storage/file.js.map +1 -0
  34. package/dist/storage/memory.d.ts +39 -0
  35. package/dist/storage/memory.d.ts.map +1 -0
  36. package/dist/storage/memory.js +292 -0
  37. package/dist/storage/memory.js.map +1 -0
  38. package/dist/storage/redis.d.ts +47 -0
  39. package/dist/storage/redis.d.ts.map +1 -0
  40. package/dist/storage/redis.js +317 -0
  41. package/dist/storage/redis.js.map +1 -0
  42. package/dist/storage/types.d.ts +179 -0
  43. package/dist/storage/types.d.ts.map +1 -0
  44. package/dist/storage/types.js +2 -0
  45. package/dist/storage/types.js.map +1 -0
  46. package/dist/types.d.ts +126 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +2 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/utils/id.d.ts +10 -0
  51. package/dist/utils/id.d.ts.map +1 -0
  52. package/dist/utils/id.js +18 -0
  53. package/dist/utils/id.js.map +1 -0
  54. package/dist/utils/state.d.ts +13 -0
  55. package/dist/utils/state.d.ts.map +1 -0
  56. package/dist/utils/state.js +22 -0
  57. package/dist/utils/state.js.map +1 -0
  58. package/package.json +45 -9
package/README.md ADDED
@@ -0,0 +1,511 @@
1
+ # @platformatic/job-queue
2
+
3
+ A reliable job queue for Node.js with deduplication, request/response support, and pluggable storage backends.
4
+
5
+ ## Features
6
+
7
+ - **Deduplication** - Prevents duplicate job processing with configurable result caching
8
+ - **Request/Response** - Enqueue jobs and wait for results with `enqueueAndWait()`
9
+ - **Multiple Storage Backends** - Redis/Valkey, filesystem, or in-memory
10
+ - **Automatic Retries** - Configurable retry attempts with exponential backoff
11
+ - **Stalled Job Recovery** - Reaper automatically recovers jobs from crashed workers
12
+ - **Graceful Shutdown** - Complete in-flight jobs before stopping
13
+ - **TypeScript Native** - Full type safety with generic payload and result types
14
+ - **Node.js 22.19+** - Uses native TypeScript type stripping
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @platformatic/job-queue
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { Queue, MemoryStorage } from '@platformatic/job-queue'
26
+
27
+ // Create a queue with in-memory storage
28
+ const storage = new MemoryStorage()
29
+ const queue = new Queue<{ email: string }, { sent: boolean }>({
30
+ storage,
31
+ concurrency: 5
32
+ })
33
+
34
+ // Register a job handler
35
+ queue.execute(async (job) => {
36
+ console.log(`Processing job ${job.id}:`, job.payload)
37
+ // Send email...
38
+ return { sent: true }
39
+ })
40
+
41
+ // Start the queue
42
+ await queue.start()
43
+
44
+ // Enqueue jobs
45
+ await queue.enqueue('email-1', { email: 'user@example.com' })
46
+
47
+ // Or wait for the result
48
+ const result = await queue.enqueueAndWait('email-2', { email: 'another@example.com' }, {
49
+ timeout: 30000
50
+ })
51
+ console.log('Result:', result) // { sent: true }
52
+
53
+ // Graceful shutdown
54
+ await queue.stop()
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ ### Queue
60
+
61
+ The main class that combines producer and consumer functionality.
62
+
63
+ ```typescript
64
+ import { Queue } from '@platformatic/job-queue'
65
+
66
+ const queue = new Queue<TPayload, TResult>(config)
67
+ ```
68
+
69
+ #### Configuration
70
+
71
+ | Option | Type | Default | Description |
72
+ |--------|------|---------|-------------|
73
+ | `storage` | `Storage` | *required* | Storage backend instance |
74
+ | `workerId` | `string` | `uuid()` | Unique identifier for this worker |
75
+ | `concurrency` | `number` | `1` | Number of jobs to process in parallel |
76
+ | `maxRetries` | `number` | `3` | Maximum retry attempts for failed jobs |
77
+ | `blockTimeout` | `number` | `5` | Seconds to wait when polling for jobs |
78
+ | `visibilityTimeout` | `number` | `30000` | Milliseconds before a processing job is considered stalled |
79
+ | `resultTTL` | `number` | `3600000` | Milliseconds to cache job results (1 hour) |
80
+ | `payloadSerde` | `Serde<TPayload>` | `JsonSerde` | Custom serializer for job payloads |
81
+ | `resultSerde` | `Serde<TResult>` | `JsonSerde` | Custom serializer for job results |
82
+
83
+ #### Methods
84
+
85
+ ##### `queue.start(): Promise<void>`
86
+
87
+ Connect to storage and start processing jobs.
88
+
89
+ ##### `queue.stop(): Promise<void>`
90
+
91
+ Gracefully stop processing. Waits for in-flight jobs to complete.
92
+
93
+ ##### `queue.execute(handler): void`
94
+
95
+ Register a job handler. Call before or after `start()`.
96
+
97
+ ```typescript
98
+ queue.execute(async (job) => {
99
+ // job.id - unique job identifier
100
+ // job.payload - the job data
101
+ // job.attempts - current attempt number (starts at 1)
102
+ // job.signal - AbortSignal for cancellation
103
+ return result
104
+ })
105
+ ```
106
+
107
+ ##### `queue.enqueue(id, payload, options?): Promise<EnqueueResult>`
108
+
109
+ Enqueue a job (fire-and-forget).
110
+
111
+ ```typescript
112
+ const result = await queue.enqueue('job-123', { data: 'value' })
113
+
114
+ // result.status can be:
115
+ // - 'queued': Job was added to the queue
116
+ // - 'duplicate': Job with this ID already exists
117
+ // - 'completed': Job already completed (returns cached result)
118
+ ```
119
+
120
+ ##### `queue.enqueueAndWait(id, payload, options?): Promise<TResult>`
121
+
122
+ Enqueue a job and wait for the result.
123
+
124
+ ```typescript
125
+ const result = await queue.enqueueAndWait('job-123', payload, {
126
+ timeout: 30000, // Timeout in milliseconds
127
+ maxAttempts: 5 // Override default max retries
128
+ })
129
+ ```
130
+
131
+ Throws `TimeoutError` if the job doesn't complete within the timeout.
132
+
133
+ ##### `queue.cancel(id): Promise<CancelResult>`
134
+
135
+ Cancel a queued job.
136
+
137
+ ```typescript
138
+ const result = await queue.cancel('job-123')
139
+
140
+ // result.status can be:
141
+ // - 'cancelled': Job was successfully cancelled
142
+ // - 'not_found': Job doesn't exist
143
+ // - 'processing': Job is currently being processed (cannot cancel)
144
+ // - 'completed': Job already completed
145
+ ```
146
+
147
+ ##### `queue.getResult(id): Promise<TResult | null>`
148
+
149
+ Get the cached result of a completed job.
150
+
151
+ ##### `queue.getStatus(id): Promise<MessageStatus | null>`
152
+
153
+ Get the current status of a job.
154
+
155
+ ```typescript
156
+ const status = await queue.getStatus('job-123')
157
+ // {
158
+ // id: 'job-123',
159
+ // state: 'completed', // 'queued' | 'processing' | 'failing' | 'completed' | 'failed'
160
+ // createdAt: 1234567890,
161
+ // attempts: 1,
162
+ // result?: { ... },
163
+ // error?: { message: 'Error message', code?: 'ERROR_CODE', stack?: '...' }
164
+ // }
165
+ ```
166
+
167
+ #### Events
168
+
169
+ ```typescript
170
+ // Lifecycle events
171
+ queue.on('started', () => {
172
+ console.log('Queue started')
173
+ })
174
+
175
+ queue.on('stopped', () => {
176
+ console.log('Queue stopped')
177
+ })
178
+
179
+ // Job events
180
+ queue.on('enqueued', (id) => {
181
+ console.log(`Job ${id} was enqueued`)
182
+ })
183
+
184
+ queue.on('completed', (id, result) => {
185
+ console.log(`Job ${id} completed:`, result)
186
+ })
187
+
188
+ queue.on('failed', (id, error) => {
189
+ console.log(`Job ${id} failed:`, error.message)
190
+ })
191
+
192
+ queue.on('failing', (id, error, attempt) => {
193
+ console.log(`Job ${id} failed attempt ${attempt}, will retry:`, error.message)
194
+ })
195
+
196
+ queue.on('requeued', (id) => {
197
+ console.log(`Job ${id} was returned to queue (e.g., during graceful shutdown)`)
198
+ })
199
+
200
+ queue.on('cancelled', (id) => {
201
+ console.log(`Job ${id} was cancelled`)
202
+ })
203
+
204
+ // Error events
205
+ queue.on('error', (error) => {
206
+ console.error('Queue error:', error)
207
+ })
208
+ ```
209
+
210
+ ### Storage Backends
211
+
212
+ #### MemoryStorage
213
+
214
+ In-memory storage for development and testing.
215
+
216
+ ```typescript
217
+ import { MemoryStorage } from '@platformatic/job-queue'
218
+
219
+ const storage = new MemoryStorage()
220
+ ```
221
+
222
+ #### RedisStorage
223
+
224
+ Production-ready storage using Redis or Valkey.
225
+
226
+ ```typescript
227
+ import { RedisStorage } from '@platformatic/job-queue'
228
+
229
+ const storage = new RedisStorage({
230
+ url: 'redis://localhost:6379',
231
+ keyPrefix: 'myapp:' // Optional prefix for all keys
232
+ })
233
+ ```
234
+
235
+ Features:
236
+ - Atomic operations via Lua scripts
237
+ - Blocking dequeue with `BLMOVE`
238
+ - Pub/sub for real-time notifications
239
+ - Compatible with Redis 7+ and Valkey 8+
240
+
241
+ #### FileStorage
242
+
243
+ Filesystem-based storage for single-node deployments.
244
+
245
+ ```typescript
246
+ import { FileStorage } from '@platformatic/job-queue'
247
+
248
+ const storage = new FileStorage({
249
+ basePath: '/var/lib/myapp/queue'
250
+ })
251
+ ```
252
+
253
+ Features:
254
+ - Atomic writes with `fast-write-atomic`
255
+ - FIFO ordering via sequence numbers
256
+ - `fs.watch` for real-time notifications
257
+ - Survives process restarts
258
+
259
+ ### Reaper
260
+
261
+ The Reaper monitors for stalled jobs and requeues them. Use when running multiple workers.
262
+
263
+ ```typescript
264
+ import { Reaper } from '@platformatic/job-queue'
265
+
266
+ const reaper = new Reaper({
267
+ storage,
268
+ visibilityTimeout: 30000 // Same as your queue's visibilityTimeout
269
+ })
270
+
271
+ await reaper.start()
272
+
273
+ reaper.on('stalled', (id) => {
274
+ console.log(`Job ${id} was stalled and requeued`)
275
+ })
276
+
277
+ // On shutdown
278
+ await reaper.stop()
279
+ ```
280
+
281
+ The Reaper uses event-based monitoring: it subscribes to job state changes and sets per-job timers. An initial scan at startup catches any jobs that were processing before the Reaper started.
282
+
283
+ #### Leader Election
284
+
285
+ For high availability, you can run multiple Reaper instances with leader election enabled. Only one instance will be active at a time, and if it fails, another will automatically take over.
286
+
287
+ ```typescript
288
+ const reaper = new Reaper({
289
+ storage,
290
+ visibilityTimeout: 30000,
291
+ leaderElection: {
292
+ enabled: true, // Enable leader election (default: false)
293
+ lockTTL: 30000, // Lock expiry in ms (default: 30s)
294
+ renewalInterval: 10000, // Renew lock every 10s (default: 1/3 of TTL)
295
+ acquireRetryInterval: 5000 // Followers retry every 5s (default: 5s)
296
+ }
297
+ })
298
+
299
+ reaper.on('leadershipAcquired', () => {
300
+ console.log('This reaper is now the leader')
301
+ })
302
+
303
+ reaper.on('leadershipLost', () => {
304
+ console.log('This reaper lost leadership')
305
+ })
306
+
307
+ await reaper.start()
308
+ ```
309
+
310
+ Leader election uses Redis's `SET NX PX` pattern for atomic lock acquisition:
311
+ - The leader acquires a lock with a TTL and renews it periodically
312
+ - If the leader stops gracefully, it releases the lock immediately
313
+ - If the leader crashes, the lock expires and a follower takes over
314
+ - Only RedisStorage supports leader election; other storage backends will emit an error
315
+
316
+ ### Custom Serialization
317
+
318
+ Implement the `Serde` interface for custom serialization:
319
+
320
+ ```typescript
321
+ import { Serde } from '@platformatic/job-queue'
322
+ import * as msgpack from 'msgpackr'
323
+
324
+ class MsgPackSerde<T> implements Serde<T> {
325
+ serialize(value: T): Buffer {
326
+ return msgpack.pack(value)
327
+ }
328
+
329
+ deserialize(buffer: Buffer): T {
330
+ return msgpack.unpack(buffer) as T
331
+ }
332
+ }
333
+
334
+ const queue = new Queue({
335
+ storage,
336
+ payloadSerde: new MsgPackSerde(),
337
+ resultSerde: new MsgPackSerde()
338
+ })
339
+ ```
340
+
341
+ ## Patterns
342
+
343
+ ### Producer/Consumer Separation
344
+
345
+ Run producers and consumers as separate processes:
346
+
347
+ ```typescript
348
+ // producer.ts
349
+ import { Queue, RedisStorage } from '@platformatic/job-queue'
350
+
351
+ const storage = new RedisStorage({ url: process.env.REDIS_URL })
352
+ const producer = new Queue({ storage })
353
+
354
+ await producer.start()
355
+ await producer.enqueue('task-1', { ... })
356
+ await producer.stop()
357
+ ```
358
+
359
+ ```typescript
360
+ // worker.ts
361
+ import { Queue, RedisStorage, Reaper } from '@platformatic/job-queue'
362
+
363
+ const storage = new RedisStorage({ url: process.env.REDIS_URL })
364
+ const queue = new Queue({
365
+ storage,
366
+ workerId: `worker-${process.pid}`,
367
+ concurrency: 10
368
+ })
369
+
370
+ const reaper = new Reaper({
371
+ storage,
372
+ visibilityTimeout: 30000
373
+ })
374
+
375
+ queue.execute(async (job) => {
376
+ // Process job
377
+ return result
378
+ })
379
+
380
+ await queue.start()
381
+ await reaper.start()
382
+
383
+ process.on('SIGTERM', async () => {
384
+ await queue.stop()
385
+ await reaper.stop()
386
+ })
387
+ ```
388
+
389
+ ### Request/Response with Timeout
390
+
391
+ ```typescript
392
+ try {
393
+ const result = await queue.enqueueAndWait('request-1', payload, {
394
+ timeout: 10000
395
+ })
396
+ console.log('Got result:', result)
397
+ } catch (error) {
398
+ if (error instanceof TimeoutError) {
399
+ console.log('Request timed out')
400
+ } else if (error instanceof JobFailedError) {
401
+ console.log('Job failed:', error.originalError)
402
+ }
403
+ }
404
+ ```
405
+
406
+ ### Graceful Shutdown
407
+
408
+ ```typescript
409
+ const queue = new Queue({
410
+ storage,
411
+ visibilityTimeout: 30000 // Jobs have 30s to complete
412
+ })
413
+
414
+ queue.execute(async (job) => {
415
+ // Check for cancellation
416
+ if (job.signal.aborted) {
417
+ throw new Error('Job cancelled')
418
+ }
419
+
420
+ // Long-running work...
421
+ await doWork()
422
+
423
+ return result
424
+ })
425
+
426
+ await queue.start()
427
+
428
+ // Handle shutdown signals
429
+ process.on('SIGTERM', async () => {
430
+ console.log('Shutting down...')
431
+ await queue.stop() // Waits for in-flight jobs
432
+ process.exit(0)
433
+ })
434
+ ```
435
+
436
+ ## Error Handling
437
+
438
+ The library exports typed errors for specific failure conditions:
439
+
440
+ ```typescript
441
+ import {
442
+ TimeoutError, // enqueueAndWait timeout
443
+ MaxRetriesError, // Job failed after all retries
444
+ JobNotFoundError, // Job doesn't exist
445
+ JobCancelledError, // Job was cancelled
446
+ JobFailedError, // Job failed with error
447
+ StorageError // Storage backend error
448
+ } from '@platformatic/job-queue'
449
+ ```
450
+
451
+ ## Benchmarks
452
+
453
+ Run request/response benchmarks over Redis:
454
+
455
+ ```bash
456
+ # With Redis
457
+ npm run bench:redis
458
+
459
+ # With Valkey
460
+ npm run bench:valkey
461
+ ```
462
+
463
+ Sample output:
464
+
465
+ ```
466
+ Single Request Baseline (100 requests, concurrency=1)
467
+ ──────────────────────────────────────────────────
468
+ Requests: 100
469
+ Throughput: 312.50 req/s
470
+ Latency:
471
+ min: 2.15 ms
472
+ p50: 3.02 ms
473
+ p95: 4.21 ms
474
+ p99: 5.43 ms
475
+ max: 6.12 ms
476
+ avg: 3.20 ms
477
+ ```
478
+
479
+ ## Testing
480
+
481
+ Run tests against different backends:
482
+
483
+ ```bash
484
+ # Memory storage only
485
+ npm run test:memory
486
+
487
+ # With Redis (requires Redis on localhost:6379)
488
+ npm run test:redis
489
+
490
+ # With Valkey (requires Valkey on localhost:6380)
491
+ npm run test:valkey
492
+
493
+ # All backends
494
+ npm test
495
+ ```
496
+
497
+ Start test infrastructure with Docker:
498
+
499
+ ```bash
500
+ npm run docker:up # Start Redis and Valkey
501
+ npm run docker:down # Stop containers
502
+ ```
503
+
504
+ ## Requirements
505
+
506
+ - Node.js 22.19.0 or later (for native TypeScript support)
507
+ - Redis 7+ or Valkey 8+ (for RedisStorage)
508
+
509
+ ## License
510
+
511
+ Apache 2.0
@@ -0,0 +1,43 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Storage } from './storage/types.ts';
3
+ import type { Serde } from './serde/index.ts';
4
+ import type { JobHandler } from './types.ts';
5
+ interface ConsumerConfig<TPayload, TResult> {
6
+ storage: Storage;
7
+ workerId: string;
8
+ payloadSerde?: Serde<TPayload>;
9
+ resultSerde?: Serde<TResult>;
10
+ concurrency?: number;
11
+ blockTimeout?: number;
12
+ maxRetries?: number;
13
+ resultTTL?: number;
14
+ visibilityTimeout?: number;
15
+ }
16
+ interface ConsumerEvents<TResult> {
17
+ error: [error: Error];
18
+ completed: [id: string, result: TResult];
19
+ failed: [id: string, error: Error];
20
+ failing: [id: string, error: Error, attempt: number];
21
+ requeued: [id: string];
22
+ }
23
+ /**
24
+ * Consumer handles processing jobs from the queue
25
+ */
26
+ export declare class Consumer<TPayload, TResult> extends EventEmitter<ConsumerEvents<TResult>> {
27
+ #private;
28
+ constructor(config: ConsumerConfig<TPayload, TResult>);
29
+ /**
30
+ * Register a job handler
31
+ */
32
+ execute(handler: JobHandler<TPayload, TResult>): void;
33
+ /**
34
+ * Start consuming jobs
35
+ */
36
+ start(): Promise<void>;
37
+ /**
38
+ * Stop consuming jobs gracefully
39
+ */
40
+ stop(): Promise<void>;
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=consumer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,YAAY,CAAA;AAI/D,UAAU,cAAc,CAAC,QAAQ,EAAE,OAAO;IACxC,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC9B,WAAW,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,UAAU,cAAc,CAAC,OAAO;IAC9B,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IACrB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAClC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;IACpD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;CACvB;AAED;;GAEG;AACH,qBAAa,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAE,SAAQ,YAAY,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;;gBAiBvE,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC;IAatD;;OAEG;IACH,OAAO,CAAE,OAAO,EAAE,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI;IAItD;;OAEG;IACG,KAAK,IAAK,OAAO,CAAC,IAAI,CAAC;IAwB7B;;OAEG;IACG,IAAI,IAAK,OAAO,CAAC,IAAI,CAAC;CA6K7B"}