hybridq 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +165 -0
- package/dist/client/index.cjs +92 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +44 -0
- package/dist/client/index.d.ts +44 -0
- package/dist/client/index.js +90 -0
- package/dist/client/index.js.map +1 -0
- package/dist/server/index.cjs +604 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +463 -0
- package/dist/server/index.d.ts +463 -0
- package/dist/server/index.js +584 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type contracts for hybridq.
|
|
3
|
+
*
|
|
4
|
+
* These are intentionally dependency-free so they can be shared by the
|
|
5
|
+
* server bundle (queueing + execution) and stay tree-shakeable.
|
|
6
|
+
*/
|
|
7
|
+
/** Lifecycle states a job moves through inside the store. */
|
|
8
|
+
type JobStatus = "pending" | "active" | "completed" | "failed" | "delayed";
|
|
9
|
+
/**
|
|
10
|
+
* A unit of deferred work.
|
|
11
|
+
*
|
|
12
|
+
* `payload` is whatever the producer enqueued. When payload encryption is
|
|
13
|
+
* enabled the adapter stores a ciphertext envelope on disk/Redis and only
|
|
14
|
+
* decrypts back into `payload` when the engine claims the job.
|
|
15
|
+
*/
|
|
16
|
+
interface Job<TPayload = unknown> {
|
|
17
|
+
/** Globally unique id. Also used as the idempotency key when provided. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Logical queue/topic name. Lets one store host many independent queues. */
|
|
20
|
+
queue: string;
|
|
21
|
+
/** The work to perform. Decrypted in-memory; never logged. */
|
|
22
|
+
payload: TPayload;
|
|
23
|
+
/** Current lifecycle state. */
|
|
24
|
+
status: JobStatus;
|
|
25
|
+
/** How many times this job has been attempted (claimed). */
|
|
26
|
+
attempts: number;
|
|
27
|
+
/** Hard cap on attempts before the job is moved to `failed`. */
|
|
28
|
+
maxAttempts: number;
|
|
29
|
+
/** Epoch ms when the job becomes eligible to run (for delays/backoff). */
|
|
30
|
+
runAt: number;
|
|
31
|
+
/** Epoch ms when the job was first enqueued. */
|
|
32
|
+
createdAt: number;
|
|
33
|
+
/**
|
|
34
|
+
* Epoch ms when the current lease expires. While `status === "active"` and
|
|
35
|
+
* `now < leaseUntil`, no other trigger may claim this job. Once the lease
|
|
36
|
+
* lapses (e.g. the serverless function was killed mid-run) the job becomes
|
|
37
|
+
* reclaimable — this is what makes processing crash-safe.
|
|
38
|
+
*/
|
|
39
|
+
leaseUntil?: number;
|
|
40
|
+
/** Last error message, recorded on failure for observability. */
|
|
41
|
+
lastError?: string;
|
|
42
|
+
}
|
|
43
|
+
/** The result the engine derives from each handler invocation. */
|
|
44
|
+
type JobResult = {
|
|
45
|
+
status: "completed";
|
|
46
|
+
} | {
|
|
47
|
+
status: "failed";
|
|
48
|
+
error: string;
|
|
49
|
+
retry?: boolean;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Storage Adapter contract.
|
|
53
|
+
*
|
|
54
|
+
* Implementations must be safe to construct per-request in stateless
|
|
55
|
+
* serverless environments. Every method takes the queue name so a single
|
|
56
|
+
* adapter instance can serve multiple queues.
|
|
57
|
+
*
|
|
58
|
+
* The claim/ack model is lease-based: `shiftBatch` atomically moves jobs to
|
|
59
|
+
* `active` with a lease; the engine must later `complete`, `fail`, or
|
|
60
|
+
* `release` each claimed job. Crashed runs simply let the lease expire.
|
|
61
|
+
*/
|
|
62
|
+
interface StorageAdapter {
|
|
63
|
+
/** Append a job to the tail of its queue. */
|
|
64
|
+
push(job: Job): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Atomically claim up to `count` eligible jobs (pending + runAt <= now, or
|
|
67
|
+
* active jobs whose lease has expired), mark them `active`, and stamp a
|
|
68
|
+
* lease of `leaseMs`. Returns the claimed jobs with decrypted payloads.
|
|
69
|
+
*/
|
|
70
|
+
shiftBatch(queue: string, count: number, leaseMs: number): Promise<Job[]>;
|
|
71
|
+
/** Mark a claimed job done and remove it from the working set. */
|
|
72
|
+
complete(queue: string, jobId: string): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Mark a claimed job as failed. If `retry` is true and attempts remain, the
|
|
75
|
+
* job is requeued as `pending` with `runAt = now + backoffMs`; otherwise it
|
|
76
|
+
* lands in `failed`.
|
|
77
|
+
*/
|
|
78
|
+
fail(queue: string, jobId: string, error: string, retry: boolean, backoffMs: number): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Release a still-leased job back to `pending` without consuming an attempt.
|
|
81
|
+
* Used when a budget is exhausted mid-run so unprocessed jobs return cleanly.
|
|
82
|
+
*/
|
|
83
|
+
release(queue: string, jobId: string): Promise<void>;
|
|
84
|
+
/** Approximate number of pending (claimable) jobs — for triggers/metrics. */
|
|
85
|
+
size(queue: string): Promise<number>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Self-limiting execution budget. Enforced on every trigger run so a single
|
|
89
|
+
* invocation can never exceed the platform's wall-clock or cost limits.
|
|
90
|
+
*/
|
|
91
|
+
interface BudgetConfig {
|
|
92
|
+
/** Hard cap on total jobs processed in one trigger run. */
|
|
93
|
+
maxJobsPerTrigger: number;
|
|
94
|
+
/**
|
|
95
|
+
* Wall-clock budget in ms. The loop stops *before* starting a new job once
|
|
96
|
+
* elapsed time would risk the platform timeout. Set comfortably under your
|
|
97
|
+
* function's limit (e.g. 8000 for a 10s Vercel function).
|
|
98
|
+
*/
|
|
99
|
+
maxExecutionTimeMs: number;
|
|
100
|
+
/**
|
|
101
|
+
* Optional soft ceiling on estimated CPU utilisation (0-100). Best-effort:
|
|
102
|
+
* derived from event-loop/CPU sampling where available, ignored otherwise.
|
|
103
|
+
*/
|
|
104
|
+
maxCpuBudgetPct?: number;
|
|
105
|
+
}
|
|
106
|
+
/** A single job handler. Throwing is treated as a retryable failure. */
|
|
107
|
+
type JobHandler<TPayload = unknown> = (job: Job<TPayload>) => Promise<void> | void;
|
|
108
|
+
/** Outcome summary returned to the trigger caller. */
|
|
109
|
+
interface DrainReport {
|
|
110
|
+
/** Jobs that completed successfully. */
|
|
111
|
+
processed: number;
|
|
112
|
+
/** Jobs that errored this run (may still retry later). */
|
|
113
|
+
failed: number;
|
|
114
|
+
/** Jobs released unprocessed because a budget was hit. */
|
|
115
|
+
released: number;
|
|
116
|
+
/** Why the loop stopped. */
|
|
117
|
+
stoppedBy: "drained" | "jobCap" | "timeBudget" | "cpuBudget" | "lockBusy";
|
|
118
|
+
/** Total wall-clock spent in the loop. */
|
|
119
|
+
elapsedMs: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Optional payload-at-rest encryption. When a secret is configured, payloads
|
|
124
|
+
* are sealed with AES-256-GCM before they ever touch the store, so sensitive
|
|
125
|
+
* data (emails, tokens) is never plaintext in Redis.
|
|
126
|
+
*/
|
|
127
|
+
interface PayloadCipher {
|
|
128
|
+
encrypt(plaintext: string): string;
|
|
129
|
+
decrypt(envelope: string): string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build a cipher from a secret. Returns `null` when no secret is supplied so
|
|
133
|
+
* callers can transparently no-op (store plaintext) in dev.
|
|
134
|
+
*/
|
|
135
|
+
declare function createPayloadCipher(secret?: string): PayloadCipher | null;
|
|
136
|
+
/**
|
|
137
|
+
* HMAC trigger tokens. A trigger presents `t=<ts>,v=<sig>` where
|
|
138
|
+
* sig = HMAC-SHA256(secret, `${ts}.${path}`). Tokens expire to blunt replay.
|
|
139
|
+
*/
|
|
140
|
+
declare function signTrigger(secret: string, path: string, ts?: number): string;
|
|
141
|
+
declare function verifyTrigger(secret: string, path: string, token: string, maxSkewMs?: number): boolean;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* In-memory storage adapter.
|
|
145
|
+
*
|
|
146
|
+
* For local dev and tests only — state lives in the process, so it does NOT
|
|
147
|
+
* survive across serverless invocations. The locking/lease semantics mirror
|
|
148
|
+
* the Redis adapter so handler code behaves identically in both.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
interface MemoryAdapterOptions {
|
|
152
|
+
/** Optional cipher to encrypt payloads at rest, matching prod behaviour. */
|
|
153
|
+
cipher?: PayloadCipher | null;
|
|
154
|
+
}
|
|
155
|
+
declare class MemoryAdapter implements StorageAdapter {
|
|
156
|
+
private readonly queues;
|
|
157
|
+
private readonly cipher;
|
|
158
|
+
constructor(opts?: MemoryAdapterOptions);
|
|
159
|
+
private bucket;
|
|
160
|
+
private seal;
|
|
161
|
+
private open;
|
|
162
|
+
push(job: Job): Promise<void>;
|
|
163
|
+
shiftBatch(queue: string, count: number, leaseMs: number): Promise<Job[]>;
|
|
164
|
+
complete(queue: string, jobId: string): Promise<void>;
|
|
165
|
+
fail(queue: string, jobId: string, error: string, retry: boolean, backoffMs: number): Promise<void>;
|
|
166
|
+
release(queue: string, jobId: string): Promise<void>;
|
|
167
|
+
size(queue: string): Promise<number>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Serverless-optimized Redis storage adapter.
|
|
172
|
+
*
|
|
173
|
+
* Works with either:
|
|
174
|
+
* - `@upstash/redis` (HTTP/fetch — ideal for edge & connectionless runtimes)
|
|
175
|
+
* - `ioredis` (TCP — uses a module-level singleton so we don't open a
|
|
176
|
+
* new socket on every cold-ish invocation)
|
|
177
|
+
*
|
|
178
|
+
* Both clients differ in their `eval` signature, so we normalize them behind a
|
|
179
|
+
* tiny `RedisDriver` and do all the claim logic in a single atomic Lua script.
|
|
180
|
+
*
|
|
181
|
+
* Data model (per queue `q`):
|
|
182
|
+
* hq:{q}:pending LIST — ids waiting to be claimed, FIFO
|
|
183
|
+
* hq:{q}:delayed ZSET — id -> runAt (promoted to pending when due)
|
|
184
|
+
* hq:{q}:active ZSET — id -> leaseUntil (reclaimed when the lease lapses)
|
|
185
|
+
* hq:{q}:jobs HASH — id -> envelope JSON (see JobEnvelope)
|
|
186
|
+
*
|
|
187
|
+
* The user payload is stored as an opaque string field (`payloadJson`) so the
|
|
188
|
+
* server-side Lua `cjson` round-trip can never mangle nested structures.
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/** Minimal normalized Redis surface the adapter needs. */
|
|
192
|
+
interface RedisDriver {
|
|
193
|
+
eval(script: string, keys: string[], args: (string | number)[]): Promise<unknown>;
|
|
194
|
+
hget(key: string, field: string): Promise<string | null>;
|
|
195
|
+
hset(key: string, field: string, value: string): Promise<unknown>;
|
|
196
|
+
hdel(key: string, field: string): Promise<unknown>;
|
|
197
|
+
rpush(key: string, value: string): Promise<unknown>;
|
|
198
|
+
zadd(key: string, score: number, member: string): Promise<unknown>;
|
|
199
|
+
zrem(key: string, member: string): Promise<unknown>;
|
|
200
|
+
llen(key: string): Promise<number>;
|
|
201
|
+
}
|
|
202
|
+
interface RedisAdapterOptions {
|
|
203
|
+
/** Optional namespace prefix (default "hq"). Lets you isolate apps/envs. */
|
|
204
|
+
namespace?: string;
|
|
205
|
+
/** Optional cipher for payload-at-rest encryption. */
|
|
206
|
+
cipher?: PayloadCipher | null;
|
|
207
|
+
}
|
|
208
|
+
declare class RedisAdapter implements StorageAdapter {
|
|
209
|
+
private readonly redis;
|
|
210
|
+
private readonly ns;
|
|
211
|
+
private readonly cipher;
|
|
212
|
+
constructor(redis: RedisDriver, opts?: RedisAdapterOptions);
|
|
213
|
+
private k;
|
|
214
|
+
private toEnvelope;
|
|
215
|
+
private fromEnvelope;
|
|
216
|
+
push(job: Job): Promise<void>;
|
|
217
|
+
shiftBatch(queue: string, count: number, leaseMs: number): Promise<Job[]>;
|
|
218
|
+
complete(queue: string, jobId: string): Promise<void>;
|
|
219
|
+
fail(queue: string, jobId: string, error: string, retry: boolean, backoffMs: number): Promise<void>;
|
|
220
|
+
release(queue: string, jobId: string): Promise<void>;
|
|
221
|
+
size(queue: string): Promise<number>;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Wrap an `@upstash/redis` client. Connectionless (HTTP), so it's safe to
|
|
225
|
+
* construct per request — no pooling needed.
|
|
226
|
+
*/
|
|
227
|
+
declare function fromUpstash(client: {
|
|
228
|
+
eval: (script: string, keys: string[], args: unknown[]) => Promise<unknown>;
|
|
229
|
+
hget: (k: string, f: string) => Promise<string | null>;
|
|
230
|
+
hset: (k: string, v: Record<string, string>) => Promise<unknown>;
|
|
231
|
+
hdel: (k: string, f: string) => Promise<unknown>;
|
|
232
|
+
rpush: (k: string, ...v: string[]) => Promise<unknown>;
|
|
233
|
+
zadd: (k: string, m: {
|
|
234
|
+
score: number;
|
|
235
|
+
member: string;
|
|
236
|
+
}) => Promise<unknown>;
|
|
237
|
+
zrem: (k: string, m: string) => Promise<unknown>;
|
|
238
|
+
llen: (k: string) => Promise<number>;
|
|
239
|
+
}): RedisDriver;
|
|
240
|
+
/**
|
|
241
|
+
* Wrap an `ioredis` client. ioredis opens a real TCP socket, so callers should
|
|
242
|
+
* reuse ONE client across invocations — see `getSharedIORedis` below.
|
|
243
|
+
*/
|
|
244
|
+
declare function fromIORedis(client: {
|
|
245
|
+
eval: (script: string, numKeys: number, ...rest: (string | number)[]) => Promise<unknown>;
|
|
246
|
+
hget: (k: string, f: string) => Promise<string | null>;
|
|
247
|
+
hset: (k: string, f: string, v: string) => Promise<unknown>;
|
|
248
|
+
hdel: (k: string, f: string) => Promise<unknown>;
|
|
249
|
+
rpush: (k: string, v: string) => Promise<unknown>;
|
|
250
|
+
zadd: (k: string, score: number, member: string) => Promise<unknown>;
|
|
251
|
+
zrem: (k: string, m: string) => Promise<unknown>;
|
|
252
|
+
llen: (k: string) => Promise<number>;
|
|
253
|
+
}): RedisDriver;
|
|
254
|
+
/**
|
|
255
|
+
* Connection-pool-safe ioredis singleton for serverless. Stashes the client on
|
|
256
|
+
* `globalThis` so warm invocations reuse the same socket instead of exhausting
|
|
257
|
+
* Redis connections under load.
|
|
258
|
+
*/
|
|
259
|
+
declare function getSharedIORedis(factory: () => RedisDriver, key?: string): RedisDriver;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Distributed lock used to guarantee single-flight processing.
|
|
263
|
+
*
|
|
264
|
+
* When several inbound requests fire a trigger at the same instant we don't
|
|
265
|
+
* want N concurrent drain loops fighting over the same jobs. Each run first
|
|
266
|
+
* tries to grab a short-TTL lock; losers exit immediately (the winner is
|
|
267
|
+
* already draining). The TTL is short and auto-expires, so a crashed run never
|
|
268
|
+
* wedges the queue.
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
interface DistributedLock {
|
|
272
|
+
/** Try to acquire `key` for `ttlMs`. Returns a release token, or null. */
|
|
273
|
+
acquire(key: string, ttlMs: number): Promise<string | null>;
|
|
274
|
+
/** Release `key` only if we still own it (compare-and-delete). */
|
|
275
|
+
release(key: string, token: string): Promise<void>;
|
|
276
|
+
}
|
|
277
|
+
/** Redis-backed lock (SET NX PX + compare-and-delete). */
|
|
278
|
+
declare class RedisLock implements DistributedLock {
|
|
279
|
+
private readonly redis;
|
|
280
|
+
constructor(redis: RedisDriver);
|
|
281
|
+
acquire(key: string, ttlMs: number): Promise<string | null>;
|
|
282
|
+
release(key: string, token: string): Promise<void>;
|
|
283
|
+
}
|
|
284
|
+
/** In-process lock for the MemoryAdapter / single-instance dev. */
|
|
285
|
+
declare class MemoryLock implements DistributedLock {
|
|
286
|
+
private readonly held;
|
|
287
|
+
acquire(key: string, ttlMs: number): Promise<string | null>;
|
|
288
|
+
release(key: string, token: string): Promise<void>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The processing engine.
|
|
293
|
+
*
|
|
294
|
+
* A trigger calls `drain()`. It claims jobs in batches and runs the handler,
|
|
295
|
+
* checking the budget *between every job*. The moment the time or job-count
|
|
296
|
+
* (or optional CPU) budget is exhausted it stops claiming, releases any jobs it
|
|
297
|
+
* grabbed-but-didn't-run back to the queue, and returns — guaranteeing the
|
|
298
|
+
* serverless function never blows past its wall-clock limit.
|
|
299
|
+
*
|
|
300
|
+
* Crash-safety comes from the lease model in the adapter: anything claimed but
|
|
301
|
+
* not completed/failed/released simply becomes reclaimable once its lease
|
|
302
|
+
* lapses, so a killed function loses no work.
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
interface DrainOptions<TPayload = unknown> {
|
|
306
|
+
queue: string;
|
|
307
|
+
adapter: StorageAdapter;
|
|
308
|
+
handler: JobHandler<TPayload>;
|
|
309
|
+
budget: BudgetConfig;
|
|
310
|
+
/** Optional single-flight lock so concurrent triggers don't double-drain. */
|
|
311
|
+
lock?: DistributedLock;
|
|
312
|
+
/** Jobs claimed per round trip to the store. Default 10. */
|
|
313
|
+
batchSize?: number;
|
|
314
|
+
/**
|
|
315
|
+
* Lease length for claimed jobs. Should exceed how long one job can take.
|
|
316
|
+
* Defaults to `maxExecutionTimeMs` so a whole run is covered by one lease.
|
|
317
|
+
*/
|
|
318
|
+
leaseMs?: number;
|
|
319
|
+
/** Retry backoff in ms given the attempt number (1-based). */
|
|
320
|
+
backoff?: (attempt: number) => number;
|
|
321
|
+
/** Observability hook for handler errors. Never throws into the loop. */
|
|
322
|
+
onError?: (err: unknown, job: Job<TPayload>) => void;
|
|
323
|
+
}
|
|
324
|
+
declare function drain<TPayload = unknown>(opts: DrainOptions<TPayload>): Promise<DrainReport>;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* High-level Queue facade.
|
|
328
|
+
*
|
|
329
|
+
* Wires a storage adapter, optional lock, and the engine into an ergonomic
|
|
330
|
+
* producer/consumer surface:
|
|
331
|
+
* - `enqueue()` from any API route to defer work.
|
|
332
|
+
* - `process()` from a trigger to drain a budgeted batch.
|
|
333
|
+
*/
|
|
334
|
+
|
|
335
|
+
interface QueueConfig {
|
|
336
|
+
/** Logical queue name (a single store can host many). */
|
|
337
|
+
name: string;
|
|
338
|
+
/** Where jobs live. */
|
|
339
|
+
adapter: StorageAdapter;
|
|
340
|
+
/** Default execution budget; overridable per `process()` call. */
|
|
341
|
+
budget?: Partial<BudgetConfig>;
|
|
342
|
+
/** Single-flight lock to prevent concurrent drains double-processing. */
|
|
343
|
+
lock?: DistributedLock;
|
|
344
|
+
/** Default retry ceiling for enqueued jobs. Default 3. */
|
|
345
|
+
defaultMaxAttempts?: number;
|
|
346
|
+
/** Jobs claimed per round trip while draining. Default 10. */
|
|
347
|
+
batchSize?: number;
|
|
348
|
+
/** Retry backoff in ms for attempt N (1-based). */
|
|
349
|
+
backoff?: (attempt: number) => number;
|
|
350
|
+
}
|
|
351
|
+
interface EnqueueOptions {
|
|
352
|
+
/** Delay before the job becomes eligible to run. */
|
|
353
|
+
delayMs?: number;
|
|
354
|
+
/** Override retry ceiling for this job. */
|
|
355
|
+
maxAttempts?: number;
|
|
356
|
+
/**
|
|
357
|
+
* Stable id → idempotency key. Re-enqueuing with the same id is a no-op at
|
|
358
|
+
* the application layer (callers should treat enqueue as best-effort unique).
|
|
359
|
+
*/
|
|
360
|
+
id?: string;
|
|
361
|
+
}
|
|
362
|
+
declare class Queue<TPayload = unknown> {
|
|
363
|
+
private readonly config;
|
|
364
|
+
private readonly budget;
|
|
365
|
+
constructor(config: QueueConfig);
|
|
366
|
+
get name(): string;
|
|
367
|
+
/** Producer side: push a unit of work. Returns the created job. */
|
|
368
|
+
enqueue(payload: TPayload, opts?: EnqueueOptions): Promise<Job<TPayload>>;
|
|
369
|
+
/** How many jobs are currently claimable. Handy for trigger gating. */
|
|
370
|
+
size(): Promise<number>;
|
|
371
|
+
/**
|
|
372
|
+
* Consumer side: drain a budgeted batch with the given handler. Safe to call
|
|
373
|
+
* from many concurrent requests — the lock ensures only one actually runs.
|
|
374
|
+
*/
|
|
375
|
+
process(handler: JobHandler<TPayload>, budgetOverride?: Partial<BudgetConfig>): Promise<DrainReport>;
|
|
376
|
+
}
|
|
377
|
+
/** Convenience factory. */
|
|
378
|
+
declare function defineQueue<TPayload = unknown>(config: QueueConfig): Queue<TPayload>;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Hybrid trigger middleware.
|
|
382
|
+
*
|
|
383
|
+
* Two ways to kick off a drain without a dedicated worker daemon:
|
|
384
|
+
*
|
|
385
|
+
* 1. `withTrigger(handler)` — wraps a normal API route / webhook. It runs your
|
|
386
|
+
* real handler, returns the response immediately, and drains the queue
|
|
387
|
+
* *after* the response is flushed using the platform's "keep the function
|
|
388
|
+
* alive a bit longer" primitive (`waitUntil`). Inbound traffic = free CPU.
|
|
389
|
+
*
|
|
390
|
+
* 2. `createTriggerEndpoint(...)` — a dedicated, authenticated endpoint that
|
|
391
|
+
* client pings (see ../client/trigger) or a cron hits to force a drain.
|
|
392
|
+
*
|
|
393
|
+
* Both verify a shared secret (X-Worker-Trigger-Secret) or an HMAC token so a
|
|
394
|
+
* stranger can't pound the endpoint and turn your worker into a money pit.
|
|
395
|
+
*/
|
|
396
|
+
|
|
397
|
+
/** Anything with `waitUntil` — Vercel/Cloudflare give you one per request. */
|
|
398
|
+
interface ExecutionContextLike {
|
|
399
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
400
|
+
}
|
|
401
|
+
declare const TRIGGER_SECRET_HEADER = "x-worker-trigger-secret";
|
|
402
|
+
declare const TRIGGER_TOKEN_HEADER = "x-worker-trigger-token";
|
|
403
|
+
interface TriggerAuthConfig {
|
|
404
|
+
/** Pre-shared secret compared in constant time against the header. */
|
|
405
|
+
secret?: string;
|
|
406
|
+
/** HMAC secret enabling short-lived signed tokens (replay-resistant). */
|
|
407
|
+
hmacSecret?: string;
|
|
408
|
+
/** Path bound into the HMAC signature. Default the request pathname. */
|
|
409
|
+
path?: string;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Verify a trigger request. Accepts EITHER a valid pre-shared secret header OR
|
|
413
|
+
* a valid HMAC token. Returns true only when at least one configured method
|
|
414
|
+
* passes — so an endpoint with no auth configured always rejects.
|
|
415
|
+
*/
|
|
416
|
+
declare function authorizeTrigger(req: Request, auth: TriggerAuthConfig): boolean;
|
|
417
|
+
interface WithTriggerOptions<TPayload> {
|
|
418
|
+
queue: Queue<TPayload>;
|
|
419
|
+
handler: JobHandler<TPayload>;
|
|
420
|
+
/** Platform execution context exposing `waitUntil`. */
|
|
421
|
+
ctx?: ExecutionContextLike;
|
|
422
|
+
/** Optional per-trigger budget override. */
|
|
423
|
+
budget?: Partial<BudgetConfig>;
|
|
424
|
+
/** Skip draining when the queue is empty to avoid pointless work. */
|
|
425
|
+
skipIfEmpty?: boolean;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Wrap a normal request handler so each inbound request opportunistically
|
|
429
|
+
* drains the queue *after* responding. This is the core "hybrid trigger".
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* export const POST = withTrigger(
|
|
433
|
+
* async (req) => Response.json({ ok: true }),
|
|
434
|
+
* { queue, handler: sendEmail, ctx },
|
|
435
|
+
* );
|
|
436
|
+
*/
|
|
437
|
+
declare function withTrigger<TPayload>(route: (req: Request) => Promise<Response> | Response, opts: WithTriggerOptions<TPayload>): (req: Request) => Promise<Response>;
|
|
438
|
+
interface TriggerEndpointOptions<TPayload> {
|
|
439
|
+
queue: Queue<TPayload>;
|
|
440
|
+
handler: JobHandler<TPayload>;
|
|
441
|
+
auth: TriggerAuthConfig;
|
|
442
|
+
ctx?: ExecutionContextLike;
|
|
443
|
+
budget?: Partial<BudgetConfig>;
|
|
444
|
+
/**
|
|
445
|
+
* When true (default) the drain runs in the background via waitUntil and the
|
|
446
|
+
* endpoint returns 202 immediately. Set false to await the drain and return
|
|
447
|
+
* the full report (useful for cron jobs that want the result).
|
|
448
|
+
*/
|
|
449
|
+
background?: boolean;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Build a standalone authenticated trigger endpoint (Fetch-API style). Mount it
|
|
453
|
+
* at e.g. `app/api/_worker/route.ts` and point client pings / cron at it.
|
|
454
|
+
*/
|
|
455
|
+
declare function createTriggerEndpoint<TPayload>(opts: TriggerEndpointOptions<TPayload>): (req: Request) => Promise<Response>;
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Small id helper. Prefers crypto.randomUUID where present (Node 18+ and all
|
|
459
|
+
* serverless edge runtimes), with a cheap fallback for exotic environments.
|
|
460
|
+
*/
|
|
461
|
+
declare function newId(prefix?: string): string;
|
|
462
|
+
|
|
463
|
+
export { type BudgetConfig, type DistributedLock, type DrainOptions, type DrainReport, type EnqueueOptions, type ExecutionContextLike, type Job, type JobHandler, type JobResult, type JobStatus, MemoryAdapter, type MemoryAdapterOptions, MemoryLock, type PayloadCipher, Queue, type QueueConfig, RedisAdapter, type RedisAdapterOptions, type RedisDriver, RedisLock, type StorageAdapter, TRIGGER_SECRET_HEADER, TRIGGER_TOKEN_HEADER, type TriggerAuthConfig, type TriggerEndpointOptions, type WithTriggerOptions, authorizeTrigger, createPayloadCipher, createTriggerEndpoint, defineQueue, drain, fromIORedis, fromUpstash, getSharedIORedis, newId, signTrigger, verifyTrigger, withTrigger };
|