@zenfs/core 1.2.6 → 1.2.8

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.
@@ -12,8 +12,9 @@ export type OptionsConfig<T> = {
12
12
  type: OptionType | readonly OptionType[];
13
13
  /**
14
14
  * Description of the option. Used in error messages and documentation.
15
+ * @deprecated
15
16
  */
16
- description: string;
17
+ description?: string;
17
18
  /**
18
19
  * Whether or not the option is required (optional can be set to null or undefined). Defaults to false.
19
20
  */
@@ -6,6 +6,10 @@ import type { IndexData } from './file_index.js';
6
6
  * Configuration options for FetchFS.
7
7
  */
8
8
  export interface FetchOptions {
9
+ /**
10
+ * Options to pass through to fetch calls
11
+ */
12
+ requestInit?: RequestInit;
9
13
  /**
10
14
  * URL to a file index as a JSON file or the file index object itself.
11
15
  * Defaults to `index.json`.
@@ -37,8 +41,9 @@ export interface FetchOptions {
37
41
  */
38
42
  export declare class FetchFS extends IndexFS {
39
43
  readonly baseUrl: string;
44
+ readonly requestInit?: RequestInit;
40
45
  ready(): Promise<void>;
41
- constructor({ index, baseUrl }: FetchOptions);
46
+ constructor({ index, baseUrl, requestInit }: FetchOptions);
42
47
  metadata(): FileSystemMetadata;
43
48
  /**
44
49
  * Preload the `path` into the index.
@@ -56,12 +61,14 @@ declare const _Fetch: {
56
61
  readonly index: {
57
62
  readonly type: readonly ["string", "object"];
58
63
  readonly required: false;
59
- readonly description: "URL to a file index as a JSON file or the file index object itself, generated with the make-index script. Defaults to `index.json`.";
60
64
  };
61
65
  readonly baseUrl: {
62
66
  readonly type: "string";
63
67
  readonly required: false;
64
- readonly description: "Used as the URL prefix for fetched files. Default: Fetch files relative to the index.";
68
+ };
69
+ readonly requestInit: {
70
+ readonly type: "object";
71
+ readonly required: false;
65
72
  };
66
73
  };
67
74
  readonly isAvailable: () => boolean;
@@ -1,7 +1,7 @@
1
1
  import { Errno, ErrnoError } from '../error.js';
2
2
  import { IndexFS } from './file_index.js';
3
- async function fetchFile(path, type) {
4
- const response = await fetch(path).catch((e) => {
3
+ async function fetchFile(path, type, init) {
4
+ const response = await fetch(path, init).catch((e) => {
5
5
  throw new ErrnoError(Errno.EIO, e.message, path);
6
6
  });
7
7
  if (!response.ok) {
@@ -57,13 +57,14 @@ export class FetchFS extends IndexFS {
57
57
  await this.getData(path, stats);
58
58
  }
59
59
  }
60
- constructor({ index = 'index.json', baseUrl = '' }) {
60
+ constructor({ index = 'index.json', baseUrl = '', requestInit }) {
61
61
  // prefix url must end in a directory separator.
62
62
  if (baseUrl.at(-1) != '/') {
63
63
  baseUrl += '/';
64
64
  }
65
- super(typeof index != 'string' ? index : fetchFile(baseUrl + index, 'json'));
65
+ super(typeof index != 'string' ? index : fetchFile(baseUrl + index, 'json', requestInit));
66
66
  this.baseUrl = baseUrl;
67
+ this.requestInit = requestInit;
67
68
  }
68
69
  metadata() {
69
70
  return {
@@ -93,7 +94,7 @@ export class FetchFS extends IndexFS {
93
94
  if (stats.fileData) {
94
95
  return stats.fileData;
95
96
  }
96
- const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
97
+ const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer', this.requestInit);
97
98
  stats.fileData = data;
98
99
  return data;
99
100
  }
@@ -107,16 +108,9 @@ export class FetchFS extends IndexFS {
107
108
  const _Fetch = {
108
109
  name: 'Fetch',
109
110
  options: {
110
- index: {
111
- type: ['string', 'object'],
112
- required: false,
113
- description: 'URL to a file index as a JSON file or the file index object itself, generated with the make-index script. Defaults to `index.json`.',
114
- },
115
- baseUrl: {
116
- type: 'string',
117
- required: false,
118
- description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
119
- },
111
+ index: { type: ['string', 'object'], required: false },
112
+ baseUrl: { type: 'string', required: false },
113
+ requestInit: { type: 'object', required: false },
120
114
  },
121
115
  isAvailable() {
122
116
  return typeof globalThis.fetch == 'function';
@@ -22,7 +22,6 @@ declare const _InMemory: {
22
22
  readonly name: {
23
23
  readonly type: "string";
24
24
  readonly required: false;
25
- readonly description: "The name of the store";
26
25
  };
27
26
  };
28
27
  readonly create: ({ name }: {
@@ -26,11 +26,7 @@ const _InMemory = {
26
26
  return true;
27
27
  },
28
28
  options: {
29
- name: {
30
- type: 'string',
31
- required: false,
32
- description: 'The name of the store',
33
- },
29
+ name: { type: 'string', required: false },
34
30
  },
35
31
  create({ name }) {
36
32
  const fs = new StoreFS(new InMemoryStore(name));
@@ -110,13 +110,11 @@ declare const _Port: {
110
110
  port: {
111
111
  type: "object";
112
112
  required: true;
113
- description: string;
114
113
  validator(port: RPC.Port): void;
115
114
  };
116
115
  timeout: {
117
116
  type: "number";
118
117
  required: false;
119
- description: string;
120
118
  };
121
119
  };
122
120
  isAvailable(): boolean;
@@ -214,7 +214,6 @@ const _Port = {
214
214
  port: {
215
215
  type: 'object',
216
216
  required: true,
217
- description: 'The target port that you want to connect to',
218
217
  validator(port) {
219
218
  // Check for a `postMessage` function.
220
219
  if (typeof port?.postMessage != 'function') {
@@ -225,7 +224,6 @@ const _Port = {
225
224
  timeout: {
226
225
  type: 'number',
227
226
  required: false,
228
- description: 'How long to wait before the request times out',
229
227
  },
230
228
  },
231
229
  isAvailable() {
package/dist/config.js CHANGED
@@ -70,8 +70,8 @@ export async function configure(configuration) {
70
70
  Object.assign(credentials, { uid, gid, suid: uid, sgid: gid, euid: uid, egid: gid });
71
71
  cache.setEnabled(configuration.cacheStats ?? false);
72
72
  config.checkAccess = !configuration.disableAccessChecks;
73
- config.syncOnRead = !configuration.onlySyncOnClose || !configuration.disableUpdateOnRead;
74
- config.syncOnWrite = !configuration.onlySyncOnClose;
73
+ config.updateOnRead = !configuration.disableUpdateOnRead;
74
+ config.syncImmediately = !configuration.onlySyncOnClose;
75
75
  if (configuration.addDevices) {
76
76
  const devfs = new DeviceFS();
77
77
  devfs.createDevice('/null', nullDevice);
package/dist/devices.d.ts CHANGED
@@ -8,9 +8,10 @@ import type { Ino } from './inode.js';
8
8
  /**
9
9
  * A device
10
10
  * @todo Maybe add major/minor number or some other device information, like a UUID?
11
- * @experimental
11
+ * @privateRemarks
12
+ * UUIDs were considered, however they don't make sense without an easy mechanism for persistance
12
13
  */
13
- export interface Device {
14
+ export interface Device<TData = any> {
14
15
  /**
15
16
  * The device's driver
16
17
  */
@@ -19,34 +20,64 @@ export interface Device {
19
20
  * Which inode the device is assigned
20
21
  */
21
22
  ino: Ino;
23
+ /**
24
+ * Data associated with a device.
25
+ * This is meant to be used by device drivers.
26
+ * @experimental
27
+ */
28
+ data: TData;
29
+ /**
30
+ * Major device number
31
+ * @experimental
32
+ */
33
+ major: number;
34
+ /**
35
+ * Minor device number
36
+ * @experimental
37
+ */
38
+ minor: number;
22
39
  }
23
40
  /**
24
41
  * A device driver
25
- * @experimental
26
42
  */
27
- export interface DeviceDriver {
43
+ export interface DeviceDriver<TData = any> {
28
44
  /**
29
45
  * The name of the device driver
30
46
  */
31
47
  name: string;
32
48
  /**
33
49
  * Whether the device is buffered (a "block" device) or unbuffered (a "character" device)
50
+ * @default false
51
+ */
52
+ isBuffered?: boolean;
53
+ /**
54
+ * Initializes a new device.
55
+ * @returns `Device.data`
56
+ * @experimental
34
57
  */
35
- isBuffered: boolean;
58
+ init?(ino: Ino): {
59
+ data?: TData;
60
+ minor?: number;
61
+ major?: number;
62
+ };
36
63
  /**
37
64
  * Synchronously read from the device
65
+ * @group File operations
38
66
  */
39
67
  read(file: DeviceFile, buffer: ArrayBufferView, offset?: number, length?: number, position?: number): number;
40
68
  /**
41
69
  * Synchronously write to the device
70
+ * @group File operations
42
71
  */
43
72
  write(file: DeviceFile, buffer: Uint8Array, offset: number, length: number, position?: number): number;
44
73
  /**
45
74
  * Sync the device
75
+ * @group File operations
46
76
  */
47
77
  sync?(file: DeviceFile): void;
48
78
  /**
49
79
  * Close the device
80
+ * @group File operations
50
81
  */
51
82
  close?(file: DeviceFile): void;
52
83
  }
@@ -55,7 +86,6 @@ export interface DeviceDriver {
55
86
  * This class only does some simple things:
56
87
  * It implements `truncate` using `write` and it has non-device methods throw.
57
88
  * It is up to device drivers to implement the rest of the functionality.
58
- * @experimental
59
89
  */
60
90
  export declare class DeviceFile extends File {
61
91
  fs: DeviceFS;
@@ -86,11 +116,14 @@ export declare class DeviceFile extends File {
86
116
  _setTypeSync(): void;
87
117
  }
88
118
  /**
89
- * @experimental
119
+ * A temporary file system that manages and interfaces with devices
90
120
  */
91
121
  export declare class DeviceFS extends StoreFS<InMemoryStore> {
92
- protected readonly devices: Map<string, Device>;
93
- createDevice(path: string, driver: DeviceDriver): Device;
122
+ protected readonly devices: Map<string, Device<any>>;
123
+ /**
124
+ * Creates a new device at `path` relative to the `DeviceFS` root.
125
+ */
126
+ createDevice<TData = any>(path: string, driver: DeviceDriver<TData>): Device<TData | Record<string, never>>;
94
127
  constructor();
95
128
  rename(oldPath: string, newPath: string): Promise<void>;
96
129
  renameSync(oldPath: string, newPath: string): void;
@@ -150,9 +183,9 @@ export declare const randomDevice: DeviceDriver;
150
183
  * @experimental
151
184
  */
152
185
  declare const _default: {
153
- null: DeviceDriver;
154
- zero: DeviceDriver;
155
- full: DeviceDriver;
156
- random: DeviceDriver;
186
+ null: DeviceDriver<any>;
187
+ zero: DeviceDriver<any>;
188
+ full: DeviceDriver<any>;
189
+ random: DeviceDriver<any>;
157
190
  };
158
191
  export default _default;
package/dist/devices.js CHANGED
@@ -1,3 +1,6 @@
1
+ /*
2
+ This is a great resource: https://www.kernel.org/doc/html/latest/admin-guide/devices.html
3
+ */
1
4
  var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
5
  if (value !== null && value !== void 0) {
3
6
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
@@ -57,7 +60,6 @@ import { basename, dirname } from './emulation/path.js';
57
60
  * This class only does some simple things:
58
61
  * It implements `truncate` using `write` and it has non-device methods throw.
59
62
  * It is up to device drivers to implement the rest of the functionality.
60
- * @experimental
61
63
  */
62
64
  export class DeviceFile extends File {
63
65
  constructor(fs, path, device) {
@@ -142,9 +144,12 @@ export class DeviceFile extends File {
142
144
  }
143
145
  }
144
146
  /**
145
- * @experimental
147
+ * A temporary file system that manages and interfaces with devices
146
148
  */
147
149
  export class DeviceFS extends StoreFS {
150
+ /**
151
+ * Creates a new device at `path` relative to the `DeviceFS` root.
152
+ */
148
153
  createDevice(path, driver) {
149
154
  if (this.existsSync(path)) {
150
155
  throw ErrnoError.With('EEXIST', path, 'mknod');
@@ -155,6 +160,10 @@ export class DeviceFS extends StoreFS {
155
160
  const dev = {
156
161
  driver,
157
162
  ino,
163
+ data: {},
164
+ minor: 0,
165
+ major: 0,
166
+ ...driver.init?.(ino),
158
167
  };
159
168
  this.devices.set(path, dev);
160
169
  return dev;
@@ -332,7 +341,9 @@ function defaultWrite(file, buffer, offset, length) {
332
341
  */
333
342
  export const nullDevice = {
334
343
  name: 'null',
335
- isBuffered: false,
344
+ init() {
345
+ return { major: 1, minor: 3 };
346
+ },
336
347
  read() {
337
348
  return 0;
338
349
  },
@@ -350,7 +361,9 @@ export const nullDevice = {
350
361
  */
351
362
  export const zeroDevice = {
352
363
  name: 'zero',
353
- isBuffered: false,
364
+ init() {
365
+ return { major: 1, minor: 5 };
366
+ },
354
367
  read(file, buffer, offset = 0, length = buffer.byteLength) {
355
368
  const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
356
369
  for (let i = offset; i < offset + length; i++) {
@@ -369,7 +382,9 @@ export const zeroDevice = {
369
382
  */
370
383
  export const fullDevice = {
371
384
  name: 'full',
372
- isBuffered: false,
385
+ init() {
386
+ return { major: 1, minor: 7 };
387
+ },
373
388
  read(file, buffer, offset = 0, length = buffer.byteLength) {
374
389
  const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
375
390
  for (let i = offset; i < offset + length; i++) {
@@ -390,7 +405,9 @@ export const fullDevice = {
390
405
  */
391
406
  export const randomDevice = {
392
407
  name: 'random',
393
- isBuffered: false,
408
+ init() {
409
+ return { major: 1, minor: 8 };
410
+ },
394
411
  read(file, buffer, offset = 0, length = buffer.byteLength) {
395
412
  const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
396
413
  for (let i = offset; i < offset + length; i++) {
@@ -4,13 +4,13 @@ export declare const config: {
4
4
  */
5
5
  checkAccess: boolean;
6
6
  /**
7
- * Whether to sync atime updates immediately when reading from a file
7
+ * Whether to mark a file as dirty after updating its `atime` when read from
8
8
  */
9
- syncOnRead: boolean;
9
+ updateOnRead: boolean;
10
10
  /**
11
- * Whether to immediately sync when files are written to
11
+ * Whether to immediately sync when files are changed
12
12
  */
13
- syncOnWrite: boolean;
13
+ syncImmediately: boolean;
14
14
  /**
15
15
  * If a file's buffer is not large enough to store content when writing and the buffer can't be resized, reuse the buffer passed to write()
16
16
  */
@@ -4,13 +4,13 @@ export const config = {
4
4
  */
5
5
  checkAccess: true,
6
6
  /**
7
- * Whether to sync atime updates immediately when reading from a file
7
+ * Whether to mark a file as dirty after updating its `atime` when read from
8
8
  */
9
- syncOnRead: true,
9
+ updateOnRead: true,
10
10
  /**
11
- * Whether to immediately sync when files are written to
11
+ * Whether to immediately sync when files are changed
12
12
  */
13
- syncOnWrite: true,
13
+ syncImmediately: true,
14
14
  /**
15
15
  * If a file's buffer is not large enough to store content when writing and the buffer can't be resized, reuse the buffer passed to write()
16
16
  */
@@ -21,7 +21,7 @@ export declare function mount(mountPoint: string, fs: FileSystem): void;
21
21
  */
22
22
  export declare function umount(mountPoint: string): void;
23
23
  /**
24
- * Gets the internal FileSystem for the path, then returns it along with the path relative to the FS' root
24
+ * Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root
25
25
  */
26
26
  export declare function resolveMount(path: string): {
27
27
  fs: FileSystem;
@@ -43,7 +43,7 @@ export function mount(mountPoint, fs) {
43
43
  */
44
44
  export function umount(mountPoint) {
45
45
  if (mountPoint[0] !== '/') {
46
- mountPoint = `/${mountPoint}`;
46
+ mountPoint = '/' + mountPoint;
47
47
  }
48
48
  mountPoint = resolve(mountPoint);
49
49
  if (!mounts.has(mountPoint)) {
@@ -52,11 +52,10 @@ export function umount(mountPoint) {
52
52
  mounts.delete(mountPoint);
53
53
  }
54
54
  /**
55
- * Gets the internal FileSystem for the path, then returns it along with the path relative to the FS' root
55
+ * Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root
56
56
  */
57
57
  export function resolveMount(path) {
58
58
  path = normalizePath(path);
59
- // Maybe do something for devices here
60
59
  const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length
61
60
  for (const [mountPoint, fs] of sortedMounts) {
62
61
  // We know path is normalized, so it would be a substring of the mount point.
package/dist/file.js CHANGED
@@ -289,12 +289,12 @@ export class PreloadFile extends File {
289
289
  }
290
290
  async truncate(length) {
291
291
  this._truncate(length);
292
- if (config.syncOnWrite)
292
+ if (config.syncImmediately)
293
293
  await this.sync();
294
294
  }
295
295
  truncateSync(length) {
296
296
  this._truncate(length);
297
- if (config.syncOnWrite)
297
+ if (config.syncImmediately)
298
298
  this.syncSync();
299
299
  }
300
300
  _write(buffer, offset = 0, length = this.stats.size, position = this.position) {
@@ -339,7 +339,7 @@ export class PreloadFile extends File {
339
339
  */
340
340
  async write(buffer, offset, length, position) {
341
341
  const bytesWritten = this._write(buffer, offset, length, position);
342
- if (config.syncOnWrite)
342
+ if (config.syncImmediately)
343
343
  await this.sync();
344
344
  return bytesWritten;
345
345
  }
@@ -354,7 +354,7 @@ export class PreloadFile extends File {
354
354
  */
355
355
  writeSync(buffer, offset = 0, length = this.stats.size, position = this.position) {
356
356
  const bytesWritten = this._write(buffer, offset, length, position);
357
- if (config.syncOnWrite)
357
+ if (config.syncImmediately)
358
358
  this.syncSync();
359
359
  return bytesWritten;
360
360
  }
@@ -365,7 +365,9 @@ export class PreloadFile extends File {
365
365
  if (!isReadable(this.flag)) {
366
366
  throw new ErrnoError(Errno.EPERM, 'File not opened with a readable mode.');
367
367
  }
368
- this.dirty = true;
368
+ if (config.updateOnRead) {
369
+ this.dirty = true;
370
+ }
369
371
  this.stats.atimeMs = Date.now();
370
372
  position ?? (position = this.position);
371
373
  let end = position + length;
@@ -391,7 +393,7 @@ export class PreloadFile extends File {
391
393
  */
392
394
  async read(buffer, offset, length, position) {
393
395
  const bytesRead = this._read(buffer, offset, length, position);
394
- if (config.syncOnRead)
396
+ if (config.syncImmediately)
395
397
  await this.sync();
396
398
  return { bytesRead, buffer };
397
399
  }
@@ -406,7 +408,7 @@ export class PreloadFile extends File {
406
408
  */
407
409
  readSync(buffer, offset, length, position) {
408
410
  const bytesRead = this._read(buffer, offset, length, position);
409
- if (config.syncOnRead)
411
+ if (config.syncImmediately)
410
412
  this.syncSync();
411
413
  return bytesRead;
412
414
  }
@@ -416,7 +418,7 @@ export class PreloadFile extends File {
416
418
  }
417
419
  this.dirty = true;
418
420
  this.stats.chmod(mode);
419
- if (config.syncOnWrite)
421
+ if (config.syncImmediately)
420
422
  await this.sync();
421
423
  }
422
424
  chmodSync(mode) {
@@ -425,7 +427,7 @@ export class PreloadFile extends File {
425
427
  }
426
428
  this.dirty = true;
427
429
  this.stats.chmod(mode);
428
- if (config.syncOnWrite)
430
+ if (config.syncImmediately)
429
431
  this.syncSync();
430
432
  }
431
433
  async chown(uid, gid) {
@@ -434,7 +436,7 @@ export class PreloadFile extends File {
434
436
  }
435
437
  this.dirty = true;
436
438
  this.stats.chown(uid, gid);
437
- if (config.syncOnWrite)
439
+ if (config.syncImmediately)
438
440
  await this.sync();
439
441
  }
440
442
  chownSync(uid, gid) {
@@ -443,7 +445,7 @@ export class PreloadFile extends File {
443
445
  }
444
446
  this.dirty = true;
445
447
  this.stats.chown(uid, gid);
446
- if (config.syncOnWrite)
448
+ if (config.syncImmediately)
447
449
  this.syncSync();
448
450
  }
449
451
  async utimes(atime, mtime) {
@@ -453,7 +455,7 @@ export class PreloadFile extends File {
453
455
  this.dirty = true;
454
456
  this.stats.atime = atime;
455
457
  this.stats.mtime = mtime;
456
- if (config.syncOnWrite)
458
+ if (config.syncImmediately)
457
459
  await this.sync();
458
460
  }
459
461
  utimesSync(atime, mtime) {
@@ -463,7 +465,7 @@ export class PreloadFile extends File {
463
465
  this.dirty = true;
464
466
  this.stats.atime = atime;
465
467
  this.stats.mtime = mtime;
466
- if (config.syncOnWrite)
468
+ if (config.syncImmediately)
467
469
  this.syncSync();
468
470
  }
469
471
  async _setType(type) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
package/readme.md CHANGED
@@ -201,6 +201,10 @@ If you would like to see a more intuitive way adding custom devices (e.g. `fs.mk
201
201
 
202
202
  ZenFS exports a drop-in for Node's `fs` module (up to the version of `@types/node` in package.json), so you can use it for your bundler of preference using the default export.
203
203
 
204
+ ## Sponsors
205
+
206
+ A huge thank you to [![Deco.cx logo](https://avatars.githubusercontent.com/deco-cx?size=20) Deco.cx](https://github.com/deco-cx) for sponsoring ZenFS and helping to make this possible.
207
+
204
208
  ## Building
205
209
 
206
210
  - Make sure you have Node and NPM installed. You must have Node v18 or newer.
@@ -17,8 +17,9 @@ export type OptionsConfig<T> = {
17
17
 
18
18
  /**
19
19
  * Description of the option. Used in error messages and documentation.
20
+ * @deprecated
20
21
  */
21
- description: string;
22
+ description?: string;
22
23
 
23
24
  /**
24
25
  * Whether or not the option is required (optional can be set to null or undefined). Defaults to false.
@@ -12,11 +12,11 @@ import type { IndexData } from './file_index.js';
12
12
  * constants.
13
13
  * @hidden
14
14
  */
15
- async function fetchFile(path: string, type: 'buffer'): Promise<Uint8Array>;
16
- async function fetchFile<T extends object>(path: string, type: 'json'): Promise<T>;
17
- async function fetchFile<T extends object>(path: string, type: 'buffer' | 'json'): Promise<T | Uint8Array>;
18
- async function fetchFile<T extends object>(path: string, type: string): Promise<T | Uint8Array> {
19
- const response = await fetch(path).catch((e: Error) => {
15
+ async function fetchFile(path: string, type: 'buffer', init?: RequestInit): Promise<Uint8Array>;
16
+ async function fetchFile<T extends object>(path: string, type: 'json', init?: RequestInit): Promise<T>;
17
+ async function fetchFile<T extends object>(path: string, type: 'buffer' | 'json', init?: RequestInit): Promise<T | Uint8Array>;
18
+ async function fetchFile<T extends object>(path: string, type: string, init?: RequestInit): Promise<T | Uint8Array> {
19
+ const response = await fetch(path, init).catch((e: Error) => {
20
20
  throw new ErrnoError(Errno.EIO, e.message, path);
21
21
  });
22
22
  if (!response.ok) {
@@ -42,6 +42,11 @@ async function fetchFile<T extends object>(path: string, type: string): Promise<
42
42
  * Configuration options for FetchFS.
43
43
  */
44
44
  export interface FetchOptions {
45
+ /**
46
+ * Options to pass through to fetch calls
47
+ */
48
+ requestInit?: RequestInit;
49
+
45
50
  /**
46
51
  * URL to a file index as a JSON file or the file index object itself.
47
52
  * Defaults to `index.json`.
@@ -75,6 +80,7 @@ export interface FetchOptions {
75
80
  */
76
81
  export class FetchFS extends IndexFS {
77
82
  public readonly baseUrl: string;
83
+ public readonly requestInit?: RequestInit;
78
84
 
79
85
  public async ready(): Promise<void> {
80
86
  if (this._isInitialized) {
@@ -94,15 +100,16 @@ export class FetchFS extends IndexFS {
94
100
  }
95
101
  }
96
102
 
97
- public constructor({ index = 'index.json', baseUrl = '' }: FetchOptions) {
103
+ public constructor({ index = 'index.json', baseUrl = '', requestInit }: FetchOptions) {
98
104
  // prefix url must end in a directory separator.
99
105
  if (baseUrl.at(-1) != '/') {
100
106
  baseUrl += '/';
101
107
  }
102
108
 
103
- super(typeof index != 'string' ? index : fetchFile<IndexData>(baseUrl + index, 'json'));
109
+ super(typeof index != 'string' ? index : fetchFile<IndexData>(baseUrl + index, 'json', requestInit));
104
110
 
105
111
  this.baseUrl = baseUrl;
112
+ this.requestInit = requestInit;
106
113
  }
107
114
 
108
115
  public metadata(): FileSystemMetadata {
@@ -136,7 +143,7 @@ export class FetchFS extends IndexFS {
136
143
  return stats.fileData;
137
144
  }
138
145
 
139
- const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer');
146
+ const data = await fetchFile(this.baseUrl + (path.startsWith('/') ? path.slice(1) : path), 'buffer', this.requestInit);
140
147
  stats.fileData = data;
141
148
  return data;
142
149
  }
@@ -154,16 +161,9 @@ const _Fetch = {
154
161
  name: 'Fetch',
155
162
 
156
163
  options: {
157
- index: {
158
- type: ['string', 'object'],
159
- required: false,
160
- description: 'URL to a file index as a JSON file or the file index object itself, generated with the make-index script. Defaults to `index.json`.',
161
- },
162
- baseUrl: {
163
- type: 'string',
164
- required: false,
165
- description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.',
166
- },
164
+ index: { type: ['string', 'object'], required: false },
165
+ baseUrl: { type: 'string', required: false },
166
+ requestInit: { type: 'object', required: false },
167
167
  },
168
168
 
169
169
  isAvailable(): boolean {
@@ -32,11 +32,7 @@ const _InMemory = {
32
32
  return true;
33
33
  },
34
34
  options: {
35
- name: {
36
- type: 'string',
37
- required: false,
38
- description: 'The name of the store',
39
- },
35
+ name: { type: 'string', required: false },
40
36
  },
41
37
  create({ name }: { name?: string }) {
42
38
  const fs = new StoreFS(new InMemoryStore(name));
@@ -298,7 +298,6 @@ const _Port = {
298
298
  port: {
299
299
  type: 'object',
300
300
  required: true,
301
- description: 'The target port that you want to connect to',
302
301
  validator(port: RPC.Port) {
303
302
  // Check for a `postMessage` function.
304
303
  if (typeof port?.postMessage != 'function') {
@@ -309,7 +308,6 @@ const _Port = {
309
308
  timeout: {
310
309
  type: 'number',
311
310
  required: false,
312
- description: 'How long to wait before the request times out',
313
311
  },
314
312
  },
315
313
 
package/src/config.ts CHANGED
@@ -163,8 +163,8 @@ export async function configure<T extends ConfigMounts>(configuration: Partial<C
163
163
 
164
164
  cache.setEnabled(configuration.cacheStats ?? false);
165
165
  config.checkAccess = !configuration.disableAccessChecks;
166
- config.syncOnRead = !configuration.onlySyncOnClose || !configuration.disableUpdateOnRead;
167
- config.syncOnWrite = !configuration.onlySyncOnClose;
166
+ config.updateOnRead = !configuration.disableUpdateOnRead;
167
+ config.syncImmediately = !configuration.onlySyncOnClose;
168
168
 
169
169
  if (configuration.addDevices) {
170
170
  const devfs = new DeviceFS();
package/src/devices.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /*
2
+ This is a great resource: https://www.kernel.org/doc/html/latest/admin-guide/devices.html
3
+ */
4
+
1
5
  import type { FileReadResult } from 'node:fs/promises';
2
6
  import { InMemoryStore } from './backends/memory.js';
3
7
  import { StoreFS } from './backends/store/fs.js';
@@ -12,9 +16,10 @@ import type { Ino } from './inode.js';
12
16
  /**
13
17
  * A device
14
18
  * @todo Maybe add major/minor number or some other device information, like a UUID?
15
- * @experimental
19
+ * @privateRemarks
20
+ * UUIDs were considered, however they don't make sense without an easy mechanism for persistance
16
21
  */
17
- export interface Device {
22
+ export interface Device<TData = any> {
18
23
  /**
19
24
  * The device's driver
20
25
  */
@@ -24,13 +29,31 @@ export interface Device {
24
29
  * Which inode the device is assigned
25
30
  */
26
31
  ino: Ino;
32
+
33
+ /**
34
+ * Data associated with a device.
35
+ * This is meant to be used by device drivers.
36
+ * @experimental
37
+ */
38
+ data: TData;
39
+
40
+ /**
41
+ * Major device number
42
+ * @experimental
43
+ */
44
+ major: number;
45
+
46
+ /**
47
+ * Minor device number
48
+ * @experimental
49
+ */
50
+ minor: number;
27
51
  }
28
52
 
29
53
  /**
30
54
  * A device driver
31
- * @experimental
32
55
  */
33
- export interface DeviceDriver {
56
+ export interface DeviceDriver<TData = any> {
34
57
  /**
35
58
  * The name of the device driver
36
59
  */
@@ -38,26 +61,42 @@ export interface DeviceDriver {
38
61
 
39
62
  /**
40
63
  * Whether the device is buffered (a "block" device) or unbuffered (a "character" device)
64
+ * @default false
41
65
  */
42
- isBuffered: boolean;
66
+ isBuffered?: boolean;
67
+
68
+ /**
69
+ * Initializes a new device.
70
+ * @returns `Device.data`
71
+ * @experimental
72
+ */
73
+ init?(ino: Ino): {
74
+ data?: TData;
75
+ minor?: number;
76
+ major?: number;
77
+ };
43
78
 
44
79
  /**
45
80
  * Synchronously read from the device
81
+ * @group File operations
46
82
  */
47
83
  read(file: DeviceFile, buffer: ArrayBufferView, offset?: number, length?: number, position?: number): number;
48
84
 
49
85
  /**
50
86
  * Synchronously write to the device
87
+ * @group File operations
51
88
  */
52
89
  write(file: DeviceFile, buffer: Uint8Array, offset: number, length: number, position?: number): number;
53
90
 
54
91
  /**
55
92
  * Sync the device
93
+ * @group File operations
56
94
  */
57
95
  sync?(file: DeviceFile): void;
58
96
 
59
97
  /**
60
98
  * Close the device
99
+ * @group File operations
61
100
  */
62
101
  close?(file: DeviceFile): void;
63
102
  }
@@ -67,7 +106,6 @@ export interface DeviceDriver {
67
106
  * This class only does some simple things:
68
107
  * It implements `truncate` using `write` and it has non-device methods throw.
69
108
  * It is up to device drivers to implement the rest of the functionality.
70
- * @experimental
71
109
  */
72
110
  export class DeviceFile extends File {
73
111
  public position = 0;
@@ -182,12 +220,15 @@ export class DeviceFile extends File {
182
220
  }
183
221
 
184
222
  /**
185
- * @experimental
223
+ * A temporary file system that manages and interfaces with devices
186
224
  */
187
225
  export class DeviceFS extends StoreFS<InMemoryStore> {
188
226
  protected readonly devices = new Map<string, Device>();
189
227
 
190
- public createDevice(path: string, driver: DeviceDriver): Device {
228
+ /**
229
+ * Creates a new device at `path` relative to the `DeviceFS` root.
230
+ */
231
+ public createDevice<TData = any>(path: string, driver: DeviceDriver<TData>): Device<TData | Record<string, never>> {
191
232
  if (this.existsSync(path)) {
192
233
  throw ErrnoError.With('EEXIST', path, 'mknod');
193
234
  }
@@ -196,6 +237,10 @@ export class DeviceFS extends StoreFS<InMemoryStore> {
196
237
  const dev = {
197
238
  driver,
198
239
  ino,
240
+ data: {},
241
+ minor: 0,
242
+ major: 0,
243
+ ...driver.init?.(ino),
199
244
  };
200
245
  this.devices.set(path, dev);
201
246
  return dev;
@@ -373,7 +418,9 @@ function defaultWrite(file: DeviceFile, buffer: Uint8Array, offset: number, leng
373
418
  */
374
419
  export const nullDevice: DeviceDriver = {
375
420
  name: 'null',
376
- isBuffered: false,
421
+ init() {
422
+ return { major: 1, minor: 3 };
423
+ },
377
424
  read(): number {
378
425
  return 0;
379
426
  },
@@ -392,7 +439,9 @@ export const nullDevice: DeviceDriver = {
392
439
  */
393
440
  export const zeroDevice: DeviceDriver = {
394
441
  name: 'zero',
395
- isBuffered: false,
442
+ init() {
443
+ return { major: 1, minor: 5 };
444
+ },
396
445
  read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number {
397
446
  const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
398
447
  for (let i = offset; i < offset + length; i++) {
@@ -412,7 +461,9 @@ export const zeroDevice: DeviceDriver = {
412
461
  */
413
462
  export const fullDevice: DeviceDriver = {
414
463
  name: 'full',
415
- isBuffered: false,
464
+ init() {
465
+ return { major: 1, minor: 7 };
466
+ },
416
467
  read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number {
417
468
  const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
418
469
  for (let i = offset; i < offset + length; i++) {
@@ -435,7 +486,9 @@ export const fullDevice: DeviceDriver = {
435
486
  */
436
487
  export const randomDevice: DeviceDriver = {
437
488
  name: 'random',
438
- isBuffered: false,
489
+ init() {
490
+ return { major: 1, minor: 8 };
491
+ },
439
492
  read(file: DeviceFile, buffer: ArrayBufferView, offset = 0, length = buffer.byteLength): number {
440
493
  const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
441
494
  for (let i = offset; i < offset + length; i++) {
@@ -5,14 +5,14 @@ export const config = {
5
5
  checkAccess: true,
6
6
 
7
7
  /**
8
- * Whether to sync atime updates immediately when reading from a file
8
+ * Whether to mark a file as dirty after updating its `atime` when read from
9
9
  */
10
- syncOnRead: true,
10
+ updateOnRead: true,
11
11
 
12
12
  /**
13
- * Whether to immediately sync when files are written to
13
+ * Whether to immediately sync when files are changed
14
14
  */
15
- syncOnWrite: true,
15
+ syncImmediately: true,
16
16
 
17
17
  /**
18
18
  * If a file's buffer is not large enough to store content when writing and the buffer can't be resized, reuse the buffer passed to write()
@@ -54,7 +54,7 @@ export function mount(mountPoint: string, fs: FileSystem): void {
54
54
  */
55
55
  export function umount(mountPoint: string): void {
56
56
  if (mountPoint[0] !== '/') {
57
- mountPoint = `/${mountPoint}`;
57
+ mountPoint = '/' + mountPoint;
58
58
  }
59
59
  mountPoint = resolve(mountPoint);
60
60
  if (!mounts.has(mountPoint)) {
@@ -64,11 +64,10 @@ export function umount(mountPoint: string): void {
64
64
  }
65
65
 
66
66
  /**
67
- * Gets the internal FileSystem for the path, then returns it along with the path relative to the FS' root
67
+ * Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root
68
68
  */
69
69
  export function resolveMount(path: string): { fs: FileSystem; path: string; mountPoint: string } {
70
70
  path = normalizePath(path);
71
- // Maybe do something for devices here
72
71
  const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length
73
72
  for (const [mountPoint, fs] of sortedMounts) {
74
73
  // We know path is normalized, so it would be a substring of the mount point.
package/src/file.ts CHANGED
@@ -441,12 +441,12 @@ export class PreloadFile<FS extends FileSystem> extends File {
441
441
 
442
442
  public async truncate(length: number): Promise<void> {
443
443
  this._truncate(length);
444
- if (config.syncOnWrite) await this.sync();
444
+ if (config.syncImmediately) await this.sync();
445
445
  }
446
446
 
447
447
  public truncateSync(length: number): void {
448
448
  this._truncate(length);
449
- if (config.syncOnWrite) this.syncSync();
449
+ if (config.syncImmediately) this.syncSync();
450
450
  }
451
451
 
452
452
  protected _write(buffer: Uint8Array, offset: number = 0, length: number = this.stats.size, position: number = this.position): number {
@@ -494,7 +494,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
494
494
  */
495
495
  public async write(buffer: Uint8Array, offset?: number, length?: number, position?: number): Promise<number> {
496
496
  const bytesWritten = this._write(buffer, offset, length, position);
497
- if (config.syncOnWrite) await this.sync();
497
+ if (config.syncImmediately) await this.sync();
498
498
  return bytesWritten;
499
499
  }
500
500
 
@@ -509,7 +509,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
509
509
  */
510
510
  public writeSync(buffer: Uint8Array, offset: number = 0, length: number = this.stats.size, position: number = this.position): number {
511
511
  const bytesWritten = this._write(buffer, offset, length, position);
512
- if (config.syncOnWrite) this.syncSync();
512
+ if (config.syncImmediately) this.syncSync();
513
513
  return bytesWritten;
514
514
  }
515
515
 
@@ -517,11 +517,17 @@ export class PreloadFile<FS extends FileSystem> extends File {
517
517
  if (this.closed) {
518
518
  throw ErrnoError.With('EBADF', this.path, 'File.read');
519
519
  }
520
+
520
521
  if (!isReadable(this.flag)) {
521
522
  throw new ErrnoError(Errno.EPERM, 'File not opened with a readable mode.');
522
523
  }
523
- this.dirty = true;
524
+
525
+ if (config.updateOnRead) {
526
+ this.dirty = true;
527
+ }
528
+
524
529
  this.stats.atimeMs = Date.now();
530
+
525
531
  position ??= this.position;
526
532
  let end = position + length;
527
533
  if (end > this.stats.size) {
@@ -547,7 +553,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
547
553
  */
548
554
  public async read<TBuffer extends ArrayBufferView>(buffer: TBuffer, offset?: number, length?: number, position?: number): Promise<{ bytesRead: number; buffer: TBuffer }> {
549
555
  const bytesRead = this._read(buffer, offset, length, position);
550
- if (config.syncOnRead) await this.sync();
556
+ if (config.syncImmediately) await this.sync();
551
557
  return { bytesRead, buffer };
552
558
  }
553
559
 
@@ -562,7 +568,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
562
568
  */
563
569
  public readSync(buffer: ArrayBufferView, offset?: number, length?: number, position?: number): number {
564
570
  const bytesRead = this._read(buffer, offset, length, position);
565
- if (config.syncOnRead) this.syncSync();
571
+ if (config.syncImmediately) this.syncSync();
566
572
  return bytesRead;
567
573
  }
568
574
 
@@ -572,7 +578,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
572
578
  }
573
579
  this.dirty = true;
574
580
  this.stats.chmod(mode);
575
- if (config.syncOnWrite) await this.sync();
581
+ if (config.syncImmediately) await this.sync();
576
582
  }
577
583
 
578
584
  public chmodSync(mode: number): void {
@@ -581,7 +587,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
581
587
  }
582
588
  this.dirty = true;
583
589
  this.stats.chmod(mode);
584
- if (config.syncOnWrite) this.syncSync();
590
+ if (config.syncImmediately) this.syncSync();
585
591
  }
586
592
 
587
593
  public async chown(uid: number, gid: number): Promise<void> {
@@ -590,7 +596,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
590
596
  }
591
597
  this.dirty = true;
592
598
  this.stats.chown(uid, gid);
593
- if (config.syncOnWrite) await this.sync();
599
+ if (config.syncImmediately) await this.sync();
594
600
  }
595
601
 
596
602
  public chownSync(uid: number, gid: number): void {
@@ -599,7 +605,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
599
605
  }
600
606
  this.dirty = true;
601
607
  this.stats.chown(uid, gid);
602
- if (config.syncOnWrite) this.syncSync();
608
+ if (config.syncImmediately) this.syncSync();
603
609
  }
604
610
 
605
611
  public async utimes(atime: Date, mtime: Date): Promise<void> {
@@ -609,7 +615,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
609
615
  this.dirty = true;
610
616
  this.stats.atime = atime;
611
617
  this.stats.mtime = mtime;
612
- if (config.syncOnWrite) await this.sync();
618
+ if (config.syncImmediately) await this.sync();
613
619
  }
614
620
 
615
621
  public utimesSync(atime: Date, mtime: Date): void {
@@ -619,7 +625,7 @@ export class PreloadFile<FS extends FileSystem> extends File {
619
625
  this.dirty = true;
620
626
  this.stats.atime = atime;
621
627
  this.stats.mtime = mtime;
622
- if (config.syncOnWrite) this.syncSync();
628
+ if (config.syncImmediately) this.syncSync();
623
629
  }
624
630
 
625
631
  public async _setType(type: FileType): Promise<void> {