@scelar/nodepod 1.0.4 → 1.0.5

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 (39) hide show
  1. package/dist/{child_process-lxSKECHq.cjs → child_process-B9qsOKHs.cjs} +7434 -7434
  2. package/dist/{child_process-lxSKECHq.cjs.map → child_process-B9qsOKHs.cjs.map} +1 -1
  3. package/dist/{child_process-53fMkug_.js → child_process-PY34i_6n.js} +8233 -8233
  4. package/dist/{child_process-53fMkug_.js.map → child_process-PY34i_6n.js.map} +1 -1
  5. package/dist/{index-C-TQIrdG.cjs → index-CyhVjVJU.cjs} +38383 -38005
  6. package/dist/index-CyhVjVJU.cjs.map +1 -0
  7. package/dist/{index-B8lyh_ti.js → index-D8Hn2kWU.js} +36455 -36065
  8. package/dist/index-D8Hn2kWU.js.map +1 -0
  9. package/dist/index.cjs +67 -65
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +88 -86
  12. package/dist/index.mjs +61 -59
  13. package/dist/memory-handler.d.ts +57 -0
  14. package/dist/memory-volume.d.ts +157 -147
  15. package/dist/packages/installer.d.ts +44 -41
  16. package/dist/persistence/idb-cache.d.ts +7 -0
  17. package/dist/script-engine.d.ts +84 -81
  18. package/dist/sdk/nodepod-process.d.ts +29 -28
  19. package/dist/sdk/nodepod.d.ts +59 -40
  20. package/dist/sdk/types.d.ts +64 -59
  21. package/package.json +97 -97
  22. package/src/index.ts +2 -0
  23. package/src/memory-handler.ts +168 -0
  24. package/src/memory-volume.ts +72 -8
  25. package/src/packages/installer.ts +49 -1
  26. package/src/packages/version-resolver.ts +421 -421
  27. package/src/persistence/idb-cache.ts +107 -0
  28. package/src/polyfills/events.ts +6 -2
  29. package/src/polyfills/stream.ts +1 -0
  30. package/src/polyfills/wasi.ts +1306 -1306
  31. package/src/polyfills/zlib.ts +881 -881
  32. package/src/script-engine.ts +3722 -3694
  33. package/src/sdk/nodepod-process.ts +94 -86
  34. package/src/sdk/nodepod.ts +52 -6
  35. package/src/sdk/types.ts +82 -77
  36. package/src/threading/process-manager.ts +11 -0
  37. package/src/threading/worker-protocol.ts +358 -358
  38. package/dist/index-B8lyh_ti.js.map +0 -1
  39. package/dist/index-C-TQIrdG.cjs.map +0 -1
@@ -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(currentPath: string, node: VolumeNode, result: VolumeEntry[], excludePrefixes?: string[]): void {
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
- return '/' + resolved.join('/');
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
- return {
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 = new TextEncoder();
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 };