@anabranch/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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/esm/anabranch/index.d.ts +44 -0
  4. package/esm/anabranch/index.d.ts.map +1 -0
  5. package/esm/anabranch/index.js +41 -0
  6. package/esm/anabranch/streams/channel.d.ts +15 -0
  7. package/esm/anabranch/streams/channel.d.ts.map +1 -0
  8. package/esm/anabranch/streams/channel.js +122 -0
  9. package/esm/anabranch/streams/source.d.ts +75 -0
  10. package/esm/anabranch/streams/source.d.ts.map +1 -0
  11. package/esm/anabranch/streams/source.js +77 -0
  12. package/esm/anabranch/streams/stream.d.ts +431 -0
  13. package/esm/anabranch/streams/stream.d.ts.map +1 -0
  14. package/esm/anabranch/streams/stream.js +627 -0
  15. package/esm/anabranch/streams/task.d.ts +117 -0
  16. package/esm/anabranch/streams/task.d.ts.map +1 -0
  17. package/esm/anabranch/streams/task.js +419 -0
  18. package/esm/anabranch/streams/util.d.ts +33 -0
  19. package/esm/anabranch/streams/util.d.ts.map +1 -0
  20. package/esm/anabranch/streams/util.js +18 -0
  21. package/esm/package.json +3 -0
  22. package/esm/queue/adapter.d.ts +98 -0
  23. package/esm/queue/adapter.d.ts.map +1 -0
  24. package/esm/queue/adapter.js +1 -0
  25. package/esm/queue/errors.d.ts +45 -0
  26. package/esm/queue/errors.d.ts.map +1 -0
  27. package/esm/queue/errors.js +113 -0
  28. package/esm/queue/in-memory.d.ts +61 -0
  29. package/esm/queue/in-memory.d.ts.map +1 -0
  30. package/esm/queue/in-memory.js +291 -0
  31. package/esm/queue/index.d.ts +79 -0
  32. package/esm/queue/index.d.ts.map +1 -0
  33. package/esm/queue/index.js +74 -0
  34. package/esm/queue/queue.d.ts +166 -0
  35. package/esm/queue/queue.d.ts.map +1 -0
  36. package/esm/queue/queue.js +284 -0
  37. package/package.json +24 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @anabranch/queue
