@poe-platform/safe-fs 0.1.250 → 0.1.252

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.
@@ -1,3 +1,3 @@
1
1
  import type { FileSystemFactory } from "../contracts/filesystem.js";
2
- import { type FileSystemAdapterDescriptor } from "../config.js";
2
+ import type { FileSystemAdapterDescriptor } from "../config.js";
3
3
  export declare function createMemoryFileSystemAdapter(create: FileSystemFactory): FileSystemAdapterDescriptor;
@@ -1,9 +1,7 @@
1
- import { readConfigRecord } from "../config.js";
1
+ import { normalizeMemoryFileSystemLimits } from "../fs/memory/limits.js";
2
2
  export function createMemoryFileSystemAdapter(create) {
3
3
  return {
4
- validateOptions(options) {
5
- readConfigRecord(options, "memory option", []);
6
- },
4
+ validateOptions: normalizeMemoryFileSystemLimits,
7
5
  create
8
6
  };
9
7
  }
@@ -8,6 +8,7 @@ export * from "./fs/readonly/index.js";
8
8
  export * from "./fs/mount/index.js";
9
9
  export * from "./fs/overlay/index.js";
10
10
  export * from "./fs/quota/index.js";
11
+ export { scopeFileSystem } from "./fs/scoped.js";
11
12
  export * from "./fs/webdav/index.js";
12
13
  export * from "./bridge/index.js";
13
14
  export { compareEntries } from "./fs/mount/comparison.js";
@@ -8,6 +8,7 @@ export * from "./fs/readonly/index.js";
8
8
  export * from "./fs/mount/index.js";
9
9
  export * from "./fs/overlay/index.js";
10
10
  export * from "./fs/quota/index.js";
11
+ export { scopeFileSystem } from "./fs/scoped.js";
11
12
  export * from "./fs/webdav/index.js";
12
13
  export * from "./bridge/index.js";
13
14
  export { compareEntries } from "./fs/mount/comparison.js";
@@ -1,5 +1,7 @@
1
1
  import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileStat, FileSystem, FsOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
