fastobjectnotation 0.3.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,19 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # FON — Fast Object Notation
2
+
3
+ [![CI](https://github.com/FastObjectNotation/FON.js/actions/workflows/ci.yml/badge.svg)](https://github.com/FastObjectNotation/FON.js/actions/workflows/ci.yml)
4
+
5
+ A fast, human-readable, line-oriented serialization format — a compact
6
+ alternative to JSON for record-style data. Each line is one record; values are
7
+ typed and can nest.
8
+
9
+ ## Features
10
+
11
+ - **Compact, readable wire format** — `key=type:value` pairs, one record per line.
12
+ - **Typed values** — numeric/bool/string primitives, binary blobs, nested
13
+ objects, and arrays of any of them.
14
+ - **Nested objects & arrays of objects**, with a configurable maximum depth.
15
+ - **Parallel** dump serialization and deserialization (Rayon-backed, via `maxThreads` arg).
16
+ - **Byte-oriented parsing** — reads straight from raw UTF-8 bytes.
17
+ - **Z85 binary encoding** for raw blobs (5 ASCII chars per 4 bytes).
18
+
19
+ ## Format
20
+
21
+ Each line is one record: a comma-separated list of `key=type:value` pairs. A
22
+ `.fon` file is a sequence of records, indexed by line number (0-based).
23
+
24
+ ```
25
+ name=s:"John",age=i:30,balance=d:1234.56
26
+ scores=i:[95,87,92],tags=s:["admin","user"]
27
+ user=o:{id=i:42,name=s:"Bob",addr=o:{city=s:"NY",zip=i:10001}}
28
+ items=o:[{id=i:1,qty=i:5},{id=i:2,qty=i:3}]
29
+ blob=r:"nm=QNzv..."
30
+ ```
31
+
32
+ ### Type codes
33
+
34
+ | Code | Type | Example |
35
+ |------|-----------------|-------------------------------|
36
+ | `e` | `u8` | `count=e:255` |
37
+ | `t` | `i16` | `year=t:2024` |
38
+ | `i` | `i32` | `id=i:42` |
39
+ | `u` | `u32` | `flags=u:12345` |
40
+ | `l` | `i64` | `ts=l:1700000000` |
41
+ | `g` | `u64` | `big=g:18446744073709551615` |
42
+ | `f` | `f32` | `ratio=f:3.14` |
43
+ | `d` | `f64` | `pi=d:3.141592653589793` |
44
+ | `s` | `String` | `name=s:"Hello"` |
45
+ | `b` | `bool` | `active=b:1` |
46
+ | `r` | `RawData` (Z85) | `data=r:"nm=QNzv"` |
47
+ | `o` | `FonCollection` | `user=o:{id=i:1}` |
48
+
49
+ Every primitive and string type also has an array form (`xs=i:[1,2,3]`), and `o`
50
+ supports both nested objects (`{...}`) and arrays of objects (`[{...},{...}]`).
51
+ Strings are double-quoted with `\n \r \t \b \f \" \\` escapes.
52
+
53
+ ## Install
54
+
55
+ ```bash
56
+ npm install fastobjectnotation
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### A single record
62
+
63
+ ```js
64
+ import { FonCollection, nativeVersion } from 'FastObjectNotation';
65
+
66
+ console.log(nativeVersion()); // "0.3.0"
67
+
68
+ const col = new FonCollection();
69
+ col.addInt('id', 42)
70
+ .addString('name', 'Test Item')
71
+ .addDouble('price', 99.99)
72
+ .addBool('active', true)
73
+ .addLong('timestamp', 1700000000n);
74
+
75
+ const line = col.serialize();
76
+ // id=i:42,name=s:"Test Item",price=d:99.99,active=b:1,timestamp=l:1700000000
77
+
78
+ const parsed = FonCollection.deserialize(line);
79
+ console.log(parsed.getInt('id')); // 42
80
+ console.log(parsed.getString('name')); // "Test Item"
81
+ console.log(parsed.getDouble('price')); // 99.99
82
+ console.log(parsed.getBool('active')); // true
83
+ console.log(parsed.getLong('timestamp')); // 1700000000n (BigInt)
84
+
85
+ col.free();
86
+ parsed.free();
87
+ ```
88
+
89
+ ### Many records (FonDump)
90
+
91
+ ```js
92
+ import { FonCollection, FonDump } from 'FastObjectNotation';
93
+
94
+ const dump = new FonDump();
95
+
96
+ for (let i = 0; i < 1000; i++) {
97
+ const col = new FonCollection();
98
+ col.addInt('id', i).addString('text', `row ${i}`);
99
+ dump.add(BigInt(i), col); // ownership transfers; col.disposed = true
100
+ }
101
+
102
+ const fon = dump.serialize(0); // 0 = use default thread pool
103
+
104
+ const loaded = FonDump.deserialize(fon);
105
+ console.log(loaded.size); // 1000
106
+
107
+ const row5 = loaded.get(5n); // borrowed — do NOT call row5.free()
108
+ console.log(row5.getString('text')); // "row 5"
109
+
110
+ dump.free();
111
+ loaded.free();
112
+ ```
113
+
114
+ ### Ownership rules
115
+
116
+ | Call | Effect |
117
+ |------|--------|
118
+ | `dump.add(id, col)` | `col` is consumed — `col.disposed` becomes `true`. Do NOT free or use `col`. |
119
+ | `dump.get(index)` | Returns a **borrowed** handle owned by `dump`. Do NOT call `.free()` on it. |
120
+ | `col.free()` | Releases memory. Safe to call only on collections you own. |
121
+ | `dump.free()` | Releases memory for the dump and all its collections. |
122
+
123
+ ## Build
124
+
125
+ ```bash
126
+ git submodule update --init --recursive
127
+ cargo build --release --manifest-path native/Cargo.toml
128
+ npm install
129
+ npm test
130
+ ```
131
+
132
+ The compiled library will be at `native/target/release/fon_native.dll` (Windows),
133
+ `libfon_native.so` (Linux), or `libfon_native.dylib` (macOS).
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "fastobjectnotation",
3
+ "version": "0.3.1",
4
+ "description": "Fast, human-readable, line-oriented serialization format — a compact alternative to JSON for record-style data",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "src/index.js",
16
+ "src/index.d.ts"
17
+ ],
18
+ "scripts": {
19
+ "test": "node --test test/roundtrip.test.js",
20
+ "build": "cargo build --release --manifest-path native/Cargo.toml"
21
+ },
22
+ "keywords": [
23
+ "fon",
24
+ "fast-object-notation",
25
+ "serialization"
26
+ ],
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "koffi": "^2.9.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/FastObjectNotation/FON.js.git"
37
+ }
38
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,153 @@
1
+ // Type declarations for FastObjectNotation
2
+
3
+ /**
4
+ * Wraps a heap-allocated FonCollection handle.
5
+ *
6
+ * Ownership rules:
7
+ * - Call `free()` when you own the collection and are done with it.
8
+ * - After `addCollection`, `addCollectionArray`, or `FonDump.add`, the handle is
9
+ * transferred and `disposed` is set to `true`. Do not use or free it afterwards.
10
+ * - Handles returned by `getCollection`, `getCollectionArray`, and `FonDump.get`
11
+ * are BORROWED — do NOT call `free()` on them.
12
+ */
13
+ export class FonCollection {
14
+ /** True when ownership of this handle has been transferred to a parent. */
15
+ readonly disposed: boolean;
16
+
17
+ constructor();
18
+
19
+ /** Throws if the handle has been transferred (disposed === true). */
20
+ assertAlive(): void;
21
+
22
+ /** Frees the native handle. No-op if already disposed. */
23
+ free(): void;
24
+
25
+ /** Number of key-value entries stored in the collection. */
26
+ readonly size: number;
27
+
28
+ // ---- Add methods ----
29
+
30
+ addInt(key: string, value: number): this;
31
+ addLong(key: string, value: bigint | number): this;
32
+ addDouble(key: string, value: number): this;
33
+ addBool(key: string, value: boolean): this;
34
+ addString(key: string, value: string): this;
35
+
36
+ /**
37
+ * Stores an array of 32-bit integers under key.
38
+ * Values are truncated to i32 range before the FFI call.
39
+ */
40
+ addIntArray(key: string, values: number[]): this;
41
+
42
+ /**
43
+ * Stores an array of 32-bit floats under key.
44
+ * Values are truncated to f32 precision before the FFI call.
45
+ */
46
+ addFloatArray(key: string, values: number[]): this;
47
+
48
+ /**
49
+ * Adds a nested collection under key.
50
+ * OWNERSHIP TRANSFER: child.disposed is set to true after this call.
51
+ */
52
+ addCollection(key: string, child: FonCollection): this;
53
+
54
+ /**
55
+ * Adds an array of nested collections under key.
56
+ * OWNERSHIP TRANSFER: every element's disposed is set to true after this call.
57
+ */
58
+ addCollectionArray(key: string, children: FonCollection[]): this;
59
+
60
+ // ---- Get methods ----
61
+
62
+ getInt(key: string): number;
63
+ getLong(key: string): bigint;
64
+ getDouble(key: string): number;
65
+ getBool(key: string): boolean;
66
+ getString(key: string): string;
67
+
68
+ /** Returns an array of 32-bit integers. Uses the two-call size-query pattern. */
69
+ getIntArray(key: string): number[];
70
+
71
+ /** Returns an array of 32-bit floats. Uses the two-call size-query pattern. */
72
+ getFloatArray(key: string): number[];
73
+
74
+ /**
75
+ * Returns a BORROWED FonCollection stored under key.
76
+ * Do NOT call free() on the returned handle.
77
+ */
78
+ getCollection(key: string): FonCollection;
79
+
80
+ /**
81
+ * Returns an array of BORROWED FonCollections stored under key.
82
+ * Do NOT call free() on any of the returned handles.
83
+ * Uses the two-call size-query pattern.
84
+ */
85
+ getCollectionArray(key: string): FonCollection[];
86
+
87
+ // ---- Serialization ----
88
+
89
+ /** Serializes this collection to a single FON line string. */
90
+ serialize(): string;
91
+
92
+ /** Parses a single FON line into a new FonCollection (caller owns the result). */
93
+ static deserialize(line: string): FonCollection;
94
+ }
95
+
96
+ /**
97
+ * Wraps a heap-allocated FonDump handle (ordered id → FonCollection map).
98
+ */
99
+ export class FonDump {
100
+ /** True when the dump has been freed. */
101
+ readonly disposed: boolean;
102
+
103
+ constructor();
104
+
105
+ /** Throws if the dump has been freed. */
106
+ assertAlive(): void;
107
+
108
+ /** Frees the native handle. No-op if already disposed. */
109
+ free(): void;
110
+
111
+ /** Number of collections stored in the dump. */
112
+ readonly size: number;
113
+
114
+ /**
115
+ * Adds a collection to the dump under the given numeric id.
116
+ * OWNERSHIP TRANSFER: col.disposed is set to true after this call.
117
+ */
118
+ add(id: number | bigint, col: FonCollection): this;
119
+
120
+ /**
121
+ * Returns a BORROWED FonCollection at the given index.
122
+ * Do NOT call free() on the returned handle.
123
+ * Returns null when the index is out of range.
124
+ */
125
+ get(index: number | bigint): FonCollection | null;
126
+
127
+ /** Serializes the entire dump to a multi-line FON string. */
128
+ serialize(maxThreads?: number): string;
129
+
130
+ /** Writes the dump directly to a file at the given path. */
131
+ serializeToFile(filePath: string, maxThreads?: number): void;
132
+
133
+ /** Parses a multi-line FON string into a new FonDump (caller owns the result). */
134
+ static deserialize(text: string, maxThreads?: number): FonDump;
135
+ }
136
+
137
+ // ---- Exported functions ----
138
+
139
+ /** Returns the native library version string (e.g. "0.3.0"). */
140
+ export function nativeVersion(): string;
141
+
142
+ /** Enables or disables raw unpack mode for deserialization (global setting). */
143
+ export function setRawUnpack(value: boolean): void;
144
+
145
+ /** Sets the maximum recursion depth for deserialization (global setting, minimum 1). */
146
+ export function setMaxDepth(depth: number): void;
147
+
148
+ /**
149
+ * Reads a FON dump directly from a file (caller owns the result).
150
+ * @param filePath Absolute or relative file path.
151
+ * @param maxThreads Number of threads to use (0 = automatic).
152
+ */
153
+ export function deserializeDumpFromFile(filePath: string, maxThreads?: number): FonDump;
package/src/index.js ADDED
@@ -0,0 +1,747 @@
1
+ 'use strict';
2
+
3
+ // FON (Fast Object Notation) — fast, human-readable, line-oriented serialization.
4
+ //
5
+ // Ownership rules:
6
+ // - fon_dump_add(dump, id, collection): transfers ownership of collection to dump.
7
+ // Caller MUST NOT free or use the collection handle afterwards.
8
+ // - fon_collection_add_collection(parent, key, child): transfers ownership of child
9
+ // to parent. Caller MUST NOT free or use child afterwards.
10
+ // - fon_collection_add_collection_array(parent, key, children[]): transfers ownership
11
+ // of every child handle. Caller MUST NOT free or use any of them afterwards.
12
+ // - fon_collection_get_collection / fon_collection_get_collection_array /
13
+ // fon_dump_get: returns BORROWED handle(s). Caller MUST NOT free them.
14
+
15
+ import koffi from 'koffi';
16
+ import { fileURLToPath } from 'url';
17
+ import path from 'path';
18
+ import fs from 'fs';
19
+
20
+ const FON_OK = 0;
21
+
22
+ // ==================== LIBRARY LOADING ====================
23
+
24
+ function resolveLibPath() {
25
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
26
+ const repoRoot = path.resolve(currentDir, '..');
27
+ const candidates = [
28
+ path.join(repoRoot, 'native', 'target', 'release', 'fon_native.dll'),
29
+ path.join(repoRoot, 'native', 'target', 'release', 'libfon_native.so'),
30
+ path.join(repoRoot, 'native', 'target', 'release', 'libfon_native.dylib'),
31
+ ];
32
+ for (const candidate of candidates) {
33
+ if (fs.existsSync(candidate)) {
34
+ return candidate;
35
+ }
36
+ }
37
+ throw new Error(
38
+ `fon_native cdylib not found. Build it first:\n cargo build --release --manifest-path native/Cargo.toml\n\nSearched:\n${candidates.join('\n')}`
39
+ );
40
+ }
41
+
42
+ const libPath = resolveLibPath();
43
+ const lib = koffi.load(libPath);
44
+
45
+ // ==================== FFI STRUCT ====================
46
+
47
+ // FonError: int code + uint8[256] message (null-terminated string).
48
+ const FonError = koffi.struct('FonError', {
49
+ code: 'int',
50
+ message: koffi.array('uint8', 256),
51
+ });
52
+
53
+ // ==================== FFI FUNCTION PROTOTYPES ====================
54
+
55
+ // Version — returns a static C string (no ownership transfer, no free).
56
+ const ffi_fon_version = lib.func('fon_version', 'const char *', []);
57
+
58
+ // Dump lifecycle
59
+ const ffi_fon_dump_create = lib.func('fon_dump_create', 'void *', []);
60
+ const ffi_fon_dump_free = lib.func('fon_dump_free', 'void', ['void *']);
61
+ const ffi_fon_dump_size = lib.func('fon_dump_size', 'int64', ['void *']);
62
+ const ffi_fon_dump_get = lib.func('fon_dump_get', 'void *', ['void *', 'uint64']);
63
+
64
+ // fon_dump_add: (dump, id, collection, error*) -> int
65
+ // error is an in/out struct pointer; we use koffi.inout to pass a FonError by reference.
66
+ const ffi_fon_dump_add = lib.func('fon_dump_add', 'int', [
67
+ 'void *',
68
+ 'uint64',
69
+ 'void *',
70
+ koffi.inout(koffi.pointer(FonError)),
71
+ ]);
72
+
73
+ // Collection lifecycle
74
+ const ffi_fon_collection_create = lib.func('fon_collection_create', 'void *', []);
75
+ const ffi_fon_collection_free = lib.func('fon_collection_free', 'void', ['void *']);
76
+ const ffi_fon_collection_size = lib.func('fon_collection_size', 'int64', ['void *']);
77
+
78
+ // Collection add — error is inout struct pointer
79
+ const ffi_fon_collection_add_int = lib.func('fon_collection_add_int', 'int', [
80
+ 'void *', 'str', 'int', koffi.inout(koffi.pointer(FonError)),
81
+ ]);
82
+ const ffi_fon_collection_add_long = lib.func('fon_collection_add_long', 'int', [
83
+ 'void *', 'str', 'int64', koffi.inout(koffi.pointer(FonError)),
84
+ ]);
85
+ const ffi_fon_collection_add_double = lib.func('fon_collection_add_double', 'int', [
86
+ 'void *', 'str', 'double', koffi.inout(koffi.pointer(FonError)),
87
+ ]);
88
+ const ffi_fon_collection_add_bool = lib.func('fon_collection_add_bool', 'int', [
89
+ 'void *', 'str', 'int', koffi.inout(koffi.pointer(FonError)),
90
+ ]);
91
+ const ffi_fon_collection_add_string = lib.func('fon_collection_add_string', 'int', [
92
+ 'void *', 'str', 'str', koffi.inout(koffi.pointer(FonError)),
93
+ ]);
94
+
95
+ // Collection get — scalars use out pointer; error is inout pointer
96
+ const ffi_fon_collection_get_int = lib.func('fon_collection_get_int', 'int', [
97
+ 'void *', 'str', koffi.out(koffi.pointer('int')), koffi.inout(koffi.pointer(FonError)),
98
+ ]);
99
+ const ffi_fon_collection_get_long = lib.func('fon_collection_get_long', 'int', [
100
+ 'void *', 'str', koffi.out(koffi.pointer('int64')), koffi.inout(koffi.pointer(FonError)),
101
+ ]);
102
+ const ffi_fon_collection_get_double = lib.func('fon_collection_get_double', 'int', [
103
+ 'void *', 'str', koffi.out(koffi.pointer('double')), koffi.inout(koffi.pointer(FonError)),
104
+ ]);
105
+ const ffi_fon_collection_get_bool = lib.func('fon_collection_get_bool', 'int', [
106
+ 'void *', 'str', koffi.out(koffi.pointer('int')), koffi.inout(koffi.pointer(FonError)),
107
+ ]);
108
+
109
+ // fon_collection_get_string(collection, key, buffer*uint8, buffer_size int64, error*) -> int
110
+ // buffer is a caller-allocated uint8 array written as a null-terminated C string.
111
+ // We declare buffer as 'uint8 *' and pass a pre-allocated Buffer.
112
+ const ffi_fon_collection_get_string = lib.func('fon_collection_get_string', 'int', [
113
+ 'void *', 'str', 'uint8 *', 'int64', koffi.inout(koffi.pointer(FonError)),
114
+ ]);
115
+
116
+ // Serialization (two-call pattern):
117
+ // fon_serialize_dump_to_buffer(dump, buffer*, buffer_size, required_size*, max_threads, error*) -> int
118
+ // fon_serialize_collection_to_buffer(collection, buffer*, buffer_size, required_size*, error*) -> int
119
+ //
120
+ // For call-1 (size query): buffer=null → pass null pointer explicitly.
121
+ // For call-2 (fill): pass a pre-allocated Buffer.
122
+ // required_size is an out int64 pointer.
123
+ const ffi_fon_serialize_dump_to_buffer = lib.func('fon_serialize_dump_to_buffer', 'int', [
124
+ 'void *', // dump
125
+ 'uint8 *', // buffer (nullable)
126
+ 'int64', // buffer_size
127
+ koffi.out(koffi.pointer('int64')), // required_size (out)
128
+ 'int', // max_threads
129
+ koffi.inout(koffi.pointer(FonError)),
130
+ ]);
131
+ const ffi_fon_serialize_collection_to_buffer = lib.func('fon_serialize_collection_to_buffer', 'int', [
132
+ 'void *', // collection
133
+ 'uint8 *', // buffer (nullable)
134
+ 'int64', // buffer_size
135
+ koffi.out(koffi.pointer('int64')), // required_size (out)
136
+ koffi.inout(koffi.pointer(FonError)),
137
+ ]);
138
+
139
+ // Deserialization from buffer
140
+ const ffi_fon_deserialize_dump_from_buffer = lib.func('fon_deserialize_dump_from_buffer', 'void *', [
141
+ 'uint8 *', // data (nullable when size==0)
142
+ 'int64', // size
143
+ 'int', // max_threads
144
+ koffi.inout(koffi.pointer(FonError)),
145
+ ]);
146
+ const ffi_fon_deserialize_collection_from_buffer = lib.func('fon_deserialize_collection_from_buffer', 'void *', [
147
+ 'uint8 *', // data
148
+ 'int64', // size
149
+ koffi.inout(koffi.pointer(FonError)),
150
+ ]);
151
+
152
+ // ==================== CONFIGURATION FFI ====================
153
+
154
+ // fon_set_raw_unpack(enable: int) -> void
155
+ const ffi_fon_set_raw_unpack = lib.func('fon_set_raw_unpack', 'void', ['int']);
156
+
157
+ // fon_set_max_depth(depth: int) -> void
158
+ const ffi_fon_set_max_depth = lib.func('fon_set_max_depth', 'void', ['int']);
159
+
160
+ // ==================== FILE I/O FFI ====================
161
+
162
+ // fon_serialize_to_file(dump, path, max_threads, error*) -> int
163
+ const ffi_fon_serialize_to_file = lib.func('fon_serialize_to_file', 'int', [
164
+ 'void *', // dump
165
+ 'str', // path (UTF-8 C string)
166
+ 'int', // max_threads
167
+ koffi.inout(koffi.pointer(FonError)),
168
+ ]);
169
+
170
+ // fon_deserialize_from_file(path, max_threads, error*) -> void*
171
+ const ffi_fon_deserialize_from_file = lib.func('fon_deserialize_from_file', 'void *', [
172
+ 'str', // path (UTF-8 C string)
173
+ 'int', // max_threads
174
+ koffi.inout(koffi.pointer(FonError)),
175
+ ]);
176
+
177
+ // ==================== ARRAY FFI ====================
178
+
179
+ // fon_collection_add_int_array(collection, key, values*, count, error*) -> int
180
+ const ffi_fon_collection_add_int_array = lib.func('fon_collection_add_int_array', 'int', [
181
+ 'void *', 'str', 'int *', 'int64', koffi.inout(koffi.pointer(FonError)),
182
+ ]);
183
+
184
+ // fon_collection_get_int_array(collection, key, buffer*|null, buffer_size, actual_size*, error*) -> int
185
+ const ffi_fon_collection_get_int_array = lib.func('fon_collection_get_int_array', 'int', [
186
+ 'void *', 'str', 'int *', 'int64',
187
+ koffi.out(koffi.pointer('int64')),
188
+ koffi.inout(koffi.pointer(FonError)),
189
+ ]);
190
+
191
+ // fon_collection_add_float_array(collection, key, values*, count, error*) -> int
192
+ const ffi_fon_collection_add_float_array = lib.func('fon_collection_add_float_array', 'int', [
193
+ 'void *', 'str', 'float *', 'int64', koffi.inout(koffi.pointer(FonError)),
194
+ ]);
195
+
196
+ // fon_collection_get_float_array(collection, key, buffer*|null, buffer_size, actual_size*, error*) -> int
197
+ const ffi_fon_collection_get_float_array = lib.func('fon_collection_get_float_array', 'int', [
198
+ 'void *', 'str', 'float *', 'int64',
199
+ koffi.out(koffi.pointer('int64')),
200
+ koffi.inout(koffi.pointer(FonError)),
201
+ ]);
202
+
203
+ // ==================== NESTED COLLECTION FFI ====================
204
+
205
+ // fon_collection_add_collection(parent, key, child, error*) -> int [ownership transfer]
206
+ const ffi_fon_collection_add_collection = lib.func('fon_collection_add_collection', 'int', [
207
+ 'void *', 'str', 'void *', koffi.inout(koffi.pointer(FonError)),
208
+ ]);
209
+
210
+ // fon_collection_get_collection(parent, key, error*) -> void* [borrowed]
211
+ const ffi_fon_collection_get_collection = lib.func('fon_collection_get_collection', 'void *', [
212
+ 'void *', 'str', koffi.inout(koffi.pointer(FonError)),
213
+ ]);
214
+
215
+ // fon_collection_add_collection_array(parent, key, children**, count, error*) -> int [ownership transfer]
216
+ // Pass a JS array of opaque handles; koffi maps them into a void** on the native side.
217
+ const ffi_fon_collection_add_collection_array = lib.func('fon_collection_add_collection_array', 'int', [
218
+ 'void *', 'str', 'void **', 'int64', koffi.inout(koffi.pointer(FonError)),
219
+ ]);
220
+
221
+ // Size-query variant: pass uint8* null buffer to get the element count.
222
+ const ffi_fon_collection_get_collection_array_size = lib.func('fon_collection_get_collection_array', 'int', [
223
+ 'void *', 'str', 'uint8 *', 'int64',
224
+ koffi.out(koffi.pointer('int64')),
225
+ koffi.inout(koffi.pointer(FonError)),
226
+ ]);
227
+ // Fill variant: created dynamically per call using a fixed-size koffi.array type.
228
+ // (No single global declaration here; see getCollectionArray implementation.)
229
+
230
+ // ==================== HELPERS ====================
231
+
232
+ function decodeErrorStruct(err) {
233
+ const bytes = err.message;
234
+ let end = bytes.indexOf(0);
235
+ if (end === -1) { end = bytes.length; }
236
+ const msg = Buffer.from(bytes.slice(0, end)).toString('utf8');
237
+ return new Error(`FON error ${err.code}: ${msg}`);
238
+ }
239
+
240
+ function makeError() {
241
+ return { code: 0, message: new Array(256).fill(0) };
242
+ }
243
+
244
+ // Two-call buffer serialization helper.
245
+ // Abstracts the size-query + fill pattern for dump and collection serialize.
246
+ //
247
+ // dumpCall: (ptr, buffer|null, bufferSize, sizeOut, ...extraArgs, err) -> int
248
+ function serializeHandleToString(ptr, extraArgs, ffiFunc) {
249
+ const err1 = makeError();
250
+ const sizeOut = [0n];
251
+
252
+ // Call 1: pass null for buffer, 0 for size → fills sizeOut.
253
+ const rc1 = ffiFunc(ptr, null, 0n, sizeOut, ...extraArgs, err1);
254
+ if (rc1 !== FON_OK) {
255
+ throw decodeErrorStruct(err1);
256
+ }
257
+
258
+ const size = Number(sizeOut[0]);
259
+ if (size === 0) {
260
+ return '';
261
+ }
262
+
263
+ // Call 2: allocate exact buffer and fill it.
264
+ const buf = Buffer.alloc(size);
265
+ const sizeOut2 = [0n];
266
+ const err2 = makeError();
267
+ const rc2 = ffiFunc(ptr, buf, BigInt(size), sizeOut2, ...extraArgs, err2);
268
+ if (rc2 !== FON_OK) {
269
+ throw decodeErrorStruct(err2);
270
+ }
271
+
272
+ return buf.toString('utf8');
273
+ }
274
+
275
+ // ==================== PUBLIC API ====================
276
+
277
+ /**
278
+ * Returns the native library version string (e.g. "0.3.0").
279
+ * @returns {string}
280
+ */
281
+ function nativeVersion() {
282
+ return ffi_fon_version();
283
+ }
284
+
285
+ /**
286
+ * Enables or disables raw unpack mode for deserialization (global setting).
287
+ * @param {boolean} value
288
+ */
289
+ function setRawUnpack(value) {
290
+ ffi_fon_set_raw_unpack(value ? 1 : 0);
291
+ }
292
+
293
+ /**
294
+ * Sets the maximum recursion depth for deserialization (global setting).
295
+ * @param {number} depth
296
+ */
297
+ function setMaxDepth(depth) {
298
+ ffi_fon_set_max_depth(depth | 0);
299
+ }
300
+
301
+ /**
302
+ * FonCollection — wraps a heap-allocated FonCollection handle.
303
+ *
304
+ * Ownership: when disposed=true the handle has been transferred to a FonDump
305
+ * or parent FonCollection (via add* calls) and MUST NOT be freed or used.
306
+ */
307
+ class FonCollection {
308
+ constructor() {
309
+ this.handle = ffi_fon_collection_create();
310
+ if (!this.handle) {
311
+ throw new Error('fon_collection_create returned null');
312
+ }
313
+ this.disposed = false;
314
+ }
315
+
316
+ assertAlive() {
317
+ if (this.disposed) {
318
+ throw new Error('FonCollection handle has been transferred and is no longer usable');
319
+ }
320
+ }
321
+
322
+ free() {
323
+ if (!this.disposed) {
324
+ ffi_fon_collection_free(this.handle);
325
+ this.disposed = true;
326
+ }
327
+ }
328
+
329
+ get size() {
330
+ this.assertAlive();
331
+ return Number(ffi_fon_collection_size(this.handle));
332
+ }
333
+
334
+ // -- Add methods --
335
+
336
+ addInt(key, value) {
337
+ this.assertAlive();
338
+ const err = makeError();
339
+ const rc = ffi_fon_collection_add_int(this.handle, key, value | 0, err);
340
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
341
+ return this;
342
+ }
343
+
344
+ addLong(key, value) {
345
+ this.assertAlive();
346
+ const err = makeError();
347
+ const rc = ffi_fon_collection_add_long(this.handle, key, BigInt(value), err);
348
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
349
+ return this;
350
+ }
351
+
352
+ addDouble(key, value) {
353
+ this.assertAlive();
354
+ const err = makeError();
355
+ const rc = ffi_fon_collection_add_double(this.handle, key, value, err);
356
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
357
+ return this;
358
+ }
359
+
360
+ addBool(key, value) {
361
+ this.assertAlive();
362
+ const err = makeError();
363
+ const rc = ffi_fon_collection_add_bool(this.handle, key, value ? 1 : 0, err);
364
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
365
+ return this;
366
+ }
367
+
368
+ addString(key, value) {
369
+ this.assertAlive();
370
+ const err = makeError();
371
+ const rc = ffi_fon_collection_add_string(this.handle, key, value, err);
372
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
373
+ return this;
374
+ }
375
+
376
+ /**
377
+ * Stores an array of 32-bit integers under key. The JS array is copied into
378
+ * a native Int32Array before the FFI call.
379
+ * @param {string} key
380
+ * @param {number[]} values
381
+ */
382
+ addIntArray(key, values) {
383
+ this.assertAlive();
384
+ const native = new Int32Array(values);
385
+ const err = makeError();
386
+ const rc = ffi_fon_collection_add_int_array(this.handle, key, native, BigInt(native.length), err);
387
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
388
+ return this;
389
+ }
390
+
391
+ /**
392
+ * Stores an array of 32-bit floats under key.
393
+ * @param {string} key
394
+ * @param {number[]} values
395
+ */
396
+ addFloatArray(key, values) {
397
+ this.assertAlive();
398
+ const native = new Float32Array(values);
399
+ const err = makeError();
400
+ const rc = ffi_fon_collection_add_float_array(this.handle, key, native, BigInt(native.length), err);
401
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
402
+ return this;
403
+ }
404
+
405
+ /**
406
+ * Adds a nested FonCollection under key.
407
+ * OWNERSHIP TRANSFER: child.disposed is set to true; do not use or free child.
408
+ * @param {string} key
409
+ * @param {FonCollection} child
410
+ */
411
+ addCollection(key, child) {
412
+ this.assertAlive();
413
+ child.assertAlive();
414
+ const err = makeError();
415
+ const rc = ffi_fon_collection_add_collection(this.handle, key, child.handle, err);
416
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
417
+ child.disposed = true; // ownership transferred to parent
418
+ return this;
419
+ }
420
+
421
+ /**
422
+ * Adds an array of nested FonCollections under key.
423
+ * OWNERSHIP TRANSFER: every element's disposed is set to true.
424
+ * @param {string} key
425
+ * @param {FonCollection[]} children
426
+ */
427
+ addCollectionArray(key, children) {
428
+ this.assertAlive();
429
+ for (const child of children) {
430
+ child.assertAlive();
431
+ }
432
+ // Build a Buffer of raw pointer values (8 bytes each on 64-bit).
433
+ // koffi expects a 'void **' — pass an array of opaque handle values.
434
+ // We use koffi's ability to accept a JS array of void* when the param type is 'void **'.
435
+ const handles = children.map((c) => c.handle);
436
+ const err = makeError();
437
+ const rc = ffi_fon_collection_add_collection_array(
438
+ this.handle, key, handles, BigInt(handles.length), err
439
+ );
440
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
441
+ for (const child of children) {
442
+ child.disposed = true; // ownership transferred to parent
443
+ }
444
+ return this;
445
+ }
446
+
447
+ // -- Get methods --
448
+
449
+ getInt(key) {
450
+ this.assertAlive();
451
+ const err = makeError();
452
+ const out = [0];
453
+ const rc = ffi_fon_collection_get_int(this.handle, key, out, err);
454
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
455
+ return out[0];
456
+ }
457
+
458
+ getLong(key) {
459
+ this.assertAlive();
460
+ const err = makeError();
461
+ const out = [0n];
462
+ const rc = ffi_fon_collection_get_long(this.handle, key, out, err);
463
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
464
+ // koffi may return int64 as a JS number or BigInt depending on magnitude;
465
+ // normalise to BigInt for a consistent API.
466
+ return BigInt(out[0]);
467
+ }
468
+
469
+ getDouble(key) {
470
+ this.assertAlive();
471
+ const err = makeError();
472
+ const out = [0.0];
473
+ const rc = ffi_fon_collection_get_double(this.handle, key, out, err);
474
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
475
+ return out[0];
476
+ }
477
+
478
+ getBool(key) {
479
+ this.assertAlive();
480
+ const err = makeError();
481
+ const out = [0];
482
+ const rc = ffi_fon_collection_get_bool(this.handle, key, out, err);
483
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
484
+ return out[0] !== 0;
485
+ }
486
+
487
+ getString(key) {
488
+ this.assertAlive();
489
+ const err = makeError();
490
+ // Allocate a 4096-byte output buffer for the null-terminated C string.
491
+ const bufSize = 4096n;
492
+ const buf = Buffer.alloc(Number(bufSize));
493
+ const rc = ffi_fon_collection_get_string(this.handle, key, buf, bufSize, err);
494
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
495
+ // Find null terminator and decode UTF-8.
496
+ let end = buf.indexOf(0);
497
+ if (end === -1) { end = buf.length; }
498
+ return buf.slice(0, end).toString('utf8');
499
+ }
500
+
501
+ /**
502
+ * Returns an array of 32-bit integers stored under key (two-call pattern).
503
+ * @param {string} key
504
+ * @returns {number[]}
505
+ */
506
+ getIntArray(key) {
507
+ this.assertAlive();
508
+ // Call 1: null buffer → read actual element count.
509
+ const sizeOut = [0n];
510
+ const err1 = makeError();
511
+ const rc1 = ffi_fon_collection_get_int_array(this.handle, key, null, 0n, sizeOut, err1);
512
+ if (rc1 !== FON_OK) { throw decodeErrorStruct(err1); }
513
+ const count = Number(sizeOut[0]);
514
+ if (count === 0) { return []; }
515
+ // Call 2: allocate typed buffer and fill.
516
+ const buf = new Int32Array(count);
517
+ const sizeOut2 = [0n];
518
+ const err2 = makeError();
519
+ const rc2 = ffi_fon_collection_get_int_array(this.handle, key, buf, BigInt(count), sizeOut2, err2);
520
+ if (rc2 !== FON_OK) { throw decodeErrorStruct(err2); }
521
+ return Array.from(buf);
522
+ }
523
+
524
+ /**
525
+ * Returns an array of 32-bit floats stored under key (two-call pattern).
526
+ * @param {string} key
527
+ * @returns {number[]}
528
+ */
529
+ getFloatArray(key) {
530
+ this.assertAlive();
531
+ // Call 1: null buffer → read actual element count.
532
+ const sizeOut = [0n];
533
+ const err1 = makeError();
534
+ const rc1 = ffi_fon_collection_get_float_array(this.handle, key, null, 0n, sizeOut, err1);
535
+ if (rc1 !== FON_OK) { throw decodeErrorStruct(err1); }
536
+ const count = Number(sizeOut[0]);
537
+ if (count === 0) { return []; }
538
+ // Call 2: allocate typed buffer and fill.
539
+ const buf = new Float32Array(count);
540
+ const sizeOut2 = [0n];
541
+ const err2 = makeError();
542
+ const rc2 = ffi_fon_collection_get_float_array(this.handle, key, buf, BigInt(count), sizeOut2, err2);
543
+ if (rc2 !== FON_OK) { throw decodeErrorStruct(err2); }
544
+ return Array.from(buf);
545
+ }
546
+
547
+ /**
548
+ * Returns a BORROWED FonCollection stored under key. Do NOT free it.
549
+ * @param {string} key
550
+ * @returns {FonCollection}
551
+ */
552
+ getCollection(key) {
553
+ this.assertAlive();
554
+ const err = makeError();
555
+ const ptr = ffi_fon_collection_get_collection(this.handle, key, err);
556
+ if (!ptr) { throw decodeErrorStruct(err); }
557
+ const col = Object.create(FonCollection.prototype);
558
+ col.handle = ptr;
559
+ col.disposed = false;
560
+ return col;
561
+ }
562
+
563
+ /**
564
+ * Returns an array of BORROWED FonCollections stored under key (two-call pattern).
565
+ * Do NOT free any of the returned handles.
566
+ * @param {string} key
567
+ * @returns {FonCollection[]}
568
+ */
569
+ getCollectionArray(key) {
570
+ this.assertAlive();
571
+ // Call 1: pass null uint8* buffer to query element count only.
572
+ const sizeOut = [0n];
573
+ const err1 = makeError();
574
+ const rc1 = ffi_fon_collection_get_collection_array_size(
575
+ this.handle, key, null, 0n, sizeOut, err1
576
+ );
577
+ if (rc1 !== FON_OK) { throw decodeErrorStruct(err1); }
578
+ const count = Number(sizeOut[0]);
579
+ if (count === 0) { return []; }
580
+ // Call 2: create a fixed-size void* array type for this count so koffi
581
+ // can properly fill each pointer slot on return.
582
+ const PtrArrayN = koffi.array('void *', count);
583
+ const ffi_fill = lib.func('fon_collection_get_collection_array', 'int', [
584
+ 'void *', 'str', koffi.out(koffi.pointer(PtrArrayN)), 'int64',
585
+ koffi.out(koffi.pointer('int64')),
586
+ koffi.inout(koffi.pointer(FonError)),
587
+ ]);
588
+ const ptrs = [[]];
589
+ const sizeOut2 = [0n];
590
+ const err2 = makeError();
591
+ const rc2 = ffi_fill(this.handle, key, ptrs, BigInt(count), sizeOut2, err2);
592
+ if (rc2 !== FON_OK) { throw decodeErrorStruct(err2); }
593
+ return ptrs[0].map((ptr) => {
594
+ const col = Object.create(FonCollection.prototype);
595
+ col.handle = ptr;
596
+ col.disposed = false;
597
+ return col;
598
+ });
599
+ }
600
+
601
+ // -- Serialization --
602
+
603
+ /**
604
+ * Serializes this collection to a FON line string (two-call pattern).
605
+ * @returns {string}
606
+ */
607
+ serialize() {
608
+ this.assertAlive();
609
+ return serializeHandleToString(this.handle, [], ffi_fon_serialize_collection_to_buffer);
610
+ }
611
+
612
+ /**
613
+ * Parses a single FON line into a new FonCollection.
614
+ * @param {string} line
615
+ * @returns {FonCollection}
616
+ */
617
+ static deserialize(line) {
618
+ const bytes = Buffer.from(line, 'utf8');
619
+ const err = makeError();
620
+ const handle = ffi_fon_deserialize_collection_from_buffer(bytes, BigInt(bytes.length), err);
621
+ if (!handle) { throw decodeErrorStruct(err); }
622
+ const col = Object.create(FonCollection.prototype);
623
+ col.handle = handle;
624
+ col.disposed = false;
625
+ return col;
626
+ }
627
+ }
628
+
629
+ /**
630
+ * FonDump — wraps a heap-allocated FonDump handle (id -> FonCollection map).
631
+ */
632
+ class FonDump {
633
+ constructor() {
634
+ this.handle = ffi_fon_dump_create();
635
+ if (!this.handle) {
636
+ throw new Error('fon_dump_create returned null');
637
+ }
638
+ this.disposed = false;
639
+ }
640
+
641
+ assertAlive() {
642
+ if (this.disposed) {
643
+ throw new Error('FonDump has already been freed');
644
+ }
645
+ }
646
+
647
+ free() {
648
+ if (!this.disposed) {
649
+ ffi_fon_dump_free(this.handle);
650
+ this.disposed = true;
651
+ }
652
+ }
653
+
654
+ get size() {
655
+ this.assertAlive();
656
+ return Number(ffi_fon_dump_size(this.handle));
657
+ }
658
+
659
+ /**
660
+ * Adds a collection to the dump under the given numeric id.
661
+ * OWNERSHIP TRANSFER: after this call, col.disposed is set to true.
662
+ * The caller MUST NOT use or free col afterwards.
663
+ * @param {number|bigint} id
664
+ * @param {FonCollection} col
665
+ */
666
+ add(id, col) {
667
+ this.assertAlive();
668
+ col.assertAlive();
669
+ const err = makeError();
670
+ const rc = ffi_fon_dump_add(this.handle, BigInt(id), col.handle, err);
671
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
672
+ col.disposed = true; // ownership transferred to dump
673
+ return this;
674
+ }
675
+
676
+ /**
677
+ * Returns a BORROWED FonCollection at the given index. Do NOT free it.
678
+ * @param {number|bigint} index
679
+ * @returns {FonCollection|null}
680
+ */
681
+ get(index) {
682
+ this.assertAlive();
683
+ const ptr = ffi_fon_dump_get(this.handle, BigInt(index));
684
+ if (!ptr) { return null; }
685
+ const col = Object.create(FonCollection.prototype);
686
+ col.handle = ptr;
687
+ col.disposed = false;
688
+ return col;
689
+ }
690
+
691
+ /**
692
+ * Serializes the entire dump to a multi-line FON string.
693
+ * @param {number} [maxThreads=0]
694
+ * @returns {string}
695
+ */
696
+ serialize(maxThreads = 0) {
697
+ this.assertAlive();
698
+ return serializeHandleToString(this.handle, [maxThreads], ffi_fon_serialize_dump_to_buffer);
699
+ }
700
+
701
+ /**
702
+ * Writes the dump directly to a file at the given path.
703
+ * @param {string} filePath
704
+ * @param {number} [maxThreads=0]
705
+ */
706
+ serializeToFile(filePath, maxThreads = 0) {
707
+ this.assertAlive();
708
+ const err = makeError();
709
+ const rc = ffi_fon_serialize_to_file(this.handle, filePath, maxThreads | 0, err);
710
+ if (rc !== FON_OK) { throw decodeErrorStruct(err); }
711
+ }
712
+
713
+ /**
714
+ * Parses a multi-line FON buffer into a new FonDump.
715
+ * @param {string} text
716
+ * @param {number} [maxThreads=0]
717
+ * @returns {FonDump}
718
+ */
719
+ static deserialize(text, maxThreads = 0) {
720
+ const bytes = Buffer.from(text, 'utf8');
721
+ const err = makeError();
722
+ const handle = ffi_fon_deserialize_dump_from_buffer(bytes, BigInt(bytes.length), maxThreads, err);
723
+ if (!handle) { throw decodeErrorStruct(err); }
724
+ const dump = Object.create(FonDump.prototype);
725
+ dump.handle = handle;
726
+ dump.disposed = false;
727
+ return dump;
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Reads a FON dump directly from a file.
733
+ * @param {string} filePath
734
+ * @param {number} [maxThreads=0]
735
+ * @returns {FonDump}
736
+ */
737
+ function deserializeDumpFromFile(filePath, maxThreads = 0) {
738
+ const err = makeError();
739
+ const handle = ffi_fon_deserialize_from_file(filePath, maxThreads | 0, err);
740
+ if (!handle) { throw decodeErrorStruct(err); }
741
+ const dump = Object.create(FonDump.prototype);
742
+ dump.handle = handle;
743
+ dump.disposed = false;
744
+ return dump;
745
+ }
746
+
747
+ export { FonCollection, FonDump, nativeVersion, setRawUnpack, setMaxDepth, deserializeDumpFromFile };