@poe-platform/safe-fs 0.1.670 → 0.1.671

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.
package/README.md CHANGED
@@ -68,6 +68,8 @@ cause. Preserve cancellation/control exceptions separately. Cross-family
68
68
 
69
69
  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`.
70
70
 
71
+ `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.
72
+
71
73
  | Backend or wrapper | Use it for |
72
74
  | --- | --- |
73
75
  | `createMemoryFileSystem()` | Isolated, nonpersistent storage with links, permissions, timestamps, and streams |
@@ -3,9 +3,10 @@ import { finishCleanup } from "../../contracts/cleanup.js";
3
3
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
4
4
  import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
5
5
  import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
6
- import { deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
6
+ import { createDeviceYield, deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
7
7
  import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
8
8
  import { pathNamespace } from "../path-namespace.js";
9
+ import { openFileDescriptor } from "../descriptor.js";
9
10
  const views = new WeakMap();
10
11
  const deviceCapabilities = Object.freeze({
11
12
  readOnly: false, read: true, stat: true, realpath: true, access: true, readdir: false,
@@ -14,7 +15,7 @@ const deviceCapabilities = Object.freeze({
14
15
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
15
16
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
16
17
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
17
- open: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
18
+ open: true, atomicFileMutation: false, atomicEntryRemoval: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false,
18
19
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
19
20
  });
20
21
  function globalCapabilities(filesystem) {
@@ -125,7 +126,7 @@ export class DeviceFileSystem {
125
126
  if (resolved === nullPath)
126
127
  return deviceCapabilities;
127
128
  if (resolved === deviceDirectory)
128
- return { ...deviceCapabilities, readdir: true, write: false, append: false,
129
+ return { ...deviceCapabilities, open: false, readdir: true, write: false, append: false,
129
130
  exclusiveCreate: false, streamingWrite: false, streamingAppend: false, independentWriteStreams: false, descriptorWriteStream: false,
130
131
  retainedRead: false, retainedResize: false, streamingRead: false, copy: false, exclusiveCopy: false };
131
132
  const query = this.#filesystem.capabilitiesFor;
@@ -239,7 +240,29 @@ export class DeviceFileSystem {
239
240
  throw new FsError("EINVAL", { syscall: "open", path });
240
241
  const resolved = await this.#resolve(path, options, options.creation !== "exclusive");
241
242
  options.signal?.throwIfAborted();
242
- if (resolved === nullPath || resolved === deviceDirectory)
243
+ if (resolved === nullPath)
244
+ return openFileDescriptor(path, options, {
245
+ position: true, positionedRead: true, positionedWrite: true, positionedAppendWrite: true,
246
+ openTruncate: true, delegateZeroLengthWrite: true, truncate: false, synchronization: "none",
247
+ }, async (admitted) => {
248
+ if (admitted.creation === "exclusive")
249
+ throw new FsError("EEXIST", { syscall: "open", path });
250
+ const yieldAfterWrite = createDeviceYield();
251
+ return {
252
+ resource: this.#nullStat,
253
+ getPosition: async () => 0,
254
+ stat: async (stat) => ({ ...stat }),
255
+ read: async () => 0,
256
+ write: async (_stat, buffer, _position, forwarded) => {
257
+ await yieldAfterWrite(buffer.byteLength, forwarded.signal);
258
+ return buffer.byteLength;
259
+ },
260
+ truncate: async () => { throw new FsError("ENOTSUP", { syscall: "ftruncate", path }); },
261
+ sync: async () => { throw new FsError("ENOTSUP", { syscall: "fsync", path }); },
262
+ close: async () => { },
263
+ };
264
+ });
265
+ if (resolved === deviceDirectory)
243
266
  throw new FsError("ENOTSUP", { syscall: "open", path });
244
267
  const query = this.#filesystem.capabilitiesFor;
245
268
  options.signal?.throwIfAborted();
@@ -2,4 +2,5 @@ import type { ByteSource } from "../../contracts/io.js";
2
2
  import type { FileReadHandle } from "../../contracts/filesystem.js";
3
3
  export declare function deviceReadStream(open: () => Promise<ByteSource>, signal?: AbortSignal): ByteSource;
4
4
  export declare function drainDeviceFile(handle: FileReadHandle, signal?: AbortSignal): Promise<void>;
5
+ export declare function createDeviceYield(): (length: number, signal?: AbortSignal) => Promise<void>;
5
6
  export declare function drainDeviceInput(source: ByteSource, signal?: AbortSignal): Promise<void>;
@@ -86,12 +86,24 @@ function nextChunk(iterator, signal) {
86
86
  }
87
87
  });
88
88
  }
89
+ export function createDeviceYield() {
90
+ let bytes = 0;
91
+ let pulls = 0;
92
+ return async (length, signal) => {
93
+ bytes += length;
94
+ if (++pulls >= 64 || bytes >= 65536) {
95
+ await new Promise(resolve => setTimeout(resolve, 0));
96
+ signal?.throwIfAborted();
97
+ bytes = 0;
98
+ pulls = 0;
99
+ }
100
+ };
101
+ }
89
102
  export async function drainDeviceInput(source, signal) {
90
103
  signal?.throwIfAborted();
91
104
  const iterator = source[Symbol.asyncIterator]();
92
105
  let failed = false;
93
- let bytes = 0;
94
- let pulls = 0;
106
+ const yieldAfterWrite = createDeviceYield();
95
107
  try {
96
108
  for (;;) {
97
109
  const result = await nextChunk(iterator, signal);
@@ -100,13 +112,7 @@ export async function drainDeviceInput(source, signal) {
100
112
  break;
101
113
  if (!(result.value instanceof Uint8Array))
102
114
  throw new TypeError("Byte sources must yield Uint8Array chunks");
103
- bytes += result.value.byteLength;
104
- if (++pulls >= 64 || bytes >= 65536) {
105
- await new Promise(resolve => setTimeout(resolve, 0));
106
- signal?.throwIfAborted();
107
- bytes = 0;
108
- pulls = 0;
109
- }
115
+ await yieldAfterWrite(result.value.byteLength, signal);
110
116
  }
111
117
  }
112
118
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.670",
3
+ "version": "0.1.671",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",