bosskit 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -307,6 +307,18 @@ boot, which is the point. Keep schedule payloads plainly JSON-serializable.
307
307
  `data` is required, and it is Zod's *output* type: a field declared with
308
308
  `.default()` must still be supplied. It is stored exactly as you write it.
309
309
 
310
+ `options` accepts pg-boss's `ScheduleOptions` without `db`: `tz` and `key`,
311
+ plus any send option — `expireInSeconds`, `retryLimit`, `retryBackoff`,
312
+ `deadLetter`, `priority`, and so on. pg-boss stores them on the schedule row and
313
+ applies them to every job the schedule creates, so `expireInSeconds` set on a
314
+ schedule bounds each of its jobs: one still running past it is failed by pg-boss
315
+ and then retries or dead-letters like any other failure. `db` is excluded because
316
+ bosskit chooses the connection, as it does for `enqueue`'s `JobOptions`.
317
+
318
+ ```ts
319
+ { cron: "0 3 * * *", data: { olderThanDays: 30 }, options: { expireInSeconds: 1800, tz: "UTC" }, queue: "nightly-cleanup" },
320
+ ```
321
+
310
322
  ## Adapters
311
323
 
312
324
  `enqueue` takes a `db` handle and adapts it to pg-boss's own database contract
@@ -500,8 +512,9 @@ starting, stopping, and caching it is still your responsibility.
500
512
  ### `ScheduleOf<D>` / `ScheduleDefinition<Name>` / `schedulesToRemove(declared, existing)`
501
513
 
