├── .gitignore ├── pnpm-workspace.yaml ├── apps ├── website │ ├── .eslintrc.json │ ├── app │ │ ├── favicon.ico │ │ ├── opengraph-image.png │ │ └── layout.tsx │ ├── postcss.config.js │ ├── public │ │ ├── BingSiteAuth.xml │ │ └── dataqueue-logo.png │ ├── lib │ │ └── utils.ts │ ├── components │ │ └── ui │ │ │ ├── aspect-ratio.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── separator.tsx │ │ │ ├── progress.tsx │ │ │ ├── toaster.tsx │ │ │ ├── input.tsx │ │ │ ├── sonner.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── slider.tsx │ │ │ ├── switch.tsx │ │ │ ├── badge.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── popover.tsx │ │ │ ├── avatar.tsx │ │ │ ├── toggle.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── alert.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── resizable.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── button.tsx │ │ │ ├── tabs.tsx │ │ │ ├── accordion.tsx │ │ │ ├── card.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── pagination.tsx │ │ │ ├── table.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dialog.tsx │ │ │ └── sheet.tsx │ ├── next.config.js │ ├── .gitignore │ ├── components.json │ ├── tsconfig.json │ ├── update-deps.sh │ ├── package.json │ ├── tailwind.config.ts │ └── hooks │ │ └── use-toast.ts ├── docs │ ├── .eslintrc.json │ ├── app │ │ ├── favicon.ico │ │ ├── opengraph-image.png │ │ ├── global.css │ │ ├── api │ │ │ └── search │ │ │ │ └── route.ts │ │ ├── (docs) │ │ │ ├── layout.tsx │ │ │ └── [[...slug]] │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── layout.config.tsx │ ├── postcss.config.mjs │ ├── public │ │ ├── dataqueue-logo.png │ │ └── dataqueue-overview.png │ ├── content │ │ └── docs │ │ │ ├── example │ │ │ ├── meta.json │ │ │ └── index.mdx │ │ │ ├── intro │ │ │ ├── meta.json │ │ │ ├── install.mdx │ │ │ ├── overview.mdx │ │ │ └── index.mdx │ │ │ ├── meta.json │ │ │ ├── api │ │ │ ├── meta.json │ │ │ ├── failure-reason.mdx │ │ │ ├── index.mdx │ │ │ ├── db-util.mdx │ │ │ ├── job-event.mdx │ │ │ ├── job-handlers.mdx │ │ │ ├── processor.mdx │ │ │ ├── job-options.mdx │ │ │ ├── job-record.mdx │ │ │ └── tags.mdx │ │ │ ├── index.mdx │ │ │ └── usage │ │ │ ├── failed-jobs.mdx │ │ │ ├── meta.json │ │ │ ├── job-events.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── cleanup-jobs.mdx │ │ │ ├── add-job.mdx │ │ │ ├── get-jobs.mdx │ │ │ ├── reclaim-jobs.mdx │ │ │ ├── job-timeout.mdx │ │ │ ├── job-handlers.mdx │ │ │ ├── process-jobs.mdx │ │ │ └── database-migration.mdx │ ├── next.config.mjs │ ├── lib │ │ └── source.ts │ ├── .gitignore │ ├── mdx-components.tsx │ ├── source.config.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md └── demo │ ├── app │ ├── favicon.ico │ ├── queue │ │ └── refresh.ts │ ├── jobs │ │ ├── cancel.ts │ │ ├── generate-image │ │ │ ├── index.ts │ │ │ └── route.ts │ │ ├── report │ │ │ ├── index.ts │ │ │ └── route.ts │ │ ├── process-jobs.ts │ │ └── email │ │ │ ├── index.ts │ │ │ └── route.ts │ ├── layout.tsx │ ├── api │ │ └── cron │ │ │ ├── cleanup │ │ │ └── route.ts │ │ │ ├── reclaim │ │ │ └── route.ts │ │ │ └── process │ │ │ └── route.ts │ ├── refresh-periodically.tsx │ ├── page.tsx │ ├── job │ │ └── [id] │ │ │ └── page.tsx │ ├── buttons.tsx │ └── globals.css │ ├── env.example │ ├── postcss.config.mjs │ ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg │ ├── lib │ ├── services │ │ ├── user.ts │ │ ├── generate-report.ts │ │ ├── email.ts │ │ └── generate-image-ai.ts │ ├── utils.ts │ └── queue.ts │ ├── next.config.ts │ ├── CHANGELOG.md │ ├── README.md │ ├── eslint.config.mjs │ ├── components.json │ ├── .gitignore │ ├── tsconfig.json │ ├── cron.sh │ ├── package.json │ └── components │ └── ui │ ├── button.tsx │ └── table.tsx ├── packages └── dataqueue │ ├── .npmignore │ ├── cli.cjs │ ├── tsup.config.ts │ ├── migrations │ ├── 1751984773000_add_tags_to_job_queue.sql │ ├── 1765809419000_add_force_kill_on_timeout_to_job_queue.sql │ ├── 1751131910825_add_timeout_seconds_to_job_queue.sql │ ├── 1751131910823_initial.sql │ └── 1751186053000_add_job_events_table.sql │ ├── .changeset │ ├── config.json │ └── README.md │ ├── tsconfig.json │ ├── vite.config.ts │ ├── src │ ├── log-context.ts │ ├── utils.ts │ ├── test-util.ts │ ├── db-util.test.ts │ ├── cli.ts │ ├── db-util.ts │ └── cli.test.ts │ ├── CHANGELOG.md │ └── package.json ├── .prettierrc ├── .prettierignore ├── docker-compose.yml ├── .cursor └── rules │ └── migration-file-creation.mdc ├── turbo.json ├── README.md ├── package.json ├── .github └── workflows │ ├── publish.yml │ ├── deploy-landing.yml │ ├── ci.yml │ └── changeset-version-pr.yml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | data 4 | .turbo -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /apps/website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /packages/dataqueue/.npmignore: -------------------------------------------------------------------------------- 1 | src/*.test.ts 2 | src/test-util.ts 3 | !migrations/ -------------------------------------------------------------------------------- /apps/docs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/demo/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/demo/app/favicon.ico -------------------------------------------------------------------------------- /apps/docs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/docs/app/favicon.ico -------------------------------------------------------------------------------- /packages/dataqueue/cli.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./dist/cli.js').runCli(process.argv); 4 | -------------------------------------------------------------------------------- /apps/website/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/website/app/favicon.ico -------------------------------------------------------------------------------- /apps/demo/env.example: -------------------------------------------------------------------------------- 1 | CRON_SECRET=abcd 2 | PG_DATAQUEUE_DATABASE=postgres://postgres:postgres@localhost:5432/postgres -------------------------------------------------------------------------------- /apps/docs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/docs/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/docs/app/opengraph-image.png -------------------------------------------------------------------------------- /apps/website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/docs/public/dataqueue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/docs/public/dataqueue-logo.png -------------------------------------------------------------------------------- /apps/website/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/website/app/opengraph-image.png -------------------------------------------------------------------------------- /apps/demo/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /apps/website/public/BingSiteAuth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9215E3198B2B0717623054373D527C04 4 | -------------------------------------------------------------------------------- /apps/website/public/dataqueue-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/website/public/dataqueue-logo.png -------------------------------------------------------------------------------- /apps/docs/app/global.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @import 'fumadocs-ui/css/neutral.css'; 3 | @import 'fumadocs-ui/css/preset.css'; 4 | -------------------------------------------------------------------------------- /apps/docs/public/dataqueue-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicnocquee/dataqueue/HEAD/apps/docs/public/dataqueue-overview.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 80, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /apps/demo/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/docs/content/docs/example/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Example", 3 | "icon": "MyIcon", 4 | "pages": ["index"], 5 | "defaultOpen": true 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/intro/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Introduction", 3 | "icon": "MyIcon", 4 | "pages": ["overview", "install"], 5 | "defaultOpen": true 6 | } 7 | -------------------------------------------------------------------------------- /apps/docs/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "DataQueue", 3 | "icon": "MyIcon", 4 | "pages": ["intro", "usage", "example", "api"], 5 | "defaultOpen": true 6 | } 7 | -------------------------------------------------------------------------------- /apps/demo/app/queue/refresh.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { revalidatePath } from 'next/cache'; 4 | 5 | export const refresh = async () => { 6 | revalidatePath('/'); 7 | }; 8 | -------------------------------------------------------------------------------- /apps/demo/lib/services/user.ts: -------------------------------------------------------------------------------- 1 | export const createUser = async (name: string, email: string) => { 2 | console.log(`Creating user ${name} with email ${email}`); 3 | return '123'; 4 | }; 5 | -------------------------------------------------------------------------------- /apps/docs/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { source } from '@/lib/source'; 2 | import { createFromSource } from 'fumadocs-core/search/server'; 3 | 4 | export const { GET } = createFromSource(source); 5 | -------------------------------------------------------------------------------- /apps/website/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/demo/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | transpilePackages: ['dataqueue'], 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /apps/website/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root; 6 | 7 | export { AspectRatio }; 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | data 4 | .turbo 5 | .vscode 6 | .env 7 | .env.local 8 | .env.development.local 9 | .env.test.local 10 | .env.production.local 11 | pnpm-lock.yaml 12 | .next 13 | .out 14 | out 15 | .source -------------------------------------------------------------------------------- /apps/demo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - dataqueue@1.12.0 9 | 10 | ## 0.1.1 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies 15 | - dataqueue@1.11.0 16 | -------------------------------------------------------------------------------- /apps/docs/content/docs/example/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Next.js Demo App 3 | --- 4 | 5 | You can see a working example of a Next.js app using DataQueue in the [apps/demo](https://github.com/nicnocquee/dataqueue/tree/main/apps/demo) folder of this repository. 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { createMDX } from 'fumadocs-mdx/next'; 2 | 3 | const withMDX = createMDX(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const config = { 7 | reactStrictMode: true, 8 | }; 9 | 10 | export default withMDX(config); 11 | -------------------------------------------------------------------------------- /apps/website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: 'export', 4 | eslint: { 5 | ignoreDuringBuilds: true, 6 | }, 7 | images: { unoptimized: true }, 8 | }; 9 | 10 | module.exports = nextConfig; 11 | -------------------------------------------------------------------------------- /apps/docs/content/docs/api/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "API", 3 | "pages": [ 4 | "job-queue", 5 | "job-options", 6 | "job-record", 7 | "job-event", 8 | "processor", 9 | "job-handlers", 10 | "db-util", 11 | "failure-reason", 12 | "tags" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/docs/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: DataQueue Docs 3 | description: Documentation for DataQueue, a lightweight job queue for Node.js/TypeScript projects, backed by PostgreSQL. 4 | --- 5 | 6 | Welcome to the DataQueue docs! Start from the [Introduction](/intro) to learn more about DataQueue. 7 | -------------------------------------------------------------------------------- /packages/dataqueue/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/cli.ts'], 5 | format: ['esm', 'cjs'], 6 | dts: true, 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | treeshake: true, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/dataqueue/migrations/1751984773000_add_tags_to_job_queue.sql: -------------------------------------------------------------------------------- 1 | -- Up Migration 2 | ALTER TABLE job_queue ADD COLUMN tags TEXT[]; 3 | CREATE INDEX IF NOT EXISTS idx_job_queue_tags ON job_queue USING GIN (tags); 4 | 5 | -- Down Migration 6 | DROP INDEX IF EXISTS idx_job_queue_tags; 7 | ALTER TABLE job_queue DROP COLUMN IF EXISTS tags; -------------------------------------------------------------------------------- /apps/docs/lib/source.ts: -------------------------------------------------------------------------------- 1 | import { docs } from '@/.source'; 2 | import { loader } from 'fumadocs-core/source'; 3 | 4 | // See https://fumadocs.vercel.app/docs/headless/source-api for more info 5 | export const source = loader({ 6 | // it assigns a URL to your pages 7 | baseUrl: '/', 8 | source: docs.toFumadocsSource(), 9 | }); 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:15-alpine 4 | ports: 5 | - 5432:5432 6 | environment: 7 | POSTGRES_USER: postgres 8 | POSTGRES_PASSWORD: postgres 9 | POSTGRES_DB: postgres 10 | volumes: 11 | - ./data:/var/lib/postgresql/data 12 | restart: unless-stopped 13 | -------------------------------------------------------------------------------- /packages/dataqueue/migrations/1765809419000_add_force_kill_on_timeout_to_job_queue.sql: -------------------------------------------------------------------------------- 1 | -- Up Migration: Add force_kill_on_timeout to job_queue 2 | ALTER TABLE job_queue ADD COLUMN force_kill_on_timeout BOOLEAN DEFAULT FALSE; 3 | 4 | -- Down Migration: Remove force_kill_on_timeout from job_queue 5 | ALTER TABLE job_queue DROP COLUMN IF EXISTS force_kill_on_timeout; 6 | 7 | -------------------------------------------------------------------------------- /apps/website/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /apps/demo/app/jobs/cancel.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { getJobQueue } from '@/lib/queue'; 4 | import { revalidatePath } from 'next/cache'; 5 | 6 | export const cancelPendingJobs = async () => { 7 | const jobQueue = getJobQueue(); 8 | await jobQueue.cancelAllUpcomingJobs(); 9 | 10 | revalidatePath('/'); 11 | return { message: 'Pending jobs cancelled' }; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/dataqueue/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["demo", "docs", "website"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/demo/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import { formatDistanceToNow } from 'date-fns'; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | export function formatTimeDistance(date: Date | number) { 10 | return formatDistanceToNow(date, { addSuffix: true }); 11 | } 12 | -------------------------------------------------------------------------------- /apps/demo/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/demo/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | .contentlayer 6 | .content-collections 7 | .source 8 | 9 | # test & build 10 | /coverage 11 | /.next/ 12 | /out/ 13 | /build 14 | *.tsbuildinfo 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | /.pnp 20 | .pnp.js 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # others 26 | .env*.local 27 | .vercel 28 | next-env.d.ts -------------------------------------------------------------------------------- /apps/website/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 12 | -------------------------------------------------------------------------------- /apps/docs/app/(docs)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout } from 'fumadocs-ui/layouts/docs'; 2 | import type { ReactNode } from 'react'; 3 | import { baseOptions } from '@/app/layout.config'; 4 | import { source } from '@/lib/source'; 5 | 6 | export default function Layout({ children }: { children: ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/dataqueue/migrations/1751131910825_add_timeout_seconds_to_job_queue.sql: -------------------------------------------------------------------------------- 1 | -- Up Migration: Add timeout_ms and failure_reason to job_queue 2 | ALTER TABLE job_queue ADD COLUMN timeout_ms INT; 3 | ALTER TABLE job_queue ADD COLUMN failure_reason VARCHAR; 4 | 5 | -- Down Migration: Remove timeout_ms and failure_reason from job_queue 6 | ALTER TABLE job_queue DROP COLUMN IF EXISTS timeout_ms; 7 | ALTER TABLE job_queue DROP COLUMN IF EXISTS failure_reason; -------------------------------------------------------------------------------- /packages/dataqueue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "outDir": "./dist", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "dist"] 15 | } 16 | -------------------------------------------------------------------------------- /.cursor/rules/migration-file-creation.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | 7 | When generating migration files, use the command `date +%s000` to get the timestamp for the prefix of the migration file. 8 | 9 | There must be tow parts in the sql file: the up migration and the down migration. 10 | 11 | The up migration should be after the comment `-- Up Migration` and the down migration should be after the comment `-- Down Migration`. 12 | -------------------------------------------------------------------------------- /apps/demo/README.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | - Run `pnpm i` from the root of the repo 4 | - Copy the `env.example` file to `.env.local` and fill in the values. 5 | - Run `env $(cat .env.local | grep -v '^#' | xargs) pnpm run migrate-dataqueue` from the `apps/demo` directory to run migrations. 6 | - Run `pnpm dev` from the root directory to start the development server, the build process of the dataqueue package in watch mode, and the cron job to process jobs, cleanup, and reclaim. 7 | -------------------------------------------------------------------------------- /apps/docs/mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import type { MDXComponents } from 'mdx/types'; 2 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 3 | import defaultComponents from 'fumadocs-ui/mdx'; 4 | 5 | // use this function to get MDX components, you will need it for rendering MDX 6 | export function getMDXComponents(components?: MDXComponents): MDXComponents { 7 | return { 8 | ...defaultComponents, 9 | Tab, 10 | Tabs, 11 | ...components, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /apps/demo/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /apps/docs/content/docs/api/failure-reason.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: FailureReason 3 | --- 4 | 5 | The `FailureReason` enum represents the possible reasons for a job failure. 6 | 7 | ## Enum 8 | 9 | ```ts 10 | enum FailureReason { 11 | Timeout = 'timeout', 12 | HandlerError = 'handler_error', 13 | NoHandler = 'no_handler', 14 | } 15 | ``` 16 | 17 | ## Values 18 | 19 | - `Timeout`: The job timed out. 20 | - `HandlerError`: The job handler threw an error. 21 | - `NoHandler`: The job handler was not found. 22 | -------------------------------------------------------------------------------- /apps/docs/content/docs/usage/failed-jobs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Failed Jobs 3 | --- 4 | 5 | A job handler can fail for many reasons, such as a bug in the code or running out of resources. 6 | 7 | When a job fails, it will be marked as `failed` and retried up to `maxAttempts` times. You can set the `maxAttempts` value when adding the job to the queue. 8 | 9 | Each retry is scheduled after `2^attempts * 1 minute` from the previous attempt. You can view the error history for a job in its `errorHistory` field. 10 | -------------------------------------------------------------------------------- /apps/docs/content/docs/usage/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Usage", 3 | "icon": "MyIcon", 4 | "pages": [ 5 | "quick-start", 6 | "database-migration", 7 | "job-handlers", 8 | "init-queue", 9 | "add-job", 10 | "process-jobs", 11 | "failed-jobs", 12 | "job-timeout", 13 | "force-kill-timeout", 14 | "reclaim-jobs", 15 | "cleanup-jobs", 16 | "cancel-jobs", 17 | "edit-jobs", 18 | "job-events", 19 | "get-jobs" 20 | ], 21 | "defaultOpen": true 22 | } 23 | -------------------------------------------------------------------------------- /packages/dataqueue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type UserConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: './src/main.tsx', 9 | name: '@nicnocquee/use-local-storage-hook', 10 | }, 11 | }, 12 | plugins: [react()], 13 | test: { 14 | globals: true, 15 | environment: 'jsdom', 16 | setupFiles: '', 17 | types: ['vitest/globals'], 18 | }, 19 | } as UserConfig); 20 | -------------------------------------------------------------------------------- /apps/demo/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /apps/demo/lib/services/generate-report.ts: -------------------------------------------------------------------------------- 1 | export const generateReport = async (reportId: string, userId: string) => { 2 | console.log(`Generating report for ${reportId} with user ${userId}`); 3 | 4 | // Simulate report generation with a 10 second delay and a 50% chance of failure 5 | await new Promise((resolve, reject) => 6 | setTimeout(() => { 7 | if (Math.random() > 0.5) { 8 | resolve(true); 9 | } else { 10 | reject(new Error('Report generation failed')); 11 | } 12 | }, 10000), 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/dataqueue/src/log-context.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from 'async_hooks'; 2 | 3 | export const logStorage = new AsyncLocalStorage<{ 4 | verbose: boolean; 5 | }>(); 6 | 7 | export const setLogContext = (verbose: boolean) => { 8 | logStorage.enterWith({ verbose }); 9 | }; 10 | 11 | export const getLogContext = () => { 12 | return logStorage.getStore(); 13 | }; 14 | 15 | export const log = (message: string) => { 16 | const context = getLogContext(); 17 | if (context?.verbose) { 18 | console.log(message); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /apps/demo/lib/services/email.ts: -------------------------------------------------------------------------------- 1 | export const sendEmail = async (to: string, subject: string, body: string) => { 2 | console.log( 3 | `Sending email to ${to} with subject ${subject} and body ${body}`, 4 | ); 5 | 6 | // Simulate email sending with a 10 second delay and a 50% chance of failure 7 | await new Promise((resolve, reject) => 8 | setTimeout(() => { 9 | if (Math.random() > 0.5) { 10 | resolve(true); 11 | } else { 12 | reject(new Error('Email failed')); 13 | } 14 | }, 10000), 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/website/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /packages/dataqueue/.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /apps/docs/content/docs/intro/install.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | --- 4 | 5 | Before you begin, make sure you have Node.js and TypeScript installed in your environment. And obviously, you need a Postgres database. 6 | 7 | You can install the required libraries using your favorite package manager, such as npm: 8 | 9 | ```package-install 10 | npm install @nicnocquee/dataqueue 11 | npm install -D node-pg-migrate ts-node 12 | ``` 13 | 14 | You need to install node-pg-migrate and ts-node as development dependencies to run database migrations. 15 | -------------------------------------------------------------------------------- /apps/docs/content/docs/api/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Reference 3 | --- 4 | 5 | This section documents the main classes, types, and functions available for managing job queues, processing jobs, and interacting with the database. 6 | 7 | ## API Surface 8 | 9 | - [JobQueue](/api/job-queue) 10 | - [JobOptions](/api/job-options) 11 | - [JobRecord](/api/job-record) 12 | - [JobEvent](/api/job-event) 13 | - [Processor](/api/processor) 14 | - [ProcessorOptions](/api/processor-options) 15 | - [JobHandlers](/api/job-handlers) 16 | - [Database Utility](/api/db-util) 17 | - [Tags](/api/tags) 18 | -------------------------------------------------------------------------------- /apps/docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './global.css'; 2 | import { RootProvider } from 'fumadocs-ui/provider'; 3 | import { Inter } from 'next/font/google'; 4 | import type { ReactNode } from 'react'; 5 | 6 | const inter = Inter({ 7 | subsets: ['latin'], 8 | }); 9 | 10 | export default function Layout({ children }: { children: ReactNode }) { 11 | return ( 12 | 13 | 14 | {children} 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "check-types": { 14 | "dependsOn": ["^check-types"] 15 | }, 16 | "dev": { 17 | "cache": false, 18 | "persistent": true 19 | }, 20 | "cron": { 21 | "cache": false, 22 | "persistent": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/docs/content/docs/usage/job-events.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Job Events 3 | --- 4 | 5 | DataQueue keeps track of every change in a job's status. You can use `getJobEvents` to see all the events for a job, such as when it was added, started processing, completed, failed, cancelled, or retried. 6 | 7 | ```typescript tab="Code" 8 | const events = await jobQueue.getJobEvents(jobId); 9 | console.log(events); 10 | ``` 11 | 12 | ```json tab="Output" 13 | [ 14 | { 15 | "id": 1, 16 | "jobId": 1, 17 | "eventType": "processing", 18 | "createdAt": "2024-06-01T12:00:00Z", 19 | "metadata": "" 20 | } 21 | ] 22 | ``` 23 | -------------------------------------------------------------------------------- /apps/docs/content/docs/usage/quick-start.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick Start 3 | description: Get started with DataQueue 4 | --- 5 | 6 | In this docs, we'll use a Next.js with App Router project which is deployed to Vercel as an example. 7 | 8 | The TL;DR is: 9 | 10 | 1. [Run migrations before deploying your app](/usage/database-migration) 11 | 2. [Define job handlers](/usage/job-handlers) 12 | 3. [Initialize the job queue](/usage/init-queue) 13 | 4. [Add a job](/usage/add-job) 14 | 5. Create three API routes to [process jobs](/usage/process-jobs), [reclaim stuck jobs](/usage/reclaim-jobs), and [cleanup old jobs](/usage/cleanup-jobs) 15 | 6. Call those API routes periodically. 16 | -------------------------------------------------------------------------------- /apps/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /packages/dataqueue/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const toCamelCase = (str: string) => { 2 | return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); 3 | }; 4 | 5 | export const toSnakeCase = (str: string) => { 6 | return str.replace(/([A-Z])/g, '_$1').toLowerCase(); 7 | }; 8 | 9 | export const objectKeysToCamelCase = (obj: Record) => { 10 | return Object.fromEntries( 11 | Object.entries(obj).map(([key, value]) => [toCamelCase(key), value]), 12 | ); 13 | }; 14 | 15 | export const objectKeysToSnakeCase = (obj: Record) => { 16 | return Object.fromEntries( 17 | Object.entries(obj).map(([key, value]) => [toSnakeCase(key), value]), 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/docs/content/docs/api/db-util.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Database Utility 3 | --- 4 | 5 | The `createPool` function creates a PostgreSQL connection pool for use with the job queue system. 6 | 7 | ## Function 8 | 9 | ```ts 10 | createPool(config: JobQueueConfig['databaseConfig']): Pool 11 | ``` 12 | 13 | - `config`: The database connection configuration (connection string, host, port, database, user, password, ssl). 14 | - Returns a `Pool` instance from `pg`. 15 | 16 | ## Example 17 | 18 | ```ts 19 | import { createPool } from 'dataqueue'; 20 | 21 | const pool = createPool({ 22 | host: 'localhost', 23 | port: 5432, 24 | database: 'mydb', 25 | user: 'postgres', 26 | password: 'secret', 27 | }); 28 | ``` 29 | -------------------------------------------------------------------------------- /apps/docs/source.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | defineDocs, 4 | frontmatterSchema, 5 | metaSchema, 6 | } from 'fumadocs-mdx/config'; 7 | import { remarkInstall } from 'fumadocs-docgen'; 8 | import { rehypeCode, remarkAdmonition } from 'fumadocs-core/mdx-plugins'; 9 | 10 | // You can customise Zod schemas for frontmatter and `meta.json` here 11 | // see https://fumadocs.vercel.app/docs/mdx/collections#define-docs 12 | export const docs = defineDocs({ 13 | docs: { 14 | schema: frontmatterSchema, 15 | }, 16 | meta: { 17 | schema: metaSchema, 18 | }, 19 | }); 20 | 21 | export default defineConfig({ 22 | mdxOptions: { 23 | remarkPlugins: [remarkInstall, remarkAdmonition], 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /apps/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/docs/app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; 2 | import Image from 'next/image'; 3 | 4 | /** 5 | * Shared layout configurations 6 | * 7 | * you can customise layouts individually from: 8 | * Home Layout: app/(home)/layout.tsx 9 | * Docs Layout: app/docs/layout.tsx 10 | */ 11 | export const baseOptions: BaseLayoutProps = { 12 | nav: { 13 | title: ( 14 | <> 15 | DataQueue 22 | DataQueue 23 | 24 | ), 25 | }, 26 | // see https://fumadocs.dev/docs/ui/navigation/links 27 | links: [], 28 | }; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dataqueue 2 | 3 | ![DataQueue](./apps/website/app/opengraph-image.png) 4 | 5 | A lightweight, PostgreSQL-backed job queue for Node.js/TypeScript projects. Schedule, process, and manage background jobs with ease. Perfect for web apps (Next.js, etc.) deployed to serverless platforms like Vercel, AWS Lambda, etc. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install @nicnocquee/dataqueue 11 | npm install -D node-pg-migrate ts-node 12 | ``` 13 | 14 | ## Documentation 15 | 16 | See [docs.dataqueue.dev](https://docs.dataqueue.dev) for the complete documentation. 17 | 18 | ## Contributing 19 | 20 | See [CONTRIBUTING.md](CONTRIBUTING.md) for details. 21 | 22 | ## License 23 | 24 | MIT 25 | 26 | ## Author 27 | 28 | [Nico Prananta](https://nico.fyi) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dataqueue-monorepo", 3 | "private": true, 4 | "devDependencies": { 5 | "pnpm": "^9.0.0", 6 | "prettier": "^3.6.2", 7 | "turbo": "^2.5.4" 8 | }, 9 | "scripts": { 10 | "dev": "turbo run dev cron --parallel --filter demo --filter \"@nicnocquee/dataqueue\"", 11 | "dev:docs": "turbo run dev --filter docs", 12 | "dev:demo": "turbo run dev --filter demo", 13 | "dev:website": "turbo run dev --filter website", 14 | "dev:dataqueue": "turbo run dev --filter \"@nicnocquee/dataqueue\"", 15 | "build": "turbo run build", 16 | "lint": "turbo run lint", 17 | "test": "turbo run test", 18 | "format": "prettier --write .", 19 | "check-format": "prettier --check ." 20 | }, 21 | "packageManager": "pnpm@9.0.0" 22 | } 23 | -------------------------------------------------------------------------------- /apps/website/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css'; 2 | import type { Metadata } from 'next'; 3 | import { Inter } from 'next/font/google'; 4 | 5 | const inter = Inter({ subsets: ['latin'] }); 6 | 7 | export const metadata: Metadata = { 8 | metadataBase: new URL('https://dataqueue.dev'), 9 | title: 'DataQueue | A lightweight, PostgreSQL-backed job queue', 10 | description: 11 | 'An open-source, lightweight, PostgreSQL-backed job queue for Node.js/TypeScript projects. Schedule, process, and manage background jobs with ease.', 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/docs/content/docs/api/job-event.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: JobEvent 3 | --- 4 | 5 | The `JobEvent` interface represents an event in the lifecycle of a job, such as when it is added, processed, completed, failed, cancelled, or retried. 6 | 7 | ## Fields 8 | 9 | - `id`: _number_ — Unique event ID. 10 | - `jobId`: _number_ — The job this event is associated with. 11 | - `eventType`: _JobEventType_ — The type of event (see below). 12 | - `createdAt`: _Date_ — When the event was created. 13 | - `metadata`: _any_ — Additional metadata for the event. 14 | 15 | ## JobEventType 16 | 17 | `JobEventType` is an enum of possible job event types: 18 | 19 | ```ts 20 | type JobEventType = 21 | | 'added' 22 | | 'processing' 23 | | 'completed' 24 | | 'failed' 25 | | 'cancelled' 26 | | 'retried'; 27 | ``` 28 | -------------------------------------------------------------------------------- /apps/demo/app/jobs/generate-image/index.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { getJobQueue } from '@/lib/queue'; 4 | import { revalidatePath } from 'next/cache'; 5 | 6 | export const generateImage = async ({ prompt }: { prompt: string }) => { 7 | // Add a generate image job 8 | const jobQueue = getJobQueue(); 9 | const delay = Math.floor(1000 + Math.random() * 2000); // 1000 to 3000 ms 10 | const runAt = new Date(Date.now() + delay); // Run between 1 and 3 seconds from now 11 | const job = await jobQueue.addJob({ 12 | jobType: 'generate_image', 13 | payload: { 14 | prompt, 15 | }, 16 | priority: 5, // Higher number = higher priority 17 | runAt: runAt, 18 | timeoutMs: 5000, // 5 second timeout 19 | }); 20 | 21 | revalidatePath('/'); 22 | 23 | return { job }; 24 | }; 25 | -------------------------------------------------------------------------------- /apps/demo/cron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -a 4 | source "$(dirname "$0")/.env.local" 5 | set +a 6 | 7 | if [ -z "$CRON_SECRET" ]; then 8 | echo "Error: CRON_SECRET environment variable is not set." 9 | exit 1 10 | fi 11 | 12 | while true; do 13 | echo "Processing jobs..." 14 | curl http://localhost:3000/api/cron/process -H "Authorization: Bearer $CRON_SECRET" 15 | echo "" 16 | sleep 10 17 | done & 18 | 19 | while true; do 20 | echo "Reclaiming stuck jobs..." 21 | curl http://localhost:3000/api/cron/reclaim -H "Authorization: Bearer $CRON_SECRET" 22 | echo "" 23 | sleep 20 24 | done & 25 | 26 | while true; do 27 | echo "Cleaning up old jobs..." 28 | curl http://localhost:3000/api/cron/cleanup -H "Authorization: Bearer $CRON_SECRET" 29 | echo "" 30 | sleep 30 31 | done & 32 | 33 | wait -------------------------------------------------------------------------------- /apps/website/update-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | PKG_FILE="package.json" 6 | TMP_FILE="package.tmp.json" 7 | 8 | update_section() { 9 | local section=$1 10 | jq -r ".$section // {} | keys[]" "$PKG_FILE" | while read -r dep; do 11 | current=$(jq -r ".$section[\"$dep\"]" "$PKG_FILE") 12 | latest=$(npm view "$dep" version 2>/dev/null) 13 | if [[ -n "$latest" && "$current" != "^$latest" ]]; then 14 | echo "Updating $dep: $current -> ^$latest" 15 | jq ".$section[\"$dep\"] = \"^$latest\"" "$PKG_FILE" > "$TMP_FILE" && mv "$TMP_FILE" "$PKG_FILE" 16 | else 17 | echo "$dep is up-to-date ($current)" 18 | fi 19 | done 20 | } 21 | 22 | update_section "dependencies" 23 | update_section "devDependencies" 24 | 25 | echo "Done. Please run your package manager to install the updates." -------------------------------------------------------------------------------- /apps/docs/content/docs/api/job-handlers.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: JobHandlers 3 | --- 4 | 5 | The `JobHandlers` type defines a map of job types to their handler functions. Each handler processes a job's payload and receives an `AbortSignal` for cancellation. 6 | 7 | ## Type 8 | 9 | ```ts 10 | type JobHandler = ( 11 | payload: PayloadMap[T], 12 | signal: AbortSignal, 13 | ) => Promise; 14 | 15 | // Map of job types to handlers 16 | 17 | export type JobHandlers = { 18 | [K in keyof PayloadMap]: JobHandler; 19 | }; 20 | ``` 21 | 22 | ## Example 23 | 24 | ```ts 25 | const handlers = { 26 | email: async (payload, signal) => { 27 | // send email 28 | }, 29 | generateReport: async (payload, signal) => { 30 | // generate report 31 | }, 32 | }; 33 | ``` 34 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/.source": ["./.source/index.ts"], 20 | "@/*": ["./*"] 21 | }, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ] 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /apps/demo/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Geist, Geist_Mono } from 'next/font/google'; 3 | import './globals.css'; 4 | 5 | const geistSans = Geist({ 6 | variable: '--font-geist-sans', 7 | subsets: ['latin'], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: '--font-geist-mono', 12 | subsets: ['latin'], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: 'Create Next App', 17 | description: 'Generated by create next app', 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | release: 5 | types: [published, edited] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | registry-url: 'https://registry.npmjs.org' 17 | - name: Set up pnpm 18 | uses: pnpm/action-setup@v4 19 | - name: Install dependencies 20 | run: pnpm install 21 | - name: Build package 22 | working-directory: packages/dataqueue 23 | run: pnpm run build 24 | - name: Publish to npm 25 | working-directory: packages/dataqueue 26 | run: pnpm exec changeset publish 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /apps/demo/lib/services/generate-image-ai.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a mock implementation of the image generation service. 3 | * It simulates a long-running task that can be aborted. 4 | * @param prompt - The prompt to generate an image for 5 | * @param signal - The abort signal to cancel the task 6 | */ 7 | export const generateImageAi = async (prompt: string, signal: AbortSignal) => { 8 | console.log('Generating image with prompt:', prompt); 9 | await new Promise( 10 | (resolve) => setTimeout(resolve, Math.random() * 1000 + 8000), // 1000 to 9000 ms 11 | ); 12 | 13 | if (signal.aborted) { 14 | console.log('Image generation aborted'); 15 | return; 16 | } 17 | 18 | if (Math.random() < 0.5) { 19 | console.log('Image generation failed'); 20 | throw new Error('Image generation failed'); 21 | } 22 | 23 | console.log('Image generated'); 24 | }; 25 | -------------------------------------------------------------------------------- /apps/website/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const labelVariants = cva( 10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "start": "next start", 9 | "postinstall": "fumadocs-mdx" 10 | }, 11 | "dependencies": { 12 | "fumadocs-core": "15.5.5", 13 | "fumadocs-docgen": "^2.0.1", 14 | "fumadocs-mdx": "11.6.9", 15 | "fumadocs-ui": "15.5.5", 16 | "next": "15.3.8", 17 | "react": "^19.1.0", 18 | "react-dom": "^19.1.0" 19 | }, 20 | "devDependencies": { 21 | "@tailwindcss/postcss": "^4.1.10", 22 | "@types/mdx": "^2.0.13", 23 | "@types/node": "24.0.3", 24 | "@types/react": "^19.1.8", 25 | "@types/react-dom": "^19.1.6", 26 | "eslint": "^8", 27 | "eslint-config-next": "15.3.4", 28 | "postcss": "^8.5.6", 29 | "tailwindcss": "^4.1.10", 30 | "typescript": "^5.8.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/website/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |