diff --git a/.changeset/README.md b/.changeset/README.md index e5b6d8d6..5654e898 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -1,8 +1,5 @@ # Changesets -Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works -with multi-package repos, or single-package repos to help you version and publish your code. You can -find the full documentation for it [in our repository](https://github.com/changesets/changesets) +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) -We have a quick list of common questions to get you started engaging with this project in -[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) +We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/adapter-drizzle-1.0.md b/.changeset/adapter-drizzle-1.0.md new file mode 100644 index 00000000..ac0b1102 --- /dev/null +++ b/.changeset/adapter-drizzle-1.0.md @@ -0,0 +1,30 @@ +--- +"@vorsteh-queue/adapter-drizzle": major +--- + +## @vorsteh-queue/adapter-drizzle 1.0 + +### Breaking: QueueAdapter Interface Rewritten + +Implements the new `QueueAdapter` interface with additional methods: + +- `getJobById(id)` — retrieve a single job +- `getNextJob(options)` — now requires `handlerNames` and `activeGroups` for FIFO support +- `getNextJobsForHandler(name, count, groupConstraints)` — batch picking with group constraints +- `updateJobStatus(id, update)` — new signature with `JobStatusUpdate` object +- `cancelJob(id, reason?)` / `cancelJobs(filter)` — cancellation support +- `getDeadJobs()` / `redriveJob(id)` / `redriveJobs(filter?)` — DLQ support +- `findJobByUniqueKey(uniqueKey)` — unique job lookup +- `updateJobSteps(id, steps)` — step state persistence + +### Breaking: Schema Changes (migration required) + +New columns: `group_key`, `unique_key`, `cancelled_at`, `cancellation_reason`, `steps` + +Changed: `timeout` from JSONB to INT (nullable, milliseconds) + +Changed: `progress` from nullable to non-nullable INT (default 0) + +Changed: `repeat_count` from nullable to non-nullable (default 0) + +New indexes: `idx_queue_jobs_polling`, `idx_queue_jobs_delayed`, `idx_queue_jobs_active_groups`, `idx_queue_jobs_stats` diff --git a/.changeset/adapter-kysely-1.0.md b/.changeset/adapter-kysely-1.0.md new file mode 100644 index 00000000..fc286d1a --- /dev/null +++ b/.changeset/adapter-kysely-1.0.md @@ -0,0 +1,32 @@ +--- +"@vorsteh-queue/adapter-kysely": major +--- + +## @vorsteh-queue/adapter-kysely 1.0 + +### Breaking: QueueAdapter Interface Rewritten + +Implements the new `QueueAdapter` interface with additional methods: + +- `getJobById(id)` — retrieve a single job +- `getNextJob(options)` — now requires `handlerNames` and `activeGroups` for FIFO support +- `getNextJobsForHandler(name, count, groupConstraints)` — batch picking with group constraints +- `updateJobStatus(id, update)` — new signature with `JobStatusUpdate` object +- `cancelJob(id, reason?)` / `cancelJobs(filter)` — cancellation support +- `getDeadJobs()` / `redriveJob(id)` / `redriveJobs(filter?)` — DLQ support +- `findJobByUniqueKey(uniqueKey)` — unique job lookup +- `updateJobSteps(id, steps)` — step state persistence + +### Breaking: Schema Changes (migration required) + +New columns: `group_key`, `unique_key`, `cancelled_at`, `cancellation_reason`, `steps` + +Changed: `timeout` from JSONB to INT (nullable, milliseconds) + +Changed: `progress` from nullable to non-nullable INT (default 0) + +Changed: `repeat_count` from nullable to non-nullable (default 0) + +New indexes: `idx_queue_jobs_polling`, `idx_queue_jobs_delayed`, `idx_queue_jobs_active_groups`, `idx_queue_jobs_stats` + +Updated `createQueueJobsTable()` helper and migration files with new schema. diff --git a/.changeset/adapter-mikroorm-initial.md b/.changeset/adapter-mikroorm-initial.md new file mode 100644 index 00000000..43e28158 --- /dev/null +++ b/.changeset/adapter-mikroorm-initial.md @@ -0,0 +1,22 @@ +--- +"@vorsteh-queue/adapter-mikroorm": major +"@vorsteh-queue/core": patch +"@vorsteh-queue/query-builder": patch +--- + +## @vorsteh-queue/adapter-mikroorm (new package) + +Initial release of the MikroORM adapter for Vorsteh Queue. + +### Features + +- `PostgresMikroormQueueAdapter` — full implementation of the `QueueAdapter` interface +- PostgreSQL support via MikroORM v6 EntityManager with unit of work pattern +- Raw SQL job picking with `FOR UPDATE SKIP LOCKED` for reliable concurrent processing +- Pre-built `QueueJobSchema` (EntitySchema) for quick database setup +- Configurable table name and schema name + +### Related changes + +- `@vorsteh-queue/core`: Added `MikroormAdapterProps` interface and extended `AdapterKind` union +- `@vorsteh-queue/query-builder`: Added `./mikroorm` export with MikroORM-compatible `buildWhere` diff --git a/.changeset/adapter-prisma-1.0.md b/.changeset/adapter-prisma-1.0.md new file mode 100644 index 00000000..5fb668e7 --- /dev/null +++ b/.changeset/adapter-prisma-1.0.md @@ -0,0 +1,32 @@ +--- +"@vorsteh-queue/adapter-prisma": major +--- + +## @vorsteh-queue/adapter-prisma 1.0 + +### Breaking: QueueAdapter Interface Rewritten + +Implements the new `QueueAdapter` interface with additional methods: + +- `getJobById(id)` — retrieve a single job +- `getNextJob(options)` — now requires `handlerNames` and `activeGroups` for FIFO support +- `getNextJobsForHandler(name, count, groupConstraints)` — batch picking with group constraints +- `updateJobStatus(id, update)` — new signature with `JobStatusUpdate` object +- `cancelJob(id, reason?)` / `cancelJobs(filter)` — cancellation support +- `getDeadJobs()` / `redriveJob(id)` / `redriveJobs(filter?)` — DLQ support +- `findJobByUniqueKey(uniqueKey)` — unique job lookup +- `updateJobSteps(id, steps)` — step state persistence + +### Breaking: Schema Changes (migration required) + +New columns: `group_key`, `unique_key`, `cancelled_at`, `cancellation_reason`, `steps` + +Changed: `timeout` from JSONB to INT (nullable, milliseconds) + +Changed: `progress` from nullable to non-nullable INT (default 0) + +Changed: `repeat_count` from nullable to non-nullable (default 0) + +New indexes: `idx_queue_jobs_polling`, `idx_queue_jobs_delayed`, `idx_queue_jobs_active_groups`, `idx_queue_jobs_stats` + +Updated `schema.prisma` with new model fields. diff --git a/.changeset/adapter-sequelize-initial.md b/.changeset/adapter-sequelize-initial.md new file mode 100644 index 00000000..9f986f3e --- /dev/null +++ b/.changeset/adapter-sequelize-initial.md @@ -0,0 +1,22 @@ +--- +"@vorsteh-queue/adapter-sequelize": major +"@vorsteh-queue/core": patch +"@vorsteh-queue/query-builder": patch +--- + +## @vorsteh-queue/adapter-sequelize (new package) + +Initial release of the Sequelize adapter for Vorsteh Queue. + +### Features + +- `PostgresSequelizeQueueAdapter` — full implementation of the `QueueAdapter` interface +- PostgreSQL support via Sequelize Model pattern with raw SQL for job picking +- Raw SQL job picking with `FOR UPDATE SKIP LOCKED` for reliable concurrent processing +- Pre-built `QueueJobModel` with `initQueueJobModel()` for quick model registration +- Configurable table name and schema name + +### Related changes + +- `@vorsteh-queue/core`: Added `SequelizeAdapterProps` interface and extended `AdapterKind` union +- `@vorsteh-queue/query-builder`: Added `./sequelize` export with Sequelize-compatible `buildWhere` diff --git a/.changeset/adapter-typeorm-initial.md b/.changeset/adapter-typeorm-initial.md new file mode 100644 index 00000000..c4b2562a --- /dev/null +++ b/.changeset/adapter-typeorm-initial.md @@ -0,0 +1,22 @@ +--- +"@vorsteh-queue/adapter-typeorm": major +"@vorsteh-queue/core": patch +"@vorsteh-queue/query-builder": patch +--- + +## @vorsteh-queue/adapter-typeorm (new package) + +Initial release of the TypeORM adapter for Vorsteh Queue. + +### Features + +- `PostgresTypeormQueueAdapter` — full implementation of the `QueueAdapter` interface +- PostgreSQL support via TypeORM's DataSource and Repository pattern +- Raw SQL job picking with `FOR UPDATE SKIP LOCKED` for reliable concurrent processing +- Pre-built `QueueJobEntity` with all required columns, types, and indexes +- Configurable table name and schema name + +### Related changes + +- `@vorsteh-queue/core`: Added `TypeormAdapterProps` interface and extended `AdapterKind` union +- `@vorsteh-queue/query-builder`: Added `./typeorm` export with TypeORM-compatible `buildWhere` diff --git a/.changeset/adapter-zenstack-initial.md b/.changeset/adapter-zenstack-initial.md new file mode 100644 index 00000000..626178e0 --- /dev/null +++ b/.changeset/adapter-zenstack-initial.md @@ -0,0 +1,22 @@ +--- +"@vorsteh-queue/adapter-zenstack": major +"@vorsteh-queue/core": patch +"@vorsteh-queue/query-builder": patch +--- + +## @vorsteh-queue/adapter-zenstack (new package) + +Initial release of the ZenStack ORM adapter for Vorsteh Queue. + +### Features + +- `PostgresZenstackQueueAdapter` — full implementation of the `QueueAdapter` interface +- PostgreSQL support via ZenStack ORM v3 (Prisma-compatible API built on Kysely) +- Raw SQL job picking with `FOR UPDATE SKIP LOCKED` for reliable concurrent processing +- Configurable model name, table name, and schema name +- ZModel schema provided for quick database setup + +### Related changes + +- `@vorsteh-queue/core`: Added `ZenstackAdapterProps` interface and extended `AdapterKind` union +- `@vorsteh-queue/query-builder`: Added `./zenstack` export (re-exports Prisma-compatible `buildWhere`) diff --git a/.changeset/cli-initial.md b/.changeset/cli-initial.md new file mode 100644 index 00000000..ada67e3e --- /dev/null +++ b/.changeset/cli-initial.md @@ -0,0 +1,14 @@ +--- +"@vorsteh-queue/cli": minor +--- + +## @vorsteh-queue/cli — Initial Release + +CLI tool for monitoring and managing vorsteh-queue jobs. + +- Commands: `status`, `inspect`, `cancel`, `redrive`, `clear` +- Transports: direct adapter connection or remote GraphQL server +- `--json` flag on all commands for machine-readable output +- Configuration via environment variables (`VORSTEH_QUEUE_URL`, `VORSTEH_QUEUE_TOKEN`) +- `defineCliConfig()` helper for typed configuration +- Built with citty (unjs CLI framework) diff --git a/.changeset/core-1.0.md b/.changeset/core-1.0.md new file mode 100644 index 00000000..3939065e --- /dev/null +++ b/.changeset/core-1.0.md @@ -0,0 +1,59 @@ +--- +"@vorsteh-queue/core": major +--- + +## @vorsteh-queue/core 1.0 + +### Breaking: Queue/Worker Architecture Split + +The `Queue` class is now a producer-only. Job processing is handled by the new `Worker` class. + +Before: + +```typescript +const queue = new Queue(adapter, { name: "my-queue" }) +queue.register("job", handler) +queue.start() +``` + +After: + +```typescript +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue", concurrency: 5 }) +worker.register("job", handler) +worker.start() + +// Queue is now producer-only — use it to add jobs +await queue.add("job", { data: "payload" }) +``` + +### Breaking: Handler Signature + +Handlers now receive a `JobContext` with an `AbortSignal` for timeout/cancellation support. + +- Before: `(job) => Promise` +- After: `(job, { signal, step }) => Promise` + +### Breaking: New Statuses + +Added `cancelled` and `dead` statuses. `QueueStats` now includes all 7 statuses. + +### Breaking: Removed APIs + +`queue.register()`, `queue.start()`, `queue.stop()`, `queue.pause()`, `queue.resume()`, `queue.enqueue()`, `queue.dequeue()` + +### New Features + +- Job cancellation (`queue.cancel()`, `worker.cancelJob()`) +- Dead-letter queue (`queue.getDeadJobs()`, `queue.redrive()`, `queue.redriveAll()`) +- Group FIFO ordering (`{ group: "tenant-1" }`) +- Unique job deduplication (`{ unique: { key: "...", action: "reject" | "replace" } }`) +- `enqueueAndWait()` — enqueue and wait for result (hybrid event + polling) +- Typed event emitter on both Queue and Worker +- Configurable retry strategies (exponential, linear, fixed, custom function) +- State machine validation for job status transitions +- Per-handler concurrency limits +- Job Steps (`step.run()`, `step.sleep()`, `step.all()`) for durable multi-step execution +- Job Dependencies (`dependsOn`, circular detection) +- Rate Limiting (token-bucket per handler) diff --git a/.changeset/drizzle-v1-migration.md b/.changeset/drizzle-v1-migration.md new file mode 100644 index 00000000..33bf176a --- /dev/null +++ b/.changeset/drizzle-v1-migration.md @@ -0,0 +1,33 @@ +--- +"@vorsteh-queue/adapter-drizzle": major +"@vorsteh-queue/query-builder": major +--- + +## Breaking: drizzle-orm peer dependency bumped to >=1.0.0-rc.4 + +The minimum `drizzle-orm` peer dependency has been raised from `>=0.45.x` to `>=1.0.0-rc.4` for both the adapter and query builder packages. + +### Breaking: Drizzle client initialization + +The `drizzle()` call now requires a `relations` object: + +**Before:** + +```typescript +import * as schema from "./schema" +const db = drizzle(pool, { schema }) +``` + +**After:** + +```typescript +import { relations } from "./schema" +const db = drizzle({ client: pool, relations }) +``` + +### Migration steps + +1. Upgrade `drizzle-orm` to `>=1.0.0-rc.4` and `drizzle-kit` to `>=1.0.0-rc.4` +2. Define your own relations with `defineRelations` including `queueJobs` +3. Pass `relations` (not `schema`) to your `drizzle()` call +4. The query builder's `buildWhere` now returns a plain RQB v2 where object instead of SQL conditions diff --git a/.changeset/job-where-filter.md b/.changeset/job-where-filter.md new file mode 100644 index 00000000..8a4aeee0 --- /dev/null +++ b/.changeset/job-where-filter.md @@ -0,0 +1,51 @@ +--- +"@vorsteh-queue/core": major +"@vorsteh-queue/query-builder": minor +"@vorsteh-queue/adapter-drizzle": major +"@vorsteh-queue/adapter-prisma": major +"@vorsteh-queue/adapter-kysely": major +"@vorsteh-queue/server": major +"@vorsteh-queue/cli": major +--- + +## Job Where Filter + +### Breaking: `getJobs` and `size` Signatures + +Replaced flat `status`/`name` filter parameters with a composable `where` input supporting multiple filter conditions with comparison operators. + +**Before:** + +```typescript +adapter.getJobs({ status: "failed", name: "send-email", limit: 10 }) +adapter.size() +``` + +**After:** + +```typescript +adapter.getJobs({ where: { status: "failed", name: "send-email" }, limit: 10 }) +adapter.size({ status: "failed" }) +``` + +### Breaking: GraphQL Schema + +- `jobs(queue, status, name, limit, offset)` → `jobs(queue, where: JobWhereInput, limit, offset)` +- `size(queue)` → `size(queue, where: JobWhereInput)` + +### New: `@vorsteh-queue/query-builder` + +New package providing filter normalization, in-memory matching, and ORM-specific where-clause builders: + +- `normalizeWhere()` — expands shorthand filters +- `matchesWhere()` — in-memory job matching +- `buildWhere()` — translates to Drizzle/Kysely/Prisma conditions + +### Supported Operators + +- **String:** eq, neq, contains, startsWith, like, in, isNull +- **Int:** eq, neq, lt, lte, gt, gte, isNull +- **DateTime:** lt, lte, gt, gte, isNull +- **Status:** eq, neq, in +- **Null:** isNull +- **Logical:** AND, OR (recursive) diff --git a/.changeset/node-22-minimum.md b/.changeset/node-22-minimum.md new file mode 100644 index 00000000..e8f1e4fa --- /dev/null +++ b/.changeset/node-22-minimum.md @@ -0,0 +1,13 @@ +--- +"@vorsteh-queue/core": major +"@vorsteh-queue/adapter-drizzle": major +"@vorsteh-queue/adapter-prisma": major +"@vorsteh-queue/adapter-kysely": major +"@vorsteh-queue/server": major +"@vorsteh-queue/cli": major +"create-vorsteh-queue": major +--- + +**Breaking:** Minimum Node.js version raised from 20 to 22. + +Node.js 22 is the current active LTS. This allows the use of stable native `fetch`, improved `AbortSignal` support, and aligns with the TypeScript 6.x target. diff --git a/.changeset/retry-runnow-delete.md b/.changeset/retry-runnow-delete.md new file mode 100644 index 00000000..4625e74e --- /dev/null +++ b/.changeset/retry-runnow-delete.md @@ -0,0 +1,23 @@ +--- +"@vorsteh-queue/core": minor +"@vorsteh-queue/adapter-drizzle": minor +"@vorsteh-queue/adapter-kysely": minor +"@vorsteh-queue/adapter-prisma": minor +"@vorsteh-queue/server": minor +"@vorsteh-queue/cli": minor +--- + +## New operations: retry, runNow, deleteJob + +Added three new job management operations across the full stack: + +- **`retry(jobId)`** — resets a `failed` job to `pending` (clears error, resets attempts to 0) +- **`runNow(jobId)`** — promotes a `delayed` job to `pending` immediately (sets processAt to now) +- **`deleteJob(jobId)`** — permanently removes a single job by ID + +Available on: + +- `Queue` class: `queue.retry()`, `queue.runNow()`, `queue.deleteJob()` +- `QueueAdapter` interface: `retryJob()`, `runJobNow()`, `deleteJob()` +- GraphQL API: mutations `retryJob`, `runJobNow`, `deleteJob` +- CLI: commands `retry`, `run-now`, `delete` diff --git a/.changeset/server-1.0.md b/.changeset/server-1.0.md new file mode 100644 index 00000000..4e650ec8 --- /dev/null +++ b/.changeset/server-1.0.md @@ -0,0 +1,33 @@ +--- +"@vorsteh-queue/server": major +--- + +## @vorsteh-queue/server 1.0 — Initial Release + +GraphQL server, web dashboard, and monitoring API for Vorsteh Queue. + +### GraphQL API + +- `createQueueServer(config)` — standalone server (Hono + Node.js) +- `createQueueMiddleware(config)` — composable Hono middleware for existing apps +- Queries: `stats`, `job`, `jobs` (with `where: JobWhereInput`), `deadJobs`, `size`, `flows` +- Mutations: `cancelJob`, `redriveJob`, `redriveAll`, `retryJob`, `runJobNow`, `deleteJob`, `clearJobs` +- Subscriptions: `jobStatusChanged`, `statsUpdated` (SSE-based) +- PubSub for real-time event streaming + +### Web Dashboard + +- Embedded dashboard served as static assets when `dashboard: true` (default) +- Overview stats, Jobs list (filtered/paginated), Job detail, DLQ, Flows, Flow DAG visualization +- Communicates with the API via `gql.tada` + `graphql-request` + +### Authentication + +- Token-based auth: `tokens: string[]` +- Custom middleware support for advanced auth scenarios +- Disable with `false` for development + +### Configuration + +- `loadConfig()` to load `queue.config.ts` via c12 +- `defineConfig()` helper for typed configuration diff --git a/.changeset/workflow-engine.md b/.changeset/workflow-engine.md new file mode 100644 index 00000000..b2694cc9 --- /dev/null +++ b/.changeset/workflow-engine.md @@ -0,0 +1,66 @@ +--- +"@vorsteh-queue/core": minor +--- + +## Workflow Engine Features + +### Saga Compensation + +Steps can declare `compensate` functions that run in reverse order when a later step fails: + +```typescript +await step.run("charge", () => payments.charge(amount), { + compensate: (result) => payments.refund(result.txId), +}) +``` + +### Signals / Human-in-the-Loop + +Jobs can pause and wait for an external signal: + +```typescript +const approval = await step.waitFor("approval", "manager-decision", { + timeout: "24h", +}) +// External: await queue.signal(jobId, "manager-decision", { approved: true }) +``` + +### Event Triggers + +Automatically create follow-up jobs on completion: + +```typescript +worker.trigger({ + on: "order", + create: "send-receipt", + data: (result, job) => ({ email: result.email }), + condition: (result) => result.total > 0, +}) +``` + +### Flow Producer (Parent-Child Job Trees) + +Declarative job trees with parent-child relationships: + +```typescript +const flow = await queue.addFlow({ + name: "deploy", + payload: { version: "1.0" }, + children: [ + { name: "build", payload: { target: "linux" } }, + { name: "test", payload: {}, children: [{ name: "lint", payload: {} }] }, + ], +}) +``` + +- Parent waits in `waiting-children` status until all children complete +- `failParentOnFailure` cascades child failures upward +- `ctx.getChildrenResults()` provides children results to parent handlers +- `queue.getFlowTree(flowId)` returns the full tree for visualization + +### New Status: `waiting-children` + +Added to the state machine. Valid transitions: + +- `pending` → `waiting-children` +- `waiting-children` → `pending` (all children done), `cancelled`, `failed` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9d9a8cc9..a3ef108c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,13 +1,15 @@ # GitHub Copilot Instructions ## Development Philosophy + - **Quality over quantity**: Write clean, maintainable, and well-structured code - **Senior-level TypeScript**: Leverage advanced TypeScript features for type safety - **Minimal and focused**: Every line of code should serve a clear purpose - **Performance-conscious**: Consider efficiency and avoid unnecessary complexity -## TypeScript & ESLint Rules -- **No console.log**: Use proper logging or remove debug statements +## TypeScript & Linting Rules (oxlint via ultracite) + +- **No console.log**: Use proper logging or remove debug statements (`no-console: error`) - **Consistent type imports**: Use `import type` for type-only imports - **No unused vars**: Prefix with `_` if intentionally unused - **No unnecessary conditions**: Avoid redundant null checks @@ -20,22 +22,46 @@ - **No useless path segments**: Avoid `/index` in import paths when possible - **Prefer directory imports**: Use `../src` instead of `../src/index` for cleaner imports -## Prettier Configuration -- **Print width**: 100 characters max +## Formatting Configuration (oxfmt via ultracite) + +- **Line width**: 100 characters max - **No semicolons**: Use semicolon-free style -- **Import order**: Follow the specified import grouping: +- **Trailing commas**: ES5 style +- **Import sorting**: Handled automatically by oxfmt (`sortImports: true`) +- **Import order** (enforced by oxfmt): 1. Types first 2. React/Next.js/Expo (if applicable) 3. Third-party modules 4. @vorsteh-queue packages 5. Relative imports (~/,../, ./) +## Tooling & Verification + +After making code changes, verify with these commands (in this order): + +1. `pnpm format:fix` — Apply formatting +2. `pnpm lint:fix` — Auto-fix lint issues +3. `pnpm typecheck` — Verify types +4. `vitest --run` — Run tests (single run, no watch mode) + +Always use the pnpm scripts from the root `package.json`. Never call tools directly via `npx` or `pnpm dlx`. + +| Tool | Check | Fix | +| ------------------ | ---------------- | ----------------- | +| Formatter (oxfmt) | `pnpm format` | `pnpm format:fix` | +| Linter (oxlint) | `pnpm lint` | `pnpm lint:fix` | +| Types (TypeScript) | `pnpm typecheck` | — | +| Tests (Vitest) | `vitest --run` | — | +| Build (Turborepo) | `pnpm build` | — | +| Workspace (sherif) | `pnpm lint:ws` | — | + ## Code Generation Guidelines + - Remove all `console.log` statements from generated code - Use proper TypeScript types instead of `any` when possible - **Use type-fest when available** - Prefer battle-tested utility types from type-fest over custom implementations -- Add ESLint disable comments only when absolutely necessary -- Follow the import order specified in prettier config +- Add lint disable comments only when absolutely necessary +- Follow the import order enforced by oxfmt - Use consistent naming conventions (camelCase for variables, PascalCase for types) - **Generic type parameters**: Always prefix with `T` (e.g., `TJobPayload`, `TResult`, `TEventData`) - Prefer explicit return types for functions @@ -46,8 +72,9 @@ - Optimize for readability and maintainability ## File Structure -- Keep imports organized according to prettier rules + +- Keep imports organized according to oxfmt rules - Use meaningful variable and function names - Add proper JSDoc comments for public APIs - Prefer composition over inheritance -- Use readonly arrays and objects where appropriate \ No newline at end of file +- Use readonly arrays and objects where appropriate diff --git a/.github/setup/action.yml b/.github/setup/action.yml index 3fb7ce12..204bf50e 100644 --- a/.github/setup/action.yml +++ b/.github/setup/action.yml @@ -4,7 +4,7 @@ description: "Common setup steps for Actions" runs: using: composite steps: - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee3a2c31..dc9e060b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,10 +74,26 @@ jobs: - name: Typecheck run: pnpm typecheck - pkg-new-release: - needs: [lint, format, typecheck] + detect-package-changes: if: github.event_name == 'pull_request' runs-on: ubuntu-latest + outputs: + packages_changed: ${{ steps.filter.outputs.packages }} + steps: + - uses: actions/checkout@v6 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + packages: + - 'packages/**' + - 'pnpm-lock.yaml' + + pkg-new-release: + needs: [lint, format, typecheck, detect-package-changes] + if: github.event_name == 'pull_request' && needs.detect-package-changes.outputs.packages_changed == 'true' + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -90,4 +106,4 @@ jobs: - name: Build run: pnpm build:pkg - - run: pnpx pkg-pr-new publish ./packages/* + - run: pnpx pkg-pr-new publish --commentWithSha --packageManager=pnpm ./packages/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff368e97..21f53b9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,8 @@ jobs: steps: - uses: actions/checkout@v6 with: - persist-credentials: false + token: ${{ secrets.CHANGESETS_TOKEN }} + persist-credentials: true - name: Setup uses: ./.github/setup @@ -32,4 +33,3 @@ jobs: publish: pnpm ci:publish env: GITHUB_TOKEN: ${{ secrets.CHANGESETS_TOKEN }} - NPM_TOKEN: "" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e2579bfa..f16c6b9e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,11 +29,31 @@ env: FORCE_COLOR: 3 jobs: + detect-package-changes: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + outputs: + packages_changed: ${{ steps.filter.outputs.packages }} + steps: + - uses: actions/checkout@v6 + + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + packages: + - 'packages/**' + - 'pnpm-lock.yaml' + node_versions: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') runs-on: ubuntu-latest strategy: matrix: - node-version: [20, 22, 24] + node-version: [22, 24, 26] name: Test (Node.js ${{ matrix.node-version }}) steps: - uses: actions/checkout@v6 @@ -44,7 +64,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false @@ -79,7 +99,31 @@ jobs: # Ensure Docker is available for Testcontainers TESTCONTAINERS_RYUK_DISABLED: true + - name: Test ZenStack + run: pnpm test:zenstack + env: + TESTCONTAINERS_RYUK_DISABLED: true + + - name: Test TypeORM + run: pnpm test:typeorm + env: + TESTCONTAINERS_RYUK_DISABLED: true + + - name: Test MikroORM + run: pnpm test:mikroorm + env: + TESTCONTAINERS_RYUK_DISABLED: true + + - name: Test Sequelize + run: pnpm test:sequelize + env: + TESTCONTAINERS_RYUK_DISABLED: true + postgres-drizzle: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') runs-on: ubuntu-latest strategy: matrix: @@ -92,7 +136,7 @@ jobs: uses: ./.github/setup - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false @@ -111,6 +155,10 @@ jobs: PG_VERSION: ${{ matrix.pg-version }} postgres-prisma: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') runs-on: ubuntu-latest strategy: matrix: @@ -123,7 +171,7 @@ jobs: uses: ./.github/setup - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false @@ -144,6 +192,10 @@ jobs: PG_VERSION: ${{ matrix.pg-version }} postgres-kysely: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') runs-on: ubuntu-latest strategy: matrix: @@ -156,7 +208,7 @@ jobs: uses: ./.github/setup - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false @@ -173,3 +225,139 @@ jobs: # Ensure Docker is available for Testcontainers TESTCONTAINERS_RYUK_DISABLED: true PG_VERSION: ${{ matrix.pg-version }} + + postgres-zenstack: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') + runs-on: ubuntu-latest + strategy: + matrix: + pg-version: [12, 13, 14, 15, 16, 17] + name: Test ZenStack Adapter (Postgres ${{ matrix.pg-version }}) + steps: + - uses: actions/checkout@v6 + + - name: Setup + uses: ./.github/setup + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + run_install: false + + - shell: bash + run: pnpm install + + - name: Build package + run: | + pnpm --filter "./packages/*" build + + - name: Test PostgreSQL (Postgres ${{ matrix.pg-version }}) + run: pnpm test:zenstack + env: + TESTCONTAINERS_RYUK_DISABLED: true + PG_VERSION: ${{ matrix.pg-version }} + + postgres-typeorm: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') + runs-on: ubuntu-latest + strategy: + matrix: + pg-version: [12, 13, 14, 15, 16, 17] + name: Test TypeORM Adapter (Postgres ${{ matrix.pg-version }}) + steps: + - uses: actions/checkout@v6 + + - name: Setup + uses: ./.github/setup + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + run_install: false + + - shell: bash + run: pnpm install + + - name: Build package + run: | + pnpm --filter "./packages/*" build + + - name: Test PostgreSQL (Postgres ${{ matrix.pg-version }}) + run: pnpm test:typeorm + env: + TESTCONTAINERS_RYUK_DISABLED: true + PG_VERSION: ${{ matrix.pg-version }} + + postgres-mikroorm: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') + runs-on: ubuntu-latest + strategy: + matrix: + pg-version: [12, 13, 14, 15, 16, 17] + name: Test MikroORM Adapter (Postgres ${{ matrix.pg-version }}) + steps: + - uses: actions/checkout@v6 + + - name: Setup + uses: ./.github/setup + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + run_install: false + + - shell: bash + run: pnpm install + + - name: Build package + run: | + pnpm --filter "./packages/*" build + + - name: Test PostgreSQL (Postgres ${{ matrix.pg-version }}) + run: pnpm test:mikroorm + env: + TESTCONTAINERS_RYUK_DISABLED: true + PG_VERSION: ${{ matrix.pg-version }} + + postgres-sequelize: + needs: [detect-package-changes] + if: | + always() && + (needs.detect-package-changes.result == 'skipped' || needs.detect-package-changes.outputs.packages_changed == 'true') + runs-on: ubuntu-latest + strategy: + matrix: + pg-version: [12, 13, 14, 15, 16, 17] + name: Test Sequelize Adapter (Postgres ${{ matrix.pg-version }}) + steps: + - uses: actions/checkout@v6 + + - name: Setup + uses: ./.github/setup + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + run_install: false + + - shell: bash + run: pnpm install + + - name: Build package + run: | + pnpm --filter "./packages/*" build + + - name: Test PostgreSQL (Postgres ${{ matrix.pg-version }}) + run: pnpm test:sequelize + env: + TESTCONTAINERS_RYUK_DISABLED: true + PG_VERSION: ${{ matrix.pg-version }} diff --git a/.gitignore b/.gitignore index dc496141..dde66633 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,4 @@ yarn-error.log* # turbo .turbo -apps/docs/public/pagefind/ \ No newline at end of file +.renoun \ No newline at end of file diff --git a/.kiro/settings/mcp.json b/.kiro/settings/mcp.json new file mode 100644 index 00000000..85839b65 --- /dev/null +++ b/.kiro/settings/mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "shadcn": { + "command": "npx", + "args": ["shadcn@latest", "mcp"], + "disabled": false + } + } +} diff --git a/.kiro/steering/project.md b/.kiro/steering/project.md new file mode 100644 index 00000000..14e58e9e --- /dev/null +++ b/.kiro/steering/project.md @@ -0,0 +1,170 @@ +# vorsteh-queue - Project Steering + +## Project Overview + +vorsteh-queue is a TypeScript-based job queue library published as a set of packages: + +- `packages/core` - Core queue logic +- `packages/adapter-drizzle` - Drizzle ORM adapter +- `packages/adapter-prisma` - Prisma adapter +- `packages/adapter-kysely` - Kysely adapter +- `packages/create-vorsteh-queue` - CLI scaffolding tool +- `packages/shared-tests` - Shared test utilities +- `tooling/typescript` - Shared TypeScript configurations + +## Monorepo Structure + +- **Package manager**: pnpm (workspace monorepo) +- **Build orchestration**: Turborepo +- **Workspace packages**: `apps/*`, `packages/*`, `tooling/*`, `examples/*` +- **Workspace constraints**: sherif (runs on `postinstall`) +- **Versioning**: changesets + +## Tooling + +### Linting (oxlint via ultracite) + +- Command: `pnpm lint` (check) / `pnpm lint:fix` (auto-fix) +- Config: `oxlint.config.ts` at workspace root +- Presets: `ultracite/oxlint/core`, `ultracite/oxlint/react`, `ultracite/oxlint/next`, `ultracite/oxlint/vitest` + +### Formatting (oxfmt via ultracite) + +- Command: `pnpm format` (check) / `pnpm format:fix` (auto-fix) +- Config: `oxfmt.config.ts` at workspace root +- Settings: line width 100, no semicolons, ES5 trailing commas, auto import sorting + +### Type Checking + +- Command: `pnpm typecheck` +- TypeScript 6.x with shared configs from `tooling/typescript` + +### Testing + +- Framework: Vitest +- Command: `pnpm test` (watch) / `vitest --run` (single run) +- Per-package: `pnpm test:core`, `pnpm test:drizzle`, `pnpm test:prisma`, `pnpm test:kysely` +- Coverage: `@vitest/coverage-v8` + +### Building + +- Command: `pnpm build` (all) / `pnpm build:pkg` (packages only) +- Turborepo handles dependency ordering + +## Code Style & Conventions + +### TypeScript + +- Use `import type` for type-only imports +- Prefix unused variables with `_` +- Use proper type guards instead of non-null assertions (`!`) +- Prefer `??` over `||` for null/undefined checks +- Prefer `?.` for optional property access +- No `.js` / `.ts` extensions in imports +- Prefer directory imports over explicit `/index` paths +- Use `readonly T[]` over `ReadonlyArray` +- Generic type parameters: always prefix with `T` (e.g., `TJobPayload`, `TResult`, `TEventData`) +- Prefer explicit return types for exported functions +- No `any` - use proper types or `unknown` +- Use type-fest utility types when available over custom implementations + +### Formatting + +- No semicolons +- 100 character line width +- ES5 trailing commas +- Import order (handled by oxfmt): + 1. Type imports + 2. React/Next.js/Expo + 3. Third-party modules + 4. `@vorsteh-queue` packages + 5. Relative imports (`~/`, `../`, `./`) + +### Documentation (JSDoc) + +All public-facing code must have JSDoc headers: + +- Classes: describe purpose, include `@example` where useful +- Functions/Methods: describe what it does, `@param`, `@returns`, `@throws`, `@example` +- Interface properties: inline `/** description */` with `@default` where applicable +- Type aliases: describe what the type represents +- Enums/Constants: describe each member + +````typescript +/** + * Register a job handler for a specific job type. + * + * @param name - The job type name (must be unique per queue) + * @param handler - Function to process jobs of this type + * @throws {Error} If a handler with this name is already registered + * + * @example + * ```typescript + * queue.register("send-email", async (job) => { + * await sendEmail(job.payload.to) + * return { sent: true } + * }) + * ``` + */ +```` + +```typescript +interface JobOptions { + /** Job priority (lower number = higher priority) + * @default 2 + */ + readonly priority?: number + + /** Delay in milliseconds before job becomes available + * @default undefined (no delay) + */ + readonly delay?: number +} +``` + +Do NOT add JSDoc to: + +- Private/internal helpers (unless complex logic) +- Test files +- Obvious one-liner utility functions + +### General + +- No `console.log` - remove debug statements +- Prefer functional programming patterns +- Prefer composition over inheritance +- Use readonly arrays and objects where appropriate +- Write self-documenting code; minimize inline comments +- Use proper error handling (no generic `throw new Error`) +- camelCase for variables/functions, PascalCase for types/interfaces/classes + +## Verification Workflow + +After making code changes, always verify with these commands (in this order): + +1. `pnpm format:fix` — Apply formatting +2. `pnpm lint:fix` — Auto-fix lint issues +3. `pnpm typecheck` — Verify types +4. `vitest --run` — Run tests (single run, no watch mode) + +Never use `npx`, `pnpm dlx`, or call tools directly (e.g. `oxlint`, `oxfmt`). Always use the pnpm scripts defined in the root `package.json`. + +## Commands Reference + +| Task | Command | +| ------------------ | ------------------ | +| Install | `pnpm install` | +| Dev (packages) | `pnpm dev` | +| Dev (docs) | `pnpm dev:docs` | +| Build all | `pnpm build` | +| Build packages | `pnpm build:pkg` | +| Lint | `pnpm lint` | +| Lint fix | `pnpm lint:fix` | +| Format check | `pnpm format` | +| Format fix | `pnpm format:fix` | +| Typecheck | `pnpm typecheck` | +| Test (watch) | `pnpm test` | +| Test (single run) | `vitest --run` | +| Changeset | `pnpm cs` | +| Clean node_modules | `pnpm clean` | +| Clean turbo cache | `pnpm clean:cache` | diff --git a/.nvmrc b/.nvmrc index 8fdd954d..cabf43b5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 \ No newline at end of file +24 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 9211984a..00000000 --- a/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -turbo/generators/scaffold/templates/**/*.hbs -**/.cache -CHANGELOG.md -**/CHANGELOG.md \ No newline at end of file diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000..8c764699 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "shadcn": { + "command": "npx", + "args": ["shadcn@latest", "mcp"] + } + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 450c6699..9006c68f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,51 +1,74 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.fixAll": "always", + "source.fixAll.oxc": "always", + "source.format.oxc": "always" }, - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.formatOnSave": true, - "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], - "eslint.workingDirectories": [ - { "pattern": "apps/*/" }, - { "pattern": "packages/*/" }, - { "pattern": "tooling/*/" } - ], - "tailwindCSS.experimental.configFile": { - "apps/docs/src/app/globals.css": ["apps/docs/**"] - }, - "tailwindCSS.emmetCompletions": true, - "tailwindCSS.files.exclude": [ - "**/.git/**", - "**/node_modules/**", - "**/.hg/**", - "**/.svn/**", - "**/.vscode/**" - ], - "tailwindCSS.experimental.classRegex": [ - ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], - ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] - ], - "prettier.ignorePath": ".gitignore", - "typescript.enablePromptUseWorkspaceTsdk": true, - "typescript.preferences.autoImportFileExcludePatterns": [ - "next/router.d.ts", - "next/dist/client/router.d.ts" - ], - "typescript.tsdk": "node_modules/typescript/lib", "vitest.nodeEnv": { "npm_lifecycle_event": "test" }, - "[shellscript]": { - "editor.defaultFormatter": "foxundermoon.shell-format" + + "files.associations": { + "*.css": "tailwindcss", + "*.scss": "tailwindcss" }, - "[dotenv]": { - "editor.defaultFormatter": "foxundermoon.shell-format" + "[xml]": { + "editor.defaultFormatter": "redhat.vscode-xml" }, - "[sql]": { - "editor.defaultFormatter": "mtxr.sqltools" + "editor.formatOnPaste": true, + "emmet.showExpandedAbbreviation": "never", + "[css]": { + "editor.defaultFormatter": "oxc.oxc-vscode" }, - "files.associations": { - "*.css": "tailwindcss" - } + "[graphql]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[handlebars]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[html]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[json]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[less]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[markdown]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[scss]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[vue-html]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[vue]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "oxc.enable.oxlint": true, + "oxc.fixKind": "safe_fix_or_suggestion", + "oxc.requireConfig": true } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f58ea3e7..05fd2744 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,9 @@ Thank you for your interest in contributing to Vorsteh Queue! This document outl payload: TJobPayload } - function process(job: BaseJob): Promise + function process( + job: BaseJob + ): Promise // ❌ Avoid interface BaseJob { @@ -48,6 +50,7 @@ Follow this import order: 5. Relative imports (`~/`, `../`, `./`) **Import Path Conventions**: + - No file extensions: Never use `.js`, `.ts` extensions - Prefer directory imports: Use `../src` instead of `../src/index` - Avoid useless path segments: Skip `/index` when possible diff --git a/README.md b/README.md index f0640644..2d654eab 100644 --- a/README.md +++ b/README.md @@ -1,362 +1,95 @@
- Vorsteh Queue + Vorsteh Queue

