@zenfs/core 2.5.8 → 2.6.1

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 (66) hide show
  1. package/dist/backends/cow.d.ts +17 -0
  2. package/dist/backends/cow.js +72 -25
  3. package/dist/backends/fetch.d.ts +4 -0
  4. package/dist/backends/fetch.js +7 -3
  5. package/dist/backends/single_buffer.d.ts +2 -2
  6. package/dist/backends/store/fs.d.ts +6 -6
  7. package/dist/backends/store/fs.js +10 -2
  8. package/dist/internal/devices.d.ts +6 -6
  9. package/dist/internal/index_fs.d.ts +4 -4
  10. package/dist/node/async.js +1 -1
  11. package/dist/node/dir.d.ts +3 -1
  12. package/dist/node/dir.js +23 -5
  13. package/dist/node/promises.d.ts +3 -1
  14. package/dist/node/promises.js +128 -89
  15. package/dist/node/streams.d.ts +4 -0
  16. package/dist/node/streams.js +9 -10
  17. package/dist/node/sync.js +71 -40
  18. package/dist/utils.d.ts +8 -1
  19. package/dist/utils.js +26 -7
  20. package/dist/vfs/async.js +224 -85
  21. package/dist/vfs/file.d.ts +13 -7
  22. package/dist/vfs/file.js +661 -357
  23. package/dist/vfs/shared.d.ts +1 -0
  24. package/dist/vfs/shared.js +18 -1
  25. package/dist/vfs/sync.js +214 -85
  26. package/dist/vfs/vcache.d.ts +66 -0
  27. package/dist/vfs/vcache.js +249 -0
  28. package/dist/vfs/vnode.d.ts +72 -0
  29. package/dist/vfs/vnode.js +215 -0
  30. package/dist/vfs/watchers.js +4 -2
  31. package/dist/vfs/xattr.js +2 -2
  32. package/eslint.shared.js +1 -1
  33. package/package.json +3 -3
  34. package/scripts/make-index.js +43 -16
  35. package/scripts/test.js +28 -21
  36. package/tests/backend/fetch.test.ts +24 -1
  37. package/tests/backend/port.test.ts +3 -3
  38. package/tests/backend/store.test.ts +49 -0
  39. package/tests/common.ts +60 -0
  40. package/tests/fetch/fetch.ts +6 -0
  41. package/tests/fetch/run.sh +2 -2
  42. package/tests/fetch/server.js +3 -3
  43. package/tests/fs/append.test.ts +5 -5
  44. package/tests/fs/concurrency.test.ts +90 -0
  45. package/tests/fs/dir.test.ts +13 -15
  46. package/tests/fs/directory.test.ts +32 -31
  47. package/tests/fs/errors.test.ts +151 -31
  48. package/tests/fs/exists.test.ts +4 -4
  49. package/tests/fs/links.test.ts +4 -11
  50. package/tests/fs/open.test.ts +4 -4
  51. package/tests/fs/permissions.test.ts +15 -15
  52. package/tests/fs/read.test.ts +9 -9
  53. package/tests/fs/readFile.test.ts +9 -9
  54. package/tests/fs/rename.test.ts +8 -8
  55. package/tests/fs/scaling.test.ts +4 -4
  56. package/tests/fs/stat.test.ts +20 -20
  57. package/tests/fs/streams.test.ts +9 -6
  58. package/tests/fs/tempdir.test.ts +33 -10
  59. package/tests/fs/times.test.ts +11 -11
  60. package/tests/fs/truncate.test.ts +6 -6
  61. package/tests/fs/watch.test.ts +107 -53
  62. package/tests/fs/write.test.ts +10 -10
  63. package/tests/fs/xattr.test.ts +3 -3
  64. package/tests/native/run.sh +3 -0
  65. package/tests/native/setup.ts +230 -0
  66. package/types/readable-stream.d.ts +2 -2
@@ -70,6 +70,19 @@ export declare class CopyOnWriteFS extends FileSystem {
70
70
  /** The journal to use for persisting deletions */
71
71
  journal?: Journal);
