@human-synthesis/norns-tron 0.0.1
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/LICENSE +21 -0
- package/PERFORMANCE.md +179 -0
- package/README.md +108 -0
- package/package.json +43 -0
- package/src/client.d.ts +39 -0
- package/src/client.js +86 -0
- package/src/core/tron-auto.js +85 -0
- package/src/core/tron-encode.js +486 -0
- package/src/core/tron-schema.js +227 -0
- package/src/core/tron-table.js +159 -0
- package/src/core/tron-trampoline.js +396 -0
- package/src/core/tron-wasm.js +169 -0
- package/src/core/tron.js +734 -0
- package/src/core/wasm-bytes.js +4 -0
- package/src/index.d.ts +105 -0
- package/src/index.js +165 -0
- package/src/server.d.ts +19 -0
- package/src/server.js +99 -0
- package/src/valibot.d.ts +14 -0
- package/src/valibot.js +84 -0
- package/wasm/parserTron2.wasm +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// Optimized TRON encoder.
|
|
2
|
+
//
|
|
3
|
+
// Profiling the original showed where the 6x went (tabular-10k, JSON.stringify
|
|
4
|
+
// = 3.98ms baseline):
|
|
5
|
+
// emit via array.push + join('') 15.24 ms <-- dominant
|
|
6
|
+
// shape scan (keys.join sig) 3.48 ms
|
|
7
|
+
// dict statistics pass 1.89 ms
|
|
8
|
+
// + a second shape-signature computation during emit (~6.5 ms)
|
|
9
|
+
// and that plain string concatenation emits the same output in 6.72 ms — V8
|
|
10
|
+
// ropes beat array-join here. Hand-rolled string quoting was *slower* than
|
|
11
|
+
// JSON.stringify, so quoting stays native.
|
|
12
|
+
//
|
|
13
|
+
// The big win is the REVERSE TRAMPOLINE, mirroring the decode side: project
|
|
14
|
+
// rows to plain arrays, let native JSON.stringify serialize them, then rewrite
|
|
15
|
+
// row brackets with native replaceAll:
|
|
16
|
+
//
|
|
17
|
+
// [[1,"Ada"],[2,"Bob"]] -> [C0(1,"Ada"),C0(2,"Bob")]
|
|
18
|
+
//
|
|
19
|
+
// SAFETY. Two properties must hold for that text rewrite to be sound, and both
|
|
20
|
+
// are verified by counting on the serialized text rather than by inspecting
|
|
21
|
+
// every string value (cheaper, and exact):
|
|
22
|
+
// 1. `],[` occurs exactly rows-1 times. Every real row separator contributes
|
|
23
|
+
// one, so any extra means a string contained the sequence.
|
|
24
|
+
// 2. `(` and `)` do not occur at all. Canonical TRON escapes parens inside
|
|
25
|
+
// strings so the decoder can treat every raw paren as structural;
|
|
26
|
+
// JSON.stringify does not escape them, so their absence must be checked.
|
|
27
|
+
// If either fails we fall back to the general encoder, which escapes properly.
|
|
28
|
+
|
|
29
|
+
import * as TBL from './tron-table.js';
|
|
30
|
+
|
|
31
|
+
const MIN_USES = 2;
|
|
32
|
+
// Shape-signature separator. MUST be identical everywhere a signature is built
|
|
33
|
+
// or looked up; a mismatch silently turns lookups into permanent misses.
|
|
34
|
+
const SIG_SEP = '\u0000';
|
|
35
|
+
const DICT_MAX_UNIQUES = 64;
|
|
36
|
+
const MIN_CHUNK = 8; // below this a table declaration costs more than it saves
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------- helpers
|
|
39
|
+
function isPlainObject(v) {
|
|
40
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
41
|
+
}
|
|
42
|
+
function isScalar(v) {
|
|
43
|
+
return v === null || typeof v !== 'object';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Field name may be bare in a class declaration only if a plain identifier.
|
|
47
|
+
function encFieldName(k) {
|
|
48
|
+
if (k.length === 0) return JSON.stringify(k);
|
|
49
|
+
const f = k.charCodeAt(0);
|
|
50
|
+
if (!((f >= 65 && f <= 90) || (f >= 97 && f <= 122) || f === 95)) return JSON.stringify(k);
|
|
51
|
+
for (let i = 1; i < k.length; i++) {
|
|
52
|
+
const c = k.charCodeAt(i);
|
|
53
|
+
if (!((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95 || c === 46)) {
|
|
54
|
+
return JSON.stringify(k);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return k;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function encEnumVal(v) {
|
|
61
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
62
|
+
if (v.length === 0 || v === 'true' || v === 'false' || v === 'null') return JSON.stringify(v);
|
|
63
|
+
const f = v.charCodeAt(0), l = v.charCodeAt(v.length - 1);
|
|
64
|
+
if (f === 32 || l === 32 || f === 9 || l === 9 || f === 34) return JSON.stringify(v);
|
|
65
|
+
for (let k = 0; k < v.length; k++) {
|
|
66
|
+
const c = v.charCodeAt(k);
|
|
67
|
+
if (c === 44 || c === 34 || c === 92 || c < 32) return JSON.stringify(v);
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isNaN(Number(v))) return JSON.stringify(v);
|
|
70
|
+
return v;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// JSON string quoting + paren escaping (canonical TRON).
|
|
74
|
+
function encStr(str) {
|
|
75
|
+
const j = JSON.stringify(str);
|
|
76
|
+
if (str.indexOf('(') < 0 && str.indexOf(')') < 0) return j;
|
|
77
|
+
let o = '';
|
|
78
|
+
for (let k = 0; k < j.length; k++) {
|
|
79
|
+
const c = j.charCodeAt(k);
|
|
80
|
+
if (c === 40) o += '\\u0028';
|
|
81
|
+
else if (c === 41) o += '\\u0029';
|
|
82
|
+
else o += j[k];
|
|
83
|
+
}
|
|
84
|
+
return o;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function numStr(v) {
|
|
88
|
+
return Number.isFinite(v) ? String(v) : 'null';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// count non-overlapping occurrences (native indexOf loop)
|
|
92
|
+
function countOcc(hay, needle) {
|
|
93
|
+
let n = 0, i = 0;
|
|
94
|
+
const step = needle.length;
|
|
95
|
+
for (;;) {
|
|
96
|
+
const p = hay.indexOf(needle, i);
|
|
97
|
+
if (p === -1) return n;
|
|
98
|
+
n++; i = p + step;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// In table mode rows are serialized by JSON.stringify, which does not escape
|
|
104
|
+
// parens. Canonical TRON requires every RAW paren in a document to be
|
|
105
|
+
// structural (the trampoline relies on it), so escape them here. In plain JSON
|
|
106
|
+
// a paren can only occur inside a string, so a blanket replace is safe and the
|
|
107
|
+
// result is still valid JSON (\u0028 decodes back to "(").
|
|
108
|
+
function escapeParens(t) {
|
|
109
|
+
if (t.indexOf('(') === -1 && t.indexOf(')') === -1) return t;
|
|
110
|
+
return t.replaceAll('(', '\\u0028').replaceAll(')', '\\u0029');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------- fast path
|
|
114
|
+
// Root array of >=2 uniform, flat objects. Returns undefined if inapplicable.
|
|
115
|
+
function tryFastTable(value, useDict, useTable, useTableNested) {
|
|
116
|
+
if (!Array.isArray(value)) return undefined;
|
|
117
|
+
const n = value.length;
|
|
118
|
+
if (n < MIN_USES) return undefined;
|
|
119
|
+
const r0 = value[0];
|
|
120
|
+
if (!isPlainObject(r0)) return undefined;
|
|
121
|
+
const keys = Object.keys(r0);
|
|
122
|
+
const nf = keys.length;
|
|
123
|
+
if (nf === 0) return undefined;
|
|
124
|
+
|
|
125
|
+
// Pass 1 — verify uniform + flat, and gather dictionary statistics.
|
|
126
|
+
const stats = useDict ? new Array(nf) : null;
|
|
127
|
+
if (useDict) for (let k = 0; k < nf; k++) stats[k] = { m: new Map(), ok: true, total: 0, bytes: 0, kind: undefined };
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < n; i++) {
|
|
130
|
+
const r = value[i];
|
|
131
|
+
if (!isPlainObject(r)) return undefined;
|
|
132
|
+
const rk = Object.keys(r);
|
|
133
|
+
if (rk.length !== nf) return undefined;
|
|
134
|
+
for (let k = 0; k < nf; k++) if (rk[k] !== keys[k]) return undefined;
|
|
135
|
+
for (let k = 0; k < nf; k++) {
|
|
136
|
+
const v = r[keys[k]];
|
|
137
|
+
// In table mode rows are plain JSON arrays, so a nested object/array is
|
|
138
|
+
// fine — it just serializes as JSON. Outside table mode a nested value
|
|
139
|
+
// would need the text transform, so it goes to the general path.
|
|
140
|
+
// Nested values are only allowed when the caller opts into nested tables:
|
|
141
|
+
// they serialize as plain JSON, which costs the token savings those inner
|
|
142
|
+
// shapes would have got from their own class. Flat rows are a pure win.
|
|
143
|
+
// Objects with toJSON (Dates) are fine: rows are serialized by native
|
|
144
|
+
// JSON.stringify further down, which resolves them exactly like JSON.
|
|
145
|
+
if (!isScalar(v) && typeof v.toJSON !== 'function' && !useTableNested) return undefined;
|
|
146
|
+
if (useDict) {
|
|
147
|
+
const st = stats[k];
|
|
148
|
+
if (!st.ok) continue;
|
|
149
|
+
const vt = typeof v;
|
|
150
|
+
if (vt !== 'string' && vt !== 'boolean') { st.ok = false; st.m = null; continue; }
|
|
151
|
+
if (st.kind === undefined) st.kind = vt;
|
|
152
|
+
else if (st.kind !== vt) { st.ok = false; st.m = null; continue; }
|
|
153
|
+
st.total++; st.bytes += (vt === 'string' ? v.length : 5);
|
|
154
|
+
const c = st.m.get(v);
|
|
155
|
+
if (c === undefined) {
|
|
156
|
+
if (st.m.size >= DICT_MAX_UNIQUES) { st.ok = false; st.m = null; continue; }
|
|
157
|
+
st.m.set(v, 1);
|
|
158
|
+
} else st.m.set(v, c + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Decide dictionaries.
|
|
164
|
+
let dictMaps = null;
|
|
165
|
+
const enumDecls = [];
|
|
166
|
+
const declFields = new Array(nf);
|
|
167
|
+
const enumBySig = new Map();
|
|
168
|
+
let en = 0;
|
|
169
|
+
for (let k = 0; k < nf; k++) {
|
|
170
|
+
declFields[k] = encFieldName(keys[k]);
|
|
171
|
+
if (!useDict) continue;
|
|
172
|
+
const st = stats[k];
|
|
173
|
+
if (st.ok && st.m && st.m.size >= 1 && n >= 3 * st.m.size && st.total > 0 && (st.bytes / st.total) >= 2) {
|
|
174
|
+
const values = Array.from(st.m.keys());
|
|
175
|
+
const vsig = JSON.stringify(values);
|
|
176
|
+
let ename = enumBySig.get(vsig);
|
|
177
|
+
if (ename === undefined) {
|
|
178
|
+
ename = 'E' + (en++);
|
|
179
|
+
enumBySig.set(vsig, ename);
|
|
180
|
+
enumDecls.push('enum ' + ename + ': ' + values.map(encEnumVal).join(','));
|
|
181
|
+
}
|
|
182
|
+
if (!dictMaps) dictMaps = new Array(nf).fill(null);
|
|
183
|
+
const idx = new Map();
|
|
184
|
+
for (let q = 0; q < values.length; q++) idx.set(values[q], q);
|
|
185
|
+
dictMaps[k] = idx;
|
|
186
|
+
declFields[k] = encFieldName(keys[k]) + '@' + ename;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const header = (enumDecls.length ? enumDecls.join('\n') + '\n' : '') +
|
|
191
|
+
'class C0: ' + declFields.join(',') + '\n';
|
|
192
|
+
|
|
193
|
+
// Pass 2 — project to plain arrays so native JSON.stringify can serialize.
|
|
194
|
+
const proj = new Array(n);
|
|
195
|
+
for (let i = 0; i < n; i++) {
|
|
196
|
+
const r = value[i];
|
|
197
|
+
const a = new Array(nf);
|
|
198
|
+
if (dictMaps === null) {
|
|
199
|
+
for (let k = 0; k < nf; k++) a[k] = r[keys[k]];
|
|
200
|
+
} else {
|
|
201
|
+
for (let k = 0; k < nf; k++) {
|
|
202
|
+
const dm = dictMaps[k];
|
|
203
|
+
a[k] = dm === null ? r[keys[k]] : dm.get(r[keys[k]]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
proj[i] = a;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const t = JSON.stringify(proj);
|
|
210
|
+
|
|
211
|
+
// TABLE MODE: rows stay plain JSON arrays and a `table` declaration says
|
|
212
|
+
// where they live. The decoder then needs no text transform at all — and the
|
|
213
|
+
// repeated class name disappears from every row, so tokens drop too.
|
|
214
|
+
if (useTable) return header + 'table C0: $\n' + escapeParens(t);
|
|
215
|
+
|
|
216
|
+
// Soundness checks on the serialized text (see header comment).
|
|
217
|
+
if (t.indexOf('(') !== -1 || t.indexOf(')') !== -1) return undefined;
|
|
218
|
+
if (countOcc(t, '],[') !== n - 1) return undefined;
|
|
219
|
+
|
|
220
|
+
// [[a,b],[c,d]] -> [C0(a,b),C0(c,d)]
|
|
221
|
+
const body = '[C0(' + t.slice(2, t.length - 2).replaceAll('],[', '),C0(') + ')]';
|
|
222
|
+
return header + body;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------- general
|
|
226
|
+
// Same semantics as before, but emitting with string concatenation rather than
|
|
227
|
+
// array push + join (measured 2.3x faster on the emit phase).
|
|
228
|
+
function stringifyGeneral(value, useDict, useTable, useTableNested) {
|
|
229
|
+
const tableDecls = [];
|
|
230
|
+
let curPath = '$'; // path of the value currently being emitted (null = unaddressable)
|
|
231
|
+
const shapeCounts = new Map();
|
|
232
|
+
// Arrays proven uniform+flat during the scan; the emit phase reuses this
|
|
233
|
+
// proof instead of walking every row's keys a second time.
|
|
234
|
+
const uniformArrays = new WeakMap();
|
|
235
|
+
|
|
236
|
+
// A run of identical flat rows shares ONE signature: compute keys.join once
|
|
237
|
+
// for the whole array instead of once per row, and skip recursing into rows
|
|
238
|
+
// whose values are all scalars. On a 10k-row array that removes 10k string
|
|
239
|
+
// joins and 10k recursive calls.
|
|
240
|
+
function scanUniformArray(arr) {
|
|
241
|
+
const n = arr.length;
|
|
242
|
+
if (n < MIN_USES) return false;
|
|
243
|
+
const r0 = arr[0];
|
|
244
|
+
if (r0 === null || typeof r0 !== 'object' || Array.isArray(r0)) return false;
|
|
245
|
+
const keys = Object.keys(r0);
|
|
246
|
+
const nf = keys.length;
|
|
247
|
+
if (nf === 0) return false;
|
|
248
|
+
for (let i = 0; i < n; i++) {
|
|
249
|
+
const r = arr[i];
|
|
250
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) return false;
|
|
251
|
+
const rk = Object.keys(r);
|
|
252
|
+
if (rk.length !== nf) return false;
|
|
253
|
+
for (let k = 0; k < nf; k++) if (rk[k] !== keys[k]) return false;
|
|
254
|
+
if (!(useTableNested && n >= MIN_CHUNK)) {
|
|
255
|
+
for (let k = 0; k < nf; k++) { const val = r[keys[k]]; if (val !== null && typeof val === 'object' && typeof val.toJSON !== 'function') return false; }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const sig = keys.join(SIG_SEP);
|
|
259
|
+
let e = shapeCounts.get(sig);
|
|
260
|
+
if (!e) {
|
|
261
|
+
e = { count: 0, keys, stats: null, cls: null };
|
|
262
|
+
if (useDict) {
|
|
263
|
+
e.stats = new Array(nf);
|
|
264
|
+
for (let k = 0; k < nf; k++) e.stats[k] = { m: new Map(), ok: true, total: 0, bytes: 0, kind: undefined };
|
|
265
|
+
}
|
|
266
|
+
shapeCounts.set(sig, e);
|
|
267
|
+
}
|
|
268
|
+
e.count += n;
|
|
269
|
+
if (useDict) {
|
|
270
|
+
for (let i = 0; i < n; i++) {
|
|
271
|
+
const r = arr[i];
|
|
272
|
+
for (let k = 0; k < nf; k++) {
|
|
273
|
+
const st = e.stats[k];
|
|
274
|
+
if (!st.ok) continue;
|
|
275
|
+
const val = r[keys[k]];
|
|
276
|
+
const vt = typeof val;
|
|
277
|
+
if (vt !== 'string' && vt !== 'boolean') { st.ok = false; st.m = null; continue; }
|
|
278
|
+
if (st.kind === undefined) st.kind = vt;
|
|
279
|
+
else if (st.kind !== vt) { st.ok = false; st.m = null; continue; }
|
|
280
|
+
st.total++; st.bytes += (vt === 'string' ? val.length : 5);
|
|
281
|
+
const c = st.m.get(val);
|
|
282
|
+
if (c === undefined) {
|
|
283
|
+
if (st.m.size >= DICT_MAX_UNIQUES) { st.ok = false; st.m = null; continue; }
|
|
284
|
+
st.m.set(val, 1);
|
|
285
|
+
} else st.m.set(val, c + 1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
uniformArrays.set(arr, keys);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
(function scan(v) {
|
|
294
|
+
if (v === null || typeof v !== 'object') return;
|
|
295
|
+
// JSON.stringify semantics: an object with toJSON serializes as its
|
|
296
|
+
// toJSON() result (Dates -> ISO strings), so scan the resolved value.
|
|
297
|
+
if (typeof v.toJSON === 'function') { scan(v.toJSON()); return; }
|
|
298
|
+
if (Array.isArray(v)) {
|
|
299
|
+
if (scanUniformArray(v)) return;
|
|
300
|
+
for (let k = 0; k < v.length; k++) scan(v[k]);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const keys = Object.keys(v);
|
|
304
|
+
const sig = keys.join(SIG_SEP);
|
|
305
|
+
let e = shapeCounts.get(sig);
|
|
306
|
+
if (!e) {
|
|
307
|
+
e = { count: 0, keys, stats: null, cls: null };
|
|
308
|
+
if (useDict) {
|
|
309
|
+
e.stats = new Array(keys.length);
|
|
310
|
+
for (let k = 0; k < keys.length; k++) e.stats[k] = { m: new Map(), ok: true, total: 0, bytes: 0, kind: undefined };
|
|
311
|
+
}
|
|
312
|
+
shapeCounts.set(sig, e);
|
|
313
|
+
}
|
|
314
|
+
e.count++;
|
|
315
|
+
if (useDict) {
|
|
316
|
+
const keysL = keys.length;
|
|
317
|
+
for (let k = 0; k < keysL; k++) {
|
|
318
|
+
const st = e.stats[k];
|
|
319
|
+
if (!st.ok) continue;
|
|
320
|
+
const val = v[keys[k]];
|
|
321
|
+
const vt = typeof val;
|
|
322
|
+
if (vt !== 'string' && vt !== 'boolean') { st.ok = false; st.m = null; continue; }
|
|
323
|
+
if (st.kind === undefined) st.kind = vt;
|
|
324
|
+
else if (st.kind !== vt) { st.ok = false; st.m = null; continue; }
|
|
325
|
+
st.total++; st.bytes += (vt === 'string' ? val.length : 5);
|
|
326
|
+
const c = st.m.get(val);
|
|
327
|
+
if (c === undefined) {
|
|
328
|
+
if (st.m.size >= DICT_MAX_UNIQUES) { st.ok = false; st.m = null; continue; }
|
|
329
|
+
st.m.set(val, 1);
|
|
330
|
+
} else st.m.set(val, c + 1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (let k = 0; k < keys.length; k++) scan(v[keys[k]]);
|
|
334
|
+
})(value);
|
|
335
|
+
|
|
336
|
+
const classDecls = [];
|
|
337
|
+
const enumDecls = [];
|
|
338
|
+
const enumBySig = new Map();
|
|
339
|
+
let cn = 0, en = 0;
|
|
340
|
+
for (const e of shapeCounts.values()) {
|
|
341
|
+
if (e.count >= MIN_USES && e.keys.length > 0) {
|
|
342
|
+
const name = 'C' + (cn++);
|
|
343
|
+
let dictMaps = null;
|
|
344
|
+
const declFields = new Array(e.keys.length);
|
|
345
|
+
for (let k = 0; k < e.keys.length; k++) {
|
|
346
|
+
declFields[k] = encFieldName(e.keys[k]);
|
|
347
|
+
if (!useDict) continue;
|
|
348
|
+
const st = e.stats[k];
|
|
349
|
+
if (st.ok && st.m && st.m.size >= 1 &&
|
|
350
|
+
e.count >= 3 * st.m.size && st.total > 0 && (st.bytes / st.total) >= 2) {
|
|
351
|
+
const values = Array.from(st.m.keys());
|
|
352
|
+
const vsig = JSON.stringify(values);
|
|
353
|
+
let ename = enumBySig.get(vsig);
|
|
354
|
+
if (ename === undefined) {
|
|
355
|
+
ename = 'E' + (en++);
|
|
356
|
+
enumBySig.set(vsig, ename);
|
|
357
|
+
enumDecls.push('enum ' + ename + ': ' + values.map(encEnumVal).join(','));
|
|
358
|
+
}
|
|
359
|
+
if (!dictMaps) dictMaps = new Array(e.keys.length).fill(null);
|
|
360
|
+
const idx = new Map();
|
|
361
|
+
for (let q = 0; q < values.length; q++) idx.set(values[q], q);
|
|
362
|
+
dictMaps[k] = idx;
|
|
363
|
+
declFields[k] = encFieldName(e.keys[k]) + '@' + ename;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
e.cls = { name, keys: e.keys, dictMaps };
|
|
367
|
+
classDecls.push('class ' + name + ': ' + declFields.join(','));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Apply the reverse trampoline to any sufficiently long run of uniform flat
|
|
372
|
+
// rows found anywhere in the tree — this is what makes the common API
|
|
373
|
+
// envelope {meta:{...}, data:[...10k rows]} fast, since its root is an object
|
|
374
|
+
// and so misses the top-level fast path. Returns null when inapplicable.
|
|
375
|
+
function tryRowChunk(arr) {
|
|
376
|
+
const n = arr.length;
|
|
377
|
+
if (n < MIN_CHUNK) return null;
|
|
378
|
+
// The scan already proved uniformity + flatness for this exact array.
|
|
379
|
+
const keys = uniformArrays.get(arr);
|
|
380
|
+
if (keys === undefined) return null;
|
|
381
|
+
const nf = keys.length;
|
|
382
|
+
const e = shapeCounts.get(keys.join(SIG_SEP));
|
|
383
|
+
const cls = e && e.cls;
|
|
384
|
+
if (!cls) return null;
|
|
385
|
+
const dm = cls.dictMaps;
|
|
386
|
+
const proj = new Array(n);
|
|
387
|
+
for (let i = 0; i < n; i++) {
|
|
388
|
+
const r = arr[i];
|
|
389
|
+
const a = new Array(nf);
|
|
390
|
+
if (dm === null) { for (let k = 0; k < nf; k++) a[k] = r[keys[k]]; }
|
|
391
|
+
else {
|
|
392
|
+
for (let k = 0; k < nf; k++) {
|
|
393
|
+
const dmk = dm[k];
|
|
394
|
+
a[k] = dmk === null ? r[keys[k]] : dmk.get(r[keys[k]]);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
proj[i] = a;
|
|
398
|
+
}
|
|
399
|
+
const t = JSON.stringify(proj);
|
|
400
|
+
if (useTable && curPath !== null) {
|
|
401
|
+
if (t.length === 0) return null;
|
|
402
|
+
tableDecls.push('table ' + cls.name + ': ' + curPath);
|
|
403
|
+
return escapeParens(t); // plain JSON rows; decoder needs no transform
|
|
404
|
+
}
|
|
405
|
+
if (t.indexOf('(') !== -1 || t.indexOf(')') !== -1) return null;
|
|
406
|
+
if (countOcc(t, '],[') !== n - 1) return null;
|
|
407
|
+
const open = cls.name + '(';
|
|
408
|
+
return '[' + open + t.slice(2, t.length - 2).replaceAll('],[', '),' + open) + ')]';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let out = '';
|
|
412
|
+
function enc(v) {
|
|
413
|
+
if (v === null) { out += 'null'; return; }
|
|
414
|
+
const t = typeof v;
|
|
415
|
+
if (t === 'string') { out += encStr(v); return; }
|
|
416
|
+
if (t === 'number') { out += numStr(v); return; }
|
|
417
|
+
if (t === 'boolean') { out += v ? 'true' : 'false'; return; }
|
|
418
|
+
if (t === 'undefined') { out += 'null'; return; }
|
|
419
|
+
if (typeof v.toJSON === 'function' && t !== 'function') { enc(v.toJSON()); return; }
|
|
420
|
+
if (Array.isArray(v)) {
|
|
421
|
+
const chunk = tryRowChunk(v);
|
|
422
|
+
if (chunk !== null) { out += chunk; return; }
|
|
423
|
+
const base = curPath;
|
|
424
|
+
out += '[';
|
|
425
|
+
for (let k = 0; k < v.length; k++) {
|
|
426
|
+
if (k) out += ',';
|
|
427
|
+
curPath = base === null ? null : TBL.pathIndex(base, k);
|
|
428
|
+
enc(v[k]);
|
|
429
|
+
}
|
|
430
|
+
curPath = base;
|
|
431
|
+
out += ']';
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const keys = Object.keys(v);
|
|
435
|
+
const e = shapeCounts.get(keys.join(SIG_SEP));
|
|
436
|
+
const cls = e && e.cls;
|
|
437
|
+
if (cls) {
|
|
438
|
+
out += cls.name + '(';
|
|
439
|
+
const dm = cls.dictMaps;
|
|
440
|
+
const ck = cls.keys;
|
|
441
|
+
const base = curPath;
|
|
442
|
+
for (let k = 0; k < ck.length; k++) {
|
|
443
|
+
if (k) out += ',';
|
|
444
|
+
const dmk = dm === null ? null : dm[k];
|
|
445
|
+
if (dmk) out += dmk.get(v[ck[k]]);
|
|
446
|
+
else { curPath = null; enc(v[ck[k]]); } // inside an instantiation: not addressable
|
|
447
|
+
}
|
|
448
|
+
curPath = base;
|
|
449
|
+
out += ')';
|
|
450
|
+
} else {
|
|
451
|
+
const base = curPath;
|
|
452
|
+
out += '{';
|
|
453
|
+
for (let k = 0; k < keys.length; k++) {
|
|
454
|
+
if (k) out += ',';
|
|
455
|
+
out += encStr(keys[k]) + ':';
|
|
456
|
+
curPath = base === null ? null : TBL.pathKey(base, keys[k]);
|
|
457
|
+
enc(v[keys[k]]);
|
|
458
|
+
}
|
|
459
|
+
curPath = base;
|
|
460
|
+
out += '}';
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
enc(value);
|
|
464
|
+
|
|
465
|
+
if (classDecls.length === 0 && enumDecls.length === 0 && tableDecls.length === 0) return out;
|
|
466
|
+
return enumDecls.concat(classDecls).concat(tableDecls).join('\n') + '\n' + out;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// opts.table:
|
|
470
|
+
// true -> table declarations for FLAT uniform rows only. Pure win:
|
|
471
|
+
// faster to decode AND fewer tokens than class instantiation.
|
|
472
|
+
// 'nested' -> also table-ify rows containing nested objects/arrays. Faster
|
|
473
|
+
// still, but those inner shapes lose their own class savings,
|
|
474
|
+
// so tokens can get worse. Opt-in for a reason.
|
|
475
|
+
function stringify(value, opts) {
|
|
476
|
+
const useDict = !!(opts && opts.dict);
|
|
477
|
+
const useTableNested = !!(opts && opts.table === 'nested');
|
|
478
|
+
const useTable = !!(opts && opts.table);
|
|
479
|
+
if (!(opts && opts.noFastPath)) {
|
|
480
|
+
const fast = tryFastTable(value, useDict, useTable, useTableNested);
|
|
481
|
+
if (fast !== undefined) return fast;
|
|
482
|
+
}
|
|
483
|
+
return stringifyGeneral(value, useDict, useTable, useTableNested);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export { stringify, stringifyGeneral, tryFastTable };
|