3
+ *
4
+ * Queue primitives with Task/Stream semantics for error-tolerant message processing.
5
+ * Integrates with anabranch's {@linkcode Task}, {@linkcode Stream}, {@linkcode Source}, and
6
+ * {@linkcode Channel} types for composable error handling and concurrent processing.
7
+ *
8
+ * ## Adapters vs Connectors
9
+ *
10
+ * A **QueueConnector** produces connected **QueueAdapter** instances. Use connectors for
11
+ * production code to properly manage connection lifecycles:
12
+ *
13
+ * - **Connector**: Manages connection pool/lifecycle, produces adapters
14
+ * - **Adapter**: Low-level send/receive/ack/nack interface
15
+ * - **Queue**: Wrapper providing Task/Stream methods over an adapter
16
+ *
17
+ * ## Core Types
18
+ *
19
+ * - {@link QueueConnector} - Interface for connection factories
20
+ * - {@link QueueAdapter} - Low-level queue operations interface
21
+ * - {@link Queue} - High-level wrapper with Task/Stream methods
22
+ * - {@link QueueMessage} - Message envelope with id, data, attempt count
23
+ *
24
+ * ## Error Types
25
+ *
26
+ * All errors are typed for catchable handling:
27
+ * - {@link QueueConnectionFailed} - Connection establishment failed
28
+ * - {@link QueueSendFailed} - Send operation failed
29
+ * - {@link QueueReceiveFailed} - Receive operation failed
30
+ * - {@link QueueAckFailed} - Acknowledgment failed
31
+ *
32
+ * @example Basic send with Task semantics
33
+ * ```ts
34
+ * import { Queue, createInMemory } from "@anabranch/queue";
35
+ *
36
+ * const messageId = await Queue.withConnection(createInMemory(), (queue) =>
37
+ * queue.send("notifications", { userId: 123, type: "welcome" })
38
+ * ).run();
39
+ * ```
40
+ *
41
+ * @example Stream messages with concurrent processing and error collection
42
+ * ```ts
43
+ * const { successes, errors } = await Queue.withConnection(createInMemory(), (queue) =>
44
+ * queue.stream("notifications")
45
+ * .withConcurrency(5)
46
+ * .map(async (msg) => await sendEmail(msg.data))
47
+ * .tapErr((err) => logError(err))
48
+ * .partition()
49
+ * ).run();
50
+ * ```
51
+ *
52
+ * @example Delayed messages with retry handling
53
+ * ```ts
54
+ * import { Queue, createInMemory } from "@anabranch/queue";
55
+ *
56
+ * await Queue.withConnection(createInMemory(), (queue) =>
57
+ * queue.send("notifications", reminder, { delayMs: 30_000 })
58
+ * ).run();
59
+ *
60
+ * const processed = await Queue.withConnection(createInMemory(), (queue) =>
61
+ * queue.stream("notifications")
62
+ * .map(async (msg) => await processWithRetry(msg.data))
63
+ * .filter((r) => r.type === "success")
64
+ * .map((r) => r.value)
65
+ * .collect()
66
+ * ).run();
67
+ * ```
68
+ *
69
+ * @module
70
+ */
71
+ export { Queue } from "./queue.js";
72
+ export type { QueueMessage } from "./adapter.js";
73
+ export type { NackOptions, QueueAdapter, QueueConnector, QueueOptions, SendOptions, } from "./adapter.js";
74
+ export * from "./errors.js";
75
+ export { createInMemory } from "./in-memory.js";
76
+ export type { InMemoryConnector } from "./in-memory.js";
77
+ export { Task } from "../anabranch/index.js";
78
+ export type { Source, Stream } from "../anabranch/index.js";
79
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/queue/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @anabranch/queue
3
+ *
4
+ * Queue primitives with Task/Stream semantics for error-tolerant message processing.
5
+ * Integrates with anabranch's {@linkcode Task}, {@linkcode Stream}, {@linkcode Source}, and
6
+ * {@linkcode Channel} types for composable error handling and concurrent processing.
7
+ *
8
+ * ## Adapters vs Connectors
9
+ *
10
+ * A **QueueConnector** produces connected **QueueAdapter** instances. Use connectors for
11
+ * production code to properly manage connection lifecycles:
12
+ *
13
+ * - **Connector**: Manages connection pool/lifecycle, produces adapters
14
+ * - **Adapter**: Low-level send/receive/ack/nack interface
15
+ * - **Queue**: Wrapper providing Task/Stream methods over an adapter
16
+ *
17
+ * ## Core Types
18
+ *
19
+ * - {@link QueueConnector} - Interface for connection factories
20
+ * - {@link QueueAdapter} - Low-level queue operations interface
21
+ * - {@link Queue} - High-level wrapper with Task/Stream methods
22
+ * - {@link QueueMessage} - Message envelope with id, data, attempt count
23
+ *
24
+ * ## Error Types
25
+ *
26
+ * All errors are typed for catchable handling:
27
+ * - {@link QueueConnectionFailed} - Connection establishment failed
28
+ * - {@link QueueSendFailed} - Send operation failed
29
+ * - {@link QueueReceiveFailed} - Receive operation failed
30
+ * - {@link QueueAckFailed} - Acknowledgment failed
31
+ *
32
+ * @example Basic send with Task semantics
33
+ * ```ts
34
+ * import { Queue, createInMemory } from "@anabranch/queue";
35
+ *
36
+ * const messageId = await Queue.withConnection(createInMemory(), (queue) =>
37
+ * queue.send("notifications", { userId: 123, type: "welcome" })
38
+ * ).run();
39
+ * ```
40
+ *
41
+ * @example Stream messages with concurrent processing and error collection
42
+ * ```ts
43
+ * const { successes, errors } = await Queue.withConnection(createInMemory(), (queue) =>
44
+ * queue.stream("notifications")
45
+ * .withConcurrency(5)
46
+ * .map(async (msg) => await sendEmail(msg.data))
47
+ * .tapErr((err) => logError(err))
48
+ * .partition()
49
+ * ).run();
50
+ * ```
51
+ *
52
+ * @example Delayed messages with retry handling
53
+ * ```ts
54
+ * import { Queue, createInMemory } from "@anabranch/queue";
55
+ *
56
+ * await Queue.withConnection(createInMemory(), (queue) =>
57
+ * queue.send("notifications", reminder, { delayMs: 30_000 })
58
+ * ).run();
59
+ *
60
+ * const processed = await Queue.withConnection(createInMemory(), (queue) =>
61
+ * queue.stream("notifications")
62
+ * .map(async (msg) => await processWithRetry(msg.data))
63
+ * .filter((r) => r.type === "success")
64
+ * .map((r) => r.value)
65
+ * .collect()
66
+ * ).run();
67
+ * ```
68
+ *
69
+ * @module
70
+ */
71
+ export { Queue } from "./queue.js";
72
+ export * from "./errors.js";
73
+ export { createInMemory } from "./in-memory.js";
74
+ export { Task } from "../anabranch/index.js";
@@ -0,0 +1,166 @@
1
+ import { Source, Task } from "../anabranch/index.js";
2
+ import type { QueueAdapter, QueueConnector, QueueMessage, SendOptions } from "./adapter.js";
3
+ import { QueueAckFailed, QueueCloseFailed, QueueConnectionFailed, QueueMaxAttemptsExceeded, QueueReceiveFailed, QueueSendFailed } from "./errors.js";
4
+ /**
5
+ * Queue wrapper with Task/Stream semantics for error-tolerant message processing.
6
+ *
7
+ * @example Basic usage
8
+ * ```ts
9
+ * import { Queue, createInMemory } from "@anabranch/queue";
10
+ *
11
+ * const connector = createInMemory();
12
+ * const queue = await Queue.connect(connector).run();
13
+ *
14
+ * await queue.send("notifications", { userId: 123 }).run();
15
+ * await queue.ack("notifications", msg.id).run();
16
+ *
17
+ * await queue.close().run();
18
+ * ```
19
+ *
20
+ * @example Stream with concurrent processing
21
+ * ```ts
22
+ * const { successes, errors } = await queue.stream("notifications")
23
+ * .withConcurrency(5)
24
+ * .map(async (msg) => await sendEmail(msg.data))
25
+ * .tapErr((err) => logError(err))
26
+ * .partition();
27
+ * ```
28
+ *
29
+ * @example Continuous streaming
30
+ * ```ts
31
+ * const ac = new AbortController();
32
+ *
33
+ * queue.continuousStream("notifications", { signal: ac.signal })
34
+ * .withConcurrency(5)
35
+ * .tap(async (msg) => await processMessage(msg))
36
+ * .collect();
37
+ *
38
+ * ac.abort();
39
+ * ```
40
+ */
41
+ export declare class Queue {
42
+ private readonly adapter;
43
+ constructor(adapter: QueueAdapter);
44
+ /**
45
+ * Connect to a queue via a connector.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * const queue = await Queue.connect(createInMemory()).run();
50
+ * ```
51
+ */
52
+ static connect(connector: QueueConnector): Task<Queue, QueueConnectionFailed>;
53
+ /**
54
+ * Release the connection back to its source (e.g., pool).
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * await queue.close().run();
59
+ * ```
60
+ */
61
+ close(): Task<void, QueueCloseFailed>;
62
+ /**
63
+ * Send a message to a queue.
64
+ *
65
+ * @example Send with delay
66
+ * ```ts
67
+ * const id = await queue.send("notifications", payload, { delayMs: 30_000 }).run();
68
+ * ```
69
+ */
70
+ send<T>(queue: string, data: T, options?: SendOptions): Task<string, QueueSendFailed>;
71
+ /**
72
+ * Send multiple messages to a queue in batch.
73
+ *
74
+ * @example Batch send
75
+ * ```ts
76
+ * const ids = await queue.sendBatch("notifications", [
77
+ * { to: "user1@example.com", subject: "Welcome!" },
78
+ * { to: "user2@example.com", subject: "Welcome!" },
79
+ * ]).run();
80
+ * ```
81
+ *
82
+ * @example Parallel batch send (for adapters that support it)
83
+ * ```ts
84
+ * const ids = await queue.sendBatch("notifications", items, { parallel: true }).run();
85
+ * ```
86
+ */
87
+ sendBatch<T>(queue: string, data: T[], options?: SendOptions & {
88
+ parallel?: boolean;
89
+ }): Task<string[], QueueSendFailed>;
90
+ /**
91
+ * Stream messages from a queue for memory-efficient concurrent processing.
92
+ *
93
+ * Messages are delivered one at a time but can be processed concurrently
94
+ * using `withConcurrency()`. Errors are collected alongside successes,
95
+ * allowing the stream to continue processing while you decide how to handle
96
+ * failures later.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * const { successes, errors } = await queue.stream("notifications")
101
+ * .withConcurrency(10)
102
+ * .map(async (msg) => await sendNotification(msg.data))
103
+ * .tapErr((err) => console.error("Failed:", err))
104
+ * .partition();
105
+ * ```
106
+ */
107
+ stream<T>(queue: string, options?: {
108
+ count?: number;
109
+ concurrency?: number;
110
+ }): Source<QueueMessage<T>, QueueReceiveFailed>;
111
+ /**
112
+ * Continuous stream that polls for messages until stopped.
113
+ *
114
+ * Uses broker-native subscribe if available (StreamAdapter), otherwise
115
+ * falls back to polling-based implementation. Errors from the adapter
116
+ * are emitted as error results, allowing pipeline-style error handling
117
+ * with .recover(), .tapErr(), etc. Use an AbortSignal to stop.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const ac = new AbortController();
122
+ *
123
+ * queue.continuousStream("notifications", { signal: ac.signal, count: 5 })
124
+ * .withConcurrency(10)
125
+ * .tap(async (msg) => await processMessage(msg))
126
+ * .tapErr((err) => console.error("Receive failed:", err.message))
127
+ * .collect();
128
+ *
129
+ * ac.abort();
130
+ * ```
131
+ */
132
+ continuousStream<T>(queue: string, options?: {
133
+ count?: number;
134
+ signal?: AbortSignal;
135
+ }): Source<QueueMessage<T>, QueueReceiveFailed>;
136
+ /**
137
+ * Acknowledge one or more messages as successfully processed.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * await queue.ack("notifications", msg1.id, msg2.id, msg3.id).run();
142
+ * ```
143
+ */
144
+ ack(queue: string, ...ids: string[]): Task<void, QueueAckFailed>;
145
+ /**
146
+ * Negative acknowledgment - indicates processing failure.
147
+ *
148
+ * @example Requeue with delay
149
+ * ```ts
150
+ * await queue.nack("notifications", msg.id, { requeue: true, delay: 5_000 }).run();
151
+ * ```
152
+ *
153
+ * @example Route to dead letter queue
154
+ * ```ts
155
+ * await queue.nack("notifications", msg.id, { deadLetter: true }).run();
156
+ * ```
157
+ */
158
+ nack(queue: string, id: string, options?: {
159
+ requeue?: boolean;
160
+ delay?: number;
161
+ deadLetter?: boolean;
162
+ }): Task<void, QueueAckFailed | QueueMaxAttemptsExceeded>;
163
+ }
164
+ /** Re-export QueueMessage for convenience */
165
+ export type { QueueMessage } from "./adapter.js";
166
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/queue/queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,WAAW,EAEZ,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,EACxB,kBAAkB,EAClB,eAAe,EAChB,MAAM,aAAa,CAAC;AAiBrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBAAa,KAAK;IACJ,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY;IAElD;;;;;;;OAOG;IACH,MAAM,CAAC,OAAO,CACZ,SAAS,EAAE,cAAc,GACxB,IAAI,CAAC,KAAK,EAAE,qBAAqB,CAAC;IAarC;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAarC;;;;;;;OAOG;IACH,IAAI,CAAC,CAAC,EACJ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,WAAW,GACpB,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC;IAchC;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,CAAC,EACT,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,CAAC,EAAE,EACT,OAAO,CAAC,EAAE,WAAW,GAAG;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAC7C,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC;IA0BlC;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,CAAC,EACN,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GACjD,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC;IAqB9C;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,gBAAgB,CAAC,CAAC,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjD,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC;IAmD9C;;;;;;;OAOG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC;IAgBhE;;;;;;;;;;;;OAYG;IACH,IAAI,CACF,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,UAAU,CAAC,EAAE,OAAO,CAAC;KACtB,GACA,IAAI,CAAC,IAAI,EAAE,cAAc,GAAG,wBAAwB,CAAC;CAczD;AAED,6CAA6C;AAC7C,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,284 @@
1
+ import { Source, Task } from "../anabranch/index.js";
2
+ import { QueueAckFailed, QueueCloseFailed, QueueConnectionFailed, QueueReceiveFailed, QueueSendFailed, } from "./errors.js";
3
+ async function sleep(ms, signal) {
4
+ if (signal?.aborted)
5
+ return;
6
+ await new Promise((resolve) => {
7
+ const timerId = setTimeout(resolve, ms);
8
+ signal?.addEventListener("abort", () => {
9
+ clearTimeout(timerId);
10
+ resolve();
11
+ }, { once: true });
12
+ });
13
+ }
14
+ /**
15
+ * Queue wrapper with Task/Stream semantics for error-tolerant message processing.
16
+ *
17
+ * @example Basic usage
18
+ * ```ts
19
+ * import { Queue, createInMemory } from "@anabranch/queue";
20
+ *
21
+ * const connector = createInMemory();
22
+ * const queue = await Queue.connect(connector).run();
23
+ *
24
+ * await queue.send("notifications", { userId: 123 }).run();
25
+ * await queue.ack("notifications", msg.id).run();
26
+ *
27
+ * await queue.close().run();
28
+ * ```
29
+ *
30
+ * @example Stream with concurrent processing
31
+ * ```ts
32
+ * const { successes, errors } = await queue.stream("notifications")
33
+ * .withConcurrency(5)
34
+ * .map(async (msg) => await sendEmail(msg.data))
35
+ * .tapErr((err) => logError(err))
36
+ * .partition();
37
+ * ```
38
+ *
39
+ * @example Continuous streaming
40
+ * ```ts
41
+ * const ac = new AbortController();
42
+ *
43
+ * queue.continuousStream("notifications", { signal: ac.signal })
44
+ * .withConcurrency(5)
45
+ * .tap(async (msg) => await processMessage(msg))
46
+ * .collect();
47
+ *
48
+ * ac.abort();
49
+ * ```
50
+ */
51
+ export class Queue {
52
+ constructor(adapter) {
53
+ Object.defineProperty(this, "adapter", {
54
+ enumerable: true,
55
+ configurable: true,
56
+ writable: true,
57
+ value: adapter
58
+ });
59
+ }
60
+ /**
61
+ * Connect to a queue via a connector.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const queue = await Queue.connect(createInMemory()).run();
66
+ * ```
67
+ */
68
+ static connect(connector) {
69
+ return Task.of(async () => {
70
+ try {
71
+ return new Queue(await connector.connect());
72
+ }
73
+ catch (error) {
74
+ throw new QueueConnectionFailed(error instanceof Error ? error.message : String(error), error);
75
+ }
76
+ });
77
+ }
78
+ /**
79
+ * Release the connection back to its source (e.g., pool).
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * await queue.close().run();
84
+ * ```
85
+ */
86
+ close() {
87
+ return Task.of(async () => {
88
+ try {
89
+ await this.adapter.close();
90
+ }
91
+ catch (error) {
92
+ throw new QueueCloseFailed(error instanceof Error ? error.message : String(error), error);
93
+ }
94
+ });
95
+ }
96
+ /**
97
+ * Send a message to a queue.
98
+ *
99
+ * @example Send with delay
100
+ * ```ts
101
+ * const id = await queue.send("notifications", payload, { delayMs: 30_000 }).run();
102
+ * ```
103
+ */
104
+ send(queue, data, options) {
105
+ return Task.of(async () => {
106
+ try {
107
+ return await this.adapter.send(queue, data, options);
108
+ }
109
+ catch (error) {
110
+ throw new QueueSendFailed(error instanceof Error ? error.message : String(error), queue, error);
111
+ }
112
+ });
113
+ }
114
+ /**
115
+ * Send multiple messages to a queue in batch.
116
+ *
117
+ * @example Batch send
118
+ * ```ts
119
+ * const ids = await queue.sendBatch("notifications", [
120
+ * { to: "user1@example.com", subject: "Welcome!" },
121
+ * { to: "user2@example.com", subject: "Welcome!" },
122
+ * ]).run();
123
+ * ```
124
+ *
125
+ * @example Parallel batch send (for adapters that support it)
126
+ * ```ts
127
+ * const ids = await queue.sendBatch("notifications", items, { parallel: true }).run();
128
+ * ```
129
+ */
130
+ sendBatch(queue, data, options) {
131
+ return Task.of(async () => {
132
+ const ids = [];
133
+ if (options?.parallel) {
134
+ const promises = data.map((item) => this.adapter.send(queue, item, options).catch((e) => {
135
+ throw new QueueSendFailed(e instanceof Error ? e.message : String(e), queue, e);
136
+ }));
137
+ const results = await Promise.all(promises);
138
+ return results;
139
+ }
140
+ for (const item of data) {
141
+ const id = await this.adapter.send(queue, item, options);
142
+ ids.push(id);
143
+ }
144
+ return ids;
145
+ });
146
+ }
147
+ /**
148
+ * Stream messages from a queue for memory-efficient concurrent processing.
149
+ *
150
+ * Messages are delivered one at a time but can be processed concurrently
151
+ * using `withConcurrency()`. Errors are collected alongside successes,
152
+ * allowing the stream to continue processing while you decide how to handle
153
+ * failures later.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * const { successes, errors } = await queue.stream("notifications")
158
+ * .withConcurrency(10)
159
+ * .map(async (msg) => await sendNotification(msg.data))
160
+ * .tapErr((err) => console.error("Failed:", err))
161
+ * .partition();
162
+ * ```
163
+ */
164
+ stream(queue, options) {
165
+ const adapter = this.adapter;
166
+ const count = options?.count ?? 10;
167
+ return Source.from(async function* () {
168
+ try {
169
+ const messages = await adapter.receive(queue, count);
170
+ for (const msg of messages) {
171
+ yield msg;
172
+ }
173
+ }
174
+ catch (error) {
175
+ throw new QueueReceiveFailed(error instanceof Error ? error.message : String(error), queue, error);
176
+ }
177
+ }).withConcurrency(options?.concurrency ?? Infinity);
178
+ }
179
+ /**
180
+ * Continuous stream that polls for messages until stopped.
181
+ *
182
+ * Uses broker-native subscribe if available (StreamAdapter), otherwise
183
+ * falls back to polling-based implementation. Errors from the adapter
184
+ * are emitted as error results, allowing pipeline-style error handling
185
+ * with .recover(), .tapErr(), etc. Use an AbortSignal to stop.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * const ac = new AbortController();
190
+ *
191
+ * queue.continuousStream("notifications", { signal: ac.signal, count: 5 })
192
+ * .withConcurrency(10)
193
+ * .tap(async (msg) => await processMessage(msg))
194
+ * .tapErr((err) => console.error("Receive failed:", err.message))
195
+ * .collect();
196
+ *
197
+ * ac.abort();
198
+ * ```
199
+ */
200
+ continuousStream(queue, options) {
201
+ const count = options?.count ?? 10;
202
+ const signal = options?.signal;
203
+ const adapter = this.adapter;
204
+ const isStreamAdapter = "subscribe" in adapter;
205
+ return Source.fromResults(async function* () {
206
+ if (isStreamAdapter) {
207
+ const streamAdapter = adapter;
208
+ const iterable = streamAdapter.subscribe(queue, { signal });
209
+ for await (const msg of iterable) {
210
+ if (signal?.aborted)
211
+ return;
212
+ yield { type: "success", value: msg };
213
+ }
214
+ return;
215
+ }
216
+ const baseDelay = 50;
217
+ let currentDelay = baseDelay;
218
+ while (!signal?.aborted) {
219
+ try {
220
+ const messages = await adapter.receive(queue, count);
221
+ currentDelay = baseDelay;
222
+ for (const msg of messages) {
223
+ if (signal?.aborted)
224
+ return;
225
+ yield { type: "success", value: msg };
226
+ }
227
+ if (!signal?.aborted && messages.length === 0) {
228
+ await sleep(currentDelay, signal);
229
+ currentDelay = Math.min(currentDelay * 2, 5000);
230
+ }
231
+ }
232
+ catch (error) {
233
+ if (signal?.aborted)
234
+ return;
235
+ const queueError = new QueueReceiveFailed(error instanceof Error ? error.message : String(error), queue, error);
236
+ yield { type: "error", error: queueError };
237
+ }
238
+ }
239
+ });
240
+ }
241
+ /**
242
+ * Acknowledge one or more messages as successfully processed.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * await queue.ack("notifications", msg1.id, msg2.id, msg3.id).run();
247
+ * ```
248
+ */
249
+ ack(queue, ...ids) {
250
+ return Task.of(async () => {
251
+ if (ids.length === 0)
252
+ return;
253
+ try {
254
+ await this.adapter.ack(queue, ...ids);
255
+ }
256
+ catch (error) {
257
+ throw new QueueAckFailed(error instanceof Error ? error.message : String(error), queue, ids[0], error);
258
+ }
259
+ });
260
+ }
261
+ /**
262
+ * Negative acknowledgment - indicates processing failure.
263
+ *
264
+ * @example Requeue with delay
265
+ * ```ts
266
+ * await queue.nack("notifications", msg.id, { requeue: true, delay: 5_000 }).run();
267
+ * ```
268
+ *
269
+ * @example Route to dead letter queue
270
+ * ```ts
271
+ * await queue.nack("notifications", msg.id, { deadLetter: true }).run();
272
+ * ```
273
+ */
274
+ nack(queue, id, options) {
275
+ return Task.of(async () => {
276
+ try {
277
+ await this.adapter.nack(queue, id, options);
278
+ }
279
+ catch (error) {
280
+ throw new QueueAckFailed(error instanceof Error ? error.message : String(error), queue, id, error);
281
+ }
282
+ });
283
+ }
284
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@anabranch/queue",
3
+ "version": "0.1.0",
4
+ "description": "Message queue with dead letter queue and visibility timeout support",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/frodi-karlsson/anabranch.git"
8
+ },
9
+ "license": "MIT",
10
+ "bugs": {
11
+ "url": "https://github.com/frodi-karlsson/anabranch/issues"
12
+ },
13
+ "module": "./esm/queue/index.js",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./esm/queue/index.js"
17
+ }
18
+ },
19
+ "scripts": {},
20
+ "dependencies": {
21
+ "anabranch": "^0"
22
+ },
23
+ "_generatedBy": "dnt@dev"
24
+ }