@bablr/fs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/lib/index.js +161 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Conrad Buck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @bablr/fs
|
|
2
|
+
|
|
3
|
+
This package offers fast, responsive, efficient access to the filesystem. Node's APIs are used under the hood, the created data streams are exposed as stream iterators.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { readFile, readDir } from '@bablr/fs';
|
|
9
|
+
|
|
10
|
+
let fileIterable = readFile(import.meta.url, 'utf8');
|
|
11
|
+
let directoryIterable = readDir('./');
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Why stream iterables?
|
|
15
|
+
|
|
16
|
+
Stream iterables offer a trifecta of desirable features:
|
|
17
|
+
|
|
18
|
+
- Abstraction: they doesn’t require the provider to copy the data or expose its internal structure (benefit over web streams)
|
|
19
|
+
- Throughput: stream iterators have high throughput and efficiency when used on streams of many small items because only some steps must wait (benefit over flat async iterators)
|
|
20
|
+
- Responsiveness: returning the first data from a 20GB file doesn’t cause a huge delay, it takes just the same amount of time as opening a one-chunk file (benefit over fs.openFileSync style)
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { open, opendir } from 'node:fs/promises';
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { getStreamIterator, StreamIterable } from '@bablr/stream-iterator';
|
|
5
|
+
import { getDefaultHighWaterMark } from 'node:stream';
|
|
6
|
+
|
|
7
|
+
let autoAllocateChunkSize = getDefaultHighWaterMark();
|
|
8
|
+
|
|
9
|
+
let inRange = (v, min, max) => v > min && v < max;
|
|
10
|
+
|
|
11
|
+
class StreamSource {
|
|
12
|
+
constructor(path) {
|
|
13
|
+
this.path = path;
|
|
14
|
+
this.type = 'bytes';
|
|
15
|
+
this.autoAllocateChunkSize = autoAllocateChunkSize;
|
|
16
|
+
this.controller = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async start(controller) {
|
|
20
|
+
this.file = await open(this.path);
|
|
21
|
+
this.controller = controller;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async pull(controller) {
|
|
25
|
+
const view = controller.byobRequest?.view;
|
|
26
|
+
const { bytesRead } = await this.file.read({
|
|
27
|
+
buffer: view,
|
|
28
|
+
offset: view.byteOffset,
|
|
29
|
+
length: view.byteLength,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (bytesRead === 0) {
|
|
33
|
+
await this.file.close();
|
|
34
|
+
this.controller.close();
|
|
35
|
+
}
|
|
36
|
+
controller.byobRequest.respond(bytesRead);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function* __decodeUTF8(bytes) {
|
|
41
|
+
let codePoints = codePointsFor(bytes);
|
|
42
|
+
let iter = getStreamIterator(codePoints);
|
|
43
|
+
let step = iter.next();
|
|
44
|
+
|
|
45
|
+
for (;;) {
|
|
46
|
+
if (step instanceof Promise) {
|
|
47
|
+
step = yield step;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (step.done) break;
|
|
51
|
+
|
|
52
|
+
let cp = step.value;
|
|
53
|
+
|
|
54
|
+
if (cp <= 0xffff) {
|
|
55
|
+
yield String.fromCharCode(cp);
|
|
56
|
+
} else {
|
|
57
|
+
cp -= 0x10000;
|
|
58
|
+
yield String.fromCharCode((cp >> 10) + 0xd800, (cp & 0x3ff) + 0xdc00);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
step = iter.next();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const decodeUTF8 = (bytes) => {
|
|
66
|
+
return new StreamIterable(__decodeUTF8(bytes));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// adapted from https://github.com/inexorabletash/text-encoding/blob/master/lib/encoding.js
|
|
70
|
+
function* __codePointsFor(bytes) {
|
|
71
|
+
let iter = getStreamIterator(bytes);
|
|
72
|
+
let step = iter.next();
|
|
73
|
+
let partialValue = null;
|
|
74
|
+
|
|
75
|
+
for (;;) {
|
|
76
|
+
if (step instanceof Promise) {
|
|
77
|
+
step = yield step;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (step.done) break;
|
|
81
|
+
|
|
82
|
+
let { value } = step;
|
|
83
|
+
|
|
84
|
+
if (partialValue) {
|
|
85
|
+
if (inRange(value, 0xdc00, 0xdfff)) {
|
|
86
|
+
yield (0x10000 + (partialValue & (0x3ff << 10)) + value) & 0x3ff;
|
|
87
|
+
} else {
|
|
88
|
+
yield 0xfffd;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
partialValue = null;
|
|
92
|
+
} else if (!inRange(value, 0xd800, 0xdfff)) {
|
|
93
|
+
yield value;
|
|
94
|
+
} else if (inRange(value, 0xdc00, 0xdfff)) {
|
|
95
|
+
yield 0xfffd;
|
|
96
|
+
} else if (inRange(value, 0xd800, 0xdbff)) {
|
|
97
|
+
partialValue = value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
step = iter.next();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (partialValue) {
|
|
104
|
+
yield 0xfffd;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const codePointsFor = (bytes) => {
|
|
109
|
+
return new StreamIterable(__codePointsFor(bytes));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function* __readFile(path) {
|
|
113
|
+
let stream = new ReadableStream(new StreamSource(path));
|
|
114
|
+
|
|
115
|
+
const reader = stream.getReader({ mode: 'byob' });
|
|
116
|
+
|
|
117
|
+
let result;
|
|
118
|
+
do {
|
|
119
|
+
result = yield reader.read(Buffer.alloc(autoAllocateChunkSize));
|
|
120
|
+
if (result.value !== undefined) yield* result.value;
|
|
121
|
+
} while (!result.done);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const readFile = (path, options) => {
|
|
125
|
+
let encoding = typeof options === 'string' ? options : options.encoding;
|
|
126
|
+
|
|
127
|
+
if (encoding && !/utf-?8/i.test(encoding)) throw new Error('unsupported encoding');
|
|
128
|
+
|
|
129
|
+
let bytes = new StreamIterable(__readFile(path, options));
|
|
130
|
+
|
|
131
|
+
return encoding ? decodeUTF8(bytes) : bytes;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function* __readDir(path) {
|
|
135
|
+
let dir = yield opendir(path);
|
|
136
|
+
|
|
137
|
+
let break_ = false;
|
|
138
|
+
let chunk = [];
|
|
139
|
+
|
|
140
|
+
while (!break_) {
|
|
141
|
+
for (let i = 0; i < 64; i++) {
|
|
142
|
+
chunk.push(dir.read());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let values = yield Promise.all(chunk);
|
|
146
|
+
|
|
147
|
+
for (let value of values) {
|
|
148
|
+
if (value === null) {
|
|
149
|
+
break_ = true;
|
|
150
|
+
} else {
|
|
151
|
+
yield value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
chunk.length = 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const readDir = (path) => {
|
|
160
|
+
return new StreamIterable(__readDir(path));
|
|
161
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bablr/fs",
|
|
3
|
+
"description": "A Node.js filesystem access library using stream iterators",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "Conrad Buck<conartist6@gmail.com>",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@bablr/stream-iterator": "1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@bablr/eslint-config-base": "github:bablr-lang/eslint-config-base#d834ccc52795d6c3b96ecc6c419960fceed221a6",
|
|
19
|
+
"enhanced-resolve": "^5.12.0",
|
|
20
|
+
"eslint": "^8.32.0",
|
|
21
|
+
"eslint-import-resolver-enhanced-resolve": "^1.0.5",
|
|
22
|
+
"eslint-plugin-import": "^2.27.5",
|
|
23
|
+
"iter-tools-es": "^7.3.1",
|
|
24
|
+
"prettier": "^2.6.2"
|
|
25
|
+
},
|
|
26
|
+
"repository": "github:bablr-lang/bablr-fs-node",
|
|
27
|
+
"homepage": "https://github.com/bablr-lang/bablr-fs-node",
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|