pi-lens 3.2.0 → 3.3.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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -10
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/agent-behavior-client.test.js +94 -0
- package/clients/biome-client.test.js +144 -0
- package/clients/cache-manager.test.js +197 -0
- package/clients/complexity-client.test.js +234 -0
- package/clients/dependency-checker.test.js +60 -0
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.test.js +116 -0
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
- package/clients/dispatch/runners/lsp.js +42 -5
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/pyright.test.js +98 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/utils/format-utils.js +1 -6
- package/clients/dispatch/utils/format-utils.ts +1 -6
- package/clients/dogfood.test.js +201 -0
- package/clients/file-kinds.test.js +169 -0
- package/clients/formatters.js +1 -1
- package/clients/go-client.test.js +127 -0
- package/clients/jscpd-client.test.js +127 -0
- package/clients/knip-client.test.js +112 -0
- package/clients/lsp/__tests__/client.test.js +310 -0
- package/clients/lsp/__tests__/client.test.ts +1 -46
- package/clients/lsp/__tests__/config.test.js +167 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/launch.test.js +313 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/service.test.js +435 -0
- package/clients/lsp/client.js +32 -44
- package/clients/lsp/client.ts +36 -45
- package/clients/lsp/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- package/clients/lsp/server.js +27 -2
- package/clients/metrics-client.test.js +141 -0
- package/clients/ruff-client.test.js +132 -0
- package/clients/rust-client.test.js +108 -0
- package/clients/sanitize.test.js +177 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/test-runner-client.test.js +192 -0
- package/clients/todo-scanner.test.js +301 -0
- package/clients/type-coverage-client.test.js +105 -0
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.test.js +105 -0
- package/commands/rate.test.js +119 -0
- package/index.ts +66 -72
- package/package.json +1 -1
- package/clients/bus/bus.js +0 -191
- package/clients/bus/bus.ts +0 -251
- package/clients/bus/events.js +0 -214
- package/clients/bus/events.ts +0 -279
- package/clients/bus/index.js +0 -8
- package/clients/bus/index.ts +0 -9
- package/clients/bus/integration.js +0 -158
- package/clients/bus/integration.ts +0 -227
- package/clients/dispatch/bus-dispatcher.js +0 -178
- package/clients/dispatch/bus-dispatcher.ts +0 -258
- package/clients/services/__tests__/effect-integration.test.ts +0 -111
- package/clients/services/effect-integration.js +0 -198
- package/clients/services/effect-integration.ts +0 -276
- package/clients/services/index.js +0 -7
- package/clients/services/index.ts +0 -8
- package/clients/services/runner-service.js +0 -134
- package/clients/services/runner-service.ts +0 -225
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatRateResult } from "./rate.js";
|
|
3
|
+
// Test the formatting functions directly with mock data
|
|
4
|
+
describe("formatRateResult", () => {
|
|
5
|
+
it("should format a visual score breakdown", () => {
|
|
6
|
+
const result = {
|
|
7
|
+
overall: 75,
|
|
8
|
+
categories: [
|
|
9
|
+
{ name: "Type Safety", score: 85, icon: "🔷", issues: [] },
|
|
10
|
+
{ name: "Complexity", score: 70, icon: "🧩", issues: [] },
|
|
11
|
+
{ name: "Security", score: 100, icon: "🔒", issues: [] },
|
|
12
|
+
{ name: "Architecture", score: 85, icon: "🏗️", issues: [] },
|
|
13
|
+
{ name: "Dead Code", score: 100, icon: "🗑️", issues: [] },
|
|
14
|
+
{ name: "Tests", score: 100, icon: "✅", issues: [] },
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
const output = formatRateResult(result);
|
|
18
|
+
expect(output).toContain("CODE QUALITY SCORE");
|
|
19
|
+
expect(output).toContain("75/100");
|
|
20
|
+
expect(output).toContain("Type Safety");
|
|
21
|
+
expect(output).toContain("Security");
|
|
22
|
+
expect(output).toContain("Tests");
|
|
23
|
+
});
|
|
24
|
+
it("should show correct grade for A", () => {
|
|
25
|
+
const result = {
|
|
26
|
+
overall: 95,
|
|
27
|
+
categories: Array(6).fill({
|
|
28
|
+
name: "Test",
|
|
29
|
+
score: 95,
|
|
30
|
+
icon: "✅",
|
|
31
|
+
issues: [],
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
const output = formatRateResult(result);
|
|
35
|
+
expect(output).toContain("A");
|
|
36
|
+
});
|
|
37
|
+
it("should show correct grade for B", () => {
|
|
38
|
+
const result = {
|
|
39
|
+
overall: 85,
|
|
40
|
+
categories: Array(6).fill({
|
|
41
|
+
name: "Test",
|
|
42
|
+
score: 85,
|
|
43
|
+
icon: "✅",
|
|
44
|
+
issues: [],
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
const output = formatRateResult(result);
|
|
48
|
+
expect(output).toContain("B");
|
|
49
|
+
});
|
|
50
|
+
it("should show correct grade for C", () => {
|
|
51
|
+
const result = {
|
|
52
|
+
overall: 75,
|
|
53
|
+
categories: Array(6).fill({
|
|
54
|
+
name: "Test",
|
|
55
|
+
score: 75,
|
|
56
|
+
icon: "✅",
|
|
57
|
+
issues: [],
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
const output = formatRateResult(result);
|
|
61
|
+
expect(output).toContain("C");
|
|
62
|
+
});
|
|
63
|
+
it("should show issues section when there are problems", () => {
|
|
64
|
+
const result = {
|
|
65
|
+
overall: 50,
|
|
66
|
+
categories: [
|
|
67
|
+
{
|
|
68
|
+
name: "Type Safety",
|
|
69
|
+
score: 50,
|
|
70
|
+
icon: "🔷",
|
|
71
|
+
issues: ["50 untyped identifiers"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "Complexity",
|
|
75
|
+
score: 50,
|
|
76
|
+
icon: "🧩",
|
|
77
|
+
issues: ["High complexity: foo.ts"],
|
|
78
|
+
},
|
|
79
|
+
{ name: "Security", score: 100, icon: "🔒", issues: [] },
|
|
80
|
+
{ name: "Architecture", score: 100, icon: "🏗️", issues: [] },
|
|
81
|
+
{ name: "Dead Code", score: 100, icon: "🗑️", issues: [] },
|
|
82
|
+
{ name: "Tests", score: 100, icon: "✅", issues: [] },
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
const output = formatRateResult(result);
|
|
86
|
+
expect(output).toContain("Issues to address");
|
|
87
|
+
expect(output).toContain("Type Safety");
|
|
88
|
+
expect(output).toContain("/lens-booboo");
|
|
89
|
+
});
|
|
90
|
+
it("should not show issues section when clean", () => {
|
|
91
|
+
const result = {
|
|
92
|
+
overall: 100,
|
|
93
|
+
categories: Array(6).fill({
|
|
94
|
+
name: "Test",
|
|
95
|
+
score: 100,
|
|
96
|
+
icon: "✅",
|
|
97
|
+
issues: [],
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
const output = formatRateResult(result);
|
|
101
|
+
expect(output).not.toContain("Issues to address");
|
|
102
|
+
});
|
|
103
|
+
it("should use colored bars based on score", () => {
|
|
104
|
+
const resultHigh = {
|
|
105
|
+
overall: 90,
|
|
106
|
+
categories: [{ name: "Test", score: 85, icon: "✅", issues: [] }],
|
|
107
|
+
};
|
|
108
|
+
const resultLow = {
|
|
109
|
+
overall: 50,
|
|
110
|
+
categories: [{ name: "Test", score: 50, icon: "✅", issues: [] }],
|
|
111
|
+
};
|
|
112
|
+
const outputHigh = formatRateResult(resultHigh);
|
|
113
|
+
const outputLow = formatRateResult(resultLow);
|
|
114
|
+
// High score should have green squares
|
|
115
|
+
expect(outputHigh).toContain("🟩");
|
|
116
|
+
// Low score should have red squares
|
|
117
|
+
expect(outputLow).toContain("🟥");
|
|
118
|
+
});
|
|
119
|
+
});
|
package/index.ts
CHANGED
|
@@ -9,17 +9,9 @@ import { AgentBehaviorClient } from "./clients/agent-behavior-client.js";
|
|
|
9
9
|
import { ArchitectClient } from "./clients/architect-client.js";
|
|
10
10
|
import { AstGrepClient } from "./clients/ast-grep-client.js";
|
|
11
11
|
import { BiomeClient } from "./clients/biome-client.js";
|
|
12
|
-
import {
|
|
13
|
-
enableDebug as enableBusDebug,
|
|
14
|
-
FileModified,
|
|
15
|
-
initBusIntegration,
|
|
16
|
-
SessionStarted,
|
|
17
|
-
TurnEnded,
|
|
18
|
-
} from "./clients/bus/index.js";
|
|
19
12
|
import { CacheManager } from "./clients/cache-manager.js";
|
|
20
13
|
import { ComplexityClient } from "./clients/complexity-client.js";
|
|
21
14
|
import { DependencyChecker } from "./clients/dependency-checker.js";
|
|
22
|
-
import { dispatchLintWithBus } from "./clients/dispatch/bus-dispatcher.js";
|
|
23
15
|
import { dispatchLint } from "./clients/dispatch/integration.js";
|
|
24
16
|
import { createFileTime, FileTimeError } from "./clients/file-time.js";
|
|
25
17
|
import {
|
|
@@ -45,7 +37,6 @@ import {
|
|
|
45
37
|
import { RustClient } from "./clients/rust-client.js";
|
|
46
38
|
import { getSourceFiles } from "./clients/scan-utils.js";
|
|
47
39
|
import { formatSecrets, scanForSecrets } from "./clients/secrets-scanner.js";
|
|
48
|
-
import { dispatchLintWithEffect } from "./clients/services/effect-integration.js";
|
|
49
40
|
import { TestRunnerClient } from "./clients/test-runner-client.js";
|
|
50
41
|
import { TodoScanner } from "./clients/todo-scanner.js";
|
|
51
42
|
import { TypeCoverageClient } from "./clients/type-coverage-client.js";
|
|
@@ -124,6 +115,47 @@ function log(msg: string) {
|
|
|
124
115
|
if (_verbose) console.error(`[pi-lens] ${msg}`);
|
|
125
116
|
}
|
|
126
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Find and delete stale tsconfig.tsbuildinfo files in the project.
|
|
120
|
+
*
|
|
121
|
+
* A tsbuildinfo is stale when its `root` array references files that no
|
|
122
|
+
* longer exist on disk. The TypeScript Language Server reads this cache
|
|
123
|
+
* on startup and will report phantom "Cannot find module" errors for
|
|
124
|
+
* every deleted file until the cache is cleared.
|
|
125
|
+
*
|
|
126
|
+
* Only called when --lens-lsp is active (that’s when tsserver runs).
|
|
127
|
+
*/
|
|
128
|
+
function cleanStaleTsBuildInfo(cwd: string): string[] {
|
|
129
|
+
const cleaned: string[] = [];
|
|
130
|
+
try {
|
|
131
|
+
// Find all tsbuildinfo files in the project (max depth 3 to avoid crawling)
|
|
132
|
+
const candidates = nodeFs
|
|
133
|
+
.readdirSync(cwd)
|
|
134
|
+
.filter((f) => f.endsWith(".tsbuildinfo"))
|
|
135
|
+
.map((f) => path.join(cwd, f));
|
|
136
|
+
|
|
137
|
+
for (const infoPath of candidates) {
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(nodeFs.readFileSync(infoPath, "utf-8"));
|
|
140
|
+
const root: string[] = data.root ?? [];
|
|
141
|
+
const dir = path.dirname(infoPath);
|
|
142
|
+
const isStale = root.some(
|
|
143
|
+
(f) => !nodeFs.existsSync(path.resolve(dir, f)),
|
|
144
|
+
);
|
|
145
|
+
if (isStale) {
|
|
146
|
+
nodeFs.unlinkSync(infoPath);
|
|
147
|
+
cleaned.push(infoPath);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Malformed or unreadable — skip
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// readdirSync failed — skip
|
|
155
|
+
}
|
|
156
|
+
return cleaned;
|
|
157
|
+
}
|
|
158
|
+
|
|
127
159
|
// --- Extension ---
|
|
128
160
|
|
|
129
161
|
export default function (pi: ExtensionAPI) {
|
|
@@ -251,25 +283,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
283
|
default: false,
|
|
252
284
|
});
|
|
253
285
|
|
|
254
|
-
// Internal flag for development/debugging (not surfaced to users)
|
|
255
|
-
pi.registerFlag("lens-bus", {
|
|
256
|
-
description: "[Internal] Enable event bus system",
|
|
257
|
-
type: "boolean",
|
|
258
|
-
default: false,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
pi.registerFlag("lens-bus-debug", {
|
|
262
|
-
description: "Enable verbose bus event logging",
|
|
263
|
-
type: "boolean",
|
|
264
|
-
default: false,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
pi.registerFlag("lens-effect", {
|
|
268
|
-
description: "Enable Effect-TS concurrent runner execution (Phase 2)",
|
|
269
|
-
type: "boolean",
|
|
270
|
-
default: false,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
286
|
pi.registerFlag("lens-lsp", {
|
|
274
287
|
description:
|
|
275
288
|
"Enable LSP (Language Server Protocol) for semantic analysis (Phase 3)",
|
|
@@ -782,23 +795,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
782
795
|
_verbose = !!pi.getFlag("lens-verbose");
|
|
783
796
|
dbg("session_start fired");
|
|
784
797
|
|
|
785
|
-
// Initialize event bus system (Phase 1)
|
|
786
|
-
const busEnabled = pi.getFlag("lens-bus");
|
|
787
|
-
if (busEnabled) {
|
|
788
|
-
const busDebug = pi.getFlag("lens-bus-debug");
|
|
789
|
-
initBusIntegration(pi, { debug: !!busDebug });
|
|
790
|
-
if (busDebug) enableBusDebug(true);
|
|
791
|
-
SessionStarted.publish({
|
|
792
|
-
cwd: ctx.cwd ?? process.cwd(),
|
|
793
|
-
timestamp: Date.now(),
|
|
794
|
-
});
|
|
795
|
-
dbg("session_start: bus integration initialized");
|
|
796
|
-
}
|
|
797
|
-
|
|
798
798
|
// Reset session state
|
|
799
799
|
metricsClient.reset();
|
|
800
800
|
complexityBaselines.clear();
|
|
801
801
|
|
|
802
|
+
// Reset LSP service so the new session starts with fresh diagnostics.
|
|
803
|
+
// Without this, stale cascade errors from a previous session persist
|
|
804
|
+
// if the extension module stayed hot between reloads.
|
|
805
|
+
if (pi.getFlag("lens-lsp")) {
|
|
806
|
+
resetLSPService();
|
|
807
|
+
dbg("session_start: LSP service reset");
|
|
808
|
+
}
|
|
809
|
+
|
|
802
810
|
// Log available tools
|
|
803
811
|
const tools: string[] = [];
|
|
804
812
|
tools.push("TypeScript LSP"); // Always available
|
|
@@ -813,6 +821,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
813
821
|
log(`Active tools: ${tools.join(", ")}`);
|
|
814
822
|
dbg(`session_start tools: ${tools.join(", ")}`);
|
|
815
823
|
|
|
824
|
+
// Clean up stale TypeScript build caches before LSP starts.
|
|
825
|
+
// tsconfig.tsbuildinfo caches the full file list from the last build.
|
|
826
|
+
// If files have been deleted since then, the LSP reads the stale list
|
|
827
|
+
// and reports phantom "Cannot find module" cascade errors for files
|
|
828
|
+
// the agent never touched.
|
|
829
|
+
if (pi.getFlag("lens-lsp")) {
|
|
830
|
+
const cleaned = cleanStaleTsBuildInfo(ctx.cwd ?? process.cwd());
|
|
831
|
+
if (cleaned.length > 0) {
|
|
832
|
+
ctx.ui.notify(
|
|
833
|
+
`🧹 Deleted stale TypeScript build cache (${cleaned.map((f) => path.basename(f)).join(", ")}) — phantom errors suppressed.`,
|
|
834
|
+
"info",
|
|
835
|
+
);
|
|
836
|
+
dbg(`session_start: cleaned stale tsbuildinfo: ${cleaned.join(", ")}`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
816
840
|
// Pre-install TypeScript LSP if --lens-lsp flag is set (avoid delay on first use)
|
|
817
841
|
if (pi.getFlag("lens-lsp")) {
|
|
818
842
|
dbg("session_start: pre-installing TypeScript LSP...");
|
|
@@ -1319,14 +1343,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1319
1343
|
phaseEnd("format", { formattersUsed, formatChanged });
|
|
1320
1344
|
|
|
1321
1345
|
// --- Publish file modified event to bus (Phase 1) ---
|
|
1322
|
-
if (pi.getFlag("lens-bus")) {
|
|
1323
|
-
FileModified.publish({
|
|
1324
|
-
filePath,
|
|
1325
|
-
content: fileContent,
|
|
1326
|
-
changeType: event.toolName === "edit" ? "edit" : "write",
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
1346
|
// --- LSP integration (Phase 3) ---
|
|
1331
1347
|
if (pi.getFlag("lens-lsp") && fileContent) {
|
|
1332
1348
|
const lspService = getLSPService();
|
|
@@ -1418,18 +1434,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1418
1434
|
// Phase 2: Replaced ~400 lines of if/else with unified dispatch system
|
|
1419
1435
|
dbg(`dispatch: running lint tools for ${filePath}`);
|
|
1420
1436
|
|
|
1421
|
-
|
|
1422
|
-
// - lens-effect: Effect-TS concurrent execution (Phase 2)
|
|
1423
|
-
// - lens-bus: Bus-enabled dispatcher (Phase 1)
|
|
1424
|
-
// - default: Original sequential dispatcher
|
|
1425
|
-
let dispatchOutput: string;
|
|
1426
|
-
if (pi.getFlag("lens-effect")) {
|
|
1427
|
-
dispatchOutput = await dispatchLintWithEffect(filePath, projectRoot, pi);
|
|
1428
|
-
} else if (pi.getFlag("lens-bus")) {
|
|
1429
|
-
dispatchOutput = await dispatchLintWithBus(filePath, projectRoot, pi);
|
|
1430
|
-
} else {
|
|
1431
|
-
dispatchOutput = await dispatchLint(filePath, projectRoot, pi);
|
|
1432
|
-
}
|
|
1437
|
+
const dispatchOutput = await dispatchLint(filePath, projectRoot, pi);
|
|
1433
1438
|
|
|
1434
1439
|
if (dispatchOutput) {
|
|
1435
1440
|
lspOutput += `\n\n${dispatchOutput}`;
|
|
@@ -1447,8 +1452,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1447
1452
|
}
|
|
1448
1453
|
phaseEnd("dispatch_lint", {
|
|
1449
1454
|
hasOutput: !!dispatchOutput,
|
|
1450
|
-
effect: pi.getFlag("lens-effect"),
|
|
1451
|
-
bus: pi.getFlag("lens-bus"),
|
|
1452
1455
|
});
|
|
1453
1456
|
|
|
1454
1457
|
// --- Test runner: run corresponding tests on write ---
|
|
@@ -1643,15 +1646,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1643
1646
|
const turnState = cacheManager.readTurnState(cwd);
|
|
1644
1647
|
const files = Object.keys(turnState.files);
|
|
1645
1648
|
|
|
1646
|
-
// Publish turn ended event to bus (Phase 1)
|
|
1647
|
-
if (pi.getFlag("lens-bus") && files.length > 0) {
|
|
1648
|
-
TurnEnded.publish({
|
|
1649
|
-
cwd,
|
|
1650
|
-
modifiedFiles: files,
|
|
1651
|
-
timestamp: Date.now(),
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
1649
|
if (files.length === 0) return;
|
|
1656
1650
|
|
|
1657
1651
|
dbg(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension for real-time code quality — 31 LSP servers, tree-sitter structural analysis, AST pattern matching, auto-install for TypeScript/Python tooling, duplicate detection, complexity metrics, and inline blockers with comprehensive /lens-booboo reports",
|
|
6
6
|
"repository": {
|
package/clients/bus/bus.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event Bus for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* Decoupled pub/sub system for diagnostic events.
|
|
5
|
-
* Enables loose coupling between diagnostic producers (runners, LSP clients)
|
|
6
|
-
* and consumers (UI, aggregators, history trackers).
|
|
7
|
-
*/
|
|
8
|
-
// --- Internal State ---
|
|
9
|
-
const subscribers = new Map();
|
|
10
|
-
const globalMiddleware = [];
|
|
11
|
-
let eventCount = 0;
|
|
12
|
-
let isDebugEnabled = false;
|
|
13
|
-
// --- Core Functions ---
|
|
14
|
-
function debug(type, event) {
|
|
15
|
-
if (!isDebugEnabled)
|
|
16
|
-
return;
|
|
17
|
-
const timestamp = new Date(event.timestamp).toISOString();
|
|
18
|
-
console.error(`[bus] [${timestamp}] ${type}: ${event.type}`);
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Publish an event to all subscribers
|
|
22
|
-
*/
|
|
23
|
-
export function publish(event) {
|
|
24
|
-
eventCount++;
|
|
25
|
-
debug("publish", event);
|
|
26
|
-
// Run through middleware
|
|
27
|
-
let currentEvent = event;
|
|
28
|
-
for (const mw of globalMiddleware) {
|
|
29
|
-
currentEvent = mw(currentEvent);
|
|
30
|
-
if (!currentEvent)
|
|
31
|
-
return; // Middleware cancelled the event
|
|
32
|
-
}
|
|
33
|
-
const handlers = subscribers.get(event.type);
|
|
34
|
-
if (!handlers || handlers.size === 0)
|
|
35
|
-
return;
|
|
36
|
-
// Notify all subscribers (fire-and-forget for async handlers)
|
|
37
|
-
for (const handler of handlers) {
|
|
38
|
-
try {
|
|
39
|
-
const result = handler(currentEvent);
|
|
40
|
-
if (result instanceof Promise) {
|
|
41
|
-
result.catch((err) => {
|
|
42
|
-
console.error(`[bus] async handler error for ${event.type}:`, err);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
console.error(`[bus] handler error for ${event.type}:`, err);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Subscribe to a specific event type
|
|
53
|
-
* @returns Unsubscribe function
|
|
54
|
-
*/
|
|
55
|
-
export function subscribe(eventType, handler) {
|
|
56
|
-
if (!subscribers.has(eventType)) {
|
|
57
|
-
subscribers.set(eventType, new Set());
|
|
58
|
-
}
|
|
59
|
-
const handlers = subscribers.get(eventType);
|
|
60
|
-
handlers.add(handler);
|
|
61
|
-
debug("subscribe", { type: eventType, properties: {}, timestamp: Date.now() });
|
|
62
|
-
// Return unsubscribe function
|
|
63
|
-
return () => {
|
|
64
|
-
handlers.delete(handler);
|
|
65
|
-
if (handlers.size === 0) {
|
|
66
|
-
subscribers.delete(eventType);
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Subscribe to an event type and automatically unsubscribe after first match
|
|
72
|
-
*/
|
|
73
|
-
export function once(eventType, predicate) {
|
|
74
|
-
return new Promise((resolve) => {
|
|
75
|
-
const unsubscribe = subscribe(eventType, (event) => {
|
|
76
|
-
if (!predicate || predicate(event)) {
|
|
77
|
-
unsubscribe();
|
|
78
|
-
resolve(event);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Wait for an event with timeout
|
|
85
|
-
*/
|
|
86
|
-
export function waitFor(eventType, timeoutMs, predicate) {
|
|
87
|
-
return new Promise((resolve) => {
|
|
88
|
-
const timer = setTimeout(() => {
|
|
89
|
-
unsubscribe();
|
|
90
|
-
resolve(undefined);
|
|
91
|
-
}, timeoutMs);
|
|
92
|
-
const unsubscribe = subscribe(eventType, (event) => {
|
|
93
|
-
if (!predicate || predicate(event)) {
|
|
94
|
-
clearTimeout(timer);
|
|
95
|
-
unsubscribe();
|
|
96
|
-
resolve(event);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
// --- Middleware ---
|
|
102
|
-
export function addMiddleware(mw) {
|
|
103
|
-
globalMiddleware.push(mw);
|
|
104
|
-
return () => {
|
|
105
|
-
const idx = globalMiddleware.indexOf(mw);
|
|
106
|
-
if (idx !== -1)
|
|
107
|
-
globalMiddleware.splice(idx, 1);
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
// --- Utilities ---
|
|
111
|
-
export function enableDebug(enabled = true) {
|
|
112
|
-
isDebugEnabled = enabled;
|
|
113
|
-
}
|
|
114
|
-
export function getStats() {
|
|
115
|
-
return {
|
|
116
|
-
subscriberCount: Array.from(subscribers.values()).reduce((sum, set) => sum + set.size, 0),
|
|
117
|
-
eventTypes: Array.from(subscribers.keys()),
|
|
118
|
-
totalEvents: eventCount,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
export function clearAllSubscribers() {
|
|
122
|
-
subscribers.clear();
|
|
123
|
-
}
|
|
124
|
-
export var BusEvent;
|
|
125
|
-
(function (BusEvent) {
|
|
126
|
-
/**
|
|
127
|
-
* Define a typed event type with Zod schema validation
|
|
128
|
-
*/
|
|
129
|
-
function define(type, _schema) {
|
|
130
|
-
return {
|
|
131
|
-
type,
|
|
132
|
-
create(properties) {
|
|
133
|
-
return {
|
|
134
|
-
type,
|
|
135
|
-
properties,
|
|
136
|
-
timestamp: Date.now(),
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
subscribe(handler) {
|
|
140
|
-
return subscribe(type, handler);
|
|
141
|
-
},
|
|
142
|
-
publish(properties) {
|
|
143
|
-
publish({
|
|
144
|
-
type,
|
|
145
|
-
properties,
|
|
146
|
-
timestamp: Date.now(),
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
BusEvent.define = define;
|
|
152
|
-
/**
|
|
153
|
-
* Create a simple event type without schema (for internal use)
|
|
154
|
-
*/
|
|
155
|
-
function defineSimple(type) {
|
|
156
|
-
return {
|
|
157
|
-
type,
|
|
158
|
-
create(properties) {
|
|
159
|
-
return {
|
|
160
|
-
type,
|
|
161
|
-
properties,
|
|
162
|
-
timestamp: Date.now(),
|
|
163
|
-
};
|
|
164
|
-
},
|
|
165
|
-
subscribe(handler) {
|
|
166
|
-
return subscribe(type, handler);
|
|
167
|
-
},
|
|
168
|
-
publish(properties) {
|
|
169
|
-
publish({
|
|
170
|
-
type,
|
|
171
|
-
properties,
|
|
172
|
-
timestamp: Date.now(),
|
|
173
|
-
});
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
BusEvent.defineSimple = defineSimple;
|
|
178
|
-
/**
|
|
179
|
-
* Create a raw event (helper)
|
|
180
|
-
*/
|
|
181
|
-
function create(type, properties) {
|
|
182
|
-
return {
|
|
183
|
-
type,
|
|
184
|
-
properties,
|
|
185
|
-
timestamp: Date.now(),
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
BusEvent.create = create;
|
|
189
|
-
})(BusEvent || (BusEvent = {}));
|
|
190
|
-
// --- Re-export for convenience ---
|
|
191
|
-
export { subscribe as on, publish as emit };
|