@scelar/nodepod 1.0.4 → 1.0.6
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/dist/{child_process-53fMkug_.js → child_process-4ZrgCVFu.js} +8234 -8233
- package/dist/{child_process-53fMkug_.js.map → child_process-4ZrgCVFu.js.map} +1 -1
- package/dist/{child_process-lxSKECHq.cjs → child_process-Cao4lyrb.cjs} +7435 -7434
- package/dist/{child_process-lxSKECHq.cjs.map → child_process-Cao4lyrb.cjs.map} +1 -1
- package/dist/{index-C-TQIrdG.cjs → index-DuYo2yDs.cjs} +38842 -38005
- package/dist/index-DuYo2yDs.cjs.map +1 -0
- package/dist/{index-B8lyh_ti.js → index-HkVqijtm.js} +36923 -36065
- package/dist/index-HkVqijtm.js.map +1 -0
- package/dist/index.cjs +67 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +61 -59
- package/dist/memory-handler.d.ts +57 -0
- package/dist/memory-volume.d.ts +12 -2
- package/dist/packages/installer.d.ts +3 -0
- package/dist/persistence/idb-cache.d.ts +7 -0
- package/dist/polyfills/readline.d.ts +108 -87
- package/dist/script-engine.d.ts +3 -0
- package/dist/sdk/nodepod-process.d.ts +2 -1
- package/dist/sdk/nodepod.d.ts +20 -1
- package/dist/sdk/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/memory-handler.ts +168 -0
- package/src/memory-volume.ts +72 -8
- package/src/packages/installer.ts +49 -1
- package/src/packages/version-resolver.ts +421 -421
- package/src/persistence/idb-cache.ts +107 -0
- package/src/polyfills/child_process.ts +3 -0
- package/src/polyfills/events.ts +22 -4
- package/src/polyfills/readline.ts +593 -71
- package/src/polyfills/stream.ts +46 -0
- package/src/polyfills/wasi.ts +1306 -1306
- package/src/polyfills/zlib.ts +881 -881
- package/src/script-engine.ts +3722 -3694
- package/src/sdk/nodepod-process.ts +94 -86
- package/src/sdk/nodepod.ts +52 -6
- package/src/sdk/types.ts +82 -77
- package/src/threading/process-manager.ts +11 -0
- package/src/threading/worker-protocol.ts +358 -358
- package/dist/index-B8lyh_ti.js.map +0 -1
- package/dist/index-C-TQIrdG.cjs.map +0 -1
package/dist/sdk/nodepod.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare class Nodepod {
|
|
|
19
19
|
private _sharedVFS;
|
|
20
20
|
private _syncChannel;
|
|
21
21
|
private _unwatchVFS;
|
|
22
|
+
private _handler;
|
|
22
23
|
private constructor();
|
|
23
24
|
static boot(opts?: NodepodOptions): Promise<Nodepod>;
|
|
24
25
|
spawn(cmd: string, args?: string[], opts?: SpawnOptions): Promise<NodepodProcess>;
|
|
@@ -27,10 +28,28 @@ export declare class Nodepod {
|
|
|
27
28
|
setPreviewScript(script: string): Promise<void>;
|
|
28
29
|
clearPreviewScript(): Promise<void>;
|
|
29
30
|
port(num: number): string | null;
|
|
30
|
-
|
|
31
|
+
/** Directory names excluded from snapshots at any depth when shallow=true. */
|
|
32
|
+
private static readonly SHALLOW_EXCLUDE_DIRS;
|
|
31
33
|
snapshot(opts?: SnapshotOptions): Snapshot;
|
|
32
34
|
restore(snapshot: Snapshot, opts?: SnapshotOptions): Promise<void>;
|
|
33
35
|
teardown(): void;
|
|
36
|
+
memoryStats(): {
|
|
37
|
+
vfs: {
|
|
38
|
+
fileCount: number;
|
|
39
|
+
totalBytes: number;
|
|
40
|
+
dirCount: number;
|
|
41
|
+
watcherCount: number;
|
|
42
|
+
};
|
|
43
|
+
engine: {
|
|
44
|
+
moduleCacheSize: number;
|
|
45
|
+
transformCacheSize: number;
|
|
46
|
+
};
|
|
47
|
+
heap: {
|
|
48
|
+
usedMB: number;
|
|
49
|
+
totalMB: number;
|
|
50
|
+
limitMB: number;
|
|
51
|
+
} | null;
|
|
52
|
+
};
|
|
34
53
|
get volume(): MemoryVolume;
|
|
35
54
|
get engine(): ScriptEngine;
|
|
36
55
|
get packages(): DependencyInstaller;
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { VolumeSnapshot } from "../engine-types";
|
|
2
|
+
import type { MemoryHandlerOptions } from "../memory-handler";
|
|
2
3
|
export interface NodepodOptions {
|
|
3
4
|
files?: Record<string, string | Uint8Array>;
|
|
4
5
|
env?: Record<string, string>;
|
|
@@ -7,6 +8,10 @@ export interface NodepodOptions {
|
|
|
7
8
|
onServerReady?: (port: number, url: string) => void;
|
|
8
9
|
/** Show a small "nodepod" watermark link in preview iframes. Defaults to true. */
|
|
9
10
|
watermark?: boolean;
|
|
11
|
+
/** Memory optimization settings. Omit to use defaults. */
|
|
12
|
+
memory?: MemoryHandlerOptions;
|
|
13
|
+
/** Cache installed node_modules in IndexedDB for faster re-boots. Default: true. */
|
|
14
|
+
enableSnapshotCache?: boolean;
|
|
10
15
|
}
|
|
11
16
|
export interface TerminalTheme {
|
|
12
17
|
background?: string;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -170,6 +170,8 @@ export type {
|
|
|
170
170
|
Snapshot,
|
|
171
171
|
SpawnOptions,
|
|
172
172
|
} from "./sdk/types";
|
|
173
|
+
export { MemoryHandler, LRUCache } from "./memory-handler";
|
|
174
|
+
export type { MemoryHandlerOptions } from "./memory-handler";
|
|
173
175
|
|
|
174
176
|
/* ---- Threading / Worker Infrastructure ---- */
|
|
175
177
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Centralized memory optimization handler for Nodepod.
|
|
2
|
+
// Provides LRU caches, heap monitoring, and pressure callbacks.
|
|
3
|
+
|
|
4
|
+
import type { FileStat } from './memory-volume';
|
|
5
|
+
|
|
6
|
+
/* ---- LRU Cache ---- */
|
|
7
|
+
|
|
8
|
+
export class LRUCache<K, V> {
|
|
9
|
+
private _map = new Map<K, V>();
|
|
10
|
+
private _capacity: number;
|
|
11
|
+
|
|
12
|
+
constructor(capacity: number) {
|
|
13
|
+
this._capacity = Math.max(1, capacity);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(key: K): V | undefined {
|
|
17
|
+
if (!this._map.has(key)) return undefined;
|
|
18
|
+
const value = this._map.get(key)!;
|
|
19
|
+
// Move to most-recently-used position
|
|
20
|
+
this._map.delete(key);
|
|
21
|
+
this._map.set(key, value);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
set(key: K, value: V): void {
|
|
26
|
+
if (this._map.has(key)) {
|
|
27
|
+
this._map.delete(key);
|
|
28
|
+
} else if (this._map.size >= this._capacity) {
|
|
29
|
+
// Evict least-recently-used (first entry in Map)
|
|
30
|
+
const oldest = this._map.keys().next().value;
|
|
31
|
+
if (oldest !== undefined) this._map.delete(oldest);
|
|
32
|
+
}
|
|
33
|
+
this._map.set(key, value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
has(key: K): boolean {
|
|
37
|
+
return this._map.has(key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
delete(key: K): boolean {
|
|
41
|
+
return this._map.delete(key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear(): void {
|
|
45
|
+
this._map.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get size(): number {
|
|
49
|
+
return this._map.size;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
keys(): IterableIterator<K> {
|
|
53
|
+
return this._map.keys();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
values(): IterableIterator<V> {
|
|
57
|
+
return this._map.values();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---- Options ---- */
|
|
62
|
+
|
|
63
|
+
export interface MemoryHandlerOptions {
|
|
64
|
+
/** LRU capacity for path normalization cache. Default: 2048 */
|
|
65
|
+
pathNormCacheSize?: number;
|
|
66
|
+
/** LRU capacity for stat result cache. Default: 512 */
|
|
67
|
+
statCacheSize?: number;
|
|
68
|
+
/** LRU capacity for module resolve cache. Default: 4096 */
|
|
69
|
+
resolveCacheSize?: number;
|
|
70
|
+
/** LRU capacity for package.json manifest cache. Default: 256 */
|
|
71
|
+
manifestCacheSize?: number;
|
|
72
|
+
/** LRU capacity for source transform cache. Default: 512 */
|
|
73
|
+
transformCacheSize?: number;
|
|
74
|
+
/** Max modules before trimming node_modules entries. Default: 512 */
|
|
75
|
+
moduleSoftCacheSize?: number;
|
|
76
|
+
/** Heap usage threshold in MB to trigger pressure callbacks. Default: 350 */
|
|
77
|
+
heapWarnThresholdMB?: number;
|
|
78
|
+
/** Monitoring poll interval in ms. Default: 30000 */
|
|
79
|
+
monitorIntervalMs?: number;
|
|
80
|
+
/** Max process stdout/stderr accumulation in bytes. Default: 4194304 (4MB) */
|
|
81
|
+
maxProcessOutputBytes?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DEFAULTS: Required<MemoryHandlerOptions> = {
|
|
85
|
+
pathNormCacheSize: 2048,
|
|
86
|
+
statCacheSize: 512,
|
|
87
|
+
resolveCacheSize: 4096,
|
|
88
|
+
manifestCacheSize: 256,
|
|
89
|
+
transformCacheSize: 512,
|
|
90
|
+
moduleSoftCacheSize: 512,
|
|
91
|
+
heapWarnThresholdMB: 350,
|
|
92
|
+
monitorIntervalMs: 30_000,
|
|
93
|
+
maxProcessOutputBytes: 4_194_304,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/* ---- MemoryHandler ---- */
|
|
97
|
+
|
|
98
|
+
export class MemoryHandler {
|
|
99
|
+
readonly options: Required<MemoryHandlerOptions>;
|
|
100
|
+
readonly pathNormCache: LRUCache<string, string>;
|
|
101
|
+
readonly statCache: LRUCache<string, FileStat>;
|
|
102
|
+
readonly transformCache: LRUCache<string, string>;
|
|
103
|
+
|
|
104
|
+
private _monitorTimer: ReturnType<typeof setInterval> | null = null;
|
|
105
|
+
private _pressureCallbacks: Array<() => void> = [];
|
|
106
|
+
private _destroyed = false;
|
|
107
|
+
|
|
108
|
+
constructor(opts?: MemoryHandlerOptions) {
|
|
109
|
+
this.options = { ...DEFAULTS, ...opts };
|
|
110
|
+
this.pathNormCache = new LRUCache(this.options.pathNormCacheSize);
|
|
111
|
+
this.statCache = new LRUCache(this.options.statCacheSize);
|
|
112
|
+
this.transformCache = new LRUCache(this.options.transformCacheSize);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Invalidate a cached stat entry (call on file write/delete). */
|
|
116
|
+
invalidateStat(normalizedPath: string): void {
|
|
117
|
+
this.statCache.delete(normalizedPath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Register a callback to be invoked when heap pressure is detected. Returns unsubscribe fn. */
|
|
121
|
+
onPressure(cb: () => void): () => void {
|
|
122
|
+
this._pressureCallbacks.push(cb);
|
|
123
|
+
return () => {
|
|
124
|
+
const idx = this._pressureCallbacks.indexOf(cb);
|
|
125
|
+
if (idx >= 0) this._pressureCallbacks.splice(idx, 1);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Start periodic heap monitoring. */
|
|
130
|
+
startMonitoring(): void {
|
|
131
|
+
if (this._monitorTimer || this._destroyed) return;
|
|
132
|
+
this._monitorTimer = setInterval(() => this._checkHeap(), this.options.monitorIntervalMs);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Stop monitoring. */
|
|
136
|
+
stopMonitoring(): void {
|
|
137
|
+
if (this._monitorTimer) {
|
|
138
|
+
clearInterval(this._monitorTimer);
|
|
139
|
+
this._monitorTimer = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Clear all owned caches. */
|
|
144
|
+
flush(): void {
|
|
145
|
+
this.pathNormCache.clear();
|
|
146
|
+
this.statCache.clear();
|
|
147
|
+
this.transformCache.clear();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Full cleanup — stop monitoring, flush caches. */
|
|
151
|
+
destroy(): void {
|
|
152
|
+
this._destroyed = true;
|
|
153
|
+
this.stopMonitoring();
|
|
154
|
+
this.flush();
|
|
155
|
+
this._pressureCallbacks.length = 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private _checkHeap(): void {
|
|
159
|
+
const perf = typeof performance !== 'undefined' ? (performance as any) : null;
|
|
160
|
+
if (!perf?.memory) return;
|
|
161
|
+
const usedMB = perf.memory.usedJSHeapSize / 1_048_576;
|
|
162
|
+
if (usedMB > this.options.heapWarnThresholdMB) {
|
|
163
|
+
for (const cb of this._pressureCallbacks) {
|
|
164
|
+
try { cb(); } catch { /* ignore */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/memory-volume.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { VolumeSnapshot, VolumeEntry } from './engine-types';
|
|
4
4
|
import { bytesToBase64, base64ToBytes } from './helpers/byte-encoding';
|
|
5
5
|
import { MOCK_IDS, MOCK_FS } from './constants/config';
|
|
6
|
+
import type { MemoryHandler } from './memory-handler';
|
|
6
7
|
|
|
7
8
|
export interface VolumeNode {
|
|
8
9
|
kind: 'file' | 'directory' | 'symlink';
|
|
@@ -172,8 +173,10 @@ export class MemoryVolume {
|
|
|
172
173
|
private textDecoder = new TextDecoder();
|
|
173
174
|
private activeWatchers = new Map<string, Set<ActiveWatcher>>();
|
|
174
175
|
private subscribers = new Map<string, Set<VolumeEventHandler>>();
|
|
176
|
+
private _handler: MemoryHandler | null;
|
|
175
177
|
|
|
176
|
-
constructor() {
|
|
178
|
+
constructor(handler?: MemoryHandler | null) {
|
|
179
|
+
this._handler = handler ?? null;
|
|
177
180
|
this.tree = {
|
|
178
181
|
kind: 'directory',
|
|
179
182
|
children: new Map(),
|
|
@@ -216,15 +219,55 @@ export class MemoryVolume {
|
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
// ---- Stats ----
|
|
223
|
+
|
|
224
|
+
getStats(): { fileCount: number; totalBytes: number; dirCount: number; watcherCount: number } {
|
|
225
|
+
let fileCount = 0;
|
|
226
|
+
let totalBytes = 0;
|
|
227
|
+
let dirCount = 0;
|
|
228
|
+
const walk = (node: VolumeNode) => {
|
|
229
|
+
if (node.kind === 'file') {
|
|
230
|
+
fileCount++;
|
|
231
|
+
totalBytes += node.content?.byteLength ?? 0;
|
|
232
|
+
} else if (node.kind === 'directory') {
|
|
233
|
+
dirCount++;
|
|
234
|
+
if (node.children) {
|
|
235
|
+
for (const child of node.children.values()) walk(child);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
walk(this.tree);
|
|
240
|
+
let watcherCount = 0;
|
|
241
|
+
for (const set of this.activeWatchers.values()) watcherCount += set.size;
|
|
242
|
+
return { fileCount, totalBytes, dirCount, watcherCount };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Clean up all watchers, subscribers, and global listeners. */
|
|
246
|
+
dispose(): void {
|
|
247
|
+
this.activeWatchers.clear();
|
|
248
|
+
this.subscribers.clear();
|
|
249
|
+
this.globalChangeListeners.clear();
|
|
250
|
+
if (this._handler) {
|
|
251
|
+
this._handler.statCache.clear();
|
|
252
|
+
this._handler.pathNormCache.clear();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
219
256
|
// ---- Snapshot serialization ----
|
|
220
257
|
|
|
221
|
-
toSnapshot(excludePrefixes?: string[]): VolumeSnapshot {
|
|
258
|
+
toSnapshot(excludePrefixes?: string[], excludeDirNames?: Set<string>): VolumeSnapshot {
|
|
222
259
|
const entries: VolumeEntry[] = [];
|
|
223
|
-
this.collectEntries('/', this.tree, entries, excludePrefixes);
|
|
260
|
+
this.collectEntries('/', this.tree, entries, excludePrefixes, excludeDirNames);
|
|
224
261
|
return { entries };
|
|
225
262
|
}
|
|
226
263
|
|
|
227
|
-
private collectEntries(
|
|
264
|
+
private collectEntries(
|
|
265
|
+
currentPath: string,
|
|
266
|
+
node: VolumeNode,
|
|
267
|
+
result: VolumeEntry[],
|
|
268
|
+
excludePrefixes?: string[],
|
|
269
|
+
excludeDirNames?: Set<string>,
|
|
270
|
+
): void {
|
|
228
271
|
if (excludePrefixes) {
|
|
229
272
|
for (const prefix of excludePrefixes) {
|
|
230
273
|
if (currentPath === prefix || currentPath.startsWith(prefix + '/')) return;
|
|
@@ -243,8 +286,10 @@ export class MemoryVolume {
|
|
|
243
286
|
result.push({ path: currentPath, kind: 'directory' });
|
|
244
287
|
if (node.children) {
|
|
245
288
|
for (const [name, child] of node.children) {
|
|
289
|
+
// Skip excluded directory names at any depth (e.g. node_modules, .cache)
|
|
290
|
+
if (excludeDirNames && child.kind === 'directory' && excludeDirNames.has(name)) continue;
|
|
246
291
|
const childPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;
|
|
247
|
-
this.collectEntries(childPath, child, result, excludePrefixes);
|
|
292
|
+
this.collectEntries(childPath, child, result, excludePrefixes, excludeDirNames);
|
|
248
293
|
}
|
|
249
294
|
}
|
|
250
295
|
}
|
|
@@ -313,6 +358,10 @@ export class MemoryVolume {
|
|
|
313
358
|
// ---- Path utilities ----
|
|
314
359
|
|
|
315
360
|
private normalize(p: string): string {
|
|
361
|
+
if (this._handler) {
|
|
362
|
+
const cached = this._handler.pathNormCache.get(p);
|
|
363
|
+
if (cached !== undefined) return cached;
|
|
364
|
+
}
|
|
316
365
|
if (!p.startsWith('/')) p = '/' + p;
|
|
317
366
|
const parts = p.split('/').filter(Boolean);
|
|
318
367
|
const resolved: string[] = [];
|
|
@@ -320,7 +369,9 @@ export class MemoryVolume {
|
|
|
320
369
|
if (part === '..') resolved.pop();
|
|
321
370
|
else if (part !== '.') resolved.push(part);
|
|
322
371
|
}
|
|
323
|
-
|
|
372
|
+
const result = '/' + resolved.join('/');
|
|
373
|
+
if (this._handler) this._handler.pathNormCache.set(p, result);
|
|
374
|
+
return result;
|
|
324
375
|
}
|
|
325
376
|
|
|
326
377
|
// assumes pre-normalized input (starts with '/', no '..' or double slashes)
|
|
@@ -418,6 +469,8 @@ export class MemoryVolume {
|
|
|
418
469
|
modified: Date.now(),
|
|
419
470
|
});
|
|
420
471
|
|
|
472
|
+
if (this._handler) this._handler.invalidateStat(norm);
|
|
473
|
+
|
|
421
474
|
if (notify) {
|
|
422
475
|
this.triggerWatchers(norm, existed ? 'change' : 'rename');
|
|
423
476
|
this.broadcast('change', norm, typeof data === 'string' ? data : this.textDecoder.decode(data));
|
|
@@ -433,13 +486,19 @@ export class MemoryVolume {
|
|
|
433
486
|
|
|
434
487
|
statSync(p: string): FileStat {
|
|
435
488
|
const norm = this.normalize(p);
|
|
489
|
+
|
|
490
|
+
if (this._handler) {
|
|
491
|
+
const cached = this._handler.statCache.get(norm);
|
|
492
|
+
if (cached !== undefined) return cached;
|
|
493
|
+
}
|
|
494
|
+
|
|
436
495
|
const node = this.locate(norm);
|
|
437
496
|
if (!node) throw makeSystemError('ENOENT', 'stat', p);
|
|
438
497
|
|
|
439
498
|
const fileSize = node.kind === 'file' ? (node.content?.length || 0) : 0;
|
|
440
499
|
const ts = node.modified;
|
|
441
500
|
|
|
442
|
-
|
|
501
|
+
const result: FileStat = {
|
|
443
502
|
isFile: () => node.kind === 'file',
|
|
444
503
|
isDirectory: () => node.kind === 'directory',
|
|
445
504
|
isSymbolicLink: () => false,
|
|
@@ -470,6 +529,9 @@ export class MemoryVolume {
|
|
|
470
529
|
ctimeNs: BigInt(ts) * 1000000n,
|
|
471
530
|
birthtimeNs: BigInt(ts) * 1000000n,
|
|
472
531
|
};
|
|
532
|
+
|
|
533
|
+
if (this._handler) this._handler.statCache.set(norm, result);
|
|
534
|
+
return result;
|
|
473
535
|
}
|
|
474
536
|
|
|
475
537
|
lstatSync(p: string): FileStat {
|
|
@@ -579,6 +641,7 @@ export class MemoryVolume {
|
|
|
579
641
|
if (target.kind !== 'file') throw makeSystemError('EISDIR', 'unlink', p);
|
|
580
642
|
|
|
581
643
|
parent.children!.delete(name);
|
|
644
|
+
if (this._handler) this._handler.invalidateStat(norm);
|
|
582
645
|
this.triggerWatchers(norm, 'rename');
|
|
583
646
|
this.broadcast('delete', norm);
|
|
584
647
|
this.notifyGlobalListeners(norm, 'unlink');
|
|
@@ -600,6 +663,7 @@ export class MemoryVolume {
|
|
|
600
663
|
if (target.children!.size > 0) throw makeSystemError('ENOTEMPTY', 'rmdir', p);
|
|
601
664
|
|
|
602
665
|
parent.children!.delete(name);
|
|
666
|
+
if (this._handler) this._handler.invalidateStat(norm);
|
|
603
667
|
}
|
|
604
668
|
|
|
605
669
|
renameSync(from: string, to: string): void {
|
|
@@ -938,7 +1002,7 @@ export class MemoryVolume {
|
|
|
938
1002
|
const self = this;
|
|
939
1003
|
const pending: Uint8Array[] = [];
|
|
940
1004
|
const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
941
|
-
const enc =
|
|
1005
|
+
const enc = this.textEncoder;
|
|
942
1006
|
|
|
943
1007
|
return {
|
|
944
1008
|
write(data: string | Uint8Array): boolean {
|
|
@@ -13,6 +13,9 @@ import { downloadAndExtract } from "./archive-extractor";
|
|
|
13
13
|
import { convertPackage, prepareTransformer } from "../module-transformer";
|
|
14
14
|
import type { PackageManifest } from "../types/manifest";
|
|
15
15
|
import * as path from "../polyfills/path";
|
|
16
|
+
import type { IDBSnapshotCache } from "../persistence/idb-cache";
|
|
17
|
+
import { quickDigest } from "../helpers/digest";
|
|
18
|
+
import { base64ToBytes } from "../helpers/byte-encoding";
|
|
16
19
|
|
|
17
20
|
// ---------------------------------------------------------------------------
|
|
18
21
|
// Public types
|
|
@@ -87,11 +90,13 @@ export class DependencyInstaller {
|
|
|
87
90
|
private vol: MemoryVolume;
|
|
88
91
|
private registryClient: RegistryClient;
|
|
89
92
|
private workingDir: string;
|
|
93
|
+
private _snapshotCache: IDBSnapshotCache | null;
|
|
90
94
|
|
|
91
|
-
constructor(vol: MemoryVolume, opts: { cwd?: string } & RegistryConfig = {}) {
|
|
95
|
+
constructor(vol: MemoryVolume, opts: { cwd?: string; snapshotCache?: IDBSnapshotCache | null } & RegistryConfig = {}) {
|
|
92
96
|
this.vol = vol;
|
|
93
97
|
this.registryClient = new RegistryClient(opts);
|
|
94
98
|
this.workingDir = opts.cwd || "/";
|
|
99
|
+
this._snapshotCache = opts.snapshotCache ?? null;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
// -----------------------------------------------------------------------
|
|
@@ -157,6 +162,37 @@ export class DependencyInstaller {
|
|
|
157
162
|
const raw = this.vol.readFileSync(jsonPath, "utf8");
|
|
158
163
|
const manifest: PackageManifest = JSON.parse(raw);
|
|
159
164
|
|
|
165
|
+
// Check IDB snapshot cache — skip full install if we have a cached node_modules
|
|
166
|
+
const cacheKey = this._snapshotCache ? quickDigest(raw) : null;
|
|
167
|
+
if (this._snapshotCache && cacheKey) {
|
|
168
|
+
try {
|
|
169
|
+
const cached = await this._snapshotCache.get(cacheKey);
|
|
170
|
+
if (cached) {
|
|
171
|
+
onProgress?.("Restoring cached node_modules...");
|
|
172
|
+
const { entries } = cached;
|
|
173
|
+
// Restore only node_modules entries from the snapshot
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (!entry.path.includes('/node_modules/')) continue;
|
|
176
|
+
if (entry.kind === 'directory') {
|
|
177
|
+
if (!this.vol.existsSync(entry.path)) {
|
|
178
|
+
this.vol.mkdirSync(entry.path, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
} else if (entry.kind === 'file' && entry.data) {
|
|
181
|
+
const parentDir = entry.path.substring(0, entry.path.lastIndexOf('/')) || '/';
|
|
182
|
+
if (parentDir !== '/' && !this.vol.existsSync(parentDir)) {
|
|
183
|
+
this.vol.mkdirSync(parentDir, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
this.vol.writeFileSync(entry.path, base64ToBytes(entry.data));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
onProgress?.(`Restored ${entries.length} cached entries`);
|
|
189
|
+
return { resolved: new Map(), newPackages: [] };
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// Cache miss or error — proceed with normal install
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
160
196
|
onProgress?.("Resolving dependency tree...");
|
|
161
197
|
|
|
162
198
|
const resolutionOpts: ResolutionConfig = {
|
|
@@ -170,6 +206,18 @@ export class DependencyInstaller {
|
|
|
170
206
|
|
|
171
207
|
const newPkgs = await this.materializePackages(tree, flags);
|
|
172
208
|
|
|
209
|
+
// Cache the installed node_modules snapshot for future reuse
|
|
210
|
+
if (this._snapshotCache && cacheKey && newPkgs.length > 0) {
|
|
211
|
+
try {
|
|
212
|
+
const snapshot = this.vol.toSnapshot();
|
|
213
|
+
// Filter to only node_modules entries to keep cache lean
|
|
214
|
+
const nmSnapshot = {
|
|
215
|
+
entries: snapshot.entries.filter(e => e.path.includes('/node_modules/')),
|
|
216
|
+
};
|
|
217
|
+
await this._snapshotCache.set(cacheKey, nmSnapshot);
|
|
218
|
+
} catch { /* cache write failure is non-fatal */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
173
221
|
onProgress?.(`Installed ${tree.size} package(s)`);
|
|
174
222
|
|
|
175
223
|
return { resolved: tree, newPackages: newPkgs };
|