502
514
  `ScheduleOf<D>` is the shape `applySchedules` takes for registry `D`: `{ queue,
503
- cron, data, options? }`, with `queue` narrowed to the registry's sendable names
504
- and `data` to that queue's payload. `ScheduleDefinition` is the loose,
515
+ cron, data, options? }`, with `queue` narrowed to the registry's sendable names,
516
+ `data` to that queue's payload, and `options` to pg-boss's `ScheduleOptions`
517
+ minus `db` (so `tz`, `key`, and every send option such as `expireInSeconds`). `ScheduleDefinition` is the loose,
505
518
  registry-agnostic version of the same shape (`data` optional, any `queue`
506
519
  string); `schedulesToRemove` consumes it, and `ScheduleOf<D>` is assignable to
507
520
  it. `schedulesToRemove(declared, existing)` takes your declared schedule list
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JobWithMetadata, PgBoss, Db, WorkOptions } from 'pg-boss';
1
+ import { JobWithMetadata, PgBoss, ScheduleOptions, Db, WorkOptions } from 'pg-boss';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -141,7 +141,7 @@ type RegisteredWorker = {
141
141
  * queue's payload would be a lie. `queue` keeps its exact literal type, so
142
142
  * branching on queue name is fully checked.
143
143
  *
144
- * Properties that will bite you if you don't know them:
144
+ * Properties to know before writing one:
145
145
  *
146
146
  * 1. Runs once per BATCH, not once per job. Identical at the default
147
147
  * `batchSize` of 1; not above it.
@@ -215,6 +215,14 @@ declare class JobPlatformError extends Error {
215
215
  constructor(message: string);
216
216
  }
217
217
 
218
+ /**
219
+ * pg-boss's `ScheduleOptions` without `db`. Beyond `tz` and `key`, that is the
220
+ * full set of send options: pg-boss stores them on the schedule row and applies
221
+ * them to every job the schedule creates, so `expireInSeconds` or `retryLimit`
222
+ * set here govern each of those jobs. `db` is excluded because bosskit chooses
223
+ * the connection pg-boss calls run on, exactly as it does for `JobOptions`.
224
+ */
225
+ type ScheduleSendOptions = Omit<ScheduleOptions, "db">;
218
226
  /**
219
227
  * The loose, registry-agnostic shape of a declared schedule: `data` is
220
228
  * optional and `queue` is any string, not narrowed to a registry's names.
@@ -231,12 +239,8 @@ type ScheduleDefinition<Name extends string = string> = {
231
239
  cron: string;
232
240
  /** Plain JSON-serializable data (no Dates/class instances) — it round-trips through jsonb. */
233
241
  data?: object;
234
- options?: {
235
- /** IANA time zone; pg-boss defaults to UTC. */
236
- tz?: string;
237
- /** Unique key when one queue needs multiple schedules. */
238
- key?: string;
239
- };
242
+ /** pg-boss `ScheduleOptions` without `db`; see `ScheduleSendOptions`. */
243
+ options?: ScheduleSendOptions;
240
244
  };
241
245
  /**
242
246
  * Identity of an existing schedule row, all `applySchedules` needs to decide
@@ -263,7 +267,7 @@ declare function schedulesToRemove(declared: ScheduleDefinition[], existing: Exi
263
267
  * A schedule declaration bound to one registry. Distributing over the sendable
264
268
  * queue names is what types `data` per queue: a schedule for queue "a" must
265
269
  * carry queue "a"'s payload, so a mismatched or missing payload is a compile
266
- * error rather than a job that fails every night at 03:00 forever.
270
+ * error rather than a job that fails every time the schedule fires.
267
271
  *
268
272
  * Dead-letter queues are excluded (`SendableOf`, not `QueueNameOf`) for the
269
273
  * same reason `enqueue` excludes them: pg-boss populates a DLQ itself.
@@ -277,12 +281,8 @@ type ScheduleOf<D extends readonly QueueDefinition[]> = {
277
281
  cron: string;
278
282
  /** This queue's payload. Must be JSON-round-trippable; it is stored as jsonb. */
279
283
  data: QueuePayloadOf<D, Q>;
280
- options?: {
281
- /** Unique key when one queue needs multiple schedules. */
282
- key?: string;
283
- /** IANA time zone; pg-boss defaults to UTC. */
284
- tz?: string;
285
- };
284
+ /** pg-boss `ScheduleOptions` without `db`; see `ScheduleSendOptions`. */
285
+ options?: ScheduleSendOptions;
286
286
  /** Queue that receives the scheduled job. */
287
287
  queue: Q;
288
288
  };
@@ -319,7 +319,7 @@ type ScheduleOf<D extends readonly QueueDefinition[]> = {
319
319
  * - `logger` — the platform never reaches for a global logger.
320
320
  * - `middleware` — optional, wraps every worker's payload validation and
321
321
  * handler. Platform-level so it cannot be forgotten on one worker; see
322
- * `JobMiddleware` for the behaviors that will bite you.
322
+ * `JobMiddleware` for the behaviors to know before writing one.
323
323
  *
324
324
  * All three type parameters are inferred from the call, so you never write an
325
325
  * explicit type argument. `const D` preserves the literal registry tuple, which
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/boss.ts","../src/errors.ts","../src/schedules.ts","../src/types.ts","../src/platform.ts"],"names":["z"],"mappings":"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","file":"index.js","sourcesContent":["import { PgBoss } from \"pg-boss\";\nimport type { JobLogger } from \"./types\";\n\n/**\n * Pure factory — no caching, no process hooks, no config reading. Owning the\n * boss lifecycle (singleton caching, shutdown hooks, reading connection\n * settings) is the application's job.\n *\n * The `error` and `warning` handlers are the reason to prefer this over\n * `new PgBoss(...)` directly: an unhandled pg-boss `error` event crashes the\n * Node process.\n */\nexport function createBoss(args: {\n connectionString: string;\n migrate: boolean;\n max?: number;\n /** Surfaces in `pg_stat_activity` — set it to something you can grep for. */\n applicationName?: string;\n /** Postgres schema pg-boss owns. Defaults to pg-boss's own default. */\n schema?: string;\n logger: JobLogger;\n}): PgBoss {\n const boss = new PgBoss({\n application_name: args.applicationName ?? \"bosskit\",\n connectionString: args.connectionString,\n max: args.max ?? 5,\n migrate: args.migrate,\n schema: args.schema ?? \"pgboss\",\n useListenNotify: true,\n });\n // Mandatory: an unhandled 'error' event would crash the Node process.\n boss.on(\"error\", (err) => args.logger.error({ err }, \"pg-boss error\"));\n boss.on(\"warning\", (warning) => args.logger.warn({ warning }, \"pg-boss warning\"));\n return boss;\n}\n","/**\n * Errors raised by the job platform itself — a misconfigured registry, or a\n * declared schedule whose payload doesn't satisfy its queue's schema. Never a\n * job that failed. Always thrown at construction, boot, or schedule sync,\n * never from a job handler.\n *\n * A plain named subclass rather than a richer error type from some error\n * framework: bosskit has zero runtime dependencies, and an error class is not\n * worth acquiring one — nor worth forcing a dependency on callers who already\n * have their own. (`instanceof` is reliable here — the package targets ES2022,\n * so no prototype fixup is needed.)\n */\nexport class JobPlatformError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"JobPlatformError\";\n }\n}\n","import { z } from \"zod\";\nimport { JobPlatformError } from \"./errors\";\nimport type { QueueDefinition, QueuePayloadOf, SendableOf } from \"./types\";\n\n/**\n * The loose, registry-agnostic shape of a declared schedule: `data` is\n * optional and `queue` is any string, not narrowed to a registry's names.\n * `schedulesToRemove` (below) consumes this shape so it can diff schedules\n * from any registry — or none — against what pg-boss has stored. `ScheduleOf`\n * is assignable to it. Declaring schedules yourself? Use `ScheduleOf`, which\n * binds `data` to one registry's queue payloads and is what `applySchedules`\n * takes.\n */\nexport type ScheduleDefinition<Name extends string = string> = {\n /** Queue that receives the scheduled job. */\n queue: Name;\n /** 5-field cron (minute precision) — pg-boss evaluates schedules every ~30s. */\n cron: string;\n /** Plain JSON-serializable data (no Dates/class instances) — it round-trips through jsonb. */\n data?: object;\n options?: {\n /** IANA time zone; pg-boss defaults to UTC. */\n tz?: string;\n /** Unique key when one queue needs multiple schedules. */\n key?: string;\n };\n};\n\n/**\n * Identity of an existing schedule row, all `applySchedules` needs to decide\n * which stored schedules are no longer declared. `key` is `string | null` so a\n * real `Schedule[]` from `boss.getSchedules()` (key is `''` when unset) and\n * explicit-null test fixtures both assign here without a cast.\n */\ntype ExistingScheduleId = { name: string; key: string | null };\n\n/** Stable identity for a schedule: same queue + key = same schedule (empty/null/undefined key all normalize together). */\nfunction idOf(name: string, key: string | null | undefined): string {\n return `${name}::${key ?? \"\"}`;\n}\n\n/**\n * Pure: existing schedules that are no longer declared, so they can be\n * unscheduled. We don't diff cron/data to decide what to *apply* — `boss.schedule`\n * is an idempotent upsert and pg-boss derives fire times from the cron expression\n * (not from `updated_on`), so re-applying an unchanged schedule is a cheap no-op\n * with no effect on timing. Only removals need a diff.\n */\nexport function schedulesToRemove(\n declared: ScheduleDefinition[],\n existing: ExistingScheduleId[]\n): Array<{ name: string; key?: string }> {\n const declaredIds = new Set(declared.map((d) => idOf(d.queue, d.options?.key)));\n return existing\n .filter((e) => !declaredIds.has(idOf(e.name, e.key)))\n .map((e) => (!e.key ? { name: e.name } : { key: e.key, name: e.name }));\n}\n\n/**\n * Throw if a declared schedule's payload would fail in a worker. Pure and\n * total — it either returns or throws, and reads nothing outside its arguments,\n * so it takes the queue's schema rather than reaching for a registry.\n *\n * Validates what the WORKER will see, not what was declared. Schedule data is\n * stored as jsonb and re-read at fire time, so a schema field that accepts a\n * non-JSON value — z.date(), z.instanceof(), z.map() — would pass on the\n * in-memory value and still fail every night on the string it became.\n * Simulating the round trip is what makes this check honest. Scheduled jobs\n * never pass through `enqueue`, so this is the only chance to catch it before\n * 03:00.\n *\n * The round trip itself can throw before safeParse ever runs — a BigInt, a\n * circular reference, or (reachable when a caller's registry type has widened\n * to `QueueDefinition[]`) a missing `data` entirely. Left unguarded those\n * surface as a raw TypeError/SyntaxError naming neither queue nor key,\n * bypassing the JobPlatformError contract `applySchedules` otherwise\n * guarantees.\n *\n * The parameter is spelled structurally rather than as `ScheduleDefinition`\n * on purpose: that type's `data` is `object | undefined`, which would reject\n * the very inputs this function exists to catch.\n */\nexport function assertValidSchedulePayload(\n schedule: { data: unknown; options?: { key?: string }; queue: string },\n schema: z.ZodType\n): void {\n const key = schedule.options?.key ? ` (key \"${schedule.options.key}\")` : \"\";\n const label = `Schedule for queue \"${schedule.queue}\"${key}`;\n let roundTripped: unknown;\n try {\n roundTripped = JSON.parse(JSON.stringify(schedule.data));\n } catch (err) {\n throw new JobPlatformError(`${label} has data that is not JSON-serializable: ${String(err)}`);\n }\n const result = schema.safeParse(roundTripped);\n if (!result.success) {\n throw new JobPlatformError(`${label} has an invalid payload: ${z.prettifyError(result.error)}`);\n }\n}\n\n/**\n * A schedule declaration bound to one registry. Distributing over the sendable\n * queue names is what types `data` per queue: a schedule for queue \"a\" must\n * carry queue \"a\"'s payload, so a mismatched or missing payload is a compile\n * error rather than a job that fails every night at 03:00 forever.\n *\n * Dead-letter queues are excluded (`SendableOf`, not `QueueNameOf`) for the\n * same reason `enqueue` excludes them: pg-boss populates a DLQ itself.\n *\n * `data` is Zod's OUTPUT type, so a field with `.default()` must still be\n * supplied here — same as `enqueue`.\n */\nexport type ScheduleOf<D extends readonly QueueDefinition[]> = {\n [Q in SendableOf<D>]: {\n /** 5-field cron (minute precision) — pg-boss evaluates schedules every ~30s. */\n cron: string;\n /** This queue's payload. Must be JSON-round-trippable; it is stored as jsonb. */\n data: QueuePayloadOf<D, Q>;\n options?: {\n /** Unique key when one queue needs multiple schedules. */\n key?: string;\n /** IANA time zone; pg-boss defaults to UTC. */\n tz?: string;\n };\n /** Queue that receives the scheduled job. */\n queue: Q;\n };\n}[SendableOf<D>];\n","import type { JobWithMetadata, PgBoss } from \"pg-boss\";\nimport { z } from \"zod\";\n\n/**\n * Generic job-platform types. Nothing in this package knows anything about the\n * application using it: no concrete queue, no configuration shape, no database\n * type. A concrete instance is built by calling `createJobPlatform` with a\n * queue registry and providers — see the README.\n */\n\n/** The minimal logging surface the platform needs; a pino logger satisfies it. */\nexport type JobLogger = {\n info(obj: Record<string, unknown>, msg: string): void;\n warn(obj: Record<string, unknown>, msg: string): void;\n error(obj: Record<string, unknown>, msg: string): void;\n};\n\n/**\n * The acting user a job runs on behalf of — the identity a worker resolves\n * credentials, tenancy or permissions from, and the one every job log line\n * carries. A user-scoped queue's payload extends this; see `QueueDefinition`\n * for the `global` opt-out used by system jobs that have no user.\n *\n * This lives in the payload (not pg-boss job metadata) because `data` is the\n * only user-controlled channel pg-boss offers — and because the DLQ hop copies\n * `data` verbatim, the acting user survives into dead-letter queues for free.\n */\nexport const UserScopedSchema = z.object({ userId: z.string() });\nexport type UserScoped = z.infer<typeof UserScopedSchema>;\n\ntype QueueOptions = NonNullable<Parameters<PgBoss[\"createQueue\"]>[1]>;\n\ntype QueueDefinitionBase = {\n name: string;\n /** pg-boss queue options. Omit entirely for a queue with nothing to configure. */\n options?: Omit<QueueOptions, \"name\">;\n};\n\n/**\n * A queue definition. By DEFAULT a queue is user-scoped: its payload schema\n * must produce a `userId`, so forgetting the acting user on a new queue is a\n * compile error rather than a runtime surprise discovered in a worker. System\n * work that genuinely has no user on whose behalf it runs — cron sweeps,\n * maintenance jobs — opts out explicitly with `global: true`.\n *\n * Because `enqueue`'s `data` parameter is derived from this schema\n * (`QueuePayloadOf`), the constraint also makes it a compile error to enqueue\n * without a user, or to drop the user across a chain hop.\n *\n * IMPORTANT: never store a registry in a variable annotated `QueueDefinition[]`\n * or `readonly QueueDefinition[]`. Both spellings widen it, and widening costs\n * two guarantees at once, silently:\n *\n * - `QueuePayloadOf` collapses to this type's base user-scoped shape, so\n * `enqueue` stops type-checking domain fields entirely.\n * - `SendableOf` collapses to `string`, so the dead-letter exclusion disappears\n * and any queue name — including one that does not exist — compiles.\n *\n * Three spellings keep it precise: `defineQueues([...])`, an array literal\n * passed straight into `createJobPlatform`, and `[...] satisfies\n * QueueDefinition[]`. Prefer `defineQueues` — it checks each entry against this\n * constraint without widening what it stores.\n */\nexport type QueueDefinition =\n | (QueueDefinitionBase & {\n global?: false;\n /** Zod schema for this queue's job payload — the single source of truth\n * for both the compile-time payload type and the runtime boundary\n * validation. Must carry the acting user (see `UserScopedSchema`). */\n schema: z.ZodType<UserScoped>;\n })\n | (QueueDefinitionBase & {\n /** This queue's jobs run on behalf of no one — system work only. */\n global: true;\n /** Zod schema for this queue's job payload — the single source of truth\n * for both the compile-time payload type and the runtime boundary\n * validation. `object` because a pg-boss payload is always JSON. */\n schema: z.ZodType<object>;\n });\n\n/** Every queue name in a registry. Broader than `SendableOf`: includes DLQs. */\nexport type QueueNameOf<D extends readonly QueueDefinition[]> = D[number][\"name\"];\n\n/**\n * Payload type per queue, inferred from each declared Zod schema — the derived\n * contract for `enqueue` and worker handlers, with no hand-written map to keep\n * in sync. Modelled as an indexed access (not `Extract` + `z.infer`) so it\n * resolves to a concrete object type for a generic `Q`, e.g. inside `enqueue`.\n */\ntype QueuePayloadMapOf<D extends readonly QueueDefinition[]> = {\n [E in D[number] as E[\"name\"]]: z.infer<E[\"schema\"]>;\n};\n/**\n * The `& object` states what is already true — a pg-boss payload is JSON — and\n * is applied here rather than inside the map on purpose: with both `D` and `Q`\n * generic the map lookup stays deferred, so only an intersection at this level\n * keeps a payload provably assignable to `boss.send`'s `object` parameter.\n */\nexport type QueuePayloadOf<\n D extends readonly QueueDefinition[],\n Q extends QueueNameOf<D>,\n> = QueuePayloadMapOf<D>[Q] & object;\n\n/**\n * Per-slot `options`, defaulting to `undefined` for a definition that omits it\n * entirely. A plain `D[number][\"options\"]` indexed access does not work once\n * `options` is optional: a tuple entry that omits the key altogether has no\n * `options` property at all, and indexed access on a union requires every\n * member to carry the key, so the lookup would fail to compile the moment any\n * entry left `options` out. Distributing over `keyof D` (each tuple slot,\n * rather than the merged `D[number]` union) sidesteps that — an entry without\n * `options` just contributes `undefined` instead of breaking the type for\n * every other entry.\n */\ntype OptionsTupleOf<D extends readonly QueueDefinition[]> = {\n [K in keyof D]: \"options\" extends keyof D[K] ? D[K][\"options\"] : undefined;\n};\n\n/**\n * Every dead-letter target named by some queue's `deadLetter` option. You never\n * enqueue to a DLQ (pg-boss copies failed jobs into it automatically), so these\n * are excluded from the enqueue-able set below.\n */\ntype DeadLetterOf<D extends readonly QueueDefinition[]> = Extract<\n OptionsTupleOf<D>[number],\n { deadLetter: string }\n>[\"deadLetter\"];\n\n/**\n * The queues application code may enqueue to: every defined queue minus the\n * dead-letter targets. Derived, so declaring a new DLQ automatically keeps it\n * off the enqueue surface.\n */\nexport type SendableOf<D extends readonly QueueDefinition[]> = Exclude<\n QueueNameOf<D>,\n DeadLetterOf<D>\n>;\n\ntype SendOptionsOf = NonNullable<Parameters<PgBoss[\"send\"]>[2]>;\n/** pg-boss send options, minus `db` — the platform owns db threading. */\nexport type JobOptions = Omit<SendOptionsOf, \"db\">;\n\n/**\n * A worker registered against a queue, type-erased for storage in a worker\n * list. `defineWorker` binds the queue → payload → handler types; `register`\n * closes over them so a heterogeneous worker list needs no shared handler type.\n */\nexport type RegisteredWorker = {\n queue: string;\n register: (boss: PgBoss) => Promise<string>;\n};\n\n/**\n * A hook wrapping every worker's run, for concerns that must not be\n * per-worker opt-in — tracing, alerting, log context. Registered once on the\n * platform, so it applies to every worker by construction.\n *\n * `jobs` is deliberately `unknown`: middleware runs OUTSIDE the parse loop, so\n * these payloads have not been validated — coercions and defaults are\n * unapplied and the data may not satisfy the schema at all. Typing them as the\n * queue's payload would be a lie. `queue` keeps its exact literal type, so\n * branching on queue name is fully checked.\n *\n * Properties that will bite you if you don't know them:\n *\n * 1. Runs once per BATCH, not once per job. Identical at the default\n * `batchSize` of 1; not above it.\n * 2. Wraps payload validation as well as the handler, so a payload that fails\n * `schema.parse` throws through `next()` and is observable here.\n * 3. Swallowing an error MARKS THE JOB COMPLETE. pg-boss completes a batch when\n * the callback resolves and fails it when the callback throws, so catching\n * without rethrowing suppresses the retry and the dead-letter hop.\n * Middleware that reports errors must rethrow.\n * 4. Not calling `next()` skips the handler and completes the job.\n *\n * `next()` is not idempotent: calling it twice re-parses the batch and\n * re-runs the handler.\n *\n * The platform awaits this and discards whatever it resolves to, so its\n * signature returns `Promise<void>` — there is no channel back into pg-boss's\n * job output.\n */\nexport type JobMiddleware<TName extends string = string> = (\n ctx: { jobs: JobWithMetadata<unknown>[]; queue: TName },\n next: () => Promise<void>\n) => Promise<void>;\n\n/**\n * Declare a queue registry.\n *\n * The `const` type parameter preserves the literal tuple, so every derived type\n * (`QueueNameOf`, `QueuePayloadOf`, `SendableOf`) stays precise. This is the\n * recommended way to build a registry: the alternative spellings\n * `const QUEUES: QueueDefinition[] = [...]` and\n * `const QUEUES: readonly QueueDefinition[] = [...]` both type-check but widen,\n * which collapses every payload to `UserScoped` AND collapses the enqueue-able\n * name set to `string` — so domain fields stop being checked and the\n * dead-letter guard quietly stops guarding. Calling a function instead of\n * writing a type annotation makes that mistake unspellable.\n */\nexport function defineQueues<const D extends readonly QueueDefinition[]>(defs: D): D {\n return defs;\n}\n","import type { JobWithMetadata, PgBoss, Db as PgBossDb, WorkOptions } from \"pg-boss\";\nimport type { z } from \"zod\";\nimport { JobPlatformError } from \"./errors\";\nimport { type ScheduleOf, assertValidSchedulePayload, schedulesToRemove } from \"./schedules\";\nimport {\n type JobLogger,\n type JobMiddleware,\n type JobOptions,\n type QueueDefinition,\n type QueueNameOf,\n type QueuePayloadOf,\n type RegisteredWorker,\n type SendableOf,\n UserScopedSchema,\n} from \"./types\";\n\n/**\n * Parse and log one batch of jobs on the way into a handler. Pure apart from\n * the logger call, and takes the schema rather than reaching for a registry, so\n * it stays readable outside the platform closure it is called from.\n *\n * The handler is handed the PARSED jobs, not the raw ones. `data` arrives as\n * jsonb, and the handler's type is the schema's OUTPUT type — so a\n * `z.coerce.date()` field must reach it as a Date and a `.default()` field must\n * be filled in, not left undefined.\n *\n * Parsing is per BATCH: a payload that fails validation throws, failing every\n * job fetched alongside it. That never comes up at the default `batchSize` of 1.\n */\nfunction parseJobBatch<T>(\n jobs: JobWithMetadata<unknown>[],\n schema: z.ZodType<T>,\n queue: string,\n logger: JobLogger\n): JobWithMetadata<T>[] {\n const parsed: JobWithMetadata<T>[] = [];\n for (const job of jobs) {\n const data = schema.parse(job.data);\n // Uniform actor trace for every queue. safeParse (not a cast) so this also\n // works for `global` queues, whose payloads carry no user.\n const actor = UserScopedSchema.safeParse(data);\n logger.info(\n {\n jobId: job.id,\n queue,\n retryCount: job.retryCount,\n userId: actor.success ? actor.data.userId : undefined,\n },\n \"job received\"\n );\n parsed.push({ ...job, data });\n }\n return parsed;\n}\n\n/**\n * Build a job platform bound to one queue registry.\n *\n * This is the package's only entry point, and the reason nothing inside it\n * knows about the application using it. Everything application-shaped arrives\n * through arguments:\n *\n * - `definitions` — the queue registry. Both the compile-time payload types and\n * the runtime boundary validation derive from it, so you declare each queue\n * exactly once.\n * - `getBoss` — resolves a *started* pg-boss instance. A provider rather than an\n * instance because the boss is not started at module-evaluation time; you own\n * its creation, caching and config.\n * - `getRuntime` — resolves whatever context handlers should receive (say,\n * `{ db, config }`). Its return type `R` is INFERRED, which is how handler\n * context gets typed without this package importing your `Db`/`Config`.\n * Resolved AT MOST ONCE for the life of the platform (see below), so anything\n * computed per call — a fresh request id, a timestamp — would be frozen at\n * the first value. Return a plain data object: handlers receive it via the\n * shallow spread `{ ...runtime, jobs }`, which drops a class instance's\n * prototype and with it every method on it.\n * - `toBossDb` — adapts your database handle to pg-boss's `executeSql`\n * contract. Its parameter type `TDb` is INFERRED and becomes the `db` every\n * enqueue takes, so this package needs no ORM: pass one of pg-boss's own\n * adapters (`fromDrizzle`, `fromKnex`, `fromKysely`, `fromPrisma`,\n * `fromPglite`) or write three lines for any other client. ANNOTATE the\n * parameter — written as `(db) => ...` it infers `unknown`, and `enqueue`\n * then accepts any value at all as its `db`.\n * - `logger` — the platform never reaches for a global logger.\n * - `middleware` — optional, wraps every worker's payload validation and\n * handler. Platform-level so it cannot be forgotten on one worker; see\n * `JobMiddleware` for the behaviors that will bite you.\n *\n * All three type parameters are inferred from the call, so you never write an\n * explicit type argument. `const D` preserves the literal registry tuple, which\n * is what keeps `QueuePayloadOf` precise (see the note on `QueueDefinition`).\n */\nexport function createJobPlatform<const D extends readonly QueueDefinition[], R, TDb>(platform: {\n definitions: D;\n getBoss: () => Promise<PgBoss>;\n getRuntime: () => Promise<R>;\n toBossDb: (db: TDb) => PgBossDb;\n logger: JobLogger;\n /** Optional hook wrapping every worker's validation and handler. See `JobMiddleware`. */\n middleware?: JobMiddleware<QueueNameOf<D>>;\n}) {\n const { definitions, getBoss, getRuntime, logger, middleware, toBossDb } = platform;\n\n /**\n * Resolve the runtime at most once, lazily, on the first worker registration.\n * `register` runs per worker, and a provider that allocated a connection pool\n * per call would quietly open one per worker. The memo is cleared only when\n * the promise rejects, so a transient failure at boot doesn't poison a later\n * retry; a successful resolution is kept for the life of the platform.\n */\n let runtimePromise: Promise<R> | undefined;\n function resolveRuntime(): Promise<R> {\n if (!runtimePromise) {\n runtimePromise = getRuntime().catch((err: unknown) => {\n runtimePromise = undefined;\n throw err;\n });\n }\n return runtimePromise;\n }\n\n type Name = QueueNameOf<D>;\n type Sendable = SendableOf<D>;\n type Payload<Q extends Name> = QueuePayloadOf<D, Q>;\n\n // Runtime name → schema lookup, built from the definitions. Duplicate names\n // are rejected rather than last-write-wins: the payload TYPE for a repeated\n // name is the union of both schemas, but only one schema would do the\n // validating, so half the payloads would be checked against the wrong shape.\n // The type system can't catch this (a duplicated key just merges), so the\n // registry is verified here, once, at construction.\n const schemaByQueue = new Map<string, QueueDefinition[\"schema\"]>();\n for (const d of definitions) {\n if (schemaByQueue.has(d.name)) {\n throw new JobPlatformError(`Queue \"${d.name}\" is declared more than once in the registry`);\n }\n schemaByQueue.set(d.name, d.schema);\n }\n\n /**\n * Look up a queue's payload schema at runtime, typed so `.parse()` returns the\n * queue's payload. Used when validating outgoing (enqueue) and incoming\n * (worker) payloads — both boundaries validate from this one schema.\n *\n * The single cast is unavoidable: a runtime lookup can't be correlated to the\n * compile-time payload type. It is sound because the map is built directly\n * from `definitions`, whose entry for `queue` carries exactly this schema.\n *\n * The miss is still checked. On the inferred path every name is present, but\n * `schemaFor` is exported and a caller whose registry type has widened to\n * `QueueDefinition[]` can reach it with any string. Without the guard that\n * surfaces as `Cannot read properties of undefined (reading 'parse')` from\n * somewhere else entirely.\n */\n function schemaFor<Q extends Name>(queue: Q): z.ZodType<Payload<Q>> {\n const schema = schemaByQueue.get(queue);\n if (!schema) {\n throw new JobPlatformError(`Unknown queue \"${queue}\"`);\n }\n return schema as z.ZodType<Payload<Q>>;\n }\n\n /**\n * Core enqueue, parameterized by boss instance for tests.\n * `db` is whatever `toBossDb` accepts — typically a pool handle or a\n * transaction handle. Pass the transaction to make job creation atomic with\n * your domain writes; the queue NOTIFY fires on commit.\n *\n * The payload is validated against the queue's schema before sending —\n * defense in depth: the worker validates again on the way out, both from the\n * one schema.\n */\n async function enqueueWith<Q extends Sendable & Name>(\n boss: PgBoss,\n args: { db: TDb; queue: Q; data: Payload<Q>; options?: JobOptions }\n ): Promise<string | null> {\n const data = schemaFor(args.queue).parse(args.data);\n return boss.send(args.queue, data, {\n ...args.options,\n db: toBossDb(args.db),\n });\n }\n\n /**\n * The one sanctioned way for application code to create a job.\n * Never call boss.send() directly. Payloads are thin references and must\n * never contain credentials — job rows persist in the database for days.\n */\n async function enqueue<Q extends Sendable & Name>(args: {\n db: TDb;\n queue: Q;\n data: Payload<Q>;\n options?: JobOptions;\n }): Promise<string | null> {\n return enqueueWith(await getBoss(), args);\n }\n\n /**\n * Cancel jobs on a queue by id (e.g. when their domain record is cancelled).\n * Best-effort: pg-boss updates only cancellable jobs, so already-settled ids\n * are a no-op. Cancelling stops a queued job from starting and prevents a\n * retry of an active one — it does NOT abort a job already running on a\n * worker; interrupt that in-process.\n */\n async function cancelJobs(queue: Name, jobIds: string[]): Promise<void> {\n if (jobIds.length === 0) return;\n const boss = await getBoss();\n await boss.cancel(queue, jobIds).catch((err: unknown) => {\n logger.warn({ err, jobIds, queue }, \"job cancel failed (jobs may be settled)\");\n });\n }\n\n /**\n * Define a worker for a queue. The handler receives the resolved runtime\n * (`R`, inferred from `getRuntime`) spread alongside the validated, typed\n * jobs — so handlers never open a database connection or parse payloads\n * themselves. The spread is shallow: a runtime that is a class instance\n * arrives without its prototype, so keep it plain data.\n *\n * A payload that fails validation throws, so the job fails → pg-boss retries\n * → dead-letters, like any other handler error — unless platform `middleware`\n * intercepts it: middleware that swallows the error or never calls `next()`\n * skips all of that, including the log line above. See `JobMiddleware`.\n * Every job is also logged here with its queue, id, retry count and acting\n * user, so no handler has to remember to trace who a job is for.\n */\n function defineWorker<Q extends Name>(w: {\n queue: Q;\n options?: Omit<WorkOptions, \"includeMetadata\">;\n handler: (ctx: R & { jobs: JobWithMetadata<Payload<Q>>[] }) => Promise<void>;\n }): RegisteredWorker {\n return {\n queue: w.queue,\n register: async (boss) => {\n // Resolved at registration (boot) time, so neither depends on module\n // init order.\n const schema = schemaFor(w.queue);\n const runtime = await resolveRuntime();\n // boss.work uses `const O`, so the literal includeMetadata:true survives\n // inference → JobWithMetadata handler; ReqData infers from the annotated\n // `jobs` param. No explicit type args, no cast.\n return boss.work(\n w.queue,\n { ...w.options, includeMetadata: true },\n async (jobs: JobWithMetadata<Payload<Q>>[]) => {\n const run = async () => {\n await w.handler({\n ...runtime,\n jobs: parseJobBatch(jobs, schema, w.queue, logger),\n });\n };\n // Middleware wraps validation as well as the handler, so a bad\n // payload throws through next() where a check-in can see it.\n if (!middleware) return run();\n // Awaited, not returned: pg-boss stores a single-job batch's\n // resolved callback value as job output, so returning\n // middleware's resolved value would leak it there. Awaiting\n // keeps this handler's own resolution at `undefined`.\n await middleware({ jobs, queue: w.queue }, run);\n }\n );\n },\n };\n }\n\n /**\n * Create missing queues; update options on existing ones (policy/partition are\n * immutable in pg-boss). Note: pg-boss's `update_queue` COALESCEs unspecified\n * options to their current values, so removing an option from a definition\n * here does not reset it to default on an already-created queue — that needs\n * a fresh queue or manual intervention.\n */\n async function ensureQueues(boss: PgBoss): Promise<void> {\n for (const def of definitions) {\n // Widen the `as const` options back to the mutable, all-optional pg-boss\n // shape so we can strip the immutable fields without narrowing errors.\n // Definitions with nothing to configure omit `options` entirely.\n const options: Omit<\n NonNullable<Parameters<PgBoss[\"createQueue\"]>[1]>,\n \"name\"\n > = def.options ?? {};\n const existing = await boss.getQueue(def.name);\n if (existing) {\n const { policy: _policy, partition: _partition, ...updatable } = options;\n // Skip the update when there is nothing to update: pg-boss asserts\n // \"no properties found to update\" and throws. That is reachable on the\n // documented happy path — a queue declared with no `options` at all, or\n // with only the immutable `policy`/`partition` stripped above — and it\n // only bites on the SECOND boot, once the queue exists and this takes\n // the update branch instead of the create branch.\n if (Object.keys(updatable).length > 0) {\n await boss.updateQueue(def.name, updatable);\n }\n } else {\n await boss.createQueue(def.name, options);\n logger.info({ queue: def.name }, \"queue created\");\n }\n }\n }\n\n /** Idempotent sync: validate, upsert every declared schedule, unschedule the rest. */\n async function applySchedules(boss: PgBoss, declared: ScheduleOf<D>[]): Promise<void> {\n // Validate EVERY schedule before applying ANY, so an invalid entry can't\n // leave half the schedules upserted and the rest not.\n for (const s of declared) {\n assertValidSchedulePayload(s, schemaFor(s.queue));\n }\n // `s.data` is sent as written, not the parse output: the value round-trips\n // through jsonb before a worker sees it and is parsed again there, so\n // rewriting it here would change what the author declared for no gain.\n for (const s of declared) {\n await boss.schedule(s.queue, s.cron, s.data, s.options ?? {});\n }\n const toRemove = schedulesToRemove(declared, await boss.getSchedules());\n for (const r of toRemove) {\n if (r.key === undefined) {\n await boss.unschedule(r.name);\n } else {\n await boss.unschedule(r.name, r.key);\n }\n }\n // Log only when there's something to report (silent on the common empty\n // case). `applied` is a count — it's every declared schedule on every boot,\n // so the identities aren't news — but list the removed ones: a schedule\n // being turned off is the rare, notable event and you want to see which.\n if (declared.length > 0 || toRemove.length > 0) {\n logger.info({ applied: declared.length, removed: toRemove }, \"schedules synced\");\n }\n }\n\n return {\n applySchedules,\n cancelJobs,\n defineWorker,\n enqueue,\n enqueueWith,\n ensureQueues,\n schemaFor,\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/boss.ts","../src/errors.ts","../src/schedules.ts","../src/types.ts","../src/platform.ts"],"names":["z"],"mappings":"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","file":"index.js","sourcesContent":["import { PgBoss } from \"pg-boss\";\nimport type { JobLogger } from \"./types\";\n\n/**\n * Pure factory — no caching, no process hooks, no config reading. Owning the\n * boss lifecycle (singleton caching, shutdown hooks, reading connection\n * settings) is the application's job.\n *\n * The `error` and `warning` handlers are the reason to prefer this over\n * `new PgBoss(...)` directly: an unhandled pg-boss `error` event crashes the\n * Node process.\n */\nexport function createBoss(args: {\n connectionString: string;\n migrate: boolean;\n max?: number;\n /** Surfaces in `pg_stat_activity` — set it to something you can grep for. */\n applicationName?: string;\n /** Postgres schema pg-boss owns. Defaults to pg-boss's own default. */\n schema?: string;\n logger: JobLogger;\n}): PgBoss {\n const boss = new PgBoss({\n application_name: args.applicationName ?? \"bosskit\",\n connectionString: args.connectionString,\n max: args.max ?? 5,\n migrate: args.migrate,\n schema: args.schema ?? \"pgboss\",\n useListenNotify: true,\n });\n // Mandatory: an unhandled 'error' event would crash the Node process.\n boss.on(\"error\", (err) => args.logger.error({ err }, \"pg-boss error\"));\n boss.on(\"warning\", (warning) => args.logger.warn({ warning }, \"pg-boss warning\"));\n return boss;\n}\n","/**\n * Errors raised by the job platform itself — a misconfigured registry, or a\n * declared schedule whose payload doesn't satisfy its queue's schema. Never a\n * job that failed. Always thrown at construction, boot, or schedule sync,\n * never from a job handler.\n *\n * A plain named subclass rather than a richer error type from some error\n * framework: bosskit has zero runtime dependencies, and an error class is not\n * worth acquiring one — nor worth forcing a dependency on callers who already\n * have their own. (`instanceof` is reliable here — the package targets ES2022,\n * so no prototype fixup is needed.)\n */\nexport class JobPlatformError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"JobPlatformError\";\n }\n}\n","import type { ScheduleOptions } from \"pg-boss\";\nimport { z } from \"zod\";\nimport { JobPlatformError } from \"./errors\";\nimport type { QueueDefinition, QueuePayloadOf, SendableOf } from \"./types\";\n\n/**\n * pg-boss's `ScheduleOptions` without `db`. Beyond `tz` and `key`, that is the\n * full set of send options: pg-boss stores them on the schedule row and applies\n * them to every job the schedule creates, so `expireInSeconds` or `retryLimit`\n * set here govern each of those jobs. `db` is excluded because bosskit chooses\n * the connection pg-boss calls run on, exactly as it does for `JobOptions`.\n */\ntype ScheduleSendOptions = Omit<ScheduleOptions, \"db\">;\n\n/**\n * The loose, registry-agnostic shape of a declared schedule: `data` is\n * optional and `queue` is any string, not narrowed to a registry's names.\n * `schedulesToRemove` (below) consumes this shape so it can diff schedules\n * from any registry — or none — against what pg-boss has stored. `ScheduleOf`\n * is assignable to it. Declaring schedules yourself? Use `ScheduleOf`, which\n * binds `data` to one registry's queue payloads and is what `applySchedules`\n * takes.\n */\nexport type ScheduleDefinition<Name extends string = string> = {\n /** Queue that receives the scheduled job. */\n queue: Name;\n /** 5-field cron (minute precision) — pg-boss evaluates schedules every ~30s. */\n cron: string;\n /** Plain JSON-serializable data (no Dates/class instances) — it round-trips through jsonb. */\n data?: object;\n /** pg-boss `ScheduleOptions` without `db`; see `ScheduleSendOptions`. */\n options?: ScheduleSendOptions;\n};\n\n/**\n * Identity of an existing schedule row, all `applySchedules` needs to decide\n * which stored schedules are no longer declared. `key` is `string | null` so a\n * real `Schedule[]` from `boss.getSchedules()` (key is `''` when unset) and\n * explicit-null test fixtures both assign here without a cast.\n */\ntype ExistingScheduleId = { name: string; key: string | null };\n\n/** Stable identity for a schedule: same queue + key = same schedule (empty/null/undefined key all normalize together). */\nfunction idOf(name: string, key: string | null | undefined): string {\n return `${name}::${key ?? \"\"}`;\n}\n\n/**\n * Pure: existing schedules that are no longer declared, so they can be\n * unscheduled. We don't diff cron/data to decide what to *apply* — `boss.schedule`\n * is an idempotent upsert and pg-boss derives fire times from the cron expression\n * (not from `updated_on`), so re-applying an unchanged schedule is a cheap no-op\n * with no effect on timing. Only removals need a diff.\n */\nexport function schedulesToRemove(\n declared: ScheduleDefinition[],\n existing: ExistingScheduleId[]\n): Array<{ name: string; key?: string }> {\n const declaredIds = new Set(declared.map((d) => idOf(d.queue, d.options?.key)));\n return existing\n .filter((e) => !declaredIds.has(idOf(e.name, e.key)))\n .map((e) => (!e.key ? { name: e.name } : { key: e.key, name: e.name }));\n}\n\n/**\n * Throw if a declared schedule's payload would fail in a worker. Pure and\n * total — it either returns or throws, and reads nothing outside its arguments,\n * so it takes the queue's schema rather than reaching for a registry.\n *\n * Validates what the WORKER will see, not what was declared. Schedule data is\n * stored as jsonb and re-read at fire time, so a schema field that accepts a\n * non-JSON value — z.date(), z.instanceof(), z.map() — would pass on the\n * in-memory value and still fail at fire time on the string it became.\n * Validating the round-tripped value checks what the worker will actually\n * parse. Scheduled jobs never pass through `enqueue`, so this is the only\n * place to catch it before the schedule fires.\n *\n * The round trip itself can throw before safeParse ever runs — a BigInt, a\n * circular reference, or (reachable when a caller's registry type has widened\n * to `QueueDefinition[]`) a missing `data` entirely. Left unguarded those\n * surface as a raw TypeError/SyntaxError naming neither queue nor key,\n * bypassing the JobPlatformError contract `applySchedules` otherwise\n * guarantees.\n *\n * The parameter is spelled structurally rather than as `ScheduleDefinition`\n * on purpose: that type's `data` is `object | undefined`, which would reject\n * the very inputs this function exists to catch.\n */\nexport function assertValidSchedulePayload(\n schedule: { data: unknown; options?: { key?: string }; queue: string },\n schema: z.ZodType\n): void {\n const key = schedule.options?.key ? ` (key \"${schedule.options.key}\")` : \"\";\n const label = `Schedule for queue \"${schedule.queue}\"${key}`;\n let roundTripped: unknown;\n try {\n roundTripped = JSON.parse(JSON.stringify(schedule.data));\n } catch (err) {\n throw new JobPlatformError(`${label} has data that is not JSON-serializable: ${String(err)}`);\n }\n const result = schema.safeParse(roundTripped);\n if (!result.success) {\n throw new JobPlatformError(`${label} has an invalid payload: ${z.prettifyError(result.error)}`);\n }\n}\n\n/**\n * A schedule declaration bound to one registry. Distributing over the sendable\n * queue names is what types `data` per queue: a schedule for queue \"a\" must\n * carry queue \"a\"'s payload, so a mismatched or missing payload is a compile\n * error rather than a job that fails every time the schedule fires.\n *\n * Dead-letter queues are excluded (`SendableOf`, not `QueueNameOf`) for the\n * same reason `enqueue` excludes them: pg-boss populates a DLQ itself.\n *\n * `data` is Zod's OUTPUT type, so a field with `.default()` must still be\n * supplied here — same as `enqueue`.\n */\nexport type ScheduleOf<D extends readonly QueueDefinition[]> = {\n [Q in SendableOf<D>]: {\n /** 5-field cron (minute precision) — pg-boss evaluates schedules every ~30s. */\n cron: string;\n /** This queue's payload. Must be JSON-round-trippable; it is stored as jsonb. */\n data: QueuePayloadOf<D, Q>;\n /** pg-boss `ScheduleOptions` without `db`; see `ScheduleSendOptions`. */\n options?: ScheduleSendOptions;\n /** Queue that receives the scheduled job. */\n queue: Q;\n };\n}[SendableOf<D>];\n","import type { JobWithMetadata, PgBoss } from \"pg-boss\";\nimport { z } from \"zod\";\n\n/**\n * Generic job-platform types. Nothing in this package knows anything about the\n * application using it: no concrete queue, no configuration shape, no database\n * type. A concrete instance is built by calling `createJobPlatform` with a\n * queue registry and providers — see the README.\n */\n\n/** The minimal logging surface the platform needs; a pino logger satisfies it. */\nexport type JobLogger = {\n info(obj: Record<string, unknown>, msg: string): void;\n warn(obj: Record<string, unknown>, msg: string): void;\n error(obj: Record<string, unknown>, msg: string): void;\n};\n\n/**\n * The acting user a job runs on behalf of — the identity a worker resolves\n * credentials, tenancy or permissions from, and the one every job log line\n * carries. A user-scoped queue's payload extends this; see `QueueDefinition`\n * for the `global` opt-out used by system jobs that have no user.\n *\n * This lives in the payload (not pg-boss job metadata) because `data` is the\n * only user-controlled channel pg-boss offers — and because the DLQ hop copies\n * `data` verbatim, the acting user survives into dead-letter queues for free.\n */\nexport const UserScopedSchema = z.object({ userId: z.string() });\nexport type UserScoped = z.infer<typeof UserScopedSchema>;\n\ntype QueueOptions = NonNullable<Parameters<PgBoss[\"createQueue\"]>[1]>;\n\ntype QueueDefinitionBase = {\n name: string;\n /** pg-boss queue options. Omit entirely for a queue with nothing to configure. */\n options?: Omit<QueueOptions, \"name\">;\n};\n\n/**\n * A queue definition. By DEFAULT a queue is user-scoped: its payload schema\n * must produce a `userId`, so forgetting the acting user on a new queue is a\n * compile error rather than a runtime surprise discovered in a worker. System\n * work that genuinely has no user on whose behalf it runs — cron sweeps,\n * maintenance jobs — opts out explicitly with `global: true`.\n *\n * Because `enqueue`'s `data` parameter is derived from this schema\n * (`QueuePayloadOf`), the constraint also makes it a compile error to enqueue\n * without a user, or to drop the user across a chain hop.\n *\n * IMPORTANT: never store a registry in a variable annotated `QueueDefinition[]`\n * or `readonly QueueDefinition[]`. Both spellings widen it, and widening costs\n * two guarantees at once, silently:\n *\n * - `QueuePayloadOf` collapses to this type's base user-scoped shape, so\n * `enqueue` stops type-checking domain fields entirely.\n * - `SendableOf` collapses to `string`, so the dead-letter exclusion disappears\n * and any queue name — including one that does not exist — compiles.\n *\n * Three spellings keep it precise: `defineQueues([...])`, an array literal\n * passed straight into `createJobPlatform`, and `[...] satisfies\n * QueueDefinition[]`. Prefer `defineQueues` — it checks each entry against this\n * constraint without widening what it stores.\n */\nexport type QueueDefinition =\n | (QueueDefinitionBase & {\n global?: false;\n /** Zod schema for this queue's job payload — the single source of truth\n * for both the compile-time payload type and the runtime boundary\n * validation. Must carry the acting user (see `UserScopedSchema`). */\n schema: z.ZodType<UserScoped>;\n })\n | (QueueDefinitionBase & {\n /** This queue's jobs run on behalf of no one — system work only. */\n global: true;\n /** Zod schema for this queue's job payload — the single source of truth\n * for both the compile-time payload type and the runtime boundary\n * validation. `object` because a pg-boss payload is always JSON. */\n schema: z.ZodType<object>;\n });\n\n/** Every queue name in a registry. Broader than `SendableOf`: includes DLQs. */\nexport type QueueNameOf<D extends readonly QueueDefinition[]> = D[number][\"name\"];\n\n/**\n * Payload type per queue, inferred from each declared Zod schema — the derived\n * contract for `enqueue` and worker handlers, with no hand-written map to keep\n * in sync. Modelled as an indexed access (not `Extract` + `z.infer`) so it\n * resolves to a concrete object type for a generic `Q`, e.g. inside `enqueue`.\n */\ntype QueuePayloadMapOf<D extends readonly QueueDefinition[]> = {\n [E in D[number] as E[\"name\"]]: z.infer<E[\"schema\"]>;\n};\n/**\n * The `& object` states what is already true — a pg-boss payload is JSON — and\n * is applied here rather than inside the map on purpose: with both `D` and `Q`\n * generic the map lookup stays deferred, so only an intersection at this level\n * keeps a payload provably assignable to `boss.send`'s `object` parameter.\n */\nexport type QueuePayloadOf<\n D extends readonly QueueDefinition[],\n Q extends QueueNameOf<D>,\n> = QueuePayloadMapOf<D>[Q] & object;\n\n/**\n * Per-slot `options`, defaulting to `undefined` for a definition that omits it\n * entirely. A plain `D[number][\"options\"]` indexed access does not work once\n * `options` is optional: a tuple entry that omits the key altogether has no\n * `options` property at all, and indexed access on a union requires every\n * member to carry the key, so the lookup would fail to compile the moment any\n * entry left `options` out. Distributing over `keyof D` (each tuple slot,\n * rather than the merged `D[number]` union) sidesteps that — an entry without\n * `options` just contributes `undefined` instead of breaking the type for\n * every other entry.\n */\ntype OptionsTupleOf<D extends readonly QueueDefinition[]> = {\n [K in keyof D]: \"options\" extends keyof D[K] ? D[K][\"options\"] : undefined;\n};\n\n/**\n * Every dead-letter target named by some queue's `deadLetter` option. You never\n * enqueue to a DLQ (pg-boss copies failed jobs into it automatically), so these\n * are excluded from the enqueue-able set below.\n */\ntype DeadLetterOf<D extends readonly QueueDefinition[]> = Extract<\n OptionsTupleOf<D>[number],\n { deadLetter: string }\n>[\"deadLetter\"];\n\n/**\n * The queues application code may enqueue to: every defined queue minus the\n * dead-letter targets. Derived, so declaring a new DLQ automatically keeps it\n * off the enqueue surface.\n */\nexport type SendableOf<D extends readonly QueueDefinition[]> = Exclude<\n QueueNameOf<D>,\n DeadLetterOf<D>\n>;\n\ntype SendOptionsOf = NonNullable<Parameters<PgBoss[\"send\"]>[2]>;\n/** pg-boss send options, minus `db` — the platform owns db threading. */\nexport type JobOptions = Omit<SendOptionsOf, \"db\">;\n\n/**\n * A worker registered against a queue, type-erased for storage in a worker\n * list. `defineWorker` binds the queue → payload → handler types; `register`\n * closes over them so a heterogeneous worker list needs no shared handler type.\n */\nexport type RegisteredWorker = {\n queue: string;\n register: (boss: PgBoss) => Promise<string>;\n};\n\n/**\n * A hook wrapping every worker's run, for concerns that must not be\n * per-worker opt-in — tracing, alerting, log context. Registered once on the\n * platform, so it applies to every worker by construction.\n *\n * `jobs` is deliberately `unknown`: middleware runs OUTSIDE the parse loop, so\n * these payloads have not been validated — coercions and defaults are\n * unapplied and the data may not satisfy the schema at all. Typing them as the\n * queue's payload would be a lie. `queue` keeps its exact literal type, so\n * branching on queue name is fully checked.\n *\n * Properties to know before writing one:\n *\n * 1. Runs once per BATCH, not once per job. Identical at the default\n * `batchSize` of 1; not above it.\n * 2. Wraps payload validation as well as the handler, so a payload that fails\n * `schema.parse` throws through `next()` and is observable here.\n * 3. Swallowing an error MARKS THE JOB COMPLETE. pg-boss completes a batch when\n * the callback resolves and fails it when the callback throws, so catching\n * without rethrowing suppresses the retry and the dead-letter hop.\n * Middleware that reports errors must rethrow.\n * 4. Not calling `next()` skips the handler and completes the job.\n *\n * `next()` is not idempotent: calling it twice re-parses the batch and\n * re-runs the handler.\n *\n * The platform awaits this and discards whatever it resolves to, so its\n * signature returns `Promise<void>` — there is no channel back into pg-boss's\n * job output.\n */\nexport type JobMiddleware<TName extends string = string> = (\n ctx: { jobs: JobWithMetadata<unknown>[]; queue: TName },\n next: () => Promise<void>\n) => Promise<void>;\n\n/**\n * Declare a queue registry.\n *\n * The `const` type parameter preserves the literal tuple, so every derived type\n * (`QueueNameOf`, `QueuePayloadOf`, `SendableOf`) stays precise. This is the\n * recommended way to build a registry: the alternative spellings\n * `const QUEUES: QueueDefinition[] = [...]` and\n * `const QUEUES: readonly QueueDefinition[] = [...]` both type-check but widen,\n * which collapses every payload to `UserScoped` AND collapses the enqueue-able\n * name set to `string` — so domain fields stop being checked and the\n * dead-letter guard quietly stops guarding. Calling a function instead of\n * writing a type annotation makes that mistake unspellable.\n */\nexport function defineQueues<const D extends readonly QueueDefinition[]>(defs: D): D {\n return defs;\n}\n","import type { JobWithMetadata, PgBoss, Db as PgBossDb, WorkOptions } from \"pg-boss\";\nimport type { z } from \"zod\";\nimport { JobPlatformError } from \"./errors\";\nimport { type ScheduleOf, assertValidSchedulePayload, schedulesToRemove } from \"./schedules\";\nimport {\n type JobLogger,\n type JobMiddleware,\n type JobOptions,\n type QueueDefinition,\n type QueueNameOf,\n type QueuePayloadOf,\n type RegisteredWorker,\n type SendableOf,\n UserScopedSchema,\n} from \"./types\";\n\n/**\n * Parse and log one batch of jobs on the way into a handler. Pure apart from\n * the logger call, and takes the schema rather than reaching for a registry, so\n * it stays readable outside the platform closure it is called from.\n *\n * The handler is handed the PARSED jobs, not the raw ones. `data` arrives as\n * jsonb, and the handler's type is the schema's OUTPUT type — so a\n * `z.coerce.date()` field must reach it as a Date and a `.default()` field must\n * be filled in, not left undefined.\n *\n * Parsing is per BATCH: a payload that fails validation throws, failing every\n * job fetched alongside it. That never comes up at the default `batchSize` of 1.\n */\nfunction parseJobBatch<T>(\n jobs: JobWithMetadata<unknown>[],\n schema: z.ZodType<T>,\n queue: string,\n logger: JobLogger\n): JobWithMetadata<T>[] {\n const parsed: JobWithMetadata<T>[] = [];\n for (const job of jobs) {\n const data = schema.parse(job.data);\n // Uniform actor trace for every queue. safeParse (not a cast) so this also\n // works for `global` queues, whose payloads carry no user.\n const actor = UserScopedSchema.safeParse(data);\n logger.info(\n {\n jobId: job.id,\n queue,\n retryCount: job.retryCount,\n userId: actor.success ? actor.data.userId : undefined,\n },\n \"job received\"\n );\n parsed.push({ ...job, data });\n }\n return parsed;\n}\n\n/**\n * Build a job platform bound to one queue registry.\n *\n * This is the package's only entry point, and the reason nothing inside it\n * knows about the application using it. Everything application-shaped arrives\n * through arguments:\n *\n * - `definitions` — the queue registry. Both the compile-time payload types and\n * the runtime boundary validation derive from it, so you declare each queue\n * exactly once.\n * - `getBoss` — resolves a *started* pg-boss instance. A provider rather than an\n * instance because the boss is not started at module-evaluation time; you own\n * its creation, caching and config.\n * - `getRuntime` — resolves whatever context handlers should receive (say,\n * `{ db, config }`). Its return type `R` is INFERRED, which is how handler\n * context gets typed without this package importing your `Db`/`Config`.\n * Resolved AT MOST ONCE for the life of the platform (see below), so anything\n * computed per call — a fresh request id, a timestamp — would be frozen at\n * the first value. Return a plain data object: handlers receive it via the\n * shallow spread `{ ...runtime, jobs }`, which drops a class instance's\n * prototype and with it every method on it.\n * - `toBossDb` — adapts your database handle to pg-boss's `executeSql`\n * contract. Its parameter type `TDb` is INFERRED and becomes the `db` every\n * enqueue takes, so this package needs no ORM: pass one of pg-boss's own\n * adapters (`fromDrizzle`, `fromKnex`, `fromKysely`, `fromPrisma`,\n * `fromPglite`) or write three lines for any other client. ANNOTATE the\n * parameter — written as `(db) => ...` it infers `unknown`, and `enqueue`\n * then accepts any value at all as its `db`.\n * - `logger` — the platform never reaches for a global logger.\n * - `middleware` — optional, wraps every worker's payload validation and\n * handler. Platform-level so it cannot be forgotten on one worker; see\n * `JobMiddleware` for the behaviors to know before writing one.\n *\n * All three type parameters are inferred from the call, so you never write an\n * explicit type argument. `const D` preserves the literal registry tuple, which\n * is what keeps `QueuePayloadOf` precise (see the note on `QueueDefinition`).\n */\nexport function createJobPlatform<const D extends readonly QueueDefinition[], R, TDb>(platform: {\n definitions: D;\n getBoss: () => Promise<PgBoss>;\n getRuntime: () => Promise<R>;\n toBossDb: (db: TDb) => PgBossDb;\n logger: JobLogger;\n /** Optional hook wrapping every worker's validation and handler. See `JobMiddleware`. */\n middleware?: JobMiddleware<QueueNameOf<D>>;\n}) {\n const { definitions, getBoss, getRuntime, logger, middleware, toBossDb } = platform;\n\n /**\n * Resolve the runtime at most once, lazily, on the first worker registration.\n * `register` runs per worker, and a provider that allocated a connection pool\n * per call would quietly open one per worker. The memo is cleared only when\n * the promise rejects, so a transient failure at boot doesn't poison a later\n * retry; a successful resolution is kept for the life of the platform.\n */\n let runtimePromise: Promise<R> | undefined;\n function resolveRuntime(): Promise<R> {\n if (!runtimePromise) {\n runtimePromise = getRuntime().catch((err: unknown) => {\n runtimePromise = undefined;\n throw err;\n });\n }\n return runtimePromise;\n }\n\n type Name = QueueNameOf<D>;\n type Sendable = SendableOf<D>;\n type Payload<Q extends Name> = QueuePayloadOf<D, Q>;\n\n // Runtime name → schema lookup, built from the definitions. Duplicate names\n // are rejected rather than last-write-wins: the payload TYPE for a repeated\n // name is the union of both schemas, but only one schema would do the\n // validating, so half the payloads would be checked against the wrong shape.\n // The type system can't catch this (a duplicated key just merges), so the\n // registry is verified here, once, at construction.\n const schemaByQueue = new Map<string, QueueDefinition[\"schema\"]>();\n for (const d of definitions) {\n if (schemaByQueue.has(d.name)) {\n throw new JobPlatformError(`Queue \"${d.name}\" is declared more than once in the registry`);\n }\n schemaByQueue.set(d.name, d.schema);\n }\n\n /**\n * Look up a queue's payload schema at runtime, typed so `.parse()` returns the\n * queue's payload. Used when validating outgoing (enqueue) and incoming\n * (worker) payloads — both boundaries validate from this one schema.\n *\n * The single cast is unavoidable: a runtime lookup can't be correlated to the\n * compile-time payload type. It is sound because the map is built directly\n * from `definitions`, whose entry for `queue` carries exactly this schema.\n *\n * The miss is still checked. On the inferred path every name is present, but\n * `schemaFor` is exported and a caller whose registry type has widened to\n * `QueueDefinition[]` can reach it with any string. Without the guard that\n * surfaces as `Cannot read properties of undefined (reading 'parse')` from\n * somewhere else entirely.\n */\n function schemaFor<Q extends Name>(queue: Q): z.ZodType<Payload<Q>> {\n const schema = schemaByQueue.get(queue);\n if (!schema) {\n throw new JobPlatformError(`Unknown queue \"${queue}\"`);\n }\n return schema as z.ZodType<Payload<Q>>;\n }\n\n /**\n * Core enqueue, parameterized by boss instance for tests.\n * `db` is whatever `toBossDb` accepts — typically a pool handle or a\n * transaction handle. Pass the transaction to make job creation atomic with\n * your domain writes; the queue NOTIFY fires on commit.\n *\n * The payload is validated against the queue's schema before sending —\n * defense in depth: the worker validates again on the way out, both from the\n * one schema.\n */\n async function enqueueWith<Q extends Sendable & Name>(\n boss: PgBoss,\n args: { db: TDb; queue: Q; data: Payload<Q>; options?: JobOptions }\n ): Promise<string | null> {\n const data = schemaFor(args.queue).parse(args.data);\n return boss.send(args.queue, data, {\n ...args.options,\n db: toBossDb(args.db),\n });\n }\n\n /**\n * The one sanctioned way for application code to create a job.\n * Never call boss.send() directly. Payloads are thin references and must\n * never contain credentials — job rows persist in the database for days.\n */\n async function enqueue<Q extends Sendable & Name>(args: {\n db: TDb;\n queue: Q;\n data: Payload<Q>;\n options?: JobOptions;\n }): Promise<string | null> {\n return enqueueWith(await getBoss(), args);\n }\n\n /**\n * Cancel jobs on a queue by id (e.g. when their domain record is cancelled).\n * Best-effort: pg-boss updates only cancellable jobs, so already-settled ids\n * are a no-op. Cancelling stops a queued job from starting and prevents a\n * retry of an active one — it does NOT abort a job already running on a\n * worker; interrupt that in-process.\n */\n async function cancelJobs(queue: Name, jobIds: string[]): Promise<void> {\n if (jobIds.length === 0) return;\n const boss = await getBoss();\n await boss.cancel(queue, jobIds).catch((err: unknown) => {\n logger.warn({ err, jobIds, queue }, \"job cancel failed (jobs may be settled)\");\n });\n }\n\n /**\n * Define a worker for a queue. The handler receives the resolved runtime\n * (`R`, inferred from `getRuntime`) spread alongside the validated, typed\n * jobs — so handlers never open a database connection or parse payloads\n * themselves. The spread is shallow: a runtime that is a class instance\n * arrives without its prototype, so keep it plain data.\n *\n * A payload that fails validation throws, so the job fails → pg-boss retries\n * → dead-letters, like any other handler error — unless platform `middleware`\n * intercepts it: middleware that swallows the error or never calls `next()`\n * skips all of that, including the log line above. See `JobMiddleware`.\n * Every job is also logged here with its queue, id, retry count and acting\n * user, so no handler has to remember to trace who a job is for.\n */\n function defineWorker<Q extends Name>(w: {\n queue: Q;\n options?: Omit<WorkOptions, \"includeMetadata\">;\n handler: (ctx: R & { jobs: JobWithMetadata<Payload<Q>>[] }) => Promise<void>;\n }): RegisteredWorker {\n return {\n queue: w.queue,\n register: async (boss) => {\n // Resolved at registration (boot) time, so neither depends on module\n // init order.\n const schema = schemaFor(w.queue);\n const runtime = await resolveRuntime();\n // boss.work uses `const O`, so the literal includeMetadata:true survives\n // inference → JobWithMetadata handler; ReqData infers from the annotated\n // `jobs` param. No explicit type args, no cast.\n return boss.work(\n w.queue,\n { ...w.options, includeMetadata: true },\n async (jobs: JobWithMetadata<Payload<Q>>[]) => {\n const run = async () => {\n await w.handler({\n ...runtime,\n jobs: parseJobBatch(jobs, schema, w.queue, logger),\n });\n };\n // Middleware wraps validation as well as the handler, so a bad\n // payload throws through next() where middleware can observe it.\n if (!middleware) return run();\n // Awaited, not returned: pg-boss stores a single-job batch's\n // resolved callback value as job output, so returning\n // middleware's resolved value would leak it there. Awaiting\n // keeps this handler's own resolution at `undefined`.\n await middleware({ jobs, queue: w.queue }, run);\n }\n );\n },\n };\n }\n\n /**\n * Create missing queues; update options on existing ones (policy/partition are\n * immutable in pg-boss). Note: pg-boss's `update_queue` COALESCEs unspecified\n * options to their current values, so removing an option from a definition\n * here does not reset it to default on an already-created queue — that needs\n * a fresh queue or manual intervention.\n */\n async function ensureQueues(boss: PgBoss): Promise<void> {\n for (const def of definitions) {\n // Widen the `as const` options back to the mutable, all-optional pg-boss\n // shape so we can strip the immutable fields without narrowing errors.\n // Definitions with nothing to configure omit `options` entirely.\n const options: Omit<\n NonNullable<Parameters<PgBoss[\"createQueue\"]>[1]>,\n \"name\"\n > = def.options ?? {};\n const existing = await boss.getQueue(def.name);\n if (existing) {\n const { policy: _policy, partition: _partition, ...updatable } = options;\n // Skip the update when there is nothing to update: pg-boss asserts\n // \"no properties found to update\" and throws. That is reachable on the\n // documented happy path — a queue declared with no `options` at all, or\n // with only the immutable `policy`/`partition` stripped above — and it\n // only bites on the SECOND boot, once the queue exists and this takes\n // the update branch instead of the create branch.\n if (Object.keys(updatable).length > 0) {\n await boss.updateQueue(def.name, updatable);\n }\n } else {\n await boss.createQueue(def.name, options);\n logger.info({ queue: def.name }, \"queue created\");\n }\n }\n }\n\n /** Idempotent sync: validate, upsert every declared schedule, unschedule the rest. */\n async function applySchedules(boss: PgBoss, declared: ScheduleOf<D>[]): Promise<void> {\n // Validate EVERY schedule before applying ANY, so an invalid entry can't\n // leave half the schedules upserted and the rest not.\n for (const s of declared) {\n assertValidSchedulePayload(s, schemaFor(s.queue));\n }\n // `s.data` is sent as written, not the parse output: the value round-trips\n // through jsonb before a worker sees it and is parsed again there, so\n // rewriting it here would change what the author declared for no gain.\n for (const s of declared) {\n await boss.schedule(s.queue, s.cron, s.data, s.options ?? {});\n }\n const toRemove = schedulesToRemove(declared, await boss.getSchedules());\n for (const r of toRemove) {\n if (r.key === undefined) {\n await boss.unschedule(r.name);\n } else {\n await boss.unschedule(r.name, r.key);\n }\n }\n // Log only when there's something to report (silent on the common empty\n // case). `applied` is a count — it's every declared schedule on every boot,\n // so the identities aren't news — but list the removed ones: a schedule\n // being turned off is the rare, notable event and you want to see which.\n if (declared.length > 0 || toRemove.length > 0) {\n logger.info({ applied: declared.length, removed: toRemove }, \"schedules synced\");\n }\n }\n\n return {\n applySchedules,\n cancelJobs,\n defineWorker,\n enqueue,\n enqueueWith,\n ensureQueues,\n schemaFor,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosskit",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Type-safe, user-scoped job queues for pg-boss, powered by Zod.",
5
5
  "license": "MIT",
6
6
  "author": "Kenny Williams",