queuebear 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/dist/index.d.ts +1209 -0
- package/dist/index.js +1313 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
// src/api/base.ts
|
|
2
|
+
var BaseClient = class {
|
|
3
|
+
baseUrl;
|
|
4
|
+
apiKey;
|
|
5
|
+
projectId;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
8
|
+
this.apiKey = options.apiKey;
|
|
9
|
+
this.projectId = options.projectId;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build the full URL for a project-scoped endpoint
|
|
13
|
+
*/
|
|
14
|
+
buildUrl(path, queryParams) {
|
|
15
|
+
const url = new URL(`${this.baseUrl}/v1/projects/${this.projectId}${path}`);
|
|
16
|
+
if (queryParams) {
|
|
17
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
18
|
+
if (value !== void 0) {
|
|
19
|
+
url.searchParams.set(key, String(value));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return url.toString();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Make an authenticated request
|
|
27
|
+
*/
|
|
28
|
+
async request(method, path, options) {
|
|
29
|
+
const url = this.buildUrl(path, options?.queryParams);
|
|
30
|
+
const headers = {
|
|
31
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
32
|
+
...options?.headers
|
|
33
|
+
};
|
|
34
|
+
if (options?.body !== void 0) {
|
|
35
|
+
headers["Content-Type"] = "application/json";
|
|
36
|
+
}
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
method,
|
|
39
|
+
headers,
|
|
40
|
+
body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorData = await response.json().catch(() => ({}));
|
|
44
|
+
throw new QueueBearError(
|
|
45
|
+
errorData.error || errorData.message || `Request failed with status ${response.status}`,
|
|
46
|
+
response.status
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return response.json();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var QueueBearError = class extends Error {
|
|
53
|
+
constructor(message, statusCode) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.statusCode = statusCode;
|
|
56
|
+
this.name = "QueueBearError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/api/messages.ts
|
|
61
|
+
var MessagesAPI = class extends BaseClient {
|
|
62
|
+
constructor(options) {
|
|
63
|
+
super(options);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Publish a message to be delivered to a destination URL
|
|
67
|
+
*
|
|
68
|
+
* @param destination - The URL to deliver the message to
|
|
69
|
+
* @param body - The message body (will be JSON-stringified)
|
|
70
|
+
* @param options - Optional configuration for delay, retries, callbacks, etc.
|
|
71
|
+
* @returns The message ID and deduplication status
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const { messageId } = await qb.messages.publish(
|
|
76
|
+
* "https://api.example.com/webhook",
|
|
77
|
+
* { event: "user.created", userId: "123" },
|
|
78
|
+
* { delay: "30s", retries: 5 }
|
|
79
|
+
* );
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
async publish(destination, body, options) {
|
|
83
|
+
const headers = {
|
|
84
|
+
"Content-Type": "application/json"
|
|
85
|
+
};
|
|
86
|
+
if (options?.delay) {
|
|
87
|
+
headers["queuebear-delay"] = options.delay;
|
|
88
|
+
}
|
|
89
|
+
if (options?.retries !== void 0) {
|
|
90
|
+
headers["queuebear-retries"] = String(options.retries);
|
|
91
|
+
}
|
|
92
|
+
if (options?.method) {
|
|
93
|
+
headers["queuebear-method"] = options.method;
|
|
94
|
+
}
|
|
95
|
+
if (options?.callbackUrl) {
|
|
96
|
+
headers["queuebear-callback"] = options.callbackUrl;
|
|
97
|
+
}
|
|
98
|
+
if (options?.failureCallbackUrl) {
|
|
99
|
+
headers["queuebear-failure-callback"] = options.failureCallbackUrl;
|
|
100
|
+
}
|
|
101
|
+
if (options?.deduplicationId) {
|
|
102
|
+
headers["queuebear-deduplication-id"] = options.deduplicationId;
|
|
103
|
+
}
|
|
104
|
+
if (options?.headers) {
|
|
105
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
106
|
+
headers[`queuebear-forward-${key}`] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const encodedDestination = encodeURIComponent(destination);
|
|
110
|
+
return this.request("POST", `/publish/${encodedDestination}`, {
|
|
111
|
+
body,
|
|
112
|
+
headers
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get message details and delivery history
|
|
117
|
+
*
|
|
118
|
+
* @param messageId - The message ID (e.g., "msg_abc123...")
|
|
119
|
+
* @returns Full message details including delivery logs
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const message = await qb.messages.get("msg_abc123");
|
|
124
|
+
* console.log(message.status); // "completed" | "pending" | "failed"
|
|
125
|
+
* console.log(message.deliveryLogs); // Delivery attempt history
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
async get(messageId) {
|
|
129
|
+
return this.request("GET", `/messages/${messageId}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* List messages with optional filtering
|
|
133
|
+
*
|
|
134
|
+
* @param options - Filter by status and pagination
|
|
135
|
+
* @returns Paginated list of messages
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* const { messages, pagination } = await qb.messages.list({
|
|
140
|
+
* status: "pending",
|
|
141
|
+
* limit: 20
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
async list(options) {
|
|
146
|
+
return this.request("GET", "/messages", {
|
|
147
|
+
queryParams: {
|
|
148
|
+
status: options?.status,
|
|
149
|
+
limit: options?.limit,
|
|
150
|
+
offset: options?.offset
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Cancel a pending or active message
|
|
156
|
+
*
|
|
157
|
+
* @param messageId - The message ID to cancel
|
|
158
|
+
* @returns Cancellation result
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* const result = await qb.messages.cancel("msg_abc123");
|
|
163
|
+
* console.log(result.cancelled); // true
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
async cancel(messageId) {
|
|
167
|
+
return this.request("DELETE", `/messages/${messageId}`);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Publish a message and wait for delivery completion
|
|
171
|
+
* Polls the message status until it reaches a terminal state
|
|
172
|
+
*
|
|
173
|
+
* @param destination - The URL to deliver the message to
|
|
174
|
+
* @param body - The message body
|
|
175
|
+
* @param options - Publish options plus polling configuration
|
|
176
|
+
* @returns The final message status after delivery
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* const message = await qb.messages.publishAndWait(
|
|
181
|
+
* "https://api.example.com/webhook",
|
|
182
|
+
* { event: "user.created" },
|
|
183
|
+
* { timeoutMs: 30000 }
|
|
184
|
+
* );
|
|
185
|
+
* console.log(message.status); // "completed"
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
async publishAndWait(destination, body, options) {
|
|
189
|
+
const { pollIntervalMs = 1e3, timeoutMs = 6e4, ...publishOptions } = options || {};
|
|
190
|
+
const { messageId } = await this.publish(destination, body, publishOptions);
|
|
191
|
+
const terminalStates = ["completed", "failed", "cancelled", "dlq"];
|
|
192
|
+
const startTime = Date.now();
|
|
193
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
194
|
+
const message = await this.get(messageId);
|
|
195
|
+
if (terminalStates.includes(message.status)) {
|
|
196
|
+
return message;
|
|
197
|
+
}
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`Message ${messageId} did not complete within ${timeoutMs}ms`);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/api/schedules.ts
|
|
205
|
+
var SchedulesAPI = class extends BaseClient {
|
|
206
|
+
constructor(options) {
|
|
207
|
+
super(options);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Create a cron-based recurring schedule
|
|
211
|
+
*
|
|
212
|
+
* @param options - Schedule configuration including destination, cron, and optional settings
|
|
213
|
+
* @returns The created schedule details
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const schedule = await qb.schedules.create({
|
|
218
|
+
* destination: "https://api.example.com/cron-job",
|
|
219
|
+
* cron: "0 9 * * *", // Daily at 9 AM
|
|
220
|
+
* timezone: "America/New_York",
|
|
221
|
+
* method: "POST",
|
|
222
|
+
* body: JSON.stringify({ type: "daily-report" }),
|
|
223
|
+
* retries: 3
|
|
224
|
+
* });
|
|
225
|
+
* console.log(schedule.scheduleId);
|
|
226
|
+
* ```
|
|
227
|
+
*
|
|
228
|
+
* @example Common cron expressions
|
|
229
|
+
* - `"* * * * *"` - Every minute
|
|
230
|
+
* - `"0 * * * *"` - Every hour
|
|
231
|
+
* - `"0 9 * * *"` - Daily at 9:00 AM
|
|
232
|
+
* - `"0 9 * * 1-5"` - Weekdays at 9:00 AM
|
|
233
|
+
* - `"0 0 1 * *"` - First day of each month
|
|
234
|
+
* - `"0 */6 * * *"` - Every 6 hours
|
|
235
|
+
*/
|
|
236
|
+
async create(options) {
|
|
237
|
+
return this.request("POST", "/schedules", {
|
|
238
|
+
body: options
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get details of a specific schedule
|
|
243
|
+
*
|
|
244
|
+
* @param scheduleId - The schedule ID (e.g., "sched_abc123...")
|
|
245
|
+
* @returns Full schedule details
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* const schedule = await qb.schedules.get("sched_abc123");
|
|
250
|
+
* console.log(schedule.nextExecutionAt);
|
|
251
|
+
* console.log(schedule.executionCount);
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
async get(scheduleId) {
|
|
255
|
+
return this.request("GET", `/schedules/${scheduleId}`);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* List all schedules for the current project
|
|
259
|
+
*
|
|
260
|
+
* @param options - Pagination options
|
|
261
|
+
* @returns Paginated list of schedules
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* const { schedules, pagination } = await qb.schedules.list({ limit: 20 });
|
|
266
|
+
* for (const schedule of schedules) {
|
|
267
|
+
* console.log(`${schedule.scheduleId}: ${schedule.cron}`);
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
async list(options) {
|
|
272
|
+
return this.request("GET", "/schedules", {
|
|
273
|
+
queryParams: {
|
|
274
|
+
limit: options?.limit,
|
|
275
|
+
offset: options?.offset
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Delete (soft-delete) a schedule
|
|
281
|
+
*
|
|
282
|
+
* @param scheduleId - The schedule ID to delete
|
|
283
|
+
* @returns Deletion confirmation
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* const result = await qb.schedules.delete("sched_abc123");
|
|
288
|
+
* console.log(result.deleted); // true
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
async delete(scheduleId) {
|
|
292
|
+
return this.request("DELETE", `/schedules/${scheduleId}`);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Pause a schedule to temporarily stop executions
|
|
296
|
+
*
|
|
297
|
+
* @param scheduleId - The schedule ID to pause
|
|
298
|
+
* @returns Pause confirmation
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* await qb.schedules.pause("sched_abc123");
|
|
303
|
+
* // Schedule will not execute until resumed
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
async pause(scheduleId) {
|
|
307
|
+
return this.request("PATCH", `/schedules/${scheduleId}/pause`);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Resume a paused schedule
|
|
311
|
+
*
|
|
312
|
+
* @param scheduleId - The schedule ID to resume
|
|
313
|
+
* @returns Resume confirmation with next execution time
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```typescript
|
|
317
|
+
* const result = await qb.schedules.resume("sched_abc123");
|
|
318
|
+
* console.log(result.nextExecutionAt); // Next scheduled run
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
async resume(scheduleId) {
|
|
322
|
+
return this.request("PATCH", `/schedules/${scheduleId}/resume`);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/api/dlq.ts
|
|
327
|
+
var DLQAPI = class extends BaseClient {
|
|
328
|
+
constructor(options) {
|
|
329
|
+
super(options);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* List all DLQ entries for the current project
|
|
333
|
+
*
|
|
334
|
+
* @param options - Pagination options
|
|
335
|
+
* @returns Paginated list of DLQ entries
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* const { entries, pagination } = await qb.dlq.list({ limit: 20 });
|
|
340
|
+
* for (const entry of entries) {
|
|
341
|
+
* console.log(`${entry.id}: ${entry.failureReason}`);
|
|
342
|
+
* }
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
async list(options) {
|
|
346
|
+
return this.request("GET", "/dlq", {
|
|
347
|
+
queryParams: {
|
|
348
|
+
limit: options?.limit,
|
|
349
|
+
offset: options?.offset
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get details of a specific DLQ entry
|
|
355
|
+
*
|
|
356
|
+
* @param dlqId - The DLQ entry UUID
|
|
357
|
+
* @returns Full DLQ entry details including original message data
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* const entry = await qb.dlq.get("uuid-123...");
|
|
362
|
+
* console.log(entry.failureReason);
|
|
363
|
+
* console.log(entry.totalAttempts);
|
|
364
|
+
* console.log(entry.body); // Original message body
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
async get(dlqId) {
|
|
368
|
+
return this.request("GET", `/dlq/${dlqId}`);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Retry a DLQ entry by creating a new message
|
|
372
|
+
*
|
|
373
|
+
* Creates a new message from the failed DLQ entry for redelivery.
|
|
374
|
+
* The DLQ entry is marked as recovered and linked to the new message.
|
|
375
|
+
*
|
|
376
|
+
* @param dlqId - The DLQ entry UUID to retry
|
|
377
|
+
* @returns The new message ID and recovery confirmation
|
|
378
|
+
* @throws Error if the entry was already recovered
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```typescript
|
|
382
|
+
* const result = await qb.dlq.retry("uuid-123...");
|
|
383
|
+
* console.log(result.newMessageId); // New message created
|
|
384
|
+
* console.log(result.recovered); // true
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
async retry(dlqId) {
|
|
388
|
+
return this.request("POST", `/dlq/${dlqId}/retry`);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Delete a DLQ entry permanently without retry
|
|
392
|
+
*
|
|
393
|
+
* @param dlqId - The DLQ entry UUID to delete
|
|
394
|
+
* @returns Deletion confirmation
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```typescript
|
|
398
|
+
* await qb.dlq.delete("uuid-123...");
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
async delete(dlqId) {
|
|
402
|
+
return this.request("DELETE", `/dlq/${dlqId}`);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Purge all DLQ entries for the current project
|
|
406
|
+
*
|
|
407
|
+
* Permanently deletes all DLQ entries without retry.
|
|
408
|
+
* Use with caution as this action cannot be undone.
|
|
409
|
+
*
|
|
410
|
+
* @returns The number of entries purged
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* const result = await qb.dlq.purge();
|
|
415
|
+
* console.log(`Purged ${result.count} entries`);
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
async purge() {
|
|
419
|
+
return this.request("POST", "/dlq/purge");
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Retry all DLQ entries for the current project
|
|
423
|
+
*
|
|
424
|
+
* Creates new messages from all failed DLQ entries.
|
|
425
|
+
*
|
|
426
|
+
* @returns Array of retry results
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* const results = await qb.dlq.retryAll();
|
|
431
|
+
* console.log(`Retried ${results.length} entries`);
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
async retryAll() {
|
|
435
|
+
const results = [];
|
|
436
|
+
let hasMore = true;
|
|
437
|
+
let offset = 0;
|
|
438
|
+
const limit = 100;
|
|
439
|
+
while (hasMore) {
|
|
440
|
+
const { entries, pagination } = await this.list({ limit, offset });
|
|
441
|
+
for (const entry of entries) {
|
|
442
|
+
try {
|
|
443
|
+
const result = await this.retry(entry.id);
|
|
444
|
+
results.push(result);
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
hasMore = pagination.hasMore;
|
|
449
|
+
offset += limit;
|
|
450
|
+
}
|
|
451
|
+
return results;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/api/workflows.ts
|
|
456
|
+
var WorkflowsAPI = class extends BaseClient {
|
|
457
|
+
constructor(options) {
|
|
458
|
+
super(options);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Trigger a new workflow run
|
|
462
|
+
*
|
|
463
|
+
* @param workflowId - Unique identifier for this workflow type
|
|
464
|
+
* @param workflowUrl - URL of the workflow endpoint
|
|
465
|
+
* @param input - Input data for the workflow
|
|
466
|
+
* @param options - Optional settings (idempotencyKey, maxDuration, metadata)
|
|
467
|
+
* @returns The run ID
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* const { runId } = await qb.workflows.trigger(
|
|
472
|
+
* "user-onboarding",
|
|
473
|
+
* "https://your-app.com/api/workflows/onboarding",
|
|
474
|
+
* { userId: "123", email: "user@example.com" },
|
|
475
|
+
* { idempotencyKey: "onboarding-user-123" }
|
|
476
|
+
* );
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
async trigger(workflowId, workflowUrl, input, options) {
|
|
480
|
+
return this.request("POST", `/workflows/${workflowId}/trigger`, {
|
|
481
|
+
body: {
|
|
482
|
+
workflowUrl,
|
|
483
|
+
input,
|
|
484
|
+
idempotencyKey: options?.idempotencyKey,
|
|
485
|
+
maxDuration: options?.maxDuration,
|
|
486
|
+
metadata: options?.metadata
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Get workflow run status with all steps
|
|
492
|
+
*
|
|
493
|
+
* @param runId - The workflow run ID
|
|
494
|
+
* @returns Full run details including steps
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* ```typescript
|
|
498
|
+
* const status = await qb.workflows.getStatus(runId);
|
|
499
|
+
* console.log(status.status); // "running" | "sleeping" | "completed"
|
|
500
|
+
* console.log(status.steps); // Array of step details
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
async getStatus(runId) {
|
|
504
|
+
return this.request("GET", `/workflows/runs/${runId}`);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Cancel a running workflow
|
|
508
|
+
*
|
|
509
|
+
* @param runId - The workflow run ID to cancel
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* ```typescript
|
|
513
|
+
* await qb.workflows.cancel(runId);
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
async cancel(runId) {
|
|
517
|
+
await this.request("DELETE", `/workflows/runs/${runId}`);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Retry a failed or timed out workflow
|
|
521
|
+
* Resumes from the last completed step
|
|
522
|
+
*
|
|
523
|
+
* @param runId - The workflow run ID to retry
|
|
524
|
+
* @returns The new run ID if a new run was created
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* ```typescript
|
|
528
|
+
* const result = await qb.workflows.retry(runId);
|
|
529
|
+
* console.log(result.runId);
|
|
530
|
+
* console.log(result.resumed); // true
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
async retry(runId) {
|
|
534
|
+
return this.request(
|
|
535
|
+
"POST",
|
|
536
|
+
`/workflows/runs/${runId}/retry`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* List runs for a specific workflow
|
|
541
|
+
*
|
|
542
|
+
* @param workflowId - The workflow type identifier
|
|
543
|
+
* @param options - Filter by status and pagination
|
|
544
|
+
* @returns Paginated list of workflow runs
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```typescript
|
|
548
|
+
* const { runs, pagination } = await qb.workflows.listRuns("user-onboarding", {
|
|
549
|
+
* status: "completed",
|
|
550
|
+
* limit: 20
|
|
551
|
+
* });
|
|
552
|
+
* ```
|
|
553
|
+
*/
|
|
554
|
+
async listRuns(workflowId, options) {
|
|
555
|
+
return this.request("GET", `/workflows/${workflowId}/runs`, {
|
|
556
|
+
queryParams: {
|
|
557
|
+
status: options?.status,
|
|
558
|
+
limit: options?.limit,
|
|
559
|
+
offset: options?.offset
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Send an event to workflows waiting with waitForEvent()
|
|
565
|
+
*
|
|
566
|
+
* @param eventName - Name of the event to send
|
|
567
|
+
* @param options - Optional event key and payload
|
|
568
|
+
* @returns The number of workflows that received the event
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ```typescript
|
|
572
|
+
* const result = await qb.workflows.sendEvent("order.approved", {
|
|
573
|
+
* eventKey: "order-123",
|
|
574
|
+
* payload: { status: "approved" }
|
|
575
|
+
* });
|
|
576
|
+
* console.log(`Notified ${result.matchedRuns} workflows`);
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
async sendEvent(eventName, options) {
|
|
580
|
+
return this.request(
|
|
581
|
+
"POST",
|
|
582
|
+
`/workflows/events/${encodeURIComponent(eventName)}`,
|
|
583
|
+
{
|
|
584
|
+
body: {
|
|
585
|
+
eventKey: options?.eventKey,
|
|
586
|
+
payload: options?.payload
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Wait for a workflow to complete
|
|
593
|
+
* Polls the status until the workflow reaches a terminal state
|
|
594
|
+
*
|
|
595
|
+
* @param runId - The workflow run ID
|
|
596
|
+
* @param options - Polling options
|
|
597
|
+
* @returns The final workflow status
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* ```typescript
|
|
601
|
+
* const result = await qb.workflows.waitForCompletion(runId, {
|
|
602
|
+
* pollIntervalMs: 2000,
|
|
603
|
+
* timeoutMs: 60000
|
|
604
|
+
* });
|
|
605
|
+
* console.log(result.status); // "completed" | "failed" | "cancelled"
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
async waitForCompletion(runId, options) {
|
|
609
|
+
const pollInterval = options?.pollIntervalMs || 1e3;
|
|
610
|
+
const timeout = options?.timeoutMs || 3e5;
|
|
611
|
+
const startTime = Date.now();
|
|
612
|
+
const terminalStates = [
|
|
613
|
+
"completed",
|
|
614
|
+
"failed",
|
|
615
|
+
"cancelled",
|
|
616
|
+
"timed_out"
|
|
617
|
+
];
|
|
618
|
+
while (Date.now() - startTime < timeout) {
|
|
619
|
+
const status = await this.getStatus(runId);
|
|
620
|
+
if (terminalStates.includes(status.status)) {
|
|
621
|
+
return status;
|
|
622
|
+
}
|
|
623
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
624
|
+
}
|
|
625
|
+
throw new Error(`Workflow did not complete within ${timeout}ms`);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Trigger a workflow and wait for completion
|
|
629
|
+
*
|
|
630
|
+
* @param workflowId - Unique identifier for this workflow type
|
|
631
|
+
* @param workflowUrl - URL of the workflow endpoint
|
|
632
|
+
* @param input - Input data for the workflow
|
|
633
|
+
* @param options - Trigger and polling options
|
|
634
|
+
* @returns The final workflow status
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```typescript
|
|
638
|
+
* const result = await qb.workflows.triggerAndWait(
|
|
639
|
+
* "user-onboarding",
|
|
640
|
+
* "https://your-app.com/api/workflows/onboarding",
|
|
641
|
+
* { userId: "123" },
|
|
642
|
+
* { timeoutMs: 120000 }
|
|
643
|
+
* );
|
|
644
|
+
* console.log(result.result); // Workflow output
|
|
645
|
+
* ```
|
|
646
|
+
*/
|
|
647
|
+
async triggerAndWait(workflowId, workflowUrl, input, options) {
|
|
648
|
+
const { pollIntervalMs, timeoutMs, ...triggerOptions } = options || {};
|
|
649
|
+
const { runId } = await this.trigger(workflowId, workflowUrl, input, triggerOptions);
|
|
650
|
+
return this.waitForCompletion(runId, { pollIntervalMs, timeoutMs });
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// src/queuebear.ts
|
|
655
|
+
var QueueBear = class {
|
|
656
|
+
options;
|
|
657
|
+
/**
|
|
658
|
+
* Messages API for publishing and managing webhook messages
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* ```typescript
|
|
662
|
+
* // Publish a message
|
|
663
|
+
* const { messageId } = await qb.messages.publish(
|
|
664
|
+
* "https://api.example.com/webhook",
|
|
665
|
+
* { event: "user.created", userId: "123" },
|
|
666
|
+
* { delay: "30s", retries: 5 }
|
|
667
|
+
* );
|
|
668
|
+
*
|
|
669
|
+
* // Get message status
|
|
670
|
+
* const message = await qb.messages.get(messageId);
|
|
671
|
+
*
|
|
672
|
+
* // List messages
|
|
673
|
+
* const { messages } = await qb.messages.list({ status: "pending" });
|
|
674
|
+
*
|
|
675
|
+
* // Cancel a message
|
|
676
|
+
* await qb.messages.cancel(messageId);
|
|
677
|
+
* ```
|
|
678
|
+
*/
|
|
679
|
+
messages;
|
|
680
|
+
/**
|
|
681
|
+
* Schedules API for managing cron-based recurring jobs
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* ```typescript
|
|
685
|
+
* // Create a schedule
|
|
686
|
+
* const schedule = await qb.schedules.create({
|
|
687
|
+
* destination: "https://api.example.com/cron-job",
|
|
688
|
+
* cron: "0 9 * * *", // Daily at 9 AM
|
|
689
|
+
* timezone: "America/New_York",
|
|
690
|
+
* });
|
|
691
|
+
*
|
|
692
|
+
* // Pause a schedule
|
|
693
|
+
* await qb.schedules.pause(schedule.scheduleId);
|
|
694
|
+
*
|
|
695
|
+
* // Resume a schedule
|
|
696
|
+
* await qb.schedules.resume(schedule.scheduleId);
|
|
697
|
+
*
|
|
698
|
+
* // Delete a schedule
|
|
699
|
+
* await qb.schedules.delete(schedule.scheduleId);
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
schedules;
|
|
703
|
+
/**
|
|
704
|
+
* DLQ API for managing failed messages in the dead letter queue
|
|
705
|
+
*
|
|
706
|
+
* @example
|
|
707
|
+
* ```typescript
|
|
708
|
+
* // List failed messages
|
|
709
|
+
* const { entries } = await qb.dlq.list();
|
|
710
|
+
*
|
|
711
|
+
* // Retry a failed message
|
|
712
|
+
* const result = await qb.dlq.retry(entries[0].id);
|
|
713
|
+
*
|
|
714
|
+
* // Delete a DLQ entry
|
|
715
|
+
* await qb.dlq.delete(entries[0].id);
|
|
716
|
+
*
|
|
717
|
+
* // Purge all DLQ entries
|
|
718
|
+
* await qb.dlq.purge();
|
|
719
|
+
* ```
|
|
720
|
+
*/
|
|
721
|
+
dlq;
|
|
722
|
+
/**
|
|
723
|
+
* Workflows API for triggering and managing durable workflows
|
|
724
|
+
*
|
|
725
|
+
* @example
|
|
726
|
+
* ```typescript
|
|
727
|
+
* // Trigger a workflow
|
|
728
|
+
* const { runId } = await qb.workflows.trigger(
|
|
729
|
+
* "user-onboarding",
|
|
730
|
+
* "https://your-app.com/api/workflows/onboarding",
|
|
731
|
+
* { userId: "123", email: "user@example.com" },
|
|
732
|
+
* { idempotencyKey: "onboarding-user-123" }
|
|
733
|
+
* );
|
|
734
|
+
*
|
|
735
|
+
* // Check status
|
|
736
|
+
* const status = await qb.workflows.getStatus(runId);
|
|
737
|
+
*
|
|
738
|
+
* // Wait for completion
|
|
739
|
+
* const result = await qb.workflows.waitForCompletion(runId);
|
|
740
|
+
*
|
|
741
|
+
* // Send event to waiting workflows
|
|
742
|
+
* await qb.workflows.sendEvent("order.approved", {
|
|
743
|
+
* eventKey: "order-123",
|
|
744
|
+
* payload: { status: "approved" }
|
|
745
|
+
* });
|
|
746
|
+
* ```
|
|
747
|
+
*/
|
|
748
|
+
workflows;
|
|
749
|
+
constructor(options) {
|
|
750
|
+
this.options = {
|
|
751
|
+
baseUrl: options.baseUrl.replace(/\/$/, ""),
|
|
752
|
+
apiKey: options.apiKey,
|
|
753
|
+
projectId: options.projectId
|
|
754
|
+
};
|
|
755
|
+
this.messages = new MessagesAPI(this.options);
|
|
756
|
+
this.schedules = new SchedulesAPI(this.options);
|
|
757
|
+
this.dlq = new DLQAPI(this.options);
|
|
758
|
+
this.workflows = new WorkflowsAPI(this.options);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Publish a message and wait for delivery completion
|
|
762
|
+
*
|
|
763
|
+
* Convenience method that combines publish + polling in one call.
|
|
764
|
+
*
|
|
765
|
+
* @param destination - The URL to deliver the message to
|
|
766
|
+
* @param body - The message body
|
|
767
|
+
* @param options - Publish options plus polling configuration
|
|
768
|
+
* @returns The final message status after delivery
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```typescript
|
|
772
|
+
* const message = await qb.publishAndWait(
|
|
773
|
+
* "https://api.example.com/webhook",
|
|
774
|
+
* { event: "user.created" },
|
|
775
|
+
* { timeoutMs: 30000 }
|
|
776
|
+
* );
|
|
777
|
+
* console.log(message.status); // "completed"
|
|
778
|
+
* ```
|
|
779
|
+
*/
|
|
780
|
+
async publishAndWait(destination, body, options) {
|
|
781
|
+
return this.messages.publishAndWait(destination, body, options);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Trigger a workflow and wait for completion
|
|
785
|
+
*
|
|
786
|
+
* Convenience method that combines trigger + polling in one call.
|
|
787
|
+
*
|
|
788
|
+
* @param workflowId - Unique identifier for this workflow type
|
|
789
|
+
* @param workflowUrl - URL of the workflow endpoint
|
|
790
|
+
* @param input - Input data for the workflow
|
|
791
|
+
* @param options - Trigger and polling options
|
|
792
|
+
* @returns The final workflow status
|
|
793
|
+
*
|
|
794
|
+
* @example
|
|
795
|
+
* ```typescript
|
|
796
|
+
* const result = await qb.triggerAndWait(
|
|
797
|
+
* "user-onboarding",
|
|
798
|
+
* "https://your-app.com/api/workflows/onboarding",
|
|
799
|
+
* { userId: "123" },
|
|
800
|
+
* { timeoutMs: 120000 }
|
|
801
|
+
* );
|
|
802
|
+
* console.log(result.result); // Workflow output
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
async triggerAndWait(workflowId, workflowUrl, input, options) {
|
|
806
|
+
return this.workflows.triggerAndWait(workflowId, workflowUrl, input, options);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/context.ts
|
|
811
|
+
var WorkflowPausedError = class extends Error {
|
|
812
|
+
constructor(reason) {
|
|
813
|
+
super(reason);
|
|
814
|
+
this.reason = reason;
|
|
815
|
+
this.name = "WorkflowPausedError";
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
var WorkflowContext = class {
|
|
819
|
+
runId;
|
|
820
|
+
input;
|
|
821
|
+
runToken;
|
|
822
|
+
baseUrl;
|
|
823
|
+
authHeader;
|
|
824
|
+
stepIndex = 0;
|
|
825
|
+
constructor(options) {
|
|
826
|
+
this.runId = options.runId;
|
|
827
|
+
this.runToken = options.runToken;
|
|
828
|
+
this.input = options.input;
|
|
829
|
+
this.baseUrl = options.baseUrl;
|
|
830
|
+
this.authHeader = options.authHeader;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Execute a step with caching
|
|
834
|
+
* Step names must be unique within a workflow run
|
|
835
|
+
* @param stepName - Unique name for this step
|
|
836
|
+
* @param fn - Function to execute
|
|
837
|
+
* @param options - Optional retry configuration
|
|
838
|
+
*/
|
|
839
|
+
async run(stepName, fn, options) {
|
|
840
|
+
const checkResponse = await fetch(
|
|
841
|
+
`${this.baseUrl}/v1/workflows/internal/step/check`,
|
|
842
|
+
{
|
|
843
|
+
method: "POST",
|
|
844
|
+
headers: {
|
|
845
|
+
"Content-Type": "application/json",
|
|
846
|
+
Authorization: this.authHeader,
|
|
847
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
848
|
+
},
|
|
849
|
+
body: JSON.stringify({
|
|
850
|
+
runId: this.runId,
|
|
851
|
+
stepName
|
|
852
|
+
})
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
const checkData = await checkResponse.json();
|
|
856
|
+
if (checkData.cached) {
|
|
857
|
+
this.stepIndex++;
|
|
858
|
+
return checkData.result;
|
|
859
|
+
}
|
|
860
|
+
const startResponse = await fetch(
|
|
861
|
+
`${this.baseUrl}/v1/workflows/internal/step/start`,
|
|
862
|
+
{
|
|
863
|
+
method: "POST",
|
|
864
|
+
headers: {
|
|
865
|
+
"Content-Type": "application/json",
|
|
866
|
+
Authorization: this.authHeader,
|
|
867
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
868
|
+
},
|
|
869
|
+
body: JSON.stringify({
|
|
870
|
+
runId: this.runId,
|
|
871
|
+
stepName,
|
|
872
|
+
stepIndex: this.stepIndex,
|
|
873
|
+
stepType: "run",
|
|
874
|
+
retryOptions: options
|
|
875
|
+
})
|
|
876
|
+
}
|
|
877
|
+
);
|
|
878
|
+
if (!startResponse.ok) {
|
|
879
|
+
const errorData = await startResponse.json();
|
|
880
|
+
throw new Error(errorData.error || "Failed to start step");
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
const result = await fn();
|
|
884
|
+
await fetch(`${this.baseUrl}/v1/workflows/internal/step/complete`, {
|
|
885
|
+
method: "POST",
|
|
886
|
+
headers: {
|
|
887
|
+
"Content-Type": "application/json",
|
|
888
|
+
Authorization: this.authHeader,
|
|
889
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
890
|
+
},
|
|
891
|
+
body: JSON.stringify({
|
|
892
|
+
runId: this.runId,
|
|
893
|
+
stepName,
|
|
894
|
+
result
|
|
895
|
+
})
|
|
896
|
+
});
|
|
897
|
+
this.stepIndex++;
|
|
898
|
+
return result;
|
|
899
|
+
} catch (error) {
|
|
900
|
+
await fetch(`${this.baseUrl}/v1/workflows/internal/step/fail`, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: {
|
|
903
|
+
"Content-Type": "application/json",
|
|
904
|
+
Authorization: this.authHeader,
|
|
905
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
906
|
+
},
|
|
907
|
+
body: JSON.stringify({
|
|
908
|
+
runId: this.runId,
|
|
909
|
+
stepName,
|
|
910
|
+
error: error instanceof Error ? error.message : String(error)
|
|
911
|
+
})
|
|
912
|
+
});
|
|
913
|
+
throw error;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Sleep for specified seconds
|
|
918
|
+
* Step names must be unique within a workflow run
|
|
919
|
+
*/
|
|
920
|
+
async sleep(stepName, seconds) {
|
|
921
|
+
const checkResponse = await fetch(
|
|
922
|
+
`${this.baseUrl}/v1/workflows/internal/step/check`,
|
|
923
|
+
{
|
|
924
|
+
method: "POST",
|
|
925
|
+
headers: {
|
|
926
|
+
"Content-Type": "application/json",
|
|
927
|
+
Authorization: this.authHeader,
|
|
928
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
929
|
+
},
|
|
930
|
+
body: JSON.stringify({
|
|
931
|
+
runId: this.runId,
|
|
932
|
+
stepName
|
|
933
|
+
})
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
const checkData = await checkResponse.json();
|
|
937
|
+
if (checkData.cached) {
|
|
938
|
+
this.stepIndex++;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const sleepResponse = await fetch(
|
|
942
|
+
`${this.baseUrl}/v1/workflows/internal/sleep`,
|
|
943
|
+
{
|
|
944
|
+
method: "POST",
|
|
945
|
+
headers: {
|
|
946
|
+
"Content-Type": "application/json",
|
|
947
|
+
Authorization: this.authHeader,
|
|
948
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
949
|
+
},
|
|
950
|
+
body: JSON.stringify({
|
|
951
|
+
runId: this.runId,
|
|
952
|
+
stepName,
|
|
953
|
+
stepIndex: this.stepIndex,
|
|
954
|
+
seconds
|
|
955
|
+
})
|
|
956
|
+
}
|
|
957
|
+
);
|
|
958
|
+
if (!sleepResponse.ok) {
|
|
959
|
+
const errorData = await sleepResponse.json();
|
|
960
|
+
throw new Error(errorData.error || "Failed to schedule sleep");
|
|
961
|
+
}
|
|
962
|
+
throw new WorkflowPausedError(`Sleeping for ${seconds}s`);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Sleep until specific date
|
|
966
|
+
*/
|
|
967
|
+
async sleepUntil(stepName, until) {
|
|
968
|
+
const seconds = Math.max(
|
|
969
|
+
0,
|
|
970
|
+
Math.ceil((until.getTime() - Date.now()) / 1e3)
|
|
971
|
+
);
|
|
972
|
+
return this.sleep(stepName, seconds);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Get all completed steps for the current run
|
|
976
|
+
* Useful for inspecting progress or debugging
|
|
977
|
+
*/
|
|
978
|
+
async getCompletedSteps() {
|
|
979
|
+
const response = await fetch(
|
|
980
|
+
`${this.baseUrl}/v1/workflows/internal/steps/${this.runId}`,
|
|
981
|
+
{
|
|
982
|
+
headers: {
|
|
983
|
+
Authorization: this.authHeader,
|
|
984
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
throw new Error("Failed to fetch completed steps");
|
|
990
|
+
}
|
|
991
|
+
const data = await response.json();
|
|
992
|
+
return data.steps;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Make HTTP call (executed as a step)
|
|
996
|
+
*/
|
|
997
|
+
async call(stepName, config) {
|
|
998
|
+
return this.run(stepName, async () => {
|
|
999
|
+
const response = await fetch(config.url, {
|
|
1000
|
+
method: config.method || "GET",
|
|
1001
|
+
headers: {
|
|
1002
|
+
"Content-Type": "application/json",
|
|
1003
|
+
...config.headers
|
|
1004
|
+
},
|
|
1005
|
+
body: config.body ? JSON.stringify(config.body) : void 0
|
|
1006
|
+
});
|
|
1007
|
+
if (!response.ok) {
|
|
1008
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1009
|
+
}
|
|
1010
|
+
const contentType = response.headers.get("content-type");
|
|
1011
|
+
if (contentType?.includes("application/json")) {
|
|
1012
|
+
return response.json();
|
|
1013
|
+
}
|
|
1014
|
+
return response.text();
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Wait for an external event
|
|
1019
|
+
* @param stepName - Unique step name
|
|
1020
|
+
* @param eventName - Name of the event to wait for
|
|
1021
|
+
* @param options - Optional event key and timeout
|
|
1022
|
+
*/
|
|
1023
|
+
async waitForEvent(stepName, eventName, options) {
|
|
1024
|
+
const checkResponse = await fetch(
|
|
1025
|
+
`${this.baseUrl}/v1/workflows/internal/event/check`,
|
|
1026
|
+
{
|
|
1027
|
+
method: "POST",
|
|
1028
|
+
headers: {
|
|
1029
|
+
"Content-Type": "application/json",
|
|
1030
|
+
Authorization: this.authHeader,
|
|
1031
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
1032
|
+
},
|
|
1033
|
+
body: JSON.stringify({
|
|
1034
|
+
runId: this.runId,
|
|
1035
|
+
stepName
|
|
1036
|
+
})
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
const checkData = await checkResponse.json();
|
|
1040
|
+
if (checkData.received) {
|
|
1041
|
+
this.stepIndex++;
|
|
1042
|
+
return checkData.payload;
|
|
1043
|
+
}
|
|
1044
|
+
if (checkData.expired) {
|
|
1045
|
+
throw new Error(`Event "${eventName}" timed out`);
|
|
1046
|
+
}
|
|
1047
|
+
const waitResponse = await fetch(
|
|
1048
|
+
`${this.baseUrl}/v1/workflows/internal/event/wait`,
|
|
1049
|
+
{
|
|
1050
|
+
method: "POST",
|
|
1051
|
+
headers: {
|
|
1052
|
+
"Content-Type": "application/json",
|
|
1053
|
+
Authorization: this.authHeader,
|
|
1054
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
1055
|
+
},
|
|
1056
|
+
body: JSON.stringify({
|
|
1057
|
+
runId: this.runId,
|
|
1058
|
+
stepName,
|
|
1059
|
+
stepIndex: this.stepIndex,
|
|
1060
|
+
eventName,
|
|
1061
|
+
eventKey: options?.eventKey,
|
|
1062
|
+
timeoutSeconds: options?.timeoutSeconds
|
|
1063
|
+
})
|
|
1064
|
+
}
|
|
1065
|
+
);
|
|
1066
|
+
if (!waitResponse.ok) {
|
|
1067
|
+
const errorData = await waitResponse.json();
|
|
1068
|
+
throw new Error(errorData.error || "Failed to register event wait");
|
|
1069
|
+
}
|
|
1070
|
+
throw new WorkflowPausedError(`Waiting for event: ${eventName}`);
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Send a fire-and-forget notification event
|
|
1074
|
+
* @param eventName - Name of the event to send
|
|
1075
|
+
* @param payload - Optional payload to include
|
|
1076
|
+
*/
|
|
1077
|
+
async notify(eventName, payload) {
|
|
1078
|
+
const response = await fetch(
|
|
1079
|
+
`${this.baseUrl}/v1/workflows/events/${encodeURIComponent(eventName)}`,
|
|
1080
|
+
{
|
|
1081
|
+
method: "POST",
|
|
1082
|
+
headers: {
|
|
1083
|
+
"Content-Type": "application/json",
|
|
1084
|
+
Authorization: this.authHeader
|
|
1085
|
+
},
|
|
1086
|
+
body: JSON.stringify({ payload })
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
if (!response.ok) {
|
|
1090
|
+
const errorData = await response.json();
|
|
1091
|
+
throw new Error(errorData.error || `Failed to send event: ${eventName}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Execute multiple steps in parallel
|
|
1096
|
+
* @param steps - Array of step definitions with name and function
|
|
1097
|
+
*/
|
|
1098
|
+
async parallel(steps) {
|
|
1099
|
+
const stepNames = steps.map((s) => s.name);
|
|
1100
|
+
const batchResponse = await fetch(
|
|
1101
|
+
`${this.baseUrl}/v1/workflows/internal/steps/batch-check`,
|
|
1102
|
+
{
|
|
1103
|
+
method: "POST",
|
|
1104
|
+
headers: {
|
|
1105
|
+
"Content-Type": "application/json",
|
|
1106
|
+
Authorization: this.authHeader,
|
|
1107
|
+
"X-QueueBear-Run-Token": this.runToken
|
|
1108
|
+
},
|
|
1109
|
+
body: JSON.stringify({
|
|
1110
|
+
runId: this.runId,
|
|
1111
|
+
stepNames
|
|
1112
|
+
})
|
|
1113
|
+
}
|
|
1114
|
+
);
|
|
1115
|
+
if (!batchResponse.ok) {
|
|
1116
|
+
throw new Error("Failed to batch check steps");
|
|
1117
|
+
}
|
|
1118
|
+
const batchData = await batchResponse.json();
|
|
1119
|
+
const results = new Array(steps.length);
|
|
1120
|
+
const pendingPromises = [];
|
|
1121
|
+
const errors = [];
|
|
1122
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1123
|
+
const step = steps[i];
|
|
1124
|
+
const cached = batchData.steps[step.name];
|
|
1125
|
+
if (cached?.cached) {
|
|
1126
|
+
results[i] = cached.result;
|
|
1127
|
+
} else {
|
|
1128
|
+
pendingPromises.push(
|
|
1129
|
+
this.run(step.name, step.fn).then((result) => {
|
|
1130
|
+
results[i] = result;
|
|
1131
|
+
}).catch((error) => {
|
|
1132
|
+
errors.push({
|
|
1133
|
+
index: i,
|
|
1134
|
+
stepName: step.name,
|
|
1135
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1136
|
+
});
|
|
1137
|
+
})
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
await Promise.all(pendingPromises);
|
|
1142
|
+
if (errors.length > 0) {
|
|
1143
|
+
const errorMessages = errors.map((e) => `Step "${e.stepName}": ${e.error.message}`).join("; ");
|
|
1144
|
+
throw new ParallelExecutionError(
|
|
1145
|
+
`${errors.length} of ${steps.length} parallel steps failed: ${errorMessages}`,
|
|
1146
|
+
errors
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
return results;
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
var ParallelExecutionError = class extends Error {
|
|
1153
|
+
constructor(message, errors) {
|
|
1154
|
+
super(message);
|
|
1155
|
+
this.errors = errors;
|
|
1156
|
+
this.name = "ParallelExecutionError";
|
|
1157
|
+
}
|
|
1158
|
+
get failedStepCount() {
|
|
1159
|
+
return this.errors.length;
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
// src/serve.ts
|
|
1164
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
1165
|
+
function serve(handler, options) {
|
|
1166
|
+
return async (request) => {
|
|
1167
|
+
let runId;
|
|
1168
|
+
let runToken;
|
|
1169
|
+
let baseUrl;
|
|
1170
|
+
try {
|
|
1171
|
+
if (options?.signingSecret) {
|
|
1172
|
+
const signature = request.headers.get("X-QueueBear-Signature");
|
|
1173
|
+
const bodyText = await request.clone().text();
|
|
1174
|
+
if (!verifySignature(bodyText, signature, options.signingSecret)) {
|
|
1175
|
+
return new Response(JSON.stringify({ error: "Invalid signature" }), {
|
|
1176
|
+
status: 401,
|
|
1177
|
+
headers: { "Content-Type": "application/json" }
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const body = await request.json();
|
|
1182
|
+
runId = body.runId || request.headers.get("X-QueueBear-Workflow-Run") || "";
|
|
1183
|
+
runToken = request.headers.get("X-QueueBear-Run-Token") || "";
|
|
1184
|
+
if (!runId) {
|
|
1185
|
+
return new Response(JSON.stringify({ error: "Missing runId" }), {
|
|
1186
|
+
status: 400,
|
|
1187
|
+
headers: { "Content-Type": "application/json" }
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
baseUrl = options?.baseUrl || getBaseUrl(request);
|
|
1191
|
+
const runResponse = await fetch(
|
|
1192
|
+
`${baseUrl}/v1/workflows/runs/${runId}`,
|
|
1193
|
+
{
|
|
1194
|
+
headers: {
|
|
1195
|
+
Authorization: request.headers.get("Authorization") || ""
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
);
|
|
1199
|
+
if (!runResponse.ok) {
|
|
1200
|
+
return new Response(
|
|
1201
|
+
JSON.stringify({ error: "Failed to fetch workflow run" }),
|
|
1202
|
+
{
|
|
1203
|
+
status: 500,
|
|
1204
|
+
headers: { "Content-Type": "application/json" }
|
|
1205
|
+
}
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
const runData = await runResponse.json();
|
|
1209
|
+
const context = new WorkflowContext({
|
|
1210
|
+
runId,
|
|
1211
|
+
runToken,
|
|
1212
|
+
input: runData.input,
|
|
1213
|
+
baseUrl,
|
|
1214
|
+
authHeader: request.headers.get("Authorization") || ""
|
|
1215
|
+
});
|
|
1216
|
+
const result = await handler(context);
|
|
1217
|
+
await fetch(`${baseUrl}/v1/workflows/internal/complete`, {
|
|
1218
|
+
method: "POST",
|
|
1219
|
+
headers: {
|
|
1220
|
+
"Content-Type": "application/json",
|
|
1221
|
+
Authorization: request.headers.get("Authorization") || "",
|
|
1222
|
+
"X-QueueBear-Run-Token": runToken
|
|
1223
|
+
},
|
|
1224
|
+
body: JSON.stringify({ runId, result })
|
|
1225
|
+
});
|
|
1226
|
+
return new Response(JSON.stringify({ completed: true, result }), {
|
|
1227
|
+
status: 200,
|
|
1228
|
+
headers: { "Content-Type": "application/json" }
|
|
1229
|
+
});
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (error instanceof WorkflowPausedError) {
|
|
1232
|
+
return new Response(
|
|
1233
|
+
JSON.stringify({
|
|
1234
|
+
completed: false,
|
|
1235
|
+
paused: true,
|
|
1236
|
+
reason: error.reason
|
|
1237
|
+
}),
|
|
1238
|
+
{
|
|
1239
|
+
status: 200,
|
|
1240
|
+
headers: { "Content-Type": "application/json" }
|
|
1241
|
+
}
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
console.error("[Workflow] Unhandled error - failing run:", error);
|
|
1245
|
+
if (runId && runToken && baseUrl) {
|
|
1246
|
+
try {
|
|
1247
|
+
await fetch(`${baseUrl}/v1/workflows/internal/fail`, {
|
|
1248
|
+
method: "POST",
|
|
1249
|
+
headers: {
|
|
1250
|
+
"Content-Type": "application/json",
|
|
1251
|
+
Authorization: request.headers.get("Authorization") || "",
|
|
1252
|
+
"X-QueueBear-Run-Token": runToken
|
|
1253
|
+
},
|
|
1254
|
+
body: JSON.stringify({
|
|
1255
|
+
runId,
|
|
1256
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1257
|
+
})
|
|
1258
|
+
});
|
|
1259
|
+
} catch (failError) {
|
|
1260
|
+
console.error("[Workflow] Failed to mark run as failed:", failError);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return new Response(
|
|
1264
|
+
JSON.stringify({
|
|
1265
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1266
|
+
failed: true
|
|
1267
|
+
}),
|
|
1268
|
+
{
|
|
1269
|
+
status: 500,
|
|
1270
|
+
headers: { "Content-Type": "application/json" }
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function verifySignature(body, signature, secret) {
|
|
1277
|
+
if (!signature) return false;
|
|
1278
|
+
const parts = signature.split(",").reduce(
|
|
1279
|
+
(acc, part) => {
|
|
1280
|
+
const [key, value] = part.split("=");
|
|
1281
|
+
acc[key] = value;
|
|
1282
|
+
return acc;
|
|
1283
|
+
},
|
|
1284
|
+
{}
|
|
1285
|
+
);
|
|
1286
|
+
if (!parts.t || !parts.v1) return false;
|
|
1287
|
+
const timestamp = parseInt(parts.t, 10);
|
|
1288
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1289
|
+
if (Math.abs(now - timestamp) > 300) return false;
|
|
1290
|
+
const payload = `${parts.t}.${body}`;
|
|
1291
|
+
const expected = createHmac("sha256", secret).update(payload).digest("hex");
|
|
1292
|
+
try {
|
|
1293
|
+
return timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
|
|
1294
|
+
} catch {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function getBaseUrl(request) {
|
|
1299
|
+
const url = new URL(request.url);
|
|
1300
|
+
return `${url.protocol}//${url.host}`;
|
|
1301
|
+
}
|
|
1302
|
+
export {
|
|
1303
|
+
DLQAPI,
|
|
1304
|
+
MessagesAPI,
|
|
1305
|
+
ParallelExecutionError,
|
|
1306
|
+
QueueBear,
|
|
1307
|
+
QueueBearError,
|
|
1308
|
+
SchedulesAPI,
|
|
1309
|
+
WorkflowContext,
|
|
1310
|
+
WorkflowPausedError,
|
|
1311
|
+
WorkflowsAPI,
|
|
1312
|
+
serve
|
|
1313
|
+
};
|