Skip to content

samsonchim/hybridq

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hybridq

A lightweight, embedded hybrid background worker for serverless. Run safe, batched, deferred tasks directly inside Vercel / Next.js / Cloudflare Workers / AWS Lambda — no persistent worker server. Incoming traffic ("hybrid triggers") drains the queue, with strict self-limiting budgets so you never blow past a function timeout.

pnpm add hybridq
# optional drivers (pick one)
pnpm add @upstash/redis   # edge / connectionless
pnpm add ioredis          # node TCP

How it works

  1. Producers call queue.enqueue(payload) from any API route.
  2. Jobs live in a pluggable store (MemoryAdapter for dev, RedisAdapter for prod) — serverless is stateless, so the queue must be external.
  3. Triggers drain the queue: either piggy-backing on normal inbound requests (withTrigger) or a dedicated authenticated endpoint (createTriggerEndpoint) that your browser or a cron pings.
  4. Every run respects a budget (maxJobsPerTrigger, maxExecutionTimeMs, optional maxCpuBudgetPct). When the budget is hit, unprocessed jobs are released back and the function returns cleanly.

Crash-safety is built in: claimed jobs hold a short lease; if the function dies mid-run, the lease lapses and the work is reclaimed — nothing is lost.

Quick start (local / dev)

import { defineQueue, MemoryAdapter } from "hybridq/server";

const queue = defineQueue<{ to: string }>({
  name: "emails",
  adapter: new MemoryAdapter(),
  budget: { maxJobsPerTrigger: 25, maxExecutionTimeMs: 8000 },
});

await queue.enqueue({ to: "[email protected]" });

const report = await queue.process(async (job) => {
  await sendEmail(job.payload.to);
});
// { processed, failed, released, stoppedBy, elapsedMs }

Production (Upstash Redis + encryption + lock)

import { Redis } from "@upstash/redis";
import {
  defineQueue,
  RedisAdapter,
  fromUpstash,
  RedisLock,
  createPayloadCipher,
} from "hybridq/server";

const driver = fromUpstash(Redis.fromEnv());
const cipher = createPayloadCipher(process.env.HYBRIDQ_ENC_KEY); // payload-at-rest

export const queue = defineQueue<{ email: string }>({
  name: "emails",
  adapter: new RedisAdapter(driver, { cipher }),
  lock: new RedisLock(driver), // single-flight: no double-processing
  budget: { maxJobsPerTrigger: 50, maxExecutionTimeMs: 9000 },
});

For ioredis, reuse one socket across invocations:

import IORedis from "ioredis";
import { fromIORedis, getSharedIORedis } from "hybridq/server";

const driver = getSharedIORedis(() =>
  fromIORedis(new IORedis(process.env.REDIS_URL!)),
);

Hybrid trigger inside a Next.js route handler

// app/api/orders/route.ts
import { withTrigger } from "hybridq/server";
import { queue } from "@/lib/queue";

export const POST = withTrigger(
  async (req) => {
    await queue.enqueue({ email: "[email protected]" });
    return Response.json({ ok: true }); // user gets this immediately…
  },
  {
    queue,
    handler: async (job) => sendReceipt(job.payload.email),
    // …draining happens AFTER the response, on the same warm function:
    // ctx is Cloudflare's ExecutionContext; on Vercel pass `{ waitUntil }`.
    skipIfEmpty: true,
  },
);

Dedicated authenticated trigger endpoint

// app/api/_worker/route.ts
import { createTriggerEndpoint } from "hybridq/server";
import { queue } from "@/lib/queue";

export const POST = createTriggerEndpoint({
  queue,
  handler: async (job) => sendReceipt(job.payload.email),
  auth: {
    secret: process.env.WORKER_SECRET,        // X-Worker-Trigger-Secret, or…
    hmacSecret: process.env.WORKER_HMAC,       // short-lived signed tokens
  },
});

Browser ambient triggers

The browser never holds the worker secret. Mint a short-lived HMAC token on the server and inject it into the page; the client just forwards it.

// server: mint a token for the page
import { signTrigger } from "hybridq/server";
const token = signTrigger(process.env.WORKER_HMAC!, "/api/_worker");
// client: ping on activity + during idle time, throttled
import { startTriggerClient } from "hybridq/client";

const trigger = startTriggerClient({
  endpoint: "/api/_worker",
  token,             // from the server-rendered page
  throttleMs: 5000,
  useIdleCallback: true,
});
// trigger.stop() on SPA unmount

Security

Concern Mechanism
Trigger abuse / DDoS Pre-shared X-Worker-Trigger-Secret (constant-time compare) or replay-resistant HMAC tokens (signTrigger/verifyTrigger).
Sensitive payloads in Redis Optional AES-256-GCM createPayloadCipher — payloads are sealed before they touch the store.
Duplicate processing RedisLock (SET NX PX + compare-and-delete) gives single-flight drains; lease TTLs auto-recover crashes.

API surface

  • hybridq/server: defineQueue / Queue, MemoryAdapter, RedisAdapter (fromUpstash, fromIORedis, getSharedIORedis), drain, RedisLock / MemoryLock, withTrigger, createTriggerEndpoint, authorizeTrigger, createPayloadCipher, signTrigger, verifyTrigger, plus all types.
  • hybridq/client: startTriggerClient.

License

MIT

About

Embedded, budget-aware background queue for Vercel & Next.js. Processes tasks in secured batches using API traffic and client-side triggers with strict execution time & job caps.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors