@parsrun/queue 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/dist/adapters/cloudflare.d.ts +94 -0
- package/dist/adapters/cloudflare.js +141 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +703 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +67 -0
- package/dist/adapters/memory.js +251 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/qstash.d.ts +110 -0
- package/dist/adapters/qstash.js +379 -0
- package/dist/adapters/qstash.js.map +1 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +884 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.js +47 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import {
|
|
3
|
+
type,
|
|
4
|
+
jobStatus,
|
|
5
|
+
job,
|
|
6
|
+
jobOptions,
|
|
7
|
+
addJobRequest,
|
|
8
|
+
jobProgressUpdate,
|
|
9
|
+
queueStats,
|
|
10
|
+
queueListOptions,
|
|
11
|
+
redisQueueConfig,
|
|
12
|
+
workerOptions,
|
|
13
|
+
queueConfig
|
|
14
|
+
} from "@parsrun/types";
|
|
15
|
+
var QueueError = class extends Error {
|
|
16
|
+
constructor(message, code, cause) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
this.name = "QueueError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var QueueErrorCodes = {
|
|
24
|
+
SEND_FAILED: "SEND_FAILED",
|
|
25
|
+
RECEIVE_FAILED: "RECEIVE_FAILED",
|
|
26
|
+
ACK_FAILED: "ACK_FAILED",
|
|
27
|
+
INVALID_CONFIG: "INVALID_CONFIG",
|
|
28
|
+
QUEUE_FULL: "QUEUE_FULL",
|
|
29
|
+
MESSAGE_NOT_FOUND: "MESSAGE_NOT_FOUND",
|
|
30
|
+
NOT_IMPLEMENTED: "NOT_IMPLEMENTED"
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/adapters/memory.ts
|
|
34
|
+
var MemoryQueueAdapter = class {
|
|
35
|
+
type = "memory";
|
|
36
|
+
name;
|
|
37
|
+
messages = [];
|
|
38
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
39
|
+
processedIds = /* @__PURE__ */ new Set();
|
|
40
|
+
maxSize;
|
|
41
|
+
visibilityTimeout;
|
|
42
|
+
messageCounter = 0;
|
|
43
|
+
isConsuming = false;
|
|
44
|
+
consumeInterval = null;
|
|
45
|
+
constructor(config) {
|
|
46
|
+
this.name = config.name;
|
|
47
|
+
this.maxSize = config.maxSize ?? Infinity;
|
|
48
|
+
this.visibilityTimeout = config.visibilityTimeout ?? 30;
|
|
49
|
+
}
|
|
50
|
+
generateId() {
|
|
51
|
+
this.messageCounter++;
|
|
52
|
+
return `msg-${Date.now()}-${this.messageCounter}`;
|
|
53
|
+
}
|
|
54
|
+
async send(body, options) {
|
|
55
|
+
if (this.messages.length >= this.maxSize) {
|
|
56
|
+
throw new QueueError(
|
|
57
|
+
`Queue ${this.name} is full`,
|
|
58
|
+
QueueErrorCodes.QUEUE_FULL
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (options?.deduplicationId && this.processedIds.has(options.deduplicationId)) {
|
|
62
|
+
return `dedup-${options.deduplicationId}`;
|
|
63
|
+
}
|
|
64
|
+
const id = this.generateId();
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const visibleAt = options?.delaySeconds ? now + options.delaySeconds * 1e3 : now;
|
|
67
|
+
const message = {
|
|
68
|
+
id,
|
|
69
|
+
body,
|
|
70
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
71
|
+
attempts: 0,
|
|
72
|
+
visibleAt,
|
|
73
|
+
deduplicationId: options?.deduplicationId,
|
|
74
|
+
metadata: options?.metadata
|
|
75
|
+
};
|
|
76
|
+
if (options?.priority !== void 0 && options.priority > 0) {
|
|
77
|
+
const insertIndex = this.messages.findIndex(
|
|
78
|
+
(m) => m.metadata?.["priority"] ?? 0 < (options.priority ?? 0)
|
|
79
|
+
);
|
|
80
|
+
if (insertIndex === -1) {
|
|
81
|
+
this.messages.push(message);
|
|
82
|
+
} else {
|
|
83
|
+
this.messages.splice(insertIndex, 0, message);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
this.messages.push(message);
|
|
87
|
+
}
|
|
88
|
+
if (options?.deduplicationId) {
|
|
89
|
+
this.processedIds.add(options.deduplicationId);
|
|
90
|
+
}
|
|
91
|
+
return id;
|
|
92
|
+
}
|
|
93
|
+
async sendBatch(messages) {
|
|
94
|
+
const messageIds = [];
|
|
95
|
+
const errors = [];
|
|
96
|
+
let successful = 0;
|
|
97
|
+
let failed = 0;
|
|
98
|
+
for (let i = 0; i < messages.length; i++) {
|
|
99
|
+
const msg = messages[i];
|
|
100
|
+
if (!msg) continue;
|
|
101
|
+
try {
|
|
102
|
+
const id = await this.send(msg.body, msg.options);
|
|
103
|
+
messageIds.push(id);
|
|
104
|
+
successful++;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
failed++;
|
|
107
|
+
errors.push({
|
|
108
|
+
index: i,
|
|
109
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
total: messages.length,
|
|
115
|
+
successful,
|
|
116
|
+
failed,
|
|
117
|
+
messageIds,
|
|
118
|
+
errors
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async receive(maxMessages = 10, visibilityTimeoutOverride) {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const timeout = (visibilityTimeoutOverride ?? this.visibilityTimeout) * 1e3;
|
|
124
|
+
const result = [];
|
|
125
|
+
const visibleMessages = [];
|
|
126
|
+
const remainingMessages = [];
|
|
127
|
+
for (const msg of this.messages) {
|
|
128
|
+
if (msg.visibleAt <= now && visibleMessages.length < maxMessages) {
|
|
129
|
+
visibleMessages.push(msg);
|
|
130
|
+
} else {
|
|
131
|
+
remainingMessages.push(msg);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.messages = remainingMessages;
|
|
135
|
+
for (const msg of visibleMessages) {
|
|
136
|
+
msg.attempts++;
|
|
137
|
+
msg.visibleAt = now + timeout;
|
|
138
|
+
this.inFlight.set(msg.id, msg);
|
|
139
|
+
result.push({
|
|
140
|
+
id: msg.id,
|
|
141
|
+
body: msg.body,
|
|
142
|
+
timestamp: msg.timestamp,
|
|
143
|
+
attempts: msg.attempts,
|
|
144
|
+
metadata: msg.metadata
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
async ack(messageId) {
|
|
150
|
+
const message = this.inFlight.get(messageId);
|
|
151
|
+
if (!message) {
|
|
152
|
+
throw new QueueError(
|
|
153
|
+
`Message ${messageId} not found in flight`,
|
|
154
|
+
QueueErrorCodes.MESSAGE_NOT_FOUND
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
this.inFlight.delete(messageId);
|
|
158
|
+
}
|
|
159
|
+
async ackBatch(messageIds) {
|
|
160
|
+
for (const id of messageIds) {
|
|
161
|
+
this.inFlight.delete(id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async nack(messageId, delaySeconds) {
|
|
165
|
+
const message = this.inFlight.get(messageId);
|
|
166
|
+
if (!message) {
|
|
167
|
+
throw new QueueError(
|
|
168
|
+
`Message ${messageId} not found in flight`,
|
|
169
|
+
QueueErrorCodes.MESSAGE_NOT_FOUND
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
this.inFlight.delete(messageId);
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
message.visibleAt = delaySeconds ? now + delaySeconds * 1e3 : now;
|
|
175
|
+
this.messages.push(message);
|
|
176
|
+
}
|
|
177
|
+
async consume(handler, options) {
|
|
178
|
+
if (this.isConsuming) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.isConsuming = true;
|
|
182
|
+
const batchSize = options?.batchSize ?? 10;
|
|
183
|
+
const pollingInterval = options?.pollingInterval ?? 1e3;
|
|
184
|
+
const visibilityTimeout = options?.visibilityTimeout ?? this.visibilityTimeout;
|
|
185
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
186
|
+
const concurrency = options?.concurrency ?? 1;
|
|
187
|
+
const processMessages = async () => {
|
|
188
|
+
if (!this.isConsuming) return;
|
|
189
|
+
const messages = await this.receive(batchSize, visibilityTimeout);
|
|
190
|
+
for (let i = 0; i < messages.length; i += concurrency) {
|
|
191
|
+
const batch = messages.slice(i, i + concurrency);
|
|
192
|
+
await Promise.all(
|
|
193
|
+
batch.map(async (msg) => {
|
|
194
|
+
try {
|
|
195
|
+
await handler(msg);
|
|
196
|
+
await this.ack(msg.id);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (msg.attempts >= maxRetries) {
|
|
199
|
+
await this.ack(msg.id);
|
|
200
|
+
console.error(
|
|
201
|
+
`[Queue ${this.name}] Message ${msg.id} exceeded max retries, dropped`
|
|
202
|
+
);
|
|
203
|
+
} else {
|
|
204
|
+
await this.nack(msg.id, 5);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
this.consumeInterval = setInterval(processMessages, pollingInterval);
|
|
212
|
+
await processMessages();
|
|
213
|
+
}
|
|
214
|
+
async stopConsuming() {
|
|
215
|
+
this.isConsuming = false;
|
|
216
|
+
if (this.consumeInterval) {
|
|
217
|
+
clearInterval(this.consumeInterval);
|
|
218
|
+
this.consumeInterval = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async getStats() {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
for (const [id, msg] of this.inFlight) {
|
|
224
|
+
if (msg.visibleAt <= now) {
|
|
225
|
+
this.inFlight.delete(id);
|
|
226
|
+
this.messages.push(msg);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
messageCount: this.messages.length,
|
|
231
|
+
inFlightCount: this.inFlight.size
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async purge() {
|
|
235
|
+
this.messages = [];
|
|
236
|
+
this.inFlight.clear();
|
|
237
|
+
}
|
|
238
|
+
async close() {
|
|
239
|
+
await this.stopConsuming();
|
|
240
|
+
await this.purge();
|
|
241
|
+
this.processedIds.clear();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function createMemoryQueueAdapter(config) {
|
|
245
|
+
return new MemoryQueueAdapter(config);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/adapters/cloudflare.ts
|
|
249
|
+
var CloudflareQueueAdapter = class {
|
|
250
|
+
type = "cloudflare";
|
|
251
|
+
name = "cloudflare-queue";
|
|
252
|
+
queue;
|
|
253
|
+
constructor(config) {
|
|
254
|
+
this.queue = config.queue;
|
|
255
|
+
}
|
|
256
|
+
async send(body, options) {
|
|
257
|
+
try {
|
|
258
|
+
await this.queue.send(body, {
|
|
259
|
+
delaySeconds: options?.delaySeconds
|
|
260
|
+
});
|
|
261
|
+
return `cf-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
262
|
+
} catch (err) {
|
|
263
|
+
throw new QueueError(
|
|
264
|
+
`Cloudflare Queue send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
265
|
+
QueueErrorCodes.SEND_FAILED,
|
|
266
|
+
err
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async sendBatch(messages) {
|
|
271
|
+
try {
|
|
272
|
+
const batchMessages = messages.map((m) => ({
|
|
273
|
+
body: m.body,
|
|
274
|
+
delaySeconds: m.options?.delaySeconds
|
|
275
|
+
}));
|
|
276
|
+
await this.queue.sendBatch(batchMessages);
|
|
277
|
+
const messageIds = messages.map(
|
|
278
|
+
() => `cf-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
|
279
|
+
);
|
|
280
|
+
return {
|
|
281
|
+
total: messages.length,
|
|
282
|
+
successful: messages.length,
|
|
283
|
+
failed: 0,
|
|
284
|
+
messageIds,
|
|
285
|
+
errors: []
|
|
286
|
+
};
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new QueueError(
|
|
289
|
+
`Cloudflare Queue batch send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
290
|
+
QueueErrorCodes.SEND_FAILED,
|
|
291
|
+
err
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Cloudflare Queues are push-based, so receive is not applicable
|
|
296
|
+
// Messages are delivered to queue handlers via Workers
|
|
297
|
+
};
|
|
298
|
+
var CloudflareQueueProcessor = class {
|
|
299
|
+
/**
|
|
300
|
+
* Process a batch of messages from Cloudflare Queues
|
|
301
|
+
*/
|
|
302
|
+
async processBatch(batch, handler, options) {
|
|
303
|
+
let processed = 0;
|
|
304
|
+
let failed = 0;
|
|
305
|
+
for (const msg of batch.messages) {
|
|
306
|
+
try {
|
|
307
|
+
const queueMessage = {
|
|
308
|
+
id: msg.id,
|
|
309
|
+
body: msg.body,
|
|
310
|
+
timestamp: msg.timestamp,
|
|
311
|
+
attempts: msg.attempts
|
|
312
|
+
};
|
|
313
|
+
await handler(queueMessage);
|
|
314
|
+
if (!options?.ackAll) {
|
|
315
|
+
msg.ack();
|
|
316
|
+
}
|
|
317
|
+
processed++;
|
|
318
|
+
} catch (err) {
|
|
319
|
+
failed++;
|
|
320
|
+
if (options?.retryAllOnFailure) {
|
|
321
|
+
batch.retryAll();
|
|
322
|
+
return { processed, failed: batch.messages.length };
|
|
323
|
+
}
|
|
324
|
+
msg.retry();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (options?.ackAll && failed === 0) {
|
|
328
|
+
batch.ackAll();
|
|
329
|
+
}
|
|
330
|
+
return { processed, failed };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Convert a Cloudflare message to QueueMessage format
|
|
334
|
+
*/
|
|
335
|
+
toQueueMessage(msg) {
|
|
336
|
+
return {
|
|
337
|
+
id: msg.id,
|
|
338
|
+
body: msg.body,
|
|
339
|
+
timestamp: msg.timestamp,
|
|
340
|
+
attempts: msg.attempts
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function createCloudflareQueueAdapter(config) {
|
|
345
|
+
return new CloudflareQueueAdapter(config);
|
|
346
|
+
}
|
|
347
|
+
function createCloudflareQueueProcessor() {
|
|
348
|
+
return new CloudflareQueueProcessor();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/adapters/qstash.ts
|
|
352
|
+
var QStashAdapter = class {
|
|
353
|
+
type = "qstash";
|
|
354
|
+
name = "qstash";
|
|
355
|
+
token;
|
|
356
|
+
destinationUrl;
|
|
357
|
+
baseUrl = "https://qstash.upstash.io/v2";
|
|
358
|
+
constructor(config) {
|
|
359
|
+
this.token = config.token;
|
|
360
|
+
this.destinationUrl = config.destinationUrl;
|
|
361
|
+
}
|
|
362
|
+
async send(body, options) {
|
|
363
|
+
try {
|
|
364
|
+
const headers = {
|
|
365
|
+
Authorization: `Bearer ${this.token}`,
|
|
366
|
+
"Content-Type": "application/json"
|
|
367
|
+
};
|
|
368
|
+
if (options?.delaySeconds) {
|
|
369
|
+
headers["Upstash-Delay"] = `${options.delaySeconds}s`;
|
|
370
|
+
}
|
|
371
|
+
if (options?.deduplicationId) {
|
|
372
|
+
headers["Upstash-Deduplication-Id"] = options.deduplicationId;
|
|
373
|
+
}
|
|
374
|
+
if (options?.metadata) {
|
|
375
|
+
for (const [key, value] of Object.entries(options.metadata)) {
|
|
376
|
+
if (typeof value === "string") {
|
|
377
|
+
headers[`Upstash-Forward-${key}`] = value;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const response = await fetch(
|
|
382
|
+
`${this.baseUrl}/publish/${encodeURIComponent(this.destinationUrl)}`,
|
|
383
|
+
{
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers,
|
|
386
|
+
body: JSON.stringify(body)
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
const error = await response.text();
|
|
391
|
+
throw new Error(`QStash API error: ${error}`);
|
|
392
|
+
}
|
|
393
|
+
const result = await response.json();
|
|
394
|
+
return result.messageId;
|
|
395
|
+
} catch (err) {
|
|
396
|
+
throw new QueueError(
|
|
397
|
+
`QStash send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
398
|
+
QueueErrorCodes.SEND_FAILED,
|
|
399
|
+
err
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async sendBatch(messages) {
|
|
404
|
+
try {
|
|
405
|
+
const batchMessages = messages.map((m) => {
|
|
406
|
+
const headers = {
|
|
407
|
+
"Content-Type": "application/json"
|
|
408
|
+
};
|
|
409
|
+
if (m.options?.delaySeconds) {
|
|
410
|
+
headers["Upstash-Delay"] = `${m.options.delaySeconds}s`;
|
|
411
|
+
}
|
|
412
|
+
if (m.options?.deduplicationId) {
|
|
413
|
+
headers["Upstash-Deduplication-Id"] = m.options.deduplicationId;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
destination: this.destinationUrl,
|
|
417
|
+
headers,
|
|
418
|
+
body: JSON.stringify(m.body)
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
const response = await fetch(`${this.baseUrl}/batch`, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: {
|
|
424
|
+
Authorization: `Bearer ${this.token}`,
|
|
425
|
+
"Content-Type": "application/json"
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify(batchMessages)
|
|
428
|
+
});
|
|
429
|
+
if (!response.ok) {
|
|
430
|
+
const error = await response.text();
|
|
431
|
+
throw new Error(`QStash API error: ${error}`);
|
|
432
|
+
}
|
|
433
|
+
const results = await response.json();
|
|
434
|
+
const messageIds = [];
|
|
435
|
+
const errors = [];
|
|
436
|
+
let successful = 0;
|
|
437
|
+
let failed = 0;
|
|
438
|
+
for (let i = 0; i < results.length; i++) {
|
|
439
|
+
const result = results[i];
|
|
440
|
+
if (result?.messageId) {
|
|
441
|
+
messageIds.push(result.messageId);
|
|
442
|
+
successful++;
|
|
443
|
+
} else {
|
|
444
|
+
failed++;
|
|
445
|
+
errors.push({
|
|
446
|
+
index: i,
|
|
447
|
+
error: result?.error ?? "Unknown error"
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
total: messages.length,
|
|
453
|
+
successful,
|
|
454
|
+
failed,
|
|
455
|
+
messageIds,
|
|
456
|
+
errors
|
|
457
|
+
};
|
|
458
|
+
} catch (err) {
|
|
459
|
+
throw new QueueError(
|
|
460
|
+
`QStash batch send failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
461
|
+
QueueErrorCodes.SEND_FAILED,
|
|
462
|
+
err
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Schedule a message for future delivery
|
|
468
|
+
*/
|
|
469
|
+
async schedule(body, cronExpression, options) {
|
|
470
|
+
try {
|
|
471
|
+
const headers = {
|
|
472
|
+
Authorization: `Bearer ${this.token}`,
|
|
473
|
+
"Content-Type": "application/json",
|
|
474
|
+
"Upstash-Cron": cronExpression
|
|
475
|
+
};
|
|
476
|
+
if (options?.deduplicationId) {
|
|
477
|
+
headers["Upstash-Deduplication-Id"] = options.deduplicationId;
|
|
478
|
+
}
|
|
479
|
+
const response = await fetch(
|
|
480
|
+
`${this.baseUrl}/schedules/${encodeURIComponent(this.destinationUrl)}`,
|
|
481
|
+
{
|
|
482
|
+
method: "POST",
|
|
483
|
+
headers,
|
|
484
|
+
body: JSON.stringify(body)
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
if (!response.ok) {
|
|
488
|
+
const error = await response.text();
|
|
489
|
+
throw new Error(`QStash API error: ${error}`);
|
|
490
|
+
}
|
|
491
|
+
const result = await response.json();
|
|
492
|
+
return result.scheduleId;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
throw new QueueError(
|
|
495
|
+
`QStash schedule failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
496
|
+
QueueErrorCodes.SEND_FAILED,
|
|
497
|
+
err
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Delete a scheduled message
|
|
503
|
+
*/
|
|
504
|
+
async deleteSchedule(scheduleId) {
|
|
505
|
+
try {
|
|
506
|
+
const response = await fetch(`${this.baseUrl}/schedules/${scheduleId}`, {
|
|
507
|
+
method: "DELETE",
|
|
508
|
+
headers: {
|
|
509
|
+
Authorization: `Bearer ${this.token}`
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
const error = await response.text();
|
|
514
|
+
throw new Error(`QStash API error: ${error}`);
|
|
515
|
+
}
|
|
516
|
+
} catch (err) {
|
|
517
|
+
throw new QueueError(
|
|
518
|
+
`QStash delete schedule failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
519
|
+
QueueErrorCodes.SEND_FAILED,
|
|
520
|
+
err
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* List all schedules
|
|
526
|
+
*/
|
|
527
|
+
async listSchedules() {
|
|
528
|
+
try {
|
|
529
|
+
const response = await fetch(`${this.baseUrl}/schedules`, {
|
|
530
|
+
headers: {
|
|
531
|
+
Authorization: `Bearer ${this.token}`
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
if (!response.ok) {
|
|
535
|
+
const error = await response.text();
|
|
536
|
+
throw new Error(`QStash API error: ${error}`);
|
|
537
|
+
}
|
|
538
|
+
return await response.json();
|
|
539
|
+
} catch (err) {
|
|
540
|
+
throw new QueueError(
|
|
541
|
+
`QStash list schedules failed: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
542
|
+
QueueErrorCodes.SEND_FAILED,
|
|
543
|
+
err
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var QStashReceiver = class {
|
|
549
|
+
currentSigningKey;
|
|
550
|
+
nextSigningKey;
|
|
551
|
+
constructor(config) {
|
|
552
|
+
this.currentSigningKey = config.currentSigningKey;
|
|
553
|
+
this.nextSigningKey = config.nextSigningKey;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Verify a request from QStash and extract the message
|
|
557
|
+
*/
|
|
558
|
+
async verify(request) {
|
|
559
|
+
const signature = request.headers.get("Upstash-Signature");
|
|
560
|
+
if (!signature) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const body = await request.text();
|
|
564
|
+
const isValid = await this.verifySignature(body, signature, this.currentSigningKey) || await this.verifySignature(body, signature, this.nextSigningKey);
|
|
565
|
+
if (!isValid) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const messageId = request.headers.get("Upstash-Message-Id") ?? `qstash-${Date.now()}`;
|
|
569
|
+
const retryCount = parseInt(
|
|
570
|
+
request.headers.get("Upstash-Retried") ?? "0",
|
|
571
|
+
10
|
|
572
|
+
);
|
|
573
|
+
let parsedBody;
|
|
574
|
+
try {
|
|
575
|
+
parsedBody = JSON.parse(body);
|
|
576
|
+
} catch {
|
|
577
|
+
parsedBody = body;
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
id: messageId,
|
|
581
|
+
body: parsedBody,
|
|
582
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
583
|
+
attempts: retryCount + 1
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async verifySignature(body, signature, key) {
|
|
587
|
+
try {
|
|
588
|
+
const parts = signature.split(".");
|
|
589
|
+
if (parts.length !== 3) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
593
|
+
if (!headerB64 || !payloadB64 || !signatureB64) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
const encoder = new TextEncoder();
|
|
597
|
+
const keyData = encoder.encode(key);
|
|
598
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
599
|
+
"raw",
|
|
600
|
+
keyData,
|
|
601
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
602
|
+
false,
|
|
603
|
+
["verify"]
|
|
604
|
+
);
|
|
605
|
+
const signatureData = this.base64UrlDecode(signatureB64);
|
|
606
|
+
const dataToVerify = encoder.encode(`${headerB64}.${payloadB64}`);
|
|
607
|
+
const isValid = await crypto.subtle.verify(
|
|
608
|
+
"HMAC",
|
|
609
|
+
cryptoKey,
|
|
610
|
+
signatureData,
|
|
611
|
+
dataToVerify
|
|
612
|
+
);
|
|
613
|
+
if (!isValid) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
const payload = JSON.parse(atob(payloadB64));
|
|
617
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
618
|
+
if (payload.exp && payload.exp < now) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
if (payload.nbf && payload.nbf > now) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
if (payload.body) {
|
|
625
|
+
const bodyHash = await this.sha256(body);
|
|
626
|
+
const expectedHash = this.base64UrlEncode(
|
|
627
|
+
new Uint8Array(
|
|
628
|
+
atob(payload.body).split("").map((c) => c.charCodeAt(0))
|
|
629
|
+
)
|
|
630
|
+
);
|
|
631
|
+
if (bodyHash !== expectedHash && payload.body !== bodyHash) {
|
|
632
|
+
const directHash = await this.sha256Base64(body);
|
|
633
|
+
if (directHash !== payload.body) {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
} catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
base64UrlDecode(str) {
|
|
644
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
645
|
+
const padding = (4 - base64.length % 4) % 4;
|
|
646
|
+
const padded = base64 + "=".repeat(padding);
|
|
647
|
+
const binary = atob(padded);
|
|
648
|
+
const bytes = new Uint8Array(binary.length);
|
|
649
|
+
for (let i = 0; i < binary.length; i++) {
|
|
650
|
+
bytes[i] = binary.charCodeAt(i);
|
|
651
|
+
}
|
|
652
|
+
return bytes;
|
|
653
|
+
}
|
|
654
|
+
base64UrlEncode(data) {
|
|
655
|
+
let binary = "";
|
|
656
|
+
for (let i = 0; i < data.length; i++) {
|
|
657
|
+
const byte = data[i];
|
|
658
|
+
if (byte !== void 0) {
|
|
659
|
+
binary += String.fromCharCode(byte);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
663
|
+
}
|
|
664
|
+
async sha256(message) {
|
|
665
|
+
const encoder = new TextEncoder();
|
|
666
|
+
const data = encoder.encode(message);
|
|
667
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
668
|
+
return this.base64UrlEncode(new Uint8Array(hash));
|
|
669
|
+
}
|
|
670
|
+
async sha256Base64(message) {
|
|
671
|
+
const encoder = new TextEncoder();
|
|
672
|
+
const data = encoder.encode(message);
|
|
673
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
674
|
+
let binary = "";
|
|
675
|
+
const bytes = new Uint8Array(hash);
|
|
676
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
677
|
+
const byte = bytes[i];
|
|
678
|
+
if (byte !== void 0) {
|
|
679
|
+
binary += String.fromCharCode(byte);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return btoa(binary);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
function createQStashAdapter(config) {
|
|
686
|
+
return new QStashAdapter(config);
|
|
687
|
+
}
|
|
688
|
+
function createQStashReceiver(config) {
|
|
689
|
+
return new QStashReceiver(config);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/index.ts
|
|
693
|
+
var QueueService = class {
|
|
694
|
+
adapter;
|
|
695
|
+
debug;
|
|
696
|
+
constructor(config) {
|
|
697
|
+
this.adapter = config.adapter;
|
|
698
|
+
this.debug = config.debug ?? false;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Get adapter type
|
|
702
|
+
*/
|
|
703
|
+
get adapterType() {
|
|
704
|
+
return this.adapter.type;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Get queue name
|
|
708
|
+
*/
|
|
709
|
+
get name() {
|
|
710
|
+
return this.adapter.name;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Send a message to the queue
|
|
714
|
+
*/
|
|
715
|
+
async send(body, options) {
|
|
716
|
+
if (this.debug) {
|
|
717
|
+
console.log(`[Queue ${this.name}] Sending message:`, body);
|
|
718
|
+
}
|
|
719
|
+
const messageId = await this.adapter.send(body, options);
|
|
720
|
+
if (this.debug) {
|
|
721
|
+
console.log(`[Queue ${this.name}] Message sent: ${messageId}`);
|
|
722
|
+
}
|
|
723
|
+
return messageId;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Send multiple messages at once
|
|
727
|
+
*/
|
|
728
|
+
async sendBatch(messages) {
|
|
729
|
+
if (this.debug) {
|
|
730
|
+
console.log(`[Queue ${this.name}] Sending batch of ${messages.length} messages`);
|
|
731
|
+
}
|
|
732
|
+
if (this.adapter.sendBatch) {
|
|
733
|
+
return this.adapter.sendBatch(messages);
|
|
734
|
+
}
|
|
735
|
+
const messageIds = [];
|
|
736
|
+
const errors = [];
|
|
737
|
+
let successful = 0;
|
|
738
|
+
let failed = 0;
|
|
739
|
+
for (let i = 0; i < messages.length; i++) {
|
|
740
|
+
const msg = messages[i];
|
|
741
|
+
if (!msg) continue;
|
|
742
|
+
try {
|
|
743
|
+
const id = await this.send(msg.body, msg.options);
|
|
744
|
+
messageIds.push(id);
|
|
745
|
+
successful++;
|
|
746
|
+
} catch (err) {
|
|
747
|
+
failed++;
|
|
748
|
+
errors.push({
|
|
749
|
+
index: i,
|
|
750
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
total: messages.length,
|
|
756
|
+
successful,
|
|
757
|
+
failed,
|
|
758
|
+
messageIds,
|
|
759
|
+
errors
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Receive messages from the queue (pull-based)
|
|
764
|
+
*/
|
|
765
|
+
async receive(maxMessages, visibilityTimeout) {
|
|
766
|
+
if (!this.adapter.receive) {
|
|
767
|
+
throw new Error(`${this.adapter.type} adapter does not support receive()`);
|
|
768
|
+
}
|
|
769
|
+
return this.adapter.receive(maxMessages, visibilityTimeout);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Acknowledge message processing (mark as complete)
|
|
773
|
+
*/
|
|
774
|
+
async ack(messageId) {
|
|
775
|
+
if (!this.adapter.ack) {
|
|
776
|
+
throw new Error(`${this.adapter.type} adapter does not support ack()`);
|
|
777
|
+
}
|
|
778
|
+
await this.adapter.ack(messageId);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Acknowledge multiple messages
|
|
782
|
+
*/
|
|
783
|
+
async ackBatch(messageIds) {
|
|
784
|
+
if (this.adapter.ackBatch) {
|
|
785
|
+
await this.adapter.ackBatch(messageIds);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (this.adapter.ack) {
|
|
789
|
+
await Promise.all(messageIds.map((id) => this.adapter.ack(id)));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
throw new Error(`${this.adapter.type} adapter does not support ack()`);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Return message to queue (negative acknowledgement)
|
|
796
|
+
*/
|
|
797
|
+
async nack(messageId, delaySeconds) {
|
|
798
|
+
if (!this.adapter.nack) {
|
|
799
|
+
throw new Error(`${this.adapter.type} adapter does not support nack()`);
|
|
800
|
+
}
|
|
801
|
+
await this.adapter.nack(messageId, delaySeconds);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Start consuming messages (push-based)
|
|
805
|
+
*/
|
|
806
|
+
async consume(handler, options) {
|
|
807
|
+
if (!this.adapter.consume) {
|
|
808
|
+
throw new Error(`${this.adapter.type} adapter does not support consume()`);
|
|
809
|
+
}
|
|
810
|
+
if (this.debug) {
|
|
811
|
+
console.log(`[Queue ${this.name}] Starting consumer`);
|
|
812
|
+
}
|
|
813
|
+
await this.adapter.consume(handler, options);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Stop consuming messages
|
|
817
|
+
*/
|
|
818
|
+
async stopConsuming() {
|
|
819
|
+
if (this.adapter.stopConsuming) {
|
|
820
|
+
await this.adapter.stopConsuming();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Get queue statistics
|
|
825
|
+
*/
|
|
826
|
+
async getStats() {
|
|
827
|
+
if (this.adapter.getStats) {
|
|
828
|
+
return this.adapter.getStats();
|
|
829
|
+
}
|
|
830
|
+
return { messageCount: -1 };
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Purge all messages from queue
|
|
834
|
+
*/
|
|
835
|
+
async purge() {
|
|
836
|
+
if (this.adapter.purge) {
|
|
837
|
+
await this.adapter.purge();
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Close/cleanup queue resources
|
|
842
|
+
*/
|
|
843
|
+
async close() {
|
|
844
|
+
if (this.adapter.close) {
|
|
845
|
+
await this.adapter.close();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
function createQueueService(config) {
|
|
850
|
+
return new QueueService(config);
|
|
851
|
+
}
|
|
852
|
+
var index_default = {
|
|
853
|
+
QueueService,
|
|
854
|
+
createQueueService
|
|
855
|
+
};
|
|
856
|
+
export {
|
|
857
|
+
CloudflareQueueAdapter,
|
|
858
|
+
CloudflareQueueProcessor,
|
|
859
|
+
MemoryQueueAdapter,
|
|
860
|
+
QStashAdapter,
|
|
861
|
+
QStashReceiver,
|
|
862
|
+
QueueError,
|
|
863
|
+
QueueErrorCodes,
|
|
864
|
+
QueueService,
|
|
865
|
+
addJobRequest,
|
|
866
|
+
createCloudflareQueueAdapter,
|
|
867
|
+
createCloudflareQueueProcessor,
|
|
868
|
+
createMemoryQueueAdapter,
|
|
869
|
+
createQStashAdapter,
|
|
870
|
+
createQStashReceiver,
|
|
871
|
+
createQueueService,
|
|
872
|
+
index_default as default,
|
|
873
|
+
job,
|
|
874
|
+
jobOptions,
|
|
875
|
+
jobProgressUpdate,
|
|
876
|
+
jobStatus,
|
|
877
|
+
queueStats as parsQueueStats,
|
|
878
|
+
queueConfig,
|
|
879
|
+
queueListOptions,
|
|
880
|
+
redisQueueConfig,
|
|
881
|
+
type,
|
|
882
|
+
workerOptions
|
|
883
|
+
};
|
|
884
|
+
//# sourceMappingURL=index.js.map
|