@substrat-run/kernel 0.114.0 → 0.117.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.
Files changed (64) hide show
  1. package/dist/capability.d.ts +220 -0
  2. package/dist/capability.d.ts.map +1 -0
  3. package/dist/capability.js +537 -0
  4. package/dist/capability.js.map +1 -0
  5. package/dist/check-key.d.ts +30 -0
  6. package/dist/check-key.d.ts.map +1 -0
  7. package/dist/check-key.js +37 -0
  8. package/dist/check-key.js.map +1 -0
  9. package/dist/denial-query.d.ts +35 -2
  10. package/dist/denial-query.d.ts.map +1 -1
  11. package/dist/denial-query.js +70 -28
  12. package/dist/denial-query.js.map +1 -1
  13. package/dist/index.d.ts +23 -6
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +16 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/job-run.d.ts +493 -0
  18. package/dist/job-run.d.ts.map +1 -0
  19. package/dist/job-run.js +655 -0
  20. package/dist/job-run.js.map +1 -0
  21. package/dist/outbox-event.d.ts +131 -0
  22. package/dist/outbox-event.d.ts.map +1 -0
  23. package/dist/outbox-event.js +185 -0
  24. package/dist/outbox-event.js.map +1 -0
  25. package/dist/permission-checker.d.ts +8 -1
  26. package/dist/permission-checker.d.ts.map +1 -1
  27. package/dist/permission-checker.js +18 -0
  28. package/dist/permission-checker.js.map +1 -1
  29. package/dist/permission-eval.d.ts +8 -0
  30. package/dist/permission-eval.d.ts.map +1 -1
  31. package/dist/permission-eval.js +164 -91
  32. package/dist/permission-eval.js.map +1 -1
  33. package/dist/platform-request-query.d.ts +58 -1
  34. package/dist/platform-request-query.d.ts.map +1 -1
  35. package/dist/platform-request-query.js +61 -1
  36. package/dist/platform-request-query.js.map +1 -1
  37. package/dist/platform-sweep.d.ts +180 -8
  38. package/dist/platform-sweep.d.ts.map +1 -1
  39. package/dist/platform-sweep.js +243 -34
  40. package/dist/platform-sweep.js.map +1 -1
  41. package/dist/row-decode.d.ts +107 -0
  42. package/dist/row-decode.d.ts.map +1 -0
  43. package/dist/row-decode.js +93 -0
  44. package/dist/row-decode.js.map +1 -0
  45. package/dist/scope-host.d.ts +444 -9
  46. package/dist/scope-host.d.ts.map +1 -1
  47. package/dist/scope-host.js +33 -0
  48. package/dist/scope-host.js.map +1 -1
  49. package/dist/scope-tuple-seat.d.ts +72 -0
  50. package/dist/scope-tuple-seat.d.ts.map +1 -0
  51. package/dist/scope-tuple-seat.js +93 -0
  52. package/dist/scope-tuple-seat.js.map +1 -0
  53. package/dist/subject-redaction.d.ts +160 -0
  54. package/dist/subject-redaction.d.ts.map +1 -0
  55. package/dist/subject-redaction.js +210 -0
  56. package/dist/subject-redaction.js.map +1 -0
  57. package/dist/system-switch.d.ts +108 -0
  58. package/dist/system-switch.d.ts.map +1 -0
  59. package/dist/system-switch.js +145 -0
  60. package/dist/system-switch.js.map +1 -0
  61. package/dist/timeline.d.ts.map +1 -1
  62. package/dist/timeline.js +115 -52
  63. package/dist/timeline.js.map +1 -1
  64. package/package.json +2 -2
