devlensio 0.2.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 (136) hide show
  1. package/LICENSE +674 -0
  2. package/dist/clustering/index.d.ts +27 -0
  3. package/dist/clustering/index.js +149 -0
  4. package/dist/config/index.d.ts +10 -0
  5. package/dist/config/index.js +78 -0
  6. package/dist/config/providers/file.d.ts +19 -0
  7. package/dist/config/providers/file.js +215 -0
  8. package/dist/config/providers/request.d.ts +2 -0
  9. package/dist/config/providers/request.js +72 -0
  10. package/dist/config/types.d.ts +46 -0
  11. package/dist/config/types.js +81 -0
  12. package/dist/config/writer.d.ts +29 -0
  13. package/dist/config/writer.js +103 -0
  14. package/dist/filesystem/appRouter.d.ts +2 -0
  15. package/dist/filesystem/appRouter.js +126 -0
  16. package/dist/filesystem/backendRoutes.d.ts +2 -0
  17. package/dist/filesystem/backendRoutes.js +161 -0
  18. package/dist/filesystem/index.d.ts +2 -0
  19. package/dist/filesystem/index.js +28 -0
  20. package/dist/filesystem/index.test.d.ts +1 -0
  21. package/dist/filesystem/index.test.js +178 -0
  22. package/dist/filesystem/pagesRouter.d.ts +2 -0
  23. package/dist/filesystem/pagesRouter.js +109 -0
  24. package/dist/fingerprint/detectors.d.ts +8 -0
  25. package/dist/fingerprint/detectors.js +174 -0
  26. package/dist/fingerprint/index.d.ts +2 -0
  27. package/dist/fingerprint/index.js +41 -0
  28. package/dist/fingerprint/index.test.d.ts +1 -0
  29. package/dist/fingerprint/index.test.js +148 -0
  30. package/dist/graph/buildLookup.d.ts +10 -0
  31. package/dist/graph/buildLookup.js +32 -0
  32. package/dist/graph/edges/callEdges.d.ts +7 -0
  33. package/dist/graph/edges/callEdges.js +145 -0
  34. package/dist/graph/edges/eventEdges.d.ts +7 -0
  35. package/dist/graph/edges/eventEdges.js +203 -0
  36. package/dist/graph/edges/guardEdges.d.ts +3 -0
  37. package/dist/graph/edges/guardEdges.js +232 -0
  38. package/dist/graph/edges/hookEdges.d.ts +3 -0
  39. package/dist/graph/edges/hookEdges.js +54 -0
  40. package/dist/graph/edges/importEdges.d.ts +8 -0
  41. package/dist/graph/edges/importEdges.js +224 -0
  42. package/dist/graph/edges/propEdges.d.ts +3 -0
  43. package/dist/graph/edges/propEdges.js +142 -0
  44. package/dist/graph/edges/routeEdge.d.ts +3 -0
  45. package/dist/graph/edges/routeEdge.js +124 -0
  46. package/dist/graph/edges/stateEdges.d.ts +3 -0
  47. package/dist/graph/edges/stateEdges.js +206 -0
  48. package/dist/graph/edges/testEdges.d.ts +3 -0
  49. package/dist/graph/edges/testEdges.js +143 -0
  50. package/dist/graph/edges/utils.d.ts +2 -0
  51. package/dist/graph/edges/utils.js +25 -0
  52. package/dist/graph/index.d.ts +6 -0
  53. package/dist/graph/index.js +65 -0
  54. package/dist/graph/index.test.d.ts +1 -0
  55. package/dist/graph/index.test.js +542 -0
  56. package/dist/graph/thirdPartyLibs.d.ts +8 -0
  57. package/dist/graph/thirdPartyLibs.js +162 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +15 -0
  60. package/dist/jobs/index.d.ts +5 -0
  61. package/dist/jobs/index.js +11 -0
  62. package/dist/jobs/queue/interface.d.ts +13 -0
  63. package/dist/jobs/queue/interface.js +1 -0
  64. package/dist/jobs/queue/memory.d.ts +24 -0
  65. package/dist/jobs/queue/memory.js +291 -0
  66. package/dist/jobs/runner.d.ts +3 -0
  67. package/dist/jobs/runner.js +136 -0
  68. package/dist/jobs/types.d.ts +112 -0
  69. package/dist/jobs/types.js +33 -0
  70. package/dist/parser/directives.d.ts +4 -0
  71. package/dist/parser/directives.js +31 -0
  72. package/dist/parser/extractors/components.d.ts +5 -0
  73. package/dist/parser/extractors/components.js +240 -0
  74. package/dist/parser/extractors/functions.d.ts +4 -0
  75. package/dist/parser/extractors/functions.js +240 -0
  76. package/dist/parser/extractors/hooks.d.ts +4 -0
  77. package/dist/parser/extractors/hooks.js +128 -0
  78. package/dist/parser/extractors/stores.d.ts +3 -0
  79. package/dist/parser/extractors/stores.js +181 -0
  80. package/dist/parser/index.d.ts +14 -0
  81. package/dist/parser/index.js +168 -0
  82. package/dist/parser/index.test.d.ts +1 -0
  83. package/dist/parser/index.test.js +319 -0
  84. package/dist/parser/typeUtils.d.ts +9 -0
  85. package/dist/parser/typeUtils.js +46 -0
  86. package/dist/pipeline/index.d.ts +50 -0
  87. package/dist/pipeline/index.js +249 -0
  88. package/dist/scoring/connectionCounter.d.ts +28 -0
  89. package/dist/scoring/connectionCounter.js +134 -0
  90. package/dist/scoring/fileScorer.d.ts +2 -0
  91. package/dist/scoring/fileScorer.js +44 -0
  92. package/dist/scoring/index.d.ts +22 -0
  93. package/dist/scoring/index.js +130 -0
  94. package/dist/scoring/index.test.d.ts +1 -0
  95. package/dist/scoring/index.test.js +453 -0
  96. package/dist/scoring/nodeScorer.d.ts +3 -0
  97. package/dist/scoring/nodeScorer.js +108 -0
  98. package/dist/scoring/noiseFilter.d.ts +18 -0
  99. package/dist/scoring/noiseFilter.js +92 -0
  100. package/dist/storage/fileStorage.d.ts +117 -0
  101. package/dist/storage/fileStorage.js +616 -0
  102. package/dist/storage/index.d.ts +4 -0
  103. package/dist/storage/index.js +2 -0
  104. package/dist/storage/interface.d.ts +27 -0
  105. package/dist/storage/interface.js +1 -0
  106. package/dist/summarizer/checkpoint.d.ts +15 -0
  107. package/dist/summarizer/checkpoint.js +110 -0
  108. package/dist/summarizer/index.d.ts +2 -0
  109. package/dist/summarizer/index.js +281 -0
  110. package/dist/summarizer/mapreduce.d.ts +4 -0
  111. package/dist/summarizer/mapreduce.js +87 -0
  112. package/dist/summarizer/prompts.d.ts +22 -0
  113. package/dist/summarizer/prompts.js +205 -0
  114. package/dist/summarizer/providers/anthropic.d.ts +9 -0
  115. package/dist/summarizer/providers/anthropic.js +78 -0
  116. package/dist/summarizer/providers/gemini.d.ts +9 -0
  117. package/dist/summarizer/providers/gemini.js +79 -0
  118. package/dist/summarizer/providers/index.d.ts +3 -0
  119. package/dist/summarizer/providers/index.js +43 -0
  120. package/dist/summarizer/providers/ollama.d.ts +9 -0
  121. package/dist/summarizer/providers/ollama.js +23 -0
  122. package/dist/summarizer/providers/openRouter.d.ts +9 -0
  123. package/dist/summarizer/providers/openRouter.js +19 -0
  124. package/dist/summarizer/providers/openai.d.ts +9 -0
  125. package/dist/summarizer/providers/openai.js +72 -0
  126. package/dist/summarizer/providers/types.d.ts +32 -0
  127. package/dist/summarizer/providers/types.js +1 -0
  128. package/dist/summarizer/retry.d.ts +7 -0
  129. package/dist/summarizer/retry.js +51 -0
  130. package/dist/summarizer/topological.d.ts +3 -0
  131. package/dist/summarizer/topological.js +105 -0
  132. package/dist/summarizer/types.d.ts +57 -0
  133. package/dist/summarizer/types.js +17 -0
  134. package/dist/types.d.ts +78 -0
  135. package/dist/types.js +1 -0
  136. package/package.json +48 -0
