trails/apps/journal/app/jobs/demo-bot-generate.ts
Ullrich Schäfer c2e8461f9c
Upgrade pg-boss from 10 to 12
Supersedes dependabot #264 — this one involves code changes that PR
couldn't make.

## Breaking changes in our usage surface

### v12: default export removed → named `PgBoss` export

Affects every file that imports the SDK:
- `packages/jobs/src/boss.ts`: `import PgBoss` → `import { PgBoss }`
- `packages/jobs/src/worker.ts`: same for the type import
- `packages/jobs/src/types.ts`: `PgBoss.Job<T>` → `Job<T>` (types.ts
  re-exports `Job` at the package root in v12)

### v11: queue names restricted to `[A-Za-z0-9_.-]`

Colon `:` is no longer allowed. Renamed the two journal queues that
had it:
- `demo-bot:generate` → `demo-bot-generate`
- `demo-bot:prune`    → `demo-bot-prune`

The planner's `expire-sessions` was already valid.

### v12: minimum Node 22.12

Not a code change for us — journal + planner Dockerfiles are on
`node:25-slim`, CI runners on 24.

## Regression guard

Added `assertValidJobName()` in `@trails-cool/jobs`, called by
`startWorker()` before any side effects. Pg-boss v11+ silently accepts
an invalid name then rejects the underlying SQL call later — we fail
loudly at boot instead. Unit tests cover the character-class rule
and exercise the exact old-name (`demo-bot:generate`) as a
regression fence.

## Prod rollout note

Pg-boss v11 dropped the auto-migration path from v10. On deploy, the
live `pgboss` schema from v10 won't migrate cleanly. Simplest path:
`DROP SCHEMA pgboss CASCADE` before the first v12 worker starts —
our jobs are all cron-scheduled and will re-register themselves on
boot, so there's nothing durable to preserve in the queue.

## Verified

- `pnpm typecheck` / `pnpm lint` / `pnpm test` — all clean
- `pnpm exec playwright test --workers=2` — 50/50 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:01:11 +02:00

60 lines
1.9 KiB
TypeScript

import type { JobDefinition } from "@trails-cool/jobs";
import {
DEMO_BACKFILL_TARGET,
DEMO_DAILY_CAP,
countSyntheticRoutesRecent,
countSyntheticRoutesTotal,
ensureDemoUser,
generateOneWalk,
isDemoBotEnabled,
refreshDemoBotGauges,
shouldWalkNow,
} from "../lib/demo-bot.server.ts";
import { logger } from "../lib/logger.server.ts";
/**
* Generate job. Fires every 30 minutes via pg-boss cron. Handler steps:
*
* 1. Bail if `DEMO_BOT_ENABLED` is not "true".
* 2. On first run (no synthetic rows yet) produce a small backfill so the
* demo user's profile has content immediately.
* 3. Otherwise apply the decide-to-walk gate (local hour + p=0.09) and
* daily cap; on pass, insert one route+activity via `generateOneWalk`.
*/
export const demoBotGenerateJob: JobDefinition = {
name: "demo-bot-generate",
cron: "0,30 * * * *",
retryLimit: 1,
expireInSeconds: 120,
async handler() {
if (!isDemoBotEnabled()) return { skipped: "disabled" };
const ownerId = await ensureDemoUser();
const total = await countSyntheticRoutesTotal();
if (total === 0) {
let inserted = 0;
for (let i = 0; i < DEMO_BACKFILL_TARGET; i++) {
const id = await generateOneWalk(ownerId);
if (id) inserted++;
}
logger.info({ inserted }, "demo-bot backfill complete");
await refreshDemoBotGauges();
return { mode: "backfill", inserted };
}
const recent = await countSyntheticRoutesRecent(14);
if (recent >= DEMO_DAILY_CAP) {
logger.info({ recent, cap: DEMO_DAILY_CAP }, "demo-bot cap hit, skipping");
return { skipped: "cap", recent };
}
const now = new Date();
if (!shouldWalkNow(now)) return { skipped: "gate" };
const id = await generateOneWalk(ownerId, { now });
logger.info({ inserted: id ? 1 : 0, routeId: id }, "demo-bot walk");
await refreshDemoBotGauges();
return { mode: "single", routeId: id };
},
};