72
72
  isDeleted(path: string): boolean;
73
+ /**
74
+ * Inode numbers presented to the VFS for each path.
75
+ *
76
+ * The readable and writable layers allocate inode numbers independently, so they can collide.
77
+ * They can not be used as-is, since the VFS uses `ino` to tell files apart.
78
+ * This also keeps a file's inode number stable when it is copied up to the writable layer.
79
+ */
80
+ protected inos: Map<string, number>;
81
+ private _nextIno;
82
+ /** Moves the public inode numbers for a rename, including a directory's descendants */
83
+ protected renameInos(oldPath: string, newPath: string): void;
84
+ /** Replaces the layer's inode number with the public one for `path` */
85
+ protected remapIno(path: string, inode: InodeLike): InodeLike;
73
86
  /**
74
87
  * @todo Consider trying to track information on the writable as well
75
88
  */
@@ -114,11 +127,15 @@ export declare class CopyOnWriteFS extends FileSystem {
114
127
  * Helper function:
115
128
  * - Ensures p is on writable before proceeding. Throws an error if it doesn't exist.
116
129
  * - Calls f to perform operation on writable.
130
+ *
131
+ * `keep` is the size the file is about to be truncated to, when the caller knows it.
132
+ * Bytes past it are discarded by the operation that follows, so they are not worth copying up.
117
133
  */
118
134
  private copyForWriteSync;
119
135
  private copyForWrite;
120
136
  /**
121
137
  * Copy from readable to writable storage.
138
+ * Only `[0, keep)` is copied; the caller is responsible for setting the final size.
122
139
  * PRECONDITION: File does not exist on writable storage.
123
140
  */
124
141
  private copyToWritableSync;
@@ -1,17 +1,17 @@
1
1
  import { EventEmitter } from 'eventemitter3';
2
2
  import { withErrno } from 'kerium';
3
3
  import { debug, err, warn } from 'kerium/log';
4
- import { canary } from 'utilium';
4
+ import { canary, pick } from 'utilium';
5
5
  import { resolveMountConfig } from '../config.js';
6
6
  import { FileSystem } from '../internal/filesystem.js';
7
- import { isDirectory } from '../internal/inode.js';
7
+ import { _inode_fields, isDirectory } from '../internal/inode.js';
8
8
  import { dirname, join } from '../path.js';
9
9
  const journalOperations = new Set(['delete']);
10
10
  /** Because TS doesn't work right w/o it */
11
11
  function isJournalOp(op) {
12
12
  return journalOperations.has(op);
13
13
  }
14
- const maxOpLength = Math.max(...journalOperations.values().map(op => op.length));
14
+ const maxOpLength = Math.max(...[...journalOperations].map(op => op.length));
15
15
  const journalMagicString = '#journal@v0\n';
16
16
  /**
17
17
  * Tracks various operations for the CoW backend
@@ -102,6 +102,35 @@ export class CopyOnWriteFS extends FileSystem {
102
102
  isDeleted(path) {
103
103
  return this.journal.isDeleted(path);
104
104
  }
105
+ /**
106
+ * Inode numbers presented to the VFS for each path.
107
+ *
108
+ * The readable and writable layers allocate inode numbers independently, so they can collide.
109
+ * They can not be used as-is, since the VFS uses `ino` to tell files apart.
110
+ * This also keeps a file's inode number stable when it is copied up to the writable layer.
111
+ */
112
+ inos = new Map();
113
+ _nextIno = 1;
114
+ /** Moves the public inode numbers for a rename, including a directory's descendants */
115
+ renameInos(oldPath, newPath) {
116
+ for (const [path, ino] of [...this.inos]) {
117
+ if (path != oldPath && !path.startsWith(oldPath + '/'))
118
+ continue;
119
+ this.inos.delete(path);
120
+ this.inos.set(newPath + path.slice(oldPath.length), ino);
121
+ }
122
+ }
123
+ /** Replaces the layer's inode number with the public one for `path` */
124
+ remapIno(path, inode) {
125
+ let ino = this.inos.get(path);
126
+ if (ino === undefined) {
127
+ ino = this._nextIno++;
128
+ this.inos.set(path, ino);
129
+ }
130
+ /* Note the layer's inode is not mutated, since it may be a persistent object.
131
+ `Inode` fields are accessors, so a copy must be explicit rather than a spread. */
132
+ return { ...pick(inode, _inode_fields), attributes: inode.attributes, ino };
133
+ }
105
134
  /**
106
135
  * @todo Consider trying to track information on the writable as well
107
136
  */
@@ -139,6 +168,7 @@ export class CopyOnWriteFS extends FileSystem {
139
168
  if (this.isDeleted(oldPath))
140
169
  throw withErrno('ENOENT');
141
170
  }
171
+ this.renameInos(oldPath, newPath);
142
172
  }
