@poe-platform/safe-fs 0.1.803 → 0.1.805

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 (40) hide show
  1. package/README.md +22 -4
  2. package/dist/safe-fs/bridge/filesystem.js +1 -1
  3. package/dist/safe-fs/contracts/errors.d.ts +1 -0
  4. package/dist/safe-fs/contracts/errors.js +16 -3
  5. package/dist/safe-fs/contracts/filesystem.d.ts +3 -2
  6. package/dist/safe-fs/contracts/io.d.ts +2 -0
  7. package/dist/safe-fs/contracts/io.js +32 -7
  8. package/dist/safe-fs/contracts/portable-path.d.ts +8 -0
  9. package/dist/safe-fs/contracts/portable-path.js +22 -1
  10. package/dist/safe-fs/core.d.ts +3 -14
  11. package/dist/safe-fs/core.js +3 -11
  12. package/dist/safe-fs/fs/devices/index.d.ts +6 -1
  13. package/dist/safe-fs/fs/devices/index.js +40 -24
  14. package/dist/safe-fs/fs/devices/path.js +1 -1
  15. package/dist/safe-fs/fs/directory-admission.js +2 -2
  16. package/dist/safe-fs/fs/memory/atomic-view.d.ts +3 -0
  17. package/dist/safe-fs/fs/memory/atomic-view.js +5 -0
  18. package/dist/safe-fs/fs/memory/index.d.ts +6 -3
  19. package/dist/safe-fs/fs/memory/index.js +147 -131
  20. package/dist/safe-fs/fs/memory/ledger.js +3 -1
  21. package/dist/safe-fs/fs/memory/limits.js +3 -0
  22. package/dist/safe-fs/fs/mount/comparison.js +3 -3
  23. package/dist/safe-fs/fs/object-publication/index.js +2 -1
  24. package/dist/safe-fs/fs/readonly/index.js +1 -1
  25. package/dist/safe-fs/fs/real/index.js +3 -2
  26. package/dist/safe-fs/fs/s3/filesystem.js +1 -1
  27. package/dist/safe-fs/fs/s3/http/request-fetch.d.ts +14 -3
  28. package/dist/safe-fs/fs/s3/http/request-node.d.ts +2 -2
  29. package/dist/safe-fs/fs/s3/http/types.d.ts +2 -2
  30. package/dist/safe-fs/fs/s3/manifest-admission.js +0 -4
  31. package/dist/safe-fs/fs/s3/namespace.js +2 -4
  32. package/dist/safe-fs/fs/webdav/webdav.js +5 -29
  33. package/dist/safe-fs/fs/webdav/xml.js +2 -2
  34. package/dist/safe-fs/python/emscripten.js +5 -2
  35. package/dist/safe-fs/python/filesystem.js +2 -2
  36. package/dist/safe-fs/python/native.js +1 -1
  37. package/dist/safe-fs/python/stat.js +5 -2
  38. package/dist/safe-fs/runtime-core.d.ts +26 -0
  39. package/dist/safe-fs/runtime-core.js +56 -0
  40. package/package.json +16 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # safe-fs
2
2
 
3
- An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges. Portable filesystem types and POSIX helpers compile without Node types.
3
+ An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges. Portable filesystem types and POSIX helpers compile without Node types. The `posixPath` export from `@poe-code/safe-fs/core` provides `basename`, `dirname`, `extname`, `join`, `isAbsolute`, `normalize`, `resolve`, `relative`, `sep`, and `delimiter`; `resolve` and `relative` use `/` as their working directory in every runtime.
4
4
 
5
5
  Under `workerd` and `browser` conditions, the root export includes portable filesystems, `S3FileSystem`, and `MockS3Client`. Supply an S3 transport to use object storage without Node globals.
6
6
 
@@ -86,14 +86,26 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
86
86
 
87
87
  All required methods must exist, but a backend may reject an operation with `FsError`, such as `EROFS` for a write or `ENOTSUP` for unsupported semantics. Check optional methods and `capabilities` rather than assuming every backend behaves like a local disk. Errors expose `code` and may include `syscall`, `path`, `dest`, and `cause`.
88
88
 
