@zenfs/core 0.9.7 → 0.10.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 (71) hide show
  1. package/dist/backends/AsyncStore.js +29 -29
  2. package/dist/backends/Fetch.d.ts +84 -0
  3. package/dist/backends/Fetch.js +171 -0
  4. package/dist/backends/Index.js +19 -19
  5. package/dist/backends/Locked.d.ts +11 -11
  6. package/dist/backends/Locked.js +50 -49
  7. package/dist/backends/Overlay.js +21 -21
  8. package/dist/backends/SyncStore.js +27 -27
  9. package/dist/backends/backend.js +4 -4
  10. package/dist/backends/port/fs.d.ts +124 -0
  11. package/dist/backends/port/fs.js +241 -0
  12. package/dist/backends/port/rpc.d.ts +60 -0
  13. package/dist/backends/port/rpc.js +71 -0
  14. package/dist/backends/port/store.d.ts +30 -0
  15. package/dist/backends/port/store.js +142 -0
  16. package/dist/browser.min.js +4 -4
  17. package/dist/browser.min.js.map +4 -4
  18. package/dist/config.d.ts +8 -10
  19. package/dist/config.js +11 -11
  20. package/dist/emulation/async.js +6 -6
  21. package/dist/emulation/dir.js +2 -2
  22. package/dist/emulation/index.d.ts +1 -1
  23. package/dist/emulation/index.js +1 -1
  24. package/dist/emulation/path.d.ts +3 -2
  25. package/dist/emulation/path.js +19 -45
  26. package/dist/emulation/promises.d.ts +7 -12
  27. package/dist/emulation/promises.js +144 -146
  28. package/dist/emulation/shared.d.ts +5 -10
  29. package/dist/emulation/shared.js +8 -8
  30. package/dist/emulation/streams.js +3 -3
  31. package/dist/emulation/sync.js +25 -25
  32. package/dist/{ApiError.d.ts → error.d.ts} +13 -14
  33. package/dist/error.js +292 -0
  34. package/dist/file.d.ts +2 -0
  35. package/dist/file.js +10 -4
  36. package/dist/filesystem.js +15 -15
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.js +4 -1
  39. package/dist/mutex.js +2 -1
  40. package/dist/utils.d.ts +8 -7
  41. package/dist/utils.js +11 -12
  42. package/package.json +3 -3
  43. package/readme.md +17 -9
  44. package/src/backends/AsyncStore.ts +29 -29
  45. package/src/backends/Fetch.ts +230 -0
  46. package/src/backends/Index.ts +19 -19
  47. package/src/backends/Locked.ts +50 -49
  48. package/src/backends/Overlay.ts +23 -23
  49. package/src/backends/SyncStore.ts +27 -27
  50. package/src/backends/backend.ts +6 -6
  51. package/src/backends/port/fs.ts +308 -0
  52. package/src/backends/port/readme.md +59 -0
  53. package/src/backends/port/rpc.ts +144 -0
  54. package/src/backends/port/store.ts +187 -0
  55. package/src/config.ts +20 -24
  56. package/src/emulation/async.ts +6 -6
  57. package/src/emulation/dir.ts +2 -2
  58. package/src/emulation/index.ts +1 -1
  59. package/src/emulation/path.ts +25 -49
  60. package/src/emulation/promises.ts +150 -159
  61. package/src/emulation/shared.ts +12 -14
  62. package/src/emulation/streams.ts +3 -3
  63. package/src/emulation/sync.ts +28 -28
  64. package/src/{ApiError.ts → error.ts} +89 -89
  65. package/src/file.ts +12 -4
  66. package/src/filesystem.ts +15 -15
  67. package/src/index.ts +4 -1
  68. package/src/mutex.ts +3 -1
  69. package/src/utils.ts +16 -18
  70. package/tsconfig.json +2 -2
  71. package/dist/ApiError.js +0 -292
