@poe-platform/safe-fs 0.1.807 → 0.1.809

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 (41) hide show
  1. package/README.md +28 -1
  2. package/dist/safe-fs/contracts/filesystem.d.ts +20 -1
  3. package/dist/safe-fs/core.d.ts +3 -1
  4. package/dist/safe-fs/core.js +3 -1
  5. package/dist/safe-fs/fs/capabilities.d.ts +1 -1
  6. package/dist/safe-fs/fs/capabilities.js +9 -5
  7. package/dist/safe-fs/fs/devices/index.d.ts +3 -2
  8. package/dist/safe-fs/fs/devices/index.js +53 -9
  9. package/dist/safe-fs/fs/memory/atomic-view.d.ts +1 -0
  10. package/dist/safe-fs/fs/memory/index.d.ts +6 -3
  11. package/dist/safe-fs/fs/memory/index.js +101 -13
  12. package/dist/safe-fs/fs/mount/index.d.ts +4 -2
  13. package/dist/safe-fs/fs/mount/index.js +169 -10
  14. package/dist/safe-fs/fs/overlay/index.d.ts +3 -0
  15. package/dist/safe-fs/fs/overlay/index.js +177 -59
  16. package/dist/safe-fs/fs/overlay/memory-publication.d.ts +1 -0
  17. package/dist/safe-fs/fs/overlay/memory-publication.js +25 -0
  18. package/dist/safe-fs/fs/quota/index.js +9 -4
  19. package/dist/safe-fs/fs/read-file-stream.d.ts +7 -0
  20. package/dist/safe-fs/fs/read-file-stream.js +103 -0
  21. package/dist/safe-fs/fs/readonly/index.d.ts +1 -0
  22. package/dist/safe-fs/fs/readonly/index.js +10 -0
  23. package/dist/safe-fs/fs/real/index.d.ts +1 -0
  24. package/dist/safe-fs/fs/real/index.js +29 -0
  25. package/dist/safe-fs/fs/s3/filesystem.d.ts +2 -1
  26. package/dist/safe-fs/fs/s3/filesystem.js +55 -16
  27. package/dist/safe-fs/fs/scoped.js +13 -5
  28. package/dist/safe-fs/index.d.ts +1 -0
  29. package/dist/safe-fs/index.js +1 -0
  30. package/dist/safe-fs/runtime-core.d.ts +1 -0
  31. package/dist/safe-fs/runtime-core.js +1 -0
  32. package/dist/safe-fs/storage.d.ts +57 -0
  33. package/dist/safe-fs/storage.js +364 -0
  34. package/dist/safe-fs/testing/object-io-metrics.js +1 -0
  35. package/dist/xml-ast/errors.d.ts +4 -0
  36. package/dist/xml-ast/errors.js +7 -0
  37. package/dist/xml-ast/index.d.ts +2 -4
  38. package/dist/xml-ast/index.js +3 -7
  39. package/dist/xml-ast/stream.d.ts +31 -0
  40. package/dist/xml-ast/stream.js +179 -0
  41. package/package.json +10 -6
@@ -405,6 +405,7 @@ const ext4HtreeEof64 = (1n << 63n) - 1n;
405
405
  const extractionStreamGuard = Symbol("extractionStreamGuard");
406
406
  const ownedStats = new WeakMap();
407
407
  const ownedStores = new WeakMap();
408
+ const receiverCapabilities = new WeakMap();
408
409
  // Ensure V8 uses Tagged representation for timestamp fields so all 4 fields share one HeapNumber pointer.