89
+ `readBytes` supports frozen host abort signals. The exported
90
+ `listenForAbort(signal, listener)` returns a cleanup function for custom
91
+ operations; if frozen host bookkeeping prevents registration, callers must
92
+ check cancellation at operation boundaries, as `readBytes` does.
93
+
89
94
  `createDeviceFileSystem(fs)` adds portable `/dev/null` whole-file, stream, and descriptor I/O. Descriptor reads return EOF and writes discard bytes; stat remains a zero-size character device and descriptor position remains zero. Truncating opens are accepted, exclusive creation fails with `EEXIST`, and descriptor resizing and synchronization are unsupported. Access modes, cancellation, and closed handles use the normal descriptor checks. With an authoritative object store, use `createDeviceFileSystem(withObjectFileDescriptors(fs, store))` so null-device I/O never acquires or publishes an object version; ordinary files retain conditional publication. The device wrapper must be outermost for this composition.
90
95
 
96
+ `PythonStatTranslator` and `createPythonRuntimeStatMapper` allocate stable guest device
97
+ identities without a default device-count budget. Their optional `maxDevices` accepts
98
+ `Infinity` or a positive safe integer. Explicit budgets remain enforced; device numbers
99
+ must still fit the disjoint application/runtime ranges of the uint32 Python stat ABI.
100
+
91
101
  `withObjectFileDescriptors(fs, store)` supports large shell and Python descriptor
92
102
  writes when the host supplies `store.createStaging`: private externally backed
93
103
  pages keep working memory bounded without publishing the growing file after
94
104
  every write. Conditional publication still occurs at sync/close, and retained
95
105
  readers keep their old versions. Without that optional backend primitive, the
96
- optional dirty-page budget limits unflushed output only when configured. See the
106
+ optional dirty-page budget limits unflushed output only when configured. Descriptor
107
+ resource limits accept `Infinity` and are unlimited when omitted; `chunkBytes`
108
+ remains a positive finite integer. See the
97
109
  [object descriptor and spill contract](src/contracts/object-publication.md) for
98
110
  backend methods, failure semantics and qualification; no provider storage is
99
111
  configured automatically.
@@ -270,7 +282,8 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
270
282
 
271
283
  | Operation | Options |
272
284
  | --- | --- |
273
- | `readFile` | `maxBytes`: upper bound on collected bytes |
285
+ | `readFile` | `maxBytes`: nonnegative safe-integer byte cap; `Infinity` adds no per-read cap |
286
+ | `readdir` | `maxEntries`: nonnegative safe-integer entry cap; omission or `Infinity` preserves backend limits and ordering |
274
287
  | `writeFile`, `writeStream` | `flag`: `w` (default, replace), `wx` (exclusive create), `a` (append), or `ax` (exclusive append/create); optional `mode` |
275
288
  | `appendFile` | Optional `mode` |
276
289
  | `mkdir` | `recursive` (default false); optional `mode` |
@@ -278,7 +291,7 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
278
291
  | `copyFile` | `exclusive` (default false) |
279
292
  | `readStream` | `start` (default 0), `endExclusive` (default end of file), `chunkSize` (built-in default 64 KiB) |
280
293
 
281
- `validatePath(path, { maxPathBytes, maxPathComponents })` accepts optional UTF-8 byte and component quotas; both default to `Infinity`. Memory accepts the same constructor options and counts cumulative symlink expansion. Device operations accept these quotas under `pathLimits`. WebDAV accepts `maxTimestampPropertyBytes` for timestamp metadata, also unlimited by default.
294
+ `validatePath(path, { maxPathBytes, maxPathComponents })` accepts optional UTF-8 byte and component quotas; both default to `Infinity`. Memory accepts the same constructor options and counts cumulative symlink expansion. Device operations and WebDAV stat, access, and writes accept these quotas under `pathLimits`. WebDAV accepts `maxTimestampPropertyBytes` for timestamp metadata, also unlimited by default.
282
295
 
283
296
  `access` takes a separate mode bitmask from `ACCESS_MODES`. `chmod` takes a mode, `utimes` takes millisecond timestamps, and `truncate` takes a byte length (default 0). For conditional chmod, check `capabilitiesFor(path, { conditionalChmod: true })` (or `capabilities`) and require `conditionalChmod: true`; supply `parent`, `expected`, and complete root-to-parent `ancestors` together. An optional mutation-free `commitGuard` must return literal `true` synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the [bridge signatures](src/bridge/filesystem.ts).
