@botejs/core 0.2.0 → 0.3.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 +90 -63
- package/dist/args.js +11 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/open.d.ts +5 -2
- package/dist/open.js +122 -29
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,94 +8,121 @@ npm install @botejs/core
|
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
import { open, fromFile } from '@botejs/core'
|
|
11
|
+
import { publish } from './message-bus'
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// e.g. { items: [...] }
|
|
14
|
+
await using cursor = await open(fromFile('./some-large.json'))
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
// items[0]
|
|
17
|
+
const first = await cursor.get('items', 0)
|
|
18
|
+
console.log(`first item: ${first}`)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
given a **seekable** source (e.g. a file, an HTTP range) and a path, it retrieves values out of a JSON quickly, without loading the whole thing in-memory.
|
|
22
|
+
|
|
23
|
+
here's a run (Apple M1 Pro 2021, ~500MB JSON array file, cold-cache, default settings):
|
|
24
|
+
|
|
25
|
+
| operation | approach | time | js heap peak Δ | rust heap peak |
|
|
26
|
+
| -------------- | ---------- | --------: | -------------: | -------------: |
|
|
27
|
+
| items[0] | JSON.parse | 616.02 ms | 1.03 GB | n/a |
|
|
28
|
+
| items[535399] | JSON.parse | 604.63 ms | 1.03 GB | n/a |
|
|
29
|
+
| items[1070797] | JSON.parse | 600.68 ms | 1.03 GB | n/a |
|
|
30
|
+
| items[0] | bote | 527.80 µs | 291.6 KB | 130.4 KB |
|
|
31
|
+
| items[535399] | bote | 187.24 ms | 742.3 KB | 36.7 MB |
|
|
32
|
+
| items[1070797] | bote | 371.61 ms | 828.7 KB | 37.1 MB |
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
## array access
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
`iter` streams the elements of an array at a path, **a batch at a time**, so you never hold the whole collection in memory and not wait for the heat death of the universe if this yielded individually. each `for await` step yields an array of items (use `walk` to step over the members of an object):
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const desc1: string = await cursor.get('users', 1000, 'name', User.shape.name)
|
|
38
|
+
```ts
|
|
39
|
+
// e.g. [{ id: 'user-1' }, { id: 'user-2' }, ...]
|
|
40
|
+
await using cursor = await open(fromFile('./users.json'))
|
|
31
41
|
|
|
32
|
-
//
|
|
33
|
-
for await (const
|
|
34
|
-
|
|
35
|
-
for (const user of batch) {
|
|
42
|
+
// root is an array
|
|
43
|
+
for await (const users of cursor.iter()) {
|
|
44
|
+
for (const user of users) {
|
|
36
45
|
console.log(user)
|
|
37
46
|
}
|
|
38
47
|
}
|
|
48
|
+
```
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
50
|
+
pass an options object as the last argument to tune what comes back: `batch`, `select`, `schema`, `onInvalid`, and `withIndex`. if you want to know more of the options, see [`arrays.js`](./examples/arrays.js).
|
|
51
|
+
|
|
52
|
+
## object access
|
|
53
|
+
|
|
54
|
+
`walk` steps over the members of an object at a path, yielding a **`[key, cursor]`** pair per member. the key is the member name, the cursor is anchored at its value. each child cursor is first-class: it outlives the loop and can be `walk`ed again, which is what lets you descend a tree of unknown depth.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// e.g. { alice: { role: 'admin' }, bob: { role: 'guest' }, ... }
|
|
58
|
+
await using cursor = await open(fromFile('./accounts.json'))
|
|
59
|
+
|
|
60
|
+
for await (const [name, account] of cursor.walk()) {
|
|
61
|
+
// name is the member name ('alice', 'bob', ...)
|
|
62
|
+
const role = await account.get('role')
|
|
63
|
+
console.log(`${name}: ${role}`)
|
|
55
64
|
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
see [`recursive.js`](./examples/recursive.js) for advanced use-cases.
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
## hopping
|
|
70
|
+
|
|
71
|
+
`hop` resolves a path once and hands back a **cursor** anchored at that value (or `null` if the path isn't there):
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// e.g. { report: { sections: [{ rows: [...] }, ...] } }
|
|
75
|
+
await using cursor = await open(fromFile('./report.json'))
|
|
76
|
+
|
|
77
|
+
const section = await cursor.hop('report', 'sections', 0)
|
|
78
|
+
if (section) {
|
|
79
|
+
console.log(await section.count('rows'))
|
|
80
|
+
for await (const rows of section.iter('rows')) {
|
|
81
|
+
console.log(rows)
|
|
65
82
|
}
|
|
66
83
|
}
|
|
84
|
+
```
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
## validation
|
|
87
|
+
|
|
88
|
+
`get`, and `iter` takes a [Standard Schema](https://standardschema.dev) validator as their last argument (for `iter`, can also be passed in an `options` object). the value is validated and the return type is inferred from the schema, so reads come back typed instead of `unknown`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { open, fromFile } from '@botejs/core'
|
|
92
|
+
import * as z from 'zod' // or any Standard Schema validator
|
|
93
|
+
|
|
94
|
+
// a downstream API that wants a typed list of recipients
|
|
95
|
+
declare function sendNewsletter(recipients: string[]): Promise<void>
|
|
96
|
+
|
|
97
|
+
const User = z.object({
|
|
98
|
+
id: z.string(),
|
|
99
|
+
name: z.string(),
|
|
100
|
+
email: z.string(),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const cursor = await open(fromFile('./users.json'))
|
|
104
|
+
|
|
105
|
+
// name: string
|
|
106
|
+
const name = await cursor.get('users', 1000, 'name', User.shape.name)
|
|
107
|
+
|
|
108
|
+
for await (const users of cursor.iter('users', User)) {
|
|
109
|
+
// user: User[]
|
|
110
|
+
const emails = users.map((user) => user.email)
|
|
111
|
+
await sendNewsletter(emails)
|
|
75
112
|
}
|
|
76
113
|
|
|
77
|
-
// 'await using' would normally clean up resources for you
|
|
78
|
-
// when it goes out of lexical scope. if you hate that,
|
|
79
|
-
// you can do it explicitly as well.
|
|
80
114
|
await cursor.close()
|
|
81
115
|
```
|
|
82
116
|
|
|
83
|
-
|
|
117
|
+
## memory
|
|
84
118
|
|
|
85
|
-
|
|
119
|
+
bote keeps a small **structural-index** cache: as scans walk containers (arrays and object), it remembers where members live, so a later query that lands in an already walked container resumes near the target instead of from the top. it caches structure, never source bytes, so it can't grow unbounded with document size.
|
|
86
120
|
|
|
87
|
-
|
|
88
|
-
| -------------- | ---------- | --------: | -------------: | -------------: |
|
|
89
|
-
| items[0] | JSON.parse | 616.02 ms | 1.03 GB | n/a |
|
|
90
|
-
| items[535399] | JSON.parse | 604.63 ms | 1.03 GB | n/a |
|
|
91
|
-
| items[1070797] | JSON.parse | 600.68 ms | 1.03 GB | n/a |
|
|
92
|
-
| items[0] | bote | 527.80 µs | 291.6 KB | 130.4 KB |
|
|
93
|
-
| items[535399] | bote | 187.24 ms | 742.3 KB | 36.7 MB |
|
|
94
|
-
| items[1070797] | bote | 371.61 ms | 828.7 KB | 37.1 MB |
|
|
121
|
+
the defaults are good, but `open` takes a few knobs: `indexCacheEntries`, `objectMemberCap`, and `arrayIndexInterval`. to bound memory tighter or turn the cache off. see [`memory.js`](./examples/memory.js) for what each does.
|
|
95
122
|
|
|
96
123
|
## sources
|
|
97
124
|
|
|
98
|
-
bote
|
|
125
|
+
bote ships `fromFile`, `fromHttpRange`, and `fromBuffer` as pre-built sources. create your own by implementing the `Source` interface. see [`sources-custom.ts`](./examples/sources-custom.ts) or [./packages/core/src/sources.ts](./packages/core/src/sources.ts) for how it works.
|
|
99
126
|
|
|
100
127
|
## status
|
|
101
128
|
|
package/dist/args.js
CHANGED
|
@@ -49,8 +49,14 @@ function serializeSelect(select) {
|
|
|
49
49
|
}
|
|
50
50
|
return JSON.stringify({ one: select });
|
|
51
51
|
}
|
|
52
|
+
if (select === null || typeof select !== 'object') {
|
|
53
|
+
throw new TypeError(`iter: select must be a segment, path, or field map, got ${describeSelect(select)}`);
|
|
54
|
+
}
|
|
52
55
|
const entries = Object.entries(select).map(([k, sub]) => {
|
|
53
56
|
const path = typeof sub === 'string' || typeof sub === 'number' ? [sub] : sub;
|
|
57
|
+
if (!Array.isArray(path)) {
|
|
58
|
+
throw new TypeError(`iter: select field ${JSON.stringify(k)} must be a segment or path, got ${describeSelect(sub)}`);
|
|
59
|
+
}
|
|
54
60
|
(0, path_ts_1.validatePath)(path);
|
|
55
61
|
if (path.length === 0) {
|
|
56
62
|
throw new RangeError(`iter: select field ${JSON.stringify(k)} sub-path must have at least one segment`);
|
|
@@ -62,3 +68,8 @@ function serializeSelect(select) {
|
|
|
62
68
|
}
|
|
63
69
|
return JSON.stringify({ map: entries });
|
|
64
70
|
}
|
|
71
|
+
function describeSelect(value) {
|
|
72
|
+
if (value === null)
|
|
73
|
+
return 'null';
|
|
74
|
+
return Array.isArray(value) ? 'array' : typeof value;
|
|
75
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { type IterOptions } from './args.ts';
|
|
2
|
-
export { ValidationError, formatPath, type Path, type Segment, type StandardSchemaV1 } from './validate.ts';
|
|
3
|
-
export { open, DEFAULT_ITER_BATCH, type Cursor, type RootCursor, type OpenOptions, type IterIndex as IterKey, } from './open.ts';
|
|
2
|
+
export { ValidationError, PathError, formatPath, type Path, type Segment, type StandardSchemaV1, } from './validate.ts';
|
|
3
|
+
export { open, DEFAULT_ITER_BATCH, MAX_ITER_BATCH, type Cursor, type RootCursor, type OpenOptions, type WalkEntry, type IterIndex as IterKey, } from './open.ts';
|
|
4
4
|
export { fromBuffer, fromFile, fromHttpRange, type FactoryOptions, type Source, type SourceReader, type HttpRangeOptions, } from './sources.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fromHttpRange = exports.fromFile = exports.fromBuffer = exports.DEFAULT_ITER_BATCH = exports.formatPath = exports.ValidationError = void 0;
|
|
3
|
+
exports.fromHttpRange = exports.fromFile = exports.fromBuffer = exports.MAX_ITER_BATCH = exports.DEFAULT_ITER_BATCH = exports.formatPath = exports.PathError = exports.ValidationError = void 0;
|
|
4
4
|
// Node 18 and Node 20.3 predate `Symbol.asyncDispose`; mirror what TS emits for
|
|
5
5
|
// `await using` so the well-known symbol is available across our engine range.
|
|
6
6
|
if (!Symbol.asyncDispose) {
|
|
@@ -9,10 +9,12 @@ if (!Symbol.asyncDispose) {
|
|
|
9
9
|
}
|
|
10
10
|
var validate_ts_1 = require("./validate.js");
|
|
11
11
|
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return validate_ts_1.ValidationError; } });
|
|
12
|
+
Object.defineProperty(exports, "PathError", { enumerable: true, get: function () { return validate_ts_1.PathError; } });
|
|
12
13
|
Object.defineProperty(exports, "formatPath", { enumerable: true, get: function () { return validate_ts_1.formatPath; } });
|
|
13
14
|
var open_ts_1 = require("./open.js");
|
|
14
15
|
Object.defineProperty(exports, "open", { enumerable: true, get: function () { return open_ts_1.open; } });
|
|
15
16
|
Object.defineProperty(exports, "DEFAULT_ITER_BATCH", { enumerable: true, get: function () { return open_ts_1.DEFAULT_ITER_BATCH; } });
|
|
17
|
+
Object.defineProperty(exports, "MAX_ITER_BATCH", { enumerable: true, get: function () { return open_ts_1.MAX_ITER_BATCH; } });
|
|
16
18
|
var sources_ts_1 = require("./sources.js");
|
|
17
19
|
Object.defineProperty(exports, "fromBuffer", { enumerable: true, get: function () { return sources_ts_1.fromBuffer; } });
|
|
18
20
|
Object.defineProperty(exports, "fromFile", { enumerable: true, get: function () { return sources_ts_1.fromFile; } });
|
package/dist/open.d.ts
CHANGED
|
@@ -7,8 +7,11 @@ type SelectMapShape<S> = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Zero-based index of an array element. */
|
|
9
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];
|
|
10
12
|
export declare const DEFAULT_SOURCE_CHUNK_BYTES: number;
|
|
11
13
|
export declare const DEFAULT_ITER_BATCH = 1000;
|
|
14
|
+
export declare const MAX_ITER_BATCH = 1000000;
|
|
12
15
|
export interface OpenOptions {
|
|
13
16
|
/**
|
|
14
17
|
* Slot budget for the structural-index cache: one slot per cached container
|
|
@@ -40,8 +43,7 @@ export interface OpenOptions {
|
|
|
40
43
|
arrayIndexInterval?: number;
|
|
41
44
|
}
|
|
42
45
|
export interface Cursor {
|
|
43
|
-
|
|
44
|
-
readonly key: string | number | null;
|
|
46
|
+
hop(...path: Segment[]): Promise<Cursor | null>;
|
|
45
47
|
has(...path: Segment[]): Promise<boolean>;
|
|
46
48
|
has(...args: [...Segment[], StandardSchemaV1]): Promise<boolean>;
|
|
47
49
|
get(...path: Segment[]): Promise<unknown>;
|
|
@@ -67,6 +69,7 @@ export interface Cursor {
|
|
|
67
69
|
withIndex: true;
|
|
68
70
|
}]): AsyncIterable<[IterIndex, unknown][]>;
|
|
69
71
|
iter(...args: [...Segment[], IterOptions]): AsyncIterable<unknown[]>;
|
|
72
|
+
walk(...path: Segment[]): AsyncIterable<WalkEntry>;
|
|
70
73
|
walk(...path: Segment[]): AsyncIterable<Cursor>;
|
|
71
74
|
}
|
|
72
75
|
export interface RootCursor extends Cursor, AsyncDisposable {
|
package/dist/open.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_ITER_BATCH = exports.DEFAULT_SOURCE_CHUNK_BYTES = void 0;
|
|
3
|
+
exports.MAX_ITER_BATCH = exports.DEFAULT_ITER_BATCH = exports.DEFAULT_SOURCE_CHUNK_BYTES = void 0;
|
|
4
4
|
exports.open = open;
|
|
5
5
|
const native_1 = require("@botejs/native");
|
|
6
6
|
const path_ts_1 = require("./path.js");
|
|
@@ -8,6 +8,7 @@ const validate_ts_1 = require("./validate.js");
|
|
|
8
8
|
const args_ts_1 = require("./args.js");
|
|
9
9
|
exports.DEFAULT_SOURCE_CHUNK_BYTES = 64 * 1024;
|
|
10
10
|
exports.DEFAULT_ITER_BATCH = 1000;
|
|
11
|
+
exports.MAX_ITER_BATCH = 1_000_000;
|
|
11
12
|
/**
|
|
12
13
|
* Open a cursor over a seekable source.
|
|
13
14
|
*
|
|
@@ -21,7 +22,7 @@ async function open(source, options) {
|
|
|
21
22
|
['objectMemberCap', objectMemberCap],
|
|
22
23
|
['arrayIndexInterval', arrayIndexInterval],
|
|
23
24
|
]) {
|
|
24
|
-
if (value !== undefined && (!Number.
|
|
25
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
25
26
|
throw new RangeError(`open: ${name} must be a non-negative integer (0 disables), got ${value}`);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -29,6 +30,15 @@ async function open(source, options) {
|
|
|
29
30
|
const chunkBytes = reader.chunkBytes ?? exports.DEFAULT_SOURCE_CHUNK_BYTES;
|
|
30
31
|
let native;
|
|
31
32
|
try {
|
|
33
|
+
if (!Number.isInteger(reader.size) || reader.size < 0) {
|
|
34
|
+
throw new RangeError(`open: source size must be a non-negative integer, got ${reader.size}`);
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isSafeInteger(chunkBytes) || chunkBytes <= 0) {
|
|
37
|
+
throw new RangeError(`open: chunkBytes must be a positive integer, got ${chunkBytes}`);
|
|
38
|
+
}
|
|
39
|
+
if (chunkBytes % 64 !== 0) {
|
|
40
|
+
throw new RangeError(`open: chunkBytes must be a multiple of 64, got ${chunkBytes}`);
|
|
41
|
+
}
|
|
32
42
|
native = (0, native_1.open)({
|
|
33
43
|
size: reader.size,
|
|
34
44
|
chunkBytes,
|
|
@@ -39,17 +49,24 @@ async function open(source, options) {
|
|
|
39
49
|
});
|
|
40
50
|
}
|
|
41
51
|
catch (err) {
|
|
42
|
-
|
|
52
|
+
// Don't let a failing cleanup mask the original open error; attach it as cause.
|
|
53
|
+
try {
|
|
54
|
+
await closeReader(reader);
|
|
55
|
+
}
|
|
56
|
+
catch (closeErr) {
|
|
57
|
+
if (err instanceof Error)
|
|
58
|
+
err.cause ??= closeErr;
|
|
59
|
+
}
|
|
43
60
|
throw err;
|
|
44
61
|
}
|
|
45
|
-
|
|
62
|
+
const state = { closed: false };
|
|
46
63
|
const close = async () => {
|
|
47
|
-
if (closed)
|
|
64
|
+
if (state.closed)
|
|
48
65
|
return;
|
|
49
|
-
closed = true;
|
|
66
|
+
state.closed = true;
|
|
50
67
|
await closeReader(reader);
|
|
51
68
|
};
|
|
52
|
-
return Object.assign(wrap(native), {
|
|
69
|
+
return Object.assign(wrap(native, state), {
|
|
53
70
|
close,
|
|
54
71
|
[Symbol.asyncDispose]: close,
|
|
55
72
|
});
|
|
@@ -58,13 +75,44 @@ async function closeReader(reader) {
|
|
|
58
75
|
if (reader.close)
|
|
59
76
|
await reader.close();
|
|
60
77
|
}
|
|
61
|
-
|
|
78
|
+
/** Sentinel the native layer prefixes onto shape-contradiction errors (see
|
|
79
|
+
* `session.rs` `SessionError::Path`). */
|
|
80
|
+
const NATIVE_PATH_ERROR_PREFIX = 'bote.PathError: ';
|
|
81
|
+
/** Rethrow a native shape-contradiction error as a `PathError` carrying the
|
|
82
|
+
* caller's path; pass anything else through unchanged. */
|
|
83
|
+
function asPathError(err, path) {
|
|
84
|
+
if (err instanceof Error && !(err instanceof validate_ts_1.PathError) && err.message.startsWith(NATIVE_PATH_ERROR_PREFIX)) {
|
|
85
|
+
return new validate_ts_1.PathError(err.message.slice(NATIVE_PATH_ERROR_PREFIX.length), path);
|
|
86
|
+
}
|
|
87
|
+
return err;
|
|
88
|
+
}
|
|
89
|
+
/** Throw a uniform error for any operation on a closed cursor, so use-after-close
|
|
90
|
+
* is one defined contract regardless of source (some readers' reads keep working
|
|
91
|
+
* after close, others throw an opaque I/O error). */
|
|
92
|
+
function ensureOpen(state) {
|
|
93
|
+
if (state.closed)
|
|
94
|
+
throw new Error('bote: cursor is closed');
|
|
95
|
+
}
|
|
96
|
+
function wrap(native, state) {
|
|
62
97
|
const cursor = {
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
async hop(...path) {
|
|
99
|
+
ensureOpen(state);
|
|
100
|
+
(0, path_ts_1.validatePath)(path);
|
|
101
|
+
let child;
|
|
102
|
+
try {
|
|
103
|
+
child = await native.hop(path);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
throw asPathError(err, path);
|
|
107
|
+
}
|
|
108
|
+
return child ? wrap(child, state) : null;
|
|
65
109
|
},
|
|
66
110
|
async has(...args) {
|
|
111
|
+
ensureOpen(state);
|
|
67
112
|
const { path, tail: schema } = (0, args_ts_1.splitArgs)(args);
|
|
113
|
+
if (schema !== undefined && !(0, args_ts_1.isSchema)(schema)) {
|
|
114
|
+
throw new TypeError('has: expected a Standard Schema as the trailing argument');
|
|
115
|
+
}
|
|
68
116
|
if (!schema)
|
|
69
117
|
return native.has(path);
|
|
70
118
|
if (!(await native.has(path)))
|
|
@@ -73,51 +121,96 @@ function wrap(native) {
|
|
|
73
121
|
return !('skip' in result);
|
|
74
122
|
},
|
|
75
123
|
async get(...args) {
|
|
124
|
+
ensureOpen(state);
|
|
76
125
|
const { path, tail: schema } = (0, args_ts_1.splitArgs)(args);
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
if (schema !== undefined && !(0, args_ts_1.isSchema)(schema)) {
|
|
127
|
+
throw new TypeError('get: expected a Standard Schema as the trailing argument');
|
|
128
|
+
}
|
|
129
|
+
let value;
|
|
130
|
+
try {
|
|
131
|
+
value = await native.get(path);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
throw asPathError(err, path);
|
|
135
|
+
}
|
|
136
|
+
if (!schema)
|
|
79
137
|
return value;
|
|
80
138
|
return (0, validate_ts_1.runStandardSchema)(schema, value, path);
|
|
81
139
|
},
|
|
82
|
-
count(...path) {
|
|
140
|
+
async count(...path) {
|
|
141
|
+
ensureOpen(state);
|
|
83
142
|
(0, path_ts_1.validatePath)(path);
|
|
84
|
-
|
|
143
|
+
try {
|
|
144
|
+
return await native.count(path);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
throw asPathError(err, path);
|
|
148
|
+
}
|
|
85
149
|
},
|
|
86
150
|
iter(...args) {
|
|
151
|
+
ensureOpen(state);
|
|
87
152
|
const { path, tail } = (0, args_ts_1.splitArgs)(args);
|
|
88
153
|
const { schema, select, batch, onInvalid, withIndex } = (0, args_ts_1.normalizeIterTail)(tail);
|
|
89
|
-
if (batch !== undefined && (!Number.isInteger(batch) || batch <= 0)) {
|
|
90
|
-
throw new RangeError(`iter: batch must be
|
|
154
|
+
if (batch !== undefined && (!Number.isInteger(batch) || batch <= 0 || batch > exports.MAX_ITER_BATCH)) {
|
|
155
|
+
throw new RangeError(`iter: batch must be an integer in 1..=${exports.MAX_ITER_BATCH}, got ${batch}`);
|
|
156
|
+
}
|
|
157
|
+
if (withIndex !== undefined && typeof withIndex !== 'boolean') {
|
|
158
|
+
throw new TypeError(`iter: withIndex must be a boolean, got ${typeof withIndex}`);
|
|
159
|
+
}
|
|
160
|
+
if (onInvalid !== undefined && onInvalid !== 'throw' && onInvalid !== 'skip') {
|
|
161
|
+
throw new RangeError(`iter: onInvalid must be "throw" or "skip", got ${JSON.stringify(onInvalid)}`);
|
|
91
162
|
}
|
|
92
163
|
const resolvedBatch = batch ?? exports.DEFAULT_ITER_BATCH;
|
|
93
164
|
const selectIr = select !== undefined ? (0, args_ts_1.serializeSelect)(select) : undefined;
|
|
94
165
|
const inner = native.iter(path, { selectIr, batch: resolvedBatch, withKey: withIndex });
|
|
95
|
-
if (!schema)
|
|
96
|
-
return
|
|
166
|
+
if (!schema) {
|
|
167
|
+
return {
|
|
168
|
+
async *[Symbol.asyncIterator]() {
|
|
169
|
+
try {
|
|
170
|
+
for await (const b of inner)
|
|
171
|
+
yield b;
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
throw asPathError(err, path);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
97
179
|
const policy = onInvalid ?? 'throw';
|
|
98
180
|
return {
|
|
99
181
|
async *[Symbol.asyncIterator]() {
|
|
100
182
|
let i = 0;
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
183
|
+
try {
|
|
184
|
+
for await (const b of inner) {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const v of b) {
|
|
187
|
+
const value = withIndex ? v[1] : v;
|
|
188
|
+
const result = await (0, validate_ts_1.validateItem)(schema, value, [...path, i++], policy);
|
|
189
|
+
if ('skip' in result)
|
|
190
|
+
continue;
|
|
191
|
+
out.push(withIndex ? [v[0], result.value] : result.value);
|
|
192
|
+
}
|
|
193
|
+
yield out;
|
|
109
194
|
}
|
|
110
|
-
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
throw asPathError(err, path);
|
|
111
198
|
}
|
|
112
199
|
},
|
|
113
200
|
};
|
|
114
201
|
},
|
|
115
202
|
walk(...path) {
|
|
203
|
+
ensureOpen(state);
|
|
116
204
|
(0, path_ts_1.validatePath)(path);
|
|
117
205
|
return {
|
|
118
206
|
async *[Symbol.asyncIterator]() {
|
|
119
|
-
|
|
120
|
-
|
|
207
|
+
try {
|
|
208
|
+
for await (const [key, child] of native.walk(path)) {
|
|
209
|
+
yield [key, wrap(child, state)];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
throw asPathError(err, path);
|
|
121
214
|
}
|
|
122
215
|
},
|
|
123
216
|
};
|
package/dist/validate.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export declare class ValidationError extends Error {
|
|
|
7
7
|
readonly path: Path;
|
|
8
8
|
constructor(issues: readonly StandardSchemaV1.Issue[], path: Path);
|
|
9
9
|
}
|
|
10
|
+
export declare class PathError extends Error {
|
|
11
|
+
readonly path: Path;
|
|
12
|
+
constructor(reason: string, path: Path);
|
|
13
|
+
}
|
|
10
14
|
export declare function runStandardSchema<O>(schema: StandardSchemaV1<unknown, O>, value: unknown, path: Path): Promise<O>;
|
|
11
15
|
export declare function validateItem<O>(schema: StandardSchemaV1<unknown, O>, value: unknown, path: Path, onInvalid: 'throw' | 'skip'): Promise<{
|
|
12
16
|
skip: true;
|
package/dist/validate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ValidationError = void 0;
|
|
3
|
+
exports.PathError = exports.ValidationError = void 0;
|
|
4
4
|
exports.runStandardSchema = runStandardSchema;
|
|
5
5
|
exports.validateItem = validateItem;
|
|
6
6
|
exports.formatPath = formatPath;
|
|
@@ -15,6 +15,15 @@ class ValidationError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
exports.ValidationError = ValidationError;
|
|
18
|
+
class PathError extends Error {
|
|
19
|
+
path;
|
|
20
|
+
constructor(reason, path) {
|
|
21
|
+
super(`bote: cannot resolve ${formatPath(path)}: ${reason}`);
|
|
22
|
+
this.name = 'PathError';
|
|
23
|
+
this.path = path;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.PathError = PathError;
|
|
18
27
|
async function runStandardSchema(schema, value, path) {
|
|
19
28
|
const result = await schema['~standard'].validate(value);
|
|
20
29
|
if (result.issues)
|