@@ -0,0 +1,230 @@
1
+ import { ErrnoError, Errno } from '../error.js';
2
+ import { NoSyncFile } from '../file.js';
3
+ import type { FileSystemMetadata } from '../filesystem.js';
4
+ import { Stats } from '../stats.js';
5
+ import { type ListingTree, FileIndex, type IndexFileInode, AsyncIndexFS } from './Index.js';
6
+ import type { Backend } from './backend.js';
7
+
8
+ /**
9
+ * @hidden
10
+ */
11
+ function convertError(e: Error): never {
12
+ throw new ErrnoError(Errno.EIO, e.message);
13
+ }
14
+
15
+ /**
16
+ * Asynchronously download a file as a buffer or a JSON object.
17
+ * Note that the third function signature with a non-specialized type is
18
+ * invalid, but TypeScript requires it when you specialize string arguments to
19
+ * constants.
20
+ * @hidden
21
+ */
22
+ async function fetchFile(p: string, type: 'buffer'): Promise<Uint8Array>;
23
+ async function fetchFile<T extends object>(p: string, type: 'json'): Promise<T>;
24
+ async function fetchFile<T extends object>(p: string, type: 'buffer' | 'json'): Promise<T | Uint8Array>;
25
+ async function fetchFile<T extends object>(p: string, type: 'buffer' | 'json'): Promise<T | Uint8Array> {
26
+ const response = await fetch(p).catch(convertError);
27
+ if (!response.ok) {
28
+ throw new ErrnoError(Errno.EIO, 'fetch failed: response returned code ' + response.status);
29
+ }
30
+ switch (type) {
31
+ case 'buffer':
32
+ const arrayBuffer = await response.arrayBuffer().catch(convertError);
33
+ return new Uint8Array(arrayBuffer);
34
+ case 'json':
35
+ return response.json().catch(convertError) as Promise<T>;
36
+ default:
37
+ throw new ErrnoError(Errno.EINVAL, 'Invalid download type: ' + type);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Asynchronously retrieves the size of the given file in bytes.
43
+ * @hidden
44
+ */
45
+ async function fetchSize(p: string): Promise<number> {
46
+ const response = await fetch(p, { method: 'HEAD' }).catch(convertError);
47
+ if (!response.ok) {
48
+ throw new ErrnoError(Errno.EIO, 'fetch failed: HEAD response returned code ' + response.status);
49
+ }
50
+ return parseInt(response.headers.get('Content-Length') || '-1', 10);
51
+ }
52
+
53
+ /**
54
+ * Configuration options for FetchFS.
55
+ */
56
+ export interface FetchOptions {
57
+ /**
58
+ * URL to a file index as a JSON file or the file index object itself.
59
+ * Defaults to `index.json`.
60
+ */
61
+ index?: string | ListingTree;
62
+
63
+ /** Used as the URL prefix for fetched files.
64
+ * Default: Fetch files relative to the index.
65
+ */
66
+ baseUrl?: string;
67
+ }
68
+
69
+ /**
70
+ * A simple filesystem backed by HTTP using the fetch API.
71
+ *
72
+ *
73
+ * Listings objects look like the following:
74
+ *
75
+ * ```json
76
+ * {
77
+ * "home": {
78
+ * "jvilk": {
79
+ * "someFile.txt": null,
80
+ * "someDir": {
81
+ * // Empty directory
82
+ * }
83
+ * }
84
+ * }
85
+ * }
86
+ * ```
87
+ *
88
+ * This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.
89
+ */
90
+ export class FetchFS extends AsyncIndexFS<Stats> {
91
+ public readonly prefixUrl: string;
92
+
93
+ protected _init: Promise<void>;
94
+
95
+ protected async _initialize(index: string | ListingTree): Promise<void> {
96
+ if (typeof index != 'string') {
97
+ this._index = FileIndex.FromListing(index);
98
+ return;
99
+ }
100
+
101
+ try {
102
+ const response = await fetch(index);
103
+ this._index = FileIndex.FromListing((await response.json()) as ListingTree);
104
+ } catch (e) {
105
+ throw new ErrnoError(Errno.EINVAL, 'Invalid or unavailable file listing tree');
106
+ }
107
+ }
108
+
109
+ public async ready(): Promise<this> {
110
+ await this._init;
111
+ return this;
112
+ }
113
+
114
+ constructor({ index = 'index.json', baseUrl = '' }: FetchOptions) {
115
+ super({});
116
+
117
+ // prefix url must end in a directory separator.
118
+ if (baseUrl.at(-1) != '/') {
119
+ baseUrl += '/';
120
+ }
121
+ this.prefixUrl = baseUrl;
122
+
123
+ this._init = this._initialize(index);
124
+ }
125
+
126
+ public metadata(): FileSystemMetadata {
127
+ return {
128
+ ...super.metadata(),
129
+ name: FetchFS.name,
130
+ readonly: true,
131
+ };
132
+ }
133
+
134
+ public empty(): void {
135
+ for (const file of this._index.files()) {
136
+ delete file.data!.fileData;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Special function: Preload the given file into the index.
142
+ * @param path
143
+ * @param buffer
144
+ */
145
+ public preloadFile(path: string, buffer: Uint8Array): void {
146
+ const inode = this._index.get(path)!;
147
+ if (!inode) {
148
+ throw ErrnoError.With('ENOENT', path, 'preloadFile');
149
+ }
150
+ if (!inode.isFile()) {
151
+ throw ErrnoError.With('EISDIR', path, 'preloadFile');
152
+ }
153
+ const stats = inode.data!;
154
+ stats.size = buffer.length;
155
+ stats.fileData = buffer;
156
+ }
157
+
158
+ protected async statFileInode(inode: IndexFileInode<Stats>, path: string): Promise<Stats> {
159
+ const stats = inode.data!;
160
+ // At this point, a non-opened file will still have default stats from the listing.
161
+ if (stats.size < 0) {
162
+ stats.size = await this._fetchSize(path);
163
+ }
164
+
165
+ return stats;
166
+ }
167
+
168
+ protected async openFileInode(inode: IndexFileInode<Stats>, path: string, flag: string): Promise<NoSyncFile<this>> {
169
+ const stats = inode.data!;
170
+ // Use existing file contents. This maintains the previously-used flag.
171
+ if (stats.fileData) {
172
+ return new NoSyncFile(this, path, flag, new Stats(stats), stats.fileData);
173
+ }
174
+ // @todo be lazier about actually requesting the file
175
+ const data = await this._fetchFile(path, 'buffer');
176
+ // we don't initially have file sizes
177
+ stats.size = data.length;
178
+ stats.fileData = data;
179
+ return new NoSyncFile(this, path, flag, new Stats(stats), data);
180
+ }
181
+
182
+ private _getRemotePath(filePath: string): string {
183
+ if (filePath.charAt(0) === '/') {
184
+ filePath = filePath.slice(1);
185
+ }
186
+ return this.prefixUrl + filePath;
187
+ }
188
+
189
+ /**
190
+ * Asynchronously download the given file.
191
+ */
192
+ protected _fetchFile(p: string, type: 'buffer'): Promise<Uint8Array>;
193
+ protected _fetchFile(p: string, type: 'json'): Promise<object>;
194
+ protected _fetchFile(p: string, type: 'buffer' | 'json'): Promise<object>;
195
+ protected _fetchFile(p: string, type: 'buffer' | 'json'): Promise<object> {
196
+ return fetchFile(this._getRemotePath(p), type);
197
+ }
198
+
199
+ /**
200
+ * Only requests the HEAD content, for the file size.
201
+ */
202
+ protected _fetchSize(path: string): Promise<number> {
203
+ return fetchSize(this._getRemotePath(path));
204
+ }
205
+ }
206
+
207
+ export const Fetch = {
208
+ name: 'Fetch',
209
+
210
+ options: {
211
+ index: {
212
+ type: ['string', 'object'],
213
+ required: false,
214
+ description: 'URL to a file index as a JSON file or the file index object itself, generated with the make_http_index script. Defaults to `index.json`.',
215
+ },
216
+ baseUrl: {
217
+ type: 'string',
218
+ required: false,
219
+ description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
220
+ },
221
+ },
222
+
223
+ isAvailable(): boolean {
224
+ return typeof globalThis.fetch == 'function';
225
+ },
226
+
227
+ create(options: FetchOptions) {
228
+ return new FetchFS(options);
229
+ },
230
+ } as const satisfies Backend<FetchFS, FetchOptions>;
@@ -1,4 +1,4 @@
1
- import { ApiError, ErrorCode } from '../ApiError.js';
1
+ import { ErrnoError, Errno } from '../error.js';
2
2
  import type { Cred } from '../cred.js';
3
3
  import { basename, dirname, join } from '../emulation/path.js';
4
4
  import { NoSyncFile, flagToMode, isWriteable } from '../file.js';
@@ -113,10 +113,10 @@ export class FileIndex<TData> {
113
113
  */
114
114
  public add(path: string, inode: IndexInode<TData>): boolean {
115
115
  if (!inode) {
116
- throw new Error('Inode must be specified');
116
+ throw new ErrnoError(Errno.EINVAL, 'Inode must be specified', path, 'FileIndex.add');
117
117
  }
118
118
  if (!path.startsWith('/')) {
119
- throw new Error('Path must be absolute, got: ' + path);
119
+ throw new ErrnoError(Errno.EINVAL, 'Path not absolute', path, 'FileIndex.add');
120
120
  }
121
121
 
122
122
  // Check if it already exists.
@@ -365,7 +365,7 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
365
365
  public async stat(path: string): Promise<Stats> {
366
366
  const inode = this._index.get(path);
367
367
  if (!inode) {
368
- throw ApiError.With('ENOENT', path, 'stat');
368
+ throw ErrnoError.With('ENOENT', path, 'stat');
369
369
  }
370
370
 
371
371
  if (inode.isDirectory()) {
@@ -376,13 +376,13 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
376
376
  return this.statFileInode(inode, path);
377
377
  }
378
378
 
379
- throw new ApiError(ErrorCode.EINVAL, 'Invalid inode.');
379
+ throw new ErrnoError(Errno.EINVAL, 'Invalid inode.');
380
380
  }
381
381
 
382
382
  public statSync(path: string): Stats {
383
383
  const inode = this._index.get(path);
384
384
  if (!inode) {
385
- throw ApiError.With('ENOENT', path, 'stat');
385
+ throw ErrnoError.With('ENOENT', path, 'stat');
386
386
  }
387
387
 
388
388
  if (inode.isDirectory()) {
@@ -393,24 +393,24 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
393
393
  return this.statFileInodeSync(inode, path);
394
394
  }
395
395
 
396
- throw new ApiError(ErrorCode.EINVAL, 'Invalid inode.');
396
+ throw new ErrnoError(Errno.EINVAL, 'Invalid inode.');
397
397
  }
398
398
 
399
399
  public async openFile(path: string, flag: string, cred: Cred): Promise<NoSyncFile<this>> {
400
400
  if (isWriteable(flag)) {
401
401
  // You can't write to files on this file system.
402
- throw new ApiError(ErrorCode.EPERM, path);
402
+ throw new ErrnoError(Errno.EPERM, path);
403
403
  }
404
404
 
405
405
  // Check if the path exists, and is a file.
406
406
  const inode = this._index.get(path);
407
407
 
408
408
  if (!inode) {
409
- throw ApiError.With('ENOENT', path, 'openFile');
409
+ throw ErrnoError.With('ENOENT', path, 'openFile');
410
410
  }
411
411
 
412
412
  if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
413
- throw ApiError.With('EACCES', path, 'openFile');
413
+ throw ErrnoError.With('EACCES', path, 'openFile');
414
414
  }
415
415
 
416
416
  if (inode.isDirectory()) {
@@ -424,18 +424,18 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
424
424
  public openFileSync(path: string, flag: string, cred: Cred): NoSyncFile<this> {
425
425
  if (isWriteable(flag)) {
426
426
  // You can't write to files on this file system.
427
- throw new ApiError(ErrorCode.EPERM, path);
427
+ throw new ErrnoError(Errno.EPERM, path);
428
428
  }
429
429
 
430
430
  // Check if the path exists, and is a file.
431
431
  const inode = this._index.get(path);
432
432
 
433
433
  if (!inode) {
434
- throw ApiError.With('ENOENT', path, 'openFile');
434
+ throw ErrnoError.With('ENOENT', path, 'openFile');
435
435
  }
436
436
 
437
437
  if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
438
- throw ApiError.With('EACCES', path, 'openFile');
438
+ throw ErrnoError.With('EACCES', path, 'openFile');
439
439
  }
440
440
 
441
441
  if (inode.isDirectory()) {
@@ -450,28 +450,28 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
450
450
  // Check if it exists.
451
451
  const inode = this._index.get(path);
452
452
  if (!inode) {
453
- throw ApiError.With('ENOENT', path, 'readdir');
453
+ throw ErrnoError.With('ENOENT', path, 'readdir');
454
454
  }
455
455
 
456
456
  if (inode.isDirectory()) {
457
457
  return inode.listing;
458
458
  }
459
459
 
460
- throw ApiError.With('ENOTDIR', path, 'readdir');
460
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
461
461
  }
462
462
 
463
463
  public readdirSync(path: string): string[] {
464
464
  // Check if it exists.
465
465
  const inode = this._index.get(path);
466
466
  if (!inode) {
467
- throw ApiError.With('ENOENT', path, 'readdir');
467
+ throw ErrnoError.With('ENOENT', path, 'readdir');
468
468
  }
469
469
 
470
470
  if (inode.isDirectory()) {
471
471
  return inode.listing;
472
472
  }
473
473
 
474
- throw ApiError.With('ENOTDIR', path, 'readdir');
474
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
475
475
  }
476
476
 
477
477
  protected abstract statFileInode(inode: IndexFileInode<TData>, path: string): Promise<Stats>;
@@ -495,10 +495,10 @@ export abstract class SyncIndexFS<TData> extends IndexFS<TData> {
495
495
 
496
496
  export abstract class AsyncIndexFS<TData> extends IndexFS<TData> {
497
497
  protected statFileInodeSync(inode: IndexFileInode<TData>, path: string): Stats {
498
- throw ApiError.With('ENOSYS', path, 'AsyncIndexFS.statFileInodeSync');
498
+ throw ErrnoError.With('ENOSYS', path, 'AsyncIndexFS.statFileInodeSync');
499
499
  }
500
500
 
501
501
  protected openFileInodeSync(inode: IndexFileInode<TData>, path: string, flag: string): NoSyncFile<this> {
502
- throw ApiError.With('ENOSYS', path, 'AsyncIndexFS.openFileInodeSync');
502
+ throw ErrnoError.With('ENOSYS', path, 'AsyncIndexFS.openFileInodeSync');
503
503
  }
504
504
  }
@@ -1,3 +1,4 @@
1
+ import { ErrnoError } from '../error.js';
1
2
  import type { Cred } from '../cred.js';
2
3
  import type { File } from '../file.js';
3
4
  import type { FileSystem, FileSystemMetadata } from '../filesystem.js';
@@ -39,23 +40,23 @@ export class LockedFS<FS extends FileSystem> implements FileSystem {
39
40
 
40
41
  public renameSync(oldPath: string, newPath: string, cred: Cred): void {
41
42
  if (this._mu.isLocked(oldPath)) {
42
- throw new Error('invalid sync call');
43
+ throw ErrnoError.With('EBUSY', oldPath, 'rename');
43
44
  }
44
45
  return this.fs.renameSync(oldPath, newPath, cred);
45
46
  }
46
47
 
47
- public async stat(p: string, cred: Cred): Promise<Stats> {
48
- await this._mu.lock(p);
49
- const stats = await this.fs.stat(p, cred);
50
- this._mu.unlock(p);
48
+ public async stat(path: string, cred: Cred): Promise<Stats> {
49
+ await this._mu.lock(path);
50
+ const stats = await this.fs.stat(path, cred);
51
+ this._mu.unlock(path);
51
52
  return stats;
52
53
  }
53
54
 
54
- public statSync(p: string, cred: Cred): Stats {
55
- if (this._mu.isLocked(p)) {
56
- throw new Error('invalid sync call');
55
+ public statSync(path: string, cred: Cred): Stats {
56
+ if (this._mu.isLocked(path)) {
57
+ throw ErrnoError.With('EBUSY', path, 'stat');
57
58
  }
58
- return this.fs.statSync(p, cred);
59
+ return this.fs.statSync(path, cred);
59
60
  }
60
61
 
61
62
  public async openFile(path: string, flag: string, cred: Cred): Promise<File> {
@@ -67,7 +68,7 @@ export class LockedFS<FS extends FileSystem> implements FileSystem {
67
68
 
68
69
  public openFileSync(path: string, flag: string, cred: Cred): File {
69
70
  if (this._mu.isLocked(path)) {
70
- throw new Error('invalid sync call');
71
+ throw ErrnoError.With('EBUSY', path, 'openFile');
71
72
  }
72
73
  return this.fs.openFileSync(path, flag, cred);
73
74
  }
@@ -81,7 +82,7 @@ export class LockedFS<FS extends FileSystem> implements FileSystem {
81
82
 
82
83
  public createFileSync(path: string, flag: string, mode: number, cred: Cred): File {
83
84
  if (this._mu.isLocked(path)) {
84
- throw new Error('invalid sync call');
85
+ throw ErrnoError.With('EBUSY', path, 'createFile');
85
86
  }
86
87
  return this.fs.createFileSync(path, flag, mode, cred);
87
88
  }
@@ -92,65 +93,65 @@ export class LockedFS<FS extends FileSystem> implements FileSystem {
92
93
  this._mu.unlock(p);
93
94
  }
94
95
 
95
- public unlinkSync(p: string, cred: Cred): void {
96
- if (this._mu.isLocked(p)) {
97
- throw new Error('invalid sync call');
96
+ public unlinkSync(path: string, cred: Cred): void {
97
+ if (this._mu.isLocked(path)) {
98
+ throw ErrnoError.With('EBUSY', path, 'unlink');
98
99
  }
99
- return this.fs.unlinkSync(p, cred);
100
+ return this.fs.unlinkSync(path, cred);
100
101
  }
101
102
 
102
- public async rmdir(p: string, cred: Cred): Promise<void> {
103
- await this._mu.lock(p);
104
- await this.fs.rmdir(p, cred);
105
- this._mu.unlock(p);
103
+ public async rmdir(path: string, cred: Cred): Promise<void> {
104
+ await this._mu.lock(path);
105
+ await this.fs.rmdir(path, cred);
106
+ this._mu.unlock(path);
106
107
  }
107
108
 
108
- public rmdirSync(p: string, cred: Cred): void {
109
- if (this._mu.isLocked(p)) {
110
- throw new Error('invalid sync call');
109
+ public rmdirSync(path: string, cred: Cred): void {
110
+ if (this._mu.isLocked(path)) {
111
+ throw ErrnoError.With('EBUSY', path, 'rmdir');
111
112
  }
112
- return this.fs.rmdirSync(p, cred);
113
+ return this.fs.rmdirSync(path, cred);
113
114
  }
114
115
 
115
- public async mkdir(p: string, mode: number, cred: Cred): Promise<void> {
116
- await this._mu.lock(p);
117
- await this.fs.mkdir(p, mode, cred);
118
- this._mu.unlock(p);
116
+ public async mkdir(path: string, mode: number, cred: Cred): Promise<void> {
117
+ await this._mu.lock(path);
118
+ await this.fs.mkdir(path, mode, cred);
119
+ this._mu.unlock(path);
119
120
  }
120
121
 
121
- public mkdirSync(p: string, mode: number, cred: Cred): void {
122
- if (this._mu.isLocked(p)) {
123
- throw new Error('invalid sync call');
122
+ public mkdirSync(path: string, mode: number, cred: Cred): void {
123
+ if (this._mu.isLocked(path)) {
124
+ throw ErrnoError.With('EBUSY', path, 'mkdir');
124
125
  }
125
- return this.fs.mkdirSync(p, mode, cred);
126
+ return this.fs.mkdirSync(path, mode, cred);
126
127
  }
127
128
 
128
- public async readdir(p: string, cred: Cred): Promise<string[]> {
129
- await this._mu.lock(p);
130
- const files = await this.fs.readdir(p, cred);
131
- this._mu.unlock(p);
129
+ public async readdir(path: string, cred: Cred): Promise<string[]> {
130
+ await this._mu.lock(path);
131
+ const files = await this.fs.readdir(path, cred);
132
+ this._mu.unlock(path);
132
133
  return files;
133
134
  }
134
135
 
135
- public readdirSync(p: string, cred: Cred): string[] {
136
- if (this._mu.isLocked(p)) {
137
- throw new Error('invalid sync call');
136
+ public readdirSync(path: string, cred: Cred): string[] {
137
+ if (this._mu.isLocked(path)) {
138
+ throw ErrnoError.With('EBUSY', path, 'readdir');
138
139
  }
139
- return this.fs.readdirSync(p, cred);
140
+ return this.fs.readdirSync(path, cred);
140
141
  }
141
142
 
142
- public async exists(p: string, cred: Cred): Promise<boolean> {
143
- await this._mu.lock(p);
144
- const exists = await this.fs.exists(p, cred);
145
- this._mu.unlock(p);
143
+ public async exists(path: string, cred: Cred): Promise<boolean> {
144
+ await this._mu.lock(path);
145
+ const exists = await this.fs.exists(path, cred);
146
+ this._mu.unlock(path);
146
147
  return exists;
147
148
  }
148
149
 
149
- public existsSync(p: string, cred: Cred): boolean {
150
- if (this._mu.isLocked(p)) {
151
- throw new Error('invalid sync call');
150
+ public existsSync(path: string, cred: Cred): boolean {
151
+ if (this._mu.isLocked(path)) {
152
+ throw ErrnoError.With('EBUSY', path, 'exists');
152
153
  }
153
- return this.fs.existsSync(p, cred);
154
+ return this.fs.existsSync(path, cred);
154
155
  }
155
156
 
156
157
  public async link(srcpath: string, dstpath: string, cred: Cred): Promise<void> {
@@ -161,7 +162,7 @@ export class LockedFS<FS extends FileSystem> implements FileSystem {
161
162
 
162
163
  public linkSync(srcpath: string, dstpath: string, cred: Cred): void {
163
164
  if (this._mu.isLocked(srcpath)) {
164
- throw new Error('invalid sync call');
165
+ throw ErrnoError.With('EBUSY', srcpath, 'link');
165
166
  }
166
167
  return this.fs.linkSync(srcpath, dstpath, cred);
167
168
  }
@@ -174,7 +175,7 @@ export class LockedFS<FS extends FileSystem> implements FileSystem {
174
175
 
175
176
  public syncSync(path: string, data: Uint8Array, stats: Readonly<Stats>): void {
176
177
  if (this._mu.isLocked(path)) {
177
- throw new Error('invalid sync call');
178
+ throw ErrnoError.With('EBUSY', path, 'sync');
178
179
  }
179
180
  return this.fs.syncSync(path, data, stats);
180
181
  }