@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.
- package/dist/internal/filesystem.d.ts +0 -1
- package/dist/internal/filesystem.js +10 -16
- package/dist/node/streams.d.ts +2 -2
- package/dist/node/streams.js +3 -9
- package/dist/vfs/file.js +10 -16
- package/dist/vfs/vnode.js +15 -3
- package/package.json +3 -3
- package/tests/fs/streams.test.ts +0 -60
- package/tests/setup.ts +1 -1
- package/types/README.md +1 -0
- package/types/readable-stream.d.ts +18 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/node/streams.d.ts
CHANGED
|
@@ -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 '
|
|
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;
|
package/dist/node/streams.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
97
|
-
const streamChunkSize =
|
|
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
|
|
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
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
"
|
|
82
|
+
"readable-stream": "^4.5.2",
|
|
83
|
+
"utilium": "^3.0.0"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@eslint/js": "^10.0.1",
|
package/tests/fs/streams.test.ts
CHANGED
|
@@ -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
|
|
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) || '/';
|
package/types/README.md
ADDED
|
@@ -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
|
+
}
|