vector-framework 1.2.1 → 1.2.3

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 (190) hide show
  1. package/README.md +18 -6
  2. package/dist/auth/protected.d.ts +4 -4
  3. package/dist/auth/protected.d.ts.map +1 -1
  4. package/dist/auth/protected.js +10 -7
  5. package/dist/auth/protected.js.map +1 -1
  6. package/dist/cache/manager.d.ts +2 -0
  7. package/dist/cache/manager.d.ts.map +1 -1
  8. package/dist/cache/manager.js +21 -4
  9. package/dist/cache/manager.js.map +1 -1
  10. package/dist/checkpoint/artifacts/compressor.d.ts +5 -0
  11. package/dist/checkpoint/artifacts/compressor.d.ts.map +1 -0
  12. package/dist/checkpoint/artifacts/compressor.js +24 -0
  13. package/dist/checkpoint/artifacts/compressor.js.map +1 -0
  14. package/dist/checkpoint/artifacts/decompress-worker.d.ts +2 -0
  15. package/dist/checkpoint/artifacts/decompress-worker.d.ts.map +1 -0
  16. package/dist/checkpoint/artifacts/decompress-worker.js +31 -0
  17. package/dist/checkpoint/artifacts/decompress-worker.js.map +1 -0
  18. package/dist/checkpoint/artifacts/hasher.d.ts +2 -0
  19. package/dist/checkpoint/artifacts/hasher.d.ts.map +1 -0
  20. package/dist/checkpoint/artifacts/hasher.js +7 -0
  21. package/dist/checkpoint/artifacts/hasher.js.map +1 -0
  22. package/dist/checkpoint/artifacts/manifest.d.ts +6 -0
  23. package/dist/checkpoint/artifacts/manifest.d.ts.map +1 -0
  24. package/dist/checkpoint/artifacts/manifest.js +55 -0
  25. package/dist/checkpoint/artifacts/manifest.js.map +1 -0
  26. package/dist/checkpoint/artifacts/materializer.d.ts +16 -0
  27. package/dist/checkpoint/artifacts/materializer.d.ts.map +1 -0
  28. package/dist/checkpoint/artifacts/materializer.js +168 -0
  29. package/dist/checkpoint/artifacts/materializer.js.map +1 -0
  30. package/dist/checkpoint/artifacts/packager.d.ts +12 -0
  31. package/dist/checkpoint/artifacts/packager.d.ts.map +1 -0
  32. package/dist/checkpoint/artifacts/packager.js +82 -0
  33. package/dist/checkpoint/artifacts/packager.js.map +1 -0
  34. package/dist/checkpoint/artifacts/repository.d.ts +11 -0
  35. package/dist/checkpoint/artifacts/repository.d.ts.map +1 -0
  36. package/dist/checkpoint/artifacts/repository.js +29 -0
  37. package/dist/checkpoint/artifacts/repository.js.map +1 -0
  38. package/dist/checkpoint/artifacts/store.d.ts +13 -0
  39. package/dist/checkpoint/artifacts/store.d.ts.map +1 -0
  40. package/dist/checkpoint/artifacts/store.js +85 -0
  41. package/dist/checkpoint/artifacts/store.js.map +1 -0
  42. package/dist/checkpoint/artifacts/types.d.ts +21 -0
  43. package/dist/checkpoint/artifacts/types.d.ts.map +1 -0
  44. package/dist/checkpoint/artifacts/types.js +2 -0
  45. package/dist/checkpoint/artifacts/types.js.map +1 -0
  46. package/dist/checkpoint/artifacts/worker-decompressor.d.ts +17 -0
  47. package/dist/checkpoint/artifacts/worker-decompressor.d.ts.map +1 -0
  48. package/dist/checkpoint/artifacts/worker-decompressor.js +148 -0
  49. package/dist/checkpoint/artifacts/worker-decompressor.js.map +1 -0
  50. package/dist/checkpoint/asset-store.d.ts +10 -0
  51. package/dist/checkpoint/asset-store.d.ts.map +1 -0
  52. package/dist/checkpoint/asset-store.js +46 -0
  53. package/dist/checkpoint/asset-store.js.map +1 -0
  54. package/dist/checkpoint/bundler.d.ts +15 -0
  55. package/dist/checkpoint/bundler.d.ts.map +1 -0
  56. package/dist/checkpoint/bundler.js +45 -0
  57. package/dist/checkpoint/bundler.js.map +1 -0
  58. package/dist/checkpoint/cli.d.ts +2 -0
  59. package/dist/checkpoint/cli.d.ts.map +1 -0
  60. package/dist/checkpoint/cli.js +157 -0
  61. package/dist/checkpoint/cli.js.map +1 -0
  62. package/dist/checkpoint/entrypoint-generator.d.ts +17 -0
  63. package/dist/checkpoint/entrypoint-generator.d.ts.map +1 -0
  64. package/dist/checkpoint/entrypoint-generator.js +251 -0
  65. package/dist/checkpoint/entrypoint-generator.js.map +1 -0
  66. package/dist/checkpoint/forwarder.d.ts +6 -0
  67. package/dist/checkpoint/forwarder.d.ts.map +1 -0
  68. package/dist/checkpoint/forwarder.js +74 -0
  69. package/dist/checkpoint/forwarder.js.map +1 -0
  70. package/dist/checkpoint/gateway.d.ts +11 -0
  71. package/dist/checkpoint/gateway.d.ts.map +1 -0
  72. package/dist/checkpoint/gateway.js +30 -0
  73. package/dist/checkpoint/gateway.js.map +1 -0
  74. package/dist/checkpoint/ipc.d.ts +12 -0
  75. package/dist/checkpoint/ipc.d.ts.map +1 -0
  76. package/dist/checkpoint/ipc.js +96 -0
  77. package/dist/checkpoint/ipc.js.map +1 -0
  78. package/dist/checkpoint/manager.d.ts +20 -0
  79. package/dist/checkpoint/manager.d.ts.map +1 -0
  80. package/dist/checkpoint/manager.js +214 -0
  81. package/dist/checkpoint/manager.js.map +1 -0
  82. package/dist/checkpoint/process-manager.d.ts +35 -0
  83. package/dist/checkpoint/process-manager.d.ts.map +1 -0
  84. package/dist/checkpoint/process-manager.js +203 -0
  85. package/dist/checkpoint/process-manager.js.map +1 -0
  86. package/dist/checkpoint/resolver.d.ts +25 -0
  87. package/dist/checkpoint/resolver.d.ts.map +1 -0
  88. package/dist/checkpoint/resolver.js +95 -0
  89. package/dist/checkpoint/resolver.js.map +1 -0
  90. package/dist/checkpoint/socket-path.d.ts +2 -0
  91. package/dist/checkpoint/socket-path.d.ts.map +1 -0
  92. package/dist/checkpoint/socket-path.js +51 -0
  93. package/dist/checkpoint/socket-path.js.map +1 -0
  94. package/dist/checkpoint/types.d.ts +54 -0
  95. package/dist/checkpoint/types.d.ts.map +1 -0
  96. package/dist/checkpoint/types.js +2 -0
  97. package/dist/checkpoint/types.js.map +1 -0
  98. package/dist/cli/index.js +10 -2
  99. package/dist/cli/index.js.map +1 -1
  100. package/dist/cli/option-resolution.d.ts +1 -1
  101. package/dist/cli/option-resolution.d.ts.map +1 -1
  102. package/dist/cli/option-resolution.js.map +1 -1
  103. package/dist/cli.js +3817 -350
  104. package/dist/core/config-loader.d.ts +1 -0
  105. package/dist/core/config-loader.d.ts.map +1 -1
  106. package/dist/core/config-loader.js +10 -2
  107. package/dist/core/config-loader.js.map +1 -1
  108. package/dist/core/router.d.ts +24 -3
  109. package/dist/core/router.d.ts.map +1 -1
  110. package/dist/core/router.js +398 -249
  111. package/dist/core/router.js.map +1 -1
  112. package/dist/core/server.d.ts +3 -0
  113. package/dist/core/server.d.ts.map +1 -1
  114. package/dist/core/server.js +35 -10
  115. package/dist/core/server.js.map +1 -1
  116. package/dist/core/vector.d.ts +3 -0
  117. package/dist/core/vector.d.ts.map +1 -1
  118. package/dist/core/vector.js +51 -1
  119. package/dist/core/vector.js.map +1 -1
  120. package/dist/dev/route-scanner.d.ts.map +1 -1
  121. package/dist/dev/route-scanner.js +2 -1
  122. package/dist/dev/route-scanner.js.map +1 -1
  123. package/dist/errors/index.cjs +2 -0
  124. package/dist/http.d.ts +32 -7
  125. package/dist/http.d.ts.map +1 -1
  126. package/dist/http.js +144 -13
  127. package/dist/http.js.map +1 -1
  128. package/dist/index.cjs +2657 -0
  129. package/dist/index.d.ts +3 -2
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +12 -1433
  132. package/dist/index.js.map +1 -1
  133. package/dist/index.mjs +1301 -77
  134. package/dist/middleware/manager.d.ts +3 -3
  135. package/dist/middleware/manager.d.ts.map +1 -1
  136. package/dist/middleware/manager.js +9 -8
  137. package/dist/middleware/manager.js.map +1 -1
  138. package/dist/openapi/docs-ui.d.ts.map +1 -1
  139. package/dist/openapi/docs-ui.js +1097 -61
  140. package/dist/openapi/docs-ui.js.map +1 -1
  141. package/dist/openapi/generator.d.ts +2 -1
  142. package/dist/openapi/generator.d.ts.map +1 -1
  143. package/dist/openapi/generator.js +332 -16
  144. package/dist/openapi/generator.js.map +1 -1
  145. package/dist/types/index.d.ts +71 -28
  146. package/dist/types/index.d.ts.map +1 -1
  147. package/dist/types/index.js +24 -1
  148. package/dist/types/index.js.map +1 -1
  149. package/dist/utils/validation.d.ts.map +1 -1
  150. package/dist/utils/validation.js +3 -2
  151. package/dist/utils/validation.js.map +1 -1
  152. package/package.json +9 -14
  153. package/src/auth/protected.ts +11 -8
  154. package/src/cache/manager.ts +23 -4
  155. package/src/checkpoint/artifacts/compressor.ts +30 -0
  156. package/src/checkpoint/artifacts/decompress-worker.ts +49 -0
  157. package/src/checkpoint/artifacts/hasher.ts +6 -0
  158. package/src/checkpoint/artifacts/manifest.ts +72 -0
  159. package/src/checkpoint/artifacts/materializer.ts +211 -0
  160. package/src/checkpoint/artifacts/packager.ts +100 -0
  161. package/src/checkpoint/artifacts/repository.ts +36 -0
  162. package/src/checkpoint/artifacts/store.ts +102 -0
  163. package/src/checkpoint/artifacts/types.ts +24 -0
  164. package/src/checkpoint/artifacts/worker-decompressor.ts +192 -0
  165. package/src/checkpoint/asset-store.ts +61 -0
  166. package/src/checkpoint/bundler.ts +64 -0
  167. package/src/checkpoint/cli.ts +177 -0
  168. package/src/checkpoint/entrypoint-generator.ts +275 -0
  169. package/src/checkpoint/forwarder.ts +84 -0
  170. package/src/checkpoint/gateway.ts +40 -0
  171. package/src/checkpoint/ipc.ts +107 -0
  172. package/src/checkpoint/manager.ts +254 -0
  173. package/src/checkpoint/process-manager.ts +250 -0
  174. package/src/checkpoint/resolver.ts +124 -0
  175. package/src/checkpoint/socket-path.ts +61 -0
  176. package/src/checkpoint/types.ts +63 -0
  177. package/src/cli/index.ts +11 -2
  178. package/src/cli/option-resolution.ts +5 -1
  179. package/src/core/config-loader.ts +11 -2
  180. package/src/core/router.ts +505 -264
  181. package/src/core/server.ts +51 -11
  182. package/src/core/vector.ts +60 -1
  183. package/src/dev/route-scanner.ts +2 -1
  184. package/src/http.ts +219 -19
  185. package/src/index.ts +3 -2
  186. package/src/middleware/manager.ts +10 -10
  187. package/src/openapi/docs-ui.ts +1097 -61
  188. package/src/openapi/generator.ts +380 -13
  189. package/src/types/index.ts +83 -30
  190. package/src/utils/validation.ts +5 -3
