@botejs/core 0.4.0 → 0.6.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/README.md +44 -115
- package/dist/args.d.ts +16 -5
- package/dist/args.js +6 -3
- package/dist/cursor.d.ts +97 -0
- package/dist/cursor.js +124 -0
- package/dist/error.d.ts +47 -0
- package/dist/error.js +113 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +6 -3
- package/dist/open.d.ts +32 -71
- package/dist/open.js +27 -190
- package/dist/path.d.ts +3 -1
- package/dist/path.js +28 -4
- package/dist/source/base.d.ts +70 -0
- package/dist/source/base.js +1 -0
- package/dist/source/forward.d.ts +49 -0
- package/dist/source/forward.js +219 -0
- package/dist/source/seekable.d.ts +8 -0
- package/dist/{sources.js → source/seekable.js} +24 -10
- package/dist/stream.d.ts +15 -0
- package/dist/stream.js +166 -0
- package/dist/validate.d.ts +2 -16
- package/dist/validate.js +5 -53
- package/package.json +3 -2
- package/dist/sources.d.ts +0 -39
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { ForwardReplayError } from "../error.js";
|
|
3
|
+
/** Default chunk size, in bytes, for forward streams: a large pull keeps a streamed scan moving. */
|
|
4
|
+
const DEFAULT_STREAM_CHUNK_BYTES = 256 * 1024;
|
|
5
|
+
const EMPTY = new Uint8Array(0);
|
|
6
|
+
/**
|
|
7
|
+
* A forward-only source backed by a re-openable readable stream. `produce` is
|
|
8
|
+
* called to acquire each pass: once up front, and again on a rewind when
|
|
9
|
+
* `rewind: 'replay'` is set. A plain `Readable` instance cannot be re-streamed,
|
|
10
|
+
* so pass a thunk (`() => createReadStream(path)`), not a live stream.
|
|
11
|
+
*
|
|
12
|
+
* Because every cursor operation is an independent scan from the start, a single
|
|
13
|
+
* forward pass serves exactly one query; a second query (or `hop` then `get`)
|
|
14
|
+
* rewinds. By default that throws {@link ForwardReplayError}; opt into
|
|
15
|
+
* `rewind: 'replay'` (idempotent producer) or `rewind: 'buffer'` (in-memory
|
|
16
|
+
* snapshot) for multi-query access.
|
|
17
|
+
*/
|
|
18
|
+
export function fromReadable(produce, options) {
|
|
19
|
+
const chunkBytes = options?.chunkBytes ?? DEFAULT_STREAM_CHUNK_BYTES;
|
|
20
|
+
const size = options?.size;
|
|
21
|
+
const decode = options?.decode;
|
|
22
|
+
const rewind = options?.rewind ?? 'forbid';
|
|
23
|
+
return {
|
|
24
|
+
seekable: false,
|
|
25
|
+
open: () => makeForwardReader(produce, { chunkBytes, size, decode, rewind }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A forward-only source over an HTTP request (GET is default), streamed in a single pass. A
|
|
30
|
+
* convenience wrapper around {@link fromReadable} whose producer re-fetches `url`
|
|
31
|
+
* (reusing `init`, so auth headers, credentials, and an `AbortSignal` survive
|
|
32
|
+
* each acquisition). For repeated or random access over HTTP, prefer the seekable
|
|
33
|
+
* {@link fromHttpRange}.
|
|
34
|
+
*/
|
|
35
|
+
export function fromHttpRequest(url, options) {
|
|
36
|
+
const { init, ...readable } = options ?? {};
|
|
37
|
+
const produce = async () => {
|
|
38
|
+
const res = await fetch(url, { ...init });
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`${url} failed: ${res.status} ${res.statusText}`);
|
|
41
|
+
}
|
|
42
|
+
if (!res.body) {
|
|
43
|
+
throw new Error(`${url} returned no body`);
|
|
44
|
+
}
|
|
45
|
+
return res.body;
|
|
46
|
+
};
|
|
47
|
+
return fromReadable(produce, readable);
|
|
48
|
+
}
|
|
49
|
+
async function makeForwardReader(produce, config) {
|
|
50
|
+
const { chunkBytes, size, decode, rewind } = config;
|
|
51
|
+
const replay = rewind === 'replay';
|
|
52
|
+
const buffer = rewind === 'buffer';
|
|
53
|
+
let reader = null;
|
|
54
|
+
let pos = 0; // bytes consumed from the active pass (served + skipped)
|
|
55
|
+
let leftover = null; // tail of the last pulled chunk, not yet served
|
|
56
|
+
let done = false;
|
|
57
|
+
let snapshot = null;
|
|
58
|
+
let chain = Promise.resolve();
|
|
59
|
+
const acquire = async () => {
|
|
60
|
+
let web = toWebStream(await produce());
|
|
61
|
+
if (decode) {
|
|
62
|
+
web = decode(web);
|
|
63
|
+
}
|
|
64
|
+
reader = web.getReader();
|
|
65
|
+
pos = 0;
|
|
66
|
+
leftover = null;
|
|
67
|
+
done = false;
|
|
68
|
+
};
|
|
69
|
+
const release = async () => {
|
|
70
|
+
const active = reader;
|
|
71
|
+
reader = null;
|
|
72
|
+
if (active) {
|
|
73
|
+
try {
|
|
74
|
+
await active.cancel();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// cancelling an already-errored/closed stream is not actionable
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
// Next run of bytes available from the active pass, or null once it is drained.
|
|
82
|
+
const pull = async () => {
|
|
83
|
+
if (leftover) {
|
|
84
|
+
const chunk = leftover;
|
|
85
|
+
leftover = null;
|
|
86
|
+
return chunk;
|
|
87
|
+
}
|
|
88
|
+
if (done || !reader) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const { value, done: streamDone } = await reader.read();
|
|
92
|
+
if (streamDone) {
|
|
93
|
+
done = true;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
98
|
+
const readForward = async (offset, length) => {
|
|
99
|
+
if (offset < pos) {
|
|
100
|
+
if (!replay) {
|
|
101
|
+
throw new ForwardReplayError(offset, pos);
|
|
102
|
+
}
|
|
103
|
+
await release();
|
|
104
|
+
await acquire();
|
|
105
|
+
}
|
|
106
|
+
if (!reader && !done) {
|
|
107
|
+
await acquire();
|
|
108
|
+
}
|
|
109
|
+
while (pos < offset) {
|
|
110
|
+
const chunk = await pull();
|
|
111
|
+
if (chunk === null) {
|
|
112
|
+
break; // stream ended before the requested offset; the read returns eof
|
|
113
|
+
}
|
|
114
|
+
const skip = offset - pos;
|
|
115
|
+
if (chunk.byteLength <= skip) {
|
|
116
|
+
pos += chunk.byteLength;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
leftover = chunk.subarray(skip);
|
|
120
|
+
pos += skip;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const parts = [];
|
|
124
|
+
let got = 0;
|
|
125
|
+
while (got < length) {
|
|
126
|
+
const chunk = await pull();
|
|
127
|
+
if (chunk === null) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
if (chunk.byteLength === 0) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const take = Math.min(chunk.byteLength, length - got);
|
|
134
|
+
parts.push(take === chunk.byteLength ? chunk : chunk.subarray(0, take));
|
|
135
|
+
if (take < chunk.byteLength) {
|
|
136
|
+
leftover = chunk.subarray(take);
|
|
137
|
+
}
|
|
138
|
+
pos += take;
|
|
139
|
+
got += take;
|
|
140
|
+
}
|
|
141
|
+
return { data: concat(parts, got), eof: done && leftover === null };
|
|
142
|
+
};
|
|
143
|
+
const readSnapshot = (offset, length) => {
|
|
144
|
+
const data = snapshot;
|
|
145
|
+
if (offset >= data.byteLength) {
|
|
146
|
+
return { data: EMPTY, eof: true };
|
|
147
|
+
}
|
|
148
|
+
const end = Math.min(offset + length, data.byteLength);
|
|
149
|
+
return { data: data.subarray(offset, end), eof: end >= data.byteLength };
|
|
150
|
+
};
|
|
151
|
+
// A forward pass has shared position state, so overlapping scans (e.g. two
|
|
152
|
+
// un-awaited queries) must not interleave their pulls. Serializing reads keeps
|
|
153
|
+
// bookkeeping intact and turns concurrent misuse into a ForwardReplayError.
|
|
154
|
+
const read = (offset, length) => {
|
|
155
|
+
const result = chain.then(async () => {
|
|
156
|
+
if (buffer) {
|
|
157
|
+
if (!snapshot) {
|
|
158
|
+
snapshot = await drainAll(produce, decode);
|
|
159
|
+
}
|
|
160
|
+
return readSnapshot(offset, length);
|
|
161
|
+
}
|
|
162
|
+
return readForward(offset, length);
|
|
163
|
+
});
|
|
164
|
+
chain = result.catch(() => { });
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
if (!buffer) {
|
|
168
|
+
await acquire();
|
|
169
|
+
}
|
|
170
|
+
return { seekable: false, size, chunkBytes, read, close: release };
|
|
171
|
+
}
|
|
172
|
+
async function drainAll(produce, decode) {
|
|
173
|
+
let web = toWebStream(await produce());
|
|
174
|
+
if (decode) {
|
|
175
|
+
web = decode(web);
|
|
176
|
+
}
|
|
177
|
+
const reader = web.getReader();
|
|
178
|
+
const parts = [];
|
|
179
|
+
let total = 0;
|
|
180
|
+
try {
|
|
181
|
+
for (;;) {
|
|
182
|
+
const { value, done } = await reader.read();
|
|
183
|
+
if (done) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (value && value.byteLength > 0) {
|
|
187
|
+
parts.push(value);
|
|
188
|
+
total += value.byteLength;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
try {
|
|
194
|
+
await reader.cancel();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// already drained/errored
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return concat(parts, total);
|
|
201
|
+
}
|
|
202
|
+
function toWebStream(stream) {
|
|
203
|
+
if (typeof stream.getReader === 'function') {
|
|
204
|
+
return stream;
|
|
205
|
+
}
|
|
206
|
+
return Readable.toWeb(stream);
|
|
207
|
+
}
|
|
208
|
+
function concat(parts, total) {
|
|
209
|
+
if (parts.length === 1 && parts[0].byteLength === total) {
|
|
210
|
+
return parts[0];
|
|
211
|
+
}
|
|
212
|
+
const out = new Uint8Array(total);
|
|
213
|
+
let offset = 0;
|
|
214
|
+
for (const part of parts) {
|
|
215
|
+
out.set(part, offset);
|
|
216
|
+
offset += part.byteLength;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FactoryOptions, SeekableSource } from './base.ts';
|
|
2
|
+
export interface HttpRangeOptions extends FactoryOptions {
|
|
3
|
+
/** Merged into every request (headers, credentials, signal, etc.). */
|
|
4
|
+
init?: RequestInit;
|
|
5
|
+
}
|
|
6
|
+
export declare function fromBuffer(buf: Uint8Array | ArrayBuffer, options?: FactoryOptions): SeekableSource;
|
|
7
|
+
export declare function fromFile(path: string, options?: FactoryOptions): SeekableSource;
|
|
8
|
+
export declare function fromHttpRange(url: string, options?: HttpRangeOptions): SeekableSource;
|
|
@@ -9,37 +9,47 @@ export function fromBuffer(buf, options) {
|
|
|
9
9
|
const view = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
10
10
|
const chunkBytes = options?.chunkBytes ?? DEFAULT_BUFFER_CHUNK_BYTES;
|
|
11
11
|
return {
|
|
12
|
+
seekable: true,
|
|
12
13
|
open: () => Promise.resolve({
|
|
14
|
+
seekable: true,
|
|
13
15
|
size: view.byteLength,
|
|
14
16
|
chunkBytes,
|
|
15
|
-
read: (offset, length) =>
|
|
17
|
+
read: (offset, length) => {
|
|
18
|
+
const data = view.subarray(offset, Math.min(offset + length, view.byteLength));
|
|
19
|
+
return Promise.resolve({ data, eof: offset + data.byteLength >= view.byteLength });
|
|
20
|
+
},
|
|
16
21
|
}),
|
|
17
22
|
};
|
|
18
23
|
}
|
|
19
24
|
export function fromFile(path, options) {
|
|
20
25
|
const chunkBytes = options?.chunkBytes ?? DEFAULT_FILE_CHUNK_BYTES;
|
|
21
26
|
return {
|
|
27
|
+
seekable: true,
|
|
22
28
|
open: async () => {
|
|
23
29
|
const handle = await fsOpen(path, 'r');
|
|
24
30
|
const stat = await handle.stat();
|
|
31
|
+
const size = stat.size;
|
|
25
32
|
let closed = false;
|
|
26
33
|
return {
|
|
27
|
-
|
|
34
|
+
seekable: true,
|
|
35
|
+
size,
|
|
28
36
|
chunkBytes,
|
|
29
37
|
read: async (offset, length) => {
|
|
30
38
|
const buf = Buffer.allocUnsafe(length);
|
|
31
39
|
let filled = 0;
|
|
32
40
|
while (filled < length) {
|
|
33
41
|
const { bytesRead } = await handle.read(buf, filled, length - filled, offset + filled);
|
|
34
|
-
if (bytesRead === 0)
|
|
42
|
+
if (bytesRead === 0) {
|
|
35
43
|
break;
|
|
44
|
+
}
|
|
36
45
|
filled += bytesRead;
|
|
37
46
|
}
|
|
38
|
-
return buf.subarray(0, filled);
|
|
47
|
+
return { data: buf.subarray(0, filled), eof: offset + filled >= size };
|
|
39
48
|
},
|
|
40
49
|
close: async () => {
|
|
41
|
-
if (closed)
|
|
50
|
+
if (closed) {
|
|
42
51
|
return;
|
|
52
|
+
}
|
|
43
53
|
closed = true;
|
|
44
54
|
await handle.close();
|
|
45
55
|
},
|
|
@@ -51,6 +61,7 @@ export function fromHttpRange(url, options) {
|
|
|
51
61
|
const init = options?.init;
|
|
52
62
|
const chunkBytes = options?.chunkBytes ?? DEFAULT_URL_CHUNK_BYTES;
|
|
53
63
|
return {
|
|
64
|
+
seekable: true,
|
|
54
65
|
open: async () => {
|
|
55
66
|
const controller = new AbortController();
|
|
56
67
|
const userSignal = init?.signal;
|
|
@@ -79,6 +90,7 @@ export function fromHttpRange(url, options) {
|
|
|
79
90
|
}
|
|
80
91
|
let closed = false;
|
|
81
92
|
return {
|
|
93
|
+
seekable: true,
|
|
82
94
|
size,
|
|
83
95
|
chunkBytes,
|
|
84
96
|
read: async (offset, length) => {
|
|
@@ -87,21 +99,23 @@ export function fromHttpRange(url, options) {
|
|
|
87
99
|
const headers = new Headers(init?.headers);
|
|
88
100
|
headers.set('Range', `bytes=${offset}-${end}`);
|
|
89
101
|
headers.set('Accept-Encoding', 'identity');
|
|
90
|
-
const res = await fetch(url, { ...init, headers,
|
|
102
|
+
const res = await fetch(url, { ...init, headers, signal: controller.signal });
|
|
91
103
|
if (res.status === 206) {
|
|
92
|
-
|
|
104
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
105
|
+
return { data, eof: offset + data.byteLength >= size };
|
|
93
106
|
}
|
|
94
107
|
// A 200 means the server ignored our Range request and returned the full
|
|
95
108
|
// body. We throw here since the point of using ranges is to not have to
|
|
96
109
|
// buffer the whole thing in memory.
|
|
97
110
|
if (res.status === 200) {
|
|
98
|
-
throw new Error(`Range
|
|
111
|
+
throw new Error(`Range ${url} (bytes=${offset}-${end}) ignored Range and returned 200.`);
|
|
99
112
|
}
|
|
100
|
-
throw new Error(`Range
|
|
113
|
+
throw new Error(`Range ${url} (bytes=${offset}-${end}) failed: ${res.status}`);
|
|
101
114
|
},
|
|
102
115
|
close: async () => {
|
|
103
|
-
if (closed)
|
|
116
|
+
if (closed) {
|
|
104
117
|
return;
|
|
118
|
+
}
|
|
105
119
|
closed = true;
|
|
106
120
|
controller.abort();
|
|
107
121
|
},
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IterStream<T> extends AsyncIterable<T> {
|
|
2
|
+
raw(): AsyncIterable<T[]>;
|
|
3
|
+
map<U>(fn: (item: T, index: number) => U | Promise<U>): IterStream<U>;
|
|
4
|
+
filter<U extends T>(fn: (item: T, index: number) => item is U): IterStream<U>;
|
|
5
|
+
filter(fn: (item: T, index: number) => boolean | Promise<boolean>): IterStream<T>;
|
|
6
|
+
take(limit: number): IterStream<T>;
|
|
7
|
+
drop(limit: number): IterStream<T>;
|
|
8
|
+
toArray(): Promise<T[]>;
|
|
9
|
+
forEach(fn: (item: T, index: number) => void | Promise<void>): Promise<void>;
|
|
10
|
+
reduce<A>(fn: (acc: A, item: T, index: number) => A | Promise<A>, init: A): Promise<A>;
|
|
11
|
+
find(fn: (item: T, index: number) => boolean | Promise<boolean>): Promise<T | undefined>;
|
|
12
|
+
some(fn: (item: T, index: number) => boolean | Promise<boolean>): Promise<boolean>;
|
|
13
|
+
every(fn: (item: T, index: number) => boolean | Promise<boolean>): Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
export declare function makeStream<T>(batches: () => AsyncIterable<T[]>, batchSize: number, regroup?: boolean): IterStream<T>;
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export function makeStream(batches, batchSize, regroup = false) {
|
|
2
|
+
const derive = (next) => makeStream(next, batchSize, true);
|
|
3
|
+
const stream = {
|
|
4
|
+
[Symbol.asyncIterator]() {
|
|
5
|
+
return flatten(batches())[Symbol.asyncIterator]();
|
|
6
|
+
},
|
|
7
|
+
raw() {
|
|
8
|
+
return regroup ? regroupBatches(batches(), batchSize) : batches();
|
|
9
|
+
},
|
|
10
|
+
map(fn) {
|
|
11
|
+
return derive(() => mapBatches(batches(), fn));
|
|
12
|
+
},
|
|
13
|
+
filter(fn) {
|
|
14
|
+
return derive(() => filterBatches(batches(), fn));
|
|
15
|
+
},
|
|
16
|
+
take(limit) {
|
|
17
|
+
return derive(() => takeBatches(batches(), limit));
|
|
18
|
+
},
|
|
19
|
+
drop(limit) {
|
|
20
|
+
return derive(() => dropBatches(batches(), limit));
|
|
21
|
+
},
|
|
22
|
+
async toArray() {
|
|
23
|
+
const out = [];
|
|
24
|
+
for await (const batch of batches()) {
|
|
25
|
+
for (let i = 0; i < batch.length; i++) {
|
|
26
|
+
out.push(batch[i]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
},
|
|
31
|
+
async forEach(fn) {
|
|
32
|
+
let index = 0;
|
|
33
|
+
for await (const batch of batches()) {
|
|
34
|
+
for (let i = 0; i < batch.length; i++) {
|
|
35
|
+
await fn(batch[i], index++);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async reduce(fn, init) {
|
|
40
|
+
let acc = init;
|
|
41
|
+
let index = 0;
|
|
42
|
+
for await (const batch of batches()) {
|
|
43
|
+
for (let i = 0; i < batch.length; i++) {
|
|
44
|
+
acc = await fn(acc, batch[i], index++);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return acc;
|
|
48
|
+
},
|
|
49
|
+
async find(fn) {
|
|
50
|
+
let index = 0;
|
|
51
|
+
for await (const batch of batches()) {
|
|
52
|
+
for (let i = 0; i < batch.length; i++) {
|
|
53
|
+
if (await fn(batch[i], index++)) {
|
|
54
|
+
return batch[i];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
},
|
|
60
|
+
async some(fn) {
|
|
61
|
+
let index = 0;
|
|
62
|
+
for await (const batch of batches()) {
|
|
63
|
+
for (let i = 0; i < batch.length; i++) {
|
|
64
|
+
if (await fn(batch[i], index++)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
},
|
|
71
|
+
async every(fn) {
|
|
72
|
+
let index = 0;
|
|
73
|
+
for await (const batch of batches()) {
|
|
74
|
+
for (let i = 0; i < batch.length; i++) {
|
|
75
|
+
if (!(await fn(batch[i], index++))) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return stream;
|
|
84
|
+
}
|
|
85
|
+
async function* flatten(batches) {
|
|
86
|
+
for await (const batch of batches) {
|
|
87
|
+
for (let i = 0; i < batch.length; i++) {
|
|
88
|
+
yield batch[i];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function* regroupBatches(batches, size) {
|
|
93
|
+
let buf = [];
|
|
94
|
+
for await (const batch of batches) {
|
|
95
|
+
for (let i = 0; i < batch.length; i++) {
|
|
96
|
+
buf.push(batch[i]);
|
|
97
|
+
if (buf.length >= size) {
|
|
98
|
+
yield buf;
|
|
99
|
+
buf = [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (buf.length > 0) {
|
|
104
|
+
yield buf;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function* mapBatches(batches, fn) {
|
|
108
|
+
let index = 0;
|
|
109
|
+
for await (const batch of batches) {
|
|
110
|
+
const out = new Array(batch.length);
|
|
111
|
+
for (let i = 0; i < batch.length; i++) {
|
|
112
|
+
const r = fn(batch[i], index++);
|
|
113
|
+
out[i] = isThenable(r) ? await r : r;
|
|
114
|
+
}
|
|
115
|
+
yield out;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function* filterBatches(batches, fn) {
|
|
119
|
+
let index = 0;
|
|
120
|
+
for await (const batch of batches) {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (let i = 0; i < batch.length; i++) {
|
|
123
|
+
const item = batch[i];
|
|
124
|
+
const r = fn(item, index++);
|
|
125
|
+
if (isThenable(r) ? await r : r) {
|
|
126
|
+
out.push(item);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (out.length > 0) {
|
|
130
|
+
yield out;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function* takeBatches(batches, limit) {
|
|
135
|
+
if (limit <= 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let remaining = limit;
|
|
139
|
+
for await (const batch of batches) {
|
|
140
|
+
if (batch.length < remaining) {
|
|
141
|
+
remaining -= batch.length;
|
|
142
|
+
yield batch;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
yield batch.length === remaining ? batch : batch.slice(0, remaining);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function* dropBatches(batches, limit) {
|
|
150
|
+
let remaining = limit;
|
|
151
|
+
for await (const batch of batches) {
|
|
152
|
+
if (remaining === 0) {
|
|
153
|
+
yield batch;
|
|
154
|
+
}
|
|
155
|
+
else if (remaining >= batch.length) {
|
|
156
|
+
remaining -= batch.length;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
yield batch.slice(remaining);
|
|
160
|
+
remaining = 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function isThenable(value) {
|
|
165
|
+
return value != null && typeof value.then === 'function';
|
|
166
|
+
}
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
-
import type
|
|
3
|
-
export type { StandardSchemaV1
|
|
4
|
-
export type Segment = string | number;
|
|
5
|
-
export type Path = readonly Segment[];
|
|
6
|
-
export declare class ValidationError extends Error {
|
|
7
|
-
readonly issues: readonly StandardSchemaV1.Issue[];
|
|
8
|
-
readonly path: Path;
|
|
9
|
-
constructor(issues: readonly StandardSchemaV1.Issue[], path: Path);
|
|
10
|
-
}
|
|
11
|
-
export declare class PathError extends Error {
|
|
12
|
-
readonly path: Path;
|
|
13
|
-
/** The fault kind; stable across versions, safe to branch on. */
|
|
14
|
-
readonly code: PathFaultCode;
|
|
15
|
-
constructor(path: Path, code: PathFaultCode, segment?: number);
|
|
16
|
-
}
|
|
2
|
+
import { type Path } from './path.ts';
|
|
3
|
+
export type { StandardSchemaV1 };
|
|
17
4
|
export declare function runStandardSchema<O>(schema: StandardSchemaV1<unknown, O>, value: unknown, path: Path): Promise<O>;
|
|
18
5
|
export declare function validateItem<O>(schema: StandardSchemaV1<unknown, O>, value: unknown, path: Path, onInvalid: 'throw' | 'skip'): Promise<{
|
|
19
6
|
skip: true;
|
|
20
7
|
} | {
|
|
21
8
|
value: O;
|
|
22
9
|
}>;
|
|
23
|
-
export declare function formatPath(path: Path): string;
|
package/dist/validate.js
CHANGED
|
@@ -1,66 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
issues;
|
|
3
|
-
path;
|
|
4
|
-
constructor(issues, path) {
|
|
5
|
-
super(`bote: schema validation failed at ${formatPath(path)}: ${issues[0]?.message ?? 'unknown'}`);
|
|
6
|
-
this.name = 'ValidationError';
|
|
7
|
-
this.issues = issues;
|
|
8
|
-
this.path = path;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
/** Human message per fault kind. The native layer ships only the code (and the
|
|
12
|
-
* offending `segment` where it matters), so this is the single source of the
|
|
13
|
-
* user-facing prose. Keyed by the Rust-generated [`PathFaultCode`]. */
|
|
14
|
-
const PATH_FAULT_MESSAGE = {
|
|
15
|
-
through_scalar: (segment) => `path traverses a non-container value at segment ${segment}`,
|
|
16
|
-
wrong_kind: (segment) => `path segment ${segment} does not match the container kind`,
|
|
17
|
-
scalar_target: () => 'target value is not a container',
|
|
18
|
-
iter_on_object: () => 'iter target is an object; use walk() to iterate object members',
|
|
19
|
-
walk_on_array: () => 'walk target is an array; use iter() to iterate array elements',
|
|
20
|
-
};
|
|
21
|
-
export class PathError extends Error {
|
|
22
|
-
path;
|
|
23
|
-
/** The fault kind; stable across versions, safe to branch on. */
|
|
24
|
-
code;
|
|
25
|
-
constructor(path, code, segment) {
|
|
26
|
-
const reason = (PATH_FAULT_MESSAGE[code] ?? (() => code))(segment);
|
|
27
|
-
super(`bote: cannot resolve ${formatPath(path)}: ${reason}`);
|
|
28
|
-
this.name = 'PathError';
|
|
29
|
-
this.path = path;
|
|
30
|
-
this.code = code;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
import { ValidationError } from "./error.js";
|
|
33
2
|
export async function runStandardSchema(schema, value, path) {
|
|
34
3
|
const result = await schema['~standard'].validate(value);
|
|
35
|
-
if (result.issues)
|
|
4
|
+
if (result.issues) {
|
|
36
5
|
throw new ValidationError(result.issues, path);
|
|
6
|
+
}
|
|
37
7
|
return result.value;
|
|
38
8
|
}
|
|
39
9
|
export async function validateItem(schema, value, path, onInvalid) {
|
|
40
10
|
const result = await schema['~standard'].validate(value);
|
|
41
11
|
if (result.issues) {
|
|
42
|
-
if (onInvalid === 'skip')
|
|
12
|
+
if (onInvalid === 'skip') {
|
|
43
13
|
return { skip: true };
|
|
14
|
+
}
|
|
44
15
|
throw new ValidationError(result.issues, path);
|
|
45
16
|
}
|
|
46
17
|
return { value: result.value };
|
|
47
18
|
}
|
|
48
|
-
export function formatPath(path) {
|
|
49
|
-
if (path.length === 0)
|
|
50
|
-
return '(root)';
|
|
51
|
-
let out = '';
|
|
52
|
-
for (let i = 0; i < path.length; i++) {
|
|
53
|
-
const seg = path[i];
|
|
54
|
-
if (typeof seg === 'number') {
|
|
55
|
-
out += `[${seg}]`;
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(seg)) {
|
|
59
|
-
out += i === 0 ? seg : `.${seg}`;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
out += `[${JSON.stringify(seg)}]`;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botejs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
"build": "tsc",
|
|
32
32
|
"build:debug": "tsc --sourceMap",
|
|
33
33
|
"test": "node --test __test__/*.spec.ts",
|
|
34
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p __test__/tsconfig.json",
|
|
34
35
|
"lint": "oxlint src",
|
|
35
36
|
"prepublishOnly": "cp ../../README.md ./README.md && tsc"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@botejs/native": "^0.
|
|
39
|
+
"@botejs/native": "^0.6.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/node": "^22.0.0",
|
package/dist/sources.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A handle on an opened seekable byte stream. The reader owns whatever
|
|
3
|
-
* resources back the stream (a file handle, an `AbortController`, etc.) and
|
|
4
|
-
* surfaces them through `close()`. Constructed by `Source.open()`; never by
|
|
5
|
-
* library callers directly.
|
|
6
|
-
*/
|
|
7
|
-
export interface SourceReader {
|
|
8
|
-
/** Total length of the underlying byte stream. */
|
|
9
|
-
readonly size: number;
|
|
10
|
-
/** Preferred read granularity in bytes. Must be a non-zero multiple of 64. */
|
|
11
|
-
readonly chunkBytes?: number;
|
|
12
|
-
/**
|
|
13
|
-
* Read up to `length` bytes starting at `offset` and resolve with the
|
|
14
|
-
* bytes read. The returned `Uint8Array`'s `.byteLength` is the actual
|
|
15
|
-
* count, which must be `<= length`.
|
|
16
|
-
*/
|
|
17
|
-
read(offset: number, length: number): Promise<Uint8Array>;
|
|
18
|
-
/** Release resources held by the reader. Driven once by the `open()` lifecycle. */
|
|
19
|
-
close?(): Promise<void> | void;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Describes how to obtain a seekable byte stream. Provide your own object implementing
|
|
23
|
-
* this interface to plug in custom backends.
|
|
24
|
-
*/
|
|
25
|
-
export interface Source {
|
|
26
|
-
/** Acquire the stream. Resolves to a `SourceReader` that owns any underlying resources. */
|
|
27
|
-
open(): Promise<SourceReader>;
|
|
28
|
-
}
|
|
29
|
-
export interface FactoryOptions {
|
|
30
|
-
/** Override the factory's default chunk size. Must be a non-zero multiple of 64. */
|
|
31
|
-
chunkBytes?: number;
|
|
32
|
-
}
|
|
33
|
-
export interface HttpRangeOptions extends FactoryOptions {
|
|
34
|
-
/** Merged into every request (headers, credentials, signal, etc.). */
|
|
35
|
-
init?: RequestInit;
|
|
36
|
-
}
|
|
37
|
-
export declare function fromBuffer(buf: Uint8Array | ArrayBuffer, options?: FactoryOptions): Source;
|
|
38
|
-
export declare function fromFile(path: string, options?: FactoryOptions): Source;
|
|
39
|
-
export declare function fromHttpRange(url: string, options?: HttpRangeOptions): Source;
|