@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Teodoroiu / Human Synthesis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PERFORMANCE.md ADDED
@@ -0,0 +1,179 @@
1
+ # APITRON — ghid de performanță maximă
2
+
3
+ Toate cifrele de mai jos sunt măsurate (Node v22, Xeon 2 nuclee, benchmark
4
+ pereche-întrețesut față de JSON nativ, mediana a 15 runde). **Raportul este
5
+ TRON/JSON — sub 1.00 înseamnă că TRON e mai rapid.**
6
+
7
+ ---
8
+
9
+ ## 1. Alege modul corect — asta contează cel mai mult
10
+
11
+ | situație | folosește | decode | encode | round-trip | tokeni |
12
+ |---|---|---:|---:|---:|---:|
13
+ | API intern (ambele capete codul tău) | **`defineSchema`** | 0.40–0.87x | 0.66–0.93x | 0.57–0.91x | 11–61% |
14
+ | Prompt LLM / consumator terț | **`encode`/`decode`** | 0.45–0.77x | 0.97–1.77x | 0.76–1.22x | 26–61% |
15
+ | Date numerice/columnare | **`decodeColumnar`** | ~0.30x | — | — | 35% |
16
+ | Payload sub ~1 KB | **JSON simplu** | — | — | — | 0% |
17
+
18
+ Diferența dintre modul cu schemă și cel auto-descriptiv este **cea mai mare
19
+ optimizare disponibilă** — mai mare decât orice reglaj fin. Modul cu schemă
20
+ câștigă pe toate axele, la orice dimensiune.
21
+
22
+ ---
23
+
24
+ ## 2. PRELOAD o singură dată, la pornirea aplicației
25
+
26
+ Greșeala numărul unu este să compilezi schema la fiecare cerere. `defineSchema`
27
+ face muncă reală: rezolvă ordinea câmpurilor, tabelele de enum, calea și
28
+ **compilează constructorul de rând cu `new Function`**. Fă asta o singură dată.
29
+
30
+ ```js
31
+ // schemas.js — modul la nivel de aplicație, evaluat o singură dată
32
+ const { defineSchema } = require('apitron');
33
+
34
+ exports.users = defineSchema({
35
+ id: 'users.v1',
36
+ fields: ['id', 'name', 'role', 'region', 'score', 'active'],
37
+ enums: {
38
+ role: ['admin', 'user', 'editor'],
39
+ region: ['us-east-1', 'eu-west-1'],
40
+ active: [false, true],
41
+ },
42
+ path: '$.data',
43
+ });
44
+ ```
45
+
46
+ ```js
47
+ // handler — per cerere, doar encode/decode
48
+ const { users } = require('./schemas');
49
+ app.get('/users', (req, res) => {
50
+ res.type('text/plain').send(users.encode({ meta: {...}, data: rows }));
51
+ });
52
+ ```
53
+
54
+ **Efect măsurat** (envelope, 25 de rânduri): decode 1.21x → **0.63x**,
55
+ encode 2.49x → **0.90x**. Pe payload-uri mici, compilarea per-cerere e diferența
56
+ dintre a pierde și a câștiga.
57
+
58
+ ---
59
+
60
+ ## 3. Declară `enums` pentru coloanele cu puține valori distincte
61
+
62
+ Orice coloană de string/boolean cu cardinalitate mică (status, rol, regiune,
63
+ tip, flag-uri) devine un întreg pe fir.
64
+
65
+ ```js
66
+ enums: { status: ['ok', 'retried', 'failed'], active: [false, true] }
67
+ ```
68
+
69
+ Trei efecte simultane:
70
+ 1. **Mai puțini tokeni** — `events-10k`: 38% → **42%**.
71
+ 2. **Decode mai rapid** — nu se mai alocă string-uri pentru coloana aceea.
72
+ 3. **Deblochează WASM** — o coloană de tip dicționar este un întreg pe fir, deci
73
+ un tabel cu string-uri devine eligibil pentru scanerul WASM. `tabular-10k`
74
+ este *respins* de WASM fără dicționare și **acceptat** cu ele.
75
+
76
+ **Restricție:** valorile din `enums` trebuie să fie string sau boolean.
77
+ Valorile numerice sunt respinse explicit — ar fi ambigue cu indicii de dicționar
78
+ și s-ar decoda ca `undefined`.
79
+
80
+ ---
81
+
82
+ ## 4. Nu codifica payload-uri mici
83
+
84
+ `encode()` are deja un prag de 1 KB și returnează JSON simplu sub el; `decode()`
85
+ îl citește transparent. Nu-l dezactiva fără motiv.
86
+
87
+ | rânduri | JSON | decode | round-trip |
88
+ |---:|---:|---:|---:|
89
+ | 5 | 0.5 KB | 4.80x | 5.16x |
90
+ | 25 | 2.1 KB | 1.21x | 1.70x |
91
+ | 100 | 8.4 KB | **0.78x** | 1.16x |
92
+ | 1000 | 86 KB | **0.68x** | **1.01x** |
93
+ | 10000 | 881 KB | **0.67x** | **0.94x** |
94
+
95
+ (Modul auto-descriptiv. Cu schemă preîncărcată pragul dispare — vezi §1.)
96
+
97
+ ---
98
+
99
+ ## 5. Când datele sunt numerice, ia banda columnară
100
+
101
+ Cea mai rapidă cale din bibliotecă. Nu construiește deloc obiecte JS.
102
+
103
+ **Atenție:** trebuie emis cu `encodeColumnar()`. Ieșirea implicită a lui
104
+ `encode()` conține declarații `table` (corp JSON simplu), pe care scanerul WASM
105
+ nu le acceptă — `decodeColumnar` va returna `undefined` pentru ea.
106
+
107
+ ```js
108
+ const wire = encodeColumnar(rows); // server
109
+ const col = decodeColumnar(wire, /* copy */ true); // client
110
+ if (col) {
111
+ // acces row-major: valoarea coloanei c din rândul r
112
+ const v = col.tape[r * col.cols + c];
113
+ } else {
114
+ const obj = decode(wire); // nu era eligibil, cale normală
115
+ }
116
+ ```
117
+
118
+ **~0.30x față de `JSON.parse`** (de ~3.3 ori mai rapid). `copy: true` detașează
119
+ banda din memoria WASM — fără el, următorul apel de decode o invalidează.
120
+
121
+ ---
122
+
123
+ ## 6. Reține că `table: 'nested'` schimbă viteza pe tokeni
124
+
125
+ Implicit (`table: true`) tabelizează doar rândurile plate: câștig pur, și mai
126
+ rapid și mai puțini tokeni. `'nested'` acceptă și rânduri cu obiecte imbricate —
127
+ mai rapid, dar formele interioare își pierd compresia proprie.
128
+
129
+ Măsurat pe `nested-500`: round-trip 2.47x → **0.89x**, dar tokenii scad de la
130
+ **26% → 11%**. Alege în funcție de ce plătești: latență sau tokeni.
131
+
132
+ ---
133
+
134
+ ## 7. Detalii de runtime care contează
135
+
136
+ - **`new Function`** — trampolina și constructorii de rând îl folosesc. Sub un
137
+ CSP strict fără `unsafe-eval` biblioteca revine automat la scaner
138
+ (`parseFast`), ~1.1–1.5x. Funcționează corect, doar mai lent.
139
+ - **WASM este opțional.** În Node se încarcă singur. În browser trimite tu
140
+ binarul: `setWasmBinary(await (await fetch('/parserTron2.wasm')).arrayBuffer())`.
141
+ Fără el nimic nu se strică — dispecerul pur și simplu nu ia calea WASM.
142
+ - **Trimite `Content-Type: text/plain`** (sau un tip propriu, ex.
143
+ `application/vnd.tron`). Nu `application/json` — corpul poate conține un
144
+ preambul care nu e JSON valid.
145
+ - **Compresia HTTP se aplică în continuare.** TRON reduce octeții *înainte* de
146
+ gzip/brotli; câștigurile se compun, dar mai puțin decât liniar.
147
+ - **Nu re-encoda ce nu s-a schimbat.** Răspunsurile cacheabile se codifică o
148
+ dată; costul de encode dispare complet și rămâne doar câștigul la decode.
149
+
150
+ ---
151
+
152
+ ## 8. Listă de verificare
153
+
154
+ - [ ] `defineSchema` apelat o singură dată, la pornire, nu per cerere
155
+ - [ ] `enums` declarate pentru toate coloanele categorice și booleene
156
+ - [ ] Payload-urile sub ~1 KB rămân JSON (pragul implicit se ocupă)
157
+ - [ ] `encodeColumnar` + `decodeColumnar` folosite împreună acolo unde datele sunt numerice
158
+ - [ ] `Content-Type` setat pe `text/plain`, nu `application/json`
159
+ - [ ] În browser: `setWasmBinary` apelat, sau acceptat conștient fallback-ul
160
+ - [ ] Măsoară cu propriile date înainte de a te baza pe aceste cifre
161
+
162
+ ---
163
+
164
+ ## 9. Metodologie (ca să poți reproduce)
165
+
166
+ Benchmark-ul naiv dădea rezultate contradictorii — două căi de cod identice
167
+ difereau cu 43% — pentru că rularea mai multor decodoare în același proces face
168
+ inline cache-urile V8 polimorfe. Cifrele de aici sunt măsurate cu:
169
+
170
+ - **un proces copil per set de date**, ca V8 să optimizeze pentru o singură formă;
171
+ - **pereche întrețesută** față de echivalentul JSON nativ (A/B/A/B, cu ordinea
172
+ alternată), ca ambele părți să vadă aceleași condiții;
173
+ - **mediana a 15 runde**.
174
+
175
+ Milisecundele absolute depind de mașină; **rapoartele** sunt semnalul.
176
+
177
+ Corectitudinea: **220.118 verificări** adversariale și fuzz pe suita completă,
178
+ plus 22 de teste dedicate modului cu schemă și 1.500 de round-trip-uri fuzz — 0
179
+ eșecuri.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @human-synthesis/norns-tron
2
+
3
+ TRON serialization for the Norns ecosystem — a token-efficient, faster-than-JSON
4
+ wire format for APIs and LLM-facing output. Zero runtime dependencies.
5
+
6
+ TRON cuts **11–61% of tokens** vs JSON and, used correctly, beats
7
+ `JSON.parse` / `JSON.stringify` on decode, encode, and round-trip. Plain JSON is
8
+ valid TRON, so adoption is progressive and rollback is trivial. See
9
+ [PERFORMANCE.md](./PERFORMANCE.md) for measured numbers and usage guidance.
10
+
11
+ The encoder/decoder core is absorbed from the `apitron` research library; this
12
+ package adds the Norns framework glue as subpath exports.
13
+
14
+ ```
15
+ @human-synthesis/norns-tron encode / decode / defineSchema / registry
16
+ @human-synthesis/norns-tron/server tronSerializer() for norns route()
17
+ @human-synthesis/norns-tron/client api fetch wrapper that speaks TRON
18
+ @human-synthesis/norns-tron/valibot derive wire schemas from valibot schemas
19
+ ```
20
+
21
+ ## Turn it on app-wide
22
+
23
+ ```coffee
24
+ # src/hooks.server.c
25
+ import { boot } from '@human-synthesis/norns/server'
26
+ import { tronSerializer } from '@human-synthesis/norns-tron/server'
27
+
28
+ app := await boot
29
+ features: import.meta.glob('./lib/*/server/module.c', eager: true)
30
+ serializer: tronSerializer()
31
+ ```
32
+
33
+ Every `route()` response is now content-negotiated: clients that send
34
+ `Accept: application/tron` get TRON, everyone else (curl, third parties,
35
+ existing code) keeps getting JSON. Nothing breaks. Remove the `serializer`
36
+ line to roll the whole thing back.
37
+
38
+ Per-route control:
39
+
40
+ ```coffee
41
+ export GET := route serializer: tronSerializer({ schema: noteWire }), handler: ...
42
+ export POST := route serializer: null, handler: ... # force plain JSON
43
+ ```
44
+
45
+ ## Call it from the client
46
+
47
+ ```coffee
48
+ import { api, createApi } from '@human-synthesis/norns-tron/client'
49
+
50
+ users := await api.get '/api/users' # sends Accept: application/tron
51
+ await api.post '/api/notes', { title, body } # body goes out as TRON too
52
+
53
+ # inside a load function, keep SvelteKit's fetch semantics:
54
+ api := createApi { fetch }
55
+ ```
56
+
57
+ The 1.7 KB WASM scanner ships embedded as base64 — no asset wiring, works in
58
+ Node, Bun, and the browser. Under a strict CSP without `unsafe-eval` the
59
+ decoder transparently falls back to the JS scanner (~1.1–1.5x).
60
+
61
+ ## Schema mode — the fastest path for internal endpoints
62
+
63
+ Both ends already know the shape from the feature contract, so nothing
64
+ descriptive needs to travel. Derive the wire schema from the valibot schema
65
+ you already have in `shared/schema.c`:
66
+
67
+ ```coffee
68
+ # src/lib/notes/shared/schema.c
69
+ import * as v from 'valibot'
70
+ import { tronSchemaFromValibot } from '@human-synthesis/norns-tron/valibot'
71
+
72
+ export noteSchema := v.object
73
+ id: v.number()
74
+ title: v.string()
75
+ status: v.picklist ['draft', 'published']
76
+
77
+ export noteWire := tronSchemaFromValibot noteSchema, { id: 'notes.v1', path: '$.data' }
78
+ ```
79
+
80
+ `picklist`/`enum` fields become dictionary columns (integers on the wire),
81
+ booleans become 0/1. Compile once at module scope — never per request.
82
+ The `#notes.v1` tag makes version mismatches fail loudly instead of
83
+ misdecoding; use `createRegistry()` on a client that consumes several shapes.
84
+
85
+ ## Semantics and limits
86
+
87
+ - Same value semantics as JSON: `toJSON()` is honored, so a `Date` arrives as
88
+ its ISO string (not a `Date` — same as `response.json()`). `Map`/`Set`
89
+ serialize as `{}` and `BigInt` throws, exactly like `JSON.stringify`; dev
90
+ mode logs a warning when a route returns them.
91
+ - **Not for `load` / form actions** — SvelteKit serializes those with devalue
92
+ (which preserves Dates, Maps, Sets). This package targets `route()`
93
+ endpoints, LLM-facing output, and service-to-service payloads.
94
+ - Payloads under ~1 KB are emitted as plain JSON automatically (still decoded
95
+ transparently) — below that size TRON's fixed costs don't pay for themselves.
96
+ - The wire content type is `application/tron`, not `application/json`: a TRON
97
+ body may carry a declaration preamble that is not valid JSON.
98
+ - Schema mode uses `new Function` for the row constructor (server-side this is
99
+ a non-issue; in CSP-restricted browsers the fallback scanner takes over).
100
+
101
+ ## Development
102
+
103
+ ```sh
104
+ bun test # 38 tests: core roundtrips, Date regression, server/client glue, valibot derivation
105
+ bun run embed-wasm # regenerate src/core/wasm-bytes.js after replacing wasm/parserTron2.wasm
106
+ ```
107
+
108
+ License: MIT.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@human-synthesis/norns-tron",
3
+ "version": "0.0.1",
4
+ "description": "TRON serialization for the Norns ecosystem — token-efficient, faster-than-JSON wire format for APIs and LLM-facing output.",
5
+ "license": "MIT",
6
+ "author": "Daniel Teodoroiu (https://humansynthesis.ai)",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/human-synthesis/norns-tron.git"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "wasm",
15
+ "README.md",
16
+ "PERFORMANCE.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "bun test",
20
+ "embed-wasm": "bun scripts/embed-wasm.mjs"
21
+ },
22
+ "exports": {
23
+ ".": "./src/index.js",
24
+ "./server": "./src/server.js",
25
+ "./client": "./src/client.js",
26
+ "./valibot": "./src/valibot.js",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "peerDependencies": {
30
+ "valibot": ">=0.31.0"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "valibot": {
34
+ "optional": true
35
+ }
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,39 @@
1
+ export const TRON_CONTENT_TYPE: 'application/tron';
2
+
3
+ export class ApiError extends Error {
4
+ status: number;
5
+ body: unknown;
6
+ response: Response;
7
+ }
8
+
9
+ /** Decode a Response by its content-type (TRON, JSON, or raw text). */
10
+ export function parseResponse(res: Response): Promise<unknown>;
11
+
12
+ export interface ApiDefaults {
13
+ /** fetch impl — pass SvelteKit's `fetch` inside load functions. */
14
+ fetch?: typeof fetch;
15
+ /** URL prefix, e.g. 'https://api.example.com'. */
16
+ base?: string;
17
+ /** Headers sent on every request. */
18
+ headers?: Record<string, string>;
19
+ }
20
+
21
+ export interface RequestOptions {
22
+ fetch?: typeof fetch;
23
+ headers?: Record<string, string>;
24
+ init?: RequestInit;
25
+ }
26
+
27
+ export interface Api {
28
+ request(method: string, url: string, body?: unknown, opts?: RequestOptions): Promise<any>;
29
+ get(url: string, opts?: RequestOptions): Promise<any>;
30
+ del(url: string, opts?: RequestOptions): Promise<any>;
31
+ post(url: string, body?: unknown, opts?: RequestOptions): Promise<any>;
32
+ put(url: string, body?: unknown, opts?: RequestOptions): Promise<any>;
33
+ patch(url: string, body?: unknown, opts?: RequestOptions): Promise<any>;
34
+ }
35
+
36
+ export function createApi(defaults?: ApiDefaults): Api;
37
+
38
+ /** Default instance for browser code. In load functions use createApi({ fetch }). */
39
+ export const api: Api;
package/src/client.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Client fetch wrapper that speaks TRON with a norns `route()` endpoint.
3
+ *
4
+ * import { api } from '@human-synthesis/norns-tron/client';
5
+ * const users = await api.get('/api/users');
6
+ * await api.post('/api/notes', { title, body });
7
+ *
8
+ * In a load function, pass SvelteKit's fetch so relative URLs and SSR
9
+ * de-duplication keep working:
10
+ * const api = createApi({ fetch });
11
+ *
12
+ * The WASM scanner ships embedded (~2.3 KB base64) and initializes lazily, so
13
+ * no asset wiring is needed in the browser. Under a strict CSP without
14
+ * `unsafe-eval` the decoder transparently falls back to the JS scanner.
15
+ */
16
+ import { decode, encode } from './index.js';
17
+
18
+ export const TRON_CONTENT_TYPE = 'application/tron';
19
+ const ACCEPT = 'application/tron, application/json;q=0.9, */*;q=0.1';
20
+
21
+ export class ApiError extends Error {
22
+ /**
23
+ * @param {number} status
24
+ * @param {any} body decoded error body (message/issues from route())
25
+ * @param {Response} response
26
+ */
27
+ constructor(status, body, response) {
28
+ super(body?.message ?? `HTTP ${status}`);
29
+ this.name = 'ApiError';
30
+ this.status = status;
31
+ this.body = body;
32
+ this.response = response;
33
+ }
34
+ }
35
+
36
+ /** Decode a Response by its content-type (TRON, JSON, or raw text). */
37
+ export async function parseResponse(res) {
38
+ if (res.status === 204) return null;
39
+ const type = res.headers.get('content-type')?.split(';', 1)[0]?.trim() ?? '';
40
+ const text = await res.text();
41
+ if (text === '') return null;
42
+ if (type === TRON_CONTENT_TYPE) return decode(text);
43
+ if (type === 'application/json' || type.endsWith('+json')) return JSON.parse(text);
44
+ return text;
45
+ }
46
+
47
+ /**
48
+ * @param {object} [defaults]
49
+ * @param {typeof fetch} [defaults.fetch] fetch impl (pass SvelteKit's in load functions)
50
+ * @param {string} [defaults.base] URL prefix, e.g. 'https://api.example.com'
51
+ * @param {Record<string, string>} [defaults.headers] headers sent on every request
52
+ */
53
+ export function createApi(defaults = {}) {
54
+ const base = defaults.base ?? '';
55
+
56
+ async function request(method, url, body, opts = {}) {
57
+ // Resolve fetch per call: in the browser `globalThis.fetch` must not be
58
+ // captured at module-import time (SSR would freeze the server's fetch).
59
+ const doFetch = opts.fetch ?? defaults.fetch ?? globalThis.fetch;
60
+ const headers = { accept: ACCEPT, ...defaults.headers, ...opts.headers };
61
+ /** @type {RequestInit} */
62
+ const init = { method, headers, ...opts.init };
63
+ if (body !== undefined) {
64
+ // encode() emits plain JSON under 1 KB — still valid TRON, and the
65
+ // server's parseBody reads both, so the content type stays stable.
66
+ headers['content-type'] = TRON_CONTENT_TYPE;
67
+ init.body = encode(body);
68
+ }
69
+ const res = await doFetch(base + url, init);
70
+ const parsed = await parseResponse(res);
71
+ if (!res.ok) throw new ApiError(res.status, parsed, res);
72
+ return parsed;
73
+ }
74
+
75
+ return {
76
+ request,
77
+ get: (url, opts) => request('GET', url, undefined, opts),
78
+ del: (url, opts) => request('DELETE', url, undefined, opts),
79
+ post: (url, body, opts) => request('POST', url, body, opts),
80
+ put: (url, body, opts) => request('PUT', url, body, opts),
81
+ patch: (url, body, opts) => request('PATCH', url, body, opts)
82
+ };
83
+ }
84
+
85
+ /** Default instance for browser code. In load functions use createApi({ fetch }). */
86
+ export const api = createApi();
@@ -0,0 +1,85 @@
1
+ // Auto-dispatching decoder. No single strategy wins on every shape, so pick
2
+ // per document from cheap signals available in the prelude + a short probe:
3
+ //
4
+ // numeric columnar, all classed -> WASM tape (4x JSON.parse)
5
+ // single/multi-class uniform tables -> trampoline (0.7-0.9x)
6
+ // nested / irregular / tiny -> parseFast scanner (~1.2x)
7
+ //
8
+ // The probe reads only the class declarations and a small prefix of the body,
9
+ // never the whole payload, so dispatch cost is O(prelude), not O(document).
10
+
11
+ import * as TRON from './tron.js';
12
+ import * as TRAMP from './tron-trampoline.js';
13
+ import * as WASM from './tron-wasm.js';
14
+ import * as TBL from './tron-table.js';
15
+
16
+ const SMALL_DOC = 4096; // below this, native JSON.parse setup dominates
17
+ const PROBE_ROWS = 3;
18
+
19
+ function pickStrategy(text) {
20
+ const pre = TRON.parsePrelude(text);
21
+ const names = Object.keys(pre.classes);
22
+ if (names.length === 0) return 'json'; // plain JSON body
23
+ if (text.length < SMALL_DOC) return 'fast';
24
+ if (text.indexOf('\u0001') !== -1) return 'fast';
25
+
26
+ // Several classes means heterogeneous, nesting-heavy data: the trampoline's
27
+ // generic walk loses badly there (measured 1.8x) while the scanner stays
28
+ // ~1.1x. One class means a dominant uniform run - the trampoline's home turf.
29
+ if (names.length > 1) return 'fast';
30
+
31
+ // Body must look like a root array of instances for the tight paths.
32
+ const body = text.slice(pre.end);
33
+ if (body.charCodeAt(0) !== 91 /* [ */) return 'tramp-generic';
34
+
35
+ // Probe the first few rows for non-scalar fields (nested objects/arrays).
36
+ let i = 1, rows = 0, sawNonScalar = false;
37
+ while (rows < PROBE_ROWS && i < body.length) {
38
+ const open = body.indexOf('(', i);
39
+ if (open === -1) break;
40
+ const close = body.indexOf(')', open);
41
+ if (close === -1) break;
42
+ const row = body.slice(open + 1, close);
43
+ if (row.indexOf('[') !== -1 || row.indexOf('{') !== -1) sawNonScalar = true;
44
+ i = close + 1; rows++;
45
+ }
46
+ if (sawNonScalar) return 'tramp-generic';
47
+ return 'tramp';
48
+ }
49
+
50
+ function decode(text, opts) {
51
+ // TABLE MODE first: if the document declares where its tables live, decoding
52
+ // is JSON.parse + a constructor loop with no text transform at all.
53
+ const pre = TRON.parsePrelude(text);
54
+ if (pre.tables && pre.tables.length > 0) {
55
+ const t = TBL.decode(text, pre);
56
+ if (t !== undefined) return t;
57
+ }
58
+ const strategy = (opts && opts.strategy) || pickStrategy(text);
59
+ switch (strategy) {
60
+ case 'json': return JSON.parse(text.slice(TRON.parsePrelude(text).end) || text);
61
+ case 'fast': return TRON.parseFast(text);
62
+ case 'tramp': {
63
+ // Try WASM first for uniform single-class rows. The WASM scanner
64
+ // validates and rejects anything it cannot represent exactly, so an
65
+ // undefined return is a definitive "not eligible", not a guess.
66
+ if (!(opts && opts.noWasm) && !(opts && opts.lazy)) {
67
+ const w = WASM.decode(text);
68
+ if (w !== undefined) return w;
69
+ }
70
+ return (opts && opts.lazy) ? TRAMP.decodeLazy(text) : TRAMP.decode(text);
71
+ }
72
+ case 'tramp-generic': return TRAMP.decode(text);
73
+ default: return TRON.parseFast(text);
74
+ }
75
+ }
76
+
77
+ // Columnar decode: returns a Float64Array view instead of JS objects for
78
+ // all-numeric (or fully dictionary-encoded) tables. Undefined when the payload
79
+ // is not eligible. This is the mode that runs ~4x faster than JSON.parse.
80
+ function decodeColumnar(text, copy) {
81
+ return WASM.decodeColumnar(text, copy);
82
+ }
83
+
84
+ const wasmAvailable = () => WASM.available();
85
+ export { decode, decodeColumnar, pickStrategy, wasmAvailable };