@karmaniverous/jeeves-server 3.0.0-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/.env.local +13 -0
- package/.env.local.template +13 -0
- package/.tsbuildinfo +1 -0
- package/CHANGELOG.md +450 -0
- package/about.md +82 -0
- package/client/README.md +73 -0
- package/client/eslint.config.js +23 -0
- package/client/index.html +14 -0
- package/client/package-lock.json +5181 -0
- package/client/package.json +60 -0
- package/client/public/vite.svg +1 -0
- package/client/src/App.tsx +22 -0
- package/client/src/components/AccountMenu.tsx +167 -0
- package/client/src/components/ActionDropdown.tsx +120 -0
- package/client/src/components/CodeEditor.tsx +143 -0
- package/client/src/components/CodeViewer.tsx +113 -0
- package/client/src/components/ConfirmDialog.tsx +32 -0
- package/client/src/components/DirectoryRow.tsx +62 -0
- package/client/src/components/DirectoryTable.tsx +42 -0
- package/client/src/components/DownloadDropdown.tsx +116 -0
- package/client/src/components/DriveList.tsx +54 -0
- package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
- package/client/src/components/FileContentView.tsx +155 -0
- package/client/src/components/InlineSvgPanzoom.ts +60 -0
- package/client/src/components/LazyDiagram.ts +93 -0
- package/client/src/components/LinkDropdown.tsx +134 -0
- package/client/src/components/MarkdownView.tsx +115 -0
- package/client/src/components/MermaidViewer.tsx +21 -0
- package/client/src/components/PlantUmlViewer.tsx +21 -0
- package/client/src/components/SearchModal.tsx +424 -0
- package/client/src/components/SvgViewer.tsx +107 -0
- package/client/src/components/TabBar.tsx +96 -0
- package/client/src/components/layout/Header.tsx +270 -0
- package/client/src/components/panzoom.ts +203 -0
- package/client/src/components/renderableUtils.ts +15 -0
- package/client/src/components/runner/JobTable.tsx +153 -0
- package/client/src/components/runner/RunHistory.tsx +140 -0
- package/client/src/components/runner/StatsBar.tsx +43 -0
- package/client/src/components/runner/StatusPill.tsx +27 -0
- package/client/src/components/runner/jobTableUtils.ts +65 -0
- package/client/src/components/scrollUtils.ts +39 -0
- package/client/src/components/ui/alert-dialog.tsx +107 -0
- package/client/src/components/ui/button.tsx +40 -0
- package/client/src/components/ui/dropdown-menu.tsx +79 -0
- package/client/src/components/ui/input.tsx +26 -0
- package/client/src/components/useActionState.ts +43 -0
- package/client/src/hooks/useFileBrowser.ts +102 -0
- package/client/src/hooks/useFileData.ts +78 -0
- package/client/src/hooks/useScrollAnchor.ts +70 -0
- package/client/src/hooks/useShareSettings.ts +22 -0
- package/client/src/hooks/useTopBar.ts +27 -0
- package/client/src/index.css +281 -0
- package/client/src/lib/AuthContext.ts +27 -0
- package/client/src/lib/api.ts +239 -0
- package/client/src/lib/auth.tsx +50 -0
- package/client/src/lib/codeBlockCm6.ts +129 -0
- package/client/src/lib/codeBlockCopy.ts +43 -0
- package/client/src/lib/codemirror.ts +77 -0
- package/client/src/lib/runner-api.ts +172 -0
- package/client/src/lib/svg.ts +50 -0
- package/client/src/lib/theme.ts +34 -0
- package/client/src/lib/utils.ts +6 -0
- package/client/src/main.tsx +11 -0
- package/client/src/pages/FileBrowser.tsx +135 -0
- package/client/src/pages/Home.tsx +46 -0
- package/client/src/pages/Runner.tsx +151 -0
- package/client/src/pages/RunnerJob.tsx +170 -0
- package/client/tsconfig.app.json +32 -0
- package/client/tsconfig.json +7 -0
- package/client/tsconfig.node.json +26 -0
- package/client/vite.config.ts +35 -0
- package/content/privacy.md +61 -0
- package/content/terms.md +41 -0
- package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
- package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
- package/dist/client/assets/index--MBieNJA.js +1 -0
- package/dist/client/assets/index-BENeXQI_.js +1 -0
- package/dist/client/assets/index-BbBpoOxz.js +1 -0
- package/dist/client/assets/index-BdV9g5AM.js +6 -0
- package/dist/client/assets/index-BjAilRri.js +2 -0
- package/dist/client/assets/index-BqbhWo2I.js +3 -0
- package/dist/client/assets/index-CVbycZ0H.js +1 -0
- package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
- package/dist/client/assets/index-D8KZVveX.js +1 -0
- package/dist/client/assets/index-DC4HMHxY.js +13 -0
- package/dist/client/assets/index-DbMebkkd.css +1 -0
- package/dist/client/assets/index-DcY2RXqX.js +1 -0
- package/dist/client/assets/index-Duy-tZYV.js +1 -0
- package/dist/client/assets/index-Dw7rDFmE.js +7 -0
- package/dist/client/assets/index-FlCUvrjv.js +2 -0
- package/dist/client/assets/index-K6OVmfhg.js +1 -0
- package/dist/client/assets/index-LjwgzZ7F.js +62 -0
- package/dist/client/assets/index-MLwyFRN0.js +1 -0
- package/dist/client/assets/index-OpqBpSjn.js +1 -0
- package/dist/client/assets/index-SsHei0HE.js +1 -0
- package/dist/client/assets/index-uQa2yckk.js +1 -0
- package/dist/client/assets/index-udkXoIER.js +1 -0
- package/dist/client/index.html +15 -0
- package/dist/client/vite.svg +1 -0
- package/dist/src/auth/google.js +57 -0
- package/dist/src/auth/keys.js +185 -0
- package/dist/src/auth/resolve.js +102 -0
- package/dist/src/auth/session.js +57 -0
- package/dist/src/cli/commands/config.js +100 -0
- package/dist/src/cli/commands/config.test.js +84 -0
- package/dist/src/cli/commands/service.js +93 -0
- package/dist/src/cli/commands/start.js +24 -0
- package/dist/src/cli/index.js +20 -0
- package/dist/src/config/index.js +90 -0
- package/dist/src/config/loadConfig.test.js +127 -0
- package/dist/src/config/resolve.js +134 -0
- package/dist/src/config/resolve.test.js +148 -0
- package/dist/src/config/schema.js +159 -0
- package/dist/src/config/substituteEnvVars.js +45 -0
- package/dist/src/config/substituteEnvVars.test.js +51 -0
- package/dist/src/config/types.js +5 -0
- package/dist/src/routes/api/auth-status.js +56 -0
- package/dist/src/routes/api/diagrams.js +35 -0
- package/dist/src/routes/api/directory.js +93 -0
- package/dist/src/routes/api/drives.js +15 -0
- package/dist/src/routes/api/export.js +218 -0
- package/dist/src/routes/api/fileContent.js +286 -0
- package/dist/src/routes/api/index.js +33 -0
- package/dist/src/routes/api/linkInfo.js +71 -0
- package/dist/src/routes/api/linkInfo.test.js +104 -0
- package/dist/src/routes/api/middleware.js +117 -0
- package/dist/src/routes/api/raw.js +38 -0
- package/dist/src/routes/api/runner.js +59 -0
- package/dist/src/routes/api/search.js +236 -0
- package/dist/src/routes/api/sharing.js +203 -0
- package/dist/src/routes/api/status.js +68 -0
- package/dist/src/routes/api/status.test.js +62 -0
- package/dist/src/routes/auth.js +99 -0
- package/dist/src/routes/event.js +77 -0
- package/dist/src/routes/event.test.js +206 -0
- package/dist/src/routes/health.js +10 -0
- package/dist/src/routes/keys.js +129 -0
- package/dist/src/routes/path/index.js +17 -0
- package/dist/src/routes/static.js +30 -0
- package/dist/src/server.js +90 -0
- package/dist/src/services/deepShareLinks.js +163 -0
- package/dist/src/services/diagramCache.js +104 -0
- package/dist/src/services/embeddedDiagrams.js +136 -0
- package/dist/src/services/eventLog.js +55 -0
- package/dist/src/services/eventLog.test.js +113 -0
- package/dist/src/services/eventQueue.js +154 -0
- package/dist/src/services/eventQueue.test.js +104 -0
- package/dist/src/services/export.js +220 -0
- package/dist/src/services/exportCache.js +196 -0
- package/dist/src/services/markdown.js +147 -0
- package/dist/src/services/mermaid.js +97 -0
- package/dist/src/services/plantuml.js +145 -0
- package/dist/src/services/puppeteer.js +156 -0
- package/dist/src/util/breadcrumbs.js +22 -0
- package/dist/src/util/crypto.js +56 -0
- package/dist/src/util/crypto.test.js +99 -0
- package/dist/src/util/fileDetection.js +66 -0
- package/dist/src/util/fileDetection.test.js +89 -0
- package/dist/src/util/formatters.js +43 -0
- package/dist/src/util/formatters.test.js +83 -0
- package/dist/src/util/packageVersion.js +25 -0
- package/dist/src/util/platform.js +148 -0
- package/dist/src/util/state.js +46 -0
- package/dist/vitest.config.js +12 -0
- package/favicon.svg +3 -0
- package/guides/access-decision-flow.mmd +24 -0
- package/guides/access-decision-flow.svg +1 -0
- package/guides/api-integration.md +236 -0
- package/guides/deployment.md +287 -0
- package/guides/event-gateway.md +204 -0
- package/guides/event-gateway.mmd +17 -0
- package/guides/event-gateway.svg +1 -0
- package/guides/exports.md +239 -0
- package/guides/setup.md +313 -0
- package/guides/sharing.md +204 -0
- package/jeeves-server.config.template.json +25 -0
- package/package.json +124 -0
- package/scripts/download-plantuml.js +70 -0
- package/src/auth/google.ts +93 -0
- package/src/auth/keys.ts +252 -0
- package/src/auth/resolve.ts +157 -0
- package/src/auth/session.ts +77 -0
- package/src/cli/commands/config.test.ts +107 -0
- package/src/cli/commands/config.ts +113 -0
- package/src/cli/commands/service.ts +129 -0
- package/src/cli/commands/start.ts +27 -0
- package/src/cli/index.ts +25 -0
- package/src/config/index.ts +113 -0
- package/src/config/loadConfig.test.ts +155 -0
- package/src/config/resolve.test.ts +192 -0
- package/src/config/resolve.ts +173 -0
- package/src/config/schema.ts +179 -0
- package/src/config/substituteEnvVars.test.ts +64 -0
- package/src/config/substituteEnvVars.ts +52 -0
- package/src/config/types.ts +129 -0
- package/src/routes/api/auth-status.ts +85 -0
- package/src/routes/api/diagrams.ts +53 -0
- package/src/routes/api/directory.ts +123 -0
- package/src/routes/api/drives.ts +23 -0
- package/src/routes/api/export.ts +314 -0
- package/src/routes/api/fileContent.ts +414 -0
- package/src/routes/api/index.ts +37 -0
- package/src/routes/api/linkInfo.test.ts +132 -0
- package/src/routes/api/linkInfo.ts +83 -0
- package/src/routes/api/middleware.ts +156 -0
- package/src/routes/api/raw.ts +54 -0
- package/src/routes/api/runner.ts +107 -0
- package/src/routes/api/search.ts +321 -0
- package/src/routes/api/sharing.ts +259 -0
- package/src/routes/api/status.test.ts +72 -0
- package/src/routes/api/status.ts +82 -0
- package/src/routes/auth.ts +143 -0
- package/src/routes/event.test.ts +248 -0
- package/src/routes/event.ts +109 -0
- package/src/routes/health.ts +13 -0
- package/src/routes/keys.ts +192 -0
- package/src/routes/path/index.ts +24 -0
- package/src/routes/static.ts +54 -0
- package/src/server.ts +104 -0
- package/src/services/deepShareLinks.ts +203 -0
- package/src/services/diagramCache.ts +128 -0
- package/src/services/embeddedDiagrams.ts +168 -0
- package/src/services/eventLog.test.ts +144 -0
- package/src/services/eventLog.ts +68 -0
- package/src/services/eventQueue.test.ts +127 -0
- package/src/services/eventQueue.ts +196 -0
- package/src/services/export.ts +267 -0
- package/src/services/exportCache.ts +216 -0
- package/src/services/markdown.ts +189 -0
- package/src/services/mermaid.ts +113 -0
- package/src/services/plantuml.ts +172 -0
- package/src/services/puppeteer.ts +188 -0
- package/src/types/fastify.d.ts +13 -0
- package/src/types/jsonmap.d.ts +10 -0
- package/src/types/plantuml-encoder.d.ts +4 -0
- package/src/util/breadcrumbs.ts +33 -0
- package/src/util/crypto.test.ts +132 -0
- package/src/util/crypto.ts +79 -0
- package/src/util/fileDetection.test.ts +115 -0
- package/src/util/fileDetection.ts +70 -0
- package/src/util/formatters.test.ts +105 -0
- package/src/util/formatters.ts +44 -0
- package/src/util/packageVersion.ts +30 -0
- package/src/util/platform.ts +178 -0
- package/src/util/state.ts +55 -0
- package/test-docs/diagram-retry-test.md +18 -0
- package/test-docs/embedded-diagrams.md +52 -0
- package/test-docs/lazy-diagrams-test.md +333 -0
- package/test-docs/page-a.md +7 -0
- package/test-docs/page-b.md +7 -0
- package/test-docs/page-c.md +7 -0
- package/test-docs/sub/page-d.md +7 -0
- package/test-docs/test-diagram.puml +13 -0
- package/test-docs/validate-deep-share.js +318 -0
- package/tsconfig.json +37 -0
- package/tsdoc.json +13 -0
- package/vendor/.plantuml-version +1 -0
- package/vendor/plantuml.jar +0 -0
- package/vitest.config.js +12 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event log service tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import type { EventLogEntry } from '../config/types.js';
|
|
11
|
+
import { nowIso } from '../util/formatters.js';
|
|
12
|
+
|
|
13
|
+
// Test helper functions (isolated from config system)
|
|
14
|
+
function parseJsonl(filePath: string): EventLogEntry[] {
|
|
15
|
+
if (!fs.existsSync(filePath)) return [];
|
|
16
|
+
|
|
17
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
18
|
+
return content
|
|
19
|
+
.split('\n')
|
|
20
|
+
.filter((line) => line.trim())
|
|
21
|
+
.map((line) => JSON.parse(line) as EventLogEntry);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeJsonl(filePath: string, entries: EventLogEntry[]): void {
|
|
25
|
+
const content = entries.map((entry) => JSON.stringify(entry)).join('\n');
|
|
26
|
+
fs.writeFileSync(filePath, content + '\n', 'utf8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function purgeOldEntries(
|
|
30
|
+
entries: EventLogEntry[],
|
|
31
|
+
retentionMs: number,
|
|
32
|
+
): EventLogEntry[] {
|
|
33
|
+
const cutoff = new Date(Date.now() - retentionMs);
|
|
34
|
+
return entries.filter((entry) => new Date(entry.ts) >= cutoff);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('eventLog', () => {
|
|
38
|
+
it('should parse JSONL entries correctly', () => {
|
|
39
|
+
const entry1: EventLogEntry = {
|
|
40
|
+
ts: nowIso(),
|
|
41
|
+
event: 'test-event',
|
|
42
|
+
matched: true,
|
|
43
|
+
exitCode: 0,
|
|
44
|
+
durationMs: 123,
|
|
45
|
+
};
|
|
46
|
+
const entry2: EventLogEntry = {
|
|
47
|
+
ts: nowIso(),
|
|
48
|
+
event: null,
|
|
49
|
+
matched: false,
|
|
50
|
+
bodyPreview: '{"type":"unknown"}',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const tempFile = path.join(process.cwd(), 'test-temp-log.jsonl');
|
|
54
|
+
writeJsonl(tempFile, [entry1, entry2]);
|
|
55
|
+
|
|
56
|
+
const parsed = parseJsonl(tempFile);
|
|
57
|
+
|
|
58
|
+
expect(parsed).toHaveLength(2);
|
|
59
|
+
expect(parsed[0].event).toBe('test-event');
|
|
60
|
+
expect(parsed[0].matched).toBe(true);
|
|
61
|
+
expect(parsed[1].event).toBeNull();
|
|
62
|
+
expect(parsed[1].matched).toBe(false);
|
|
63
|
+
|
|
64
|
+
// Cleanup
|
|
65
|
+
fs.unlinkSync(tempFile);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should purge entries older than retention period', () => {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const retentionMs = 86400000; // 1 day
|
|
71
|
+
|
|
72
|
+
const oldEntry: EventLogEntry = {
|
|
73
|
+
ts: new Date(now - 2 * retentionMs).toISOString(),
|
|
74
|
+
event: 'old-event',
|
|
75
|
+
matched: true,
|
|
76
|
+
exitCode: 0,
|
|
77
|
+
durationMs: 100,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const recentEntry: EventLogEntry = {
|
|
81
|
+
ts: new Date(now - 0.5 * retentionMs).toISOString(),
|
|
82
|
+
event: 'recent-event',
|
|
83
|
+
matched: true,
|
|
84
|
+
exitCode: 0,
|
|
85
|
+
durationMs: 200,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const currentEntry: EventLogEntry = {
|
|
89
|
+
ts: new Date(now).toISOString(),
|
|
90
|
+
event: 'current-event',
|
|
91
|
+
matched: true,
|
|
92
|
+
exitCode: 0,
|
|
93
|
+
durationMs: 300,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const purged = purgeOldEntries(
|
|
97
|
+
[oldEntry, recentEntry, currentEntry],
|
|
98
|
+
retentionMs,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(purged).toHaveLength(2);
|
|
102
|
+
expect(purged[0].event).toBe('recent-event');
|
|
103
|
+
expect(purged[1].event).toBe('current-event');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should write JSONL with proper newline formatting', () => {
|
|
107
|
+
const entries: EventLogEntry[] = [
|
|
108
|
+
{
|
|
109
|
+
ts: nowIso(),
|
|
110
|
+
event: 'event1',
|
|
111
|
+
matched: true,
|
|
112
|
+
exitCode: 0,
|
|
113
|
+
durationMs: 100,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
ts: nowIso(),
|
|
117
|
+
event: 'event2',
|
|
118
|
+
matched: true,
|
|
119
|
+
exitCode: 1,
|
|
120
|
+
durationMs: 200,
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const tempFile = path.join(process.cwd(), 'test-temp-write.jsonl');
|
|
125
|
+
writeJsonl(tempFile, entries);
|
|
126
|
+
|
|
127
|
+
const content = fs.readFileSync(tempFile, 'utf8');
|
|
128
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
129
|
+
|
|
130
|
+
expect(lines).toHaveLength(2);
|
|
131
|
+
expect((JSON.parse(lines[0]) as EventLogEntry).event).toBe('event1');
|
|
132
|
+
expect((JSON.parse(lines[1]) as EventLogEntry).event).toBe('event2');
|
|
133
|
+
|
|
134
|
+
// Cleanup
|
|
135
|
+
fs.unlinkSync(tempFile);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle empty log file', () => {
|
|
139
|
+
const tempFile = path.join(process.cwd(), 'test-temp-empty.jsonl');
|
|
140
|
+
const parsed = parseJsonl(tempFile);
|
|
141
|
+
|
|
142
|
+
expect(parsed).toHaveLength(0);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event logging service - logs all events (matched and unmatched)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { getConfig } from '../config/index.js';
|
|
9
|
+
import type { EventLogEntry } from '../config/types.js';
|
|
10
|
+
import { nowIso } from '../util/formatters.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensure directory exists
|
|
14
|
+
*/
|
|
15
|
+
function ensureDir(dirPath: string): void {
|
|
16
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse JSONL file into entries
|
|
21
|
+
*/
|
|
22
|
+
function parseJsonl(filePath: string): EventLogEntry[] {
|
|
23
|
+
if (!fs.existsSync(filePath)) return [];
|
|
24
|
+
|
|
25
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
return content
|
|
27
|
+
.split('\n')
|
|
28
|
+
.filter((line) => line.trim())
|
|
29
|
+
.map((line) => JSON.parse(line) as EventLogEntry);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write entries to JSONL file
|
|
34
|
+
*/
|
|
35
|
+
function writeJsonl(filePath: string, entries: EventLogEntry[]): void {
|
|
36
|
+
const content = entries.map((entry) => JSON.stringify(entry)).join('\n');
|
|
37
|
+
fs.writeFileSync(filePath, content + '\n', 'utf8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Purge entries older than configured retention period
|
|
42
|
+
*/
|
|
43
|
+
function purgeOldEntries(entries: EventLogEntry[]): EventLogEntry[] {
|
|
44
|
+
const { eventLogPurgeMs } = getConfig();
|
|
45
|
+
const cutoff = new Date(Date.now() - eventLogPurgeMs);
|
|
46
|
+
|
|
47
|
+
return entries.filter((entry) => new Date(entry.ts) >= cutoff);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Append an event to the log and purge old entries
|
|
52
|
+
*/
|
|
53
|
+
export function logEvent(entry: Omit<EventLogEntry, 'ts'>): void {
|
|
54
|
+
const { eventLogPath } = getConfig();
|
|
55
|
+
ensureDir(path.dirname(eventLogPath));
|
|
56
|
+
|
|
57
|
+
// Read existing entries
|
|
58
|
+
const entries = parseJsonl(eventLogPath);
|
|
59
|
+
|
|
60
|
+
// Add new entry with timestamp
|
|
61
|
+
entries.push({ ts: nowIso(), ...entry });
|
|
62
|
+
|
|
63
|
+
// Purge old entries
|
|
64
|
+
const purgedEntries = purgeOldEntries(entries);
|
|
65
|
+
|
|
66
|
+
// Write back
|
|
67
|
+
writeJsonl(eventLogPath, purgedEntries);
|
|
68
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event queue service tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import type { QueueEntry } from '../config/types.js';
|
|
11
|
+
import { nowIso } from '../util/formatters.js';
|
|
12
|
+
|
|
13
|
+
describe('eventQueue', () => {
|
|
14
|
+
it('should format queue entries correctly', () => {
|
|
15
|
+
const entry: QueueEntry = {
|
|
16
|
+
ts: nowIso(),
|
|
17
|
+
event: 'test-event',
|
|
18
|
+
cmd: 'node test.js',
|
|
19
|
+
body: { foo: 'bar', baz: 123 },
|
|
20
|
+
timeoutMs: 30000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const json = JSON.stringify(entry);
|
|
24
|
+
const parsed = JSON.parse(json) as QueueEntry;
|
|
25
|
+
|
|
26
|
+
expect(parsed.event).toBe('test-event');
|
|
27
|
+
expect(parsed.cmd).toBe('node test.js');
|
|
28
|
+
expect(parsed.body).toEqual({ foo: 'bar', baz: 123 });
|
|
29
|
+
expect(parsed.timeoutMs).toBe(30000);
|
|
30
|
+
expect(parsed.ts).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should write entries as JSONL', () => {
|
|
34
|
+
const entry1: QueueEntry = {
|
|
35
|
+
ts: nowIso(),
|
|
36
|
+
event: 'event1',
|
|
37
|
+
cmd: 'cmd1',
|
|
38
|
+
body: { a: 1 },
|
|
39
|
+
timeoutMs: 10000,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const entry2: QueueEntry = {
|
|
43
|
+
ts: nowIso(),
|
|
44
|
+
event: 'event2',
|
|
45
|
+
cmd: 'cmd2',
|
|
46
|
+
body: { b: 2 },
|
|
47
|
+
timeoutMs: 20000,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const tempFile = path.join(process.cwd(), 'test-queue.jsonl');
|
|
51
|
+
const lines = [entry1, entry2].map((e) => JSON.stringify(e)).join('\n');
|
|
52
|
+
fs.writeFileSync(tempFile, lines + '\n', 'utf8');
|
|
53
|
+
|
|
54
|
+
const content = fs.readFileSync(tempFile, 'utf8');
|
|
55
|
+
const parsed = content
|
|
56
|
+
.split('\n')
|
|
57
|
+
.filter((line) => line.trim())
|
|
58
|
+
.map((line) => JSON.parse(line) as QueueEntry);
|
|
59
|
+
|
|
60
|
+
expect(parsed).toHaveLength(2);
|
|
61
|
+
expect(parsed[0].event).toBe('event1');
|
|
62
|
+
expect(parsed[1].event).toBe('event2');
|
|
63
|
+
|
|
64
|
+
// Cleanup
|
|
65
|
+
fs.unlinkSync(tempFile);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle cursor-based reading', () => {
|
|
69
|
+
const entry1 = JSON.stringify({
|
|
70
|
+
ts: nowIso(),
|
|
71
|
+
event: 'e1',
|
|
72
|
+
cmd: 'c1',
|
|
73
|
+
body: {},
|
|
74
|
+
timeoutMs: 1000,
|
|
75
|
+
});
|
|
76
|
+
const entry2 = JSON.stringify({
|
|
77
|
+
ts: nowIso(),
|
|
78
|
+
event: 'e2',
|
|
79
|
+
cmd: 'c2',
|
|
80
|
+
body: {},
|
|
81
|
+
timeoutMs: 2000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const tempFile = path.join(process.cwd(), 'test-cursor-queue.jsonl');
|
|
85
|
+
fs.writeFileSync(tempFile, entry1 + '\n' + entry2 + '\n', 'utf8');
|
|
86
|
+
|
|
87
|
+
const fullContent = fs.readFileSync(tempFile, 'utf8');
|
|
88
|
+
const cursorPosition = Buffer.byteLength(entry1 + '\n', 'utf8');
|
|
89
|
+
|
|
90
|
+
// Read from cursor
|
|
91
|
+
const remaining = fullContent.slice(cursorPosition);
|
|
92
|
+
const lines = remaining
|
|
93
|
+
.split('\n')
|
|
94
|
+
.filter((line) => line.trim())
|
|
95
|
+
.map((line) => JSON.parse(line) as QueueEntry);
|
|
96
|
+
|
|
97
|
+
expect(lines).toHaveLength(1);
|
|
98
|
+
expect(lines[0].event).toBe('e2');
|
|
99
|
+
|
|
100
|
+
// Cleanup
|
|
101
|
+
fs.unlinkSync(tempFile);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should preserve body structure through JSON serialization', () => {
|
|
105
|
+
const complexBody = {
|
|
106
|
+
string: 'test',
|
|
107
|
+
number: 123,
|
|
108
|
+
boolean: true,
|
|
109
|
+
null: null,
|
|
110
|
+
array: [1, 2, 3],
|
|
111
|
+
nested: { a: { b: { c: 'deep' } } },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const entry: QueueEntry = {
|
|
115
|
+
ts: nowIso(),
|
|
116
|
+
event: 'complex',
|
|
117
|
+
cmd: 'node test.js',
|
|
118
|
+
body: complexBody,
|
|
119
|
+
timeoutMs: 5000,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const json = JSON.stringify(entry);
|
|
123
|
+
const parsed = JSON.parse(json) as QueueEntry;
|
|
124
|
+
|
|
125
|
+
expect(parsed.body).toEqual(complexBody);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable event queue for webhook event processing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
import { getConfig } from '../config/index.js';
|
|
10
|
+
import type { QueueEntry } from '../config/types.js';
|
|
11
|
+
import { nowIso } from '../util/formatters.js';
|
|
12
|
+
import { logEvent } from './eventLog.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ensure directory exists
|
|
16
|
+
*/
|
|
17
|
+
function ensureDir(dirPath: string): void {
|
|
18
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Append a JSON object as a line to the general event log (backward compatibility)
|
|
23
|
+
* This is separate from the Event Gateway event log
|
|
24
|
+
*/
|
|
25
|
+
export function appendEvent(event: Record<string, unknown>): void {
|
|
26
|
+
const { eventsLog } = getConfig();
|
|
27
|
+
ensureDir(path.dirname(eventsLog));
|
|
28
|
+
const line = JSON.stringify({ at: nowIso(), ...event }) + '\n';
|
|
29
|
+
fs.appendFileSync(eventsLog, line, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Append a queue entry to the durable queue
|
|
34
|
+
*/
|
|
35
|
+
export function enqueue(
|
|
36
|
+
event: string,
|
|
37
|
+
cmd: string,
|
|
38
|
+
body: Record<string, unknown>,
|
|
39
|
+
timeoutMs: number,
|
|
40
|
+
): void {
|
|
41
|
+
const { eventQueuePath } = getConfig();
|
|
42
|
+
ensureDir(path.dirname(eventQueuePath));
|
|
43
|
+
|
|
44
|
+
const entry: QueueEntry = {
|
|
45
|
+
ts: nowIso(),
|
|
46
|
+
event,
|
|
47
|
+
cmd,
|
|
48
|
+
body,
|
|
49
|
+
timeoutMs,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const line = JSON.stringify(entry) + '\n';
|
|
53
|
+
fs.appendFileSync(eventQueuePath, line, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read cursor position (byte offset of last processed entry)
|
|
58
|
+
*/
|
|
59
|
+
function readCursor(): number {
|
|
60
|
+
const { eventQueueCursorPath } = getConfig();
|
|
61
|
+
if (!fs.existsSync(eventQueueCursorPath)) return 0;
|
|
62
|
+
|
|
63
|
+
const content = fs.readFileSync(eventQueueCursorPath, 'utf8').trim();
|
|
64
|
+
return parseInt(content, 10) || 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Write cursor position
|
|
69
|
+
*/
|
|
70
|
+
function writeCursor(position: number): void {
|
|
71
|
+
const { eventQueueCursorPath } = getConfig();
|
|
72
|
+
ensureDir(path.dirname(eventQueueCursorPath));
|
|
73
|
+
fs.writeFileSync(eventQueueCursorPath, position.toString(), 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse JSONL entries from file starting at cursor position
|
|
78
|
+
*/
|
|
79
|
+
function readEntriesFromCursor(): {
|
|
80
|
+
entries: QueueEntry[];
|
|
81
|
+
newPosition: number;
|
|
82
|
+
} {
|
|
83
|
+
const { eventQueuePath } = getConfig();
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(eventQueuePath)) {
|
|
86
|
+
return { entries: [], newPosition: 0 };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cursor = readCursor();
|
|
90
|
+
const buf = fs.readFileSync(eventQueuePath);
|
|
91
|
+
|
|
92
|
+
// Read from cursor position (byte-based to match writeCursor)
|
|
93
|
+
const remaining = buf.subarray(cursor).toString('utf8');
|
|
94
|
+
const lines = remaining
|
|
95
|
+
.split('\n')
|
|
96
|
+
.filter((line) => line.trim())
|
|
97
|
+
.map((line) => JSON.parse(line) as QueueEntry);
|
|
98
|
+
|
|
99
|
+
const newPosition = buf.length;
|
|
100
|
+
|
|
101
|
+
return { entries: lines, newPosition };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute a queue entry (spawn command with body piped to stdin)
|
|
106
|
+
*/
|
|
107
|
+
async function executeEntry(entry: QueueEntry): Promise<{
|
|
108
|
+
exitCode: number;
|
|
109
|
+
durationMs: number;
|
|
110
|
+
}> {
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
// Parse command and args
|
|
115
|
+
const parts = entry.cmd.split(/\s+/);
|
|
116
|
+
const command = parts[0];
|
|
117
|
+
const args = parts.slice(1);
|
|
118
|
+
|
|
119
|
+
const proc = spawn(command, args, {
|
|
120
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
121
|
+
shell: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Pipe body as JSON to stdin
|
|
125
|
+
const bodyJson = JSON.stringify(entry.body);
|
|
126
|
+
proc.stdin.write(bodyJson);
|
|
127
|
+
proc.stdin.end();
|
|
128
|
+
|
|
129
|
+
// Setup timeout
|
|
130
|
+
const timeout = setTimeout(() => {
|
|
131
|
+
proc.kill('SIGTERM');
|
|
132
|
+
}, entry.timeoutMs);
|
|
133
|
+
|
|
134
|
+
proc.on('exit', (code) => {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
const exitCode = code ?? -1;
|
|
137
|
+
const durationMs = Date.now() - startTime;
|
|
138
|
+
resolve({ exitCode, durationMs });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
proc.on('error', () => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
const durationMs = Date.now() - startTime;
|
|
144
|
+
resolve({ exitCode: -1, durationMs });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Process one batch of queue entries
|
|
151
|
+
*/
|
|
152
|
+
async function processBatch(): Promise<void> {
|
|
153
|
+
const { entries, newPosition } = readEntriesFromCursor();
|
|
154
|
+
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
try {
|
|
157
|
+
const { exitCode, durationMs } = await executeEntry(entry);
|
|
158
|
+
|
|
159
|
+
// Log to event log
|
|
160
|
+
logEvent({
|
|
161
|
+
event: entry.event,
|
|
162
|
+
matched: true,
|
|
163
|
+
exitCode,
|
|
164
|
+
durationMs,
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
// Log error but continue (errors are ignored per spec)
|
|
168
|
+
logEvent({
|
|
169
|
+
event: entry.event,
|
|
170
|
+
matched: true,
|
|
171
|
+
exitCode: -1,
|
|
172
|
+
durationMs: 0,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update cursor
|
|
178
|
+
if (entries.length > 0) {
|
|
179
|
+
writeCursor(newPosition);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Start the queue drain loop
|
|
185
|
+
*/
|
|
186
|
+
export function startQueueProcessor(): void {
|
|
187
|
+
setInterval(
|
|
188
|
+
() => {
|
|
189
|
+
void processBatch();
|
|
190
|
+
},
|
|
191
|
+
5000, // Check every 5 seconds
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Process immediately on start
|
|
195
|
+
void processBatch();
|
|
196
|
+
}
|