+ import { type MemoryFileSystemOptions } from "./limits.js";
4
+ export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
3
5
  export declare class MemoryFileSystem implements FileSystem {
4
6
  readonly capabilities: Readonly<{
5
7
  read: true;
@@ -37,12 +39,17 @@ export declare class MemoryFileSystem implements FileSystem {
37
39
  }>;
38
40
  private readonly identityScope;
39
41
  private nextInode;
42
+ private readonly ledger;
40
43
  private readonly root;
41
- constructor();
44
+ constructor(options?: MemoryFileSystemOptions);
42
45
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
43
46
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
44
47
  private metadata;
45
48
  private directory;
49
+ private addNode;
50
+ private releaseNode;
51
+ private releaseReference;
52
+ private replaceData;
46
53
  private fail;
47
54
  private validatePath;
48
55
  private permission;
@@ -56,7 +63,9 @@ export declare class MemoryFileSystem implements FileSystem {
56
63
  private snapshot;
57
64
  private bytes;
58
65
  private allocate;
66
+ private prepareWrite;
59
67
  private openWrite;
68
+ private writeData;
60
69
  private writeAt;
61
70
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
62
71
  writeFile(path: string, data: Uint8Array, options?: WriteFileOptions): Promise<void>;
@@ -81,4 +90,4 @@ export declare class MemoryFileSystem implements FileSystem {
81
90
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
82
91
  writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
83
92
  }
84
- export declare function createMemoryFileSystem(): MemoryFileSystem;
93
+ export declare function createMemoryFileSystem(options?: MemoryFileSystemOptions | Readonly<Record<string, unknown>>): MemoryFileSystem;
@@ -4,6 +4,9 @@ import { getOwnedS3Entry } from "../s3/registry.js";
4
4
  import { getOwnedWebDavEntry } from "../webdav/resource-id.js";
5
5
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
6
  import { resolveMissingTarget } from "./missing-target.js";
7
+ import { MemoryAllocation, MemoryLedger } from "./ledger.js";
8
+ import { normalizeMemoryFileSystemLimits } from "./limits.js";
9
+ export { defaultMemoryFileSystemLimits } from "./limits.js";
7
10
  const typeModes = { file: 0o100000, directory: 0o040000, symlink: 0o120000 };
8
11
  const ownedStats = new WeakMap();
9
12
  const ownedStores = new WeakMap();
@@ -70,8 +73,12 @@ export class MemoryFileSystem {
70
73
  })(this);
71
74
  identityScope = Symbol();
72
75
  nextInode = 1;
73
- root = this.directory(0o755);
74
- constructor() {
76
+ ledger;
77
+ root;
78
+ constructor(options = {}) {
79
+ this.ledger = new MemoryLedger(normalizeMemoryFileSystemLimits(options));
80
+ this.ledger.reserve(0, 1, "mkdir", "/");
81
+ this.root = this.directory(0o755);
75
82
  const root = this.root;
76
83
  ownedStores.set(this, {
77
84
  root,
@@ -105,13 +112,45 @@ export class MemoryFileSystem {
105
112
  metadata(mode) {
106
113
  const now = Date.now();
107
114
  return {
108
- mode, ino: this.nextInode++, nlink: 1,
115
+ mode, ino: this.nextInode++, nlink: 1, references: 0,
109
116
  atimeMs: now, mtimeMs: now, ctimeMs: now, birthtimeMs: now,
110
117
  };
111
118
  }
112
119
  directory(mode) {
113
120
  return { ...this.metadata(typeModes.directory | mode), type: "directory", entries: new Map() };
114
121
  }
122
+ addNode(parent, name, create, syscall, path, retainedBytes = 0) {
123
+ const bytes = name.length * 2 + retainedBytes;
124
+ this.ledger.reserve(bytes, 2, syscall, path);
125
+ try {
126
+ const node = create();
127
+ parent.entries.set(name, node);
128
+ this.changed(parent);
129
+ return node;
130
+ }
131
+ catch (error) {
132
+ this.ledger.release(bytes, 2);
133
+ throw error;
134
+ }
135
+ }
136
+ releaseNode(node) {
137
+ if (node.nlink !== 0 || node.references !== 0)
138
+ return;
139
+ this.ledger.release(node.type === "symlink" ? node.target.length * 2 : 0, 1);
140
+ if (node.type === "file")
141
+ node.allocation.release();
142
+ }
143
+ releaseReference(node, path) {
144
+ node.references--;
145
+ this.ledger.release(path.length * 2, 1);
146
+ this.releaseNode(node);
147
+ }
148
+ replaceData(node, allocation, length = allocation.data.byteLength) {
149
+ const previous = node.allocation;
150
+ node.data = allocation.data.subarray(0, length);
151
+ node.allocation = allocation;
152
+ previous.release();
153
+ }
115
154
  fail(code, syscall, path, dest) {
116
155
  throw new FsError(code, { syscall, path, ...(dest === undefined ? {} : { dest }) });
117
156
  }
@@ -163,9 +202,8 @@ export class MemoryFileSystem {
163
202
  let node = current.entries.get(component);
164
203
  if (!node && options.createDirectories !== undefined) {
165
204
  this.permission(current, 3, syscall, path);
166
- node = this.directory(options.createDirectories);
167
- current.entries.set(component, node);
168
- this.changed(current);
205
+ const mode = options.createDirectories;
206
+ node = this.addNode(current, component, () => this.directory(mode), syscall, path);
169
207
  }
170
208
  if (!node) {
171
209
  if (options.allowMissing && pending.length === 0) {
@@ -222,20 +260,29 @@ export class MemoryFileSystem {
222
260
  atimeMs: node.atimeMs, mtimeMs: node.mtimeMs, ctimeMs: node.ctimeMs, birthtimeMs: node.birthtimeMs,
223
261
  };
224
262
  }
225
- bytes(data) {
226
- if (!(data instanceof Uint8Array))
227
- throw new TypeError("Memory files require Uint8Array data");
228
- return new Uint8Array(data);
263
+ bytes(data, syscall, path) {
264
+ const allocation = this.allocate(data.byteLength, syscall, path);
265
+ try {
266
+ allocation.data.set(data);
267
+ return allocation;
268
+ }
269
+ catch (error) {
270
+ allocation.release();
271
+ throw error;
272
+ }
229
273
  }
230
274
  allocate(length, syscall, path) {
275
+ this.ledger.fileSize(length, syscall, path);
276
+ this.ledger.reserve(length, 0, syscall, path);
231
277
  try {
232
- return new Uint8Array(length);
278
+ return new MemoryAllocation(new Uint8Array(length), this.ledger);
233
279
  }
234
280
  catch (cause) {
281
+ this.ledger.release(length, 0);
235
282
  throw new FsError("EFBIG", { syscall, path, cause });
236
283
  }
237
284
  }
238
- openWrite(path, options, syscall) {
285
+ prepareWrite(path, options, syscall) {
239
286
  const flag = options.flag ?? "w";
240
287
  if (!["w", "wx", "a", "ax"].includes(flag))
241
288
  this.fail("EINVAL", syscall, path);
@@ -248,13 +295,62 @@ export class MemoryFileSystem {
248
295
  if (location.node.type !== "file")
249
296
  this.fail("EISDIR", syscall, path);
250
297
  this.permission(location.node, 2, syscall, path);
298
+ }
299
+ else {
300
+ this.permission(location.parent, 3, syscall, path);
301
+ }
302
+ return { location, mode, append: flag === "a" || flag === "ax" };
303
+ }
304
+ openWrite(path, options, syscall, target = this.prepareWrite(path, options, syscall)) {
305
+ const { location, mode } = target;
306
+ if (location.node)
251
307
  return location.node;
308
+ return this.addNode(location.parent, location.name, () => {
309
+ const allocation = new MemoryAllocation(new Uint8Array(), this.ledger);
310
+ return { ...this.metadata(typeModes.file | mode), type: "file", data: allocation.data, allocation };
311
+ }, syscall, path);
312
+ }
313
+ writeData(path, data, options, syscall) {
314
+ if (!(data instanceof Uint8Array))
315
+ throw new TypeError("Memory files require Uint8Array data");
316
+ const target = this.prepareWrite(path, options, syscall);
317
+ const current = target.location.node;
318
+ const length = (target.append ? current?.data.byteLength ?? 0 : 0) + data.byteLength;
319
+ this.ledger.fileSize(length, syscall, path);
320
+ const growth = target.append && length > (current?.allocation.data.byteLength ?? 0) ? length : 0;
321
+ this.ledger.check(data.byteLength + growth + (current ? 0 : target.location.name.length * 2), current ? 0 : 2, syscall, path);
322
+ const copied = this.bytes(data, syscall, path);
323
+ let allocation = copied;
324
+ let transferred = false;
325
+ try {
326
+ if (!current) {
327
+ if (target.append && length > 0) {
328
+ const capacity = Math.min(Math.max(length, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes - target.location.name.length * 2);
329
+ allocation = this.allocate(capacity, syscall, path);
330
+ allocation.data.set(copied.data);
331
+ }
332
+ const node = this.addNode(target.location.parent, target.location.name, () => ({
333
+ ...this.metadata(typeModes.file | target.mode), type: "file",
334
+ data: allocation.data.subarray(0, length), allocation,
335
+ }), syscall, path);
336
+ transferred = true;
337
+ return node;
338
+ }
339
+ if (target.append)
340
+ this.writeAt(current, copied.data, current.data.byteLength, syscall, path);
341
+ else {
342
+ this.replaceData(current, copied);
343
+ transferred = true;
344
+ this.changed(current);
345
+ }
346
+ return current;
347
+ }
348
+ finally {
349
+ if (allocation !== copied && !transferred)
350
+ allocation.release();
351
+ if (!transferred || allocation !== copied)
352
+ copied.release();
252
353
  }
253
- this.permission(location.parent, 3, syscall, path);
254
- const node = { ...this.metadata(typeModes.file | mode), type: "file", data: new Uint8Array() };
255
- location.parent.entries.set(location.name, node);
256
- this.changed(location.parent);
257
- return node;
258
354
  }
259
355
  writeAt(node, data, position, syscall, path) {
260
356
  if (data.byteLength === 0) {
@@ -262,21 +358,31 @@ export class MemoryFileSystem {
262
358
  return;
263
359
  }
264
360
  const end = position + data.byteLength;
265
- if (!Number.isSafeInteger(end))
266
- this.fail("EFBIG", syscall, path);
361
+ this.ledger.fileSize(end, syscall, path);
267
362
  const length = Math.max(node.data.byteLength, end);
268
- let storage;
269
- if (length <= node.data.buffer.byteLength) {
270
- storage = new Uint8Array(node.data.buffer);
363
+ let allocation = node.allocation;
364
+ if (length > allocation.data.byteLength) {
365
+ this.ledger.check(length, 0, syscall, path);
366
+ const capacity = Math.min(Math.max(length, node.data.byteLength * 2, 64), this.ledger.limits.maxFileBytes, this.ledger.availableBytes);
367
+ allocation = this.allocate(capacity, syscall, path);
271
368
  }
272
- else {
273
- storage = this.allocate(Math.max(length, node.data.byteLength * 2, 64), syscall, path);
274
- storage.set(node.data);
369
+ try {
370
+ const storage = allocation.data;
371
+ if (allocation !== node.allocation)
372
+ storage.set(node.data);
373
+ if (position > node.data.byteLength)
374
+ storage.fill(0, node.data.byteLength, position);
375
+ storage.set(data, position);
275
376
  }
276
- if (position > node.data.byteLength)
277
- storage.fill(0, node.data.byteLength, position);
278
- storage.set(data, position);
279
- node.data = storage.subarray(0, length);
377
+ catch (error) {
378
+ if (allocation !== node.allocation)
379
+ allocation.release();
380
+ throw error;
381
+ }
382
+ if (allocation !== node.allocation)
383
+ this.replaceData(node, allocation, length);
384
+ else
385
+ node.data = allocation.data.subarray(0, length);
280
386
  this.changed(node);
281
387
  }
282
388
  async readFile(path, options = {}) {
@@ -292,20 +398,11 @@ export class MemoryFileSystem {
292
398
  }
293
399
  async writeFile(path, data, options = {}) {
294
400
  options.signal?.throwIfAborted();
295
- const copied = this.bytes(data);
296
- const node = this.openWrite(path, options, "writeFile");
297
- if (options.flag === "a" || options.flag === "ax")
298
- this.writeAt(node, copied, node.data.byteLength, "writeFile", path);
299
- else {
300
- node.data = copied;
301
- this.changed(node);
302
- }
401
+ this.writeData(path, data, options, "writeFile");
303
402
  }
304
403
  async appendFile(path, data, options = {}) {
305
404
  options.signal?.throwIfAborted();
306
- const copied = this.bytes(data);
307
- const node = this.openWrite(path, { ...options, flag: "a" }, "appendFile");
308
- this.writeAt(node, copied, node.data.byteLength, "appendFile", path);
405
+ this.writeData(path, data, { ...options, flag: "a" }, "appendFile");
309
406
  }
310
407
  async stat(path, options = {}) {
311
408
  options.signal?.throwIfAborted();
@@ -342,8 +439,7 @@ export class MemoryFileSystem {
342
439
  if (location.node)
343
440
  this.fail("EEXIST", "mkdir", path);
344
441
  this.permission(location.parent, 3, "mkdir", path);
345
- location.parent.entries.set(location.name, this.directory(mode));
346
- this.changed(location.parent);
442
+ this.addNode(location.parent, location.name, () => this.directory(mode), "mkdir", path);
347
443
  }
348
444
  async rmdir(path, options = {}) {
349
445
  options.signal?.throwIfAborted();
@@ -359,8 +455,10 @@ export class MemoryFileSystem {
359
455
  if (node.entries.size > 0)
360
456
  this.fail("ENOTEMPTY", "rmdir", path);
361
457
  location.parent.entries.delete(location.name);
458
+ this.ledger.release(location.name.length * 2, 1);
362
459
  node.nlink = 0;
363
460
  node.ctimeMs = Date.now();
461
+ this.releaseNode(node);
364
462
  this.changed(location.parent);
365
463
  }
366
464
  async rm(path, options = {}) {
@@ -392,9 +490,16 @@ export class MemoryFileSystem {
392
490
  }
393
491
  }
394
492
  location.parent.entries.delete(location.name);
493
+ this.ledger.release(location.name.length * 2, 1);
395
494
  for (const entry of removed) {
495
+ if (entry.type === "directory") {
496
+ for (const name of entry.entries.keys())
497
+ this.ledger.release(name.length * 2, 1);
498
+ entry.entries.clear();
499
+ }
396
500
  entry.nlink--;
397
501
  entry.ctimeMs = Date.now();
502
+ this.releaseNode(entry);
398
503
  }
399
504
  this.changed(location.parent);
400
505
  }
@@ -423,11 +528,25 @@ export class MemoryFileSystem {
423
528
  this.fail("EISDIR", "rename", source, destination);
424
529
  if (target.node.type === "directory" && target.node.entries.size > 0)
425
530
  this.fail("ENOTEMPTY", "rename", source, destination);
531
+ }
532
+ const nameGrowth = target.node ? 0 : (target.name.length - origin.name.length) * 2;
533
+ this.ledger.reserve(Math.max(0, nameGrowth), 0, "rename", source);
534
+ try {
535
+ target.parent.entries.set(target.name, node);
536
+ }
537
+ catch (error) {
538
+ this.ledger.release(Math.max(0, nameGrowth), 0);
539
+ throw error;
540
+ }
541
+ origin.parent.entries.delete(origin.name);
542
+ if (target.node) {
543
+ this.ledger.release(origin.name.length * 2, 1);
426
544
  target.node.nlink--;
427
545
  target.node.ctimeMs = Date.now();
546
+ this.releaseNode(target.node);
428
547
  }
429
- origin.parent.entries.delete(origin.name);
430
- target.parent.entries.set(target.name, node);
548
+ else if (nameGrowth < 0)
549
+ this.ledger.release(-nameGrowth, 0);
431
550
  this.changed(origin.parent);
432
551
  this.changed(target.parent);
433
552
  node.ctimeMs = Date.now();
@@ -448,8 +567,7 @@ export class MemoryFileSystem {
448
567
  this.fail("EEXIST", "copyFile", source, destination);
449
568
  if (target.node === origin)
450
569
  this.fail("EINVAL", "copyFile", source, destination);
451
- const node = this.openWrite(destination, { mode: origin.mode & 0o7777, flag: options.exclusive ? "wx" : "w" }, "copyFile");
452
- node.data = new Uint8Array(origin.data);
570
+ const node = this.writeData(destination, origin.data, { mode: origin.mode & 0o7777, flag: options.exclusive ? "wx" : "w" }, "copyFile");
453
571
  node.mode = origin.mode;
454
572
  this.changed(node);
455
573
  origin.atimeMs = Date.now();
@@ -484,8 +602,9 @@ export class MemoryFileSystem {
484
602
  if (location.node)
485
603
  this.fail("EEXIST", "symlink", path);
486
604
  this.permission(location.parent, 3, "symlink", path);
487
- location.parent.entries.set(location.name, { ...this.metadata(typeModes.symlink | 0o777), type: "symlink", target });
488
- this.changed(location.parent);
605
+ this.addNode(location.parent, location.name, () => ({
606
+ ...this.metadata(typeModes.symlink | 0o777), type: "symlink", target,
607
+ }), "symlink", path, target.length * 2);
489
608
  }
490
609
  async link(existingPath, newPath, options = {}) {
491
610
  options.signal?.throwIfAborted();
@@ -496,7 +615,14 @@ export class MemoryFileSystem {
496
615
  if (target.node)
497
616
  this.fail("EEXIST", "link", existingPath, newPath);
498
617
  this.permission(target.parent, 3, "link", newPath);
499
- target.parent.entries.set(target.name, node);
618
+ this.ledger.reserve(target.name.length * 2, 1, "link", newPath);
619
+ try {
620
+ target.parent.entries.set(target.name, node);
621
+ }
622
+ catch (error) {
623
+ this.ledger.release(target.name.length * 2, 1);
624
+ throw error;
625
+ }
500
626
  node.nlink++;
501
627
  node.ctimeMs = Date.now();
502
628
  this.changed(target.parent);
@@ -523,8 +649,14 @@ export class MemoryFileSystem {
523
649
  const node = this.file(path, "truncate");
524
650
  this.permission(node, 2, "truncate", path);
525
651
  const data = this.allocate(length, "truncate", path);
526
- data.set(node.data.subarray(0, length));
527
- node.data = data;
652
+ try {
653
+ data.data.set(node.data.subarray(0, length));
654
+ }
655
+ catch (error) {
656
+ data.release();
657
+ throw error;
658
+ }
659
+ this.replaceData(node, data);
528
660
  this.changed(node);
529
661
  }
530
662
  async openReadFile(path, options = {}) {
@@ -535,7 +667,7 @@ export class MemoryFileSystem {
535
667
  || Object.getOwnPropertyDescriptor(this, "root")?.value !== owner.root)
536
668
  unsupported();
537
669
  for (const name of ["openReadFile", "readFile", "readStream", "stat", "lstat", "realpath", "access",
538
- "file", "resolve", "permission", "validatePath", "fail", "snapshot", "integer"]) {
670
+ "file", "resolve", "permission", "validatePath", "fail", "snapshot", "integer", "releaseReference", "releaseNode"]) {
539
671
  const descriptor = Object.getOwnPropertyDescriptor(this, name)
540
672
  ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
541
673
  if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
@@ -545,8 +677,11 @@ export class MemoryFileSystem {
545
677
  unsupported();
546
678
  let node = this.file(path, "openReadFile");
547
679
  this.permission(node, 4, "openReadFile", path);
680
+ this.ledger.reserve(path.length * 2, 1, "openReadFile", path);
681
+ node.references++;
548
682
  const snapshot = this.snapshot.bind(this);
549
683
  const integer = this.integer.bind(this);
684
+ const releaseReference = this.releaseReference.bind(this);
550
685
  let closing;
551
686
  const current = (signal, syscall) => {
552
687
  signal?.throwIfAborted();
@@ -570,7 +705,10 @@ export class MemoryFileSystem {
570
705
  return bytes;
571
706
  },
572
707
  close() {
573
- node = undefined;
708
+ if (node) {
709
+ releaseReference(node, path);
710
+ node = undefined;
711
+ }
574
712
  closing ??= Promise.resolve();
575
713
  return closing;
576
714
  },
@@ -591,33 +729,55 @@ export class MemoryFileSystem {
591
729
  }
592
730
  const node = this.file(path, "readStream");
593
731
  this.permission(node, 4, "readStream", path);
732
+ this.ledger.reserve(path.length * 2, 1, "readStream", path);
733
+ node.references++;
734
+ const allocation = node.allocation;
735
+ allocation.retain();
594
736
  const data = node.data;
595
- const end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
596
- node.atimeMs = Date.now();
597
- for (let offset = start; offset < end; offset += chunkSize) {
737
+ try {
738
+ const end = Math.min(options.endExclusive ?? data.byteLength, data.byteLength);
739
+ node.atimeMs = Date.now();
740
+ for (let offset = start; offset < end; offset += chunkSize) {
741
+ options.signal?.throwIfAborted();
742
+ yield data.slice(offset, Math.min(offset + chunkSize, end));
743
+ }
598
744
  options.signal?.throwIfAborted();
599
- yield data.slice(offset, Math.min(offset + chunkSize, end));
600
745
  }
601
- options.signal?.throwIfAborted();
746
+ finally {
747
+ allocation.release();
748
+ this.releaseReference(node, path);
749
+ }
602
750
  }
603
751
  async writeStream(path, source, options = {}) {
604
752
  options.signal?.throwIfAborted();
605
- const node = this.openWrite(path, options, "writeStream");
606
- const append = options.flag === "a" || options.flag === "ax";
607
- let position = 0;
608
- if (!append) {
609
- node.data = new Uint8Array();
610
- this.changed(node);
611
- }
612
- for await (const chunk of source) {
753
+ this.validatePath(path, "writeStream");
754
+ this.ledger.reserve(path.length * 2, 1, "writeStream", path);
755
+ let node;
756
+ try {
757
+ node = this.openWrite(path, options, "writeStream");
758
+ node.references++;
759
+ const append = options.flag === "a" || options.flag === "ax";
760
+ let position = 0;
761
+ if (!append) {
762
+ this.replaceData(node, new MemoryAllocation(new Uint8Array(), this.ledger));
763
+ this.changed(node);
764
+ }
765
+ for await (const chunk of source) {
766
+ options.signal?.throwIfAborted();
767
+ if (!(chunk instanceof Uint8Array))
768
+ throw new TypeError("Memory files require Uint8Array data");
769
+ this.writeAt(node, chunk, append ? node.data.byteLength : position, "writeStream", path);
770
+ if (!append)
771
+ position += chunk.byteLength;
772
+ }
613
773
  options.signal?.throwIfAborted();
614
- if (!(chunk instanceof Uint8Array))
615
- throw new TypeError("Memory files require Uint8Array data");
616
- this.writeAt(node, chunk, append ? node.data.byteLength : position, "writeStream", path);
617
- if (!append)
618
- position += chunk.byteLength;
619
774
  }
620
- options.signal?.throwIfAborted();
775
+ finally {
776
+ if (node)
777
+ this.releaseReference(node, path);
778
+ else
779
+ this.ledger.release(path.length * 2, 1);
780
+ }
621
781
  }
622
782
  }
623
783
  const memoryImplementation = Object.getOwnPropertyDescriptors(MemoryFileSystem.prototype);
@@ -629,7 +789,8 @@ function stockDescriptorWrite(filesystem) {
629
789
  || Object.getOwnPropertyDescriptor(filesystem, "capabilities")?.value !== owner.capabilities)
630
790
  return false;
631
791
  for (const name of ["writeStream", "writeFile", "appendFile", "access", "stat", "lstat", "realpath",
632
- "openWrite", "resolve", "permission", "validatePath", "mode", "bytes", "allocate", "changed", "metadata", "integer", "writeAt", "fail"]) {
792
+ "openWrite", "prepareWrite", "writeData", "addNode", "replaceData", "releaseReference", "releaseNode",
793
+ "resolve", "permission", "validatePath", "mode", "bytes", "allocate", "changed", "metadata", "integer", "writeAt", "fail"]) {
633
794
  const descriptor = Object.getOwnPropertyDescriptor(filesystem, name)
634
795
  ?? Object.getOwnPropertyDescriptor(MemoryFileSystem.prototype, name);
635
796
  if (!descriptor || !("value" in descriptor) || descriptor.value !== memoryImplementation[name]?.value)
@@ -637,6 +798,6 @@ function stockDescriptorWrite(filesystem) {
637
798
  }
638
799
  return true;
639
800
  }
640
- export function createMemoryFileSystem() {
641
- return new MemoryFileSystem();
801
+ export function createMemoryFileSystem(options = {}) {
802
+ return new MemoryFileSystem(normalizeMemoryFileSystemLimits(options));
642
803
  }
@@ -0,0 +1,20 @@
1
+ import type { MemoryFileSystemLimits } from "./limits.js";
2
+ export declare class MemoryLedger {
3
+ readonly limits: Readonly<MemoryFileSystemLimits>;
4
+ private retainedBytes;
5
+ private metadataUnits;
6
+ constructor(limits: Readonly<MemoryFileSystemLimits>);
7
+ get availableBytes(): number;
8
+ fileSize(length: number, syscall: string, path: string): void;
9
+ check(bytes: number, units: number, syscall: string, path: string): void;
10
+ reserve(bytes: number, units: number, syscall: string, path: string): void;
11
+ release(bytes: number, units: number): void;
12
+ }
13
+ export declare class MemoryAllocation {
14
+ readonly data: Uint8Array;
15
+ private readonly ledger;
16
+ private references;
17
+ constructor(data: Uint8Array, ledger: MemoryLedger);
18
+ retain(): void;
19
+ release(): void;
20
+ }
@@ -0,0 +1,53 @@
1
+ import { FsError } from "../../contracts/errors.js";
2
+ export class MemoryLedger {
3
+ limits;
4
+ retainedBytes = 0;
5
+ metadataUnits = 0;
6
+ constructor(limits) {
7
+ this.limits = limits;
8
+ }
9
+ get availableBytes() {
10
+ return this.limits.maxRetainedBytes - this.retainedBytes;
11
+ }
12
+ fileSize(length, syscall, path) {
13
+ if (!Number.isSafeInteger(length) || length < 0 || length > this.limits.maxFileBytes) {
14
+ throw new FsError("EFBIG", { syscall, path });
15
+ }
16
+ }
17
+ check(bytes, units, syscall, path) {
18
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.availableBytes ||
19
+ !Number.isSafeInteger(units) || units < 0 || units > this.limits.maxMetadataUnits - this.metadataUnits) {
20
+ throw new FsError("ENOSPC", { syscall, path });
21
+ }
22
+ }
23
+ reserve(bytes, units, syscall, path) {
24
+ this.check(bytes, units, syscall, path);
25
+ this.retainedBytes += bytes;
26
+ this.metadataUnits += units;
27
+ }
28
+ release(bytes, units) {
29
+ if (bytes < 0 || units < 0 || bytes > this.retainedBytes || units > this.metadataUnits) {
30
+ throw new Error("Invalid Memory filesystem reservation release");
31
+ }
32
+ this.retainedBytes -= bytes;
33
+ this.metadataUnits -= units;
34
+ }
35
+ }
36
+ export class MemoryAllocation {
37
+ data;
38
+ ledger;
39
+ references = 1;
40
+ constructor(data, ledger) {
41
+ this.data = data;
42
+ this.ledger = ledger;
43
+ }
44
+ retain() {
45
+ this.references++;
46
+ }
47
+ release() {
48
+ if (this.references <= 0)
49
+ throw new Error("Memory allocation already released");
50
+ if (--this.references === 0)
51
+ this.ledger.release(this.data.byteLength, 0);
52
+ }
53
+ }
@@ -0,0 +1,8 @@
1
+ export interface MemoryFileSystemLimits {
2
+ readonly maxFileBytes: number;
3
+ readonly maxRetainedBytes: number;
4
+ readonly maxMetadataUnits: number;
5
+ }
6
+ export type MemoryFileSystemOptions = Partial<MemoryFileSystemLimits>;
7
+ export declare const defaultMemoryFileSystemLimits: Readonly<MemoryFileSystemLimits>;
8
+ export declare function normalizeMemoryFileSystemLimits(options: unknown): Readonly<MemoryFileSystemLimits>;
@@ -0,0 +1,19 @@
1
+ import { readConfigRecord } from "../../config.js";
2
+ export const defaultMemoryFileSystemLimits = Object.freeze({
3
+ maxFileBytes: 16 * 1024 * 1024,
4
+ maxRetainedBytes: 64 * 1024 * 1024,
5
+ maxMetadataUnits: 10_000,
6
+ });
7
+ export function normalizeMemoryFileSystemLimits(options) {
8
+ const keys = ["maxFileBytes", "maxRetainedBytes", "maxMetadataUnits"];
9
+ const record = readConfigRecord(options, "memory option", keys);
10
+ const limits = { ...defaultMemoryFileSystemLimits };
11
+ for (const key of keys) {
12
+ const value = Object.hasOwn(record, key) ? record[key] : limits[key];
13
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (key === "maxMetadataUnits" ? 1 : 0)) {
14
+ throw new RangeError(`${key} must be a ${key === "maxMetadataUnits" ? "positive" : "nonnegative"} safe integer`);
15
+ }
16
+ limits[key] = value;
17
+ }
18
+ return Object.freeze(limits);
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { FileSystem } from "../contracts/filesystem.js";
2
+ export declare function scopeFileSystem(filesystem: FileSystem, charge: () => void, signal: AbortSignal): FileSystem;
@@ -0,0 +1,98 @@
1
+ import { finishCleanup } from "../contracts/cleanup.js";
2
+ import { registerEntryView } from "./mount/comparison.js";
3
+ const originals = new WeakMap();
4
+ const operations = new Set([
5
+ "access", "appendFile", "canonicalizeMissingTarget", "capabilitiesFor", "chmod", "compareEntry",
6
+ "copyFile", "link", "lstat", "mkdir", "openReadFile", "readFile", "readStream", "readdir",
7
+ "readlink", "realpath", "rename", "rm", "rmdir", "stat", "symlink", "truncate", "utimes",
8
+ "writeFile", "writeStream",
9
+ ]);
10
+ export function scopeFileSystem(filesystem, charge, signal) {
11
+ const original = originals.get(filesystem) ?? filesystem;
12
+ const methods = new Map();
13
+ const assertOpen = (options) => {
14
+ signal.throwIfAborted();
15
+ options?.signal?.throwIfAborted();
16
+ };
17
+ const admit = (options) => {
18
+ assertOpen(options);
19
+ charge();
20
+ };
21
+ const wrapHandle = (handle) => ({
22
+ async stat(options) { admit(options); return handle.stat(options); },
23
+ async read(position, maxBytes, options) { admit(options); return handle.read(position, maxBytes, options); },
24
+ close: handle.close.bind(handle),
25
+ });
26
+ const wrapStream = (source, options) => ({
27
+ [Symbol.asyncIterator]() {
28
+ assertOpen(options);
29
+ const iterator = source[Symbol.asyncIterator]();
30
+ let closing;
31
+ const close = (value) => closing ??= Promise.resolve().then(() => iterator.return ? iterator.return(value) : { done: true, value: undefined });
32
+ const advance = async (operation) => {
33
+ try {
34
+ assertOpen(options);
35
+ if (closing) {
36
+ await closing;
37
+ return { done: true, value: undefined };
38
+ }
39
+ const result = await operation();
40
+ assertOpen(options);
41
+ if (closing) {
42
+ await closing;
43
+ return { done: true, value: undefined };
44
+ }
45
+ return result;
46
+ }
47
+ catch (error) {
48
+ await finishCleanup(close, true);
49
+ throw error;
50
+ }
51
+ };
52
+ return {
53
+ next: () => advance(() => iterator.next()),
54
+ return: close,
55
+ ...(iterator.throw ? { throw: (error) => advance(() => iterator.throw(error)) } : {}),
56
+ };
57
+ },
58
+ });
59
+ const view = new Proxy(Object.create(original), {
60
+ set(_target, property, value) {
61
+ return Reflect.set(original, property, value, original);
62
+ },
63
+ get(_target, property) {
64
+ const method = Reflect.get(original, property, original);
65
+ if (typeof method !== "function")
66
+ return method;
67
+ const cached = methods.get(property);
68
+ if (cached?.original === method)
69
+ return cached.scoped;
70
+ const dispatch = (...args) => {
71
+ if (operations.has(property)) {
72
+ const options = args.at(-1);
73
+ admit(options && typeof options === "object" && "signal" in options ? options : undefined);
74
+ }
75
+ if (property === "compareEntry") {
76
+ const peer = args[1];
77
+ args[1] = originals.get(peer) ?? peer;
78
+ }
79
+ return Reflect.apply(method, original, args);
80
+ };
81
+ const scoped = property === "openReadFile"
82
+ ? async (...args) => wrapHandle(await dispatch(...args))
83
+ : property === "readStream"
84
+ ? (...args) => wrapStream(dispatch(...args), args[1])
85
+ : operations.has(property) && property !== "canonicalizeMissingTarget"
86
+ ? async (...args) => dispatch(...args)
87
+ : dispatch;
88
+ methods.set(property, { original: method, scoped });
89
+ return scoped;
90
+ },
91
+ });
92
+ originals.set(view, original);
93
+ registerEntryView(view, async (path, options) => {
94
+ assertOpen(options);
95
+ return { filesystem: original, path };
96
+ });
97
+ return view;
98
+ }
@@ -9,6 +9,7 @@ export * from "./fs/readonly/index.js";
9
9
  export * from "./fs/mount/index.js";
10
10
  export * from "./fs/overlay/index.js";
11
11
  export * from "./fs/quota/index.js";
12
+ export { scopeFileSystem } from "./fs/scoped.js";
12
13
  export * from "./node/index.js";
13
14
  export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
14
15
  export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
@@ -8,6 +8,7 @@ export * from "./fs/readonly/index.js";
8
8
  export * from "./fs/mount/index.js";
9
9
  export * from "./fs/overlay/index.js";
10
10
  export * from "./fs/quota/index.js";
11
+ export { scopeFileSystem } from "./fs/scoped.js";
11
12
  export * from "./node/index.js";
12
13
  export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
13
14
  export { createNodeFileSystemAdapterRegistry } from "./config.node.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.250",
3
+ "version": "0.1.252",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",