@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
package/src/events.ts ADDED
@@ -0,0 +1,71 @@
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
+
13
+ export class AsyncEventChannel<T> implements AsyncIterable<T> {
14
+ private readonly buffer: T[] = [];
15
+ private closed = false;
16
+ private failure: { error: unknown } | null = null;
17
+ private wakers: Array<() => void> = [];
18
+
19
+ /** Enqueues one event. Throws when the channel is already closed/failed. */
20
+ public emit(event: T): void {
21
+ if (this.closed) {
22
+ throw new Error("AsyncEventChannel.emit() called after close()/fail()");
23
+ }
24
+ this.buffer.push(event);
25
+ this.notify();
26
+ }
27
+
28
+ /** Ends the stream after all buffered events were consumed. Idempotent. */
29
+ public close(): void {
30
+ this.closed = true;
31
+ this.notify();
32
+ }
33
+
34
+ /**
35
+ * Ends the stream with a rejection after all buffered events were
36
+ * consumed. Idempotent; a later `fail()` does not override an earlier one.
37
+ */
38
+ public fail(error: unknown): void {
39
+ if (this.failure === null) {
40
+ this.failure = { error };
41
+ }
42
+ this.closed = true;
43
+ this.notify();
44
+ }
45
+
46
+ private notify(): void {
47
+ const wakers = this.wakers;
48
+ this.wakers = [];
49
+ for (const wake of wakers) {
50
+ wake();
51
+ }
52
+ }
53
+
54
+ public async *[Symbol.asyncIterator](): AsyncIterator<T> {
55
+ while (true) {
56
+ if (this.buffer.length > 0) {
57
+ // `length > 0` guarantees the shift result is a T, even for a T that
58
+ // could itself include `undefined`.
59
+ yield this.buffer.shift() as T;
60
+ continue;
61
+ }
62
+ if (this.failure !== null) {
63
+ throw this.failure.error;
64
+ }
65
+ if (this.closed) {
66
+ return;
67
+ }
68
+ await new Promise<void>((resolve) => this.wakers.push(resolve));
69
+ }
70
+ }
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
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
+
15
+ // Abort helpers (spec.md §22).
16
+ export * from "./abort.js";
17
+
18
+ // State machine (spec.md §11.1).
19
+ export * from "./state-machine.js";
20
+
21
+ // Async event channel (architecture.md §2.2).
22
+ export * from "./events.js";
23
+
24
+ // Bounded queues with backpressure (architecture.md §13.2).
25
+ export * from "./bounded-queue.js";
26
+
27
+ // Injected stage interfaces (architecture.md §13.1).
28
+ export * from "./stages.js";
29
+
30
+ // Checkpoint store (architecture.md §13.3; spec.md §10.5).
31
+ export * from "./checkpoint-store.js";
32
+
33
+ // Resume planning (spec.md §11.2).
34
+ export * from "./resume.js";
35
+
36
+ // Index job runner (spec.md §11; architecture.md §13.4, §21.3).
37
+ export * from "./runner.js";
package/src/resume.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Resume planning (architecture.md §6.14): reconstruct the remaining work
3
+ * plan from the last CONFIRMED checkpoint, so a restarted job continues
4
+ * exactly where confirmed work ended (spec.md §11.2) and never repeats an
5
+ * already-confirmed file.
6
+ */
7
+ import type { IndexCheckpointV1 } from "@untangle-code/domain";
8
+
9
+ import type { PlannedFileV1 } from "./stages.js";
10
+
11
+ /** The identity a checkpoint must match to be resumable for this job. */
12
+ export interface ResumeIdentityV1 {
13
+ workspaceId: string;
14
+ repoId: string;
15
+ refId: string;
16
+ effectiveProfileId: string;
17
+ }
18
+
19
+ /**
20
+ * True when `checkpoint` belongs to exactly this workspace/repo/ref/profile.
21
+ * A profile mismatch means an incompatible embedding profile — such a
22
+ * checkpoint MUST NOT be resumed; the job needs its own new generation
23
+ * (spec.md §11.2).
24
+ */
25
+ export function checkpointMatches(
26
+ checkpoint: IndexCheckpointV1,
27
+ identity: ResumeIdentityV1,
28
+ ): boolean {
29
+ return (
30
+ checkpoint.workspaceId === identity.workspaceId &&
31
+ checkpoint.repoId === identity.repoId &&
32
+ checkpoint.refId === identity.refId &&
33
+ checkpoint.effectiveProfileId === identity.effectiveProfileId
34
+ );
35
+ }
36
+
37
+ export interface ResumePlanV1 {
38
+ /** True when a matching checkpoint was adopted. */
39
+ resumed: boolean;
40
+ /** Generation to continue (from the checkpoint), or null for a fresh run. */
41
+ generationId: string | null;
42
+ /** Files that still need the full pipeline. */
43
+ remaining: PlannedFileV1[];
44
+ /** Confirmed state carried over from the checkpoint (planned ∩ unchanged). */
45
+ completedFiles: string[];
46
+ storedPointIdsByFile: Record<string, string[]>;
47
+ manifestHashes: Record<string, string>;
48
+ /** How many planned files were skipped because they are already confirmed. */
49
+ skippedCompleted: number;
50
+ }
51
+
52
+ /**
53
+ * Builds the remaining work plan. A planned file is skipped only when the
54
+ * checkpoint confirms it AND its content hash is unchanged — the content
55
+ * hash is the source of truth (spec.md §12.1). Confirmed files whose hash
56
+ * changed (or that are no longer planned) are dropped from the carried
57
+ * state so they are re-confirmed by this run, never counted twice.
58
+ */
59
+ export function planResume(
60
+ planned: readonly PlannedFileV1[],
61
+ checkpoint: IndexCheckpointV1 | null,
62
+ identity: ResumeIdentityV1,
63
+ ): ResumePlanV1 {
64
+ if (checkpoint === null || !checkpointMatches(checkpoint, identity)) {
65
+ return {
66
+ resumed: false,
67
+ generationId: null,
68
+ remaining: [...planned],
69
+ completedFiles: [],
70
+ storedPointIdsByFile: {},
71
+ manifestHashes: {},
72
+ skippedCompleted: 0,
73
+ };
74
+ }
75
+
76
+ const confirmed = new Set(checkpoint.completedFiles);
77
+ const remaining: PlannedFileV1[] = [];
78
+ const completedFiles: string[] = [];
79
+ const storedPointIdsByFile: Record<string, string[]> = {};
80
+ const manifestHashes: Record<string, string> = {};
81
+
82
+ for (const file of planned) {
83
+ const isConfirmedUnchanged =
84
+ confirmed.has(file.fileId) && checkpoint.manifestHashes[file.fileId] === file.contentHash;
85
+ if (!isConfirmedUnchanged) {
86
+ remaining.push(file);
87
+ continue;
88
+ }
89
+ completedFiles.push(file.fileId);
90
+ manifestHashes[file.fileId] = file.contentHash;
91
+ const pointIds = checkpoint.storedPointIdsByFile[file.fileId];
92
+ if (pointIds !== undefined) {
93
+ storedPointIdsByFile[file.fileId] = [...pointIds];
94
+ }
95
+ }
96
+
97
+ return {
98
+ resumed: true,
99
+ generationId: checkpoint.generationId,
100
+ remaining,
101
+ completedFiles,
102
+ storedPointIdsByFile,
103
+ manifestHashes,
104
+ skippedCompleted: completedFiles.length,
105
+ };
106
+ }