409
410
  {
410
411
  const dummyAlloc = new MemoryAllocation(new Uint8Array(0), new MemoryLedger(normalizeMemoryFileSystemLimits({})));
@@ -583,8 +584,8 @@ const SHARED_STOCK_MEMORY_CAPABILITIES = Object.freeze({
583
584
  permissions: true,
584
585
  timestamps: true,
585
586
  atomicRename: true,
586
- atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
587
- synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
587
+ atomicStagedFileMutation: true, atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
588
+ synchronousDirectoryValidation: true, synchronousStagingResolution: true, synchronousFollowedStagingResolution: true, guardedStagingPublication: true,
588
589
  atomicDirectoryMetadata: true,
589
590
  streamingRead: true,
590
591
  retainedRead: true,
@@ -601,17 +602,24 @@ function customMemoryCapabilities(filesystem) {
601
602
  }
602
603
  export class MemoryFileSystem {
603
604
  capabilitiesFor;
604
- #customCapabilities;
605
- #identityScopeSym;
606
605
  get identityScope() {
607
- return (this.#identityScopeSym ??= Symbol());
606
+ return (this._owner.identityScope ??= Symbol());
608
607
  }
609
608
  get capabilities() {
610
- if (!this.#customCapabilities) {
611
- this.#customCapabilities = customMemoryCapabilities(this);
612
- this._owner.capabilities = this.#customCapabilities;
609
+ const owner = ownedStores.get(this);
610
+ if (owner) {
611
+ if (!owner.customCapabilities) {
612
+ owner.customCapabilities = customMemoryCapabilities(this);
613
+ owner.capabilities = owner.customCapabilities;
614
+ }
615
+ return owner.customCapabilities;
613
616
  }
614
- return this.#customCapabilities;
617
+ let caps = receiverCapabilities.get(this);
618
+ if (!caps) {
619
+ caps = customMemoryCapabilities(this);
620
+ receiverCapabilities.set(this, caps);
621
+ }
622
+ return caps;
615
623
  }
616
624
  nextInode = 1;
617
625
  ledger;
@@ -621,7 +629,6 @@ export class MemoryFileSystem {
621
629
  root;
622
630
  totalBytes = 0;
623
631
  symlinkCount = 0;
624
- #atomicView;
625
632
  _getAtomicView() {
626
633
  const owner = ownedStores.get(this);
627
634
  if (!owner ||
@@ -648,7 +655,27 @@ export class MemoryFileSystem {
648
655
  return undefined;
649
656
  }
650
657
  }
651
- return this.#atomicView ??= {
658
+ return owner.atomicView ??= {
659
+ replaceContents: (source, destination) => {
660
+ const from = this.entry(source, "overlayAtomicView").node;
661
+ const to = this.entry(destination, "overlayAtomicView").node;
662
+ if (from?.type !== "file" || to?.type !== "file" || from === to || from.nlink !== 1)
663
+ this.fail("EINVAL", "overlayAtomicView", destination);
664
+ this.permission(to, 2, "overlayAtomicView", destination);
665
+ // Exchange owned storage, preserving destination inode/hardlinks and
666
+ // retained allocation snapshots. Stage cleanup releases the old contents.
667
+ const allocation = to.allocation, length = to.byteLength;
668
+ to.allocation = from.allocation;
669
+ to.byteLength = from.byteLength;
670
+ from.allocation = allocation;
671
+ from.byteLength = length;
672
+ to.view = undefined;
673
+ from.view = undefined;
674
+ to.sourceRef = undefined;
675
+ from.sourceRef = undefined;
676
+ this.changed(to);
677
+ this.changed(from);
678
+ },
652
679
  stat: (path) => {
653
680
  this.validatePath(path, "overlayAtomicView");
654
681
  let node = this.root;
@@ -2159,13 +2186,14 @@ export class MemoryFileSystem {
2159
2186
  return () => this.verifyDirectoryAncestry(entries, controls);
2160
2187
  }
2161
2188
  async prepareStagingResolution(path, options = {}) {
2189
+ const followFinal = options.followFinalSymlink === true;
2162
2190
  const controls = options.signal === undefined ? {} : { signal: options.signal };
2163
2191
  controls.signal?.throwIfAborted();
2164
2192
  this.validatePath(path, "prepareStagingResolution");
2165
2193
  if (!path.startsWith("/") || normalizePath(path) !== path)
2166
2194
  this.fail("EINVAL", "prepareStagingResolution", path);
2167
2195
  const traversed = [];
2168
- const location = this.resolve(path, "prepareStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: traversed });
2196
+ const location = this.resolve(path, "prepareStagingResolution", { followFinal, allowMissing: true, resolutionSteps: traversed });
2169
2197
  if (location.node && location.node.type !== "file")
2170
2198
  this.fail("ENOTSUP", "prepareStagingResolution", path);
2171
2199
  const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
@@ -2180,7 +2208,7 @@ export class MemoryFileSystem {
2180
2208
  const currentSteps = [];
2181
2209
  let current;
2182
2210
  try {
2183
- current = this.resolve(path, "verifyStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: currentSteps });
2211
+ current = this.resolve(path, "verifyStagingResolution", { followFinal, allowMissing: true, resolutionSteps: currentSteps });
2184
2212
  }
2185
2213
  catch (error) {
2186
2214
  if (error instanceof FsError && ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code))
@@ -2218,6 +2246,7 @@ export class MemoryFileSystem {
2218
2246
  }
2219
2247
  async publishStagedFile(receipt, destination, options) {
2220
2248
  const signal = options.signal;
2249
+ const preserveIdentity = options.preserveIdentity === true;
2221
2250
  const guard = options.commitGuard;
2222
2251
  const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
2223
2252
  const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
@@ -2245,9 +2274,48 @@ export class MemoryFileSystem {
2245
2274
  this.fail("EINVAL", "publishStagedFile", destination);
2246
2275
  if (target.node && target.node.type !== "file")
2247
2276
  this.fail("EAGAIN", "publishStagedFile", destination);
2277
+ if (preserveIdentity) {
2278
+ if (this.capabilities.atomicStagedFileMutation !== true)
2279
+ this.fail("ENOTSUP", "publishStagedFile", destination);
2280
+ if (file.node?.type !== "file" || target.node?.type !== "file" || expected === null)
2281
+ this.fail("EINVAL", "publishStagedFile", destination);
2282
+ this.permission(file.parent, 3, "publishStagedFile", staging.file.path);
2283
+ this.permission(target.parent, 3, "publishStagedFile", destination);
2284
+ this.permission(target.node, 2, "publishStagedFile", destination);
2285
+ // Provider-owned storage is copied directly, without assembling an archive
2286
+ // in caller memory. Preserve retained staging readers and independent writes.
2287
+ this.admitSize(target.node, file.node.byteLength, "publishStagedFile", destination);
2288
+ const allocation = this.allocate(file.node.byteLength, "publishStagedFile", destination);
2289
+ try {
2290
+ for (let offset = 0; offset < file.node.byteLength; offset += 65536) {
2291
+ allocation.data.set(file.node.data.subarray(offset, offset + 65536), offset);
2292
+ }
2293
+ }
2294
+ catch (error) {
2295
+ allocation.release();
2296
+ throw error;
2297
+ }
2298
+ // No awaits or overridable operations occur between admission and commit.
2299
+ const now = Date.now();
2300
+ this.replaceData(target.node, allocation);
2301
+ this.changed(target.node, now);
2302
+ target.node.atimeMs = file.node.atimeMs;
2303
+ target.node.mtimeMs = file.node.mtimeMs;
2304
+ file.parent.entries.delete(file.name);
2305
+ this.ledger.release(file.name.length * 2, 1);
2306
+ file.node.nlink--;
2307
+ file.node.ctimeMs = now;
2308
+ file.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, file.node.revision + 1);
2309
+ this.releaseNode(file.node);
2310
+ this.changed(file.parent, now);
2311
+ memoryCaches.get(this.ledger).clearWrites();
2312
+ return;
2313
+ }
2248
2314
  return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
2249
2315
  }
2250
2316
  async publishStagedFileSet(receipt, destination, options) {
2317
+ if (options.preserveIdentity)
2318
+ this.fail("ENOTSUP", "publishStagedFileSet", destination);
2251
2319
  const signal = options.signal;
2252
2320
  const guard = options.commitGuard;
2253
2321
  const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
@@ -2481,6 +2549,26 @@ export class MemoryFileSystem {
2481
2549
  ownedStats.set(stat, { filesystem: this, path, root: this.root });
2482
2550
  return stat;
2483
2551
  }
2552
+ async *iterateDirectory(path, options = {}) {
2553
+ options.signal?.throwIfAborted();
2554
+ const root = this.root, node = this.resolveNode(path, "iterateDirectory", true);
2555
+ if (node.type !== "directory")
2556
+ this.fail("ENOTDIR", "iterateDirectory", path);
2557
+ this.permission(node, 4, "iterateDirectory", path);
2558
+ const revision = node.revision, ino = node.ino;
2559
+ const check = () => {
2560
+ options.signal?.throwIfAborted();
2561
+ if (this.root !== root || node.ino !== ino || node.revision !== revision || this.resolveNode(path, "iterateDirectory", true) !== node)
2562
+ this.fail("EBUSY", "iterateDirectory", path);
2563
+ this.permission(node, 4, "iterateDirectory", path);
2564
+ };
2565
+ node.atimeMs = Date.now();
2566
+ for (const [name, entry] of node.entries) {
2567
+ check();
2568
+ yield { name, type: entry.type };
2569
+ }
2570
+ check();
2571
+ }
2484
2572
  async readdir(path, options = {}) {
2485
2573
  const limit = directoryEntryLimit(options, path);
2486
2574
  const node = this.resolveNode(path, "readdir", true);
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
4
4
  export interface MountFileSystemOptions {
@@ -18,7 +18,7 @@ export declare class MountFileSystem implements FileSystem {
18
18
  private supportsStagingAncestry;
19
19
  private supportsDirectoryValidation;
20
20
  prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
21
- prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
21
+ prepareStagingResolution(path: string, options?: PrepareStagingResolutionOptions): Promise<FileStagingResolution>;
22
22
  private prepareAncestry;
23
23
  private protected;
24
24
  openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
@@ -40,6 +40,7 @@ export declare class MountFileSystem implements FileSystem {
40
40
  stat(path: string, options?: FsOptions): Promise<FileStat>;
41
41
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
42
42
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
43
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
43
44
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
44
45
  rmdir(path: string, options?: FsOptions): Promise<void>;
45
46
  unlink(path: string, options?: FsOptions): Promise<void>;
@@ -55,6 +56,7 @@ export declare class MountFileSystem implements FileSystem {
55
56
  removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
56
57
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
57
58
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
59
+ publishStagedFileSet(staging: FileStaging, destination: string, supplied: PublishStagedFileSetOptions): Promise<FileStat>;
58
60
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
59
61
  prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
60
62
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
@@ -109,6 +109,7 @@ export class MountFileSystem {
109
109
  retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
110
110
  synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
111
111
  trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
112
+ atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
112
113
  atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
113
114
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
114
115
  descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
@@ -128,7 +129,7 @@ export class MountFileSystem {
128
129
  return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
129
130
  };
130
131
  const semantics = Object.fromEntries([
131
- "guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt",
132
+ "guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt", "atomicStagedFileMutation",
132
133
  "trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
133
134
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
134
135
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
@@ -190,7 +191,8 @@ export class MountFileSystem {
190
191
  fail("EINVAL");
191
192
  const location = await this.resolve(path, options, {
192
193
  allowMissing: options.create ?? true,
193
- followFinal: options.stagingResolution !== true && (options.create !== undefined || options.creation !== "exclusive"),
194
+ retainedResolution: options.stagingResolution === true && options.followFinalSymlink === true,
195
+ followFinal: options.stagingResolution === true ? options.followFinalSymlink === true : options.create !== undefined || options.creation !== "exclusive",
194
196
  ...(options.create === undefined ? {} : { resizeCreate: options.create }),
195
197
  });
196
198
  const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
@@ -211,15 +213,15 @@ export class MountFileSystem {
211
213
  const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
212
214
  const declared = input === location.local ? resize : ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(input, options) ?? location.mount.backend.capabilities);
213
215
  options.signal?.throwIfAborted();
214
- resolution = declared.synchronousStagingResolution === true && await this.supportsStagingAncestry(location, resize, options);
216
+ resolution = declared.synchronousStagingResolution === true && (options.followFinalSymlink !== true || declared.synchronousFollowedStagingResolution === true) && await this.supportsStagingAncestry(location, resize, options);
215
217
  }
216
218
  }
217
219
  const conditionalChmod = options.conditionalChmod === true
218
220
  ? resize.conditionalChmod === true && typeof location.mount.backend.chmod === "function"
219
221
  && await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
220
222
  : this.capabilities.conditionalChmod;
221
- const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, ...ordinary } = resize;
222
- const withOpen = { ...ordinary, retainedStagingWrite: false, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
223
+ const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, synchronousFollowedStagingResolution: ignoredFollowedResolution, ...ordinary } = resize;
224
+ const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution, ...(options.followFinalSymlink === true ? { synchronousFollowedStagingResolution: resolution } : {}) }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
223
225
  const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
224
226
  : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
225
227
  if (location.synthetic)
@@ -263,19 +265,19 @@ export class MountFileSystem {
263
265
  return this.operation("prepareDirectoryAncestry", ancestors.at(-1)?.path ?? "/", options, () => this.prepareAncestry(ancestors, options));
264
266
  }
265
267
  prepareStagingResolution(path, options = {}) {
266
- const controls = options.signal === undefined ? {} : { signal: options.signal };
268
+ const controls = { ...(options.signal === undefined ? {} : { signal: options.signal }), ...(options.followFinalSymlink === true ? { followFinalSymlink: true } : {}) };
267
269
  return this.operation("prepareStagingResolution", path, controls, async () => {
268
270
  if (normalizePath(globalPath(path)) !== path)
269
271
  fail("EINVAL");
270
272
  const observed = [];
271
- const location = await this.resolve(path, controls, { followFinal: false, allowMissing: true, resolutionSteps: observed });
273
+ const location = await this.resolve(path, controls, { followFinal: controls.followFinalSymlink === true, retainedResolution: controls.followFinalSymlink === true, allowMissing: true, resolutionSteps: observed });
272
274
  if (location.synthetic || !within(location.mount.path, path))
273
275
  fail("ENOTSUP");
274
276
  const backend = location.mount.backend;
275
277
  const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
276
278
  const declared = ownedMutationCapabilities(backend, await backend.capabilitiesFor?.(input, { ...controls, stagingResolution: true }) ?? backend.capabilities);
277
279
  controls.signal?.throwIfAborted();
278
- if (declared.synchronousStagingResolution !== true || !backend.prepareStagingResolution)
280
+ if (declared.synchronousStagingResolution !== true || controls.followFinalSymlink === true && declared.synchronousFollowedStagingResolution !== true || !backend.prepareStagingResolution)
279
281
  fail("ENOTSUP");
280
282
  const prefix = directoryAncestryPaths(location.mount.path).map(ancestor => {
281
283
  const entry = observed.find(step => step.path === ancestor);
@@ -483,7 +485,7 @@ export class MountFileSystem {
483
485
  release = await this.acquireNamespace(options, [
484
486
  "rename", "symlink", "link", "unlink", "rm", "rmdir",
485
487
  "removeEntryConditional", "removeFileConditional", "removeTreeConditional",
486
- "publishStagedFile", "removeStagedFile",
488
+ "publishStagedFile", "publishStagedFileSet", "removeStagedFile",
487
489
  ].includes(syscall));
488
490
  const result = await action();
489
491
  if (!preserveReceipt)
@@ -545,6 +547,10 @@ export class MountFileSystem {
545
547
  let followedFinalLink = false;
546
548
  const verified = async (location) => {
547
549
  let inspection = !location.stat && settings.resizeCreate === true ? creationVerification : verification;
550
+ // A guarded resolution binds the original link chain and missing target
551
+ // through the authoritative backend receipt; realpath cannot name absence.
552
+ if (settings.retainedResolution && !location.stat && followedFinalLink)
553
+ inspection = undefined;
548
554
  // A missing entry has no realpath. Verify its existing parent while
549
555
  // leaving dangling final symlinks on the ordinary following path.
550
556
  if (inspection && !location.stat && settings.allowMissing && !followedFinalLink && inspection.finalName === undefined) {
@@ -802,6 +808,76 @@ export class MountFileSystem {
802
808
  return [...entries.values()].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
803
809
  });
804
810
  }
811
+ async *iterateDirectory(path, options = {}) {
812
+ try {
813
+ const location = await this.operation("iterateDirectory", path, options, async () => {
814
+ const resolved = await this.resolve(path, options);
815
+ if (resolved.stat?.type !== "directory")
816
+ fail("ENOTDIR");
817
+ return resolved;
818
+ });
819
+ // Only configured mount children are retained, never backend entries.
820
+ const mounted = new Set();
821
+ for (const mount of this.mounts) {
822
+ if (mount.path !== location.path && within(location.path, mount.path)) {
823
+ const suffix = mount.path.slice(location.path === "/" ? 1 : location.path.length + 1);
824
+ mounted.add(suffix.split("/")[0]);
825
+ }
826
+ }
827
+ const admit = async () => {
828
+ const current = await this.resolve(path, options);
829
+ if (current.path !== location.path || current.mount !== location.mount ||
830
+ current.local !== location.local || current.synthetic !== location.synthetic ||
831
+ current.stat?.type !== "directory")
832
+ fail("EBUSY");
833
+ };
834
+ if (!location.synthetic) {
835
+ const iterator = await this.operation("iterateDirectory", path, options, async () => {
836
+ await admit();
837
+ const backend = location.mount.backend;
838
+ if (!backend.iterateDirectory)
839
+ fail("ENOTSUP");
840
+ return backend.iterateDirectory(location.local, options)[Symbol.asyncIterator]();
841
+ }, undefined, true); // Transfer ownership before checking cancellation on the first pull.
842
+ // Admission covers each backend pull, but never a consumer pause. In
843
+ // particular, caller-backed traversal may publish scratch files between pulls.
844
+ let complete = false, failed = false;
845
+ try {
846
+ while (true) {
847
+ const result = await this.operation("iterateDirectory", path, options, async () => {
848
+ await admit();
849
+ return iterator.next();
850
+ });
851
+ if (result.done) {
852
+ complete = true;
853
+ break;
854
+ }
855
+ const { name, type } = result.value;
856
+ if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\0"))
857
+ fail("EIO");
858
+ if (!mounted.has(name))
859
+ yield { name, type };
860
+ }
861
+ }
862
+ catch (error) {
863
+ failed = true;
864
+ throw error;
865
+ }
866
+ finally {
867
+ if (!complete)
868
+ await finishCleanup(() => iterator.return?.(), failed);
869
+ }
870
+ }
871
+ for (const name of mounted) {
872
+ await this.operation("iterateDirectory", path, options, admit);
873
+ yield { name, type: "directory" };
874
+ }
875
+ options.signal?.throwIfAborted();
876
+ }
877
+ catch (error) {
878
+ throw this.error(error, "iterateDirectory", path, options);
879
+ }
880
+ }
805
881
  mkdir(path, options = {}) {
806
882
  return this.operation("mkdir", path, options, async () => {
807
883
  const directories = new Map();
@@ -973,7 +1049,11 @@ export class MountFileSystem {
973
1049
  return Object.freeze(staging);
974
1050
  const backendCleanup = receipt.cleanup;
975
1051
  const cleanup = createStagingCleanup(staging.directory.path, controls => this.operation("removeStagedFile", staging.directory.path, controls, () => backendCleanup.remove(controls), undefined, true), () => backendCleanup.close());
976
- return Object.freeze({ ...staging, cleanup });
1052
+ const writer = receipt.writer;
1053
+ return Object.freeze({ ...staging, cleanup, ...(writer ? { writer: Object.freeze({
1054
+ write: (bytes, controls = {}) => this.operation("stagedWrite", staging.file.path, controls, () => writer.write(bytes, controls), undefined, true),
1055
+ finish: (controls = {}) => this.operation("stagedFinish", staging.file.path, controls, async () => snapshotStat(await writer.finish(controls)), undefined, true),
1056
+ }) } : {}) });
977
1057
  }
978
1058
  catch (error) {
979
1059
  await finishCleanup(() => receipt.cleanup?.close(), true);
@@ -1022,6 +1102,8 @@ export class MountFileSystem {
1022
1102
  await requireOwnedMutation(local.mount.backend, target.local, "atomicFileStaging", options, options.destination === null);
1023
1103
  if (!local.mount.backend.publishStagedFile)
1024
1104
  fail("ENOTSUP");
1105
+ if (options.preserveIdentity)
1106
+ await requireOwnedMutation(local.mount.backend, target.local, "atomicStagedFileMutation", options);
1025
1107
  if (ancestors || callerGuard !== undefined)
1026
1108
  await requireOwnedMutation(local.mount.backend, target.local, "guardedStagingPublication", options, options.destination === null);
1027
1109
  if (!ancestors) {
@@ -1065,6 +1147,83 @@ export class MountFileSystem {
1065
1147
  });
1066
1148
  }, destination, true);
1067
1149
  }
1150
+ async publishStagedFileSet(staging, destination, supplied) {
1151
+ const entry = (value) => ({ path: value.path, stat: snapshotStat(value.stat) });
1152
+ staging = { parent: entry(staging.parent), directory: entry(staging.directory), file: entry(staging.file) };
1153
+ const options = {
1154
+ ...supplied, parent: snapshotStat(supplied.parent),
1155
+ destination: supplied.destination === null ? null : snapshotStat(supplied.destination),
1156
+ companions: supplied.companions.map(value => ({ ...value, expected: value.expected === null ? null : snapshotStat(value.expected) })),
1157
+ ...(supplied.ancestors === undefined ? {} : { ancestors: snapshotDirectoryAncestry(supplied.ancestors) }),
1158
+ };
1159
+ return this.operation("publishStagedFileSet", staging.file.path, options, async () => {
1160
+ validatePath(destination);
1161
+ if (normalizePath(globalPath(destination)) !== destination || destination === "/")
1162
+ fail("EINVAL");
1163
+ const parent = destination.slice(0, destination.lastIndexOf("/")) || "/";
1164
+ const paths = new Set([destination]);
1165
+ for (const companion of options.companions) {
1166
+ validatePath(companion.path);
1167
+ if (normalizePath(globalPath(companion.path)) !== companion.path || paths.has(companion.path) ||
1168
+ (companion.path.slice(0, companion.path.lastIndexOf("/")) || "/") !== parent)
1169
+ fail("EINVAL");
1170
+ paths.add(companion.path);
1171
+ }
1172
+ const ancestors = options.ancestors;
1173
+ if (ancestors && ancestors.at(-1)?.path !== parent)
1174
+ fail("EINVAL");
1175
+ const guard = ancestors ? await this.prepareAncestry(ancestors, options) : undefined;
1176
+ const local = await this.localStaging(staging, options);
1177
+ const target = await this.resolve(destination, options, { followFinal: false, entry: true, allowMissing: true });
1178
+ if (this.protected(target.path))
1179
+ fail("EBUSY");
1180
+ if (local.mount !== target.mount)
1181
+ fail("EXDEV");
1182
+ if (ancestors && target.path !== destination)
1183
+ fail("EAGAIN");
1184
+ this.mutable(target);
1185
+ const backend = local.mount.backend;
1186
+ if (!backend.publishStagedFileSet)
1187
+ fail("ENOTSUP");
1188
+ await requireOwnedMutation(backend, local.staging.directory.path, "atomicFileStaging", options);
1189
+ await requireOwnedMutation(backend, target.local, "atomicFileStaging", options, options.destination === null);
1190
+ const companions = [];
1191
+ for (const companion of options.companions) {
1192
+ const resolved = await this.resolve(companion.path, options, { followFinal: false, entry: true, allowMissing: true });
1193
+ if (this.protected(resolved.path))
1194
+ fail("EBUSY");
1195
+ if (resolved.mount !== target.mount)
1196
+ fail("EXDEV");
1197
+ this.mutable(resolved);
1198
+ companions.push({ ...companion, path: resolved.local });
1199
+ }
1200
+ let localAncestors;
1201
+ if (ancestors) {
1202
+ const first = ancestors.findIndex(value => value.path === target.mount.path);
1203
+ if (first < 0)
1204
+ fail("EINVAL");
1205
+ localAncestors = ancestors.slice(first).map(value => ({
1206
+ path: target.mount.path === "/" ? value.path : value.path.slice(target.mount.path.length) || "/", stat: value.stat,
1207
+ }));
1208
+ }
1209
+ const callerGuard = options.commitGuard;
1210
+ if (guard || callerGuard !== undefined)
1211
+ await requireOwnedMutation(backend, target.local, "guardedStagingPublication", options, options.destination === null);
1212
+ // One backend performs all version checks and the indivisible commit.
1213
+ // The namespace lock covers admission through its committed receipt.
1214
+ return snapshotStat(await backend.publishStagedFileSet(local.staging, target.local, {
1215
+ ...options, companions,
1216
+ ...(localAncestors === undefined ? {} : { ancestors: localAncestors }),
1217
+ ...(guard || callerGuard !== undefined ? { commitGuard: () => {
1218
+ if (callerGuard !== undefined)
1219
+ runStagingGuard(callerGuard);
1220
+ if (guard)
1221
+ runStagingGuard(guard);
1222
+ return true;
1223
+ } } : {}),
1224
+ }));
1225
+ }, destination, true);
1226
+ }
1068
1227
  removeStagedFile(staging, options = {}) {
1069
1228
  return this.operation("removeStagedFile", staging.directory.path, options, async () => {
1070
1229
  const local = await this.localStaging(staging, options);
@@ -43,6 +43,7 @@ export declare class OverlayFileSystem implements FileSystem {
43
43
  private bytes;
44
44
  private cleanGarbage;
45
45
  cleanup(options?: FsOptions): Promise<void>;
46
+ private stagingOptions;
46
47
  private staged;
47
48
  private preserve;
48
49
  private clone;
@@ -64,6 +65,7 @@ export declare class OverlayFileSystem implements FileSystem {
64
65
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
65
66
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
66
67
  readdir(path: string, options?: ReadDirectoryOptions): Promise<DirectoryEntry[]>;
68
+ iterateDirectory(path: string, options?: FsOptions): AsyncIterable<DirectoryEntry>;
67
69
  realpath(path: string, options?: FsOptions): Promise<string>;
68
70
  access(path: string, requestedMode?: number, options?: FsOptions): Promise<void>;
69
71
  mkdir(path: string, options?: MkdirOptions): Promise<void>;
@@ -82,6 +84,7 @@ export declare class OverlayFileSystem implements FileSystem {
82
84
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
83
85
  private bounded;
84
86
  private streamToUpper;
87
+ private entryBytes;
85
88
  writeStream(path: string, source: ByteSource, options?: WriteFileOptions): Promise<void>;
86
89
  }
87
90
  export declare function createOverlayFileSystem(options: OverlayFileSystemOptions): OverlayFileSystem;