@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
package/dist/open.d.ts
CHANGED
|
@@ -1,86 +1,47 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
3
|
-
import { type IterOptions } from './args.ts';
|
|
4
|
-
type InferOutput<Sch> = Sch extends StandardSchemaV1<unknown, infer O> ? O : never;
|
|
5
|
-
type SelectMapShape<S> = {
|
|
6
|
-
-readonly [K in keyof S]: unknown;
|
|
7
|
-
};
|
|
8
|
-
/** Zero-based index of an array element. */
|
|
9
|
-
export type IterIndex = number;
|
|
10
|
-
/** One `walk` step: the member's key paired with a cursor anchored at its value. */
|
|
11
|
-
export type WalkEntry = [key: string, cursor: Cursor];
|
|
1
|
+
import { type RootCursor } from './cursor.ts';
|
|
2
|
+
import type { SeekableSource, ForwardSource } from './source/base.ts';
|
|
12
3
|
export declare const DEFAULT_SOURCE_CHUNK_BYTES: number;
|
|
13
|
-
export declare const DEFAULT_ITER_BATCH = 1000;
|
|
14
|
-
export declare const MAX_ITER_BATCH = 1000000;
|
|
15
4
|
export interface OpenOptions {
|
|
16
5
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* scans. Bounds resident cache memory regardless of document size. `0`
|
|
22
|
-
* disables the cache entirely. Omit for the native default (1024).
|
|
6
|
+
* How much of the index that speeds up repeat lookups to keep in memory,
|
|
7
|
+
* measured in entries. Higher means faster repeat queries but more memory;
|
|
8
|
+
* lower means less memory but slower repeats. Set to `0` to turn the cache
|
|
9
|
+
* off. Defaults to 1024.
|
|
23
10
|
*/
|
|
24
11
|
indexCacheEntries?: number;
|
|
25
12
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* for the native default (unbounded).
|
|
13
|
+
* How many keys per object to index for fast lookup. Higher speeds up access
|
|
14
|
+
* to keys later in large objects but uses more memory; lower saves memory at
|
|
15
|
+
* the cost of slower lookups for those keys. Set to `0` to skip indexing
|
|
16
|
+
* object keys. Defaults to unlimited.
|
|
31
17
|
*/
|
|
32
18
|
objectMemberCap?: number;
|
|
33
19
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Setting both `objectMemberCap` and `arrayIndexInterval` to `0` disables the
|
|
40
|
-
* cache entirely (no source bytes are ever cached either way), as does
|
|
41
|
-
* `indexCacheEntries: 0`.
|
|
20
|
+
* How often to index array positions, e.g. every 16th element. Lower means
|
|
21
|
+
* faster access to arbitrary array elements but more memory; higher saves
|
|
22
|
+
* memory at the cost of slower access. Set to `0` to skip indexing array
|
|
23
|
+
* positions. Defaults to 16.
|
|
42
24
|
*/
|
|
43
25
|
arrayIndexInterval?: number;
|
|
44
26
|
}
|
|
45
|
-
export interface Cursor {
|
|
46
|
-
hop(...path: Segment[]): Promise<Cursor | null>;
|
|
47
|
-
has(...path: Segment[]): Promise<boolean>;
|
|
48
|
-
has(...args: [...Segment[], StandardSchemaV1]): Promise<boolean>;
|
|
49
|
-
get(...path: Segment[]): Promise<unknown>;
|
|
50
|
-
get<Sch extends StandardSchemaV1>(...args: [...Segment[], Sch]): Promise<InferOutput<Sch>>;
|
|
51
|
-
count(...path: Segment[]): Promise<number>;
|
|
52
|
-
iter(...path: Segment[]): AsyncIterable<unknown[]>;
|
|
53
|
-
iter<Sch extends StandardSchemaV1>(...args: [...Segment[], Sch]): AsyncIterable<InferOutput<Sch>[]>;
|
|
54
|
-
iter<Sch extends StandardSchemaV1>(...args: [...Segment[], IterOptions & {
|
|
55
|
-
withIndex: true;
|
|
56
|
-
schema: Sch;
|
|
57
|
-
}]): AsyncIterable<[IterIndex, InferOutput<Sch>][]>;
|
|
58
|
-
iter<Sch extends StandardSchemaV1>(...args: [...Segment[], IterOptions & {
|
|
59
|
-
schema: Sch;
|
|
60
|
-
}]): AsyncIterable<InferOutput<Sch>[]>;
|
|
61
|
-
iter<S extends Record<string, Segment | Path>>(...args: [...Segment[], IterOptions & {
|
|
62
|
-
withIndex: true;
|
|
63
|
-
select: S;
|
|
64
|
-
}]): AsyncIterable<[IterIndex, SelectMapShape<S>][]>;
|
|
65
|
-
iter<S extends Record<string, Segment | Path>>(...args: [...Segment[], IterOptions & {
|
|
66
|
-
select: S;
|
|
67
|
-
}]): AsyncIterable<SelectMapShape<S>[]>;
|
|
68
|
-
iter(...args: [...Segment[], IterOptions & {
|
|
69
|
-
withIndex: true;
|
|
70
|
-
}]): AsyncIterable<[IterIndex, unknown][]>;
|
|
71
|
-
iter(...args: [...Segment[], IterOptions]): AsyncIterable<unknown[]>;
|
|
72
|
-
walk(...path: Segment[]): AsyncIterable<WalkEntry>;
|
|
73
|
-
walk(...path: Segment[]): AsyncIterable<Cursor>;
|
|
74
|
-
}
|
|
75
|
-
export interface RootCursor extends Cursor, AsyncDisposable {
|
|
76
|
-
/** Close the underlying source. Idempotent. */
|
|
77
|
-
close(): Promise<void>;
|
|
78
|
-
}
|
|
79
27
|
/**
|
|
80
|
-
*
|
|
28
|
+
* Options for a forward source: every cache knob is forbidden (the structural-index
|
|
29
|
+
* cache is forced off). `Omit` would collapse to `{}` and silently permit them, so
|
|
30
|
+
* the knobs are mapped to `never` to reject them at compile time.
|
|
31
|
+
*/
|
|
32
|
+
export type ForwardOpenOptions = {
|
|
33
|
+
[K in keyof OpenOptions]?: never;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Open a cursor over a source.
|
|
37
|
+
*
|
|
38
|
+
* A seekable source (`fromFile`/`fromBuffer`/`fromHttpRange`) supports the cache
|
|
39
|
+
* and repeated, out-of-order queries. A forward source (`fromReadable`/`fromHttpStream`)
|
|
40
|
+
* is a single forward pass: the cache is forced off, so its cache knobs are
|
|
41
|
+
* rejected at compile time and at runtime.
|
|
81
42
|
*
|
|
82
|
-
* The returned `RootCursor` owns the reader: `close()` (or `await using`)
|
|
83
|
-
*
|
|
43
|
+
* The returned `RootCursor` owns the reader: `close()` (or `await using`) drives
|
|
44
|
+
* the reader's own `close()` exactly once.
|
|
84
45
|
*/
|
|
85
|
-
export declare function open(source:
|
|
86
|
-
export
|
|
46
|
+
export declare function open(source: SeekableSource, options?: OpenOptions): Promise<RootCursor>;
|
|
47
|
+
export declare function open(source: ForwardSource, options?: ForwardOpenOptions): Promise<RootCursor>;
|
package/dist/open.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import { open as openNative } from '@botejs/native';
|
|
2
|
-
import {
|
|
3
|
-
import { runStandardSchema, validateItem, formatPath, PathError, } from "./validate.js";
|
|
4
|
-
import { splitArgs, isSchema, serializeSelect, normalizeIterTail, } from "./args.js";
|
|
2
|
+
import { wrap } from "./cursor.js";
|
|
5
3
|
export const DEFAULT_SOURCE_CHUNK_BYTES = 64 * 1024;
|
|
6
|
-
|
|
7
|
-
export const MAX_ITER_BATCH = 1_000_000;
|
|
8
|
-
/**
|
|
9
|
-
* Open a cursor over a seekable source.
|
|
10
|
-
*
|
|
11
|
-
* The returned `RootCursor` owns the reader: `close()` (or `await using`)
|
|
12
|
-
* drives the reader's own `close()` exactly once.
|
|
13
|
-
*/
|
|
4
|
+
const CACHE_KNOBS = ['indexCacheEntries', 'objectMemberCap', 'arrayIndexInterval'];
|
|
14
5
|
export async function open(source, options) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
if (!source.seekable) {
|
|
7
|
+
for (const name of CACHE_KNOBS) {
|
|
8
|
+
if (options?.[name] !== undefined) {
|
|
9
|
+
throw new RangeError(`open: ${name} is not allowed for a forward source; the structural-index cache is forced off`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
for (const name of CACHE_KNOBS) {
|
|
14
|
+
const value = options?.[name];
|
|
21
15
|
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
22
16
|
throw new RangeError(`open: ${name} must be a non-negative integer (0 disables), got ${value}`);
|
|
23
17
|
}
|
|
24
18
|
}
|
|
19
|
+
// A forward source disables every cache dimension, so the engine never resolves
|
|
20
|
+
// a cached container offset into a backward read on a stream it cannot rewind.
|
|
21
|
+
const knobs = source.seekable ? options : { indexCacheEntries: 0, objectMemberCap: 0, arrayIndexInterval: 0 };
|
|
25
22
|
const reader = await source.open();
|
|
26
23
|
const chunkBytes = reader.chunkBytes ?? DEFAULT_SOURCE_CHUNK_BYTES;
|
|
27
24
|
let native;
|
|
28
25
|
try {
|
|
29
|
-
if (
|
|
26
|
+
if (reader.seekable && reader.size === undefined) {
|
|
27
|
+
throw new RangeError('open: a seekable source must report a size');
|
|
28
|
+
}
|
|
29
|
+
if (reader.size !== undefined && (!Number.isInteger(reader.size) || reader.size < 0)) {
|
|
30
30
|
throw new RangeError(`open: source size must be a non-negative integer, got ${reader.size}`);
|
|
31
31
|
}
|
|
32
32
|
if (!Number.isSafeInteger(chunkBytes) || chunkBytes <= 0) {
|
|
@@ -38,10 +38,10 @@ export async function open(source, options) {
|
|
|
38
38
|
native = openNative({
|
|
39
39
|
size: reader.size,
|
|
40
40
|
chunkBytes,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
arrayIndexInterval,
|
|
44
|
-
read:
|
|
41
|
+
objectMemberCap: knobs?.objectMemberCap,
|
|
42
|
+
indexCacheEntries: knobs?.indexCacheEntries,
|
|
43
|
+
arrayIndexInterval: knobs?.arrayIndexInterval,
|
|
44
|
+
read: ({ offset, length }) => reader.read(offset, length),
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
catch (err) {
|
|
@@ -50,15 +50,17 @@ export async function open(source, options) {
|
|
|
50
50
|
await closeReader(reader);
|
|
51
51
|
}
|
|
52
52
|
catch (closeErr) {
|
|
53
|
-
if (err instanceof Error)
|
|
53
|
+
if (err instanceof Error) {
|
|
54
54
|
err.cause ??= closeErr;
|
|
55
|
+
}
|
|
55
56
|
}
|
|
56
57
|
throw err;
|
|
57
58
|
}
|
|
58
59
|
const state = { closed: false };
|
|
59
60
|
const close = async () => {
|
|
60
|
-
if (state.closed)
|
|
61
|
+
if (state.closed) {
|
|
61
62
|
return;
|
|
63
|
+
}
|
|
62
64
|
state.closed = true;
|
|
63
65
|
await closeReader(reader);
|
|
64
66
|
};
|
|
@@ -68,172 +70,7 @@ export async function open(source, options) {
|
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
async function closeReader(reader) {
|
|
71
|
-
if (reader.close)
|
|
73
|
+
if (reader.close) {
|
|
72
74
|
await reader.close();
|
|
73
|
-
}
|
|
74
|
-
const NATIVE_PATH_ERROR = /^bote:path:([a-z_]+)(?::(\d+))?$/;
|
|
75
|
-
function deserializeError(err, path) {
|
|
76
|
-
if (err instanceof Error && !(err instanceof PathError)) {
|
|
77
|
-
const match = NATIVE_PATH_ERROR.exec(err.message);
|
|
78
|
-
if (match) {
|
|
79
|
-
const segment = match[2] === undefined ? undefined : Number(match[2]);
|
|
80
|
-
return new PathError(path, match[1], segment);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return err;
|
|
84
|
-
}
|
|
85
|
-
/** Throw a uniform error for any operation on a closed cursor, so use-after-close
|
|
86
|
-
* is one defined contract regardless of source (some readers' reads keep working
|
|
87
|
-
* after close, others throw an opaque I/O error). */
|
|
88
|
-
function ensureOpen(state) {
|
|
89
|
-
if (state.closed)
|
|
90
|
-
throw new Error('bote: cursor is closed');
|
|
91
|
-
}
|
|
92
|
-
function wrap(native, state) {
|
|
93
|
-
const cursor = {
|
|
94
|
-
async hop(...path) {
|
|
95
|
-
ensureOpen(state);
|
|
96
|
-
validatePath(path);
|
|
97
|
-
let child;
|
|
98
|
-
try {
|
|
99
|
-
child = await native.hop(path);
|
|
100
|
-
}
|
|
101
|
-
catch (err) {
|
|
102
|
-
throw deserializeError(err, path);
|
|
103
|
-
}
|
|
104
|
-
return child ? wrap(child, state) : null;
|
|
105
|
-
},
|
|
106
|
-
async has(...args) {
|
|
107
|
-
ensureOpen(state);
|
|
108
|
-
const { path, tail: schema } = splitArgs(args);
|
|
109
|
-
if (schema !== undefined && !isSchema(schema)) {
|
|
110
|
-
throw new TypeError('has: expected a Standard Schema as the trailing argument');
|
|
111
|
-
}
|
|
112
|
-
if (!schema)
|
|
113
|
-
return native.has(path);
|
|
114
|
-
if (!(await native.has(path)))
|
|
115
|
-
return false;
|
|
116
|
-
const text = await native.get(path);
|
|
117
|
-
const value = text === undefined ? undefined : parseValue(text, path);
|
|
118
|
-
const result = await validateItem(schema, value, path, 'skip');
|
|
119
|
-
return !('skip' in result);
|
|
120
|
-
},
|
|
121
|
-
async get(...args) {
|
|
122
|
-
ensureOpen(state);
|
|
123
|
-
const { path, tail: schema } = splitArgs(args);
|
|
124
|
-
if (schema !== undefined && !isSchema(schema)) {
|
|
125
|
-
throw new TypeError('get: expected a Standard Schema as the trailing argument');
|
|
126
|
-
}
|
|
127
|
-
let value;
|
|
128
|
-
try {
|
|
129
|
-
const text = await native.get(path);
|
|
130
|
-
value = text === undefined ? undefined : parseValue(text, path);
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
throw deserializeError(err, path);
|
|
134
|
-
}
|
|
135
|
-
if (!schema)
|
|
136
|
-
return value;
|
|
137
|
-
return runStandardSchema(schema, value, path);
|
|
138
|
-
},
|
|
139
|
-
async count(...path) {
|
|
140
|
-
ensureOpen(state);
|
|
141
|
-
validatePath(path);
|
|
142
|
-
try {
|
|
143
|
-
return await native.count(path);
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
throw deserializeError(err, path);
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
iter(...args) {
|
|
150
|
-
ensureOpen(state);
|
|
151
|
-
const { path, tail } = splitArgs(args);
|
|
152
|
-
const { schema, select, batch, onInvalid, withIndex } = normalizeIterTail(tail);
|
|
153
|
-
if (batch !== undefined && (!Number.isInteger(batch) || batch <= 0 || batch > MAX_ITER_BATCH)) {
|
|
154
|
-
throw new RangeError(`iter: batch must be an integer in 1..=${MAX_ITER_BATCH}, got ${batch}`);
|
|
155
|
-
}
|
|
156
|
-
if (withIndex !== undefined && typeof withIndex !== 'boolean') {
|
|
157
|
-
throw new TypeError(`iter: withIndex must be a boolean, got ${typeof withIndex}`);
|
|
158
|
-
}
|
|
159
|
-
if (onInvalid !== undefined && onInvalid !== 'throw' && onInvalid !== 'skip') {
|
|
160
|
-
throw new RangeError(`iter: onInvalid must be "throw" or "skip", got ${JSON.stringify(onInvalid)}`);
|
|
161
|
-
}
|
|
162
|
-
const resolvedBatch = batch ?? DEFAULT_ITER_BATCH;
|
|
163
|
-
const selectIr = select !== undefined ? serializeSelect(select) : undefined;
|
|
164
|
-
const inner = native.iter(path, { selectIr, batch: resolvedBatch });
|
|
165
|
-
if (!schema) {
|
|
166
|
-
return {
|
|
167
|
-
async *[Symbol.asyncIterator]() {
|
|
168
|
-
let i = 0;
|
|
169
|
-
try {
|
|
170
|
-
for await (const b of inner) {
|
|
171
|
-
const batch = parseValue(b, path);
|
|
172
|
-
if (!withIndex) {
|
|
173
|
-
yield batch;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
const out = new Array(batch.length);
|
|
177
|
-
for (let j = 0; j < batch.length; j++) {
|
|
178
|
-
out[j] = [i++, batch[j]];
|
|
179
|
-
}
|
|
180
|
-
yield out;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
throw deserializeError(err, path);
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
const policy = onInvalid ?? 'throw';
|
|
190
|
-
return {
|
|
191
|
-
async *[Symbol.asyncIterator]() {
|
|
192
|
-
let i = 0;
|
|
193
|
-
try {
|
|
194
|
-
for await (const b of inner) {
|
|
195
|
-
const out = [];
|
|
196
|
-
for (const v of parseValue(b, path)) {
|
|
197
|
-
const index = i++;
|
|
198
|
-
const result = await validateItem(schema, v, [...path, index], policy);
|
|
199
|
-
if ('skip' in result) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
out.push(withIndex ? [index, result.value] : result.value);
|
|
203
|
-
}
|
|
204
|
-
yield out;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
throw deserializeError(err, path);
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
|
-
walk(...path) {
|
|
214
|
-
ensureOpen(state);
|
|
215
|
-
validatePath(path);
|
|
216
|
-
return {
|
|
217
|
-
async *[Symbol.asyncIterator]() {
|
|
218
|
-
try {
|
|
219
|
-
for await (const [key, child] of native.walk(path)) {
|
|
220
|
-
yield [key, wrap(child, state)];
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
catch (err) {
|
|
224
|
-
throw deserializeError(err, path);
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
return cursor;
|
|
231
|
-
}
|
|
232
|
-
function parseValue(text, path) {
|
|
233
|
-
try {
|
|
234
|
-
return JSON.parse(text);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
throw new Error(`bote: malformed JSON value at ${formatPath(path)}`);
|
|
238
75
|
}
|
|
239
76
|
}
|
package/dist/path.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
export type Segment = string | number;
|
|
2
|
+
export type Path = readonly Segment[];
|
|
2
3
|
/** Upper bound on numeric segments (napi takes them as `u32`). 2^32 - 1
|
|
3
4
|
* comfortably covers any in-memory JSON array. */
|
|
4
5
|
export declare const MAX_ARRAY_INDEX = 4294967295;
|
|
5
6
|
export declare function validatePath(path: readonly unknown[]): asserts path is readonly Segment[];
|
|
7
|
+
export declare function formatPath(path: Path): string;
|
package/dist/path.js
CHANGED
|
@@ -4,17 +4,41 @@ export const MAX_ARRAY_INDEX = 0xffffffff;
|
|
|
4
4
|
export function validatePath(path) {
|
|
5
5
|
for (let i = 0; i < path.length; i++) {
|
|
6
6
|
const s = path[i];
|
|
7
|
-
if (typeof s === 'string')
|
|
7
|
+
if (typeof s === 'string') {
|
|
8
8
|
continue;
|
|
9
|
-
|
|
9
|
+
}
|
|
10
|
+
if (typeof s === 'number' && Number.isInteger(s) && s >= 0 && s <= MAX_ARRAY_INDEX) {
|
|
10
11
|
continue;
|
|
12
|
+
}
|
|
11
13
|
throw new TypeError(`path segment ${i}: expected string or non-negative integer (<= ${MAX_ARRAY_INDEX}), got ${describeBadSegment(s)}`);
|
|
12
14
|
}
|
|
13
15
|
}
|
|
16
|
+
export function formatPath(path) {
|
|
17
|
+
if (path.length === 0) {
|
|
18
|
+
return '(root)';
|
|
19
|
+
}
|
|
20
|
+
let out = '';
|
|
21
|
+
for (let i = 0; i < path.length; i++) {
|
|
22
|
+
const seg = path[i];
|
|
23
|
+
if (typeof seg === 'number') {
|
|
24
|
+
out += `[${seg}]`;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(seg)) {
|
|
28
|
+
out += i === 0 ? seg : `.${seg}`;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
out += `[${JSON.stringify(seg)}]`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
14
36
|
function describeBadSegment(s) {
|
|
15
|
-
if (typeof s === 'number')
|
|
37
|
+
if (typeof s === 'number') {
|
|
16
38
|
return `${s}`;
|
|
17
|
-
|
|
39
|
+
}
|
|
40
|
+
if (s === null) {
|
|
18
41
|
return 'null';
|
|
42
|
+
}
|
|
19
43
|
return typeof s;
|
|
20
44
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bytes a `read` resolves to, plus an end-of-stream flag. `eof` is `true`
|
|
3
|
+
* iff this read reached the end of the underlying stream. A seekable reader can
|
|
4
|
+
* compute it from `size`; a forward reader discovers it as the stream drains.
|
|
5
|
+
*/
|
|
6
|
+
export interface ReadResult {
|
|
7
|
+
readonly data: Uint8Array;
|
|
8
|
+
readonly eof: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A handle on an opened byte stream. The reader owns whatever resources back the
|
|
12
|
+
* stream (a file handle, a `fetch` body, an `AbortController`, etc.) and surfaces
|
|
13
|
+
* them through `close()`. Constructed by `Source.open()`; never by callers directly.
|
|
14
|
+
*
|
|
15
|
+
* `seekable` declares the access model:
|
|
16
|
+
* - `true`: `read(offset, length)` may be called at any offset, in any order.
|
|
17
|
+
* `size` is required. This random access is what lets the structural-index
|
|
18
|
+
* cache resume scans near a target.
|
|
19
|
+
* - `false`: a single forward pass. `read` is called with non-decreasing
|
|
20
|
+
* offsets; the cache is forced off. `size` may be omitted (the end is found
|
|
21
|
+
* via `eof`). Rewinding to an earlier offset re-acquires the stream (see
|
|
22
|
+
* `fromReadable`'s `rewind` option) or throws {@link ForwardReplayError}.
|
|
23
|
+
*/
|
|
24
|
+
export interface Reader {
|
|
25
|
+
/** Whether reads may target any offset/order (`true`) or a single forward pass (`false`). */
|
|
26
|
+
readonly seekable: boolean;
|
|
27
|
+
/** Total length in bytes. Required for seekable readers; optional for forward ones. */
|
|
28
|
+
readonly size?: number;
|
|
29
|
+
/** Preferred read granularity in bytes. Must be a non-zero multiple of 64. */
|
|
30
|
+
readonly chunkBytes?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Read up to `length` bytes starting at `offset`. `data.byteLength` is the
|
|
33
|
+
* actual count read (`<= length`); `eof` is `true` iff the read reached the
|
|
34
|
+
* end of the stream.
|
|
35
|
+
*/
|
|
36
|
+
read(offset: number, length: number): Promise<ReadResult>;
|
|
37
|
+
/** Release resources held by the reader. Driven once by the `open()` lifecycle. */
|
|
38
|
+
close?(): Promise<void> | void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Describes how to obtain a byte stream. `open()` is called once per cursor for
|
|
42
|
+
* a seekable source; a forward source's reader re-acquires its stream on demand.
|
|
43
|
+
* Provide your own object to plug in a custom backend.
|
|
44
|
+
*/
|
|
45
|
+
export interface Source {
|
|
46
|
+
/** Mirrors {@link Reader.seekable}; lets `open()` enforce the right knobs at compile time. */
|
|
47
|
+
readonly seekable: boolean;
|
|
48
|
+
/** Acquire the stream. Resolves to a `Reader` that owns any underlying resources. */
|
|
49
|
+
open(): Promise<Reader>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A {@link Source} statically known to be seekable (random access, cache-eligible).
|
|
53
|
+
* `open()` discriminates on this, so a custom backend must brand its `seekable`
|
|
54
|
+
* as the literal `true` (annotate the object `SeekableSource`) to be accepted.
|
|
55
|
+
*/
|
|
56
|
+
export type SeekableSource = Source & {
|
|
57
|
+
readonly seekable: true;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A {@link Source} statically known to be forward-only (single pass, cache forced
|
|
61
|
+
* off). Brand a custom backend's object `ForwardSource` so `open()` accepts it and
|
|
62
|
+
* rejects cache knobs at compile time.
|
|
63
|
+
*/
|
|
64
|
+
export type ForwardSource = Source & {
|
|
65
|
+
readonly seekable: false;
|
|
66
|
+
};
|
|
67
|
+
export interface FactoryOptions {
|
|
68
|
+
/** Override the factory's default chunk size. Must be a non-zero multiple of 64. */
|
|
69
|
+
chunkBytes?: number;
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FactoryOptions, ForwardSource } from './base.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A function that produces a fresh readable stream each time it is called. A
|
|
4
|
+
* forward reader invokes it once up front, and again on every re-acquisition, so
|
|
5
|
+
* it is also the seam for per-acquisition customization (a freshly minted auth
|
|
6
|
+
* token, a new `AbortSignal`, etc.).
|
|
7
|
+
*/
|
|
8
|
+
export type ReadableProducer = () => NodeJS.ReadableStream | ReadableStream<Uint8Array> | Promise<NodeJS.ReadableStream | ReadableStream<Uint8Array>>;
|
|
9
|
+
export interface ReadableOptions extends FactoryOptions {
|
|
10
|
+
/** Known total length, if any. Forwarded to the engine so it can skip rediscovering the end. */
|
|
11
|
+
size?: number;
|
|
12
|
+
/** Transform applied to every (re)acquired stream, e.g. `s => s.pipeThrough(new DecompressionStream('gzip'))`. */
|
|
13
|
+
decode?: (raw: ReadableStream<Uint8Array>) => ReadableStream<Uint8Array>;
|
|
14
|
+
/**
|
|
15
|
+
* What a later query that must re-read from an earlier offset does. Defaults
|
|
16
|
+
* to `'forbid'`. The three settings trade resident memory for re-read ability:
|
|
17
|
+
* - `'forbid'`: a single forward pass; a rewind throws {@link ForwardReplayError}.
|
|
18
|
+
* - `'replay'`: re-acquire the stream from the start. Only safe when the
|
|
19
|
+
* producer is idempotent (yields the same bytes each call). No extra memory.
|
|
20
|
+
* - `'buffer'`: snapshot the whole stream into memory on first read, enabling
|
|
21
|
+
* random access at O(n) resident memory.
|
|
22
|
+
*/
|
|
23
|
+
rewind?: 'forbid' | 'replay' | 'buffer';
|
|
24
|
+
}
|
|
25
|
+
export interface HttpRequestOptions extends Omit<ReadableOptions, 'size'> {
|
|
26
|
+
/** Merged into every `fetch` (headers, credentials, signal, etc.). */
|
|
27
|
+
init?: RequestInit;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A forward-only source backed by a re-openable readable stream. `produce` is
|
|
31
|
+
* called to acquire each pass: once up front, and again on a rewind when
|
|
32
|
+
* `rewind: 'replay'` is set. A plain `Readable` instance cannot be re-streamed,
|
|
33
|
+
* so pass a thunk (`() => createReadStream(path)`), not a live stream.
|
|
34
|
+
*
|
|
35
|
+
* Because every cursor operation is an independent scan from the start, a single
|
|
36
|
+
* forward pass serves exactly one query; a second query (or `hop` then `get`)
|
|
37
|
+
* rewinds. By default that throws {@link ForwardReplayError}; opt into
|
|
38
|
+
* `rewind: 'replay'` (idempotent producer) or `rewind: 'buffer'` (in-memory
|
|
39
|
+
* snapshot) for multi-query access.
|
|
40
|
+
*/
|
|
41
|
+
export declare function fromReadable(produce: ReadableProducer, options?: ReadableOptions): ForwardSource;
|
|
42
|
+
/**
|
|
43
|
+
* A forward-only source over an HTTP request (GET is default), streamed in a single pass. A
|
|
44
|
+
* convenience wrapper around {@link fromReadable} whose producer re-fetches `url`
|
|
45
|
+
* (reusing `init`, so auth headers, credentials, and an `AbortSignal` survive
|
|
46
|
+
* each acquisition). For repeated or random access over HTTP, prefer the seekable
|
|
47
|
+
* {@link fromHttpRange}.
|
|
48
|
+
*/
|
|
49
|
+
export declare function fromHttpRequest(url: string, options?: HttpRequestOptions): ForwardSource;
|