Vorsteh Queue

-

A powerful, ORM-agnostic queue engine for PostgreSQL 12+. Handle background jobs, scheduled tasks, and recurring processes with ease.

+

A type-safe PostgreSQL job queue for Node.js with durable execution, flow orchestration, and first-class ORM adapters.

## Features - **Type-safe**: Full TypeScript support with generic job payloads -- **Multiple adapters**: Drizzle ORM (PostgreSQL), Prisma ORM (PostgreSQL), Kysely (PostgreSQL) and in-memory implementations +- **Multiple adapters**: Drizzle ORM, Prisma ORM, Kysely, ZenStack, TypeORM, MikroORM, Sequelize (all PostgreSQL) - **Priority queues**: Numeric priority system (lower = higher priority) - **Delayed jobs**: Schedule jobs for future execution - **Recurring jobs**: Cron expressions and interval-based repetition -- **Batch processing**: Process multiple jobs concurrently for higher efficiency and better performance by supporting parallel execution and grouping of jobs. +- **Batch processing**: Process multiple jobs concurrently with grouping support +- **Flow producer**: Define parent/child job dependencies - **UTC-first timezone support**: Reliable timezone handling with UTC storage - **Progress tracking**: Real-time job progress updates - **Event system**: Listen to job lifecycle events - **Graceful shutdown**: Clean job processing termination - -## Repository Structure - -``` -. -├── packages/ -│ ├── core/ # Core queue logic and interfaces -│ ├── adapter-drizzle/ # Drizzle ORM adapter (PostgreSQL) -│ └── adapter-prisma/ # Prisma ORM adapter (PostgreSQL) -│ └── adapter-kysely/ # Kysely adapter (PostgreSQL) -├── examples/ # Standalone usage examples -└── tooling/ # Shared development tools -``` +- **GraphQL API**: Built-in GraphQL server for queue management and monitoring + +## Packages + +| Package | Description | +| --- | --- | +| [`@vorsteh-queue/core`](./packages/core) | Core queue engine and interfaces | +| [`@vorsteh-queue/adapter-drizzle`](./packages/adapter-drizzle) | Drizzle ORM adapter (PostgreSQL) | +| [`@vorsteh-queue/adapter-prisma`](./packages/adapter-prisma) | Prisma ORM adapter (PostgreSQL) | +| [`@vorsteh-queue/adapter-kysely`](./packages/adapter-kysely) | Kysely adapter (PostgreSQL) | +| [`@vorsteh-queue/adapter-zenstack`](./packages/adapter-zenstack) | ZenStack ORM adapter (PostgreSQL) | +| [`@vorsteh-queue/adapter-typeorm`](./packages/adapter-typeorm) | TypeORM adapter (PostgreSQL) | +| [`@vorsteh-queue/adapter-mikroorm`](./packages/adapter-mikroorm) | MikroORM adapter (PostgreSQL) | +| [`@vorsteh-queue/adapter-sequelize`](./packages/adapter-sequelize) | Sequelize adapter (PostgreSQL) | +| [`@vorsteh-queue/server`](./packages/server) | GraphQL API server for queue management | +| [`create-vorsteh-queue`](./packages/create-vorsteh-queue) | CLI scaffolding tool | ## Quick Start -### Requirements - -- **Node.js 20+** -- **ESM only** - This package is ESM-only and cannot be imported with `require()` +```bash +pnpm dlx create-vorsteh-queue my-queue-app +``` -### Installation +Or install manually: ```bash -npm install @vorsteh-queue/core @vorsteh-queue/adapter-drizzle -# or pnpm add @vorsteh-queue/core @vorsteh-queue/adapter-drizzle ``` -> **Note**: Make sure your project has `"type": "module"` in package.json or use `.mjs` file extensions. - -### Basic Usage - ```typescript -// Drizzle ORM with PostgreSQL - -// Prisma ORM with PostgreSQL -import { PrismaClient } from "@prisma/client" import { drizzle } from "drizzle-orm/node-postgres" import { Pool } from "pg" import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" -import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" import { Queue } from "@vorsteh-queue/core" -interface EmailPayload { - to: string - subject: string - body: string -} - -interface EmailResult { - messageId: string - sent: boolean -} - const pool = new Pool({ connectionString: "postgresql://..." }) const db = drizzle(pool) const queue = new Queue(new PostgresQueueAdapter(db), { name: "my-queue" }) -const prisma = new PrismaClient() -const queue = new Queue(new PostgresPrismaQueueAdapter(prisma), { name: "my-queue" }) - -// Register job handlers -queue.register("send-email", async (job) => { - console.log(`Sending email to ${job.payload.to}`) - - // Send email logic here +queue.register("send-email", async (job) => { await sendEmail(job.payload) - - // Return result - will be stored in job.result field - return { - messageId: "msg_123", - sent: true, - } + return { sent: true } }) -// Add jobs -await queue.add("send-email", { - to: "user@example.com", - subject: "Welcome!", - body: "Welcome to our service!", -}) -await queue.add( - "send-email", - { to: "admin@example.com", subject: "Report" }, - { - priority: 1, // Higher priority - delay: 5000, // Delay 5 seconds - }, -) +await queue.add("send-email", { to: "user@example.com", subject: "Welcome!" }) -// Start processing queue.start() ``` -## Examples - -Check out the [examples directory](./examples/) for complete, runnable examples. - -> **Note**: All examples demonstrate the UTC-first timezone approach and automatic job cleanup features. - -## Priority System - -Jobs are processed by priority (lower number = higher priority): - -```typescript -await queue.add("urgent-task", payload, { priority: 1 }) // Processed first -await queue.add("normal-task", payload, { priority: 2 }) // Default priority -await queue.add("low-task", payload, { priority: 3 }) // Processed last -``` - -## Recurring Jobs - -```typescript -// Cron expression -await queue.add("daily-report", payload, { - cron: "0 9 * * *", // Every day at 9 AM -}) - -// Interval with limit -await queue.add("health-check", payload, { - repeat: { every: 30000, limit: 10 }, // Every 30s, 10 times -}) -``` - -## Batch Processing - -Process multiple jobs in a single batch for higher throughput and efficiency. - -```typescript -queue.registerBatch<{ file: string }, { ok: boolean }>("process-files", async (jobs) => { - console.log(`Processing batch of ${jobs.length} files...`) - return jobs.map(() => ({ ok: true })) -}) - -await queue.addJobs("process-files", [{ file: "a.csv" }, { file: "b.csv" }, { file: "c.csv" }]) - -queue.on("batch:processing", (jobs) => { - console.log(`Batch started: ${jobs.length} jobs`) -}) -queue.on("batch:completed", (jobs) => { - console.log(`Batch completed: ${jobs.length} jobs`) -}) -``` - -## Job Cleanup - -```typescript -// Automatic cleanup configuration -const queue = new Queue(adapter, { - name: "my-queue", - removeOnComplete: true, // Remove completed jobs immediately - removeOnFail: false, // Keep failed jobs for debugging - // Or use numbers to keep N jobs - removeOnComplete: 100, // Keep last 100 completed jobs - removeOnFail: 50, // Keep last 50 failed jobs -}) -``` - -## Timezone Support - -Vorsteh Queue uses a **UTC-first approach** for reliable timezone handling: - -```typescript -// Schedule job for 9 AM New York time - converted to UTC immediately -await queue.add("daily-report", payload, { - cron: "0 9 * * *", - timezone: "America/New_York", // Timezone used for conversion only -}) - -// Schedule job for specific time in Tokyo - stored as UTC -await queue.add("notification", payload, { - runAt: new Date("2024-01-15T10:00:00"), - timezone: "Asia/Tokyo", // Interprets runAt in Tokyo time -}) - -// Complex cron with timezone - result always UTC -await queue.add("business-task", payload, { - cron: "*/15 9-17 * * 1-5", - timezone: "Europe/London", // Business hours in London time -}) -``` - -### How It Works - -1. **Timezone conversion happens at job creation** - not at execution -2. **All timestamps stored in database are UTC** - no timezone ambiguity -3. **Recurring jobs recalculate using original timezone** - maintains accuracy -4. **Simple and predictable** - no runtime timezone complexity -5. **Server timezone independent** - works consistently across environments - -## Job Results - -Job handlers can return results that are automatically stored and made available: - -```typescript -interface ProcessResult { - processed: number - errors: string[] - duration: number -} +## Requirements -queue.register<{ items: string[] }, ProcessResult>("process-data", async (job) => { - const startTime = Date.now() - const errors: string[] = [] - let processed = 0 +- Node.js 22+ +- PostgreSQL 12+ +- ESM only (`"type": "module"` in package.json) - for (const item of job.payload.items) { - try { - await processItem(item) - processed++ - } catch (error) { - errors.push(`Failed to process ${item}: ${error.message}`) - } - } +## Documentation - // Return result - automatically stored in job.result field - return { - processed, - errors, - duration: Date.now() - startTime, - } -}) - -// Access results in events -queue.on("job:completed", (job) => { - const result = job.result as ProcessResult - console.log(`Processed ${result.processed} items in ${result.duration}ms`) - if (result.errors.length > 0) { - console.warn(`Errors: ${result.errors.join(", ")}`) - } -}) -``` - -## Progress Tracking - -```typescript -// Register a job that reports progress -queue.register("process-data", async (job) => { - const items = job.payload.items - - for (let i = 0; i < items.length; i++) { - // Process item - await processItem(items[i]) - - // Update progress (0-100) - const progress = Math.round(((i + 1) / items.length) * 100) - await job.updateProgress(progress) - } - - return { processed: items.length } -}) - -// Listen to progress updates -queue.on("job:progress", (job) => { - console.log(`Job ${job.name}: ${job.progress}% complete`) -}) -``` - -## Event System - -```typescript -// Listen to all job lifecycle events -queue.on("job:added", (job) => { - console.log(`✅ Job ${job.name} added to queue`) -}) - -queue.on("job:processing", (job) => { - console.log(`⚡ Job ${job.name} started processing`) -}) - -queue.on("job:completed", (job) => { - console.log(`🎉 Job ${job.name} completed successfully`) - console.log(`📊 Result:`, job.result) // Access job result -}) - -queue.on("job:failed", (job) => { - console.error(`❌ Job ${job.name} failed: ${job.error}`) -}) - -queue.on("job:retried", (job) => { - console.log(`🔄 Job ${job.name} retrying (attempt ${job.attempts})`) -}) - -// Queue-level events -queue.on("queue:paused", () => { - console.log("⏸️ Queue paused") -}) - -queue.on("queue:resumed", () => { - console.log("▶️ Queue resumed") -}) -``` - -## Architecture - -### UTC-First Design - -- **Database storage**: All timestamps stored as UTC -- **Timezone conversion**: Happens at job creation time -- **No timezone fields**: Database schema contains no timezone columns -- **Predictable behavior**: Same results across different server timezones - -### Adapter Pattern - -- **Pluggable storage**: Easy to add new database adapters -- **Consistent interface**: Same API across all adapters -- **Type safety**: Full TypeScript support throughout +See the [Vorsteh Queue Documentation](https://vorsteh-queue.dev/docs) for detailed guides, API reference, and examples. ## Development -This project was developed with AI assistance, combining human expertise with AI-powered code generation to accelerate development while maintaining high code quality standards. - ```bash -# Install dependencies -pnpm install - -# Autofix format issues -pnpm format:fix - -# Fix issues in the `package.json` ( order, version mismatch ) -pnpm lint:ws -f - -# Lint -pnpm lint - -# Type check -pnpm typecheck - -# Run all tests -pnpm test - -# Build packages -pnpm build +pnpm install # Install dependencies +pnpm dev # Start dev mode +pnpm build # Build all packages +pnpm typecheck # Type check +pnpm lint:fix # Lint and auto-fix +pnpm format:fix # Format code +pnpm test # Run tests (watch) +vitest --run # Run tests (single run) ``` -## Contributing - -Contributions are welcome! Please read our [contributing guidelines](./CONTRIBUTING.md) and submit pull requests to our repository. - ## License -MIT License - see [LICENSE](./LICENSE) file for details. +[MIT](./LICENSE) diff --git a/_oxlint.config.ts b/_oxlint.config.ts new file mode 100644 index 00000000..1db06e3d --- /dev/null +++ b/_oxlint.config.ts @@ -0,0 +1,704 @@ +import { defineConfig } from "oxlint" +import next from "ultracite/oxlint/next" +import react from "ultracite/oxlint/react" +import vitest from "ultracite/oxlint/vitest" + +export default defineConfig({ + env: { + browser: true, + }, + extends: [vitest, react, next], + ignorePatterns: [ + "**/node_modules", + "**/.git", + "**/dist", + "**/build", + "**/out", + "**/.next", + "**/.nuxt", + "**/.output", + "**/.turbo", + "**/.vercel", + "**/.cache", + "**/.vite", + "**/storybook-static", + "**/_generated", + "**/*.gen.*", + "**/*.generated.*", + "**/*.auto.*", + "**/generated", + "**/__generated__", + "**/*.d.ts.map", + "**/.yarn", + "**/coverage", + "**/bun.lock", + "**/bun.lockb", + "**/package-lock.json", + "**/yarn.lock", + "**/pnpm-lock.yaml", + "**/next-env.d.ts", + // Project-specific + "apps/docs/src/components/beautiful-mermaid/**", + "packages/server/src/ui/routeTree.gen.ts", + "packages/server/src/ui/graphql-env.d.ts", + ], + overrides: [ + { + files: [ + "**/*.{test,spec}.{ts,tsx,js,jsx}", + "**/__tests__/**/*.{ts,tsx,js,jsx}", + ], + rules: { + "no-empty-function": "off", + "promise/prefer-await-to-then": "off", + }, + }, + { + files: ["examples/**/*.ts"], + rules: { + "no-console": "off", + "no-restricted-properties": "off", + "no-await-in-loop": "off", + "no-plusplus": "off", + "no-promise-executor-return": "off", + "promise/avoid-new": "off", + "promise/prefer-await-to-then": "off", + "promise/prefer-await-to-callbacks": "off", + "react-doctor/async-parallel": "off", + "react-doctor/async-await-in-loop": "off", + "react-doctor/async-defer-await": "off", + "react-doctor/server-sequential-independent-await": "off", + }, + }, + { + files: [ + "apps/docs/src/app/**/*.{ts,tsx}", + "packages/cli/src/**/*.ts", + "packages/query-builder/src/**/*.ts", + "packages/trino-client/src/**/*.ts", + ], + rules: { + "no-use-before-define": [ + "error", + { + allowNamedExports: true, + functions: false, + ignoreTypeReferences: true, + }, + ], + }, + }, + ], + plugins: [ + "eslint", + "typescript", + "unicorn", + "oxc", + "import", + "jsdoc", + "node", + "promise", + "react", + ], + rules: { + // ── eslint ────────────────────────────────────────────────────────────── + "accessor-pairs": "error", + "array-callback-return": "error", + "arrow-body-style": ["error", "as-needed"], + "block-scoped-var": "error", + "capitalized-comments": "off", + "class-methods-use-this": "error", + complexity: "error", + "constructor-super": "error", + curly: "error", + "default-case": "error", + "default-case-last": "error", + "default-param-last": "error", + eqeqeq: "error", + "for-direction": "error", + "func-name-matching": "error", + "func-names": "error", + "func-style": "off", + "getter-return": "error", + "grouped-accessor-pairs": "error", + "guard-for-in": "error", + "id-length": "off", + "id-match": "error", + "init-declarations": "off", + "logical-assignment-operators": "error", + "max-classes-per-file": "error", + "max-depth": "off", + "max-lines": "off", + "max-lines-per-function": "off", + "max-nested-callbacks": "error", + "max-params": "off", + "max-statements": "off", + "new-cap": "off", + "no-alert": "error", + "no-array-constructor": "error", + "no-async-promise-executor": "error", + "no-await-in-loop": "error", + "no-bitwise": "error", + "no-caller": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-binary-expression": "error", + "no-constant-condition": "error", + "no-console": "error", + "no-constructor-return": "error", + "no-continue": "off", + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-div-regex": "error", + "no-dupe-class-members": "error", + "no-dupe-else-if": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-duplicate-imports": ["error", { allowSeparateTypeImports: true }], + "no-else-return": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-empty-function": "error", + "no-empty-pattern": "error", + "no-empty-static-block": "error", + "no-eq-null": "error", + "no-eval": "error", + "no-ex-assign": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-boolean-cast": "error", + "no-extra-label": "error", + "no-fallthrough": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-implicit-coercion": "off", + "no-implicit-globals": "error", + "no-implied-eval": "error", + "no-import-assign": "error", + "no-inline-comments": "off", + "no-inner-declarations": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-lonely-if": "error", + "no-loop-func": "error", + "no-loss-of-precision": "error", + "no-magic-numbers": "off", + "no-misleading-character-class": "error", + "no-multi-assign": "error", + "no-multi-str": "error", + "no-negated-condition": "error", + "no-nested-ternary": "off", + "no-new": "error", + "no-new-func": "error", + "no-new-native-nonconstructor": "error", + "no-new-wrappers": "error", + "no-nonoctal-decimal-escape": "error", + "no-obj-calls": "error", + "no-object-constructor": "error", + "no-param-reassign": "error", + "no-plusplus": "error", + "no-promise-executor-return": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-restricted-globals": "error", + "no-restricted-imports": [ + "error", + { + importNames: ["env"], + message: + "Use `import { env } from '~/env'` instead to ensure validated types.", + name: "process", + }, + ], + "no-restricted-properties": [ + "error", + { + message: + "Use `import { env } from '~/env'` instead to ensure validated types.", + object: "process", + property: "env", + }, + ], + "no-return-assign": "error", + "no-script-url": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-setter-return": "error", + "no-shadow": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-template-curly-in-string": "error", + "no-ternary": "off", + "no-this-before-super": "error", + "no-throw-literal": "error", + "no-unassigned-vars": "error", + "no-undefined": "off", + "no-underscore-dangle": "off", + "no-unexpected-multiline": "error", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": "error", + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unsafe-optional-chaining": "error", + "no-unused-expressions": "error", + "no-unused-labels": "error", + "no-unused-private-class-members": "error", + "no-unused-vars": "error", + "no-use-before-define": "off", + "no-useless-backreference": "error", + "no-useless-call": "error", + "no-useless-catch": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + "no-useless-escape": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-var": "error", + "no-void": ["error", { allowAsStatement: true }], + "no-warning-comments": "error", + "no-with": "error", + "object-shorthand": "error", + "operator-assignment": "error", + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "error", + "prefer-exponentiation-operator": "error", + "prefer-named-capture-group": "error", + "prefer-numeric-literals": "error", + "prefer-object-has-own": "error", + "prefer-object-spread": "error", + "prefer-promise-reject-errors": "error", + "prefer-regex-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "preserve-caught-error": "error", + radix: "error", + "require-await": "off", + "require-unicode-regexp": "error", + "require-yield": "error", + "sort-imports": "off", + "sort-keys": "off", + "sort-vars": "error", + "symbol-description": "error", + "unicode-bom": "error", + "use-isnan": "error", + "valid-typeof": "error", + "vars-on-top": "error", + yoda: "error", + + // ── import ────────────────────────────────────────────────────────────── + "import/consistent-type-specifier-style": ["error", "prefer-top-level"], + "import/default": "error", + "import/exports-last": "off", + "import/extensions": "off", + "import/first": "error", + "import/group-exports": "off", + "import/max-dependencies": "off", + "import/namespace": "error", + "import/newline-after-import": "error", + "import/no-absolute-path": "error", + "import/no-amd": "error", + "import/no-anonymous-default-export": "off", + "import/no-commonjs": "off", + "import/no-cycle": "error", + "import/no-default-export": "off", + "import/no-duplicates": "error", + "import/no-dynamic-require": "off", + "import/no-empty-named-blocks": "error", + "import/no-mutable-exports": "error", + "import/no-named-as-default": "error", + "import/no-named-as-default-member": "error", + "import/no-named-default": "error", + "import/no-named-export": "off", + "import/no-namespace": "off", + "import/no-nodejs-modules": "off", + "import/no-relative-parent-imports": "off", + "import/no-self-import": "error", + "import/no-unassigned-import": "off", + "import/no-webpack-loader-syntax": "error", + "import/prefer-default-export": "off", + "import/unambiguous": "off", + + // ── jsdoc ─────────────────────────────────────────────────────────────── + "jsdoc/check-access": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/empty-tags": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/no-defaults": "error", + "jsdoc/require-param": "off", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-property": "error", + "jsdoc/require-property-description": "error", + "jsdoc/require-property-name": "error", + "jsdoc/require-property-type": "error", + "jsdoc/require-returns": "off", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-throws-description": "error", + "jsdoc/require-throws-type": "error", + "jsdoc/require-yields": "error", + "jsdoc/require-yields-description": "error", + "jsdoc/require-yields-type": "error", + + // ── node ──────────────────────────────────────────────────────────────── + "node/callback-return": "error", + "node/global-require": "error", + "node/handle-callback-err": "error", + "node/no-exports-assign": "error", + "node/no-mixed-requires": "error", + "node/no-new-require": "error", + "node/no-path-concat": "error", + "node/no-process-env": "off", + "node/no-sync": "off", + + // ── oxc ───────────────────────────────────────────────────────────────── + "oxc/approx-constant": "error", + "oxc/bad-array-method-on-arguments": "error", + "oxc/bad-bitwise-operator": "error", + "oxc/bad-char-at-comparison": "error", + "oxc/bad-comparison-sequence": "error", + "oxc/bad-min-max-func": "error", + "oxc/bad-object-literal-comparison": "error", + "oxc/bad-replace-all-arg": "error", + "oxc/branches-sharing-code": "error", + "oxc/const-comparisons": "error", + "oxc/double-comparisons": "error", + "oxc/erasing-op": "error", + "oxc/misrefactored-assign-op": "error", + "oxc/missing-throw": "error", + "oxc/no-accumulating-spread": "error", + "oxc/no-async-await": "off", + "oxc/no-async-endpoint-handlers": "error", + "oxc/no-barrel-file": "error", + "oxc/no-const-enum": "error", + "oxc/no-map-spread": "off", + "oxc/no-optional-chaining": "off", + "oxc/no-rest-spread-properties": "off", + "oxc/no-this-in-exported-function": "error", + "oxc/number-arg-out-of-range": "error", + "oxc/only-used-in-recursion": "error", + "oxc/uninvoked-array-callback": "error", + + // ── promise ───────────────────────────────────────────────────────────── + "promise/always-return": "off", + "promise/avoid-new": "error", + "promise/catch-or-return": "off", + "promise/no-callback-in-promise": "error", + "promise/no-multiple-resolved": "error", + "promise/no-nesting": "error", + "promise/no-new-statics": "error", + "promise/no-promise-in-callback": "error", + "promise/no-return-wrap": "error", + "promise/param-names": "error", + "promise/prefer-await-to-callbacks": "error", + "promise/prefer-await-to-then": "error", + "promise/prefer-catch": "error", + "promise/spec-only": "error", + "promise/valid-params": "error", + + // ── typescript ────────────────────────────────────────────────────────── + "typescript/adjacent-overload-signatures": "error", + "typescript/array-type": "error", + "typescript/await-thenable": "error", + "typescript/ban-ts-comment": "error", + "typescript/ban-tslint-comment": "error", + "typescript/ban-types": "error", + "typescript/class-literal-property-style": "error", + "typescript/consistent-generic-constructors": "error", + "typescript/consistent-indexed-object-style": "error", + "typescript/consistent-return": "error", + "typescript/consistent-type-assertions": "error", + "typescript/consistent-type-definitions": "off", + "typescript/consistent-type-exports": "error", + "typescript/consistent-type-imports": "error", + "typescript/default-param-last": "error", + "typescript/explicit-function-return-type": "off", + "typescript/explicit-member-accessibility": "off", + "typescript/explicit-module-boundary-types": "off", + "typescript/init-declarations": "off", + "typescript/max-params": "off", + "typescript/method-signature-style": "error", + + "typescript/no-confusing-non-null-assertion": "error", + "typescript/no-confusing-void-expression": "error", + "typescript/no-deprecated": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-duplicate-type-constituents": "error", + "typescript/no-dynamic-delete": "error", + "typescript/no-empty-function": "error", + "typescript/no-empty-interface": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-extraneous-class": "error", + "typescript/no-floating-promises": "error", + "typescript/no-for-in-array": "error", + "typescript/no-import-type-side-effects": "error", + "typescript/no-inferrable-types": "error", + "typescript/no-invalid-void-type": "error", + "typescript/no-meaningless-void-operator": "error", + "typescript/no-misused-new": "error", + "typescript/no-misused-promises": "error", + "typescript/no-mixed-enums": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-nullish-coalescing": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-non-null-assertion": "error", + "typescript/no-redeclare": "error", + "typescript/no-redundant-type-constituents": "error", + "typescript/no-require-imports": "error", + "typescript/no-restricted-types": "error", + "typescript/no-shadow": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-boolean-literal-compare": "error", + "typescript/no-unnecessary-condition": "error", + "typescript/no-unnecessary-qualifier": "error", + "typescript/no-unnecessary-template-expression": "error", + "typescript/no-unnecessary-type-arguments": "error", + "typescript/no-unnecessary-type-assertion": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unnecessary-type-parameters": "error", + "typescript/no-unsafe-argument": "error", + "typescript/no-unsafe-assignment": "error", + "typescript/no-unsafe-call": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-unsafe-enum-comparison": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-unsafe-member-access": "error", + "typescript/no-unsafe-return": "error", + "typescript/no-unsafe-unary-minus": "error", + "typescript/no-unused-expressions": "error", + "typescript/no-unused-vars": "error", + "typescript/no-use-before-define": "off", + "typescript/no-useless-constructor": "error", + "typescript/no-useless-empty-export": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/non-nullable-type-assertion-style": "error", + "typescript/only-throw-error": "error", + "typescript/parameter-properties": "off", + "typescript/prefer-as-const": "error", + "typescript/prefer-enum-initializers": "error", + "typescript/prefer-find": "error", + "typescript/prefer-for-of": "error", + "typescript/prefer-function-type": "error", + "typescript/prefer-includes": "error", + "typescript/prefer-literal-enum-member": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/prefer-nullish-coalescing": "error", + "typescript/prefer-optional-chain": "error", + "typescript/prefer-readonly": "error", + "typescript/prefer-reduce-type-parameter": "error", + "typescript/prefer-regexp-exec": "error", + "typescript/prefer-return-this-type": "error", + "typescript/prefer-string-starts-ends-with": "error", + "typescript/prefer-ts-expect-error": "error", + "typescript/promise-function-async": "off", + "typescript/require-array-sort-compare": "error", + "typescript/restrict-plus-operands": "error", + "typescript/restrict-template-expressions": "error", + "typescript/return-await": "error", + "typescript/strict-boolean-expressions": "off", + "typescript/switch-exhaustiveness-check": "error", + "typescript/triple-slash-reference": "error", + + "typescript/unified-signatures": "error", + "typescript/use-unknown-in-catch-callback-variable": "error", + + // ── unicorn ───────────────────────────────────────────────────────────── + + "unicorn/catch-error-name": "error", + "unicorn/consistent-date-clone": "error", + + "unicorn/consistent-empty-array-spread": "error", + "unicorn/consistent-existence-index-check": "error", + "unicorn/consistent-function-scoping": "error", + "unicorn/custom-error-definition": "error", + "unicorn/error-message": "error", + "unicorn/escape-case": "error", + + "unicorn/explicit-length-check": "off", + "unicorn/filename-case": "error", + "unicorn/import-style": "error", + "unicorn/max-nested-calls": "off", + "unicorn/new-for-builtins": "error", + "unicorn/no-abusive-eslint-disable": "error", + "unicorn/no-accessor-recursion": "error", + "unicorn/no-anonymous-default-export": "error", + "unicorn/no-array-callback-reference": "off", + "unicorn/no-array-fill-with-reference-type": "error", + "unicorn/no-array-for-each": "error", + "unicorn/no-array-method-this-argument": "error", + "unicorn/no-array-reduce": "error", + "unicorn/no-array-reverse": "error", + "unicorn/no-array-sort": "error", + "unicorn/no-await-expression-member": "error", + "unicorn/no-await-in-promise-methods": "error", + "unicorn/no-console-spaces": "error", + "unicorn/no-document-cookie": "error", + "unicorn/no-empty-file": "error", + "unicorn/no-hex-escape": "error", + "unicorn/no-immediate-mutation": "error", + "unicorn/no-instanceof-array": "error", + "unicorn/no-instanceof-builtins": "error", + "unicorn/no-invalid-fetch-options": "error", + "unicorn/no-invalid-remove-event-listener": "error", + "unicorn/no-length-as-slice-end": "error", + "unicorn/no-lonely-if": "error", + "unicorn/no-magic-array-flat-depth": "error", + "unicorn/no-negated-condition": "error", + "unicorn/no-negation-in-equality-check": "error", + "unicorn/no-nested-ternary": "off", + "unicorn/no-new-array": "error", + "unicorn/no-new-buffer": "error", + "unicorn/no-null": "off", + "unicorn/no-object-as-default-parameter": "error", + "unicorn/no-process-exit": "off", + "unicorn/no-single-promise-in-promise-methods": "error", + "unicorn/no-static-only-class": "error", + "unicorn/no-thenable": "error", + "unicorn/no-this-assignment": "error", + "unicorn/no-typeof-undefined": "error", + "unicorn/no-unnecessary-array-flat-depth": "error", + "unicorn/no-unnecessary-array-splice-count": "error", + "unicorn/no-unnecessary-await": "error", + "unicorn/no-unnecessary-slice-end": "error", + "unicorn/no-unreadable-array-destructuring": "error", + "unicorn/no-unreadable-iife": "error", + "unicorn/no-useless-collection-argument": "error", + "unicorn/no-useless-error-capture-stack-trace": "error", + "unicorn/no-useless-fallback-in-spread": "error", + "unicorn/no-useless-length-check": "error", + "unicorn/no-useless-promise-resolve-reject": "error", + "unicorn/no-useless-spread": "error", + "unicorn/no-useless-switch-case": "error", + "unicorn/no-useless-undefined": "error", + "unicorn/no-zero-fractions": "error", + "unicorn/number-literal-case": "off", + "unicorn/numeric-separators-style": "error", + "unicorn/prefer-add-event-listener": "error", + "unicorn/prefer-array-find": "error", + "unicorn/prefer-array-flat": "error", + "unicorn/prefer-array-flat-map": "error", + "unicorn/prefer-array-index-of": "error", + "unicorn/prefer-array-some": "error", + "unicorn/prefer-at": "error", + "unicorn/prefer-bigint-literals": "error", + "unicorn/prefer-blob-reading-methods": "error", + "unicorn/prefer-class-fields": "error", + "unicorn/prefer-classlist-toggle": "error", + "unicorn/prefer-code-point": "error", + "unicorn/prefer-date-now": "error", + "unicorn/prefer-default-parameters": "error", + "unicorn/prefer-dom-node-append": "error", + "unicorn/prefer-dom-node-dataset": "error", + "unicorn/prefer-dom-node-remove": "error", + "unicorn/prefer-dom-node-text-content": "error", + "unicorn/prefer-event-target": "error", + "unicorn/prefer-export-from": "warn", + "unicorn/prefer-global-this": "off", + "unicorn/prefer-import-meta-properties": "error", + "unicorn/prefer-includes": "error", + "unicorn/prefer-keyboard-event-key": "error", + "unicorn/prefer-logical-operator-over-ternary": "error", + "unicorn/prefer-math-min-max": "error", + "unicorn/prefer-math-trunc": "error", + "unicorn/prefer-modern-dom-apis": "error", + "unicorn/prefer-modern-math-apis": "error", + "unicorn/prefer-module": "error", + "unicorn/prefer-native-coercion-functions": "error", + "unicorn/prefer-negative-index": "error", + "unicorn/prefer-node-protocol": "error", + "unicorn/prefer-number-coercion": "warn", + "unicorn/prefer-number-properties": "error", + "unicorn/prefer-object-from-entries": "error", + "unicorn/prefer-optional-catch-binding": "error", + "unicorn/prefer-prototype-methods": "error", + "unicorn/prefer-query-selector": "error", + "unicorn/prefer-reflect-apply": "error", + "unicorn/prefer-regexp-test": "error", + "unicorn/prefer-response-static-json": "error", + "unicorn/prefer-set-has": "error", + "unicorn/prefer-set-size": "error", + "unicorn/prefer-single-call": "error", + "unicorn/prefer-spread": "error", + "unicorn/prefer-string-raw": "off", + "unicorn/prefer-string-replace-all": "error", + "unicorn/prefer-string-slice": "error", + "unicorn/prefer-string-starts-ends-with": "error", + "unicorn/prefer-string-trim-start-end": "error", + "unicorn/prefer-structured-clone": "error", + "unicorn/prefer-ternary": "error", + "unicorn/prefer-top-level-await": "off", + "unicorn/prefer-type-error": "error", + "unicorn/relative-url-style": "error", + "unicorn/require-array-join-separator": "error", + "unicorn/require-module-attributes": "error", + "unicorn/require-module-specifiers": "error", + "unicorn/require-number-to-fixed-digits-argument": "error", + "unicorn/require-post-message-target-origin": "error", + "unicorn/switch-case-braces": "error", + "unicorn/switch-case-break-position": "error", + "unicorn/text-encoding-identifier-case": ["error", { withDash: true }], + "unicorn/throw-new-error": "error", + + // ── react (project-level overrides) ───────────────────────────────────── + "react/react-compiler": "off", + "react/no-danger": "warn", + "react/no-clone-element": "warn", + "react/no-react-children": "warn", + "react/jsx-pascal-case": "off", + "react/state-in-constructor": "off", + "react/jsx-no-constructed-context-values": "warn", + "react/hook-use-state": "warn", + "react/button-has-type": "warn", + "react/jsx-no-useless-fragment": "warn", + "jsx-a11y/prefer-tag-over-role": "off", + "jsx-a11y/control-has-associated-label": "off", + + // ── react-doctor (project-level overrides) ────────────────────────────── + "react-doctor/react-compiler-no-manual-memoization": "off", + "react-doctor/no-react19-deprecated-apis": "off", + "react-doctor/async-parallel": "warn", + "react-doctor/async-await-in-loop": "warn", + "react-doctor/async-defer-await": "warn", + "react-doctor/server-sequential-independent-await": "warn", + "react-doctor/server-hoist-static-io": "warn", + "react-doctor/js-set-map-lookups": "warn", + "react-doctor/no-giant-component": "warn", + "react-doctor/only-export-components": "warn", + "react-doctor/no-barrel-import": "warn", + "react-doctor/no-polymorphic-children": "warn", + "react-doctor/prefer-module-scope-pure-function": "warn", + "react-doctor/prefer-module-scope-static-value": "warn", + "react-doctor/rerender-state-only-in-handlers": "warn", + "react-doctor/rerender-memo-with-default-value": "warn", + "react-doctor/no-render-in-render": "warn", + "react-doctor/no-cascading-set-state": "warn", + "react-doctor/no-array-index-as-key": "warn", + "react-doctor/no-derived-useState": "warn", + "react-doctor/no-derived-state": "warn", + "react-doctor/no-initialize-state": "warn", + "react-doctor/rendering-hydration-no-flicker": "warn", + "react-doctor/rendering-hydration-mismatch-time": "warn", + "react-doctor/auth-token-in-web-storage": "warn", + "react-doctor/js-flatmap-filter": "warn", + "react-doctor/js-combine-iterations": "warn", + }, +}) diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index f650315f..e6ce5a4c 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -1,7 +1,15 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies /node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage # next.js /.next/ @@ -10,18 +18,19 @@ # production /build +# misc +.DS_Store +*.pem + # debug npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* +pnpm-debug.log* # env files -.env* - -# vercel -.vercel +.env*.local # typescript *.tsbuildinfo -next-env.d.ts \ No newline at end of file +next-env.d.ts diff --git a/apps/docs/README.md b/apps/docs/README.md new file mode 100644 index 00000000..29a87698 --- /dev/null +++ b/apps/docs/README.md @@ -0,0 +1,21 @@ +# Next.js template + +This is a Next.js template with shadcn/ui. + +## Adding components + +To add components to your app, run the following command: + +```bash +npx shadcn@latest add button +``` + +This will place the ui components in the `components` directory. + +## Using components + +To use the components in your app, import them as follows: + +```tsx +import { Button } from "@/components/ui/button" +``` diff --git a/apps/docs/components.json b/apps/docs/components.json index 540fc6e9..028d1561 100644 --- a/apps/docs/components.json +++ b/apps/docs/components.json @@ -1,21 +1,27 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "style": "base-nova", "rsc": true, "tsx": true, "tailwind": { "config": "", - "css": "src/app/globals.css", + "css": "app/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", + "rtl": false, + "menuColor": "default", + "menuAccent": "subtle", "aliases": { - "components": "~/components", - "utils": "~/lib/utils", - "ui": "~/components/ui", - "lib": "~/lib", - "hooks": "~/hooks" + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" }, - "iconLibrary": "lucide" + "registries": { + "@reui": "https://reui.io/r/{style}/{name}.json" + } } diff --git a/apps/docs/content/api-reference/01.core/01.queue.mdx b/apps/docs/content/api-reference/01.core/01.queue.mdx new file mode 100644 index 00000000..adb676e5 --- /dev/null +++ b/apps/docs/content/api-reference/01.core/01.queue.mdx @@ -0,0 +1,9 @@ +--- +title: Queue +description: The main Queue class for managing jobs, handlers, and processing lifecycle. +apiReference: + - name: Queue + file: core/src/queue +--- + +The `Queue` class is the primary interface for adding jobs, registering handlers, and controlling processing. diff --git a/apps/docs/content/api-reference/01.core/02.worker.mdx b/apps/docs/content/api-reference/01.core/02.worker.mdx new file mode 100644 index 00000000..6546d9a4 --- /dev/null +++ b/apps/docs/content/api-reference/01.core/02.worker.mdx @@ -0,0 +1,9 @@ +--- +title: Worker +description: The Worker class handles job execution, retries, timeouts, and batch processing. +apiReference: + - name: Worker + file: core/src/worker +--- + +The `Worker` class is responsible for polling, executing, and managing the lifecycle of jobs. diff --git a/apps/docs/content/api-reference/01.core/03.types.mdx b/apps/docs/content/api-reference/01.core/03.types.mdx new file mode 100644 index 00000000..e1967b21 --- /dev/null +++ b/apps/docs/content/api-reference/01.core/03.types.mdx @@ -0,0 +1,9 @@ +--- +title: Types +description: Core type definitions — Job, JobOptions, QueueConfig, and adapter interfaces. +apiReference: + - name: Types + file: core/src/types.d +--- + +Type definitions for jobs, queue configuration, adapter contracts, and handler signatures. diff --git a/apps/docs/content/api-reference/01.core/04.events.mdx b/apps/docs/content/api-reference/01.core/04.events.mdx new file mode 100644 index 00000000..55cb1796 --- /dev/null +++ b/apps/docs/content/api-reference/01.core/04.events.mdx @@ -0,0 +1,9 @@ +--- +title: Events +description: Typed event emitter and event type definitions for job lifecycle. +apiReference: + - name: TypedEventEmitter + file: core/src/events.d +--- + +The typed event system for subscribing to job and queue lifecycle events. diff --git a/apps/docs/content/api-reference/01.core/05.errors.mdx b/apps/docs/content/api-reference/01.core/05.errors.mdx new file mode 100644 index 00000000..9c27ce07 --- /dev/null +++ b/apps/docs/content/api-reference/01.core/05.errors.mdx @@ -0,0 +1,9 @@ +--- +title: Errors +description: Custom error classes for timeout, duplicate jobs, cancellation, and invalid transitions. +apiReference: + - name: Errors + file: core/src/errors.d +--- + +Error classes thrown by the queue engine during job processing. diff --git a/apps/docs/content/api-reference/01.core/06.adapters.mdx b/apps/docs/content/api-reference/01.core/06.adapters.mdx new file mode 100644 index 00000000..31a26a16 --- /dev/null +++ b/apps/docs/content/api-reference/01.core/06.adapters.mdx @@ -0,0 +1,11 @@ +--- +title: Base Adapters +description: Abstract base class and memory adapter for implementing queue storage backends. +apiReference: + - name: BaseQueueAdapter + file: core/src/adapters/base.d + - name: MemoryQueueAdapter + file: core/src/adapters/memory.d +--- + +The base adapter interface that all storage backends must implement, plus the built-in memory adapter for testing. diff --git a/apps/docs/content/api-reference/01.core/index.mdx b/apps/docs/content/api-reference/01.core/index.mdx new file mode 100644 index 00000000..09cba2e5 --- /dev/null +++ b/apps/docs/content/api-reference/01.core/index.mdx @@ -0,0 +1,5 @@ +--- +title: "@vorsteh-queue/core" +navTitle: Core +description: Core queue engine — Queue, Worker, adapters, events, errors, and type definitions. +--- diff --git a/apps/docs/content/api-reference/index.mdx b/apps/docs/content/api-reference/index.mdx new file mode 100644 index 00000000..b1ef8b90 --- /dev/null +++ b/apps/docs/content/api-reference/index.mdx @@ -0,0 +1,6 @@ +--- +title: API Reference +navTitle: API Reference +description: Auto-generated API reference for all Vorsteh Queue packages. +entrypoint: /docs/api-reference/core/queue +--- diff --git a/apps/docs/content/cli/01.overview/01.installation.mdx b/apps/docs/content/cli/01.overview/01.installation.mdx new file mode 100644 index 00000000..c3395e8b --- /dev/null +++ b/apps/docs/content/cli/01.overview/01.installation.mdx @@ -0,0 +1,174 @@ +--- +title: Installation +description: How to install and configure the Vorsteh Queue CLI for job management and monitoring. +--- + +The Vorsteh Queue CLI (`@vorsteh-queue/cli`) provides commands for inspecting, managing, and monitoring queue jobs from the terminal. + +## Install as a project dependency + +@vorsteh-queue/cli + +## Run without installing + +vorsteh-queue + +## Transport Modes + +The CLI supports two transport modes for communicating with your queue: + +### Direct (default) + +In direct mode, the CLI loads your `queue.config.ts` and uses the configured adapter to interact with the database directly. No running server is required. + +This is the default behavior when no `--url` flag or `VORSTEH_QUEUE_URL` environment variable is provided. + +### Remote + +In remote mode, the CLI connects to a running Vorsteh Queue server via GraphQL. This mode is activated by providing a URL through the `--url` flag or the `VORSTEH_QUEUE_URL` environment variable. + +```bash +# Via flag +vorsteh-queue status --url https://my-server.example.com/graphql + +# Via environment variable +export VORSTEH_QUEUE_URL=https://my-server.example.com/graphql +vorsteh-queue status +``` + +## Configuration + +For direct mode, create a `queue.config.ts` in your project root: + + + + ```typescript + // queue.config.ts + import { defineRelations } from "drizzle-orm" + import { drizzle } from "drizzle-orm/node-postgres" + import { Pool } from "pg" + + import { + PostgresQueueAdapter, + postgresSchema, + } from "@vorsteh-queue/adapter-drizzle" + import { Queue } from "@vorsteh-queue/core" + + const { queueJobs } = postgresSchema + const relations = defineRelations({ queueJobs }) + + const pool = new Pool({ connectionString: process.env.DATABASE_URL }) + const db = drizzle({ client: pool, relations }) + const adapter = new PostgresQueueAdapter(db) + + const emailQueue = new Queue(adapter, { name: "email-queue" }) + const reportQueue = new Queue(adapter, { name: "report-queue" }) + + export default { + adapter, + queues: [emailQueue, reportQueue], + defaultQueue: "email-queue", + } + ``` + + + + ```typescript + // queue.config.ts + import { PrismaClient } from "@prisma/client" + + import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" + import { Queue } from "@vorsteh-queue/core" + + const prisma = new PrismaClient() + const adapter = new PostgresPrismaQueueAdapter(prisma) + + const emailQueue = new Queue(adapter, { name: "email-queue" }) + const reportQueue = new Queue(adapter, { name: "report-queue" }) + + export default { + adapter, + queues: [emailQueue, reportQueue], + defaultQueue: "email-queue", + } + ``` + + + + ```typescript + // queue.config.ts + import { Kysely, PostgresDialect } from "kysely" + import { Pool } from "pg" + + import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-kysely" + import { Queue } from "@vorsteh-queue/core" + + const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), + }), + }) + const adapter = new PostgresQueueAdapter(db) + + const emailQueue = new Queue(adapter, { name: "email-queue" }) + const reportQueue = new Queue(adapter, { name: "report-queue" }) + + export default { + adapter, + queues: [emailQueue, reportQueue], + defaultQueue: "email-queue", + } + ``` + + + + +The `adapter` and `queues` fields are required for direct mode. If either is missing, the CLI will exit with an error. When multiple queues are configured, `defaultQueue` is also required. + +For a single-queue setup, `defaultQueue` is optional (the single queue is used automatically): + +```typescript +export default { + adapter, + queues: [emailQueue], +} +``` + +## Global Options + +These options can be passed to any command: + +| Option | Description | +| --- | --- | +| `--url ` | Remote server GraphQL endpoint. Enables remote mode. | +| `--token ` | Bearer token for authentication with the remote server. | +| `--queue ` | Target queue name. Overrides the default queue from configuration. | + +The `--url` flag takes precedence over the `VORSTEH_QUEUE_URL` environment variable. Likewise, `--token` takes precedence over `VORSTEH_QUEUE_TOKEN`. + +### Queue Resolution + +The `--queue` flag determines which queue a command targets. Resolution priority: + +1. `--queue` flag (highest priority) +2. `defaultQueue` from config +3. Single queue in array (when only one queue is configured) + +In remote mode (`--url` without a local config file), the `--queue` flag is required. + +## Environment Variables + +| Variable | Description | +| --- | --- | +| `VORSTEH_QUEUE_URL` | Remote server GraphQL endpoint. Enables remote mode when set. | +| `VORSTEH_QUEUE_TOKEN` | Bearer token for authentication with the remote server. | + +## Troubleshooting + +Use the `doctor` command to diagnose configuration and connectivity issues: + +```bash +vorsteh-queue doctor +``` + +The doctor command displays your current transport mode, adapter or endpoint details, authentication status, and verifies connectivity. It exits with code 0 on success or 1 if there is a problem. diff --git a/apps/docs/content/cli/01.overview/index.mdx b/apps/docs/content/cli/01.overview/index.mdx new file mode 100644 index 00000000..87ec9b8b --- /dev/null +++ b/apps/docs/content/cli/01.overview/index.mdx @@ -0,0 +1,4 @@ +--- +title: Overview +description: Overview of the Vorsteh Queue CLI tool. +--- diff --git a/apps/docs/content/cli/02.commands/00.global-options.mdx b/apps/docs/content/cli/02.commands/00.global-options.mdx new file mode 100644 index 00000000..cae3f645 --- /dev/null +++ b/apps/docs/content/cli/02.commands/00.global-options.mdx @@ -0,0 +1,23 @@ +--- +title: Global CLI Options +description: Reference for all available global CLI options with syntax, options, and usage examples. +--- + +All commands support the same global transport and targeting options: + + + +Queue resolution priority: + +1. `--queue` flag +2. `defaultQueue` from `queue.config.ts` +3. Single configured queue (when only one queue exists) + +In remote mode without a local config file, `--queue` is required. + +Examples: + +```bash +vorsteh-queue status --queue email-queue +vorsteh-queue inspect --url https://my-server.example.com/graphql --queue report-queue +``` diff --git a/apps/docs/content/cli/02.commands/01.status.mdx b/apps/docs/content/cli/02.commands/01.status.mdx new file mode 100644 index 00000000..2e382ee7 --- /dev/null +++ b/apps/docs/content/cli/02.commands/01.status.mdx @@ -0,0 +1,36 @@ +--- +title: "vorsteh-queue status" +navTitle: "status" +description: Show queue status overview with job counts per state. +cliCommand: "status" +--- + +Displays a summary of job counts grouped by status. + +## Usage + +```bash +vorsteh-queue status +``` + +``` +ℹ Queue Status: + Pending: 12 + Delayed: 3 + Processing: 2 + Completed: 847 + Failed: 5 + Cancelled: 1 + Dead: 0 + Size: 870 +``` + +## JSON Output + +```bash +vorsteh-queue status --json +``` + +Returns structured JSON for scripting and CI pipelines. + + diff --git a/apps/docs/content/cli/02.commands/02.inspect.mdx b/apps/docs/content/cli/02.commands/02.inspect.mdx new file mode 100644 index 00000000..93008db4 --- /dev/null +++ b/apps/docs/content/cli/02.commands/02.inspect.mdx @@ -0,0 +1,29 @@ +--- +title: "vorsteh-queue inspect" +navTitle: "inspect" +description: Show details of a specific job. +cliCommand: "inspect" +--- + +Displays detailed information about a single job including status, payload, result, and error data. + +## Usage + +```bash +vorsteh-queue inspect +``` + +``` +ℹ Job: abc-123 + Name: send-email + Status: completed + Priority: 2 + Attempts: 1/3 + Progress: 100% + Created: 2025-01-15T10:30:00Z + Process At: 2025-01-15T10:30:00Z + Result: {"sent":true} + Payload: {"to":"user@example.com"} +``` + + diff --git a/apps/docs/content/cli/02.commands/03.cancel.mdx b/apps/docs/content/cli/02.commands/03.cancel.mdx new file mode 100644 index 00000000..04aac173 --- /dev/null +++ b/apps/docs/content/cli/02.commands/03.cancel.mdx @@ -0,0 +1,20 @@ +--- +title: "vorsteh-queue cancel" +navTitle: "cancel" +description: Cancel a pending or delayed job. +cliCommand: "cancel" +--- + +Cancels a job, moving it to the `cancelled` status. Only jobs that are not yet in a terminal state can be cancelled. + +## Usage + +```bash +vorsteh-queue cancel +``` + +```bash +vorsteh-queue cancel --reason "no longer needed" +``` + + diff --git a/apps/docs/content/cli/02.commands/04.retry.mdx b/apps/docs/content/cli/02.commands/04.retry.mdx new file mode 100644 index 00000000..e1f43ab7 --- /dev/null +++ b/apps/docs/content/cli/02.commands/04.retry.mdx @@ -0,0 +1,16 @@ +--- +title: "vorsteh-queue retry" +navTitle: "retry" +description: Retry a failed job by resetting it to pending. +cliCommand: "retry" +--- + +Resets a failed job to `pending` status so it can be processed again. The job must be in `failed` status. + +## Usage + +```bash +vorsteh-queue retry +``` + + diff --git a/apps/docs/content/cli/02.commands/05.redrive.mdx b/apps/docs/content/cli/02.commands/05.redrive.mdx new file mode 100644 index 00000000..0ebbe376 --- /dev/null +++ b/apps/docs/content/cli/02.commands/05.redrive.mdx @@ -0,0 +1,23 @@ +--- +title: "vorsteh-queue redrive" +navTitle: "redrive" +description: Redrive dead jobs back to pending for reprocessing. +cliCommand: "redrive" +--- + +Moves dead jobs (jobs that exhausted all retry attempts) back to `pending` status for another attempt. + +## Usage + +```bash +# Redrive a single job +vorsteh-queue redrive + +# Redrive all dead jobs +vorsteh-queue redrive --all + +# Redrive dead jobs filtered by name +vorsteh-queue redrive --all --name send-email +``` + + diff --git a/apps/docs/content/cli/02.commands/06.run-now.mdx b/apps/docs/content/cli/02.commands/06.run-now.mdx new file mode 100644 index 00000000..b562e695 --- /dev/null +++ b/apps/docs/content/cli/02.commands/06.run-now.mdx @@ -0,0 +1,16 @@ +--- +title: "vorsteh-queue run-now" +navTitle: "run-now" +description: Promote a delayed job to run immediately. +cliCommand: "run-now" +--- + +Promotes a delayed job to `pending` status, making it eligible for immediate processing. The job must be in `delayed` status. + +## Usage + +```bash +vorsteh-queue run-now +``` + + diff --git a/apps/docs/content/cli/02.commands/07.delete.mdx b/apps/docs/content/cli/02.commands/07.delete.mdx new file mode 100644 index 00000000..28c9cae4 --- /dev/null +++ b/apps/docs/content/cli/02.commands/07.delete.mdx @@ -0,0 +1,16 @@ +--- +title: "vorsteh-queue delete" +navTitle: "delete" +description: Delete a single job from the queue. +cliCommand: "delete" +--- + +Permanently removes a job from the queue. This operation cannot be undone. + +## Usage + +```bash +vorsteh-queue delete +``` + + diff --git a/apps/docs/content/cli/02.commands/08.clear.mdx b/apps/docs/content/cli/02.commands/08.clear.mdx new file mode 100644 index 00000000..b5e03dad --- /dev/null +++ b/apps/docs/content/cli/02.commands/08.clear.mdx @@ -0,0 +1,24 @@ +--- +title: "vorsteh-queue clear" +navTitle: "clear" +description: Clear jobs from the queue by status. +cliCommand: "clear" +--- + +Removes multiple jobs from the queue. Use `--status` to target a specific state or `--all` to clear everything. + +## Usage + +```bash +# Clear completed jobs +vorsteh-queue clear --status completed + +# Clear all jobs +vorsteh-queue clear --all +``` + +## Valid Statuses + +`pending`, `delayed`, `processing`, `completed`, `failed`, `cancelled`, `dead` + + diff --git a/apps/docs/content/cli/02.commands/09.flow.mdx b/apps/docs/content/cli/02.commands/09.flow.mdx new file mode 100644 index 00000000..4e658e44 --- /dev/null +++ b/apps/docs/content/cli/02.commands/09.flow.mdx @@ -0,0 +1,25 @@ +--- +title: "vorsteh-queue flow" +navTitle: "flow" +description: Show a flow tree (parent-child job hierarchy). +cliCommand: "flow" +--- + +Displays the parent-child job hierarchy as a tree, useful for debugging flow-based job pipelines. + +## Usage + +```bash +vorsteh-queue flow +``` + +``` +ℹ Flow: process-order [flow-id: flow_abc123] + +process-order (completed) ✓ +├── validate-payment (completed) ✓ +├── reserve-stock (completed) ✓ +└── send-confirmation (processing) ⟳ +``` + + diff --git a/apps/docs/content/cli/02.commands/10.serve.mdx b/apps/docs/content/cli/02.commands/10.serve.mdx new file mode 100644 index 00000000..d2cf8fde --- /dev/null +++ b/apps/docs/content/cli/02.commands/10.serve.mdx @@ -0,0 +1,110 @@ +--- +title: "vorsteh-queue serve" +navTitle: "serve" +description: Start the graphql server +cliCommand: "serve" +--- + +Starts the GraphQL API server. Requires a `queue.config.ts` with a direct adapter configuration. + +## Usage + +```bash +vorsteh-queue serve +``` + +```bash +vorsteh-queue serve --port 4000 +``` + +## Configuration + +The serve command reads from `queue.config.ts`: + + + + ```typescript + // queue.config.ts + import { defineRelations } from "drizzle-orm" + import { drizzle } from "drizzle-orm/node-postgres" + import { Pool } from "pg" + + import { + PostgresQueueAdapter, + postgresSchema, + } from "@vorsteh-queue/adapter-drizzle" + import { Queue } from "@vorsteh-queue/core" + + const { queueJobs } = postgresSchema + const relations = defineRelations({ queueJobs }) + + const pool = new Pool({ connectionString: process.env.DATABASE_URL }) + const db = drizzle({ client: pool, relations }) + const adapter = new PostgresQueueAdapter(db) + + const emailQueue = new Queue(adapter, { name: "email-queue" }) + const reportQueue = new Queue(adapter, { name: "report-queue" }) + + export default { + adapter, + queues: [emailQueue, reportQueue], + defaultQueue: "email-queue", + port: 3000, + } + ``` + + + + ```typescript + // queue.config.ts + import { PrismaClient } from "@prisma/client" + + import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" + import { Queue } from "@vorsteh-queue/core" + + const prisma = new PrismaClient() + const adapter = new PostgresPrismaQueueAdapter(prisma) + + const emailQueue = new Queue(adapter, { name: "email-queue" }) + const reportQueue = new Queue(adapter, { name: "report-queue" }) + + export default { + adapter, + queues: [emailQueue, reportQueue], + defaultQueue: "email-queue", + port: 3000, + } + ``` + + + + ```typescript + // queue.config.ts + import { Kysely, PostgresDialect } from "kysely" + import { Pool } from "pg" + + import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-kysely" + import { Queue } from "@vorsteh-queue/core" + + const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), + }), + }) + const adapter = new PostgresQueueAdapter(db) + + const emailQueue = new Queue(adapter, { name: "email-queue" }) + const reportQueue = new Queue(adapter, { name: "report-queue" }) + + export default { + adapter, + queues: [emailQueue, reportQueue], + defaultQueue: "email-queue", + port: 3000, + } + ``` + + + + + diff --git a/apps/docs/content/cli/02.commands/11.doctor.mdx b/apps/docs/content/cli/02.commands/11.doctor.mdx new file mode 100644 index 00000000..c04d6dff --- /dev/null +++ b/apps/docs/content/cli/02.commands/11.doctor.mdx @@ -0,0 +1,54 @@ +--- +title: "vorsteh-queue doctor" +navTitle: "doctor" +description: Check configuration and connectivity. +cliCommand: "doctor" +--- + +Verifies your CLI configuration and tests connectivity to the queue backend. Useful for troubleshooting connection issues. + +## Usage + +```bash +vorsteh-queue doctor +``` + +### Direct Mode Output + +``` +ℹ Mode: Direct +ℹ Adapter: configured via queue.config.ts +✔ Connection: OK +``` + +### Remote Mode Output + +```bash +vorsteh-queue doctor --url http://localhost:3000/graphql --token my-token +``` + +``` +ℹ Mode: Remote +ℹ Endpoint: http://localhost:3000/graphql +ℹ Authentication: bearer token +✔ Connection: OK +``` + +### Connection Failure + +``` +ℹ Mode: Remote +ℹ Endpoint: http://localhost:3000/graphql +ℹ Authentication: none +✗ Connection: FAILED +✗ Reason: Could not connect to http://localhost:3000/health. Verify the server is running and the URL is correct. +``` + +## Exit Codes + +| Code | Meaning | +| ---- | --------------------------------------------- | +| `0` | Configuration valid and connection successful | +| `1` | Connection failed or configuration error | + + diff --git a/apps/docs/content/cli/02.commands/12.queues.mdx b/apps/docs/content/cli/02.commands/12.queues.mdx new file mode 100644 index 00000000..a4248fc3 --- /dev/null +++ b/apps/docs/content/cli/02.commands/12.queues.mdx @@ -0,0 +1,39 @@ +--- +title: "vorsteh-queue queues" +navTitle: "queues" +description: List all configured queues. +cliCommand: "queues" +--- + +Lists all queues defined in your `queue.config.ts`. Shows each queue name and indicates which one is the default. + +## Usage + +```bash +vorsteh-queue queues +``` + +``` +ℹ Available queues: + email-queue (default) + report-queue +``` + +## JSON Output + +```bash +vorsteh-queue queues --json +``` + +```json +[ + { "name": "email-queue", "isDefault": true }, + { "name": "report-queue", "isDefault": false } +] +``` + +## Notes + +This command reads directly from the config file and does not require a database connection or running server. + + diff --git a/apps/docs/content/cli/02.commands/13.dashboard.mdx b/apps/docs/content/cli/02.commands/13.dashboard.mdx new file mode 100644 index 00000000..c93ce793 --- /dev/null +++ b/apps/docs/content/cli/02.commands/13.dashboard.mdx @@ -0,0 +1,81 @@ +--- +title: "vorsteh-queue dashboard" +navTitle: "dashboard" +description: Interactive TUI dashboard for monitoring and managing jobs +cliCommand: "dashboard" +--- + +Opens an interactive terminal dashboard for real-time queue monitoring and job management. + +## Usage + +```bash +vorsteh-queue dashboard +``` + +```bash +vorsteh-queue dashboard --url http://localhost:3000/graphql --token secret +``` + +```bash +vorsteh-queue dashboard --refresh 5000 +``` + +## Views + +The dashboard provides three views, navigable via keyboard: + +### Stats View + +Displays queue statistics with visual bar chart: + +- Pending, Delayed, Processing, Completed, Failed, Cancelled, Dead counts +- Auto-refreshes at the configured interval + +### Jobs View + +Paginated job list with: + +- Status filter (all, pending, processing, completed, failed, dead, delayed, cancelled) +- Priority and attempt count per job +- Keyboard navigation and pagination + +### Detail View + +Full job inspection with: + +- All job metadata (ID, status, priority, timestamps, progress) +- Payload and result display +- Error details for failed jobs +- Inline actions: Cancel, Retry, Run Now, Delete (with confirmation) + +## Keyboard Navigation + +| View | Key | Action | +| ------ | ------------- | ---------------------- | +| Stats | `j` / `Enter` | Go to Jobs view | +| Jobs | `↑` / `k` | Move cursor up | +| Jobs | `↓` / `j` | Move cursor down | +| Jobs | `←` / `h` | Previous status filter | +| Jobs | `→` / `l` | Next status filter | +| Jobs | `Enter` | Open job detail | +| Jobs | `Ctrl+D` | Next page | +| Jobs | `Ctrl+U` | Previous page | +| Jobs | `g` | Jump to first page | +| Jobs | `G` | Jump to last item | +| Detail | `c` | Cancel job | +| Detail | `r` | Retry job | +| Detail | `n` | Run job now | +| Detail | `d` | Delete job | +| Any | `Esc` | Go back | +| Any | `q` | Quit | + +## Transport + +The dashboard uses the same transport resolution as all other commands: + +1. `--url` flag → connects to remote GraphQL server +2. `VORSTEH_QUEUE_URL` env var → remote mode +3. `queue.config.ts` → direct adapter connection + + diff --git a/apps/docs/content/cli/02.commands/index.mdx b/apps/docs/content/cli/02.commands/index.mdx new file mode 100644 index 00000000..41bf98c0 --- /dev/null +++ b/apps/docs/content/cli/02.commands/index.mdx @@ -0,0 +1,23 @@ +--- +title: Commands +description: Reference for all available CLI commands with syntax, options, and usage examples. +--- + +All commands support the same global transport and targeting options: + + + +Queue resolution priority: + +1. `--queue` flag +2. `defaultQueue` from `queue.config.ts` +3. Single configured queue (when only one queue exists) + +In remote mode without a local config file, `--queue` is required. + +Examples: + +```bash +vorsteh-queue status --queue email-queue +vorsteh-queue inspect --url https://my-server.example.com/graphql --queue report-queue +``` diff --git a/apps/docs/content/cli/index.mdx b/apps/docs/content/cli/index.mdx new file mode 100644 index 00000000..ee00abbf --- /dev/null +++ b/apps/docs/content/cli/index.mdx @@ -0,0 +1,6 @@ +--- +title: Vorsteh Queue CLI +navTitle: CLI +description: Command-line tool for monitoring and managing Vorsteh Queue jobs. +entrypoint: /docs/cli/overview/installation +--- diff --git a/apps/docs/content/docs/01.getting-started/01.introduction.mdx b/apps/docs/content/docs/01.getting-started/01.introduction.mdx deleted file mode 100644 index 0fffb6fe..00000000 --- a/apps/docs/content/docs/01.getting-started/01.introduction.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Introduction -description: Get started with Vorsteh Queue, a powerful TypeScript-first job queue for PostgreSQL. ---- - -Vorsteh Queue is a powerful, type-safe queue engine designed for PostgreSQL 12+. It provides a robust solution for handling background jobs, scheduled tasks, and recurring processes in your Node.js applications. - -## Key Features - -- **Type-safe**: Full TypeScript support with generic job payloads -- **Multiple adapters**: Drizzle ORM (PostgreSQL), Prisma ORM (PostgreSQL), Kysely (PostgreSQL) and in-memory implementations -- **Priority queues**: Numeric priority system (lower = higher priority) -- **Delayed jobs**: Schedule jobs for future execution -- **Recurring jobs**: Cron expressions and interval-based repetition -- **UTC-first timezone support**: Reliable timezone handling with UTC storage -- **Progress tracking**: Real-time job progress updates -- **Event system**: Listen to job lifecycle events -- **Graceful shutdown**: Clean job processing termination - -## Requirements - -- **Node.js 20+** -- **ESM only** - This package is ESM-only -- **PostgreSQL 12+** for production use - -## Architecture - -Vorsteh Queue follows a modular architecture with: - -- **Core package** - Contains the main queue logic and interfaces -- **Adapter packages** - ORM-specific implementations -- **UTC-first design** - All timestamps stored as UTC for consistency -- **Event-driven** - Comprehensive event system for monitoring and debugging diff --git a/apps/docs/content/docs/01.getting-started/02.installation.mdx b/apps/docs/content/docs/01.getting-started/02.installation.mdx deleted file mode 100644 index 5e36346b..00000000 --- a/apps/docs/content/docs/01.getting-started/02.installation.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Installation -description: Learn how to install Vorsteh Queue packages for your preferred ORM adapter. ---- - -## Prerequisites - -Before installing Vorsteh Queue, ensure you have: - -- **Node.js 20+** -- **PostgreSQL 12+** database -- **ESM project** - Add `"type": "module"` to your `package.json` or use `.mjs` file extensions - -## Quick Start with CLI (Recommended) - -The fastest way to get started is using the `create-vorsteh-queue` CLI tool: - -```bash -# Interactive setup -npx create-vorsteh-queue my-queue-app - -# Or with specific options -npx create-vorsteh-queue worker-service \ - --template=drizzle-postgres \ - --package-manager=pnpm \ - --quiet -``` - -### Available Templates - -- **drizzle-pg** - Basic Drizzle ORM with node-postgres -- **drizzle-pglite** - Zero-setup with embedded PostgreSQL -- **drizzle-postgres** - Advanced with recurring jobs -- **event-system** - Comprehensive event monitoring -- **progress-tracking** - Real-time progress updates -- **pm2-workers** - Multi-process workers with PM2 - -## Manual Installation - -If you prefer to add Vorsteh Queue to an existing project: - -### Drizzle ORM (PostgreSQL) - - - @vorsteh-queue/core @vorsteh-queue/adapter-drizzle - - -### Prisma ORM (PostgreSQL) - - - @vorsteh-queue/core @vorsteh-queue/adapter-prisma - - -### Kysely (PostgreSQL) - - - @vorsteh-queue/core @vorsteh-queue/adapter-kysely - - -## Database Setup - -After installation, you'll need to set up the required database tables. Each adapter provides migration scripts or schema definitions: - -- **Drizzle**: Use the provided schema and run migrations -- **Prisma**: Add the schema to your `schema.prisma` and run `prisma migrate` -- **Kysely**: Use the provided schema and run migrations - -Refer to the specific adapter documentation for detailed setup instructions. - -## Verify Installation - -Create a simple test file to verify your installation: - -```typescript -import { Queue } from "@vorsteh-queue/core" - -// Your adapter import will vary based on your choice -console.log("Vorsteh Queue installed successfully!") -``` - -Run the file with Node.js to confirm everything is working correctly. diff --git a/apps/docs/content/docs/02.packages/01.core.mdx b/apps/docs/content/docs/02.packages/01.core.mdx deleted file mode 100644 index f062737b..00000000 --- a/apps/docs/content/docs/02.packages/01.core.mdx +++ /dev/null @@ -1,325 +0,0 @@ ---- -title: Core Package -navTitle: "@vorsteh-queue/core" -description: The main queue engine containing all core functionality for job processing, scheduling, and management. ---- - -The core package contains the main queue engine and all essential functionality for job processing, scheduling, and event management. - -## Installation - - - @vorsteh-queue/core - - -## Key Features - -- **Queue Management** - Create and manage multiple queues -- **Job Processing** - Register handlers and process jobs -- **Event System** - Comprehensive event lifecycle -- **Progress Tracking** - Real-time job progress updates -- **Scheduling** - Delayed and recurring jobs -- **Priority System** - Numeric priority-based processing -- **Graceful Shutdown** - Clean termination handling - -## Core Methods - -### Registering Job Handlers - -Define how jobs should be processed: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -interface Payload { - to: string - subject: string -} - -interface Result { - messageId: string - sent: true -} - -queue.register("send-email", async (job) => { - //await sendEmail(job.payload) - return { messageId: "msg_123", sent: true } -}) -``` - -### Adding Jobs - -Add jobs to the queue for processing: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -interface EmailPayload { - to: string - subject: string -} - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -// Basic job -await queue.add("send-email", { - to: "user@example.com", - subject: "Welcome!", -}) - -// Job with options -const payload: EmailPayload = { - to: "user@example.com", - subject: "Welcome!", -} - -await queue.add("send-email", payload, { - priority: 1, - delay: 5000, - maxAttempts: 5, -}) -``` - -### Queue Control - -Manage queue processing: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -// Start processing jobs -queue.start() - -// Pause processing -queue.pause() - -// Resume processing -queue.resume() - -// Graceful shutdown -await queue.stop() -``` - -### Progress Tracking - -Update job progress in real-time: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -interface ProcessPayload { - items: string[] -} - -interface ProcessResult { - processed: number -} - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -queue.register("process-data", async (job) => { - const items = job.payload.items - - for (let i = 0; i < items.length; i++) { - //await processItem(items[i]) - - // Update progress (0-100) - const progress = Math.round(((i + 1) / items.length) * 100) - await job.updateProgress(progress) - } - - return { processed: items.length } -}) -``` - -### Scheduling Jobs - -Schedule jobs for future execution: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -interface CleanupPayload { - type: string -} - -interface ReportPayload { - date: string -} - -interface HealthCheckPayload { - endpoint: string -} - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -// Delayed job -await queue.add( - "cleanup", - { type: "temp-files" }, - { - delay: 60000, // 1 minute - }, -) - -// Recurring job with cron -await queue.add( - "daily-report", - { date: new Date().toISOString() }, - { - cron: "0 9 * * *", // Every day at 9 AM - timezone: "America/New_York", - }, -) - -// Recurring job with interval -await queue.add( - "health-check", - { endpoint: "/api/health" }, - { - repeat: { every: 30000, limit: 10 }, // Every 30s, 10 times - }, -) -``` - -### Event Handling - -Listen to job and queue events: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -// Job lifecycle events -queue.on("job:added", (job) => { - console.log(`Job ${job.name} added to queue`) -}) - -queue.on("job:completed", (job) => { - console.log(`Job ${job.name} completed`) - console.log("Result:", job.result) -}) - -queue.on("job:failed", (job) => { - console.error(`Job ${job.name} failed:`, job.error) -}) - -queue.on("job:progress", (job) => { - console.log(`Job ${job.name}: ${job.progress}% complete`) -}) - -// Queue control events -queue.on("queue:paused", () => { - console.log("Queue paused") -}) - -queue.on("queue:resumed", () => { - console.log("Queue resumed") -}) -``` - -### Queue Statistics - -Get queue metrics and status: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -// Get current stats -const stats = await queue.getStats() -console.log(stats) // { pending: 5, processing: 2, completed: 100, failed: 3 } - -// Clear jobs -await queue.clear() // Clear all jobs -await queue.clear("failed") // Clear only failed jobs -``` - -### Error Handling - -Handle job failures and timeouts: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -interface RiskyJobPayload { - operation: string - data: unknown -} - -interface RiskyJobResult { - success: boolean - error?: string -} - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -queue.register("risky-job", async (job) => { - try { - //await riskyOperation(job.payload) - return { success: true } - } catch (error) { - throw error // Re-throw to mark job as failed - } -}) - -// Set job timeout -const payload: RiskyJobPayload = { - operation: "data-processing", - data: { items: [1, 2, 3] }, -} - -await queue.add("risky-job", payload, { - timeout: 30000, // 30 seconds -}) -``` - -## Memory Adapter - -Includes a built-in memory adapter for testing and development: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) -``` - -## TypeScript Support - -Full TypeScript support with generic job payloads: - -```ts -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -const queue = new Queue(new MemoryQueueAdapter(), { name: "example" }) - -interface EmailPayload { - to: string - subject: string - body: string -} - -interface EmailResult { - success: boolean -} - -queue.register("send-email", async (job) => { - // job.payload is typed as EmailPayload - //await sendEmail(job.payload) - - return { - success: true, - } -}) -``` - -## References - -- Sources: https://github.com/noxify/vorsteh-queue/tree/main/packages/core -- NPM: https://www.npmjs.com/package/@vorsteh-queue/core diff --git a/apps/docs/content/docs/02.packages/02.create-vorsteh-queue.mdx b/apps/docs/content/docs/02.packages/02.create-vorsteh-queue.mdx deleted file mode 100644 index 0020fa3c..00000000 --- a/apps/docs/content/docs/02.packages/02.create-vorsteh-queue.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: "create-vorsteh-queue CLI" -navTitle: create-vorsteh-queue -description: CLI tool for quickly creating new Vorsteh Queue projects with pre-configured templates. ---- - -The `create-vorsteh-queue` CLI tool provides the fastest way to get started with Vorsteh Queue by creating new projects from pre-built templates. - -## Installation - -No installation required - use with `npx`: - -```bash -npx create-vorsteh-queue -``` - -## Usage - -### Interactive Mode - -```bash -# Full interactive experience -npx create-vorsteh-queue -``` - -The CLI will prompt you for: - -- Project name -- Template selection -- Package manager preference -- Dependency installation - -### Direct Mode - -```bash -# With project name -npx create-vorsteh-queue my-queue-app - -# With template selection -npx create-vorsteh-queue my-app --template=drizzle-postgres - -# With package manager -npx create-vorsteh-queue my-app --package-manager=pnpm - -# Fully automated -npx create-vorsteh-queue my-app \ - --template=drizzle-postgres \ - --package-manager=pnpm \ - --quiet -``` - -## CLI Options - -| Option | Short | Description | Example | -| ------------------------ | ----------- | ----------------------- | --------------------- | -| `--template=` | `-t=` | Choose template | `-t=drizzle-postgres` | -| `--package-manager=` | `-pm=` | Package manager | `-pm=pnpm` | -| `--no-install` | - | Skip dependency install | `--no-install` | -| `--quiet` | `-q` | Minimal output | `--quiet` | - -## Available Templates - -Templates are dynamically discovered from the repository: - -- **drizzle-pg** - Basic Drizzle ORM with node-postgres -- **drizzle-pglite** - Zero-setup with embedded PostgreSQL -- **drizzle-postgres** - Advanced with recurring jobs -- **event-system** - Comprehensive event monitoring -- **progress-tracking** - Real-time progress updates -- **pm2-workers** - Multi-process workers with PM2 -- **prisma-client** - Example with the new `prisma-client` -- **prisma-client-js** - Example with the old `prisma-client-js` - -## Package Managers - -Supported package managers: - -- **npm** - Default Node.js package manager -- **pnpm** - Fast, disk space efficient -- **yarn** - Popular alternative -- **bun** - Ultra-fast (experimental) - -## Template Structure - -Each template includes: - -- **Complete source code** - Ready-to-run example -- **Database schema** - Pre-configured tables -- **Environment setup** - Example `.env` files -- **Documentation** - README with setup instructions -- **TypeScript configuration** - Optimized settings - -## Examples - -### Quick Start - -```bash -npx create-vorsteh-queue my-app --template=drizzle-pglite -cd my-app -npm run dev -``` - -### Production Setup - -```bash -npx create-vorsteh-queue worker-service \ - --template=drizzle-postgres \ - --package-manager=pnpm -cd worker-service -pnpm db:push -pnpm dev -``` - -### CI/CD Friendly - -```bash -npx create-vorsteh-queue my-app \ - --template=event-system \ - --package-manager=pnpm \ - --no-install \ - --quiet -``` - -## Template Development - -Templates are automatically discovered from the `examples/` directory in the repository. To add a new template: - -1. Create a new example in `examples/` -2. Include a `package.json` with proper metadata -3. Add a `README.md` with setup instructions -4. The template becomes available automatically diff --git a/apps/docs/content/docs/02.packages/adapter-drizzle.mdx b/apps/docs/content/docs/02.packages/adapter-drizzle.mdx deleted file mode 100644 index 12b22c4d..00000000 --- a/apps/docs/content/docs/02.packages/adapter-drizzle.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Drizzle Adapter -navTitle: "@vorsteh-queue/adapter-drizzle" -description: Drizzle ORM adapter for PostgreSQL providing type-safe database operations with excellent performance. ---- - -The Drizzle adapter provides PostgreSQL support using Drizzle ORM, offering excellent TypeScript integration and performance. - -## Installation - -@vorsteh-queue/core @vorsteh-queue/adapter-drizzle - -## Quick Start - -```typescript -import { drizzle } from "drizzle-orm/node-postgres" -import { Pool } from "pg" - -import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" -import { Queue } from "@vorsteh-queue/core" - -const pool = new Pool({ connectionString: "postgresql://..." }) -const db = drizzle(pool) -const adapter = new PostgresQueueAdapter(db) -const queue = new Queue(adapter) -``` - -## Supported providers - -- PGlite - https://orm.drizzle.team/docs/connect-pglite -- Postgres.JS - https://orm.drizzle.team/docs/get-started-postgresql#postgresjs -- Node Progress - https://orm.drizzle.team/docs/get-started-postgresql#node-postgres - -## Database Setup - -### Schema - -The adapter includes a pre-defined schema: - -```typescript -import { queueJobsTable } from "@vorsteh-queue/adapter-drizzle" - -// Use in your schema file -export { queueJobsTable } -``` - -If you don't want to use the pre-defined schema, you can create your own schema definition. - - - -### Custom Table & Schema Names - -To create a table with custom name and schema, you can use the `createQueueJobsTable` helper, which will create the table without taking care about the field definitions: - -```typescript path="drizzle-schema.ts" -import { createQueueJobsTable } from "@vorsteh-queue/adapter-drizzle" - -export const { table: customQueueJobs, schema: customSchema } = createQueueJobsTable( - "custom_queue_jobs", // your custom table name - "custom_schema", // your custom schema name -) -``` - -To use the custom table and schema in the adapter, pass the adapter configuration to the `PostgresQueueAdapter`: - -```typescript -import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" - -const adapter = new PostgresQueueAdapter(db, { - // Your custom model name ( based on the example above - `table: customQueueJobs`) - modelName: "customQueueJobs", -}) -``` - - - Checkout our drizzle example with [custom schema and - table](/docs/examples/drizzle-custom-schema-table/). - - -### Migration - -To run the migrations, we recommend to use [`drizzle-kit`](https://orm.drizzle.team/docs/kit-overview). - -> Drizzle Kit is a CLI tool for managing SQL database migrations with Drizzle. - -## References - -- Sources: https://github.com/noxify/vorsteh-queue/tree/main/packages/adapter-drizzle -- NPM: https://www.npmjs.com/package/@vorsteh-queue/adapter-drizzle diff --git a/apps/docs/content/docs/02.packages/adapter-kysely.mdx b/apps/docs/content/docs/02.packages/adapter-kysely.mdx deleted file mode 100644 index 7e07d1d8..00000000 --- a/apps/docs/content/docs/02.packages/adapter-kysely.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Kysely Adapter -navTitle: "@vorsteh-queue/adapter-kysely" -description: Kysely adapter for PostgreSQL providing type-safe database operations with excellent performance. ---- - -The Kysely adapter provides PostgreSQL support using Kysely, offering excellent TypeScript integration and performance. - -## Installation - -@vorsteh-queue/core @vorsteh-queue/adapter-kysely - -## Quick Start - -```typescript -import { Kysely } from "kysely" -import { PostgresJSDialect } from "kysely-postgres-js" -import postgres from "postgres" - -import type { QueueJobTableDefinition } from "@vorsteh-queue/adapter-kysely/types" -import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-kysely" -import { Queue } from "@vorsteh-queue/core" - -interface DB { - queue_jobs: QueueJobTableDefinition - other_table: { - name: string - } -} - -// Shared database connection -const client = postgres( - process.env.DATABASE_URL || "postgresql://postgres:password@localhost:5432/queue_db", - { max: 10 }, // Connection pool -) - -const db = new Kysely({ - dialect: new PostgresJSDialect({ - postgres: client, - }), -}) - -const adapter = new PostgresQueueAdapter(db) -const queue = new Queue(adapter) -``` - -## Supported providers - -- PGlite - https://github.com/czeidler/kysely-pglite-dialect -- Postgres.JS - https://github.com/kysely-org/kysely-postgres-js -- Node Progress - https://kysely-org.github.io/kysely-apidoc/classes/PostgresDialect.html - -## Database Setup - -### Schema - -The adapter includes a pre-defined migration: - -```typescript path="migrations/queue-jobs.ts" -export { down, up } from "@vorsteh-queue/adapter-kysely/migrations" -``` - -If you don't want to use the pre-defined schema, you can create your own migration. - - - -### Custom Table & Schema Names - -To create a table with custom name and schema, you can use the `createQueueJobsTable` helper, which will create the table without taking care about the field definitions: - -```typescript path="migrations/queue-jobs.ts" -import { createQueueJobsTable } from "@vorsteh-queue/adapter-kysely" - -export const { up, down } = createQueueJobsTable( - // your custom table name - "custom_queue_jobs", - // your custom schema name - "custom_schema", -) -``` - -To use the custom table and schema in the adapter, pass the adapter configuration to the `PostgresQueueAdapter`: - -```typescript -import { Kysely } from "kysely" - -import type { QueueJobTableDefinition } from "@vorsteh-queue/adapter-kysely/types" -import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-kysely" - -interface DB { - custom_queue_jobs: QueueJobTableDefinition -} - -const db = new Kysely({ - //... your Kysely configuration -}) - -const adapter = new PostgresQueueAdapter(db, { - // Your custom schema name - schemaName: "custom_schema", - // Your custom table name - tableName: "custom_queue_jobs", -}) -``` - - - Checkout our kysely example with [custom schema and - table](/docs/examples/kysely-custom-schema-table/). - - -### Migration - -To run the migrations, we recommend to use [`kysely-ctl`](https://github.com/kysely-org/kysely-ctl). - -> `kysely-ctl` is the command-line tool for Kysely. - -## References - -- Sources: https://github.com/noxify/vorsteh-queue/tree/main/packages/adapter-kysely -- NPM: https://www.npmjs.com/package/@vorsteh-queue/adapter-kysely diff --git a/apps/docs/content/docs/02.packages/adapter-prisma.mdx b/apps/docs/content/docs/02.packages/adapter-prisma.mdx deleted file mode 100644 index 0ed47178..00000000 --- a/apps/docs/content/docs/02.packages/adapter-prisma.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Prisma Adapter -navTitle: "@vorsteh-queue/adapter-prisma" -description: Prisma ORM adapter for PostgreSQL with generated types and excellent developer tooling. ---- - -The Prisma adapter provides PostgreSQL support using Prisma ORM, offering generated types, migrations, and excellent developer tooling. - -## Installation - -@vorsteh-queue/core @vorsteh-queue/adapter-prisma - -## Quick Start - -### With prisma-client-js - -```typescript -import { PrismaClient } from "@prisma/client" - -import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" -import { Queue } from "@vorsteh-queue/core" - -const prisma = new PrismaClient() -const adapter = new PostgresPrismaQueueAdapter(prisma) -const queue = new Queue(adapter) -``` - -### With prisma-client - -```typescript -import { PrismaPg } from "@prisma/adapter-pg" -import { PrismaClient } from "src/generated/prisma/client" - -import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" -import { Queue } from "@vorsteh-queue/core" - -const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }) -const prisma = new PrismaClient({ adapter }) -const adapter = new PostgresPrismaQueueAdapter(prisma) -const queue = new Queue(adapter) -``` - -## Database Setup - -### Schema - -Add the queue job model to your `schema.prisma`: - - - -### Custom Table & Schema Names - -To use the custom table and schema with prisma, you have to customize the prisma schema. -Here an example with custom schema and table name ( the relevant parts are highlighted ): - - - -To use the custom table and schema in the adapter, pass the adapter configuration to the `PostgresQueueAdapter`: - -```typescript -import { PrismaPg } from "@prisma/adapter-pg" -import { PrismaClient } from "src/generated/prisma/client" - -import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" -import { Queue } from "@vorsteh-queue/core" - -const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }) -const prisma = new PrismaClient({ adapter }) - -const adapter = new PostgresQueueAdapter(prisma, { - // Your custom model name ( based on the example above ) - modelName: "CustomQueueJobs", - // Your custom schema name - schemaName: "custom_schema", - // Your custom table name - tableName: "custom_queue_jobs", -}) - -const queue = new Queue(adapter) -``` - - - Checkout our prisma example with [custom schema and - table](/docs/examples/prisma-custom-schema-table/). - - -### Migration - -To run the migrations, we recommend to use the [`prisma cli`](https://www.prisma.io/docs/orm/tools/prisma-cli). - -> The Prisma command line interface (CLI) is the primary way to interact with your Prisma project from the command line. - -## References - -- Sources: https://github.com/noxify/vorsteh-queue/tree/main/packages/adapter-prisma -- NPM: https://www.npmjs.com/package/@vorsteh-queue/adapter-prisma diff --git a/apps/docs/content/docs/02.packages/index.mdx b/apps/docs/content/docs/02.packages/index.mdx deleted file mode 100644 index d6d378ea..00000000 --- a/apps/docs/content/docs/02.packages/index.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Packages -description: Overview of all Vorsteh Queue packages and their purposes. ---- - -Vorsteh Queue is built as a modular system with separate packages for different functionality. This allows you to install only what you need and keeps the core lightweight. - -## Core Package - -- **[@vorsteh-queue/core](/docs/packages/core)** - The main queue engine with all core functionality - -## Adapter Packages - -- **[@vorsteh-queue/adapter-drizzle](/docs/packages/adapter-drizzle)** - Drizzle ORM adapter for PostgreSQL -- **[@vorsteh-queue/adapter-prisma](/docs/packages/adapter-prisma)** - Prisma ORM adapter for PostgreSQL -- **[@vorsteh-queue/adapter-kysely](/docs/packages/adapter-kysely)** - Kysely adapter for PostgreSQL - -## CLI Tools - -- **[create-vorsteh-queue](/docs/packages/create-vorsteh-queue)** - CLI tool for creating new projects - -## Package Architecture - -The modular design allows for: - -- **Minimal dependencies** - Only install what you need -- **Flexible adapters** - Easy to add new database adapters -- **Type safety** - Full TypeScript support across all packages -- **Independent versioning** - Packages can be updated independently - -## Installation Patterns - -### Basic Setup - -```bash -npm install @vorsteh-queue/core @vorsteh-queue/adapter-drizzle -``` - -### With CLI - -```bash -npx create-vorsteh-queue my-app --template=drizzle-postgres -``` - -Choose the packages that match your project's needs and ORM preference. diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx deleted file mode 100644 index 1595f571..00000000 --- a/apps/docs/content/docs/index.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Documentation -ignoreSearch: true ---- diff --git a/apps/docs/content/examples/01.getting-started/01.drizzle-pglite.mdx b/apps/docs/content/examples/01.getting-started/01.drizzle-pglite.mdx new file mode 100644 index 00000000..760572d4 --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/01.drizzle-pglite.mdx @@ -0,0 +1,39 @@ +--- +title: Drizzle + PGlite +description: Zero-setup example using Drizzle ORM with PGlite (embedded PostgreSQL). +tags: ["drizzle", "pglite", "beginner"] +--- + +A zero-dependency-on-external-services example that uses PGlite as an embedded PostgreSQL database. Great for quick experimentation without running a Postgres server. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=drizzle-pglite + + +Then install dependencies and start the dev server: + +```bash +cd my-app +npm install +npm run dev +``` + +## What it demonstrates + +- Basic queue setup with Drizzle ORM +- Embedded PGlite database (no external PostgreSQL needed) +- Job registration and processing +- Simple job handler returning results + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [Drizzle Adapter](/docs/vorsteh-queue/adapters/drizzle) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/drizzle-pglite) diff --git a/apps/docs/content/examples/01.getting-started/02.drizzle-pg.mdx b/apps/docs/content/examples/01.getting-started/02.drizzle-pg.mdx new file mode 100644 index 00000000..0563e8c0 --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/02.drizzle-pg.mdx @@ -0,0 +1,40 @@ +--- +title: Drizzle + node-postgres +navTitle: Drizzle + pg +description: Basic example using Drizzle ORM with node-postgres (pg). +tags: ["drizzle", "node-postgres", "beginner"] +--- + +A minimal example using Drizzle ORM with the `node-postgres` (pg) driver connecting to a real PostgreSQL database. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=drizzle-pg + + +Then install dependencies, push the schema and start the dev server: + +```bash +cd my-app +npm install +npm run db:push +npm run dev +``` + +## What it demonstrates + +- Drizzle ORM with node-postgres driver +- Schema migration with drizzle-kit +- Queue creation, handler registration, and job processing + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [Drizzle Adapter](/docs/vorsteh-queue/adapters/drizzle) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/drizzle-pg) diff --git a/apps/docs/content/examples/01.getting-started/03.drizzle-postgres.mdx b/apps/docs/content/examples/01.getting-started/03.drizzle-postgres.mdx new file mode 100644 index 00000000..e4f59e4b --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/03.drizzle-postgres.mdx @@ -0,0 +1,42 @@ +--- +title: Drizzle + postgres.js +navTitle: Drizzle + postgres.js +description: Advanced example using Drizzle ORM with postgres.js and recurring jobs. +tags: ["drizzle", "postgres.js", "recurring"] +--- + +An advanced example using Drizzle ORM with the `postgres.js` driver, demonstrating recurring jobs with cron expressions. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=drizzle-postgres + + +Then install dependencies, push the schema and start the dev server: + +```bash +cd my-app +pnpm install +pnpm db:push +pnpm dev +``` + +## What it demonstrates + +- Drizzle ORM with postgres.js driver +- Recurring jobs using cron expressions +- Timezone-aware scheduling +- Multiple job types in one queue + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [Drizzle Adapter](/docs/vorsteh-queue/adapters/drizzle) +- [Job Types](/docs/vorsteh-queue/core/job-types) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/drizzle-postgres) diff --git a/apps/docs/content/examples/01.getting-started/04.kysely-postgres.mdx b/apps/docs/content/examples/01.getting-started/04.kysely-postgres.mdx new file mode 100644 index 00000000..650d90a6 --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/04.kysely-postgres.mdx @@ -0,0 +1,41 @@ +--- +title: Kysely + PostgreSQL +navTitle: Kysely +description: Example using Kysely with postgres.js and recurring jobs. +tags: ["kysely", "postgres.js", "recurring"] +--- + +An example using Kysely as the query builder with a PostgreSQL database, demonstrating recurring job scheduling. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=kysely-postgres + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Kysely adapter setup +- Migration-based schema creation +- Recurring job scheduling +- Job handler with typed payloads + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [Kysely Adapter](/docs/vorsteh-queue/adapters/kysely) +- [Job Types](/docs/vorsteh-queue/core/job-types) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/kysely-postgres) diff --git a/apps/docs/content/examples/01.getting-started/05.prisma-client.mdx b/apps/docs/content/examples/01.getting-started/05.prisma-client.mdx new file mode 100644 index 00000000..315d6cdb --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/05.prisma-client.mdx @@ -0,0 +1,39 @@ +--- +title: Prisma Client +description: Example using Prisma with prisma-client provider and PostgreSQL. +tags: ["prisma", "postgresql"] +--- + +An example using the modern `prisma-client` provider with PostgreSQL and the `@prisma/adapter-pg` driver adapter. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=prisma-client + + +Then install dependencies, generate the Prisma client and start the dev server: + +```bash +cd my-app +pnpm install +pnpm prisma:generate +pnpm dev +``` + +## What it demonstrates + +- Prisma adapter with the `prisma-client` provider +- TypeScript-generated Prisma client +- Queue setup and job processing + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [Prisma Adapter](/docs/vorsteh-queue/adapters/prisma) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/prisma-client) diff --git a/apps/docs/content/examples/01.getting-started/06.zenstack-postgres.mdx b/apps/docs/content/examples/01.getting-started/06.zenstack-postgres.mdx new file mode 100644 index 00000000..4c621f66 --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/06.zenstack-postgres.mdx @@ -0,0 +1,39 @@ +--- +title: ZenStack PostgreSQL +description: Example using ZenStack ORM with PostgreSQL. +tags: ["zenstack", "postgresql"] +--- + +An example using ZenStack ORM v3 with PostgreSQL and the `pg` driver. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=zenstack-postgres + + +Then install dependencies, generate the ZenStack schema and start the dev server: + +```bash +cd my-app +pnpm install +pnpm zen:generate +pnpm dev +``` + +## What it demonstrates + +- ZenStack adapter with PostgreSQL +- ZenStackClient initialization with PostgresDialect +- Queue setup and job processing + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [ZenStack Adapter](/docs/vorsteh-queue/adapters/zenstack) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/zenstack-postgres) diff --git a/apps/docs/content/examples/01.getting-started/07.typeorm-postgres.mdx b/apps/docs/content/examples/01.getting-started/07.typeorm-postgres.mdx new file mode 100644 index 00000000..be40a67c --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/07.typeorm-postgres.mdx @@ -0,0 +1,40 @@ +--- +title: TypeORM PostgreSQL +description: Example using TypeORM with PostgreSQL. +tags: ["typeorm", "postgresql"] +--- + +An example using TypeORM with PostgreSQL and the `pg` driver. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=typeorm-postgres + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +The `QueueJobEntity` with `synchronize: true` will automatically create the table on first run. + +## What it demonstrates + +- TypeORM adapter with PostgreSQL +- DataSource initialization with entity registration +- Queue setup and job processing + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [TypeORM Adapter](/docs/vorsteh-queue/adapters/typeorm) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/typeorm-postgres) diff --git a/apps/docs/content/examples/01.getting-started/08.mikroorm-postgres.mdx b/apps/docs/content/examples/01.getting-started/08.mikroorm-postgres.mdx new file mode 100644 index 00000000..c58c1156 --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/08.mikroorm-postgres.mdx @@ -0,0 +1,38 @@ +--- +title: MikroORM PostgreSQL +description: Example using MikroORM with PostgreSQL. +tags: ["mikroorm", "postgresql"] +--- + +An example using MikroORM v6 with PostgreSQL. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=mikroorm-postgres + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- MikroORM adapter with PostgreSQL +- MikroORM initialization with EntitySchema +- Queue setup and job processing + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [MikroORM Adapter](/docs/vorsteh-queue/adapters/mikroorm) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/mikroorm-postgres) diff --git a/apps/docs/content/examples/01.getting-started/09.sequelize-postgres.mdx b/apps/docs/content/examples/01.getting-started/09.sequelize-postgres.mdx new file mode 100644 index 00000000..67fe2b2a --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/09.sequelize-postgres.mdx @@ -0,0 +1,38 @@ +--- +title: Sequelize PostgreSQL +description: Example using Sequelize with PostgreSQL. +tags: ["sequelize", "postgresql"] +--- + +An example using Sequelize v6 with PostgreSQL. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=sequelize-postgres + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Sequelize adapter with PostgreSQL +- Sequelize initialization with model registration +- Queue setup and job processing + +## Related documentation + +- [Quickstart](/docs/vorsteh-queue/getting-started/quickstart) +- [Sequelize Adapter](/docs/vorsteh-queue/adapters/sequelize) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/sequelize-postgres) diff --git a/apps/docs/content/examples/01.getting-started/index.mdx b/apps/docs/content/examples/01.getting-started/index.mdx new file mode 100644 index 00000000..1eae5d56 --- /dev/null +++ b/apps/docs/content/examples/01.getting-started/index.mdx @@ -0,0 +1,4 @@ +--- +title: Getting Started +description: Basic examples for each supported adapter. +--- diff --git a/apps/docs/content/examples/02.features/01.event-system.mdx b/apps/docs/content/examples/02.features/01.event-system.mdx new file mode 100644 index 00000000..08b99a51 --- /dev/null +++ b/apps/docs/content/examples/02.features/01.event-system.mdx @@ -0,0 +1,40 @@ +--- +title: Event System +description: Comprehensive event monitoring and statistics using the queue event system. +tags: ["events", "monitoring"] +--- + +Demonstrates the full event lifecycle — listen to job additions, completions, failures, progress updates, and queue control events. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=event-system + + +Then install dependencies, push the schema and start the dev server: + +```bash +cd my-app +pnpm install +pnpm db:push +pnpm dev +``` + +## What it demonstrates + +- All job lifecycle events (`job:added`, `job:completed`, `job:failed`, `job:progress`) +- Queue control events (`queue:paused`, `queue:resumed`) +- Real-time statistics tracking via events +- Event-driven logging patterns + +## Related documentation + +- [Events](/docs/vorsteh-queue/core/events) +- [Queue](/docs/vorsteh-queue/core/queue) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/event-system) diff --git a/apps/docs/content/examples/02.features/02.progress-tracking.mdx b/apps/docs/content/examples/02.features/02.progress-tracking.mdx new file mode 100644 index 00000000..9f1fe25a --- /dev/null +++ b/apps/docs/content/examples/02.features/02.progress-tracking.mdx @@ -0,0 +1,39 @@ +--- +title: Progress Tracking +description: Real-time job progress tracking with percentage updates. +tags: ["progress", "monitoring"] +--- + +Shows how to report and monitor real-time progress during long-running job execution. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=progress-tracking + + +Then install dependencies, push the schema and start the dev server: + +```bash +cd my-app +pnpm install +pnpm db:push +pnpm dev +``` + +## What it demonstrates + +- `job.updateProgress()` calls during processing +- Progress event listening +- Batch processing with progress reporting + +## Related documentation + +- [Progress Tracking](/docs/vorsteh-queue/core/progress-tracking) +- [Events](/docs/vorsteh-queue/core/events) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/progress-tracking) diff --git a/apps/docs/content/examples/02.features/03.flow-producer.mdx b/apps/docs/content/examples/02.features/03.flow-producer.mdx new file mode 100644 index 00000000..20703fa9 --- /dev/null +++ b/apps/docs/content/examples/02.features/03.flow-producer.mdx @@ -0,0 +1,39 @@ +--- +title: Flow Producer +description: Parent-child job flows for multi-step pipelines. +tags: ["flows", "pipeline", "parent-child"] +--- + +Demonstrates parent-child job relationships where a parent job spawns child jobs and waits for their completion before finishing. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=flow-producer + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Flow creation with parent and child jobs +- `waiting-children` status handling +- Flow tree visualization +- Multi-step deploy pipeline pattern + +## Related documentation + +- [Flows Overview](/docs/vorsteh-queue/core/flows/overview) +- [Flow Dependencies](/docs/vorsteh-queue/core/flows/dependencies) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/flow-producer) diff --git a/apps/docs/content/examples/02.features/04.batch-processing.mdx b/apps/docs/content/examples/02.features/04.batch-processing.mdx new file mode 100644 index 00000000..14e5d193 --- /dev/null +++ b/apps/docs/content/examples/02.features/04.batch-processing.mdx @@ -0,0 +1,38 @@ +--- +title: Batch Processing +description: Process large datasets in batches with progress tracking. +tags: ["batch", "progress"] +--- + +Shows how to split large workloads into batches and process them with progress reporting. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=batch-processing + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Splitting large payloads into manageable batches +- Progress reporting across batch iterations +- Job result aggregation + +## Related documentation + +- [Queue](/docs/vorsteh-queue/core/queue) +- [Job Types](/docs/vorsteh-queue/core/job-types) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/batch-processing) diff --git a/apps/docs/content/examples/02.features/05.rate-limiting.mdx b/apps/docs/content/examples/02.features/05.rate-limiting.mdx new file mode 100644 index 00000000..1e5eecff --- /dev/null +++ b/apps/docs/content/examples/02.features/05.rate-limiting.mdx @@ -0,0 +1,38 @@ +--- +title: Rate Limiting +description: Per-handler rate limiting to control processing throughput. +tags: ["rate-limiting", "throttling"] +--- + +Demonstrates rate-limited job processing to respect external API limits or control resource usage. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=rate-limiting + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Per-handler rate limiting configuration +- Controlling concurrent execution +- Respecting external API rate limits + +## Related documentation + +- [Job Types](/docs/vorsteh-queue/core/job-types) +- [Queue](/docs/vorsteh-queue/core/queue) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/rate-limiting) diff --git a/apps/docs/content/examples/02.features/06.unique-jobs.mdx b/apps/docs/content/examples/02.features/06.unique-jobs.mdx new file mode 100644 index 00000000..5ecfcd98 --- /dev/null +++ b/apps/docs/content/examples/02.features/06.unique-jobs.mdx @@ -0,0 +1,38 @@ +--- +title: Unique Jobs +description: Job deduplication with unique keys to prevent duplicate processing. +tags: ["unique", "deduplication"] +--- + +Shows how to use unique keys to prevent duplicate jobs from being added to the queue. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=unique-jobs + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Unique key assignment to prevent duplicates +- Handling duplicate job attempts gracefully +- Use cases like email deduplication or idempotent operations + +## Related documentation + +- [Job Types](/docs/vorsteh-queue/core/job-types) +- [Queue](/docs/vorsteh-queue/core/queue) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/unique-jobs) diff --git a/apps/docs/content/examples/02.features/07.group-fifo.mdx b/apps/docs/content/examples/02.features/07.group-fifo.mdx new file mode 100644 index 00000000..ead216b2 --- /dev/null +++ b/apps/docs/content/examples/02.features/07.group-fifo.mdx @@ -0,0 +1,39 @@ +--- +title: Group FIFO +description: Per-group FIFO ordering for multi-tenant job processing. +tags: ["groups", "fifo", "multi-tenant"] +--- + +Demonstrates grouped job processing where jobs within the same group are processed in strict FIFO order, while different groups can be processed concurrently. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=group-fifo + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Group key assignment for job ordering +- FIFO processing within a group +- Concurrent processing across groups +- Multi-tenant isolation patterns + +## Related documentation + +- [Job Types](/docs/vorsteh-queue/core/job-types) +- [Queue](/docs/vorsteh-queue/core/queue) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/group-fifo) diff --git a/apps/docs/content/examples/02.features/08.result-storage.mdx b/apps/docs/content/examples/02.features/08.result-storage.mdx new file mode 100644 index 00000000..1940123f --- /dev/null +++ b/apps/docs/content/examples/02.features/08.result-storage.mdx @@ -0,0 +1,38 @@ +--- +title: Result Storage +description: Storing and retrieving job results after completion. +tags: ["results", "storage"] +--- + +Shows how to store structured results from job handlers and retrieve them after completion. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=result-storage + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Returning typed results from job handlers +- Retrieving results by job ID +- Result inspection via CLI and API + +## Related documentation + +- [Job Types](/docs/vorsteh-queue/core/job-types) +- [Queue](/docs/vorsteh-queue/core/queue) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/result-storage) diff --git a/apps/docs/content/examples/02.features/index.mdx b/apps/docs/content/examples/02.features/index.mdx new file mode 100644 index 00000000..4ce0676d --- /dev/null +++ b/apps/docs/content/examples/02.features/index.mdx @@ -0,0 +1,4 @@ +--- +title: Features +description: Examples demonstrating specific Vorsteh Queue features and patterns. +--- diff --git a/apps/docs/content/examples/03.advanced/01.pm2-workers.mdx b/apps/docs/content/examples/03.advanced/01.pm2-workers.mdx new file mode 100644 index 00000000..7c7b0bd7 --- /dev/null +++ b/apps/docs/content/examples/03.advanced/01.pm2-workers.mdx @@ -0,0 +1,40 @@ +--- +title: PM2 Workers +description: Multi-process workers managed by PM2 for production deployments. +tags: ["pm2", "workers", "production"] +--- + +Shows how to run multiple queue worker processes managed by PM2 for horizontal scaling. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=pm2-workers + + +Then install dependencies, push the schema and start the workers: + +```bash +cd my-app +pnpm install +pnpm db:push +pnpm start +``` + +## What it demonstrates + +- PM2 ecosystem configuration +- Multiple worker processes sharing a queue +- Graceful shutdown handling +- Production-ready process management + +## Related documentation + +- [Queue](/docs/vorsteh-queue/core/queue) +- [Job Types](/docs/vorsteh-queue/core/job-types) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/pm2-workers) diff --git a/apps/docs/content/examples/03.advanced/02.graphql-server.mdx b/apps/docs/content/examples/03.advanced/02.graphql-server.mdx new file mode 100644 index 00000000..2b5721c0 --- /dev/null +++ b/apps/docs/content/examples/03.advanced/02.graphql-server.mdx @@ -0,0 +1,39 @@ +--- +title: GraphQL Server +description: Standalone GraphQL server for queue monitoring and management. +tags: ["graphql", "server", "monitoring"] +--- + +A standalone GraphQL server exposing queue operations — inspect jobs, get stats, cancel, retry, and redrive from any GraphQL client. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=graphql-server + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- `@vorsteh-queue/server` setup +- GraphQL API with Yoga and Hono +- Query queue stats and job details +- Mutation operations (cancel, retry, redrive) + +## Related documentation + +- [Queue](/docs/vorsteh-queue/core/queue) +- [Events](/docs/vorsteh-queue/core/events) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/graphql-server) diff --git a/apps/docs/content/examples/03.advanced/03.workflow-saga.mdx b/apps/docs/content/examples/03.advanced/03.workflow-saga.mdx new file mode 100644 index 00000000..6dd091fc --- /dev/null +++ b/apps/docs/content/examples/03.advanced/03.workflow-saga.mdx @@ -0,0 +1,57 @@ +--- +title: Workflow Saga +description: Multi-step workflow with Saga compensation pattern for distributed transactions. +tags: ["workflow", "saga", "compensation"] +--- + +Implements the Saga pattern for multi-step workflows where each step has a compensation action that runs on failure. + +## Flow + +```mermaid +flowchart TD + Start([Job starts]) --> S1[Step 1: Reserve Stock] + S1 -->|success| S2[Step 2: Charge Payment] + S2 -->|success| S3[Step 3: Ship Order] + S3 -->|success| Done([Job completed]) + + S3 -->|failure| C2[Compensate: Refund Payment] + C2 --> C1[Compensate: Release Reservation] + C1 --> Failed([Job failed]) + + S2 -->|failure| C1b[Compensate: Release Reservation] + C1b --> Failed +``` + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=workflow-saga + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- Multi-step workflow orchestration +- Compensation actions for rollback +- Step-by-step execution with error boundaries +- Distributed transaction patterns without 2PC + +## Related documentation + +- [Flows Overview](/docs/vorsteh-queue/core/flows/overview) +- [Steps](/docs/vorsteh-queue/core/flows/steps) +- [Error Handling](/docs/vorsteh-queue/core/error-handling) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/workflow-saga) diff --git a/apps/docs/content/examples/03.advanced/04.workflow-signals.mdx b/apps/docs/content/examples/03.advanced/04.workflow-signals.mdx new file mode 100644 index 00000000..deeff70a --- /dev/null +++ b/apps/docs/content/examples/03.advanced/04.workflow-signals.mdx @@ -0,0 +1,61 @@ +--- +title: Workflow Signals +description: Human-in-the-loop workflow with waitFor signals for external approvals. +tags: ["workflow", "signals", "human-in-the-loop"] +--- + +Demonstrates workflows that pause and wait for external signals (e.g. human approval, webhook callback) before continuing. + +## Flow + +```mermaid +sequenceDiagram + participant E as Employee + participant Q as Queue + participant W as Worker + participant M as Manager + + E->>Q: Add expense-request job + Q->>W: Pick up job + W->>W: Step 1: Submit expense + W-->>Q: Pause (waitFor "manager-decision") + + Note over W,M: Job is suspended, waiting for signal + + M->>Q: queue.signal(jobId, "manager-decision", { approved }) + Q->>W: Resume job with signal data + W->>W: Step 2: Process decision + W-->>Q: Job completed +``` + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=workflow-signals + + +Then install dependencies and start the dev server: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +## What it demonstrates + +- `waitFor` signal mechanism +- Pausing a workflow until external input arrives +- Human-in-the-loop approval patterns +- Signal timeout handling + +## Related documentation + +- [Flows Overview](/docs/vorsteh-queue/core/flows/overview) +- [Steps](/docs/vorsteh-queue/core/flows/steps) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/workflow-signals) diff --git a/apps/docs/content/examples/03.advanced/05.tui-dashboard.mdx b/apps/docs/content/examples/03.advanced/05.tui-dashboard.mdx new file mode 100644 index 00000000..cfa65a3c --- /dev/null +++ b/apps/docs/content/examples/03.advanced/05.tui-dashboard.mdx @@ -0,0 +1,61 @@ +--- +title: TUI Dashboard +description: Interactive terminal dashboard with in-memory PGlite for exploring multi-queue monitoring. +tags: ["tui", "dashboard", "cli", "pglite", "monitoring"] +--- + +A zero-setup example that spins up multiple queues with diverse job types using PGlite (in-memory PostgreSQL), then launches the interactive TUI dashboard for real-time monitoring. + +## Setup + +First, generate the project: + + + create-vorsteh-queue my-app --template=advanced-tui + + +Then install dependencies and start: + +```bash +cd my-app +pnpm install +pnpm dev +``` + +In a second terminal, launch the dashboard: + +```bash +pnpm dashboard +``` + +## What it demonstrates + +- In-memory PGlite database (no external PostgreSQL needed) +- Multiple queues: email, data-processing, notifications +- Diverse job types with different priorities, delays, and failure rates +- GraphQL server for CLI/dashboard communication +- Real-time TUI dashboard with queue switching, job filtering, and keyboard navigation + +## Dashboard commands + +The example provides per-queue dashboard shortcuts: + +```bash +# Monitor all queues (default) +pnpm dashboard + +# Monitor a specific queue +pnpm dashboard:email +pnpm dashboard:data +pnpm dashboard:notifications +``` + +## Related documentation + +- [CLI Overview](/docs/cli/overview/installation) +- [Queue](/docs/vorsteh-queue/core/queue) +- [Drizzle Adapter](/docs/vorsteh-queue/adapters/drizzle) + +## Source + +[View on GitHub](https://github.com/noxify/vorsteh-queue/tree/main/examples/advanced-tui) diff --git a/apps/docs/content/examples/03.advanced/index.mdx b/apps/docs/content/examples/03.advanced/index.mdx new file mode 100644 index 00000000..c862f41f --- /dev/null +++ b/apps/docs/content/examples/03.advanced/index.mdx @@ -0,0 +1,4 @@ +--- +title: Advanced +description: Advanced patterns and production deployment examples. +--- diff --git a/apps/docs/content/examples/index.mdx b/apps/docs/content/examples/index.mdx new file mode 100644 index 00000000..fab69415 --- /dev/null +++ b/apps/docs/content/examples/index.mdx @@ -0,0 +1,6 @@ +--- +title: Examples +navTitle: Examples +description: Ready-to-run example projects demonstrating Vorsteh Queue features and patterns. +entrypoint: /docs/examples/getting-started/drizzle-pglite +--- diff --git a/apps/docs/content/features/10.excellent-dx.mdx b/apps/docs/content/features/10.excellent-dx.mdx deleted file mode 100644 index f930b532..00000000 --- a/apps/docs/content/features/10.excellent-dx.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: key_feature -title: Excellent DX -icon: Zap ---- - -Intuitive API design with TypeScript support, comprehensive documentation, and helpful error messages that make development a breeze. diff --git a/apps/docs/content/features/11.orm-agnostic.mdx b/apps/docs/content/features/11.orm-agnostic.mdx deleted file mode 100644 index 29f7d2ba..00000000 --- a/apps/docs/content/features/11.orm-agnostic.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: key_feature -title: ORM Agnostic -icon: Database ---- - -Works with Drizzle ORM, Kysely and Prisma ORM for PostgreSQL. Adapter pattern allows easy integration with your existing database setup. diff --git a/apps/docs/content/features/12.production-ready.mdx b/apps/docs/content/features/12.production-ready.mdx deleted file mode 100644 index 8d8af46e..00000000 --- a/apps/docs/content/features/12.production-ready.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: key_feature -title: Production Ready -icon: Shield ---- - -Battle-tested with built-in retry logic, job cleanup, progress tracking, and graceful shutdown handling for mission-critical applications. diff --git a/apps/docs/content/features/13.event-system.mdx b/apps/docs/content/features/13.event-system.mdx deleted file mode 100644 index 2d0664d1..00000000 --- a/apps/docs/content/features/13.event-system.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: key_feature -title: Event System -icon: Zap ---- - -Comprehensive event system for monitoring job lifecycle. Listen to job progress, completion, failures, and queue state changes. diff --git a/apps/docs/content/features/14.timezone.mdx b/apps/docs/content/features/14.timezone.mdx deleted file mode 100644 index af011523..00000000 --- a/apps/docs/content/features/14.timezone.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: key_feature -title: UTC-First Timezone -icon: Globe ---- - -Reliable timezone handling with UTC-first approach. All timestamps stored as UTC with timezone conversion at job creation time. diff --git a/apps/docs/content/features/15.type-safety.mdx b/apps/docs/content/features/15.type-safety.mdx deleted file mode 100644 index 87a605c8..00000000 --- a/apps/docs/content/features/15.type-safety.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: key_feature -title: Type Safety -icon: Shield ---- - -Full TypeScript support with generic job payloads and results. Compile-time type checking ensures your jobs are properly typed and safe. diff --git a/apps/docs/content/features/20.one-time-jobs.mdx b/apps/docs/content/features/20.one-time-jobs.mdx deleted file mode 100644 index 9d256397..00000000 --- a/apps/docs/content/features/20.one-time-jobs.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: One-time Jobs -icon: Play ---- - -Execute tasks once with optional delays and priority levels. diff --git a/apps/docs/content/features/21.recurring-jobs.mdx b/apps/docs/content/features/21.recurring-jobs.mdx deleted file mode 100644 index 25cd4c5b..00000000 --- a/apps/docs/content/features/21.recurring-jobs.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Recurring Jobs -icon: RotateCcw ---- - -Set up repeating tasks with flexible intervals and cron expressions. diff --git a/apps/docs/content/features/22.scheduled-jobs.mdx b/apps/docs/content/features/22.scheduled-jobs.mdx deleted file mode 100644 index e7647f09..00000000 --- a/apps/docs/content/features/22.scheduled-jobs.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Scheduled Jobs -icon: Calendar ---- - -Schedule jobs for specific dates and times with UTC-first timezone conversion for reliable execution. diff --git a/apps/docs/content/features/23.retry-logic.mdx b/apps/docs/content/features/23.retry-logic.mdx deleted file mode 100644 index e8780a94..00000000 --- a/apps/docs/content/features/23.retry-logic.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Retry Logic -icon: RotateCcw ---- - -Configurable retry strategies with exponential backoff and maximum attempt limits for failed jobs. diff --git a/apps/docs/content/features/24.job-delays.mdx b/apps/docs/content/features/24.job-delays.mdx deleted file mode 100644 index ebeb80a4..00000000 --- a/apps/docs/content/features/24.job-delays.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Job Delays -icon: Clock ---- - -Delay job execution with precise timing control. diff --git a/apps/docs/content/features/25.priority-queues.mdx b/apps/docs/content/features/25.priority-queues.mdx deleted file mode 100644 index c18a9d91..00000000 --- a/apps/docs/content/features/25.priority-queues.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Priority Queues -icon: ArrowUp ---- - -Process high-priority jobs first with numeric priority system (lower number = higher priority). diff --git a/apps/docs/content/features/26.job-results.mdx b/apps/docs/content/features/26.job-results.mdx deleted file mode 100644 index a51238be..00000000 --- a/apps/docs/content/features/26.job-results.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Job Results -icon: CheckCircle ---- - -Store and access job results returned by handlers. Results are automatically persisted and available through events. diff --git a/apps/docs/content/features/27.progress-tracking.mdx b/apps/docs/content/features/27.progress-tracking.mdx deleted file mode 100644 index 5cfeab1e..00000000 --- a/apps/docs/content/features/27.progress-tracking.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Progress Tracking -icon: TrendingUp ---- - -Real-time job progress updates with percentage tracking. Monitor long-running tasks with built-in progress reporting. diff --git a/apps/docs/content/features/28.job-cleanup.mdx b/apps/docs/content/features/28.job-cleanup.mdx deleted file mode 100644 index 30703581..00000000 --- a/apps/docs/content/features/28.job-cleanup.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Job Cleanup -icon: Trash2 ---- - -Automatic cleanup of completed and failed jobs with configurable retention policies to manage database size. diff --git a/apps/docs/content/features/29.job-timeouts.mdx b/apps/docs/content/features/29.job-timeouts.mdx deleted file mode 100644 index e7d3a37e..00000000 --- a/apps/docs/content/features/29.job-timeouts.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Job Timeouts -icon: Clock ---- - -Configurable job timeouts to prevent long-running jobs from blocking the queue. Set per-job or global timeout limits. diff --git a/apps/docs/content/features/30.queue-stats.mdx b/apps/docs/content/features/30.queue-stats.mdx deleted file mode 100644 index 44bdbb43..00000000 --- a/apps/docs/content/features/30.queue-stats.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Queue Statistics -icon: BarChart3 ---- - -Get real-time queue statistics including pending, processing, completed, failed, and delayed job counts. diff --git a/apps/docs/content/features/31.graceful-shutdown.mdx b/apps/docs/content/features/31.graceful-shutdown.mdx deleted file mode 100644 index 6b9d9059..00000000 --- a/apps/docs/content/features/31.graceful-shutdown.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: feature -title: Graceful Shutdown -icon: Power ---- - -Clean job processing termination that waits for active jobs to complete before stopping the queue. diff --git a/apps/docs/content/vorsteh-queue/01.getting-started/01.quickstart.mdx b/apps/docs/content/vorsteh-queue/01.getting-started/01.quickstart.mdx new file mode 100644 index 00000000..e9b10fff --- /dev/null +++ b/apps/docs/content/vorsteh-queue/01.getting-started/01.quickstart.mdx @@ -0,0 +1,93 @@ +--- +title: Quickstart +description: Get started with Vorsteh Queue in under 5 minutes. +--- + +Vorsteh Queue is a type-safe job queue engine for PostgreSQL 12+. It supports multiple ORM adapters, priority queues, delayed and recurring jobs, progress tracking, and a comprehensive event system. + +## Prerequisites + +- **Node.js 22+** +- **PostgreSQL 12+** database +- **ESM only** + +## Create a new project (recommended) + +The fastest way to get started is using the scaffolding CLI: + +create-vorsteh-queue my-queue-app + +This walks you through template selection, package manager choice, and dependency installation. + +## Manual installation + +Pick the adapter that matches your ORM: + +### Drizzle ORM + + + @vorsteh-queue/core @vorsteh-queue/adapter-drizzle + + +### Prisma ORM + + + @vorsteh-queue/core @vorsteh-queue/adapter-prisma + + +### Kysely + + + @vorsteh-queue/core @vorsteh-queue/adapter-kysely + + +### ZenStack ORM + + + @vorsteh-queue/core @vorsteh-queue/adapter-zenstack @zenstackhq/orm + + +### TypeORM + + + @vorsteh-queue/core @vorsteh-queue/adapter-typeorm typeorm pg + + +### MikroORM + + + @vorsteh-queue/core @vorsteh-queue/adapter-mikroorm @mikro-orm/core + @mikro-orm/postgresql + + +### Sequelize + + + @vorsteh-queue/core @vorsteh-queue/adapter-sequelize sequelize pg + + +## Minimal example + +```typescript +import { Queue, MemoryQueueAdapter } from "@vorsteh-queue/core" + +const queue = new Queue(new MemoryQueueAdapter(), { name: "my-queue" }) + +// Register a handler +queue.register("send-email", async (job) => { + console.log(`Sending email to ${job.payload.to}`) + return { sent: true } +}) + +// Add a job +await queue.add("send-email", { to: "user@example.com", subject: "Hello!" }) + +// Start processing +queue.start() +``` + +Replace `MemoryQueueAdapter` with a PostgreSQL adapter for production use. See the adapter docs for setup instructions. + +## Recommended Setup + +For real projects, we recommend centralizing your queue definitions in a shared file. This avoids repetition and makes configuration changes easy. See [Project Structure](/docs/vorsteh-queue/getting-started/project-structure) for the recommended approach. diff --git a/apps/docs/content/vorsteh-queue/01.getting-started/02.installation.mdx b/apps/docs/content/vorsteh-queue/01.getting-started/02.installation.mdx new file mode 100644 index 00000000..cd2bad02 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/01.getting-started/02.installation.mdx @@ -0,0 +1,79 @@ +--- +title: Installation +description: Install Vorsteh Queue packages for your preferred ORM adapter. +--- + +## Quick Start with CLI + +create-vorsteh-queue my-queue-app + +The scaffolding CLI provides interactive prompts for template selection, package manager, and dependency installation. + +See the [create-vorsteh-queue docs](/docs/vorsteh-queue/packages/create-vorsteh-queue) for details. + +## Manual Installation + +### Drizzle ORM (PostgreSQL) + + + @vorsteh-queue/core @vorsteh-queue/adapter-drizzle + + +### Prisma ORM (PostgreSQL) + + + @vorsteh-queue/core @vorsteh-queue/adapter-prisma + + +### Kysely (PostgreSQL) + + + @vorsteh-queue/core @vorsteh-queue/adapter-kysely + + +### ZenStack ORM (PostgreSQL) + + + @vorsteh-queue/core @vorsteh-queue/adapter-zenstack @zenstackhq/orm + + +### TypeORM (PostgreSQL) + + + @vorsteh-queue/core @vorsteh-queue/adapter-typeorm typeorm pg + + +### MikroORM (PostgreSQL) + + + @vorsteh-queue/core @vorsteh-queue/adapter-mikroorm @mikro-orm/core + @mikro-orm/postgresql + + +### Sequelize (PostgreSQL) + + + @vorsteh-queue/core @vorsteh-queue/adapter-sequelize sequelize pg + + +## Database Setup + +Each adapter provides schema definitions and migration tooling: + +- **Drizzle** — use the included schema and run `drizzle-kit` migrations +- **Prisma** — add the model to `schema.prisma` and run `prisma migrate` +- **Kysely** — use the provided migration and run via `kysely-ctl` +- **ZenStack** — add the model to `schema.zmodel` and run `zen db push` +- **TypeORM** — use the included `QueueJobEntity` with `synchronize: true` or generate migrations +- **MikroORM** — use the included EntitySchema and run `mikro-orm migration:create` +- **Sequelize** — use the included model with `sequelize.sync()` or CLI migrations + +Refer to the specific adapter documentation for detailed instructions. + +## Requirements + +| Requirement | Version | +| ------------- | ------- | +| Node.js | 22+ | +| PostgreSQL | 12+ | +| Module system | ESM | diff --git a/apps/docs/content/vorsteh-queue/01.getting-started/03.project-structure.mdx b/apps/docs/content/vorsteh-queue/01.getting-started/03.project-structure.mdx new file mode 100644 index 00000000..a4e619ff --- /dev/null +++ b/apps/docs/content/vorsteh-queue/01.getting-started/03.project-structure.mdx @@ -0,0 +1,396 @@ +--- +title: Project Structure +description: Recommended project structure for Vorsteh Queue applications. +--- + +## Why centralize? + +Defining your adapter and queue instances in a single shared file keeps your project DRY. Instead of recreating the adapter in every file that needs it, you export once and import everywhere — your config file, workers, and application code all reference the same instances. This makes refactoring painless and ensures configuration changes propagate automatically. + +## Recommended structure + +``` +my-app/ +├── src/ +│ ├── queues.ts # Queue definitions (adapter + Queue instances) +│ ├── handlers/ +│ │ ├── email.ts # Job handlers +│ │ └── reports.ts +│ └── worker.ts # Worker setup +├── queue.config.ts # CLI/Server config (imports from src/queues.ts) +└── package.json +``` + +## The shared queue file + +`src/queues.ts` is the single source of truth for your adapter and queue instances: + + + + ```typescript + // src/queues.ts + import { defineRelations } from "drizzle-orm" + import { drizzle } from "drizzle-orm/node-postgres" + import { Pool } from "pg" + + import { + PostgresQueueAdapter, + postgresSchema, + } from "@vorsteh-queue/adapter-drizzle" + import { Queue } from "@vorsteh-queue/core" + + const { queueJobs } = postgresSchema + const relations = defineRelations({ queueJobs }) + + const pool = new Pool({ connectionString: process.env.DATABASE_URL }) + const db = drizzle({ client: pool, relations }) + + export const adapter = new PostgresQueueAdapter(db) + + export const emailQueue = new Queue(adapter, { name: "email-queue" }) + export const reportQueue = new Queue(adapter, { name: "report-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { PrismaClient } from "@prisma/client" + + import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" + import { Queue } from "@vorsteh-queue/core" + + const prisma = new PrismaClient() + + export const adapter = new PostgresPrismaQueueAdapter(prisma) + + export const emailQueue = new Queue(adapter, { name: "email-queue" }) + export const reportQueue = new Queue(adapter, { name: "report-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { Kysely, PostgresDialect } from "kysely" + import { Pool } from "pg" + + import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-kysely" + import { Queue } from "@vorsteh-queue/core" + + const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), + }), + }) + + export const adapter = new PostgresQueueAdapter(db) + + export const emailQueue = new Queue(adapter, { name: "email-queue" }) + export const reportQueue = new Queue(adapter, { name: "report-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { ZenStackClient } from "@zenstackhq/orm" + import { PostgresDialect } from "@zenstackhq/orm/dialects/postgres" + import { Pool } from "pg" + + import { PostgresZenstackQueueAdapter } from "@vorsteh-queue/adapter-zenstack" + import { Queue } from "@vorsteh-queue/core" + + import { schema } from "../zenstack/schema" + + const db = new ZenStackClient(schema, { + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), + }), + }) + + export const adapter = new PostgresZenstackQueueAdapter(db) + + export const emailQueue = new Queue(adapter, { name: "email-queue" }) + export const reportQueue = new Queue(adapter, { name: "report-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { DataSource } from "typeorm" + + import { + PostgresTypeormQueueAdapter, + QueueJobEntity, + } from "@vorsteh-queue/adapter-typeorm" + import { Queue } from "@vorsteh-queue/core" + + const dataSource = new DataSource({ + type: "postgres", + url: process.env.DATABASE_URL, + entities: [QueueJobEntity], + synchronize: true, + }) + + export const adapter = new PostgresTypeormQueueAdapter(dataSource) + + export const emailQueue = new Queue(adapter, { name: "email-queue" }) + export const reportQueue = new Queue(adapter, { name: "report-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { MikroORM } from "@mikro-orm/postgresql" + + import { + PostgresMikroormQueueAdapter, + QueueJobSchema, + } from "@vorsteh-queue/adapter-mikroorm" + import { Queue } from "@vorsteh-queue/core" + + const orm = await MikroORM.init({ + entities: [QueueJobSchema], + clientUrl: process.env.DATABASE_URL, + }) + + export const adapter = new PostgresMikroormQueueAdapter(orm) + + export const emailQueue = new Queue(adapter, { name: "email-queue" }) + export const reportQueue = new Queue(adapter, { name: "report-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { Sequelize } from "sequelize" + + import { + PostgresSequelizeQueueAdapter, + initQueueJobModel, + } from "@vorsteh-queue/adapter-sequelize" + import { Queue } from "@vorsteh-queue/core" + + const sequelize = new Sequelize(process.env.DATABASE_URL, { + dialect: "postgres", + logging: false, + }) + initQueueJobModel(sequelize) + + export const adapter = new PostgresSequelizeQueueAdapter(sequelize) + + export const emailQueue = new Queue(adapter, { name: "email-queue" }) + export const reportQueue = new Queue(adapter, { name: "report-queue" }) + ``` + + + + +## CLI and server configuration + +Your `queue.config.ts` simply imports from the shared file: + +```typescript +// queue.config.ts +import { adapter, emailQueue, reportQueue } from "./src/queues" + +export default { + adapter, + queues: [emailQueue, reportQueue], + defaultQueue: "email-queue", +} +``` + +## Worker setup + +Workers import the queue they process: + +```typescript +// src/worker.ts +import { emailQueue } from "./queues" + +emailQueue.register("send-email", async (job) => { + // handle job +}) + +emailQueue.start() +``` + +## Adding jobs from your app + +Application code imports the same queue instance to add jobs: + +```typescript +import { emailQueue } from "./queues" + +await emailQueue.add("send-email", { to: "user@example.com", subject: "Hello" }) +``` + +## Single queue simplification + +For projects with a single queue, the pattern is the same — just simpler: + + + + ```typescript + // src/queues.ts + import { defineRelations } from "drizzle-orm" + import { drizzle } from "drizzle-orm/node-postgres" + import { Pool } from "pg" + + import { + PostgresQueueAdapter, + postgresSchema, + } from "@vorsteh-queue/adapter-drizzle" + import { Queue } from "@vorsteh-queue/core" + + const { queueJobs } = postgresSchema + const relations = defineRelations({ queueJobs }) + + const pool = new Pool({ connectionString: process.env.DATABASE_URL }) + const db = drizzle({ client: pool, relations }) + + export const adapter = new PostgresQueueAdapter(db) + export const queue = new Queue(adapter, { name: "my-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { PrismaClient } from "@prisma/client" + + import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" + import { Queue } from "@vorsteh-queue/core" + + const prisma = new PrismaClient() + + export const adapter = new PostgresPrismaQueueAdapter(prisma) + export const queue = new Queue(adapter, { name: "my-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { Kysely, PostgresDialect } from "kysely" + import { Pool } from "pg" + + import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-kysely" + import { Queue } from "@vorsteh-queue/core" + + const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), + }), + }) + + export const adapter = new PostgresQueueAdapter(db) + export const queue = new Queue(adapter, { name: "my-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { ZenStackClient } from "@zenstackhq/orm" + import { PostgresDialect } from "@zenstackhq/orm/dialects/postgres" + import { Pool } from "pg" + + import { PostgresZenstackQueueAdapter } from "@vorsteh-queue/adapter-zenstack" + import { Queue } from "@vorsteh-queue/core" + + import { schema } from "../zenstack/schema" + + const db = new ZenStackClient(schema, { + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), + }), + }) + + export const adapter = new PostgresZenstackQueueAdapter(db) + export const queue = new Queue(adapter, { name: "my-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { DataSource } from "typeorm" + + import { + PostgresTypeormQueueAdapter, + QueueJobEntity, + } from "@vorsteh-queue/adapter-typeorm" + import { Queue } from "@vorsteh-queue/core" + + const dataSource = new DataSource({ + type: "postgres", + url: process.env.DATABASE_URL, + entities: [QueueJobEntity], + synchronize: true, + }) + + export const adapter = new PostgresTypeormQueueAdapter(dataSource) + export const queue = new Queue(adapter, { name: "my-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { MikroORM } from "@mikro-orm/postgresql" + + import { + PostgresMikroormQueueAdapter, + QueueJobSchema, + } from "@vorsteh-queue/adapter-mikroorm" + import { Queue } from "@vorsteh-queue/core" + + const orm = await MikroORM.init({ + entities: [QueueJobSchema], + clientUrl: process.env.DATABASE_URL, + }) + + export const adapter = new PostgresMikroormQueueAdapter(orm) + export const queue = new Queue(adapter, { name: "my-queue" }) + ``` + + + + ```typescript + // src/queues.ts + import { Sequelize } from "sequelize" + + import { + PostgresSequelizeQueueAdapter, + initQueueJobModel, + } from "@vorsteh-queue/adapter-sequelize" + import { Queue } from "@vorsteh-queue/core" + + const sequelize = new Sequelize(process.env.DATABASE_URL, { + dialect: "postgres", + logging: false, + }) + initQueueJobModel(sequelize) + + export const adapter = new PostgresSequelizeQueueAdapter(sequelize) + export const queue = new Queue(adapter, { name: "my-queue" }) + ``` + + + + +```typescript +// queue.config.ts +import { adapter, queue } from "./src/queues" + +export default { + adapter, + queues: [queue], +} +``` diff --git a/apps/docs/content/vorsteh-queue/01.getting-started/04.alternatives.mdx b/apps/docs/content/vorsteh-queue/01.getting-started/04.alternatives.mdx new file mode 100644 index 00000000..fd859f39 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/01.getting-started/04.alternatives.mdx @@ -0,0 +1,247 @@ +--- +title: Alternatives +description: Other job queue libraries for Node.js and when they might be a better fit. +--- + +Vorsteh Queue isn't the only option — and depending on your stack, it may not even be the best one. + +This page compares other established queue engines and where they shine. Vorsteh Queue is inspired by multiple projects in this space: we've learned a lot from Redis-first queues, Postgres-native workers, and scheduler-focused libraries. + +The goal is to bring those patterns together into one cohesive package — PostgreSQL durability, TypeScript-first DX, ORM adapters, and modern ops tooling — while staying honest about when a dedicated alternative is the better fit. + + + If you think something is wrong here, please feel free to raise an issue. + + +## BullMQ + +[BullMQ](https://bullmq.io/) is the most popular Node.js job queue, backed by Redis. + +- **Backend:** Redis +- **TypeScript:** First-class +- **Flows (DAG):** Yes (FlowProducer) +- **Web Dashboard:** community / paid + - community: [Bull Board](https://github.com/felixmosh/bull-board) + - paid: [Taskforce](https://taskforce.sh/) +- **Cron:** Yes (repeatable jobs) +- **Rate Limiting:** Yes + +**Choose BullMQ when:** + +- You already run Redis in production +- You need extremely high throughput (10k+ jobs/sec) +- You want the largest community and ecosystem + +**Choose Vorsteh Queue instead when:** + +- You want a **PostgreSQL-only stack** (no Redis) for queueing +- You want **enqueue + app data writes in the same PostgreSQL transaction** +- You want **first-class ORM adapters** (Drizzle, Prisma, Kysely, ZenStack, TypeORM, MikroORM, Sequelize) and typed DX around your schema + +## pg-boss + +[pg-boss](https://github.com/timgit/pg-boss) is a PostgreSQL-based job queue built on `SKIP LOCKED`. + +- **Backend:** PostgreSQL +- **TypeScript:** First-class +- **Flows (DAG):** No (supports dependency/workflow orchestration, but not a FlowProducer-style tree model) +- **Web Dashboard:** official (add-on) + - [@pg-boss/dashboard](https://github.com/timgit/pg-boss/tree/master/packages/dashboard) +- **CLI:** Yes +- **Cron:** Yes +- **Rate Limiting:** Yes (throttling) +- **DLQ:** Yes +- **ORM integration:** Transaction adapters (Drizzle, Prisma, Kysely, Knex) + +**Choose pg-boss when:** + +- You want a battle-tested, mature PostgreSQL queue +- You want strong operational fundamentals (DLQ, throttling, retries) +- You don't need FlowProducer-style DAG flows + +**Choose Vorsteh Queue instead when:** + +- You need **FlowProducer-style parent-child DAG trees** (children-first, parent waits, tree introspection) +- You want **end-to-end typed payload + typed result contracts** (not just "types exist") +- You want **built-in** monitoring/ops UX (dashboard/CLI) rather than relying on add-ons + +## Graphile Worker + +[Graphile Worker](https://github.com/graphile/worker) is a high-performance PostgreSQL job queue focused on simplicity and low latency. + +- **Backend:** PostgreSQL +- **TypeScript:** Types available +- **Flows (DAG):** No +- **Web Dashboard:** none +- **Cron:** Yes +- **Rate Limiting:** No (community package exists) + +**Choose Graphile Worker when:** + +- You want minimal dependencies and proven Postgres fundamentals +- You like a SQL-first approach +- You care about low latency (LISTEN/NOTIFY) + +**Choose Vorsteh Queue instead when:** + +- You want **built-in** progress tracking and richer job state APIs (not DIY via tables/logs) +- You need **FlowProducer-style DAG flows** +- You want **ORM adapters + schema-managed setup** rather than hand-managed SQL + +## Agenda + +[Agenda](https://github.com/agenda/agenda) is a job scheduler with pluggable persistence backends. + +- **Backend:** Pluggable (MongoDB, PostgreSQL, Redis) +- **TypeScript:** Types available +- **Flows (DAG):** No +- **Web Dashboard:** community + - [Agendash](https://github.com/agenda/agendash) +- **Cron:** Yes +- **Rate Limiting:** No (not a core feature) + +**Choose Agenda when:** + +- You want scheduler-first recurring jobs +- You're already in an ecosystem where Agenda is a good fit + +**Choose Vorsteh Queue instead when:** + +- You want a **queue-first system** (workers, retries/DLQ, visibility) rather than mainly a scheduler +- You want **PostgreSQL-backed durability** aligned with application data +- You need **flows / group FIFO / progress tracking** as first-class features + +## GroupMQ + +[GroupMQ](https://github.com/Openpanel-dev/groupmq) is a Redis-backed queue with first-class **per-group FIFO** ordering. + +- **Backend:** Redis +- **TypeScript:** First-class +- **Flows (DAG):** No +- **Web Dashboard:** community / paid + - community: [Bull Board integration](https://github.com/Openpanel-dev/groupmq) + - paid: [QueueDash integration](https://www.queuedash.com/docs/integrations/groupmq) +- **Cron:** Yes (repeatable jobs / cron patterns) +- **Group FIFO:** Yes +- **Rate Limiting:** No (not a core feature) + +**Choose GroupMQ when:** + +- Per-group FIFO ordering is the main requirement +- You already run Redis and want strict sequential processing per group + +**Choose Vorsteh Queue instead when:** + +- You want **PostgreSQL** instead of Redis for queue storage +- You want **one system** that combines **flows + cron + progress + monitoring** on Postgres +- You want **ORM adapters** and schema management integrated with your app + +## Platformatic Job Queue + +[Platformatic Job Queue](https://github.com/platformatic/job-queue) is a TypeScript queue with pluggable storage and a request/response pattern. + +- **Backend:** Pluggable (Memory, Redis/Valkey, Filesystem) +- **TypeScript:** First-class +- **Flows (DAG):** No +- **Web Dashboard:** none +- **CLI:** none +- **Cron:** No +- **Deduplication:** Yes +- **Request/Response:** Yes (enqueue and await result) + +**Choose Platformatic Job Queue when:** + +- You want enqueue-and-wait request/response semantics +- You need deduplication built-in +- You're in the Platformatic ecosystem + +**Choose Vorsteh Queue instead when:** + +- You want **PostgreSQL-backed durability** and multi-instance workers +- You need **cron scheduling** and **flow orchestration** (DAG flows) +- You want **monitoring UX** (dashboard/CLI) designed for ops, not just an API + +## prisma-queue + +[prisma-queue](https://github.com/mgcrea/prisma-queue) is a lightweight Prisma-based PostgreSQL queue. + +- **Backend:** PostgreSQL +- **TypeScript:** First-class +- **ORM Integration:** Prisma +- **Flows (DAG):** No +- **Web Dashboard:** none +- **CLI:** none +- **Cron:** Yes (cron syntax scheduling) +- **Rate Limiting:** No + +**Choose prisma-queue when:** + +- You want a small Prisma-only queue with minimal surface area +- You're happy to bring your own monitoring/ops + +**Choose Vorsteh Queue instead when:** + +- You want **multiple ORM adapters** (not Prisma-only) +- You need **FlowProducer-style DAG flows** and built-in progress reporting +- You want **dashboard + CLI** for monitoring instead of building your own + +## BunQueue + +[BunQueue](https://bunqueue.dev/) is a high-performance queue for the Bun runtime using SQLite persistence. + +- **Backend:** SQLite (Bun-native) +- **TypeScript:** First-class +- **Flows (DAG):** Yes (workflows / parent-child flows) +- **Web Dashboard:** official (beta / early access) +- **CLI:** Yes +- **Cron:** Yes +- **DLQ:** Yes +- **Rate Limiting:** Yes + +**Choose BunQueue when:** + +- You run Bun in production and want a native Bun queue +- You like SQLite persistence (single-file deployments) +- You want workflows in the Bun ecosystem + +**Choose Vorsteh Queue instead when:** + +- You need **Node.js** compatibility (not Bun-only) +- You want **PostgreSQL** for multi-instance / HA worker setups +- You want **ORM adapters** (Drizzle, Prisma, Kysely, ZenStack, TypeORM, MikroORM, Sequelize) in a Postgres-first design + +## Comparison + +**Terminology** + +- **Flows (DAG):** FlowProducer-style parent-child job trees (children run first; parent waits; flow tree can be inspected). +- **Web Dashboard:** browser-based monitoring UI (not logs/metrics). +- **Progress:** built-in API for reporting and querying job progress. + +### Backend & Developer Experience + +| Library | Backend | TypeScript | ORM Integration | +| --- | --- | --- | --- | +| Vorsteh Queue | PostgreSQL | First-class | Drizzle, Prisma, Kysely, ZenStack, TypeORM, MikroORM, Sequelize | +| BullMQ | Redis | First-class | — | +| pg-boss | PostgreSQL | First-class | Tx adapters (Drizzle, Prisma, Kysely, Knex) | +| Graphile Worker | PostgreSQL | Types avail. | — | +| Agenda | Pluggable (MongoDB, PostgreSQL, Redis) | Types avail. | — | +| GroupMQ | Redis | First-class | — | +| Platformatic | Pluggable (Memory, Redis/Valkey, FS) | First-class | — | +| prisma-queue | PostgreSQL | First-class | Prisma | +| BunQueue | SQLite | First-class | — | + +### Features + +| Library | Flows (DAG) | Progress | Web Dashboard | CLI | Cron | Rate Limit | Group FIFO | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Vorsteh Queue | Yes | Yes | built-in | Yes | Yes | Yes | Yes | +| BullMQ | Yes | Yes | community / paid | No | Yes | Yes | No | +| pg-boss | No | No | official (add-on) | Yes | Yes | Yes | No | +| Graphile Worker | No | No | none | No | Yes | No | No | +| Agenda | No | No | community | No | Yes | No | No | +| GroupMQ | No | No | community / paid | No | Yes | No | Yes | +| Platformatic | No | No | none | No | No | No | No | +| prisma-queue | No | No | none | No | Yes | No | No | +| BunQueue | Yes | No | official (beta) | Yes | Yes | Yes | No | diff --git a/apps/docs/content/vorsteh-queue/01.getting-started/05.about.mdx b/apps/docs/content/vorsteh-queue/01.getting-started/05.about.mdx new file mode 100644 index 00000000..2c622023 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/01.getting-started/05.about.mdx @@ -0,0 +1,21 @@ +--- +title: About Vorsteh Queue +navTitle: About +description: The story behind the name and the project. +--- + +The name "Vorsteh Queue" is a tribute to our beloved German Spaniel. This breed is closely related to the Münsterländer, a type of pointing dog — known in German as a "Vorstehhund". + +Just as a pointing dog steadfastly indicates its target, Vorsteh Queue aims to reliably point your application towards efficient and robust background job processing. + +The inspiration for naming a tech project after a dog comes from the delightful story of **Bruno**, the API client. It's a nod to the personal touch and passion that drives open-source development, much like the loyalty and dedication of our canine companions. + +## Meet Raja + +Raja, the German Spaniel who inspired the Vorsteh Queue logo + +This is Raja — our German Spaniel and the direct inspiration behind the Vorsteh Queue logo. diff --git a/apps/docs/content/vorsteh-queue/01.getting-started/index.mdx b/apps/docs/content/vorsteh-queue/01.getting-started/index.mdx new file mode 100644 index 00000000..15aa0b87 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/01.getting-started/index.mdx @@ -0,0 +1,4 @@ +--- +title: Getting Started +description: Learn how to set up and use Vorsteh Queue in your Node.js project. +--- diff --git a/apps/docs/content/vorsteh-queue/02.core/01.queue.mdx b/apps/docs/content/vorsteh-queue/02.core/01.queue.mdx new file mode 100644 index 00000000..768e27ea --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/01.queue.mdx @@ -0,0 +1,138 @@ +--- +title: Queue +description: Create and manage job queues — add jobs, control processing, and monitor statistics. +--- + +The `Queue` class is the producer-side API. It manages adding jobs, cancellation, statistics, and flows. It does **not** process jobs — that responsibility belongs to the [Worker](/docs/vorsteh-queue/core/worker). + +## Creating a Queue + +A Queue requires an adapter (database connection) and a configuration with a name: + +```typescript +import { Queue, MemoryQueueAdapter } from "@vorsteh-queue/core" + +const adapter = new MemoryQueueAdapter() +const queue = new Queue(adapter, { name: "my-queue" }) + +await queue.connect() +``` + +For production, replace `MemoryQueueAdapter` with a PostgreSQL adapter: + +```typescript +import { drizzle } from "drizzle-orm/node-postgres" +import { Pool } from "pg" +import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" +import { Queue } from "@vorsteh-queue/core" + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }) +const db = drizzle(pool) +const adapter = new PostgresQueueAdapter(db) +const queue = new Queue(adapter, { name: "email-queue" }) + +await queue.connect() +``` + +## Public Getters + +```typescript +queue.name // The queue name ("email-queue") +queue.adapter // The adapter instance +``` + +## Adding Jobs + +```typescript +// Basic job +await queue.add("send-email", { to: "user@example.com", subject: "Welcome!" }) + +// Job with options +await queue.add( + "send-email", + { to: "user@example.com", subject: "Urgent" }, + { + priority: 1, // lower number = higher priority (default: 2) + delay: 5000, // delay 5 seconds before processing + maxAttempts: 5, // retry up to 5 times on failure + } +) + +// Batch add (all jobs get same options) +await queue.addJobs("send-email", [ + { to: "a@example.com", subject: "Hello" }, + { to: "b@example.com", subject: "Hello" }, +]) +``` + +See [Job Types](/docs/vorsteh-queue/core/job-types) for all scheduling patterns (delayed, cron, repeating, unique, grouped). + +## Cancellation + +```typescript +// Cancel a single job +await queue.cancel(jobId, "No longer needed") + +// Cancel multiple jobs by filter +const count = await queue.cancelAll({ name: "send-email", status: "pending" }) +``` + +## Statistics + +```typescript +const stats = await queue.getStats() +// { pending, delayed, processing, completed, failed, cancelled, dead, "waiting-children" } + +// Clear jobs +await queue.clear() // Clear all jobs +await queue.clear("completed") // Clear only completed jobs +``` + +## Dead Letter Queue + +```typescript +// Get dead jobs +const deadJobs = await queue.getDeadJobs({ limit: 20 }) + +// Redrive a single dead job back to pending +await queue.redrive(jobId) + +// Redrive all dead jobs (optionally filtered by handler name) +await queue.redriveAll({ name: "send-email" }) +``` + +## Job Lookup + +```typescript +const job = await queue.getJob(jobId) +// Returns Job | null +``` + +## Signals (for step.waitFor) + +Send a signal to a job that is waiting via `step.waitFor`: + +```typescript +await queue.signal(jobId, "manager-approved", { approved: true }) +``` + +## Lifecycle + +```typescript +await queue.connect() // Connect adapter to database +await queue.disconnect() // Disconnect adapter +``` + +## Queue Configuration + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `string` | required | Queue name for job isolation | +| `defaultJobOptions` | `Partial` | `{}` | Default options applied to all jobs | +| `telemetry` | `Telemetry` | `noopTelemetry` | Optional telemetry instance for observability | +| `removeOnComplete` | `boolean \| number` | `100` | Auto-remove completed jobs (true = immediate, number = keep N) | +| `removeOnFail` | `boolean \| number` | `50` | Auto-remove failed jobs | + +## Project Organization + +For multi-queue setups, see [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). diff --git a/apps/docs/content/vorsteh-queue/02.core/02.job-types.mdx b/apps/docs/content/vorsteh-queue/02.core/02.job-types.mdx new file mode 100644 index 00000000..6f2c120c --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/02.job-types.mdx @@ -0,0 +1,144 @@ +--- +title: Job Types +description: Delayed jobs, recurring jobs with cron expressions, interval-based repetition, and unique jobs. +--- + +Vorsteh Queue supports multiple job patterns beyond simple fire-and-forget. This page covers all the ways you can configure a job when adding it to the queue. + +## Methods + +Three methods are available for adding jobs: + +| Method | Description | +| --- | --- | +| `queue.add(name, payload, options?)` | Add a single job | +| `queue.addJobs(name, payloads, options?)` | Add multiple jobs of the same type in a batch | +| `queue.enqueueAndWait(name, payload, options?)` | Add a job and await its result | +| `queue.addFlow(flow)` | Add a parent job with children — see [Flows](/docs/vorsteh-queue/core/flows/overview) | + +## Immediate Jobs + +The simplest form — a job that is processed as soon as a worker picks it up: + +```typescript +await queue.add("send-email", { to: "user@example.com", subject: "Welcome!" }) +``` + +## Delayed Jobs + +Schedule a job to run after a specified delay: + +```typescript +await queue.add( + "cleanup", + { type: "temp-files" }, + { + delay: 60000, // 1 minute from now + } +) +``` + +Or schedule for a specific date/time: + +```typescript +await queue.add( + "reminder", + { userId: "123" }, + { + runAt: new Date("2025-03-01T09:00:00Z"), + } +) +``` + +## Recurring Jobs (Cron) + +Use standard cron expressions for recurring jobs: + +```typescript +await queue.add( + "daily-report", + { date: new Date().toISOString() }, + { + cron: "0 9 * * *", // Every day at 9 AM + timezone: "America/New_York", + } +) +``` + +Cron expressions follow the standard 5-field format. Timezone support uses the IANA timezone database with UTC as the storage format. + + + Need help writing cron expressions? Try + [natural-cron](https://github.com/satyajitnayk/natural-cron) to generate them + from human-readable descriptions. + + +## Interval-based Repetition + +Repeat a job at fixed intervals: + +```typescript +await queue.add( + "health-check", + { endpoint: "/api/health" }, + { + repeat: { + every: 30000, // Every 30 seconds + limit: 10, // Maximum 10 executions + }, + } +) +``` + +## Unique Jobs + +Prevent duplicate jobs using a deduplication key: + +```typescript +await queue.add( + "sync-user", + { userId: "123" }, + { + unique: { + key: "sync-user-123", + action: "reject", // or "replace" + }, + } +) +``` + +## Group FIFO + +Ensure jobs within the same group are processed sequentially: + +```typescript +await queue.add( + "process-order", + { orderId: "abc" }, + { + group: "customer-42", + } +) +``` + +## Enqueue and Wait + +Add a job and block until it completes, fails, or times out. Useful for request/response patterns where the caller needs the result: + +```typescript +const result = await queue.enqueueAndWait( + "process-image", + { url: "https://example.com/img.png" }, + { waitTimeout: 60000 } +) + +console.log(result) // { thumbnail: "..." } +``` + +The method uses event listeners (fast, in-process) with database polling as a cross-process fallback. It throws typed errors for failures, cancellations, timeouts, and dead-letter moves. + +## Options + + + +All timestamps are stored as UTC internally, regardless of the timezone specified in scheduling options. diff --git a/apps/docs/content/vorsteh-queue/02.core/03.flows/01.overview.mdx b/apps/docs/content/vorsteh-queue/02.core/03.flows/01.overview.mdx new file mode 100644 index 00000000..2868b2fc --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/03.flows/01.overview.mdx @@ -0,0 +1,65 @@ +--- +title: Overview +description: Orchestrate complex job workflows using flow trees, multi-step execution, and job dependencies. +--- + +Vorsteh Queue provides three complementary patterns for orchestrating work beyond simple independent jobs: + +## Flow Trees + +Declarative parent-child job hierarchies. Children are processed first; the parent only runs when all children complete. Ideal for fan-out/fan-in patterns like build pipelines. + +```typescript +const flow = await queue.addFlow({ + name: "deploy", + payload: { version: "1.0" }, + children: [ + { name: "build", payload: { target: "linux" } }, + { name: "build", payload: { target: "macos" } }, + ], +}) +``` + +[Learn more about Flow Trees →](/docs/vorsteh-queue/core/flows/flow-trees) + +## Steps + +Multi-step job execution with durable state, sleep/wait primitives, and saga-style compensation. Each step is idempotent — on retry, completed steps replay from cache. + +```typescript +worker.register("onboarding", async (job, { step }) => { + const user = await step.run("create-user", () => createUser(job.payload), { + compensate: (result) => deleteUser(result.id), + }) + await step.sleep("cooldown", "1h") + await step.run("send-welcome", () => sendEmail(user.email)) + return { userId: user.id } +}) +``` + +[Learn more about Steps →](/docs/vorsteh-queue/core/flows/steps) + +## Dependencies + +Lightweight job-to-job ordering. A job with `dependsOn` won't be picked for processing until all referenced jobs have completed. Useful when you don't need a full tree structure but want to ensure ordering. + +```typescript +const jobA = await queue.add("prepare-data", { source: "s3" }) +await queue.add( + "analyze", + { report: true }, + { + dependsOn: [jobA.id], + } +) +``` + +[Learn more about Dependencies →](/docs/vorsteh-queue/core/flows/dependencies) + +## When to Use What + +| Pattern | Use Case | Coordination | +| --- | --- | --- | +| **Flow Trees** | Fan-out/fan-in, pipelines, DAGs | Parent waits for all children | +| **Steps** | Multi-stage processing within a single job | Sequential steps with retry/compensation | +| **Dependencies** | Simple ordering between independent jobs | Job waits for specific other jobs | diff --git a/apps/docs/content/vorsteh-queue/02.core/03.flows/02.flow-trees.mdx b/apps/docs/content/vorsteh-queue/02.core/03.flows/02.flow-trees.mdx new file mode 100644 index 00000000..850c0587 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/03.flows/02.flow-trees.mdx @@ -0,0 +1,106 @@ +--- +title: Flows +description: Parent-child job trees where children run first and the parent waits until all are complete. +--- + +Flows let you define a tree of jobs with parent-child relationships. Children are processed first, and the parent only becomes eligible for processing once all its children complete. This enables DAG-style orchestration without external coordinators. + +## Creating a Flow + +Use `queue.addFlow()` with a declarative tree definition: + +```typescript +const flow = await queue.addFlow({ + name: "deploy", + payload: { version: "1.2.0" }, + children: [ + { name: "build", payload: { target: "linux" } }, + { name: "build", payload: { target: "macos" } }, + { + name: "test", + payload: {}, + children: [{ name: "lint", payload: {} }], + }, + ], +}) + +console.log(flow.id) // shared flow ID +console.log(flow.job.id) // root job ID (status: "waiting-children") +``` + +## How It Works + +1. `addFlow` traverses the tree and creates all jobs. Leaf nodes get status `"pending"`, parent nodes get status `"waiting-children"`. +2. Workers pick up leaf jobs first (since they're `"pending"`). +3. When a child completes, the Worker checks if all siblings are done. If yes, the parent is promoted to `"pending"` and becomes eligible for processing. +4. This cascades up the tree until the root job runs. + +``` +deploy (waiting-children) +├── build:linux (pending) → processed first +├── build:macos (pending) → processed first +└── test (waiting-children) + └── lint (pending) → processed first +``` + +## Accessing Children Results + +Inside a handler, use `ctx.getChildrenResults()` to access completed children: + +```typescript +worker.register("deploy", async (job, ctx) => { + const childResults = await ctx.getChildrenResults() + // Map + + console.log(`All ${childResults.size} children completed`) + return { deployed: true } +}) +``` + +## Failure Propagation + +By default, a failing child does not affect the parent. Set `failParentOnFailure: true` to cascade failures upward: + +```typescript +const flow = await queue.addFlow({ + name: "pipeline", + payload: {}, + children: [ + { + name: "critical-step", + payload: {}, + failParentOnFailure: true, // parent fails if this child fails + }, + { + name: "optional-step", + payload: {}, + // parent continues even if this fails + }, + ], +}) +``` + +When a child with `failParentOnFailure: true` moves to the dead-letter queue, its parent is immediately marked as failed. If the parent also has `failParentOnFailure`, the failure cascades further up the tree. + +## Inspecting a Flow + +Retrieve the full tree structure for visualization or debugging: + +```typescript +const tree = await queue.getFlowTree(flow.id) + +if (tree) { + console.log(tree.job.name, tree.job.status) + for (const child of tree.children) { + console.log(` ${child.job.name}: ${child.job.status}`) + } +} +``` + +## Flow Definition + + + +## Flow Node (Tree Output) + + diff --git a/apps/docs/content/vorsteh-queue/02.core/03.flows/03.steps.mdx b/apps/docs/content/vorsteh-queue/02.core/03.flows/03.steps.mdx new file mode 100644 index 00000000..88664ad2 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/03.flows/03.steps.mdx @@ -0,0 +1,163 @@ +--- +title: Steps +description: Multi-step job execution with durable state, sleep/wait primitives, and saga-style compensation. +--- + +Steps let you break a job handler into named, durable stages. Each step is idempotent — if a job is retried, completed steps replay their cached results without re-executing. This makes long-running workflows safe to retry. + +## Basic Usage + +Access the step API via the second argument in your handler: + +```typescript +worker.register("process-order", async (job, { step }) => { + const payment = await step.run("charge", () => + chargeCard(job.payload.cardId, job.payload.amount) + ) + + await step.run("fulfill", () => + shipOrder(job.payload.orderId, payment.transactionId) + ) + + await step.run("notify", () => + sendConfirmation(job.payload.email, payment.transactionId) + ) + + return { transactionId: payment.transactionId } +}) +``` + +On retry, if `"charge"` already completed, its result is returned from cache and the function is not called again. + +## step.run + +Execute a named step. Returns the cached result on replay. + +```typescript +const result = await step.run("step-name", async () => { + // your logic here + return { data: "value" } +}) +``` + +### With Compensation (Saga Pattern) + +Provide a `compensate` function to undo the step if a later step fails: + +```typescript +worker.register("onboarding", async (job, { step }) => { + const user = await step.run("create-user", () => createUser(job.payload), { + compensate: (result) => deleteUser(result.id), + }) + + const subscription = await step.run( + "create-subscription", + () => createSubscription(user.id), + { + compensate: (result) => cancelSubscription(result.id), + } + ) + + // If this step throws, compensations run in reverse order: + // cancelSubscription → deleteUser + await step.run("send-welcome", () => sendEmail(user.email)) + + return { userId: user.id, subscriptionId: subscription.id } +}) +``` + +Compensations run in reverse order (last registered first), following the saga pattern. If a compensation itself fails, it logs the error and continues running remaining compensations. + +## step.sleep + +Pause execution for a duration. The job is moved to `"delayed"` status and resumes after the specified time. + +```typescript +worker.register("drip-campaign", async (job, { step }) => { + await step.run("send-day-1", () => sendEmail(job.payload.email, "welcome")) + + await step.sleep("wait-3-days", "3d") + + await step.run("send-day-4", () => sendEmail(job.payload.email, "tips")) + + await step.sleep("wait-7-days", "7d") + + await step.run("send-day-11", () => sendEmail(job.payload.email, "offer")) +}) +``` + +Supported duration formats: + +| Format | Example | Description | +| -------- | --------- | ------------ | +| `number` | `5000` | Milliseconds | +| `"Nms"` | `"500ms"` | Milliseconds | +| `"Ns"` | `"30s"` | Seconds | +| `"Nm"` | `"5m"` | Minutes | +| `"Nh"` | `"1h"` | Hours | +| `"Nd"` | `"3d"` | Days | + +## step.waitFor + +Pause execution until an external signal is received. Useful for human-in-the-loop workflows or external system callbacks. + +```typescript +worker.register("approval-flow", async (job, { step }) => { + await step.run("submit", () => createApprovalRequest(job.payload)) + + const approval = await step.waitFor("wait-approval", "manager-approved", { + timeout: "24h", + }) + + if (approval.approved) { + await step.run("provision", () => provisionAccess(job.payload.userId)) + } + + return { approved: approval.approved } +}) +``` + +Send a signal to a waiting job using `queue.sendSignal()`: + +```typescript +await queue.sendSignal(jobId, "manager-approved", { + approved: true, + approvedBy: "manager@company.com", +}) +``` + +If the timeout expires before the signal arrives, the step fails. + +## step.all + +Run multiple async operations in parallel within a step context: + +```typescript +worker.register("parallel-fetch", async (job, { step }) => { + const [users, orders] = await step.all([ + step.run("fetch-users", () => fetchUsers()), + step.run("fetch-orders", () => fetchOrders()), + ]) + + return { users, orders } +}) +``` + +## How Retries Work with Steps + +When a job with steps is retried: + +1. The step context loads previously completed steps from the database +2. Each `step.run` checks if it already has a cached result +3. Completed steps return their cached result instantly (no re-execution) +4. Execution resumes from the first incomplete step + +This makes steps safe for operations with side effects (payments, emails, API calls) — they won't be duplicated on retry. + +## Step Context API + + + +## Step State + + diff --git a/apps/docs/content/vorsteh-queue/02.core/03.flows/04.dependencies.mdx b/apps/docs/content/vorsteh-queue/02.core/03.flows/04.dependencies.mdx new file mode 100644 index 00000000..adda946f --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/03.flows/04.dependencies.mdx @@ -0,0 +1,81 @@ +--- +title: Dependencies +description: Declare ordering constraints between jobs using dependsOn for lightweight coordination. +--- + +Dependencies let you declare that a job should only be processed after specific other jobs have completed. This is lighter than a full flow tree — you don't need to define a parent, and the jobs remain independent apart from their ordering constraint. + +## Basic Usage + +Pass `dependsOn` with an array of job IDs when adding a job: + +```typescript +const dataJob = await queue.add("prepare-data", { source: "s3://bucket/raw" }) +const transformJob = await queue.add("transform", { format: "parquet" }) + +// This job only runs after both dependencies complete +await queue.add( + "generate-report", + { output: "s3://bucket/report.pdf" }, + { + dependsOn: [dataJob.id, transformJob.id], + } +) +``` + +The `"generate-report"` job stays in `"pending"` but won't be picked by a worker until both `"prepare-data"` and `"transform"` have status `"completed"`. + +## Dependency Failure + +Control what happens when a dependency fails using `onDependencyFailure`: + +```typescript +await queue.add( + "deploy", + { version: "2.0" }, + { + dependsOn: [buildJob.id, testJob.id], + onDependencyFailure: "fail", // "fail" | "cancel" + } +) +``` + +| Action | Behavior | +| ---------- | -------------------------------------------------- | +| `"fail"` | Dependent job is marked as failed (default) | +| `"cancel"` | Dependent job is cancelled when a dependency fails | + +## Circular Dependency Detection + +Vorsteh Queue detects circular dependencies at enqueue time and throws a `CircularDependencyError`: + +```typescript +import { CircularDependencyError } from "@vorsteh-queue/core" + +try { + const jobA = await queue.add("step-a", {}) + const jobB = await queue.add("step-b", {}, { dependsOn: [jobA.id] }) + // This would create a cycle: A → B → A + await queue.add("step-a-v2", {}, { dependsOn: [jobB.id] }) +} catch (error) { + if (error instanceof CircularDependencyError) { + console.error("Cycle detected:", error.cycle) + // e.g. ["step-a-v2", "jobB-id", "jobA-id", "step-a-v2"] + } +} +``` + +The detection walks the dependency graph before inserting the job, preventing infinite loops. + +## Dependencies vs Flow Trees + +| | Dependencies | Flow Trees | +| --- | --- | --- | +| **Definition** | `dependsOn: [jobId]` on individual jobs | Declarative tree via `queue.addFlow()` | +| **Coordination** | Job waits for specific IDs to complete | Parent waits for all children | +| **Results** | No built-in result passing | Parent accesses children results via `ctx.getChildrenResults()` | +| **Failure cascade** | Configurable per-job (`onDependencyFailure`) | Configurable per-child (`failParentOnFailure`) | +| **Use case** | Simple ordering between independent jobs | Structured pipelines with fan-out/fan-in | + +- Use dependencies when you need lightweight ordering without the overhead of a tree structure. +- Use flow trees when you need result aggregation, tree introspection, or complex DAG patterns. diff --git a/apps/docs/content/vorsteh-queue/02.core/03.flows/index.mdx b/apps/docs/content/vorsteh-queue/02.core/03.flows/index.mdx new file mode 100644 index 00000000..651743ad --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/03.flows/index.mdx @@ -0,0 +1,8 @@ +--- +title: Flows +description: Workflow orchestration with flow trees, durable steps, and job dependencies. +--- + +Flows help you orchestrate jobs across multiple stages and dependency constraints. + +Use flow trees for structured parent-child pipelines, steps for durable resumable workflows, and dependencies for lightweight ordering between independent jobs. diff --git a/apps/docs/content/vorsteh-queue/02.core/04.events.mdx b/apps/docs/content/vorsteh-queue/02.core/04.events.mdx new file mode 100644 index 00000000..9fe7a8af --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/04.events.mdx @@ -0,0 +1,91 @@ +--- +title: Events +description: Listen to job lifecycle and queue control events for monitoring and debugging. +--- + +The event system provides real-time notifications for job state changes and queue operations. Both `Queue` (producer) and `Worker` (consumer) emit typed events you can subscribe to with `.on()`, `.once()`, and `.off()`. + +## Usage + +### Queue Events + +The `Queue` instance emits events when jobs are added or their state changes through the public API (e.g. cancellation). + +```typescript +import { Queue, MemoryQueueAdapter } from "@vorsteh-queue/core" + +const adapter = new MemoryQueueAdapter() +const queue = new Queue(adapter, { name: "my-queue" }) + +queue.on("job:added", (job) => { + console.log(`[${job.name}] added with priority ${job.priority}`) +}) + +queue.on("job:cancelled", (job) => { + console.log(`[${job.id}] cancelled: ${job.cancellationReason}`) +}) +``` + +### Worker Events + +The `Worker` instance emits events during job processing — lifecycle transitions, errors, and batch operations. + +```typescript +import { Worker } from "@vorsteh-queue/core" + +const worker = new Worker(adapter, { name: "my-queue", concurrency: 5 }) + +worker.on("job:completed", (job) => { + console.log(`[${job.name}] completed in queue`) +}) + +worker.on("job:failed", (job) => { + console.error(`[${job.name}] failed:`, job.error.message) +}) + +worker.on("job:dead", (job) => { + // Job exhausted all retry attempts + console.error(`[${job.name}] moved to DLQ after ${job.attempts} attempts`) +}) + +worker.on("worker:error", (error) => { + // Unexpected error in polling loop + console.error("Unexpected worker error:", error) +}) +``` + +### One-Time Listeners + +Use `.once()` to listen for a single occurrence: + +```typescript +worker.once("worker:started", () => { + console.log("Worker is ready") +}) +``` + +### Removing Listeners + +```typescript +const handler = (job) => console.log(job.id) +worker.on("job:completed", handler) +worker.off("job:completed", handler) +``` + +## Available Events + +### Queue Events + + + +### Worker Events + + + +## Use Cases + +- Logging and audit trails +- Custom metrics collection +- Chaining dependent operations +- Notifications on failures +- Real-time dashboards (via the server subscription bridge) diff --git a/apps/docs/content/vorsteh-queue/02.core/05.progress-tracking.mdx b/apps/docs/content/vorsteh-queue/02.core/05.progress-tracking.mdx new file mode 100644 index 00000000..54380193 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/05.progress-tracking.mdx @@ -0,0 +1,47 @@ +--- +title: Progress Tracking +description: Report and monitor real-time job progress within handlers. +--- + +Jobs can report progress (0–100) during execution, useful for long-running operations like batch processing or file uploads. + +## Reporting Progress + +Use `job.updateProgress()` inside your handler: + +```typescript +interface BatchPayload { + items: string[] +} + +interface BatchResult { + processed: number +} + +queue.register("process-batch", async (job) => { + const { items } = job.payload + + for (let i = 0; i < items.length; i++) { + await processItem(items[i]) + await job.updateProgress(Math.round(((i + 1) / items.length) * 100)) + } + + return { processed: items.length } +}) +``` + +## Listening to Progress + +Subscribe to progress updates via the event system: + +```typescript +queue.on("job:progress", (job) => { + console.log(`[${job.name}] ${job.progress}%`) +}) +``` + +## Notes + +- Progress is stored as an integer between 0 and 100 +- Updates are persisted to the database, surviving restarts +- Calling `updateProgress` with the same value is a no-op (no extra writes) diff --git a/apps/docs/content/vorsteh-queue/02.core/05.worker.mdx b/apps/docs/content/vorsteh-queue/02.core/05.worker.mdx new file mode 100644 index 00000000..5ecbf82f --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/05.worker.mdx @@ -0,0 +1,170 @@ +--- +title: Worker +description: Process jobs with handlers, concurrency, retries, timeouts, and batch processing. +--- + +The `Worker` class is the consumer-side component. It polls for jobs, executes handlers, manages retries, timeouts, and emits lifecycle events. A Worker connects to the same adapter as the Queue and processes jobs from the same named queue. + +## Creating a Worker + +```typescript +import { Worker, MemoryQueueAdapter } from "@vorsteh-queue/core" + +const adapter = new MemoryQueueAdapter() +const worker = new Worker(adapter, { + name: "email-queue", // must match the Queue name + concurrency: 5, // process up to 5 jobs simultaneously + pollInterval: 100, // poll every 100ms (default) +}) +``` + +In production, the adapter is shared with the Queue: + +```typescript +import { drizzle } from "drizzle-orm/node-postgres" +import { Pool } from "pg" +import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" +import { Queue, Worker } from "@vorsteh-queue/core" + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }) +const db = drizzle(pool) +const adapter = new PostgresQueueAdapter(db) + +const queue = new Queue(adapter, { name: "email-queue" }) +const worker = new Worker(adapter, { name: "email-queue", concurrency: 5 }) +``` + +## Registering Handlers + +Each job type needs a handler registered on the Worker: + +```typescript +interface EmailPayload { + to: string + subject: string + body: string +} + +interface EmailResult { + messageId: string +} + +worker.register( + "send-email", + async (job, { signal }) => { + const id = await sendEmail(job.payload, { signal }) + return { messageId: id } + } +) +``` + +The handler receives: + +- `job` — the full Job object with payload, metadata, and a `updateProgress()` method +- `ctx` — context with `signal` (AbortSignal for timeout/cancellation), `step` (multi-step API), and `getChildrenResults` (for flow parents) + +## Starting and Stopping + +```typescript +worker.start() // Begin polling and processing +worker.pause() // Stop picking new jobs (in-flight jobs continue) +worker.resume() // Resume polling +await worker.stop() // Graceful shutdown (waits for active jobs) +``` + +## Batch Processing + +Process multiple jobs of the same type in a single handler call: + +```typescript +worker.registerBatch( + "index-documents", + async (jobs, { signal }) => { + const results = await bulkIndex( + jobs.map((j) => j.payload), + { signal } + ) + return results // must return one result per job + }, + { + minSize: 5, // wait for at least 5 jobs before processing + maxSize: 50, // process up to 50 at once + } +) +``` + +## Per-Handler Options + +```typescript +worker.register("send-email", handler, { + concurrency: 3, // max 3 concurrent jobs for this handler + rateLimit: { + max: 100, // max 100 jobs + duration: 60_000, // per 60 seconds + }, +}) +``` + +## Event Triggers + +Automatically create follow-up jobs when a job completes: + +```typescript +worker.trigger({ + on: "process-image", // when this job completes... + create: "send-notification", // ...create this job + data: (result, job) => ({ + // ...with this payload + userId: job.payload.userId, + imageUrl: result.url, + }), + condition: (result) => result.success, // only when condition is met +}) +``` + +## Worker Properties + +```typescript +worker.isRunning // boolean — is the worker polling? +worker.activeJobCount // number — currently processing jobs +worker.name // string — the queue name +``` + +## Cancellation + +Cancel a specific job being processed by this worker: + +```typescript +await worker.cancelJob(jobId, "User requested cancellation") +``` + +## Worker Configuration + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `string` | required | Queue name to consume from (must match Queue) | +| `concurrency` | `number` | `1` | Max concurrent jobs | +| `pollInterval` | `number` | `100` | Polling interval in ms | +| `telemetry` | `Telemetry` | `noopTelemetry` | Optional telemetry instance | +| `retryStrategy` | `RetryStrategyConfig` | exponential backoff | Retry delay strategy | +| `removeOnComplete` | `boolean \| number` | `100` | Auto-remove completed jobs | +| `removeOnFail` | `boolean \| number` | `50` | Auto-remove failed jobs | + +## Retry Strategy + +```typescript +const worker = new Worker(adapter, { + name: "email-queue", + retryStrategy: { + type: "exponential", // "exponential" | "linear" | "fixed" + delay: 1000, // base delay in ms + maxDelay: 30000, // cap for exponential/linear + }, +}) + +// Or a custom function: +const worker = new Worker(adapter, { + name: "email-queue", + retryStrategy: (attempt) => Math.min(1000 * 2 ** attempt, 60000), +}) +``` diff --git a/apps/docs/content/vorsteh-queue/02.core/06.error-handling.mdx b/apps/docs/content/vorsteh-queue/02.core/06.error-handling.mdx new file mode 100644 index 00000000..b205c572 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/06.error-handling.mdx @@ -0,0 +1,63 @@ +--- +title: Error Handling +description: Retry logic, timeouts, dead letter queue, and job failure management. +--- + +Vorsteh Queue provides multiple layers of error handling to ensure reliable job processing. + +## Retry Logic + +Jobs are retried automatically when they fail, up to the configured maximum: + +```typescript +await queue.add("risky-job", payload, { + maxAttempts: 5, // Retry up to 5 times (default: 3) +}) +``` + +After all attempts are exhausted, the job moves to the **dead letter queue** (status: `dead`). + +## Job Timeouts + +Prevent stuck jobs with a timeout: + +```typescript +await queue.add("long-task", payload, { + timeout: 30000, // 30 seconds +}) +``` + +If a job exceeds the timeout, it is marked as failed and retried according to `maxAttempts`. + +## Dead Letter Queue + +Jobs that exceed their retry limit land in the DLQ with status `dead`. You can inspect and redrive them: + +```typescript +// Get dead jobs +const deadJobs = await adapter.getDeadJobs({ limit: 50 }) + +// Redrive a single job (resets to pending) +await adapter.redriveJob(jobId) + +// Redrive all dead jobs +await adapter.redriveJobs() +``` + +## Error Events + +```typescript +queue.on("job:failed", (job) => { + console.error( + `Job ${job.name} failed on attempt ${job.attempts}/${job.maxAttempts}` + ) + console.error(job.error?.message) +}) +``` + +## Best Practices + +- Set `maxAttempts` based on whether the operation is idempotent +- Use timeouts for external API calls and network operations +- Monitor the DLQ and set up alerts for accumulated dead jobs +- Use the API or CLI to inspect and redrive dead jobs diff --git a/apps/docs/content/vorsteh-queue/02.core/07.observability.mdx b/apps/docs/content/vorsteh-queue/02.core/07.observability.mdx new file mode 100644 index 00000000..7d81f260 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/07.observability.mdx @@ -0,0 +1,164 @@ +--- +title: Observability +description: Optional OpenTelemetry metrics and distributed tracing for monitoring your queue in production. +--- + +vorsteh-queue supports OpenTelemetry instrumentation via an opt-in telemetry instance. By default, no OTel code is loaded — the core runs with zero observability overhead. To enable metrics and tracing, create a telemetry instance and pass it to your Queue and Worker. + +## Setup + +Install the OpenTelemetry packages: + + + {`@opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-metrics-otlp-http @opentelemetry/exporter-trace-otlp-http`} + + +Configure your application: + +```typescript +import { NodeSDK } from "@opentelemetry/sdk-node" +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" +import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics" + +// 1. Start the OTel SDK +const sdk = new NodeSDK({ + traceExporter: new OTLPTraceExporter(), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), + }), +}) +sdk.start() + +// 2. Create vorsteh-queue telemetry instance +import { Queue, Worker } from "@vorsteh-queue/core" +import { createOtelTelemetry } from "@vorsteh-queue/core" +import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" + +const telemetry = createOtelTelemetry({ queueName: "email-queue" }) + +const adapter = new PostgresQueueAdapter(db) +const queue = new Queue(adapter, { name: "email-queue", telemetry }) +const worker = new Worker(adapter, { name: "email-queue", telemetry }) +``` + +The `telemetry` parameter is optional on both Queue and Worker. When omitted, a no-op implementation is used with zero overhead. + +## How It Works + +- `createOtelTelemetry(config)` — creates a telemetry instance backed by the global OTel providers. Requires `@opentelemetry/api` to be installed. +- `noopTelemetry` — the default. All methods are empty stubs. Used when no telemetry is passed. + +The core package never imports `@opentelemetry/api` at module load time. The import happens only inside `createOtelTelemetry` via a deferred require, so your bundle and startup are unaffected if you don't use OTel. + +## Metrics + +All metrics use the `vorsteh_queue` namespace and include queue name and job name attributes. + +### Counters + +| Metric | Description | +| ------------------------------ | ------------------------------------- | +| `vorsteh_queue.jobs.added` | Total jobs added to the queue | +| `vorsteh_queue.jobs.processed` | Total jobs successfully completed | +| `vorsteh_queue.jobs.failed` | Total job failures (includes retries) | +| `vorsteh_queue.jobs.retried` | Total retry attempts | +| `vorsteh_queue.jobs.dead` | Jobs moved to the dead letter queue | +| `vorsteh_queue.jobs.cancelled` | Jobs cancelled | + +### Gauges + +| Metric | Description | +| --------------------------- | ------------------------------ | +| `vorsteh_queue.jobs.active` | Jobs currently being processed | + +### Histograms + +| Metric | Unit | Description | +| --- | --- | --- | +| `vorsteh_queue.jobs.duration` | ms | Time spent processing a job | +| `vorsteh_queue.jobs.wait_time` | ms | Time from enqueue to processing start | + +## Distributed Tracing + +Every job execution creates a span with these attributes: + +| Attribute | Description | +| -------------------------------- | --------------------------- | +| `messaging.system` | Always `vorsteh-queue` | +| `messaging.operation` | Always `process` | +| `messaging.destination.name` | Queue name | +| `vorsteh_queue.job.id` | Job ID | +| `vorsteh_queue.job.name` | Handler name | +| `vorsteh_queue.job.priority` | Job priority | +| `vorsteh_queue.job.attempt` | Current attempt number | +| `vorsteh_queue.job.max_attempts` | Maximum attempts configured | + +Spans follow the [OTel Messaging Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/messaging/). + +### Success and Failure + +- Successful jobs end with `SpanStatusCode.OK` +- Failed jobs end with `SpanStatusCode.ERROR`, include the error message, and record an exception event + +## Examples + +### Prometheus Export + +```typescript +import { NodeSDK } from "@opentelemetry/sdk-node" +import { PrometheusExporter } from "@opentelemetry/exporter-prometheus" + +const sdk = new NodeSDK({ + metricReader: new PrometheusExporter({ port: 9464 }), +}) + +sdk.start() +// Metrics available at http://localhost:9464/metrics +``` + +### Grafana / OTLP + +```typescript +import { NodeSDK } from "@opentelemetry/sdk-node" +import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" +import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics" + +const sdk = new NodeSDK({ + traceExporter: new OTLPTraceExporter({ + url: "http://localhost:4318/v1/traces", + }), + metricReader: new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ + url: "http://localhost:4318/v1/metrics", + }), + }), +}) + +sdk.start() +``` + +### Custom Alerting with Metrics + +Combine OTel metrics with your alerting system to get notified on anomalies: + +```typescript +// Set up alerts in your monitoring platform: +// +// - Alert when vorsteh_queue.jobs.dead > 0 (DLQ is filling up) +// - Alert when vorsteh_queue.jobs.duration p99 > 30s (jobs are slow) +// - Alert when vorsteh_queue.jobs.active stays at max concurrency (backpressure) +``` + +## Without OTel + +If you don't pass a `telemetry` instance, the Queue and Worker use `noopTelemetry` — all operations are no-ops with zero performance penalty. You don't need `@opentelemetry/api` installed at all. + +```typescript +import { Queue, Worker } from "@vorsteh-queue/core" + +// No telemetry parameter → noopTelemetry is used automatically +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue" }) +``` diff --git a/apps/docs/content/vorsteh-queue/02.core/index.mdx b/apps/docs/content/vorsteh-queue/02.core/index.mdx new file mode 100644 index 00000000..46090fda --- /dev/null +++ b/apps/docs/content/vorsteh-queue/02.core/index.mdx @@ -0,0 +1,4 @@ +--- +title: Core +description: The main queue engine containing all core functionality for job processing, scheduling, and management. +--- diff --git a/apps/docs/content/vorsteh-queue/03.adapters/01.drizzle.mdx b/apps/docs/content/vorsteh-queue/03.adapters/01.drizzle.mdx new file mode 100644 index 00000000..ee330130 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/01.drizzle.mdx @@ -0,0 +1,104 @@ +--- +title: Drizzle Adapter +navTitle: Drizzle +description: Drizzle ORM adapter for PostgreSQL with type-safe database operations. +--- + +The Drizzle adapter provides PostgreSQL support using Drizzle ORM, offering excellent TypeScript integration and performance. + +## Installation + + + @vorsteh-queue/core @vorsteh-queue/adapter-drizzle + + +## Quick Start + +```typescript +import { defineRelations } from "drizzle-orm" +import { drizzle } from "drizzle-orm/node-postgres" +import { Pool } from "pg" + +import { + PostgresQueueAdapter, + postgresSchema, +} from "@vorsteh-queue/adapter-drizzle" +import { Queue } from "@vorsteh-queue/core" + +// 1. Export the queue table from your schema +const { queueJobs } = postgresSchema + +// 2. Define relations (include your own tables here too) +const relations = defineRelations({ queueJobs }) + +// 3. Create drizzle client with relations +const pool = new Pool({ connectionString: "postgresql://..." }) +const db = drizzle({ client: pool, relations }) + +// 4. Create adapter and queue +const adapter = new PostgresQueueAdapter(db) +const queue = new Queue(adapter, { name: "my-queue" }) +``` + + + For real projects, extract this setup into a shared `src/queues.ts` file. See + [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). + + +## Supported Drivers + +- **node-postgres (pg)** — `drizzle-orm/node-postgres` ( [Docs](https://orm.drizzle.team/docs/get-started-postgresql#node-postgres) ) +- **Postgres.js** — `drizzle-orm/postgres-js` ( [Docs](https://orm.drizzle.team/docs/get-started-postgresql#postgresjs) ) +- **PGlite** — `drizzle-orm/pglite` (embedded, great for development) ( [Docs](https://orm.drizzle.team/docs/connect-pglite) ) + +## Database Schema + +The adapter ships with a pre-defined schema: + +```typescript +import { defineRelations } from "drizzle-orm" + +import { postgresSchema } from "@vorsteh-queue/adapter-drizzle" + +// Export the queue table for drizzle-kit migrations +export const { queueJobs } = postgresSchema + +// Define relations — add your own tables here alongside queueJobs +export const relations = defineRelations({ queueJobs }) +``` + +## Custom Table and Schema Names + +Use `createQueueJobsTable` to customize the table name and database schema: + +```typescript +import { defineRelations } from "drizzle-orm" + +import { createQueueJobsTable } from "@vorsteh-queue/adapter-drizzle" + +export const { table: customQueueJobs, schema: customSchema } = + createQueueJobsTable("custom_queue_jobs", "custom_schema") + +export const relations = defineRelations({ customQueueJobs }) +``` + +Pass the custom model name to the adapter: + +```typescript +const adapter = new PostgresQueueAdapter(db, { + modelName: "customQueueJobs", +}) +``` + +## Migrations + +Use [drizzle-kit](https://orm.drizzle.team/docs/kit-overview) to generate and run migrations: + +```bash +npx drizzle-kit generate +npx drizzle-kit migrate +``` + +## Adapter Properties + + diff --git a/apps/docs/content/vorsteh-queue/03.adapters/02.prisma.mdx b/apps/docs/content/vorsteh-queue/03.adapters/02.prisma.mdx new file mode 100644 index 00000000..f12e7a12 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/02.prisma.mdx @@ -0,0 +1,105 @@ +--- +title: Prisma Adapter +navTitle: Prisma +description: Prisma ORM adapter for PostgreSQL with generated types and migrations. +--- + +The Prisma adapter provides PostgreSQL support using Prisma ORM, offering generated types, migrations, and developer tooling. + +## Installation + + + @vorsteh-queue/core @vorsteh-queue/adapter-prisma + + +## Quick Start + +```typescript +import { PrismaClient } from "@prisma/client" +import { PostgresPrismaQueueAdapter } from "@vorsteh-queue/adapter-prisma" +import { Queue, Worker } from "@vorsteh-queue/core" + +const prisma = new PrismaClient() +const adapter = new PostgresPrismaQueueAdapter(prisma) + +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue" }) + +await queue.connect() +``` + + + For real projects, extract this setup into a shared `src/queues.ts` file. See + [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). + + +## Database Schema + +Add this model to your `schema.prisma`: + +```prisma +model QueueJob { + id String @id @default(uuid()) + queueName String @map("queue_name") @db.VarChar(255) + name String @db.VarChar(255) + payload Json @db.JsonB + status String @db.VarChar(50) + priority Int + attempts Int @default(0) + maxAttempts Int @map("max_attempts") + timeout Int? + progress Int @default(0) + groupKey String? @map("group_key") @db.VarChar(255) + uniqueKey String? @map("unique_key") @db.VarChar(255) + cron String? @db.VarChar(255) + repeatEvery Int? @map("repeat_every") + repeatLimit Int? @map("repeat_limit") + repeatCount Int @default(0) @map("repeat_count") + cancellationReason String? @map("cancellation_reason") @db.Text + dependsOn Json? @map("depends_on") @db.JsonB + onDependencyFailure String? @map("on_dependency_failure") @db.VarChar(10) + error Json? @db.JsonB + result Json? @db.JsonB + steps Json? @db.JsonB + signals Json? @db.JsonB + parentId String? @map("parent_id") + flowId String? @map("flow_id") + childrenCount Int @default(0) @map("children_count") + childrenCompleted Int @default(0) @map("children_completed") + failParentOnFailure Int @default(0) @map("fail_parent_on_failure") + createdAt DateTime @default(dbgenerated("timezone('utc', now())")) @map("created_at") @db.Timestamptz(6) + processAt DateTime @map("process_at") @db.Timestamptz(6) + processedAt DateTime? @map("processed_at") @db.Timestamptz(6) + completedAt DateTime? @map("completed_at") @db.Timestamptz(6) + failedAt DateTime? @map("failed_at") @db.Timestamptz(6) + cancelledAt DateTime? @map("cancelled_at") @db.Timestamptz(6) + + @@index([queueName, status, priority, createdAt], map: "idx_queue_jobs_polling") + @@index([queueName, processAt], map: "idx_queue_jobs_delayed") + @@index([queueName, groupKey], map: "idx_queue_jobs_active_groups") + @@index([queueName, status], map: "idx_queue_jobs_stats") + @@map("queue_jobs") +} +``` + +## Custom Table and Schema Names + +```typescript +const adapter = new PostgresPrismaQueueAdapter(prisma, { + modelName: "CustomQueueJob", // model name in schema.prisma + tableName: "custom_queue_jobs", // @@map value + schemaName: "custom_schema", // PostgreSQL schema +}) +``` + +## Migrations + +Use the [Prisma CLI](https://www.prisma.io/docs/orm/tools/prisma-cli) for migrations: + +```bash +npx prisma migrate dev --name add-queue-table +``` + +## Adapter Properties + + diff --git a/apps/docs/content/vorsteh-queue/03.adapters/03.kysely.mdx b/apps/docs/content/vorsteh-queue/03.adapters/03.kysely.mdx new file mode 100644 index 00000000..87bdc645 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/03.kysely.mdx @@ -0,0 +1,71 @@ +--- +title: Kysely Adapter +navTitle: Kysely +description: Kysely adapter for PostgreSQL with migration support. +--- + +The Kysely adapter provides PostgreSQL support using Kysely, a type-safe SQL query builder. + +## Installation + + + @vorsteh-queue/core @vorsteh-queue/adapter-kysely + + +## Quick Start + +```typescript +import { Kysely, PostgresDialect } from "kysely" +import { Pool } from "pg" + +import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-kysely/postgres-adapter" +import { Queue } from "@vorsteh-queue/core" + +const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: "postgresql://..." }), + }), +}) + +const adapter = new PostgresQueueAdapter(db) +const queue = new Queue(adapter, { name: "my-queue" }) +``` + + + For real projects, extract this setup into a shared `src/queues.ts` file. See + [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). + + +## Database Schema + +The adapter provides a migration for creating the queue table: + +```typescript +import { createQueueTable } from "@vorsteh-queue/adapter-kysely/migrations" +``` + +## Migrations + +Use [kysely-ctl](https://github.com/kysely-org/kysely-ctl) or the provided migration function: + +```typescript +import { Kysely } from "kysely" +import { createQueueTable } from "@vorsteh-queue/adapter-kysely/migrations" + +export async function up(db: Kysely): Promise { + await createQueueTable(db) +} +``` + +## Custom Table and Schema Names + +```typescript +const adapter = new PostgresQueueAdapter(db, { + tableName: "custom_queue_jobs", + schemaName: "custom_schema", +}) +``` + +## Adapter Properties + + diff --git a/apps/docs/content/vorsteh-queue/03.adapters/04.zenstack.mdx b/apps/docs/content/vorsteh-queue/03.adapters/04.zenstack.mdx new file mode 100644 index 00000000..e36d5ef7 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/04.zenstack.mdx @@ -0,0 +1,154 @@ +--- +title: ZenStack Adapter +navTitle: ZenStack +description: ZenStack ORM adapter for PostgreSQL with built-in access control and Prisma-compatible API. +--- + +The ZenStack adapter provides PostgreSQL support using ZenStack ORM v3, offering a Prisma-compatible query API built on Kysely with additional features like built-in access control, polymorphic models, and a plugin system. + +## Installation + + + @vorsteh-queue/core @vorsteh-queue/adapter-zenstack @zenstackhq/orm + + +You also need a PostgreSQL database driver: + +pg + +And the ZenStack CLI as a dev dependency: + + + @zenstackhq/cli + + +## Quick Start + +```typescript +import { ZenStackClient } from "@zenstackhq/orm" +import { PostgresDialect } from "@zenstackhq/orm/dialects/postgres" +import { PostgresZenstackQueueAdapter } from "@vorsteh-queue/adapter-zenstack" +import { Queue, Worker } from "@vorsteh-queue/core" +import { Pool } from "pg" + +import { schema } from "./zenstack/schema" + +const db = new ZenStackClient(schema, { + dialect: new PostgresDialect({ + pool: new Pool({ connectionString: process.env.DATABASE_URL }), + }), +}) + +const adapter = new PostgresZenstackQueueAdapter(db) + +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue" }) + +await queue.connect() +``` + + + For real projects, extract this setup into a shared `src/queues.ts` file. See + [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). + + +## Database Schema + +Add this model to your `zenstack/schema.zmodel`: + +```prisma +datasource db { + provider = "postgresql" +} + +model QueueJob { + id String @id @default(uuid()) + queueName String @map("queue_name") @db.VarChar(255) + name String @db.VarChar(255) + payload Json @db.JsonB + status String @db.VarChar(50) + priority Int + attempts Int @default(0) + maxAttempts Int @map("max_attempts") + timeout Int? + progress Int @default(0) + groupKey String? @map("group_key") @db.VarChar(255) + uniqueKey String? @map("unique_key") @db.VarChar(255) + cron String? @db.VarChar(255) + repeatEvery Int? @map("repeat_every") + repeatLimit Int? @map("repeat_limit") + repeatCount Int @default(0) @map("repeat_count") + cancellationReason String? @map("cancellation_reason") @db.Text + dependsOn Json? @map("depends_on") @db.JsonB + onDependencyFailure String? @map("on_dependency_failure") @db.VarChar(10) + error Json? @db.JsonB + result Json? @db.JsonB + steps Json? @db.JsonB + signals Json? @db.JsonB + parentId String? @map("parent_id") + flowId String? @map("flow_id") + childrenCount Int @default(0) @map("children_count") + childrenCompleted Int @default(0) @map("children_completed") + failParentOnFailure Int @default(0) @map("fail_parent_on_failure") + createdAt DateTime @default(dbgenerated("timezone('utc', now())")) @map("created_at") @db.Timestamptz(6) + processAt DateTime @map("process_at") @db.Timestamptz(6) + processedAt DateTime? @map("processed_at") @db.Timestamptz(6) + completedAt DateTime? @map("completed_at") @db.Timestamptz(6) + failedAt DateTime? @map("failed_at") @db.Timestamptz(6) + cancelledAt DateTime? @map("cancelled_at") @db.Timestamptz(6) + + @@index([queueName, status, priority, createdAt], map: "idx_queue_jobs_polling") + @@index([queueName, processAt], map: "idx_queue_jobs_delayed") + @@index([queueName, groupKey], map: "idx_queue_jobs_active_groups") + @@index([queueName, status], map: "idx_queue_jobs_stats") + @@map("queue_jobs") +} +``` + +Then generate the TypeScript schema and push the database: + +```bash +npx zen generate +npx zen db push +``` + +## Custom Table and Schema Names + +```typescript +const adapter = new PostgresZenstackQueueAdapter(db, { + modelName: "customQueueJob", // model accessor name (camelCase) + tableName: "custom_queue_jobs", // @@map value + schemaName: "custom_schema", // PostgreSQL schema +}) +``` + +## Migrations + +Use the [ZenStack CLI](https://zenstack.dev/docs/orm/cli) for migrations: + +```bash +npx zen migrate dev --name add-queue-table +npx zen migrate deploy +``` + +## Why ZenStack? + +ZenStack v3 is a standalone ORM that provides: + +- **Intuitive schema language** — model data, relations, access control, and more in one place +- **Powerful ORM** — awesomely-typed API, built-in access control, and unmatched flexibility +- **Query-as-a-Service** — provides a full-fledged data API without the need to code it up +- **Prisma-compatible query API** — familiar `findMany`, `create`, `update` syntax +- **Built-in access control** — define policies directly in the schema with `@@allow` / `@@deny` +- **Low-level Kysely query builder** — escape hatch for complex queries via `db.$qb` +- **Polymorphic models** — model inheritance with `extends` and `@@delegate` +- **No Prisma runtime dependency** — lighter footprint, no Rust/WASM engines + + + ZenStack v3 does not depend on Prisma at runtime. The schema language is a + superset of Prisma Schema Language, making migration straightforward. + + +## Adapter Properties + + diff --git a/apps/docs/content/vorsteh-queue/03.adapters/05.typeorm.mdx b/apps/docs/content/vorsteh-queue/03.adapters/05.typeorm.mdx new file mode 100644 index 00000000..9d00b729 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/05.typeorm.mdx @@ -0,0 +1,96 @@ +--- +title: TypeORM Adapter +navTitle: TypeORM +description: TypeORM adapter for PostgreSQL with entity-based schema and decorator-driven models. +--- + +The TypeORM adapter provides PostgreSQL support using TypeORM, offering a mature ORM with decorator-based entity definitions, migrations, and broad database support. + +## Installation + + + @vorsteh-queue/core @vorsteh-queue/adapter-typeorm typeorm pg + + +## Quick Start + +```typescript +import { DataSource } from "typeorm" +import { + PostgresTypeormQueueAdapter, + QueueJobEntity, +} from "@vorsteh-queue/adapter-typeorm" +import { Queue, Worker } from "@vorsteh-queue/core" + +const dataSource = new DataSource({ + type: "postgres", + url: process.env.DATABASE_URL, + entities: [QueueJobEntity], + synchronize: true, +}) + +const adapter = new PostgresTypeormQueueAdapter(dataSource) + +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue" }) + +await queue.connect() +``` + + + For real projects, extract this setup into a shared `src/queues.ts` file. See + [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). + + +## Database Schema + +The adapter ships with a pre-built `QueueJobEntity` that you can use directly with TypeORM's `synchronize: true` or migrations. The entity maps to the `queue_jobs` table. + +If you prefer manual control, you can import the entity and use TypeORM migrations: + +```typescript +import { QueueJobEntity } from "@vorsteh-queue/adapter-typeorm/entity" +``` + +The entity defines all required columns with proper types, indexes, and column mappings. + +## Custom Table and Schema Names + +```typescript +const adapter = new PostgresTypeormQueueAdapter(dataSource, { + tableName: "custom_queue_jobs", // database table name + schemaName: "custom_schema", // PostgreSQL schema +}) +``` + + + When using custom table names, you need to create your own entity class with + the matching `@Entity("custom_queue_jobs")` decorator, or use raw SQL + migrations. + + +## Migrations + +Use the [TypeORM CLI](https://typeorm.io/docs/migrations/) for migrations: + +```bash +npx typeorm migration:generate -d src/data-source.ts src/migrations/AddQueueTable +npx typeorm migration:run -d src/data-source.ts +``` + +Alternatively, set `synchronize: true` in development for automatic schema creation. + +## Why TypeORM? + +TypeORM is one of the most established ORMs in the Node.js ecosystem: + +- **Decorator-based entities** — define schemas with familiar TypeScript decorators +- **Active Record and Data Mapper** — choose the pattern that fits your architecture +- **Broad database support** — PostgreSQL, MySQL, SQLite, MSSQL, Oracle, and more +- **Mature migration system** — automatic generation and manual migration support +- **QueryBuilder** — powerful and flexible SQL query construction +- **Large ecosystem** — extensive community and plugin support + +## Adapter Properties + + diff --git a/apps/docs/content/vorsteh-queue/03.adapters/06.mikroorm.mdx b/apps/docs/content/vorsteh-queue/03.adapters/06.mikroorm.mdx new file mode 100644 index 00000000..a9108f68 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/06.mikroorm.mdx @@ -0,0 +1,83 @@ +--- +title: MikroORM Adapter +navTitle: MikroORM +description: MikroORM adapter for PostgreSQL with schema-first entity definitions and identity map. +--- + +The MikroORM adapter provides PostgreSQL support using MikroORM v6, offering a powerful ORM with unit of work pattern, identity map, and schema-first entity definitions. + +## Installation + + + @vorsteh-queue/core @vorsteh-queue/adapter-mikroorm @mikro-orm/core + @mikro-orm/postgresql + + +## Quick Start + +```typescript +import { MikroORM } from "@mikro-orm/postgresql" +import { + PostgresMikroormQueueAdapter, + QueueJobSchema, +} from "@vorsteh-queue/adapter-mikroorm" +import { Queue, Worker } from "@vorsteh-queue/core" + +const orm = await MikroORM.init({ + entities: [QueueJobSchema], + clientUrl: process.env.DATABASE_URL, +}) + +const adapter = new PostgresMikroormQueueAdapter(orm) + +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue" }) + +await queue.connect() +``` + + + For real projects, extract this setup into a shared `src/queues.ts` file. See + [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). + + +## Database Schema + +The adapter ships with a pre-built `QueueJobSchema` (EntitySchema) that you can use directly with MikroORM's schema generator or migrations. + +```bash +npx mikro-orm schema:create +``` + +## Custom Table and Schema Names + +```typescript +const adapter = new PostgresMikroormQueueAdapter(orm, { + tableName: "custom_queue_jobs", + schemaName: "custom_schema", +}) +``` + +## Migrations + +Use the [MikroORM CLI](https://mikro-orm.io/docs/migrations) for migrations: + +```bash +npx mikro-orm migration:create +npx mikro-orm migration:up +``` + +## Why MikroORM? + +MikroORM v6 is a modern TypeScript ORM with: + +- **Unit of Work pattern** — automatic change tracking and optimized flushes +- **Identity Map** — ensures entity identity across queries +- **Schema-first entities** — define schemas without decorators using `EntitySchema` +- **Powerful QueryBuilder** — Knex-powered query construction +- **Database-agnostic** — supports PostgreSQL, MySQL, SQLite, MongoDB, and more +- **Strict typing** — full TypeScript support with generic entities + +## Adapter Properties + + diff --git a/apps/docs/content/vorsteh-queue/03.adapters/07.sequelize.mdx b/apps/docs/content/vorsteh-queue/03.adapters/07.sequelize.mdx new file mode 100644 index 00000000..91a88fe6 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/07.sequelize.mdx @@ -0,0 +1,86 @@ +--- +title: Sequelize Adapter +navTitle: Sequelize +description: Sequelize adapter for PostgreSQL with model-based schema and battle-tested ORM. +--- + +The Sequelize adapter provides PostgreSQL support using Sequelize, one of the most established Node.js ORMs with model-based schema definitions, migrations, and broad database support. + +## Installation + + + @vorsteh-queue/core @vorsteh-queue/adapter-sequelize sequelize pg + + +## Quick Start + +```typescript +import { Sequelize } from "sequelize" +import { + PostgresSequelizeQueueAdapter, + initQueueJobModel, +} from "@vorsteh-queue/adapter-sequelize" +import { Queue, Worker } from "@vorsteh-queue/core" + +const sequelize = new Sequelize(process.env.DATABASE_URL, { + dialect: "postgres", + logging: false, +}) + +initQueueJobModel(sequelize) + +const adapter = new PostgresSequelizeQueueAdapter(sequelize) + +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue" }) + +await queue.connect() +``` + + + For real projects, extract this setup into a shared `src/queues.ts` file. See + [Project Structure](/docs/vorsteh-queue/getting-started/project-structure). + + +## Database Schema + +The adapter ships with a pre-built model that can be synced with: + +```typescript +await sequelize.sync() +``` + +Or use Sequelize migrations for production. + +## Custom Table and Schema Names + +```typescript +const adapter = new PostgresSequelizeQueueAdapter(sequelize, { + tableName: "custom_queue_jobs", + schemaName: "custom_schema", +}) +``` + +## Migrations + +Use the [Sequelize CLI](https://sequelize.org/docs/v6/other-topics/migrations/) for migrations: + +```bash +npx sequelize-cli migration:generate --name add-queue-table +npx sequelize-cli db:migrate +``` + +## Why Sequelize? + +Sequelize is one of the most battle-tested ORMs for Node.js: + +- **Broad database support** — PostgreSQL, MySQL, MariaDB, SQLite, MSSQL +- **Model-based schemas** — define schemas with familiar `Model.init()` pattern +- **Mature migration system** — CLI-driven migration workflow +- **Transaction support** — managed and unmanaged transactions +- **Hooks and validators** — lifecycle hooks and built-in validation +- **Large community** — extensive ecosystem and documentation + +## Adapter Properties + + diff --git a/apps/docs/content/vorsteh-queue/03.adapters/index.mdx b/apps/docs/content/vorsteh-queue/03.adapters/index.mdx new file mode 100644 index 00000000..7c39ba0c --- /dev/null +++ b/apps/docs/content/vorsteh-queue/03.adapters/index.mdx @@ -0,0 +1,4 @@ +--- +title: Adapters +description: ORM-specific database adapters for PostgreSQL. +--- diff --git a/apps/docs/content/vorsteh-queue/04.packages/01.create-vorsteh-queue.mdx b/apps/docs/content/vorsteh-queue/04.packages/01.create-vorsteh-queue.mdx new file mode 100644 index 00000000..1d0b92c0 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/04.packages/01.create-vorsteh-queue.mdx @@ -0,0 +1,46 @@ +--- +title: create-vorsteh-queue +description: CLI scaffolding tool for quickly creating new Vorsteh Queue projects from templates. +--- + +The `create-vorsteh-queue` CLI provides the fastest way to get started by creating new projects from pre-built templates. + +## Usage + +### Interactive Mode + +```bash +npx create-vorsteh-queue +``` + +The CLI prompts for project name, template, package manager, and dependency installation. + +### Direct Mode + +```bash +npx create-vorsteh-queue my-app --template=drizzle-postgres --package-manager=pnpm --quiet +``` + +## Options + +| Option | Short | Description | +| ------------------------ | ----------- | ----------------------- | +| `--template=` | `-t=` | Choose template | +| `--package-manager=` | `-p=` | Package manager | +| `--no-install` | n/a | Skip dependency install | +| `--quiet` | `-q` | Minimal output | + +## Available Templates + + + +## Example + +```bash +npx create-vorsteh-queue worker-service \ + --template=drizzle-postgres \ + --package-manager=pnpm +cd worker-service +pnpm db:push +pnpm dev +``` diff --git a/apps/docs/content/vorsteh-queue/04.packages/02.server.mdx b/apps/docs/content/vorsteh-queue/04.packages/02.server.mdx new file mode 100644 index 00000000..835d929e --- /dev/null +++ b/apps/docs/content/vorsteh-queue/04.packages/02.server.mdx @@ -0,0 +1,214 @@ +--- +title: Server API +navTitle: Server +description: GraphQL API server for monitoring and managing queues. +--- + +The `@vorsteh-queue/server` package provides a standalone GraphQL API for managing Vorsteh Queue jobs. It can run as a standalone server or be mounted as middleware in an existing Hono app. + +## Installation + +@vorsteh-queue/server + +## Quick Start + +```typescript +import { Queue, Worker, MemoryQueueAdapter } from "@vorsteh-queue/core" +import { createQueueServer } from "@vorsteh-queue/server" + +const adapter = new MemoryQueueAdapter() +const queue = new Queue(adapter, { name: "email-queue" }) +const worker = new Worker(adapter, { name: "email-queue" }) + +worker.register("send-email", async (job) => { + // process job... + return { sent: true } +}) + +const server = createQueueServer({ + queues: [queue], + workers: [worker], // enables real-time subscription events + port: 3000, + auth: { tokens: [process.env.QUEUE_TOKEN] }, +}) + +worker.start() +await server.start() +``` + +## Features + +- **GraphQL API** — query queue stats, inspect jobs, cancel, retry, redrive +- **Multi-queue support** — manage multiple queues from a single server +- **Authentication** — optional token-based auth with custom middleware support +- **Subscriptions** — real-time job lifecycle events via SSE +- **Hono middleware** — mount on existing apps via `createQueueMiddleware` + +## Configuration + +### Standalone Server + +```typescript +import { Queue, Worker } from "@vorsteh-queue/core" +import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" +import { createQueueServer } from "@vorsteh-queue/server" + +// 1. Create adapter (shared between queues and workers) +const adapter = new PostgresQueueAdapter(db) + +// 2. Create Queue instances (one per logical queue) +const emailQueue = new Queue(adapter, { name: "email-queue" }) +const reportQueue = new Queue(adapter, { name: "report-queue" }) + +// 3. Create Worker instances (must use same name as their queue) +const emailWorker = new Worker(adapter, { name: "email-queue", concurrency: 5 }) +const reportWorker = new Worker(adapter, { name: "report-queue" }) + +// 4. Register handlers +emailWorker.register("send-email", async (job) => { + await sendEmail(job.payload) + return { sent: true } +}) + +reportWorker.register("generate-report", async (job) => { + return generateReport(job.payload) +}) + +// 5. Create server — pass both queues and workers +const server = createQueueServer({ + queues: [emailQueue, reportQueue], + workers: [emailWorker, reportWorker], + port: 3000, + auth: { tokens: [process.env.QUEUE_TOKEN] }, +}) + +// 6. Start workers and server +emailWorker.start() +reportWorker.start() +await server.start() +``` + +### As Hono Middleware + +```typescript +import { Hono } from "hono" +import { Queue, Worker } from "@vorsteh-queue/core" +import { PostgresQueueAdapter } from "@vorsteh-queue/adapter-drizzle" +import { createQueueMiddleware } from "@vorsteh-queue/server" + +const adapter = new PostgresQueueAdapter(db) +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue" }) + +worker.register("process-task", async (job) => { + // handle job... + return { ok: true } +}) + +const app = new Hono() +app.route( + "/queue", + createQueueMiddleware({ + queues: [queue], + workers: [worker], + auth: { tokens: ["secret"] }, + }) +) + +worker.start() +export default app +``` + +### Via CLI + +Create a `queue.config.ts` in your project root and use the CLI: + +```bash +vorsteh-queue serve +vorsteh-queue serve --port 4000 +``` + +## Server Config + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `queues` | `readonly Queue[]` | required | Queue instances to manage | +| `workers?` | `readonly Worker[]` | `[]` | Workers to observe for subscription events | +| `auth?` | `AuthConfig` | `false` | Authentication configuration | +| `port?` | `number` | `3000` | Server port (standalone mode) | + +### Authentication + +```typescript +// Token-based +auth: { + tokens: ["my-secret-token"] +} + +// Custom middleware +auth: { + middleware: myAuthMiddleware +} + +// Disabled +auth: false +``` + +## GraphQL API + +The server exposes a GraphQL endpoint at `/graphql`. + +### Queries + +| Query | Description | +| --- | --- | +| `queues` | List all configured queues | +| `stats(queue)` | Queue statistics by status | +| `size(queue)` | Count of pending + delayed jobs | +| `job(id, queue?)` | Get a single job by ID | +| `jobs(queue, status?, name?, limit?, offset?)` | Paginated job list | +| `deadJobs(queue, limit?, offset?)` | Dead letter queue jobs | +| `flows(queue, limit?, offset?)` | Flow trees | +| `flowTree(queue, flowId)` | Full flow tree structure | + +### Mutations + +| Mutation | Description | +| ------------------------------- | --------------------- | +| `cancelJob(id, queue, reason?)` | Cancel a job | +| `retryJob(id, queue)` | Retry a failed job | +| `runJobNow(id, queue)` | Promote a delayed job | +| `deleteJob(id, queue)` | Delete a job | +| `redriveAll(queue, name?)` | Redrive dead jobs | +| `clearJobs(queue, status?)` | Clear jobs by status | + +### Subscriptions + +| Subscription | Payload | Description | +| --- | --- | --- | +| `jobStatusChanged` | `JobLifecycleEvent` | Fires on any job status transition | +| `statsUpdated` | `QueueStats` | Fires when queue stats change | + +The `JobLifecycleEvent` envelope contains: + +```graphql +type JobLifecycleEvent { + jobId: ID! + jobName: String! + queueName: String! + previousStatus: JobStatus + currentStatus: JobStatus! + timestamp: String! + progress: Int +} +``` + +Subscriptions are delivered via Server-Sent Events (SSE). They require `workers` to be passed in the server config for processing lifecycle events. + +## Health Check + +The server exposes a `/health` endpoint (behind auth) that returns: + +```json +{ "status": "ok" } +``` diff --git a/apps/docs/content/vorsteh-queue/04.packages/index.mdx b/apps/docs/content/vorsteh-queue/04.packages/index.mdx new file mode 100644 index 00000000..b56d0e38 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/04.packages/index.mdx @@ -0,0 +1,4 @@ +--- +title: Packages +description: Additional packages in the Vorsteh Queue ecosystem. +--- diff --git a/apps/docs/content/vorsteh-queue/05.migration/01.v0-to-v1.mdx b/apps/docs/content/vorsteh-queue/05.migration/01.v0-to-v1.mdx new file mode 100644 index 00000000..a54f404c --- /dev/null +++ b/apps/docs/content/vorsteh-queue/05.migration/01.v0-to-v1.mdx @@ -0,0 +1,190 @@ +--- +title: Migrating to v1.0 +navTitle: v0 → v1 +description: Complete migration guide from Vorsteh Queue v0.x to v1.0. +--- + +This guide covers all breaking changes when upgrading from v0.x to v1.0 across all packages. + +## Requirements + +- **Node.js 22+** — minimum version raised from 20 to 22 + +## Core (`@vorsteh-queue/core`) + +### Queue/Worker Split + +The `Queue` class is now producer-only. Job processing is handled by the new `Worker` class. + +**Before:** + +```typescript +const queue = new Queue(adapter, { name: "my-queue" }) +queue.register("send-email", handler) +queue.start() +``` + +**After:** + +```typescript +import { Queue, Worker } from "@vorsteh-queue/core" + +const queue = new Queue(adapter, { name: "my-queue" }) +const worker = new Worker(adapter, { name: "my-queue", concurrency: 5 }) + +worker.register("send-email", handler) +worker.start() + +await queue.add("send-email", { to: "user@example.com" }) +``` + +### Handler Signature + +Handlers now receive a `JobContext` as second argument with `signal` and `step` for timeout/cancellation and durable steps. + +**Before:** + +```typescript +queue.register("job", async (job) => { + return { done: true } +}) +``` + +**After:** + +```typescript +worker.register("job", async (job, { signal, step }) => { + // signal: AbortSignal for timeout/cancellation + // step: durable step execution (step.run, step.sleep, step.waitFor) + return { done: true } +}) +``` + +### New Statuses + +Added `cancelled`, `dead`, and `waiting-children` statuses. Update any code that checks job statuses. + +### Removed APIs + +The following methods no longer exist on `Queue`: + +- `queue.register()` → use `worker.register()` +- `queue.start()` / `queue.stop()` → use `worker.start()` / `worker.stop()` +- `queue.pause()` / `queue.resume()` → use `worker.pause()` / `worker.resume()` +- `queue.enqueue()` / `queue.dequeue()` → use `queue.add()` / removed + +### Job Filter Changes + +`getJobs` and `size` now use a `where` input instead of flat parameters. + +**Before:** + +```typescript +await queue.getJobs({ status: "failed", name: "send-email", limit: 10 }) +await queue.size() +``` + +**After:** + +```typescript +await queue.getJobs({ + where: { status: "failed", name: "send-email" }, + limit: 10, +}) +await queue.size({ status: { eq: "failed" } }) +``` + +## Drizzle Adapter (`@vorsteh-queue/adapter-drizzle`) + +### Peer Dependency + +Upgrade `drizzle-orm` to `>=1.0.0-rc.4` and `drizzle-kit` to `>=1.0.0-rc.4` (was `>=0.30.0`). + + + For a complete list of changes in Drizzle ORM v1, see the official [Drizzle v0 + → v1 migration guide](https://orm.drizzle.team/docs/v0-v1-changes). + + +### Drizzle Client Initialization + +Drizzle ORM v1 requires a `relations` object instead of a `schema` object: + +**Before:** + +```typescript +import * as schema from "./schema" + +const db = drizzle(pool, { schema }) +``` + +**After:** + +```typescript +import { defineRelations } from "drizzle-orm" +import { postgresSchema } from "@vorsteh-queue/adapter-drizzle" + +export const { queueJobs } = postgresSchema +export const relations = defineRelations({ queueJobs }) + +// In your database setup: +import { relations } from "./schema" + +const db = drizzle({ client: pool, relations }) +``` + +### Schema Migration + +New columns and indexes have been added. Run migrations: + +```bash +npx drizzle-kit generate +npx drizzle-kit migrate +``` + +New columns: `group_key`, `unique_key`, `cancelled_at`, `cancellation_reason`, `steps` + +Changed columns: + +- `timeout`: JSONB → INT (nullable, milliseconds) +- `progress`: nullable → non-nullable INT (default 0) +- `repeat_count`: nullable → non-nullable (default 0) + +New indexes: `idx_queue_jobs_polling`, `idx_queue_jobs_delayed`, `idx_queue_jobs_active_groups`, `idx_queue_jobs_stats` + +## Prisma Adapter (`@vorsteh-queue/adapter-prisma`) + +### Peer Dependency + +Upgrade `@prisma/client` to `>=7.0.0` (was `>=5.0.0`). + + + For a complete list of changes in Prisma 7, see the official [Prisma v7 + upgrade guide](https://www.prisma.io/docs/guides/upgrade-prisma-orm/v7). + + +### Schema Migration + +Update your `schema.prisma` with the new model fields and run migrations: + +```bash +npx prisma migrate dev +``` + +Same column changes as Drizzle (see above). + +## Kysely Adapter (`@vorsteh-queue/adapter-kysely`) + +Run the updated migration (same schema changes as Drizzle/Prisma above). The `createQueueJobsTable()` helper has been updated. + +## New Features in v1.0 + +These are not breaking changes but notable additions: + +- **Job Steps** — durable multi-step execution (`step.run()`, `step.sleep()`, `step.waitFor()`) +- **Flow Producer** — parent-child job trees with dependency tracking +- **Group FIFO** — ordered processing per group key +- **Unique Jobs** — deduplication via unique keys +- **Rate Limiting** — token-bucket per handler +- **Saga Compensation** — automatic rollback on step failure +- **Signals** — human-in-the-loop pause/resume +- **Event Triggers** — automatic follow-up job creation diff --git a/apps/docs/content/vorsteh-queue/05.migration/index.mdx b/apps/docs/content/vorsteh-queue/05.migration/index.mdx new file mode 100644 index 00000000..46c2da50 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/05.migration/index.mdx @@ -0,0 +1,6 @@ +--- +title: Migration Guides +description: Step-by-step guides for upgrading between versions of Vorsteh Queue. +--- + +Guides for migrating between versions of Vorsteh Queue packages. diff --git a/apps/docs/content/vorsteh-queue/index.mdx b/apps/docs/content/vorsteh-queue/index.mdx new file mode 100644 index 00000000..19af70d7 --- /dev/null +++ b/apps/docs/content/vorsteh-queue/index.mdx @@ -0,0 +1,6 @@ +--- +title: Vorsteh Queue +navTitle: Vorsteh Queue +description: A type-safe PostgreSQL job queue for Node.js with durable execution, flow orchestration, and first-class ORM adapters. +entrypoint: /docs/vorsteh-queue/getting-started/quickstart +--- diff --git a/apps/docs/eslint.config.js b/apps/docs/eslint.config.js deleted file mode 100644 index 966eba3d..00000000 --- a/apps/docs/eslint.config.js +++ /dev/null @@ -1,14 +0,0 @@ -import baseConfig, { restrictEnvAccess } from "@vorsteh-queue/eslint-config/base" -import nextjsConfig from "@vorsteh-queue/eslint-config/nextjs" -import reactConfig from "@vorsteh-queue/eslint-config/react" - -/** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [".next/**"], - }, - ...baseConfig, - ...reactConfig, - ...nextjsConfig, - ...restrictEnvAccess, -] diff --git a/apps/docs/global.d.ts b/apps/docs/global.d.ts new file mode 100644 index 00000000..d7e961ee --- /dev/null +++ b/apps/docs/global.d.ts @@ -0,0 +1 @@ +declare module "*.css" diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.ts similarity index 60% rename from apps/docs/next.config.mjs rename to apps/docs/next.config.ts index 478622b7..7715e19f 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.ts @@ -1,4 +1,22 @@ import createMDXPlugin from "@next/mdx" +import type { NextConfig } from "next" + +function normalizeBasePath(basePath: string | undefined): string { + if (!basePath) { + return "" + } + + const withLeadingSlash = basePath.startsWith("/") ? basePath : `/${basePath}` + + if (withLeadingSlash === "/") { + return "" + } + + return withLeadingSlash.replace(/\/+$/u, "") +} + +// oxlint-disable-next-line no-restricted-properties +const basePath = normalizeBasePath(process.env.BASE_PATH) const withMDX = createMDXPlugin({ options: { @@ -20,13 +38,23 @@ const withMDX = createMDXPlugin({ }, }) -/** @type {import('next').NextConfig} */ -const nextConfig = { +const nextConfig: NextConfig = { + ...(basePath + ? { + assetPrefix: basePath, + basePath, + } + : {}), + images: { + unoptimized: true, + }, output: "export", + pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], + poweredByHeader: false, reactStrictMode: true, + // staticPageGenerationTimeout: 180, + trailingSlash: true, - poweredByHeader: false, - pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], /** Enables hot reloading for local packages without a build step */ transpilePackages: [ "@vorsteh-queue/core", @@ -35,12 +63,14 @@ const nextConfig = { "@vorsteh-queue/adapter-kysely", "create-vorsteh-queue", ], - typescript: { ignoreBuildErrors: true, }, - images: { - unoptimized: true, + productionBrowserSourceMaps: false, + enablePrerenderSourceMaps: false, + experimental: { + cpus: 1, + serverSourceMaps: false, }, } diff --git a/apps/docs/package.json b/apps/docs/package.json index 47e6bc1f..f75fb002 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,92 +1,94 @@ { "name": "docs", - "version": "0.0.0", + "version": "0.1.0", "private": true, "type": "module", "scripts": { - "build": "next typegen && renoun next build && pnpm generate-pagefind", - "clean": "git clean -xdf .cache .turbo dist node_modules .next", - "clean:cache": "git clean -xdf .cache", + "build": "next typegen && renoun next build", + "clean": "git clean -xdf .cache .turbo dist node_modules dist", + "clean:cache": "git clean -xdf .cache .turbo", + "clean:dist": "git clean -xdf dist", "dev": "next typegen && renoun next dev", - "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore", - "generate-pagefind": "pagefind --site out --output-path out/pagefind", - "lint": "eslint .", - "lint:links": "node --import ./src/register.js --import tsx/esm src/link-check.ts", - "matchtest": "tsx src/match.ts", + "generate:search-index": "tsx src/orama/generate.ts", "preview": "tsx src/localserver.ts", + "start": "next start", "typecheck": "tsc --noEmit", "typegen": "next typegen" }, - "prettier": "@vorsteh-queue/prettier-config", "dependencies": { - "@icons-pack/react-simple-icons": "13.8.0", + "@base-ui/react": "1.6.0", + "@choo-choo/core": "0.2.1", + "@choo-choo/parser-ebnf": "0.1.3", + "@choo-choo/parser-utils": "0.1.3", + "@choo-choo/react": "0.1.3", + "@dnd-kit/core": "6.3.1", + "@dnd-kit/modifiers": "9.0.0", + "@dnd-kit/sortable": "10.0.0", + "@dnd-kit/utilities": "3.2.2", + "@icons-pack/react-simple-icons": "13.13.0", "@mdx-js/loader": "3.1.1", "@mdx-js/node-loader": "3.1.1", "@mdx-js/react": "3.1.1", - "@next/mdx": "16.1.6", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.4", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-tooltip": "^1.2.8", - "@vercel/og": "0.8.6", + "@next/mdx": "16.2.10", + "@orama/orama": "3.1.18", + "@orama/plugin-data-persistence": "3.1.18", + "@renoun/screenshot": "0.3.3", + "@tailwindcss/typography": "0.5.20", + "@vorsteh-queue/cli": "workspace:*", + "@vorsteh-queue/server": "workspace:*", + "agentic-mermaid": "0.1.1", + "better-themes": "1.1.0", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "date-fns": "^4.1.0", - "globby": "16.1.0", - "interweave": "13.1.1", - "lucide-react": "0.563.0", - "multimatch": "7.0.0", - "next": "16.1.6", - "next-themes": "latest", - "p-map": "7.0.4", - "react": "19.2.4", - "react-dom": "19.2.4", - "read-pkg": "^10.0.0", - "rehype-mdx-import-media": "1.2.0", + "dedent": "1.7.2", + "elkjs": "0.12.0", + "entities": "8.0.0", + "globby": "16.2.2", + "lucide-react": "1.25.0", + "multimatch": "8.0.0", + "next": "16.2.10", + "onedollarstats": "0.0.23", + "ora": "9.4.1", + "p-map": "7.0.5", + "react": "19.2.7", + "react-dom": "19.2.7", + "read-pkg": "10.1.0", + "rehype-mdx-import-media": "1.4.0", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.1", "remark-mdx-frontmatter": "5.2.0", "remark-squeeze-paragraphs": "6.0.0", "remark-strip-badges": "7.0.0", - "renoun": "11.1.0", - "tm-grammars": "1.30.0", - "tm-themes": "1.11.0", - "ts-morph": "27.0.2", - "tw-animate-css": "^1.4.0", - "use-debounce": "10.1.0", - "zod": "4.3.6" + "renoun": "11.7.1", + "server-only": "0.0.1", + "shadcn": "4.13.1", + "tailwind-merge": "3.6.0", + "tm-grammars": "1.31.15", + "tm-themes": "1.12.2", + "ts-morph": "28.0.0", + "tw-animate-css": "1.4.0", + "uuid": "14.0.1", + "vaul": "1.1.2", + "yaml": "2.9.0", + "zod": "4.4.3" }, "devDependencies": { - "@tailwindcss/postcss": "4.1.18", - "@tailwindcss/typography": "0.5.19", - "@types/mdx": "2.0.13", - "@types/node": "22.19.0", - "@types/react": "19.2.10", + "@tailwindcss/postcss": "4.3.3", + "@types/mdx": "2.0.14", + "@types/node": "24.13.3", + "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@types/serve-handler": "6.1.4", "@vorsteh-queue/adapter-drizzle": "workspace:*", "@vorsteh-queue/adapter-kysely": "workspace:*", "@vorsteh-queue/adapter-prisma": "workspace:*", "@vorsteh-queue/core": "workspace:*", - "@vorsteh-queue/eslint-config": "workspace:*", - "@vorsteh-queue/prettier-config": "workspace:*", "@vorsteh-queue/tsconfig": "workspace:*", - "eslint": "^9.39.2", - "next-validate-link": "1.6.4", - "pagefind": "1.4.0", - "postcss": "8.5.6", - "prettier": "^3.8.1", - "serve-handler": "6.1.6", - "tailwind-merge": "3.4.0", - "tailwindcss": "4.1.18", - "tailwindcss-animate": "1.0.7", - "tsx": "4.21.0", - "typescript": "^5.9.3" + "commander": "15.0.0", + "postcss": "8.5.19", + "serve-handler": "6.1.7", + "tailwindcss": "4.3.3", + "tsx": "4.23.1", + "typescript": "6.0.3" } } diff --git a/apps/docs/postcss.config.mjs b/apps/docs/postcss.config.mjs index 2f8795a9..f6c75ff9 100644 --- a/apps/docs/postcss.config.mjs +++ b/apps/docs/postcss.config.mjs @@ -1,3 +1,4 @@ +/** @type {import('postcss-load-config').Config} */ const config = { plugins: { "@tailwindcss/postcss": {}, diff --git a/apps/docs/public/.gitkeep b/apps/docs/public/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/apps/docs/public/apple-touch-icon.png b/apps/docs/public/apple-touch-icon.png new file mode 100644 index 00000000..09089c0f Binary files /dev/null and b/apps/docs/public/apple-touch-icon.png differ diff --git a/apps/docs/public/favicon.ico b/apps/docs/public/favicon.ico index 9910ac25..e6b66713 100644 Binary files a/apps/docs/public/favicon.ico and b/apps/docs/public/favicon.ico differ diff --git a/apps/docs/public/icon.svg b/apps/docs/public/icon.svg new file mode 100644 index 00000000..fd93c537 --- /dev/null +++ b/apps/docs/public/icon.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + diff --git a/apps/docs/public/icons/icon-128x128.png b/apps/docs/public/icons/icon-128x128.png deleted file mode 100644 index d36d2e6c..00000000 Binary files a/apps/docs/public/icons/icon-128x128.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-144x144.png b/apps/docs/public/icons/icon-144x144.png deleted file mode 100644 index 22e65d02..00000000 Binary files a/apps/docs/public/icons/icon-144x144.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-152x152.png b/apps/docs/public/icons/icon-152x152.png deleted file mode 100644 index 6e09fbfa..00000000 Binary files a/apps/docs/public/icons/icon-152x152.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-192x192.png b/apps/docs/public/icons/icon-192x192.png deleted file mode 100644 index b9496865..00000000 Binary files a/apps/docs/public/icons/icon-192x192.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-256x256.png b/apps/docs/public/icons/icon-256x256.png deleted file mode 100644 index c5728b43..00000000 Binary files a/apps/docs/public/icons/icon-256x256.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-384x384.png b/apps/docs/public/icons/icon-384x384.png deleted file mode 100644 index d7dbc327..00000000 Binary files a/apps/docs/public/icons/icon-384x384.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-48x48.png b/apps/docs/public/icons/icon-48x48.png deleted file mode 100644 index 0b1dc070..00000000 Binary files a/apps/docs/public/icons/icon-48x48.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-512x512.png b/apps/docs/public/icons/icon-512x512.png deleted file mode 100644 index 8ca4f44d..00000000 Binary files a/apps/docs/public/icons/icon-512x512.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-72x72.png b/apps/docs/public/icons/icon-72x72.png deleted file mode 100644 index 9f5eb1d2..00000000 Binary files a/apps/docs/public/icons/icon-72x72.png and /dev/null differ diff --git a/apps/docs/public/icons/icon-96x96.png b/apps/docs/public/icons/icon-96x96.png deleted file mode 100644 index 633e4182..00000000 Binary files a/apps/docs/public/icons/icon-96x96.png and /dev/null differ diff --git a/apps/docs/public/raja.jpg b/apps/docs/public/raja.jpg new file mode 100644 index 00000000..51f5f4b8 Binary files /dev/null and b/apps/docs/public/raja.jpg differ diff --git a/apps/docs/public/vorsteh-queue-logo-nobg.png b/apps/docs/public/vorsteh-queue-logo-nobg.png deleted file mode 100644 index 54c1d56a..00000000 Binary files a/apps/docs/public/vorsteh-queue-logo-nobg.png and /dev/null differ diff --git a/apps/docs/public/vorsteh-queue-logo.svg b/apps/docs/public/vorsteh-queue-logo.svg deleted file mode 100644 index 4afa9ef3..00000000 --- a/apps/docs/public/vorsteh-queue-logo.svg +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/docs/public/web-app-manifest-192x192.png b/apps/docs/public/web-app-manifest-192x192.png new file mode 100644 index 00000000..21596f16 Binary files /dev/null and b/apps/docs/public/web-app-manifest-192x192.png differ diff --git a/apps/docs/public/web-app-manifest-512x512.png b/apps/docs/public/web-app-manifest-512x512.png new file mode 100644 index 00000000..37242edd Binary files /dev/null and b/apps/docs/public/web-app-manifest-512x512.png differ diff --git a/apps/docs/src/app/(homepage)/_sections/dashboard.tsx b/apps/docs/src/app/(homepage)/_sections/dashboard.tsx new file mode 100644 index 00000000..dc6b515b --- /dev/null +++ b/apps/docs/src/app/(homepage)/_sections/dashboard.tsx @@ -0,0 +1,156 @@ +import { Eye, Filter, Keyboard, Monitor } from "lucide-react" +import { Space_Grotesk } from "next/font/google" +import Link from "next/link" + +import { PageContainer } from "@/components/page-container" +import { buttonVariants } from "@/components/ui/button" + +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"] }) + +const bulletPoints = [ + { icon: Monitor, label: "Real-time queue stats with auto-refresh" }, + { icon: Filter, label: "Filter jobs by status, name, and time range" }, + { icon: Eye, label: "Inspect job details, payload, and errors" }, + { icon: Keyboard, label: "Full keyboard navigation with shortcuts" }, +] + +function TerminalMockup() { + return ( +
+ {/* Terminal title bar */} +
+
+
+
+
+
+ + vorsteh-queue dashboard + +
+ + {/* Terminal content */} +
+ {/* Sidebar + Content layout */} +
+ {/* Sidebar */} +
+ Queues + ▸ email + data-processing + notifications + Views + ▸ Overview + Jobs + Dead Letter +
+ + {/* Main content - Bar chart */} +
+ Job Status +
+ Pending + ████████░░░░░░░░ + 247 +
+
+ Processing + ███░░░░░░░░░░░░░ + 12 +
+
+ Completed + ████████████████ + 1,893 +
+
+ Failed + ██░░░░░░░░░░░░░░ + 8 +
+
+ Dead + █░░░░░░░░░░░░░░░ + 3 +
+
+ Total: 2,163 {" "}[j/↵] View jobs +
+
+
+ + {/* Footer - key hints */} +
+ Tab switch pane {" "} + o overview {" "} + j jobs {" "} + d dead letter {" "} + ? help {" "} + q quit +
+
+
+ ) +} + +export function DashboardSection() { + return ( +
+ +
+ {/* Left column - terminal mockup */} +
+ +
+ + {/* Right column - text */} +
+ + Terminal Dashboard + +

+ Monitor your queues without leaving the terminal. +

+

+ A full-featured interactive TUI dashboard for real-time queue + monitoring. Navigate between queues, inspect jobs, retry failures, + and manage dead letters — all from your terminal. +

+ +
    + {bulletPoints.map((point) => ( +
  • + + {point.label} +
  • + ))} +
+ +
+ + CLI Documentation + + + Try the Example + +
+
+
+
+
+ ) +} diff --git a/apps/docs/src/app/(homepage)/_sections/features.tsx b/apps/docs/src/app/(homepage)/_sections/features.tsx new file mode 100644 index 00000000..42eff12e --- /dev/null +++ b/apps/docs/src/app/(homepage)/_sections/features.tsx @@ -0,0 +1,118 @@ +import { + Activity, + ArrowUpDown, + Calendar, + Clock, + Code2, + Gauge, + GitBranch, + Inbox, + Pause, + RefreshCw, + Repeat, + TrendingUp, +} from "lucide-react" +import { Space_Grotesk } from "next/font/google" + +import { PageContainer } from "@/components/page-container" + +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"] }) + +const features = [ + { + description: "Fire and forget or run exactly once jobs.", + icon: Clock, + title: "One-time Jobs", + }, + { + description: "Schedule jobs to run in the future with precision.", + icon: Pause, + title: "Delayed Jobs", + }, + { + description: "Cron-based scheduling with timezone support.", + icon: Calendar, + title: "Scheduled Jobs", + }, + { + description: "Run jobs on fixed intervals with ease.", + icon: Repeat, + title: "Recurring Jobs", + }, + { + description: "Prioritize what matters most. High to low.", + icon: ArrowUpDown, + title: "Job Priorities", + }, + { + description: "Automatic retries with customizable backoff strategies.", + icon: RefreshCw, + title: "Retries & Backoff", + }, + { + description: "Failed jobs don't disappear. Inspect and retry them safely.", + icon: Inbox, + title: "Dead Letter Queue", + }, + { + description: "Parent-child job hierarchies with dependency tracking.", + icon: GitBranch, + title: "Flow Pipelines", + }, + { + description: "Real-time percentage updates during job execution.", + icon: TrendingUp, + title: "Progress Tracking", + }, + { + description: "Opt-in metrics and distributed tracing via OpenTelemetry.", + icon: Activity, + title: "OpenTelemetry Built-in", + }, + { + description: "Pause, resume, and manage workers with graceful shutdown.", + icon: Gauge, + title: "Worker Controls", + }, + { + description: "Full TypeScript support with excellent DX.", + icon: Code2, + title: "TypeScript First", + }, +] + +export function FeaturesSection() { + return ( +
+ +
+ + Powerful Features + +

