@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,227 @@
|
|
|
1
|
+
// SCHEMA PRE-REGISTRATION — "preload, peek, process".
|
|
2
|
+
//
|
|
3
|
+
// In a real API the client already knows the response shape from the interface
|
|
4
|
+
// contract (OpenAPI / TypeScript type / proto). Everything the self-describing
|
|
5
|
+
// format spends per request to *discover* that shape is therefore waste:
|
|
6
|
+
//
|
|
7
|
+
// encode side shape-detection pass, dictionary statistics pass, building
|
|
8
|
+
// the class/enum/table declarations, emitting the prelude
|
|
9
|
+
// wire the prelude bytes/tokens, on every single response
|
|
10
|
+
// decode side parsing the prelude, parsing the table path, compiling the
|
|
11
|
+
// row constructor
|
|
12
|
+
//
|
|
13
|
+
// With the schema registered once at startup, all of that collapses to:
|
|
14
|
+
//
|
|
15
|
+
// PRELOAD compile(spec) -> field order, enum tables, row constructor,
|
|
16
|
+
// resolved path — built once, reused forever
|
|
17
|
+
// PEEK (optional) read a short version tag to pick the right schema
|
|
18
|
+
// PROCESS JSON.parse + a tight constructor loop
|
|
19
|
+
//
|
|
20
|
+
// This matters most exactly where the self-describing format was weakest: small
|
|
21
|
+
// responses, where the fixed costs have nothing to amortize against, and where
|
|
22
|
+
// the prelude can be a large fraction of the payload.
|
|
23
|
+
//
|
|
24
|
+
// Wire format is plain JSON — no prelude at all:
|
|
25
|
+
// {"meta":{...},"data":[[1,"Ada",0],[2,"Bob",1]]}
|
|
26
|
+
// Optionally prefixed with a version tag for the PEEK step:
|
|
27
|
+
// #users.v1
|
|
28
|
+
// {"meta":{...},"data":[[...]]}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------- helpers
|
|
31
|
+
function parsePathSegs(p) {
|
|
32
|
+
if (!p || p === '$') return [];
|
|
33
|
+
if (p.charCodeAt(0) !== 36) throw new Error('schema path must start with $');
|
|
34
|
+
const segs = [];
|
|
35
|
+
let i = 1;
|
|
36
|
+
const n = p.length;
|
|
37
|
+
while (i < n) {
|
|
38
|
+
const c = p.charCodeAt(i);
|
|
39
|
+
if (c === 46 /* . */) {
|
|
40
|
+
i++;
|
|
41
|
+
const st = i;
|
|
42
|
+
while (i < n) { const d = p.charCodeAt(i); if (d === 46 || d === 91) break; i++; }
|
|
43
|
+
segs.push(p.slice(st, i));
|
|
44
|
+
} else if (c === 91 /* [ */) {
|
|
45
|
+
i++;
|
|
46
|
+
if (p.charCodeAt(i) === 34) {
|
|
47
|
+
let out = '', seg = ++i;
|
|
48
|
+
for (;;) {
|
|
49
|
+
if (i >= n) throw new Error('bad path');
|
|
50
|
+
const d = p.charCodeAt(i);
|
|
51
|
+
if (d === 34) { out += p.slice(seg, i); i++; break; }
|
|
52
|
+
if (d === 92) { out += p.slice(seg, i); i++; out += p[i]; i++; seg = i; }
|
|
53
|
+
else i++;
|
|
54
|
+
}
|
|
55
|
+
segs.push(out);
|
|
56
|
+
if (p.charCodeAt(i) !== 93) throw new Error('bad path');
|
|
57
|
+
i++;
|
|
58
|
+
} else {
|
|
59
|
+
const st = i;
|
|
60
|
+
while (i < n && p.charCodeAt(i) !== 93) i++;
|
|
61
|
+
segs.push(Number(p.slice(st, i)));
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
} else throw new Error('bad path');
|
|
65
|
+
}
|
|
66
|
+
return segs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------- compile
|
|
70
|
+
// spec = {
|
|
71
|
+
// id: optional string, emitted/matched as a "#id" peek tag
|
|
72
|
+
// fields: ordered field names (from the interface)
|
|
73
|
+
// enums: optional { fieldName: [values...] } dictionaries
|
|
74
|
+
// path: where the table sits, default '$' (root array)
|
|
75
|
+
// }
|
|
76
|
+
function compile(spec) {
|
|
77
|
+
const fields = spec.fields.slice();
|
|
78
|
+
const nf = fields.length;
|
|
79
|
+
const segs = parsePathSegs(spec.path || '$');
|
|
80
|
+
const id = spec.id || null;
|
|
81
|
+
const tag = id ? '#' + id + '\n' : '';
|
|
82
|
+
|
|
83
|
+
// per-field dictionaries, resolved once
|
|
84
|
+
const decTables = new Array(nf).fill(null); // index -> value
|
|
85
|
+
const encTables = new Array(nf).fill(null); // value -> index
|
|
86
|
+
if (spec.enums) {
|
|
87
|
+
for (let k = 0; k < nf; k++) {
|
|
88
|
+
const vals = spec.enums[fields[k]];
|
|
89
|
+
if (!vals) continue;
|
|
90
|
+
// SOUNDNESS: a dictionary column carries an integer index on the wire, and
|
|
91
|
+
// a value outside the declared set is passed through literally. The
|
|
92
|
+
// decoder tells them apart by `typeof === "number"`, so NUMERIC enum
|
|
93
|
+
// values are ambiguous with indices — an out-of-set number would decode
|
|
94
|
+
// as undefined. Only string/boolean dictionaries are representable.
|
|
95
|
+
for (let q = 0; q < vals.length; q++) {
|
|
96
|
+
const t = typeof vals[q];
|
|
97
|
+
if (t !== 'string' && t !== 'boolean') {
|
|
98
|
+
throw new Error('enum values for "' + fields[k] + '" must be strings or booleans (got ' + t + '); numeric values are ambiguous with dictionary indices');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
decTables[k] = vals.slice();
|
|
102
|
+
const m = new Map();
|
|
103
|
+
for (let q = 0; q < vals.length; q++) m.set(vals[q], q);
|
|
104
|
+
encTables[k] = m;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const anyDict = decTables.some(d => d !== null);
|
|
108
|
+
|
|
109
|
+
// ---- row constructor, compiled ONCE at startup rather than per request ----
|
|
110
|
+
// A dictionary column may still carry a literal string if the encoder saw a
|
|
111
|
+
// value outside the declared set, so those columns test the wire type.
|
|
112
|
+
let body = 'return function(r){return {';
|
|
113
|
+
for (let k = 0; k < nf; k++) {
|
|
114
|
+
if (k) body += ',';
|
|
115
|
+
const src = decTables[k] === null
|
|
116
|
+
? 'r[' + k + ']'
|
|
117
|
+
: '(typeof r[' + k + ']==="number"?D[' + k + '][r[' + k + ']]:r[' + k + '])';
|
|
118
|
+
body += JSON.stringify(fields[k]) + ':' + src;
|
|
119
|
+
}
|
|
120
|
+
body += '}}';
|
|
121
|
+
const ctor = new Function('D', body)(decTables);
|
|
122
|
+
|
|
123
|
+
// navigate to the table's container; compiled to a straight-line walk
|
|
124
|
+
function locate(root) {
|
|
125
|
+
let parent = null, last = null, cur = root;
|
|
126
|
+
for (let q = 0; q < segs.length; q++) {
|
|
127
|
+
if (cur === null || typeof cur !== 'object') return null;
|
|
128
|
+
parent = cur; last = segs[q]; cur = cur[segs[q]];
|
|
129
|
+
}
|
|
130
|
+
return { parent, last, cur };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------- PROCESS
|
|
134
|
+
function decode(text) {
|
|
135
|
+
// PEEK: skip an optional "#id" tag line without scanning the payload
|
|
136
|
+
let body2 = text;
|
|
137
|
+
if (text.charCodeAt(0) === 35 /* # */) {
|
|
138
|
+
const nl = text.indexOf('\n');
|
|
139
|
+
if (nl === -1) throw new Error('schema tag without body');
|
|
140
|
+
body2 = text.slice(nl + 1);
|
|
141
|
+
}
|
|
142
|
+
const root = JSON.parse(body2);
|
|
143
|
+
const loc = locate(root);
|
|
144
|
+
if (loc === null || !Array.isArray(loc.cur)) {
|
|
145
|
+
throw new Error('schema path did not resolve to an array');
|
|
146
|
+
}
|
|
147
|
+
const rows = loc.cur;
|
|
148
|
+
const n = rows.length;
|
|
149
|
+
const out = new Array(n);
|
|
150
|
+
for (let i = 0; i < n; i++) out[i] = ctor(rows[i]);
|
|
151
|
+
if (loc.parent === null) return out;
|
|
152
|
+
loc.parent[loc.last] = out;
|
|
153
|
+
return root;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Returns the id of a document without parsing it (cheap routing).
|
|
157
|
+
function peek(text) {
|
|
158
|
+
if (text.charCodeAt(0) !== 35) return null;
|
|
159
|
+
const nl = text.indexOf('\n');
|
|
160
|
+
return nl === -1 ? null : text.slice(1, nl);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------- encode
|
|
164
|
+
// No shape detection and no dictionary statistics: the schema already says
|
|
165
|
+
// the field order and the enum tables.
|
|
166
|
+
function projectRows(rows) {
|
|
167
|
+
const n = rows.length;
|
|
168
|
+
const proj = new Array(n);
|
|
169
|
+
if (!anyDict) {
|
|
170
|
+
for (let i = 0; i < n; i++) {
|
|
171
|
+
const r = rows[i];
|
|
172
|
+
const a = new Array(nf);
|
|
173
|
+
for (let k = 0; k < nf; k++) a[k] = r[fields[k]];
|
|
174
|
+
proj[i] = a;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
for (let i = 0; i < n; i++) {
|
|
178
|
+
const r = rows[i];
|
|
179
|
+
const a = new Array(nf);
|
|
180
|
+
for (let k = 0; k < nf; k++) {
|
|
181
|
+
const m = encTables[k];
|
|
182
|
+
if (m === null) { a[k] = r[fields[k]]; continue; }
|
|
183
|
+
const v = r[fields[k]];
|
|
184
|
+
const idx = m.get(v);
|
|
185
|
+
a[k] = idx === undefined ? v : idx; // unknown value passes through
|
|
186
|
+
}
|
|
187
|
+
proj[i] = a;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return proj;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function encode(value) {
|
|
194
|
+
if (segs.length === 0) return tag + JSON.stringify(projectRows(value));
|
|
195
|
+
// replace the table in a shallow copy so the caller's object is untouched
|
|
196
|
+
const cloneAlong = (node, depth) => {
|
|
197
|
+
if (depth === segs.length) return projectRows(node);
|
|
198
|
+
const key = segs[depth];
|
|
199
|
+
const copy = Array.isArray(node) ? node.slice() : Object.assign({}, node);
|
|
200
|
+
copy[key] = cloneAlong(node[key], depth + 1);
|
|
201
|
+
return copy;
|
|
202
|
+
};
|
|
203
|
+
return tag + JSON.stringify(cloneAlong(value, 0));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { encode, decode, peek, fields, id };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// A registry, for services that serve several response shapes.
|
|
210
|
+
function createRegistry() {
|
|
211
|
+
const byId = new Map();
|
|
212
|
+
return {
|
|
213
|
+
register(spec) { const s = compile(spec); if (s.id) byId.set(s.id, s); return s; },
|
|
214
|
+
get(id) { return byId.get(id); },
|
|
215
|
+
// PEEK then PROCESS: route a document to its schema by tag alone.
|
|
216
|
+
decode(text) {
|
|
217
|
+
if (text.charCodeAt(0) !== 35) throw new Error('document has no schema tag');
|
|
218
|
+
const nl = text.indexOf('\n');
|
|
219
|
+
const id = text.slice(1, nl);
|
|
220
|
+
const s = byId.get(id);
|
|
221
|
+
if (!s) throw new Error('unknown schema id: ' + id);
|
|
222
|
+
return s.decode(text);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export { compile, createRegistry };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// TABLE MODE — transform-free decoding.
|
|
2
|
+
//
|
|
3
|
+
// Profiling the remaining losses showed the decoder was paying for text
|
|
4
|
+
// rewriting it should not need. On the API envelope (10k rows) the budget was:
|
|
5
|
+
// replaceAll x2 1.88 ms
|
|
6
|
+
// JSON.parse(transformed) 3.88 ms
|
|
7
|
+
// ambiguity guard 0.31 ms
|
|
8
|
+
// walk + constructors 1.98 ms
|
|
9
|
+
// against a 6.63 ms JSON.parse baseline — only ~0.9 ms of headroom, so no
|
|
10
|
+
// amount of micro-tuning gets under 1.00x.
|
|
11
|
+
//
|
|
12
|
+
// The fix is to remove the transform rather than optimize it. The encoder
|
|
13
|
+
// declares WHERE each table lives:
|
|
14
|
+
//
|
|
15
|
+
// enum E0: admin,user,editor
|
|
16
|
+
// class C0: id,name,role@E0
|
|
17
|
+
// table C0: $.data
|
|
18
|
+
// {"meta":{...},"data":[[1,"Ada",0],[2,"Bob",1]]}
|
|
19
|
+
//
|
|
20
|
+
// Rows are plain JSON arrays, so the body is ordinary JSON. Decoding becomes
|
|
21
|
+
// JSON.parse + navigate + a compiled constructor loop: no replaceAll passes,
|
|
22
|
+
// no marker strings, and no ambiguity guard (the path says exactly which array
|
|
23
|
+
// is a table, so a look-alike array elsewhere can never be misread).
|
|
24
|
+
//
|
|
25
|
+
// This is an extension of TRON, like the `enum` dictionary — not part of the
|
|
26
|
+
// published format. It also *reduces* tokens, since the repeated class name
|
|
27
|
+
// disappears from every row.
|
|
28
|
+
//
|
|
29
|
+
// Path grammar: `$` for the root, then `.ident` for identifier-safe keys,
|
|
30
|
+
// `["quoted"]` for other keys, and `[123]` for array indices.
|
|
31
|
+
|
|
32
|
+
import { parsePrelude } from './tron.js';
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------- paths
|
|
35
|
+
function isIdentKey(k) {
|
|
36
|
+
if (k.length === 0) return false;
|
|
37
|
+
const f = k.charCodeAt(0);
|
|
38
|
+
if (!((f >= 65 && f <= 90) || (f >= 97 && f <= 122) || f === 95)) return false;
|
|
39
|
+
for (let i = 1; i < k.length; i++) {
|
|
40
|
+
const c = k.charCodeAt(i);
|
|
41
|
+
if (!((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c === 95)) return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
function pathKey(base, key) {
|
|
46
|
+
return base + (isIdentKey(key) ? '.' + key : '[' + JSON.stringify(key) + ']');
|
|
47
|
+
}
|
|
48
|
+
function pathIndex(base, i) { return base + '[' + i + ']'; }
|
|
49
|
+
|
|
50
|
+
// Parse a path into segments. Returns null if malformed.
|
|
51
|
+
function parsePath(p) {
|
|
52
|
+
if (p.charCodeAt(0) !== 36 /* $ */) return null;
|
|
53
|
+
const segs = [];
|
|
54
|
+
let i = 1;
|
|
55
|
+
const n = p.length;
|
|
56
|
+
while (i < n) {
|
|
57
|
+
const c = p.charCodeAt(i);
|
|
58
|
+
if (c === 46 /* . */) {
|
|
59
|
+
i++;
|
|
60
|
+
const st = i;
|
|
61
|
+
while (i < n) { const d = p.charCodeAt(i); if (d === 46 || d === 91) break; i++; }
|
|
62
|
+
segs.push(p.slice(st, i));
|
|
63
|
+
} else if (c === 91 /* [ */) {
|
|
64
|
+
i++;
|
|
65
|
+
if (p.charCodeAt(i) === 34 /* " */) {
|
|
66
|
+
// quoted key — find the matching close quote, honouring escapes
|
|
67
|
+
let out = '', seg = ++i;
|
|
68
|
+
for (;;) {
|
|
69
|
+
if (i >= n) return null;
|
|
70
|
+
const d = p.charCodeAt(i);
|
|
71
|
+
if (d === 34) { out += p.slice(seg, i); i++; break; }
|
|
72
|
+
if (d === 92) { out += p.slice(seg, i); i++; out += p[i]; i++; seg = i; }
|
|
73
|
+
else i++;
|
|
74
|
+
}
|
|
75
|
+
try { segs.push(JSON.parse('"' + out.replace(/"/g, '\\"') + '"')); }
|
|
76
|
+
catch (e) { segs.push(out); }
|
|
77
|
+
if (p.charCodeAt(i) !== 93) return null;
|
|
78
|
+
i++;
|
|
79
|
+
} else {
|
|
80
|
+
const st = i;
|
|
81
|
+
while (i < n && p.charCodeAt(i) !== 93) i++;
|
|
82
|
+
if (i >= n) return null;
|
|
83
|
+
segs.push(Number(p.slice(st, i)));
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
} else return null;
|
|
87
|
+
}
|
|
88
|
+
return segs;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------- decode
|
|
92
|
+
// Compiled row constructors, cached across documents by shape.
|
|
93
|
+
const ctorCache = new Map();
|
|
94
|
+
function getCtor(fields, dictFlags, dicts) {
|
|
95
|
+
const key = dictFlags.join('') + '|' + fields.join(',');
|
|
96
|
+
let fac = ctorCache.get(key);
|
|
97
|
+
if (!fac) {
|
|
98
|
+
let body = 'return function(r){return {';
|
|
99
|
+
for (let k = 0; k < fields.length; k++) {
|
|
100
|
+
if (k) body += ',';
|
|
101
|
+
body += JSON.stringify(fields[k]) + ':' + (dictFlags[k] === 1 ? ('D[' + k + '][r[' + k + ']]') : ('r[' + k + ']'));
|
|
102
|
+
}
|
|
103
|
+
body += '}}';
|
|
104
|
+
fac = new Function('D', body);
|
|
105
|
+
ctorCache.set(key, fac);
|
|
106
|
+
}
|
|
107
|
+
return fac(dicts);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Returns the decoded value, or undefined if this document is not table-mode
|
|
111
|
+
// (or mixes tables with class instantiations, which needs the trampoline).
|
|
112
|
+
function decode(text, pre) {
|
|
113
|
+
pre = pre || parsePrelude(text);
|
|
114
|
+
const tables = pre.tables;
|
|
115
|
+
if (!tables || tables.length === 0) return undefined;
|
|
116
|
+
// The real precondition is simply "the body is plain JSON". A body that still
|
|
117
|
+
// contains `Ck(...)` instantiations cannot parse as JSON, so JSON.parse
|
|
118
|
+
// succeeding IS the proof — no separate bookkeeping needed, and it allows
|
|
119
|
+
// documents that declare classes used only inside table rows.
|
|
120
|
+
const body = text.slice(pre.end);
|
|
121
|
+
let root;
|
|
122
|
+
try { root = JSON.parse(body); } catch (e) { return undefined; }
|
|
123
|
+
|
|
124
|
+
for (let t = 0; t < tables.length; t++) {
|
|
125
|
+
const decl = tables[t];
|
|
126
|
+
const cls = pre.classes[decl.cls];
|
|
127
|
+
if (!cls) return undefined;
|
|
128
|
+
const segs = parsePath(decl.path);
|
|
129
|
+
if (segs === null) return undefined;
|
|
130
|
+
|
|
131
|
+
// navigate to the parent container so the table can be replaced in place
|
|
132
|
+
let parent = null, lastSeg = null, cur = root;
|
|
133
|
+
for (let q = 0; q < segs.length; q++) {
|
|
134
|
+
if (cur === null || typeof cur !== 'object') return undefined;
|
|
135
|
+
parent = cur; lastSeg = segs[q]; cur = cur[segs[q]];
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(cur)) return undefined;
|
|
138
|
+
|
|
139
|
+
const fields = cls.f, dicts = cls.d;
|
|
140
|
+
const nf = fields.length;
|
|
141
|
+
const dictFlags = new Array(nf);
|
|
142
|
+
let hasDict = false;
|
|
143
|
+
for (let k = 0; k < nf; k++) { dictFlags[k] = dicts[k] === null ? 0 : 1; if (dictFlags[k]) hasDict = true; }
|
|
144
|
+
const ctor = getCtor(fields, dictFlags, hasDict ? dicts : null);
|
|
145
|
+
|
|
146
|
+
const n = cur.length;
|
|
147
|
+
const out = new Array(n);
|
|
148
|
+
for (let i = 0; i < n; i++) {
|
|
149
|
+
const row = cur[i];
|
|
150
|
+
if (!Array.isArray(row) || row.length !== nf) return undefined;
|
|
151
|
+
out[i] = ctor(row);
|
|
152
|
+
}
|
|
153
|
+
if (parent === null) root = out;
|
|
154
|
+
else parent[lastSeg] = out;
|
|
155
|
+
}
|
|
156
|
+
return root;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { decode, parsePath, pathKey, pathIndex, isIdentKey };
|