@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/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @tabnas/transduce
|
|
2
|
+
|
|
3
|
+
The TypeScript implementation of Tabnas's streaming structured-data
|
|
4
|
+
transducer, including event sources, routing, captures, and protocol checks.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install @tabnas/transduce
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
See the [project README](https://github.com/tabnas/transduce#readme) for the
|
|
11
|
+
architecture, API examples, limits, and protocol documentation.
|
|
12
|
+
|
|
13
|
+
This package includes its TypeScript sources under `src/` alongside the
|
|
14
|
+
compiled JavaScript and declarations under `dist/`.
|
package/dist/transduce.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.2";
|
|
2
2
|
export { Datum, DatumBuilder, byteSize, fromJSON, fromTabnas, getPath, takePath, toJSON, toText, walkDatum, } from './datum';
|
|
3
3
|
export type { Duplicates } from './datum';
|
|
4
4
|
export { Code, Fail, isFail } from './error';
|
package/dist/transduce.js
CHANGED
|
@@ -24,7 +24,7 @@ exports.TableFromJson = exports.columnFromMeta = exports.boundColumn = exports.T
|
|
|
24
24
|
// sink from inside its own callback, and a slow writer at the end of the
|
|
25
25
|
// chain slows the parse at the start. That is the backpressure.
|
|
26
26
|
// This package's version, as package.json declares it.
|
|
27
|
-
exports.VERSION = '0.1.
|
|
27
|
+
exports.VERSION = '0.1.2';
|
|
28
28
|
var datum_1 = require("./datum");
|
|
29
29
|
Object.defineProperty(exports, "Datum", { enumerable: true, get: function () { return datum_1.Datum; } });
|
|
30
30
|
Object.defineProperty(exports, "DatumBuilder", { enumerable: true, get: function () { return datum_1.DatumBuilder; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tabnas/transduce",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"main": "dist/transduce.js",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"types": "dist/transduce.d.ts",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
|
+
"src",
|
|
31
|
+
"README.md",
|
|
30
32
|
"LICENSE"
|
|
31
33
|
],
|
|
32
34
|
"exports": {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// Which grammars the incremental source is verified for, in THIS runtime.
|
|
4
|
+
//
|
|
5
|
+
// Incremental streaming is a per-grammar VERIFIED capability, never an
|
|
6
|
+
// assumption, and it is earned per runtime: the TypeScript grammars build
|
|
7
|
+
// their values through TypeScript rule events, whose shapes need not be the
|
|
8
|
+
// Rust ones, so this list is not copied from the Rust crate.
|
|
9
|
+
// `test/incremental.test.ts` runs every fixture under rs/tests/fixtures
|
|
10
|
+
// (and the generated documents) through both the incremental and the
|
|
11
|
+
// materialized path for every grammar in the devDependencies, and asserts
|
|
12
|
+
// this list in both directions: a listed grammar that ever completes a
|
|
13
|
+
// stream the walk contradicts, or that the adapter refuses for how it
|
|
14
|
+
// builds its values, fails the suite, and so does an unlisted grammar that
|
|
15
|
+
// never does. A grammar not listed still works through materialize mode
|
|
16
|
+
// and `ValueSource`; it just retains the whole value, and `ParserSource`
|
|
17
|
+
// refuses to run it incrementally.
|
|
18
|
+
//
|
|
19
|
+
// What "verified" promises is the Rust runtime's promise (docs/reference.md,
|
|
20
|
+
// "The verified grammars"): an incremental run either streams exactly what
|
|
21
|
+
// the walk would (number lexemes aside), or streams every occurrence of a
|
|
22
|
+
// repeated member name so that a `last_wins` router builds the walk's
|
|
23
|
+
// value, or fails before `end` with a documented code, never with a wrong
|
|
24
|
+
// stream.
|
|
25
|
+
|
|
26
|
+
// Measured: the TypeScript grammars earn the same eight as the Rust ones;
|
|
27
|
+
// `toml`, `ini`, `csv`, `xml` and `feed` open a section's, a record's or an
|
|
28
|
+
// element's container before its key, or build a record the stream does not
|
|
29
|
+
// see, and the adapter refuses each on its own samples.
|
|
30
|
+
|
|
31
|
+
// The grammars whose incremental events never contradict the whole-value
|
|
32
|
+
// walk on any fixture, by their package's name (`@tabnas/<name>`).
|
|
33
|
+
export const INCREMENTAL: readonly string[] = Object.freeze([
|
|
34
|
+
'json',
|
|
35
|
+
'json5',
|
|
36
|
+
'jsonc',
|
|
37
|
+
'jsonic',
|
|
38
|
+
'jsonl',
|
|
39
|
+
'markdown',
|
|
40
|
+
'yaml',
|
|
41
|
+
'zon',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
// Whether `grammar` may be run in incremental mode.
|
|
45
|
+
export function isIncremental(grammar: string): boolean {
|
|
46
|
+
return INCREMENTAL.includes(grammar)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const capability = Object.freeze({
|
|
50
|
+
INCREMENTAL,
|
|
51
|
+
incremental: isIncremental,
|
|
52
|
+
})
|
package/src/datum.ts
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
2
|
+
|
|
3
|
+
// The retained value type.
|
|
4
|
+
//
|
|
5
|
+
// What a capture materializes and what a projected cell holds. Distinct
|
|
6
|
+
// from the engine's value on purpose: a datum keeps a number's lexeme,
|
|
7
|
+
// keeps member order whatever the keys look like (a `Map`, since a plain
|
|
8
|
+
// object enumerates integer-like keys first), measures its own size
|
|
9
|
+
// against limits in UTF-8 bytes, and is owned by the transducer rather
|
|
10
|
+
// than shared with a parse.
|
|
11
|
+
|
|
12
|
+
import { Fail } from './error'
|
|
13
|
+
import { Ev, JsonEvent } from './event'
|
|
14
|
+
import { jsonNumber, jsonString } from './json'
|
|
15
|
+
import { NODE_BYTES, utf8Bytes } from './limits'
|
|
16
|
+
import { Segment } from './selector'
|
|
17
|
+
import { Flow, Sink } from './sink'
|
|
18
|
+
import { engineKeys, engineScalar } from './value'
|
|
19
|
+
|
|
20
|
+
// A retained JSON-like value. Members of an object are in source order; a
|
|
21
|
+
// repeated member replaces the earlier one in its place (last value wins)
|
|
22
|
+
// unless the builder's policy rejected it first.
|
|
23
|
+
export type Datum =
|
|
24
|
+
| { readonly type: 'null' }
|
|
25
|
+
| { readonly type: 'bool'; readonly value: boolean }
|
|
26
|
+
| { readonly type: 'number'; readonly value: number; readonly lexeme: string | null }
|
|
27
|
+
| { readonly type: 'string'; readonly value: string }
|
|
28
|
+
| { readonly type: 'array'; readonly items: Datum[] }
|
|
29
|
+
| { readonly type: 'object'; readonly members: Map<string, Datum> }
|
|
30
|
+
|
|
31
|
+
const NULL: Datum = Object.freeze({ type: 'null' })
|
|
32
|
+
const TRUE: Datum = Object.freeze({ type: 'bool', value: true })
|
|
33
|
+
const FALSE: Datum = Object.freeze({ type: 'bool', value: false })
|
|
34
|
+
|
|
35
|
+
export const Datum = Object.freeze({
|
|
36
|
+
null: NULL,
|
|
37
|
+
bool(value: boolean): Datum {
|
|
38
|
+
return value ? TRUE : FALSE
|
|
39
|
+
},
|
|
40
|
+
number(value: number, lexeme?: string | null): Datum {
|
|
41
|
+
return { type: 'number', value, lexeme: lexeme ?? null }
|
|
42
|
+
},
|
|
43
|
+
string(value: string): Datum {
|
|
44
|
+
return { type: 'string', value }
|
|
45
|
+
},
|
|
46
|
+
array(items: Datum[] = []): Datum {
|
|
47
|
+
return { type: 'array', items }
|
|
48
|
+
},
|
|
49
|
+
object(members: Map<string, Datum> | Iterable<[string, Datum]> = []): Datum {
|
|
50
|
+
return { type: 'object', members: members instanceof Map ? members : new Map(members) }
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Payload bytes plus `NODE_BYTES` per node: the measure limits use.
|
|
54
|
+
byteSize,
|
|
55
|
+
getPath,
|
|
56
|
+
takePath,
|
|
57
|
+
isContainer(d: Datum): boolean {
|
|
58
|
+
return 'array' === d.type || 'object' === d.type
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// From an engine value: `undefined` is `null`, as the engine serializes
|
|
62
|
+
// it; a number has no lexeme.
|
|
63
|
+
fromTabnas,
|
|
64
|
+
|
|
65
|
+
// As a plain JavaScript value, for oracles and tests. An object becomes a
|
|
66
|
+
// plain object (integer-like keys reorder), a number its value.
|
|
67
|
+
toJSON,
|
|
68
|
+
|
|
69
|
+
// From a plain JavaScript value, for tests. A number's lexeme is its
|
|
70
|
+
// shortest text.
|
|
71
|
+
fromJSON,
|
|
72
|
+
|
|
73
|
+
// Compact JSON, keeping number lexemes.
|
|
74
|
+
toText,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export function byteSize(d: Datum): number {
|
|
78
|
+
switch (d.type) {
|
|
79
|
+
case 'null':
|
|
80
|
+
case 'bool':
|
|
81
|
+
return NODE_BYTES
|
|
82
|
+
case 'number':
|
|
83
|
+
return NODE_BYTES + (null != d.lexeme ? utf8Bytes(d.lexeme) : 8)
|
|
84
|
+
case 'string':
|
|
85
|
+
return NODE_BYTES + utf8Bytes(d.value)
|
|
86
|
+
case 'array': {
|
|
87
|
+
let n = NODE_BYTES
|
|
88
|
+
for (const item of d.items) n += byteSize(item)
|
|
89
|
+
return n
|
|
90
|
+
}
|
|
91
|
+
case 'object': {
|
|
92
|
+
let n = NODE_BYTES
|
|
93
|
+
for (const [k, v] of d.members) n += utf8Bytes(k) + byteSize(v)
|
|
94
|
+
return n
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The value at a concrete path below this one.
|
|
100
|
+
export function getPath(d: Datum, path: readonly Segment[]): Datum | undefined {
|
|
101
|
+
let here: Datum | undefined = d
|
|
102
|
+
for (const seg of path) {
|
|
103
|
+
if (undefined === here) return undefined
|
|
104
|
+
if ('string' === typeof seg) {
|
|
105
|
+
here = 'object' === here.type ? here.members.get(seg) : undefined
|
|
106
|
+
} else {
|
|
107
|
+
here = 'array' === here.type ? here.items[seg] : undefined
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return here
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// The value at a concrete path below this one, moved out and replaced by
|
|
114
|
+
// `null` in its container. The root itself cannot be replaced in place, so
|
|
115
|
+
// an empty path returns the datum and leaves it alone.
|
|
116
|
+
export function takePath(d: Datum, path: readonly Segment[]): Datum | undefined {
|
|
117
|
+
if (0 === path.length) return d
|
|
118
|
+
const parent = getPath(d, path.slice(0, -1))
|
|
119
|
+
if (undefined === parent) return undefined
|
|
120
|
+
const last = path[path.length - 1]
|
|
121
|
+
if ('string' === typeof last) {
|
|
122
|
+
if ('object' !== parent.type || !parent.members.has(last)) return undefined
|
|
123
|
+
const v = parent.members.get(last) as Datum
|
|
124
|
+
parent.members.set(last, NULL)
|
|
125
|
+
return v
|
|
126
|
+
}
|
|
127
|
+
if ('array' !== parent.type || last >= parent.items.length) return undefined
|
|
128
|
+
const v = parent.items[last]
|
|
129
|
+
parent.items[last] = NULL
|
|
130
|
+
return v
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function fromTabnas(value: unknown): Datum {
|
|
134
|
+
if (Array.isArray(value)) return Datum.array(value.map(fromTabnas))
|
|
135
|
+
const scalar = engineScalar(value)
|
|
136
|
+
if (undefined !== scalar) {
|
|
137
|
+
switch (scalar.type) {
|
|
138
|
+
case 'null':
|
|
139
|
+
return NULL
|
|
140
|
+
case 'bool':
|
|
141
|
+
return Datum.bool(scalar.value)
|
|
142
|
+
case 'number':
|
|
143
|
+
return Datum.number(scalar.value)
|
|
144
|
+
case 'string':
|
|
145
|
+
return Datum.string(scalar.value)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const members = new Map<string, Datum>()
|
|
149
|
+
for (const k of engineKeys(value)) members.set(k, fromTabnas((value as any)[k]))
|
|
150
|
+
return { type: 'object', members }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function toJSON(d: Datum): unknown {
|
|
154
|
+
switch (d.type) {
|
|
155
|
+
case 'null':
|
|
156
|
+
return null
|
|
157
|
+
case 'bool':
|
|
158
|
+
case 'string':
|
|
159
|
+
return d.value
|
|
160
|
+
case 'number':
|
|
161
|
+
return null != d.lexeme ? Number(d.lexeme) : d.value
|
|
162
|
+
case 'array':
|
|
163
|
+
return d.items.map(toJSON)
|
|
164
|
+
case 'object': {
|
|
165
|
+
const out: Record<string, unknown> = {}
|
|
166
|
+
for (const [k, v] of d.members) out[k] = toJSON(v)
|
|
167
|
+
return out
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function fromJSON(value: unknown): Datum {
|
|
173
|
+
if (null == value) return NULL
|
|
174
|
+
if ('boolean' === typeof value) return Datum.bool(value)
|
|
175
|
+
if ('number' === typeof value) return Datum.number(value, String(value))
|
|
176
|
+
if ('string' === typeof value) return Datum.string(value)
|
|
177
|
+
if (Array.isArray(value)) return Datum.array(value.map(fromJSON))
|
|
178
|
+
const members = new Map<string, Datum>()
|
|
179
|
+
if (value instanceof Map) {
|
|
180
|
+
for (const [k, v] of value) members.set(String(k), fromJSON(v))
|
|
181
|
+
} else {
|
|
182
|
+
for (const k of Object.keys(value as object)) {
|
|
183
|
+
members.set(k, fromJSON((value as any)[k]))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { type: 'object', members }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function toText(d: Datum): string {
|
|
190
|
+
switch (d.type) {
|
|
191
|
+
case 'null':
|
|
192
|
+
return 'null'
|
|
193
|
+
case 'bool':
|
|
194
|
+
return d.value ? 'true' : 'false'
|
|
195
|
+
case 'number':
|
|
196
|
+
return jsonNumber(d.value, d.lexeme)
|
|
197
|
+
case 'string':
|
|
198
|
+
return jsonString(d.value)
|
|
199
|
+
case 'array': {
|
|
200
|
+
let out = '['
|
|
201
|
+
d.items.forEach((item, i) => {
|
|
202
|
+
if (i > 0) out += ','
|
|
203
|
+
out += toText(item)
|
|
204
|
+
})
|
|
205
|
+
return out + ']'
|
|
206
|
+
}
|
|
207
|
+
case 'object': {
|
|
208
|
+
let out = '{'
|
|
209
|
+
let i = 0
|
|
210
|
+
for (const [k, v] of d.members) {
|
|
211
|
+
if (i++ > 0) out += ','
|
|
212
|
+
out += jsonString(k) + ':' + toText(v)
|
|
213
|
+
}
|
|
214
|
+
return out + '}'
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Emit a datum as `JsonEvents/1` (without the final `end`, so a datum can
|
|
220
|
+
// stand in for any part of a document).
|
|
221
|
+
export function walkDatum(d: Datum, sink: Sink): Flow {
|
|
222
|
+
switch (d.type) {
|
|
223
|
+
case 'null':
|
|
224
|
+
return sink.event(Ev.null)
|
|
225
|
+
case 'bool':
|
|
226
|
+
return sink.event(Ev.bool(d.value))
|
|
227
|
+
case 'number':
|
|
228
|
+
return sink.event(Ev.number(d.value, d.lexeme))
|
|
229
|
+
case 'string':
|
|
230
|
+
return sink.event(Ev.string(d.value))
|
|
231
|
+
case 'array':
|
|
232
|
+
if ('stop' === sink.event(Ev.arrayStart)) return 'stop'
|
|
233
|
+
for (const item of d.items) {
|
|
234
|
+
if ('stop' === walkDatum(item, sink)) return 'stop'
|
|
235
|
+
}
|
|
236
|
+
return sink.event(Ev.arrayEnd)
|
|
237
|
+
case 'object':
|
|
238
|
+
if ('stop' === sink.event(Ev.objectStart)) return 'stop'
|
|
239
|
+
for (const [k, v] of d.members) {
|
|
240
|
+
if ('stop' === sink.event(Ev.key(k))) return 'stop'
|
|
241
|
+
if ('stop' === walkDatum(v, sink)) return 'stop'
|
|
242
|
+
}
|
|
243
|
+
return sink.event(Ev.objectEnd)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// How a builder treats a repeated member name: `reject` fails with
|
|
248
|
+
// `DUPLICATE_MEMBER`, `last_wins` replaces the earlier value, `first_wins`
|
|
249
|
+
// keeps it.
|
|
250
|
+
export type Duplicates = 'reject' | 'last_wins' | 'first_wins'
|
|
251
|
+
|
|
252
|
+
type Frame =
|
|
253
|
+
| { array: true; items: Datum[] }
|
|
254
|
+
| { array: false; members: Map<string, Datum>; key: string | null }
|
|
255
|
+
|
|
256
|
+
// Builds one `Datum` from the events of one value, under a byte limit.
|
|
257
|
+
//
|
|
258
|
+
// Feed it every event from the value's first to its last; `finished()`
|
|
259
|
+
// says when the value is complete. Bytes are counted as they arrive, and
|
|
260
|
+
// the limit fails at the first byte over it rather than after the value is
|
|
261
|
+
// whole. The builder knows nothing of where in a document its value sits:
|
|
262
|
+
// a failure leaves `path` unset and the stage that placed the builder adds
|
|
263
|
+
// it.
|
|
264
|
+
export class DatumBuilder {
|
|
265
|
+
private stack: Frame[] = []
|
|
266
|
+
private done: Datum | undefined = undefined
|
|
267
|
+
private held = 0
|
|
268
|
+
private readonly limit: number
|
|
269
|
+
private readonly limitName: string
|
|
270
|
+
private readonly duplicates: Duplicates
|
|
271
|
+
|
|
272
|
+
// A builder whose limit failure names `limitName` (a `Limits` field).
|
|
273
|
+
constructor(limit: number, limitName: string, duplicates: Duplicates) {
|
|
274
|
+
this.limit = limit
|
|
275
|
+
this.limitName = limitName
|
|
276
|
+
this.duplicates = duplicates
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
bytes(): number {
|
|
280
|
+
return this.held
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
finished(): boolean {
|
|
284
|
+
return undefined !== this.done
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// The value, once finished. Probing an unfinished builder returns
|
|
288
|
+
// `undefined` and leaves the charge for the partial value in place.
|
|
289
|
+
take(): Datum | undefined {
|
|
290
|
+
const done = this.done
|
|
291
|
+
if (undefined !== done) {
|
|
292
|
+
this.done = undefined
|
|
293
|
+
this.held = 0
|
|
294
|
+
}
|
|
295
|
+
return done
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private charge(n: number): void {
|
|
299
|
+
this.held += n
|
|
300
|
+
if (this.held > this.limit) {
|
|
301
|
+
throw Fail.limit(
|
|
302
|
+
this.limitName,
|
|
303
|
+
this.limit,
|
|
304
|
+
`a value is larger than ${this.limit} bytes`,
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private place(value: Datum): void {
|
|
310
|
+
const top = this.stack[this.stack.length - 1]
|
|
311
|
+
if (undefined === top) {
|
|
312
|
+
this.done = value
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (top.array) {
|
|
316
|
+
top.items.push(value)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
const key = top.key
|
|
320
|
+
if (null === key) {
|
|
321
|
+
throw Fail.protocol('a value arrived inside an object without a key')
|
|
322
|
+
}
|
|
323
|
+
top.key = null
|
|
324
|
+
// A repeated member was charged in full as its events arrived, which is
|
|
325
|
+
// right: for a moment both were held. Whichever one goes now gives its
|
|
326
|
+
// bytes back, so an object that keeps repeating a small key does not
|
|
327
|
+
// grow towards the limit while the value it holds stays the same size.
|
|
328
|
+
const existing = top.members.get(key)
|
|
329
|
+
let released = 0
|
|
330
|
+
if (undefined !== existing) {
|
|
331
|
+
switch (this.duplicates) {
|
|
332
|
+
case 'reject':
|
|
333
|
+
throw new Fail('DUPLICATE_MEMBER', `member ${JSON.stringify(key)} appears twice`)
|
|
334
|
+
case 'first_wins':
|
|
335
|
+
this.held = Math.max(0, this.held - (utf8Bytes(key) + byteSize(value)))
|
|
336
|
+
return
|
|
337
|
+
case 'last_wins':
|
|
338
|
+
released = utf8Bytes(key) + byteSize(existing)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
top.members.set(key, value)
|
|
342
|
+
this.held = Math.max(0, this.held - released)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// One event of the value being built.
|
|
346
|
+
event(ev: JsonEvent): void {
|
|
347
|
+
if (undefined !== this.done) {
|
|
348
|
+
throw Fail.protocol('an event arrived after the value was complete')
|
|
349
|
+
}
|
|
350
|
+
switch (ev.type) {
|
|
351
|
+
case 'object_start':
|
|
352
|
+
this.charge(NODE_BYTES)
|
|
353
|
+
this.stack.push({ array: false, members: new Map(), key: null })
|
|
354
|
+
break
|
|
355
|
+
case 'array_start':
|
|
356
|
+
this.charge(NODE_BYTES)
|
|
357
|
+
this.stack.push({ array: true, items: [] })
|
|
358
|
+
break
|
|
359
|
+
case 'key': {
|
|
360
|
+
this.charge(utf8Bytes(ev.key))
|
|
361
|
+
const top = this.stack[this.stack.length - 1]
|
|
362
|
+
if (undefined === top || top.array || null !== top.key) {
|
|
363
|
+
throw Fail.protocol('a key arrived where no member was expected')
|
|
364
|
+
}
|
|
365
|
+
top.key = ev.key
|
|
366
|
+
break
|
|
367
|
+
}
|
|
368
|
+
case 'object_end': {
|
|
369
|
+
const top = this.stack.pop()
|
|
370
|
+
if (undefined === top || top.array) {
|
|
371
|
+
throw Fail.protocol('an object ended that had not started')
|
|
372
|
+
}
|
|
373
|
+
if (null !== top.key) {
|
|
374
|
+
throw Fail.protocol('an object ended after a key without its value')
|
|
375
|
+
}
|
|
376
|
+
this.place({ type: 'object', members: top.members })
|
|
377
|
+
break
|
|
378
|
+
}
|
|
379
|
+
case 'array_end': {
|
|
380
|
+
const top = this.stack.pop()
|
|
381
|
+
if (undefined === top || !top.array) {
|
|
382
|
+
throw Fail.protocol('an array ended that had not started')
|
|
383
|
+
}
|
|
384
|
+
this.place({ type: 'array', items: top.items })
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
case 'null':
|
|
388
|
+
this.charge(NODE_BYTES)
|
|
389
|
+
this.place(NULL)
|
|
390
|
+
break
|
|
391
|
+
case 'bool':
|
|
392
|
+
this.charge(NODE_BYTES)
|
|
393
|
+
this.place(Datum.bool(ev.value))
|
|
394
|
+
break
|
|
395
|
+
case 'number':
|
|
396
|
+
this.charge(NODE_BYTES + (null != ev.lexeme ? utf8Bytes(ev.lexeme) : 8))
|
|
397
|
+
this.place(Datum.number(ev.value, ev.lexeme))
|
|
398
|
+
break
|
|
399
|
+
case 'string':
|
|
400
|
+
this.charge(NODE_BYTES + utf8Bytes(ev.value))
|
|
401
|
+
this.place(Datum.string(ev.value))
|
|
402
|
+
break
|
|
403
|
+
case 'end':
|
|
404
|
+
throw Fail.protocol('the document ended inside a value being captured')
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|