+ Everything you need to build robust background systems. +

+
+ +
+ {features.map((feature) => ( +
+
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+
+
+ ) +} diff --git a/apps/docs/src/app/(homepage)/_sections/hero.tsx b/apps/docs/src/app/(homepage)/_sections/hero.tsx new file mode 100644 index 00000000..c0fa779c --- /dev/null +++ b/apps/docs/src/app/(homepage)/_sections/hero.tsx @@ -0,0 +1,124 @@ +import { + ArrowRight, + CheckCircle2, + Database, + Layers, + Shield, +} from "lucide-react" +import { Space_Grotesk } from "next/font/google" +import Link from "next/link" + +import { PageContainer } from "@/components/page-container" +import { buttonVariants } from "@/components/ui/button" +import { useMDXComponents } from "@/mdx-components" + +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"] }) + +const codeSnippet = `import { Queue, MemoryQueueAdapter } from "@vorsteh-queue/core" + +const queue = new Queue(new MemoryQueueAdapter(), { name: "my-queue" }) + +// Register a handler +queue.register("send-email", async (job) => { + await sendEmail(job.payload.to, job.payload.template) + return { sent: true } +}) + +// Add a job +await queue.add("send-email", { + to: "user@example.com", + template: "welcome", +}) + +// Start processing +queue.start()` + +const features = [ + { icon: Database, label: "PostgreSQL 12+" }, + { icon: Layers, label: "ORM-Agnostic" }, + { icon: Shield, label: "Production-Ready" }, + { icon: CheckCircle2, label: "Open Source" }, +] + +export function HeroSection() { + return ( +
+ {/* Dot pattern background — fades to transparent at bottom */} +
+ {/* Orange gradient glow - top center */} +
+ + +
+ {/* Left column - text content */} +
+

+ Reliable Job Queue for Modern Applications +

+ +

+ A type-safe PostgreSQL job queue for Node.js with durable + execution, flow orchestration, and first-class ORM adapters. +

+ +
+ + Get Started + + + + View on GitHub + +
+ +
+ {features.map((feature) => ( + + + {feature.label} + + ))} +
+
+ + {/* Right column - code snippet */} +
+ {useMDXComponents().CodeBlock({ + children: codeSnippet, + language: "ts", + shouldAnalyze: false, + })} +
+
+
+
+ ) +} diff --git a/apps/docs/src/app/(homepage)/_sections/index.ts b/apps/docs/src/app/(homepage)/_sections/index.ts new file mode 100644 index 00000000..84572ffc --- /dev/null +++ b/apps/docs/src/app/(homepage)/_sections/index.ts @@ -0,0 +1,5 @@ +export { HeroSection } from "./hero" +export { WhyChooseSection } from "./why-choose" +export { FeaturesSection } from "./features" +export { ObservabilitySection } from "./observability" +export { OpenSourceSection } from "./open-source" diff --git a/apps/docs/src/app/(homepage)/_sections/observability.tsx b/apps/docs/src/app/(homepage)/_sections/observability.tsx new file mode 100644 index 00000000..23f430a4 --- /dev/null +++ b/apps/docs/src/app/(homepage)/_sections/observability.tsx @@ -0,0 +1,143 @@ +import { Activity, BarChart3, GitBranch } from "lucide-react" +import { Space_Grotesk } from "next/font/google" +import Link from "next/link" + +import { PageContainer } from "@/components/page-container" +import { buttonVariants } from "@/components/ui/button" + +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"] }) + +const bulletPoints = [ + { icon: Activity, label: "Automatic metrics: counters, histograms, gauges" }, + { icon: GitBranch, label: "Distributed tracing with job-level spans" }, + { + icon: BarChart3, + label: "Export to Prometheus, Grafana, Datadog, and more", + }, +] + +export function ObservabilitySection() { + return ( +
+ +
+ {/* Left column - text */} +
+ + OpenTelemetry Native + +

