@dudousxd/nestjs-durable-transport-sqs 0.2.1 → 0.3.1
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/dist/index.cjs +252 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -6
- package/dist/index.d.ts +45 -6
- package/dist/index.js +253 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -28,7 +28,10 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
// src/sqs-transport.ts
|
|
29
29
|
var import_client_sqs = require("@aws-sdk/client-sqs");
|
|
30
30
|
var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
|
|
31
|
+
var SQS_BATCH_MAX = 10;
|
|
31
32
|
var RESULT_VISIBILITY_SEC = 30;
|
|
33
|
+
var RESULT_RECEIVERS = 8;
|
|
34
|
+
var TASK_BATCHES_IN_FLIGHT = 10;
|
|
32
35
|
var SqsTransport = class {
|
|
33
36
|
static {
|
|
34
37
|
__name(this, "SqsTransport");
|
|
@@ -43,6 +46,8 @@ var SqsTransport = class {
|
|
|
43
46
|
visibilityTimeoutSec;
|
|
44
47
|
taskQueueUrlResolver;
|
|
45
48
|
resultsQueueUrlOption;
|
|
49
|
+
runsQueueUrlResolver;
|
|
50
|
+
runConsumers = /* @__PURE__ */ new Set();
|
|
46
51
|
onError;
|
|
47
52
|
handlers = /* @__PURE__ */ new Map();
|
|
48
53
|
urlCache = /* @__PURE__ */ new Map();
|
|
@@ -63,6 +68,7 @@ var SqsTransport = class {
|
|
|
63
68
|
this.visibilityTimeoutSec = options.visibilityTimeoutSec ?? 60;
|
|
64
69
|
this.taskQueueUrlResolver = options.taskQueueUrl;
|
|
65
70
|
this.resultsQueueUrlOption = options.resultsQueueUrl;
|
|
71
|
+
this.runsQueueUrlResolver = options.runsQueueUrl;
|
|
66
72
|
this.onError = options.onError ?? ((err) => console.error("[SqsTransport]", err));
|
|
67
73
|
}
|
|
68
74
|
// ── queue resolution ───────────────────────────────────────────────────────────────────────
|
|
@@ -107,6 +113,175 @@ var SqsTransport = class {
|
|
|
107
113
|
if (typeof opt === "function") return opt();
|
|
108
114
|
return this.resolveByName(`${this.prefix}-results`);
|
|
109
115
|
}
|
|
116
|
+
async runsUrl(namespace) {
|
|
117
|
+
if (this.runsQueueUrlResolver) return this.runsQueueUrlResolver(namespace);
|
|
118
|
+
const name = namespace ? `${this.prefix}-runs-${namespace}` : `${this.prefix}-runs`;
|
|
119
|
+
return this.resolveByName(name.replace(/[^A-Za-z0-9_-]/g, "-"));
|
|
120
|
+
}
|
|
121
|
+
// ── run queue (engine ↔ engine) ─────────────────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Queue `pending` runs for whichever engine instance receives them first, ten per
|
|
124
|
+
* `SendMessageBatch`. SQS is at-least-once and a standard queue does not deduplicate, so a run may
|
|
125
|
+
* be received twice; the engine skips a run that is no longer due. Throws if any entry failed.
|
|
126
|
+
*/
|
|
127
|
+
async dispatchRuns(runs, opts) {
|
|
128
|
+
if (runs.length === 0) return;
|
|
129
|
+
const QueueUrl = await this.runsUrl(opts?.namespace);
|
|
130
|
+
const fifo = QueueUrl.endsWith(".fifo");
|
|
131
|
+
for (let at = 0; at < runs.length; at += SQS_BATCH_MAX) {
|
|
132
|
+
const chunk = runs.slice(at, at + SQS_BATCH_MAX);
|
|
133
|
+
const out = await this.client.send(new import_client_sqs.SendMessageBatchCommand({
|
|
134
|
+
QueueUrl,
|
|
135
|
+
Entries: chunk.map((run, i) => ({
|
|
136
|
+
Id: String(i),
|
|
137
|
+
MessageBody: JSON.stringify(run),
|
|
138
|
+
MessageAttributes: this.marker ? {
|
|
139
|
+
[this.marker]: {
|
|
140
|
+
DataType: "String",
|
|
141
|
+
StringValue: "1"
|
|
142
|
+
}
|
|
143
|
+
} : void 0,
|
|
144
|
+
// FIFO: one group per run (runs are independent; only one run's entries are ordered),
|
|
145
|
+
// and a fresh deduplication id, so a run queued again later is not dropped as a repeat.
|
|
146
|
+
...fifo ? {
|
|
147
|
+
MessageGroupId: run.runId.slice(0, 128),
|
|
148
|
+
MessageDeduplicationId: globalThis.crypto.randomUUID()
|
|
149
|
+
} : {}
|
|
150
|
+
}))
|
|
151
|
+
}));
|
|
152
|
+
if (out.Failed && out.Failed.length > 0) {
|
|
153
|
+
throw new Error(`SQS refused ${out.Failed.length} run(s): ${out.Failed.map((f) => f.Message ?? f.Code).join("; ")}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Receive from the run queue, at most `opts.concurrency` runs in flight on this instance. A
|
|
159
|
+
* message is deleted once its handler settles, and its visibility is extended meanwhile, so it
|
|
160
|
+
* reappears only after the instance holding it went down. The returned stop takes no more
|
|
161
|
+
* messages and resolves once the runs in flight settled.
|
|
162
|
+
*/
|
|
163
|
+
onDispatchedRun(handler, opts) {
|
|
164
|
+
const consumer = {
|
|
165
|
+
stopped: false,
|
|
166
|
+
abort: new AbortController(),
|
|
167
|
+
inflight: /* @__PURE__ */ new Set(),
|
|
168
|
+
slotFreed: void 0,
|
|
169
|
+
idle: [],
|
|
170
|
+
loop: Promise.resolve()
|
|
171
|
+
};
|
|
172
|
+
consumer.loop = this.consumeRuns(consumer, handler, Math.max(1, Math.floor(opts.concurrency)), opts.namespace).catch((err) => {
|
|
173
|
+
this.onError(err);
|
|
174
|
+
});
|
|
175
|
+
this.runConsumers.add(consumer);
|
|
176
|
+
return () => this.stopRunConsumer(consumer);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Stop taking messages and wait for the runs in flight. A receive still long-polling is not
|
|
180
|
+
* aborted, since SQS would keep what it hands an aborted request invisible for the visibility
|
|
181
|
+
* timeout: whatever it returns is released for another instance. `close()` aborts it.
|
|
182
|
+
*/
|
|
183
|
+
async stopRunConsumer(consumer, abort = false) {
|
|
184
|
+
consumer.stopped = true;
|
|
185
|
+
consumer.slotFreed?.();
|
|
186
|
+
if (abort) consumer.abort.abort();
|
|
187
|
+
if (consumer.inflight.size > 0) {
|
|
188
|
+
await new Promise((resolve) => consumer.idle.push(resolve));
|
|
189
|
+
}
|
|
190
|
+
if (abort) await consumer.loop;
|
|
191
|
+
this.runConsumers.delete(consumer);
|
|
192
|
+
}
|
|
193
|
+
async consumeRuns(consumer, handler, concurrency, namespace) {
|
|
194
|
+
let QueueUrl;
|
|
195
|
+
while (QueueUrl === void 0 && this.running && !consumer.stopped) {
|
|
196
|
+
try {
|
|
197
|
+
QueueUrl = await this.runsUrl(namespace);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
this.onError(err);
|
|
200
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (QueueUrl === void 0) return;
|
|
204
|
+
const url = QueueUrl;
|
|
205
|
+
const extend = setInterval(() => void this.extendRunVisibility(url, consumer), Math.max(1e3, Math.floor(this.visibilityTimeoutSec * 1e3 / 3)));
|
|
206
|
+
extend.unref?.();
|
|
207
|
+
const settled = /* @__PURE__ */ new Set();
|
|
208
|
+
try {
|
|
209
|
+
while (this.running && !consumer.stopped) {
|
|
210
|
+
const free = concurrency - consumer.inflight.size;
|
|
211
|
+
if (free <= 0) {
|
|
212
|
+
await new Promise((resolve) => {
|
|
213
|
+
consumer.slotFreed = resolve;
|
|
214
|
+
});
|
|
215
|
+
consumer.slotFreed = void 0;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const messages = await this.receiveRuns(url, Math.min(SQS_BATCH_MAX, free), consumer);
|
|
219
|
+
if (consumer.stopped) {
|
|
220
|
+
await Promise.allSettled(messages.map((m) => this.release(url, m.ReceiptHandle)));
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
for (const msg of messages) {
|
|
224
|
+
if (this.shouldSkip(msg)) {
|
|
225
|
+
await this.release(url, msg.ReceiptHandle);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const receipt = msg.ReceiptHandle;
|
|
229
|
+
if (!receipt) continue;
|
|
230
|
+
const run = JSON.parse(msg.Body ?? "{}");
|
|
231
|
+
if (typeof run.runId !== "string") {
|
|
232
|
+
await this.del(url, receipt).catch(() => void 0);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
consumer.inflight.add(receipt);
|
|
236
|
+
const done = handler(run).catch(() => void 0).then(() => this.del(url, receipt).catch(() => void 0)).finally(() => {
|
|
237
|
+
consumer.inflight.delete(receipt);
|
|
238
|
+
settled.delete(done);
|
|
239
|
+
consumer.slotFreed?.();
|
|
240
|
+
if (consumer.stopped && consumer.inflight.size === 0) {
|
|
241
|
+
for (const resolve of consumer.idle.splice(0)) resolve();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
settled.add(done);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
await Promise.allSettled([
|
|
248
|
+
...settled
|
|
249
|
+
]);
|
|
250
|
+
} finally {
|
|
251
|
+
clearInterval(extend);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async receiveRuns(QueueUrl, max, consumer) {
|
|
255
|
+
try {
|
|
256
|
+
const out = await this.client.send(new import_client_sqs.ReceiveMessageCommand({
|
|
257
|
+
QueueUrl,
|
|
258
|
+
MaxNumberOfMessages: max,
|
|
259
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
260
|
+
VisibilityTimeout: this.visibilityTimeoutSec,
|
|
261
|
+
MessageAttributeNames: this.marker ? [
|
|
262
|
+
"All"
|
|
263
|
+
] : void 0
|
|
264
|
+
}), {
|
|
265
|
+
abortSignal: consumer.abort.signal
|
|
266
|
+
});
|
|
267
|
+
return out.Messages ?? [];
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (!this.running || consumer.stopped) return [];
|
|
270
|
+
this.onError(err);
|
|
271
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Keep the messages of the runs in flight invisible, so a long turn is not received again. */
|
|
276
|
+
async extendRunVisibility(QueueUrl, consumer) {
|
|
277
|
+
await Promise.allSettled([
|
|
278
|
+
...consumer.inflight
|
|
279
|
+
].map((ReceiptHandle) => this.client.send(new import_client_sqs.ChangeMessageVisibilityCommand({
|
|
280
|
+
QueueUrl,
|
|
281
|
+
ReceiptHandle,
|
|
282
|
+
VisibilityTimeout: this.visibilityTimeoutSec
|
|
283
|
+
}))));
|
|
284
|
+
}
|
|
110
285
|
// ── engine → worker ────────────────────────────────────────────────────────────────────────
|
|
111
286
|
async dispatch(task) {
|
|
112
287
|
const QueueUrl = await this.tasksUrl(task.group);
|
|
@@ -121,6 +296,47 @@ var SqsTransport = class {
|
|
|
121
296
|
} : void 0
|
|
122
297
|
}));
|
|
123
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* {@link dispatch} for many tasks: ten per `SendMessageBatch`, a few batches at a time. Throws if
|
|
301
|
+
* any entry failed (the engine then dispatches the batch again one task at a time).
|
|
302
|
+
*/
|
|
303
|
+
async dispatchMany(tasks) {
|
|
304
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
305
|
+
for (const task of tasks) {
|
|
306
|
+
const grouped = byGroup.get(task.group);
|
|
307
|
+
if (grouped) grouped.push(task);
|
|
308
|
+
else byGroup.set(task.group, [
|
|
309
|
+
task
|
|
310
|
+
]);
|
|
311
|
+
}
|
|
312
|
+
for (const [group, grouped] of byGroup) {
|
|
313
|
+
const QueueUrl = await this.tasksUrl(group);
|
|
314
|
+
const batches = [];
|
|
315
|
+
for (let at = 0; at < grouped.length; at += SQS_BATCH_MAX) {
|
|
316
|
+
batches.push(grouped.slice(at, at + SQS_BATCH_MAX));
|
|
317
|
+
}
|
|
318
|
+
for (let at = 0; at < batches.length; at += TASK_BATCHES_IN_FLIGHT) {
|
|
319
|
+
await Promise.all(batches.slice(at, at + TASK_BATCHES_IN_FLIGHT).map(async (batch) => {
|
|
320
|
+
const out = await this.client.send(new import_client_sqs.SendMessageBatchCommand({
|
|
321
|
+
QueueUrl,
|
|
322
|
+
Entries: batch.map((task, i) => ({
|
|
323
|
+
Id: String(i),
|
|
324
|
+
MessageBody: JSON.stringify(task),
|
|
325
|
+
MessageAttributes: this.marker ? {
|
|
326
|
+
[this.marker]: {
|
|
327
|
+
DataType: "String",
|
|
328
|
+
StringValue: "1"
|
|
329
|
+
}
|
|
330
|
+
} : void 0
|
|
331
|
+
}))
|
|
332
|
+
}));
|
|
333
|
+
if (out.Failed && out.Failed.length > 0) {
|
|
334
|
+
throw new Error(`SQS refused ${out.Failed.length} task(s): ${out.Failed.map((f) => f.Message ?? f.Code).join("; ")}`);
|
|
335
|
+
}
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
124
340
|
// ── worker → engine ────────────────────────────────────────────────────────────────────────
|
|
125
341
|
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
126
342
|
handle(name, fn) {
|
|
@@ -129,19 +345,23 @@ var SqsTransport = class {
|
|
|
129
345
|
if (!this.taskLoop) this.taskLoop = this.pollTasks(this.group);
|
|
130
346
|
}
|
|
131
347
|
onResult(handler) {
|
|
132
|
-
if (
|
|
348
|
+
if (this.resultLoop) return;
|
|
349
|
+
this.resultLoop = Promise.all(Array.from({
|
|
350
|
+
length: RESULT_RECEIVERS
|
|
351
|
+
}, () => this.pollResults(handler))).then(() => void 0);
|
|
133
352
|
}
|
|
134
353
|
onHeartbeat(_handler) {
|
|
135
354
|
}
|
|
136
355
|
// ── polling loops ──────────────────────────────────────────────────────────────────────────
|
|
137
356
|
/**
|
|
138
|
-
* Receive a batch and hand each message to `each
|
|
139
|
-
* a throw is reported through `onError` and its
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* slow handler are never redelivered to
|
|
357
|
+
* Receive a batch and hand each message to `each` — one at a time, or all at once when
|
|
358
|
+
* `concurrent` — never letting a failure escape: a throw is reported through `onError` and its
|
|
359
|
+
* message is left on the queue (SQS redelivers it after the visibility timeout; a redrive policy
|
|
360
|
+
* DLQs one that keeps failing). Every message of the batch is kept invisible until it is handled
|
|
361
|
+
* (see {@link holdVisible}), so the ones waiting behind a slow handler are never redelivered to
|
|
362
|
+
* another consumer meanwhile.
|
|
143
363
|
*/
|
|
144
|
-
async pollLoop(queueUrl, visibilitySec, each) {
|
|
364
|
+
async pollLoop(queueUrl, visibilitySec, each, concurrent = false) {
|
|
145
365
|
while (this.running) {
|
|
146
366
|
let QueueUrl;
|
|
147
367
|
try {
|
|
@@ -155,20 +375,27 @@ var SqsTransport = class {
|
|
|
155
375
|
const messages = await this.receive(QueueUrl, visibilitySec);
|
|
156
376
|
const pending = new Set(messages);
|
|
157
377
|
const stopHolding = this.holdVisible(QueueUrl, pending, visibilitySec);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
try {
|
|
162
|
-
if (this.shouldSkip(msg)) {
|
|
163
|
-
pending.delete(msg);
|
|
164
|
-
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
await each(QueueUrl, msg);
|
|
168
|
-
} catch (err) {
|
|
169
|
-
this.onError(err);
|
|
170
|
-
} finally {
|
|
378
|
+
const handle = /* @__PURE__ */ __name(async (msg) => {
|
|
379
|
+
try {
|
|
380
|
+
if (this.shouldSkip(msg)) {
|
|
171
381
|
pending.delete(msg);
|
|
382
|
+
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
await each(QueueUrl, msg);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
this.onError(err);
|
|
388
|
+
} finally {
|
|
389
|
+
pending.delete(msg);
|
|
390
|
+
}
|
|
391
|
+
}, "handle");
|
|
392
|
+
try {
|
|
393
|
+
if (concurrent) {
|
|
394
|
+
if (this.running) await Promise.all(messages.map(handle));
|
|
395
|
+
} else {
|
|
396
|
+
for (const msg of messages) {
|
|
397
|
+
if (!this.running) break;
|
|
398
|
+
await handle(msg);
|
|
172
399
|
}
|
|
173
400
|
}
|
|
174
401
|
} finally {
|
|
@@ -230,7 +457,7 @@ var SqsTransport = class {
|
|
|
230
457
|
return this.pollLoop(() => this.resultsUrl(), RESULT_VISIBILITY_SEC, async (QueueUrl, msg) => {
|
|
231
458
|
await handler(this.parse(msg));
|
|
232
459
|
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
233
|
-
});
|
|
460
|
+
}, true);
|
|
234
461
|
}
|
|
235
462
|
/** Wait 1s, or until `close()` aborts. The abort listener is removed when the timer fires, so
|
|
236
463
|
* sustained failures never accumulate listeners on the shared signal. */
|
|
@@ -298,7 +525,10 @@ var SqsTransport = class {
|
|
|
298
525
|
this.abort.abort();
|
|
299
526
|
await Promise.allSettled([
|
|
300
527
|
this.taskLoop,
|
|
301
|
-
this.resultLoop
|
|
528
|
+
this.resultLoop,
|
|
529
|
+
...[
|
|
530
|
+
...this.runConsumers
|
|
531
|
+
].map((c) => this.stopRunConsumer(c, true))
|
|
302
532
|
]);
|
|
303
533
|
if (this.ownsClient) this.client.destroy();
|
|
304
534
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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 SQSClient,\n type SQSClientConfig,\n SendMessageCommand,\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 per group and cached (a rejected\n * resolution is not cached — the next call asks again). */\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 per group.\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 /**\n * Visibility timeout for a received task, in seconds. Default 60. Every message this instance has\n * received — the one running and the ones of the same batch waiting behind it — is kept invisible by\n * re-extending it (`ChangeMessageVisibility`) every third of this, so a step may run longer; only a\n * crashed instance's messages reappear, after at most this long.\n */\n visibilityTimeoutSec?: number;\n /**\n * Where a transport failure is reported: a failing receive, a result handler that throws, a result\n * send that fails, a message whose body isn't JSON. The message involved is NOT deleted: SQS\n * redelivers it after its visibility timeout, and a queue redrive policy (`maxReceiveCount` → DLQ)\n * moves one that keeps failing aside. Default `console.error`.\n */\n onError?: (err: unknown) => void;\n}\n\n/** A received message, as the poll loops use it. */\ninterface Received {\n Body?: string | undefined;\n ReceiptHandle?: string | undefined;\n MessageId?: string | undefined;\n MessageAttributes?: Record<string, unknown> | undefined;\n}\n\n/** The results queue's visibility timeout (seconds). */\nconst RESULT_VISIBILITY_SEC = 30;\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 private readonly onError: (err: unknown) => void;\n\n private readonly handlers = new Map<string, StepHandler>();\n private readonly urlCache = new Map<string, string>();\n /** `taskQueueUrl` resolutions per group (the promise, so concurrent dispatches share one call). */\n private readonly groupUrlCache = new Map<string, Promise<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 this.onError = options.onError ?? ((err) => console.error('[SqsTransport]', err));\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 const resolver = this.taskQueueUrlResolver;\n if (!resolver) return this.resolveByName(`${this.prefix}-tasks-${group}`);\n let url = this.groupUrlCache.get(group);\n if (!url) {\n url = Promise.resolve()\n .then(() => resolver(group))\n .catch((err) => {\n this.groupUrlCache.delete(group);\n throw err;\n });\n this.groupUrlCache.set(group, url);\n }\n return url;\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 /**\n * Receive a batch and hand each message to `each`, one at a time, never letting a failure escape:\n * a throw is reported through `onError` and its message is left on the queue (SQS redelivers it\n * after the visibility timeout; a redrive policy DLQs one that keeps failing). Every message of the\n * batch is kept invisible until it is handled (see {@link holdVisible}), so the ones waiting behind a\n * slow handler are never redelivered to another consumer meanwhile.\n */\n private async pollLoop(\n queueUrl: () => Promise<string>,\n visibilitySec: number,\n each: (QueueUrl: string, msg: Received) => Promise<void>,\n ): Promise<void> {\n while (this.running) {\n let QueueUrl: string;\n try {\n QueueUrl = await queueUrl();\n } catch (err) {\n if (!this.running) return;\n this.onError(err);\n await this.backoff();\n continue;\n }\n const messages = await this.receive(QueueUrl, visibilitySec);\n const pending = new Set<Received>(messages);\n const stopHolding = this.holdVisible(QueueUrl, pending, visibilitySec);\n try {\n for (const msg of messages) {\n if (!this.running) break; // close(): what is left reappears after its visibility timeout\n try {\n if (this.shouldSkip(msg)) {\n // Not ours (shared queue): release immediately so the legacy consumer can take it.\n pending.delete(msg);\n await this.release(QueueUrl, msg.ReceiptHandle);\n continue;\n }\n await each(QueueUrl, msg);\n } catch (err) {\n this.onError(err);\n } finally {\n pending.delete(msg);\n }\n }\n } finally {\n stopHolding();\n }\n }\n }\n\n /**\n * Keep `messages` invisible while they wait or run: every third of the visibility timeout, extend\n * each one still in the set back to the full timeout. Returns the stop function.\n */\n private holdVisible(\n QueueUrl: string,\n messages: Set<Received>,\n visibilitySec: number,\n ): () => void {\n if (messages.size === 0) return () => {};\n const intervalMs = Math.max(200, Math.floor((visibilitySec * 1000) / 3));\n let round: Promise<void> | undefined;\n const timer = setInterval(() => {\n if (round) return;\n round = Promise.all(\n [...messages].map((msg) =>\n msg.ReceiptHandle\n ? this.client\n .send(\n new ChangeMessageVisibilityCommand({\n QueueUrl,\n ReceiptHandle: msg.ReceiptHandle,\n VisibilityTimeout: visibilitySec,\n }),\n )\n .then(() => undefined)\n .catch((err) => {\n if (this.running) this.onError(err);\n })\n : undefined,\n ),\n ).then(() => {\n round = undefined;\n });\n }, intervalMs);\n timer.unref?.();\n return () => clearInterval(timer);\n }\n\n private parse<T>(msg: Received): T {\n try {\n return JSON.parse(msg.Body ?? '') as T;\n } catch {\n throw new Error(\n `[SqsTransport] message ${msg.MessageId ?? '?'} has a non-JSON body; leaving it for the queue's redrive policy`,\n );\n }\n }\n\n private pollTasks(group: string): Promise<void> {\n return this.pollLoop(\n () => this.tasksUrl(group),\n this.visibilityTimeoutSec,\n async (QueueUrl, msg) => {\n const task = this.parse<RemoteTask>(msg);\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 pollResults(handler: (result: StepResult) => Promise<void>): Promise<void> {\n return this.pollLoop(\n () => this.resultsUrl(),\n RESULT_VISIBILITY_SEC,\n async (QueueUrl, msg) => {\n await handler(this.parse<StepResult>(msg));\n await this.del(QueueUrl, msg.ReceiptHandle);\n },\n );\n }\n\n /** Wait 1s, or until `close()` aborts. The abort listener is removed when the timer fires, so\n * sustained failures never accumulate listeners on the shared signal. */\n private backoff(): Promise<void> {\n return new Promise((resolve) => {\n const signal = this.abort.signal;\n if (signal.aborted) return resolve();\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, 1000);\n signal.addEventListener('abort', onAbort, { once: true });\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): Promise<Received[]> {\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 this.onError(err);\n await this.backoff();\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":["RESULT_VISIBILITY_SEC","SqsTransport","client","ownsClient","group","prefix","autoCreate","marker","waitTimeSeconds","visibilityTimeoutSec","taskQueueUrlResolver","resultsQueueUrlOption","onError","handlers","Map","urlCache","groupUrlCache","running","abort","AbortController","taskLoop","resultLoop","options","SQSClient","clientConfig","taskQueueUrl","resultsQueueUrl","err","console","error","resolveByName","name","cached","get","url","out","send","GetQueueUrlCommand","QueueName","QueueUrl","created","CreateQueueCommand","set","tasksUrl","resolver","Promise","resolve","then","catch","delete","resultsUrl","opt","dispatch","task","SendMessageCommand","MessageBody","JSON","stringify","MessageAttributes","DataType","StringValue","undefined","handle","fn","Error","pollTasks","onResult","handler","pollResults","onHeartbeat","_handler","pollLoop","queueUrl","visibilitySec","each","backoff","messages","receive","pending","Set","stopHolding","holdVisible","msg","shouldSkip","release","ReceiptHandle","size","intervalMs","Math","max","floor","round","timer","setInterval","all","map","ChangeMessageVisibilityCommand","VisibilityTimeout","unref","clearInterval","parse","Body","MessageId","result","runStepHandler","del","signal","aborted","onAbort","clearTimeout","setTimeout","removeEventListener","addEventListener","once","visibility","ReceiveMessageCommand","MaxNumberOfMessages","WaitTimeSeconds","MessageAttributeNames","abortSignal","Messages","DeleteMessageCommand","close","allSettled","destroy"]}
|
|
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 SQSClient,\n type SQSClientConfig,\n SendMessageBatchCommand,\n SendMessageCommand,\n} from '@aws-sdk/client-sqs';\nimport {\n type Heartbeat,\n type RemoteTask,\n type RunConsumerOptions,\n type RunDispatch,\n type RunQueueOptions,\n type StepHandler,\n type StepResult,\n type Transport,\n runStepHandler,\n} from '@dudousxd/nestjs-durable-core';\n\n/** SQS's limit on the entries of one `SendMessageBatch`. */\nconst SQS_BATCH_MAX = 10;\n\n/** A run-queue consumer started by {@link SqsTransport.onDispatchedRun}. */\ninterface RunConsumer {\n stopped: boolean;\n abort: AbortController;\n /** Receipt handles of the messages whose runs are in flight. */\n inflight: Set<string>;\n slotFreed: (() => void) | undefined;\n /** Resolved, once stopped, when the runs in flight settled. */\n idle: (() => void)[];\n loop: Promise<void>;\n}\n\n/** Resolve a group's tasks queue URL. Sync or async; resolved once per group and cached (a rejected\n * resolution is not cached — the next call asks again). */\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 per group.\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 /**\n * The run queue URL for an engine namespace (undefined for an engine that drives every\n * namespace): where the engine queues runs for whichever instance takes them first. When omitted,\n * the transport resolves `${prefix}-runs` (or `${prefix}-runs-<namespace>`) by name, creating it\n * if `autoCreate`. A FIFO queue (`.fifo`) gets one message group per run.\n */\n runsQueueUrl?: (namespace: string | undefined) => 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 /**\n * Visibility timeout for a received task, in seconds. Default 60. Every message this instance has\n * received — the one running and the ones of the same batch waiting behind it — is kept invisible by\n * re-extending it (`ChangeMessageVisibility`) every third of this, so a step may run longer; only a\n * crashed instance's messages reappear, after at most this long.\n */\n visibilityTimeoutSec?: number;\n /**\n * Where a transport failure is reported: a failing receive, a result handler that throws, a result\n * send that fails, a message whose body isn't JSON. The message involved is NOT deleted: SQS\n * redelivers it after its visibility timeout, and a queue redrive policy (`maxReceiveCount` → DLQ)\n * moves one that keeps failing aside. Default `console.error`.\n */\n onError?: (err: unknown) => void;\n}\n\n/** A received message, as the poll loops use it. */\ninterface Received {\n Body?: string | undefined;\n ReceiptHandle?: string | undefined;\n MessageId?: string | undefined;\n MessageAttributes?: Record<string, unknown> | undefined;\n}\n\n/** The results queue's visibility timeout (seconds). */\nconst RESULT_VISIBILITY_SEC = 30;\n\n/**\n * How many receive loops consume the results queue on one instance. Each hands every message of a\n * receive to the engine at once, so up to this many times ten results are in flight here: the\n * engine writes one run's waiting results together, so a wide fan settles in few writes.\n */\nconst RESULT_RECEIVERS = 8;\n\n/** `SendMessageBatch` calls {@link SqsTransport.dispatchMany} has in flight at once. */\nconst TASK_BATCHES_IN_FLIGHT = 10;\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 private readonly runsQueueUrlResolver?:\n | ((namespace: string | undefined) => string | Promise<string>)\n | undefined;\n private readonly runConsumers = new Set<RunConsumer>();\n private readonly onError: (err: unknown) => void;\n\n private readonly handlers = new Map<string, StepHandler>();\n private readonly urlCache = new Map<string, string>();\n /** `taskQueueUrl` resolutions per group (the promise, so concurrent dispatches share one call). */\n private readonly groupUrlCache = new Map<string, Promise<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 this.runsQueueUrlResolver = options.runsQueueUrl;\n this.onError = options.onError ?? ((err) => console.error('[SqsTransport]', err));\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 const resolver = this.taskQueueUrlResolver;\n if (!resolver) return this.resolveByName(`${this.prefix}-tasks-${group}`);\n let url = this.groupUrlCache.get(group);\n if (!url) {\n url = Promise.resolve()\n .then(() => resolver(group))\n .catch((err) => {\n this.groupUrlCache.delete(group);\n throw err;\n });\n this.groupUrlCache.set(group, url);\n }\n return url;\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 private async runsUrl(namespace: string | undefined): Promise<string> {\n if (this.runsQueueUrlResolver) return this.runsQueueUrlResolver(namespace);\n const name = namespace ? `${this.prefix}-runs-${namespace}` : `${this.prefix}-runs`;\n return this.resolveByName(name.replace(/[^A-Za-z0-9_-]/g, '-'));\n }\n\n // ── run queue (engine ↔ engine) ─────────────────────────────────────────────────────────────\n\n /**\n * Queue `pending` runs for whichever engine instance receives them first, ten per\n * `SendMessageBatch`. SQS is at-least-once and a standard queue does not deduplicate, so a run may\n * be received twice; the engine skips a run that is no longer due. Throws if any entry failed.\n */\n async dispatchRuns(runs: readonly RunDispatch[], opts?: RunQueueOptions): Promise<void> {\n if (runs.length === 0) return;\n const QueueUrl = await this.runsUrl(opts?.namespace);\n const fifo = QueueUrl.endsWith('.fifo');\n for (let at = 0; at < runs.length; at += SQS_BATCH_MAX) {\n const chunk = runs.slice(at, at + SQS_BATCH_MAX);\n const out = await this.client.send(\n new SendMessageBatchCommand({\n QueueUrl,\n Entries: chunk.map((run, i) => ({\n Id: String(i),\n MessageBody: JSON.stringify(run),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n // FIFO: one group per run (runs are independent; only one run's entries are ordered),\n // and a fresh deduplication id, so a run queued again later is not dropped as a repeat.\n ...(fifo\n ? {\n MessageGroupId: run.runId.slice(0, 128),\n MessageDeduplicationId: globalThis.crypto.randomUUID(),\n }\n : {}),\n })),\n }),\n );\n if (out.Failed && out.Failed.length > 0) {\n throw new Error(\n `SQS refused ${out.Failed.length} run(s): ${out.Failed.map((f) => f.Message ?? f.Code).join('; ')}`,\n );\n }\n }\n }\n\n /**\n * Receive from the run queue, at most `opts.concurrency` runs in flight on this instance. A\n * message is deleted once its handler settles, and its visibility is extended meanwhile, so it\n * reappears only after the instance holding it went down. The returned stop takes no more\n * messages and resolves once the runs in flight settled.\n */\n onDispatchedRun(\n handler: (run: RunDispatch) => Promise<void>,\n opts: RunConsumerOptions,\n ): () => Promise<void> {\n const consumer: RunConsumer = {\n stopped: false,\n abort: new AbortController(),\n inflight: new Set(),\n slotFreed: undefined,\n idle: [],\n loop: Promise.resolve(),\n };\n consumer.loop = this.consumeRuns(\n consumer,\n handler,\n Math.max(1, Math.floor(opts.concurrency)),\n opts.namespace,\n ).catch((err) => {\n this.onError(err);\n });\n this.runConsumers.add(consumer);\n return () => this.stopRunConsumer(consumer);\n }\n\n /**\n * Stop taking messages and wait for the runs in flight. A receive still long-polling is not\n * aborted, since SQS would keep what it hands an aborted request invisible for the visibility\n * timeout: whatever it returns is released for another instance. `close()` aborts it.\n */\n private async stopRunConsumer(consumer: RunConsumer, abort = false): Promise<void> {\n consumer.stopped = true;\n consumer.slotFreed?.();\n if (abort) consumer.abort.abort();\n if (consumer.inflight.size > 0) {\n await new Promise<void>((resolve) => consumer.idle.push(resolve));\n }\n if (abort) await consumer.loop;\n this.runConsumers.delete(consumer);\n }\n\n private async consumeRuns(\n consumer: RunConsumer,\n handler: (run: RunDispatch) => Promise<void>,\n concurrency: number,\n namespace: string | undefined,\n ): Promise<void> {\n let QueueUrl: string | undefined;\n while (QueueUrl === undefined && this.running && !consumer.stopped) {\n try {\n QueueUrl = await this.runsUrl(namespace);\n } catch (err) {\n this.onError(err);\n await new Promise((r) => setTimeout(r, 1000));\n }\n }\n if (QueueUrl === undefined) return;\n const url = QueueUrl;\n const extend = setInterval(\n () => void this.extendRunVisibility(url, consumer),\n Math.max(1_000, Math.floor((this.visibilityTimeoutSec * 1000) / 3)),\n );\n extend.unref?.();\n const settled = new Set<Promise<unknown>>();\n try {\n while (this.running && !consumer.stopped) {\n const free = concurrency - consumer.inflight.size;\n if (free <= 0) {\n await new Promise<void>((resolve) => {\n consumer.slotFreed = resolve;\n });\n consumer.slotFreed = undefined;\n continue;\n }\n const messages = await this.receiveRuns(url, Math.min(SQS_BATCH_MAX, free), consumer);\n if (consumer.stopped) {\n await Promise.allSettled(messages.map((m) => this.release(url, m.ReceiptHandle)));\n break;\n }\n for (const msg of messages) {\n if (this.shouldSkip(msg)) {\n await this.release(url, msg.ReceiptHandle);\n continue;\n }\n const receipt = msg.ReceiptHandle;\n if (!receipt) continue;\n const run = JSON.parse(msg.Body ?? '{}') as Partial<RunDispatch>;\n if (typeof run.runId !== 'string') {\n await this.del(url, receipt).catch(() => undefined);\n continue;\n }\n consumer.inflight.add(receipt);\n const done = handler(run as RunDispatch)\n .catch(() => undefined)\n .then(() => this.del(url, receipt).catch(() => undefined))\n .finally(() => {\n consumer.inflight.delete(receipt);\n settled.delete(done);\n consumer.slotFreed?.();\n if (consumer.stopped && consumer.inflight.size === 0) {\n for (const resolve of consumer.idle.splice(0)) resolve();\n }\n });\n settled.add(done);\n }\n }\n await Promise.allSettled([...settled]);\n } finally {\n clearInterval(extend);\n }\n }\n\n private async receiveRuns(QueueUrl: string, max: number, consumer: RunConsumer) {\n try {\n const out = await this.client.send(\n new ReceiveMessageCommand({\n QueueUrl,\n MaxNumberOfMessages: max,\n WaitTimeSeconds: this.waitTimeSeconds,\n VisibilityTimeout: this.visibilityTimeoutSec,\n MessageAttributeNames: this.marker ? ['All'] : undefined,\n }),\n { abortSignal: consumer.abort.signal },\n );\n return out.Messages ?? [];\n } catch (err) {\n if (!this.running || consumer.stopped) return [];\n this.onError(err);\n await new Promise((r) => setTimeout(r, 1000));\n return [];\n }\n }\n\n /** Keep the messages of the runs in flight invisible, so a long turn is not received again. */\n private async extendRunVisibility(QueueUrl: string, consumer: RunConsumer): Promise<void> {\n await Promise.allSettled(\n [...consumer.inflight].map((ReceiptHandle) =>\n this.client.send(\n new ChangeMessageVisibilityCommand({\n QueueUrl,\n ReceiptHandle,\n VisibilityTimeout: this.visibilityTimeoutSec,\n }),\n ),\n ),\n );\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 /**\n * {@link dispatch} for many tasks: ten per `SendMessageBatch`, a few batches at a time. Throws if\n * any entry failed (the engine then dispatches the batch again one task at a time).\n */\n async dispatchMany(tasks: RemoteTask[]): Promise<void> {\n const byGroup = new Map<string, RemoteTask[]>();\n for (const task of tasks) {\n const grouped = byGroup.get(task.group);\n if (grouped) grouped.push(task);\n else byGroup.set(task.group, [task]);\n }\n for (const [group, grouped] of byGroup) {\n const QueueUrl = await this.tasksUrl(group);\n const batches: RemoteTask[][] = [];\n for (let at = 0; at < grouped.length; at += SQS_BATCH_MAX) {\n batches.push(grouped.slice(at, at + SQS_BATCH_MAX));\n }\n for (let at = 0; at < batches.length; at += TASK_BATCHES_IN_FLIGHT) {\n await Promise.all(\n batches.slice(at, at + TASK_BATCHES_IN_FLIGHT).map(async (batch) => {\n const out = await this.client.send(\n new SendMessageBatchCommand({\n QueueUrl,\n Entries: batch.map((task, i) => ({\n Id: String(i),\n MessageBody: JSON.stringify(task),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n })),\n }),\n );\n if (out.Failed && out.Failed.length > 0) {\n throw new Error(\n `SQS refused ${out.Failed.length} task(s): ${out.Failed.map((f) => f.Message ?? f.Code).join('; ')}`,\n );\n }\n }),\n );\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) return;\n this.resultLoop = Promise.all(\n Array.from({ length: RESULT_RECEIVERS }, () => this.pollResults(handler)),\n ).then(() => undefined);\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 /**\n * Receive a batch and hand each message to `each` — one at a time, or all at once when\n * `concurrent` — never letting a failure escape: a throw is reported through `onError` and its\n * message is left on the queue (SQS redelivers it after the visibility timeout; a redrive policy\n * DLQs one that keeps failing). Every message of the batch is kept invisible until it is handled\n * (see {@link holdVisible}), so the ones waiting behind a slow handler are never redelivered to\n * another consumer meanwhile.\n */\n private async pollLoop(\n queueUrl: () => Promise<string>,\n visibilitySec: number,\n each: (QueueUrl: string, msg: Received) => Promise<void>,\n concurrent = false,\n ): Promise<void> {\n while (this.running) {\n let QueueUrl: string;\n try {\n QueueUrl = await queueUrl();\n } catch (err) {\n if (!this.running) return;\n this.onError(err);\n await this.backoff();\n continue;\n }\n const messages = await this.receive(QueueUrl, visibilitySec);\n const pending = new Set<Received>(messages);\n const stopHolding = this.holdVisible(QueueUrl, pending, visibilitySec);\n const handle = async (msg: Received): Promise<void> => {\n try {\n if (this.shouldSkip(msg)) {\n // Not ours (shared queue): release immediately so the legacy consumer can take it.\n pending.delete(msg);\n await this.release(QueueUrl, msg.ReceiptHandle);\n return;\n }\n await each(QueueUrl, msg);\n } catch (err) {\n this.onError(err);\n } finally {\n pending.delete(msg);\n }\n };\n try {\n if (concurrent) {\n if (this.running) await Promise.all(messages.map(handle));\n } else {\n for (const msg of messages) {\n if (!this.running) break; // close(): what is left reappears after its visibility timeout\n await handle(msg);\n }\n }\n } finally {\n stopHolding();\n }\n }\n }\n\n /**\n * Keep `messages` invisible while they wait or run: every third of the visibility timeout, extend\n * each one still in the set back to the full timeout. Returns the stop function.\n */\n private holdVisible(\n QueueUrl: string,\n messages: Set<Received>,\n visibilitySec: number,\n ): () => void {\n if (messages.size === 0) return () => {};\n const intervalMs = Math.max(200, Math.floor((visibilitySec * 1000) / 3));\n let round: Promise<void> | undefined;\n const timer = setInterval(() => {\n if (round) return;\n round = Promise.all(\n [...messages].map((msg) =>\n msg.ReceiptHandle\n ? this.client\n .send(\n new ChangeMessageVisibilityCommand({\n QueueUrl,\n ReceiptHandle: msg.ReceiptHandle,\n VisibilityTimeout: visibilitySec,\n }),\n )\n .then(() => undefined)\n .catch((err) => {\n if (this.running) this.onError(err);\n })\n : undefined,\n ),\n ).then(() => {\n round = undefined;\n });\n }, intervalMs);\n timer.unref?.();\n return () => clearInterval(timer);\n }\n\n private parse<T>(msg: Received): T {\n try {\n return JSON.parse(msg.Body ?? '') as T;\n } catch {\n throw new Error(\n `[SqsTransport] message ${msg.MessageId ?? '?'} has a non-JSON body; leaving it for the queue's redrive policy`,\n );\n }\n }\n\n private pollTasks(group: string): Promise<void> {\n return this.pollLoop(\n () => this.tasksUrl(group),\n this.visibilityTimeoutSec,\n async (QueueUrl, msg) => {\n const task = this.parse<RemoteTask>(msg);\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 pollResults(handler: (result: StepResult) => Promise<void>): Promise<void> {\n return this.pollLoop(\n () => this.resultsUrl(),\n RESULT_VISIBILITY_SEC,\n async (QueueUrl, msg) => {\n await handler(this.parse<StepResult>(msg));\n await this.del(QueueUrl, msg.ReceiptHandle);\n },\n true,\n );\n }\n\n /** Wait 1s, or until `close()` aborts. The abort listener is removed when the timer fires, so\n * sustained failures never accumulate listeners on the shared signal. */\n private backoff(): Promise<void> {\n return new Promise((resolve) => {\n const signal = this.abort.signal;\n if (signal.aborted) return resolve();\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, 1000);\n signal.addEventListener('abort', onAbort, { once: true });\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): Promise<Received[]> {\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 this.onError(err);\n await this.backoff();\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([\n this.taskLoop,\n this.resultLoop,\n ...[...this.runConsumers].map((c) => this.stopRunConsumer(c, true)),\n ]);\n if (this.ownsClient) this.client.destroy();\n }\n}\n"],"mappings":"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","names":["SQS_BATCH_MAX","RESULT_VISIBILITY_SEC","RESULT_RECEIVERS","TASK_BATCHES_IN_FLIGHT","SqsTransport","client","ownsClient","group","prefix","autoCreate","marker","waitTimeSeconds","visibilityTimeoutSec","taskQueueUrlResolver","resultsQueueUrlOption","runsQueueUrlResolver","runConsumers","Set","onError","handlers","Map","urlCache","groupUrlCache","running","abort","AbortController","taskLoop","resultLoop","options","SQSClient","clientConfig","taskQueueUrl","resultsQueueUrl","runsQueueUrl","err","console","error","resolveByName","name","cached","get","url","out","send","GetQueueUrlCommand","QueueName","QueueUrl","created","CreateQueueCommand","set","tasksUrl","resolver","Promise","resolve","then","catch","delete","resultsUrl","opt","runsUrl","namespace","replace","dispatchRuns","runs","opts","length","fifo","endsWith","at","chunk","slice","SendMessageBatchCommand","Entries","map","run","i","Id","String","MessageBody","JSON","stringify","MessageAttributes","DataType","StringValue","undefined","MessageGroupId","runId","MessageDeduplicationId","globalThis","crypto","randomUUID","Failed","Error","f","Message","Code","join","onDispatchedRun","handler","consumer","stopped","inflight","slotFreed","idle","loop","consumeRuns","Math","max","floor","concurrency","add","stopRunConsumer","size","push","r","setTimeout","extend","setInterval","extendRunVisibility","unref","settled","free","messages","receiveRuns","min","allSettled","m","release","ReceiptHandle","msg","shouldSkip","receipt","parse","Body","del","done","finally","splice","clearInterval","ReceiveMessageCommand","MaxNumberOfMessages","WaitTimeSeconds","VisibilityTimeout","MessageAttributeNames","abortSignal","signal","Messages","ChangeMessageVisibilityCommand","dispatch","task","SendMessageCommand","dispatchMany","tasks","byGroup","grouped","batches","all","batch","handle","fn","pollTasks","onResult","Array","from","pollResults","onHeartbeat","_handler","pollLoop","queueUrl","visibilitySec","each","concurrent","backoff","receive","pending","stopHolding","holdVisible","intervalMs","round","timer","MessageId","result","runStepHandler","aborted","onAbort","clearTimeout","removeEventListener","addEventListener","once","visibility","DeleteMessageCommand","close","c","destroy"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs';
|
|
2
|
-
import { Transport, RemoteTask, StepHandler, StepResult, Heartbeat } from '@dudousxd/nestjs-durable-core';
|
|
2
|
+
import { Transport, RunDispatch, RunQueueOptions, RunConsumerOptions, RemoteTask, StepHandler, StepResult, Heartbeat } from '@dudousxd/nestjs-durable-core';
|
|
3
3
|
|
|
4
4
|
/** Resolve a group's tasks queue URL. Sync or async; resolved once per group and cached (a rejected
|
|
5
5
|
* resolution is not cached — the next call asks again). */
|
|
@@ -26,6 +26,13 @@ interface SqsTransportOptions {
|
|
|
26
26
|
* transport resolves `${prefix}-results` by name (and creates it if `autoCreate`).
|
|
27
27
|
*/
|
|
28
28
|
resultsQueueUrl?: string | (() => string | Promise<string>);
|
|
29
|
+
/**
|
|
30
|
+
* The run queue URL for an engine namespace (undefined for an engine that drives every
|
|
31
|
+
* namespace): where the engine queues runs for whichever instance takes them first. When omitted,
|
|
32
|
+
* the transport resolves `${prefix}-runs` (or `${prefix}-runs-<namespace>`) by name, creating it
|
|
33
|
+
* if `autoCreate`. A FIFO queue (`.fifo`) gets one message group per run.
|
|
34
|
+
*/
|
|
35
|
+
runsQueueUrl?: (namespace: string | undefined) => string | Promise<string>;
|
|
29
36
|
/** Prefix for the fallback `${prefix}-tasks-*` / `${prefix}-results` names. Default `durable`. */
|
|
30
37
|
prefix?: string;
|
|
31
38
|
/**
|
|
@@ -79,6 +86,8 @@ declare class SqsTransport implements Transport {
|
|
|
79
86
|
private readonly visibilityTimeoutSec;
|
|
80
87
|
private readonly taskQueueUrlResolver?;
|
|
81
88
|
private readonly resultsQueueUrlOption?;
|
|
89
|
+
private readonly runsQueueUrlResolver?;
|
|
90
|
+
private readonly runConsumers;
|
|
82
91
|
private readonly onError;
|
|
83
92
|
private readonly handlers;
|
|
84
93
|
private readonly urlCache;
|
|
@@ -92,17 +101,47 @@ declare class SqsTransport implements Transport {
|
|
|
92
101
|
private resolveByName;
|
|
93
102
|
private tasksUrl;
|
|
94
103
|
private resultsUrl;
|
|
104
|
+
private runsUrl;
|
|
105
|
+
/**
|
|
106
|
+
* Queue `pending` runs for whichever engine instance receives them first, ten per
|
|
107
|
+
* `SendMessageBatch`. SQS is at-least-once and a standard queue does not deduplicate, so a run may
|
|
108
|
+
* be received twice; the engine skips a run that is no longer due. Throws if any entry failed.
|
|
109
|
+
*/
|
|
110
|
+
dispatchRuns(runs: readonly RunDispatch[], opts?: RunQueueOptions): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Receive from the run queue, at most `opts.concurrency` runs in flight on this instance. A
|
|
113
|
+
* message is deleted once its handler settles, and its visibility is extended meanwhile, so it
|
|
114
|
+
* reappears only after the instance holding it went down. The returned stop takes no more
|
|
115
|
+
* messages and resolves once the runs in flight settled.
|
|
116
|
+
*/
|
|
117
|
+
onDispatchedRun(handler: (run: RunDispatch) => Promise<void>, opts: RunConsumerOptions): () => Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Stop taking messages and wait for the runs in flight. A receive still long-polling is not
|
|
120
|
+
* aborted, since SQS would keep what it hands an aborted request invisible for the visibility
|
|
121
|
+
* timeout: whatever it returns is released for another instance. `close()` aborts it.
|
|
122
|
+
*/
|
|
123
|
+
private stopRunConsumer;
|
|
124
|
+
private consumeRuns;
|
|
125
|
+
private receiveRuns;
|
|
126
|
+
/** Keep the messages of the runs in flight invisible, so a long turn is not received again. */
|
|
127
|
+
private extendRunVisibility;
|
|
95
128
|
dispatch(task: RemoteTask): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* {@link dispatch} for many tasks: ten per `SendMessageBatch`, a few batches at a time. Throws if
|
|
131
|
+
* any entry failed (the engine then dispatches the batch again one task at a time).
|
|
132
|
+
*/
|
|
133
|
+
dispatchMany(tasks: RemoteTask[]): Promise<void>;
|
|
96
134
|
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
97
135
|
handle(name: string, fn: StepHandler): void;
|
|
98
136
|
onResult(handler: (result: StepResult) => Promise<void>): void;
|
|
99
137
|
onHeartbeat(_handler: (beat: Heartbeat) => Promise<void>): void;
|
|
100
138
|
/**
|
|
101
|
-
* Receive a batch and hand each message to `each
|
|
102
|
-
* a throw is reported through `onError` and its
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* slow handler are never redelivered to
|
|
139
|
+
* Receive a batch and hand each message to `each` — one at a time, or all at once when
|
|
140
|
+
* `concurrent` — never letting a failure escape: a throw is reported through `onError` and its
|
|
141
|
+
* message is left on the queue (SQS redelivers it after the visibility timeout; a redrive policy
|
|
142
|
+
* DLQs one that keeps failing). Every message of the batch is kept invisible until it is handled
|
|
143
|
+
* (see {@link holdVisible}), so the ones waiting behind a slow handler are never redelivered to
|
|
144
|
+
* another consumer meanwhile.
|
|
106
145
|
*/
|
|
107
146
|
private pollLoop;
|
|
108
147
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs';
|
|
2
|
-
import { Transport, RemoteTask, StepHandler, StepResult, Heartbeat } from '@dudousxd/nestjs-durable-core';
|
|
2
|
+
import { Transport, RunDispatch, RunQueueOptions, RunConsumerOptions, RemoteTask, StepHandler, StepResult, Heartbeat } from '@dudousxd/nestjs-durable-core';
|
|
3
3
|
|
|
4
4
|
/** Resolve a group's tasks queue URL. Sync or async; resolved once per group and cached (a rejected
|
|
5
5
|
* resolution is not cached — the next call asks again). */
|
|
@@ -26,6 +26,13 @@ interface SqsTransportOptions {
|
|
|
26
26
|
* transport resolves `${prefix}-results` by name (and creates it if `autoCreate`).
|
|
27
27
|
*/
|
|
28
28
|
resultsQueueUrl?: string | (() => string | Promise<string>);
|
|
29
|
+
/**
|
|
30
|
+
* The run queue URL for an engine namespace (undefined for an engine that drives every
|
|
31
|
+
* namespace): where the engine queues runs for whichever instance takes them first. When omitted,
|
|
32
|
+
* the transport resolves `${prefix}-runs` (or `${prefix}-runs-<namespace>`) by name, creating it
|
|
33
|
+
* if `autoCreate`. A FIFO queue (`.fifo`) gets one message group per run.
|
|
34
|
+
*/
|
|
35
|
+
runsQueueUrl?: (namespace: string | undefined) => string | Promise<string>;
|
|
29
36
|
/** Prefix for the fallback `${prefix}-tasks-*` / `${prefix}-results` names. Default `durable`. */
|
|
30
37
|
prefix?: string;
|
|
31
38
|
/**
|
|
@@ -79,6 +86,8 @@ declare class SqsTransport implements Transport {
|
|
|
79
86
|
private readonly visibilityTimeoutSec;
|
|
80
87
|
private readonly taskQueueUrlResolver?;
|
|
81
88
|
private readonly resultsQueueUrlOption?;
|
|
89
|
+
private readonly runsQueueUrlResolver?;
|
|
90
|
+
private readonly runConsumers;
|
|
82
91
|
private readonly onError;
|
|
83
92
|
private readonly handlers;
|
|
84
93
|
private readonly urlCache;
|
|
@@ -92,17 +101,47 @@ declare class SqsTransport implements Transport {
|
|
|
92
101
|
private resolveByName;
|
|
93
102
|
private tasksUrl;
|
|
94
103
|
private resultsUrl;
|
|
104
|
+
private runsUrl;
|
|
105
|
+
/**
|
|
106
|
+
* Queue `pending` runs for whichever engine instance receives them first, ten per
|
|
107
|
+
* `SendMessageBatch`. SQS is at-least-once and a standard queue does not deduplicate, so a run may
|
|
108
|
+
* be received twice; the engine skips a run that is no longer due. Throws if any entry failed.
|
|
109
|
+
*/
|
|
110
|
+
dispatchRuns(runs: readonly RunDispatch[], opts?: RunQueueOptions): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Receive from the run queue, at most `opts.concurrency` runs in flight on this instance. A
|
|
113
|
+
* message is deleted once its handler settles, and its visibility is extended meanwhile, so it
|
|
114
|
+
* reappears only after the instance holding it went down. The returned stop takes no more
|
|
115
|
+
* messages and resolves once the runs in flight settled.
|
|
116
|
+
*/
|
|
117
|
+
onDispatchedRun(handler: (run: RunDispatch) => Promise<void>, opts: RunConsumerOptions): () => Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Stop taking messages and wait for the runs in flight. A receive still long-polling is not
|
|
120
|
+
* aborted, since SQS would keep what it hands an aborted request invisible for the visibility
|
|
121
|
+
* timeout: whatever it returns is released for another instance. `close()` aborts it.
|
|
122
|
+
*/
|
|
123
|
+
private stopRunConsumer;
|
|
124
|
+
private consumeRuns;
|
|
125
|
+
private receiveRuns;
|
|
126
|
+
/** Keep the messages of the runs in flight invisible, so a long turn is not received again. */
|
|
127
|
+
private extendRunVisibility;
|
|
95
128
|
dispatch(task: RemoteTask): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* {@link dispatch} for many tasks: ten per `SendMessageBatch`, a few batches at a time. Throws if
|
|
131
|
+
* any entry failed (the engine then dispatches the batch again one task at a time).
|
|
132
|
+
*/
|
|
133
|
+
dispatchMany(tasks: RemoteTask[]): Promise<void>;
|
|
96
134
|
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
97
135
|
handle(name: string, fn: StepHandler): void;
|
|
98
136
|
onResult(handler: (result: StepResult) => Promise<void>): void;
|
|
99
137
|
onHeartbeat(_handler: (beat: Heartbeat) => Promise<void>): void;
|
|
100
138
|
/**
|
|
101
|
-
* Receive a batch and hand each message to `each
|
|
102
|
-
* a throw is reported through `onError` and its
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* slow handler are never redelivered to
|
|
139
|
+
* Receive a batch and hand each message to `each` — one at a time, or all at once when
|
|
140
|
+
* `concurrent` — never letting a failure escape: a throw is reported through `onError` and its
|
|
141
|
+
* message is left on the queue (SQS redelivers it after the visibility timeout; a redrive policy
|
|
142
|
+
* DLQs one that keeps failing). Every message of the batch is kept invisible until it is handled
|
|
143
|
+
* (see {@link holdVisible}), so the ones waiting behind a slow handler are never redelivered to
|
|
144
|
+
* another consumer meanwhile.
|
|
106
145
|
*/
|
|
107
146
|
private pollLoop;
|
|
108
147
|
/**
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,12 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/sqs-transport.ts
|
|
5
|
-
import { ChangeMessageVisibilityCommand, CreateQueueCommand, DeleteMessageCommand, GetQueueUrlCommand, ReceiveMessageCommand, SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
5
|
+
import { ChangeMessageVisibilityCommand, CreateQueueCommand, DeleteMessageCommand, GetQueueUrlCommand, ReceiveMessageCommand, SQSClient, SendMessageBatchCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
6
6
|
import { runStepHandler } from "@dudousxd/nestjs-durable-core";
|
|
7
|
+
var SQS_BATCH_MAX = 10;
|
|
7
8
|
var RESULT_VISIBILITY_SEC = 30;
|
|
9
|
+
var RESULT_RECEIVERS = 8;
|
|
10
|
+
var TASK_BATCHES_IN_FLIGHT = 10;
|
|
8
11
|
var SqsTransport = class {
|
|
9
12
|
static {
|
|
10
13
|
__name(this, "SqsTransport");
|
|
@@ -19,6 +22,8 @@ var SqsTransport = class {
|
|
|
19
22
|
visibilityTimeoutSec;
|
|
20
23
|
taskQueueUrlResolver;
|
|
21
24
|
resultsQueueUrlOption;
|
|
25
|
+
runsQueueUrlResolver;
|
|
26
|
+
runConsumers = /* @__PURE__ */ new Set();
|
|
22
27
|
onError;
|
|
23
28
|
handlers = /* @__PURE__ */ new Map();
|
|
24
29
|
urlCache = /* @__PURE__ */ new Map();
|
|
@@ -39,6 +44,7 @@ var SqsTransport = class {
|
|
|
39
44
|
this.visibilityTimeoutSec = options.visibilityTimeoutSec ?? 60;
|
|
40
45
|
this.taskQueueUrlResolver = options.taskQueueUrl;
|
|
41
46
|
this.resultsQueueUrlOption = options.resultsQueueUrl;
|
|
47
|
+
this.runsQueueUrlResolver = options.runsQueueUrl;
|
|
42
48
|
this.onError = options.onError ?? ((err) => console.error("[SqsTransport]", err));
|
|
43
49
|
}
|
|
44
50
|
// ── queue resolution ───────────────────────────────────────────────────────────────────────
|
|
@@ -83,6 +89,175 @@ var SqsTransport = class {
|
|
|
83
89
|
if (typeof opt === "function") return opt();
|
|
84
90
|
return this.resolveByName(`${this.prefix}-results`);
|
|
85
91
|
}
|
|
92
|
+
async runsUrl(namespace) {
|
|
93
|
+
if (this.runsQueueUrlResolver) return this.runsQueueUrlResolver(namespace);
|
|
94
|
+
const name = namespace ? `${this.prefix}-runs-${namespace}` : `${this.prefix}-runs`;
|
|
95
|
+
return this.resolveByName(name.replace(/[^A-Za-z0-9_-]/g, "-"));
|
|
96
|
+
}
|
|
97
|
+
// ── run queue (engine ↔ engine) ─────────────────────────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Queue `pending` runs for whichever engine instance receives them first, ten per
|
|
100
|
+
* `SendMessageBatch`. SQS is at-least-once and a standard queue does not deduplicate, so a run may
|
|
101
|
+
* be received twice; the engine skips a run that is no longer due. Throws if any entry failed.
|
|
102
|
+
*/
|
|
103
|
+
async dispatchRuns(runs, opts) {
|
|
104
|
+
if (runs.length === 0) return;
|
|
105
|
+
const QueueUrl = await this.runsUrl(opts?.namespace);
|
|
106
|
+
const fifo = QueueUrl.endsWith(".fifo");
|
|
107
|
+
for (let at = 0; at < runs.length; at += SQS_BATCH_MAX) {
|
|
108
|
+
const chunk = runs.slice(at, at + SQS_BATCH_MAX);
|
|
109
|
+
const out = await this.client.send(new SendMessageBatchCommand({
|
|
110
|
+
QueueUrl,
|
|
111
|
+
Entries: chunk.map((run, i) => ({
|
|
112
|
+
Id: String(i),
|
|
113
|
+
MessageBody: JSON.stringify(run),
|
|
114
|
+
MessageAttributes: this.marker ? {
|
|
115
|
+
[this.marker]: {
|
|
116
|
+
DataType: "String",
|
|
117
|
+
StringValue: "1"
|
|
118
|
+
}
|
|
119
|
+
} : void 0,
|
|
120
|
+
// FIFO: one group per run (runs are independent; only one run's entries are ordered),
|
|
121
|
+
// and a fresh deduplication id, so a run queued again later is not dropped as a repeat.
|
|
122
|
+
...fifo ? {
|
|
123
|
+
MessageGroupId: run.runId.slice(0, 128),
|
|
124
|
+
MessageDeduplicationId: globalThis.crypto.randomUUID()
|
|
125
|
+
} : {}
|
|
126
|
+
}))
|
|
127
|
+
}));
|
|
128
|
+
if (out.Failed && out.Failed.length > 0) {
|
|
129
|
+
throw new Error(`SQS refused ${out.Failed.length} run(s): ${out.Failed.map((f) => f.Message ?? f.Code).join("; ")}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Receive from the run queue, at most `opts.concurrency` runs in flight on this instance. A
|
|
135
|
+
* message is deleted once its handler settles, and its visibility is extended meanwhile, so it
|
|
136
|
+
* reappears only after the instance holding it went down. The returned stop takes no more
|
|
137
|
+
* messages and resolves once the runs in flight settled.
|
|
138
|
+
*/
|
|
139
|
+
onDispatchedRun(handler, opts) {
|
|
140
|
+
const consumer = {
|
|
141
|
+
stopped: false,
|
|
142
|
+
abort: new AbortController(),
|
|
143
|
+
inflight: /* @__PURE__ */ new Set(),
|
|
144
|
+
slotFreed: void 0,
|
|
145
|
+
idle: [],
|
|
146
|
+
loop: Promise.resolve()
|
|
147
|
+
};
|
|
148
|
+
consumer.loop = this.consumeRuns(consumer, handler, Math.max(1, Math.floor(opts.concurrency)), opts.namespace).catch((err) => {
|
|
149
|
+
this.onError(err);
|
|
150
|
+
});
|
|
151
|
+
this.runConsumers.add(consumer);
|
|
152
|
+
return () => this.stopRunConsumer(consumer);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Stop taking messages and wait for the runs in flight. A receive still long-polling is not
|
|
156
|
+
* aborted, since SQS would keep what it hands an aborted request invisible for the visibility
|
|
157
|
+
* timeout: whatever it returns is released for another instance. `close()` aborts it.
|
|
158
|
+
*/
|
|
159
|
+
async stopRunConsumer(consumer, abort = false) {
|
|
160
|
+
consumer.stopped = true;
|
|
161
|
+
consumer.slotFreed?.();
|
|
162
|
+
if (abort) consumer.abort.abort();
|
|
163
|
+
if (consumer.inflight.size > 0) {
|
|
164
|
+
await new Promise((resolve) => consumer.idle.push(resolve));
|
|
165
|
+
}
|
|
166
|
+
if (abort) await consumer.loop;
|
|
167
|
+
this.runConsumers.delete(consumer);
|
|
168
|
+
}
|
|
169
|
+
async consumeRuns(consumer, handler, concurrency, namespace) {
|
|
170
|
+
let QueueUrl;
|
|
171
|
+
while (QueueUrl === void 0 && this.running && !consumer.stopped) {
|
|
172
|
+
try {
|
|
173
|
+
QueueUrl = await this.runsUrl(namespace);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
this.onError(err);
|
|
176
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (QueueUrl === void 0) return;
|
|
180
|
+
const url = QueueUrl;
|
|
181
|
+
const extend = setInterval(() => void this.extendRunVisibility(url, consumer), Math.max(1e3, Math.floor(this.visibilityTimeoutSec * 1e3 / 3)));
|
|
182
|
+
extend.unref?.();
|
|
183
|
+
const settled = /* @__PURE__ */ new Set();
|
|
184
|
+
try {
|
|
185
|
+
while (this.running && !consumer.stopped) {
|
|
186
|
+
const free = concurrency - consumer.inflight.size;
|
|
187
|
+
if (free <= 0) {
|
|
188
|
+
await new Promise((resolve) => {
|
|
189
|
+
consumer.slotFreed = resolve;
|
|
190
|
+
});
|
|
191
|
+
consumer.slotFreed = void 0;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const messages = await this.receiveRuns(url, Math.min(SQS_BATCH_MAX, free), consumer);
|
|
195
|
+
if (consumer.stopped) {
|
|
196
|
+
await Promise.allSettled(messages.map((m) => this.release(url, m.ReceiptHandle)));
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
for (const msg of messages) {
|
|
200
|
+
if (this.shouldSkip(msg)) {
|
|
201
|
+
await this.release(url, msg.ReceiptHandle);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const receipt = msg.ReceiptHandle;
|
|
205
|
+
if (!receipt) continue;
|
|
206
|
+
const run = JSON.parse(msg.Body ?? "{}");
|
|
207
|
+
if (typeof run.runId !== "string") {
|
|
208
|
+
await this.del(url, receipt).catch(() => void 0);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
consumer.inflight.add(receipt);
|
|
212
|
+
const done = handler(run).catch(() => void 0).then(() => this.del(url, receipt).catch(() => void 0)).finally(() => {
|
|
213
|
+
consumer.inflight.delete(receipt);
|
|
214
|
+
settled.delete(done);
|
|
215
|
+
consumer.slotFreed?.();
|
|
216
|
+
if (consumer.stopped && consumer.inflight.size === 0) {
|
|
217
|
+
for (const resolve of consumer.idle.splice(0)) resolve();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
settled.add(done);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
await Promise.allSettled([
|
|
224
|
+
...settled
|
|
225
|
+
]);
|
|
226
|
+
} finally {
|
|
227
|
+
clearInterval(extend);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async receiveRuns(QueueUrl, max, consumer) {
|
|
231
|
+
try {
|
|
232
|
+
const out = await this.client.send(new ReceiveMessageCommand({
|
|
233
|
+
QueueUrl,
|
|
234
|
+
MaxNumberOfMessages: max,
|
|
235
|
+
WaitTimeSeconds: this.waitTimeSeconds,
|
|
236
|
+
VisibilityTimeout: this.visibilityTimeoutSec,
|
|
237
|
+
MessageAttributeNames: this.marker ? [
|
|
238
|
+
"All"
|
|
239
|
+
] : void 0
|
|
240
|
+
}), {
|
|
241
|
+
abortSignal: consumer.abort.signal
|
|
242
|
+
});
|
|
243
|
+
return out.Messages ?? [];
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (!this.running || consumer.stopped) return [];
|
|
246
|
+
this.onError(err);
|
|
247
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Keep the messages of the runs in flight invisible, so a long turn is not received again. */
|
|
252
|
+
async extendRunVisibility(QueueUrl, consumer) {
|
|
253
|
+
await Promise.allSettled([
|
|
254
|
+
...consumer.inflight
|
|
255
|
+
].map((ReceiptHandle) => this.client.send(new ChangeMessageVisibilityCommand({
|
|
256
|
+
QueueUrl,
|
|
257
|
+
ReceiptHandle,
|
|
258
|
+
VisibilityTimeout: this.visibilityTimeoutSec
|
|
259
|
+
}))));
|
|
260
|
+
}
|
|
86
261
|
// ── engine → worker ────────────────────────────────────────────────────────────────────────
|
|
87
262
|
async dispatch(task) {
|
|
88
263
|
const QueueUrl = await this.tasksUrl(task.group);
|
|
@@ -97,6 +272,47 @@ var SqsTransport = class {
|
|
|
97
272
|
} : void 0
|
|
98
273
|
}));
|
|
99
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* {@link dispatch} for many tasks: ten per `SendMessageBatch`, a few batches at a time. Throws if
|
|
277
|
+
* any entry failed (the engine then dispatches the batch again one task at a time).
|
|
278
|
+
*/
|
|
279
|
+
async dispatchMany(tasks) {
|
|
280
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
281
|
+
for (const task of tasks) {
|
|
282
|
+
const grouped = byGroup.get(task.group);
|
|
283
|
+
if (grouped) grouped.push(task);
|
|
284
|
+
else byGroup.set(task.group, [
|
|
285
|
+
task
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
for (const [group, grouped] of byGroup) {
|
|
289
|
+
const QueueUrl = await this.tasksUrl(group);
|
|
290
|
+
const batches = [];
|
|
291
|
+
for (let at = 0; at < grouped.length; at += SQS_BATCH_MAX) {
|
|
292
|
+
batches.push(grouped.slice(at, at + SQS_BATCH_MAX));
|
|
293
|
+
}
|
|
294
|
+
for (let at = 0; at < batches.length; at += TASK_BATCHES_IN_FLIGHT) {
|
|
295
|
+
await Promise.all(batches.slice(at, at + TASK_BATCHES_IN_FLIGHT).map(async (batch) => {
|
|
296
|
+
const out = await this.client.send(new SendMessageBatchCommand({
|
|
297
|
+
QueueUrl,
|
|
298
|
+
Entries: batch.map((task, i) => ({
|
|
299
|
+
Id: String(i),
|
|
300
|
+
MessageBody: JSON.stringify(task),
|
|
301
|
+
MessageAttributes: this.marker ? {
|
|
302
|
+
[this.marker]: {
|
|
303
|
+
DataType: "String",
|
|
304
|
+
StringValue: "1"
|
|
305
|
+
}
|
|
306
|
+
} : void 0
|
|
307
|
+
}))
|
|
308
|
+
}));
|
|
309
|
+
if (out.Failed && out.Failed.length > 0) {
|
|
310
|
+
throw new Error(`SQS refused ${out.Failed.length} task(s): ${out.Failed.map((f) => f.Message ?? f.Code).join("; ")}`);
|
|
311
|
+
}
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
100
316
|
// ── worker → engine ────────────────────────────────────────────────────────────────────────
|
|
101
317
|
/** Register a step handler (worker side). Starts the group's task consumer on first call. */
|
|
102
318
|
handle(name, fn) {
|
|
@@ -105,19 +321,23 @@ var SqsTransport = class {
|
|
|
105
321
|
if (!this.taskLoop) this.taskLoop = this.pollTasks(this.group);
|
|
106
322
|
}
|
|
107
323
|
onResult(handler) {
|
|
108
|
-
if (
|
|
324
|
+
if (this.resultLoop) return;
|
|
325
|
+
this.resultLoop = Promise.all(Array.from({
|
|
326
|
+
length: RESULT_RECEIVERS
|
|
327
|
+
}, () => this.pollResults(handler))).then(() => void 0);
|
|
109
328
|
}
|
|
110
329
|
onHeartbeat(_handler) {
|
|
111
330
|
}
|
|
112
331
|
// ── polling loops ──────────────────────────────────────────────────────────────────────────
|
|
113
332
|
/**
|
|
114
|
-
* Receive a batch and hand each message to `each
|
|
115
|
-
* a throw is reported through `onError` and its
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* slow handler are never redelivered to
|
|
333
|
+
* Receive a batch and hand each message to `each` — one at a time, or all at once when
|
|
334
|
+
* `concurrent` — never letting a failure escape: a throw is reported through `onError` and its
|
|
335
|
+
* message is left on the queue (SQS redelivers it after the visibility timeout; a redrive policy
|
|
336
|
+
* DLQs one that keeps failing). Every message of the batch is kept invisible until it is handled
|
|
337
|
+
* (see {@link holdVisible}), so the ones waiting behind a slow handler are never redelivered to
|
|
338
|
+
* another consumer meanwhile.
|
|
119
339
|
*/
|
|
120
|
-
async pollLoop(queueUrl, visibilitySec, each) {
|
|
340
|
+
async pollLoop(queueUrl, visibilitySec, each, concurrent = false) {
|
|
121
341
|
while (this.running) {
|
|
122
342
|
let QueueUrl;
|
|
123
343
|
try {
|
|
@@ -131,20 +351,27 @@ var SqsTransport = class {
|
|
|
131
351
|
const messages = await this.receive(QueueUrl, visibilitySec);
|
|
132
352
|
const pending = new Set(messages);
|
|
133
353
|
const stopHolding = this.holdVisible(QueueUrl, pending, visibilitySec);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
try {
|
|
138
|
-
if (this.shouldSkip(msg)) {
|
|
139
|
-
pending.delete(msg);
|
|
140
|
-
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
await each(QueueUrl, msg);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
this.onError(err);
|
|
146
|
-
} finally {
|
|
354
|
+
const handle = /* @__PURE__ */ __name(async (msg) => {
|
|
355
|
+
try {
|
|
356
|
+
if (this.shouldSkip(msg)) {
|
|
147
357
|
pending.delete(msg);
|
|
358
|
+
await this.release(QueueUrl, msg.ReceiptHandle);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await each(QueueUrl, msg);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
this.onError(err);
|
|
364
|
+
} finally {
|
|
365
|
+
pending.delete(msg);
|
|
366
|
+
}
|
|
367
|
+
}, "handle");
|
|
368
|
+
try {
|
|
369
|
+
if (concurrent) {
|
|
370
|
+
if (this.running) await Promise.all(messages.map(handle));
|
|
371
|
+
} else {
|
|
372
|
+
for (const msg of messages) {
|
|
373
|
+
if (!this.running) break;
|
|
374
|
+
await handle(msg);
|
|
148
375
|
}
|
|
149
376
|
}
|
|
150
377
|
} finally {
|
|
@@ -206,7 +433,7 @@ var SqsTransport = class {
|
|
|
206
433
|
return this.pollLoop(() => this.resultsUrl(), RESULT_VISIBILITY_SEC, async (QueueUrl, msg) => {
|
|
207
434
|
await handler(this.parse(msg));
|
|
208
435
|
await this.del(QueueUrl, msg.ReceiptHandle);
|
|
209
|
-
});
|
|
436
|
+
}, true);
|
|
210
437
|
}
|
|
211
438
|
/** Wait 1s, or until `close()` aborts. The abort listener is removed when the timer fires, so
|
|
212
439
|
* sustained failures never accumulate listeners on the shared signal. */
|
|
@@ -274,7 +501,10 @@ var SqsTransport = class {
|
|
|
274
501
|
this.abort.abort();
|
|
275
502
|
await Promise.allSettled([
|
|
276
503
|
this.taskLoop,
|
|
277
|
-
this.resultLoop
|
|
504
|
+
this.resultLoop,
|
|
505
|
+
...[
|
|
506
|
+
...this.runConsumers
|
|
507
|
+
].map((c) => this.stopRunConsumer(c, true))
|
|
278
508
|
]);
|
|
279
509
|
if (this.ownsClient) this.client.destroy();
|
|
280
510
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sqs-transport.ts"],"sourcesContent":["import {\n ChangeMessageVisibilityCommand,\n CreateQueueCommand,\n DeleteMessageCommand,\n GetQueueUrlCommand,\n ReceiveMessageCommand,\n SQSClient,\n type SQSClientConfig,\n SendMessageCommand,\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 per group and cached (a rejected\n * resolution is not cached — the next call asks again). */\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 per group.\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 /**\n * Visibility timeout for a received task, in seconds. Default 60. Every message this instance has\n * received — the one running and the ones of the same batch waiting behind it — is kept invisible by\n * re-extending it (`ChangeMessageVisibility`) every third of this, so a step may run longer; only a\n * crashed instance's messages reappear, after at most this long.\n */\n visibilityTimeoutSec?: number;\n /**\n * Where a transport failure is reported: a failing receive, a result handler that throws, a result\n * send that fails, a message whose body isn't JSON. The message involved is NOT deleted: SQS\n * redelivers it after its visibility timeout, and a queue redrive policy (`maxReceiveCount` → DLQ)\n * moves one that keeps failing aside. Default `console.error`.\n */\n onError?: (err: unknown) => void;\n}\n\n/** A received message, as the poll loops use it. */\ninterface Received {\n Body?: string | undefined;\n ReceiptHandle?: string | undefined;\n MessageId?: string | undefined;\n MessageAttributes?: Record<string, unknown> | undefined;\n}\n\n/** The results queue's visibility timeout (seconds). */\nconst RESULT_VISIBILITY_SEC = 30;\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 private readonly onError: (err: unknown) => void;\n\n private readonly handlers = new Map<string, StepHandler>();\n private readonly urlCache = new Map<string, string>();\n /** `taskQueueUrl` resolutions per group (the promise, so concurrent dispatches share one call). */\n private readonly groupUrlCache = new Map<string, Promise<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 this.onError = options.onError ?? ((err) => console.error('[SqsTransport]', err));\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 const resolver = this.taskQueueUrlResolver;\n if (!resolver) return this.resolveByName(`${this.prefix}-tasks-${group}`);\n let url = this.groupUrlCache.get(group);\n if (!url) {\n url = Promise.resolve()\n .then(() => resolver(group))\n .catch((err) => {\n this.groupUrlCache.delete(group);\n throw err;\n });\n this.groupUrlCache.set(group, url);\n }\n return url;\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 /**\n * Receive a batch and hand each message to `each`, one at a time, never letting a failure escape:\n * a throw is reported through `onError` and its message is left on the queue (SQS redelivers it\n * after the visibility timeout; a redrive policy DLQs one that keeps failing). Every message of the\n * batch is kept invisible until it is handled (see {@link holdVisible}), so the ones waiting behind a\n * slow handler are never redelivered to another consumer meanwhile.\n */\n private async pollLoop(\n queueUrl: () => Promise<string>,\n visibilitySec: number,\n each: (QueueUrl: string, msg: Received) => Promise<void>,\n ): Promise<void> {\n while (this.running) {\n let QueueUrl: string;\n try {\n QueueUrl = await queueUrl();\n } catch (err) {\n if (!this.running) return;\n this.onError(err);\n await this.backoff();\n continue;\n }\n const messages = await this.receive(QueueUrl, visibilitySec);\n const pending = new Set<Received>(messages);\n const stopHolding = this.holdVisible(QueueUrl, pending, visibilitySec);\n try {\n for (const msg of messages) {\n if (!this.running) break; // close(): what is left reappears after its visibility timeout\n try {\n if (this.shouldSkip(msg)) {\n // Not ours (shared queue): release immediately so the legacy consumer can take it.\n pending.delete(msg);\n await this.release(QueueUrl, msg.ReceiptHandle);\n continue;\n }\n await each(QueueUrl, msg);\n } catch (err) {\n this.onError(err);\n } finally {\n pending.delete(msg);\n }\n }\n } finally {\n stopHolding();\n }\n }\n }\n\n /**\n * Keep `messages` invisible while they wait or run: every third of the visibility timeout, extend\n * each one still in the set back to the full timeout. Returns the stop function.\n */\n private holdVisible(\n QueueUrl: string,\n messages: Set<Received>,\n visibilitySec: number,\n ): () => void {\n if (messages.size === 0) return () => {};\n const intervalMs = Math.max(200, Math.floor((visibilitySec * 1000) / 3));\n let round: Promise<void> | undefined;\n const timer = setInterval(() => {\n if (round) return;\n round = Promise.all(\n [...messages].map((msg) =>\n msg.ReceiptHandle\n ? this.client\n .send(\n new ChangeMessageVisibilityCommand({\n QueueUrl,\n ReceiptHandle: msg.ReceiptHandle,\n VisibilityTimeout: visibilitySec,\n }),\n )\n .then(() => undefined)\n .catch((err) => {\n if (this.running) this.onError(err);\n })\n : undefined,\n ),\n ).then(() => {\n round = undefined;\n });\n }, intervalMs);\n timer.unref?.();\n return () => clearInterval(timer);\n }\n\n private parse<T>(msg: Received): T {\n try {\n return JSON.parse(msg.Body ?? '') as T;\n } catch {\n throw new Error(\n `[SqsTransport] message ${msg.MessageId ?? '?'} has a non-JSON body; leaving it for the queue's redrive policy`,\n );\n }\n }\n\n private pollTasks(group: string): Promise<void> {\n return this.pollLoop(\n () => this.tasksUrl(group),\n this.visibilityTimeoutSec,\n async (QueueUrl, msg) => {\n const task = this.parse<RemoteTask>(msg);\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 pollResults(handler: (result: StepResult) => Promise<void>): Promise<void> {\n return this.pollLoop(\n () => this.resultsUrl(),\n RESULT_VISIBILITY_SEC,\n async (QueueUrl, msg) => {\n await handler(this.parse<StepResult>(msg));\n await this.del(QueueUrl, msg.ReceiptHandle);\n },\n );\n }\n\n /** Wait 1s, or until `close()` aborts. The abort listener is removed when the timer fires, so\n * sustained failures never accumulate listeners on the shared signal. */\n private backoff(): Promise<void> {\n return new Promise((resolve) => {\n const signal = this.abort.signal;\n if (signal.aborted) return resolve();\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, 1000);\n signal.addEventListener('abort', onAbort, { once: true });\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): Promise<Received[]> {\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 this.onError(err);\n await this.backoff();\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","SQSClient","SendMessageCommand","runStepHandler","RESULT_VISIBILITY_SEC","SqsTransport","client","ownsClient","group","prefix","autoCreate","marker","waitTimeSeconds","visibilityTimeoutSec","taskQueueUrlResolver","resultsQueueUrlOption","onError","handlers","Map","urlCache","groupUrlCache","running","abort","AbortController","taskLoop","resultLoop","options","SQSClient","clientConfig","taskQueueUrl","resultsQueueUrl","err","console","error","resolveByName","name","cached","get","url","out","send","GetQueueUrlCommand","QueueName","QueueUrl","created","CreateQueueCommand","set","tasksUrl","resolver","Promise","resolve","then","catch","delete","resultsUrl","opt","dispatch","task","SendMessageCommand","MessageBody","JSON","stringify","MessageAttributes","DataType","StringValue","undefined","handle","fn","Error","pollTasks","onResult","handler","pollResults","onHeartbeat","_handler","pollLoop","queueUrl","visibilitySec","each","backoff","messages","receive","pending","Set","stopHolding","holdVisible","msg","shouldSkip","release","ReceiptHandle","size","intervalMs","Math","max","floor","round","timer","setInterval","all","map","ChangeMessageVisibilityCommand","VisibilityTimeout","unref","clearInterval","parse","Body","MessageId","result","runStepHandler","del","signal","aborted","onAbort","clearTimeout","setTimeout","removeEventListener","addEventListener","once","visibility","ReceiveMessageCommand","MaxNumberOfMessages","WaitTimeSeconds","MessageAttributeNames","abortSignal","Messages","DeleteMessageCommand","close","allSettled","destroy"]}
|
|
1
|
+
{"version":3,"sources":["../src/sqs-transport.ts"],"sourcesContent":["import {\n ChangeMessageVisibilityCommand,\n CreateQueueCommand,\n DeleteMessageCommand,\n GetQueueUrlCommand,\n ReceiveMessageCommand,\n SQSClient,\n type SQSClientConfig,\n SendMessageBatchCommand,\n SendMessageCommand,\n} from '@aws-sdk/client-sqs';\nimport {\n type Heartbeat,\n type RemoteTask,\n type RunConsumerOptions,\n type RunDispatch,\n type RunQueueOptions,\n type StepHandler,\n type StepResult,\n type Transport,\n runStepHandler,\n} from '@dudousxd/nestjs-durable-core';\n\n/** SQS's limit on the entries of one `SendMessageBatch`. */\nconst SQS_BATCH_MAX = 10;\n\n/** A run-queue consumer started by {@link SqsTransport.onDispatchedRun}. */\ninterface RunConsumer {\n stopped: boolean;\n abort: AbortController;\n /** Receipt handles of the messages whose runs are in flight. */\n inflight: Set<string>;\n slotFreed: (() => void) | undefined;\n /** Resolved, once stopped, when the runs in flight settled. */\n idle: (() => void)[];\n loop: Promise<void>;\n}\n\n/** Resolve a group's tasks queue URL. Sync or async; resolved once per group and cached (a rejected\n * resolution is not cached — the next call asks again). */\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 per group.\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 /**\n * The run queue URL for an engine namespace (undefined for an engine that drives every\n * namespace): where the engine queues runs for whichever instance takes them first. When omitted,\n * the transport resolves `${prefix}-runs` (or `${prefix}-runs-<namespace>`) by name, creating it\n * if `autoCreate`. A FIFO queue (`.fifo`) gets one message group per run.\n */\n runsQueueUrl?: (namespace: string | undefined) => 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 /**\n * Visibility timeout for a received task, in seconds. Default 60. Every message this instance has\n * received — the one running and the ones of the same batch waiting behind it — is kept invisible by\n * re-extending it (`ChangeMessageVisibility`) every third of this, so a step may run longer; only a\n * crashed instance's messages reappear, after at most this long.\n */\n visibilityTimeoutSec?: number;\n /**\n * Where a transport failure is reported: a failing receive, a result handler that throws, a result\n * send that fails, a message whose body isn't JSON. The message involved is NOT deleted: SQS\n * redelivers it after its visibility timeout, and a queue redrive policy (`maxReceiveCount` → DLQ)\n * moves one that keeps failing aside. Default `console.error`.\n */\n onError?: (err: unknown) => void;\n}\n\n/** A received message, as the poll loops use it. */\ninterface Received {\n Body?: string | undefined;\n ReceiptHandle?: string | undefined;\n MessageId?: string | undefined;\n MessageAttributes?: Record<string, unknown> | undefined;\n}\n\n/** The results queue's visibility timeout (seconds). */\nconst RESULT_VISIBILITY_SEC = 30;\n\n/**\n * How many receive loops consume the results queue on one instance. Each hands every message of a\n * receive to the engine at once, so up to this many times ten results are in flight here: the\n * engine writes one run's waiting results together, so a wide fan settles in few writes.\n */\nconst RESULT_RECEIVERS = 8;\n\n/** `SendMessageBatch` calls {@link SqsTransport.dispatchMany} has in flight at once. */\nconst TASK_BATCHES_IN_FLIGHT = 10;\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 private readonly runsQueueUrlResolver?:\n | ((namespace: string | undefined) => string | Promise<string>)\n | undefined;\n private readonly runConsumers = new Set<RunConsumer>();\n private readonly onError: (err: unknown) => void;\n\n private readonly handlers = new Map<string, StepHandler>();\n private readonly urlCache = new Map<string, string>();\n /** `taskQueueUrl` resolutions per group (the promise, so concurrent dispatches share one call). */\n private readonly groupUrlCache = new Map<string, Promise<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 this.runsQueueUrlResolver = options.runsQueueUrl;\n this.onError = options.onError ?? ((err) => console.error('[SqsTransport]', err));\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 const resolver = this.taskQueueUrlResolver;\n if (!resolver) return this.resolveByName(`${this.prefix}-tasks-${group}`);\n let url = this.groupUrlCache.get(group);\n if (!url) {\n url = Promise.resolve()\n .then(() => resolver(group))\n .catch((err) => {\n this.groupUrlCache.delete(group);\n throw err;\n });\n this.groupUrlCache.set(group, url);\n }\n return url;\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 private async runsUrl(namespace: string | undefined): Promise<string> {\n if (this.runsQueueUrlResolver) return this.runsQueueUrlResolver(namespace);\n const name = namespace ? `${this.prefix}-runs-${namespace}` : `${this.prefix}-runs`;\n return this.resolveByName(name.replace(/[^A-Za-z0-9_-]/g, '-'));\n }\n\n // ── run queue (engine ↔ engine) ─────────────────────────────────────────────────────────────\n\n /**\n * Queue `pending` runs for whichever engine instance receives them first, ten per\n * `SendMessageBatch`. SQS is at-least-once and a standard queue does not deduplicate, so a run may\n * be received twice; the engine skips a run that is no longer due. Throws if any entry failed.\n */\n async dispatchRuns(runs: readonly RunDispatch[], opts?: RunQueueOptions): Promise<void> {\n if (runs.length === 0) return;\n const QueueUrl = await this.runsUrl(opts?.namespace);\n const fifo = QueueUrl.endsWith('.fifo');\n for (let at = 0; at < runs.length; at += SQS_BATCH_MAX) {\n const chunk = runs.slice(at, at + SQS_BATCH_MAX);\n const out = await this.client.send(\n new SendMessageBatchCommand({\n QueueUrl,\n Entries: chunk.map((run, i) => ({\n Id: String(i),\n MessageBody: JSON.stringify(run),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n // FIFO: one group per run (runs are independent; only one run's entries are ordered),\n // and a fresh deduplication id, so a run queued again later is not dropped as a repeat.\n ...(fifo\n ? {\n MessageGroupId: run.runId.slice(0, 128),\n MessageDeduplicationId: globalThis.crypto.randomUUID(),\n }\n : {}),\n })),\n }),\n );\n if (out.Failed && out.Failed.length > 0) {\n throw new Error(\n `SQS refused ${out.Failed.length} run(s): ${out.Failed.map((f) => f.Message ?? f.Code).join('; ')}`,\n );\n }\n }\n }\n\n /**\n * Receive from the run queue, at most `opts.concurrency` runs in flight on this instance. A\n * message is deleted once its handler settles, and its visibility is extended meanwhile, so it\n * reappears only after the instance holding it went down. The returned stop takes no more\n * messages and resolves once the runs in flight settled.\n */\n onDispatchedRun(\n handler: (run: RunDispatch) => Promise<void>,\n opts: RunConsumerOptions,\n ): () => Promise<void> {\n const consumer: RunConsumer = {\n stopped: false,\n abort: new AbortController(),\n inflight: new Set(),\n slotFreed: undefined,\n idle: [],\n loop: Promise.resolve(),\n };\n consumer.loop = this.consumeRuns(\n consumer,\n handler,\n Math.max(1, Math.floor(opts.concurrency)),\n opts.namespace,\n ).catch((err) => {\n this.onError(err);\n });\n this.runConsumers.add(consumer);\n return () => this.stopRunConsumer(consumer);\n }\n\n /**\n * Stop taking messages and wait for the runs in flight. A receive still long-polling is not\n * aborted, since SQS would keep what it hands an aborted request invisible for the visibility\n * timeout: whatever it returns is released for another instance. `close()` aborts it.\n */\n private async stopRunConsumer(consumer: RunConsumer, abort = false): Promise<void> {\n consumer.stopped = true;\n consumer.slotFreed?.();\n if (abort) consumer.abort.abort();\n if (consumer.inflight.size > 0) {\n await new Promise<void>((resolve) => consumer.idle.push(resolve));\n }\n if (abort) await consumer.loop;\n this.runConsumers.delete(consumer);\n }\n\n private async consumeRuns(\n consumer: RunConsumer,\n handler: (run: RunDispatch) => Promise<void>,\n concurrency: number,\n namespace: string | undefined,\n ): Promise<void> {\n let QueueUrl: string | undefined;\n while (QueueUrl === undefined && this.running && !consumer.stopped) {\n try {\n QueueUrl = await this.runsUrl(namespace);\n } catch (err) {\n this.onError(err);\n await new Promise((r) => setTimeout(r, 1000));\n }\n }\n if (QueueUrl === undefined) return;\n const url = QueueUrl;\n const extend = setInterval(\n () => void this.extendRunVisibility(url, consumer),\n Math.max(1_000, Math.floor((this.visibilityTimeoutSec * 1000) / 3)),\n );\n extend.unref?.();\n const settled = new Set<Promise<unknown>>();\n try {\n while (this.running && !consumer.stopped) {\n const free = concurrency - consumer.inflight.size;\n if (free <= 0) {\n await new Promise<void>((resolve) => {\n consumer.slotFreed = resolve;\n });\n consumer.slotFreed = undefined;\n continue;\n }\n const messages = await this.receiveRuns(url, Math.min(SQS_BATCH_MAX, free), consumer);\n if (consumer.stopped) {\n await Promise.allSettled(messages.map((m) => this.release(url, m.ReceiptHandle)));\n break;\n }\n for (const msg of messages) {\n if (this.shouldSkip(msg)) {\n await this.release(url, msg.ReceiptHandle);\n continue;\n }\n const receipt = msg.ReceiptHandle;\n if (!receipt) continue;\n const run = JSON.parse(msg.Body ?? '{}') as Partial<RunDispatch>;\n if (typeof run.runId !== 'string') {\n await this.del(url, receipt).catch(() => undefined);\n continue;\n }\n consumer.inflight.add(receipt);\n const done = handler(run as RunDispatch)\n .catch(() => undefined)\n .then(() => this.del(url, receipt).catch(() => undefined))\n .finally(() => {\n consumer.inflight.delete(receipt);\n settled.delete(done);\n consumer.slotFreed?.();\n if (consumer.stopped && consumer.inflight.size === 0) {\n for (const resolve of consumer.idle.splice(0)) resolve();\n }\n });\n settled.add(done);\n }\n }\n await Promise.allSettled([...settled]);\n } finally {\n clearInterval(extend);\n }\n }\n\n private async receiveRuns(QueueUrl: string, max: number, consumer: RunConsumer) {\n try {\n const out = await this.client.send(\n new ReceiveMessageCommand({\n QueueUrl,\n MaxNumberOfMessages: max,\n WaitTimeSeconds: this.waitTimeSeconds,\n VisibilityTimeout: this.visibilityTimeoutSec,\n MessageAttributeNames: this.marker ? ['All'] : undefined,\n }),\n { abortSignal: consumer.abort.signal },\n );\n return out.Messages ?? [];\n } catch (err) {\n if (!this.running || consumer.stopped) return [];\n this.onError(err);\n await new Promise((r) => setTimeout(r, 1000));\n return [];\n }\n }\n\n /** Keep the messages of the runs in flight invisible, so a long turn is not received again. */\n private async extendRunVisibility(QueueUrl: string, consumer: RunConsumer): Promise<void> {\n await Promise.allSettled(\n [...consumer.inflight].map((ReceiptHandle) =>\n this.client.send(\n new ChangeMessageVisibilityCommand({\n QueueUrl,\n ReceiptHandle,\n VisibilityTimeout: this.visibilityTimeoutSec,\n }),\n ),\n ),\n );\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 /**\n * {@link dispatch} for many tasks: ten per `SendMessageBatch`, a few batches at a time. Throws if\n * any entry failed (the engine then dispatches the batch again one task at a time).\n */\n async dispatchMany(tasks: RemoteTask[]): Promise<void> {\n const byGroup = new Map<string, RemoteTask[]>();\n for (const task of tasks) {\n const grouped = byGroup.get(task.group);\n if (grouped) grouped.push(task);\n else byGroup.set(task.group, [task]);\n }\n for (const [group, grouped] of byGroup) {\n const QueueUrl = await this.tasksUrl(group);\n const batches: RemoteTask[][] = [];\n for (let at = 0; at < grouped.length; at += SQS_BATCH_MAX) {\n batches.push(grouped.slice(at, at + SQS_BATCH_MAX));\n }\n for (let at = 0; at < batches.length; at += TASK_BATCHES_IN_FLIGHT) {\n await Promise.all(\n batches.slice(at, at + TASK_BATCHES_IN_FLIGHT).map(async (batch) => {\n const out = await this.client.send(\n new SendMessageBatchCommand({\n QueueUrl,\n Entries: batch.map((task, i) => ({\n Id: String(i),\n MessageBody: JSON.stringify(task),\n MessageAttributes: this.marker\n ? { [this.marker]: { DataType: 'String', StringValue: '1' } }\n : undefined,\n })),\n }),\n );\n if (out.Failed && out.Failed.length > 0) {\n throw new Error(\n `SQS refused ${out.Failed.length} task(s): ${out.Failed.map((f) => f.Message ?? f.Code).join('; ')}`,\n );\n }\n }),\n );\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) return;\n this.resultLoop = Promise.all(\n Array.from({ length: RESULT_RECEIVERS }, () => this.pollResults(handler)),\n ).then(() => undefined);\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 /**\n * Receive a batch and hand each message to `each` — one at a time, or all at once when\n * `concurrent` — never letting a failure escape: a throw is reported through `onError` and its\n * message is left on the queue (SQS redelivers it after the visibility timeout; a redrive policy\n * DLQs one that keeps failing). Every message of the batch is kept invisible until it is handled\n * (see {@link holdVisible}), so the ones waiting behind a slow handler are never redelivered to\n * another consumer meanwhile.\n */\n private async pollLoop(\n queueUrl: () => Promise<string>,\n visibilitySec: number,\n each: (QueueUrl: string, msg: Received) => Promise<void>,\n concurrent = false,\n ): Promise<void> {\n while (this.running) {\n let QueueUrl: string;\n try {\n QueueUrl = await queueUrl();\n } catch (err) {\n if (!this.running) return;\n this.onError(err);\n await this.backoff();\n continue;\n }\n const messages = await this.receive(QueueUrl, visibilitySec);\n const pending = new Set<Received>(messages);\n const stopHolding = this.holdVisible(QueueUrl, pending, visibilitySec);\n const handle = async (msg: Received): Promise<void> => {\n try {\n if (this.shouldSkip(msg)) {\n // Not ours (shared queue): release immediately so the legacy consumer can take it.\n pending.delete(msg);\n await this.release(QueueUrl, msg.ReceiptHandle);\n return;\n }\n await each(QueueUrl, msg);\n } catch (err) {\n this.onError(err);\n } finally {\n pending.delete(msg);\n }\n };\n try {\n if (concurrent) {\n if (this.running) await Promise.all(messages.map(handle));\n } else {\n for (const msg of messages) {\n if (!this.running) break; // close(): what is left reappears after its visibility timeout\n await handle(msg);\n }\n }\n } finally {\n stopHolding();\n }\n }\n }\n\n /**\n * Keep `messages` invisible while they wait or run: every third of the visibility timeout, extend\n * each one still in the set back to the full timeout. Returns the stop function.\n */\n private holdVisible(\n QueueUrl: string,\n messages: Set<Received>,\n visibilitySec: number,\n ): () => void {\n if (messages.size === 0) return () => {};\n const intervalMs = Math.max(200, Math.floor((visibilitySec * 1000) / 3));\n let round: Promise<void> | undefined;\n const timer = setInterval(() => {\n if (round) return;\n round = Promise.all(\n [...messages].map((msg) =>\n msg.ReceiptHandle\n ? this.client\n .send(\n new ChangeMessageVisibilityCommand({\n QueueUrl,\n ReceiptHandle: msg.ReceiptHandle,\n VisibilityTimeout: visibilitySec,\n }),\n )\n .then(() => undefined)\n .catch((err) => {\n if (this.running) this.onError(err);\n })\n : undefined,\n ),\n ).then(() => {\n round = undefined;\n });\n }, intervalMs);\n timer.unref?.();\n return () => clearInterval(timer);\n }\n\n private parse<T>(msg: Received): T {\n try {\n return JSON.parse(msg.Body ?? '') as T;\n } catch {\n throw new Error(\n `[SqsTransport] message ${msg.MessageId ?? '?'} has a non-JSON body; leaving it for the queue's redrive policy`,\n );\n }\n }\n\n private pollTasks(group: string): Promise<void> {\n return this.pollLoop(\n () => this.tasksUrl(group),\n this.visibilityTimeoutSec,\n async (QueueUrl, msg) => {\n const task = this.parse<RemoteTask>(msg);\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 pollResults(handler: (result: StepResult) => Promise<void>): Promise<void> {\n return this.pollLoop(\n () => this.resultsUrl(),\n RESULT_VISIBILITY_SEC,\n async (QueueUrl, msg) => {\n await handler(this.parse<StepResult>(msg));\n await this.del(QueueUrl, msg.ReceiptHandle);\n },\n true,\n );\n }\n\n /** Wait 1s, or until `close()` aborts. The abort listener is removed when the timer fires, so\n * sustained failures never accumulate listeners on the shared signal. */\n private backoff(): Promise<void> {\n return new Promise((resolve) => {\n const signal = this.abort.signal;\n if (signal.aborted) return resolve();\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, 1000);\n signal.addEventListener('abort', onAbort, { once: true });\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): Promise<Received[]> {\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 this.onError(err);\n await this.backoff();\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([\n this.taskLoop,\n this.resultLoop,\n ...[...this.runConsumers].map((c) => this.stopRunConsumer(c, true)),\n ]);\n if (this.ownsClient) this.client.destroy();\n }\n}\n"],"mappings":"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","names":["ChangeMessageVisibilityCommand","CreateQueueCommand","DeleteMessageCommand","GetQueueUrlCommand","ReceiveMessageCommand","SQSClient","SendMessageBatchCommand","SendMessageCommand","runStepHandler","SQS_BATCH_MAX","RESULT_VISIBILITY_SEC","RESULT_RECEIVERS","TASK_BATCHES_IN_FLIGHT","SqsTransport","client","ownsClient","group","prefix","autoCreate","marker","waitTimeSeconds","visibilityTimeoutSec","taskQueueUrlResolver","resultsQueueUrlOption","runsQueueUrlResolver","runConsumers","Set","onError","handlers","Map","urlCache","groupUrlCache","running","abort","AbortController","taskLoop","resultLoop","options","SQSClient","clientConfig","taskQueueUrl","resultsQueueUrl","runsQueueUrl","err","console","error","resolveByName","name","cached","get","url","out","send","GetQueueUrlCommand","QueueName","QueueUrl","created","CreateQueueCommand","set","tasksUrl","resolver","Promise","resolve","then","catch","delete","resultsUrl","opt","runsUrl","namespace","replace","dispatchRuns","runs","opts","length","fifo","endsWith","at","chunk","slice","SendMessageBatchCommand","Entries","map","run","i","Id","String","MessageBody","JSON","stringify","MessageAttributes","DataType","StringValue","undefined","MessageGroupId","runId","MessageDeduplicationId","globalThis","crypto","randomUUID","Failed","Error","f","Message","Code","join","onDispatchedRun","handler","consumer","stopped","inflight","slotFreed","idle","loop","consumeRuns","Math","max","floor","concurrency","add","stopRunConsumer","size","push","r","setTimeout","extend","setInterval","extendRunVisibility","unref","settled","free","messages","receiveRuns","min","allSettled","m","release","ReceiptHandle","msg","shouldSkip","receipt","parse","Body","del","done","finally","splice","clearInterval","ReceiveMessageCommand","MaxNumberOfMessages","WaitTimeSeconds","VisibilityTimeout","MessageAttributeNames","abortSignal","signal","Messages","ChangeMessageVisibilityCommand","dispatch","task","SendMessageCommand","dispatchMany","tasks","byGroup","grouped","batches","all","batch","handle","fn","pollTasks","onResult","Array","from","pollResults","onHeartbeat","_handler","pollLoop","queueUrl","visibilitySec","each","concurrent","backoff","receive","pending","stopHolding","holdVisible","intervalMs","round","timer","MessageId","result","runStepHandler","aborted","onAbort","clearTimeout","removeEventListener","addEventListener","once","visibility","DeleteMessageCommand","close","c","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.3.1",
|
|
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"typescript": "5.9.3",
|
|
36
36
|
"zod": "3.25.76",
|
|
37
37
|
"tsup": "8.3.5",
|
|
38
|
-
"@dudousxd/nestjs-durable-core": "^0.
|
|
38
|
+
"@dudousxd/nestjs-durable-core": "^0.85.7"
|
|
39
39
|
},
|
|
40
40
|
"module": "./dist/index.js",
|
|
41
41
|
"exports": {
|