@@ -0,0 +1,162 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ // ─── Exclusion / inclusion sets ───────────────────────────────────────────────
4
+ // Packages whose primary purpose is UI rendering, styling, or testing —
5
+ // they appear in almost every repo and add no analytical signal.
6
+ const UI_EXCLUSION_SET = new Set([
7
+ // Core UI frameworks
8
+ "react", "react-dom", "next", "gatsby", "nuxt", "@sveltejs/kit",
9
+ "solid-js", "preact", "remix", "@remix-run/react", "@remix-run/node",
10
+ // Styling
11
+ "tailwindcss", "styled-components", "@emotion/react", "@emotion/styled",
12
+ "sass", "less", "postcss", "autoprefixer",
13
+ "clsx", "class-variance-authority", "tailwind-merge", "cva",
14
+ // Animation / transition
15
+ "framer-motion", "react-spring", "react-transition-group", "motion",
16
+ // UI component kits (prefix-matched below for @radix-ui/* etc.)
17
+ "@headlessui/react", "lucide-react", "react-icons", "@heroicons/react",
18
+ "@phosphor-icons/react", "react-feather",
19
+ // Testing
20
+ "jest", "vitest", "@testing-library/react", "@testing-library/dom",
21
+ "@testing-library/jest-dom", "@testing-library/user-event",
22
+ "@playwright/test", "cypress", "mocha", "chai", "supertest",
23
+ // Dev tools
24
+ "typescript", "eslint", "prettier", "webpack", "vite", "esbuild",
25
+ "rollup", "parcel", "turbopack",
26
+ "@types/node", "@types/react", "@types/react-dom",
27
+ "ts-node", "tsx", "nodemon", "concurrently", "husky", "lint-staged",
28
+ "rimraf", "cross-env", "dotenv-cli",
29
+ ]);
30
+ // Packages that ARE runtime logic — they represent real architectural decisions.
31
+ const RUNTIME_INCLUSION_SET = new Set([
32
+ // HTTP clients
33
+ "axios", "ky", "node-fetch", "got", "superagent", "undici",
34
+ // Auth
35
+ "next-auth", "@auth/core", "jsonwebtoken", "bcryptjs", "bcrypt",
36
+ "passport", "clerk", "@clerk/nextjs", "@clerk/clerk-react", "lucia",
37
+ // State management
38
+ "zustand", "jotai", "recoil", "redux", "@reduxjs/toolkit", "mobx",
39
+ "xstate", "valtio", "nanostores",
40
+ // Data fetching / caching
41
+ "@tanstack/react-query", "@tanstack/query-core",
42
+ "swr", "@apollo/client", "urql", "graphql",
43
+ // Validation / schema
44
+ "zod", "yup", "joi", "valibot", "superstruct", "ajv",
45
+ // ORM / DB
46
+ "@prisma/client", "drizzle-orm", "mongoose", "sequelize", "typeorm",
47
+ "pg", "mysql2", "better-sqlite3", "ioredis", "redis",
48
+ "@neondatabase/serverless", "@vercel/postgres",
49
+ // Payments
50
+ "stripe", "@stripe/stripe-js", "@stripe/react-stripe-js",
51
+ // Email
52
+ "nodemailer", "@sendgrid/mail", "resend", "@aws-sdk/client-ses",
53
+ // BaaS
54
+ "@supabase/supabase-js", "firebase", "firebase-admin",
55
+ // Cloud SDKs
56
+ "aws-sdk", "@aws-sdk/client-s3", "@aws-sdk/client-dynamodb",
57
+ "@aws-sdk/client-lambda", "@aws-sdk/client-sqs",
58
+ // Realtime
59
+ "socket.io", "socket.io-client", "ws", "pusher", "pusher-js",
60
+ "@ably/ably",
61
+ // Queues / jobs
62
+ "bull", "bullmq", "node-cron", "agenda", "bee-queue",
63
+ // File processing
64
+ "sharp", "multer", "formidable",
65
+ // CMS
66
+ "contentful", "@sanity/client", "@sanity/image-url",
67
+ // Observability
68
+ "@sentry/nextjs", "@sentry/node", "posthog-js", "@posthog/node",
69
+ "newrelic", "dd-trace",
70
+ // Utilities
71
+ "lodash", "date-fns", "dayjs", "moment", "uuid", "nanoid",
72
+ "slugify", "cheerio", "marked", "gray-matter",
73
+ // Backend frameworks
74
+ "express", "fastify", "koa", "hono", "nestjs", "@nestjs/core",
75
+ // AI / LLM
76
+ "openai", "@anthropic-ai/sdk", "langchain", "@langchain/core",
77
+ "ai", "@ai-sdk/openai", "@ai-sdk/anthropic",
78
+ ]);
79
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
80
+ export function readPackageDependencies(repoPath) {
81
+ const pkgPath = path.join(repoPath, "package.json");
82
+ if (!fs.existsSync(pkgPath)) {
83
+ return { dependencies: {}, devDependencies: {} };
84
+ }
85
+ try {
86
+ const raw = fs.readFileSync(pkgPath, "utf-8");
87
+ const pkg = JSON.parse(raw);
88
+ return {
89
+ dependencies: pkg.dependencies ?? {},
90
+ devDependencies: pkg.devDependencies ?? {},
91
+ };
92
+ }
93
+ catch {
94
+ return { dependencies: {}, devDependencies: {} };
95
+ }
96
+ }
97
+ export function categorizeLibrary(packageName, isDev) {
98
+ // Check exact match in UI set
99
+ if (UI_EXCLUSION_SET.has(packageName))
100
+ return "ui";
101
+ // Check prefix matches for scoped UI packages like @radix-ui/*, @types/*
102
+ if (packageName.startsWith("@radix-ui/") ||
103
+ packageName.startsWith("@types/") ||
104
+ packageName.startsWith("@testing-library/") ||
105
+ packageName.startsWith("@storybook/") ||
106
+ packageName.startsWith("eslint-") ||
107
+ packageName.startsWith("prettier-") ||
108
+ packageName.startsWith("babel-") ||
109
+ packageName.startsWith("@babel/"))
110
+ return "ui";
111
+ // Check runtime set
112
+ if (RUNTIME_INCLUSION_SET.has(packageName))
113
+ return "runtime";
114
+ // Check prefix matches for scoped runtime packages
115
+ if (packageName.startsWith("@aws-sdk/") ||
116
+ packageName.startsWith("@tanstack/") ||
117
+ packageName.startsWith("@sentry/") ||
118
+ packageName.startsWith("@clerk/") ||
119
+ packageName.startsWith("@auth/") ||
120
+ packageName.startsWith("@ai-sdk/") ||
121
+ packageName.startsWith("@nestjs/"))
122
+ return "runtime";
123
+ // devDependencies that didn't match anything above
124
+ if (isDev)
125
+ return "devtool";
126
+ return "unknown";
127
+ }
128
+ export function extractPackageName(importSpecifier) {
129
+ // Scoped package: @org/pkg/sub/path → @org/pkg
130
+ if (importSpecifier.startsWith("@")) {
131
+ const parts = importSpecifier.split("/");
132
+ return parts.slice(0, 2).join("/");
133
+ }
134
+ // Regular: axios/lib/core → axios
135
+ return importSpecifier.split("/")[0];
136
+ }
137
+ export function buildThirdPartyNodes(repoPath, includedLibs) {
138
+ if (!includedLibs.length)
139
+ return [];
140
+ const { dependencies, devDependencies } = readPackageDependencies(repoPath);
141
+ const nodes = [];
142
+ for (const name of includedLibs) {
143
+ const version = dependencies[name] ?? devDependencies[name] ?? "unknown";
144
+ const isDev = name in devDependencies && !(name in dependencies);
145
+ nodes.push({
146
+ id: `[npm]/${name}`,
147
+ name,
148
+ type: "THIRD_PARTY",
149
+ filePath: `[npm]/${name}`,
150
+ startLine: 0,
151
+ endLine: 0,
152
+ rawCode: undefined,
153
+ codeHash: undefined,
154
+ metadata: {
155
+ isThirdParty: true,
156
+ packageVersion: version,
157
+ category: categorizeLibrary(name, isDev),
158
+ },
159
+ });
160
+ }
161
+ return nodes;
162
+ }
@@ -0,0 +1,15 @@
1
+ export * from "./types.js";
2
+ export * from "./jobs/types.js";
3
+ export * from "./storage/fileStorage.js";
4
+ export * from "./storage/interface.js";
5
+ export * from "./jobs/queue/interface.js";
6
+ export * from "./summarizer/types.js";
7
+ export * from "./clustering/index.js";
8
+ export * from "./pipeline/index.js";
9
+ export * from "./config/types.js";
10
+ export { analyzePipeline } from "./pipeline/index.js";
11
+ export { runSummarization } from "./summarizer/index.js";
12
+ export { resolveConfig } from "./config/index.js";
13
+ export { computeClusters } from "./clustering/index.js";
14
+ export { EDGE_LABELS } from "./summarizer/prompts.js";
15
+ export { CycleGroup, TopologicalResult } from "./summarizer/types.js";
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export * from "./types.js";
2
+ export * from "./jobs/types.js";
3
+ export * from "./storage/fileStorage.js";
4
+ export * from "./storage/interface.js";
5
+ export * from "./jobs/queue/interface.js";
6
+ export * from "./summarizer/types.js";
7
+ export * from "./clustering/index.js";
8
+ export * from "./pipeline/index.js";
9
+ export * from "./config/types.js";
10
+ // Functions that cloud backend will need
11
+ export { analyzePipeline } from "./pipeline/index.js";
12
+ export { runSummarization } from "./summarizer/index.js";
13
+ export { resolveConfig } from "./config/index.js";
14
+ export { computeClusters } from "./clustering/index.js";
15
+ export { EDGE_LABELS } from "./summarizer/prompts.js";
@@ -0,0 +1,5 @@
1
+ import { JobQueue } from "./queue/interface.js";
2
+ export declare const queue: JobQueue;
3
+ export type { JobQueue } from "./queue/interface.js";
4
+ export type { Job, JobSummary, JobStatus, JobPhase, ProgressEvent, JobInput } from "./types.js";
5
+ export { isTerminal, isResumable, toJobSummary } from "./types.js";
@@ -0,0 +1,11 @@
1
+ import { InMemoryQueue } from "./queue/memory.js";
2
+ function createQueue() {
3
+ return new InMemoryQueue();
4
+ }
5
+ // Singleton Queue
6
+ //
7
+ // One queue instance for the entire server process.
8
+ // All handlers import this — never instantiate their own queue.
9
+ // This is what ensures job deduplication works across requests.
10
+ export const queue = createQueue();
11
+ export { isTerminal, isResumable, toJobSummary } from "./types.js";
@@ -0,0 +1,13 @@
1
+ import type { Job, JobInput, JobSummary, ProgressEvent } from "../types.js";
2
+ export interface JobQueue {
3
+ enqueue(input: JobInput): Job;
4
+ getJob(jobId: string): Job | undefined;
5
+ listJobs(): JobSummary[];
6
+ findActiveJob(repoPath: string): Job | undefined;
7
+ pauseJob(jobId: string): boolean;
8
+ resumeJob(jobId: string): boolean;
9
+ cancelJob(jobId: string): boolean;
10
+ emitEvent(jobId: string, event: ProgressEvent): void;
11
+ subscribe(jobId: string, onEvent: (event: ProgressEvent) => void, onCompleted: () => void): () => void;
12
+ updateJob(jobId: string, updates: Partial<Job>): void;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { Job, JobInput, JobSummary, ProgressEvent } from "../types.js";
2
+ import { JobQueue } from "./interface.js";
3
+ export declare class InMemoryQueue implements JobQueue {
4
+ private jobs;
5
+ private subscribers;
6
+ private waitingQueue;
7
+ enqueue(input: JobInput): Job;
8
+ getJob(jobId: string): Job | undefined;
9
+ listJobs(): JobSummary[];
10
+ findActiveJob(repoPath: string): Job | undefined;
11
+ pauseJob(jobId: string): boolean;
12
+ resumeJob(jobId: string): boolean;
13
+ cancelJob(jobId: string): boolean;
14
+ subscribe(jobId: string, onEvent: (event: ProgressEvent) => void, onCompleted: () => void): () => void;
15
+ updateJob(jobId: string, updates: Partial<Job>): void;
16
+ emitEvent(jobId: string, event: ProgressEvent): void;
17
+ private getRunningCount;
18
+ _markFailed(jobId: string, error: string): void;
19
+ private onJobTerminated;
20
+ _markPaused(jobId: string): void;
21
+ _markCancelled(jobId: string, cleanedUp: boolean): void;
22
+ _markCompleted(jobId: string, graphId: string): void;
23
+ private startJob;
24
+ }
@@ -0,0 +1,291 @@
1
+ import { randomUUIDv7 } from "bun";
2
+ import { isTerminal, toJobSummary } from "../types.js";
3
+ const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT_JOBS ?? "3", 10);
4
+ export class InMemoryQueue {
5
+ constructor() {
6
+ this.jobs = new Map();
7
+ this.subscribers = new Map(); //Active Subscribers per job
8
+ this.waitingQueue = []; //job Ids waiting for their turn
9
+ }
10
+ enqueue(input) {
11
+ const existing = this.findActiveJob(input.repoPath);
12
+ if (existing) {
13
+ console.log(`Job is already active for ${input.repoPath} - returning ${existing.jobId}`);
14
+ return existing;
15
+ }
16
+ const jobId = randomUUIDv7();
17
+ const now = new Date().toISOString();
18
+ const job = {
19
+ jobId,
20
+ status: "queued",
21
+ phase: null,
22
+ repoPath: input.repoPath,
23
+ isGithubRepo: input.isGithubRepo ?? false,
24
+ thresholds: input.thresholds,
25
+ config: input.config,
26
+ skipSummarization: input.skipSummarization ?? false,
27
+ forceSummarize: input.forceSummarize ?? false,
28
+ includedThirdPartyLibs: input.includedThirdPartyLibs ?? [],
29
+ graphId: undefined,
30
+ events: [],
31
+ pauseRequested: false,
32
+ cancelRequested: false,
33
+ createdAt: now,
34
+ };
35
+ this.jobs.set(jobId, job);
36
+ const position = this.waitingQueue.length + 1;
37
+ this.emitEvent(jobId, { event: "queued", jobId, position });
38
+ // If we have capacity, start immediately — no need to wait
39
+ const runningCount = this.getRunningCount();
40
+ if (runningCount < MAX_CONCURRENT) {
41
+ // Start async — does not block enqueue()
42
+ // The runner import is deferred to avoid circular dependency
43
+ // jobs/index.ts wires the runner in after both are initialized
44
+ setImmediate(() => this.startJob(jobId));
45
+ }
46
+ else {
47
+ console.log(`⏳ Job ${jobId} queued at position ${position} (${runningCount}/${MAX_CONCURRENT} slots used)`);
48
+ this.waitingQueue.push(jobId);
49
+ }
50
+ return job;
51
+ }
52
+ getJob(jobId) {
53
+ return this.jobs.get(jobId);
54
+ }
55
+ listJobs() {
56
+ return Array.from(this.jobs.values())
57
+ .map(toJobSummary)
58
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
59
+ }
60
+ findActiveJob(repoPath) {
61
+ for (const job of this.jobs.values()) {
62
+ if (job.repoPath === repoPath &&
63
+ (job.status === "queued" ||
64
+ job.status === "running" ||
65
+ job.status === "paused")) {
66
+ return job;
67
+ }
68
+ }
69
+ return undefined;
70
+ }
71
+ // Only valid during summarization phase.
72
+ // Analysis phase cannot be paused — it's too fast and atomic.
73
+ // Sets the pauseRequested signal — runner checks between batches.
74
+ pauseJob(jobId) {
75
+ const job = this.jobs.get(jobId);
76
+ if (!job)
77
+ return false;
78
+ // Can only pause a running job that is in summarization phase
79
+ if (job.status !== "running" || job.phase !== "summarization") {
80
+ return false;
81
+ }
82
+ job.pauseRequested = true;
83
+ console.log(`⏸️ Pause requested for job ${jobId}`);
84
+ return true;
85
+ }
86
+ resumeJob(jobId) {
87
+ const job = this.jobs.get(jobId);
88
+ if (!job || job.status !== "paused")
89
+ return false;
90
+ job.pauseRequested = false;
91
+ job.cancelRequested = false;
92
+ job.status = "running";
93
+ job.pausedAt = undefined;
94
+ this.emitEvent(jobId, {
95
+ event: "resumed",
96
+ jobId,
97
+ completedNodes: job.summarizationCompleted ?? 0,
98
+ totalNodes: job.summarizationTotal ?? 0,
99
+ });
100
+ // Re-start the job from checkpoint
101
+ setImmediate(() => this.startJob(jobId));
102
+ return true;
103
+ }
104
+ // ── cancelJob ───────────────────────────────────────────────────────────────
105
+ //
106
+ // Works from any non-terminal state.
107
+ // Queued jobs are cancelled immediately.
108
+ // Running/paused jobs set cancelRequested — runner handles cleanup.
109
+ cancelJob(jobId) {
110
+ const job = this.jobs.get(jobId);
111
+ if (!job || isTerminal(job.status))
112
+ return false;
113
+ if (job.status === "queued") {
114
+ // Remove from waiting queue immediately
115
+ this.waitingQueue = this.waitingQueue.filter(id => id !== jobId);
116
+ this._markCancelled(jobId, true);
117
+ return true;
118
+ }
119
+ // Running or paused — signal the runner
120
+ job.cancelRequested = true;
121
+ job.pauseRequested = false; // clear pause signal if both were set
122
+ console.log(`🚫 Cancel requested for job ${jobId}`);
123
+ return true;
124
+ }
125
+ // ── subscribe ───────────────────────────────────────────────────────────────
126
+ //
127
+ // Attaches an SSE subscriber to a job.
128
+ // Replays all past events immediately, then streams live.
129
+ // Returns unsubscribe function — call when SSE connection closes.
130
+ subscribe(jobId, onEvent, onCompleted) {
131
+ const job = this.jobs.get(jobId);
132
+ if (!job) {
133
+ // Job not found — call onCompleted immediately so handler closes stream
134
+ onCompleted();
135
+ return () => { };
136
+ }
137
+ // Replay all past events for catch-up
138
+ for (const event of job.events) {
139
+ try {
140
+ onEvent(event);
141
+ }
142
+ catch { /* ignore replay errors */ }
143
+ }
144
+ // If job is already terminal, close immediately after replay
145
+ if (isTerminal(job.status)) {
146
+ onCompleted();
147
+ return () => { };
148
+ }
149
+ // Register subscriber for live events
150
+ const sub = { onEvent, onCompleted };
151
+ if (!this.subscribers.has(jobId)) {
152
+ this.subscribers.set(jobId, new Set());
153
+ }
154
+ this.subscribers.get(jobId).add(sub);
155
+ // Return unsubscribe function
156
+ return () => {
157
+ const subs = this.subscribers.get(jobId);
158
+ if (subs) {
159
+ subs.delete(sub);
160
+ if (subs.size === 0)
161
+ this.subscribers.delete(jobId);
162
+ }
163
+ };
164
+ }
165
+ // ── updateJob ───────────────────────────────────────────────────────────────
166
+ //
167
+ // Direct field updates — only called by runner.
168
+ // Uses Object.assign for a clean partial update.
169
+ updateJob(jobId, updates) {
170
+ const job = this.jobs.get(jobId);
171
+ if (!job)
172
+ return;
173
+ Object.assign(job, updates);
174
+ }
175
+ //broadcasts event to every subscribers
176
+ emitEvent(jobId, event) {
177
+ const job = this.jobs.get(jobId);
178
+ if (!job)
179
+ return;
180
+ // Store in history — enables SSE replay on reconnect
181
+ job.events.push(event);
182
+ // Broadcast to all active subscribers
183
+ const subs = this.subscribers.get(jobId);
184
+ if (!subs || subs.size === 0)
185
+ return;
186
+ for (const sub of subs) {
187
+ try {
188
+ sub.onEvent(event);
189
+ }
190
+ catch (err) {
191
+ // Subscriber errored (e.g. connection dropped) — remove it
192
+ console.warn(`SSE subscriber error for job ${jobId}:`, err);
193
+ subs.delete(sub);
194
+ }
195
+ }
196
+ }
197
+ //private helpers
198
+ getRunningCount() {
199
+ let count = 0;
200
+ for (const job of this.jobs.values()) {
201
+ if (job.status === "running")
202
+ count++;
203
+ }
204
+ return count;
205
+ }
206
+ _markFailed(jobId, error) {
207
+ const job = this.jobs.get(jobId);
208
+ if (!job)
209
+ return;
210
+ job.status = "failed";
211
+ job.error = error;
212
+ job.failedAt = new Date().toISOString();
213
+ this.emitEvent(jobId, { event: "failed", jobId, error });
214
+ this.onJobTerminated(jobId);
215
+ console.error(`❌ Job ${jobId} failed: ${error}`);
216
+ }
217
+ // Called when a job reaches a terminal state.
218
+ // Notifies all subscribers to close their SSE streams.
219
+ // Then promotes the next waiting job if a slot opened up.
220
+ onJobTerminated(jobId) {
221
+ const subs = this.subscribers.get(jobId);
222
+ if (subs) {
223
+ for (const sub of subs) {
224
+ try {
225
+ sub.onCompleted();
226
+ }
227
+ catch { /* ignore */ }
228
+ }
229
+ this.subscribers.delete(jobId);
230
+ }
231
+ // Promote next waiting job if capacity available
232
+ if (this.waitingQueue.length > 0) {
233
+ const nextJobId = this.waitingQueue.shift();
234
+ console.log(`▶️ Promoting queued job ${nextJobId}`);
235
+ setImmediate(() => this.startJob(nextJobId));
236
+ }
237
+ }
238
+ // Marks a job as completed and notifies subscribers + queue.
239
+ _markPaused(jobId) {
240
+ const job = this.jobs.get(jobId);
241
+ if (!job)
242
+ return;
243
+ job.status = "paused";
244
+ job.pausedAt = new Date().toISOString();
245
+ this.emitEvent(jobId, { event: "paused", jobId, completedNodes: job.summarizationCompleted ?? 0, totalNodes: job.summarizationTotal ?? 0 });
246
+ console.log(`⏸️ Job ${jobId} paused at ${job.summarizationCompleted}/${job.summarizationTotal} nodes`);
247
+ }
248
+ // Marks a job as cancelled and notifies subscribers + queue.
249
+ // cleanedUp = whether checkpoint file was deleted from disk.
250
+ // Called directly for queued jobs, called by runner for running/paused jobs.
251
+ _markCancelled(jobId, cleanedUp) {
252
+ const job = this.jobs.get(jobId);
253
+ if (!job)
254
+ return;
255
+ job.status = "cancelled";
256
+ job.cancelledAt = new Date().toISOString();
257
+ this.emitEvent(jobId, { event: "cancelled", jobId, cleanedUp });
258
+ this.onJobTerminated(jobId);
259
+ console.log(`🚫 Job ${jobId} cancelled (cleanedUp: ${cleanedUp})`);
260
+ }
261
+ // Marks a job as completed and notifies subscribers + queue.
262
+ _markCompleted(jobId, graphId) {
263
+ const job = this.jobs.get(jobId);
264
+ if (!job)
265
+ return;
266
+ job.status = "completed";
267
+ job.graphId = graphId;
268
+ job.completedAt = new Date().toISOString();
269
+ this.emitEvent(jobId, { event: "completed", jobId, graphId });
270
+ this.onJobTerminated(jobId);
271
+ console.log(`✅ Job ${jobId} completed — graph ${graphId}`);
272
+ }
273
+ async startJob(jobId) {
274
+ const job = this.jobs.get(jobId);
275
+ if (!job)
276
+ return;
277
+ // Job may have been cancelled while waiting in queue
278
+ if (job.status === "cancelled")
279
+ return;
280
+ try {
281
+ // Dynamic import breaks circular dependency:
282
+ // queue → runner → queue (for emitEvent/updateJob)
283
+ const { runJob } = await import("../runner.js");
284
+ await runJob(job, this);
285
+ }
286
+ catch (err) {
287
+ const message = err instanceof Error ? err.message : "Unknown error";
288
+ this._markFailed(jobId, message);
289
+ }
290
+ }
291
+ }
@@ -0,0 +1,3 @@
1
+ import { Job } from "./types.js";
2
+ import { JobQueue } from "./queue/interface.js";
3
+ export declare function runJob(job: Job, queue: JobQueue): Promise<void>;