@tabnas/transduce 0.1.0 → 0.1.2
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 +14 -0
- package/dist/transduce.d.ts +1 -1
- package/dist/transduce.js +1 -1
- package/package.json +3 -1
- package/src/capability.ts +52 -0
- package/src/datum.ts +407 -0
- package/src/error.ts +198 -0
- package/src/event.ts +134 -0
- package/src/guard.ts +112 -0
- package/src/json.ts +82 -0
- package/src/limits.ts +165 -0
- package/src/lines.ts +1247 -0
- package/src/matcher.ts +287 -0
- package/src/parser-source.ts +225 -0
- package/src/route.ts +275 -0
- package/src/rule-events.ts +661 -0
- package/src/scan.ts +83 -0
- package/src/selector.ts +191 -0
- package/src/sink.ts +199 -0
- package/src/source.ts +135 -0
- package/src/table-from-json.ts +296 -0
- package/src/table.ts +213 -0
- package/src/transduce.ts +72 -0
- package/src/tsconfig.json +16 -0
- package/src/value.ts +93 -0
package/src/matcher.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// Shared-prefix matching of many selectors in one pass.
|
|
4
|
+
//
|
|
5
|
+
// The router has to recognize every capture's selector over one event
|
|
6
|
+
// stream, and a document can be hundreds of megabytes, so the matcher
|
|
7
|
+
// never spells out a path per event: the selectors are compiled into one
|
|
8
|
+
// trie of steps, and the matcher walks the document with a stack of open
|
|
9
|
+
// containers (a level per container: its kind, how many values it has
|
|
10
|
+
// started, the current member's key) and, per level, the trie nodes that
|
|
11
|
+
// named that container, kept on one flat stack.
|
|
12
|
+
//
|
|
13
|
+
// The matcher also validates the protocol as it goes, because it holds the
|
|
14
|
+
// state that makes a malformed stream visible (a key outside an object, an
|
|
15
|
+
// array ending an object, a value after the root). Every stage downstream
|
|
16
|
+
// of it can therefore trust the sequence. A concrete `Path` is built only
|
|
17
|
+
// on request, for a delivered match or a failure.
|
|
18
|
+
|
|
19
|
+
import { Fail } from './error'
|
|
20
|
+
import { JsonEvent } from './event'
|
|
21
|
+
import { Path, Segment, Selector } from './selector'
|
|
22
|
+
|
|
23
|
+
// Which selector matched: its position in the list given to the matcher.
|
|
24
|
+
export type CaptureId = number
|
|
25
|
+
|
|
26
|
+
const ROOT = 0
|
|
27
|
+
|
|
28
|
+
// One trie node: the selectors' next steps below one position.
|
|
29
|
+
type Node = {
|
|
30
|
+
properties: Map<string, number>
|
|
31
|
+
indexes: Map<number, number>
|
|
32
|
+
eachIndex: number
|
|
33
|
+
eachMember: number
|
|
34
|
+
// The selectors that end here, in id order.
|
|
35
|
+
terminals: CaptureId[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function newNode(): Node {
|
|
39
|
+
return {
|
|
40
|
+
properties: new Map(),
|
|
41
|
+
indexes: new Map(),
|
|
42
|
+
eachIndex: -1,
|
|
43
|
+
eachMember: -1,
|
|
44
|
+
terminals: [],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// One open container.
|
|
49
|
+
type Level = {
|
|
50
|
+
array: boolean
|
|
51
|
+
// Values started in this container so far; the current or last one is
|
|
52
|
+
// at index `started - 1`.
|
|
53
|
+
started: number
|
|
54
|
+
// The current member's key, for objects.
|
|
55
|
+
key: string
|
|
56
|
+
// Between a key and its value.
|
|
57
|
+
inValue: boolean
|
|
58
|
+
// The trie nodes that named this container: `nodeStack[start..end]`.
|
|
59
|
+
start: number
|
|
60
|
+
end: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// What one event did to the document's structure: a member name (`key`),
|
|
64
|
+
// a container began (`start`), a whole scalar (`scalar`), a container
|
|
65
|
+
// completed (`close`), the document completed (`end`).
|
|
66
|
+
export type HitKind = 'key' | 'start' | 'scalar' | 'close' | 'end'
|
|
67
|
+
|
|
68
|
+
// The matcher's answer for one event.
|
|
69
|
+
export type Hit = {
|
|
70
|
+
kind: HitKind
|
|
71
|
+
// How many captures begin with this event: their ids are
|
|
72
|
+
// `Matcher.begins()`, valid until the next event.
|
|
73
|
+
begins: number
|
|
74
|
+
// For `start` and `scalar`, the number of containers enclosing the
|
|
75
|
+
// value; for `close`, the number enclosing the container that closed.
|
|
76
|
+
depth: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Recognizes every selector of a set over one event stream.
|
|
80
|
+
export class Matcher {
|
|
81
|
+
private nodes: Node[] = [newNode()]
|
|
82
|
+
private levels: Level[] = []
|
|
83
|
+
private open = 0
|
|
84
|
+
private nodeStack: number[] = []
|
|
85
|
+
private begun: CaptureId[] = []
|
|
86
|
+
private rootDone = false
|
|
87
|
+
private isEnded = false
|
|
88
|
+
|
|
89
|
+
// Compile the selectors; their positions are the capture ids.
|
|
90
|
+
constructor(selectors: readonly Selector[]) {
|
|
91
|
+
selectors.forEach((selector, id) => {
|
|
92
|
+
let at = ROOT
|
|
93
|
+
for (const step of selector.steps) {
|
|
94
|
+
const node = this.nodes[at]
|
|
95
|
+
let next: number | undefined
|
|
96
|
+
switch (step.type) {
|
|
97
|
+
case 'property':
|
|
98
|
+
next = node.properties.get(step.name)
|
|
99
|
+
break
|
|
100
|
+
case 'index':
|
|
101
|
+
next = node.indexes.get(step.index)
|
|
102
|
+
break
|
|
103
|
+
case 'each_index':
|
|
104
|
+
next = node.eachIndex < 0 ? undefined : node.eachIndex
|
|
105
|
+
break
|
|
106
|
+
case 'each_member':
|
|
107
|
+
next = node.eachMember < 0 ? undefined : node.eachMember
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
if (undefined === next) {
|
|
111
|
+
next = this.nodes.length
|
|
112
|
+
this.nodes.push(newNode())
|
|
113
|
+
switch (step.type) {
|
|
114
|
+
case 'property':
|
|
115
|
+
node.properties.set(step.name, next)
|
|
116
|
+
break
|
|
117
|
+
case 'index':
|
|
118
|
+
node.indexes.set(step.index, next)
|
|
119
|
+
break
|
|
120
|
+
case 'each_index':
|
|
121
|
+
node.eachIndex = next
|
|
122
|
+
break
|
|
123
|
+
case 'each_member':
|
|
124
|
+
node.eachMember = next
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
at = next
|
|
129
|
+
}
|
|
130
|
+
this.nodes[at].terminals.push(id)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Open containers right now.
|
|
135
|
+
depth(): number {
|
|
136
|
+
return this.open
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Whether `end` has been seen.
|
|
140
|
+
ended(): boolean {
|
|
141
|
+
return this.isEnded
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// The concrete path of the value at `depth` enclosing containers. Asked
|
|
145
|
+
// at a `start` or `scalar` hit it names the value; asked at a `close`
|
|
146
|
+
// hit it names the container that closed.
|
|
147
|
+
path(depth: number): Path {
|
|
148
|
+
const segments: Segment[] = []
|
|
149
|
+
const n = Math.min(depth, this.open)
|
|
150
|
+
for (let i = 0; i < n; i++) {
|
|
151
|
+
const level = this.levels[i]
|
|
152
|
+
segments.push(level.array ? Math.max(0, level.started - 1) : level.key)
|
|
153
|
+
}
|
|
154
|
+
return new Path(segments)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The captures that began with the last event, in id order.
|
|
158
|
+
begins(): readonly CaptureId[] {
|
|
159
|
+
return this.begun
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// One event. A malformed sequence is a `PROTOCOL_ORDER_ERROR`.
|
|
163
|
+
event(ev: JsonEvent): Hit {
|
|
164
|
+
if (this.isEnded) {
|
|
165
|
+
throw Fail.protocol('an event arrived after the document ended')
|
|
166
|
+
}
|
|
167
|
+
switch (ev.type) {
|
|
168
|
+
case 'key': {
|
|
169
|
+
const level = 0 < this.open ? this.levels[this.open - 1] : undefined
|
|
170
|
+
if (undefined === level || level.array) {
|
|
171
|
+
throw Fail.protocol('a key arrived outside an object')
|
|
172
|
+
}
|
|
173
|
+
if (level.inValue) {
|
|
174
|
+
throw Fail.protocol('a key arrived where a value was expected')
|
|
175
|
+
}
|
|
176
|
+
level.key = ev.key
|
|
177
|
+
level.inValue = true
|
|
178
|
+
this.begun.length = 0
|
|
179
|
+
return { kind: 'key', begins: 0, depth: this.open }
|
|
180
|
+
}
|
|
181
|
+
case 'object_start':
|
|
182
|
+
case 'array_start': {
|
|
183
|
+
const depth = this.open
|
|
184
|
+
const start = this.beginValue()
|
|
185
|
+
const end = this.nodeStack.length
|
|
186
|
+
const array = 'array_start' === ev.type
|
|
187
|
+
const level: Level = {
|
|
188
|
+
array,
|
|
189
|
+
started: 0,
|
|
190
|
+
key: '',
|
|
191
|
+
inValue: false,
|
|
192
|
+
start,
|
|
193
|
+
end,
|
|
194
|
+
}
|
|
195
|
+
if (depth === this.levels.length) this.levels.push(level)
|
|
196
|
+
else this.levels[depth] = level
|
|
197
|
+
this.open++
|
|
198
|
+
return { kind: 'start', begins: this.begun.length, depth }
|
|
199
|
+
}
|
|
200
|
+
case 'object_end':
|
|
201
|
+
case 'array_end': {
|
|
202
|
+
const wantArray = 'array_end' === ev.type
|
|
203
|
+
const level = 0 < this.open ? this.levels[this.open - 1] : undefined
|
|
204
|
+
if (undefined === level) {
|
|
205
|
+
throw Fail.protocol('a container ended that had not started')
|
|
206
|
+
}
|
|
207
|
+
if (level.array !== wantArray) {
|
|
208
|
+
throw Fail.protocol(
|
|
209
|
+
wantArray ? 'an array ended inside an object' : 'an object ended inside an array',
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
if (level.inValue) {
|
|
213
|
+
throw Fail.protocol('an object ended after a key without its value')
|
|
214
|
+
}
|
|
215
|
+
this.open--
|
|
216
|
+
this.nodeStack.length = level.start
|
|
217
|
+
this.completeValue()
|
|
218
|
+
this.begun.length = 0
|
|
219
|
+
return { kind: 'close', begins: 0, depth: this.open }
|
|
220
|
+
}
|
|
221
|
+
case 'end':
|
|
222
|
+
if (0 < this.open) {
|
|
223
|
+
throw Fail.protocol('the document ended inside a container')
|
|
224
|
+
}
|
|
225
|
+
if (!this.rootDone) {
|
|
226
|
+
throw Fail.protocol('the document ended before its root value')
|
|
227
|
+
}
|
|
228
|
+
this.isEnded = true
|
|
229
|
+
this.begun.length = 0
|
|
230
|
+
return { kind: 'end', begins: 0, depth: 0 }
|
|
231
|
+
default: {
|
|
232
|
+
const depth = this.open
|
|
233
|
+
const start = this.beginValue()
|
|
234
|
+
this.nodeStack.length = start
|
|
235
|
+
this.completeValue()
|
|
236
|
+
return { kind: 'scalar', begins: this.begun.length, depth }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A value begins at the current position: check that one is allowed
|
|
242
|
+
// here, push the trie nodes naming it onto the node stack, and fill
|
|
243
|
+
// `begun`. Returns where on the node stack the value's nodes start.
|
|
244
|
+
private beginValue(): number {
|
|
245
|
+
let base: number
|
|
246
|
+
if (0 === this.open) {
|
|
247
|
+
if (this.rootDone) throw Fail.protocol('a second root value arrived')
|
|
248
|
+
this.nodeStack.length = 0
|
|
249
|
+
this.nodeStack.push(ROOT)
|
|
250
|
+
base = 0
|
|
251
|
+
} else {
|
|
252
|
+
const level = this.levels[this.open - 1]
|
|
253
|
+
if (!level.array && !level.inValue) {
|
|
254
|
+
throw Fail.protocol('a value arrived inside an object without a key')
|
|
255
|
+
}
|
|
256
|
+
const index = level.started
|
|
257
|
+
level.started++
|
|
258
|
+
this.nodeStack.length = level.end
|
|
259
|
+
for (let at = level.start; at < level.end; at++) {
|
|
260
|
+
const node = this.nodes[this.nodeStack[at]]
|
|
261
|
+
if (level.array) {
|
|
262
|
+
if (0 <= node.eachIndex) this.nodeStack.push(node.eachIndex)
|
|
263
|
+
const c = node.indexes.get(index)
|
|
264
|
+
if (undefined !== c) this.nodeStack.push(c)
|
|
265
|
+
} else {
|
|
266
|
+
if (0 <= node.eachMember) this.nodeStack.push(node.eachMember)
|
|
267
|
+
const c = node.properties.get(level.key)
|
|
268
|
+
if (undefined !== c) this.nodeStack.push(c)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
base = level.end
|
|
272
|
+
}
|
|
273
|
+
this.begun.length = 0
|
|
274
|
+
for (let i = base; i < this.nodeStack.length; i++) {
|
|
275
|
+
const terminals = this.nodes[this.nodeStack[i]].terminals
|
|
276
|
+
for (const t of terminals) this.begun.push(t)
|
|
277
|
+
}
|
|
278
|
+
if (this.begun.length > 1) this.begun.sort((a, b) => a - b)
|
|
279
|
+
return base
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// The value at the current position is complete.
|
|
283
|
+
private completeValue(): void {
|
|
284
|
+
if (0 === this.open) this.rootDone = true
|
|
285
|
+
else this.levels[this.open - 1].inValue = false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// `ParserSource`: `JsonEvents/1` from a tabnas parse of one text.
|
|
4
|
+
//
|
|
5
|
+
// Two modes. `materialize` parses, then walks the value: always sound, and
|
|
6
|
+
// retains the whole value. `incremental` installs the rule-event adapter
|
|
7
|
+
// so events leave the parse as containers open and entries land, and
|
|
8
|
+
// optionally prunes streamed array elements from the engine's tree. It is
|
|
9
|
+
// sound for the grammars `capability.incremental` lists, which the
|
|
10
|
+
// differential suite verifies in this runtime, and for no other: an
|
|
11
|
+
// imperative grammar's rule events give a well-formed stream of the wrong
|
|
12
|
+
// shape or a malformed one, and the run would still return. So the
|
|
13
|
+
// incremental path is gated on the list, by the grammar's name, which the
|
|
14
|
+
// source cannot learn from the `Tabnas` and so must be told (`grammar`);
|
|
15
|
+
// without a listed name it fails with `STREAMABILITY_UNKNOWN` before the
|
|
16
|
+
// parse, having emitted nothing. `unverified()` lifts the gate for the
|
|
17
|
+
// differential suite that maintains the list.
|
|
18
|
+
//
|
|
19
|
+
// The parser is the source's: the source subscribes to its rule events and
|
|
20
|
+
// sets its parse budget (the engine has no way to take either back), so a
|
|
21
|
+
// parser is given to one source and not reused afterwards, and a source
|
|
22
|
+
// runs once (a second run is `STREAM_REUSED`).
|
|
23
|
+
//
|
|
24
|
+
// Failure mapping, in this order: a sink failure is thrown as it was; a
|
|
25
|
+
// sink that stopped is `'stop'`; a parse cancelled through the caller's
|
|
26
|
+
// `AbortFlag` is `ABORTED`; any other engine error is `INPUT_INVALID`
|
|
27
|
+
// with the engine's code and position; a grammar's own guard cancelling
|
|
28
|
+
// the parse is `INPUT_INVALID` too, with a message that names the
|
|
29
|
+
// grammar's guard. An incremental parse that returned without one rule
|
|
30
|
+
// event the adapter could turn into a value (YAML's empty document is
|
|
31
|
+
// `null`) has its value walked instead.
|
|
32
|
+
|
|
33
|
+
import { isIncremental } from './capability'
|
|
34
|
+
import { Fail } from './error'
|
|
35
|
+
import { Ev } from './event'
|
|
36
|
+
import { Guarded } from './guard'
|
|
37
|
+
import { AbortFlag, Limits, Metrics } from './limits'
|
|
38
|
+
import { Adapter, notStreamable, prepare } from './rule-events'
|
|
39
|
+
import { Flow, Sink } from './sink'
|
|
40
|
+
import { Prune, Source, SourceMode, engineFailure, walkValue } from './source'
|
|
41
|
+
|
|
42
|
+
// A tabnas parser applied to one text, as a source.
|
|
43
|
+
export class ParserSource implements Source {
|
|
44
|
+
private parser: any
|
|
45
|
+
private text: string
|
|
46
|
+
private sourceMode: SourceMode = SourceMode.materialize()
|
|
47
|
+
private sourceLimits: Limits = Limits.default()
|
|
48
|
+
private abortFlag: AbortFlag = new AbortFlag()
|
|
49
|
+
private sourceMetrics: Metrics = new Metrics()
|
|
50
|
+
private grammarName: string | null = null
|
|
51
|
+
private isUnverified = false
|
|
52
|
+
private used = false
|
|
53
|
+
|
|
54
|
+
// A source in materialize mode with default limits, its own abort flag
|
|
55
|
+
// and fresh metrics; the builder methods change each.
|
|
56
|
+
constructor(parser: any, text: string) {
|
|
57
|
+
this.parser = parser
|
|
58
|
+
this.text = text
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
mode(mode: SourceMode): this {
|
|
62
|
+
this.sourceMode = mode
|
|
63
|
+
return this
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The grammar the parser implements, by its package's name (`json` for
|
|
67
|
+
// `@tabnas/json`). Incremental mode runs only for a name
|
|
68
|
+
// `capability.incremental` lists.
|
|
69
|
+
grammar(name: string): this {
|
|
70
|
+
this.grammarName = name
|
|
71
|
+
return this
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Run incremental mode whatever the verified list says. This exists for
|
|
75
|
+
// the differential suite that maintains the list and for nothing else.
|
|
76
|
+
unverified(): this {
|
|
77
|
+
this.isUnverified = true
|
|
78
|
+
return this
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
limits(limits: Limits): this {
|
|
82
|
+
this.sourceLimits = limits
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
abort(abort: AbortFlag): this {
|
|
87
|
+
this.abortFlag = abort
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
metrics(metrics: Metrics): this {
|
|
92
|
+
this.sourceMetrics = metrics
|
|
93
|
+
return this
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Why the incremental path may not run, when it may not.
|
|
97
|
+
private gate(): Fail | null {
|
|
98
|
+
if (this.isUnverified) return null
|
|
99
|
+
const name = this.grammarName
|
|
100
|
+
if (null !== name && isIncremental(name)) return null
|
|
101
|
+
if (null !== name) {
|
|
102
|
+
return new Fail(
|
|
103
|
+
'STREAMABILITY_UNKNOWN',
|
|
104
|
+
`grammar ${JSON.stringify(name)} is not in capability.incremental: the differential ` +
|
|
105
|
+
'suite has not verified that its rule events stream as the walk does; run it with ' +
|
|
106
|
+
'SourceMode.materialize',
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
return new Fail(
|
|
110
|
+
'STREAMABILITY_UNKNOWN',
|
|
111
|
+
"SourceMode.incremental needs the grammar's name (ParserSource.grammar) to check " +
|
|
112
|
+
'capability.incremental; without one, run SourceMode.materialize',
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Run in the configured mode.
|
|
117
|
+
run(sink: Sink): Flow {
|
|
118
|
+
return this.runWithValue(sink).flow
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `run`, also handing back the value the engine returned. In materialize
|
|
122
|
+
// mode that is the grammar's value. In incremental mode it is the
|
|
123
|
+
// engine's tree AFTER pruning, which is neither the grammar's value nor
|
|
124
|
+
// the run's result (the events are): it exists so a test can measure
|
|
125
|
+
// what pruning left in the tree, and nothing else should read it.
|
|
126
|
+
runWithValue(sink: Sink): { flow: Flow; value: unknown } {
|
|
127
|
+
if (this.used) {
|
|
128
|
+
throw new Fail(
|
|
129
|
+
'STREAM_REUSED',
|
|
130
|
+
'a ParserSource runs once: its parser carries the run\'s subscriber and budget; ' +
|
|
131
|
+
'make a new source, with a new parser, for another run',
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
this.used = true
|
|
135
|
+
const mode = this.sourceMode
|
|
136
|
+
if ('materialize' === mode.type) {
|
|
137
|
+
const guarded = new Guarded(sink, this.sourceLimits, this.abortFlag, this.sourceMetrics)
|
|
138
|
+
try {
|
|
139
|
+
return materialize(this.parser, this.text, this.abortFlag, guarded)
|
|
140
|
+
} finally {
|
|
141
|
+
guarded.flush()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const refused = this.gate()
|
|
145
|
+
if (null !== refused) throw refused
|
|
146
|
+
return incremental(
|
|
147
|
+
this.parser,
|
|
148
|
+
this.text,
|
|
149
|
+
this.sourceLimits,
|
|
150
|
+
this.abortFlag,
|
|
151
|
+
this.sourceMetrics,
|
|
152
|
+
mode.prune,
|
|
153
|
+
sink,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Parse, then walk; the grammar's value comes back beside the outcome.
|
|
159
|
+
function materialize<S extends Sink>(
|
|
160
|
+
parser: any,
|
|
161
|
+
text: string,
|
|
162
|
+
abort: AbortFlag,
|
|
163
|
+
guarded: Guarded<S>,
|
|
164
|
+
): { flow: Flow; value: unknown } {
|
|
165
|
+
prepare(parser, () => !abort.isAborted())
|
|
166
|
+
let value: unknown
|
|
167
|
+
try {
|
|
168
|
+
value = parser.parse(text)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
throw engineFailure(err, abort)
|
|
171
|
+
}
|
|
172
|
+
let flow = walkValue(value, guarded)
|
|
173
|
+
if ('continue' === flow) flow = guarded.event(Ev.end)
|
|
174
|
+
return { flow, value }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function incremental<S extends Sink>(
|
|
178
|
+
parser: any,
|
|
179
|
+
text: string,
|
|
180
|
+
limits: Limits,
|
|
181
|
+
abort: AbortFlag,
|
|
182
|
+
metrics: Metrics,
|
|
183
|
+
prune: Prune,
|
|
184
|
+
sink: S,
|
|
185
|
+
): { flow: Flow; value: unknown } {
|
|
186
|
+
const stop = new AbortFlag()
|
|
187
|
+
const adapter = new Adapter(sink, limits, abort, metrics, prune, stop)
|
|
188
|
+
prepare(
|
|
189
|
+
parser,
|
|
190
|
+
() => !abort.isAborted() && !stop.isAborted(),
|
|
191
|
+
(rule, done) => adapter.onDone(rule, done),
|
|
192
|
+
)
|
|
193
|
+
let parsed: { ok: true; value: unknown } | { ok: false; error: unknown }
|
|
194
|
+
try {
|
|
195
|
+
parsed = { ok: true, value: parser.parse(text) }
|
|
196
|
+
} catch (error) {
|
|
197
|
+
parsed = { ok: false, error }
|
|
198
|
+
}
|
|
199
|
+
let outcome: { flow: Flow } | { error: unknown } = { flow: 'continue' }
|
|
200
|
+
try {
|
|
201
|
+
if ('running' === adapter.status.type) {
|
|
202
|
+
if (parsed.ok && adapter.complete()) {
|
|
203
|
+
outcome = { flow: adapter.send(Ev.end) }
|
|
204
|
+
} else if (parsed.ok && adapter.idle()) {
|
|
205
|
+
let flow = adapter.walkWhole(parsed.value)
|
|
206
|
+
if ('continue' === flow) flow = adapter.send(Ev.end)
|
|
207
|
+
outcome = { flow }
|
|
208
|
+
} else if (parsed.ok) {
|
|
209
|
+
outcome = { error: notStreamable() }
|
|
210
|
+
} else {
|
|
211
|
+
outcome = { error: engineFailure(parsed.error, abort) }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
outcome = { error }
|
|
216
|
+
} finally {
|
|
217
|
+
adapter.sink.flush()
|
|
218
|
+
}
|
|
219
|
+
const value = parsed.ok ? parsed.value : undefined
|
|
220
|
+
const status = adapter.status
|
|
221
|
+
if ('failed' === status.type) throw status.error
|
|
222
|
+
if ('stopped' === status.type) return { flow: 'stop', value }
|
|
223
|
+
if ('error' in outcome) throw outcome.error
|
|
224
|
+
return { flow: outcome.flow, value }
|
|
225
|
+
}
|