@zenfs/core 2.5.8 → 2.6.0

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 (63) hide show
  1. package/dist/backends/cow.d.ts +13 -0
  2. package/dist/backends/cow.js +48 -11
  3. package/dist/backends/single_buffer.d.ts +2 -2
  4. package/dist/backends/store/fs.d.ts +6 -6
  5. package/dist/backends/store/fs.js +10 -2
  6. package/dist/internal/devices.d.ts +6 -6
  7. package/dist/internal/index_fs.d.ts +4 -4
  8. package/dist/node/async.js +1 -1
  9. package/dist/node/dir.d.ts +3 -1
  10. package/dist/node/dir.js +23 -5
  11. package/dist/node/promises.d.ts +3 -1
  12. package/dist/node/promises.js +128 -89
  13. package/dist/node/streams.d.ts +4 -0
  14. package/dist/node/streams.js +9 -10
  15. package/dist/node/sync.js +71 -40
  16. package/dist/utils.d.ts +8 -1
  17. package/dist/utils.js +26 -7
  18. package/dist/vfs/async.js +224 -85
  19. package/dist/vfs/file.d.ts +13 -7
  20. package/dist/vfs/file.js +661 -357
  21. package/dist/vfs/shared.d.ts +1 -0
  22. package/dist/vfs/shared.js +19 -3
  23. package/dist/vfs/sync.js +214 -85
  24. package/dist/vfs/vcache.d.ts +66 -0
  25. package/dist/vfs/vcache.js +249 -0
  26. package/dist/vfs/vnode.d.ts +66 -0
  27. package/dist/vfs/vnode.js +200 -0
  28. package/dist/vfs/watchers.js +4 -2
  29. package/dist/vfs/xattr.js +2 -2
  30. package/eslint.shared.js +1 -1
  31. package/package.json +3 -3
  32. package/scripts/make-index.js +7 -1
  33. package/scripts/test.js +28 -21
  34. package/tests/backend/port.test.ts +3 -3
  35. package/tests/backend/store.test.ts +49 -0
  36. package/tests/common.ts +62 -0
  37. package/tests/fetch/fetch.ts +6 -0
  38. package/tests/fetch/run.sh +2 -2
  39. package/tests/fetch/server.js +3 -3
  40. package/tests/fs/append.test.ts +5 -5
  41. package/tests/fs/concurrency.test.ts +52 -0
  42. package/tests/fs/dir.test.ts +13 -15
  43. package/tests/fs/directory.test.ts +32 -31
  44. package/tests/fs/errors.test.ts +151 -31
  45. package/tests/fs/exists.test.ts +4 -4
  46. package/tests/fs/links.test.ts +4 -11
  47. package/tests/fs/open.test.ts +4 -4
  48. package/tests/fs/permissions.test.ts +15 -15
  49. package/tests/fs/read.test.ts +9 -9
  50. package/tests/fs/readFile.test.ts +9 -9
  51. package/tests/fs/rename.test.ts +8 -8
  52. package/tests/fs/scaling.test.ts +4 -4
  53. package/tests/fs/stat.test.ts +20 -20
  54. package/tests/fs/streams.test.ts +9 -6
  55. package/tests/fs/tempdir.test.ts +33 -10
  56. package/tests/fs/times.test.ts +11 -11
  57. package/tests/fs/truncate.test.ts +6 -6
  58. package/tests/fs/watch.test.ts +107 -53
  59. package/tests/fs/write.test.ts +10 -10
  60. package/tests/fs/xattr.test.ts +3 -3
  61. package/tests/native/run.sh +3 -0
  62. package/tests/native/setup.ts +230 -0
  63. package/types/readable-stream.d.ts +2 -2
