@zenfs/core 1.10.4 → 1.11.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.
@@ -1,18 +1,19 @@
1
1
  import type { File } from '../internal/file.js';
2
- import type { CreationOptions, UsageInfo } from '../internal/filesystem.js';
2
+ import type { CreationOptions, StreamOptions, UsageInfo } from '../internal/filesystem.js';
3
3
  import type { InodeLike } from '../internal/inode.js';
4
4
  import type { Stats } from '../stats.js';
5
5
  import { FileSystem } from '../internal/filesystem.js';
6
6
  import { EventEmitter } from 'eventemitter3';
7
+ import { type MountConfiguration } from '../config.js';
7
8
  /**
8
9
  * Configuration options for CoW.
9
10
  * @category Backends and Configuration
10
11
  */
11
12
  export interface CopyOnWriteOptions {
12
13
  /** The file system that initially populates this file system. */
13
- readable: FileSystem;
14
+ readable: MountConfiguration<any>;
14
15
  /** The file system to write modified files to. */
15
- writable: FileSystem;
16
+ writable: MountConfiguration<any>;
16
17
  /** @see {@link Journal} */
17
18
  journal?: Journal;
18
19
  }
@@ -98,6 +99,8 @@ export declare class CopyOnWriteFS extends FileSystem {
98
99
  mkdirSync(path: string, mode: number, options: CreationOptions): void;
99
100
  readdir(path: string): Promise<string[]>;
100
101
  readdirSync(path: string): string[];
102
+ streamRead(path: string, options: StreamOptions): ReadableStream;
103
+ streamWrite(path: string, options: StreamOptions): WritableStream;
101
104
  /**
102
105
  * Create the needed parent directories on the writable storage should they not exist.
103
106
  * Use modes from the read-only storage.
@@ -143,7 +146,7 @@ declare const _CopyOnWrite: {
143
146
  readonly required: false;
144
147
  };
145
148
  };
146
- readonly create: (options: CopyOnWriteOptions) => CopyOnWriteFS;
149
+ readonly create: (options: CopyOnWriteOptions) => Promise<CopyOnWriteFS>;
147
150
  };
148
151
  type _CopyOnWrite = typeof _CopyOnWrite;
149
152
  export interface CopyOnWrite extends _CopyOnWrite {
@@ -57,6 +57,7 @@ import { FileSystem } from '../internal/filesystem.js';
57
57
  import { debug, err, warn } from '../internal/log.js';
58
58
  import { dirname, join } from '../vfs/path.js';
59
59
  import { EventEmitter } from 'eventemitter3';
60
+ import { resolveMountConfig } from '../config.js';
60
61
  const journalOperations = ['delete'];
61
62
  /** Because TS doesn't work right w/o it */
62
63
  function isJournalOp(op) {
@@ -323,9 +324,9 @@ export class CopyOnWriteFS extends FileSystem {
323
324
  this.writable.mkdirSync(path, mode, options);
324
325
  }
325
326
  async readdir(path) {
326
- if (this.isDeleted(path))
327
+ if (this.isDeleted(path) || !(await this.exists(path)))
327
328
  throw ErrnoError.With('ENOENT', path, 'readdir');
328
- const entries = await this.writable.readdir(path);
329
+ const entries = (await this.readable.exists(path)) ? await this.readable.readdir(path) : [];
329
330
  if (await this.writable.exists(path))
330
331
  for (const entry of await this.writable.readdir(path)) {
331
332
  if (!entries.includes(entry))
@@ -334,9 +335,9 @@ export class CopyOnWriteFS extends FileSystem {
334
335
  return entries.filter(entry => !this.isDeleted(join(path, entry)));
335
336
  }
336
337
  readdirSync(path) {
337
- if (this.isDeleted(path))
338
+ if (this.isDeleted(path) || !this.existsSync(path))
338
339
  throw ErrnoError.With('ENOENT', path, 'readdir');
339
- const entries = this.writable.readdirSync(path);
340
+ const entries = this.readable.existsSync(path) ? this.readable.readdirSync(path) : [];
340
341
  if (this.writable.existsSync(path))
341
342
  for (const entry of this.writable.readdirSync(path)) {
342
343
  if (!entries.includes(entry))
@@ -344,6 +345,13 @@ export class CopyOnWriteFS extends FileSystem {
344
345
  }
345
346
  return entries.filter(entry => !this.isDeleted(join(path, entry)));
346
347
  }
348
+ streamRead(path, options) {
349
+ return this.writable.existsSync(path) ? this.writable.streamRead(path, options) : this.readable.streamRead(path, options);
350
+ }
351
+ streamWrite(path, options) {
352
+ this.copyForWriteSync(path);
353
+ return this.writable.streamWrite(path, options);
354
+ }
347
355
  /**
348
356
  * Create the needed parent directories on the writable storage should they not exist.
349
357
  * Use modes from the read-only storage.
@@ -478,8 +486,10 @@ const _CopyOnWrite = {
478
486
  readable: { type: 'object', required: true },
479
487
  journal: { type: 'object', required: false },
480
488
  },
481
- create(options) {
482
- return new CopyOnWriteFS(options.readable, options.writable, options.journal);
489
+ async create(options) {
490
+ const readable = await resolveMountConfig(options.readable);
491
+ const writable = await resolveMountConfig(options.writable);
492
+ return new CopyOnWriteFS(readable, writable, options.journal);
483
493
  },
484
494
  };
485
495
  /**
@@ -50,7 +50,6 @@ export class FetchFS extends IndexFS {
50
50
  const inode = this.index.get(path);
51
51
  if (!inode)
52
52
  throw ErrnoError.With('ENOENT', path, 'read');
53
- end !== null && end !== void 0 ? end : (end = inode.size);
54
53
  if (end - offset == 0)
55
54
  return;
56
55
  const data = await requests
@@ -65,7 +64,6 @@ export class FetchFS extends IndexFS {
65
64
  const inode = this.index.get(path);
66
65
  if (!inode)
67
66
  throw ErrnoError.With('ENOENT', path, 'read');
68
- end !== null && end !== void 0 ? end : (end = inode.size);
69
67
  if (end - offset == 0)
70
68
  return;
71
69
  const { data, missing } = requests.getCached(this.baseUrl + path, { start: offset, end, size: inode.size, warn });
@@ -78,9 +76,17 @@ export class FetchFS extends IndexFS {
78
76
  buffer.set(data);
79
77
  }
80
78
  async write(path, data, offset) {
79
+ const inode = this.index.get(path);
80
+ if (!inode)
81
+ throw ErrnoError.With('ENOENT', path, 'write');
82
+ inode.update({ mtimeMs: Date.now(), size: Math.max(inode.size, data.byteLength + offset) });
81
83
  await requests.set(this.baseUrl + path, data, { offset, warn, cacheOnly: !this.remoteWrite }, this.requestInit).catch(parseError(path, this));
82
84
  }
83
85
  writeSync(path, data, offset) {
86
+ const inode = this.index.get(path);
87
+ if (!inode)
88
+ throw ErrnoError.With('ENOENT', path, 'write');
89
+ inode.update({ mtimeMs: Date.now(), size: Math.max(inode.size, data.byteLength + offset) });
84
90
  this._async(requests.set(this.baseUrl + path, data, { offset, warn, cacheOnly: !this.remoteWrite }, this.requestInit).catch(parseError(path, this)));
85
91
  }
86
92
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- /// <reference path="../types/readable-stream.d.ts" preserve="true" />
2
1
  export * from './backends/index.js';
3
2
  export * from './config.js';
4
3
  export * from './context.js';
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- // eslint-disable-next-line @typescript-eslint/triple-slash-reference
2
- /// <reference path="../types/readable-stream.d.ts" preserve="true" />
3
1
  export * from './backends/index.js';
4
2
  export * from './config.js';
5
3
  export * from './context.js';
@@ -96,6 +96,16 @@ export type FileSystemAttributes = {
96
96
  * This should be set for read-only file systems.
97
97
  */
98
98
  no_write: void;
99
+ /**
100
+ * The FS is using the default implementation for `streamRead`
101
+ * @internal
102
+ */
103
+ default_stream_read: void;
104
+ /**
105
+ * The FS is using the default implementation for `streamWrite`
106
+ * @internal
107
+ */
108
+ default_stream_write: void;
99
109
  };
100
110
  /**
101
111
  * Options used when creating files and directories.
@@ -131,6 +141,13 @@ export interface PureCreationOptions extends CreationOptions {
131
141
  */
132
142
  mode: number;
133
143
  }
144
+ /**
145
+ * @internal
146
+ */
147
+ export interface StreamOptions {
148
+ start?: number;
149
+ end?: number;
150
+ }
134
151
  /**
135
152
  * Provides a consistent and easy to use internal API.
136
153
  * Default implementations for `exists` and `existsSync` are included.
@@ -158,7 +175,7 @@ export declare abstract class FileSystem {
158
175
  /**
159
176
  * @see FileSystemAttributes
160
177
  */
161
- readonly attributes: ConstMap<FileSystemAttributes>;
178
+ readonly attributes: ConstMap<FileSystemAttributes> & Map<string, any>;
162
179
  constructor(
163
180
  /**
164
181
  * A unique ID for this kind of file system.
@@ -253,4 +270,14 @@ export declare abstract class FileSystem {
253
270
  * @param offset The offset in the file to start writing
254
271
  */
255
272
  abstract writeSync(path: string, buffer: Uint8Array, offset: number): void;
273
+ /**
274
+ * Read a file using a stream.
275
+ * @privateRemarks The default implementation of `streamRead` uses "chunked" `read`s
276
+ */
277
+ streamRead(path: string, options: StreamOptions): ReadableStream;
278
+ /**
279
+ * Write a file using stream.
280
+ * @privateRemarks The default implementation of `streamWrite` uses "chunked" `write`s
281
+ */
282
+ streamWrite(path: string, options: StreamOptions): WritableStream;
256
283
  }
@@ -1,3 +1,4 @@
1
+ const _chunkSize = 0x1000;
1
2
  /**
2
3
  * Provides a consistent and easy to use internal API.
3
4
  * Default implementations for `exists` and `existsSync` are included.
@@ -23,6 +24,10 @@ export class FileSystem {
23
24
  * @see FileSystemAttributes
24
25
  */
25
26
  this.attributes = new Map();
27
+ if (this.streamRead === FileSystem.prototype.streamRead)
28
+ this.attributes.set('default_stream_read');
29
+ if (this.streamWrite === FileSystem.prototype.streamWrite)
30
+ this.attributes.set('default_stream_write');
26
31
  }
27
32
  toString() {
28
33
  var _a;
@@ -81,4 +86,38 @@ export class FileSystem {
81
86
  return e.code != 'ENOENT';
82
87
  }
83
88
  }
89
+ /**
90
+ * Read a file using a stream.
91
+ * @privateRemarks The default implementation of `streamRead` uses "chunked" `read`s
92
+ */
93
+ streamRead(path, options) {
94
+ return new ReadableStream({
95
+ start: async (controller) => {
96
+ const { size } = await this.stat(path);
97
+ const { start = 0, end = size } = options;
98
+ for (let offset = start; offset < end; offset += _chunkSize) {
99
+ const bytesRead = offset + _chunkSize > end ? end - offset : _chunkSize;
100
+ const buffer = new Uint8Array(bytesRead);
101
+ await this.read(path, buffer, offset, offset + bytesRead).catch(controller.error.bind(controller));
102
+ controller.enqueue(buffer);
103
+ }
104
+ controller.close();
105
+ },
106
+ type: 'bytes',
107
+ });
108
+ }
109
+ /**
110
+ * Write a file using stream.
111
+ * @privateRemarks The default implementation of `streamWrite` uses "chunked" `write`s
112
+ */
113
+ streamWrite(path, options) {
114
+ var _a;
115
+ let position = (_a = options.start) !== null && _a !== void 0 ? _a : 0;
116
+ return new WritableStream({
117
+ write: async (chunk, controller) => {
118
+ await this.write(path, chunk, position).catch(controller.error.bind(controller));
119
+ position += chunk.byteLength;
120
+ },
121
+ });
122
+ }
84
123
  }
@@ -96,7 +96,7 @@ export declare const formats: {
96
96
  readonly ansi_message: (this: void, entry: Entry) => string;
97
97
  readonly css_level: (this: void, entry: Entry) => string[];
98
98
  readonly css_message: (this: void, entry: Entry) => string[];
99
- readonly default: (this: void, entry: Entry) => string;
99
+ readonly default: (this: void, entry: Entry) => string[];
100
100
  };
101
101
  export declare function format(entry: Entry): string[];
102
102
  /** Whether log entries are being recorded */
@@ -172,20 +172,20 @@ export const formats = {
172
172
  },
173
173
  css_level(entry) {
174
174
  const levelLabel = levels[entry.level].toUpperCase();
175
- return [..._prettyMs(entry, 'css'), `%c${levelLabel}%c ${entry.message}`, _cssLevelColor[entry.level], ''];
175
+ return [..._prettyMs(entry, 'css'), '%c' + levelLabel, _cssLevelColor[entry.level], entry.message];
176
176
  },
177
177
  css_message(entry) {
178
178
  const text = _prettyMs(entry, 'css');
179
179
  const isImportant = entry.level < Level.CRIT;
180
180
  if (isImportant) {
181
181
  const levelLabel = levels[entry.level].toUpperCase();
182
- text.push(`%c${levelLabel}%c:`, _cssLevelColor[entry.level], '');
182
+ text.push('%c' + levelLabel, _cssLevelColor[entry.level]);
183
183
  }
184
184
  text.push('%c' + entry.message, _cssMessageColor[entry.level]);
185
185
  return text;
186
186
  },
187
187
  default(entry) {
188
- return `[${_prettyMs(entry)}] ${entry.message}`;
188
+ return [_prettyMs(entry), entry.message];
189
189
  },
190
190
  };
