@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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|