@@ -0,0 +1,493 @@
1
+ import { type ModuleId } from '@substrat-run/contracts';
2
+ import { type ExecutorRetryPolicy, type ScopeStub } from './scope-host.js';
3
+ /**
4
+ * The fourth driver (#1577): long, resumable, coalesced work.
5
+ *
6
+ * Three drivers already move work off a request, and each is correct for what it
7
+ * is. `registerExecutor` + `drainDue` retries ONE delivery, whole. A declared
8
+ * schedule + `runDueSchedules` fires ONE operation inside a cadence window, which
9
+ * must then finish. `runPlatformSweep` does a pass of per-unit maintenance and
10
+ * reports it. None of them covers the shape this file names:
11
+ *
12
+ * Walk 100 000 objects in an external system. It takes an hour. It must survive a
13
+ * worker eviction, a deploy and a transient upstream failure by CONTINUING WHERE IT
14
+ * STOPPED. Only one walk per source may run at a time, and asking for a second while
15
+ * one is in flight must join the first. When it ends the outcome has to be legible.
16
+ *
17
+ * ## The model, in four words: run, pass, step, cursor
18
+ *
19
+ * A **run** is the durable record — one `_substrat_job_runs` row, in the scope, with
20
+ * its status, its resume cursor, a counter bag, its start/end and its last error. It
21
+ * is what an operator reads afterwards, and it is the thing coalescing joins.
22
+ *
23
+ * A **pass** is one invocation of the job's handler. A pass does a BOUNDED chunk of
24
+ * the walk and hands a cursor forward; the next pass resumes from it. That is the
25
+ * whole of resume at the outer level, and it is why a 100 000-object walk is not a
26
+ * single hour-long call anybody has to keep alive.
27
+ *
28
+ * A **step** is a named unit inside a pass. Its result is committed as it completes,
29
+ * so a step that already succeeded is NOT re-run — not on a retry of the step after
30
+ * it, and not after the process was killed mid-pass. That is resume at the inner
31
+ * level, and it is what "no step before it repeated" means.
32
+ *
33
+ * A **cursor** is whatever the handler says the next pass should resume from — an id,
34
+ * a page token, an offset. Opaque to the driver, stored as JSON, held to the payload
35
+ * rule below because it has to survive the same trip.
36
+ *
37
+ * **The step ledger is per PASS, and that is deliberate.** When a pass commits, its
38
+ * step rows are dropped: the next pass is new work, named by the new cursor, and a
39
+ * ledger that accumulated across an hour-long walk would be the unbounded table this
40
+ * design exists to avoid. The cursor carries progress BETWEEN passes; the ledger
41
+ * carries it WITHIN one.
42
+ *
43
+ * ## The determinism rule, and the half of it that is mechanical
44
+ *
45
+ * Step names must be a pure function of the payload and prior results. If a name
46
+ * varies between passes — a timestamp in it, a random suffix — the memo never hits,
47
+ * every resume replays work that already happened, and resume is a lie told in a
48
+ * green test. That rule cannot be fully checked from here; what CAN be checked is
49
+ * the sharpest way to break it, and is: two `step()` calls under ONE name in ONE
50
+ * pass are refused (`JOB_STEP_REUSED`), because the second would read the first's
51
+ * memo and silently skip its own work.
52
+ *
53
+ * ## Coalescing is the DRIVER's, never a unique index
54
+ *
55
+ * One run in flight per `(module, job, instance)`. `startJobRun` looks for a live row
56
+ * and RETURNS it rather than inserting a second. It is not a `UNIQUE` constraint, and
57
+ * that is the point: a run whose process was killed is still `running`, and it must be
58
+ * restartable — a uniqueness constraint would be "one row ever", which would refuse
59
+ * the re-import that has to happen next year as loudly as it refuses the duplicate.
60
+ * `_substrat_sweep_runs` is a RECEIPT and carries such a constraint; a cursor is not a
61
+ * receipt, and the two must not be re-merged (#1571, #1572).
62
+ *
63
+ * ## One driver per scope at a time — a stated bound, not a mechanism
64
+ *
65
+ * Coalescing stops duplicate RUNS. It does not stop two concurrent callers of
66
+ * `runDueJobs` from picking the same run out of the due read and advancing it at the
67
+ * same time: there is no lease, and a lease is not smuggled in here. The topology the
68
+ * driver is built for has one tick per scope — `runPlatformSweep` enumerates scopes
69
+ * and does one call each, a scope DO's alarm fires for its own scope — so the bound
70
+ * is satisfied by construction rather than defended against.
71
+ *
72
+ * What the overlap would cost, if a deployment did drive one scope twice at once: the
73
+ * step ledger absorbs most of it (a step already committed returns its memo to both),
74
+ * so the exposure is a step neither pass has finished yet, which both would run. That
75
+ * is the same at-least-once residue an executor already has and which handlers already
76
+ * have to absorb — but it is NOT what "only one walk per source at a time" promises,
77
+ * so it is written down rather than implied. Adding a lease is a real design with a
78
+ * real expiry question behind it (a leaked lease is a run nothing will ever touch
79
+ * again), and it wants a consumer's numbers before it gets one.
80
+ *
81
+ * ## What this is NOT
82
+ *
83
+ * Not a workflow engine: no branching, no fan-out, no BPMN, no timers between steps.
84
+ * A linear, resumable, coalesced sequence of steps with a cursor — the smallest thing
85
+ * that makes an hour-long import survivable. A use case that needs branching is a
86
+ * different issue and probably a different answer.
87
+ */
88
+ /**
89
+ * The run table and its step ledger, as both adapters build them.
90
+ *
91
+ * Shared rather than spelled twice for the reason `IDEMPOTENCY_DDL` and
92
+ * `SCHEDULE_STATE_DDL` are: `lint:spine-ddl` compares what each adapter's
93
+ * `KERNEL_DDL` executes, and one definition is what keeps the self-hosted store
94
+ * and the hosted store the same shape rather than merely the same intention.
95
+ * Spine — kernel-written, `_substrat_*`, never a module migration.
96
+ */
97
+ export declare const JOB_RUN_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_job_runs (\n id TEXT PRIMARY KEY,\n -- The coalescing key: one LIVE run per (module_id, job, instance). Not a UNIQUE\n -- index, deliberately -- see this file's header. instance names WHAT is being\n -- walked (a source id, a mapping version); a job with one walk per scope uses\n -- the 'default' the input schema fills in.\n module_id TEXT NOT NULL,\n job TEXT NOT NULL,\n instance TEXT NOT NULL,\n -- What start() was handed, as JSON. Held to the queue-safety rule\n -- (assertQueueSafe): ids and configuration, never bytes, class instances or\n -- functions, because this value has to survive a queue message unchanged.\n payload TEXT NOT NULL,\n -- 'running' | 'done' | 'failed'. A killed run stays 'running' and is picked up\n -- again by the next drive -- which is exactly what makes it restartable.\n status TEXT NOT NULL,\n -- What the last COMMITTED pass handed forward, as JSON. NULL = no pass has\n -- committed yet, which is a fact ('start from the beginning'), not missing data.\n cursor TEXT,\n -- The run's counter bag (JSON object of numbers), merged on each commit. An\n -- uncommitted pass's counts are discarded with the rest of the pass.\n counters TEXT NOT NULL DEFAULT '{}',\n -- CONSECUTIVE failed passes. Reset to 0 whenever a pass commits, so this reads\n -- as \"how stuck is it now\", not \"how much work has it done\".\n attempts INTEGER NOT NULL DEFAULT 0,\n -- The error the last failed pass left. Retained after the run goes 'failed' --\n -- the record is the evidence, so it must still say why.\n last_error TEXT,\n started_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n -- When the next pass may run, on the executor's own backoff curve. NULL = now\n -- (or terminal), which is why the due read tests IS NULL as well as <= now.\n next_attempt_at TEXT,\n -- When the run reached 'done' or 'failed'. NULL while it is still running.\n ended_at TEXT\n );\n -- The drive's read: WHERE status = 'running' AND (next_attempt_at IS NULL OR <= ?)\n -- ORDER BY id. Leading with status makes the live runs a seekable range over a\n -- table that RETAINS every finished run, so the cost tracks how much is in flight\n -- rather than how much the scope has ever imported.\n CREATE INDEX IF NOT EXISTS _substrat_job_runs_due ON _substrat_job_runs (status, next_attempt_at, id);\n -- Coalescing's read, and the operator read's filter.\n CREATE INDEX IF NOT EXISTS _substrat_job_runs_key ON _substrat_job_runs (module_id, job, instance, id);\n -- The step ledger of the pass currently in flight. Rows are written as each step\n -- COMPLETES and dropped when the pass COMMITS, so this holds one pass's worth of\n -- steps and never grows with the length of the walk.\n --\n -- A FAILED run keeps its last pass's rows, deliberately: they are the difference\n -- between \"it got nowhere\" and \"it got three quarters of the way and then the\n -- provider went down\", which the run row's single last_error cannot say. Still\n -- bounded -- one pass's worth per failed run -- because a restart is a NEW run\n -- with a new id and therefore its own ledger.\n CREATE TABLE IF NOT EXISTS _substrat_job_steps (\n run_id TEXT NOT NULL,\n step TEXT NOT NULL,\n -- The step's return value as JSON. NOT NULL is what MEANS completed: a step that\n -- threw leaves the row with a NULL result and a raised attempts count, so the\n -- next pass runs it again rather than reading a success that never happened. A\n -- step returning nothing stores the JSON text 'null', which is not SQL NULL.\n result TEXT,\n attempts INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n recorded_at TEXT NOT NULL,\n PRIMARY KEY (run_id, step)\n );\n";
98
+ /** Where a run is. A killed run is `running` — that is what makes it resumable. */
99
+ export type JobRunStatus = 'running' | 'done' | 'failed';
100
+ /** The coalescing key: one LIVE run per triple. */
101
+ export interface JobRunKey {
102
+ moduleId: ModuleId;
103
+ /** The job's name, as `registerJob` declared it. */
104
+ job: string;
105
+ /** What is being walked — a source id, a mapping version. `'default'` when there is one. */
106
+ instance: string;
107
+ }
108
+ /** The durable run record, as an operator reads it. */
109
+ export interface JobRun extends JobRunKey {
110
+ id: string;
111
+ status: JobRunStatus;
112
+ /** What `startJobRun` was handed. */
113
+ payload: unknown;
114
+ /** What the last COMMITTED pass handed forward; null before the first commit. */
115
+ cursor: unknown;
116
+ counters: Record<string, number>;
117
+ /** Consecutive failed passes; 0 after any commit. */
118
+ attempts: number;
119
+ lastError: string | null;
120
+ startedAt: string;
121
+ updatedAt: string;
122
+ nextAttemptAt: string | null;
123
+ endedAt: string | null;
124
+ /**
125
+ * Why this row could not be read whole, or null — which is the ordinary case and
126
+ * what every run written by this driver carries.
127
+ *
128
+ * Present because the read and the DRIVER want opposite things from a malformed
129
+ * row, and both are right. A pass cannot run on a payload it cannot decode, so the
130
+ * driver fails the run and records why. The READ exists so an operator can see
131
+ * exactly that — and a read that threw on the one row being investigated would
132
+ * take every other run on the scope with it, since this returns a list. So the
133
+ * decode here is tolerant and SAYS SO: the undecodable columns come back empty
134
+ * (`null` / `{}`) with the parse error named here, rather than a silent `null`
135
+ * that reads as "no cursor".
136
+ *
137
+ * Reachable without any forge: `importDump` replays a dump's rows verbatim, so a
138
+ * dump from another world or edited by hand is enough.
139
+ */
140
+ decodeError: string | null;
141
+ }
142
+ /** What `startJobRun` is handed. */
143
+ export interface StartJobRunInput {
144
+ moduleId: ModuleId;
145
+ job: string;
146
+ /** Defaults to `'default'` — a job with one walk per scope needs no instance. */
147
+ instance?: string;
148
+ /** Ids and configuration. Refused at this boundary if it is anything else. */
149
+ payload?: unknown;
150
+ }
151
+ /** The operator read's filter. Every field narrows; none is required. */
152
+ export interface JobRunFilter {
153
+ moduleId?: ModuleId;
154
+ job?: string;
155
+ instance?: string;
156
+ status?: JobRunStatus;
157
+ /** Default `JOB_RUN_LIST_LIMIT`. */
158
+ limit?: number;
159
+ }
160
+ /** Rows one `jobRuns` read returns by default. */
161
+ export declare const JOB_RUN_LIST_LIMIT = 50;
162
+ /** The most rows one `jobRuns` read will ever return, whatever the caller asks for. */
163
+ export declare const JOB_RUN_LIST_MAX = 500;
164
+ /**
165
+ * The row budget for one operator read, normalised before it reaches SQL.
166
+ *
167
+ * `JobRunFilter.limit` comes from a caller and was bound straight to `LIMIT`, where
168
+ * SQLite reads a NEGATIVE value as unbounded, refuses a fractional one outright, and
169
+ * honours an oversized one. Since finished runs are retained, "unbounded" means the
170
+ * scope's entire history in one response — a read whose cost grows with retention,
171
+ * reachable by passing `-1`. Clamped here rather than in each adapter so the pure and
172
+ * the hosted read cannot answer the same filter differently.
173
+ */
174
+ export declare function jobRunListLimit(limit: number | undefined): number;
175
+ /** Runs one `runDueJobs` call picks up by default. */
176
+ export declare const JOB_DRIVE_LIMIT = 50;
177
+ /**
178
+ * Rows one `runDueJobs` call will READ while looking for runnable ones.
179
+ *
180
+ * The drive skips runs whose job this host does not register, so "read `limit` rows"
181
+ * and "find `limit` runs to drive" are different numbers, and a scope can hold an
182
+ * arbitrary number of the unrunnable kind. This caps the difference: past it the call
183
+ * drives what it found and returns, rather than scanning a scope's whole history on a
184
+ * maintenance tick.
185
+ */
186
+ export declare const JOB_DRIVE_SCAN_MAX = 500;
187
+ /** What a handler says at the end of a pass. */
188
+ export interface JobPassResult {
189
+ /**
190
+ * What the NEXT pass resumes from. Omitted keeps the cursor the last pass
191
+ * committed — which is how a pass that only did steps, and moved nothing on,
192
+ * says so. Held to the same queue-safety rule as the payload.
193
+ */
194
+ cursor?: unknown;
195
+ /** True when the walk is finished: the run goes `done` and is never driven again. */
196
+ done?: boolean;
197
+ }
198
+ /** What one pass of a job is given. */
199
+ export interface JobPassContext {
200
+ /** This run's id and coalescing key. */
201
+ readonly run: JobRunKey & {
202
+ id: string;
203
+ };
204
+ /** What `startJobRun` was handed, decoded. */
205
+ readonly payload: unknown;
206
+ /** What the last committed pass handed forward; `null` on the first pass. */
207
+ readonly cursor: unknown;
208
+ /** The counter bag as of the last commit. `count()` adds to the pass's copy. */
209
+ readonly counters: Readonly<Record<string, number>>;
210
+ /**
211
+ * Run one NAMED step, at most once per run-pass.
212
+ *
213
+ * A step whose result is already committed returns it WITHOUT running `fn` —
214
+ * that is the memo, and it is what survives both a retry of a later step and a
215
+ * process kill mid-pass. A step that throws records its attempt and fails the
216
+ * pass; the next pass replays the handler, skips every committed step above,
217
+ * and runs this one again until its own `retry` is exhausted.
218
+ *
219
+ * `name` must be a pure function of the payload and prior results. Two calls
220
+ * under one name in one pass are refused rather than silently memo-aliased.
221
+ *
222
+ * **AT-LEAST-ONCE. `fn` must be idempotent, and this is not a formality.** The
223
+ * ledger row is written AFTER `fn` resolves, which is the only ordering that is
224
+ * safe — claiming the step first would make it at-most-once and lose the effect on
225
+ * any crash in between. The cost is the opposite window: a stop after `fn`'s effect
226
+ * lands and before `recordStep` commits leaves no memo, so the next pass runs that
227
+ * effect a second time. It is the same trade, made the same way and for the same
228
+ * reason, as `recordExecutorDelivery` in the executor journal — whose docblock says
229
+ * it plainly — and like an executor handler, a step body absorbs the residue.
230
+ *
231
+ * `value` is round-tripped through its stored JSON before being returned, so what a
232
+ * handler sees is identical whether the step just ran or was replayed from the memo.
233
+ */
234
+ step<T>(name: string, fn: () => T | Promise<T>, retry?: ExecutorRetryPolicy): Promise<T>;
235
+ /** Add to a counter. Committed with the pass; discarded if the pass fails. */
236
+ count(name: string, by?: number): void;
237
+ /**
238
+ * The scope, through the SYSTEM door — the same `getSystemScope` a declared
239
+ * schedule invokes through, so a job's writes are attributed `{ system: moduleId }`
240
+ * and gated by an ordinary `ctx.check` against `system:<moduleId>` grants. Opened
241
+ * on first call, then reused for the rest of the pass.
242
+ */
243
+ scope(): Promise<ScopeStub>;
244
+ }
245
+ /**
246
+ * A job's body: one pass, given where the last one stopped.
247
+ *
248
+ * HOST code, never module code — a walk of an external system holds credentials and
249
+ * makes network calls, which is exactly what module code may not do. It reaches the
250
+ * scope the way a schedule does, through `pass.scope()`.
251
+ */
252
+ export type JobHandler = (pass: JobPassContext) => JobPassResult | void | Promise<JobPassResult | void>;
253
+ /** What `runDueJobs` did in one call. */
254
+ export interface JobDriveReport {
255
+ /** Runs this call picked up — due, and `running`. */
256
+ attempted: number;
257
+ /** Passes that COMMITTED with the run still going. With `maxPasses > 1`, more than one per run. */
258
+ advanced: number;
259
+ /** Runs that reported `done` this call. */
260
+ completed: number;
261
+ /** Passes that failed with retries left — the run stays `running`, due after its backoff. */
262
+ retrying: number;
263
+ /** Runs whose step exhausted its retries this call. Terminal: `failed`, with the error on the record. */
264
+ failed: number;
265
+ /** Per-run failures, the same shape `ScheduleRunReport.errors` has: what failed and why. */
266
+ errors: {
267
+ runId: string;
268
+ error: string;
269
+ }[];
270
+ }
271
+ /** The `_substrat_job_runs` row, as both adapters' SQL returns it. */
272
+ export interface JobRunRow {
273
+ readonly id: string;
274
+ readonly module_id: string;
275
+ readonly job: string;
276
+ readonly instance: string;
277
+ readonly payload: string;
278
+ readonly status: string;
279
+ readonly cursor: string | null;
280
+ readonly counters: string;
281
+ readonly attempts: number;
282
+ readonly last_error: string | null;
283
+ readonly started_at: string;
284
+ readonly updated_at: string;
285
+ readonly next_attempt_at: string | null;
286
+ readonly ended_at: string | null;
287
+ }
288
+ /** The `_substrat_job_steps` row, as both adapters' SQL returns it. */
289
+ export interface JobStepRow {
290
+ readonly step: string;
291
+ readonly result: string | null;
292
+ readonly attempts: number;
293
+ readonly last_error: string | null;
294
+ }
295
+ /** Everything a pass writes onto the run row, in one statement — never a partial update. */
296
+ export interface JobRunPatch {
297
+ readonly status: JobRunStatus;
298
+ readonly cursor: string | null;
299
+ readonly counters: string;
300
+ readonly attempts: number;
301
+ readonly lastError: string | null;
302
+ readonly updatedAt: string;
303
+ readonly nextAttemptAt: string | null;
304
+ readonly endedAt: string | null;
305
+ }
306
+ /**
307
+ * The storage the driver runs against — the ONE thing each adapter supplies.
308
+ *
309
+ * D-14's two drivers are two implementations of this and nothing else: the pure
310
+ * adapter's is direct SQL on the scope db, the hosted adapter's is RPC to the scope
311
+ * DO. Every decision — what coalescing means, when a step is skipped, when a run
312
+ * fails — lives in the functions below, so the two drivers cannot disagree about
313
+ * any of it. What they may legitimately differ on is only how a row is read.
314
+ *
315
+ * **Each step commits on its own round trip**, and that is not an accident of the
316
+ * port's shape. Batching a pass's steps into one write at the end would lose exactly
317
+ * the work a mid-pass kill is supposed to keep.
318
+ */
319
+ export interface JobRunStore {
320
+ /**
321
+ * The coalescing decision itself, as ONE operation: return the live (`running`)
322
+ * run for `row`'s key if there is one, otherwise insert `row` and return it.
323
+ *
324
+ * **Atomic, and it has to be.** Split into a lookup and an insert — which is how
325
+ * this was first written — two concurrent starts both see no live row and both
326
+ * insert, and the schema deliberately carries no constraint that could reject the
327
+ * second. The result is two runs walking one source: exactly what "a start against
328
+ * a live key joins it" promises not to happen, defeated by the promise's own
329
+ * mechanism. Nothing underneath made it safe: no unique index (by design), no
330
+ * transaction, and neither the pure host's actor queue nor a single DO RPC wrapped
331
+ * the pair.
332
+ *
333
+ * It is the port's job rather than the kernel's because atomicity is the one thing
334
+ * only the adapter can supply — a transaction on the pure side, a single RPC on the
335
+ * DO side, where the input gate makes one round trip indivisible.
336
+ */
337
+ startOrJoin(key: JobRunKey, row: JobRunRow): Promise<JobRunRow>;
338
+ /** One run by id, whatever its status. */
339
+ get(id: string): Promise<JobRunRow | null>;
340
+ /**
341
+ * `running` runs whose `next_attempt_at` has passed (or is NULL), oldest first,
342
+ * starting strictly after `afterId` when one is given.
343
+ *
344
+ * The cursor exists for starvation, not for paging convenience: the driver skips
345
+ * runs whose job this host does not register, and without a cursor those rows sit
346
+ * at the head of every batch forever, so a scope holding `limit` of them never
347
+ * drives anything newer. See `runDueJobRuns`.
348
+ */
349
+ due(now: string, limit: number, afterId?: string): Promise<JobRunRow[]>;
350
+ list(filter: JobRunFilter): Promise<JobRunRow[]>;
351
+ patch(id: string, patch: JobRunPatch): Promise<void>;
352
+ /**
353
+ * A COMMITTED pass: write the run's new state and drop its step ledger together,
354
+ * indivisibly.
355
+ *
356
+ * Two statements, one operation, for the reason `startOrJoin` is one: a stop
357
+ * between them leaves the advanced cursor beside the finished pass's memo rows,
358
+ * and the next pass — which is entitled to reuse a step name, since the
359
+ * determinism rule binds names to the payload and prior results, NOT to the
360
+ * cursor — reads that stale memo and skips work it never did. The first cut of
361
+ * this file ordered the two calls carefully and explained in a comment why the
362
+ * gap was harmless. The comment was wrong; only atomicity makes it true.
363
+ */
364
+ commitPass(id: string, patch: JobRunPatch): Promise<void>;
365
+ step(runId: string, name: string): Promise<JobStepRow | null>;
366
+ recordStep(runId: string, name: string, result: string | null, attempts: number, lastError: string | null, at: string): Promise<void>;
367
+ }
368
+ /** `conflict` reason: two `step()` calls under one name in one pass. */
369
+ export declare const JOB_STEP_REUSED = "job_step_reused";
370
+ /**
371
+ * Refuse anything that cannot survive a queue message, naming the path.
372
+ *
373
+ * What may be handed to a run, and handed forward by one, is **ids and
374
+ * configuration**: JSON's own values, nothing else. Bytes, class instances and
375
+ * functions are refused — and so, less obviously, are `undefined` in a nested
376
+ * position, a non-finite number and a cycle, because JSON turns each of those into
377
+ * something else without saying so. A payload silently reshaped on its way to
378
+ * storage is the failure this rule exists to prevent: the run resumes against a
379
+ * value that is not the one it was started with, and nothing anywhere reports it.
380
+ *
381
+ * `validation_failed` with the offending path in `errors`, exactly as an operation's
382
+ * own input failure arrives, so a transport renders it with no special case.
383
+ *
384
+ * Held against the payload at `start` and against the cursor at every commit —
385
+ * the two values that cross the boundary and land on the record. NOT held against a
386
+ * step's result, deliberately: that value is the handler's own, round-tripped to
387
+ * itself on resume, and the common shape of a step done for its effect is to return
388
+ * nothing at all — which this would refuse.
389
+ */
390
+ export declare function assertQueueSafe(value: unknown, root: string): void;
391
+ /**
392
+ * A row, decoded into the record an operator reads — TOLERANTLY, and saying so.
393
+ *
394
+ * The status, the attempts, the timestamps and the `last_error` are columns and
395
+ * always readable; only `payload`, `cursor` and `counters` are JSON, and a row whose
396
+ * JSON will not parse still has to be visible. It comes back with those three empty
397
+ * and `decodeError` naming the parse failure — never a bare `null` cursor, which a
398
+ * reader would take for "no pass has committed yet".
399
+ *
400
+ * This is deliberately NOT what the driver does with the same row: a pass cannot run
401
+ * on a payload it cannot decode, so `runJobPass` treats the parse failure as a failed
402
+ * pass and lets the run retry and then fail with the reason on its record. Strict
403
+ * where work happens, tolerant where evidence is read.
404
+ */
405
+ export declare function jobRunOf(row: JobRunRow): JobRun;
406
+ /**
407
+ * Start a run, or JOIN the one already in flight (#1577's first acceptance).
408
+ *
409
+ * The payload is refused here, before a row exists, so a run is never recorded
410
+ * carrying a value it cannot resume from.
411
+ *
412
+ * The join returns the LIVE run unchanged — its cursor, its counters, its start
413
+ * time. A second caller therefore learns the id of the walk that is already
414
+ * happening and can watch it, which is what "joins the first" has to mean for the
415
+ * caller to be able to do anything with the answer.
416
+ *
417
+ * **The lookup and the insert are ONE store operation** (`startOrJoin`), not two.
418
+ * As two, concurrent starts both find no live run and both insert — and there is no
419
+ * unique index to catch the second, deliberately, because a crashed run must stay
420
+ * restartable. The coalescing guarantee would then be false exactly when it is load
421
+ * bearing: two callers asking at once, which is the case it exists for.
422
+ */
423
+ export declare function startJobRun(store: JobRunStore, input: StartJobRunInput, mintId: () => string, now: () => string): Promise<JobRunRow>;
424
+ /** What one pass did, as the drive loop reads it. */
425
+ export interface JobPassOutcome {
426
+ status: 'advanced' | 'completed' | 'retrying' | 'failed';
427
+ /** The error left on the record. Present exactly on `retrying` and `failed`. */
428
+ error?: string;
429
+ }
430
+ /**
431
+ * Run ONE pass of one run, and write what happened.
432
+ *
433
+ * The whole of the contract is here, which is why both adapters call it rather than
434
+ * porting it:
435
+ *
436
+ * - A committed pass writes the new cursor and counters, clears `attempts` and
437
+ * `last_error`, and DROPS the step ledger — a new pass is new work.
438
+ * - A failed pass writes NOTHING the handler produced: the cursor and counters stay
439
+ * where the last commit left them, so a partially-walked chunk is never mistaken
440
+ * for a committed one. The step ledger SURVIVES, which is what stops the next pass
441
+ * repeating the steps that did succeed.
442
+ * - A step at its `maxAttempts` fails the RUN: status `failed`, the step's error on
443
+ * the record, `ended_at` stamped. It is reported, never thrown — a driver that
444
+ * let one run's failure escape would take down every run behind it, which is the
445
+ * failure `runDueSchedules` already refuses for schedules.
446
+ */
447
+ export declare function runJobPass(options: {
448
+ store: JobRunStore;
449
+ run: JobRunRow;
450
+ handler: JobHandler;
451
+ /** The job's own retry policy — a step may narrow it, none may widen past its own. */
452
+ retry?: ExecutorRetryPolicy;
453
+ now: () => string;
454
+ /** Opens the system door for this run's module. Called at most once per pass. */
455
+ openScope: () => Promise<ScopeStub>;
456
+ }): Promise<JobPassOutcome>;
457
+ /**
458
+ * Advance every due run on one scope — the driver both adapters expose as
459
+ * `runDueJobs`.
460
+ *
461
+ * Bounded on both axes, because a maintenance tick has a budget: `limit` runs per
462
+ * call, `maxPasses` passes per run. A run that still has work left after its budget
463
+ * is simply left `running` and due, and the next call takes it — the same "reported
464
+ * rather than looped" shape the event drain's `incomplete` has. Default `maxPasses`
465
+ * is 1, so a caller gets one predictable unit of work unless it asks for more.
466
+ *
467
+ * **`limit` counts RUNNABLE runs, not rows read**, and that distinction is a fix
468
+ * rather than a nicety. Runs whose job this host does not register are skipped, and
469
+ * when the budget was applied to the query instead, a scope holding `limit` such rows
470
+ * at the head of the due order returned the same unrunnable batch on every call —
471
+ * nothing newer was ever reached, and the report said `attempted: 0` forever with no
472
+ * indication why. So the read pages past them, bounded by `JOB_DRIVE_SCAN_MAX` rows
473
+ * examined so one scope full of orphans cannot turn a tick into a table scan.
474
+ *
475
+ * The starvation was spotted while re-reading this file, judged unlikely and left
476
+ * alone — and then found independently by a reviewer. The judgement may even have
477
+ * been right; recording it only in the author's head was not, because a decision
478
+ * nobody can see is indistinguishable from an oversight. Hence the fix and hence
479
+ * this paragraph.
480
+ */
481
+ export declare function runDueJobRuns(options: {
482
+ store: JobRunStore;
483
+ /** job name → its handler and policy, as `registerJob` recorded them. */
484
+ handlerFor: (run: JobRunRow) => {
485
+ handler: JobHandler;
486
+ retry?: ExecutorRetryPolicy;
487
+ } | undefined;
488
+ now: () => string;
489
+ openScope: (run: JobRunRow) => Promise<ScopeStub>;
490
+ maxPasses?: number;
491
+ limit?: number;
492
+ }): Promise<JobDriveReport>;
493
+ //# sourceMappingURL=job-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-run.d.ts","sourceRoot":"","sources":["../src/job-run.ts"],"names":[],"mappings":"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"}