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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Robert Marshall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,484 @@
1
+ # queuebear
2
+
3
+ QueueBear SDK for building durable workflows and managing message queues.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install queuebear
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { QueueBear, serve } from "queuebear";
15
+
16
+ // Create client
17
+ const qb = new QueueBear({
18
+ baseUrl: "https://your-queuebear-instance.com",
19
+ apiKey: "qb_live_xxx",
20
+ projectId: "proj_xxx",
21
+ });
22
+ ```
23
+
24
+ ## API Overview
25
+
26
+ The SDK provides access to all QueueBear APIs:
27
+
28
+ | API | Description |
29
+ |-----|-------------|
30
+ | `qb.messages` | Publish and manage webhook messages |
31
+ | `qb.schedules` | Create and manage cron-based recurring jobs |
32
+ | `qb.dlq` | Manage failed messages in the dead letter queue |
33
+ | `qb.workflows` | Trigger and manage durable workflows |
34
+
35
+ ---
36
+
37
+ ## Messages API
38
+
39
+ Publish messages to be delivered to webhook destinations with automatic retries.
40
+
41
+ ### Publish a Message
42
+
43
+ ```typescript
44
+ const { messageId } = await qb.messages.publish(
45
+ "https://api.example.com/webhook",
46
+ { event: "user.created", userId: "123" },
47
+ {
48
+ delay: "30s", // Delay before delivery
49
+ retries: 5, // Number of retry attempts
50
+ method: "POST", // HTTP method
51
+ headers: { "X-API-Key": "secret" }, // Headers to forward
52
+ callbackUrl: "https://...", // Success callback
53
+ failureCallbackUrl: "https://...", // Failure callback
54
+ deduplicationId: "unique-id", // Prevent duplicate messages
55
+ }
56
+ );
57
+ ```
58
+
59
+ ### Get Message Status
60
+
61
+ ```typescript
62
+ const message = await qb.messages.get(messageId);
63
+ console.log(message.status); // "pending" | "completed" | "failed"
64
+ console.log(message.deliveryLogs); // Delivery attempt history
65
+ ```
66
+
67
+ ### List Messages
68
+
69
+ ```typescript
70
+ const { messages, pagination } = await qb.messages.list({
71
+ status: "pending",
72
+ limit: 20,
73
+ offset: 0,
74
+ });
75
+ ```
76
+
77
+ ### Cancel a Message
78
+
79
+ ```typescript
80
+ await qb.messages.cancel(messageId);
81
+ ```
82
+
83
+ ### Publish and Wait
84
+
85
+ ```typescript
86
+ const message = await qb.messages.publishAndWait(
87
+ "https://api.example.com/webhook",
88
+ { event: "user.created" },
89
+ { timeoutMs: 30000 }
90
+ );
91
+ console.log(message.status); // "completed"
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Schedules API
97
+
98
+ Create cron-based recurring jobs.
99
+
100
+ ### Create a Schedule
101
+
102
+ ```typescript
103
+ const schedule = await qb.schedules.create({
104
+ destination: "https://api.example.com/cron-job",
105
+ cron: "0 9 * * *", // Daily at 9 AM
106
+ timezone: "America/New_York",
107
+ method: "POST",
108
+ body: JSON.stringify({ type: "daily-report" }),
109
+ headers: { "Content-Type": "application/json" },
110
+ retries: 3,
111
+ metadata: { jobName: "daily-report" },
112
+ });
113
+ ```
114
+
115
+ ### Common Cron Expressions
116
+
117
+ | Expression | Description |
118
+ |------------|-------------|
119
+ | `* * * * *` | Every minute |
120
+ | `0 * * * *` | Every hour |
121
+ | `0 9 * * *` | Daily at 9:00 AM |
122
+ | `0 9 * * 1-5` | Weekdays at 9:00 AM |
123
+ | `0 0 1 * *` | First day of each month |
124
+ | `0 */6 * * *` | Every 6 hours |
125
+
126
+ ### List Schedules
127
+
128
+ ```typescript
129
+ const { schedules } = await qb.schedules.list();
130
+ ```
131
+
132
+ ### Pause / Resume
133
+
134
+ ```typescript
135
+ await qb.schedules.pause(scheduleId);
136
+ await qb.schedules.resume(scheduleId);
137
+ ```
138
+
139
+ ### Delete a Schedule
140
+
141
+ ```typescript
142
+ await qb.schedules.delete(scheduleId);
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Dead Letter Queue (DLQ) API
148
+
149
+ Manage messages that failed all retry attempts.
150
+
151
+ ### List DLQ Entries
152
+
153
+ ```typescript
154
+ const { entries } = await qb.dlq.list();
155
+ for (const entry of entries) {
156
+ console.log(`${entry.id}: ${entry.failureReason}`);
157
+ }
158
+ ```
159
+
160
+ ### Get Entry Details
161
+
162
+ ```typescript
163
+ const entry = await qb.dlq.get(dlqId);
164
+ console.log(entry.body); // Original message body
165
+ console.log(entry.totalAttempts); // Number of failed attempts
166
+ ```
167
+
168
+ ### Retry a Failed Message
169
+
170
+ ```typescript
171
+ const result = await qb.dlq.retry(dlqId);
172
+ console.log(result.newMessageId); // New message created
173
+ ```
174
+
175
+ ### Delete Entry / Purge All
176
+
177
+ ```typescript
178
+ await qb.dlq.delete(dlqId);
179
+ await qb.dlq.purge(); // Delete all entries
180
+ ```
181
+
182
+ ### Retry All Failed Messages
183
+
184
+ ```typescript
185
+ const results = await qb.dlq.retryAll();
186
+ console.log(`Retried ${results.length} entries`);
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Workflows API
192
+
193
+ Build durable, fault-tolerant workflows with automatic step caching.
194
+
195
+ Workflows consist of two parts:
196
+ 1. **Workflow endpoint** - Created with `serve()`, handles workflow execution
197
+ 2. **Client** - Uses `qb.workflows` to trigger and manage workflow runs
198
+
199
+ ---
200
+
201
+ ### `serve()` - Create a Workflow Endpoint
202
+
203
+ The `serve()` function creates an HTTP handler for your workflow. It receives requests from QueueBear, executes your workflow code, and manages step caching automatically.
204
+
205
+ ```typescript
206
+ import { serve } from "queuebear";
207
+
208
+ export const POST = serve<InputType>(async (context) => {
209
+ // Your workflow logic here
210
+ return result;
211
+ }, options);
212
+ ```
213
+
214
+ **Parameters:**
215
+
216
+ | Parameter | Type | Description |
217
+ |-----------|------|-------------|
218
+ | `handler` | `(context: WorkflowContext<T>) => Promise<R>` | Your workflow function |
219
+ | `options` | `ServeOptions` | Optional configuration |
220
+
221
+ **Options:**
222
+
223
+ | Option | Type | Description |
224
+ |--------|------|-------------|
225
+ | `signingSecret` | `string` | Secret to verify requests come from QueueBear |
226
+ | `baseUrl` | `string` | Override QueueBear base URL (auto-detected if not set) |
227
+
228
+ ### Framework Integration
229
+
230
+ **Next.js (App Router)**
231
+ ```typescript
232
+ // app/api/workflows/my-workflow/route.ts
233
+ import { serve } from "queuebear";
234
+
235
+ export const POST = serve(async (context) => {
236
+ await context.run("step-1", async () => { /* ... */ });
237
+ return { success: true };
238
+ });
239
+ ```
240
+
241
+ **Express**
242
+ ```typescript
243
+ import express from "express";
244
+ import { serve } from "queuebear";
245
+
246
+ const app = express();
247
+ app.use(express.json());
248
+
249
+ const handler = serve(async (context) => {
250
+ await context.run("step-1", async () => { /* ... */ });
251
+ return { success: true };
252
+ });
253
+
254
+ app.post("/api/workflows/my-workflow", async (req, res) => {
255
+ const response = await handler(new Request(req.url, {
256
+ method: "POST",
257
+ headers: req.headers as HeadersInit,
258
+ body: JSON.stringify(req.body),
259
+ }));
260
+ res.status(response.status).json(await response.json());
261
+ });
262
+ ```
263
+
264
+ **Hono**
265
+ ```typescript
266
+ import { Hono } from "hono";
267
+ import { serve } from "queuebear";
268
+
269
+ const app = new Hono();
270
+
271
+ const handler = serve(async (context) => {
272
+ await context.run("step-1", async () => { /* ... */ });
273
+ return { success: true };
274
+ });
275
+
276
+ app.post("/api/workflows/my-workflow", async (c) => {
277
+ const response = await handler(c.req.raw);
278
+ return response;
279
+ });
280
+ ```
281
+
282
+ ---
283
+
284
+ ### Complete Workflow Example
285
+
286
+ ```typescript
287
+ // app/api/workflows/onboarding/route.ts
288
+ import { serve } from "queuebear";
289
+
290
+ interface OnboardingInput {
291
+ userId: string;
292
+ email: string;
293
+ }
294
+
295
+ export const POST = serve<OnboardingInput>(async (context) => {
296
+ const { userId, email } = context.input;
297
+
298
+ // Step 1: Send welcome email (cached if already done)
299
+ await context.run("send-welcome", async () => {
300
+ await sendEmail(email, "welcome");
301
+ });
302
+
303
+ // Step 2: Wait 3 days
304
+ await context.sleep("wait-3-days", 60 * 60 * 24 * 3);
305
+
306
+ // Step 3: Send tips email
307
+ await context.run("send-tips", async () => {
308
+ await sendEmail(email, "tips");
309
+ });
310
+
311
+ return { completed: true };
312
+ }, {
313
+ signingSecret: process.env.QUEUEBEAR_SIGNING_SECRET,
314
+ });
315
+ ```
316
+
317
+ ### Trigger a Workflow
318
+
319
+ ```typescript
320
+ const { runId } = await qb.workflows.trigger(
321
+ "user-onboarding",
322
+ "https://your-app.com/api/workflows/onboarding",
323
+ { userId: "123", email: "user@example.com" },
324
+ {
325
+ idempotencyKey: "onboarding-user-123",
326
+ maxDuration: 60 * 60 * 24 * 7, // 7 day timeout
327
+ }
328
+ );
329
+ ```
330
+
331
+ ### Check Workflow Status
332
+
333
+ ```typescript
334
+ const status = await qb.workflows.getStatus(runId);
335
+ console.log(status.status); // "running" | "sleeping" | "completed"
336
+ console.log(status.steps); // Array of step details
337
+ ```
338
+
339
+ ### Wait for Completion
340
+
341
+ ```typescript
342
+ const result = await qb.workflows.waitForCompletion(runId, {
343
+ pollIntervalMs: 2000,
344
+ timeoutMs: 60000,
345
+ });
346
+ ```
347
+
348
+ ### Trigger and Wait
349
+
350
+ ```typescript
351
+ const result = await qb.triggerAndWait(
352
+ "user-onboarding",
353
+ "https://your-app.com/api/workflows/onboarding",
354
+ { userId: "123" },
355
+ { timeoutMs: 120000 }
356
+ );
357
+ console.log(result.result); // Workflow output
358
+ ```
359
+
360
+ ### Cancel / Retry
361
+
362
+ ```typescript
363
+ await qb.workflows.cancel(runId);
364
+ await qb.workflows.retry(runId); // Resume from last completed step
365
+ ```
366
+
367
+ ### Send Events
368
+
369
+ ```typescript
370
+ // In workflow: await context.waitForEvent("order-approved", "order.approved")
371
+
372
+ // From external code:
373
+ await qb.workflows.sendEvent("order.approved", {
374
+ eventKey: "order-123",
375
+ payload: { status: "approved" },
376
+ });
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Context Methods
382
+
383
+ Available in `serve()` handlers:
384
+
385
+ ### `context.run(stepName, fn, options?)`
386
+
387
+ Execute a step with automatic caching.
388
+
389
+ ```typescript
390
+ const result = await context.run("fetch-user", async () => {
391
+ return await db.users.findById(userId);
392
+ });
393
+ ```
394
+
395
+ ### `context.sleep(stepName, seconds)`
396
+
397
+ Pause workflow for specified duration.
398
+
399
+ ```typescript
400
+ await context.sleep("wait-1-hour", 3600);
401
+ ```
402
+
403
+ ### `context.sleepUntil(stepName, date)`
404
+
405
+ Pause until a specific date/time.
406
+
407
+ ```typescript
408
+ await context.sleepUntil("wait-until-tomorrow", new Date("2024-01-15"));
409
+ ```
410
+
411
+ ### `context.call(stepName, config)`
412
+
413
+ Make an HTTP call as a cached step.
414
+
415
+ ```typescript
416
+ const data = await context.call("fetch-api", {
417
+ url: "https://api.example.com/data",
418
+ method: "POST",
419
+ headers: { "Authorization": "Bearer xxx" },
420
+ body: { key: "value" },
421
+ });
422
+ ```
423
+
424
+ ### `context.waitForEvent(stepName, eventName, options?)`
425
+
426
+ Wait for an external event.
427
+
428
+ ```typescript
429
+ const payload = await context.waitForEvent("wait-approval", "order.approved", {
430
+ eventKey: "order-123",
431
+ timeoutSeconds: 86400, // 1 day
432
+ });
433
+ ```
434
+
435
+ ### `context.notify(eventName, payload?)`
436
+
437
+ Send fire-and-forget event.
438
+
439
+ ```typescript
440
+ await context.notify("user.onboarded", { userId: "123" });
441
+ ```
442
+
443
+ ### `context.parallel(steps)`
444
+
445
+ Execute steps in parallel.
446
+
447
+ ```typescript
448
+ const [user, orders, preferences] = await context.parallel([
449
+ { name: "fetch-user", fn: () => fetchUser(userId) },
450
+ { name: "fetch-orders", fn: () => fetchOrders(userId) },
451
+ { name: "fetch-preferences", fn: () => fetchPreferences(userId) },
452
+ ]);
453
+ ```
454
+
455
+ ### `context.getCompletedSteps()`
456
+
457
+ Get all completed steps for debugging.
458
+
459
+ ```typescript
460
+ const steps = await context.getCompletedSteps();
461
+ console.log(`Completed ${steps.length} steps`);
462
+ ```
463
+
464
+ ---
465
+
466
+ ## Security
467
+
468
+ ### Signature Verification
469
+
470
+ Verify that workflow requests come from your QueueBear instance:
471
+
472
+ ```typescript
473
+ export const POST = serve(handler, {
474
+ signingSecret: process.env.QUEUEBEAR_SIGNING_SECRET,
475
+ });
476
+ ```
477
+
478
+ The signing secret is available in your QueueBear project settings. When configured, requests without a valid signature will be rejected with a 401 error.
479
+
480
+ ---
481
+
482
+ ## License
483
+
484
+ MIT