284
297
 
@@ -289,6 +302,11 @@ Every raw filesystem operation accepts an optional `signal`. Additional fields a
289
302
 
290
303
  `S3FileSystem` requires `transport` and `bucket`. A transport supplies `headObject`, `getObject`, `putObject`, `deleteObject`, `copyObject`, and `listObjectsV2`; see the [transport interface](src/fs/s3/transport.ts).
291
304
 
305
+ `createS3NamespaceFileSystem` resource limits (`maxBytes`, `maxEntries`,
306
+ `maxManifestBytes`, `maxAttempts`, and inherited descriptor limits) are unlimited
307
+ when omitted or set to `Infinity`; positive safe integers enable finite limits.
308
+ Namespace paths have no implicit length cap; configured manifest budgets still apply.
309
+
292
310
  `createS3Transport(client, capabilities?)` wraps a compatible client with explicit `streamingRead`, `streamingWrite`, `conditionalPut`, `conditionalCopy`, and `conditionalDelete` flags (all absent by default). Direct transport requests accept an optional `abortSignal`.
293
311
 
294
312
  For in-memory S3 simulations, `new MockS3Client({ buckets, pageSize?, now?, authorize? })` requires bucket names. `pageSize` defaults to 1,000 (range 1–1,000), `now` defaults to the current date/time, and optional `authorize(request)` can reject requests. It does not verify a real provider.
