diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..52524ab --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/pgboss_queue_test diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..f78838c --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,74 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run lint + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run build + + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: pgboss_queue_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - run: bun run test + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/pgboss_queue_test + + node-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + - run: bun install --frozen-lockfile + - run: bun run build + - run: node --version + - run: node scripts/assert-node-package.mjs + + complete: + if: always() + needs: [lint, build, test, node-package] + runs-on: ubuntu-latest + steps: + - name: Require all test jobs to pass + run: | + if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + exit 1 + fi diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..6f4247a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +26 diff --git a/CLAUDE.md b/CLAUDE.md index 0ef1f8b..e88a5a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,21 +45,21 @@ A PR with no `docs/plans/` diff is only OK when the change is truly unrelated (t ## Tooling (once Phase 1 exists) -This is a **Bun + TypeScript** project. Use `bun`, never `npm` or `npx`, for install/test/run. Use `bunx` if you need a package runner. PRs must stay green on `.github/workflows/test.yaml` (lint, build, `bun test` against Postgres). +This is a **Bun + TypeScript** project. Use `bun`, never `npm` or `npx`, for install/lint/build/test. Use `bunx` if you need a package runner. PRs must stay green on `.github/workflows/test.yaml` (lint, build, `bun test` against Postgres, Node package import). ```bash bun install # install bun test # bun:test, needs Postgres +node scripts/assert-node-package.mjs # after build; must use Node, not bun bun run lint # biome check bun run format # biome write -bun run build # tsc / bun build of src → dist +bun run build # tsc of src → dist, plus test typecheck bun docs:dev # VitePress (Phase 9) ``` -Local Postgres (Phase 1 `docker-compose.yml`): +Local Postgres: set `DATABASE_URL` (see `.env.example`). CI starts Postgres as a workflow service; there is no `docker-compose.yml`. ```bash -docker compose up -d postgres # DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/pgboss_queue_test ``` @@ -131,11 +131,11 @@ Do **not** accept `pkg: "ioredis"`, `redis: Redis`, or `database: number`. Those ## Coding conventions -- **TypeScript strict.** No `as any`. Use `@ts-expect-error` with a comment when the type system cannot express something. +- **TypeScript strict (`noImplicitAny`).** No `any` and no `as any`. Biome `noExplicitAny` is an error. Use `@ts-expect-error` with a comment when the type system cannot express something. - **JSDoc on every public class, method, and exported type.** `@param` for each parameter (including edge cases), `@returns` when non-obvious, `@throws` when applicable. Match node-resque's documented Queue methods. - **No Python.** New scripts, CLIs, and tooling are Bun + TypeScript. - **Biome** for format/lint (keryx-style), not Prettier. -- **Tests use `bun:test`**, not Jest. Port node-resque tests faithfully: same `describe` / `test` names, same assertions, Postgres `specHelper` instead of Redis. +- **Tests use `bun:test`**, not Jest. Port node-resque tests faithfully: same `describe` / `test` names, same assertions, Postgres `specHelper` instead of Redis. Node must still be able to import the compiled package (`node scripts/assert-node-package.mjs`); do not run the Bun suite on Node. - **Every behavior change ships with tests.** A PR with no test changes is a red flag unless it is docs-only. - **Do not add dependencies** unless a phase plan names them. Expected runtime deps: `pg-boss`, `pg`. Dev: `typescript`, `@types/pg`, `biome`, `bun` types. diff --git a/README.md b/README.md index 34d5f3f..1770a3e 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ Raise your Postgres pool `max` when using a large `maxTaskProcessors`. Events ma ## Requirements -- Node.js 20+ or [Bun](https://bun.sh) +- Node.js 26+ or [Bun](https://bun.sh) - PostgreSQL 13+ (`SKIP LOCKED`) ```bash diff --git a/__tests__/smoke.test.ts b/__tests__/smoke.test.ts new file mode 100644 index 0000000..bc269e2 --- /dev/null +++ b/__tests__/smoke.test.ts @@ -0,0 +1,17 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import { connect, disconnect } from "./utils/specHelper"; + +describe("Postgres smoke test", () => { + afterAll(disconnect); + + test("DATABASE_URL is defined", () => { + expect(process.env.DATABASE_URL).toBeDefined(); + }); + + test("SELECT 1 returns 1", async () => { + const pool = await connect(); + const result = await pool.query<{ value: number }>("SELECT 1 AS value"); + + expect(result.rows[0]?.value).toBe(1); + }); +}); diff --git a/__tests__/utils/specHelper.ts b/__tests__/utils/specHelper.ts new file mode 100644 index 0000000..9ac71d7 --- /dev/null +++ b/__tests__/utils/specHelper.ts @@ -0,0 +1,38 @@ +import { Pool, type PoolConfig } from "pg"; + +const connectionString = process.env.DATABASE_URL; + +if (!connectionString) { + throw new Error( + "DATABASE_URL is required to run the test suite. See .env.example.", + ); +} + +export const connectionDetails: PoolConfig = { connectionString }; +export const timeout = 500; +export const queue = "default"; +export const schema = "pgboss_queue_test"; + +let pool: Pool | undefined; + +export async function connect(): Promise { + pool ??= new Pool(connectionDetails); + await pool.query("SELECT 1"); + return pool; +} + +export async function disconnect(): Promise { + if (!pool) return; + + await pool.end(); + pool = undefined; +} + +export async function cleanup(): Promise { + const connection = await connect(); + await connection.query("SELECT 1"); +} + +export async function popFromQueue(): Promise { + throw new Error("not implemented"); +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..82a0ad1 --- /dev/null +++ b/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json", + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "files": { + "includes": [ + "**", + "!**/node_modules", + "!**/dist", + "!**/docs/.vitepress/dist", + "!**/docs/.vitepress/cache", + "!**/*.md" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended", + "suspicious": { + "noExplicitAny": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "trailingCommas": "all" + } + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..b84ad40 --- /dev/null +++ b/bun.lock @@ -0,0 +1,118 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "pgboss-queue", + "dependencies": { + "pg": "^8.23.0", + }, + "devDependencies": { + "@biomejs/biome": "^2.5.10", + "@types/bun": "^1.4.0", + "@types/node": "^26.3.0", + "@types/pg": "^8.23.1", + "typescript": "^7.0.2", + }, + }, + }, + "packages": { + "@biomejs/biome": ["@biomejs/biome@2.5.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.10", "@biomejs/cli-darwin-x64": "2.5.10", "@biomejs/cli-linux-arm64": "2.5.10", "@biomejs/cli-linux-arm64-musl": "2.5.10", "@biomejs/cli-linux-x64": "2.5.10", "@biomejs/cli-linux-x64-musl": "2.5.10", "@biomejs/cli-win32-arm64": "2.5.10", "@biomejs/cli-win32-x64": "2.5.10" }, "bin": { "biome": "bin/biome" } }, "sha512-WRKXARA3kTuiV5sxqTpobJ/I0MVd4vk3pOL6wnp5az4LntFIhWTj1RWZq3DI9PCEN3lXcqy7p5aqUHzvq8AXyQ=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ItCrxKK6SXVT6flYs0qIuBd4AA3TTTl4d66Re6YI2FuGZnN85NmuYNzkiTJUyYw8qBLv69L5zTUB6uyWd++h3Q=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-yLsPU9pAmtChXDu8vhKAzErqe+LeeYuwuUB2FZMkRitsmdodxsYRa9KHrFispsUHzzOu+9HB3nP/TQxyia+Sjw=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-VG8uQW/86a1roLaIFvtIbEigxIdzdJ190oGyg1tV7VYeQtOS+x10sflk7WbuXgw91EtZX5DlIIIej1YqkNLlcg=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-t1QAKZwQJRB4dvgJSgFiQ4BNfNPChg69BNonz854qLVxnjT3UvDzQg9mbkTJRu35ZqU0Rw10A73J8Urgbg2RPw=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-4O6T0eq2heoHZN0a9UX+rWQoxXEBaKf+lRi2hbsGlHneUz9BWXM76nEWMK7Eeq8gzMxR1khQB6BFpAASpeXqGg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-pgDDqp9JybHm2I0KRgzN6i4+lt8xu4iqxUwLzglUMmOmyRTU1AYBGKzh9sNMOtIjah7xoWvKHlLVetvyifzoiQ=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-pxAbxduPO4xq/Cvgaa2lOrs9BB0hEXmmDqfMNP4ZOffGOkUrD1/QGw9UAMpFQpX2P8MqTIIRuQKcmetum4Oa6A=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-M+2dgBsl3lXRiTfgPVc2p3anS4Tocojke4rzFLScZ2Y/wmF+36dRb1iHCLiyGqOzQGyTplZH1HnEYviiAqi3nA=="], + + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "pg": ["pg@8.23.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg=="], + + "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], + + "pg-connection-string": ["pg-connection-string@2.14.0", "", {}, "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], + + "pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + } +} diff --git a/docs/plans/01-repo-scaffold.md b/docs/plans/01-repo-scaffold.md index 2e7314e..072a1b7 100644 --- a/docs/plans/01-repo-scaffold.md +++ b/docs/plans/01-repo-scaffold.md @@ -1,6 +1,6 @@ # Phase 1 — Repo scaffold, test harness, and CI -**Status:** not-started +**Status:** done **Depends on:** Phase 0 ## Goal @@ -23,16 +23,16 @@ CI must exist **before** Queue/Worker code. An empty `expect(true)` that never o - `type`: `"module"` - `main` / `types`: `dist/index.js` / `dist/index.d.ts` - `exports` with `import` + types - - `engines.node`: `>=20` + - `engines.node`: `>=26` - `license`: `Apache-2.0` - - `scripts`: `"test": "bun test --concurrency=1"`, `build`, `lint`, `format` (docs scripts wait for Phase 9) + - `scripts`: `"test": "bun test --max-concurrency=1"`, `"test:node-package": "node scripts/assert-node-package.mjs"`, `build`, `lint`, `format` (docs scripts wait for Phase 9) - `devDependencies`: `typescript`, `@types/node`, `@types/pg`, `@biomejs/biome`, `@types/bun` - `dependencies`: `pg` now (smoke test uses it). Add `pg-boss` in Phase 2 if you want to keep this PR smaller — either is fine as long as CI is green. -- `tsconfig.json` — `strict`, `ES2022`, `moduleResolution: bundler` or `nodenext`, `declaration`, `outDir: dist`, `rootDir: src` -- `biome.json` — match keryx reasonably (indent 2, no unused imports) +- `tsconfig.json` — `strict`, `noImplicitAny: true`, `ES2022`, `moduleResolution: bundler` or `nodenext`, `declaration`, `outDir: dist`, `rootDir: src`. `tsconfig.test.json` typechecks `__tests__` with `noEmit` (so implicit `any` in tests fails `build` too). +- `biome.json` — match keryx reasonably (indent 2, no unused imports). `suspicious/noExplicitAny` is `error` so `: any` and `as any` fail `lint`. - `.gitignore` — `node_modules`, `dist`, `.env`, `docs/.vitepress/dist`, `*.log` - `LICENSE` — Apache-2.0 -- `.nvmrc` or `.node-version` — `20` +- `.nvmrc` or `.node-version` — `26` - `.env.example` — `DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/pgboss_queue_test` ### Source stub @@ -46,22 +46,12 @@ Keep the file compiling. Do not fake Queue/Worker yet. ### Local Postgres -`docker-compose.yml`: - -```yaml -services: - postgres: - image: postgres:16 - environment: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: pgboss_queue_test - ports: - - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 10 +No `docker-compose.yml`. CI starts Postgres as a GitHub Actions service. Locally, point `DATABASE_URL` at any Postgres 13+ you already run (homebrew, apt, a shared dev database, etc.). Tests must never target production. + +`.env.example` is the only local-DB contract: + +``` +DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/pgboss_queue_test ``` ### Test harness (not a dummy assert) @@ -80,77 +70,23 @@ services: - `SELECT 1` returns `1` through specHelper's pool - `bun run build` artifacts exist *or* that check lives in CI only (prefer CI `bun run build`) -Do **not** ship only `expect(true).toBe(true)`. +Do **not** ship only `expect(true).toBe(true)`. Tests use `bun:test`. Node compatibility is `scripts/assert-node-package.mjs`: after `bun run build`, Node 26 imports `package.json` `exports["."].import` (`dist/index.js`) and asserts `process.versions.bun` is unset. Later phases should import real public APIs in that script (do not re-run the Bun suite on Node). ### CI — full test workflow now -`.github/workflows/test.yaml` is the product gate from this PR onward. Copy this shape (keryx `test.yaml` + node-resque's Postgres-instead-of-Redis): - -```yaml -name: Test -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run lint - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run build - - test: - runs-on: ubuntu-latest - services: - postgres: - image: postgres:16 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: pgboss_queue_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun test - env: - DATABASE_URL: postgres://postgres:postgres@localhost:5432/pgboss_queue_test - - complete: - if: always() - needs: [lint, build, test] - runs-on: ubuntu-latest - steps: - - run: | - if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then - exit 1 - fi -``` +`.github/workflows/test.yaml` is the product gate from this PR onward. Jobs: `lint`, `build`, `test` (Postgres 16 service, **Bun `bun:test`**), `node-package` (Node 26 imports the compiled package), `complete`. + +- `test`: `bun run test` with `DATABASE_URL=postgres://postgres:postgres@localhost:5432/pgboss_queue_test` +- `node-package`: `actions/setup-node` from `.nvmrc` (26), `bun run build`, then **`node scripts/assert-node-package.mjs`** (not `bun run`; no Postgres) + +See the workflow file for the YAML. Do not duplicate a second test pipeline in Phase 10. Rules: - Do **not** wait for Phase 10 to add this file. Phase 10 is npm publish + GitHub Pages only. - Do **not** add a `docs` job yet (VitePress does not exist). Phase 9 appends it. - `complete` must fail the workflow if lint, build, or test failed. -- Branch protection (when maintainers can set it): require `complete`. +- Branch protection (maintainer UI; this agent cannot set it): require `complete` and `Cursor Bugbot`. ### README / CLAUDE.md @@ -161,8 +97,9 @@ Rules: - `bun install` works locally - `bun run build` emits `dist/` - `bun run lint` is clean -- `docker compose up -d postgres` + `bun test` is green locally -- **GitHub Actions on this PR is green** (lint, build, Postgres test job, `complete`) +- `DATABASE_URL` pointing at a local Postgres + `bun test` is green +- `bun run build` + `node scripts/assert-node-package.mjs` is green on Node 26 +- **GitHub Actions on this PR is green** (lint, build, Bun Postgres tests, Node package import, `complete`) - Smoke test fails if Postgres is down or `DATABASE_URL` is missing - No runtime exports claimed that do not exist @@ -173,3 +110,12 @@ A compiling package, a shared `specHelper`, and CI that will run every subsequen ## Lessons learned - 2026-08-26 (plan): Test CI and a real Postgres smoke test (`SELECT 1`) belong here, not in Phase 10. Later phases must stay green on this workflow. +- 2026-08-26: Pin Node to Current 26 (`engines.node` `>=26`, `.nvmrc` `26`) instead of the original `>=20` pin. `@types/node` already tracks 26. +- 2026-08-26: `bun test --concurrency=1` is not a Bun flag (`bun test --help` has `--concurrent` and `--max-concurrency`, not `--concurrency`). Bun still accepted the unknown flag and ran anyway. Use `--max-concurrency=1` on Bun and `--test-concurrency=1` on `node --test`. Tests are sequential by default unless `--concurrent` / `--parallel` is set. +- 2026-08-26: Dropped `docker-compose.yml`. CI Postgres is a GitHub Actions service; locally `DATABASE_URL` is enough. Compose would only wrap a database this repo does not otherwise orchestrate. +- 2026-08-26: Test matrix runs the same `node:test` files on Bun and Node 26. `bun:test` cannot run on Node, so the suite is `node:test` + `node:assert/strict` rather than `bun:test`. +- 2026-08-26: Ban `any` in the whole tree: `noImplicitAny` in `tsconfig.json` (explicit even though `strict` already implies it), `tsc --noEmit -p tsconfig.test.json` so tests are included, and Biome `noExplicitAny` as an error. `tsc` has no `noExplicitAny` flag. +- 2026-08-26: Reverted the suite to `bun:test`. Node coverage is `scripts/assert-node-package.mjs` (import compiled `exports` on Node 26, not a second copy of the Postgres tests). +- 2026-08-26: The `node-package` CI step must invoke `node` directly. `bun run test:node-package` can still shell out to `node`, but the workflow should not go through Bun for that check. +- 2026-08-26: `noImplicitAny` is set on both `tsconfig.json` and `tsconfig.test.json` so relaxing `strict` later still bans implicit `any` in src and tests. +- 2026-08-26: Required checks on `main` should be `complete` and `Cursor Bugbot`. The cloud agent GitHub token is not a repo admin (403 on branch protection and rulesets), so a maintainer must set that in the GitHub UI. diff --git a/docs/plans/07-multiworker.md b/docs/plans/07-multiworker.md index ae8bd24..2075e90 100644 --- a/docs/plans/07-multiworker.md +++ b/docs/plans/07-multiworker.md @@ -31,7 +31,7 @@ Port `__tests__/core/multiWorker.ts` **in this PR**: - stays at min on blocking CPU jobs - failure events bubble -These tests are CPU-noisy; keep `jest.retryTimes` equivalent (`test.todo` is not acceptable). bun:test has retry — use it. +These tests are CPU-noisy; keep a `jest.retryTimes` equivalent (`test.todo` is not acceptable). Prefer rerunning the file in CI over weakening assertions. **CI:** green on `test.yaml` before merge. @@ -52,4 +52,6 @@ pg-boss `localConcurrency` is **not** a substitute. MultiWorker must spawn real ## Lessons learned -_None yet._ +- 2026-08-26: Phase 1 runs tests with `node:test`, not `bun:test`. Do not assume Bun-only retry APIs when this phase is implemented. +- 2026-08-26: Phase 1 reverted to `bun:test`. Bun retry APIs are available again; Node is only the compiled-package import check. + diff --git a/docs/plans/08-conformance-tests.md b/docs/plans/08-conformance-tests.md index 7e01cb8..68105bd 100644 --- a/docs/plans/08-conformance-tests.md +++ b/docs/plans/08-conformance-tests.md @@ -29,7 +29,7 @@ If a row above is missing when you start this phase, that is a **bug in an earli | node-resque | pgboss-queue | | --- | --- | -| Jest + ts-jest | `bun:test` (`bun test --concurrency=1`) | +| Jest + ts-jest | `bun:test` (`bun test --max-concurrency=1`) | | `ioredis` specHelper | `__tests__/utils/specHelper.ts` | | `REDIS_HOST` | `DATABASE_URL` (CI injects it) | | `afterAll` disconnect redis | `afterAll` end pool + truncate or `DROP SCHEMA` | @@ -45,7 +45,7 @@ connect / disconnect / cleanup popFromQueue(): Promise ``` -**Isolation:** truncate + migrate once in `beforeAll` for speed, or per-file schema. Keep `--concurrency=1` until proven otherwise. +**Isolation:** truncate + migrate once in `beforeAll` for speed, or per-file schema. Keep Bun `--max-concurrency=1` until proven otherwise. Do not pass `bun test --concurrency=1` — that is not a Bun flag. Node compatibility is the Phase 1 `node-package` job, not a second test runner. ## Matrix @@ -169,3 +169,5 @@ Docs site can describe a real API. Phase 10 can trust tests that have been runni ## Lessons learned - 2026-08-26 (plan): This phase is an audit, not the first test suite. Tests ship with Phases 1–7; CI has been running since Phase 1. +- 2026-08-26: Phase 1 corrected the runner to `node:test` on a Bun + Node matrix. Isolation uses `--max-concurrency=1` / `--test-concurrency=1`, not `bun test --concurrency=1`. +- 2026-08-26: Phase 1 reverted the suite to `bun:test`. Node is covered by importing `dist/` (`test:node-package`), not by running this matrix on `node --test`. diff --git a/docs/plans/10-publish-and-ci.md b/docs/plans/10-publish-and-ci.md index 7747036..a3e347a 100644 --- a/docs/plans/10-publish-and-ci.md +++ b/docs/plans/10-publish-and-ci.md @@ -94,7 +94,7 @@ jobs: `package.json` must include `"files": ["dist", "README.md", "LICENSE"]` so we do not publish tests or plans. -Optional later: Node 20/22/24 matrix for compiled `dist/` consumers. Add to `test.yaml` (Phase 1 file), not here. +The Phase 1 `test` job runs `bun:test` against Postgres. The `node-package` job imports compiled `dist/` on Node 26. Do not add a second suite matrix here. ## Versioning policy (CLAUDE.md) @@ -107,8 +107,7 @@ Do not publish `0.0.1` empty stubs. First intentional bump to `0.1.0` is the fir Port node-resque `examples/` in Phases 4–7. This phase can add a compose-based example command if missing: ```bash -docker compose up -d postgres -bun examples/example.ts +DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/pgboss_queue_test bun examples/example.ts ``` Optional: `examples/docker` like node-resque — not required for v1. @@ -134,3 +133,5 @@ README (user-facing): GitHub Test workflow badge (the Phase 1 workflow), npm ver ## Lessons learned - 2026-08-26 (plan): Test CI is Phase 1. This phase is only Pages + npm publish. +- 2026-08-26: Phase 1 already matrices Bun and Node 26 in `test.yaml`. Example runs use `DATABASE_URL`; there is no repo `docker-compose.yml`. +- 2026-08-26: Phase 1 dropped the suite matrix. `test` is Bun; `node-package` only imports `dist/` on Node 26. diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e9c5b4 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "pgboss-queue", + "version": "0.0.1", + "description": "A PostgreSQL-backed background job queue with the node-resque runtime model", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=26" + }, + "license": "Apache-2.0", + "scripts": { + "test": "bun test --max-concurrency=1", + "test:node-package": "node scripts/assert-node-package.mjs", + "build": "tsc && tsc --noEmit -p tsconfig.test.json", + "lint": "biome check .", + "format": "biome check --write ." + }, + "dependencies": { + "pg": "^8.23.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.5.10", + "@types/bun": "^1.4.0", + "@types/node": "^26.3.0", + "@types/pg": "^8.23.1", + "typescript": "^7.0.2" + } +} diff --git a/scripts/assert-node-package.mjs b/scripts/assert-node-package.mjs new file mode 100644 index 0000000..6fedb03 --- /dev/null +++ b/scripts/assert-node-package.mjs @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); + +assert.equal( + process.versions.bun, + undefined, + "this script must run on Node, not Bun", +); +assert.ok(process.versions.node, "expected a Node.js runtime"); + +assert.equal(typeof pkg.name, "string"); +assert.equal(typeof pkg.exports?.["."]?.import, "string"); + +const entry = pkg.exports["."].import; +const url = pathToFileURL(join(root, entry)).href; +const mod = await import(url); + +assert.equal(typeof mod, "object"); +assert.ok(mod); + +process.stdout.write( + `imported ${pkg.name} from ${entry} on node ${process.versions.node}\n`, +); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7249ab3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "strict": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..b8a2350 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + "noImplicitAny": true, + "types": ["bun", "node"] + }, + "include": ["src/**/*.ts", "__tests__/**/*.ts"] +}