@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.
@@ -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 };