@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 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
- import * as z from 'zod' // or bring your own Standard Schema validator
13
+ // e.g. { items: [...] }
14
+ await using cursor = await open(fromFile('./some-large.json'))
13
15
 
14
- const User = z.object({
15
- id: z.string(),
16
- name: z.string(),
17
- email: z.string(),
18
- details: z.object({
19
- lastLoggedIn: z.number(),
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
- type User = z.infer<typeof User>
34
+ ## array access
24
35
 
25
- await using cursor = await open(fromFile('./your-big.json'))
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
- // users[1000].name
28
- const desc0: unknown = await cursor.get('users', 1000, 'name')
29
- // for .get and .iter, you can supply a validator as the last argument
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
- // iterate an array in batches
33
- for await (const batch of cursor.iter('users', User)) {
34
- // batch: User[]
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
- // pick several fields into a named object to avoid resolving big items
41
- for await (const batch of cursor.iter('users', {
42
- select: {
43
- id: 'id',
44
- logged: ['details', 'lastLoggedIn'],
45
- },
46
- schema: z.object({
47
- id: User.shape.id,
48
- logged: User.shape.details.lastLoggedIn,
49
- }),
50
- })) {
51
- // batch: { id: string, logged: number }[]
52
- for (const userLog of batch) {
53
- console.log(userLog)
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
- // or pick a single field
58
- for await (const batch of cursor.iter('users', {
59
- select: 'name',
60
- schema: User.shape.name,
61
- })) {
62
- // batch: string[]
63
- for (const name of batch) {
64
- console.log({ name })
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
- // for open-ended per-child work (e.g. conditional reads, recursive descent, nested
69
- // iters), `walk` yields a subcursor positioned at each child:
70
- for await (const metaCursor of cursor.walk('meta')) {
71
- if (metaCursor.key === 'details') {
72
- const detailsValue = await metaCursor.get()
73
- console.log(detailsValue)
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
- given a **seekable** source (e.g. a file, an HTTP range) and a path, it can retrieve values in a JSON quickly, without loading the whole thing in-memory.
117
+ ## memory
84
118
 
85
- here's a run (Apple M1 Pro 2021, ~500MB JSON array file, cold-cache, default settings):
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
- | operation | approach | time | js heap peak Δ | rust heap peak |
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 currently only has `fromFile` and `fromHttpRange` as pre-built sources. create your own by implementing the `Source` interface. see [./packages/core/src/sources.ts](./packages/core/src/sources.ts) on how it works.
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
- /** Object-member key or array-element index that this cursor was yielded under by `walk`. `null` on the root cursor. */
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.isInteger(value) || value < 0)) {
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
- await closeReader(reader);
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
- let closed = false;
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
- function wrap(native) {
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
- get key() {
64
- return native.key;
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
- const value = await native.get(path);
78
- if (!schema || value === undefined)
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
- return native.count(path);
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 a positive integer, got ${batch}`);
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 inner;
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
- for await (const b of inner) {
102
- const out = [];
103
- for (const v of b) {
104
- const value = withIndex ? v[1] : v;
105
- const result = await (0, validate_ts_1.validateItem)(schema, value, [...path, i++], policy);
106
- if ('skip' in result)
107
- continue;
108
- out.push(withIndex ? [v[0], result.value] : result.value);
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
- yield out;
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
- for await (const child of native.walk(path)) {
120
- yield wrap(child);
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
  };
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botejs/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",