@untangle-code/jobs 0.0.0-stage → 0.1.1

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 (50) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +138 -2
  4. package/THIRD_PARTY_LICENSES.md +24 -0
  5. package/dist/abort.d.ts +14 -0
  6. package/dist/abort.d.ts.map +1 -0
  7. package/dist/abort.js +35 -0
  8. package/dist/abort.js.map +1 -0
  9. package/dist/bounded-queue.d.ts +200 -0
  10. package/dist/bounded-queue.d.ts.map +1 -0
  11. package/dist/bounded-queue.js +338 -0
  12. package/dist/bounded-queue.js.map +1 -0
  13. package/dist/checkpoint-store.d.ts +30 -0
  14. package/dist/checkpoint-store.d.ts.map +1 -0
  15. package/dist/checkpoint-store.js +131 -0
  16. package/dist/checkpoint-store.js.map +1 -0
  17. package/dist/events.d.ts +29 -0
  18. package/dist/events.d.ts.map +1 -0
  19. package/dist/events.js +66 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/index.d.ts +22 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +30 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/resume.d.ts +45 -0
  26. package/dist/resume.d.ts.map +1 -0
  27. package/dist/resume.js +60 -0
  28. package/dist/resume.js.map +1 -0
  29. package/dist/runner.d.ts +148 -0
  30. package/dist/runner.d.ts.map +1 -0
  31. package/dist/runner.js +686 -0
  32. package/dist/runner.js.map +1 -0
  33. package/dist/stages.d.ts +146 -0
  34. package/dist/stages.d.ts.map +1 -0
  35. package/dist/stages.js +2 -0
  36. package/dist/stages.js.map +1 -0
  37. package/dist/state-machine.d.ts +52 -0
  38. package/dist/state-machine.d.ts.map +1 -0
  39. package/dist/state-machine.js +114 -0
  40. package/dist/state-machine.js.map +1 -0
  41. package/package.json +61 -3
  42. package/src/abort.ts +37 -0
  43. package/src/bounded-queue.ts +462 -0
  44. package/src/checkpoint-store.ts +171 -0
  45. package/src/events.ts +71 -0
  46. package/src/index.ts +37 -0
  47. package/src/resume.ts +106 -0
  48. package/src/runner.ts +916 -0
  49. package/src/stages.ts +167 -0
  50. package/src/state-machine.ts +132 -0
