@zenfs/core 0.9.6 → 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 (82) hide show
  1. package/dist/backends/AsyncStore.d.ts +3 -2
  2. package/dist/backends/AsyncStore.js +40 -33
  3. package/dist/backends/Fetch.d.ts +84 -0
  4. package/dist/backends/Fetch.js +171 -0
  5. package/dist/backends/InMemory.d.ts +1 -1
  6. package/dist/backends/Index.d.ts +7 -10
  7. package/dist/backends/Index.js +26 -24
  8. package/dist/backends/Locked.d.ts +11 -11
  9. package/dist/backends/Locked.js +50 -49
  10. package/dist/backends/Overlay.js +22 -22
  11. package/dist/backends/SyncStore.d.ts +6 -6
  12. package/dist/backends/SyncStore.js +30 -30
  13. package/dist/backends/backend.d.ts +5 -4
  14. package/dist/backends/backend.js +6 -6
  15. package/dist/backends/port/fs.d.ts +124 -0
  16. package/dist/backends/port/fs.js +241 -0
  17. package/dist/backends/port/rpc.d.ts +60 -0
  18. package/dist/backends/port/rpc.js +71 -0
  19. package/dist/backends/port/store.d.ts +30 -0
  20. package/dist/backends/port/store.js +142 -0
  21. package/dist/browser.min.js +4 -4
  22. package/dist/browser.min.js.map +4 -4
  23. package/dist/config.d.ts +9 -11
  24. package/dist/config.js +13 -13
  25. package/dist/emulation/async.d.ts +76 -77
  26. package/dist/emulation/async.js +45 -45
  27. package/dist/emulation/dir.js +8 -7
  28. package/dist/emulation/index.d.ts +1 -1
  29. package/dist/emulation/index.js +1 -1
  30. package/dist/emulation/path.d.ts +3 -2
  31. package/dist/emulation/path.js +19 -45
  32. package/dist/emulation/promises.d.ts +112 -113
  33. package/dist/emulation/promises.js +167 -173
  34. package/dist/emulation/shared.d.ts +6 -17
  35. package/dist/emulation/shared.js +9 -9
  36. package/dist/emulation/streams.js +3 -2
  37. package/dist/emulation/sync.d.ts +71 -64
  38. package/dist/emulation/sync.js +62 -63
  39. package/dist/{ApiError.d.ts → error.d.ts} +15 -15
  40. package/dist/error.js +292 -0
  41. package/dist/file.d.ts +6 -4
  42. package/dist/file.js +17 -9
  43. package/dist/filesystem.d.ts +1 -1
  44. package/dist/filesystem.js +18 -15
  45. package/dist/index.d.ts +4 -1
  46. package/dist/index.js +4 -1
  47. package/dist/mutex.js +4 -3
  48. package/dist/stats.d.ts +7 -7
  49. package/dist/stats.js +50 -10
  50. package/dist/utils.d.ts +10 -9
  51. package/dist/utils.js +15 -15
  52. package/package.json +5 -5
  53. package/readme.md +19 -11
  54. package/src/backends/AsyncStore.ts +42 -36
  55. package/src/backends/Fetch.ts +230 -0
  56. package/src/backends/Index.ts +33 -29
  57. package/src/backends/Locked.ts +50 -49
  58. package/src/backends/Overlay.ts +24 -24
  59. package/src/backends/SyncStore.ts +34 -34
  60. package/src/backends/backend.ts +13 -11
  61. package/src/backends/port/fs.ts +308 -0
  62. package/src/backends/port/readme.md +59 -0
  63. package/src/backends/port/rpc.ts +144 -0
  64. package/src/backends/port/store.ts +187 -0
  65. package/src/config.ts +25 -29
  66. package/src/emulation/async.ts +191 -199
  67. package/src/emulation/dir.ts +8 -8
  68. package/src/emulation/index.ts +1 -1
  69. package/src/emulation/path.ts +25 -49
  70. package/src/emulation/promises.ts +286 -287
  71. package/src/emulation/shared.ts +14 -23
  72. package/src/emulation/streams.ts +9 -8
  73. package/src/emulation/sync.ts +182 -182
  74. package/src/{ApiError.ts → error.ts} +91 -89
  75. package/src/file.ts +23 -13
  76. package/src/filesystem.ts +26 -22
  77. package/src/index.ts +4 -1
  78. package/src/mutex.ts +6 -4
  79. package/src/stats.ts +32 -23
  80. package/src/utils.ts +23 -24
  81. package/tsconfig.json +4 -3
  82. 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';