+ Observability without the glue code. +

+

+ vorsteh-queue instruments itself automatically via OpenTelemetry. + Metrics, traces, and job spans flow into your existing + observability stack with zero configuration. +

+ +
    + {bulletPoints.map((point) => ( +
  • + + {point.label} +
  • + ))} +
+ +
+ + Observability Guide + +
+
+ + {/* Right column - code example */} +
+
+ {/* Tab bar */} +
+
+ instrumentation.ts +
+
+ + Zero overhead without SDK + +
+
+ + {/* Code content */} +
+
+                  
+                    
+                      {"// Your existing OTel setup — that's it\n"}
+                    
+                    import
+                    {" { NodeSDK } "}
+                    from
+                    
+                      {" '@opentelemetry/sdk-node'\n"}
+                    
+                    import
+                    
+                      {" { OTLPMetricExporter } "}
+                    
+                    from
+                    
+                      {" '@opentelemetry/exporter-metrics-otlp-http'\n\n"}
+                    
+                    const
+                     sdk = 
+                    new
+                     NodeSDK
+                    {"({\n"}
+                    {"  metricReader: "}
+                    new
+                    
+                      {" OTLPMetricExporter"}
+                    
+                    {"(),\n"})
+                    {"\n\n"}
+                    sdk.
+                    start
+                    ()
+                    {"\n\n"}
+                    
+                      {"// vorsteh-queue automatically emits:\n"}
+                    
+                    
+                      {"// ✓ vorsteh_queue.jobs.processed\n"}
+                    
+                    
+                      {"// ✓ vorsteh_queue.jobs.failed\n"}
+                    
+                    
+                      {"// ✓ vorsteh_queue.jobs.duration\n"}
+                    
+                    
+                      {"// ✓ vorsteh_queue.jobs.wait_time\n"}
+                    
+                    
+                      {"// ✓ Distributed traces per job\n"}
+                    
+                  
+                
+
+
+
+
+
+
+ ) +} diff --git a/apps/docs/src/app/(homepage)/_sections/open-source.tsx b/apps/docs/src/app/(homepage)/_sections/open-source.tsx new file mode 100644 index 00000000..59e46539 --- /dev/null +++ b/apps/docs/src/app/(homepage)/_sections/open-source.tsx @@ -0,0 +1,70 @@ +import { GitFork, Heart, Scale, Star, Users } from "lucide-react" +import { Space_Grotesk } from "next/font/google" + +import { PageContainer } from "@/components/page-container" +import { buttonVariants } from "@/components/ui/button" + +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"] }) + +const badges = [ + { icon: Scale, label: "MIT License" }, + { icon: Users, label: "Community Driven" }, + { icon: GitFork, label: "No vendor lock-in" }, +] + +export function OpenSourceSection() { + return ( +
+ +
+ +
+ +