@@ -0,0 +1,338 @@
1
+ import { indexIncompleteError } from "@untangle-code/observability";
2
+ import { abortedPromise, throwIfAborted } from "./abort.js";
3
+ /**
4
+ * Vorgabe fuer `maxOverBudgetAdmissions`: genau EIN Element, das das Budget
5
+ * allein sprengt, darf passieren (sonst haengt die Queue an ihm fest). Jedes
6
+ * weitere ist ein Konfigurationsfehler und wird als solcher gemeldet.
7
+ */
8
+ const DEFAULT_OVER_BUDGET_ALLOWANCE = 1;
9
+ export class BoundedQueue {
10
+ slots = [];
11
+ capacity;
12
+ maxBytes;
13
+ sizeOf;
14
+ overBudgetAllowance;
15
+ onOverBudgetAdmission;
16
+ overBudgetAdmissions = 0;
17
+ maxItemBytesObserved = 0;
18
+ bytes = 0;
19
+ closed = false;
20
+ failure = null;
21
+ wakers = [];
22
+ maxDepthObserved = 0;
23
+ maxBytesObserved = 0;
24
+ constructor(options) {
25
+ // WP-116: `capacity` ist ueber `IndexJobRunnerOptions.queueCapacities`
26
+ // frei konfigurierbar und entscheidet mit darueber, ob die Byte-Grenze
27
+ // ueberhaupt greifen kann — deshalb wird sie hier so hart geprueft wie
28
+ // `maxBytes`. `Number.isInteger(1e21)` ist true, `isSafeInteger` nicht:
29
+ // eine Kapazitaet jenseits der sicheren Ganzzahlen ist als Grenze
30
+ // wertlos, weil Vergleiche und Zaehler darauf nicht mehr exakt sind.
31
+ if (!Number.isSafeInteger(options.capacity) || options.capacity < 1) {
32
+ throw new Error(`BoundedQueue capacity must be a safe integer >= 1, got ${String(options.capacity)}`);
33
+ }
34
+ if (options.maxBytes !== undefined) {
35
+ if (options.sizeOf === undefined) {
36
+ throw new Error("BoundedQueue maxBytes requires a sizeOf cost function");
37
+ }
38
+ // Same defect class as an invalid per-item size (see `validateSize`
39
+ // below): a non-finite/negative budget disables the byte check just
40
+ // as surely as a poisoned item does, only permanently instead of
41
+ // transiently — e.g. `Math.floor(memoryBudgetBytes / 2)` silently
42
+ // produces `NaN`/`Infinity` when a caller's `memoryBudgetBytes` is
43
+ // itself malformed (runner.ts). Reject at construction, before any
44
+ // item is ever admitted.
45
+ BoundedQueue.validateSize(options.maxBytes, "maxBytes");
46
+ }
47
+ const allowance = options.maxOverBudgetAdmissions ?? DEFAULT_OVER_BUDGET_ALLOWANCE;
48
+ if (!Number.isSafeInteger(allowance) || allowance < 0) {
49
+ throw new Error(`BoundedQueue maxOverBudgetAdmissions must be a safe integer >= 0, got ${String(options.maxOverBudgetAdmissions)}`);
50
+ }
51
+ this.capacity = options.capacity;
52
+ this.maxBytes = options.maxBytes ?? null;
53
+ this.sizeOf = options.sizeOf ?? null;
54
+ this.overBudgetAllowance = allowance;
55
+ this.onOverBudgetAdmission = options.onOverBudgetAdmission ?? null;
56
+ }
57
+ /**
58
+ * Rejects a caller-supplied byte size (a per-item `sizeOf(item)` result or
59
+ * the constructor's `maxBytes`) that is not finite and >= 0.
60
+ *
61
+ * This is deliberately a hard rejection, never a clamp to some
62
+ * conservative substitute: the whole point of `maxBytes`/`sizeOf` is that
63
+ * the QUEUE trusts the caller's cost estimate to keep the pipeline's
64
+ * memory bound truthful (architecture.md §13.2, "a hard limit, not a
65
+ * hope"). A `NaN`/`Infinity`/negative size means that trust is already
66
+ * broken — the caller's cost model produced a value with no relationship
67
+ * to real memory use — so any number the queue might invent instead
68
+ * (0, the budget itself, the last good size, …) would just re-establish
69
+ * that same false trust one layer down and let corrupted accounting
70
+ * accumulate exactly as the adversarial repro showed (`Infinity` on an
71
+ * empty queue turns `bytes` into `NaN` on the very next `pull()`, which
72
+ * then compares `false` against every future budget check forever). A
73
+ * caller that cannot produce a valid size has a bug that must surface as
74
+ * a failed job (spec.md §25.2: write-side operations fail closed when
75
+ * consistency cannot be guaranteed), not as a silently-disabled budget.
76
+ */
77
+ static validateSize(size, field) {
78
+ if (Number.isFinite(size) && size >= 0) {
79
+ return;
80
+ }
81
+ throw indexIncompleteError(`BoundedQueue received an invalid byte size from ${field} (${String(size)}); ` +
82
+ "sizes must be finite numbers >= 0 — a NaN/Infinity/negative size would " +
83
+ "silently disable the byte budget (architecture.md §13.2)", { retryable: false, operation: "bounded_queue_size", context: { field, size } });
84
+ }
85
+ /**
86
+ * Defense-in-depth invariant on the running `bytes` counter itself, on
87
+ * top of `validateSize()` at every entry point: `bytes` is a sum of
88
+ * values that already passed `validateSize()`, so it should never go
89
+ * non-finite or negative — but "should never" is not a guarantee, and a
90
+ * counter that silently drifts into `NaN`/negative is exactly as
91
+ * dangerous as an unchecked input (it disables the same budget check the
92
+ * same way). Called after every mutation of `bytes`; poisons the queue
93
+ * and throws rather than letting `push()`/`pull()` keep computing on a
94
+ * counter that can no longer prove the budget (spec.md §25.2, fail
95
+ * closed).
96
+ */
97
+ assertByteCounterIntact(operation) {
98
+ if (Number.isFinite(this.bytes) && this.bytes >= 0) {
99
+ return;
100
+ }
101
+ const corrupted = indexIncompleteError(`BoundedQueue's internal byte counter became invalid (${String(this.bytes)}) during ` +
102
+ `${operation}() despite validated inputs — refusing to continue accounting on a ` +
103
+ "counter that can no longer prove the byte budget (architecture.md §13.2)", { retryable: false, operation: `bounded_queue_${operation}`, context: { bytes: this.bytes } });
104
+ // Poison rather than merely throw: a corrupted counter is a queue-wide
105
+ // invariant violation, not a one-off bad call — every other pending and
106
+ // future push()/pull() must see the same failure.
107
+ this.fail(corrupted);
108
+ throw corrupted;
109
+ }
110
+ notify() {
111
+ const wakers = this.wakers;
112
+ this.wakers = [];
113
+ for (const wake of wakers) {
114
+ wake();
115
+ }
116
+ }
117
+ async waitForChange(signal) {
118
+ const wake = new Promise((resolve) => this.wakers.push(resolve));
119
+ if (signal === undefined) {
120
+ await wake;
121
+ return;
122
+ }
123
+ const cleanup = new AbortController();
124
+ try {
125
+ await Promise.race([wake, abortedPromise(signal, cleanup.signal)]);
126
+ }
127
+ finally {
128
+ cleanup.abort();
129
+ }
130
+ }
131
+ /**
132
+ * Entscheidet ueber eine Aufnahme, ohne etwas zu veraendern.
133
+ *
134
+ * - `"admit"` — Element passt in beide Grenzen.
135
+ * - `"wait"` — kein Platz; Warten kann Platz schaffen (Rueckstau).
136
+ * - `"over-budget"` — Element ist ALLEIN groesser als das ganze Budget:
137
+ * Warten kann fuer dieses Element nie Platz schaffen, die Ausnahme greift.
138
+ *
139
+ * WP-116: Frueher haengte der Byte-Zweig an `slots.length > 0`. Auf einer
140
+ * leeren Queue ist `bytes === 0`, also fiel „Element passt nicht ins
141
+ * Budget" mit „Element ist allein zu gross" nur zufaellig zusammen — und
142
+ * bei `capacity: 1` ist die Queue vor JEDEM Push leer, womit die Ausnahme
143
+ * zum Dauerzustand wurde und die Byte-Grenze mit voellig gueltigen Werten
144
+ * abgeschaltet war (belegt: 20 x 50 MB durch ein 100-Byte-Budget). Die
145
+ * Bedingung steht jetzt dort, wo sie hingehoert: am ELEMENT
146
+ * (`size > maxBytes`), nicht am Fuellstand.
147
+ */
148
+ admissionVerdict(size) {
149
+ if (this.slots.length >= this.capacity) {
150
+ return "wait";
151
+ }
152
+ if (this.maxBytes === null) {
153
+ return "admit";
154
+ }
155
+ this.assertEmptyStateCounter();
156
+ if (this.bytes + size <= this.maxBytes) {
157
+ return "admit";
158
+ }
159
+ if (size > this.maxBytes) {
160
+ // Das Element sprengt das Budget aus eigener Kraft. Es wird nur in die
161
+ // LEERE Queue aufgenommen, damit die Verletzung nie mit anderen
162
+ // Elementen kumuliert; ist noch etwas gepuffert, schafft Warten sehr
163
+ // wohl Platz (naemlich die leere Queue).
164
+ return this.slots.length === 0 ? "over-budget" : "wait";
165
+ }
166
+ // Passt grundsaetzlich ins Budget, gerade nur nicht: klassischer
167
+ // Rueckstau — genau der Mechanismus, den §13.2 will.
168
+ return "wait";
169
+ }
170
+ /**
171
+ * Invariante der leeren Queue: ohne gepufferte Elemente MUSS `bytes` 0
172
+ * sein. Frueher hat der Byte-Zweig diese Annahme stillschweigend benutzt
173
+ * (`slots.length > 0` als Ersatz fuer `bytes === 0`); jetzt wird sie
174
+ * geprueft, damit eine kuenftige Abweichung als Fehler auffaellt und nicht
175
+ * als lautlos abgeschaltete Grenze.
176
+ */
177
+ assertEmptyStateCounter() {
178
+ if (this.slots.length !== 0 || this.bytes === 0) {
179
+ return;
180
+ }
181
+ const corrupted = indexIncompleteError(`BoundedQueue holds no items but its byte counter reads ${String(this.bytes)} — the byte ` +
182
+ "budget can no longer be proven, refusing to admit further items (architecture.md §13.2)", {
183
+ retryable: false,
184
+ operation: "bounded_queue_empty_state",
185
+ context: { bytes: this.bytes, depth: this.slots.length },
186
+ });
187
+ this.fail(corrupted);
188
+ throw corrupted;
189
+ }
190
+ /**
191
+ * Bucht eine Ausnahme. Ist das Kontingent erschoepft, scheitert der Push
192
+ * mit benanntem, nicht wiederholbarem Fehler — fail closed (spec.md
193
+ * §25.2) statt einer Budgetverletzung, die niemand sieht. Absichtlich
194
+ * KEIN Warten: fuer ein Element groesser als das Budget kann Warten nie
195
+ * Platz schaffen, ein Warten waere ein Haenger statt eines Rueckstaus.
196
+ */
197
+ admitOverBudget(size) {
198
+ const maxBytes = this.maxBytes ?? 0;
199
+ if (this.overBudgetAdmissions >= this.overBudgetAllowance) {
200
+ throw indexIncompleteError(`BoundedQueue refuses a ${String(size)}-byte item: it exceeds the queue's entire ` +
201
+ `${String(maxBytes)}-byte budget on its own, and this queue already admitted ` +
202
+ `${String(this.overBudgetAdmissions)} such item(s) (allowance ` +
203
+ `${String(this.overBudgetAllowance)}). The exception exists for a SINGLE oversized ` +
204
+ "item that would otherwise block the queue forever, not as a permanent state that " +
205
+ "turns the byte budget off (architecture.md §13.2). Raise the byte budget, reduce the " +
206
+ "item size, or skip this item and record the skip with a reason.", {
207
+ retryable: false,
208
+ operation: "bounded_queue_push",
209
+ context: {
210
+ itemBytes: size,
211
+ maxBytes,
212
+ overBudgetAdmissions: this.overBudgetAdmissions,
213
+ allowance: this.overBudgetAllowance,
214
+ },
215
+ });
216
+ }
217
+ this.overBudgetAdmissions += 1;
218
+ this.onOverBudgetAdmission?.({
219
+ itemBytes: size,
220
+ maxBytes,
221
+ ordinal: this.overBudgetAdmissions,
222
+ allowance: this.overBudgetAllowance,
223
+ });
224
+ }
225
+ /**
226
+ * Enqueues `item`, waiting while the queue is full (count or byte bound) —
227
+ * this wait IS the backpressure that pauses the previous stage. Rejects
228
+ * with `JOB_CANCELLED` when `options.signal` aborts, and with the failure
229
+ * error after `fail()`.
230
+ */
231
+ async push(item, options) {
232
+ const size = this.sizeOf === null ? 0 : this.sizeOf(item);
233
+ // Validate BEFORE the item ever reaches `hasRoomFor()`/`bytes` — an
234
+ // invalid size must never be accepted "for now" and dealt with later;
235
+ // by the time it is in `bytes` it has already corrupted the budget.
236
+ BoundedQueue.validateSize(size, "sizeOf(item)");
237
+ while (true) {
238
+ if (this.failure !== null) {
239
+ throw this.failure.error;
240
+ }
241
+ if (this.closed) {
242
+ throw new Error("BoundedQueue.push() called after close()");
243
+ }
244
+ throwIfAborted(options?.signal, "bounded_queue_push");
245
+ const verdict = this.admissionVerdict(size);
246
+ if (verdict === "admit") {
247
+ break;
248
+ }
249
+ if (verdict === "over-budget") {
250
+ // Bucht die Ausnahme oder wirft, wenn das Kontingent aufgebraucht
251
+ // ist. Erst NACH dieser Buchung wird das Element aufgenommen.
252
+ this.admitOverBudget(size);
253
+ break;
254
+ }
255
+ await this.waitForChange(options?.signal);
256
+ }
257
+ this.slots.push({ item, size });
258
+ this.bytes += size;
259
+ this.assertByteCounterIntact("push");
260
+ this.maxDepthObserved = Math.max(this.maxDepthObserved, this.slots.length);
261
+ this.maxBytesObserved = Math.max(this.maxBytesObserved, this.bytes);
262
+ this.maxItemBytesObserved = Math.max(this.maxItemBytesObserved, size);
263
+ this.notify();
264
+ }
265
+ /**
266
+ * Dequeues the oldest item, waiting while the queue is empty. Returns
267
+ * `{ done: true }` once the queue is closed AND drained. Rejects with
268
+ * `JOB_CANCELLED` when `options.signal` aborts, and with the failure
269
+ * error after `fail()` (buffered items are NOT delivered after a failure —
270
+ * the pipeline is broken and must not continue on stale work).
271
+ */
272
+ async pull(options) {
273
+ while (true) {
274
+ if (this.failure !== null) {
275
+ throw this.failure.error;
276
+ }
277
+ throwIfAborted(options?.signal, "bounded_queue_pull");
278
+ const slot = this.slots.shift();
279
+ if (slot !== undefined) {
280
+ this.bytes -= slot.size;
281
+ this.assertByteCounterIntact("pull");
282
+ this.notify();
283
+ return { done: false, value: slot.item };
284
+ }
285
+ if (this.closed) {
286
+ return { done: true };
287
+ }
288
+ await this.waitForChange(options?.signal);
289
+ }
290
+ }
291
+ /** Non-blocking pull: `null` when nothing is buffered right now. */
292
+ tryPull() {
293
+ if (this.failure !== null) {
294
+ throw this.failure.error;
295
+ }
296
+ const slot = this.slots.shift();
297
+ if (slot !== undefined) {
298
+ this.bytes -= slot.size;
299
+ this.assertByteCounterIntact("tryPull");
300
+ this.notify();
301
+ return { done: false, value: slot.item };
302
+ }
303
+ if (this.closed) {
304
+ return { done: true };
305
+ }
306
+ return null;
307
+ }
308
+ /** No more pushes; pulls drain the remaining items, then report done. */
309
+ close() {
310
+ this.closed = true;
311
+ this.notify();
312
+ }
313
+ /**
314
+ * Poisons the queue: every pending and future `push`/`pull` rejects with
315
+ * `error`. Used to unblock sibling pipeline workers when one stage fails.
316
+ */
317
+ fail(error) {
318
+ if (this.failure === null) {
319
+ this.failure = { error };
320
+ }
321
+ this.closed = true;
322
+ this.notify();
323
+ }
324
+ stats() {
325
+ return {
326
+ depth: this.slots.length,
327
+ capacity: this.capacity,
328
+ bytes: this.bytes,
329
+ maxBytes: this.maxBytes,
330
+ maxDepthObserved: this.maxDepthObserved,
331
+ maxBytesObserved: this.maxBytesObserved,
332
+ overBudgetAdmissions: this.overBudgetAdmissions,
333
+ overBudgetAllowance: this.overBudgetAllowance,
334
+ maxItemBytesObserved: this.maxItemBytesObserved,
335
+ };
336
+ }
337
+ }
338
+ //# sourceMappingURL=bounded-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded-queue.js","sourceRoot":"","sources":["../src/bounded-queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * File-backed `CheckpointStorePort` (architecture.md §6.14, §13.3) on top of
3
+ * the injected `FileSystemPort` — atomic via the temp+rename pattern the
4
+ * port guarantees for `rename`.
5
+ *
6
+ * Error semantics (spec.md §25.2): a MISSING checkpoint is `null` — never an
7
+ * error; a CORRUPT checkpoint file is an `UntangleError` with code
8
+ * `INDEX_INCOMPLETE` — never silently treated as "not found", because that
9
+ * would silently discard confirmed work and restart from scratch.
10
+ */
11
+ import type { FileSystemPort, IndexCheckpointV1, PortCallOptions, CheckpointStorePort } from "@untangle-code/domain";
12
+ /** Structural guard for the exact `IndexCheckpointV1` shape (§13.3). */
13
+ export declare function isIndexCheckpointV1(value: unknown): value is IndexCheckpointV1;
14
+ export interface FileCheckpointStoreOptions {
15
+ /** Directory the checkpoint JSON files live in; created on first save. */
16
+ directory: string;
17
+ }
18
+ export declare class FileCheckpointStore implements CheckpointStorePort {
19
+ private readonly fs;
20
+ private readonly directory;
21
+ private tmpCounter;
22
+ constructor(fs: FileSystemPort, options: FileCheckpointStoreOptions);
23
+ private pathFor;
24
+ save(checkpoint: IndexCheckpointV1, options?: PortCallOptions): Promise<void>;
25
+ load(jobId: string, options?: PortCallOptions): Promise<IndexCheckpointV1 | null>;
26
+ loadLatest(repoId: string, refId: string, options?: PortCallOptions): Promise<IndexCheckpointV1 | null>;
27
+ delete(jobId: string, options?: PortCallOptions): Promise<void>;
28
+ private parseCheckpoint;
29
+ }
30
+ //# sourceMappingURL=checkpoint-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-store.d.ts","sourceRoot":"","sources":["../src/checkpoint-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAwB/B,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAkB9E;AAED,MAAM,WAAW,0BAA0B;IACzC,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,mBAAoB,YAAW,mBAAmB;IAC7D,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,UAAU,CAAK;gBAEJ,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,0BAA0B;IAK1E,OAAO,CAAC,OAAO;IAOF,IAAI,CAAC,UAAU,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB7E,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAWjF,UAAU,CACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IA4BvB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5E,OAAO,CAAC,eAAe;CAqBxB"}
@@ -0,0 +1,131 @@
1
+ import { sha256Hex } from "@untangle-code/domain";
2
+ import { indexIncompleteError } from "@untangle-code/observability";
3
+ import { throwIfAborted } from "./abort.js";
4
+ function isStringArray(value) {
5
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
6
+ }
7
+ function isStringRecord(value) {
8
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
9
+ return false;
10
+ }
11
+ return Object.values(value).every((entry) => typeof entry === "string");
12
+ }
13
+ function isStringArrayRecord(value) {
14
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
15
+ return false;
16
+ }
17
+ return Object.values(value).every((entry) => isStringArray(entry));
18
+ }
19
+ /** Structural guard for the exact `IndexCheckpointV1` shape (§13.3). */
20
+ export function isIndexCheckpointV1(value) {
21
+ if (typeof value !== "object" || value === null) {
22
+ return false;
23
+ }
24
+ const record = value;
25
+ return (typeof record["jobId"] === "string" &&
26
+ typeof record["generationId"] === "string" &&
27
+ typeof record["workspaceId"] === "string" &&
28
+ typeof record["repoId"] === "string" &&
29
+ typeof record["refId"] === "string" &&
30
+ typeof record["effectiveProfileId"] === "string" &&
31
+ isStringArray(record["completedFiles"]) &&
32
+ isStringArrayRecord(record["storedPointIdsByFile"]) &&
33
+ isStringRecord(record["manifestHashes"]) &&
34
+ typeof record["stage"] === "string" &&
35
+ typeof record["savedAt"] === "string");
36
+ }
37
+ export class FileCheckpointStore {
38
+ fs;
39
+ directory;
40
+ tmpCounter = 0;
41
+ constructor(fs, options) {
42
+ this.fs = fs;
43
+ this.directory = options.directory.replace(/\/+$/u, "");
44
+ }
45
+ pathFor(jobId) {
46
+ // jobIds are usually UUIDs, but the file name must stay collision-free
47
+ // and path-safe for ANY id: sanitize, then disambiguate with a hash.
48
+ const safe = jobId.replace(/[^A-Za-z0-9._-]/gu, "_");
49
+ return `${this.directory}/${safe}-${sha256Hex(jobId).slice(0, 8)}.json`;
50
+ }
51
+ async save(checkpoint, options) {
52
+ throwIfAborted(options?.signal, "checkpoint_save");
53
+ if (!isIndexCheckpointV1(checkpoint)) {
54
+ throw indexIncompleteError("refusing to persist a malformed checkpoint", {
55
+ retryable: false,
56
+ operation: "checkpoint_save",
57
+ });
58
+ }
59
+ await this.fs.mkdir(this.directory, { recursive: true, ...(options ?? {}) });
60
+ const path = this.pathFor(checkpoint.jobId);
61
+ this.tmpCounter += 1;
62
+ const tmpPath = `${path}.tmp-${this.tmpCounter}`;
63
+ const bytes = new TextEncoder().encode(`${JSON.stringify(checkpoint, null, 2)}\n`);
64
+ await this.fs.writeFile(tmpPath, bytes, options);
65
+ await this.fs.rename(tmpPath, path, options);
66
+ }
67
+ async load(jobId, options) {
68
+ throwIfAborted(options?.signal, "checkpoint_load");
69
+ const path = this.pathFor(jobId);
70
+ const stat = await this.fs.stat(path, options);
71
+ if (stat === null) {
72
+ return null;
73
+ }
74
+ const text = await this.fs.readTextFile(path, options);
75
+ return this.parseCheckpoint(text, path);
76
+ }
77
+ async loadLatest(repoId, refId, options) {
78
+ throwIfAborted(options?.signal, "checkpoint_load_latest");
79
+ const dirStat = await this.fs.stat(this.directory, options);
80
+ if (dirStat === null) {
81
+ return null;
82
+ }
83
+ const entries = await this.fs.readDir(this.directory, options);
84
+ let latest = null;
85
+ for (const entry of entries) {
86
+ if (entry.kind !== "file" || !entry.name.endsWith(".json")) {
87
+ continue;
88
+ }
89
+ const text = await this.fs.readTextFile(entry.path, options);
90
+ const checkpoint = this.parseCheckpoint(text, entry.path);
91
+ if (checkpoint.repoId !== repoId || checkpoint.refId !== refId) {
92
+ continue;
93
+ }
94
+ if (latest === null ||
95
+ checkpoint.savedAt > latest.savedAt ||
96
+ (checkpoint.savedAt === latest.savedAt && checkpoint.jobId > latest.jobId)) {
97
+ latest = checkpoint;
98
+ }
99
+ }
100
+ return latest;
101
+ }
102
+ async delete(jobId, options) {
103
+ throwIfAborted(options?.signal, "checkpoint_delete");
104
+ // `FileSystemPort.remove` is force-style: a missing file is not an error,
105
+ // which makes delete idempotent by design.
106
+ await this.fs.remove(this.pathFor(jobId), options);
107
+ }
108
+ parseCheckpoint(text, path) {
109
+ let parsed;
110
+ try {
111
+ parsed = JSON.parse(text);
112
+ }
113
+ catch (cause) {
114
+ throw indexIncompleteError(`checkpoint file is not valid JSON: ${path}`, {
115
+ retryable: false,
116
+ operation: "checkpoint_load",
117
+ context: { path },
118
+ cause,
119
+ });
120
+ }
121
+ if (!isIndexCheckpointV1(parsed)) {
122
+ throw indexIncompleteError(`checkpoint file has an invalid shape: ${path}`, {
123
+ retryable: false,
124
+ operation: "checkpoint_load",
125
+ context: { path },
126
+ });
127
+ }
128
+ return parsed;
129
+ }
130
+ }
131
+ //# sourceMappingURL=checkpoint-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-store.js","sourceRoot":"","sources":["../src/checkpoint-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Single-consumer async event channel — the transport that makes the typed
3
+ * job events of architecture.md §2.2 consumable as an `AsyncIterable`
4
+ * (spec.md §22: `index()` returns `AsyncIterable<JobEvent>`).
5
+ *
6
+ * The producer side (`emit`) never blocks: job events are small, bounded in
7
+ * number per batch, and the pipeline's memory pressure is controlled by the
8
+ * bounded work queues, not by the event stream. `fail()` exists for
9
+ * catastrophic driver bugs only — regular job failures are emitted as
10
+ * `job_failed` events and end the stream via `close()`.
11
+ */
12
+ export declare class AsyncEventChannel<T> implements AsyncIterable<T> {
13
+ private readonly buffer;
14
+ private closed;
15
+ private failure;
16
+ private wakers;
17
+ /** Enqueues one event. Throws when the channel is already closed/failed. */
18
+ emit(event: T): void;
19
+ /** Ends the stream after all buffered events were consumed. Idempotent. */
20
+ close(): void;
21
+ /**
22
+ * Ends the stream with a rejection after all buffered events were
23
+ * consumed. Idempotent; a later `fail()` does not override an earlier one.
24
+ */
25
+ fail(error: unknown): void;
26
+ private notify;
27
+ [Symbol.asyncIterator](): AsyncIterator<T>;
28
+ }
29
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,qBAAa,iBAAiB,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,MAAM,CAAyB;IAEvC,4EAA4E;IACrE,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAQ3B,2EAA2E;IACpE,KAAK,IAAI,IAAI;IAKpB;;;OAGG;IACI,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAQjC,OAAO,CAAC,MAAM;IAQA,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC;CAiBzD"}
package/dist/events.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Single-consumer async event channel — the transport that makes the typed
3
+ * job events of architecture.md §2.2 consumable as an `AsyncIterable`
4
+ * (spec.md §22: `index()` returns `AsyncIterable<JobEvent>`).
5
+ *
6
+ * The producer side (`emit`) never blocks: job events are small, bounded in
7
+ * number per batch, and the pipeline's memory pressure is controlled by the
8
+ * bounded work queues, not by the event stream. `fail()` exists for
9
+ * catastrophic driver bugs only — regular job failures are emitted as
10
+ * `job_failed` events and end the stream via `close()`.
11
+ */
12
+ export class AsyncEventChannel {
13
+ buffer = [];
14
+ closed = false;
15
+ failure = null;
16
+ wakers = [];
17
+ /** Enqueues one event. Throws when the channel is already closed/failed. */
18
+ emit(event) {
19
+ if (this.closed) {
20
+ throw new Error("AsyncEventChannel.emit() called after close()/fail()");
21
+ }
22
+ this.buffer.push(event);
23
+ this.notify();
24
+ }
25
+ /** Ends the stream after all buffered events were consumed. Idempotent. */
26
+ close() {
27
+ this.closed = true;
28
+ this.notify();
29
+ }
30
+ /**
31
+ * Ends the stream with a rejection after all buffered events were
32
+ * consumed. Idempotent; a later `fail()` does not override an earlier one.
33
+ */
34
+ fail(error) {
35
+ if (this.failure === null) {
36
+ this.failure = { error };
37
+ }
38
+ this.closed = true;
39
+ this.notify();
40
+ }
41
+ notify() {
42
+ const wakers = this.wakers;
43
+ this.wakers = [];
44
+ for (const wake of wakers) {
45
+ wake();
46
+ }
47
+ }
48
+ async *[Symbol.asyncIterator]() {
49
+ while (true) {
50
+ if (this.buffer.length > 0) {
51
+ // `length > 0` guarantees the shift result is a T, even for a T that
52
+ // could itself include `undefined`.
53
+ yield this.buffer.shift();
54
+ continue;
55
+ }
56
+ if (this.failure !== null) {
57
+ throw this.failure.error;
58
+ }
59
+ if (this.closed) {
60
+ return;
61
+ }
62
+ await new Promise((resolve) => this.wakers.push(resolve));
63
+ }
64
+ }
65
+ }
66
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,OAAO,iBAAiB;IACX,MAAM,GAAQ,EAAE,CAAC;IAC1B,MAAM,GAAG,KAAK,CAAC;IACf,OAAO,GAA8B,IAAI,CAAC;IAC1C,MAAM,GAAsB,EAAE,CAAC;IAEvC,4EAA4E;IACrE,IAAI,CAAC,KAAQ;QAClB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED,2EAA2E;IACpE,KAAK;QACV,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAED;;;OAGG;IACI,IAAI,CAAC,KAAc;QACxB,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,GAAG,EAAE,KAAK,EAAE,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,MAAM,EAAE,CAAC;IAChB,CAAC;IAEO,MAAM;QACZ,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACjB,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,IAAI,EAAE,CAAC;QACT,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;QAClC,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,qEAAqE;gBACrE,oCAAoC;gBACpC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAO,CAAC;gBAC/B,SAAS;YACX,CAAC;YACD,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;gBAC1B,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;YAC3B,CAAC;YACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,OAAO;YACT,CAAC;YACD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @untangle-code/jobs — the job core of the Untangle indexer
3
+ * (spec.md §11, §12.2; architecture.md §2.2, §6.14, §13, §21.3–§21.4).
4
+ *
5
+ * Pipeline MECHANICS only: the exact spec.md §11.1 state machine, typed job
6
+ * events as `AsyncIterable`, a checkpoint store (`IndexCheckpointV1`,
7
+ * checkpoints only after confirmed upserts), resume planning, generation
8
+ * commit per §13.4, bounded queues with backpressure and a visible memory
9
+ * budget, and `AbortSignal` cancellation at safe batch boundaries. The
10
+ * domain-specific stages (scanner, parser, chunker, embedder, stores) are
11
+ * injected behind narrow interfaces. This module is the ONLY public export
12
+ * of the package.
13
+ */
14
+ export * from "./abort.js";
15
+ export * from "./state-machine.js";
16
+ export * from "./events.js";
17
+ export * from "./bounded-queue.js";
18
+ export * from "./stages.js";
19
+ export * from "./checkpoint-store.js";
20
+ export * from "./resume.js";
21
+ export * from "./runner.js";
22
+ //# sourceMappingURL=index.d.ts.map