@@ -86,7 +86,10 @@ export class FileIndex<TData> {
86
86
  for (const dir of this._index.values()) {
87
87
  for (const file of dir.listing) {
88
88
  const item = dir.get(file);
89
- if (!item?.isFile()) {
89
+ if (!item) {
90
+ continue;
91
+ }
92
+ if (!item.isFile()) {
90
93
  continue;
91
94
  }
92
95
 
@@ -110,10 +113,10 @@ export class FileIndex<TData> {
110
113
  */
111
114
  public add(path: string, inode: IndexInode<TData>): boolean {
112
115
  if (!inode) {
113
- throw new Error('Inode must be specified');
116
+ throw new ErrnoError(Errno.EINVAL, 'Inode must be specified', path, 'FileIndex.add');
114
117
  }
115
118
  if (!path.startsWith('/')) {
116
- throw new Error('Path must be absolute, got: ' + path);
119
+ throw new ErrnoError(Errno.EINVAL, 'Path not absolute', path, 'FileIndex.add');
117
120
  }
118
121
 
119
122
  // Check if it already exists.
@@ -123,11 +126,12 @@ export class FileIndex<TData> {
123
126
 
124
127
  const dirpath = dirname(path);
125
128
 
129
+ const hasParent = this._index.has(dirpath);
130
+
131
+ const parent = hasParent ? this._index.get(dirpath)! : new IndexDirInode<TData>();
132
+
126
133
  // Try to add to its parent directory first.
127
- let parent = this._index.get(dirpath);
128
- if (!parent && path != '/') {
129
- // Create parent.
130
- parent = new IndexDirInode<TData>();
134
+ if (!hasParent && path != '/') {
131
135
  if (!this.add(dirpath, parent)) {
132
136
  return false;
133
137
  }
@@ -184,7 +188,7 @@ export class FileIndex<TData> {
184
188
  * @return The removed item,
185
189
  * or null if it did not exist.
186
190
  */
187
- public remove(path: string): IndexInode<TData> | null {
191
+ public remove(path: string): IndexInode<TData> | void {
188
192
  const dirpath = dirname(path);
189
193
 
190
194
  // Try to remove it from its parent directory first.
@@ -217,7 +221,7 @@ export class FileIndex<TData> {
217
221
  * Retrieves the directory listing of the given path.
218
222
  * @return An array of files in the given path, or 'null' if it does not exist.
219
223
  */
220
- public ls(path: string): string[] | null {
224
+ public ls(path: string): string[] | void {
221
225
  return this._index.get(path)?.listing;
222
226
  }
223
227
 
@@ -225,7 +229,7 @@ export class FileIndex<TData> {
225
229
  * Returns the inode of the given item.
226
230
  * @return Returns null if the item does not exist.
227
231
  */
228
- public get(path: string): IndexInode<TData> | null {
232
+ public get(path: string): IndexInode<TData> | void | null {
229
233
  const dirpath = dirname(path);
230
234
 
231
235
  // Retrieve from its parent directory.
@@ -316,7 +320,7 @@ export class IndexDirInode<TData> extends IndexInode<TData> {
316
320
  * Returns the inode for the indicated item, or null if it does not exist.
317
321
  * @param path Name of item in this directory.
318
322
  */
319
- public get(path: string): IndexInode<TData> | null {
323
+ public get(path: string): IndexInode<TData> | void {
320
324
  return this._listing.get(path);
321
325
  }
322
326
  /**
@@ -340,7 +344,7 @@ export class IndexDirInode<TData> extends IndexInode<TData> {
340
344
  * @return Returns the item
341
345
  * removed, or null if the item did not exist.
342
346
  */
343
- public remove(p: string): IndexInode<TData> | null {
347
+ public remove(p: string): IndexInode<TData> | void {
344
348
  const item = this._listing.get(p);
345
349
  if (!item) {
346
350
  return;
@@ -361,7 +365,7 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
361
365
  public async stat(path: string): Promise<Stats> {
362
366
  const inode = this._index.get(path);
363
367
  if (!inode) {
364
- throw ApiError.With('ENOENT', path, 'stat');
368
+ throw ErrnoError.With('ENOENT', path, 'stat');
365
369
  }
366
370
 
367
371
  if (inode.isDirectory()) {
@@ -372,13 +376,13 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
372
376
  return this.statFileInode(inode, path);
373
377
  }
374
378
 
375
- throw new ApiError(ErrorCode.EINVAL, 'Invalid inode.');
379
+ throw new ErrnoError(Errno.EINVAL, 'Invalid inode.');
376
380
  }
377
381
 
378
382
  public statSync(path: string): Stats {
379
383
  const inode = this._index.get(path);
380
384
  if (!inode) {
381
- throw ApiError.With('ENOENT', path, 'stat');
385
+ throw ErrnoError.With('ENOENT', path, 'stat');
382
386
  }
383
387
 
384
388
  if (inode.isDirectory()) {
@@ -389,24 +393,24 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
389
393
  return this.statFileInodeSync(inode, path);
390
394
  }
391
395
 
392
- throw new ApiError(ErrorCode.EINVAL, 'Invalid inode.');
396
+ throw new ErrnoError(Errno.EINVAL, 'Invalid inode.');
393
397
  }
394
398
 
395
399
  public async openFile(path: string, flag: string, cred: Cred): Promise<NoSyncFile<this>> {
396
400
  if (isWriteable(flag)) {
397
401
  // You can't write to files on this file system.
398
- throw new ApiError(ErrorCode.EPERM, path);
402
+ throw new ErrnoError(Errno.EPERM, path);
399
403
  }
400
404
 
401
405
  // Check if the path exists, and is a file.
402
406
  const inode = this._index.get(path);
403
407
 
404
408
  if (!inode) {
405
- throw ApiError.With('ENOENT', path, 'openFile');
409
+ throw ErrnoError.With('ENOENT', path, 'openFile');
406
410
  }
407
411
 
408
412
  if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
409
- throw ApiError.With('EACCES', path, 'openFile');
413
+ throw ErrnoError.With('EACCES', path, 'openFile');
410
414
  }
411
415
 
412
416
  if (inode.isDirectory()) {
@@ -420,18 +424,18 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
420
424
  public openFileSync(path: string, flag: string, cred: Cred): NoSyncFile<this> {
421
425
  if (isWriteable(flag)) {
422
426
  // You can't write to files on this file system.
423
- throw new ApiError(ErrorCode.EPERM, path);
427
+ throw new ErrnoError(Errno.EPERM, path);
424
428
  }
425
429
 
426
430
  // Check if the path exists, and is a file.
427
431
  const inode = this._index.get(path);
428
432
 
429
433
  if (!inode) {
430
- throw ApiError.With('ENOENT', path, 'openFile');
434
+ throw ErrnoError.With('ENOENT', path, 'openFile');
431
435
  }
432
436
 
433
437
  if (!inode.toStats().hasAccess(flagToMode(flag), cred)) {
434
- throw ApiError.With('EACCES', path, 'openFile');
438
+ throw ErrnoError.With('EACCES', path, 'openFile');
435
439
  }
436
440
 
437
441
  if (inode.isDirectory()) {
@@ -446,28 +450,28 @@ export abstract class IndexFS<TData> extends Readonly(FileSystem) {
446
450
  // Check if it exists.
447
451
  const inode = this._index.get(path);
448
452
  if (!inode) {
449
- throw ApiError.With('ENOENT', path, 'readdir');
453
+ throw ErrnoError.With('ENOENT', path, 'readdir');
450
454
  }
451
455
 
452
456
  if (inode.isDirectory()) {
453
457
  return inode.listing;
454
458
  }
455
459
 
456
- throw ApiError.With('ENOTDIR', path, 'readdir');
460
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
457
461
  }
458
462
 
459
463
  public readdirSync(path: string): string[] {
460
464
  // Check if it exists.
461
465
  const inode = this._index.get(path);
462
466
  if (!inode) {
463
- throw ApiError.With('ENOENT', path, 'readdir');
467
+ throw ErrnoError.With('ENOENT', path, 'readdir');
464
468
  }
465
469
 
466
470
  if (inode.isDirectory()) {
467
471
  return inode.listing;
468
472
  }
469
473
 
470
- throw ApiError.With('ENOTDIR', path, 'readdir');
474
+ throw ErrnoError.With('ENOTDIR', path, 'readdir');
471
475
  }
472
476
 
473
477
  protected abstract statFileInode(inode: IndexFileInode<TData>, path: string): Promise<Stats>;
@@ -491,10 +495,10 @@ export abstract class SyncIndexFS<TData> extends IndexFS<TData> {
491
495
 
492
496
  export abstract class AsyncIndexFS<TData> extends IndexFS<TData> {
493
497
  protected statFileInodeSync(inode: IndexFileInode<TData>, path: string): Stats {
494
- throw ApiError.With('ENOSYS', path, 'AsyncIndexFS.statFileInodeSync');
498
+ throw ErrnoError.With('ENOSYS', path, 'AsyncIndexFS.statFileInodeSync');
495
499
  }
496
500
 
497
501
  protected openFileInodeSync(inode: IndexFileInode<TData>, path: string, flag: string): NoSyncFile<this> {
498
- throw ApiError.With('ENOSYS', path, 'AsyncIndexFS.openFileInodeSync');
502
+ throw ErrnoError.With('ENOSYS', path, 'AsyncIndexFS.openFileInodeSync');
499
503
  }
500
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
  }