@scelar/nodepod 1.0.3 → 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 (55) hide show
  1. package/dist/__sw__.js +642 -642
  2. package/dist/{child_process-hmVqFcF7.cjs → child_process-B9qsOKHs.cjs} +7434 -7434
  3. package/dist/child_process-B9qsOKHs.cjs.map +1 -0
  4. package/dist/{child_process-D6oDN2MX.js → child_process-PY34i_6n.js} +8233 -8233
  5. package/dist/child_process-PY34i_6n.js.map +1 -0
  6. package/dist/{index-BO1i013L.cjs → index-CyhVjVJU.cjs} +38383 -37240
  7. package/dist/index-CyhVjVJU.cjs.map +1 -0
  8. package/dist/index-D8Hn2kWU.js +36455 -0
  9. package/dist/index-D8Hn2kWU.js.map +1 -0
  10. package/dist/index.cjs +67 -65
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +88 -86
  13. package/dist/index.mjs +61 -59
  14. package/dist/memory-handler.d.ts +57 -0
  15. package/dist/memory-volume.d.ts +157 -147
  16. package/dist/packages/installer.d.ts +44 -41
  17. package/dist/persistence/idb-cache.d.ts +7 -0
  18. package/dist/polyfills/wasi.d.ts +45 -4
  19. package/dist/script-engine.d.ts +84 -81
  20. package/dist/sdk/nodepod-process.d.ts +29 -28
  21. package/dist/sdk/nodepod.d.ts +59 -39
  22. package/dist/sdk/types.d.ts +64 -53
  23. package/dist/threading/process-manager.d.ts +1 -1
  24. package/dist/threading/worker-protocol.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/src/index.ts +194 -192
  27. package/src/memory-handler.ts +168 -0
  28. package/src/memory-volume.ts +78 -8
  29. package/src/packages/installer.ts +49 -1
  30. package/src/packages/version-resolver.ts +421 -411
  31. package/src/persistence/idb-cache.ts +107 -0
  32. package/src/polyfills/child_process.ts +2288 -2288
  33. package/src/polyfills/events.ts +6 -2
  34. package/src/polyfills/fs.ts +2888 -2888
  35. package/src/polyfills/http.ts +1450 -1449
  36. package/src/polyfills/process.ts +27 -1
  37. package/src/polyfills/stream.ts +1621 -1620
  38. package/src/polyfills/wasi.ts +1306 -44
  39. package/src/polyfills/zlib.ts +881 -881
  40. package/src/request-proxy.ts +716 -716
  41. package/src/script-engine.ts +444 -118
  42. package/src/sdk/nodepod-process.ts +94 -86
  43. package/src/sdk/nodepod.ts +571 -509
  44. package/src/sdk/types.ts +82 -70
  45. package/src/syntax-transforms.ts +2 -2
  46. package/src/threading/offload-worker.ts +383 -383
  47. package/src/threading/offload.ts +271 -271
  48. package/src/threading/process-manager.ts +967 -956
  49. package/src/threading/process-worker-entry.ts +858 -854
  50. package/src/threading/worker-protocol.ts +358 -358
  51. package/dist/child_process-D6oDN2MX.js.map +0 -1
  52. package/dist/child_process-hmVqFcF7.cjs.map +0 -1
  53. package/dist/index-Ale2oba_.js +0 -35412
  54. package/dist/index-Ale2oba_.js.map +0 -1
  55. package/dist/index-BO1i013L.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,61 @@ 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(): VolumeSnapshot {
258
+ toSnapshot(excludePrefixes?: string[], excludeDirNames?: Set<string>): VolumeSnapshot {
222
259
  const entries: VolumeEntry[] = [];
223
- this.collectEntries('/', this.tree, entries);
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[]): void {
264
+ private collectEntries(
265
+ currentPath: string,
266
+ node: VolumeNode,
267
+ result: VolumeEntry[],
268
+ excludePrefixes?: string[],
269
+ excludeDirNames?: Set<string>,
270
+ ): void {
271
+ if (excludePrefixes) {
272
+ for (const prefix of excludePrefixes) {
273
+ if (currentPath === prefix || currentPath.startsWith(prefix + '/')) return;
274
+ }
275
+ }
276
+
228
277
  if (node.kind === 'file') {
229
278
  let data = '';
230
279
  if (node.content && node.content.length > 0) {
@@ -237,8 +286,10 @@ export class MemoryVolume {
237
286
  result.push({ path: currentPath, kind: 'directory' });
238
287
  if (node.children) {
239
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;
240
291
  const childPath = currentPath === '/' ? `/${name}` : `${currentPath}/${name}`;
241
- this.collectEntries(childPath, child, result);
292
+ this.collectEntries(childPath, child, result, excludePrefixes, excludeDirNames);
242
293
  }
243
294
  }
244
295
  }
@@ -307,6 +358,10 @@ export class MemoryVolume {
307
358
  // ---- Path utilities ----
308
359
 
309
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
+ }
310
365
  if (!p.startsWith('/')) p = '/' + p;
311
366
  const parts = p.split('/').filter(Boolean);
312
367
  const resolved: string[] = [];
@@ -314,7 +369,9 @@ export class MemoryVolume {
314
369
  if (part === '..') resolved.pop();
315
370
  else if (part !== '.') resolved.push(part);
316
371
  }
317
- return '/' + resolved.join('/');
372
+ const result = '/' + resolved.join('/');
373
+ if (this._handler) this._handler.pathNormCache.set(p, result);
374
+ return result;
318
375
  }
319
376
 
320
377
  // assumes pre-normalized input (starts with '/', no '..' or double slashes)
@@ -412,6 +469,8 @@ export class MemoryVolume {
412
469
  modified: Date.now(),
413
470
  });
414
471
 
472
+ if (this._handler) this._handler.invalidateStat(norm);
473
+
415
474
  if (notify) {
416
475
  this.triggerWatchers(norm, existed ? 'change' : 'rename');
417
476
  this.broadcast('change', norm, typeof data === 'string' ? data : this.textDecoder.decode(data));
@@ -427,13 +486,19 @@ export class MemoryVolume {
427
486
 
428
487
  statSync(p: string): FileStat {
429
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
+
430
495
  const node = this.locate(norm);
431
496
  if (!node) throw makeSystemError('ENOENT', 'stat', p);
432
497
 
433
498
  const fileSize = node.kind === 'file' ? (node.content?.length || 0) : 0;
434
499
  const ts = node.modified;
435
500
 
436
- return {
501
+ const result: FileStat = {
437
502
  isFile: () => node.kind === 'file',
438
503
  isDirectory: () => node.kind === 'directory',
439
504
  isSymbolicLink: () => false,
@@ -464,6 +529,9 @@ export class MemoryVolume {
464
529
  ctimeNs: BigInt(ts) * 1000000n,
465
530
  birthtimeNs: BigInt(ts) * 1000000n,
466
531
  };
532
+
533
+ if (this._handler) this._handler.statCache.set(norm, result);
534
+ return result;
467
535
  }
468
536
 
469
537
  lstatSync(p: string): FileStat {
@@ -573,6 +641,7 @@ export class MemoryVolume {
573
641
  if (target.kind !== 'file') throw makeSystemError('EISDIR', 'unlink', p);
574
642
 
575
643
  parent.children!.delete(name);
644
+ if (this._handler) this._handler.invalidateStat(norm);
576
645
  this.triggerWatchers(norm, 'rename');
577
646
  this.broadcast('delete', norm);
578
647
  this.notifyGlobalListeners(norm, 'unlink');
@@ -594,6 +663,7 @@ export class MemoryVolume {
594
663
  if (target.children!.size > 0) throw makeSystemError('ENOTEMPTY', 'rmdir', p);
595
664
 
596
665
  parent.children!.delete(name);
666
+ if (this._handler) this._handler.invalidateStat(norm);
597
667
  }
598
668
 
599
669
  renameSync(from: string, to: string): void {
@@ -932,7 +1002,7 @@ export class MemoryVolume {
932
1002
  const self = this;
933
1003
  const pending: Uint8Array[] = [];
934
1004
  const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
935
- const enc = new TextEncoder();
1005
+ const enc = this.textEncoder;
936
1006
 
937
1007
  return {
938
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 };