+ Free & Open Source +

+ +

+ Vorsteh Queue is completely free and open source. Built by developers, + for developers. +

+ +
+ {badges.map((badge) => ( + + + {badge.label} + + ))} +
+ + +
+
+ ) +} diff --git a/apps/docs/src/app/(homepage)/_sections/why-choose.tsx b/apps/docs/src/app/(homepage)/_sections/why-choose.tsx new file mode 100644 index 00000000..964b2ad2 --- /dev/null +++ b/apps/docs/src/app/(homepage)/_sections/why-choose.tsx @@ -0,0 +1,92 @@ +import { Blocks, Clock, Gauge, Layers, Shield, Zap } from "lucide-react" +import { Space_Grotesk } from "next/font/google" + +import { PageContainer } from "@/components/page-container" +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" + +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"] }) + +const reasons = [ + { + description: + "Optimized for high throughput with minimal overhead. Built on PostgreSQL, tuned for performance.", + icon: Zap, + title: "Blazing Fast", + }, + { + description: + "Backed by PostgreSQL's ACID guarantees and advanced locking. Your jobs are safe and consistent.", + icon: Shield, + title: "Reliable by Design", + }, + { + description: + "Works with any ORM or query builder. Use it your way, without being locked in.", + icon: Layers, + title: "ORM Agnostic", + }, + { + description: + "Cron jobs, delayed jobs, and recurring tasks. Powerful scheduling without added complexity.", + icon: Clock, + title: "Flexible Scheduling", + }, + { + description: + "Monitoring, retries, backoff strategies, dead letter queues, and more. Everything you need in production.", + icon: Gauge, + title: "Production Ready", + }, + { + description: + "100% open source. Clean architecture, extensible APIs, and a welcoming community.", + icon: Blocks, + title: "Open & Extensible", + }, +] + +export function WhyChooseSection() { + return ( +
+ +
+ + Why Choose Vorsteh Queue? + +

+ Built for developers who value reliability, +
+ simplicity, and control. +

+
+ +
+ {reasons.map((reason) => ( + + +
+ +
+ {reason.title} + + {reason.description} + +
+
+ ))} +
+
+
+ ) +} diff --git a/apps/docs/src/app/(homepage)/page.tsx b/apps/docs/src/app/(homepage)/page.tsx new file mode 100644 index 00000000..b64f26da --- /dev/null +++ b/apps/docs/src/app/(homepage)/page.tsx @@ -0,0 +1,26 @@ +import { SiteFooter } from "@/components/site-footer" +import { SiteHeader } from "@/components/site-header" + +import { DashboardSection } from "./_sections/dashboard" +import { FeaturesSection } from "./_sections/features" +import { HeroSection } from "./_sections/hero" +import { ObservabilitySection } from "./_sections/observability" +import { OpenSourceSection } from "./_sections/open-source" +import { WhyChooseSection } from "./_sections/why-choose" + +export default function Page() { + return ( +
+ + + + + + + + + + +
+ ) +} diff --git a/apps/docs/src/app/(site)/(docs)/[...slug]/page.tsx b/apps/docs/src/app/(site)/(docs)/[...slug]/page.tsx deleted file mode 100644 index df69416e..00000000 --- a/apps/docs/src/app/(site)/(docs)/[...slug]/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { Metadata } from "next" -import { notFound } from "next/navigation" - -import { getBreadcrumbItems, transformedEntries } from "~/collections" -import { DirectoryContent } from "~/components/directory-content" -import { FileContent } from "~/components/file-content" - -export async function generateStaticParams() { - const slugs = (await transformedEntries()).map((entry) => ({ - slug: entry.segments, - })) - - return slugs -} - -export async function generateMetadata(props: PageProps<"/[...slug]">): Promise { - const { slug } = await props.params - const breadcrumbItems = await getBreadcrumbItems(slug) - - const titles = breadcrumbItems.map((ele) => ele.title) - - return { - title: `${titles.join(" - ")}`, - } -} - -export default async function DocsPage(props: PageProps<"/[...slug]">) { - const { slug } = await props.params - - const searchParam = `/${slug.join("/")}` - - const transformedEntry = (await transformedEntries()).find( - (ele) => ele.raw_pathname == searchParam, - ) - - if (!transformedEntry) { - return notFound() - } - - // if we can't find an index file, but we have a valid directory - // use the directory component for rendering - if (!transformedEntry.file && transformedEntry.isDirectory) { - return ( - <> - - - ) - } - - // if we have a valid file ( including the index file ) - // use the file component for rendering - if (transformedEntry.file) { - return ( - <> - - - ) - } - - // seems to be an invalid path - return notFound() -} diff --git a/apps/docs/src/app/(site)/(docs)/layout.tsx b/apps/docs/src/app/(site)/(docs)/layout.tsx deleted file mode 100644 index aa227b3b..00000000 --- a/apps/docs/src/app/(site)/(docs)/layout.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { TreeItem } from "~/lib/navigation" -import { AllDocumentation } from "~/collections" -import { DocsSidebar } from "~/components/docs-sidebar" -import { Sidebar, SidebarContent, SidebarInset, SidebarProvider } from "~/components/ui/sidebar" -import { getTree } from "~/lib/navigation" - -export function AppSidebar({ items }: { items: TreeItem[] }) { - return ( - - - - - - ) -} - -export default async function DocsLayout(props: LayoutProps<"/">) { - const recursiveCollections = await AllDocumentation.getEntries({ - recursive: true, - }) - - // here we're generating the items for the dropdown menu in the sidebar - // it's used to provide a short link for the user to switch easily between the different collections - // it expects an `index.mdx` file in each collection at the root level ( e.g. `aria-docs/index.mdx`) - - const tree = recursiveCollections.filter((ele) => ele.depth === 0) - - const sidebarItems = await getTree(tree) - - return ( -
- - - -
-
- {props.children} -
-
-
-
-
- ) -} diff --git a/apps/docs/src/app/(site)/(home)/page.tsx b/apps/docs/src/app/(site)/(home)/page.tsx deleted file mode 100644 index 7494cc0e..00000000 --- a/apps/docs/src/app/(site)/(home)/page.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import Link from "next/link" -import { CheckCircle, Code, Heart, Info } from "lucide-react" -import pMap from "p-map" -import { CodeBlock, Link as GitLink, Logo as GitLogo } from "renoun/components" - -import type { AllowedIcon } from "~/lib/icon" -import { features } from "~/collections" -import { Button } from "~/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card" -import { asyncFilter } from "~/lib/array-helper" -import { getIcon } from "~/lib/icon" - -const example_snippet = /* typescript */ ` -import { MemoryQueueAdapter, Queue } from "@vorsteh-queue/core" - -interface TEmailPayload { - to: string - subject: string -} - -interface TEmailResult { - sent: boolean -} - -const adapter = new MemoryQueueAdapter() -const queue = new Queue(adapter, { name: "email-queue" }) - -queue.register("send-email", async ({ payload }) => { - // Send email logic here - return { sent: true } -}) - -await queue.add("send-email", { to: "user@example.com", subject: "Welcome!" }) -queue.start() -` - -export default async function Home() { - const featuresCollection = await features.getEntries() - - const filterKeyFeatures = await asyncFilter(featuresCollection, async (ele) => { - const frontmatter = await ele.getExportValue("frontmatter") - - return frontmatter.type === "key_feature" - }) - - const filterOtherFeatures = await asyncFilter(featuresCollection, async (ele) => { - const frontmatter = await ele.getExportValue("frontmatter") - - return frontmatter.type === "feature" - }) - - const keyFeatures = await pMap(filterKeyFeatures, async (ele) => { - const frontmatter = await ele.getExportValue("frontmatter") - const description = await ele.getExportValue("default") - return { - title: frontmatter.title, - description, - icon: frontmatter.icon as AllowedIcon, - } - }) - - const otherFeatures = await pMap(filterOtherFeatures, async (ele) => { - const frontmatter = await ele.getExportValue("frontmatter") - const description = await ele.getExportValue("default") - return { - title: frontmatter.title, - description, - icon: frontmatter.icon as AllowedIcon, - } - }) - - return ( - <> - {/* Hero Section */} -
-
-
-
-

- Reliable Job Queue for Modern Applications -

-

- A powerful, ORM-agnostic queue engine for PostgreSQL 12+. Handle background jobs, - scheduled tasks, and recurring processes with ease. -

-
- - - -
-
-
- {/* Code Example */} - - {example_snippet} - -
-
-
-
- - {/* Why Section */} -
-
-
-

- Why Choose Vorsteh Queue? -

-

- Built for developers who need reliability, flexibility, and excellent developer - experience -

-
- -
- {keyFeatures.map((ele, eleIdx) => ( - - -
- {getIcon(ele.icon, { className: "text-orange-primary h-6 w-6" })} -
- {ele.title} -
- - - - - -
- ))} -
-
-
- - {/* Features Section */} -
-
-
-

- Powerful Features -

-

- Everything you need to handle background processing in your applications -

-
- -
- {otherFeatures.map((ele, eleIdx) => ( -
-
- {getIcon(ele.icon, { className: "text-orange-primary h-6 w-6" })} -
-

{ele.title}

-
- -
-
- ))} -
-
-
- - {/* About Section */} -
-
-
-
- -
-

- About the Name: Vorsteh Queue -

-

- The name "Vorsteh Queue" is a tribute to our beloved German Spaniel. This breed is - closely related to the Münsterländer, a type of pointing dog, known in German as a - "Vorstehhund". Just as a pointing dog steadfastly indicates its target, Vorsteh Queue - aims to reliably point your application towards efficient and robust background job - processing. -

-

- The inspiration for naming a tech project after a dog comes from the delightful story - of{" "} - - Bruno - - , the API client. It's a nod to the personal touch and passion that drives open-source - development, much like the loyalty and dedication of our canine companions. -

-
-
-
- - {/* Open Source Section */} -
-
-
-
- -
-

- Free & Open Source -

-

- Vorsteh Queue is completely free and open source. Built by developers, for developers. - No hidden costs, no vendor lock-in, no limitations. Use it in your personal projects, - startups, or enterprise applications. -

-
-
- - MIT License -
-
- - Community Driven -
-
- - No Vendor Lock-in -
-
-
- - - -
-
-
-
- - ) -} diff --git a/apps/docs/src/app/(site)/layout.tsx b/apps/docs/src/app/(site)/layout.tsx deleted file mode 100644 index b29b2002..00000000 --- a/apps/docs/src/app/(site)/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import Footer from "~/components/footer" -import MainHeader from "~/components/main-header" - -export default function SiteLayout({ children }: LayoutProps<"/">) { - return ( -
- {/* Header */} - - {children} -
-
- ) -} diff --git a/apps/docs/src/app/_opengraph-image.js b/apps/docs/src/app/_opengraph-image.js deleted file mode 100644 index 5440aca0..00000000 --- a/apps/docs/src/app/_opengraph-image.js +++ /dev/null @@ -1,99 +0,0 @@ -import { ImageResponse } from "next/og" - -export const dynamic = "force-static" -export const contentType = "image/png" - -// eslint-disable-next-line @typescript-eslint/require-await -export default async function Image() { - return new ImageResponse( -
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- Vorsteh Queue -
-
-
- A powerful, ORM-agnostic queue engine for PostgreSQL 12+. -
-
-
-
, - { - width: 1200, - height: 630, - }, - ) -} diff --git a/apps/docs/src/app/docs.snapshot.jsonl/route.ts b/apps/docs/src/app/docs.snapshot.jsonl/route.ts new file mode 100644 index 00000000..4d272c1c --- /dev/null +++ b/apps/docs/src/app/docs.snapshot.jsonl/route.ts @@ -0,0 +1,14 @@ +import { buildDocsSnapshotJsonl } from "@/lib/docs-snapshot" + +export const dynamic = "force-static" +export const runtime = "nodejs" + +export async function GET(): Promise { + const jsonl = await buildDocsSnapshotJsonl() + + return new Response(jsonl, { + headers: { + "Content-Type": "application/x-ndjson; charset=utf-8", + }, + }) +} diff --git a/apps/docs/src/app/docs.snapshot/[collectionName]/route.ts b/apps/docs/src/app/docs.snapshot/[collectionName]/route.ts new file mode 100644 index 00000000..9f5708cc --- /dev/null +++ b/apps/docs/src/app/docs.snapshot/[collectionName]/route.ts @@ -0,0 +1,54 @@ +import { notFound } from "next/navigation" + +import type { AvailableCollection } from "@/collections" +import { availableCollections } from "@/collections" +import { buildDocsSnapshotJsonl } from "@/lib/docs-snapshot" + +export const dynamic = "force-static" +export const runtime = "nodejs" + +function normalizeCollectionName( + collectionName: string +): AvailableCollection | undefined { + if (!collectionName.endsWith(".jsonl")) { + return undefined + } + + const collection = collectionName.slice(0, -6) + + if (!collection) { + return undefined + } + + return availableCollections.includes(collection as AvailableCollection) + ? (collection as AvailableCollection) + : undefined +} + +export async function generateStaticParams(): Promise< + { collectionName: string }[] +> { + return availableCollections.map((collection) => ({ + collectionName: `${collection}.jsonl`, + })) +} + +export async function GET( + _request: Request, + ctx: { params: Promise<{ collectionName: string }> } +): Promise { + const { collectionName } = await ctx.params + const collection = normalizeCollectionName(collectionName) + + if (!collection) { + notFound() + } + + const jsonl = await buildDocsSnapshotJsonl({ collection }) + + return new Response(jsonl, { + headers: { + "Content-Type": "application/x-ndjson; charset=utf-8", + }, + }) +} diff --git a/apps/docs/src/app/docs/[...slug]/_loading.tsx b/apps/docs/src/app/docs/[...slug]/_loading.tsx new file mode 100644 index 00000000..c5adb126 --- /dev/null +++ b/apps/docs/src/app/docs/[...slug]/_loading.tsx @@ -0,0 +1,28 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export default function Loading() { + // The docs shell (sidebar, header, TOC rail) is rendered by the segment layout. + // Keep the loading UI limited to the page content to avoid duplicate sidebars. + return ( +
+
+ + +
+ +
+ + + + +
+ +
+ + + + +
+
+ ) +} diff --git a/apps/docs/src/app/docs/[...slug]/layout.tsx b/apps/docs/src/app/docs/[...slug]/layout.tsx new file mode 100644 index 00000000..0bdfca60 --- /dev/null +++ b/apps/docs/src/app/docs/[...slug]/layout.tsx @@ -0,0 +1,257 @@ +import type { ContentSection } from "renoun/file-system" +import { isFile } from "renoun/file-system" +import type z from "zod" + +import { + getFileContent, + getDocumentationEntryBySlug, + getApiReferenceExports, + rootCollections, + getBreadcrumbItems, + getMetadata, +} from "@/collection-helpers" +import type { ApiReferenceResult } from "@/collection-helpers" +import { SiteBreadcrumb } from "@/components/breadcrumb" +import { CollectionChooser } from "@/components/collection-chooser" +import { DocsLeftRailBackground } from "@/components/docs-left-rail-background" +import { DocsSidebar } from "@/components/docs-sidebar" +import { MobileDocsHeader } from "@/components/mobile-docs-header" +import { SidebarToggle } from "@/components/sidebar-toggle" +import { + MobileTableOfContents, + TableOfContents, +} from "@/components/table-of-contents" +import { TableOfContentsScript } from "@/components/toc" +import { SidebarInset } from "@/components/ui/sidebar" +import { SidebarGridProvider } from "@/components/ui/sidebar-grid-provider" +import { getCliCommandTocSections } from "@/lib/cli-commands" +import { + getCollectionNavigation, + getFavoriteNavigationItems, +} from "@/lib/navigation" +import { cn } from "@/lib/utils" +import type { frontmatterSchema } from "@/validations" + +function createDocsLayoutConfig(input: { + layoutWidth: string + sidebarMinWidth: string + sidebarPreferredWidth: string + sidebarMaxWidth: string + tocContentWidth: string + tocPaddingX?: string + tocBorderWidth?: string +}) { + const { + layoutWidth, + sidebarMinWidth, + sidebarPreferredWidth, + sidebarMaxWidth, + tocContentWidth, + tocPaddingX = "1.5rem", + tocBorderWidth = "1px", + } = input + + return { + cssVars: { + "--docs-content-track-width": + "minmax(0, calc(var(--docs-layout-width) - var(--docs-sidebar-track-width) - var(--docs-toc-width)))", + "--docs-gutter-width": `clamp(0px, calc((100vw - var(--docs-layout-width)) / 2), calc(var(--docs-layout-width)))`, + "--docs-layout-width": layoutWidth, + "--docs-left-rail-width": + "calc(var(--docs-gutter-width) + var(--docs-sidebar-track-width))", + "--docs-sidebar-track-width": `clamp(${sidebarMinWidth}, ${sidebarPreferredWidth}, ${sidebarMaxWidth})`, + "--docs-toc-border-width": tocBorderWidth, + "--docs-toc-content-width": tocContentWidth, + "--docs-toc-padding-x": tocPaddingX, + "--docs-toc-width": `calc(var(--docs-toc-content-width) + (2 * var(--docs-toc-padding-x)) + var(--docs-toc-border-width))`, + } as React.CSSProperties, + layoutWidth, + sidebarMaxWidth, + sidebarMinWidth, + sidebarPreferredWidth, + tocBorderWidth, + tocContentWidth, + tocPaddingX, + } +} + +// Keep the previous 3-column docs shell semantics, just without parallel routes. +const DOCS_LAYOUT = createDocsLayoutConfig({ + layoutWidth: "100rem", + sidebarMaxWidth: "300px", + sidebarMinWidth: "250px", + sidebarPreferredWidth: "260px", + tocBorderWidth: "0px", + tocContentWidth: "250px", +}) + +async function fetchApiReferenceSources( + references: z.infer["apiReference"] +) { + return getApiReferenceExports(references) +} + +function flatExportsToTocItems( + exports: ApiReferenceResult["exports"], + baseDepth: number +) { + return exports.map((exp) => ({ + depth: baseDepth, + id: exp.slug, + title: exp.title, + ...(exp.kind + ? { + jsx: ( + + + {exp.kind.charAt(0)} + + {exp.title} + + ), + } + : {}), + ...(exp.methods?.length + ? { + children: exp.methods.map((m) => ({ + depth: baseDepth, + id: m.slug, + jsx: {m.title}, + title: m.title, + })), + } + : {}), + })) +} + +export default async function DocsSlugLayout({ + children, + params, +}: { + children: React.ReactNode + params: Promise<{ slug: string[] }> +}) { + const { slug } = await params + const docsLayoutVars = DOCS_LAYOUT.cssVars + const docsGridClasses = cn( + "relative z-10 overflow-x-clip", + // Default desktop grid: left gutter, sidebar, content, toc, right gutter. + "xl:grid-cols-[minmax(min-content,1fr)_var(--docs-sidebar-track-width)_var(--docs-content-track-width)_var(--docs-toc-width)_minmax(min-content,1fr)]", + // Offcanvas desktop grid: keep content + TOC centered when the sidebar collapses. + "has-data-[collapsible=offcanvas]:xl:grid-cols-[minmax(min-content,1fr)_0_var(--docs-content-track-width)_var(--docs-toc-width)_minmax(min-content,1fr)]" + ) + + const [collection] = slug + + if (!collection) { + return null + } + + const [availableCollections, entry, navigationItems, breadcrumbItems] = + await Promise.all([ + rootCollections(), + getDocumentationEntryBySlug(slug), + getCollectionNavigation(collection), + getBreadcrumbItems(slug), + ]) + + const currentCollection = availableCollections.find( + (item) => item.group === collection + ) + const favoriteItems = getFavoriteNavigationItems(navigationItems) + + let headings: ContentSection[] = [] + + if (isFile(entry)) { + const fileContent = await getFileContent(entry) + const frontmatter = await getMetadata(fileContent) + headings = (await fileContent?.getSections()) ?? [] + + if (frontmatter?.apiReference && frontmatter.apiReference.length > 0) { + const results = await fetchApiReferenceSources(frontmatter.apiReference) + const hasMultipleSources = results.length > 1 + const referenceSections = hasMultipleSources + ? results.map((result) => ({ + children: flatExportsToTocItems(result.exports, 4), + depth: 3, + id: result.name.replaceAll(/[^a-z0-9-]/gu, "-"), + title: result.name, + })) + : flatExportsToTocItems( + results.flatMap((r) => r.exports), + 3 + ) + + headings = [ + ...headings, + + ...(referenceSections.length + ? [ + { + children: referenceSections, + depth: 2, + id: "api-reference", + title: "API Reference", + }, + ] + : []), + ] + } + + if (frontmatter?.cliCommand) { + const cliSections = getCliCommandTocSections(frontmatter.cliCommand) + // Replace any static "Options"/"Arguments" headings from the MDX + // with the dynamic ones that have individual items as children + headings = [ + ...headings.filter((h) => h.id !== "options" && h.id !== "arguments"), + ...cliSections, + ] + } + } + + return ( +
+ + + + + } + /> + + + + {headings.length > 0 && } +
+
+ + {children} +
+
+
+ +
+
+ ) +} diff --git a/apps/docs/src/app/docs/[...slug]/page.tsx b/apps/docs/src/app/docs/[...slug]/page.tsx new file mode 100644 index 00000000..051b0d7e --- /dev/null +++ b/apps/docs/src/app/docs/[...slug]/page.tsx @@ -0,0 +1,351 @@ +import type { Metadata } from "next" +import { Space_Grotesk } from "next/font/google" +import { notFound } from "next/navigation" +import { isDirectory, isFile, MDX } from "renoun" +import type { ModuleExport } from "renoun" + +import { + getFileContent, + getDocumentationEntryBySlug, + getMetadata, + getSections, + getTitle, + staticRoutes, + getBreadcrumbItems, + getEntryFrontmatter, +} from "@/collection-helpers" +import type { EntryType } from "@/collection-helpers" +import { PackagesDirectory } from "@/collections" +import { DocsPageActions } from "@/components/docs-page-actions" +import { References } from "@/components/mdx/reference" +import SectionGrid from "@/components/section-grid" +import Siblings from "@/components/siblings" +import { cn } from "@/lib/utils" +import { toRawHref } from "@/shared/doc-paths" + +const spaceGrotesk = Space_Grotesk({ subsets: ["latin"] }) +const SITE_URL = "https://vorsteh-queue.dev" + +function toJsonLd(value: unknown) { + return JSON.stringify(value).replaceAll(" ({ slug })) +} +export async function generateMetadata( + params: PageProps<"/docs/[...slug]"> +): Promise { + const { slug } = await params.params + const [collection] = slug + + let entry: EntryType | null = null + + try { + entry = await getDocumentationEntryBySlug(slug) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch { + entry = null + } + const breadcrumbItems = await getBreadcrumbItems(slug) + const metadata = entry ? await getEntryFrontmatter(entry) : null + + const titles = breadcrumbItems.map((ele) => ele.title) + + const alternateTypes: NonNullable["types"] = { + "application/x-ndjson": "/docs.snapshot.jsonl", + } + + if (collection) { + alternateTypes["application/x-ndjson+collection"] = + `/docs.snapshot/${collection}.jsonl` + } + + return { + alternates: { + types: alternateTypes, + }, + description: metadata?.description ?? "", + title: `${titles.join(" - ")}`, + } +} +export default async function DocsPage({ + params, +}: PageProps<"/docs/[...slug]">) { + const { slug } = await params + const normalizedSlug = slug.filter((segment) => segment !== "index") + const docsPath = + normalizedSlug.length > 0 ? `/docs/${normalizedSlug.join("/")}` : "/docs" + const pageUrl = `${SITE_URL}${docsPath}` + + let entry: EntryType | null = null + try { + entry = await getDocumentationEntryBySlug(slug) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch { + notFound() + } + + const sections = await getSections(entry) + // oxlint-disable-next-line react-doctor/server-sequential-independent-await -- acceptable for readability + const breadcrumbItems = await getBreadcrumbItems(slug) + const breadcrumbJsonLd = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + item: `${SITE_URL}/docs`, + name: "Docs", + position: 1, + }, + ...breadcrumbItems.map((item, index) => ({ + "@type": "ListItem", + item: `${SITE_URL}/docs/${item.path.join("/")}`, + name: item.title, + position: index + 2, + })), + ], + } + + if (!isFile(entry) && isDirectory(entry)) { + const pageJsonLd = { + "@context": "https://schema.org", + "@type": "WebPage", + inLanguage: "en", + isPartOf: { + "@id": SITE_URL, + }, + name: getTitle(entry), + url: pageUrl, + } + + return ( +
+ {/* oxlint-disable react/no-danger -- rendered HTML content from trusted source */} +