@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.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/esm/anabranch/index.d.ts +44 -0
- package/esm/anabranch/index.d.ts.map +1 -0
- package/esm/anabranch/index.js +41 -0
- package/esm/anabranch/streams/channel.d.ts +15 -0
- package/esm/anabranch/streams/channel.d.ts.map +1 -0
- package/esm/anabranch/streams/channel.js +122 -0
- package/esm/anabranch/streams/source.d.ts +75 -0
- package/esm/anabranch/streams/source.d.ts.map +1 -0
- package/esm/anabranch/streams/source.js +77 -0
- package/esm/anabranch/streams/stream.d.ts +431 -0
- package/esm/anabranch/streams/stream.d.ts.map +1 -0
- package/esm/anabranch/streams/stream.js +627 -0
- package/esm/anabranch/streams/task.d.ts +117 -0
- package/esm/anabranch/streams/task.d.ts.map +1 -0
- package/esm/anabranch/streams/task.js +419 -0
- package/esm/anabranch/streams/util.d.ts +33 -0
- package/esm/anabranch/streams/util.d.ts.map +1 -0
- package/esm/anabranch/streams/util.js +18 -0
- package/esm/package.json +3 -0
- package/esm/queue/adapter.d.ts +98 -0
- package/esm/queue/adapter.d.ts.map +1 -0
- package/esm/queue/adapter.js +1 -0
- package/esm/queue/errors.d.ts +45 -0
- package/esm/queue/errors.d.ts.map +1 -0
- package/esm/queue/errors.js +113 -0
- package/esm/queue/in-memory.d.ts +61 -0
- package/esm/queue/in-memory.d.ts.map +1 -0
- package/esm/queue/in-memory.js +291 -0
- package/esm/queue/index.d.ts +79 -0
- package/esm/queue/index.d.ts.map +1 -0
- package/esm/queue/index.js +74 -0
- package/esm/queue/queue.d.ts +166 -0
- package/esm/queue/queue.d.ts.map +1 -0
- package/esm/queue/queue.js +284 -0
- 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
|
+
}
|