143
173
  renameSync(oldPath, newPath) {
144
174
  this.copyForWriteSync(oldPath);
@@ -149,42 +179,43 @@ export class CopyOnWriteFS extends FileSystem {
149
179
  if (this.isDeleted(oldPath))
150
180
  throw withErrno('ENOENT');
151
181
  }
182
+ this.renameInos(oldPath, newPath);
152
183
  }
153
184
  async stat(path) {
154
185
  try {
155
- return await this.writable.stat(path);
186
+ return this.remapIno(path, await this.writable.stat(path));
156
187
  }
157
188
  catch {
158
189
  if (this.isDeleted(path))
159
190
  throw withErrno('ENOENT');
160
- return await this.readable.stat(path);
191
+ return this.remapIno(path, await this.readable.stat(path));
161
192
  }
162
193
  }
163
194
  statSync(path) {
164
195
  try {
165
- return this.writable.statSync(path);
196
+ return this.remapIno(path, this.writable.statSync(path));
166
197
  }
167
198
  catch {
168
199
  if (this.isDeleted(path))
169
200
  throw withErrno('ENOENT');
170
- return this.readable.statSync(path);
201
+ return this.remapIno(path, this.readable.statSync(path));
171
202
  }
172
203
  }
173
204
  async touch(path, metadata) {
174
- await this.copyForWrite(path);
205
+ await this.copyForWrite(path, metadata.size);
175
206
  await this.writable.touch(path, metadata);
176
207
  }
177
208
  touchSync(path, metadata) {
178
- this.copyForWriteSync(path);
209
+ this.copyForWriteSync(path, metadata.size);
179
210
  this.writable.touchSync(path, metadata);
180
211
  }
181
212
  async createFile(path, options) {
182
213
  await this.createParentDirectories(path);
183
- return await this.writable.createFile(path, options);
214
+ return this.remapIno(path, await this.writable.createFile(path, options));
184
215
  }
185
216
  createFileSync(path, options) {
186
217
  this.createParentDirectoriesSync(path);
187
- return this.writable.createFileSync(path, options);
218
+ return this.remapIno(path, this.writable.createFileSync(path, options));
188
219
  }