191
191
  let _format = formats.default;
@@ -186,6 +186,22 @@ export function Async(FS) {
186
186
  this._sync.writeSync(path, buffer, offset);
187
187
  this._async(this.write(path, buffer, offset));
188
188
  }
189
+ streamWrite(path, options) {
190
+ this.checkSync(path, 'streamWrite');
191
+ const sync = this._sync.streamWrite(path, options).getWriter();
192
+ const async = super.streamWrite(path, options).getWriter();
193
+ return new WritableStream({
194
+ async write(chunk, controller) {
195
+ await Promise.all([sync.write(chunk), async.write(chunk)]).catch(controller.error.bind(controller));
196
+ },
197
+ async close() {
198
+ await Promise.all([sync.close(), async.close()]);
199
+ },
200
+ async abort(reason) {
201
+ await Promise.all([sync.abort(reason), async.abort(reason)]);
202
+ },
203
+ });
204
+ }
189
205
  /**
190
206
  * @internal
191
207
  */
@@ -1,5 +1,5 @@
1
1
  import type { File } from '../internal/file.js';
2
- import type { CreationOptions, FileSystem, FileSystemMetadata, UsageInfo } from '../internal/filesystem.js';
2
+ import type { CreationOptions, FileSystem, FileSystemMetadata, StreamOptions, UsageInfo } from '../internal/filesystem.js';
3
3
  import type { InodeLike } from '../internal/inode.js';