@@ -70,7 +70,7 @@ export class FileSystemBridge {
70
70
  assertBridgePath(this.#root, this.#cwd);
71
71
  this.#signal = options.signal;
72
72
  const maximum = options.readFileMaxBytes;
73
- if (maximum !== undefined && (!Number.isSafeInteger(maximum) || maximum < 0))
73
+ if (maximum !== undefined && maximum !== Infinity && (!Number.isSafeInteger(maximum) || maximum < 0))
74
74
  throw new TypeError("Invalid readFileMaxBytes");
75
75
  this.#readFileMaxBytes = maximum;
76
76
  if (options.reserveReadFile !== undefined && typeof options.reserveReadFile !== "function")
@@ -40,6 +40,7 @@ export interface FsErrorOptions extends ErrorOptions {
40
40
  readonly message?: string;
41
41
  }
42
42
  export declare class FsError extends Error {
43
+ static [Symbol.hasInstance](value: unknown): boolean;
43
44
  readonly code: ErrnoCode;
44
45
  readonly errno: PlatformErrno;
45
46
  readonly syscall?: string;
@@ -31,7 +31,18 @@ const descriptions = {
31
31
  ETIMEDOUT: "operation timed out",
32
32
  EXDEV: "cross-device link not permitted",
33
33
  };
34
+ const FS_ERROR_BRAND = Symbol.for("@poe-code/safe-fs.FsError");
34
35
  export class FsError extends Error {
36
+ static [Symbol.hasInstance](value) {
37
+ if (typeof value !== "object" || value === null)
38
+ return false;
39
+ if (this !== FsError)
40
+ return Function.prototype[Symbol.hasInstance].call(this, value);
41
+ if (Function.prototype[Symbol.hasInstance].call(FsError, value))
42
+ return true;
43
+ const record = value;
44
+ return record[FS_ERROR_BRAND] === true && isErrnoCode(record.code);
45
+ }
35
46
  code;
36
47
  errno;
37
48
  syscall;
@@ -44,14 +55,16 @@ export class FsError extends Error {
44
55
  const operation = options.syscall ? `, ${options.syscall}` : "";
45
56
  const path = options.path === undefined ? "" : ` '${options.path}'`;
46
57
  const destination = options.dest === undefined ? "" : ` -> '${options.dest}'`;
47
- const prevStackLimit = Error.stackTraceLimit;
48
- Error.stackTraceLimit = 0;
58
+ const errorConstructor = Error;
59
+ const prevStackLimit = errorConstructor.stackTraceLimit;
60
+ errorConstructor.stackTraceLimit = 0;
49
61
  try {
50
62
  super(`${code}: ${options.message ?? descriptions[code]}${operation}${path}${destination}`, options);
51
63
  }
52
64
  finally {
53
- Error.stackTraceLimit = prevStackLimit;
65
+ errorConstructor.stackTraceLimit = prevStackLimit;
54
66
  }
67
+ Object.defineProperty(this, FS_ERROR_BRAND, { value: true, enumerable: false, configurable: false, writable: false });
55
68
  this.name = "FsError";
56
69
  this.code = code;
57
70
  this.errno = errno;
@@ -170,13 +170,14 @@ export interface RenameOptions extends FsOptions {
170
170
  readonly noReplace?: boolean;
171
171
  }
172
172
  export interface ReadFileOptions extends FsOptions {
173
+ /** Nonnegative safe-integer byte cap; Infinity adds no per-read cap. */
173
174
  readonly maxBytes?: number;
174
175
  }
175
176
  export interface ReadDirectoryOptions extends FsOptions {
176
177
  /**
177
- * Optional per-listing entry admission limit, a nonnegative safe integer.
178
+ * Optional per-listing entry admission limit, a nonnegative safe integer or Infinity.
178
179
  * Zero permits an empty listing; overflow rejects with EFBIG, never truncates.
179
- * Omission preserves the adapter's existing limits and ordering. Composed
180
+ * Omission or Infinity preserves the adapter's existing limits and ordering. Composed
180
181
  * adapters may conservatively count distinct candidates before visibility
181
182
  * filtering. This is not a global traversal, host-allocation or work quota.
182
183
  */
@@ -7,4 +7,6 @@ export interface CollectOptions {
7
7
  }
8
8
  export declare function toByteSource(input: string | Uint8Array): ByteSource;
9
9
  export declare function collectBytes(source: ByteSource, options: CollectOptions): Promise<Uint8Array>;
10
+ /** Subscribe when supported; callers must still check cancellation at operation boundaries. */
11
+ export declare function listenForAbort(signal: AbortSignal, listener: () => void): () => void;
10
12
  export declare function readBytes(source: ByteSource, signal?: AbortSignal): AsyncGenerator<Uint8Array>;
@@ -108,27 +108,52 @@ export async function collectBytes(source, options) {
108
108
  options.signal?.throwIfAborted();
109
109
  return buffer.subarray(0, size);
110
110
  }
111
+ /** Subscribe when supported; callers must still check cancellation at operation boundaries. */
112
+ export function listenForAbort(signal, listener) {
113
+ let callback = listener;
114
+ const onAbort = () => callback?.();
115
+ const dispose = () => {
116
+ // Release operation state even when a frozen host refuses removal.
117
+ callback = undefined;
118
+ try {
119
+ signal.removeEventListener("abort", onAbort);
120
+ }
121
+ catch (error) {
122
+ if (!(error instanceof TypeError) || Object.isExtensible(signal))
123
+ throw error;
124
+ }
125
+ };
126
+ try {
127
+ signal.addEventListener("abort", onAbort, { once: true });
128
+ }
129
+ catch (error) {
130
+ // Frozen hosts may register before failing to update their bookkeeping.
131
+ dispose();
132
+ if (!(error instanceof TypeError) || Object.isExtensible(signal))
133
+ throw error;
134
+ }
135
+ return dispose;
136
+ }
111
137
  async function abortable(operation, signal) {
112
138
  signal?.throwIfAborted();
113
139
  if (!signal)
114
140
  return operation();
115
141
  return new Promise((resolve, reject) => {
116
- const onAbort = () => {
117
- signal.removeEventListener("abort", onAbort);
142
+ const dispose = listenForAbort(signal, () => {
143
+ dispose();
118
144
  reject(signal.reason);
119
- };
120
- signal.addEventListener("abort", onAbort, { once: true });
145
+ });
121
146
  try {
122
147
  Promise.resolve(operation()).then((result) => {
123
- signal.removeEventListener("abort", onAbort);
148
+ dispose();
124
149
  resolve(result);
125
150
  }, (error) => {
126
- signal.removeEventListener("abort", onAbort);
151
+ dispose();
127
152
  reject(error);
128
153
  });
129
154
  }
130
155
  catch (error) {
131
- signal.removeEventListener("abort", onAbort);
156
+ dispose();
132
157
  reject(error);
133
158
  }
134
159
  });
@@ -4,10 +4,18 @@ export declare const dirname: typeof virtualDirname;
4
4
  export declare function extname(path: string): string;
5
5
  export declare function isAbsolutePath(path: string): boolean;
6
6
  export declare function joinPath(...paths: string[]): string;
7
+ declare function resolve(...paths: string[]): string;
8
+ declare function relative(from: string, to: string): string;
7
9
  export declare const posixPath: Readonly<{
8
10
  basename: typeof basename;
9
11
  dirname: typeof virtualDirname;
10
12
  extname: typeof extname;
11
13
  join: typeof joinPath;
12
14
  isAbsolute: typeof isAbsolutePath;
15
+ normalize: typeof joinPath;
16
+ resolve: typeof resolve;
17
+ relative: typeof relative;
18
+ sep: "/";
19
+ delimiter: ":";
13
20
  }>;
21
+ export {};
@@ -54,4 +54,25 @@ export function joinPath(...paths) {
54
54
  const result = `${absolute ? "/" : ""}${components.join("/")}` || ".";
55
55
  return joined.endsWith("/") && result !== "/" ? `${result}/` : result;
56
56
  }
57
- export const posixPath = Object.freeze({ basename, dirname, extname, join: joinPath, isAbsolute: isAbsolutePath });
57
+ // Portable paths have a stable root instead of a process working directory.
58
+ function resolve(...paths) {
59
+ let joined = "/";
60
+ for (const path of paths) {
61
+ assertString(path);
62
+ joined = path.startsWith("/") ? path : `${joined}/${path}`;
63
+ }
64
+ const normalized = joinPath(joined);
65
+ return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
66
+ }
67
+ function relative(from, to) {
68
+ const source = resolve(from).split("/").filter(Boolean);
69
+ const target = resolve(to).split("/").filter(Boolean);
70
+ let common = 0;
71
+ while (common < source.length && common < target.length && source[common] === target[common])
72
+ common++;
73
+ return [...Array(source.length - common).fill(".."), ...target.slice(common)].join("/");
74
+ }
75
+ export const posixPath = Object.freeze({
76
+ basename, dirname, extname, join: joinPath, isAbsolute: isAbsolutePath,
77
+ normalize: joinPath, resolve, relative, sep: "/", delimiter: ":"
78
+ });
@@ -1,27 +1,16 @@
1
- export * from "./contracts/errors.js";
2
- export * from "./contracts/filesystem.js";
3
- export * from "./contracts/io.js";
4
- export { openFileDescriptor } from "./fs/descriptor.js";
5
- export type { DescriptorBackend, DescriptorOpenOptions } from "./fs/descriptor.js";
6
- export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
7
- export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
8
- export * from "./fs/memory/index.js";
9
- export * from "./fs/devices/index.js";
1
+ export * from "./runtime-core.js";
2
+ export { MemoryFileSystem, MemoryRedirectHandle, createMemoryFileSystem, defaultMemoryFileSystemLimits, bindConditionalMutation, getLastReadMemoryFileSourceRef, isCleanAbsolutePath, tryGetMemoryDirectoryEntryNamesSync, tryMkdirMemorySync, tryOpenMemoryRedirectHandleSync, tryReadMemoryFileViewSync, tryResolveMemoryDevicePath, tryRmRfMemorySync, tryWriteMemoryFileInDirSync, tryWriteMemoryFileSync, utf8ByteLength } from "./fs/memory/index.js";
10
3
  export * from "./fs/readonly/index.js";
11
4
  export * from "./fs/mount/index.js";
12
5
  export * from "./fs/overlay/index.js";
13
6
  export * from "./fs/quota/index.js";
14
7
  export * from "./fs/object-publication/index.js";
15
- export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
16
- export type { RetainedFileSystemCleanupView, RetainedFileSystemCleanupOptions } from "./fs/scoped.js";
17
8
  export * from "./fs/webdav/index.js";
18
9
  export * from "./bridge/index.js";
19
10
  export * from "./python/index.js";
20
- export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
21
- export type { EntryViewResolver } from "./fs/mount/comparison.js";
22
11
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
23
12
  export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
24
13
  export * from "./contracts/object.js";
25
14
  export { ObjectAuthority } from "./fs/object-authority.js";
26
- export { bindConditionalMutation, type ConditionalMutationBinding } from "./fs/memory/index.js";
27
15
  export * from "./fs/s3/index.js";
16
+ export * from "./fs/s3/http/index.js";
@@ -1,23 +1,15 @@
1
- export * from "./contracts/errors.js";
2
- export * from "./contracts/filesystem.js";
3
- export * from "./contracts/io.js";
4
- export { openFileDescriptor } from "./fs/descriptor.js";
5
- export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
6
- export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
7
- export * from "./fs/memory/index.js";
8
- export * from "./fs/devices/index.js";
1
+ export * from "./runtime-core.js";
2
+ export { MemoryFileSystem, MemoryRedirectHandle, createMemoryFileSystem, defaultMemoryFileSystemLimits, bindConditionalMutation, getLastReadMemoryFileSourceRef, isCleanAbsolutePath, tryGetMemoryDirectoryEntryNamesSync, tryMkdirMemorySync, tryOpenMemoryRedirectHandleSync, tryReadMemoryFileViewSync, tryResolveMemoryDevicePath, tryRmRfMemorySync, tryWriteMemoryFileInDirSync, tryWriteMemoryFileSync, utf8ByteLength } from "./fs/memory/index.js";
9
3
  export * from "./fs/readonly/index.js";
10
4
  export * from "./fs/mount/index.js";
11
5
  export * from "./fs/overlay/index.js";
12
6
  export * from "./fs/quota/index.js";
13
7
  export * from "./fs/object-publication/index.js";
14
- export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
15
8
  export * from "./fs/webdav/index.js";
16
9
  export * from "./bridge/index.js";
17
10
  export * from "./python/index.js";
18
- export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
19
11
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
20
12
  export * from "./contracts/object.js";
21
13
  export { ObjectAuthority } from "./fs/object-authority.js";
22
- export { bindConditionalMutation } from "./fs/memory/index.js";
23
14
  export * from "./fs/s3/index.js";
15
+ export * from "./fs/s3/http/index.js";
@@ -1,15 +1,20 @@
1
1
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
2
  import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
3
3
  import type { ByteSource } from "../../contracts/io.js";
4
+ import { type EntryLocation } from "../mount/comparison.js";
5
+ import { pathNamespace } from "../path-namespace.js";
4
6
  export declare class DeviceFileSystem implements FileSystem {
5
7
  #private;
6
8
  readonly capabilities: FileSystemCapabilities;
9
+ get [pathNamespace](): any;
10
+ _resolveEntryView: (path: string, options: FsOptions) => Promise<EntryLocation>;
11
+ _entryAuthority: () => Promise<"unknown">;
7
12
  constructor(filesystem: FileSystem);
8
13
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
9
14
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
10
15
  stat(path: string, options?: FsOptions): Promise<FileStat>;
11
16
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
12
- compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
17
+ compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../runtime-core.js").EntryComparison>;
13
18
  realpath(path: string, options?: FsOptions): Promise<string>;
14
19
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
15
20
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
@@ -2,9 +2,9 @@ import { snapshotStagingCreation } from "../staging-cleanup.js";
2
2
  import { FsError, isFsError } from "../../contracts/errors.js";
3
3
  import { finishCleanup } from "../../contracts/cleanup.js";
4
4
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
5
- import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
5
+ import { compareEntries } from "../mount/comparison.js";
6
6
  import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
7
- import { tryResolveMemoryDevicePath } from "../memory/index.js";
7
+ import { tryResolveMemoryDevicePath } from "../../runtime-core.js";
8
8
  import { createDeviceYield, deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
9
9
  import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
10
10
  import { pathNamespace } from "../path-namespace.js";
@@ -71,21 +71,46 @@ function nonnegative(value, path) {
71
71
  if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
72
72
  throw new FsError("EINVAL", { path });
73
73
  }
74
+ const sharedUnknownEntryAuthority = async () => "unknown";
74
75
  export class DeviceFileSystem {
75
76
  #filesystem;
76
77
  capabilities;
77
- #nullStat;
78
- #directoryStat;
78
+ #identityScopeObj;
79
+ #nullStatObj;
80
+ #directoryStatObj;
81
+ get [pathNamespace]() {
82
+ return Reflect.get(this.#filesystem, pathNamespace);
83
+ }
84
+ get #nullStat() {
85
+ if (this.#nullStatObj)
86
+ return this.#nullStatObj;
87
+ const identityScope = (this.#identityScopeObj ??= Object.freeze({}));
88
+ return (this.#nullStatObj = Object.freeze({
89
+ type: "character", mode: 0o020666, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
90
+ mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1, preferredIoBlockSize: 4096,
91
+ }));
92
+ }
93
+ get #directoryStat() {
94
+ if (this.#directoryStatObj)
95
+ return this.#directoryStatObj;
96
+ const identityScope = (this.#identityScopeObj ??= Object.freeze({}));
97
+ return (this.#directoryStatObj = Object.freeze({
98
+ type: "directory", mode: 0o040755, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
99
+ mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 2, dev: 0, nlink: 1,
100
+ }));
101
+ }
102
+ _resolveEntryView = async (path, options) => {
103
+ const resolved = await this.#resolve(path, options);
104
+ if (resolved === nullPath || resolved === deviceDirectory) {
105
+ return { filesystem: this, path: resolved, stat: resolved === nullPath ? this.#nullStat : this.#directoryStat, readOnly: false };
106
+ }
107
+ return { filesystem: this.#filesystem, path };
108
+ };
109
+ _entryAuthority = sharedUnknownEntryAuthority;
79
110
  constructor(filesystem) {
80
111
  this.#filesystem = filesystem;
81
- Object.defineProperty(this, pathNamespace, { get: () => Reflect.get(filesystem, pathNamespace) });
82
112
  this.capabilities = globalCapabilities(filesystem);
83
- const identityScope = Object.freeze({});
84
- const stat = { mode: 0o020666, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
85
- mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1 };
86
- this.#nullStat = Object.freeze({ ...stat, type: "character", preferredIoBlockSize: 4096 });
87
- this.#directoryStat = Object.freeze({ ...stat, type: "directory", mode: 0o040755, ino: 2 });
88
- const methods = new Map();
113
+ let methods;
89
114
  const view = new Proxy(this, {
90
115
  set: (_target, property, value) => Reflect.set(filesystem, property, value, filesystem),
91
116
  get(target, property) {
@@ -93,24 +118,14 @@ export class DeviceFileSystem {
93
118
  if (typeof value !== "function" || property === "constructor")
94
119
  return value;
95
120
  const backing = Reflect.get(filesystem, property, filesystem);
96
- const cached = methods.get(property);
121
+ const cached = methods?.get(property);
97
122
  if (cached && cached.backing === backing)
98
123
  return cached.bound;
99
124
  const bound = value.bind(target);
100
- methods.set(property, { backing, bound });
125
+ (methods ??= new Map()).set(property, { backing, bound });
101
126
  return bound;
102
127
  },
103
128
  });
104
- for (const identity of [this, view])
105
- registerEntryView(identity, async (path, options) => {
106
- const resolved = await this.#resolve(path, options);
107
- if (resolved === nullPath || resolved === deviceDirectory)
108
- return { filesystem: this, path: resolved,
109
- stat: resolved === nullPath ? this.#nullStat : this.#directoryStat, readOnly: false };
110
- return { filesystem, path };
111
- });
112
- // This internal view has no identity proof beyond its synthetic stats.
113
- registerEntryAuthority(this, async () => "unknown");
114
129
  return view;
115
130
  }
116
131
  #resolve(path, options, followFinal = true, resizeCreate, traversal) {
@@ -309,7 +324,8 @@ export class DeviceFileSystem {
309
324
  async #readFileSlow(path, options) {
310
325
  const resolved = await this.#resolve(path, options);
311
326
  if (resolved === nullPath) {
312
- nonnegative(options.maxBytes, path);
327
+ if (options.maxBytes !== Infinity)
328
+ nonnegative(options.maxBytes, path);
313
329
  return new Uint8Array();
314
330
  }
315
331
  if (resolved === deviceDirectory)
@@ -1,7 +1,7 @@
1
1
  import { FsError, isFsError } from "../../contracts/errors.js";
2
2
  import { pathByteLength, validatePath } from "../../contracts/virtual-path.js";
3
3
  import { capturePathNamespace, pathNamespace } from "../path-namespace.js";
4
- import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../memory/index.js";
4
+ import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../../runtime-core.js";
5
5
  export const nullPath = "/dev/null";
6
6
  export const deviceDirectory = "/dev";
7
7
  function isNullParts(parts) {
@@ -1,9 +1,9 @@
1
1
  import { FsError } from "../contracts/errors.js";
2
2
  export function directoryEntryLimit(options, path) {
3
3
  options.signal?.throwIfAborted();
4
- const limit = options.maxEntries;
4
+ const limit = options.maxEntries === Infinity ? undefined : options.maxEntries;
5
5
  if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) {
6
- throw new FsError("EINVAL", { syscall: "readdir", path, message: "directory entry limit must be a nonnegative safe integer" });
6
+ throw new FsError("EINVAL", { syscall: "readdir", path, message: "directory entry limit must be a nonnegative safe integer or Infinity" });
7
7
  }
8
8
  return limit;
9
9
  }
@@ -4,5 +4,8 @@ export interface MemoryAtomicView {
4
4
  stat(path: string): FileStat | undefined;
5
5
  names(path: string): readonly string[];
6
6
  }
7
+ type LazyMemoryAtomicView = (this: FileSystem) => MemoryAtomicView | undefined;
7
8
  export declare function registerMemoryAtomicView(fs: FileSystem, view: MemoryAtomicView, intact: () => boolean): void;
9
+ export declare function registerLazyMemoryAtomicView(fs: FileSystem, create: LazyMemoryAtomicView): void;
8
10
  export declare function memoryAtomicView(fs: FileSystem): MemoryAtomicView | undefined;
11
+ export {};
@@ -2,7 +2,12 @@ const views = new WeakMap();
2
2
  export function registerMemoryAtomicView(fs, view, intact) {
3
3
  views.set(fs, { view, intact });
4
4
  }
5
+ export function registerLazyMemoryAtomicView(fs, create) {
6
+ views.set(fs, create);
7
+ }
5
8
  export function memoryAtomicView(fs) {
6
9
  const registered = views.get(fs);
10
+ if (typeof registered === "function")
11
+ return registered.call(fs);
7
12
  return registered?.intact() ? registered.view : undefined;
8
13
  }
@@ -1,5 +1,6 @@
1
1
  import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
+ import { type MemoryAtomicView } from "./atomic-view.js";
3
4
  import { MemoryAllocation, MemoryLedger } from "./ledger.js";
4
5
  import { type MemoryFileSystemOptions } from "./limits.js";
5
6
  export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
@@ -59,9 +60,10 @@ export interface ConditionalMutationBinding {
59
60
  }
60
61
  export declare function bindConditionalMutation<Result>(identityScope: object | symbol | undefined, binding: ConditionalMutationBinding, action: () => Promise<Result>): Promise<Result>;
61
62
  export declare class MemoryFileSystem implements FileSystem {
63
+ #private;
62
64
  capabilitiesFor?: NonNullable<FileSystem["capabilitiesFor"]>;
63
- readonly capabilities: FileSystemCapabilities;
64
- private readonly identityScope;
65
+ private get identityScope();
66
+ get capabilities(): FileSystemCapabilities;
65
67
  private nextInode;
66
68
  private readonly ledger;
67
69
  readonly _cache: MemoryCache;
@@ -70,6 +72,7 @@ export declare class MemoryFileSystem implements FileSystem {
70
72
  private readonly root;
71
73
  private totalBytes;
72
74
  symlinkCount: number;
75
+ _getAtomicView(): MemoryAtomicView | undefined;
73
76
  constructor(options?: MemoryFileSystemOptions);
74
77
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
75
78
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
@@ -173,7 +176,7 @@ export declare function utf8ByteLength(value: string): number;
173
176
  export declare function isCleanAbsolutePath(path: string): boolean;
174
177
  export declare function tryResolveMemoryDevicePath(filesystem: FileSystem, path: string, resizeCreate?: boolean): string | undefined;
175
178
  export declare function getLastReadMemoryFileSourceRef(view: Uint8Array): Uint8Array | undefined;
176
- export declare function tryReadMemoryFileViewSync(filesystem: FileSystem, path: string, maxBytes?: number, signal?: AbortSignal, captureSourceRef?: boolean): Uint8Array | undefined;
179
+ export declare function tryReadMemoryFileViewSync(filesystem: FileSystem, path: string, maxBytes?: number, signal?: AbortSignal, captureSourceRef?: boolean, beforeRead?: () => void): Uint8Array | undefined;
177
180
  export declare function tryGetMemoryDirectoryEntryNamesSync(filesystem: FileSystem, path: string): ReadonlyMap<string, {
178
181
  readonly type: "file" | "directory" | "symlink";
179
182
  }> | undefined;