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.
- package/LICENSE +674 -0
- package/dist/clustering/index.d.ts +27 -0
- package/dist/clustering/index.js +149 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +78 -0
- package/dist/config/providers/file.d.ts +19 -0
- package/dist/config/providers/file.js +215 -0
- package/dist/config/providers/request.d.ts +2 -0
- package/dist/config/providers/request.js +72 -0
- package/dist/config/types.d.ts +46 -0
- package/dist/config/types.js +81 -0
- package/dist/config/writer.d.ts +29 -0
- package/dist/config/writer.js +103 -0
- package/dist/filesystem/appRouter.d.ts +2 -0
- package/dist/filesystem/appRouter.js +126 -0
- package/dist/filesystem/backendRoutes.d.ts +2 -0
- package/dist/filesystem/backendRoutes.js +161 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.js +28 -0
- package/dist/filesystem/index.test.d.ts +1 -0
- package/dist/filesystem/index.test.js +178 -0
- package/dist/filesystem/pagesRouter.d.ts +2 -0
- package/dist/filesystem/pagesRouter.js +109 -0
- package/dist/fingerprint/detectors.d.ts +8 -0
- package/dist/fingerprint/detectors.js +174 -0
- package/dist/fingerprint/index.d.ts +2 -0
- package/dist/fingerprint/index.js +41 -0
- package/dist/fingerprint/index.test.d.ts +1 -0
- package/dist/fingerprint/index.test.js +148 -0
- package/dist/graph/buildLookup.d.ts +10 -0
- package/dist/graph/buildLookup.js +32 -0
- package/dist/graph/edges/callEdges.d.ts +7 -0
- package/dist/graph/edges/callEdges.js +145 -0
- package/dist/graph/edges/eventEdges.d.ts +7 -0
- package/dist/graph/edges/eventEdges.js +203 -0
- package/dist/graph/edges/guardEdges.d.ts +3 -0
- package/dist/graph/edges/guardEdges.js +232 -0
- package/dist/graph/edges/hookEdges.d.ts +3 -0
- package/dist/graph/edges/hookEdges.js +54 -0
- package/dist/graph/edges/importEdges.d.ts +8 -0
- package/dist/graph/edges/importEdges.js +224 -0
- package/dist/graph/edges/propEdges.d.ts +3 -0
- package/dist/graph/edges/propEdges.js +142 -0
- package/dist/graph/edges/routeEdge.d.ts +3 -0
- package/dist/graph/edges/routeEdge.js +124 -0
- package/dist/graph/edges/stateEdges.d.ts +3 -0
- package/dist/graph/edges/stateEdges.js +206 -0
- package/dist/graph/edges/testEdges.d.ts +3 -0
- package/dist/graph/edges/testEdges.js +143 -0
- package/dist/graph/edges/utils.d.ts +2 -0
- package/dist/graph/edges/utils.js +25 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.js +65 -0
- package/dist/graph/index.test.d.ts +1 -0
- package/dist/graph/index.test.js +542 -0
- package/dist/graph/thirdPartyLibs.d.ts +8 -0
- package/dist/graph/thirdPartyLibs.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/index.js +11 -0
- package/dist/jobs/queue/interface.d.ts +13 -0
- package/dist/jobs/queue/interface.js +1 -0
- package/dist/jobs/queue/memory.d.ts +24 -0
- package/dist/jobs/queue/memory.js +291 -0
- package/dist/jobs/runner.d.ts +3 -0
- package/dist/jobs/runner.js +136 -0
- package/dist/jobs/types.d.ts +112 -0
- package/dist/jobs/types.js +33 -0
- package/dist/parser/directives.d.ts +4 -0
- package/dist/parser/directives.js +31 -0
- package/dist/parser/extractors/components.d.ts +5 -0
- package/dist/parser/extractors/components.js +240 -0
- package/dist/parser/extractors/functions.d.ts +4 -0
- package/dist/parser/extractors/functions.js +240 -0
- package/dist/parser/extractors/hooks.d.ts +4 -0
- package/dist/parser/extractors/hooks.js +128 -0
- package/dist/parser/extractors/stores.d.ts +3 -0
- package/dist/parser/extractors/stores.js +181 -0
- package/dist/parser/index.d.ts +14 -0
- package/dist/parser/index.js +168 -0
- package/dist/parser/index.test.d.ts +1 -0
- package/dist/parser/index.test.js +319 -0
- package/dist/parser/typeUtils.d.ts +9 -0
- package/dist/parser/typeUtils.js +46 -0
- package/dist/pipeline/index.d.ts +50 -0
- package/dist/pipeline/index.js +249 -0
- package/dist/scoring/connectionCounter.d.ts +28 -0
- package/dist/scoring/connectionCounter.js +134 -0
- package/dist/scoring/fileScorer.d.ts +2 -0
- package/dist/scoring/fileScorer.js +44 -0
- package/dist/scoring/index.d.ts +22 -0
- package/dist/scoring/index.js +130 -0
- package/dist/scoring/index.test.d.ts +1 -0
- package/dist/scoring/index.test.js +453 -0
- package/dist/scoring/nodeScorer.d.ts +3 -0
- package/dist/scoring/nodeScorer.js +108 -0
- package/dist/scoring/noiseFilter.d.ts +18 -0
- package/dist/scoring/noiseFilter.js +92 -0
- package/dist/storage/fileStorage.d.ts +117 -0
- package/dist/storage/fileStorage.js +616 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/interface.d.ts +27 -0
- package/dist/storage/interface.js +1 -0
- package/dist/summarizer/checkpoint.d.ts +15 -0
- package/dist/summarizer/checkpoint.js +110 -0
- package/dist/summarizer/index.d.ts +2 -0
- package/dist/summarizer/index.js +281 -0
- package/dist/summarizer/mapreduce.d.ts +4 -0
- package/dist/summarizer/mapreduce.js +87 -0
- package/dist/summarizer/prompts.d.ts +22 -0
- package/dist/summarizer/prompts.js +205 -0
- package/dist/summarizer/providers/anthropic.d.ts +9 -0
- package/dist/summarizer/providers/anthropic.js +78 -0
- package/dist/summarizer/providers/gemini.d.ts +9 -0
- package/dist/summarizer/providers/gemini.js +79 -0
- package/dist/summarizer/providers/index.d.ts +3 -0
- package/dist/summarizer/providers/index.js +43 -0
- package/dist/summarizer/providers/ollama.d.ts +9 -0
- package/dist/summarizer/providers/ollama.js +23 -0
- package/dist/summarizer/providers/openRouter.d.ts +9 -0
- package/dist/summarizer/providers/openRouter.js +19 -0
- package/dist/summarizer/providers/openai.d.ts +9 -0
- package/dist/summarizer/providers/openai.js +72 -0
- package/dist/summarizer/providers/types.d.ts +32 -0
- package/dist/summarizer/providers/types.js +1 -0
- package/dist/summarizer/retry.d.ts +7 -0
- package/dist/summarizer/retry.js +51 -0
- package/dist/summarizer/topological.d.ts +3 -0
- package/dist/summarizer/topological.js +105 -0
- package/dist/summarizer/types.d.ts +57 -0
- package/dist/summarizer/types.js +17 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- 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
|
+
}
|
package/dist/index.d.ts
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
|
+
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
|
+
}
|