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 +19 -0
- package/README.md +133 -0
- package/package.json +38 -0
- package/src/index.d.ts +153 -0
- package/src/index.js +747 -0
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
|
+
[](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 };
|