@@ -0,0 +1,249 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { VNode } from './vnode.js';
54
+ /**
55
+ * The VFS-level cache of vnodes for a single file system. Modeled after Linux's dcache/icache.
56
+ *
57
+ * Vnodes are keyed by inode number, so hard links share a vnode.
58
+ * A vnode stays cached while it is referenced or dirty, and is evicted otherwise.
59
+ * @category VFS
60
+ * @todo Add LRU or something to limit memory usage
61
+ */
62
+ export class VCache {
63
+ fs;
64
+ byIno = new Map();
65
+ byPath = new Map();
66
+ constructor(fs) {
67
+ this.fs = fs;
68
+ }
69
+ /** Get the vnode for a path, if one is cached. Note paths are relative to the FS root, not the VFS root */
70
+ get(path) {
71
+ return this.byPath.get(path);
72
+ }
73
+ /**
74
+ * Get or create the vnode for `path`, incrementing its reference count.
75
+ * Callers must `unref` the vnode when done with it.
76
+ */
77
+ ref(path, inode) {
78
+ let node = this.byIno.get(inode.ino);
79
+ if (!node) {
80
+ node = new VNode(this.fs, path, inode);
81
+ this.byIno.set(inode.ino, node);
82
+ }
83
+ else {
84
+ node.paths.add(path);
85
+ // The vnode's inode is authoritative when dirty; otherwise refresh it with the newer stats
86
+ if (!node.dirty && inode !== node.inode)
87
+ Object.assign(node.inode, inode);
88
+ }
89
+ this.byPath.set(path, node);
90
+ node.refs++;
91
+ return node;
92
+ }
93
+ /** Release a reference to a vnode, evicting it if it is unreferenced and clean */
94
+ unref(node) {
95
+ node.refs--;
96
+ if (node.refs > 0 || node.dirty)
97
+ return;
98
+ this.evict(node);
99
+ }
100
+ evict(node) {
101
+ this.byIno.delete(node.ino);
102
+ for (const path of node.paths)
103
+ this.byPath.delete(path);
104
+ }
105
+ /** Update cached paths for a rename. Handles directories, whose descendants' paths change too */
106
+ rename(oldPath, newPath) {
107
+ for (const [path, node] of this.byPath) {
108
+ if (path != oldPath && !path.startsWith(oldPath + '/'))
109
+ continue;
110
+ const moved = newPath + path.slice(oldPath.length);
111
+ this.byPath.delete(path);
112
+ this.byPath.set(moved, node);
113
+ node.paths.delete(path);
114
+ node.paths.add(moved);
115
+ }
116
+ }
117
+ /** Add a path for a newly created hard link, if the target is cached */
118
+ link(targetPath, linkPath) {
119
+ const node = this.byPath.get(targetPath);
120
+ if (!node)
121
+ return;
122
+ node.paths.add(linkPath);
123
+ this.byPath.set(linkPath, node);
124
+ }
125
+ /** Remove a path for an unlink/rmdir. The vnode is kept if other paths (hard links) refer to it */
126
+ remove(path) {
127
+ const node = this.byPath.get(path);
128
+ if (!node)
129
+ return;
130
+ this.byPath.delete(path);
131
+ node.paths.delete(path);
132
+ if (!node.paths.size && !node.refs)
133
+ this.evict(node);
134
+ }
135
+ /** Sync all dirty vnodes to the backend */
136
+ async sync() {
137
+ for (const node of this.byIno.values()) {
138
+ const env_1 = { stack: [], error: void 0, hasError: false };
139
+ try {
140
+ if (!node.dirty)
141
+ continue;
142
+ const _ = __addDisposableResource(env_1, await node.lock('ro'), false);
143
+ await node.sync();
144
+ if (!node.refs)
145
+ this.evict(node);
146
+ }
147
+ catch (e_1) {
148
+ env_1.error = e_1;
149
+ env_1.hasError = true;
150
+ }
151
+ finally {
152
+ __disposeResources(env_1);
153
+ }
154
+ }
155
+ }
156
+ /** Sync all dirty vnodes to the backend synchronously */
157
+ syncSync() {
158
+ for (const node of this.byIno.values()) {
159
+ const env_2 = { stack: [], error: void 0, hasError: false };
160
+ try {
161
+ if (!node.dirty)
162
+ continue;
163
+ const _ = __addDisposableResource(env_2, node.lockSync('ro'), false);
164
+ node.syncSync();
165
+ if (!node.refs)
166
+ this.evict(node);
167
+ }
168
+ catch (e_2) {
169
+ env_2.error = e_2;
170
+ env_2.hasError = true;
171
+ }
172
+ finally {
173
+ __disposeResources(env_2);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ /**
179
+ * All vnode caches, keyed by file system UUID.
180
+ * Keyed by UUID rather than the `FileSystem` object because `resolveMount`
181
+ * wraps file systems in a new exception-context proxy on every call.
182
+ * @category VFS
183
+ * @internal
184
+ */
185
+ export const caches = new Map();
186
+ /**
187
+ * Get the vnode cache for a file system, creating it if needed.
188
+ * @category VFS
189
+ * @internal
190
+ */
191
+ export function cacheOf(fs) {
192
+ let cache = caches.get(fs.uuid);
193
+ if (!cache) {
194
+ cache = new VCache(fs);
195
+ caches.set(fs.uuid, cache);
196
+ }
197
+ return cache;
198
+ }
199
+ /**
200
+ * Ref and lock the vnode for a path, usually a parent directory during a namespace operation.
201
+ * The returned release function unlocks and unrefs the vnode.
202
+ * If `inode` is not provided and the vnode is not cached, the backend is `stat`ed.
203
+ * @category VFS
204
+ * @internal
205
+ */
206
+ export async function lockPath(fs, path, mode, inode) {
207
+ const cache = cacheOf(fs);
208
+ inode ??= cache.get(path)?.inode ?? (await fs.stat(path));
209
+ const node = cache.ref(path, inode);
210
+ const unlock = await node.lock(mode);
211
+ let released = false;
212
+ const release = () => {
213
+ if (released)
214
+ return;
215
+ released = true;
216
+ unlock();
217
+ cache.unref(node);
218
+ };
219
+ release[Symbol.dispose] = release;
220
+ return release;
221
+ }
222
+ /**
223
+ * Synchronous version of `lockPath`.
224
+ * @category VFS
225
+ * @internal
226
+ */
227
+ export function lockPathSync(fs, path, mode, inode) {
228
+ const cache = cacheOf(fs);
229
+ inode ??= cache.get(path)?.inode ?? fs.statSync(path);
230
+ const node = cache.ref(path, inode);
231
+ let unlock;
232
+ try {
233
+ unlock = node.lockSync(mode);
234
+ }
235
+ catch (e) {
236
+ cache.unref(node);
237
+ throw e;
238
+ }
239
+ let released = false;
240
+ const release = () => {
241
+ if (released)
242
+ return;
243
+ released = true;
244
+ unlock();
245
+ cache.unref(node);
246
+ };
247
+ release[Symbol.dispose] = release;
248
+ return release;
249
+ }
@@ -0,0 +1,66 @@
1
+ import { RwLockable } from 'kerium/locks';
2
+ import { Resource, type Range } from 'utilium/cache';
3
+ import type { FileSystem } from '../internal/filesystem.js';
4
+ import { type InodeLike } from '../internal/inode.js';
5
+ /**
6
+ * A VFS-level node, roughly equivalent to Linux's in-memory `struct inode`.
7
+ *
8
+ * A `VNode` is the single authoritative copy of an inode's metadata and cached data at the VFS level.
9
+ * All open `Handle`s for the same file share one `VNode`, so metadata changes are immediately visible across handles.
10
+ * File data written through a `VNode` is cached and only written to the backend when the vnode is synced.
11
+ *
12
+ * `VNode` methods do *not* acquire the vnode's lock. Callers are responsible for locking, since operations usually span multiple vnode calls.
13
+ * @category VFS
14
+ */
15
+ export declare class VNode extends RwLockable {
16
+ readonly fs: FileSystem;
17
+ readonly inode: InodeLike;
18
+ /** Cached file data. This is a page cache, but with byte granularity */
19
+ readonly data: Resource<number>;
20
+ /** Ranges of `data` that have been written but not yet synced to the backend */
21
+ protected dirtyRanges: Range[];
22
+ /** Whether `inode` has changes that have not been synced to the backend */
23
+ metadataDirty: boolean;
24
+ /** The number of active references: open handles and in-progress VFS operations */
25
+ refs: number;
26
+ /** Paths on `fs` that refer to this vnode. Multiple paths refer to the same vnode with hard links */
27
+ readonly paths: Set<string>;
28
+ /** Fallback for when the last path is removed (i.e. the file is unlinked while open) */
29
+ protected _lastPath: string;
30
+ constructor(fs: FileSystem, path: string, inode: InodeLike);
31
+ get ino(): number;
32
+ /** A path that can be used to address the backend */
33
+ get path(): string;
34
+ /** Whether this vnode has data or metadata that has not been synced to the backend */
35
+ get dirty(): boolean;
36
+ /** Character and block devices bypass the data cache */
37
+ protected get bypassCache(): boolean;
38
+ /** Add a range to `dirtyRanges`, merging overlapping and adjacent ranges */
39
+ protected markDirty(start: number, end: number): void;
40
+ /**
41
+ * Copy `[start, end)` from the cache into `buffer`.
42
+ * Uncached spans are holes (e.g. from an extending truncate) and are zero-filled.
43
+ */
44
+ protected copyFromCache(buffer: Uint8Array, start: number, end: number): void;
45
+ /**
46
+ * Read `[start, end)` into `buffer`, fetching any missing ranges from the backend.
47
+ */
48
+ read(buffer: Uint8Array, start: number, end: number): Promise<void>;
49
+ readSync(buffer: Uint8Array, start: number, end: number): void;
50
+ /**
51
+ * Write `data` at `offset` into the cache and update metadata.
52
+ * The backend is not written to until `sync`, except for devices, which bypass the cache.
53
+ */
54
+ write(data: Uint8Array, offset: number): Promise<void>;
55
+ writeSync(data: Uint8Array, offset: number): void;
56
+ protected _writeCached(data: Uint8Array, offset: number): void;
57
+ protected _writeMetadata(end: number): void;
58
+ /**
59
+ * Change the size of the file.
60
+ * Note an extending truncate creates a hole, which reads as zeroes.
61
+ */
62
+ truncate(length: number): void;
63
+ /** Write all unsynced data to the backend, then metadata */
64
+ sync(): Promise<void>;
65
+ syncSync(): void;
66
+ }
@@ -0,0 +1,200 @@
1
+ // SPDX-License-Identifier: LGPL-3.0-or-later
2
+ import { RwLockable } from 'kerium/locks';
3
+ import { Resource } from 'utilium/cache';
4
+ import { InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js';
5
+ /**
6
+ * A VFS-level node, roughly equivalent to Linux's in-memory `struct inode`.
7
+ *
8
+ * A `VNode` is the single authoritative copy of an inode's metadata and cached data at the VFS level.
9
+ * All open `Handle`s for the same file share one `VNode`, so metadata changes are immediately visible across handles.
10
+ * File data written through a `VNode` is cached and only written to the backend when the vnode is synced.
11
+ *
12
+ * `VNode` methods do *not* acquire the vnode's lock. Callers are responsible for locking, since operations usually span multiple vnode calls.
13
+ * @category VFS
14
+ */
15
+ export class VNode extends RwLockable {
16
+ fs;
17
+ inode;
18
+ /** Cached file data. This is a page cache, but with byte granularity */
19
+ data;
20
+ /** Ranges of `data` that have been written but not yet synced to the backend */
21
+ dirtyRanges = [];
22
+ /** Whether `inode` has changes that have not been synced to the backend */
23
+ metadataDirty = false;
24
+ /** The number of active references: open handles and in-progress VFS operations */
25
+ refs = 0;
26
+ /** Paths on `fs` that refer to this vnode. Multiple paths refer to the same vnode with hard links */
27
+ paths = new Set();
28
+ /** Fallback for when the last path is removed (i.e. the file is unlinked while open) */
29
+ _lastPath;
30
+ constructor(fs, path, inode) {
31
+ super();
32
+ this.fs = fs;
33
+ this.inode = inode;
34
+ this.paths.add(path);
35
+ this._lastPath = path;
36
+ this.data = new Resource(inode.ino, inode.size, { sparse: true });
37
+ }
38
+ get ino() {
39
+ return this.inode.ino;
40
+ }
41
+ /** A path that can be used to address the backend */
42
+ get path() {
43
+ const [path] = this.paths;
44
+ if (path !== undefined)
45
+ this._lastPath = path;
46
+ return path ?? this._lastPath;
47
+ }
48
+ /** Whether this vnode has data or metadata that has not been synced to the backend */
49
+ get dirty() {
50
+ return this.metadataDirty || this.dirtyRanges.length > 0;
51
+ }
52
+ /** Character and block devices bypass the data cache */
53
+ get bypassCache() {
54
+ return isCharacterDevice(this.inode) || isBlockDevice(this.inode) || !!(this.inode.flags & InodeFlags.DAX);
55
+ }
56
+ /** Add a range to `dirtyRanges`, merging overlapping and adjacent ranges */
57
+ markDirty(start, end) {
58
+ this.dirtyRanges.push({ start, end });
59
+ this.dirtyRanges.sort((a, b) => a.start - b.start);
60
+ const merged = [];
61
+ for (const range of this.dirtyRanges) {
62
+ const last = merged.at(-1);
63
+ if (last && range.start <= last.end)
64
+ last.end = Math.max(last.end, range.end);
65
+ else
66
+ merged.push(range);
67
+ }
68
+ this.dirtyRanges = merged;
69
+ }
70
+ /**
71
+ * Copy `[start, end)` from the cache into `buffer`.
72
+ * Uncached spans are holes (e.g. from an extending truncate) and are zero-filled.
73
+ */
74
+ copyFromCache(buffer, start, end) {
75
+ let pos = start;
76
+ for (const region of this.data.regions) {
77
+ const regionEnd = region.offset + region.data.byteLength;
78
+ if (regionEnd <= pos)
79
+ continue;
80
+ if (region.offset >= end)
81
+ break;
82
+ if (region.offset > pos) {
83
+ buffer.fill(0, pos - start, region.offset - start);
84
+ pos = region.offset;
85
+ }
86
+ const copyEnd = Math.min(end, regionEnd);
87
+ buffer.set(region.data.subarray(pos - region.offset, copyEnd - region.offset), pos - start);
88
+ pos = copyEnd;
89
+ }
90
+ if (pos < end)
91
+ buffer.fill(0, pos - start, end - start);
92
+ }
93
+ /**
94
+ * Read `[start, end)` into `buffer`, fetching any missing ranges from the backend.
95
+ */
96
+ async read(buffer, start, end) {
97
+ if (this.bypassCache)
98
+ return await this.fs.read(this.path, buffer, start, end);
99
+ for (const { start: rangeStart, end: rangeEnd } of this.data.missing(start, end)) {
100
+ const chunk = new Uint8Array(rangeEnd - rangeStart);
101
+ await this.fs.read(this.path, chunk, rangeStart, rangeEnd);
102
+ this.data.add(chunk, rangeStart);
103
+ }
104
+ this.copyFromCache(buffer, start, end);
105
+ }
106
+ readSync(buffer, start, end) {
107
+ if (this.bypassCache)
108
+ return this.fs.readSync(this.path, buffer, start, end);
109
+ for (const { start: rangeStart, end: rangeEnd } of this.data.missing(start, end)) {
110
+ const chunk = new Uint8Array(rangeEnd - rangeStart);
111
+ this.fs.readSync(this.path, chunk, rangeStart, rangeEnd);
112
+ this.data.add(chunk, rangeStart);
113
+ }
114
+ this.copyFromCache(buffer, start, end);
115
+ }
116
+ /**
117
+ * Write `data` at `offset` into the cache and update metadata.
118
+ * The backend is not written to until `sync`, except for devices, which bypass the cache.
119
+ */
120
+ async write(data, offset) {
121
+ if (this.bypassCache)
122
+ await this.fs.write(this.path, data, offset);
123
+ else
124
+ this._writeCached(data, offset);
125
+ this._writeMetadata(offset + data.byteLength);
126
+ }
127
+ writeSync(data, offset) {
128
+ if (this.bypassCache)
129
+ this.fs.writeSync(this.path, data, offset);
130
+ else
131
+ this._writeCached(data, offset);
132
+ this._writeMetadata(offset + data.byteLength);
133
+ }
134
+ _writeCached(data, offset) {
135
+ const end = offset + data.byteLength;
136
+ // Copy since the region can keep a reference and the caller may reuse the buffer
137
+ this.data.add(data.slice(), offset);
138
+ if (end > this.data.size)
139
+ this.data.size = end;
140
+ this.markDirty(offset, end);
141
+ }
142
+ _writeMetadata(end) {
143
+ if (!this.bypassCache && end > this.inode.size)
144
+ this.inode.size = end;
145
+ this.inode.mtimeMs = Date.now();
146
+ this.inode.ctimeMs = Date.now();
147
+ this.metadataDirty = true;
148
+ }
149
+ /**
150
+ * Change the size of the file.
151
+ * Note an extending truncate creates a hole, which reads as zeroes.
152
+ */
153
+ truncate(length) {
154
+ this.data.size = length;
155
+ this.dirtyRanges = this.dirtyRanges
156
+ .filter(range => range.start < length)
157
+ .map(range => (range.end > length ? { start: range.start, end: length } : range));
158
+ this.inode.size = length;
159
+ this.inode.mtimeMs = Date.now();
160
+ this.inode.ctimeMs = Date.now();
161
+ this.metadataDirty = true;
162
+ }
163
+ /** Write all unsynced data to the backend, then metadata */
164
+ async sync() {
165
+ for (const range of this.dirtyRanges) {
166
+ for (let pos = range.start; pos < range.end;) {
167
+ const region = this.data.regionAt(pos);
168
+ if (!region)
169
+ break; // should not happen since dirty ranges are always cached
170
+ const end = Math.min(range.end, region.offset + region.data.byteLength);
171
+ await this.fs.write(this.path, region.data.subarray(pos - region.offset, end - region.offset), pos);
172
+ pos = end;
173
+ }
174
+ }
175
+ this.dirtyRanges = [];
176
+ if (!this.metadataDirty)
177
+ return;
178
+ if (!this.fs.attributes.has('no_write'))
179
+ await this.fs.touch(this.path, this.inode);
180
+ this.metadataDirty = false;
181
+ }
182
+ syncSync() {
183
+ for (const range of this.dirtyRanges) {
184
+ for (let pos = range.start; pos < range.end;) {
185
+ const region = this.data.regionAt(pos);
186
+ if (!region)
187
+ break; // should not happen since dirty ranges are always cached
188
+ const end = Math.min(range.end, region.offset + region.data.byteLength);
189
+ this.fs.writeSync(this.path, region.data.subarray(pos - region.offset, end - region.offset), pos);
190
+ pos = end;
191
+ }
192
+ }
193
+ this.dirtyRanges = [];
194
+ if (!this.metadataDirty)
195
+ return;
196
+ if (!this.fs.attributes.has('no_write'))
197
+ this.fs.touchSync(this.path, this.inode);
198
+ this.metadataDirty = false;
199
+ }
200
+ }
@@ -5,6 +5,8 @@ import { isStatsEqual } from '../node/stats.js';
5
5
  import { statSync } from '../node/sync.js';
6
6
  import { basename, dirname, join, relative } from '../path.js';
7
7
  import { normalizePath } from '../utils.js';
8
+ /** Alias so `.call` resolves to the non-bigint overload */
9
+ const _statSync = statSync;
8
10
  /**
9
11
  * Base class for file system watchers.
10
12
  * Provides event handling capabilities for watching file system changes.
@@ -90,7 +92,7 @@ export class StatWatcher extends Watcher {
90
92
  }
91
93
  onInterval() {
92
94
  try {
93
- const current = statSync(this.path);
95
+ const current = _statSync.call(this._context, this.path);
94
96
  if (!isStatsEqual(this.previous, current)) {
95
97
  this.emit('change', current, this.previous);
96
98
  this.previous = current;
@@ -103,7 +105,7 @@ export class StatWatcher extends Watcher {
103
105
  start() {
104
106
  const interval = this.options.interval || 5000;
105
107
  try {
106
- this.previous = statSync(this.path);
108
+ this.previous = _statSync.call(this._context, this.path);
107
109
  }
108
110
  catch (e) {
109
111
  this.emit('error', e);
package/dist/vfs/xattr.js CHANGED
@@ -172,7 +172,7 @@ export async function list(path) {
172
172
  const inode = await fs.stat(resolved).catch(rethrow('xattr.list', path));
173
173
  if (!inode.attributes)
174
174
  return [];
175
- return inode.attributes.keys().toArray();
175
+ return [...inode.attributes.keys()];
176
176
  }
177
177
  /**
178
178
  * Synchronously lists all extended attributes of a file.
@@ -192,5 +192,5 @@ export function listSync(path) {
192
192
  }
193
193
  if (!inode.attributes)
194
194
  return [];
195
- return inode.attributes.keys().toArray();
195
+ return [...inode.attributes.keys()];
196
196
  }
package/eslint.shared.js CHANGED
@@ -23,7 +23,7 @@ export default [
23
23
  'prefer-rest-params': 'warn',
24
24
  'prefer-spread': 'warn',
25
25
  'no-unused-vars': 'off',
26
- '@typescript-eslint/no-unused-vars': 'warn',
26
+ '@typescript-eslint/no-unused-vars': ['warn', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
27
27
  '@typescript-eslint/no-inferrable-types': 'off',
28
28
  '@typescript-eslint/no-this-alias': 'off',
29
29
  '@typescript-eslint/no-unsafe-function-type': 'warn',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.5.8",
3
+ "version": "2.6.0",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -64,7 +64,7 @@
64
64
  "spdx:check": "npx lice -a src tests/*.ts tests/**/*.test.ts",
65
65
  "spdx:fix": "npx lice -aw src tests/*.ts tests/**/*.test.ts",
66
66
  "lint": "eslint src tests",
67
- "test": "npx zenfs-test --clean; npx zenfs-test -abcp; tests/fetch/run.sh; npx zenfs-test --report",
67
+ "test": "npx --silent zenfs-test --clean; npx --silent zenfs-test -abcp; tests/fetch/run.sh; npx --silent zenfs-test --report",
68
68
  "build": "tsc -p tsconfig.json",
69
69
  "build:docs": "typedoc",
70
70
  "dev": "tsc -p tsconfig.json --watch",
@@ -74,7 +74,7 @@
74
74
  "@types/node": "^25.2.0",
75
75
  "buffer": "^6.0.3",
76
76
  "eventemitter3": "^5.0.1",
77
- "kerium": "^1.3.4",
77
+ "kerium": "^1.4.2",
78
78
  "memium": "^0.4.0",
79
79
  "readable-stream": "^4.5.2",
80
80
  "utilium": "^3.0.0"
@@ -36,6 +36,9 @@ if (options.quiet && options.verbose) {
36
36
  process.exit();
37
37
  }
38
38
 
39
+ /**
40
+ * @param {string} path
41
+ */
39
42
  function fixSlash(path) {
40
43
  return path.replaceAll('\\', '/');
41
44
  }
@@ -44,6 +47,9 @@ const resolvedRoot = root || '.';
44
47
 
45
48
  const entries = new Map();
46
49
 
50
+ /**
51
+ * @param {string} path
52
+ */
47
53
  function computeEntries(path) {
48
54
  try {
49
55
  if (options.ignore.some(pattern => matchesGlob(path, pattern))) {
@@ -68,7 +74,7 @@ function computeEntries(path) {
68
74
  if (options.verbose) {
69
75
  console.log(`${styleText('greenBright', ' dir')} ${path}`);
70
76
  }
71
- } catch (e) {
77
+ } catch (/** @type {any} */ e) {
72
78
  if (!options.quiet) {
73
79
  console.log(`${styleText('red', 'fail')} ${path}: ${e.message}`);
74
80
  }