@texuli/spec 0.5.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 +37 -10
- package/dist/generated/typescript/blueprint.d.ts +44 -3
- package/dist/generated/typescript/blueprint.js +1 -1
- package/dist/generated/typescript/catalog-schema.js +1 -1
- package/dist/generated/typescript/diagnostics.js +1 -1
- package/dist/generated/typescript/manifest.js +1 -1
- package/dist/generated/typescript/vocabulary.js +1 -1
- package/dist/generated/typescript/workflow.js +1 -1
- package/dist/migrations/0.5.0-0.6.0.d.ts +9 -0
- package/dist/migrations/0.5.0-0.6.0.js +12 -0
- package/dist/migrations/index.js +2 -0
- package/dist/motion.d.ts +40 -0
- package/dist/motion.js +133 -0
- package/dist/stream/assembler.d.ts +101 -0
- package/dist/stream/assembler.js +284 -0
- package/dist/stream/index.d.ts +11 -0
- package/dist/stream/index.js +11 -0
- package/dist/stream/source.d.ts +22 -0
- package/dist/stream/source.js +72 -0
- package/dist/stream/tokenizer.d.ts +67 -0
- package/dist/stream/tokenizer.js +383 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { createJsonTokenizer, } from "./tokenizer.js";
|
|
2
|
+
/**
|
|
3
|
+
* The partial-blueprint assembler (ADR-014): builds the value the tokenizer
|
|
4
|
+
* reads and reports, for every catalog node, when its **shell** is final.
|
|
5
|
+
* Streamed nodes put `children` last (`key`, `id`, `slot`, `props`, `when`,
|
|
6
|
+
* `bind`, `on`, `children`), so a node's shell (every member but `children`)
|
|
7
|
+
* is final once `children` starts, or once the node closes without it.
|
|
8
|
+
* Envelopes put `state` and `flow` before the tree, so the envelope header
|
|
9
|
+
* is final once `tree` starts. A member after `children` (or after `tree`)
|
|
10
|
+
* is a `stream-order` issue: the node it lands in is held until the final
|
|
11
|
+
* verdict. The assembler judges no rule itself; the catalog checks each
|
|
12
|
+
* shell and `validateDraft` judges the whole value.
|
|
13
|
+
*/
|
|
14
|
+
/** Members a streamed node may have, in the canonical order (`children` last). */
|
|
15
|
+
export const STREAM_NODE_ORDER = Object.freeze([
|
|
16
|
+
'key',
|
|
17
|
+
'id',
|
|
18
|
+
'slot',
|
|
19
|
+
'props',
|
|
20
|
+
'when',
|
|
21
|
+
'bind',
|
|
22
|
+
'on',
|
|
23
|
+
'motion',
|
|
24
|
+
'children',
|
|
25
|
+
]);
|
|
26
|
+
/** Node path of child `index` under `parent` (`null` for a root). */
|
|
27
|
+
function childPath(parent, index) {
|
|
28
|
+
return parent === null ? String(index) : `${parent}.children.${index}`;
|
|
29
|
+
}
|
|
30
|
+
export function createBlueprintAssembler(options = {}) {
|
|
31
|
+
const tokenizer = createJsonTokenizer(options);
|
|
32
|
+
const treeMembers = new Set(options.treeMembers ?? ['tree']);
|
|
33
|
+
const stack = [];
|
|
34
|
+
let root;
|
|
35
|
+
let rootSet = false;
|
|
36
|
+
let error = null;
|
|
37
|
+
let finished = false;
|
|
38
|
+
// `true` when the root object turned out to be an envelope or wrapper.
|
|
39
|
+
let rootIsEnvelope = false;
|
|
40
|
+
const header = (frame) => {
|
|
41
|
+
const out = {};
|
|
42
|
+
for (const [key, value] of Object.entries(frame.container)) {
|
|
43
|
+
if (!treeMembers.has(key))
|
|
44
|
+
out[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
const shellOf = (frame) => {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const [key, value] of Object.entries(frame.container)) {
|
|
51
|
+
if (key !== 'children')
|
|
52
|
+
out[key] = value;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
};
|
|
56
|
+
const nodeEvent = (frame, parent, index, hasChildren, offset) => {
|
|
57
|
+
const shell = shellOf(frame);
|
|
58
|
+
const key = shell.key;
|
|
59
|
+
return {
|
|
60
|
+
type: 'node',
|
|
61
|
+
path: frame.path,
|
|
62
|
+
streamId: typeof key === 'string' && key !== '' ? key : frame.path,
|
|
63
|
+
parent,
|
|
64
|
+
index,
|
|
65
|
+
shell,
|
|
66
|
+
hasChildren,
|
|
67
|
+
inArray: frame.inArray,
|
|
68
|
+
offset,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
/** Where a node sits: its parent path and index, from its path. */
|
|
72
|
+
const position = (path) => {
|
|
73
|
+
const at = path.lastIndexOf('.children.');
|
|
74
|
+
if (at === -1)
|
|
75
|
+
return { parent: null, index: Number(path) };
|
|
76
|
+
return { parent: path.slice(0, at), index: Number(path.slice(at + '.children.'.length)) };
|
|
77
|
+
};
|
|
78
|
+
/** Attach a closed value to the container on top of the stack (or make it the root). */
|
|
79
|
+
function attach(value, out, offset) {
|
|
80
|
+
const top = stack[stack.length - 1];
|
|
81
|
+
if (!top) {
|
|
82
|
+
root = value;
|
|
83
|
+
rootSet = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(top.container)) {
|
|
87
|
+
top.container.push(value);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const key = top.key;
|
|
91
|
+
top.container[key] = value;
|
|
92
|
+
top.key = null;
|
|
93
|
+
if (top.role === 'node' && key === 'id' && !top.final) {
|
|
94
|
+
out.push({ type: 'node-id', path: top.path, id: value, offset });
|
|
95
|
+
}
|
|
96
|
+
else if (top.role === 'root' && rootIsEnvelope && top.final && !treeMembers.has(key)) {
|
|
97
|
+
// A late envelope member (after the tree) changes the header the shells were checked with.
|
|
98
|
+
out.push({ type: 'header', header: header(top), offset });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The role and path a new container gets from where it opens. */
|
|
102
|
+
function open(kind, out, offset) {
|
|
103
|
+
const container = kind === 'object' ? {} : [];
|
|
104
|
+
const top = stack[stack.length - 1];
|
|
105
|
+
const frame = {
|
|
106
|
+
role: 'value',
|
|
107
|
+
container,
|
|
108
|
+
key: null,
|
|
109
|
+
path: null,
|
|
110
|
+
final: false,
|
|
111
|
+
inArray: false,
|
|
112
|
+
};
|
|
113
|
+
if (!top) {
|
|
114
|
+
// The top level: an object is an envelope, a wrapper or a root node; an array holds roots.
|
|
115
|
+
if (kind === 'object') {
|
|
116
|
+
frame.role = 'root';
|
|
117
|
+
frame.path = '0';
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
frame.role = 'children';
|
|
121
|
+
frame.path = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (top.role === 'children' && kind === 'object') {
|
|
125
|
+
const index = top.container.length;
|
|
126
|
+
frame.role = 'node';
|
|
127
|
+
frame.path = childPath(top.path, index);
|
|
128
|
+
frame.inArray = true;
|
|
129
|
+
out.push({
|
|
130
|
+
type: 'node-start',
|
|
131
|
+
path: frame.path,
|
|
132
|
+
parent: top.path,
|
|
133
|
+
index,
|
|
134
|
+
inArray: true,
|
|
135
|
+
offset,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else if (top.role === 'node' && top.key === 'children' && kind === 'array') {
|
|
139
|
+
frame.role = 'children';
|
|
140
|
+
frame.path = top.path;
|
|
141
|
+
}
|
|
142
|
+
else if (top.role === 'root' && top.key !== null && treeMembers.has(top.key)) {
|
|
143
|
+
// The envelope's tree: one root node at `0`, or an array of roots.
|
|
144
|
+
if (kind === 'object') {
|
|
145
|
+
frame.role = 'node';
|
|
146
|
+
frame.path = '0';
|
|
147
|
+
out.push({ type: 'node-start', path: '0', parent: null, index: 0, inArray: false, offset });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
frame.role = 'children';
|
|
151
|
+
frame.path = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (top.role === 'root' &&
|
|
155
|
+
top.key === 'children' &&
|
|
156
|
+
!rootIsEnvelope &&
|
|
157
|
+
kind === 'array') {
|
|
158
|
+
frame.role = 'children';
|
|
159
|
+
frame.path = '0';
|
|
160
|
+
}
|
|
161
|
+
stack.push(frame);
|
|
162
|
+
}
|
|
163
|
+
function onKey(key, out, offset) {
|
|
164
|
+
const top = stack[stack.length - 1];
|
|
165
|
+
top.key = key;
|
|
166
|
+
if (top.role === 'root') {
|
|
167
|
+
if (top.final) {
|
|
168
|
+
out.push({ type: 'stream-order', path: rootIsEnvelope ? '$' : '0', key, offset });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (treeMembers.has(key)) {
|
|
172
|
+
top.final = true;
|
|
173
|
+
rootIsEnvelope = true;
|
|
174
|
+
top.path = null;
|
|
175
|
+
out.push({ type: 'header', header: header(top), offset });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (key === 'children') {
|
|
179
|
+
// The root is a node: say so (late) and report its shell.
|
|
180
|
+
top.final = true;
|
|
181
|
+
out.push({ type: 'node-start', path: '0', parent: null, index: 0, inArray: false, offset });
|
|
182
|
+
out.push(nodeEvent(top, null, 0, true, offset));
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (top.role !== 'node')
|
|
187
|
+
return;
|
|
188
|
+
if (top.final) {
|
|
189
|
+
out.push({ type: 'stream-order', path: top.path, key, offset });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (key === 'children') {
|
|
193
|
+
top.final = true;
|
|
194
|
+
const { parent, index } = position(top.path);
|
|
195
|
+
out.push(nodeEvent(top, parent, index, true, offset));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function onClose(out, offset) {
|
|
199
|
+
const frame = stack.pop();
|
|
200
|
+
if (frame.role === 'node' || (frame.role === 'root' && !rootIsEnvelope)) {
|
|
201
|
+
const path = frame.path;
|
|
202
|
+
if (!frame.final) {
|
|
203
|
+
if (frame.role === 'root') {
|
|
204
|
+
const record = frame.container;
|
|
205
|
+
// A childless top-level object is a node unless it reads as an envelope.
|
|
206
|
+
if (typeof record.format !== 'string') {
|
|
207
|
+
out.push({ type: 'node-start', path, parent: null, index: 0, inArray: false, offset });
|
|
208
|
+
out.push(nodeEvent(frame, null, 0, false, offset));
|
|
209
|
+
out.push({ type: 'node-end', path, offset });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const { parent, index } = position(path);
|
|
214
|
+
out.push(nodeEvent(frame, parent, index, false, offset));
|
|
215
|
+
out.push({ type: 'node-end', path, offset });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
out.push({ type: 'node-end', path, offset });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
attach(frame.container, out, offset);
|
|
223
|
+
}
|
|
224
|
+
function apply(events, out) {
|
|
225
|
+
for (const event of events) {
|
|
226
|
+
if (event.type === 'open')
|
|
227
|
+
open(event.kind, out, event.offset);
|
|
228
|
+
else if (event.type === 'key')
|
|
229
|
+
onKey(event.key, out, event.offset);
|
|
230
|
+
else if (event.type === 'value')
|
|
231
|
+
attach(event.value, out, event.offset);
|
|
232
|
+
else
|
|
233
|
+
onClose(out, event.offset);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function settle(out) {
|
|
237
|
+
if (error === null && tokenizer.error !== null) {
|
|
238
|
+
error = tokenizer.error;
|
|
239
|
+
out.push({ type: 'error', error });
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
write(chunk) {
|
|
245
|
+
if (error !== null || finished)
|
|
246
|
+
return [];
|
|
247
|
+
const out = [];
|
|
248
|
+
apply(tokenizer.write(chunk), out);
|
|
249
|
+
return settle(out);
|
|
250
|
+
},
|
|
251
|
+
end() {
|
|
252
|
+
if (error !== null || finished)
|
|
253
|
+
return [];
|
|
254
|
+
const out = [];
|
|
255
|
+
apply(tokenizer.end(), out);
|
|
256
|
+
settle(out);
|
|
257
|
+
if (error === null && tokenizer.done && rootSet) {
|
|
258
|
+
finished = true;
|
|
259
|
+
out.push({ type: 'complete', value: root, offset: tokenizer.bytes });
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
},
|
|
263
|
+
abort(code = 'aborted', message) {
|
|
264
|
+
if (error !== null || finished)
|
|
265
|
+
return [];
|
|
266
|
+
error = {
|
|
267
|
+
code,
|
|
268
|
+
offset: tokenizer.bytes,
|
|
269
|
+
message: message ??
|
|
270
|
+
(code === 'aborted' ? 'The stream was aborted.' : 'The stream connection dropped.'),
|
|
271
|
+
};
|
|
272
|
+
return [{ type: 'error', error }];
|
|
273
|
+
},
|
|
274
|
+
get bytes() {
|
|
275
|
+
return tokenizer.bytes;
|
|
276
|
+
},
|
|
277
|
+
get error() {
|
|
278
|
+
return error;
|
|
279
|
+
},
|
|
280
|
+
get value() {
|
|
281
|
+
return rootSet ? root : stack[0]?.container;
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/spec/stream` — streamed blueprints (ADR-014): an incremental JSON
|
|
3
|
+
* tokenizer with a byte budget and a depth cap, the partial-blueprint
|
|
4
|
+
* assembler that reports when each node's shell is final, and a push handle
|
|
5
|
+
* hosts write chunks into. Framework-free: no DOM, no Svelte, no Node APIs.
|
|
6
|
+
* Nothing here validates a node; `@texuli/catalog/validate`
|
|
7
|
+
* (`validateStreamNode`, then `validateDraft`) does.
|
|
8
|
+
*/
|
|
9
|
+
export { type BlueprintAssembler, type BlueprintAssemblerOptions, type BlueprintStreamEvent, createBlueprintAssembler, STREAM_NODE_ORDER, type StreamNodeShell, } from './assembler.js';
|
|
10
|
+
export { BlueprintStreamAbortError, type BlueprintStreamHandle, createBlueprintStream, } from './source.js';
|
|
11
|
+
export { createJsonTokenizer, DEFAULT_STREAM_MAX_BYTES, DEFAULT_STREAM_MAX_DEPTH, type JsonScalar, type JsonStreamEvent, type JsonTokenizer, type JsonTokenizerOptions, STREAM_PROTOTYPE_KEYS, type StreamError, type StreamErrorCode, } from './tokenizer.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@texuli/spec/stream` — streamed blueprints (ADR-014): an incremental JSON
|
|
3
|
+
* tokenizer with a byte budget and a depth cap, the partial-blueprint
|
|
4
|
+
* assembler that reports when each node's shell is final, and a push handle
|
|
5
|
+
* hosts write chunks into. Framework-free: no DOM, no Svelte, no Node APIs.
|
|
6
|
+
* Nothing here validates a node; `@texuli/catalog/validate`
|
|
7
|
+
* (`validateStreamNode`, then `validateDraft`) does.
|
|
8
|
+
*/
|
|
9
|
+
export { createBlueprintAssembler, STREAM_NODE_ORDER, } from "./assembler.js";
|
|
10
|
+
export { BlueprintStreamAbortError, createBlueprintStream, } from "./source.js";
|
|
11
|
+
export { createJsonTokenizer, DEFAULT_STREAM_MAX_BYTES, DEFAULT_STREAM_MAX_DEPTH, STREAM_PROTOTYPE_KEYS, } from "./tokenizer.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A push handle for a blueprint stream (ADR-014): the host writes text as it
|
|
3
|
+
* arrives (an agent's tool deltas, server-sent events) and a reader such as
|
|
4
|
+
* `<Player stream={handle}>` iterates it. One reader per handle. `abort()`
|
|
5
|
+
* makes the reader's iteration throw a {@link BlueprintStreamAbortError};
|
|
6
|
+
* chunks written before it are still read first.
|
|
7
|
+
*/
|
|
8
|
+
export interface BlueprintStreamHandle extends AsyncIterable<string> {
|
|
9
|
+
/** Queue the next chunk. Ignored after `end()` or `abort()`. */
|
|
10
|
+
write(text: string): void;
|
|
11
|
+
/** The stream is complete: the reader finishes after the queued chunks. */
|
|
12
|
+
end(): void;
|
|
13
|
+
/** The stream stopped early: the reader throws after the queued chunks. */
|
|
14
|
+
abort(reason?: string): void;
|
|
15
|
+
/** `true` after `end()` or `abort()`. */
|
|
16
|
+
readonly closed: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** What a reader of an aborted {@link BlueprintStreamHandle} throws. */
|
|
19
|
+
export declare class BlueprintStreamAbortError extends Error {
|
|
20
|
+
readonly name = "AbortError";
|
|
21
|
+
}
|
|
22
|
+
export declare function createBlueprintStream(): BlueprintStreamHandle;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A push handle for a blueprint stream (ADR-014): the host writes text as it
|
|
3
|
+
* arrives (an agent's tool deltas, server-sent events) and a reader such as
|
|
4
|
+
* `<Player stream={handle}>` iterates it. One reader per handle. `abort()`
|
|
5
|
+
* makes the reader's iteration throw a {@link BlueprintStreamAbortError};
|
|
6
|
+
* chunks written before it are still read first.
|
|
7
|
+
*/
|
|
8
|
+
/** What a reader of an aborted {@link BlueprintStreamHandle} throws. */
|
|
9
|
+
export class BlueprintStreamAbortError extends Error {
|
|
10
|
+
name = 'AbortError';
|
|
11
|
+
}
|
|
12
|
+
export function createBlueprintStream() {
|
|
13
|
+
const queue = [];
|
|
14
|
+
let state = 'open';
|
|
15
|
+
let reason = 'The stream was aborted.';
|
|
16
|
+
let wake = null;
|
|
17
|
+
const notify = () => {
|
|
18
|
+
const resume = wake;
|
|
19
|
+
wake = null;
|
|
20
|
+
resume?.();
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
write(text) {
|
|
24
|
+
if (state !== 'open' || typeof text !== 'string' || text === '')
|
|
25
|
+
return;
|
|
26
|
+
queue.push(text);
|
|
27
|
+
notify();
|
|
28
|
+
},
|
|
29
|
+
end() {
|
|
30
|
+
if (state !== 'open')
|
|
31
|
+
return;
|
|
32
|
+
state = 'ended';
|
|
33
|
+
notify();
|
|
34
|
+
},
|
|
35
|
+
abort(message) {
|
|
36
|
+
if (state !== 'open')
|
|
37
|
+
return;
|
|
38
|
+
state = 'aborted';
|
|
39
|
+
if (typeof message === 'string' && message !== '')
|
|
40
|
+
reason = message;
|
|
41
|
+
notify();
|
|
42
|
+
},
|
|
43
|
+
get closed() {
|
|
44
|
+
return state !== 'open';
|
|
45
|
+
},
|
|
46
|
+
[Symbol.asyncIterator]() {
|
|
47
|
+
return {
|
|
48
|
+
async next() {
|
|
49
|
+
while (queue.length === 0 && state === 'open') {
|
|
50
|
+
await new Promise((resolve) => {
|
|
51
|
+
wake = resolve;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const chunk = queue.shift();
|
|
55
|
+
if (chunk !== undefined)
|
|
56
|
+
return { value: chunk, done: false };
|
|
57
|
+
if (state === 'aborted')
|
|
58
|
+
throw new BlueprintStreamAbortError(reason);
|
|
59
|
+
return { value: undefined, done: true };
|
|
60
|
+
},
|
|
61
|
+
async return() {
|
|
62
|
+
// The reader stopped listening: drop what is queued.
|
|
63
|
+
queue.length = 0;
|
|
64
|
+
if (state === 'open')
|
|
65
|
+
state = 'ended';
|
|
66
|
+
notify();
|
|
67
|
+
return { value: undefined, done: true };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An incremental JSON tokenizer for untrusted streams (ADR-014). Text
|
|
3
|
+
* arrives in chunks split anywhere (inside a string, an escape, a number, a
|
|
4
|
+
* literal or a UTF-16 surrogate pair); the tokenizer reports each value as
|
|
5
|
+
* it closes. One pass over every code unit, no backtracking and no
|
|
6
|
+
* recursion, so the time is linear in the input and the memory is bounded
|
|
7
|
+
* by the byte budget and the depth cap. Framework-free: no DOM, no Node.
|
|
8
|
+
*
|
|
9
|
+
* It refuses, at a UTF-8 byte offset: malformed JSON, input over the byte
|
|
10
|
+
* budget, containers nested past the depth cap, duplicate keys and the
|
|
11
|
+
* prototype keys (`__proto__`, `constructor`, `prototype`). After an error
|
|
12
|
+
* it reports nothing more.
|
|
13
|
+
*/
|
|
14
|
+
/** Why a stream stopped. Tokenizer codes, plus the codes a stream reader adds. */
|
|
15
|
+
export type StreamErrorCode = 'bad-json' | 'too-large' | 'too-deep' | 'duplicate-key' | 'prototype-key' | 'truncated' | 'aborted' | 'connection';
|
|
16
|
+
export interface StreamError {
|
|
17
|
+
code: StreamErrorCode;
|
|
18
|
+
/** UTF-8 byte offset of the refused input (the bytes read so far for a reader error). */
|
|
19
|
+
offset: number;
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
export type JsonScalar = string | number | boolean | null;
|
|
23
|
+
/** One closed token. `offset` is the UTF-8 byte offset where it starts. */
|
|
24
|
+
export type JsonStreamEvent = {
|
|
25
|
+
type: 'open';
|
|
26
|
+
kind: 'object' | 'array';
|
|
27
|
+
offset: number;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'key';
|
|
30
|
+
key: string;
|
|
31
|
+
offset: number;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'value';
|
|
34
|
+
value: JsonScalar;
|
|
35
|
+
offset: number;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'close';
|
|
38
|
+
kind: 'object' | 'array';
|
|
39
|
+
offset: number;
|
|
40
|
+
};
|
|
41
|
+
export interface JsonTokenizerOptions {
|
|
42
|
+
/** UTF-8 bytes the whole stream may take. Default {@link DEFAULT_STREAM_MAX_BYTES}. */
|
|
43
|
+
maxBytes?: number;
|
|
44
|
+
/** Containers that may nest. Default {@link DEFAULT_STREAM_MAX_DEPTH}. */
|
|
45
|
+
maxDepth?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface JsonTokenizer {
|
|
48
|
+
/** Feed the next chunk; returns the tokens that closed inside it. */
|
|
49
|
+
write(chunk: string): JsonStreamEvent[];
|
|
50
|
+
/** No more input: closes a trailing top-level number or literal, or reports `truncated`. */
|
|
51
|
+
end(): JsonStreamEvent[];
|
|
52
|
+
/** UTF-8 bytes read so far. */
|
|
53
|
+
readonly bytes: number;
|
|
54
|
+
/** Open containers right now. */
|
|
55
|
+
readonly depth: number;
|
|
56
|
+
/** The first error, or `null`. */
|
|
57
|
+
readonly error: StreamError | null;
|
|
58
|
+
/** `true` once `end()` accepted one complete JSON value. */
|
|
59
|
+
readonly done: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Default byte budget: the catalog's default tree cap (256 KiB) plus 8 KiB of whitespace. */
|
|
62
|
+
export declare const DEFAULT_STREAM_MAX_BYTES: number;
|
|
63
|
+
/** Default depth cap: the catalog's value-depth guard. */
|
|
64
|
+
export declare const DEFAULT_STREAM_MAX_DEPTH = 128;
|
|
65
|
+
/** Keys refused anywhere in a stream. */
|
|
66
|
+
export declare const STREAM_PROTOTYPE_KEYS: readonly string[];
|
|
67
|
+
export declare function createJsonTokenizer(options?: JsonTokenizerOptions): JsonTokenizer;
|