189
220
  async link(srcpath, dstpath) {
190
221
  await this.copyForWrite(srcpath);
@@ -204,6 +235,7 @@ export class CopyOnWriteFS extends FileSystem {
204
235
  if (await this.exists(path)) {
205
236
  this.journal.add('delete', path);
206
237
  }
238
+ this.inos.delete(path);
207
239
  }
208
240
  unlinkSync(path) {
209
241
  if (!this.existsSync(path))
@@ -215,6 +247,7 @@ export class CopyOnWriteFS extends FileSystem {
215
247
  if (this.existsSync(path)) {
216
248
  this.journal.add('delete', path);
217
249
  }
250
+ this.inos.delete(path);
218
251
  }
219
252
  async rmdir(path) {
220
253
  if (!(await this.exists(path)))
@@ -223,12 +256,14 @@ export class CopyOnWriteFS extends FileSystem {
223
256
  await this.writable.rmdir(path);
224
257
  }
225
258
  if (!(await this.exists(path))) {
259
+ this.inos.delete(path);
226
260
  return;
227
261
  }
228
262
  // Check if directory is empty.
229
263
  if ((await this.readdir(path)).length)
230
264
  throw withErrno('ENOTEMPTY');
231
265
  this.journal.add('delete', path);
266
+ this.inos.delete(path);
232
267
  }
233
268
  rmdirSync(path) {
234
269
  if (!this.existsSync(path))
@@ -237,24 +272,26 @@ export class CopyOnWriteFS extends FileSystem {
237
272
  this.writable.rmdirSync(path);
238
273
  }
239
274
  if (!this.existsSync(path)) {
275
+ this.inos.delete(path);
240
276
  return;
241
277
  }
242
278
  // Check if directory is empty.
243
279
  if (this.readdirSync(path).length)
244
280
  throw withErrno('ENOTEMPTY');
245
281
  this.journal.add('delete', path);
282
+ this.inos.delete(path);
246
283
  }
247
284
  async mkdir(path, options) {
248
285
  if (await this.exists(path))
249
286
  throw withErrno('EEXIST');
250
287
  await this.createParentDirectories(path);
251
- return await this.writable.mkdir(path, options);
288
+ return this.remapIno(path, await this.writable.mkdir(path, options));
252
289
  }
253
290
  mkdirSync(path, options) {
254
291
  if (this.existsSync(path))
255
292
  throw withErrno('EEXIST');
256
293
  this.createParentDirectoriesSync(path);
257
- return this.writable.mkdirSync(path, options);
294
+ return this.remapIno(path, this.writable.mkdirSync(path, options));
258
295
  }
259
296
  async readdir(path) {
260
297
  if (this.isDeleted(path) || !(await this.exists(path)))
@@ -323,32 +360,36 @@ export class CopyOnWriteFS extends FileSystem {
323
360
  * Helper function:
324
361
  * - Ensures p is on writable before proceeding. Throws an error if it doesn't exist.
325
362
  * - Calls f to perform operation on writable.
363
+ *
364
+ * `keep` is the size the file is about to be truncated to, when the caller knows it.
365
+ * Bytes past it are discarded by the operation that follows, so they are not worth copying up.
326
366
  */
327
- copyForWriteSync(path) {
367
+ copyForWriteSync(path, keep) {
328
368
  if (!this.existsSync(path))
329
369
  throw withErrno('ENOENT');
330
370
  if (!this.writable.existsSync(dirname(path))) {
331
371
  this.createParentDirectoriesSync(path);
332
372
  }
333
373
  if (!this.writable.existsSync(path)) {
334
- this.copyToWritableSync(path);
374
+ this.copyToWritableSync(path, keep);
335
375
  }
336
376
  }
337
- async copyForWrite(path) {
377
+ async copyForWrite(path, keep) {
338
378
  if (!(await this.exists(path)))
339
379
  throw withErrno('ENOENT');
340
380
  if (!(await this.writable.exists(dirname(path)))) {
341
381
  await this.createParentDirectories(path);
342
382
  }
343
383
  if (!(await this.writable.exists(path))) {
344
- return this.copyToWritable(path);
384
+ return this.copyToWritable(path, keep);
345
385
  }
346
386
  }
347
387
  /**
348
388
  * Copy from readable to writable storage.
389
+ * Only `[0, keep)` is copied; the caller is responsible for setting the final size.
349
390
  * PRECONDITION: File does not exist on writable storage.
350
391
  */
351
- copyToWritableSync(path) {
392
+ copyToWritableSync(path, keep) {
352
393
  const stats = this.readable.statSync(path);
353
394
  if (isDirectory(stats)) {
354
395
  this.writable.mkdirSync(path, stats);
@@ -357,13 +398,16 @@ export class CopyOnWriteFS extends FileSystem {
357
398
  }
358
399
  return;
359
400
  }
360
- const data = new Uint8Array(stats.size);
361
- this.readable.readSync(path, data, 0, data.byteLength);
401
+ const size = Math.min(stats.size, keep ?? stats.size);
402
+ const data = new Uint8Array(size);
403
+ if (size)
404
+ this.readable.readSync(path, data, 0, size);
362
405
  this.writable.createFileSync(path, stats);
363
406
  this.writable.touchSync(path, stats);
364
- this.writable.writeSync(path, data, 0);
407
+ if (size)
408
+ this.writable.writeSync(path, data, 0);
365
409
  }
366
- async copyToWritable(path) {
410
+ async copyToWritable(path, keep) {
367
411
  const stats = await this.readable.stat(path);
368
412
  if (isDirectory(stats)) {
369
413
  await this.writable.mkdir(path, stats);
@@ -372,11 +416,14 @@ export class CopyOnWriteFS extends FileSystem {
372
416
  }
373
417
  return;
374
418
  }
375
- const data = new Uint8Array(stats.size);
376
- await this.readable.read(path, data, 0, stats.size);
419
+ const size = Math.min(stats.size, keep ?? stats.size);
420
+ const data = new Uint8Array(size);
421
+ if (size)
422
+ await this.readable.read(path, data, 0, size);
377
423
  await this.writable.createFile(path, stats);
378
424
  await this.writable.touch(path, stats);
379
- await this.writable.write(path, data, 0);
425
+ if (size)
426
+ await this.writable.write(path, data, 0);
380
427
  }
381
428
  }
382
429
  const _CopyOnWrite = {
@@ -39,6 +39,10 @@ export declare class FetchFS extends IndexFS {
39
39
  * @internal @hidden
40
40
  */
41
41
  _asyncDone: Promise<unknown>;
42
+ /**
43
+ * Queue an async operation started from a sync method.
44
+ * Failures aren't rethrown to prevent further async ops from failing
45
+ */
42
46
  protected _async(p: Promise<unknown>): void;
43
47
  constructor(index: Index, baseUrl: string, requestInit?: RequestInit, remoteWrite?: boolean | undefined);
44
48
  protected remove(path: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { withErrno } from 'kerium';
3
- import { err, warn } from 'kerium/log';
3
+ import { debug, err, warn } from 'kerium/log';
4
4
  import { decodeUTF8 } from 'utilium';
5
5
  import * as requests from 'utilium/requests';
6
6
  import { Index } from '../internal/file_index.js';
@@ -35,8 +35,12 @@ export class FetchFS extends IndexFS {
35
35
  * @internal @hidden
36
36
  */
37
37
  _asyncDone = Promise.resolve();
38
+ /**
39
+ * Queue an async operation started from a sync method.
40
+ * Failures aren't rethrown to prevent further async ops from failing
41
+ */
38
42
  _async(p) {
39
- this._asyncDone = this._asyncDone.then(() => p);
43
+ this._asyncDone = this._asyncDone.then(() => p).catch(e => debug('Async operation failed: ' + (e.message ?? e)));
40
44
  }
41
45
  constructor(index, baseUrl, requestInit = {}, remoteWrite) {
42
46
  super(0x206e6673, 'nfs', index);
@@ -74,7 +78,7 @@ export class FetchFS extends IndexFS {
74
78
  if (!data)
75
79
  throw withErrno('ENODATA');
76
80
  if (missing.length) {
77
- this._async(requests.get(this.baseUrl + path, { start: offset, end, size: inode.size, warn }));
81
+ this._async(requests.get(this.baseUrl + path, { start: offset, end, size: inode.size, warn }, this.requestInit));
78
82
  throw withErrno('EAGAIN');
79
83
  }
80
84
  buffer.set(data);
@@ -5,7 +5,7 @@ import type { UsageInfo } from '../internal/filesystem.js';
5
5
  import { StoreFS } from './store/fs.js';
6
6
  import { SyncMapTransaction, type SyncMapStore } from './store/map.js';
7
7
  import type { Store } from './store/store.js';
8
- type Lock = Disposable & (() => void);
8
+ type SBLock = Disposable & (() => void);
9
9
  declare const MetadataEntry_base: import("memium").StructConstructor<BufferView<any>>;
10
10
  declare class MetadataEntry extends MetadataEntry_base {
11
11
  static name: string;
@@ -52,7 +52,7 @@ export declare class MetadataBlock extends MetadataBlock_base<ArrayBufferLike> {
52
52
  * Wait for the block to be unlocked.
53
53
  */
54
54
  waitUnlocked(depth?: number): void;
55
- lock(): Lock;
55
+ lock(): SBLock;
56
56
  }
57
57
  declare const SuperBlock_base: import("memium/decorators").StructFromTypedArray<BigUint64Array<ArrayBuffer>>;
58
58
  /**
@@ -63,18 +63,18 @@ export declare class StoreFS<T extends Store = Store> extends FileSystem {
63
63
  */
64
64
  rename(oldPath: string, newPath: string): Promise<void>;
65
65
  renameSync(oldPath: string, newPath: string): void;
66
- stat(path: string): Promise<InodeLike>;
67
- statSync(path: string): InodeLike;
66
+ stat(path: string): Promise<Inode>;
67
+ statSync(path: string): Inode;
68
68
  touch(path: string, metadata: Partial<InodeLike>): Promise<void>;
69
69
  touchSync(path: string, metadata: Partial<InodeLike>): void;
70
- createFile(path: string, options: CreationOptions): Promise<InodeLike>;
71
- createFileSync(path: string, options: CreationOptions): InodeLike;
70
+ createFile(path: string, options: CreationOptions): Promise<Inode>;
71
+ createFileSync(path: string, options: CreationOptions): Inode;
72
72
  unlink(path: string): Promise<void>;
73
73
  unlinkSync(path: string): void;
74
74
  rmdir(path: string): Promise<void>;
75
75
  rmdirSync(path: string): void;
76
- mkdir(path: string, options: CreationOptions): Promise<InodeLike>;
77
- mkdirSync(path: string, options: CreationOptions): InodeLike;
76
+ mkdir(path: string, options: CreationOptions): Promise<Inode>;
77
+ mkdirSync(path: string, options: CreationOptions): Inode;
78
78
  readdir(path: string): Promise<string[]>;
79
79
  readdirSync(path: string): string[];
80
80
  /**
@@ -614,13 +614,17 @@ export class StoreFS extends FileSystem {
614
614
  try {
615
615
  const tx = __addDisposableResource(env_17, this.transaction(), true);
616
616
  const inode = await this.findInode(tx, path);
617
+ const end = offset + data.byteLength;
617
618
  let buffer = data;
618
619
  if (!tx.flag('partial')) {
619
- buffer = extendBuffer((await tx.get(inode.data)) ?? new Uint8Array(), offset + data.byteLength);
620
+ buffer = extendBuffer((await tx.get(inode.data)) ?? new Uint8Array(), end);
620
621
  buffer.set(data, offset);
621
622
  offset = 0;
622
623
  }
623
624
  await tx.set(inode.data, buffer, offset);
625
+ // Update the inode in the same transaction, so data and metadata can not get out of sync (#286)
626
+ inode.update({ size: Math.max(inode.size, end), mtimeMs: Date.now() });
627
+ await tx.set(inode.ino, inode);
624
628
  this._add(inode.ino, path);
625
629
  await tx.commit();
626
630
  }
@@ -639,13 +643,17 @@ export class StoreFS extends FileSystem {
639
643
  try {
640
644
  const tx = __addDisposableResource(env_18, this.transaction(), false);
641
645
  const inode = this.findInodeSync(tx, path);
646
+ const end = offset + data.byteLength;
642
647
  let buffer = data;
643
648
  if (!tx.flag('partial')) {
644
- buffer = extendBuffer(tx.getSync(inode.data) ?? new Uint8Array(), offset + data.byteLength);
649
+ buffer = extendBuffer(tx.getSync(inode.data) ?? new Uint8Array(), end);
645
650
  buffer.set(data, offset);
646
651
  offset = 0;
647
652
  }
648
653
  tx.setSync(inode.data, buffer, offset);
654
+ // Update the inode in the same transaction, so data and metadata can not get out of sync (#286)
655
+ inode.update({ size: Math.max(inode.size, end), mtimeMs: Date.now() });
656
+ tx.setSync(inode.ino, inode);
649
657
  this._add(inode.ino, path);
650
658
  tx.commitSync();
651
659
  }
@@ -103,18 +103,18 @@ export declare class DeviceFS extends StoreFS<InMemoryStore> {
103
103
  constructor();
104
104
  rename(oldPath: string, newPath: string): Promise<void>;
105
105
  renameSync(oldPath: string, newPath: string): void;
106
- stat(path: string): Promise<InodeLike>;
107
- statSync(path: string): InodeLike;
106
+ stat(path: string): Promise<Inode>;
107
+ statSync(path: string): Inode;
108
108
  touch(path: string, metadata: InodeLike): Promise<void>;
109
109
  touchSync(path: string, metadata: InodeLike): void;
110
- createFile(path: string, options: CreationOptions): Promise<InodeLike>;
111
- createFileSync(path: string, options: CreationOptions): InodeLike;
110
+ createFile(path: string, options: CreationOptions): Promise<Inode>;
111
+ createFileSync(path: string, options: CreationOptions): Inode;
112
112
  unlink(path: string): Promise<void>;
113
113
  unlinkSync(path: string): void;
114
114
  rmdir(path: string): Promise<void>;
115
115
  rmdirSync(path: string): void;
116
- mkdir(path: string, options: CreationOptions): Promise<InodeLike>;
117
- mkdirSync(path: string, options: CreationOptions): InodeLike;
116
+ mkdir(path: string, options: CreationOptions): Promise<Inode>;
117
+ mkdirSync(path: string, options: CreationOptions): Inode;
118
118
  readdir(path: string): Promise<string[]>;
119
119
  readdirSync(path: string): string[];
120
120
  link(target: string, link: string): Promise<void>;
@@ -28,12 +28,12 @@ export declare abstract class IndexFS extends FileSystem {
28
28
  rmdir(path: string): Promise<void>;
29
29
  rmdirSync(path: string): void;
30
30
  protected create(path: string, options: CreationOptions): Inode;
31
- createFile(path: string, options: CreationOptions): Promise<InodeLike>;
32
- createFileSync(path: string, options: CreationOptions): InodeLike;
31
+ createFile(path: string, options: CreationOptions): Promise<Inode>;
32
+ createFileSync(path: string, options: CreationOptions): Inode;
33
33
  protected _mkdir?(path: string, options: CreationOptions): Promise<void>;
34
34
  protected _mkdirSync?(path: string, options: CreationOptions): void;
35
- mkdir(path: string, options: CreationOptions): Promise<InodeLike>;
36
- mkdirSync(path: string, options: CreationOptions): InodeLike;
35
+ mkdir(path: string, options: CreationOptions): Promise<Inode>;
36
+ mkdirSync(path: string, options: CreationOptions): Inode;
37
37
  link(target: string, link: string): Promise<void>;
38
38
  linkSync(target: string, link: string): void;
39
39
  readdir(path: string): Promise<string[]>;
@@ -373,7 +373,7 @@ export function watchFile(path, options, listener) {
373
373
  listener(curr, prev);
374
374
  }
375
375
  });
376
- statWatchers.set(normalizedPath, { watcher, listeners: new Set() });
376
+ statWatchers.set(normalizedPath, { watcher, listeners: new Set([listener]) });
377
377
  }
378
378
  watchFile;
379
379
  /**
@@ -22,9 +22,11 @@ export declare class Dirent<Name extends string | Buffer = string> implements _D
22
22
  */
23
23
  get path(): string;
24
24
  /**
25
+ * `vfs.path` is relative to the directory that was read,
26
+ * so `parentPath` is resolved against it— like Node does.
25
27
  * @internal
26
28
  */
27
- static from(vfs: VFSDirent, encoding?: BufferEncoding | 'buffer' | null): Dirent<string>;
29
+ static from(vfs: VFSDirent, encoding?: BufferEncoding | 'buffer' | null, parentPath?: string): Dirent<string>;
28
30
  isFile(): boolean;
29
31
  isDirectory(): boolean;
30
32
  isBlockDevice(): boolean;
package/dist/node/dir.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Buffer } from 'buffer';
2
- import { withErrno } from 'kerium';
3
2
  import { warn } from 'kerium/log';
4
- import { parse } from '../path.js';
3
+ import { join, parse } from '../path.js';
5
4
  import { DirType } from '../vfs/dir.js';
6
5
  import { readdir } from './promises.js';
7
6
  import { readdirSync } from './sync.js';
@@ -32,12 +31,14 @@ export class Dirent {
32
31
  return this._parentPath;
33
32
  }
34
33
  /**
34
+ * `vfs.path` is relative to the directory that was read,
35
+ * so `parentPath` is resolved against it— like Node does.
35
36
  * @internal
36
37
  */
37
- static from(vfs, encoding) {
38
+ static from(vfs, encoding, parentPath = '.') {
38
39
  const dirent = new Dirent();
39
40
  const { base, dir } = parse(vfs.path);
40
- dirent._parentPath = dir || '.';
41
+ dirent._parentPath = join(parentPath, dir);
41
42
  dirent._name = base;
42
43
  dirent.ino = vfs.ino;
43
44
  dirent.type = vfs.type;
@@ -66,6 +67,14 @@ export class Dirent {
66
67
  return this.type === DirType.SOCK;
67
68
  }
68
69
  }
70
+ /**
71
+ * Thrown when a `Dir` is used after it has been closed.
72
+ * Note this is a plain `Error` with a Node error code, not an `Exception`.
73
+ * @internal @hidden
74
+ */
75
+ function dirClosed() {
76
+ return Object.assign(new Error('Directory handle was closed'), { code: 'ERR_DIR_CLOSED' });
77
+ }
69
78
  /**
70
79
  * A class representing a directory stream.
71
80
  */
@@ -75,7 +84,7 @@ export class Dir {
75
84
  closed = false;
76
85
  checkClosed() {
77
86
  if (this.closed)
78
- throw withErrno('EBADF', 'Can not use closed Dir');
87
+ throw dirClosed();
79
88
  }
80
89
  _entries;
81
90
  constructor(path, context) {
@@ -83,6 +92,13 @@ export class Dir {
83
92
  this.context = context;
84
93
  }
85
94
  close(cb) {
95
+ if (this.closed) {
96
+ const error = dirClosed();
97
+ if (!cb)
98
+ return Promise.reject(error);
99
+ cb(error);
100
+ return;
101
+ }
86
102
  this.closed = true;
87
103
  if (!cb) {
88
104
  return Promise.resolve();
@@ -94,6 +110,8 @@ export class Dir {
94
110
  * Subsequent reads will result in errors.
95
111
  */
96
112
  closeSync() {
113
+ if (this.closed)
114
+ throw dirClosed();
97
115
  this.closed = true;
98
116
  }
99
117
  async _read() {
@@ -18,8 +18,10 @@ import { ReadStream, WriteStream } from './streams.js';
18
18
  export * as constants from '../constants.js';
19
19
  export declare class FileHandle implements promises.FileHandle {
20
20
  protected context: V_Context;
21
- readonly fd: number;
22
21
  protected vfs: Handle;
22
+ protected _fd: number;
23
+ /** The file descriptor, or -1 once the handle has been closed */
24
+ get fd(): number;
23
25
  constructor(context: V_Context, fd: number);
24
26
  private _emitChange;
25
27
  /**