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 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/storage/memory.ts","../../src/storage/redis.ts","../../src/engine/processor.ts","../../src/util/id.ts","../../src/engine/lock.ts","../../src/server/queue.ts","../../src/security/crypto.ts","../../src/server/middleware.ts"],"names":["_enc","job","hit","createHmac","randomBytes","createCipheriv","createDecipheriv","timingSafeEqual"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * In-memory storage adapter.\n *\n * For local dev and tests only — state lives in the process, so it does NOT\n * survive across serverless invocations. The locking/lease semantics mirror\n * the Redis adapter so handler code behaves identically in both.\n */\nimport type { Job, StorageAdapter } from \"../types.js\";\nimport type { PayloadCipher } from \"../security/crypto.js\";\n\ninterface StoredJob extends Job {\n /** Encrypted-at-rest payload envelope, when a cipher is configured. */\n _enc?: string;\n}\n\nexport interface MemoryAdapterOptions {\n /** Optional cipher to encrypt payloads at rest, matching prod behaviour. */\n cipher?: PayloadCipher | null;\n}\n\nexport class MemoryAdapter implements StorageAdapter {\n private readonly queues = new Map<string, StoredJob[]>();\n private readonly cipher: PayloadCipher | null;\n\n constructor(opts: MemoryAdapterOptions = {}) {\n this.cipher = opts.cipher ?? null;\n }\n\n private bucket(queue: string): StoredJob[] {\n let b = this.queues.get(queue);\n if (!b) this.queues.set(queue, (b = []));\n return b;\n }\n\n private seal(job: Job): StoredJob {\n if (!this.cipher) return { ...job };\n const enc = this.cipher.encrypt(JSON.stringify(job.payload));\n return { ...job, payload: null, _enc: enc };\n }\n\n private open(stored: StoredJob): Job {\n if (!this.cipher || !stored._enc) {\n const { _enc, ...job } = stored;\n void _enc;\n return job;\n }\n const payload = JSON.parse(this.cipher.decrypt(stored._enc));\n const { _enc, ...job } = stored;\n void _enc;\n return { ...job, payload };\n }\n\n async push(job: Job): Promise<void> {\n this.bucket(job.queue).push(this.seal(job));\n }\n\n async shiftBatch(\n queue: string,\n count: number,\n leaseMs: number,\n ): Promise<Job[]> {\n const now = Date.now();\n const bucket = this.bucket(queue);\n const claimed: Job[] = [];\n\n for (const stored of bucket) {\n if (claimed.length >= count) break;\n const eligible =\n (stored.status === \"pending\" || stored.status === \"delayed\") &&\n stored.runAt <= now;\n // Reclaim jobs whose lease lapsed (the prior runner died mid-flight).\n const lapsed =\n stored.status === \"active\" &&\n stored.leaseUntil !== undefined &&\n stored.leaseUntil <= now;\n if (!eligible && !lapsed) continue;\n\n stored.status = \"active\";\n stored.attempts += 1;\n stored.leaseUntil = now + leaseMs;\n claimed.push(this.open(stored));\n }\n return claimed;\n }\n\n async complete(queue: string, jobId: string): Promise<void> {\n const bucket = this.bucket(queue);\n const i = bucket.findIndex((j) => j.id === jobId);\n if (i !== -1) bucket.splice(i, 1);\n }\n\n async fail(\n queue: string,\n jobId: string,\n error: string,\n retry: boolean,\n backoffMs: number,\n ): Promise<void> {\n const job = this.bucket(queue).find((j) => j.id === jobId);\n if (!job) return;\n job.lastError = error;\n job.leaseUntil = undefined;\n if (retry && job.attempts < job.maxAttempts) {\n job.status = \"pending\";\n job.runAt = Date.now() + backoffMs;\n } else {\n job.status = \"failed\";\n }\n }\n\n async release(queue: string, jobId: string): Promise<void> {\n const job = this.bucket(queue).find((j) => j.id === jobId);\n if (!job || job.status !== \"active\") return;\n job.status = \"pending\";\n job.leaseUntil = undefined;\n // Releasing for budget reasons must not burn a retry attempt.\n job.attempts = Math.max(0, job.attempts - 1);\n }\n\n async size(queue: string): Promise<number> {\n const now = Date.now();\n return this.bucket(queue).filter(\n (j) =>\n (j.status === \"pending\" || j.status === \"delayed\") && j.runAt <= now,\n ).length;\n }\n}\n","/**\n * Serverless-optimized Redis storage adapter.\n *\n * Works with either:\n * - `@upstash/redis` (HTTP/fetch — ideal for edge & connectionless runtimes)\n * - `ioredis` (TCP — uses a module-level singleton so we don't open a\n * new socket on every cold-ish invocation)\n *\n * Both clients differ in their `eval` signature, so we normalize them behind a\n * tiny `RedisDriver` and do all the claim logic in a single atomic Lua script.\n *\n * Data model (per queue `q`):\n * hq:{q}:pending LIST — ids waiting to be claimed, FIFO\n * hq:{q}:delayed ZSET — id -> runAt (promoted to pending when due)\n * hq:{q}:active ZSET — id -> leaseUntil (reclaimed when the lease lapses)\n * hq:{q}:jobs HASH — id -> envelope JSON (see JobEnvelope)\n *\n * The user payload is stored as an opaque string field (`payloadJson`) so the\n * server-side Lua `cjson` round-trip can never mangle nested structures.\n */\nimport type { Job, StorageAdapter } from \"../types.js\";\nimport type { PayloadCipher } from \"../security/crypto.js\";\n\n/** Minimal normalized Redis surface the adapter needs. */\nexport interface RedisDriver {\n eval(\n script: string,\n keys: string[],\n args: (string | number)[],\n ): Promise<unknown>;\n hget(key: string, field: string): Promise<string | null>;\n hset(key: string, field: string, value: string): Promise<unknown>;\n hdel(key: string, field: string): Promise<unknown>;\n rpush(key: string, value: string): Promise<unknown>;\n zadd(key: string, score: number, member: string): Promise<unknown>;\n zrem(key: string, member: string): Promise<unknown>;\n llen(key: string): Promise<number>;\n}\n\n/** On-disk envelope. `payloadJson` is opaque: plaintext JSON or cipher text. */\ninterface JobEnvelope {\n id: string;\n queue: string;\n status: Job[\"status\"];\n attempts: number;\n maxAttempts: number;\n runAt: number;\n createdAt: number;\n leaseUntil?: number;\n lastError?: string;\n payloadJson: string;\n enc: boolean;\n}\n\nexport interface RedisAdapterOptions {\n /** Optional namespace prefix (default \"hq\"). Lets you isolate apps/envs. */\n namespace?: string;\n /** Optional cipher for payload-at-rest encryption. */\n cipher?: PayloadCipher | null;\n}\n\n/**\n * Atomic claim: promote due delayed jobs, reclaim lapsed leases, then pop up to\n * `count` ids, mark them active with a fresh lease, and return their envelopes.\n * Doing this in one round trip is what gives us cross-request idempotency — two\n * concurrent triggers can never claim the same id.\n */\nconst CLAIM_LUA = `\nlocal pending = KEYS[1]\nlocal delayed = KEYS[2]\nlocal active = KEYS[3]\nlocal jobs = KEYS[4]\nlocal now = tonumber(ARGV[1])\nlocal count = tonumber(ARGV[2])\nlocal lease = tonumber(ARGV[3])\n\nlocal due = redis.call('ZRANGEBYSCORE', delayed, '-inf', now)\nfor _, id in ipairs(due) do\n redis.call('RPUSH', pending, id)\n redis.call('ZREM', delayed, id)\nend\n\nlocal lapsed = redis.call('ZRANGEBYSCORE', active, '-inf', now)\nfor _, id in ipairs(lapsed) do\n redis.call('RPUSH', pending, id)\n redis.call('ZREM', active, id)\nend\n\nlocal out = {}\nfor i = 1, count do\n local id = redis.call('LPOP', pending)\n if not id then break end\n local raw = redis.call('HGET', jobs, id)\n if raw then\n local env = cjson.decode(raw)\n env.status = 'active'\n env.attempts = (env.attempts or 0) + 1\n env.leaseUntil = now + lease\n local enc = cjson.encode(env)\n redis.call('HSET', jobs, id, enc)\n redis.call('ZADD', active, env.leaseUntil, id)\n table.insert(out, enc)\n end\nend\nreturn out\n`;\n\nexport class RedisAdapter implements StorageAdapter {\n private readonly ns: string;\n private readonly cipher: PayloadCipher | null;\n\n constructor(\n private readonly redis: RedisDriver,\n opts: RedisAdapterOptions = {},\n ) {\n this.ns = opts.namespace ?? \"hq\";\n this.cipher = opts.cipher ?? null;\n }\n\n private k(queue: string, suffix: string): string {\n return `${this.ns}:${queue}:${suffix}`;\n }\n\n private toEnvelope(job: Job): JobEnvelope {\n const plain = JSON.stringify(job.payload ?? null);\n const enc = !!this.cipher;\n return {\n id: job.id,\n queue: job.queue,\n status: job.status,\n attempts: job.attempts,\n maxAttempts: job.maxAttempts,\n runAt: job.runAt,\n createdAt: job.createdAt,\n leaseUntil: job.leaseUntil,\n lastError: job.lastError,\n payloadJson: enc ? this.cipher!.encrypt(plain) : plain,\n enc,\n };\n }\n\n private fromEnvelope(env: JobEnvelope): Job {\n const plain = env.enc\n ? this.cipher\n ? this.cipher.decrypt(env.payloadJson)\n : (() => {\n throw new Error(\n \"hybridq: job is encrypted but no cipher is configured\",\n );\n })()\n : env.payloadJson;\n return {\n id: env.id,\n queue: env.queue,\n payload: JSON.parse(plain),\n status: env.status,\n attempts: env.attempts,\n maxAttempts: env.maxAttempts,\n runAt: env.runAt,\n createdAt: env.createdAt,\n leaseUntil: env.leaseUntil,\n lastError: env.lastError,\n };\n }\n\n async push(job: Job): Promise<void> {\n const env = this.toEnvelope(job);\n const raw = JSON.stringify(env);\n await this.redis.hset(this.k(job.queue, \"jobs\"), job.id, raw);\n if (job.runAt > Date.now()) {\n await this.redis.zadd(this.k(job.queue, \"delayed\"), job.runAt, job.id);\n } else {\n await this.redis.rpush(this.k(job.queue, \"pending\"), job.id);\n }\n }\n\n async shiftBatch(\n queue: string,\n count: number,\n leaseMs: number,\n ): Promise<Job[]> {\n const res = (await this.redis.eval(\n CLAIM_LUA,\n [\n this.k(queue, \"pending\"),\n this.k(queue, \"delayed\"),\n this.k(queue, \"active\"),\n this.k(queue, \"jobs\"),\n ],\n [Date.now(), count, leaseMs],\n )) as string[] | null;\n if (!res || res.length === 0) return [];\n return res.map((raw) => this.fromEnvelope(JSON.parse(raw) as JobEnvelope));\n }\n\n async complete(queue: string, jobId: string): Promise<void> {\n await this.redis.zrem(this.k(queue, \"active\"), jobId);\n await this.redis.hdel(this.k(queue, \"jobs\"), jobId);\n }\n\n async fail(\n queue: string,\n jobId: string,\n error: string,\n retry: boolean,\n backoffMs: number,\n ): Promise<void> {\n const raw = await this.redis.hget(this.k(queue, \"jobs\"), jobId);\n if (!raw) return;\n const env = JSON.parse(raw) as JobEnvelope;\n env.lastError = error;\n env.leaseUntil = undefined;\n await this.redis.zrem(this.k(queue, \"active\"), jobId);\n\n if (retry && env.attempts < env.maxAttempts) {\n env.status = \"pending\";\n env.runAt = Date.now() + backoffMs;\n await this.redis.hset(this.k(queue, \"jobs\"), jobId, JSON.stringify(env));\n if (backoffMs > 0) {\n await this.redis.zadd(this.k(queue, \"delayed\"), env.runAt, jobId);\n } else {\n await this.redis.rpush(this.k(queue, \"pending\"), jobId);\n }\n } else {\n env.status = \"failed\";\n // Keep failed envelopes for inspection; drop from working sets.\n await this.redis.hset(this.k(queue, \"jobs\"), jobId, JSON.stringify(env));\n }\n }\n\n async release(queue: string, jobId: string): Promise<void> {\n const raw = await this.redis.hget(this.k(queue, \"jobs\"), jobId);\n if (!raw) return;\n const env = JSON.parse(raw) as JobEnvelope;\n if (env.status !== \"active\") return;\n env.status = \"pending\";\n env.leaseUntil = undefined;\n // Budget releases must not consume a retry attempt.\n env.attempts = Math.max(0, env.attempts - 1);\n await this.redis.zrem(this.k(queue, \"active\"), jobId);\n await this.redis.hset(this.k(queue, \"jobs\"), jobId, JSON.stringify(env));\n await this.redis.rpush(this.k(queue, \"pending\"), jobId);\n }\n\n async size(queue: string): Promise<number> {\n return this.redis.llen(this.k(queue, \"pending\"));\n }\n}\n\n/* ----------------------------- Driver factories ---------------------------- */\n\n/**\n * Wrap an `@upstash/redis` client. Connectionless (HTTP), so it's safe to\n * construct per request — no pooling needed.\n */\nexport function fromUpstash(client: {\n eval: (script: string, keys: string[], args: unknown[]) => Promise<unknown>;\n hget: (k: string, f: string) => Promise<string | null>;\n hset: (k: string, v: Record<string, string>) => Promise<unknown>;\n hdel: (k: string, f: string) => Promise<unknown>;\n rpush: (k: string, ...v: string[]) => Promise<unknown>;\n zadd: (k: string, m: { score: number; member: string }) => Promise<unknown>;\n zrem: (k: string, m: string) => Promise<unknown>;\n llen: (k: string) => Promise<number>;\n}): RedisDriver {\n return {\n eval: (script, keys, args) => client.eval(script, keys, args),\n hget: (k, f) => client.hget(k, f),\n hset: (k, f, v) => client.hset(k, { [f]: v }),\n hdel: (k, f) => client.hdel(k, f),\n rpush: (k, v) => client.rpush(k, v),\n zadd: (k, score, member) => client.zadd(k, { score, member }),\n zrem: (k, m) => client.zrem(k, m),\n llen: (k) => client.llen(k),\n };\n}\n\n/**\n * Wrap an `ioredis` client. ioredis opens a real TCP socket, so callers should\n * reuse ONE client across invocations — see `getSharedIORedis` below.\n */\nexport function fromIORedis(client: {\n eval: (\n script: string,\n numKeys: number,\n ...rest: (string | number)[]\n ) => Promise<unknown>;\n hget: (k: string, f: string) => Promise<string | null>;\n hset: (k: string, f: string, v: string) => Promise<unknown>;\n hdel: (k: string, f: string) => Promise<unknown>;\n rpush: (k: string, v: string) => Promise<unknown>;\n zadd: (k: string, score: number, member: string) => Promise<unknown>;\n zrem: (k: string, m: string) => Promise<unknown>;\n llen: (k: string) => Promise<number>;\n}): RedisDriver {\n return {\n eval: (script, keys, args) =>\n client.eval(script, keys.length, ...keys, ...args),\n hget: (k, f) => client.hget(k, f),\n hset: (k, f, v) => client.hset(k, f, v),\n hdel: (k, f) => client.hdel(k, f),\n rpush: (k, v) => client.rpush(k, v),\n zadd: (k, score, member) => client.zadd(k, score, member),\n zrem: (k, m) => client.zrem(k, m),\n llen: (k) => client.llen(k),\n };\n}\n\n/**\n * Connection-pool-safe ioredis singleton for serverless. Stashes the client on\n * `globalThis` so warm invocations reuse the same socket instead of exhausting\n * Redis connections under load.\n */\nexport function getSharedIORedis(\n factory: () => RedisDriver,\n key = \"__hybridq_ioredis__\",\n): RedisDriver {\n const g = globalThis as Record<string, unknown>;\n if (!g[key]) g[key] = factory();\n return g[key] as RedisDriver;\n}\n","/**\n * The processing engine.\n *\n * A trigger calls `drain()`. It claims jobs in batches and runs the handler,\n * checking the budget *between every job*. The moment the time or job-count\n * (or optional CPU) budget is exhausted it stops claiming, releases any jobs it\n * grabbed-but-didn't-run back to the queue, and returns — guaranteeing the\n * serverless function never blows past its wall-clock limit.\n *\n * Crash-safety comes from the lease model in the adapter: anything claimed but\n * not completed/failed/released simply becomes reclaimable once its lease\n * lapses, so a killed function loses no work.\n */\nimport type {\n BudgetConfig,\n DrainReport,\n Job,\n JobHandler,\n StorageAdapter,\n} from \"../types.js\";\nimport type { DistributedLock } from \"./lock.js\";\n\nexport interface DrainOptions<TPayload = unknown> {\n queue: string;\n adapter: StorageAdapter;\n handler: JobHandler<TPayload>;\n budget: BudgetConfig;\n /** Optional single-flight lock so concurrent triggers don't double-drain. */\n lock?: DistributedLock;\n /** Jobs claimed per round trip to the store. Default 10. */\n batchSize?: number;\n /**\n * Lease length for claimed jobs. Should exceed how long one job can take.\n * Defaults to `maxExecutionTimeMs` so a whole run is covered by one lease.\n */\n leaseMs?: number;\n /** Retry backoff in ms given the attempt number (1-based). */\n backoff?: (attempt: number) => number;\n /** Observability hook for handler errors. Never throws into the loop. */\n onError?: (err: unknown, job: Job<TPayload>) => void;\n}\n\n/** Default exponential backoff: 1s, 2s, 4s, … capped at 30s. */\nconst defaultBackoff = (attempt: number): number =>\n Math.min(30_000, 1000 * 2 ** Math.max(0, attempt - 1));\n\n/** Best-effort CPU utilisation sampler (Node only). */\nfunction cpuSampler(): () => number {\n const proc = (\n globalThis as { process?: { cpuUsage?: (p?: unknown) => unknown } }\n ).process;\n if (!proc?.cpuUsage) return () => 0;\n const startCpu = proc.cpuUsage() as { user: number; system: number };\n const startWall = Date.now();\n return () => {\n const d = proc.cpuUsage!(startCpu) as { user: number; system: number };\n const wallUs = Math.max(1, (Date.now() - startWall) * 1000);\n return ((d.user + d.system) / wallUs) * 100;\n };\n}\n\nexport async function drain<TPayload = unknown>(\n opts: DrainOptions<TPayload>,\n): Promise<DrainReport> {\n const {\n queue,\n adapter,\n handler,\n budget,\n lock,\n batchSize = 10,\n leaseMs = budget.maxExecutionTimeMs,\n backoff = defaultBackoff,\n onError,\n } = opts;\n\n const startedAt = Date.now();\n const elapsed = () => Date.now() - startedAt;\n const sampleCpu = cpuSampler();\n\n let processed = 0;\n let failed = 0;\n let released = 0;\n let stoppedBy: DrainReport[\"stoppedBy\"] = \"drained\";\n\n // ---- Reentrancy guard: only one drain per queue at a time. -------------\n const lockKey = `${queue}:drain-lock`;\n let lockToken: string | null = null;\n if (lock) {\n // TTL covers the run plus a margin so a slow run keeps its lock.\n lockToken = await lock.acquire(lockKey, budget.maxExecutionTimeMs + 2000);\n if (!lockToken) {\n return { processed, failed, released, stoppedBy: \"lockBusy\", elapsedMs: 0 };\n }\n }\n\n const done = () => processed + failed; // jobs we've fully accounted for\n\n /** True when any budget says \"stop starting new work\". */\n const budgetExhausted = (): DrainReport[\"stoppedBy\"] | null => {\n if (done() >= budget.maxJobsPerTrigger) return \"jobCap\";\n if (elapsed() >= budget.maxExecutionTimeMs) return \"timeBudget\";\n if (\n budget.maxCpuBudgetPct !== undefined &&\n sampleCpu() >= budget.maxCpuBudgetPct\n ) {\n return \"cpuBudget\";\n }\n return null;\n };\n\n try {\n outer: while (true) {\n const hit = budgetExhausted();\n if (hit) {\n stoppedBy = hit;\n break;\n }\n\n const remainingByCount = budget.maxJobsPerTrigger - done();\n const claimCount = Math.max(1, Math.min(batchSize, remainingByCount));\n const batch = await adapter.shiftBatch(queue, claimCount, leaseMs);\n\n if (batch.length === 0) {\n stoppedBy = \"drained\";\n break;\n }\n\n for (let i = 0; i < batch.length; i++) {\n const job = batch[i] as Job<TPayload>;\n\n // Re-check the budget before *each* job. If exhausted, hand back this\n // job and everything after it untouched (no attempt consumed).\n const hit = budgetExhausted();\n if (hit) {\n for (let j = i; j < batch.length; j++) {\n await adapter.release(queue, batch[j]!.id);\n released++;\n }\n stoppedBy = hit;\n break outer;\n }\n\n try {\n await handler(job);\n await adapter.complete(queue, job.id);\n processed++;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n onError?.(err, job);\n await adapter.fail(\n queue,\n job.id,\n message,\n /* retry */ true,\n backoff(job.attempts),\n );\n failed++;\n }\n }\n }\n } finally {\n if (lock && lockToken) await lock.release(lockKey, lockToken);\n }\n\n return { processed, failed, released, stoppedBy, elapsedMs: elapsed() };\n}\n","/**\n * Small id helper. Prefers crypto.randomUUID where present (Node 18+ and all\n * serverless edge runtimes), with a cheap fallback for exotic environments.\n */\nexport function newId(prefix = \"job\"): string {\n const g = globalThis as { crypto?: { randomUUID?: () => string } };\n const uuid =\n g.crypto?.randomUUID?.() ??\n `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n return `${prefix}_${uuid}`;\n}\n","/**\n * Distributed lock used to guarantee single-flight processing.\n *\n * When several inbound requests fire a trigger at the same instant we don't\n * want N concurrent drain loops fighting over the same jobs. Each run first\n * tries to grab a short-TTL lock; losers exit immediately (the winner is\n * already draining). The TTL is short and auto-expires, so a crashed run never\n * wedges the queue.\n */\nimport type { RedisDriver } from \"../storage/redis.js\";\nimport { newId } from \"../util/id.js\";\n\nexport interface DistributedLock {\n /** Try to acquire `key` for `ttlMs`. Returns a release token, or null. */\n acquire(key: string, ttlMs: number): Promise<string | null>;\n /** Release `key` only if we still own it (compare-and-delete). */\n release(key: string, token: string): Promise<void>;\n}\n\nconst ACQUIRE_LUA = `return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])`;\nconst RELEASE_LUA = `\nif redis.call('GET', KEYS[1]) == ARGV[1] then\n return redis.call('DEL', KEYS[1])\nend\nreturn 0`;\n\n/** Redis-backed lock (SET NX PX + compare-and-delete). */\nexport class RedisLock implements DistributedLock {\n constructor(private readonly redis: RedisDriver) {}\n\n async acquire(key: string, ttlMs: number): Promise<string | null> {\n const token = newId(\"lock\");\n const res = await this.redis.eval(ACQUIRE_LUA, [key], [token, ttlMs]);\n return res === \"OK\" || res === true ? token : null;\n }\n\n async release(key: string, token: string): Promise<void> {\n await this.redis.eval(RELEASE_LUA, [key], [token]);\n }\n}\n\n/** In-process lock for the MemoryAdapter / single-instance dev. */\nexport class MemoryLock implements DistributedLock {\n private readonly held = new Map<string, { token: string; until: number }>();\n\n async acquire(key: string, ttlMs: number): Promise<string | null> {\n const now = Date.now();\n const cur = this.held.get(key);\n if (cur && cur.until > now) return null;\n const token = newId(\"lock\");\n this.held.set(key, { token, until: now + ttlMs });\n return token;\n }\n\n async release(key: string, token: string): Promise<void> {\n const cur = this.held.get(key);\n if (cur && cur.token === token) this.held.delete(key);\n }\n}\n","/**\n * High-level Queue facade.\n *\n * Wires a storage adapter, optional lock, and the engine into an ergonomic\n * producer/consumer surface:\n * - `enqueue()` from any API route to defer work.\n * - `process()` from a trigger to drain a budgeted batch.\n */\nimport type {\n BudgetConfig,\n DrainReport,\n Job,\n JobHandler,\n StorageAdapter,\n} from \"../types.js\";\nimport { drain } from \"../engine/processor.js\";\nimport type { DistributedLock } from \"../engine/lock.js\";\nimport { newId } from \"../util/id.js\";\n\nconst DEFAULT_BUDGET: BudgetConfig = {\n maxJobsPerTrigger: 25,\n maxExecutionTimeMs: 8000, // safe under a 10s serverless limit\n};\n\nexport interface QueueConfig {\n /** Logical queue name (a single store can host many). */\n name: string;\n /** Where jobs live. */\n adapter: StorageAdapter;\n /** Default execution budget; overridable per `process()` call. */\n budget?: Partial<BudgetConfig>;\n /** Single-flight lock to prevent concurrent drains double-processing. */\n lock?: DistributedLock;\n /** Default retry ceiling for enqueued jobs. Default 3. */\n defaultMaxAttempts?: number;\n /** Jobs claimed per round trip while draining. Default 10. */\n batchSize?: number;\n /** Retry backoff in ms for attempt N (1-based). */\n backoff?: (attempt: number) => number;\n}\n\nexport interface EnqueueOptions {\n /** Delay before the job becomes eligible to run. */\n delayMs?: number;\n /** Override retry ceiling for this job. */\n maxAttempts?: number;\n /**\n * Stable id → idempotency key. Re-enqueuing with the same id is a no-op at\n * the application layer (callers should treat enqueue as best-effort unique).\n */\n id?: string;\n}\n\nexport class Queue<TPayload = unknown> {\n private readonly budget: BudgetConfig;\n\n constructor(private readonly config: QueueConfig) {\n this.budget = { ...DEFAULT_BUDGET, ...config.budget };\n }\n\n get name(): string {\n return this.config.name;\n }\n\n /** Producer side: push a unit of work. Returns the created job. */\n async enqueue(\n payload: TPayload,\n opts: EnqueueOptions = {},\n ): Promise<Job<TPayload>> {\n const now = Date.now();\n const job: Job<TPayload> = {\n id: opts.id ?? newId(\"job\"),\n queue: this.config.name,\n payload,\n status: opts.delayMs && opts.delayMs > 0 ? \"delayed\" : \"pending\",\n attempts: 0,\n maxAttempts: opts.maxAttempts ?? this.config.defaultMaxAttempts ?? 3,\n runAt: now + (opts.delayMs ?? 0),\n createdAt: now,\n };\n await this.config.adapter.push(job as Job);\n return job;\n }\n\n /** How many jobs are currently claimable. Handy for trigger gating. */\n size(): Promise<number> {\n return this.config.adapter.size(this.config.name);\n }\n\n /**\n * Consumer side: drain a budgeted batch with the given handler. Safe to call\n * from many concurrent requests — the lock ensures only one actually runs.\n */\n process(\n handler: JobHandler<TPayload>,\n budgetOverride?: Partial<BudgetConfig>,\n ): Promise<DrainReport> {\n return drain<TPayload>({\n queue: this.config.name,\n adapter: this.config.adapter,\n handler,\n budget: { ...this.budget, ...budgetOverride },\n lock: this.config.lock,\n batchSize: this.config.batchSize,\n backoff: this.config.backoff,\n });\n }\n}\n\n/** Convenience factory. */\nexport function defineQueue<TPayload = unknown>(\n config: QueueConfig,\n): Queue<TPayload> {\n return new Queue<TPayload>(config);\n}\n","/**\n * Server-side cryptography helpers.\n *\n * Uses the Node `crypto` module (available on Vercel/Lambda Node runtimes and\n * on the Web Crypto-compatible Node API). Kept out of the client bundle.\n */\nimport {\n createCipheriv,\n createDecipheriv,\n createHmac,\n randomBytes,\n timingSafeEqual,\n} from \"node:crypto\";\n\nconst ALGO = \"aes-256-gcm\";\nconst IV_BYTES = 12;\nconst ENVELOPE_PREFIX = \"hqv1\"; // versioned so the format can evolve\n\n/**\n * Optional payload-at-rest encryption. When a secret is configured, payloads\n * are sealed with AES-256-GCM before they ever touch the store, so sensitive\n * data (emails, tokens) is never plaintext in Redis.\n */\nexport interface PayloadCipher {\n encrypt(plaintext: string): string;\n decrypt(envelope: string): string;\n}\n\n/** Derive a stable 32-byte key from an arbitrary-length secret. */\nfunction deriveKey(secret: string): Buffer {\n // HKDF-lite: a single HMAC-SHA256 round gives us a fixed 32-byte key.\n return createHmac(\"sha256\", \"hybridq:payload-key\").update(secret).digest();\n}\n\n/**\n * Build a cipher from a secret. Returns `null` when no secret is supplied so\n * callers can transparently no-op (store plaintext) in dev.\n */\nexport function createPayloadCipher(secret?: string): PayloadCipher | null {\n if (!secret) return null;\n const key = deriveKey(secret);\n\n return {\n encrypt(plaintext) {\n const iv = randomBytes(IV_BYTES);\n const cipher = createCipheriv(ALGO, key, iv);\n const ct = Buffer.concat([\n cipher.update(plaintext, \"utf8\"),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n // envelope: prefix.iv.tag.ciphertext (all base64url)\n return [\n ENVELOPE_PREFIX,\n iv.toString(\"base64url\"),\n tag.toString(\"base64url\"),\n ct.toString(\"base64url\"),\n ].join(\".\");\n },\n\n decrypt(envelope) {\n const parts = envelope.split(\".\");\n if (parts.length !== 4 || parts[0] !== ENVELOPE_PREFIX) {\n throw new Error(\"hybridq: malformed encrypted payload envelope\");\n }\n const [, ivB64, tagB64, ctB64] = parts;\n const iv = Buffer.from(ivB64!, \"base64url\");\n const tag = Buffer.from(tagB64!, \"base64url\");\n const ct = Buffer.from(ctB64!, \"base64url\");\n const decipher = createDecipheriv(ALGO, key, iv);\n decipher.setAuthTag(tag);\n return Buffer.concat([decipher.update(ct), decipher.final()]).toString(\n \"utf8\",\n );\n },\n };\n}\n\n/** Constant-time string comparison that won't throw on length mismatch. */\nexport function safeEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n if (ab.length !== bb.length) return false;\n return timingSafeEqual(ab, bb);\n}\n\n/**\n * HMAC trigger tokens. A trigger presents `t=<ts>,v=<sig>` where\n * sig = HMAC-SHA256(secret, `${ts}.${path}`). Tokens expire to blunt replay.\n */\nexport function signTrigger(\n secret: string,\n path: string,\n ts: number = Date.now(),\n): string {\n const sig = createHmac(\"sha256\", secret)\n .update(`${ts}.${path}`)\n .digest(\"base64url\");\n return `t=${ts},v=${sig}`;\n}\n\nexport function verifyTrigger(\n secret: string,\n path: string,\n token: string,\n maxSkewMs = 5 * 60_000,\n): boolean {\n const m = /^t=(\\d+),v=([A-Za-z0-9_-]+)$/.exec(token.trim());\n if (!m) return false;\n const ts = Number(m[1]);\n const sig = m[2]!;\n if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > maxSkewMs) {\n return false;\n }\n const expected = createHmac(\"sha256\", secret)\n .update(`${ts}.${path}`)\n .digest(\"base64url\");\n return safeEqual(sig, expected);\n}\n","/**\n * Hybrid trigger middleware.\n *\n * Two ways to kick off a drain without a dedicated worker daemon:\n *\n * 1. `withTrigger(handler)` — wraps a normal API route / webhook. It runs your\n * real handler, returns the response immediately, and drains the queue\n * *after* the response is flushed using the platform's \"keep the function\n * alive a bit longer\" primitive (`waitUntil`). Inbound traffic = free CPU.\n *\n * 2. `createTriggerEndpoint(...)` — a dedicated, authenticated endpoint that\n * client pings (see ../client/trigger) or a cron hits to force a drain.\n *\n * Both verify a shared secret (X-Worker-Trigger-Secret) or an HMAC token so a\n * stranger can't pound the endpoint and turn your worker into a money pit.\n */\nimport type { BudgetConfig, DrainReport, JobHandler } from \"../types.js\";\nimport type { Queue } from \"./queue.js\";\nimport { safeEqual, verifyTrigger } from \"../security/crypto.js\";\n\n/** Anything with `waitUntil` — Vercel/Cloudflare give you one per request. */\nexport interface ExecutionContextLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport const TRIGGER_SECRET_HEADER = \"x-worker-trigger-secret\";\nexport const TRIGGER_TOKEN_HEADER = \"x-worker-trigger-token\";\n\nexport interface TriggerAuthConfig {\n /** Pre-shared secret compared in constant time against the header. */\n secret?: string;\n /** HMAC secret enabling short-lived signed tokens (replay-resistant). */\n hmacSecret?: string;\n /** Path bound into the HMAC signature. Default the request pathname. */\n path?: string;\n}\n\n/**\n * Verify a trigger request. Accepts EITHER a valid pre-shared secret header OR\n * a valid HMAC token. Returns true only when at least one configured method\n * passes — so an endpoint with no auth configured always rejects.\n */\nexport function authorizeTrigger(\n req: Request,\n auth: TriggerAuthConfig,\n): boolean {\n let configured = false;\n\n if (auth.secret) {\n configured = true;\n const provided = req.headers.get(TRIGGER_SECRET_HEADER);\n if (provided && safeEqual(provided, auth.secret)) return true;\n }\n\n if (auth.hmacSecret) {\n configured = true;\n const token = req.headers.get(TRIGGER_TOKEN_HEADER);\n const path = auth.path ?? new URL(req.url).pathname;\n if (token && verifyTrigger(auth.hmacSecret, path, token)) return true;\n }\n\n // Fail closed: never allow when nothing is configured or nothing matched.\n return configured ? false : false;\n}\n\n/**\n * Schedule a drain to run after the response is sent. Uses `waitUntil` when a\n * context is supplied (recommended); otherwise fires a detached promise as a\n * best-effort fallback (works on long-lived Node servers, not edge).\n */\nfunction deferDrain(\n run: () => Promise<DrainReport>,\n ctx?: ExecutionContextLike,\n): void {\n const p = run().catch(() => {\n /* swallow — draining must never break the main response */\n });\n if (ctx?.waitUntil) ctx.waitUntil(p);\n else void p; // detached; fine for persistent servers\n}\n\nexport interface WithTriggerOptions<TPayload> {\n queue: Queue<TPayload>;\n handler: JobHandler<TPayload>;\n /** Platform execution context exposing `waitUntil`. */\n ctx?: ExecutionContextLike;\n /** Optional per-trigger budget override. */\n budget?: Partial<BudgetConfig>;\n /** Skip draining when the queue is empty to avoid pointless work. */\n skipIfEmpty?: boolean;\n}\n\n/**\n * Wrap a normal request handler so each inbound request opportunistically\n * drains the queue *after* responding. This is the core \"hybrid trigger\".\n *\n * @example\n * export const POST = withTrigger(\n * async (req) => Response.json({ ok: true }),\n * { queue, handler: sendEmail, ctx },\n * );\n */\nexport function withTrigger<TPayload>(\n route: (req: Request) => Promise<Response> | Response,\n opts: WithTriggerOptions<TPayload>,\n): (req: Request) => Promise<Response> {\n return async (req: Request): Promise<Response> => {\n const res = await route(req); // produce the real response first\n deferDrain(async () => {\n if (opts.skipIfEmpty && (await opts.queue.size()) === 0) {\n return {\n processed: 0,\n failed: 0,\n released: 0,\n stoppedBy: \"drained\" as const,\n elapsedMs: 0,\n };\n }\n return opts.queue.process(opts.handler, opts.budget);\n }, opts.ctx);\n return res;\n };\n}\n\nexport interface TriggerEndpointOptions<TPayload> {\n queue: Queue<TPayload>;\n handler: JobHandler<TPayload>;\n auth: TriggerAuthConfig;\n ctx?: ExecutionContextLike;\n budget?: Partial<BudgetConfig>;\n /**\n * When true (default) the drain runs in the background via waitUntil and the\n * endpoint returns 202 immediately. Set false to await the drain and return\n * the full report (useful for cron jobs that want the result).\n */\n background?: boolean;\n}\n\n/**\n * Build a standalone authenticated trigger endpoint (Fetch-API style). Mount it\n * at e.g. `app/api/_worker/route.ts` and point client pings / cron at it.\n */\nexport function createTriggerEndpoint<TPayload>(\n opts: TriggerEndpointOptions<TPayload>,\n): (req: Request) => Promise<Response> {\n const background = opts.background ?? true;\n\n return async (req: Request): Promise<Response> => {\n if (!authorizeTrigger(req, opts.auth)) {\n return new Response(\"Forbidden\", { status: 403 });\n }\n\n if (background) {\n deferDrain(() => opts.queue.process(opts.handler, opts.budget), opts.ctx);\n return new Response(JSON.stringify({ accepted: true }), {\n status: 202,\n headers: { \"content-type\": \"application/json\" },\n });\n }\n\n const report = await opts.queue.process(opts.handler, opts.budget);\n return new Response(JSON.stringify(report), {\n status: 200,\n headers: { \"content-type\": \"application/json\" },\n });\n };\n}\n"]}
|
|
@@ -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 };
|