@dudousxd/nestjs-durable-transport-sqs 0.1.0 → 0.2.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/dist/index.cjs +218 -0
- package/dist/index.cjs.map +1 -0
- package/dist/{sqs-transport.d.ts → index.d.cts} +8 -6
- package/dist/index.d.ts +95 -2
- package/dist/index.js +190 -15
- package/dist/index.js.map +1 -1
- package/package.json +21 -6
- package/dist/index.d.ts.map +0 -1
- package/dist/sqs-transport.d.ts.map +0 -1
- package/dist/sqs-transport.js +0 -191
- package/dist/sqs-transport.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Davide Carvalho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
SqsTransport: () => SqsTransport
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/sqs-transport.ts
|
|
29
|
+
var import_client_sqs = require("@aws-sdk/client-sqs");
|
|
30
|
+
var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
|
|
31
|
+
var SqsTransport = class {
|
|
32
|
+
static {
|
|
33
|
+
__name(this, "SqsTransport");
|
|
34
|
+
}
|
|
35
|
+
client;
|
|
36
|
+
ownsClient;
|
|
37
|
+
group;
|
|
38
|
+
prefix;
|
|
39
|
+
autoCreate;
|
|
40
|
+
marker;
|
|
41
|
+
waitTimeSeconds;
|
|
42
|
+
visibilityTimeoutSec;
|
|
43
|
+
taskQueueUrlResolver;
|
|
44
|
+
resultsQueueUrlOption;
|
|
45
|
+
handlers = /* @__PURE__ */ new Map();
|
|
46
|
+
urlCache = /* @__PURE__ */ new Map();
|
|
47
|
+
running = true;
|
|
48
|
+
abort = new AbortController();
|
|
49
|
+
taskLoop;
|
|
50
|
+
resultLoop;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.client = options.client ?? new import_client_sqs.SQSClient(options.clientConfig ?? {});
|
|
53
|
+
this.ownsClient = !options.client;
|
|
54
|
+
this.group = options.group;
|
|
55
|
+
this.prefix = options.prefix ?? "durable";
|
|
56
|
+
this.autoCreate = options.autoCreate ?? false;
|
|
57
|
+
this.marker = options.marker;
|
|
58
|
+
this.waitTimeSeconds = options.waitTimeSeconds ?? 20;
|
|
59
|
+
this.visibilityTimeoutSec = options.visibilityTimeoutSec ?? 60;
|
|
60
|
+
this.taskQueueUrlResolver = options.taskQueueUrl;
|
|
61
|
+
this.resultsQueueUrlOption = options.resultsQueueUrl;
|
|
62
|
+
}
|
|
63
|
+
// ── queue resolution ───────────────────────────────────────────────────────────────────────
|
|
64
|
+
async resolveByName(name) {
|
|
65
|
+
const cached = this.urlCache.get(name);
|
|
66
|
+
if (cached) return cached;
|
|
67
|
+
let url;
|
|
68
|
+
try {
|
|
69
|
+
const out = await this.client.send(new import_client_sqs.GetQueueUrlCommand({
|
|
70
|
+
QueueName: name
|
|
71
|
+
}));
|
|
72
|
+
url = out.QueueUrl;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (this.autoCreate) {
|
|
75
|
+
const created = await this.client.send(new import_client_sqs.CreateQueueCommand({
|
|
76
|
+
QueueName: name
|
|
77
|
+
}));
|
|
78
|
+
url = created.QueueUrl;
|
|
79
|
+
} else {
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.urlCache.set(name, url);
|
|
84
|
+
return url;
|
|
85
|
+
}
|
|
86
|
+
async tasksUrl(group) {
|
|
87
|
+
if (this.taskQueueUrlResolver) return this.taskQueueUrlResolver(group);
|
|
88
|
+
return this.resolveByName(`${this.prefix}-tasks-${group}`);
|
|
89
|
+
}
|
|
90
|
+
async resultsUrl() {
|
|
91
|
+
const opt = this.resultsQueueUrlOption;
|
|
92
|
+
if (typeof opt === "string") return opt;
|
|
93
|
+
if (typeof opt === "function") return opt();
|
|
94
|
+
return this.resolveByName(`${this.prefix}-results`);
|
|
95
|
+
}
|
|
96
|
+
// ── engine → worker ────────────────────────────────────────────────────────────────────────
|
|
97
|
+
async dispatch(task) {
|
|
98
|
+
const QueueUrl = await this.tasksUrl(task.group);
|
|
99
|
+
await this.client.send(new import_client_sqs.SendMessageCommand({
|
|
100
|
+
QueueUrl,
|
|
101
|
+
MessageBody: JSON.stringify(task),
|
|
102
|
+
MessageAttributes: this.marker ? {
|
|
103
|
+
[this.marker]: {
|
|
104
|
+
DataType: "String",
|
|
105
|
+
StringValue: "1"
|
|
106
|
+
}
|
|
107
|
+
} : void 0
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
// ── worker → engine ────────────────────────────────────────────────────────────────────────
|
|
111
|
+
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
112
|
+
handle(name, fn) {
|
|
113
|
+
if (!this.group) throw new Error("SqsTransport needs a `group` to register handlers");
|
|
114
|
+
this.handlers.set(name, fn);
|
|
115
|
+
if (!this.taskLoop) this.taskLoop = this.pollTasks(this.group);
|
|
116
|
+
}
|
|
117
|
+
onResult(handler) {
|
|
118
|
+
if (!this.resultLoop) this.resultLoop = this.pollResults(handler);
|
|
119
|
+
}
|
|
120
|
+
onHeartbeat(_handler) {
|
|
121
|
+
}
|
|
122
|
+
// ── polling loops ──────────────────────────────────────────────────────────────────────────
|
|
123
|
+
async pollTasks(group) {
|
|
124
|
+
const QueueUrl = await this.tasksUrl(group);
|
|
125
|
+
while (this.running) {
|
|
126
|
+
const messages = await this.receive(QueueUrl, this.visibilityTimeoutSec);
|
|
127
|
+
for (const msg of messages) {
|
|
128
|
+
if (this.shouldSkip(msg)) {
|
|
129
|
+
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const task = JSON.parse(msg.Body ?? "{}");
|
|
133
|
+
const result = await (0, import_nestjs_durable_core.runStepHandler)(task, this.handlers.get(task.name));
|
|
134
|
+
await this.client.send(new import_client_sqs.SendMessageCommand({
|
|
135
|
+
QueueUrl: await this.resultsUrl(),
|
|
136
|
+
MessageBody: JSON.stringify(result),
|
|
137
|
+
MessageAttributes: this.marker ? {
|
|
138
|
+
[this.marker]: {
|
|
139
|
+
DataType: "String",
|
|
140
|
+
StringValue: "1"
|
|
141
|
+
}
|
|
142
|
+
} : void 0
|
|
143
|
+
}));
|
|
144
|
+
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async pollResults(handler) {
|
|
149
|
+
const QueueUrl = await this.resultsUrl();
|
|
150
|
+
while (this.running) {
|
|
151
|
+
const messages = await this.receive(QueueUrl, 30);
|
|
152
|
+
for (const msg of messages) {
|
|
153
|
+
if (this.shouldSkip(msg)) {
|
|
154
|
+
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
await handler(JSON.parse(msg.Body ?? "{}"));
|
|
158
|
+
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** One long-poll receive. Swallows the abort thrown on `close()`; logs and backs off otherwise. */
|
|
163
|
+
async receive(QueueUrl, visibility) {
|
|
164
|
+
try {
|
|
165
|
+
const out = await this.client.send(new import_client_sqs.ReceiveMessageCommand({
|
|
166
|
+
QueueUrl,
|
|
167
|
+
MaxNumberOfMessages: 10,
|
|
168
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
169
|
+
VisibilityTimeout: visibility,
|
|
170
|
+
MessageAttributeNames: this.marker ? [
|
|
171
|
+
"All"
|
|
172
|
+
] : void 0
|
|
173
|
+
}), {
|
|
174
|
+
abortSignal: this.abort.signal
|
|
175
|
+
});
|
|
176
|
+
return out.Messages ?? [];
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (!this.running) return [];
|
|
179
|
+
console.error("[SqsTransport] receive failed", err);
|
|
180
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
shouldSkip(msg) {
|
|
185
|
+
return !!this.marker && !msg.MessageAttributes?.[this.marker];
|
|
186
|
+
}
|
|
187
|
+
async del(QueueUrl, ReceiptHandle) {
|
|
188
|
+
if (!ReceiptHandle) return;
|
|
189
|
+
await this.client.send(new import_client_sqs.DeleteMessageCommand({
|
|
190
|
+
QueueUrl,
|
|
191
|
+
ReceiptHandle
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
async release(QueueUrl, ReceiptHandle) {
|
|
195
|
+
if (!ReceiptHandle) return;
|
|
196
|
+
await this.client.send(new import_client_sqs.ChangeMessageVisibilityCommand({
|
|
197
|
+
QueueUrl,
|
|
198
|
+
ReceiptHandle,
|
|
199
|
+
VisibilityTimeout: 0
|
|
200
|
+
})).catch(() => {
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** Stop the pollers and (if we created it) destroy the SQS client so the process can exit. */
|
|
204
|
+
async close() {
|
|
205
|
+
this.running = false;
|
|
206
|
+
this.abort.abort();
|
|
207
|
+
await Promise.allSettled([
|
|
208
|
+
this.taskLoop,
|
|
209
|
+
this.resultLoop
|
|
210
|
+
]);
|
|
211
|
+
if (this.ownsClient) this.client.destroy();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
215
|
+
0 && (module.exports = {
|
|
216
|
+
SqsTransport
|
|
217
|
+
});
|
|
218
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sqs-transport.ts"],"sourcesContent":["export * from './sqs-transport';\n","import {\n ChangeMessageVisibilityCommand,\n CreateQueueCommand,\n DeleteMessageCommand,\n GetQueueUrlCommand,\n ReceiveMessageCommand,\n SendMessageCommand,\n SQSClient,\n type SQSClientConfig,\n} from '@aws-sdk/client-sqs';\nimport {\n type Heartbeat,\n type RemoteTask,\n type StepHandler,\n type StepResult,\n type Transport,\n runStepHandler,\n} from '@dudousxd/nestjs-durable-core';\n\n/** Resolve a group's tasks queue URL. Sync or async; resolved once and cached. */\nexport type QueueUrlResolver = (group: string) => string | Promise<string>;\n\nexport interface SqsTransportOptions {\n /**\n * Reuse an existing SQS client (recommended — share the app's). When omitted, one is built\n * from `clientConfig` and destroyed on `close()`.\n */\n client?: SQSClient;\n /** Used only when `client` is omitted — e.g. `{ region, endpoint, credentials }` for ElasticMQ. */\n clientConfig?: SQSClientConfig;\n\n /** The worker group this instance serves. Required to register `handle()` consumers. */\n group?: string;\n\n /**\n * Map a group → an **existing** tasks queue URL. This is the seam for reusing queues you already\n * have (e.g. `extraction`, `ingestion`) instead of letting the transport mint its own. When\n * omitted, the transport resolves `${prefix}-tasks-${group}` by name (and creates it if\n * `autoCreate`). Return value is awaited and cached.\n */\n taskQueueUrl?: QueueUrlResolver;\n /**\n * The results queue URL (worker → engine). A literal URL or a resolver. When omitted, the\n * transport resolves `${prefix}-results` by name (and creates it if `autoCreate`).\n */\n resultsQueueUrl?: string | (() => string | Promise<string>);\n\n /** Prefix for the fallback `${prefix}-tasks-*` / `${prefix}-results` names. Default `durable`. */\n prefix?: string;\n /**\n * Create the fallback-named queues on first use if they don't exist (handy for ElasticMQ / dev).\n * Ignored for queues you supply via `taskQueueUrl` / `resultsQueueUrl` — those must already exist.\n */\n autoCreate?: boolean;\n\n /**\n * When set, durable messages carry this SQS message attribute and pollers **skip** messages\n * that lack it (releasing them for a co-resident legacy consumer). Enables sharing one physical\n * queue with a non-durable consumer — but only if that consumer likewise ignores tagged messages.\n * Leave unset for the clean case: a queue dedicated to durable traffic.\n */\n marker?: string;\n\n /** SQS receive long-poll seconds (0–20). Default 20. */\n waitTimeSeconds?: number;\n /** Visibility timeout for an in-flight task, in seconds. Must exceed the step's runtime. Default 60. */\n visibilityTimeoutSec?: number;\n}\n\n/**\n * An SQS-backed `Transport` — dispatch remote steps over Amazon SQS (or ElasticMQ). Unlike the\n * BullMQ transport, it does **not** mint its own queue names: pass `taskQueueUrl` / `resultsQueueUrl`\n * to ride the queues you already operate. The wire payload is the documented `RemoteTask` /\n * `StepResult` JSON, so non-Node workers on the same queues interoperate.\n *\n * Run one instance engine-side (calls `onResult`, dispatches tasks) and one per worker process\n * (calls `handle()` for its group). SQS is at-least-once: a task whose process crashes before the\n * result is sent reappears after the visibility timeout (durable retry); a duplicate `StepResult`\n * is harmless because the engine resolves a step's `pending` exactly once by `stepId`.\n */\nexport class SqsTransport implements Transport {\n private readonly client: SQSClient;\n private readonly ownsClient: boolean;\n private readonly group?: string | undefined;\n private readonly prefix: string;\n private readonly autoCreate: boolean;\n private readonly marker?: string | undefined;\n private readonly waitTimeSeconds: number;\n private readonly visibilityTimeoutSec: number;\n private readonly taskQueueUrlResolver?: QueueUrlResolver | undefined;\n private readonly resultsQueueUrlOption?: string | (() => string | Promise<string>) | undefined;\n\n private readonly handlers = new Map<string, StepHandler>();\n private readonly urlCache = new Map<string, string>();\n private running = true;\n private readonly abort = new AbortController();\n private taskLoop?: Promise<void>;\n private resultLoop?: Promise<void>;\n\n constructor(options: SqsTransportOptions) {\n this.client = options.client ?? new SQSClient(options.clientConfig ?? {});\n this.ownsClient = !options.client;\n this.group = options.group;\n this.prefix = options.prefix ?? 'durable';\n this.autoCreate = options.autoCreate ?? false;\n this.marker = options.marker;\n this.waitTimeSeconds = options.waitTimeSeconds ?? 20;\n this.visibilityTimeoutSec = options.visibilityTimeoutSec ?? 60;\n this.taskQueueUrlResolver = options.taskQueueUrl;\n this.resultsQueueUrlOption = options.resultsQueueUrl;\n }\n\n // ── queue resolution ───────────────────────────────────────────────────────────────────────\n\n private async resolveByName(name: string): Promise<string> {\n const cached = this.urlCache.get(name);\n if (cached) return cached;\n let url: string;\n try {\n const out = await this.client.send(new GetQueueUrlCommand({ QueueName: name }));\n url = out.QueueUrl as string;\n } catch (err) {\n if (this.autoCreate) {\n const created = await this.client.send(new CreateQueueCommand({ QueueName: name }));\n url = created.QueueUrl as string;\n } else {\n throw err;\n }\n }\n this.urlCache.set(name, url);\n return url;\n }\n\n private async tasksUrl(group: string): Promise<string> {\n if (this.taskQueueUrlResolver) return this.taskQueueUrlResolver(group);\n return this.resolveByName(`${this.prefix}-tasks-${group}`);\n }\n\n private async resultsUrl(): Promise<string> {\n const opt = this.resultsQueueUrlOption;\n if (typeof opt === 'string') return opt;\n if (typeof opt === 'function') return opt();\n return this.resolveByName(`${this.prefix}-results`);\n }\n\n // ── engine → worker ────────────────────────────────────────────────────────────────────────\n\n async dispatch(task: RemoteTask): Promise<void> {\n const QueueUrl = await this.tasksUrl(task.group);\n await this.client.send(\n new SendMessageCommand({\n QueueUrl,\n MessageBody: JSON.stringify(task),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n }),\n );\n }\n\n // ── worker → engine ────────────────────────────────────────────────────────────────────────\n\n /** Register a step handler (worker side). Starts the group's task consumer on first call. */\n handle(name: string, fn: StepHandler): void {\n if (!this.group) throw new Error('SqsTransport needs a `group` to register handlers');\n this.handlers.set(name, fn);\n if (!this.taskLoop) this.taskLoop = this.pollTasks(this.group);\n }\n\n onResult(handler: (result: StepResult) => Promise<void>): void {\n if (!this.resultLoop) this.resultLoop = this.pollResults(handler);\n }\n\n onHeartbeat(_handler: (beat: Heartbeat) => Promise<void>): void {\n // Not modelled over SQS; the queue's visibility-timeout redelivery is the liveness mechanism.\n }\n\n // ── polling loops ──────────────────────────────────────────────────────────────────────────\n\n private async pollTasks(group: string): Promise<void> {\n const QueueUrl = await this.tasksUrl(group);\n while (this.running) {\n const messages = await this.receive(QueueUrl, this.visibilityTimeoutSec);\n for (const msg of messages) {\n if (this.shouldSkip(msg)) {\n // Not ours (shared queue): release immediately so the legacy consumer can take it.\n await this.release(QueueUrl, msg.ReceiptHandle);\n continue;\n }\n const task = JSON.parse(msg.Body ?? '{}') as RemoteTask;\n const result = await runStepHandler(task, this.handlers.get(task.name));\n await this.client.send(\n new SendMessageCommand({\n QueueUrl: await this.resultsUrl(),\n MessageBody: JSON.stringify(result),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n }),\n );\n await this.del(QueueUrl, msg.ReceiptHandle);\n }\n }\n }\n\n private async pollResults(handler: (result: StepResult) => Promise<void>): Promise<void> {\n const QueueUrl = await this.resultsUrl();\n while (this.running) {\n const messages = await this.receive(QueueUrl, 30);\n for (const msg of messages) {\n if (this.shouldSkip(msg)) {\n await this.release(QueueUrl, msg.ReceiptHandle);\n continue;\n }\n await handler(JSON.parse(msg.Body ?? '{}') as StepResult);\n await this.del(QueueUrl, msg.ReceiptHandle);\n }\n }\n }\n\n /** One long-poll receive. Swallows the abort thrown on `close()`; logs and backs off otherwise. */\n private async receive(QueueUrl: string, visibility: number) {\n try {\n const out = await this.client.send(\n new ReceiveMessageCommand({\n QueueUrl,\n MaxNumberOfMessages: 10,\n WaitTimeSeconds: this.waitTimeSeconds,\n VisibilityTimeout: visibility,\n MessageAttributeNames: this.marker ? ['All'] : undefined,\n }),\n { abortSignal: this.abort.signal },\n );\n return out.Messages ?? [];\n } catch (err) {\n if (!this.running) return [];\n console.error('[SqsTransport] receive failed', err);\n await new Promise((r) => setTimeout(r, 1000));\n return [];\n }\n }\n\n private shouldSkip(msg: { MessageAttributes?: Record<string, unknown> | undefined }): boolean {\n return !!this.marker && !msg.MessageAttributes?.[this.marker];\n }\n\n private async del(QueueUrl: string, ReceiptHandle?: string): Promise<void> {\n if (!ReceiptHandle) return;\n await this.client.send(new DeleteMessageCommand({ QueueUrl, ReceiptHandle }));\n }\n\n private async release(QueueUrl: string, ReceiptHandle?: string): Promise<void> {\n if (!ReceiptHandle) return;\n await this.client\n .send(new ChangeMessageVisibilityCommand({ QueueUrl, ReceiptHandle, VisibilityTimeout: 0 }))\n .catch(() => {});\n }\n\n /** Stop the pollers and (if we created it) destroy the SQS client so the process can exit. */\n async close(): Promise<void> {\n this.running = false;\n this.abort.abort();\n await Promise.allSettled([this.taskLoop, this.resultLoop]);\n if (this.ownsClient) this.client.destroy();\n }\n}\n"],"mappings":"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","names":["SqsTransport","client","ownsClient","group","prefix","autoCreate","marker","waitTimeSeconds","visibilityTimeoutSec","taskQueueUrlResolver","resultsQueueUrlOption","handlers","Map","urlCache","running","abort","AbortController","taskLoop","resultLoop","options","SQSClient","clientConfig","taskQueueUrl","resultsQueueUrl","resolveByName","name","cached","get","url","out","send","GetQueueUrlCommand","QueueName","QueueUrl","err","created","CreateQueueCommand","set","tasksUrl","resultsUrl","opt","dispatch","task","SendMessageCommand","MessageBody","JSON","stringify","MessageAttributes","DataType","StringValue","undefined","handle","fn","Error","pollTasks","onResult","handler","pollResults","onHeartbeat","_handler","messages","receive","msg","shouldSkip","release","ReceiptHandle","parse","Body","result","runStepHandler","del","visibility","ReceiveMessageCommand","MaxNumberOfMessages","WaitTimeSeconds","VisibilityTimeout","MessageAttributeNames","abortSignal","signal","Messages","console","error","Promise","r","setTimeout","DeleteMessageCommand","ChangeMessageVisibilityCommand","catch","close","allSettled","destroy"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { SQSClient,
|
|
2
|
-
import {
|
|
1
|
+
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { Transport, RemoteTask, StepHandler, StepResult, Heartbeat } from '@dudousxd/nestjs-durable-core';
|
|
3
|
+
|
|
3
4
|
/** Resolve a group's tasks queue URL. Sync or async; resolved once and cached. */
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
type QueueUrlResolver = (group: string) => string | Promise<string>;
|
|
6
|
+
interface SqsTransportOptions {
|
|
6
7
|
/**
|
|
7
8
|
* Reuse an existing SQS client (recommended — share the app's). When omitted, one is built
|
|
8
9
|
* from `clientConfig` and destroyed on `close()`.
|
|
@@ -54,7 +55,7 @@ export interface SqsTransportOptions {
|
|
|
54
55
|
* result is sent reappears after the visibility timeout (durable retry); a duplicate `StepResult`
|
|
55
56
|
* is harmless because the engine resolves a step's `pending` exactly once by `stepId`.
|
|
56
57
|
*/
|
|
57
|
-
|
|
58
|
+
declare class SqsTransport implements Transport {
|
|
58
59
|
private readonly client;
|
|
59
60
|
private readonly ownsClient;
|
|
60
61
|
private readonly group?;
|
|
@@ -90,4 +91,5 @@ export declare class SqsTransport implements Transport {
|
|
|
90
91
|
/** Stop the pollers and (if we created it) destroy the SQS client so the process can exit. */
|
|
91
92
|
close(): Promise<void>;
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
+
|
|
95
|
+
export { type QueueUrlResolver, SqsTransport, type SqsTransportOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,95 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs';
|
|
2
|
+
import { Transport, RemoteTask, StepHandler, StepResult, Heartbeat } from '@dudousxd/nestjs-durable-core';
|
|
3
|
+
|
|
4
|
+
/** Resolve a group's tasks queue URL. Sync or async; resolved once and cached. */
|
|
5
|
+
type QueueUrlResolver = (group: string) => string | Promise<string>;
|
|
6
|
+
interface SqsTransportOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Reuse an existing SQS client (recommended — share the app's). When omitted, one is built
|
|
9
|
+
* from `clientConfig` and destroyed on `close()`.
|
|
10
|
+
*/
|
|
11
|
+
client?: SQSClient;
|
|
12
|
+
/** Used only when `client` is omitted — e.g. `{ region, endpoint, credentials }` for ElasticMQ. */
|
|
13
|
+
clientConfig?: SQSClientConfig;
|
|
14
|
+
/** The worker group this instance serves. Required to register `handle()` consumers. */
|
|
15
|
+
group?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Map a group → an **existing** tasks queue URL. This is the seam for reusing queues you already
|
|
18
|
+
* have (e.g. `extraction`, `ingestion`) instead of letting the transport mint its own. When
|
|
19
|
+
* omitted, the transport resolves `${prefix}-tasks-${group}` by name (and creates it if
|
|
20
|
+
* `autoCreate`). Return value is awaited and cached.
|
|
21
|
+
*/
|
|
22
|
+
taskQueueUrl?: QueueUrlResolver;
|
|
23
|
+
/**
|
|
24
|
+
* The results queue URL (worker → engine). A literal URL or a resolver. When omitted, the
|
|
25
|
+
* transport resolves `${prefix}-results` by name (and creates it if `autoCreate`).
|
|
26
|
+
*/
|
|
27
|
+
resultsQueueUrl?: string | (() => string | Promise<string>);
|
|
28
|
+
/** Prefix for the fallback `${prefix}-tasks-*` / `${prefix}-results` names. Default `durable`. */
|
|
29
|
+
prefix?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Create the fallback-named queues on first use if they don't exist (handy for ElasticMQ / dev).
|
|
32
|
+
* Ignored for queues you supply via `taskQueueUrl` / `resultsQueueUrl` — those must already exist.
|
|
33
|
+
*/
|
|
34
|
+
autoCreate?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* When set, durable messages carry this SQS message attribute and pollers **skip** messages
|
|
37
|
+
* that lack it (releasing them for a co-resident legacy consumer). Enables sharing one physical
|
|
38
|
+
* queue with a non-durable consumer — but only if that consumer likewise ignores tagged messages.
|
|
39
|
+
* Leave unset for the clean case: a queue dedicated to durable traffic.
|
|
40
|
+
*/
|
|
41
|
+
marker?: string;
|
|
42
|
+
/** SQS receive long-poll seconds (0–20). Default 20. */
|
|
43
|
+
waitTimeSeconds?: number;
|
|
44
|
+
/** Visibility timeout for an in-flight task, in seconds. Must exceed the step's runtime. Default 60. */
|
|
45
|
+
visibilityTimeoutSec?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* An SQS-backed `Transport` — dispatch remote steps over Amazon SQS (or ElasticMQ). Unlike the
|
|
49
|
+
* BullMQ transport, it does **not** mint its own queue names: pass `taskQueueUrl` / `resultsQueueUrl`
|
|
50
|
+
* to ride the queues you already operate. The wire payload is the documented `RemoteTask` /
|
|
51
|
+
* `StepResult` JSON, so non-Node workers on the same queues interoperate.
|
|
52
|
+
*
|
|
53
|
+
* Run one instance engine-side (calls `onResult`, dispatches tasks) and one per worker process
|
|
54
|
+
* (calls `handle()` for its group). SQS is at-least-once: a task whose process crashes before the
|
|
55
|
+
* result is sent reappears after the visibility timeout (durable retry); a duplicate `StepResult`
|
|
56
|
+
* is harmless because the engine resolves a step's `pending` exactly once by `stepId`.
|
|
57
|
+
*/
|
|
58
|
+
declare class SqsTransport implements Transport {
|
|
59
|
+
private readonly client;
|
|
60
|
+
private readonly ownsClient;
|
|
61
|
+
private readonly group?;
|
|
62
|
+
private readonly prefix;
|
|
63
|
+
private readonly autoCreate;
|
|
64
|
+
private readonly marker?;
|
|
65
|
+
private readonly waitTimeSeconds;
|
|
66
|
+
private readonly visibilityTimeoutSec;
|
|
67
|
+
private readonly taskQueueUrlResolver?;
|
|
68
|
+
private readonly resultsQueueUrlOption?;
|
|
69
|
+
private readonly handlers;
|
|
70
|
+
private readonly urlCache;
|
|
71
|
+
private running;
|
|
72
|
+
private readonly abort;
|
|
73
|
+
private taskLoop?;
|
|
74
|
+
private resultLoop?;
|
|
75
|
+
constructor(options: SqsTransportOptions);
|
|
76
|
+
private resolveByName;
|
|
77
|
+
private tasksUrl;
|
|
78
|
+
private resultsUrl;
|
|
79
|
+
dispatch(task: RemoteTask): Promise<void>;
|
|
80
|
+
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
81
|
+
handle(name: string, fn: StepHandler): void;
|
|
82
|
+
onResult(handler: (result: StepResult) => Promise<void>): void;
|
|
83
|
+
onHeartbeat(_handler: (beat: Heartbeat) => Promise<void>): void;
|
|
84
|
+
private pollTasks;
|
|
85
|
+
private pollResults;
|
|
86
|
+
/** One long-poll receive. Swallows the abort thrown on `close()`; logs and backs off otherwise. */
|
|
87
|
+
private receive;
|
|
88
|
+
private shouldSkip;
|
|
89
|
+
private del;
|
|
90
|
+
private release;
|
|
91
|
+
/** Stop the pollers and (if we created it) destroy the SQS client so the process can exit. */
|
|
92
|
+
close(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { type QueueUrlResolver, SqsTransport, type SqsTransportOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,193 @@
|
|
|
1
|
-
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/sqs-transport.ts
|
|
5
|
+
import { ChangeMessageVisibilityCommand, CreateQueueCommand, DeleteMessageCommand, GetQueueUrlCommand, ReceiveMessageCommand, SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
6
|
+
import { runStepHandler } from "@dudousxd/nestjs-durable-core";
|
|
7
|
+
var SqsTransport = class {
|
|
8
|
+
static {
|
|
9
|
+
__name(this, "SqsTransport");
|
|
10
|
+
}
|
|
11
|
+
client;
|
|
12
|
+
ownsClient;
|
|
13
|
+
group;
|
|
14
|
+
prefix;
|
|
15
|
+
autoCreate;
|
|
16
|
+
marker;
|
|
17
|
+
waitTimeSeconds;
|
|
18
|
+
visibilityTimeoutSec;
|
|
19
|
+
taskQueueUrlResolver;
|
|
20
|
+
resultsQueueUrlOption;
|
|
21
|
+
handlers = /* @__PURE__ */ new Map();
|
|
22
|
+
urlCache = /* @__PURE__ */ new Map();
|
|
23
|
+
running = true;
|
|
24
|
+
abort = new AbortController();
|
|
25
|
+
taskLoop;
|
|
26
|
+
resultLoop;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.client = options.client ?? new SQSClient(options.clientConfig ?? {});
|
|
29
|
+
this.ownsClient = !options.client;
|
|
30
|
+
this.group = options.group;
|
|
31
|
+
this.prefix = options.prefix ?? "durable";
|
|
32
|
+
this.autoCreate = options.autoCreate ?? false;
|
|
33
|
+
this.marker = options.marker;
|
|
34
|
+
this.waitTimeSeconds = options.waitTimeSeconds ?? 20;
|
|
35
|
+
this.visibilityTimeoutSec = options.visibilityTimeoutSec ?? 60;
|
|
36
|
+
this.taskQueueUrlResolver = options.taskQueueUrl;
|
|
37
|
+
this.resultsQueueUrlOption = options.resultsQueueUrl;
|
|
38
|
+
}
|
|
39
|
+
// ── queue resolution ───────────────────────────────────────────────────────────────────────
|
|
40
|
+
async resolveByName(name) {
|
|
41
|
+
const cached = this.urlCache.get(name);
|
|
42
|
+
if (cached) return cached;
|
|
43
|
+
let url;
|
|
44
|
+
try {
|
|
45
|
+
const out = await this.client.send(new GetQueueUrlCommand({
|
|
46
|
+
QueueName: name
|
|
47
|
+
}));
|
|
48
|
+
url = out.QueueUrl;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (this.autoCreate) {
|
|
51
|
+
const created = await this.client.send(new CreateQueueCommand({
|
|
52
|
+
QueueName: name
|
|
53
|
+
}));
|
|
54
|
+
url = created.QueueUrl;
|
|
55
|
+
} else {
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
7
58
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
59
|
+
this.urlCache.set(name, url);
|
|
60
|
+
return url;
|
|
61
|
+
}
|
|
62
|
+
async tasksUrl(group) {
|
|
63
|
+
if (this.taskQueueUrlResolver) return this.taskQueueUrlResolver(group);
|
|
64
|
+
return this.resolveByName(`${this.prefix}-tasks-${group}`);
|
|
65
|
+
}
|
|
66
|
+
async resultsUrl() {
|
|
67
|
+
const opt = this.resultsQueueUrlOption;
|
|
68
|
+
if (typeof opt === "string") return opt;
|
|
69
|
+
if (typeof opt === "function") return opt();
|
|
70
|
+
return this.resolveByName(`${this.prefix}-results`);
|
|
71
|
+
}
|
|
72
|
+
// ── engine → worker ────────────────────────────────────────────────────────────────────────
|
|
73
|
+
async dispatch(task) {
|
|
74
|
+
const QueueUrl = await this.tasksUrl(task.group);
|
|
75
|
+
await this.client.send(new SendMessageCommand({
|
|
76
|
+
QueueUrl,
|
|
77
|
+
MessageBody: JSON.stringify(task),
|
|
78
|
+
MessageAttributes: this.marker ? {
|
|
79
|
+
[this.marker]: {
|
|
80
|
+
DataType: "String",
|
|
81
|
+
StringValue: "1"
|
|
82
|
+
}
|
|
83
|
+
} : void 0
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
// ── worker → engine ────────────────────────────────────────────────────────────────────────
|
|
87
|
+
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
88
|
+
handle(name, fn) {
|
|
89
|
+
if (!this.group) throw new Error("SqsTransport needs a `group` to register handlers");
|
|
90
|
+
this.handlers.set(name, fn);
|
|
91
|
+
if (!this.taskLoop) this.taskLoop = this.pollTasks(this.group);
|
|
92
|
+
}
|
|
93
|
+
onResult(handler) {
|
|
94
|
+
if (!this.resultLoop) this.resultLoop = this.pollResults(handler);
|
|
95
|
+
}
|
|
96
|
+
onHeartbeat(_handler) {
|
|
97
|
+
}
|
|
98
|
+
// ── polling loops ──────────────────────────────────────────────────────────────────────────
|
|
99
|
+
async pollTasks(group) {
|
|
100
|
+
const QueueUrl = await this.tasksUrl(group);
|
|
101
|
+
while (this.running) {
|
|
102
|
+
const messages = await this.receive(QueueUrl, this.visibilityTimeoutSec);
|
|
103
|
+
for (const msg of messages) {
|
|
104
|
+
if (this.shouldSkip(msg)) {
|
|
105
|
+
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const task = JSON.parse(msg.Body ?? "{}");
|
|
109
|
+
const result = await runStepHandler(task, this.handlers.get(task.name));
|
|
110
|
+
await this.client.send(new SendMessageCommand({
|
|
111
|
+
QueueUrl: await this.resultsUrl(),
|
|
112
|
+
MessageBody: JSON.stringify(result),
|
|
113
|
+
MessageAttributes: this.marker ? {
|
|
114
|
+
[this.marker]: {
|
|
115
|
+
DataType: "String",
|
|
116
|
+
StringValue: "1"
|
|
117
|
+
}
|
|
118
|
+
} : void 0
|
|
119
|
+
}));
|
|
120
|
+
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async pollResults(handler) {
|
|
125
|
+
const QueueUrl = await this.resultsUrl();
|
|
126
|
+
while (this.running) {
|
|
127
|
+
const messages = await this.receive(QueueUrl, 30);
|
|
128
|
+
for (const msg of messages) {
|
|
129
|
+
if (this.shouldSkip(msg)) {
|
|
130
|
+
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
await handler(JSON.parse(msg.Body ?? "{}"));
|
|
134
|
+
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** One long-poll receive. Swallows the abort thrown on `close()`; logs and backs off otherwise. */
|
|
139
|
+
async receive(QueueUrl, visibility) {
|
|
140
|
+
try {
|
|
141
|
+
const out = await this.client.send(new ReceiveMessageCommand({
|
|
142
|
+
QueueUrl,
|
|
143
|
+
MaxNumberOfMessages: 10,
|
|
144
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
145
|
+
VisibilityTimeout: visibility,
|
|
146
|
+
MessageAttributeNames: this.marker ? [
|
|
147
|
+
"All"
|
|
148
|
+
] : void 0
|
|
149
|
+
}), {
|
|
150
|
+
abortSignal: this.abort.signal
|
|
151
|
+
});
|
|
152
|
+
return out.Messages ?? [];
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (!this.running) return [];
|
|
155
|
+
console.error("[SqsTransport] receive failed", err);
|
|
156
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
shouldSkip(msg) {
|
|
161
|
+
return !!this.marker && !msg.MessageAttributes?.[this.marker];
|
|
162
|
+
}
|
|
163
|
+
async del(QueueUrl, ReceiptHandle) {
|
|
164
|
+
if (!ReceiptHandle) return;
|
|
165
|
+
await this.client.send(new DeleteMessageCommand({
|
|
166
|
+
QueueUrl,
|
|
167
|
+
ReceiptHandle
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
async release(QueueUrl, ReceiptHandle) {
|
|
171
|
+
if (!ReceiptHandle) return;
|
|
172
|
+
await this.client.send(new ChangeMessageVisibilityCommand({
|
|
173
|
+
QueueUrl,
|
|
174
|
+
ReceiptHandle,
|
|
175
|
+
VisibilityTimeout: 0
|
|
176
|
+
})).catch(() => {
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Stop the pollers and (if we created it) destroy the SQS client so the process can exit. */
|
|
180
|
+
async close() {
|
|
181
|
+
this.running = false;
|
|
182
|
+
this.abort.abort();
|
|
183
|
+
await Promise.allSettled([
|
|
184
|
+
this.taskLoop,
|
|
185
|
+
this.resultLoop
|
|
186
|
+
]);
|
|
187
|
+
if (this.ownsClient) this.client.destroy();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
export {
|
|
191
|
+
SqsTransport
|
|
15
192
|
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./sqs-transport"), exports);
|
|
18
193
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,kDAAgC"}
|
|
1
|
+
{"version":3,"sources":["../src/sqs-transport.ts"],"sourcesContent":["import {\n ChangeMessageVisibilityCommand,\n CreateQueueCommand,\n DeleteMessageCommand,\n GetQueueUrlCommand,\n ReceiveMessageCommand,\n SendMessageCommand,\n SQSClient,\n type SQSClientConfig,\n} from '@aws-sdk/client-sqs';\nimport {\n type Heartbeat,\n type RemoteTask,\n type StepHandler,\n type StepResult,\n type Transport,\n runStepHandler,\n} from '@dudousxd/nestjs-durable-core';\n\n/** Resolve a group's tasks queue URL. Sync or async; resolved once and cached. */\nexport type QueueUrlResolver = (group: string) => string | Promise<string>;\n\nexport interface SqsTransportOptions {\n /**\n * Reuse an existing SQS client (recommended — share the app's). When omitted, one is built\n * from `clientConfig` and destroyed on `close()`.\n */\n client?: SQSClient;\n /** Used only when `client` is omitted — e.g. `{ region, endpoint, credentials }` for ElasticMQ. */\n clientConfig?: SQSClientConfig;\n\n /** The worker group this instance serves. Required to register `handle()` consumers. */\n group?: string;\n\n /**\n * Map a group → an **existing** tasks queue URL. This is the seam for reusing queues you already\n * have (e.g. `extraction`, `ingestion`) instead of letting the transport mint its own. When\n * omitted, the transport resolves `${prefix}-tasks-${group}` by name (and creates it if\n * `autoCreate`). Return value is awaited and cached.\n */\n taskQueueUrl?: QueueUrlResolver;\n /**\n * The results queue URL (worker → engine). A literal URL or a resolver. When omitted, the\n * transport resolves `${prefix}-results` by name (and creates it if `autoCreate`).\n */\n resultsQueueUrl?: string | (() => string | Promise<string>);\n\n /** Prefix for the fallback `${prefix}-tasks-*` / `${prefix}-results` names. Default `durable`. */\n prefix?: string;\n /**\n * Create the fallback-named queues on first use if they don't exist (handy for ElasticMQ / dev).\n * Ignored for queues you supply via `taskQueueUrl` / `resultsQueueUrl` — those must already exist.\n */\n autoCreate?: boolean;\n\n /**\n * When set, durable messages carry this SQS message attribute and pollers **skip** messages\n * that lack it (releasing them for a co-resident legacy consumer). Enables sharing one physical\n * queue with a non-durable consumer — but only if that consumer likewise ignores tagged messages.\n * Leave unset for the clean case: a queue dedicated to durable traffic.\n */\n marker?: string;\n\n /** SQS receive long-poll seconds (0–20). Default 20. */\n waitTimeSeconds?: number;\n /** Visibility timeout for an in-flight task, in seconds. Must exceed the step's runtime. Default 60. */\n visibilityTimeoutSec?: number;\n}\n\n/**\n * An SQS-backed `Transport` — dispatch remote steps over Amazon SQS (or ElasticMQ). Unlike the\n * BullMQ transport, it does **not** mint its own queue names: pass `taskQueueUrl` / `resultsQueueUrl`\n * to ride the queues you already operate. The wire payload is the documented `RemoteTask` /\n * `StepResult` JSON, so non-Node workers on the same queues interoperate.\n *\n * Run one instance engine-side (calls `onResult`, dispatches tasks) and one per worker process\n * (calls `handle()` for its group). SQS is at-least-once: a task whose process crashes before the\n * result is sent reappears after the visibility timeout (durable retry); a duplicate `StepResult`\n * is harmless because the engine resolves a step's `pending` exactly once by `stepId`.\n */\nexport class SqsTransport implements Transport {\n private readonly client: SQSClient;\n private readonly ownsClient: boolean;\n private readonly group?: string | undefined;\n private readonly prefix: string;\n private readonly autoCreate: boolean;\n private readonly marker?: string | undefined;\n private readonly waitTimeSeconds: number;\n private readonly visibilityTimeoutSec: number;\n private readonly taskQueueUrlResolver?: QueueUrlResolver | undefined;\n private readonly resultsQueueUrlOption?: string | (() => string | Promise<string>) | undefined;\n\n private readonly handlers = new Map<string, StepHandler>();\n private readonly urlCache = new Map<string, string>();\n private running = true;\n private readonly abort = new AbortController();\n private taskLoop?: Promise<void>;\n private resultLoop?: Promise<void>;\n\n constructor(options: SqsTransportOptions) {\n this.client = options.client ?? new SQSClient(options.clientConfig ?? {});\n this.ownsClient = !options.client;\n this.group = options.group;\n this.prefix = options.prefix ?? 'durable';\n this.autoCreate = options.autoCreate ?? false;\n this.marker = options.marker;\n this.waitTimeSeconds = options.waitTimeSeconds ?? 20;\n this.visibilityTimeoutSec = options.visibilityTimeoutSec ?? 60;\n this.taskQueueUrlResolver = options.taskQueueUrl;\n this.resultsQueueUrlOption = options.resultsQueueUrl;\n }\n\n // ── queue resolution ───────────────────────────────────────────────────────────────────────\n\n private async resolveByName(name: string): Promise<string> {\n const cached = this.urlCache.get(name);\n if (cached) return cached;\n let url: string;\n try {\n const out = await this.client.send(new GetQueueUrlCommand({ QueueName: name }));\n url = out.QueueUrl as string;\n } catch (err) {\n if (this.autoCreate) {\n const created = await this.client.send(new CreateQueueCommand({ QueueName: name }));\n url = created.QueueUrl as string;\n } else {\n throw err;\n }\n }\n this.urlCache.set(name, url);\n return url;\n }\n\n private async tasksUrl(group: string): Promise<string> {\n if (this.taskQueueUrlResolver) return this.taskQueueUrlResolver(group);\n return this.resolveByName(`${this.prefix}-tasks-${group}`);\n }\n\n private async resultsUrl(): Promise<string> {\n const opt = this.resultsQueueUrlOption;\n if (typeof opt === 'string') return opt;\n if (typeof opt === 'function') return opt();\n return this.resolveByName(`${this.prefix}-results`);\n }\n\n // ── engine → worker ────────────────────────────────────────────────────────────────────────\n\n async dispatch(task: RemoteTask): Promise<void> {\n const QueueUrl = await this.tasksUrl(task.group);\n await this.client.send(\n new SendMessageCommand({\n QueueUrl,\n MessageBody: JSON.stringify(task),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n }),\n );\n }\n\n // ── worker → engine ────────────────────────────────────────────────────────────────────────\n\n /** Register a step handler (worker side). Starts the group's task consumer on first call. */\n handle(name: string, fn: StepHandler): void {\n if (!this.group) throw new Error('SqsTransport needs a `group` to register handlers');\n this.handlers.set(name, fn);\n if (!this.taskLoop) this.taskLoop = this.pollTasks(this.group);\n }\n\n onResult(handler: (result: StepResult) => Promise<void>): void {\n if (!this.resultLoop) this.resultLoop = this.pollResults(handler);\n }\n\n onHeartbeat(_handler: (beat: Heartbeat) => Promise<void>): void {\n // Not modelled over SQS; the queue's visibility-timeout redelivery is the liveness mechanism.\n }\n\n // ── polling loops ──────────────────────────────────────────────────────────────────────────\n\n private async pollTasks(group: string): Promise<void> {\n const QueueUrl = await this.tasksUrl(group);\n while (this.running) {\n const messages = await this.receive(QueueUrl, this.visibilityTimeoutSec);\n for (const msg of messages) {\n if (this.shouldSkip(msg)) {\n // Not ours (shared queue): release immediately so the legacy consumer can take it.\n await this.release(QueueUrl, msg.ReceiptHandle);\n continue;\n }\n const task = JSON.parse(msg.Body ?? '{}') as RemoteTask;\n const result = await runStepHandler(task, this.handlers.get(task.name));\n await this.client.send(\n new SendMessageCommand({\n QueueUrl: await this.resultsUrl(),\n MessageBody: JSON.stringify(result),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n }),\n );\n await this.del(QueueUrl, msg.ReceiptHandle);\n }\n }\n }\n\n private async pollResults(handler: (result: StepResult) => Promise<void>): Promise<void> {\n const QueueUrl = await this.resultsUrl();\n while (this.running) {\n const messages = await this.receive(QueueUrl, 30);\n for (const msg of messages) {\n if (this.shouldSkip(msg)) {\n await this.release(QueueUrl, msg.ReceiptHandle);\n continue;\n }\n await handler(JSON.parse(msg.Body ?? '{}') as StepResult);\n await this.del(QueueUrl, msg.ReceiptHandle);\n }\n }\n }\n\n /** One long-poll receive. Swallows the abort thrown on `close()`; logs and backs off otherwise. */\n private async receive(QueueUrl: string, visibility: number) {\n try {\n const out = await this.client.send(\n new ReceiveMessageCommand({\n QueueUrl,\n MaxNumberOfMessages: 10,\n WaitTimeSeconds: this.waitTimeSeconds,\n VisibilityTimeout: visibility,\n MessageAttributeNames: this.marker ? ['All'] : undefined,\n }),\n { abortSignal: this.abort.signal },\n );\n return out.Messages ?? [];\n } catch (err) {\n if (!this.running) return [];\n console.error('[SqsTransport] receive failed', err);\n await new Promise((r) => setTimeout(r, 1000));\n return [];\n }\n }\n\n private shouldSkip(msg: { MessageAttributes?: Record<string, unknown> | undefined }): boolean {\n return !!this.marker && !msg.MessageAttributes?.[this.marker];\n }\n\n private async del(QueueUrl: string, ReceiptHandle?: string): Promise<void> {\n if (!ReceiptHandle) return;\n await this.client.send(new DeleteMessageCommand({ QueueUrl, ReceiptHandle }));\n }\n\n private async release(QueueUrl: string, ReceiptHandle?: string): Promise<void> {\n if (!ReceiptHandle) return;\n await this.client\n .send(new ChangeMessageVisibilityCommand({ QueueUrl, ReceiptHandle, VisibilityTimeout: 0 }))\n .catch(() => {});\n }\n\n /** Stop the pollers and (if we created it) destroy the SQS client so the process can exit. */\n async close(): Promise<void> {\n this.running = false;\n this.abort.abort();\n await Promise.allSettled([this.taskLoop, this.resultLoop]);\n if (this.ownsClient) this.client.destroy();\n }\n}\n"],"mappings":"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","names":["ChangeMessageVisibilityCommand","CreateQueueCommand","DeleteMessageCommand","GetQueueUrlCommand","ReceiveMessageCommand","SendMessageCommand","SQSClient","runStepHandler","SqsTransport","client","ownsClient","group","prefix","autoCreate","marker","waitTimeSeconds","visibilityTimeoutSec","taskQueueUrlResolver","resultsQueueUrlOption","handlers","Map","urlCache","running","abort","AbortController","taskLoop","resultLoop","options","SQSClient","clientConfig","taskQueueUrl","resultsQueueUrl","resolveByName","name","cached","get","url","out","send","GetQueueUrlCommand","QueueName","QueueUrl","err","created","CreateQueueCommand","set","tasksUrl","resultsUrl","opt","dispatch","task","SendMessageCommand","MessageBody","JSON","stringify","MessageAttributes","DataType","StringValue","undefined","handle","fn","Error","pollTasks","onResult","handler","pollResults","onHeartbeat","_handler","messages","receive","msg","shouldSkip","release","ReceiptHandle","parse","Body","result","runStepHandler","del","visibility","ReceiveMessageCommand","MaxNumberOfMessages","WaitTimeSeconds","VisibilityTimeout","MessageAttributeNames","abortSignal","signal","Messages","console","error","Promise","r","setTimeout","DeleteMessageCommand","ChangeMessageVisibilityCommand","catch","close","allSettled","destroy"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-durable-transport-sqs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Amazon SQS Transport for nestjs-durable — dispatch remote steps over existing SQS queues (ElasticMQ-compatible)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"url": "https://github.com/DavideCarvalho/nestjs-durable.git",
|
|
10
10
|
"directory": "packages/transport-sqs"
|
|
11
11
|
},
|
|
12
|
-
"type": "
|
|
13
|
-
"main": "dist/index.
|
|
14
|
-
"types": "dist/index.d.ts",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.cjs",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
@@ -21,12 +21,27 @@
|
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@aws-sdk/client-sqs": "3.1034.0",
|
|
24
|
+
"testcontainers": "10.28.0",
|
|
24
25
|
"typescript": "5.9.3",
|
|
25
26
|
"zod": "3.25.76",
|
|
26
|
-
"
|
|
27
|
+
"tsup": "8.3.5",
|
|
28
|
+
"@dudousxd/nestjs-durable-core": "^0.28.0"
|
|
29
|
+
},
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/index.d.cts",
|
|
39
|
+
"default": "./dist/index.cjs"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
},
|
|
28
43
|
"scripts": {
|
|
29
|
-
"build": "
|
|
44
|
+
"build": "tsup",
|
|
30
45
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
31
46
|
}
|
|
32
47
|
}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sqs-transport.d.ts","sourceRoot":"","sources":["../src/sqs-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,SAAS,EACT,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EAEf,MAAM,+BAA+B,CAAC;AAEvC,kFAAkF;AAClF,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAE3E,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,mGAAmG;IACnG,YAAY,CAAC,EAAE,eAAe,CAAC;IAE/B,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;OAKG;IACH,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAE5D,kGAAkG;IAClG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wGAAwG;IACxG,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,YAAa,YAAW,SAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC,CAAmB;IACzD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CAA4C;IAEnF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,UAAU,CAAC,CAAgB;gBAEvB,OAAO,EAAE,mBAAmB;YAe1B,aAAa;YAmBb,QAAQ;YAKR,UAAU;IASlB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/C,6FAA6F;IAC7F,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI;IAM3C,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI9D,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;YAMjD,SAAS;YA0BT,WAAW;IAezB,mGAAmG;YACrF,OAAO;IAqBrB,OAAO,CAAC,UAAU;YAIJ,GAAG;YAKH,OAAO;IAOrB,8FAA8F;IACxF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAM7B"}
|
package/dist/sqs-transport.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SqsTransport = void 0;
|
|
4
|
-
const client_sqs_1 = require("@aws-sdk/client-sqs");
|
|
5
|
-
const nestjs_durable_core_1 = require("@dudousxd/nestjs-durable-core");
|
|
6
|
-
/**
|
|
7
|
-
* An SQS-backed `Transport` — dispatch remote steps over Amazon SQS (or ElasticMQ). Unlike the
|
|
8
|
-
* BullMQ transport, it does **not** mint its own queue names: pass `taskQueueUrl` / `resultsQueueUrl`
|
|
9
|
-
* to ride the queues you already operate. The wire payload is the documented `RemoteTask` /
|
|
10
|
-
* `StepResult` JSON, so non-Node workers on the same queues interoperate.
|
|
11
|
-
*
|
|
12
|
-
* Run one instance engine-side (calls `onResult`, dispatches tasks) and one per worker process
|
|
13
|
-
* (calls `handle()` for its group). SQS is at-least-once: a task whose process crashes before the
|
|
14
|
-
* result is sent reappears after the visibility timeout (durable retry); a duplicate `StepResult`
|
|
15
|
-
* is harmless because the engine resolves a step's `pending` exactly once by `stepId`.
|
|
16
|
-
*/
|
|
17
|
-
class SqsTransport {
|
|
18
|
-
client;
|
|
19
|
-
ownsClient;
|
|
20
|
-
group;
|
|
21
|
-
prefix;
|
|
22
|
-
autoCreate;
|
|
23
|
-
marker;
|
|
24
|
-
waitTimeSeconds;
|
|
25
|
-
visibilityTimeoutSec;
|
|
26
|
-
taskQueueUrlResolver;
|
|
27
|
-
resultsQueueUrlOption;
|
|
28
|
-
handlers = new Map();
|
|
29
|
-
urlCache = new Map();
|
|
30
|
-
running = true;
|
|
31
|
-
abort = new AbortController();
|
|
32
|
-
taskLoop;
|
|
33
|
-
resultLoop;
|
|
34
|
-
constructor(options) {
|
|
35
|
-
this.client = options.client ?? new client_sqs_1.SQSClient(options.clientConfig ?? {});
|
|
36
|
-
this.ownsClient = !options.client;
|
|
37
|
-
this.group = options.group;
|
|
38
|
-
this.prefix = options.prefix ?? 'durable';
|
|
39
|
-
this.autoCreate = options.autoCreate ?? false;
|
|
40
|
-
this.marker = options.marker;
|
|
41
|
-
this.waitTimeSeconds = options.waitTimeSeconds ?? 20;
|
|
42
|
-
this.visibilityTimeoutSec = options.visibilityTimeoutSec ?? 60;
|
|
43
|
-
this.taskQueueUrlResolver = options.taskQueueUrl;
|
|
44
|
-
this.resultsQueueUrlOption = options.resultsQueueUrl;
|
|
45
|
-
}
|
|
46
|
-
// ── queue resolution ───────────────────────────────────────────────────────────────────────
|
|
47
|
-
async resolveByName(name) {
|
|
48
|
-
const cached = this.urlCache.get(name);
|
|
49
|
-
if (cached)
|
|
50
|
-
return cached;
|
|
51
|
-
let url;
|
|
52
|
-
try {
|
|
53
|
-
const out = await this.client.send(new client_sqs_1.GetQueueUrlCommand({ QueueName: name }));
|
|
54
|
-
url = out.QueueUrl;
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
if (this.autoCreate) {
|
|
58
|
-
const created = await this.client.send(new client_sqs_1.CreateQueueCommand({ QueueName: name }));
|
|
59
|
-
url = created.QueueUrl;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
this.urlCache.set(name, url);
|
|
66
|
-
return url;
|
|
67
|
-
}
|
|
68
|
-
async tasksUrl(group) {
|
|
69
|
-
if (this.taskQueueUrlResolver)
|
|
70
|
-
return this.taskQueueUrlResolver(group);
|
|
71
|
-
return this.resolveByName(`${this.prefix}-tasks-${group}`);
|
|
72
|
-
}
|
|
73
|
-
async resultsUrl() {
|
|
74
|
-
const opt = this.resultsQueueUrlOption;
|
|
75
|
-
if (typeof opt === 'string')
|
|
76
|
-
return opt;
|
|
77
|
-
if (typeof opt === 'function')
|
|
78
|
-
return opt();
|
|
79
|
-
return this.resolveByName(`${this.prefix}-results`);
|
|
80
|
-
}
|
|
81
|
-
// ── engine → worker ────────────────────────────────────────────────────────────────────────
|
|
82
|
-
async dispatch(task) {
|
|
83
|
-
const QueueUrl = await this.tasksUrl(task.group);
|
|
84
|
-
await this.client.send(new client_sqs_1.SendMessageCommand({
|
|
85
|
-
QueueUrl,
|
|
86
|
-
MessageBody: JSON.stringify(task),
|
|
87
|
-
MessageAttributes: this.marker
|
|
88
|
-
? { [this.marker]: { DataType: 'String', StringValue: '1' } }
|
|
89
|
-
: undefined,
|
|
90
|
-
}));
|
|
91
|
-
}
|
|
92
|
-
// ── worker → engine ────────────────────────────────────────────────────────────────────────
|
|
93
|
-
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
94
|
-
handle(name, fn) {
|
|
95
|
-
if (!this.group)
|
|
96
|
-
throw new Error('SqsTransport needs a `group` to register handlers');
|
|
97
|
-
this.handlers.set(name, fn);
|
|
98
|
-
if (!this.taskLoop)
|
|
99
|
-
this.taskLoop = this.pollTasks(this.group);
|
|
100
|
-
}
|
|
101
|
-
onResult(handler) {
|
|
102
|
-
if (!this.resultLoop)
|
|
103
|
-
this.resultLoop = this.pollResults(handler);
|
|
104
|
-
}
|
|
105
|
-
onHeartbeat(_handler) {
|
|
106
|
-
// Not modelled over SQS; the queue's visibility-timeout redelivery is the liveness mechanism.
|
|
107
|
-
}
|
|
108
|
-
// ── polling loops ──────────────────────────────────────────────────────────────────────────
|
|
109
|
-
async pollTasks(group) {
|
|
110
|
-
const QueueUrl = await this.tasksUrl(group);
|
|
111
|
-
while (this.running) {
|
|
112
|
-
const messages = await this.receive(QueueUrl, this.visibilityTimeoutSec);
|
|
113
|
-
for (const msg of messages) {
|
|
114
|
-
if (this.shouldSkip(msg)) {
|
|
115
|
-
// Not ours (shared queue): release immediately so the legacy consumer can take it.
|
|
116
|
-
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const task = JSON.parse(msg.Body ?? '{}');
|
|
120
|
-
const result = await (0, nestjs_durable_core_1.runStepHandler)(task, this.handlers.get(task.name));
|
|
121
|
-
await this.client.send(new client_sqs_1.SendMessageCommand({
|
|
122
|
-
QueueUrl: await this.resultsUrl(),
|
|
123
|
-
MessageBody: JSON.stringify(result),
|
|
124
|
-
MessageAttributes: this.marker
|
|
125
|
-
? { [this.marker]: { DataType: 'String', StringValue: '1' } }
|
|
126
|
-
: undefined,
|
|
127
|
-
}));
|
|
128
|
-
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
async pollResults(handler) {
|
|
133
|
-
const QueueUrl = await this.resultsUrl();
|
|
134
|
-
while (this.running) {
|
|
135
|
-
const messages = await this.receive(QueueUrl, 30);
|
|
136
|
-
for (const msg of messages) {
|
|
137
|
-
if (this.shouldSkip(msg)) {
|
|
138
|
-
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
await handler(JSON.parse(msg.Body ?? '{}'));
|
|
142
|
-
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
/** One long-poll receive. Swallows the abort thrown on `close()`; logs and backs off otherwise. */
|
|
147
|
-
async receive(QueueUrl, visibility) {
|
|
148
|
-
try {
|
|
149
|
-
const out = await this.client.send(new client_sqs_1.ReceiveMessageCommand({
|
|
150
|
-
QueueUrl,
|
|
151
|
-
MaxNumberOfMessages: 10,
|
|
152
|
-
WaitTimeSeconds: this.waitTimeSeconds,
|
|
153
|
-
VisibilityTimeout: visibility,
|
|
154
|
-
MessageAttributeNames: this.marker ? ['All'] : undefined,
|
|
155
|
-
}), { abortSignal: this.abort.signal });
|
|
156
|
-
return out.Messages ?? [];
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
if (!this.running)
|
|
160
|
-
return [];
|
|
161
|
-
console.error('[SqsTransport] receive failed', err);
|
|
162
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
shouldSkip(msg) {
|
|
167
|
-
return !!this.marker && !msg.MessageAttributes?.[this.marker];
|
|
168
|
-
}
|
|
169
|
-
async del(QueueUrl, ReceiptHandle) {
|
|
170
|
-
if (!ReceiptHandle)
|
|
171
|
-
return;
|
|
172
|
-
await this.client.send(new client_sqs_1.DeleteMessageCommand({ QueueUrl, ReceiptHandle }));
|
|
173
|
-
}
|
|
174
|
-
async release(QueueUrl, ReceiptHandle) {
|
|
175
|
-
if (!ReceiptHandle)
|
|
176
|
-
return;
|
|
177
|
-
await this.client
|
|
178
|
-
.send(new client_sqs_1.ChangeMessageVisibilityCommand({ QueueUrl, ReceiptHandle, VisibilityTimeout: 0 }))
|
|
179
|
-
.catch(() => { });
|
|
180
|
-
}
|
|
181
|
-
/** Stop the pollers and (if we created it) destroy the SQS client so the process can exit. */
|
|
182
|
-
async close() {
|
|
183
|
-
this.running = false;
|
|
184
|
-
this.abort.abort();
|
|
185
|
-
await Promise.allSettled([this.taskLoop, this.resultLoop]);
|
|
186
|
-
if (this.ownsClient)
|
|
187
|
-
this.client.destroy();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
exports.SqsTransport = SqsTransport;
|
|
191
|
-
//# sourceMappingURL=sqs-transport.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sqs-transport.js","sourceRoot":"","sources":["../src/sqs-transport.ts"],"names":[],"mappings":"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"}
|