4
4
  import type { Stats } from '../stats.js';
5
5
  import type { Concrete } from '../utils.js';
@@ -31,7 +31,7 @@ export declare class _MutexedFS<T extends FileSystem> implements FileSystem {
31
31
  get name(): string;
32
32
  get label(): string | undefined;
33
33
  set label(value: string | undefined);
34
- get attributes(): import("utilium").ConstMap<import("../internal/filesystem.js").FileSystemAttributes, keyof import("../internal/filesystem.js").FileSystemAttributes, void>;
34
+ get attributes(): import("utilium").ConstMap<import("../internal/filesystem.js").FileSystemAttributes, keyof import("../internal/filesystem.js").FileSystemAttributes, void> & Map<string, any>;
35
35
  ready(): Promise<void>;
36
36
  usage(): UsageInfo;
37
37
  metadata(): FileSystemMetadata;
@@ -86,6 +86,8 @@ export declare class _MutexedFS<T extends FileSystem> implements FileSystem {
86
86
  readSync(path: string, buffer: Uint8Array, offset: number, end: number): void;
87
87
  write(path: string, buffer: Uint8Array, offset: number): Promise<void>;
88
88
  writeSync(path: string, buffer: Uint8Array, offset: number): void;
89
+ streamRead(path: string, options: StreamOptions): ReadableStream;
90
+ streamWrite(path: string, options: StreamOptions): WritableStream;
89
91
  }
90
92
  /**
91
93
  * This serializes access to an underlying async filesystem.
@@ -528,6 +528,34 @@ export class _MutexedFS {
528
528
  __disposeResources(env_26);
529
529
  }
530
530
  }
531
+ streamRead(path, options) {
532
+ const env_27 = { stack: [], error: void 0, hasError: false };
533
+ try {
534
+ const _ = __addDisposableResource(env_27, this.lockSync(path, 'streamRead'), false);
535
+ return this._fs.streamRead(path, options);
536
+ }
537
+ catch (e_27) {
538
+ env_27.error = e_27;
539
+ env_27.hasError = true;
540
+ }
541
+ finally {
542
+ __disposeResources(env_27);
543
+ }
544
+ }
545
+ streamWrite(path, options) {
546
+ const env_28 = { stack: [], error: void 0, hasError: false };
547
+ try {
548
+ const _ = __addDisposableResource(env_28, this.lockSync(path, 'streamWrite'), false);
549
+ return this._fs.streamWrite(path, options);
550
+ }
551
+ catch (e_28) {
552
+ env_28.error = e_28;
553
+ env_28.hasError = true;
554
+ }
555
+ finally {
556
+ __disposeResources(env_28);
557
+ }
558
+ }
531
559
  }
532
560
  /**
533
561
  * This serializes access to an underlying async filesystem.
@@ -58,6 +58,9 @@ export function Readonly(FS) {
58
58
  writeSync() {
59
59
  throw new ErrnoError(Errno.EROFS);
60
60
  }
61
+ streamWrite() {
62
+ throw new ErrnoError(Errno.EROFS);
63
+ }
61
64
  }
62
65
  return ReadonlyFS;
63
66
  }
package/dist/stats.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { InodeFields, InodeLike } from './internal/inode.js';
4
4
  import * as c from './vfs/constants.js';
5
5
  /**
6
6
  * Indicates the type of a file. Applied to 'mode'.
7
+ * @deprecated
7
8
  */
8
9
  export type FileType = typeof c.S_IFREG | typeof c.S_IFDIR | typeof c.S_IFLNK;
9
10
  export interface StatsLike<T extends number | bigint = number | bigint> {
@@ -60,7 +61,6 @@ export declare abstract class StatsCommon<T extends number | bigint> implements
60
61
  set blocks(value: T);
61
62
  /**
62
63
  * Unix-style file mode (e.g. 0o644) that includes the type of the item.
63
- * Type of the item can be FILE, DIRECTORY, SYMLINK, or SOCKET
64
64
  */
65
65
  mode: T;
66
66
  /**
@@ -7,8 +7,7 @@ import type { FileContents } from './types.js';
7
7
  import { Buffer } from 'buffer';
8
8
  import { ErrnoError } from '../internal/error.js';
9
9
  import { BigIntStats } from '../stats.js';
10
- import * as promises from './promises.js';
11
- import { ReadStream, WriteStream } from './streams.js';
10
+ import { ReadStream, WriteStream, type ReadStreamOptions, type WriteStreamOptions } from './streams.js';
12
11
  import { FSWatcher } from './watchers.js';
13
12
  /**
14
13
  * Asynchronous rename. No arguments other than a possible exception are given to the completion callback.
@@ -237,34 +236,6 @@ export declare function watch(this: V_Context, path: fs.PathLike, listener?: (ev
237
236
  export declare function watch(this: V_Context, path: fs.PathLike, options: {
238
237
  persistent?: boolean;
239
238
  }, listener?: (event: string, filename: string) => any): FSWatcher;
240
- interface StreamOptions {
241
- flags?: string;
242
- encoding?: BufferEncoding;
243
- fd?: number | promises.FileHandle;
244
- mode?: number;
245
- autoClose?: boolean;
246
- emitClose?: boolean;
247
- start?: number;
248
- signal?: AbortSignal;
249
- highWaterMark?: number;
250
- }
251
- interface FSImplementation {
252
- open?: (...args: unknown[]) => unknown;
253
- close?: (...args: unknown[]) => unknown;
254
- }
255
- interface ReadStreamOptions extends StreamOptions {
256
- fs?: FSImplementation & {
257
- read: (...args: unknown[]) => unknown;
258
- };
259
- end?: number;
260
- }
261
- interface WriteStreamOptions extends StreamOptions {
262
- fs?: FSImplementation & {
263
- write: (...args: unknown[]) => unknown;
264
- writev?: (...args: unknown[]) => unknown;
265
- };
266
- flush?: boolean;
267
- }
268
239
  /**
269
240
  * Opens a file in read mode and creates a Node.js-like ReadStream.
270
241
  *
package/dist/vfs/async.js CHANGED
@@ -421,47 +421,9 @@ watch;
421
421
  * @returns A ReadStream object for interacting with the file's contents.
422
422
  */
423
423
  export function createReadStream(path, options) {
424
- var _a;
425
424
  options = typeof options == 'object' ? options : { encoding: options };
426
425
  const _handle = promises.open.call(this, path, 'r', options === null || options === void 0 ? void 0 : options.mode);
427
- void _handle.then(handle => {
428
- if (typeof options.start == 'number')
429
- handle.file.position = options.start;
430
- });
431
- let handle;
432
- const stream = new ReadStream({
433
- highWaterMark: options.highWaterMark || 0x1000,
434
- encoding: (_a = options.encoding) !== null && _a !== void 0 ? _a : undefined,
435
- async read(size) {
436
- var _a;
437
- try {
438
- handle || (handle = await _handle);
439
- const start = (_a = options.start) !== null && _a !== void 0 ? _a : handle.file.position;
440
- if (typeof options.end === 'number' && start >= options.end) {
441
- stream.push(null);
442
- await handle.close();
443
- return;
444
- }
445
- if (typeof options.end === 'number') {
446
- size = Math.min(size, options.end - start);
447
- }
448
- const result = await handle.read(new Uint8Array(size), 0, size);
449
- stream.push(!result.bytesRead ? null : result.buffer.subarray(0, result.bytesRead));
450
- if (!result.bytesRead)
451
- await handle.close();
452
- }
453
- catch (error) {
454
- await (handle === null || handle === void 0 ? void 0 : handle.close());
455
- stream.destroy(error);
456
- }
457
- },
458
- async destroy(error, callback) {
459
- await (handle === null || handle === void 0 ? void 0 : handle.close().catch(nop));
460
- callback(error);
461
- },
462
- });
463
- stream.path = path.toString();
464
- return stream;
426
+ return new ReadStream({ ...options, autoClose: true }, _handle);
465
427
  }
466
428
  createReadStream;
467
429
  /**
@@ -474,39 +436,7 @@ createReadStream;
474
436
  export function createWriteStream(path, options) {
475
437
  options = typeof options == 'object' ? options : { encoding: options };
476
438
  const _handle = promises.open.call(this, path, 'w', options === null || options === void 0 ? void 0 : options.mode);
477
- void _handle.then(handle => {
478
- if (typeof options.start == 'number')
479
- handle.file.position = options.start;
480
- });
481
- let handle;
482
- const { stack } = new Error();
483
- const stream = new WriteStream({
484
- highWaterMark: options === null || options === void 0 ? void 0 : options.highWaterMark,
485
- async write(chunk, encoding, callback) {
486
- try {
487
- handle || (handle = await _handle);
488
- const { bytesWritten } = await handle.write(chunk, null, encoding);
489
- if (bytesWritten == chunk.length)
490
- return callback();
491
- throw new ErrnoError(Errno.EIO, `Failed to write full chunk of write stream (wrote ${bytesWritten}/${chunk.length} bytes)`, handle.file.path);
492
- }
493
- catch (error) {
494
- await (handle === null || handle === void 0 ? void 0 : handle.close());
495
- error.stack += stack === null || stack === void 0 ? void 0 : stack.slice(5);
496
- callback(error);
497
- }
498
- },
499
- async destroy(error, callback) {
500
- await (handle === null || handle === void 0 ? void 0 : handle.close().catch(callback));
501
- callback(error);
502
- },
503
- async final(callback) {
504
- await (handle === null || handle === void 0 ? void 0 : handle.close().catch(nop));
505
- callback();
506
- },
507
- });
508
- stream.path = path.toString();
509
- return stream;
439
+ return new WriteStream(options, _handle);
510
440
  }
511
441
  createWriteStream;
512
442
  export function rm(path, options, callback = nop) {
@@ -1,7 +1,8 @@
1
1
  import type * as fs from 'node:fs';
2
2
  import type * as promises from 'node:fs/promises';
3
+ import type { Interface as ReadlineInterface } from 'node:readline';
3
4
  import type { Stream } from 'node:stream';
4
- import type { Interface as ReadlineInterface } from 'readline';
5
+ import type { ReadableStream as NodeReadableStream } from 'node:stream/web';
5
6
  import type { V_Context } from '../context.js';
6
7
  import type { File } from '../internal/file.js';
7
8
  import type { Stats } from '../stats.js';
@@ -88,7 +89,7 @@ export declare class FileHandle implements promises.FileHandle {
88
89
  * Read file data using a `ReadableStream`.
89
90
  * The handle will not be closed automatically.
90
91
  */
91
- readableWebStream(options?: promises.ReadableWebStreamOptions): ReadableStream<Uint8Array>;
92
+ readableWebStream(options?: promises.ReadableWebStreamOptions): NodeReadableStream<Uint8Array>;
92
93
  /**
93
94
  * @todo Implement
94
95
  */
@@ -51,6 +51,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
53
  import { Buffer } from 'buffer';
54
+ import { _throw } from 'utilium';
54
55
  import { credentials } from '../internal/credentials.js';
55
56
  import { Errno, ErrnoError } from '../internal/error.js';
56
57
  import { flagToMode, isAppendable, isExclusive, isReadable, isTruncating, isWriteable, parseFlag } from '../internal/file.js';
@@ -64,7 +65,6 @@ import { dirname, join, parse, resolve } from './path.js';
64
65
  import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount } from './shared.js';
65
66
  import { ReadStream, WriteStream } from './streams.js';
66
67
  import { FSWatcher, emitChange } from './watchers.js';
67
- import { _throw } from 'utilium';
68
68
  export * as constants from './constants.js';
69
69
  export class FileHandle {
70
70
  constructor(fdOrFile, context) {
@@ -184,23 +184,7 @@ export class FileHandle {
184
184
  * The handle will not be closed automatically.
185
185
  */
186
186
  readableWebStream(options = {}) {
187
- return new ReadableStream({
188
- start: async (controller) => {
189
- const chunkSize = 0x1000;
190
- for (let i = 0; i < 1e7; i++) {
191
- const result = await this.read(new Uint8Array(chunkSize), 0, chunkSize).catch(controller.error);
192
- if (!result)
193
- return;
194
- if (!result.bytesRead) {
195
- controller.close();
196
- return;
197
- }
198
- controller.enqueue(result.buffer.subarray(0, result.bytesRead));
199
- }
200
- controller.error(new ErrnoError(Errno.EFBIG, 'Too many iterations on readable stream', this.file.path, 'readableWebStream'));
201
- },
202
- type: options.type,
203
- });
187
+ return this.file.fs.streamRead(this.file.path, {});
204
188
  }
205
189
  /**
206
190
  * @todo Implement
@@ -312,59 +296,15 @@ export class FileHandle {
312
296
  * Creates a stream for reading from the file.
313
297
  * @param options Options for the readable stream
314
298
  */
315
- createReadStream(options) {
316
- var _a, _b;
317
- const start = (_a = options === null || options === void 0 ? void 0 : options.start) !== null && _a !== void 0 ? _a : this.file.position;
318
- const stream = new ReadStream({
319
- highWaterMark: (options === null || options === void 0 ? void 0 : options.highWaterMark) || 64 * 1024,
320
- encoding: (_b = options === null || options === void 0 ? void 0 : options.encoding) !== null && _b !== void 0 ? _b : undefined,
321
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
322
- read: async (size) => {
323
- try {
324
- if (typeof (options === null || options === void 0 ? void 0 : options.end) === 'number' && start >= options.end) {
325
- stream.push(null);
326
- return;
327
- }
328
- if (typeof (options === null || options === void 0 ? void 0 : options.end) === 'number') {
329
- size = Math.min(size, options.end - start);
330
- }
331
- const result = await this.read(new Uint8Array(size), 0, size, options === null || options === void 0 ? void 0 : options.start);
332
- stream.push(!result.bytesRead ? null : result.buffer.subarray(0, result.bytesRead));
333
- }
334
- catch (error) {
335
- stream.destroy(error);
336
- }
337
- },
338
- });
339
- stream.path = this.file.path;
340
- return stream;
299
+ createReadStream(options = {}) {
300
+ return new ReadStream(options, this);
341
301
  }
342
302
  /**
343
303
  * Creates a stream for writing to the file.
344
304
  * @param options Options for the writeable stream.
345
305
  */
346
- createWriteStream(options) {
347
- if (typeof (options === null || options === void 0 ? void 0 : options.start) == 'number')
348
- this.file.position = options.start;
349
- const { stack } = new Error();
350
- const stream = new WriteStream({
351
- highWaterMark: options === null || options === void 0 ? void 0 : options.highWaterMark,
352
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
353
- write: async (chunk, encoding, callback) => {
354
- try {
355
- const { bytesWritten } = await this.write(chunk, null, encoding);
356
- if (bytesWritten == chunk.length)
357
- return callback();
358
- throw new ErrnoError(Errno.EIO, `Failed to write full chunk of write stream (wrote ${bytesWritten}/${chunk.length} bytes)`);
359
- }
360
- catch (error) {
361
- error.stack += stack === null || stack === void 0 ? void 0 : stack.slice(5);
362
- callback(error);
363
- }
364
- },
365
- });
366
- stream.path = this.file.path;
367
- return stream;
306
+ createWriteStream(options = {}) {
307
+ return new WriteStream(options, this);
368
308
  }
369
309
  }
370
310
  export async function rename(oldPath, newPath) {
@@ -46,13 +46,12 @@ export function mount(mountPoint, fs) {
46
46
  if (mountPoint[0] != '/')
47
47
  mountPoint = '/' + mountPoint;
48
48
  mountPoint = resolve(mountPoint);
49
- if (mounts.has(mountPoint)) {
50
- throw err(new ErrnoError(Errno.EINVAL, 'Mount point ' + mountPoint + ' is already in use'));
51
- }
49
+ if (mounts.has(mountPoint))
50
+ throw err(new ErrnoError(Errno.EINVAL, 'Mount point is already in use: ' + mountPoint));
52
51
  fs._mountPoint = mountPoint;
53
52
  mounts.set(mountPoint, fs);
54
53
  info(`Mounted ${fs.name} on ${mountPoint}`);
55
- debug(`${fs.name} attributes: ${[...fs.attributes].map(([k, v]) => (v !== undefined && v !== null ? k + '=' + v : v)).join(', ')}`);
54
+ debug(`${fs.name} attributes: ${[...fs.attributes].map(([k, v]) => (v !== undefined && v !== null ? k + '=' + v : k)).join(', ')}`);
56
55
  }
57
56
  /**
58
57
  * Unmounts the file system at `mountPoint`.
@@ -1,16 +1,74 @@
1
+ /// <reference path="../../types/readable-stream.d.ts" preserve="true" />
2
+ import type { Abortable } from 'node:events';
1
3
  import type * as fs from 'node:fs';
4
+ import type { CreateReadStreamOptions, CreateWriteStreamOptions } from 'node:fs/promises';
2
5
  import type { Callback } from '../utils.js';
6
+ import type { FileHandle } from './promises.js';
3
7
  import { Readable, Writable } from 'readable-stream';
8
+ interface FSImplementation {
9
+ open?: (...args: unknown[]) => unknown;
10
+ close?: (...args: unknown[]) => unknown;
11
+ }
12
+ interface StreamOptions extends Abortable {
13
+ flags?: string;
14
+ encoding?: BufferEncoding;
15
+ fd?: number | FileHandle;
16
+ mode?: number;
17
+ autoClose?: boolean;
18
+ emitClose?: boolean;
19
+ start?: number;
20
+ highWaterMark?: number;
21
+ }
22
+ /**
23
+ * This type is from node:fs but not exported.
24
+ * @hidden
25
+ */
26
+ export interface ReadStreamOptions extends StreamOptions {
27
+ fs?: FSImplementation & {
28
+ read: (...args: unknown[]) => unknown;
29
+ };
30
+ end?: number;
31
+ }
32
+ /**
33
+ * This type is from node:fs but not exported.
34
+ * @hidden
35
+ */
36
+ export interface WriteStreamOptions extends StreamOptions {
37
+ flush?: boolean;
38
+ fs?: FSImplementation & {
39
+ write: (...args: unknown[]) => unknown;
40
+ writev?: (...args: unknown[]) => unknown;
41
+ };
42
+ }
43
+ /**
44
+ * A ReadStream implementation that wraps an underlying global ReadableStream.
45
+ */
4
46
  export declare class ReadStream extends Readable implements fs.ReadStream {
5
- close: (callback?: Callback<[void], null>) => void;
6
- wrap(oldStream: NodeJS.ReadableStream): this;
7
- bytesRead: number;
8
- path: string | Buffer;
9
47
  pending: boolean;
48
+ private _path;
49
+ private _bytesRead;
50
+ private reader?;
51
+ constructor(opts: CreateReadStreamOptions | undefined, handleOrPromise: FileHandle | Promise<FileHandle>);
52
+ _read(): Promise<void>;
53
+ close(callback?: Callback<[void], null>): void;
54
+ get path(): string;
55
+ get bytesRead(): number;
56
+ wrap(oldStream: NodeJS.ReadableStream): this;
10
57
  }
58
+ /**
59
+ * A WriteStream implementation that wraps an underlying global WritableStream.
60
+ */
11
61
  export declare class WriteStream extends Writable implements fs.WriteStream {
12
- close: (callback?: Callback<[void], null>) => void;
13
- bytesWritten: number;
14
- path: string | Buffer;
15
62
  pending: boolean;
63
+ private _path;
64
+ private _bytesWritten;
65
+ private writer?;
66
+ private ready;
67
+ constructor(opts: CreateWriteStreamOptions | undefined, handleOrPromise: FileHandle | Promise<FileHandle>);
68
+ _write(chunk: any, encoding: BufferEncoding | 'buffer', callback: (error?: Error | null) => void): Promise<void>;
69
+ _final(callback: (error?: Error | null) => void): Promise<void>;
70
+ close(callback?: Callback<[void], null>): void;
71
+ get path(): string;
72
+ get bytesWritten(): number;
16
73
  }
74
+ export {};
@@ -1,36 +1,120 @@
1
1
  import { Readable, Writable } from 'readable-stream';
2
2
  import { Errno, ErrnoError } from '../internal/error.js';
3
+ import { warn } from '../internal/log.js';
4
+ /**
5
+ * A ReadStream implementation that wraps an underlying global ReadableStream.
6
+ */
3
7
  export class ReadStream extends Readable {
4
- constructor() {
5
- super(...arguments);
6
- this.close = (callback = () => null) => {
7
- try {
8
- super.destroy();
9
- super.emit('close');
10
- callback(null);
11
- }
12
- catch (err) {
13
- callback(new ErrnoError(Errno.EIO, err.toString()));
14
- }
15
- };
8
+ constructor(opts = {}, handleOrPromise) {
9
+ var _a;
10
+ super({ ...opts, encoding: (_a = opts.encoding) !== null && _a !== void 0 ? _a : undefined });
11
+ this.pending = true;
12
+ this._path = '<unknown>';
13
+ this._bytesRead = 0;
14
+ Promise.resolve(handleOrPromise)
15
+ .then(({ file }) => {
16
+ this._path = file.path;
17
+ const internal = file.fs.streamRead(file.path, { start: opts.start, end: opts.end });
18
+ this.reader = internal.getReader();
19
+ this.pending = false;
20
+ return this._read();
21
+ })
22
+ .catch(err => this.destroy(err));
23
+ }
24
+ async _read() {
25
+ if (!this.reader)
26
+ return;
27
+ const { done, value } = await this.reader.read();
28
+ if (done) {
29
+ this.push(null);
30
+ return;
31
+ }
32
+ this._bytesRead += value.byteLength;
33
+ if (!this.push(value))
34
+ return;
35
+ await this._read();
36
+ }
37
+ close(callback = () => null) {
38
+ try {
39
+ this.destroy();
40
+ this.emit('close');
41
+ callback(null);
42
+ }
43
+ catch (err) {
44
+ callback(new ErrnoError(Errno.EIO, err.toString()));
45
+ }
46
+ }
47
+ get path() {
48
+ return this._path;
49
+ }
50
+ get bytesRead() {
51
+ return this._bytesRead;
16
52
  }
17
53
  wrap(oldStream) {
18
54
  super.wrap(oldStream);
19
55
  return this;
20
56
  }
21
57
  }
58
+ /**
59
+ * A WriteStream implementation that wraps an underlying global WritableStream.
60
+ */
22
61
  export class WriteStream extends Writable {
23
- constructor() {
24
- super(...arguments);
25
- this.close = (callback = () => null) => {
26
- try {
27
- super.destroy();
28
- super.emit('close');
29
- callback(null);
30
- }
31
- catch (err) {
32
- callback(new ErrnoError(Errno.EIO, err.toString()));
33
- }
34
- };
62
+ constructor(opts = {}, handleOrPromise) {
63
+ super(opts);
64
+ this.pending = true;
65
+ this._path = '<unknown>';
66
+ this._bytesWritten = 0;
67
+ this.ready = Promise.resolve(handleOrPromise)
68
+ .then(({ file }) => {
69
+ this._path = file.path;
70
+ const internal = file.fs.streamWrite(file.path, { start: opts.start });
71
+ this.writer = internal.getWriter();
72
+ this.pending = false;
73
+ })
74
+ .catch(err => this.destroy(err));
75
+ }
76
+ async _write(chunk, encoding, callback) {
77
+ await this.ready;
78
+ if (!this.writer)
79
+ return callback(warn(new ErrnoError(Errno.EAGAIN, 'Underlying writable stream not ready', this._path)));
80
+ if (encoding != 'buffer')
81
+ return callback(warn(new ErrnoError(Errno.ENOTSUP, 'Unsupported encoding for stream', this._path)));
82
+ const data = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
83
+ try {
84
+ await this.writer.write(data);
85
+ this._bytesWritten += chunk.byteLength;
86
+ callback();
87
+ }
88
+ catch (error) {
89
+ callback(new ErrnoError(Errno.EIO, error.toString()));
90
+ }
91
+ }
92
+ async _final(callback) {
93
+ await this.ready;
94
+ if (!this.writer)
95
+ return callback();
96
+ try {
97
+ await this.writer.close();
98
+ callback();
99
+ }
100
+ catch (error) {
101
+ callback(new ErrnoError(Errno.EIO, error.toString()));
102
+ }
103
+ }
104
+ close(callback = () => null) {
105
+ try {
106
+ this.destroy();
107
+ this.emit('close');
108
+ callback(null);
109
+ }
110
+ catch (error) {
111
+ callback(new ErrnoError(Errno.EIO, error.toString()));
112
+ }
113
+ }
114
+ get path() {
115
+ return this._path;
116
+ }
117
+ get bytesWritten() {
118
+ return this._bytesWritten;
35
119
  }
36
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "1.10.4",
3
+ "version": "1.11.1",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -40,7 +40,7 @@
40
40
  "url": "https://github.com/zen-fs/core/issues"
41
41
  },
42
42
  "engines": {
43
- "node": ">= 16"
43
+ "node": ">= 18"
44
44
  },
45
45
  "exports": {
46
46
  ".": "./dist/index.js",
@@ -49,7 +49,8 @@
49
49
  "./promises": "./dist/vfs/promises.js",
50
50
  "./path": "./dist/vfs/path.js",
51
51
  "./eslint": "./eslint.shared.js",
52
- "./tests/*": "./tests/*"
52
+ "./tests/*": "./tests/*",
53
+ "./types/*": "./types/*"
53
54
  },
54
55
  "publishConfig": {
55
56
  "access": "public",
@@ -1,7 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { suite, test } from 'node:test';
3
3
  import { fs } from '../common.js';
4
- import { promisify } from 'node:util';
5
4
 
6
5
  // Top-level initialization
7
6
  const testFilePath = 'test-file.txt';
@@ -43,30 +42,6 @@ suite('Streams', () => {
43
42
  });
44
43
  });
45
44
 
46
- test('ReadStream declared properties', () => {
47
- const readStream = new fs.ReadStream();
48
- assert.equal(readStream.bytesRead, undefined);
49
- assert.equal(readStream.path, undefined);
50
- assert.equal(readStream.pending, undefined);
51
-
52
- // Assign values
53
- readStream.bytesRead = 10;
54
- readStream.path = testFilePath;
55
- readStream.pending = false;
56
-
57
- assert.equal(readStream.bytesRead, 10);
58
- assert.equal(readStream.path, testFilePath);
59
- assert(!readStream.pending);
60
- });
61
-
62
- test('ReadStream close method can be called multiple times', async () => {
63
- const readStream = new fs.ReadStream();
64
-
65
- const close = promisify(readStream.close);
66
- await close();
67
- await close();
68
- });
69
-
70
45
  test('WriteStream writes data correctly', async () => {
71
46
  const writeStream = fs.createWriteStream(testFilePathWrite);
72
47
 
@@ -92,30 +67,6 @@ suite('Streams', () => {
92
67
  });
93
68
  });
94
69
 
95
- test('WriteStream declared properties', () => {
96
- const writeStream = new fs.WriteStream();
97
- assert.equal(writeStream.bytesWritten, undefined);
98
- assert.equal(writeStream.path, undefined);
99
- assert.equal(writeStream.pending, undefined);
100
-
101
- // Assign values
102
- writeStream.bytesWritten = 20;
103
- writeStream.path = testFilePathWrite;
104
- writeStream.pending = true;
105
-
106
- assert.equal(writeStream.bytesWritten, 20);
107
- assert.equal(writeStream.path, testFilePathWrite);
108
- assert(writeStream.pending);
109
- });
110
-
111
- test('WriteStream close method can be called multiple times', async () => {
112
- const writeStream = new fs.WriteStream();
113
-
114
- const close = promisify(writeStream.close);
115
- await close();
116
- await close();
117
- });
118
-
119
70
  test('createReadStream with start', async () => {
120
71
  await fs.promises.writeFile('hello.txt', 'Hello world');
121
72
 
@@ -159,6 +110,22 @@ suite('Streams', () => {
159
110
  await fileHandle.close();
160
111
  });
161
112
 
113
+ test('readable web stream', async () => {
114
+ const fileHandle = await fs.promises.open(testFilePath, 'r');
115
+ const webStream = fileHandle.readableWebStream();
116
+
117
+ let data = '';
118
+
119
+ const decoder = new TextDecoder();
120
+
121
+ for await (const chunk of webStream) {
122
+ data += decoder.decode(chunk);
123
+ }
124
+
125
+ assert.equal(data, testData);
126
+ await fileHandle.close();
127
+ });
128
+
162
129
  test('FileHandle.createReadStream after close should give an error', async () => {
163
130
  const fileHandle = await fs.promises.open(testFilePath, 'r');
164
131
  await fileHandle.close();