@helipod/scheduler 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/schema.ts","../src/facade.ts","../src/modules.ts","../src/backoff.ts","../src/crons.ts","../src/driver.ts"],"sourcesContent":["import { defineComponent, type ComponentDefinition, type BootContext } from \"@helipod/component\";\nimport { schedulerSchema } from \"./schema\";\nimport { schedulerContext, schedulerActionContext } from \"./facade\";\nimport { _peekDue, _claim, _complete, _reclaim, _cronTick, _enqueue, _cancel } from \"./modules\";\nimport { schedulerDriver } from \"./driver\";\nimport { reconcileCrons, type CronJobs } from \"./crons\";\n\nexport * from \"./schema\";\nexport type { SchedulerContext, SchedulerActionContext, FunctionReference, FnRef, EnqueueOpts, JobState, EnqueueTables, OnCompleteResult } from \"./facade\";\nexport { getFunctionPath, enqueueInternal, fireOnComplete, schedulerActionContext } from \"./facade\";\nexport type { PeekDueResult, ClaimResult, JobResult, DueJob } from \"./modules\";\nexport { BATCH_CAP, LEASE_MS, SWEEP_MS, CATCHUP_CAP } from \"./modules\";\nexport type { SchedulerDriver } from \"./driver\";\nexport { schedulerDriver } from \"./driver\";\nexport type { BackoffOptions } from \"./backoff\";\nexport { computeBackoff, DEFAULT_BACKOFF_OPTIONS } from \"./backoff\";\nexport type {\n CronJobs,\n CronSpec,\n CronRegistryEntry,\n CatchUpPolicy,\n CronOpts,\n CronUtcOpts,\n IntervalPeriod,\n DailyAt,\n HourlyAt,\n WeeklyAt,\n MonthlyAt,\n DayOfWeek,\n} from \"./crons\";\nexport { cronJobs, computeNextRun, computePrevRun, enqueueCadenceJob } from \"./crons\";\n\n/**\n * `defineScheduler()` — the `@helipod/scheduler` component: the `jobs`/`job_args`/`crons`\n * schema, the `ctx.scheduler` facade (`runAfter`/`runAt`/`cancel`/`enqueue`), the\n * internal `_peekDue`/`_claim`/`_complete`/`_cronTick` modules, and the `schedulerDriver`\n * event-loop that actually RUNS due jobs — reactive on commits touching `scheduler/*` plus a\n * wall-clock timer re-armed to the earliest future job (see `./driver.ts`).\n *\n * `contextWrite: true` is load-bearing: it's what lets the facade write (via the calling\n * mutation's own transaction) instead of only reading — see `schedulerContext` in `./facade.ts`\n * and the `ContextProvider.write` opt-in on `@helipod/executor`.\n *\n * `opts.crons` — an app's `crons.ts` (`export default crons` from `cronJobs()` + `.interval()`/\n * `.cron()`/etc.) — is reconciled into the `crons` table once at boot (`reconcileCrons`, see its\n * doc comment in `./crons.ts` for why this is a config value rather than file-discovery magic).\n */\nexport function defineScheduler(opts?: { crons?: CronJobs }): ComponentDefinition {\n return defineComponent({\n name: \"scheduler\",\n schema: schedulerSchema,\n modules: { _peekDue, _claim, _complete, _reclaim, _cronTick, _enqueue, _cancel },\n context: (cctx) => schedulerContext(cctx),\n contextType: { import: \"@helipod/scheduler\", type: \"SchedulerContext\" },\n serverExports: [\"cronJobs\"],\n contextWrite: true,\n driver: schedulerDriver(),\n boot: (ctx: BootContext) => reconcileCrons(ctx, opts?.crons),\n // Action-mode `ctx.scheduler` (Convex parity: portable between a mutation and an action) —\n // see `schedulerActionContext`'s doc comment in `./facade.ts`.\n buildAction: (api) => schedulerActionContext(api),\n });\n}\n","import { defineSchema, defineTable, v } from \"@helipod/values\";\n\n/**\n * The `@helipod/scheduler` component schema (namespaced `scheduler/*` when composed).\n *\n * - `jobs` / `job_args`: split so a job's identity/state (small, hot — scanned by the driver's\n * `by_next_ts` index) never carries the (possibly large) `args`/`context` payload.\n * - `crons`: declared here for the schema to be stable across Task 2→5; only Task 5's cron\n * scheduler reads/writes it (`cadenceJobId` links a cron to its currently-pending job).\n */\nexport const schedulerSchema = defineSchema({\n jobs: defineTable({\n fnPath: v.string(),\n kind: v.union(v.literal(\"mutation\"), v.literal(\"action\")),\n state: v.union(\n v.literal(\"pending\"),\n v.literal(\"inProgress\"),\n v.literal(\"success\"),\n v.literal(\"failed\"),\n v.literal(\"canceled\"),\n ),\n nextTs: v.number(),\n attempts: v.number(),\n maxFailures: v.number(),\n leaseHolder: v.optional(v.string()),\n leaseExpiresAt: v.optional(v.number()),\n idempotencyKey: v.optional(v.string()),\n appVersion: v.optional(v.string()),\n name: v.optional(v.string()),\n hasArgs: v.boolean(),\n onComplete: v.optional(v.string()),\n parentId: v.optional(v.string()),\n completedTs: v.optional(v.number()),\n /** The most recent failure's `String(error)` — set on retry AND on dead-letter (Task 4). */\n lastError: v.optional(v.string()),\n })\n .index(\"by_next_ts\", [\"state\", \"nextTs\"])\n .index(\"by_completed_ts\", [\"completedTs\"])\n .index(\"by_parent\", [\"parentId\"])\n // Task 5: the idempotent-enqueue insert-or-noop lookup (`enqueueInternal` in `./facade.ts`)\n // needs an indexed point-lookup on `idempotencyKey` — without this it'd be a full table scan\n // on every enqueue that passes one, which the cron cadence (`_cronTick` in `./modules.ts`)\n // does for every occurrence it schedules (`${cronName}:${fireTs}`).\n .index(\"by_idempotency\", [\"idempotencyKey\"]),\n\n job_args: defineTable({\n jobId: v.string(),\n args: v.any(),\n context: v.optional(v.any()),\n }).index(\"by_job\", [\"jobId\"]),\n\n // Task 5: recurring/cron schedules. `cadenceJobId` points at the currently-pending `jobs` row\n // for this cron's CADENCE job (the dual-job design's self-rescheduling half — see\n // `_cronTick` in `./modules.ts`); the work job(s) it fires are ordinary `jobs` rows, tracked\n // only via their deterministic `idempotencyKey` (`${name}:${fireTs}`), not by this pointer.\n // `spec` is a JSON-serialized `CronSpec` (`./crons.ts`) — either `{kind:\"interval\",ms}` or\n // `{kind:\"cron\",expr}`. `catchUp` was originally typed `boolean`; Task 5 widened it to the\n // three-way policy `_cronTick` actually implements (`skip` | `fireOnce` | `fireAll`).\n crons: defineTable({\n name: v.string(),\n spec: v.string(),\n tz: v.string(),\n catchUp: v.union(v.literal(\"skip\"), v.literal(\"fireOnce\"), v.literal(\"fireAll\")),\n lastScheduledTs: v.optional(v.number()),\n workFnPath: v.string(),\n workArgs: v.any(),\n cadenceJobId: v.optional(v.string()),\n }).index(\"by_name\", [\"name\"]),\n});\n","import type { ComponentContext, ActionApi } from \"@helipod/executor\";\nimport { GuestDatabaseWriter } from \"@helipod/executor\";\nimport type { JSONValue } from \"@helipod/values\";\n\n/**\n * A function reference as produced by codegen's `api`/`internal` proxy (see\n * `@helipod/client`'s `FunctionReference`/`getFunctionPath`). Replicated here (rather than\n * depending on `@helipod/client`, a client-facing SDK package) since a server component\n * needs only this one-field shape.\n */\nexport interface FunctionReference {\n readonly __path: string;\n}\n\nexport type FnRef = FunctionReference | string;\n\n/** Resolve a `fnRef` (string path or codegen ref) to its string path. Mirrors `@helipod/client`'s `getFunctionPath`. */\nexport function getFunctionPath(ref: FnRef): string {\n return typeof ref === \"string\" ? ref : ref.__path;\n}\n\n/**\n * Task 4 design note — `parentId` threading (cascading cancel):\n *\n * The original design was for the driver to set an ambient \"current job id\" while running a job,\n * so a job scheduling a child (`ctx.scheduler.runAfter(...)` called from inside a driver-run job)\n * would have that child's `parentId` populated automatically via `currentJobId()` below. That\n * requires the ambient to survive from `driver.ts`'s `runPass()` — which only has the string\n * `fnPath`/`jobId`, and calls `ctx.runFunction(claimed.fnPath, claimed.args)` — through\n * `DriverContext.runFunction` → `InlineUdfExecutor.run` → this component's `context` builder,\n * none of which currently carry a \"who's calling\" field. Wiring it soundly means extending\n * `DriverContext.runFunction`'s signature, `RunOptions` (`packages/executor/src/executor.ts`),\n * and `ComponentContext` (used by every `context:` facade, not just this one) — a cross-package\n * change well outside `components/scheduler/*` with blast radius on every component, for a single\n * driver's benefit.\n *\n * Chosen instead: cascading cancel is implemented generically over whatever `parentId` a job\n * happens to have (`cancel()` below walks `by_parent` regardless of how `parentId` got set), and\n * `currentJobId()` stays a stub returning `undefined`. Known limitation: a child scheduled from\n * *inside* a driver-run job today gets `parentId: undefined` (not chained) — cascading cancel\n * only reaches jobs whose `parentId` was set explicitly. The test suite (`test/reliability.test.ts`)\n * exercises the cascade via the test-only `_system:insertJob` escape hatch, which sets `parentId`\n * directly. Revisit if/when a later slice needs real parent/child chaining from driver-run jobs.\n */\nexport type JobState = \"pending\" | \"inProgress\" | \"success\" | \"failed\" | \"canceled\";\n\nexport interface EnqueueOpts {\n /** Relative delay in ms from the enqueueing call's `now()`. `runAt` wins when both are set. */\n runAfter?: number;\n /** Absolute due time (epoch ms). Takes precedence over `runAfter`. */\n runAt?: number;\n retry?: { maxFailures: number };\n name?: string;\n onComplete?: string;\n context?: JSONValue;\n idempotencyKey?: string;\n}\n\nexport interface SchedulerContext {\n runAfter(delayMs: number, fnRef: FnRef, args: JSONValue): Promise<string>;\n runAt(ts: number | Date, fnRef: FnRef, args: JSONValue): Promise<string>;\n cancel(id: string): Promise<void>;\n /** Internal: the general enqueue path (workflow-style callers pass `opts` directly). */\n enqueue(fnRef: FnRef, args: JSONValue, opts?: EnqueueOpts): Promise<string>;\n}\n\n/** App-version stamping (for rolling-deploy replay safety) is wired in a later slice. */\nfunction currentAppVersion(): string | undefined {\n return undefined;\n}\n\n/**\n * The job that scheduled the CURRENT call, if any. Still unset (returns `undefined` always) —\n * see the Task 4 design note below the module doc comment for why threading a real ambient\n * `currentJobId` through the driver/executor was deliberately deferred rather than done here.\n * Every `ctx.scheduler.*` call is therefore a top-level job (`parentId: undefined`) for now;\n * `cancel`'s cascading walk below still works for any job whose `parentId` IS set some other way\n * (currently only the test-only `_system:insertJob` escape hatch), and the born-canceled check in\n * `enqueueInternal` is wired and ready for whenever a future slice sets this ambient for real.\n */\nfunction currentJobId(): string | undefined {\n return undefined;\n}\n\n/** Drop keys whose value is `undefined` — the wire codec (`convexToJson`) rejects `undefined`; omit rather than null it out. */\nexport function compact<T extends Record<string, unknown>>(obj: T): { [K in keyof T]: Exclude<T[K], undefined> } {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) if (v !== undefined) out[k] = v;\n return out as { [K in keyof T]: Exclude<T[K], undefined> };\n}\n\n/**\n * The two `jobs`/`job_args` table names `enqueueInternal` writes to — bare (`\"jobs\"`) when\n * called from this file's own namespaced `ctx.scheduler` facade, or fully qualified\n * (`\"scheduler/jobs\"`) when called from a privileged context (`_cronTick` in `./modules.ts`, and\n * the boot-time cron reconciler in `./crons.ts`) that bypasses namespace prefixing entirely —\n * see `./modules.ts`'s module doc comment for why those callers must use fully-qualified names.\n */\nexport interface EnqueueTables {\n jobs: string;\n jobArgs: string;\n}\n\n/**\n * The shared enqueue path: inserts a `pending` `jobs` row (+ `job_args`), honoring the\n * born-canceled check and, since Task 5, an idempotent insert-or-noop on `opts.idempotencyKey`\n * (looked up via `by_idempotency`) — if a job with that key already exists, its id is returned\n * unchanged and nothing new is inserted. This dedup is what makes the cron cadence's occurrence\n * key (`${cronName}:${fireTs}`, `_cronTick` in `./modules.ts`) safe to call more than once for\n * the same occurrence (e.g. a reclaim-driven re-run of a cadence job).\n *\n * Factored out of `schedulerContext` (which calls it with bare table names, scoped to the calling\n * mutation's own transaction via `db`) so `_cronTick` and the boot-time cron reconciler — both of\n * which write with fully-qualified table names outside a normal namespaced call — can reuse the\n * exact same logic rather than a parallel reimplementation drifting out of sync.\n */\nexport async function enqueueInternal(\n db: GuestDatabaseWriter,\n now: () => number,\n tables: EnqueueTables,\n fnRef: FnRef,\n args: JSONValue,\n opts: EnqueueOpts,\n /**\n * Resolves the target's real kind for the job's `kind` column. Defaults to always-\"mutation\"\n * — correct for every caller EXCEPT `schedulerContext`'s facade methods below, which pass a\n * closure over `cctx.functionKind` (the only caller whose target might actually be an action;\n * `_cronTick`'s cadence/work jobs and `fireOnComplete`'s callback are always mutations).\n */\n kindOf: (fnPath: string) => \"mutation\" | \"action\" = () => \"mutation\",\n): Promise<string> {\n const fnPath = getFunctionPath(fnRef);\n\n if (opts.idempotencyKey !== undefined) {\n const existing = await db.query(tables.jobs, \"by_idempotency\").eq(\"idempotencyKey\", opts.idempotencyKey).take(1).collect();\n const found = existing[0];\n if (found) return found._id as string;\n }\n\n const parentId = currentJobId();\n // Born-canceled: if the ambient parent job is already `canceled`, its children should never\n // get a chance to run either — see the Task 4 design note above `JobState` for why\n // `currentJobId()` is currently always `undefined` (making this branch unreachable today, but\n // wired and correct for whenever a future slice sets the ambient for real).\n let parentCanceled = false;\n if (parentId !== undefined) {\n const parent = await db.get(parentId);\n parentCanceled = parent !== null && parent.state === \"canceled\";\n }\n const bornState: JobState = parentCanceled ? \"canceled\" : \"pending\";\n const kind = kindOf(fnPath);\n // Due time: absolute `runAt` wins; else relative `runAfter` (clamped to non-negative, matching\n // the facade's own `runAfter` method); else immediately due.\n const nextTs = opts.runAt ?? (opts.runAfter !== undefined ? now() + Math.max(0, opts.runAfter) : now());\n const jobId = await db.insert(\n tables.jobs,\n compact({\n fnPath,\n kind,\n state: bornState,\n nextTs,\n attempts: 0,\n // Retry budget. Mutations are transactional (a failed one committed nothing), so retrying is\n // always safe: default 4. Actions have arbitrary external side effects, so a CLEANLY-failed\n // action (its own code threw after possibly running partway) must NOT be blindly re-run:\n // default 1, i.e. dead-letter on the first failure (`_complete`'s `attempts >= maxFailures`\n // check). An explicit `opts.retry` is the author's opt-in that the target is safe to re-run\n // (e.g. `@helipod/workflow` threading a step's declared `maxAttempts` through here) and is\n // honored as-is for both kinds.\n maxFailures: opts.retry?.maxFailures ?? (kind === \"action\" ? 1 : 4),\n leaseHolder: undefined,\n leaseExpiresAt: undefined,\n idempotencyKey: opts.idempotencyKey,\n appVersion: currentAppVersion(),\n name: opts.name,\n hasArgs: true,\n onComplete: opts.onComplete,\n parentId,\n completedTs: parentCanceled ? now() : undefined,\n }),\n );\n await db.insert(tables.jobArgs, compact({ jobId, args, context: opts.context }));\n return jobId;\n}\n\n/** Bare (namespaced-by-the-executor) table names — what `schedulerContext`'s facade writes through. */\nconst FACADE_TABLES: EnqueueTables = { jobs: \"jobs\", jobArgs: \"job_args\" };\n\n/**\n * The terminal outcome an `onComplete` callback is invoked with — a strict superset of\n * `./modules.ts`'s `JobResult` (which only ever carries `success`/`failed`, the two outcomes\n * `_complete` itself produces) plus `canceled`, produced by `cancel()` below. Defined here (not\n * `./modules.ts`) so `./modules.ts` — which already imports from this file — can reuse it without\n * a circular import back the other way.\n */\nexport type OnCompleteResult = { kind: \"success\"; value: unknown } | { kind: \"failed\"; error: string } | { kind: \"canceled\" };\n\n/**\n * Task 6 — the workflow-ready `onComplete`/`context` round-trip: if `job.onComplete` (a mutation\n * path) is set, enqueue it with `{ jobId, context, result }`, where `context` is `job_args`'s\n * `context` for THIS job read back verbatim (opaque to the scheduler — never interpreted, just\n * round-tripped) and `result` is the terminal outcome. Enqueued via `runAt: now()` (the `runAfter:\n * 0` semantics) so it's immediately due — the reactive wake picks it up on the very next\n * commit-driven iteration, no extra latency beyond one dispatch pass.\n *\n * A no-op when `onComplete` is `undefined` (the common case — most jobs don't register a\n * completion callback). Called from exactly two terminal transitions, never a retry:\n * - `./modules.ts`'s `_complete`, on `state:\"success\"` and on dead-lettered `state:\"failed\"`\n * (NOT on the back-to-`\"pending\"` retry branch — the job isn't actually done yet).\n * - `cancel()` below, for both the directly-canceled job and any cascaded-canceled descendant.\n */\nexport async function fireOnComplete(\n db: GuestDatabaseWriter,\n now: () => number,\n tables: EnqueueTables,\n jobId: string,\n onComplete: string | undefined,\n result: OnCompleteResult,\n): Promise<void> {\n if (onComplete === undefined) return;\n const argRows = await db.query(tables.jobArgs, \"by_job\").eq(\"jobId\", jobId).take(1).collect();\n const context = argRows[0]?.context as JSONValue | undefined;\n await enqueueInternal(db, now, tables, onComplete, compact({ jobId, context, result }) as unknown as JSONValue, { runAt: now() });\n}\n\n/**\n * Builds `ctx.scheduler`. Requires `contextWrite: true` on the component definition (see\n * `defineScheduler` in `./index.ts`) — without it, `cctx.db` is a read-only `GuestDatabaseReader`\n * and every write below throws `ForbiddenOperationError`. With it, and ONLY during a mutation\n * call, `cctx.db` is a `GuestDatabaseWriter` scoped to the calling mutation's own transaction —\n * so `runAfter`/`cancel` are transactional: they commit (or roll back) with the rest of the\n * calling mutation, and the resulting write fans out to reactive subscriptions like any other\n * write once it commits.\n */\nexport function schedulerContext(cctx: ComponentContext): SchedulerContext {\n const db = cctx.db as GuestDatabaseWriter;\n const now = (): number => cctx.now;\n\n // Resolves the target's REAL registered kind via the runtime-supplied `cctx.functionKind` (see\n // its doc comment in `packages/executor/src/executor.ts`) — no longer a stub. Actions are the\n // only non-transactional kind → the only one `_reclaim` (`./modules.ts`) must not blind-retry.\n // A query is never a valid schedule target; treat anything not \"action\" as \"mutation\"\n // (conservative: retryable). Unknown path (resolver absent, or the runtime didn't recognize the\n // path) also defaults \"mutation\" — matches prior behavior; an unknown fn fails at dispatch\n // regardless of what `kind` its job row was born with.\n const kindOf = (fnPath: string): \"mutation\" | \"action\" => (cctx.functionKind?.(fnPath) === \"action\" ? \"action\" : \"mutation\");\n\n return {\n async runAfter(delayMs, fnRef, args) {\n return enqueueInternal(db, now, FACADE_TABLES, fnRef, args, { runAt: now() + Math.max(0, delayMs) }, kindOf);\n },\n async runAt(ts, fnRef, args) {\n return enqueueInternal(db, now, FACADE_TABLES, fnRef, args, { runAt: ts instanceof Date ? ts.getTime() : ts }, kindOf);\n },\n async cancel(id) {\n const job = await db.get(id);\n if (job !== null && job.state === \"pending\") {\n await db.replace(id, { ...job, state: \"canceled\" as JobState, completedTs: now() });\n await fireOnComplete(db, now, FACADE_TABLES, id, job.onComplete as string | undefined, { kind: \"canceled\" });\n }\n // Cascading cancel: walk `id`'s descendants via the `by_parent` index and cancel any that\n // are still `pending` — regardless of whether `id` itself was cancelable above (it may\n // already be `inProgress`/terminal; canceling in-flight children of an already-finished or\n // still-running job is still correct — Task 4 doesn't preempt an in-flight job, but its\n // not-yet-dispatched children shouldn't run just because their ancestor already moved on).\n // Iterative BFS (a queue, not recursion) so a deep chain can't blow the stack; `seen` guards\n // against revisiting a node (defensive against a cyclic `parentId`, which shouldn't exist,\n // but costs nothing to guard).\n const queue: string[] = [id];\n const seen = new Set<string>([id]);\n while (queue.length > 0) {\n const parentId = queue.shift() as string;\n const children = await db.query(\"jobs\", \"by_parent\").eq(\"parentId\", parentId).collect();\n for (const child of children) {\n const childId = child._id as string;\n if (seen.has(childId)) continue;\n seen.add(childId);\n if (child.state === \"pending\") {\n await db.replace(childId, { ...child, state: \"canceled\" as JobState, completedTs: now() });\n await fireOnComplete(db, now, FACADE_TABLES, childId, child.onComplete as string | undefined, { kind: \"canceled\" });\n }\n queue.push(childId); // walk deeper regardless of this child's own state\n }\n }\n },\n async enqueue(fnRef, args, opts) {\n return enqueueInternal(db, now, FACADE_TABLES, fnRef, args, opts ?? {}, kindOf);\n },\n };\n}\n\n/**\n * The action-mode counterpart to `SchedulerContext` — same `runAfter`/`runAt`/`cancel` method\n * signatures (so a function body scheduling work is portable between a mutation and an action),\n * minus `enqueue` (the general opts-carrying path stays a mutation-only internal primitive; no\n * action caller needs it yet). Deliberately NOT structurally assignable to `SchedulerContext` as a\n * type (no `enqueue`) even though the three shared methods match exactly.\n */\nexport interface SchedulerActionContext {\n runAfter(delayMs: number, fnRef: FnRef, args: JSONValue): Promise<string>;\n runAt(ts: number | Date, fnRef: FnRef, args: JSONValue): Promise<string>;\n cancel(id: string): Promise<void>;\n}\n\n/**\n * Builds the action-mode `ctx.scheduler` — wired as `defineScheduler()`'s `buildAction` (see\n * `./index.ts`). An action has no `db`, so `runAfter`/`runAt`/`cancel` can't write a `jobs` row\n * directly the way `schedulerContext` above does; instead each delegates to `api.runMutation` of\n * the internal `scheduler:_enqueue`/`scheduler:_cancel` mutations (`./modules.ts`), which run the\n * SAME `enqueueInternal`/`cancel` logic inside their own fresh top-level transaction.\n *\n * `Date.now()` below (converting `runAfter`'s relative `delayMs` to an absolute `runAtMs`) is fine\n * here even though queries/mutations must stay deterministic: an action is non-deterministic by\n * design (see `ActionCtx`'s doc comment in `@helipod/executor`), and the scheduler never\n * recomputes anything from this timestamp — `scheduler:_enqueue` just stores it as `nextTs`.\n */\nexport function schedulerActionContext(api: ActionApi): SchedulerActionContext {\n return {\n async runAfter(delayMs, fnRef, args) {\n return api.runMutation<string>(\"scheduler:_enqueue\", { fnPath: getFunctionPath(fnRef), args, runAtMs: Date.now() + delayMs });\n },\n async runAt(ts, fnRef, args) {\n return api.runMutation<string>(\"scheduler:_enqueue\", {\n fnPath: getFunctionPath(fnRef),\n args,\n runAtMs: ts instanceof Date ? ts.getTime() : ts,\n });\n },\n async cancel(id) {\n await api.runMutation(\"scheduler:_cancel\", { id });\n },\n };\n}\n","import { query, mutation } from \"@helipod/executor\";\nimport type { QueryCtx, MutationCtx } from \"@helipod/executor\";\nimport type { JSONValue } from \"@helipod/values\";\nimport type { JobState } from \"./facade\";\nimport { fireOnComplete, type EnqueueTables } from \"./facade\";\nimport { computeBackoff } from \"./backoff\";\nimport { computeNextRun, computePrevRun, enqueueCadenceJob, type CronSpec, type CatchUpPolicy } from \"./crons\";\n\n/**\n * Internal modules for `@helipod/scheduler` — registered on `defineScheduler()`'s `modules` map\n * (so they're reachable as `scheduler:_peekDue` / `scheduler:_claim` / `scheduler:_complete`),\n * consumed ONLY by the Task 3 driver loop (`./driver.ts`) via `DriverContext.runFunction`, which\n * always calls privileged (`runtime-embedded/src/runtime.ts`'s `driverCtx.runFunction` sets\n * `privileged: true`). Privileged calls bypass namespace prefixing entirely (`kernel.ts`'s\n * `requireTable`), so — unlike `facade.ts`, which runs namespaced and uses bare table names\n * (`\"jobs\"`, `\"job_args\"`) — these modules must use the fully-qualified names\n * (`\"scheduler/jobs\"`, `\"scheduler/job_args\"`).\n *\n * `_peekDue`/`_claim`/`_complete` are internal by convention (the `_` prefix + being paired only\n * with the driver), not by enforced access control — see Task 3's research notes. That's an\n * accepted gap for this slice (nothing else in the codebase enforces \"driver-only\" beyond\n * `_system:*`/`_admin:*`'s separate privileged registries).\n */\n\n/** Cap on how many due jobs a single `_peekDue` batch returns, so one loop iteration can't run unbounded. */\nexport const BATCH_CAP = 64;\n\n/**\n * Hard ceiling on how many missed occurrences `_cronTick`'s `catchUp:\"fireAll\"` path will\n * materialize (and fire a work job for) in a single tick — see `_cronTick`'s doc comment. Without\n * this, a `\"fireAll\"` cron down for a long time on a fast interval could try to enqueue an\n * unbounded number of work jobs synchronously inside one mutation. Occurrences beyond the cap are\n * discarded (logged), not deferred to a later tick — the cron's cadence always re-anchors past\n * the ENTIRE true backlog on the SAME tick, regardless of how much of it actually got fired.\n */\nexport const CATCHUP_CAP = 1000;\n\n/** How long a claim's lease is valid before it could be reclaimed by the sweep below. */\nexport const LEASE_MS = 30_000;\n\n/**\n * The driver's ONLY periodic timer: how often `scheduler:_reclaim` runs to sweep `inProgress`\n * jobs whose lease has expired (an infra kill mid-run — the process that claimed the job died\n * before completing it). Normal dispatch stays fully reactive/event-driven; this is a backstop.\n */\nexport const SWEEP_MS = 30_000;\n\n/** Drop `undefined`-valued keys before a `db.replace` (the wire codec rejects `undefined`; omit rather than null it out). Mirrors `facade.ts`'s `compact`. */\nfunction compact<T extends Record<string, unknown>>(obj: T): { [K in keyof T]: Exclude<T[K], undefined> } {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) if (v !== undefined) out[k] = v;\n return out as { [K in keyof T]: Exclude<T[K], undefined> };\n}\n\nexport interface DueJob {\n _id: string;\n fnPath: string;\n kind: \"mutation\" | \"action\";\n state: JobState;\n nextTs: number;\n [key: string]: unknown;\n}\n\nexport interface PeekDueResult {\n due: DueJob[];\n earliestFutureTs: number | null;\n}\n\n/**\n * `scheduler:_peekDue` — a QUERY (snapshot read, no writes). Scans `jobs`' `by_next_ts` index\n * (`[\"state\", \"nextTs\"]`) for `state:\"pending\"`: `due` = rows with `nextTs <= now` (ascending,\n * capped at `BATCH_CAP`), `earliestFutureTs` = the smallest `nextTs > now` among the rest — so the\n * driver can re-arm its wake timer precisely instead of polling.\n */\nexport const _peekDue = query(async (ctx: QueryCtx): Promise<PeekDueResult> => {\n const now = ctx.now();\n const due = await ctx.db\n .query(\"scheduler/jobs\", \"by_next_ts\")\n .eq(\"state\", \"pending\")\n .lte(\"nextTs\", now)\n .order(\"asc\")\n .take(BATCH_CAP)\n .collect();\n const future = await ctx.db\n .query(\"scheduler/jobs\", \"by_next_ts\")\n .eq(\"state\", \"pending\")\n .gt(\"nextTs\", now)\n .order(\"asc\")\n .take(1)\n .collect();\n const next = future[0];\n return {\n due: due as unknown as DueJob[],\n earliestFutureTs: next ? (next.nextTs as number) : null,\n };\n});\n\nexport interface ClaimResult {\n jobId: string;\n fnPath: string;\n kind: \"mutation\" | \"action\";\n args: JSONValue;\n context: JSONValue | undefined;\n onComplete: string | undefined;\n}\n\n/**\n * `scheduler:_claim` — a MUTATION: re-reads the job by id and transitions `pending → inProgress`\n * ONLY if it is still exactly `state:\"pending\"` (a snapshot-read + exact-match guard). Returns\n * `null` if the job is missing or was already claimed/canceled by someone else — the caller\n * (the driver loop) skips it. The single-writer OCC transactor serializes concurrent `_claim`\n * calls on the same job, so this check is the AUTHORITATIVE double-run guard: at most one caller\n * ever observes `state===\"pending\"` for a given job.\n */\nexport const _claim = mutation(async (ctx: MutationCtx, args: { jobId: string }): Promise<ClaimResult | null> => {\n const job = await ctx.db.get(args.jobId);\n if (job === null || job.state !== \"pending\") return null; // gone, or already claimed — lost the race\n const now = ctx.now();\n await ctx.db.replace(args.jobId, {\n ...job,\n state: \"inProgress\" as JobState,\n leaseHolder: \"driver\",\n leaseExpiresAt: now + LEASE_MS,\n });\n const argRows = await ctx.db.query(\"scheduler/job_args\", \"by_job\").eq(\"jobId\", args.jobId).take(1).collect();\n const argRow = argRows[0];\n return {\n jobId: args.jobId,\n fnPath: job.fnPath as string,\n kind: job.kind as \"mutation\" | \"action\",\n args: (argRow?.args ?? null) as JSONValue,\n context: argRow?.context as JSONValue | undefined,\n onComplete: job.onComplete as string | undefined,\n };\n});\n\nexport type JobResult =\n | { kind: \"success\"; value: unknown }\n | { kind: \"failed\"; error: string; retryable?: boolean };\n\n/**\n * `scheduler:_complete` — a MUTATION: finalizes a claimed job.\n *\n * - `result.kind === \"success\"` → terminal `state:\"success\"`, `completedTs`, lease cleared.\n * - `result.kind === \"failed\"` → `attempts += 1`; if `attempts >= maxFailures`, terminal\n * `state:\"failed\"` (dead-letter) + `completedTs` + `lastError`, same as success but with the\n * error recorded. Otherwise, back to `state:\"pending\"` with `nextTs: now() +\n * computeBackoff(attempts, ctx.random)` (exponential backoff, jittered via the mutation's own\n * seeded PRNG — see `./backoff.ts`), lease cleared, `lastError` recorded — the driver's reactive\n * `onCommit` wake picks up the `state` transition and re-arms its timer for the retry.\n *\n * No-ops (returns `null`) if the job vanished or isn't `inProgress` — defensive against a stray\n * double-complete; `_claim`'s guard is what actually prevents double-dispatch.\n */\nexport const _complete = mutation(async (ctx: MutationCtx, args: { jobId: string; result: JobResult }): Promise<null> => {\n const job = await ctx.db.get(args.jobId);\n if (job === null || job.state !== \"inProgress\") return null;\n const now = ctx.now();\n const nowFn = (): number => now;\n\n if (args.result.kind === \"success\") {\n await ctx.db.replace(\n args.jobId,\n compact({\n ...job,\n state: \"success\" as JobState,\n completedTs: now,\n leaseHolder: undefined,\n leaseExpiresAt: undefined,\n }),\n );\n // Task 6 — workflow-ready onComplete/context round-trip: see `fireOnComplete`'s doc comment\n // in `./facade.ts`. A no-op when `job.onComplete` is unset.\n await fireOnComplete(ctx.db, nowFn, CRON_TABLES, args.jobId, job.onComplete as string | undefined, {\n kind: \"success\",\n value: args.result.value,\n });\n return null;\n }\n\n // result.kind === \"failed\" — retry with backoff, or dead-letter at maxFailures.\n // Action safety lives in the job's OWN `maxFailures`, not a kind-check here: `enqueueInternal`\n // (`./facade.ts`) defaults `kind:\"action\"` jobs to `maxFailures: 1`, so a CLEANLY-failed action\n // (its own code threw — its side effects may have partially run) dead-letters on the first\n // failure below rather than blind-retrying, while an explicit `retry: { maxFailures }` (the\n // author's opt-in that the target is safe to re-run, e.g. a workflow step's `maxAttempts`)\n // retries actions through this same backoff path like any mutation.\n const attempts = (job.attempts as number) + 1;\n const maxFailures = job.maxFailures as number;\n const lastError = args.result.error;\n // A non-retryable failure (a deterministic error like `DocumentValidationError`) is dead-lettered\n // immediately — retrying it would just reproduce the same error and burn every attempt. An older\n // result without the flag (or a genuinely transient failure) keeps the existing retry behavior.\n const retryable = args.result.retryable ?? true;\n\n if (attempts >= maxFailures || !retryable) {\n await ctx.db.replace(\n args.jobId,\n compact({\n ...job,\n state: \"failed\" as JobState,\n attempts,\n completedTs: now,\n lastError,\n leaseHolder: undefined,\n leaseExpiresAt: undefined,\n }),\n );\n // Task 6 — dead-lettered (terminal) failure fires onComplete too, same as success; the\n // back-to-\"pending\" retry branch below does NOT (the job isn't actually done yet).\n await fireOnComplete(ctx.db, nowFn, CRON_TABLES, args.jobId, job.onComplete as string | undefined, {\n kind: \"failed\",\n error: lastError,\n });\n return null;\n }\n\n const nextTs = now + computeBackoff(attempts, ctx.random);\n await ctx.db.replace(\n args.jobId,\n compact({\n ...job,\n state: \"pending\" as JobState,\n attempts,\n nextTs,\n lastError,\n leaseHolder: undefined,\n leaseExpiresAt: undefined,\n }),\n );\n return null;\n});\n\n/** Fully-qualified table names — see this file's module doc comment for why (`_cronTick` runs privileged, dispatched by the driver like any other due job). */\nconst CRON_TABLES: EnqueueTables = { jobs: \"scheduler/jobs\", jobArgs: \"scheduler/job_args\" };\n\n/**\n * `scheduler:_enqueue` / `scheduler:_cancel` — internal (`_`-prefixed, so not client-callable)\n * MUTATIONS that back the action-mode `ctx.scheduler` facade (`schedulerActionContext` in\n * `./facade.ts`): an action has no `db`, so it can't write a `jobs` row itself — instead it calls\n * `ctx.runMutation(\"scheduler:_enqueue\"/\"_cancel\", ...)`, a fresh top-level mutation the trusted\n * `invoke` seam resolves (see `ExecutorDeps.invoke`'s doc comment in `packages/executor/src/\n * executor.ts` — it resolves ANY registered path, `_`-prefixed included, unlike the public\n * `runtime.run`/`runAction`, which block `_`).\n *\n * Both run namespaced (NOT privileged) — `namespaceForPath(\"scheduler:_enqueue\", ...)` resolves to\n * `\"scheduler\"`, the same namespace `schedulerContext`'s facade runs in — so `ctx.scheduler` here\n * is the ordinary in-txn facade from `./facade.ts`, writing through the SAME bare table names\n * (`FACADE_TABLES`) as a normal mutation's `ctx.scheduler.runAfter(...)` call. `ctx: any` because\n * `ctx.scheduler` isn't part of the exported `MutationCtx` shape (it's a dynamic per-component\n * facade attached at run time — see `InlineUdfExecutor.run`'s `guestCtx` loop).\n */\nexport const _enqueue = mutation(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async (ctx: any, a: { fnPath: string; args: JSONValue; runAtMs: number }): Promise<string> =>\n ctx.scheduler.runAt(a.runAtMs, a.fnPath, a.args),\n);\n\nexport const _cancel = mutation(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async (ctx: any, a: { id: string }): Promise<null> => {\n await ctx.scheduler.cancel(a.id);\n return null;\n },\n);\n\n/**\n * `scheduler:_cronTick` — a MUTATION: the dual-job cron cadence. Registered as an ordinary\n * `jobs` row itself (`fnPath:\"scheduler:_cronTick\"`, `kind:\"mutation\"`) — the driver dispatches\n * it through the exact same `_peekDue`/`_claim`/`_complete` path as any other due job (see\n * `driver.ts`'s `runPass`), which is what makes it decoupled: a slow/failing WORK job (enqueued\n * below) is a completely separate `jobs` row with its own lease/retry lifecycle, so it can never\n * block or delay this cadence job's own on-time reschedule.\n *\n * Per fire:\n * 1. Read the `crons` row by `name` (via `by_name`) — a no-op if it's gone (the cron was\n * deregistered by boot's `reconcileCrons` since this cadence job was scheduled; see\n * `./crons.ts`).\n * 2. Self-terminate check (duplicate-cadence-chain fix, belt-and-braces half): if `args.jobId`\n * is present and doesn't match `cron.cadenceJobId`, this invocation is a STALE duplicate\n * chain (one that slipped past `reconcileCrons`'s `hasLiveCadence` check some other way —\n * see that function's doc comment in `./crons.ts`) — return immediately without firing\n * anything or rescheduling, so at most one chain survives past its own next tick.\n * 3. `anchor = lastScheduledTs` — CLOCK-ANCHORED, never `now()`: every occurrence this tick\n * computes chains off the cron's own last-fired timestamp, not off whenever this mutation\n * happens to run. A late dispatch (a busy driver, a slow prior tick) never shifts the phase\n * of later occurrences.\n * 4. **Bounded catch-up** (unbounded-loop fix): unlike the old implementation, this NEVER steps\n * one occurrence at a time through however much backlog has accumulated — a fast interval\n * cron down for months could mean hundreds of millions of occurrences, and computing (let\n * alone materializing) that many synchronously inside one mutation would block the\n * single-writer transactor for the duration, even for `\"skip\"` whose entire point is to\n * discard the backlog. Instead:\n * - **Interval specs**: the occurrence count `n` since `anchor` is computed by O(1)\n * arithmetic (`Math.floor((now-anchor)/period)`), never a loop.\n * - **Cron-expression specs**: whether zero/one/many occurrences are due is determined by\n * at most two O(1) `cron-parser` calls (`computeNextRun` from `anchor`, then again from\n * that result) — no stepping through the backlog to count it.\n * - `\"skip\"` and `\"fireOnce\"` NEVER materialize a backlog array at all, for either spec\n * kind — `\"fireOnce\"`'s single catch-up fire and every kind's `next` reschedule point are\n * each an O(1) computation (`computePrevRun`/arithmetic for the most recent missed\n * occurrence, `computeNextRun`/arithmetic for the next future one).\n * - `\"fireAll\"` is the only policy that genuinely needs a list of occurrences to fire, and\n * its materialization loop is hard-capped at `CATCHUP_CAP` — occurrences beyond the cap\n * are discarded (logged), not deferred to a later tick.\n * In every case, `lastScheduledTs` (see step 6) re-anchors to the schedule's TRUE last\n * occurrence `<= now`, computed in O(1) regardless of catchUp policy — so even `\"skip\"` or a\n * capped `\"fireAll\"` never drifts the cron's future phase, only the discarded/skipped\n * occurrences themselves are lost.\n * 5. Each fired occurrence gets its OWN work job via `enqueueInternal`, keyed\n * `idempotencyKey: \"${cronName}:${fireTs}\"` — insert-or-noop, so two cadence fires that ever\n * computed the same occurrence (shouldn't happen in normal operation, but is the deterministic\n * safety net if it did) collapse into one work job rather than double-running it.\n * 6. The cadence reschedules ITSELF at `next` (the first occurrence strictly after `now`) via\n * `enqueueCadenceJob` (which embeds the new job's own id into its args — see that function's\n * doc comment in `./crons.ts` — powering step 2's self-terminate check on ITS next tick).\n */\nexport const _cronTick = mutation(async (ctx: MutationCtx, args: { cronName: string; jobId?: string }): Promise<null> => {\n const rows = await ctx.db.query(\"scheduler/crons\", \"by_name\").eq(\"name\", args.cronName).take(1).collect();\n const cron = rows[0];\n if (cron === undefined) return null; // deregistered since this cadence job was scheduled — stop, don't reschedule\n\n // Belt-and-braces convergence: `args.jobId` is absent only for cadence jobs enqueued before\n // this field existed (pre-fix rows mid-upgrade) — those fall back to trusting themselves,\n // matching pre-fix behavior exactly. Otherwise, a mismatch means this is a stale duplicate\n // chain; die quietly without touching the cron row or firing anything.\n if (args.jobId !== undefined && cron.cadenceJobId !== undefined && cron.cadenceJobId !== args.jobId) {\n return null;\n }\n\n const now = ctx.now();\n const spec = JSON.parse(cron.spec as string) as CronSpec;\n const tz = cron.tz as string;\n const catchUp = cron.catchUp as CatchUpPolicy;\n const anchor = (cron.lastScheduledTs as number | undefined) ?? now;\n const nowFn = (): number => ctx.now();\n\n // See step 4 above — every branch below is O(1) arithmetic / a small constant number of\n // `cron-parser` calls, except `\"fireAll\"`'s materialization loop, explicitly capped at\n // `CATCHUP_CAP`. Nothing here scales with how large the actual backlog is.\n let toFire: number[];\n let next: number; // first occurrence strictly after `now` — where the cadence reschedules\n let newLastScheduledTs: number; // re-anchor point for the NEXT tick\n\n if (spec.kind === \"interval\") {\n const period = spec.ms;\n const elapsed = now - anchor;\n // How many occurrences have elapsed (<= now) since `anchor` — computed directly, never by\n // stepping, so this is cheap even when `n` is in the hundreds of millions. (`anchor + i*period\n // <= now` iff `i <= elapsed/period`, so the count of such `i >= 1` is `floor(elapsed/period)`.)\n const n = elapsed >= 0 ? Math.floor(elapsed / period) : 0;\n\n if (n === 0) {\n // Nothing due yet (defensive: the driver only dispatches this job once its own `nextTs` is\n // <= now, so this shouldn't normally happen — but a clock oddity shouldn't crash it).\n toFire = [];\n next = anchor + period;\n newLastScheduledTs = anchor;\n } else if (n === 1) {\n // On-time (or first-ever) single fire — always happens, independent of `catchUp`.\n const only = anchor + period;\n toFire = [only];\n next = only + period;\n newLastScheduledTs = only;\n } else {\n // A backlog of `n` missed occurrences, `n` known exactly via O(1) arithmetic.\n const lastOccurrence = anchor + n * period; // the TRUE last occurrence <= now\n next = lastOccurrence + period; // strictly after `now`, by construction of `n`\n newLastScheduledTs = lastOccurrence; // re-anchor to the real schedule regardless of `catchUp`\n\n if (catchUp === \"fireAll\") {\n // The only policy that genuinely needs a materialized list. Hard-capped at\n // `CATCHUP_CAP` so even a multi-hundred-million-occurrence backlog does bounded work —\n // fires the OLDEST `min(n, CATCHUP_CAP)` occurrences; anything beyond the cap is\n // discarded for good (`newLastScheduledTs` above already re-anchors past the ENTIRE true\n // backlog, not just the fired subset).\n const fireCount = Math.min(n, CATCHUP_CAP);\n toFire = [];\n for (let i = 0; i < fireCount; i++) toFire.push(anchor + (i + 1) * period);\n if (n > CATCHUP_CAP) {\n console.warn(\n `[scheduler] cron \"${cron.name as string}\": fireAll backlog of ${n} occurrences exceeded CATCHUP_CAP (${CATCHUP_CAP}) — fired the oldest ${CATCHUP_CAP}, discarded the rest.`,\n );\n }\n } else if (catchUp === \"fireOnce\") {\n toFire = [lastOccurrence];\n } else {\n toFire = []; // \"skip\" (default) — jump straight past the backlog, fire nothing for it\n }\n }\n } else {\n // spec.kind === \"cron\". `computeNextRun`/`computePrevRun` are each a single O(1) cron-parser\n // call (field arithmetic, not calendar stepping), so every branch below stays bounded\n // regardless of backlog length EXCEPT `\"fireAll\"`'s materialization loop (capped below).\n const firstAfterAnchor = computeNextRun(spec, tz, anchor);\n\n if (firstAfterAnchor > now) {\n // Nothing due yet.\n toFire = [];\n next = firstAfterAnchor;\n newLastScheduledTs = anchor;\n } else {\n const secondAfterAnchor = computeNextRun(spec, tz, firstAfterAnchor);\n if (secondAfterAnchor > now) {\n // Exactly one occurrence due — on-time (or first-ever) single fire.\n toFire = [firstAfterAnchor];\n next = secondAfterAnchor;\n newLastScheduledTs = firstAfterAnchor;\n } else {\n // A backlog of 2+ occurrences. Unlike interval specs, the EXACT count isn't cheap to\n // know for an arbitrary cron expression without stepping through it — so `\"skip\"` and\n // `\"fireOnce\"` never do that: both get everything they need from two more O(1) calls\n // (the real next occurrence, and the single most recent missed one). Only `\"fireAll\"`\n // steps at all, and that step is hard-capped at `CATCHUP_CAP` iterations.\n next = computeNextRun(spec, tz, now); // first real occurrence strictly after `now`\n const lastOccurrence = computePrevRun(spec, tz, now); // most recent missed occurrence\n newLastScheduledTs = lastOccurrence; // re-anchor to the real schedule regardless of `catchUp`\n\n if (catchUp === \"fireAll\") {\n toFire = [];\n let cursor = firstAfterAnchor;\n let truncated = false;\n while (cursor <= now) {\n if (toFire.length >= CATCHUP_CAP) {\n truncated = true;\n break;\n }\n toFire.push(cursor);\n cursor = computeNextRun(spec, tz, cursor);\n }\n if (truncated) {\n console.warn(\n `[scheduler] cron \"${cron.name as string}\": fireAll backlog exceeded CATCHUP_CAP (${CATCHUP_CAP}) — fired the oldest ${CATCHUP_CAP}, discarded the rest.`,\n );\n }\n } else if (catchUp === \"fireOnce\") {\n toFire = [lastOccurrence];\n } else {\n // \"skip\" (default) — the exact missed count isn't tracked for cron-expression specs\n // (see this branch's comment above); a log marker stands in for it.\n toFire = [];\n console.warn(\n `[scheduler] cron \"${cron.name as string}\": skipped a downtime backlog (catchUp:\"skip\") — exact occurrence count not tracked for cron-expression specs.`,\n );\n }\n }\n }\n }\n\n // Task 3c: route the WORK-job enqueue through `ctx.scheduler` (the scheduler's own\n // `schedulerContext` facade, `./facade.ts`) rather than calling `enqueueInternal` directly as\n // this loop used to. `_cronTick` is a mutation, so — exactly like `_enqueue`/`_cancel` above —\n // the component's `context` provider is attached to its `ctx` regardless of the privileged\n // dispatch (`InlineUdfExecutor.run` builds every context-provider facade unconditionally; see\n // `packages/executor/src/executor.ts`'s `guestCtx` loop). That facade's `enqueue` closes over\n // `cctx.functionKind` and resolves the target's REAL registered kind (facade.ts:233), so a work\n // job whose `cron.workFnPath` is a registered action is correctly tagged `kind:\"action\"` instead\n // of unconditionally defaulting to `\"mutation\"` the way a bare `enqueueInternal(...)` call (no\n // `kindOf` arg) always did. VERIFIED equivalent to the old direct call: the facade's `db` runs\n // namespaced under `namespace:\"scheduler\"` (not privileged), so its bare `\"jobs\"`/`\"job_args\"`\n // table names resolve (via `getFullTableName`) to the exact same `\"scheduler/jobs\"`/\n // `\"scheduler/job_args\"` physical tables `CRON_TABLES` names explicitly — and `EnqueueOpts`\n // covers every opt this loop passes (`runAt`, `idempotencyKey`, `name`); it drops nothing.\n // `ctx: any` mirrors `_enqueue`/`_cancel` below — `ctx.scheduler` isn't part of the exported\n // `MutationCtx` shape (it's a dynamic per-component facade attached at run time).\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const schedulerFacade = (ctx as any).scheduler as { enqueue(fnPath: string, args: JSONValue, opts: Record<string, unknown>): Promise<string> };\n for (const fireTs of toFire) {\n await schedulerFacade.enqueue(cron.workFnPath as string, cron.workArgs as JSONValue, {\n runAt: fireTs,\n idempotencyKey: `${cron.name as string}:${fireTs}`,\n name: cron.name as string,\n });\n }\n\n const cadenceJobId = await enqueueCadenceJob(ctx.db, nowFn, CRON_TABLES, cron.name as string, next);\n\n await ctx.db.replace(\n cron._id as string,\n compact({\n ...cron,\n lastScheduledTs: newLastScheduledTs,\n cadenceJobId,\n }),\n );\n return null;\n});\n\n/**\n * `scheduler:_reclaim` — a MUTATION: the driver's safety-sweep backstop for infra kills. Scans\n * `inProgress` jobs whose lease has expired (`leaseExpiresAt < now` — the process that `_claim`ed\n * them died, or is at least still holding a lease well past its promised deadline) and reclaims\n * each:\n * - `kind:\"mutation\"` with attempts left → safe to retry (mutations are deterministic/\n * idempotent-by-replay in this engine's model): `attempts += 1`, back to `state:\"pending\"` with\n * `nextTs: now()` (immediate — no backoff; an infra kill isn't the job's own fault). Bounded by\n * the job's `maxFailures`, the same budget `_complete`'s failed path spends, so a mutation that\n * crash-loops its host process dead-letters once the budget is spent instead of retrying forever.\n * - `kind:\"action\"` (always), or a mutation out of attempts → terminal `state:\"failed\"`\n * (dead-letter) with `lastError`. Actions are never reclaim-retried — even with an explicit\n * `retry` opt-in — because an expired lease can't tell us whether the action's side effects\n * already ran. Fires `onComplete` (if set) with a `\"failed\"` result — same as `_complete`'s\n * dead-letter branch — so a caller relying on the onComplete round-trip (e.g.\n * `@helipod/workflow`'s `_stepDone`) actually observes the terminal failure instead of\n * hanging forever waiting for a callback that never fires.\n *\n * Uses the `by_next_ts` index (`[\"state\",\"nextTs\"]`) to scan `state:\"inProgress\"` cheaply, then a\n * post-filter on `leaseExpiresAt` (not part of that index) — `inProgress` job counts are expected\n * to be small (bounded by in-flight concurrency), so this is capped at `BATCH_CAP` per sweep\n * rather than truly unbounded, consistent with `_peekDue`.\n */\nexport const _reclaim = mutation(async (ctx: MutationCtx): Promise<{ reclaimed: number }> => {\n const now = ctx.now();\n const nowFn = (): number => now;\n const expired = await ctx.db\n .query(\"scheduler/jobs\", \"by_next_ts\")\n .eq(\"state\", \"inProgress\")\n .where(\"lt\", \"leaseExpiresAt\", now)\n .take(BATCH_CAP)\n .collect();\n\n let reclaimed = 0;\n for (const job of expired) {\n const jobId = job._id as string;\n const attempts = (job.attempts as number) + 1;\n const lastError = \"lease expired: driver did not complete the job before its lease deadline (infra kill)\";\n if (job.kind === \"mutation\" && attempts < (job.maxFailures as number)) {\n // Reclaim-retry, bounded by the same `maxFailures` budget `_complete`'s failed path uses: a\n // mutation that reliably CRASHES the process it's claimed on (rather than throwing, which\n // `_complete` would catch) burns one attempt per reclaim and dead-letters below once the\n // budget is spent, instead of being re-dispatched forever.\n await ctx.db.replace(\n jobId,\n compact({\n ...job,\n state: \"pending\" as JobState,\n attempts,\n nextTs: now, // immediate — the delay was the crash, not the job's own backoff\n lastError,\n leaseHolder: undefined,\n leaseExpiresAt: undefined,\n }),\n );\n } else {\n // Dead-letter. Two ways here: a mutation whose reclaim budget is spent (crash-loop, above),\n // or `kind:\"action\"` — always at-most-once on an expired lease, even with an explicit\n // `retry` opt-in: the lease expiring means we can't tell whether the action's side effects\n // already ran (unlike a clean failure, where the author's opt-in covers the re-run), so\n // retrying could double-run them.\n await ctx.db.replace(\n jobId,\n compact({\n ...job,\n state: \"failed\" as JobState,\n attempts,\n completedTs: now,\n lastError,\n leaseHolder: undefined,\n leaseExpiresAt: undefined,\n }),\n );\n // Terminal (dead-lettered) failure fires onComplete, mirroring `_complete`'s dead-letter\n // branch — a no-op when `job.onComplete` is unset.\n await fireOnComplete(ctx.db, nowFn, CRON_TABLES, jobId, job.onComplete as string | undefined, {\n kind: \"failed\",\n error: lastError,\n });\n }\n reclaimed++;\n }\n return { reclaimed };\n});\n","/**\n * Exponential backoff (with jitter) for a scheduler job's Nth failure. Pure and side-effect-free\n * so it's trivially unit-testable and safe to call from a deterministic mutation (`_complete` in\n * `./modules.ts`) — it takes its randomness as an injected `rng`, never touching `Math.random`\n * directly unless the caller passes none (the default, for callers outside a UDF's determinism\n * boundary — there are none in this codebase yet, but this keeps the function usable standalone).\n *\n * `_complete` passes `ctx.random` (the query/mutation guest ctx's seeded PRNG — see\n * `@helipod/executor`'s `seeded-random.ts`), which is what actually gives the retry jitter its\n * determinism-for-replay property: a mutation and its OCC-conflict replay call `ctx.random()` in\n * the same sequence and get the same numbers, and tests get a computable, non-flaky result.\n */\nexport interface BackoffOptions {\n /** Backoff for the first retry (attempts=1), before jitter. */\n initialBackoffMs: number;\n /** Multiplier applied per additional attempt. */\n base: number;\n}\n\nexport const DEFAULT_BACKOFF_OPTIONS: BackoffOptions = { initialBackoffMs: 250, base: 2 };\n\n/**\n * `attempts` is the failure count AFTER this failure is recorded (i.e. call with the\n * already-incremented `attempts`, not the pre-failure count) — so the first retry (attempts=1)\n * backs off `initialBackoffMs * base^2`, jittered to 50–100% of that.\n */\nexport function computeBackoff(\n attempts: number,\n rng: () => number = Math.random,\n o: BackoffOptions = DEFAULT_BACKOFF_OPTIONS,\n): number {\n const raw = o.initialBackoffMs * o.base ** (attempts + 1);\n return Math.round(raw * (0.5 + 0.5 * rng())); // 50–100% jitter\n}\n","/**\n * `cronJobs()` — the declarative recurring-schedule registry, Convex-parity surface. An app's\n * `crons.ts` does:\n *\n * ```ts\n * import { cronJobs } from \"./_generated/server\";\n * const crons = cronJobs();\n * crons.interval(\"cleanup\", { minutes: 5 }, internal.maintenance.purge, {});\n * crons.cron(\"nightly\", \"0 3 * * *\", internal.reports.build, {}, { tz: \"America/New_York\" });\n * crons.daily(\"digest\", { hourUTC: 8, minuteUTC: 0 }, internal.email.digest, {});\n * export default crons;\n * ```\n *\n * `cronJobs()` itself just COLLECTS entries in-memory (`register` below) — nothing is scheduled\n * until `reconcileCrons` (this file) runs as the scheduler component's `boot` step (wired in\n * `./index.ts`), which diffs the registered entries against the `crons` table by `name` and\n * ensures each has a live cadence job. See `reconcileCrons`'s doc comment for the \"how does the\n * app's crons.ts reach the component\" wiring decision.\n *\n * `computeNextRun` wraps `cron-parser` (cron-expression specs, IANA `tz`) and does plain\n * arithmetic (interval specs) — the one function `_cronTick` (`./modules.ts`) calls to advance\n * the cadence, always from the last-fired anchor, never from `now()` (clock-anchored, no drift).\n */\n// `cron-parser` ships CommonJS; a NAMED esm import (`import { parseExpression }`) only works under\n// Bun's lenient interop — native Node ESM can't statically detect a CJS package's named exports and\n// throws `does not provide an export named 'parseExpression'`. Import the default (the CJS\n// module.exports) and read the member off it, which works on Bun and Node alike.\nimport cronParser from \"cron-parser\";\nconst { parseExpression } = cronParser;\nimport type { JSONValue } from \"@helipod/values\";\nimport type { BootContext } from \"@helipod/component\";\nimport { GuestDatabaseWriter } from \"@helipod/executor\";\nimport { getFunctionPath, enqueueInternal, type FnRef, type EnqueueTables } from \"./facade\";\n\nexport type CatchUpPolicy = \"skip\" | \"fireOnce\" | \"fireAll\";\n\n/** The two spec shapes `computeNextRun` understands. Stored on the `crons` row JSON-serialized (`spec: v.string()`). */\nexport type CronSpec = { kind: \"interval\"; ms: number } | { kind: \"cron\"; expr: string };\n\nexport interface CronRegistryEntry {\n name: string;\n spec: CronSpec;\n tz: string;\n catchUp: CatchUpPolicy;\n workFnPath: string;\n workArgs: JSONValue;\n}\n\nexport interface IntervalPeriod {\n seconds?: number;\n minutes?: number;\n hours?: number;\n}\nexport interface DailyAt {\n hourUTC: number;\n minuteUTC: number;\n}\nexport interface HourlyAt {\n minuteUTC: number;\n}\nexport type DayOfWeek = \"sunday\" | \"monday\" | \"tuesday\" | \"wednesday\" | \"thursday\" | \"friday\" | \"saturday\";\nexport interface WeeklyAt {\n dayOfWeek: DayOfWeek;\n hourUTC: number;\n minuteUTC: number;\n}\nexport interface MonthlyAt {\n day: number;\n hourUTC: number;\n minuteUTC: number;\n}\n\n/** `tz`/`catchUp` are additive Helipod extensions (absent on Convex) — see the design spec §5.2. */\nexport interface CronOpts {\n tz?: string;\n catchUp?: CatchUpPolicy;\n}\n/** `.daily`/`.hourly`/`.weekly`/`.monthly` take *UTC fields by name — `tz` isn't accepted (it'd be ambiguous which field it applies to); only `catchUp` is overridable. */\nexport type CronUtcOpts = Pick<CronOpts, \"catchUp\">;\n\nexport interface CronJobs {\n interval(name: string, period: IntervalPeriod, fnRef: FnRef, args: JSONValue, opts?: CronOpts): void;\n cron(name: string, expr: string, fnRef: FnRef, args: JSONValue, opts?: CronOpts): void;\n daily(name: string, at: DailyAt, fnRef: FnRef, args: JSONValue, opts?: CronUtcOpts): void;\n hourly(name: string, at: HourlyAt, fnRef: FnRef, args: JSONValue, opts?: CronUtcOpts): void;\n weekly(name: string, at: WeeklyAt, fnRef: FnRef, args: JSONValue, opts?: CronUtcOpts): void;\n monthly(name: string, at: MonthlyAt, fnRef: FnRef, args: JSONValue, opts?: CronUtcOpts): void;\n /** Boot-reconciliation seam — not part of the public Convex-parity surface. */\n __entries(): CronRegistryEntry[];\n}\n\nconst DEFAULT_CATCH_UP: CatchUpPolicy = \"skip\";\nconst DEFAULT_TZ = \"UTC\";\n\nconst DAY_NUMBERS: Record<DayOfWeek, number> = {\n sunday: 0,\n monday: 1,\n tuesday: 2,\n wednesday: 3,\n thursday: 4,\n friday: 5,\n saturday: 6,\n};\n\n/** `cronJobs()` — see this file's module doc comment for the full `crons.ts` shape. */\nexport function cronJobs(): CronJobs {\n const entries = new Map<string, CronRegistryEntry>();\n\n function register(name: string, spec: CronSpec, tz: string | undefined, catchUp: CatchUpPolicy | undefined, fnRef: FnRef, args: JSONValue): void {\n if (entries.has(name)) throw new Error(`cron \"${name}\" is already registered — cron identifiers must be unique`);\n entries.set(name, {\n name,\n spec,\n tz: tz ?? DEFAULT_TZ,\n catchUp: catchUp ?? DEFAULT_CATCH_UP,\n workFnPath: getFunctionPath(fnRef),\n workArgs: args,\n });\n }\n\n return {\n interval(name, period, fnRef, args, opts) {\n const ms = (period.seconds ?? 0) * 1000 + (period.minutes ?? 0) * 60_000 + (period.hours ?? 0) * 3_600_000;\n if (ms <= 0) throw new Error(`cron \"${name}\": interval must resolve to a positive duration`);\n register(name, { kind: \"interval\", ms }, opts?.tz, opts?.catchUp, fnRef, args);\n },\n cron(name, expr, fnRef, args, opts) {\n register(name, { kind: \"cron\", expr }, opts?.tz, opts?.catchUp, fnRef, args);\n },\n daily(name, at, fnRef, args, opts) {\n register(name, { kind: \"cron\", expr: `${at.minuteUTC} ${at.hourUTC} * * *` }, DEFAULT_TZ, opts?.catchUp, fnRef, args);\n },\n hourly(name, at, fnRef, args, opts) {\n register(name, { kind: \"cron\", expr: `${at.minuteUTC} * * * *` }, DEFAULT_TZ, opts?.catchUp, fnRef, args);\n },\n weekly(name, at, fnRef, args, opts) {\n register(name, { kind: \"cron\", expr: `${at.minuteUTC} ${at.hourUTC} * * ${DAY_NUMBERS[at.dayOfWeek]}` }, DEFAULT_TZ, opts?.catchUp, fnRef, args);\n },\n monthly(name, at, fnRef, args, opts) {\n register(name, { kind: \"cron\", expr: `${at.minuteUTC} ${at.hourUTC} ${at.day} * *` }, DEFAULT_TZ, opts?.catchUp, fnRef, args);\n },\n __entries: () => [...entries.values()],\n };\n}\n\n/**\n * Computes the next fire time strictly AFTER `afterTs`, per `spec`. For `{kind:\"interval\"}`\n * that's plain arithmetic; for `{kind:\"cron\"}` it's `cron-parser`'s `parseExpression(...).next()`\n * against `afterTs` as `currentDate` in the given IANA `tz` — verified to return a date strictly\n * greater than `afterTs` even when `afterTs` itself exactly matches the pattern (i.e. calling\n * this again with the returned value as the new `afterTs` always advances — no infinite loop,\n * no repeat).\n */\nexport function computeNextRun(spec: CronSpec, tz: string, afterTs: number): number {\n if (spec.kind === \"interval\") return afterTs + spec.ms;\n const interval = parseExpression(spec.expr, { currentDate: new Date(afterTs), tz });\n return interval.next().toDate().getTime();\n}\n\n/**\n * Computes the LAST fire time AT-OR-BEFORE `atOrBeforeTs`, per `spec` — the mirror of\n * `computeNextRun`, used only by `_cronTick`'s bounded catch-up path (`./modules.ts`, the\n * unbounded-catch-up-loop fix) to find the single most recent missed occurrence in O(1) without\n * stepping through a potentially enormous backlog. Only meaningful for `{kind:\"cron\"}` — an\n * interval spec's \"previous occurrence\" is plain reverse arithmetic from its own numeric anchor,\n * which `_cronTick` computes inline without a cron-parser round-trip, so this throws for\n * `{kind:\"interval\"}` rather than silently doing the wrong thing.\n *\n * Implemented via `cron-parser`'s `.prev()`, called with `currentDate` one ms PAST\n * `atOrBeforeTs` so an occurrence landing EXACTLY on `atOrBeforeTs` is still included —\n * `cron-parser`'s `.next()`/`.prev()` are both strictly exclusive of `currentDate` itself\n * (verified empirically, matching `computeNextRun`'s own \"strictly after\" contract on the other\n * side).\n */\nexport function computePrevRun(spec: CronSpec, tz: string, atOrBeforeTs: number): number {\n if (spec.kind === \"interval\") {\n throw new Error(\n \"computePrevRun: interval specs are anchor-relative — compute their previous occurrence via arithmetic from the cron's own anchor instead of calling this\",\n );\n }\n const interval = parseExpression(spec.expr, { currentDate: new Date(atOrBeforeTs + 1), tz });\n return interval.prev().toDate().getTime();\n}\n\n/**\n * Enqueues (or re-enqueues) a cron's CADENCE job, embedding the resulting job's own id into its\n * args (`{cronName, jobId}`) — the duplicate-cadence-chain fix's belt-and-braces half: a future\n * `_cronTick` invocation can then recognize whether it's still the CURRENT canonical chain for\n * this cron (by comparing its own `args.jobId` against `crons.cadenceJobId`) and self-terminate\n * without rescheduling if not — see `_cronTick`'s doc comment in `./modules.ts`. This convergence\n * check is a backstop even if `hasLiveCadence` (below) somehow still let two chains coexist.\n *\n * Two writes (insert, then patch `job_args`) rather than one, because the job's id isn't known\n * until after the insert — self-referential args can't be supplied up front. This runs at most\n * once per cadence tick (or once per boot-time reconcile), so the extra write is negligible.\n */\nexport async function enqueueCadenceJob(\n db: GuestDatabaseWriter,\n now: () => number,\n tables: EnqueueTables,\n cronName: string,\n runAt: number,\n): Promise<string> {\n const jobId = await enqueueInternal(db, now, tables, \"scheduler:_cronTick\", { cronName }, { runAt, name: cronName });\n const rows = await db.query(tables.jobArgs, \"by_job\").eq(\"jobId\", jobId).take(1).collect();\n const row = rows[0];\n if (row !== undefined) {\n await db.replace(row._id as string, { ...row, args: { cronName, jobId } });\n }\n return jobId;\n}\n\n/**\n * How the app's `crons.ts` reaches this component (Task 5 design decision):\n *\n * The brief's straw man was a magic file-discovery mechanism (\"the E2E-facing convention — a\n * `crons.ts` file default-exporting `cronJobs()`\"), but nothing in `packages/component`'s compose\n * flow (`composeComponents`/`defineComponent`) loads app files by convention — components are\n * plain data (`ComponentDefinition`) assembled by whatever calls `composeComponents`, and the\n * only place with actual filesystem/module-loading responsibility is `packages/cli` (not touched\n * this task). Wiring \"auto-discover `crons.ts`\" now would mean guessing at that not-yet-built\n * CLI's shape.\n *\n * Chosen instead — the least invasive sound option: `defineScheduler(opts?: { crons? })` takes\n * the ALREADY-BUILT `CronJobs` registry (whatever the app's `crons.ts` produced by calling\n * `cronJobs()` + `.interval()`/`.cron()`/etc. and `export default`ing) as a plain config value,\n * exactly like `defineComponent`'s other config fields. The Convex-parity file shape (`import {\n * cronJobs } from \"./_generated/server\"; const crons = cronJobs(); ...; export default crons;`)\n * is UNCHANGED and still the file an app author writes — Task 6 (or the CLI) just needs to\n * `import crons from \"./crons\"` and pass it to `defineScheduler({ crons })` in the app's compose\n * step, which is a few lines of glue, not a new loading mechanism. This keeps `@helipod/\n * scheduler` itself free of filesystem/module-resolution concerns (which belong in `packages/\n * cli`), while the public authoring convention Task 6 needs is already fully exercised by this\n * task's tests (`cronJobs()` → `defineScheduler({ crons })`).\n */\nexport async function reconcileCrons(ctx: BootContext, registry: CronJobs | undefined): Promise<void> {\n const db = ctx.db;\n const now = ctx.now;\n const nowFn = (): number => now;\n const tables: EnqueueTables = { jobs: \"jobs\", jobArgs: \"job_args\" };\n\n const desired = registry ? registry.__entries() : [];\n const desiredByName = new Map(desired.map((e) => [e.name, e]));\n\n const existingRows = await db.query(\"crons\", \"by_creation\").collect();\n const existingByName = new Map(existingRows.map((r) => [r.name as string, r]));\n\n // Removed: the app no longer registers this cron. Best-effort cancel its pending cadence job\n // (so it stops self-rescheduling) and drop the row. Already-enqueued work jobs are left alone —\n // they're ordinary `jobs` rows now, disconnected from this cron's identity.\n for (const row of existingRows) {\n if (desiredByName.has(row.name as string)) continue;\n const cadenceJobId = row.cadenceJobId as string | undefined;\n if (cadenceJobId !== undefined) {\n const job = await db.get(cadenceJobId);\n if (job !== null && job.state === \"pending\") {\n await db.replace(cadenceJobId, { ...job, state: \"canceled\", completedTs: now });\n }\n }\n await db.delete(row._id as string);\n }\n\n for (const entry of desired) {\n const specJson = JSON.stringify(entry.spec);\n const existingRow = existingByName.get(entry.name);\n\n if (existingRow === undefined) {\n // New cron: insert the row, anchor its cadence at `now` (boot time — see `_cronTick`'s\n // clock-anchoring: the anchor is whatever `lastScheduledTs` says, so setting it here\n // rather than leaving it `undefined` means even the FIRST fire is anchored to a fixed\n // instant, not to whatever `now()` happens to be when the cadence job is dispatched).\n const cronId = await db.insert(\"crons\", {\n name: entry.name,\n spec: specJson,\n tz: entry.tz,\n catchUp: entry.catchUp,\n lastScheduledTs: now,\n workFnPath: entry.workFnPath,\n workArgs: entry.workArgs,\n });\n const firstRun = computeNextRun(entry.spec, entry.tz, now);\n const cadenceJobId = await enqueueCadenceJob(db, nowFn, tables, entry.name, firstRun);\n await db.replace(cronId, {\n name: entry.name,\n spec: specJson,\n tz: entry.tz,\n catchUp: entry.catchUp,\n lastScheduledTs: now,\n workFnPath: entry.workFnPath,\n workArgs: entry.workArgs,\n cadenceJobId,\n });\n continue;\n }\n\n const specChanged =\n existingRow.spec !== specJson ||\n existingRow.tz !== entry.tz ||\n existingRow.workFnPath !== entry.workFnPath ||\n JSON.stringify(existingRow.workArgs) !== JSON.stringify(entry.workArgs);\n\n if (specChanged) {\n // Cadence/target changed: cancel the old cadence job (if still pending) and restart the\n // cadence anchored at `now`, same as a fresh registration — an in-place spec edit\n // shouldn't inherit the OLD spec's phase.\n const oldCadenceJobId = existingRow.cadenceJobId as string | undefined;\n if (oldCadenceJobId !== undefined) {\n const job = await db.get(oldCadenceJobId);\n if (job !== null && job.state === \"pending\") {\n await db.replace(oldCadenceJobId, { ...job, state: \"canceled\", completedTs: now });\n }\n }\n const firstRun = computeNextRun(entry.spec, entry.tz, now);\n const cadenceJobId = await enqueueCadenceJob(db, nowFn, tables, entry.name, firstRun);\n await db.replace(existingRow._id as string, {\n ...existingRow,\n spec: specJson,\n tz: entry.tz,\n catchUp: entry.catchUp,\n workFnPath: entry.workFnPath,\n workArgs: entry.workArgs,\n lastScheduledTs: now,\n cadenceJobId,\n });\n continue;\n }\n\n if (existingRow.catchUp !== entry.catchUp) {\n // Policy-only change: doesn't touch the cadence's phase, just the row's `catchUp` field —\n // `_cronTick` reads it fresh on its next fire.\n await db.replace(existingRow._id as string, { ...existingRow, catchUp: entry.catchUp });\n }\n\n // Idempotent-across-restarts: only (re)schedule a cadence job if this cron doesn't already\n // have one live. A restart that re-runs boot with an unchanged registry must NOT double the\n // cadence — `cadenceJobId` names the single currently-pending cadence job, if any.\n //\n // Duplicate-cadence-chain fix: `hasLiveCadence` must treat ANY non-terminal cadence job\n // (`\"pending\"` OR `\"inProgress\"`) as live, not just `\"pending\"`. If the process dies between\n // `_claim` and `_complete` of the cadence job (see `_cronTick`'s doc comment in\n // `./modules.ts`), the job is left `\"inProgress\"` — that's still the live chain, just\n // mid-flight, not a dead one. Treating only `\"pending\"` as live let a crash-mid-dispatch\n // restart land here, see \"no live cadence\", and start a SECOND chain — while the first\n // (inProgress, leased) chain was later revived by `scheduler:_reclaim`'s lease sweep, leaving\n // two immortal chains both advancing `lastScheduledTs` forever.\n //\n // The cheapest sound fix given the schema: trust the single `cadenceJobId` pointer recorded\n // on this row (an O(1) `db.get`) rather than a full scan over `jobs` for other\n // `fnPath:\"scheduler:_cronTick\"` rows matching this cron by name — there's no index for that\n // (args aren't indexed), so a scan would cost O(all non-terminal jobs) on every boot for\n // every cron. Pointer-trust is O(1) but not airtight against every conceivable race, so\n // `enqueueCadenceJob`'s embedded `{jobId}` + `_cronTick`'s self-terminate check (top of\n // `_cronTick`, `./modules.ts`) is the belt-and-braces backstop: even if this check somehow\n // still admits a duplicate, the stale chain dies at its very next tick instead of persisting\n // forever.\n const cadenceJobId = existingRow.cadenceJobId as string | undefined;\n let hasLiveCadence = false;\n if (cadenceJobId !== undefined) {\n const job = await db.get(cadenceJobId);\n hasLiveCadence = job !== null && (job.state === \"pending\" || job.state === \"inProgress\");\n }\n if (!hasLiveCadence) {\n const anchor = (existingRow.lastScheduledTs as number | undefined) ?? now;\n const nextRun = computeNextRun(entry.spec, entry.tz, anchor);\n const newCadenceJobId = await enqueueCadenceJob(db, nowFn, tables, entry.name, nextRun);\n await db.replace(existingRow._id as string, { ...existingRow, cadenceJobId: newCadenceJobId });\n }\n }\n}\n","import type { Driver, DriverContext } from \"@helipod/component\";\nimport type { JSONValue } from \"@helipod/values\";\nimport { isHelipodError } from \"@helipod/errors\";\nimport type { ClaimResult, JobResult, PeekDueResult } from \"./modules\";\nimport { SWEEP_MS } from \"./modules\";\n\n/**\n * A `schedulerDriver()` also exposes:\n * - `__tick`: a deterministic test seam for one loop iteration (no real timers).\n * - `__wake`: the same fire-and-forget signal `DriverContext.onCommit`/timers use internally,\n * exposed so a test can simulate a commit notification landing at a precise moment (e.g. from\n * inside a job's own mutation body, to interleave with an in-flight `__tick()`) — the reactive\n * `onCommit` path itself can't be driven precisely from a test, since it fires off the real\n * commit fan-out on whatever schedule the runtime gives it.\n * - `__sweep`: runs the lease-reclaim sweep (`scheduler:_reclaim`) exactly once, without arming\n * (or waiting on) the real `SWEEP_MS`-interval timer — the deterministic test seam for Task 4's\n * infra-kill reclaim path.\n */\nexport interface SchedulerDriver extends Driver {\n __tick: () => Promise<void>;\n __wake: () => void;\n __sweep: () => Promise<void>;\n}\n\n/**\n * `@helipod/scheduler`'s driver — the event-driven loop that actually RUNS due jobs.\n *\n * Two wake sources, NO fixed-interval polling:\n * - **Reactive**: taps the runtime's commit fan-out (`DriverContext.onCommit`) and re-runs\n * `iterate()` whenever a commit touches any `scheduler/*` table (an `enqueue`/`cancel`/\n * `complete` write) — so a freshly-enqueued due job gets picked up with ~0 latency.\n * - **Timer**: re-arms a single wall-clock timer to `earliestFutureTs` (the soonest still-pending\n * job) after every iteration, so a job scheduled for later still fires once its time arrives\n * without anything scanning `jobs` in between.\n *\n * Single-owner: an in-process `running` flag collapses overlapping wake-ups (two commits, or a\n * commit racing a timer) into a single iteration at a time. Because `running` is set\n * synchronously — before the first `await` — two `iterate()` calls issued back-to-back in the\n * same synchronous turn can never both proceed; the second observes `running === true` and\n * returns immediately. That said, the in-process flag is only a throughput optimization, NOT the\n * correctness guarantee: the AUTHORITATIVE double-run guard is `scheduler:_claim`'s snapshot-read\n * + exact `state === \"pending\"` check (`./modules.ts`), serialized by the single-writer OCC\n * transactor — even if two iterations somehow ran concurrently (e.g. two runtimes sharing a\n * store), at most one `_claim` call per job ever observes `\"pending\"`.\n *\n * A wake that arrives while `running` is already true is NOT dropped: it sets a coalesced\n * `pendingWake` bit that the in-flight iteration checks at the end of every pass, looping for one\n * more fresh peek/claim/complete pass instead of exiting. Without this, a commit that lands\n * mid-iteration (an app mutation enqueuing a due-now job between the loop's awaits) would be\n * silently swallowed — the timer re-arm at the end of the pass would use the `earliestFutureTs`\n * that pass captured, which doesn't account for the new job, and the job would sit `pending`\n * until some unrelated future wake.\n *\n * A job that throws while running is caught per-job (not allowed to escape the loop), so one bad\n * job can't wedge the whole batch or leave `running` stuck `true` — `_complete` is always called\n * (with a `failed` result) and the outer `try/finally` always clears `running`.\n *\n * `iterate()` always returns a promise that settles when the due set it's responsible for has\n * actually been drained — including a coalesced call. A caller that arrives while a pass is\n * already in flight (setting `pendingWake` per above) gets back the SAME promise as the in-flight\n * pass, not an already-resolved one: with the reactive `onCommit` wake now real (not dead code —\n * see `packages/runtime-embedded/src/runtime.ts`), a test's `__tick()` frequently races an\n * app-mutation's own commit, which fires `wake()` before `__tick()` gets a turn. If `__tick()`\n * merely no-op'd on a coalesced call, tests would observe results before the real work finished.\n * `wake()`'s callers (reactive `onCommit`/timer) never await this return value, so they're\n * unaffected — this only changes behavior for synchronous callers like `__tick()`.\n */\nexport function schedulerDriver(): SchedulerDriver {\n let ctx: DriverContext;\n let running = false;\n let pendingWake = false;\n let timer: number | null = null;\n let inFlight: Promise<void> | null = null;\n // The ONLY periodic timer in this driver — everything else (dispatch) is reactive. Backstops\n // infra kills: a process that `_claim`ed a job and died before `_complete`ing it leaves the job\n // `inProgress` forever without this sweep. Kept on its own handle (separate from `timer`, the\n // due-job wake timer) so re-arming one never clobbers the other.\n let sweepTimer: number | null = null;\n // Set by `stop()` BEFORE it tears down the timers/subscription. Guards against the driver\n // resurrecting ITSELF: an in-flight pass's end-of-pass `setTimer` (`runPass`), a settling\n // `sweepOnce`'s `finally → armSweep`, and `iterate`'s `finally → wake` all run unconditionally\n // when work that was already in flight settles — even if `stop()` raced in while a\n // `runFunction` was awaiting. Without this flag they re-arm a fresh timer after `stop()`\n // already returned, and the loop keeps running forever. Checked at every AUTOMATIC re-entry/\n // re-arm point — `wake` (the onCommit/timer callback), `runPass`'s end-of-pass re-arm, and\n // `armSweep` — so a timer/commit callback that fires concurrently with `stop()` can't start or\n // re-schedule work either. (Mirrors the same guard in `@helipod/storage`'s reaper driver.)\n //\n // `iterate()` itself is deliberately NOT guarded: the ONLY caller that reaches it while\n // `stopped` is the `__tick()` test seam (an explicit, synchronous manual drive), which tests\n // use precisely to step a stopped driver one pass at a time after `driver.stop()` unsubscribes\n // the reactive `onCommit` cascade. Guarding `iterate` would break that pattern (a no-op tick\n // dispatches nothing); the automatic wake paths are already covered by `wake()`'s guard.\n let stopped = false;\n\n function wake(): void {\n if (stopped) return;\n // Fire-and-forget from a sync callback (onCommit/setTimer); swallow+log rather than let an\n // unexpected internal error (a bug in _peekDue/_claim/_complete, not a job's own throw —\n // those are caught per-job below) surface as an unhandled rejection. If an iteration is\n // already in flight, `iterate()`'s own guard below coalesces this into `pendingWake` instead\n // of no-oping outright — see its comment.\n iterate().catch((e: unknown) => {\n console.error(\"[scheduler] driver iteration failed:\", e);\n });\n }\n\n function iterate(): Promise<void> {\n if (running) {\n // A commit (or another wake) landed while a pass is already in flight — e.g. an app\n // mutation enqueuing a due-now job between the in-flight pass's awaits. That pass may\n // already have read a due set that doesn't include the new job, and its end-of-pass timer\n // re-arm would otherwise use a stale `earliestFutureTs`, silently stranding the new job\n // until some unrelated future wake. Coalesce into a bit the in-flight call checks before\n // releasing `running`, so it loops for one more fresh pass instead of exiting — this call\n // itself doesn't start a new pass; it hands back the in-flight pass's own promise (see the\n // class doc above) so an awaiting caller still observes real completion.\n pendingWake = true;\n return inFlight ?? Promise.resolve();\n }\n running = true;\n const pass = runPass().finally(() => {\n running = false;\n inFlight = null;\n // Closes a residual micro-window: a wake can land between the do/while loop's final\n // `pendingWake` check (inside `runPass`, which already exited) and this `finally` running —\n // e.g. another microtask's `wake()` call resolving in between. Without this, that wake would\n // set `pendingWake` but nothing would ever consume it (the pass that would have looped on it\n // already returned), stranding whatever it was signaling until some unrelated future wake.\n if (pendingWake) void wake();\n });\n inFlight = pass;\n return pass;\n }\n\n async function runPass(): Promise<void> {\n // Loop passes until a pass completes with no wake pending — a wake that arrives mid-pass\n // (an app mutation enqueuing a due-now job between our awaits) sets `pendingWake`, and we\n // re-run the peek/claim/complete cycle instead of exiting with a stale due set.\n let earliestFutureTs: number | null = null;\n do {\n pendingWake = false;\n const peeked = (await ctx.runFunction(\"scheduler:_peekDue\", {})) as PeekDueResult;\n earliestFutureTs = peeked.earliestFutureTs;\n for (const job of peeked.due) {\n const claimed = (await ctx.runFunction(\"scheduler:_claim\", { jobId: job._id })) as ClaimResult | null;\n if (claimed === null) continue; // lost the claim race → another caller got there first, skip\n\n // Mutations and actions dispatch through the identical path: `ctx.runFunction` routes to\n // the runtime, which routes to the executor's action branch for a `kind:\"action\"` fnPath\n // (CLAUDE.md build-order #5's action runtime — see @helipod/executor) — the driver\n // itself doesn't need to know which kind it claimed. At-most-once for actions is NOT this\n // try/catch's job: it's already guaranteed by `_claim` committing `state:\"inProgress\"`\n // BEFORE this call runs, so a crash mid-action leaves the job for `_reclaim`'s lease sweep\n // to dead-letter (`modules.ts`'s `_reclaim`) rather than ever re-dispatching it here.\n let result: JobResult;\n try {\n const value = await ctx.runFunction(claimed.fnPath, claimed.args);\n // A function with no explicit `return` yields `undefined`, which the wire codec\n // (`jsonToConvex`) rejects — coerce to `null` (Convex parity) so `_complete` never crashes.\n result = { kind: \"success\", value: value === undefined ? null : value };\n } catch (e) {\n // Preserve the error's retryability so `_complete` can fail fast on a deterministic,\n // non-retryable failure (e.g. a `DocumentValidationError` from a schema-violating write)\n // instead of burning every retry. A plain (non-Helipod) error defaults to retryable, so\n // the scheduler's existing \"retry unknown failures up to maxFailures\" behavior is unchanged.\n result = { kind: \"failed\", error: String(e), retryable: isHelipodError(e) ? e.retryable : true };\n }\n // `result.value` (an action/mutation's arbitrary return) is `unknown`, not provably a\n // `JSONValue` — it's already been through the same JSON syscall round-trip as any other\n // UDF return, so this cast just bridges the gap TS can't see across.\n await ctx.runFunction(\"scheduler:_complete\", { jobId: job._id, result } as unknown as JSONValue);\n }\n } while (pendingWake);\n\n // Re-arm the timer to the LAST pass's fresh earliest future job (clearing any stale one\n // first), so a wake that changed the due set still leaves the timer pointed at the right\n // instant.\n if (timer !== null) {\n ctx.clearTimer(timer);\n timer = null;\n }\n // Guard against re-arming after `stop()` raced in while this pass was awaiting a `runFunction`.\n if (!stopped && earliestFutureTs != null) timer = ctx.setTimer(earliestFutureTs, wake);\n }\n\n // Runs `scheduler:_reclaim` once, then re-arms itself `SWEEP_MS` out — the recurring safety\n // sweep. Errors are swallowed+logged (mirroring `wake()`): a bug in `_reclaim` itself must never\n // stop the sweep from re-arming, or the whole infra-kill backstop silently dies.\n async function sweepOnce(): Promise<void> {\n try {\n await ctx.runFunction(\"scheduler:_reclaim\", {});\n } catch (e) {\n console.error(\"[scheduler] lease-reclaim sweep failed:\", e);\n } finally {\n armSweep();\n }\n }\n\n function armSweep(): void {\n // A settling `sweepOnce()` calls this from its `finally` even if `stop()` raced in mid-sweep.\n if (stopped) return;\n if (sweepTimer !== null) {\n ctx.clearTimer(sweepTimer);\n sweepTimer = null;\n }\n // `backstopMs` (not `SWEEP_MS` raw): this sweep is a pure infra-kill backstop, never next-work —\n // the call site is how a driver declares that, so a host where every wake costs a cold start can\n // stretch it. The due-job `timer` above deliberately does NOT go through it: an `earliestFutureTs`\n // wake is the real work.\n sweepTimer = ctx.setTimer(ctx.now() + ctx.backstopMs(SWEEP_MS), () => {\n void sweepOnce();\n });\n }\n\n let unsubscribeCommit: (() => void) | null = null;\n\n return {\n name: \"scheduler\",\n start(c) {\n ctx = c;\n unsubscribeCommit = c.onCommit((inv) => {\n if (inv.tables.some((t) => t.startsWith(\"scheduler/\"))) wake();\n });\n wake();\n armSweep();\n },\n stop() {\n // Set BEFORE teardown so any in-flight pass/sweep that settles after this returns sees\n // `stopped` already true and its re-arm (`runPass`'s `setTimer`, `sweepOnce`'s `armSweep`,\n // `iterate`'s `finally → wake`) no-ops instead of resurrecting the driver.\n stopped = true;\n unsubscribeCommit?.();\n unsubscribeCommit = null;\n if (timer !== null) {\n ctx.clearTimer(timer);\n timer = null;\n }\n if (sweepTimer !== null) {\n ctx.clearTimer(sweepTimer);\n sweepTimer = null;\n }\n },\n // Test seam: drives a loop pass and awaits its actual completion (coalescing into an\n // already-in-flight pass — e.g. one a same-turn reactive `onCommit` wake already started —\n // rather than resolving early), and lets errors propagate to the caller (unlike `wake()`,\n // used by the reactive/timer paths, which swallows+logs), so tests see real failures instead\n // of them being silently logged.\n __tick: () => iterate(),\n // Test seam: the same fire-and-forget signal `onCommit`/timers send internally — see the\n // interface doc above.\n __wake: () => wake(),\n // Test seam: runs the lease-reclaim sweep exactly once, without the real `SWEEP_MS` wait and\n // without re-arming a live timer (unlike `armSweep`/`sweepOnce` above) — errors propagate\n // (unlike the internal `sweepOnce`) so a test sees real `_reclaim` failures.\n __sweep: () => ctx.runFunction(\"scheduler:_reclaim\", {}).then(() => undefined),\n };\n}\n"],"mappings":"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","names":["v","cadenceJobId","compact","v"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@helipod/scheduler",
3
+ "version": "0.1.0",
4
+ "license": "FSL-1.1-Apache-2.0",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "vitest run"
18
+ },
19
+ "dependencies": {
20
+ "@helipod/component": "0.1.0",
21
+ "@helipod/errors": "0.1.0",
22
+ "@helipod/executor": "0.1.0",
23
+ "@helipod/values": "0.1.0",
24
+ "cron-parser": "^4.9.0"
25
+ },
26
+ "devDependencies": {
27
+ "@helipod/client": "0.1.0",
28
+ "@helipod/docstore-sqlite": "0.1.0",
29
+ "@helipod/runtime-embedded": "0.1.0",
30
+ "@types/node": "^22.10.5",
31
+ "tsup": "^8.3.5",
32
+ "typescript": "^5.7.2",
33
+ "vitest": "^2.1.8"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/helipod-sh/helipod.git",
44
+ "directory": "components/scheduler"
45
+ },
46
+ "homepage": "https://github.com/helipod-sh/helipod"
47
+ }