@molecule/api-queue-memory 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.
Files changed (2) hide show
  1. package/README.md +502 -0
  2. package/package.json +6 -5
package/README.md ADDED
@@ -0,0 +1,502 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-04T01:48:59.969Z
7
+ -->
8
+
9
+ # @molecule/api-queue-memory
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ In-memory queue provider for molecule.dev.
16
+
17
+ A zero-dependency, zero-configuration, in-process queue provider for
18
+ development and testing — no broker, no credentials, no environment
19
+ variables. Implements the full `@molecule/api-queue` contract with
20
+ SQS-style semantics: named queues, at-least-once delivery via visibility
21
+ leases, delayed messages (`delaySeconds`), redelivery on subscriber
22
+ handler failure or `nack()`, a bounded delivery cap with optional
23
+ dead-letter routing, FIFO group ordering + deduplication, long-polling
24
+ `receive()`, and `maxMessages`-bounded subscriber concurrency.
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { setProvider, queue } from '@molecule/api-queue'
30
+ import { provider } from '@molecule/api-queue-memory'
31
+
32
+ setProvider(provider) // no configuration, no env vars
33
+
34
+ const emails = queue('emails')
35
+ const unsubscribe = emails.subscribe(async (message) => {
36
+ await deliver(message.body)
37
+ await message.ack() // handler success also auto-acks
38
+ })
39
+
40
+ await emails.send({ body: { to: 'a@b.c' } })
41
+ await emails.send({ body: { to: 'later@b.c' }, delaySeconds: 60 })
42
+ ```
43
+
44
+ ## Type
45
+
46
+ `provider`
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ npm install @molecule/api-queue-memory @molecule/api-bond @molecule/api-queue
52
+ ```
53
+
54
+ ## API
55
+
56
+ ### Interfaces
57
+
58
+ #### `MemoryQueueConfig`
59
+
60
+ Internal configuration for a single in-memory queue instance, resolved by
61
+ the provider from `MemoryQueueOptions` and per-queue `QueueCreateOptions`.
62
+
63
+ ```typescript
64
+ interface MemoryQueueConfig {
65
+ /**
66
+ * Default visibility timeout in seconds for leases without an explicit
67
+ * `ReceiveOptions.visibilityTimeout`.
68
+ */
69
+ defaultVisibilityTimeoutSeconds: number
70
+
71
+ /**
72
+ * Maximum deliveries before dead-lettering/dropping. Overridden per queue
73
+ * by `deadLetterQueue.maxReceiveCount` when a dead-letter queue is set.
74
+ */
75
+ maxReceiveCount: number
76
+
77
+ /**
78
+ * Delay in seconds before redelivery after an explicit `nack()`.
79
+ */
80
+ redeliveryDelaySeconds: number
81
+
82
+ /**
83
+ * Delay in seconds before redelivery after an uncaught `subscribe()`
84
+ * handler failure.
85
+ */
86
+ handlerFailureRedeliveryDelaySeconds: number
87
+
88
+ /**
89
+ * Whether this queue enforces FIFO semantics (per-`groupId` ordered,
90
+ * head-of-line-blocking delivery plus `deduplicationId` deduplication).
91
+ */
92
+ fifo: boolean
93
+
94
+ /**
95
+ * Optional retention period in seconds; messages older than this are
96
+ * discarded when next scanned.
97
+ */
98
+ messageRetentionSeconds?: number
99
+
100
+ /**
101
+ * Optional dead-letter queue for messages exceeding the delivery cap.
102
+ */
103
+ deadLetterQueue?: QueueCreateOptions['deadLetterQueue']
104
+
105
+ /**
106
+ * Resolves another queue by name — used to route dead-lettered messages.
107
+ */
108
+ resolveQueue: (name: string) => Queue
109
+ }
110
+ ```
111
+
112
+ #### `MemoryQueueHandle`
113
+
114
+ Handle pairing a `Queue` with the internal lifecycle control the provider
115
+ uses to shut it down (`close()` is not part of the core `Queue` interface).
116
+
117
+ ```typescript
118
+ interface MemoryQueueHandle {
119
+ /**
120
+ * The in-memory queue implementation.
121
+ */
122
+ queue: Queue
123
+
124
+ /**
125
+ * Stops all timers, resolves pending long-polls with `[]`, discards all
126
+ * messages and subscribers, and rejects further sends.
127
+ */
128
+ close(): void
129
+ }
130
+ ```
131
+
132
+ #### `MemoryQueueOptions`
133
+
134
+ Options for creating an in-memory queue provider.
135
+
136
+ All options have working defaults — the provider is fully functional with
137
+ zero configuration and zero environment variables.
138
+
139
+ ```typescript
140
+ interface MemoryQueueOptions {
141
+ /**
142
+ * Default visibility timeout in seconds applied to received/dispatched
143
+ * messages when `ReceiveOptions.visibilityTimeout` is not given.
144
+ * A leased (in-flight) message whose lease expires without an `ack()`
145
+ * becomes visible again and is redelivered (at-least-once delivery).
146
+ * Defaults to `30`.
147
+ */
148
+ visibilityTimeoutSeconds?: number
149
+
150
+ /**
151
+ * Maximum number of times a message may be delivered before it is routed
152
+ * to the queue's dead-letter queue (when configured via
153
+ * `QueueCreateOptions.deadLetterQueue`) or dropped with an error log.
154
+ * Mirrors the Redis bond's `attempts: 3`. Defaults to `3`.
155
+ */
156
+ maxReceiveCount?: number
157
+
158
+ /**
159
+ * Delay in seconds before a message is redelivered after an explicit
160
+ * `nack()` (a pull `receive()` consumer's deliberate "put this back now").
161
+ * Defaults to `0` (immediate redelivery) — an explicit `nack()` is a
162
+ * caller decision that should be honored right away, not throttled.
163
+ */
164
+ redeliveryDelaySeconds?: number
165
+
166
+ /**
167
+ * Delay in seconds before a message is redelivered after an UNCAUGHT
168
+ * `subscribe()` handler failure (a thrown error) — distinct from
169
+ * `redeliveryDelaySeconds` because a throw is an unplanned failure (e.g. a
170
+ * downstream 503) that deserves a real retry window, mirroring the Redis
171
+ * bond's `attempts: 3, backoff: { type: 'exponential', delay: 1000 }`.
172
+ * Defaults to `1`. With the default `maxReceiveCount` of `3`, a
173
+ * `redeliveryDelaySeconds` of `0` would burn all delivery attempts within
174
+ * milliseconds and drop the message with no real chance for a transient
175
+ * downstream failure to recover — this option exists so "retry" means a
176
+ * few real seconds apart, not a hot loop.
177
+ */
178
+ handlerFailureRedeliveryDelaySeconds?: number
179
+ }
180
+ ```
181
+
182
+ #### `Queue`
183
+
184
+ Handle for a named queue, providing send, receive, and subscribe operations.
185
+
186
+ ```typescript
187
+ interface Queue {
188
+ /**
189
+ * Queue name.
190
+ */
191
+ name: string
192
+ /**
193
+ * Sends a message to the queue.
194
+ */
195
+ send<T = unknown>(message: QueueMessage<T>): Promise<string>
196
+ /**
197
+ * Sends multiple messages to the queue.
198
+ */
199
+ sendBatch?<T = unknown>(messages: QueueMessage<T>[]): Promise<string[]>
200
+ /**
201
+ * Receives messages from the queue.
202
+ */
203
+ receive<T = unknown>(options?: ReceiveOptions): Promise<ReceivedMessage<T>[]>
204
+ /**
205
+ * Subscribes to messages from the queue.
206
+ * Returns a function to unsubscribe.
207
+ */
208
+ subscribe<T = unknown>(handler: MessageHandler<T>, options?: ReceiveOptions): () => void
209
+ /**
210
+ * Gets the approximate number of messages in the queue.
211
+ */
212
+ size?(): Promise<number>
213
+ /**
214
+ * Purges all messages from the queue.
215
+ */
216
+ purge?(): Promise<void>
217
+ }
218
+ ```
219
+
220
+ #### `QueueCreateOptions`
221
+
222
+ Options for creating a new queue, including FIFO mode, timeouts,
223
+ retention periods, and dead-letter queue configuration.
224
+
225
+ ```typescript
226
+ interface QueueCreateOptions {
227
+ /**
228
+ * Whether this is a FIFO queue.
229
+ */
230
+ fifo?: boolean
231
+ /**
232
+ * Default visibility timeout in seconds.
233
+ */
234
+ visibilityTimeout?: number
235
+ /**
236
+ * Message retention period in seconds.
237
+ */
238
+ messageRetentionSeconds?: number
239
+ /**
240
+ * Maximum message size in bytes.
241
+ */
242
+ maxMessageSize?: number
243
+ /**
244
+ * Dead letter queue configuration.
245
+ */
246
+ deadLetterQueue?: {
247
+ name: string
248
+ maxReceiveCount: number
249
+ }
250
+ }
251
+ ```
252
+
253
+ #### `QueueMessage`
254
+
255
+ Message to be sent to a queue.
256
+
257
+ ```typescript
258
+ interface QueueMessage<T = unknown> {
259
+ /**
260
+ * Message payload.
261
+ */
262
+ body: T
263
+ /**
264
+ * Message ID (auto-generated if not provided).
265
+ */
266
+ id?: string
267
+ /**
268
+ * Delay in seconds before the message becomes visible.
269
+ */
270
+ delaySeconds?: number
271
+ /**
272
+ * Message attributes/headers.
273
+ */
274
+ attributes?: Record<string, string | number | boolean>
275
+ /**
276
+ * Message group ID (for FIFO queues).
277
+ */
278
+ groupId?: string
279
+ /**
280
+ * Deduplication ID (for FIFO queues).
281
+ */
282
+ deduplicationId?: string
283
+ }
284
+ ```
285
+
286
+ #### `QueueProvider`
287
+
288
+ Queue provider interface that all queue bond packages must implement.
289
+ Provides queue handle creation and optional queue management operations.
290
+
291
+ ```typescript
292
+ interface QueueProvider {
293
+ /**
294
+ * Gets or creates a queue by name.
295
+ */
296
+ queue(name: string): Queue
297
+ /**
298
+ * Lists all available queues.
299
+ */
300
+ listQueues?(): Promise<string[]>
301
+ /**
302
+ * Creates a new queue.
303
+ */
304
+ createQueue?(name: string, options?: QueueCreateOptions): Promise<Queue>
305
+ /**
306
+ * Deletes a queue.
307
+ */
308
+ deleteQueue?(name: string): Promise<void>
309
+ /**
310
+ * Closes all connections.
311
+ */
312
+ close?(): Promise<void>
313
+ }
314
+ ```
315
+
316
+ #### `ReceivedMessage`
317
+
318
+ Received message from a queue.
319
+
320
+ ```typescript
321
+ interface ReceivedMessage<T = unknown> {
322
+ /**
323
+ * Message ID.
324
+ */
325
+ id: string
326
+ /**
327
+ * Message payload.
328
+ */
329
+ body: T
330
+ /**
331
+ * Receipt handle for acknowledging the message.
332
+ */
333
+ receiptHandle: string
334
+ /**
335
+ * Message attributes/headers.
336
+ */
337
+ attributes?: Record<string, string | number | boolean>
338
+ /**
339
+ * Number of times this message has been received.
340
+ */
341
+ receiveCount?: number
342
+ /**
343
+ * Timestamp when the message was sent.
344
+ */
345
+ sentTimestamp?: Date
346
+ /**
347
+ * Acknowledges (deletes) the message from the queue.
348
+ */
349
+ ack(): Promise<void>
350
+ /**
351
+ * Rejects the message (returns it to the queue).
352
+ */
353
+ nack?(): Promise<void>
354
+ }
355
+ ```
356
+
357
+ #### `ReceiveOptions`
358
+
359
+ Options for receiving messages.
360
+
361
+ ```typescript
362
+ interface ReceiveOptions {
363
+ /**
364
+ * Maximum number of messages to receive.
365
+ */
366
+ maxMessages?: number
367
+ /**
368
+ * Visibility timeout in seconds.
369
+ */
370
+ visibilityTimeout?: number
371
+ /**
372
+ * Wait time in seconds for long polling.
373
+ */
374
+ waitTimeSeconds?: number
375
+ }
376
+ ```
377
+
378
+ ### Types
379
+
380
+ #### `MessageHandler`
381
+
382
+ Async callback invoked for each message received from a queue subscription.
383
+
384
+ ```typescript
385
+ type MessageHandler<T = unknown> = (message: ReceivedMessage<T>) => Promise<void>
386
+ ```
387
+
388
+ ### Functions
389
+
390
+ #### `createProvider(options)`
391
+
392
+ Creates an in-memory queue provider. Queues are created implicitly on first
393
+ access (like the Redis bond) or explicitly via `createQueue()` with FIFO,
394
+ visibility-timeout, retention, and dead-letter options. All state lives in
395
+ this process and is lost on restart.
396
+
397
+ ```typescript
398
+ function createProvider(options?: MemoryQueueOptions): QueueProvider
399
+ ```
400
+
401
+ - `options` — Optional delivery defaults (visibility timeout, delivery cap, nack redelivery delay, handler-failure redelivery delay). Everything defaults sensibly — no configuration is required.
402
+
403
+ **Returns:** A `QueueProvider` backed by in-process queues.
404
+
405
+ ### Constants
406
+
407
+ #### `provider`
408
+
409
+ Lazily-initialized in-memory queue provider proxy that creates the provider on first access.
410
+
411
+ ```typescript
412
+ const provider: QueueProvider
413
+ ```
414
+
415
+ ## Core Interface
416
+
417
+ Implements `@molecule/api-queue` interface.
418
+
419
+ ## Bond Wiring
420
+
421
+ Setup function to register this provider with the core interface:
422
+
423
+ ```typescript
424
+ import { setProvider } from '@molecule/api-queue'
425
+ import { provider } from '@molecule/api-queue-memory'
426
+
427
+ export function setupQueueMemory(): void {
428
+ setProvider(provider)
429
+ }
430
+ ```
431
+
432
+ ## Injection Notes
433
+
434
+ ### Requirements
435
+
436
+ Peer dependencies:
437
+
438
+ - `@molecule/api-bond` ^1.0.1
439
+ - `@molecule/api-queue` ^1.0.1
440
+
441
+ ### Runtime Dependencies
442
+
443
+ - `@molecule/api-bond`
444
+ - `@molecule/api-queue`
445
+
446
+ Single-process and DEV-ONLY. Messages live in this process's memory: there
447
+ is NO persistence (everything is lost on restart) and NO cross-instance
448
+ delivery, so it must not be used for multi-instance production — swap in
449
+ `@molecule/api-queue-redis`, `@molecule/api-queue-rabbitmq`, or
450
+ `@molecule/api-queue-sqs` for production workloads. Delivery is
451
+ at-least-once: a message whose visibility lease expires without `ack()` is
452
+ redelivered (with an incremented `receiveCount`), and a message delivered
453
+ more than `maxReceiveCount` times (default 3) is routed to the queue's
454
+ dead-letter queue when one was configured via `createQueue()` — otherwise
455
+ it is dropped with an error log. Message bodies are `structuredClone`d on
456
+ send and per delivery (like a real broker's serialization), so bodies must
457
+ be structured-cloneable and post-send mutations never leak to consumers.
458
+ `QueueCreateOptions.maxMessageSize` is not enforced (nothing is
459
+ serialized). `close()` clears all timers, resolves pending long-polls with
460
+ `[]`, and stops all delivery.
461
+
462
+ Two distinct redelivery delays (both `MemoryQueueOptions`, provider-wide):
463
+ an explicit `nack()` redelivers per `redeliveryDelaySeconds` (default `0`
464
+ — a deliberate caller decision, honored immediately), while an UNCAUGHT
465
+ `subscribe()` handler throw redelivers per
466
+ `handlerFailureRedeliveryDelaySeconds` (default `1` — an unplanned failure
467
+ gets a real retry window instead of burning all `maxReceiveCount` attempts
468
+ within milliseconds, mirroring the Redis bond's `attempts: 3, backoff:
469
+ { type: 'exponential', delay: 1000 }`).
470
+
471
+ ## E2E Tests
472
+
473
+ Integration checklist — exercise the REAL behavior end-to-end (drive the app
474
+ action that enqueues/consumes work in the live preview, no mocks), adapt each
475
+ item to this app's actual screens/flows, and check every box off one by one. A
476
+ box you can't check is an integration bug to fix — not a skip:
477
+
478
+ - [ ] The action that enqueues work responds fast — `send()` returns a message
479
+ id immediately and the request/response does NOT block on the job. The #1
480
+ trap: the executor awaits the heavy work inline (defeating the queue);
481
+ confirm the triggering UI action returns quickly and the work happens in the
482
+ background.
483
+ - [ ] The enqueued job actually RUNS — a `subscribe()` consumer (a
484
+ `MessageHandler`) is wired and running, so the message's real side effect
485
+ (email sent, file processed, notification delivered — whatever the app does)
486
+ actually appears in the UI/data. A message enqueued with no worker wired is
487
+ the silent failure.
488
+ - [ ] Payload round-trips intact — the `ReceivedMessage.body` the handler sees
489
+ equals the `QueueMessage.body` that was sent, with no dropped or renamed
490
+ fields.
491
+ - [ ] Failure is handled — a handler that throws is redelivered (up to
492
+ `QueueCreateOptions.deadLetterQueue.maxReceiveCount`, tracked via
493
+ `receiveCount`) or dead-lettered, never silently lost. Delivery is
494
+ at-least-once, so the handler is idempotent (dedupe on the job/record id) — a
495
+ redelivery must not double-charge or double-send.
496
+ - [ ] Ordering/concurrency is not assumed — the app does not rely on strict
497
+ FIFO (`QueueMessage.groupId`/`fifo`) or exactly-once delivery unless the
498
+ bonded provider actually guarantees it.
499
+ - [ ] Least-authority payloads — the `body` carries only the ids/refs the job
500
+ needs (never a secret or stale authority); the consumer re-loads and
501
+ re-scopes on the CURRENT data (owner id from `body`, re-checked server-side)
502
+ so one user's job cannot act on another user's resource.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-queue-memory",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "In-memory queue provider for molecule.dev.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,8 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "README.md"
21
22
  ],
22
23
  "keywords": [
23
24
  "molecule",
@@ -27,13 +28,13 @@
27
28
  ],
28
29
  "license": "Apache-2.0",
29
30
  "peerDependencies": {
30
- "@molecule/api-bond": "^1.0.0",
31
- "@molecule/api-queue": "^1.0.0"
31
+ "@molecule/api-bond": "^1.0.1",
32
+ "@molecule/api-queue": "^1.0.1"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "26.1.2",
35
36
  "typescript": "6.0.3",
36
- "vitest": "4.1.10"
37
+ "vitest": "4.1.11"
37
38
  },
38
39
  "repository": {
39
40
  "type": "git",