@zenfs/core 2.7.3-newstreams.0 → 2.7.3

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.
@@ -117,7 +117,6 @@ export interface StreamOptions {
117
117
  start?: number;
118
118
  end?: number;
119
119
  autoClose?: boolean;
120
- chunkSize?: number;
121
120
  }
122
121
  /**
123
122
  * Provides a consistent and easy to use internal API.
@@ -1,8 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { withErrno } from 'kerium';
3
3
  import { ioctl_default_ops_async, ioctl_default_ops_sync } from './ioctl.js';
4
- /** Default bytes per chunk for streamed reads, matching Node's default stream `highWaterMark` */
5
- const defaultChunkSize = 0x10000;
4
+ const _chunkSize = 0x1000;
6
5
  /**
7
6
  * Provides a consistent and easy to use internal API.
8
7
  * Default implementations for `exists` and `existsSync` are included.
@@ -98,22 +97,17 @@ export class FileSystem {
98
97
  * @privateRemarks The default implementation of `streamRead` uses "chunked" `read`s
99
98
  */
100
99
  streamRead(path, options) {
101
- const { chunkSize = defaultChunkSize } = options;
102
- let offset = options.start ?? 0;
103
- let end = options.end;
104
100
  return new ReadableStream({
105
- pull: async (controller) => {
106
- end ??= (await this.stat(path)).size;
107
- if (offset >= end) {
108
- controller.close();
109
- return;
101
+ start: async (controller) => {
102
+ const { size } = await this.stat(path);
103
+ const { start = 0, end = size } = options;
104
+ for (let offset = start; offset < end; offset += _chunkSize) {
105
+ const bytesRead = offset + _chunkSize > end ? end - offset : _chunkSize;
106
+ const buffer = new Uint8Array(bytesRead);
107
+ await this.read(path, buffer, offset, offset + bytesRead).catch(controller.error.bind(controller));
108
+ controller.enqueue(buffer);
110
109
  }
111
- const buffer = new Uint8Array(Math.min(chunkSize, end - offset));
112
- await this.read(path, buffer, offset, offset + buffer.byteLength);
113
- offset += buffer.byteLength;
114
- controller.enqueue(buffer);
115
- if (offset >= end)
116
- controller.close();
110
+ controller.close();
117
111
  },
118
112
  type: 'bytes',
119
113
  });
@@ -1,10 +1,10 @@
1
+ /// <reference path="../../types/readable-stream.d.ts" preserve="true" />
1
2
  import type { Abortable } from 'node:events';
2
3
  import type * as fs from 'node:fs';
3
4
  import type { CreateReadStreamOptions, CreateWriteStreamOptions } from 'node:fs/promises';
4
5
  import type { Callback } from '../utils.js';
5
6
  import type { FileHandle } from './promises.js';
6
- import { Readable } from '@zenfs/streams/readable';
7
- import { Writable } from '@zenfs/streams/writable';
7
+ import { Readable, Writable } from 'readable-stream';
8
8
  interface FSImplementation {
9
9
  open?: (...args: unknown[]) => unknown;
10
10
  close?: (...args: unknown[]) => unknown;
@@ -1,8 +1,6 @@
1
- import { eos } from '@zenfs/streams/end-of-stream';
2
- import { Readable } from '@zenfs/streams/readable';
3
- import { Writable } from '@zenfs/streams/writable';
4
1
  import { Errno, Exception, UV } from 'kerium';
5
2
  import { warn } from 'kerium/log';
3
+ import { finished, Readable, Writable } from 'readable-stream';
6
4
  /**
7
5
  * A ReadStream implementation that wraps an underlying global ReadableStream.
8
6
  */
@@ -18,11 +16,7 @@ export class ReadStream extends Readable {
18
16
  .then(handle => {
19
17
  this._path = handle['vfs'].path;
20
18
  // `end` is inclusive for streams, but the internal range is not
21
- const internal = handle.readableWebStream({
22
- start: opts.start,
23
- end: opts.end === undefined ? undefined : opts.end + 1,
24
- chunkSize: opts.highWaterMark,
25
- });
19
+ const internal = handle.readableWebStream({ start: opts.start, end: opts.end === undefined ? undefined : opts.end + 1 });
26
20
  this.reader = internal.getReader();
27
21
  this.pending = false;
28
22
  })
@@ -52,7 +46,7 @@ export class ReadStream extends Readable {
52
46
  * Closing before the stream has finished reports `ERR_STREAM_PREMATURE_CLOSE`.
53
47
  */
54
48
  close(callback = () => null) {
55
- eos(this, error => callback(error ?? null));
49
+ finished(this, error => callback(error ?? null));
56
50
  this.destroy();
57
51
  }
58
52
  get path() {
package/dist/vfs/file.js CHANGED
@@ -93,8 +93,8 @@ import { _chown, InodeFlags, isBlockDevice, isCharacterDevice } from '../interna
93
93
  import '../polyfills.js';
94
94
  import { validateFD } from '../utils.js';
95
95
  import { cacheOf } from './vcache.js';
96
- /** Default bytes per chunk for streamed reads, matching Node's default stream `highWaterMark` */
97
- const streamChunkSize = 0x10000;
96
+ /** The chunk size used when streaming reads and writes */
97
+ const streamChunkSize = 0x1000;
98
98
  /**
99
99
  * @internal
100
100
  */
@@ -608,25 +608,19 @@ let Handle = (() => {
608
608
  if (this.closed)
609
609
  throw UV('EBADF', 'streamRead', this.path);
610
610
  const { vnode } = this;
611
- const { chunkSize = streamChunkSize } = options;
612
- let offset = options.start ?? 0;
613
- let end = options.end;
614
611
  return new ReadableStream({
615
- async pull(controller) {
612
+ async start(controller) {
616
613
  const env_15 = { stack: [], error: void 0, hasError: false };
617
614
  try {
618
615
  const _ = __addDisposableResource(env_15, await vnode.lock('ro'), false);
619
- end ??= vnode.inode.size;
620
- if (offset >= end) {
621
- controller.close();
622
- return;
616
+ const { start = 0, end = vnode.inode.size } = options;
617
+ for (let offset = start; offset < end; offset += streamChunkSize) {
618
+ const bytesRead = offset + streamChunkSize > end ? end - offset : streamChunkSize;
619
+ const buffer = new Uint8Array(bytesRead);
620
+ await vnode.read(buffer, offset, offset + bytesRead).catch(controller.error.bind(controller));
621
+ controller.enqueue(buffer);
623
622
  }
624
- const buffer = new Uint8Array(Math.min(chunkSize, end - offset));
625
- await vnode.read(buffer, offset, offset + buffer.byteLength);
626
- offset += buffer.byteLength;
627
- controller.enqueue(buffer);
628
- if (offset >= end)
629
- controller.close();
623
+ controller.close();
630
624
  }
631
625
  catch (e_15) {
632
626
  env_15.error = e_15;
package/dist/vfs/vnode.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  import { RwLockable } from 'kerium/locks';
3
- import { addRange, Resource } from 'utilium/cache';
3
+ import { Resource } from 'utilium/cache';
4
4
  import { InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js';
5
5
  /**
6
6
  * A VFS-level node, roughly equivalent to Linux's in-memory `struct inode`.
@@ -57,7 +57,17 @@ export class VNode extends RwLockable {
57
57
  }
58
58
  /** Add a range to `dirtyRanges`, merging overlapping and adjacent ranges */
59
59
  markDirty(start, end) {
60
- addRange(this.dirtyRanges, start, end);
60
+ this.dirtyRanges.push({ start, end });
61
+ this.dirtyRanges.sort((a, b) => a.start - b.start);
62
+ const merged = [];
63
+ for (const range of this.dirtyRanges) {
64
+ const last = merged.at(-1);
65
+ if (last && range.start <= last.end)
66
+ last.end = Math.max(last.end, range.end);
67
+ else
68
+ merged.push(range);
69
+ }
70
+ this.dirtyRanges = merged;
61
71
  }
62
72
  /**
63
73
  * Copy `[start, end)` from the cache into `buffer`.
@@ -65,10 +75,12 @@ export class VNode extends RwLockable {
65
75
  */
66
76
  copyFromCache(buffer, start, end) {
67
77
  let pos = start;
68
- for (const region of this.data.regionsIn(start, end)) {
78
+ for (const region of this.data.regions) {
69
79
  const regionEnd = region.offset + region.data.byteLength;
70
80
  if (regionEnd <= pos)
71
81
  continue;
82
+ if (region.offset >= end)
83
+ break;
72
84
  if (region.offset > pos) {
73
85
  buffer.fill(0, pos - start, region.offset - start);
74
86
  pos = region.offset;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenfs/core",
3
- "version": "2.7.3-newstreams.0",
3
+ "version": "2.7.3",
4
4
  "description": "A filesystem, anywhere",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -75,12 +75,12 @@
75
75
  },
76
76
  "dependencies": {
77
77
  "@types/node": "^26.0.0",
78
- "@zenfs/streams": "^0.0.1",
79
78
  "buffer": "^6.0.3",
80
79
  "eventemitter3": "^5.0.1",
81
80
  "kerium": "^1.5.0",
82
81
  "memium": "^1.0.0",
83
- "utilium": "^3.7.0"
82
+ "readable-stream": "^4.5.2",
83
+ "utilium": "^3.0.0"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@eslint/js": "^10.0.1",
@@ -141,64 +141,4 @@ suite('Streams', config('streams'), () => {
141
141
  // Closing invalidates the descriptor, so it fails range validation
142
142
  assert.throws(() => fileHandle.createWriteStream(), { code: 'ERR_OUT_OF_RANGE' });
143
143
  });
144
-
145
- // A file large enough to span several chunks, so the read path has to stitch
146
- // them back together and the vnode cache has to grow to hold them.
147
- const large = Buffer.alloc(200 * 1024);
148
- for (let i = 0; i < large.length; i++) large[i] = (i * 31 + 7) & 0xff;
149
-
150
- test('createReadStream reads a multi-chunk file byte for byte', async () => {
151
- await fs.promises.writeFile('large.bin', large);
152
-
153
- const chunks: Uint8Array[] = [];
154
- for await (const chunk of fs.createReadStream('large.bin')) chunks.push(chunk as Uint8Array);
155
-
156
- assert.deepEqual(Buffer.concat(chunks), large);
157
- });
158
-
159
- test('createReadStream honors highWaterMark as the chunk size', async () => {
160
- await fs.promises.writeFile('large.bin', large);
161
-
162
- const highWaterMark = 64 * 1024;
163
- const sizes: number[] = [];
164
- for await (const chunk of fs.createReadStream('large.bin', { highWaterMark })) sizes.push((chunk as Uint8Array).byteLength);
165
-
166
- // Every chunk but the last is a full highWaterMark, and they sum to the file
167
- assert.deepEqual(sizes.slice(0, -1), Array(sizes.length - 1).fill(highWaterMark));
168
- assert.equal(
169
- sizes.reduce((a, b) => a + b, 0),
170
- large.length
171
- );
172
- });
173
-
174
- test('createReadStream with start and end spanning chunks', async () => {
175
- await fs.promises.writeFile('large.bin', large);
176
-
177
- // `end` is inclusive, and this range deliberately starts and stops mid-chunk
178
- const start = 1000;
179
- const end = 150_000;
180
- const chunks: Uint8Array[] = [];
181
- for await (const chunk of fs.createReadStream('large.bin', { start, end, highWaterMark: 8192 })) chunks.push(chunk as Uint8Array);
182
-
183
- assert.deepEqual(Buffer.concat(chunks), large.subarray(start, end + 1));
184
- });
185
-
186
- test('a read stream does not read past the end of the file', async () => {
187
- await fs.promises.writeFile('small.bin', large.subarray(0, 10));
188
-
189
- const chunks: Uint8Array[] = [];
190
- for await (const chunk of fs.createReadStream('small.bin', { highWaterMark: 64 * 1024 })) chunks.push(chunk as Uint8Array);
191
-
192
- assert.equal(chunks.length, 1);
193
- assert.deepEqual(chunks[0], large.subarray(0, 10));
194
- });
195
-
196
- test('a read stream over an empty file ends without a chunk', async () => {
197
- await fs.promises.writeFile('empty.bin', '');
198
-
199
- const chunks: unknown[] = [];
200
- for await (const chunk of fs.createReadStream('empty.bin')) chunks.push(chunk);
201
-
202
- assert.deepEqual(chunks, []);
203
- });
204
144
  });
package/tests/setup.ts CHANGED
@@ -8,7 +8,7 @@ export const data = join(import.meta.dirname, 'data');
8
8
 
9
9
  export const tmp = join(import.meta.dirname, 'tmp');
10
10
 
11
- if (!existsSync(tmp)) mkdirSync(tmp, { recursive: true });
11
+ if (!existsSync(tmp)) mkdirSync(tmp);
12
12
 
13
13
  export async function copyAsync(_path: string, fs: NodeFS = _fs): Promise<void> {
14
14
  const path = relative(data, _path) || '/';
@@ -0,0 +1 @@
1
+ This directory contains declaration files. Due to numerous TypeScript shenanigans, placing them into `src` is a really bad idea. Instead, we put them here.
@@ -0,0 +1,18 @@
1
+ /*
2
+ @types/readable-stream has *many* problems:
3
+ https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71923
4
+ https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71307
5
+ https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71083
6
+ https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/66049
7
+ ... and many more
8
+
9
+ This allows us to bypass those problems.
10
+
11
+ Warning: Do not install @types/readable-stream alongside this package!
12
+ */
13
+
14
+ /// <reference types="node" preserve="true" />
15
+ declare module 'readable-stream' {
16
+ import { finished, Readable, Writable } from 'node:stream';
17
+ export { finished, Readable, Writable };
18
+ }