@@ -0,0 +1,107 @@
1
+ export type CheckpointIpcMessage =
2
+ | { type: 'ready' }
3
+ | { type: 'error'; message: string }
4
+ | { type: 'health'; status: 'ok' | 'degraded' };
5
+
6
+ const MAX_PENDING_STDOUT_CHARS = 1_048_576; // 1 MiB guard against unbounded startup log lines.
7
+
8
+ export function parseIpcLine(line: string): CheckpointIpcMessage | null {
9
+ const trimmed = line.trim();
10
+ if (!trimmed) return null;
11
+ if (trimmed === 'READY') return { type: 'ready' };
12
+
13
+ try {
14
+ return JSON.parse(trimmed) as CheckpointIpcMessage;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ export async function waitForReady(stdout: ReadableStream<Uint8Array>, timeoutMs: number = 10_000): Promise<void> {
21
+ return new Promise<void>((resolve, reject) => {
22
+ let settled = false;
23
+ const reader = stdout.getReader();
24
+ const decoder = new TextDecoder();
25
+ let buffer = '';
26
+
27
+ function settle(fn: () => void) {
28
+ if (settled) return;
29
+ settled = true;
30
+ clearTimeout(timer);
31
+ try {
32
+ reader.releaseLock();
33
+ } catch {
34
+ // Ignore release failures when the reader is already detached.
35
+ }
36
+ fn();
37
+ }
38
+
39
+ const timer = setTimeout(() => {
40
+ settle(() => reject(new Error(`Checkpoint process did not become ready within ${timeoutMs}ms`)));
41
+ }, timeoutMs);
42
+
43
+ const processLine = (line: string): 'ready' | 'error' | 'continue' => {
44
+ const msg = parseIpcLine(line);
45
+ if (msg?.type === 'ready') {
46
+ settle(() => resolve());
47
+ return 'ready';
48
+ }
49
+ if (msg?.type === 'error') {
50
+ settle(() => reject(new Error(`Checkpoint process error: ${msg.message}`)));
51
+ return 'error';
52
+ }
53
+ return 'continue';
54
+ };
55
+
56
+ const processBufferLines = (): 'ready' | 'error' | 'continue' => {
57
+ let lineEnd = buffer.indexOf('\n');
58
+ while (lineEnd !== -1) {
59
+ const line = buffer.slice(0, lineEnd);
60
+ buffer = buffer.slice(lineEnd + 1);
61
+ const result = processLine(line);
62
+ if (result !== 'continue') {
63
+ return result;
64
+ }
65
+ lineEnd = buffer.indexOf('\n');
66
+ }
67
+ return 'continue';
68
+ };
69
+
70
+ // Iterative read loop (not recursive) to avoid stack overflow on large stdout
71
+ (async () => {
72
+ try {
73
+ while (true) {
74
+ const { done, value } = await reader.read();
75
+
76
+ if (done) {
77
+ buffer += decoder.decode();
78
+ const lastLine = buffer.trim();
79
+ if (lastLine.length > 0) {
80
+ const result = processLine(lastLine);
81
+ if (result !== 'continue') {
82
+ return;
83
+ }
84
+ }
85
+ settle(() => reject(new Error('Checkpoint process stdout closed before READY signal')));
86
+ return;
87
+ }
88
+
89
+ buffer += decoder.decode(value, { stream: true });
90
+ if (buffer.length > MAX_PENDING_STDOUT_CHARS) {
91
+ settle(() =>
92
+ reject(new Error(`Checkpoint process stdout exceeded ${MAX_PENDING_STDOUT_CHARS} chars before READY`))
93
+ );
94
+ return;
95
+ }
96
+
97
+ const result = processBufferLines();
98
+ if (result !== 'continue') {
99
+ return;
100
+ }
101
+ }
102
+ } catch (err) {
103
+ settle(() => reject(err));
104
+ }
105
+ })();
106
+ });
107
+ }
@@ -0,0 +1,254 @@
1
+ import { existsSync, promises as fs } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import type { ActivePointer, CheckpointConfig, CheckpointManifest, CheckpointPublishOptions } from './types';
4
+ import { CHECKPOINT_FORMAT_VERSION } from './types';
5
+ import { AssetStore } from './asset-store';
6
+ import { CheckpointBundler } from './bundler';
7
+ import { CheckpointEntrypointGenerator } from './entrypoint-generator';
8
+ import { CheckpointPackager } from './artifacts/packager';
9
+ import { resolveCheckpointSocketPath } from './socket-path';
10
+
11
+ const DEFAULT_STORAGE_DIR = '.vector/checkpoints';
12
+ const ACTIVE_POINTER_FILE = 'active.json';
13
+
14
+ function inferLegacyAssetCodec(asset: {
15
+ codec?: 'none' | 'gzip';
16
+ blobPath?: string;
17
+ storedPath?: string;
18
+ }): 'none' | 'gzip' {
19
+ if (asset.codec) {
20
+ return asset.codec;
21
+ }
22
+
23
+ const rawPath = (asset.blobPath ?? asset.storedPath ?? '').trim().toLowerCase();
24
+ return rawPath.endsWith('.gz') ? 'gzip' : 'none';
25
+ }
26
+
27
+ export class CheckpointManager {
28
+ private storageDir: string;
29
+ private maxCheckpoints: number;
30
+
31
+ constructor(config: CheckpointConfig = {}) {
32
+ this.storageDir = resolve(process.cwd(), config.storageDir ?? DEFAULT_STORAGE_DIR);
33
+ this.maxCheckpoints = config.maxCheckpoints ?? 10;
34
+ }
35
+
36
+ getStorageDir(): string {
37
+ return this.storageDir;
38
+ }
39
+
40
+ versionDir(version: string): string {
41
+ return join(this.storageDir, version);
42
+ }
43
+
44
+ socketPath(version: string): string {
45
+ return resolveCheckpointSocketPath(this.storageDir, version);
46
+ }
47
+
48
+ async ensureStorageDir(): Promise<void> {
49
+ await fs.mkdir(this.storageDir, { recursive: true });
50
+ }
51
+
52
+ async publish(options: CheckpointPublishOptions): Promise<CheckpointManifest> {
53
+ await this.ensureStorageDir();
54
+
55
+ const versionDir = this.versionDir(options.version);
56
+ await fs.mkdir(versionDir, { recursive: true });
57
+
58
+ // Generate entrypoint
59
+ const generator = new CheckpointEntrypointGenerator();
60
+ const entrypointPath = await generator.generate({
61
+ version: options.version,
62
+ outputDir: versionDir,
63
+ routesDir: resolve(process.cwd(), options.routesDir),
64
+ socketPath: this.socketPath(options.version),
65
+ });
66
+
67
+ // Bundle
68
+ const bundler = new CheckpointBundler();
69
+ const bundleResult = await bundler.bundle({
70
+ entrypointPath,
71
+ outputDir: versionDir,
72
+ });
73
+
74
+ // Collect assets
75
+ const assetStore = new AssetStore(this.storageDir);
76
+ const assets = await assetStore.collect(options.embeddedAssetPaths ?? [], options.sidecarAssetPaths ?? []);
77
+ assetStore.validateBudgets(assets);
78
+
79
+ // Build manifest
80
+ const manifest: CheckpointManifest = {
81
+ formatVersion: CHECKPOINT_FORMAT_VERSION,
82
+ version: options.version,
83
+ createdAt: new Date().toISOString(),
84
+ entrypoint: 'checkpoint.js',
85
+ routes: generator.getDiscoveredRoutes(),
86
+ assets,
87
+ bundleHash: bundleResult.hash,
88
+ bundleSize: bundleResult.size,
89
+ checkpointArchivePath: undefined,
90
+ checkpointArchiveHash: undefined,
91
+ checkpointArchiveSize: undefined,
92
+ checkpointArchiveCodec: undefined,
93
+ };
94
+
95
+ await this.writeManifest(options.version, manifest);
96
+
97
+ // Clean up entrypoint source (keep only the bundle)
98
+ try {
99
+ await fs.unlink(entrypointPath);
100
+ } catch {
101
+ // Ignore cleanup failures
102
+ }
103
+
104
+ // Package checkpoint folder with compression for local transport workflows.
105
+ const packager = new CheckpointPackager(this.storageDir);
106
+ const archive = await packager.packageVersion(options.version);
107
+ manifest.checkpointArchivePath = archive.archivePath;
108
+ manifest.checkpointArchiveHash = archive.archiveHash;
109
+ manifest.checkpointArchiveSize = archive.archiveSize;
110
+ manifest.checkpointArchiveCodec = archive.codec;
111
+ await this.writeManifest(options.version, manifest);
112
+
113
+ await this.pruneOld();
114
+
115
+ return manifest;
116
+ }
117
+
118
+ async readManifest(version: string): Promise<CheckpointManifest> {
119
+ const manifestPath = join(this.versionDir(version), 'manifest.json');
120
+ const content = await fs.readFile(manifestPath, 'utf-8');
121
+ return this.normalizeManifest(JSON.parse(content) as Partial<CheckpointManifest>);
122
+ }
123
+
124
+ async writeManifest(version: string, manifest: CheckpointManifest): Promise<void> {
125
+ const manifestPath = join(this.versionDir(version), 'manifest.json');
126
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
127
+ }
128
+
129
+ async listVersions(): Promise<CheckpointManifest[]> {
130
+ if (!existsSync(this.storageDir)) {
131
+ return [];
132
+ }
133
+
134
+ const entries = await fs.readdir(this.storageDir);
135
+ const manifests: CheckpointManifest[] = [];
136
+
137
+ for (const entry of entries) {
138
+ const manifestPath = join(this.storageDir, entry, 'manifest.json');
139
+ if (existsSync(manifestPath)) {
140
+ try {
141
+ const content = await fs.readFile(manifestPath, 'utf-8');
142
+ manifests.push(this.normalizeManifest(JSON.parse(content) as Partial<CheckpointManifest>));
143
+ } catch {
144
+ // Skip corrupted manifests
145
+ }
146
+ }
147
+ }
148
+
149
+ // Sort by createdAt descending (newest first)
150
+ manifests.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
151
+ return manifests;
152
+ }
153
+
154
+ async getActive(): Promise<ActivePointer | null> {
155
+ const pointerPath = join(this.storageDir, ACTIVE_POINTER_FILE);
156
+ if (!existsSync(pointerPath)) {
157
+ return null;
158
+ }
159
+
160
+ try {
161
+ const content = await fs.readFile(pointerPath, 'utf-8');
162
+ return JSON.parse(content) as ActivePointer;
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+
168
+ async setActive(version: string): Promise<void> {
169
+ // Verify version exists
170
+ const versionDir = this.versionDir(version);
171
+ if (!existsSync(versionDir)) {
172
+ throw new Error(`Checkpoint version ${version} does not exist`);
173
+ }
174
+
175
+ const manifestPath = join(versionDir, 'manifest.json');
176
+ if (!existsSync(manifestPath)) {
177
+ throw new Error(`Checkpoint version ${version} has no manifest`);
178
+ }
179
+
180
+ await this.ensureStorageDir();
181
+
182
+ const pointer: ActivePointer = {
183
+ version,
184
+ activatedAt: new Date().toISOString(),
185
+ };
186
+
187
+ const pointerPath = join(this.storageDir, ACTIVE_POINTER_FILE);
188
+ await fs.writeFile(pointerPath, JSON.stringify(pointer, null, 2), 'utf-8');
189
+ }
190
+
191
+ async remove(version: string): Promise<void> {
192
+ // Check if this is the active version
193
+ const active = await this.getActive();
194
+ if (active?.version === version) {
195
+ throw new Error(`Cannot remove active checkpoint version ${version}. Rollback to a different version first.`);
196
+ }
197
+
198
+ const versionDir = this.versionDir(version);
199
+ if (!existsSync(versionDir)) {
200
+ throw new Error(`Checkpoint version ${version} does not exist`);
201
+ }
202
+
203
+ await fs.rm(versionDir, { recursive: true, force: true });
204
+ }
205
+
206
+ async pruneOld(): Promise<void> {
207
+ if (this.maxCheckpoints <= 0) return;
208
+
209
+ const manifests = await this.listVersions();
210
+ if (manifests.length <= this.maxCheckpoints) return;
211
+
212
+ const active = await this.getActive();
213
+ const toRemove = manifests.slice(this.maxCheckpoints);
214
+
215
+ for (const manifest of toRemove) {
216
+ // Never remove the active version
217
+ if (active?.version === manifest.version) continue;
218
+
219
+ try {
220
+ const versionDir = this.versionDir(manifest.version);
221
+ await fs.rm(versionDir, { recursive: true, force: true });
222
+ } catch {
223
+ // Ignore removal failures during pruning
224
+ }
225
+ }
226
+ }
227
+
228
+ private normalizeManifest(manifest: Partial<CheckpointManifest>): CheckpointManifest {
229
+ const normalizedAssets = (manifest.assets ?? []).map((asset) => ({
230
+ ...asset,
231
+ contentHash: asset.contentHash ?? asset.hash,
232
+ contentSize: asset.contentSize ?? asset.size,
233
+ blobPath: asset.blobPath ?? asset.storedPath,
234
+ blobHash: asset.blobHash,
235
+ blobSize: asset.blobSize,
236
+ codec: inferLegacyAssetCodec(asset),
237
+ }));
238
+
239
+ return {
240
+ formatVersion: manifest.formatVersion ?? CHECKPOINT_FORMAT_VERSION,
241
+ version: manifest.version ?? 'unknown',
242
+ createdAt: manifest.createdAt ?? new Date(0).toISOString(),
243
+ entrypoint: manifest.entrypoint ?? 'checkpoint.js',
244
+ routes: manifest.routes ?? [],
245
+ assets: normalizedAssets,
246
+ bundleHash: manifest.bundleHash ?? '',
247
+ bundleSize: manifest.bundleSize ?? 0,
248
+ checkpointArchivePath: manifest.checkpointArchivePath,
249
+ checkpointArchiveHash: manifest.checkpointArchiveHash,
250
+ checkpointArchiveSize: manifest.checkpointArchiveSize,
251
+ checkpointArchiveCodec: manifest.checkpointArchiveCodec,
252
+ };
253
+ }
254
+ }
@@ -0,0 +1,250 @@
1
+ import { existsSync, promises as fs, unlinkSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import type { CheckpointManifest } from './types';
4
+ import { waitForReady } from './ipc';
5
+ import { CheckpointArtifactMaterializer } from './artifacts/materializer';
6
+ import { resolveCheckpointSocketPath } from './socket-path';
7
+
8
+ export interface SpawnedCheckpoint {
9
+ version: string;
10
+ socketPath: string;
11
+ process: ReturnType<typeof Bun.spawn>;
12
+ pid: number;
13
+ }
14
+
15
+ const DEFAULT_READY_TIMEOUT_MS = 10_000;
16
+ const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
17
+ const STOP_TIMEOUT_MS = 5_000;
18
+
19
+ export interface CheckpointProcessManagerOptions {
20
+ readyTimeoutMs?: number;
21
+ idleTimeoutMs?: number;
22
+ }
23
+
24
+ export class CheckpointProcessManager {
25
+ private running: Map<string, SpawnedCheckpoint> = new Map();
26
+ private pending: Map<string, Promise<SpawnedCheckpoint>> = new Map();
27
+ private readyTimeoutMs: number;
28
+ private idleTimeoutMs: number;
29
+ private idleTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
30
+ private lastUsedAt: Map<string, number> = new Map();
31
+ private materializer: CheckpointArtifactMaterializer;
32
+
33
+ constructor(options: number | CheckpointProcessManagerOptions = DEFAULT_READY_TIMEOUT_MS) {
34
+ if (typeof options === 'number') {
35
+ this.readyTimeoutMs = options;
36
+ this.idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS;
37
+ } else {
38
+ this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
39
+ this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
40
+ }
41
+
42
+ this.materializer = new CheckpointArtifactMaterializer();
43
+ }
44
+
45
+ async spawn(manifest: CheckpointManifest, storageDir: string): Promise<SpawnedCheckpoint> {
46
+ // Return already-running checkpoint
47
+ if (this.running.has(manifest.version)) {
48
+ return this.running.get(manifest.version)!;
49
+ }
50
+
51
+ // Return in-flight spawn to prevent duplicate processes for same version
52
+ if (this.pending.has(manifest.version)) {
53
+ return this.pending.get(manifest.version)!;
54
+ }
55
+
56
+ const promise = this.doSpawn(manifest, storageDir);
57
+ this.pending.set(manifest.version, promise);
58
+
59
+ try {
60
+ const result = await promise;
61
+ return result;
62
+ } finally {
63
+ this.pending.delete(manifest.version);
64
+ }
65
+ }
66
+
67
+ private async doSpawn(manifest: CheckpointManifest, storageDir: string): Promise<SpawnedCheckpoint> {
68
+ const versionDir = join(storageDir, manifest.version);
69
+ const bundlePath = join(versionDir, manifest.entrypoint);
70
+ const socketPath = resolveCheckpointSocketPath(storageDir, manifest.version);
71
+
72
+ if (!existsSync(bundlePath)) {
73
+ throw new Error(`Checkpoint bundle not found: ${bundlePath}`);
74
+ }
75
+
76
+ // Materialize declared assets into the checkpoint version directory before boot.
77
+ await this.materializer.materialize(manifest, storageDir);
78
+
79
+ // Ensure socket parent exists when fallback roots are used.
80
+ await fs.mkdir(dirname(socketPath), { recursive: true });
81
+
82
+ // Clean up stale socket file if it exists
83
+ this.tryUnlinkSocket(socketPath);
84
+
85
+ const proc = Bun.spawn(['bun', 'run', bundlePath], {
86
+ env: {
87
+ ...process.env,
88
+ VECTOR_CHECKPOINT_SOCKET: socketPath,
89
+ VECTOR_CHECKPOINT_VERSION: manifest.version,
90
+ },
91
+ stdout: 'pipe',
92
+ stderr: 'inherit',
93
+ });
94
+
95
+ // Guard against null stdout (shouldn't happen with stdout: 'pipe' but be safe)
96
+ if (!proc.stdout) {
97
+ proc.kill('SIGTERM');
98
+ throw new Error(`Checkpoint process for ${manifest.version} did not provide stdout`);
99
+ }
100
+
101
+ try {
102
+ await waitForReady(proc.stdout as ReadableStream<Uint8Array>, this.readyTimeoutMs);
103
+ } catch (err) {
104
+ proc.kill('SIGTERM');
105
+ throw err;
106
+ }
107
+
108
+ const spawned: SpawnedCheckpoint = {
109
+ version: manifest.version,
110
+ socketPath,
111
+ process: proc,
112
+ pid: proc.pid,
113
+ };
114
+
115
+ this.running.set(manifest.version, spawned);
116
+ this.lastUsedAt.set(manifest.version, Date.now());
117
+ this.scheduleIdleCheck(manifest.version);
118
+ return spawned;
119
+ }
120
+
121
+ markUsed(version: string): void {
122
+ if (!this.running.has(version)) {
123
+ return;
124
+ }
125
+ this.lastUsedAt.set(version, Date.now());
126
+ }
127
+
128
+ async stop(version: string): Promise<void> {
129
+ const snap = this.running.get(version);
130
+ if (!snap) return;
131
+
132
+ this.running.delete(version);
133
+ this.clearIdleTimer(version);
134
+ this.lastUsedAt.delete(version);
135
+
136
+ snap.process.kill('SIGTERM');
137
+
138
+ // Wait for exit with a timeout
139
+ const exited = await Promise.race([
140
+ snap.process.exited.then(() => true),
141
+ new Promise<false>((resolve) => setTimeout(() => resolve(false), STOP_TIMEOUT_MS)),
142
+ ]);
143
+
144
+ // Force kill if SIGTERM didn't work
145
+ if (!exited) {
146
+ try {
147
+ snap.process.kill('SIGKILL');
148
+ await snap.process.exited;
149
+ } catch {
150
+ // Process may have already exited between the check and SIGKILL
151
+ }
152
+ }
153
+
154
+ this.tryUnlinkSocket(snap.socketPath);
155
+ }
156
+
157
+ async stopAll(): Promise<void> {
158
+ const versions = [...this.running.keys()];
159
+ for (const version of versions) {
160
+ await this.stop(version);
161
+ }
162
+ }
163
+
164
+ isRunning(version: string): boolean {
165
+ return this.running.has(version);
166
+ }
167
+
168
+ getRunning(version: string): SpawnedCheckpoint | undefined {
169
+ return this.running.get(version);
170
+ }
171
+
172
+ async health(version: string): Promise<boolean> {
173
+ const snap = this.running.get(version);
174
+ if (!snap) return false;
175
+
176
+ try {
177
+ const response = await fetch('http://localhost/_vector/health', {
178
+ unix: snap.socketPath,
179
+ signal: AbortSignal.timeout(2000),
180
+ } as any);
181
+ return response.ok;
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ getRunningVersions(): string[] {
188
+ return [...this.running.keys()];
189
+ }
190
+
191
+ private scheduleIdleCheck(version: string, delayMs = this.idleTimeoutMs): void {
192
+ this.clearIdleTimer(version);
193
+ if (this.idleTimeoutMs <= 0) {
194
+ return;
195
+ }
196
+
197
+ const timer = setTimeout(
198
+ () => {
199
+ void this.handleIdleCheck(version);
200
+ },
201
+ Math.max(1, delayMs)
202
+ );
203
+
204
+ if (typeof (timer as any).unref === 'function') {
205
+ (timer as any).unref();
206
+ }
207
+
208
+ this.idleTimers.set(version, timer);
209
+ }
210
+
211
+ private async handleIdleCheck(version: string): Promise<void> {
212
+ if (!this.running.has(version)) {
213
+ return;
214
+ }
215
+
216
+ const lastUsedAt = this.lastUsedAt.get(version) ?? 0;
217
+ const idleForMs = Date.now() - lastUsedAt;
218
+ const remainingMs = this.idleTimeoutMs - idleForMs;
219
+
220
+ if (remainingMs > 0) {
221
+ this.scheduleIdleCheck(version, remainingMs);
222
+ return;
223
+ }
224
+
225
+ try {
226
+ await this.stop(version);
227
+ } catch (error) {
228
+ console.error(`[CheckpointProcessManager] Failed to stop idle checkpoint ${version}:`, error);
229
+ }
230
+ }
231
+
232
+ private clearIdleTimer(version: string): void {
233
+ const timer = this.idleTimers.get(version);
234
+ if (!timer) {
235
+ return;
236
+ }
237
+ clearTimeout(timer);
238
+ this.idleTimers.delete(version);
239
+ }
240
+
241
+ private tryUnlinkSocket(socketPath: string): void {
242
+ try {
243
+ if (existsSync(socketPath)) {
244
+ unlinkSync(socketPath);
245
+ }
246
+ } catch {
247
+ // Ignore cleanup failures
248
+ }
249
+ }
250
+ }