@felixdrp/gcf-wasm 0.1.0 → 0.2.0
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/README.md +35 -0
- package/dist/cli.js +44 -11
- package/dist/gcf.wasm +0 -0
- package/dist/host.d.ts +44 -2
- package/dist/index.d.ts +7 -4
- package/dist/index.js +45 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,41 @@ text as rows arrive, and crossing into wasm for every row would cost more
|
|
|
92
92
|
than it saves. They follow the same quoting and number rules as the module,
|
|
93
93
|
and the tests check that the two agree.
|
|
94
94
|
|
|
95
|
+
## Compatibility and limits
|
|
96
|
+
|
|
97
|
+
* **Spec 3.6.0 output.** The encoder factors constant columns
|
|
98
|
+
(`## users [3]{id,name=Ada}`), as SPEC v3.6.0 §7.4.7 requires, byte for
|
|
99
|
+
byte like Blackwell's gcf-typescript 2.7.0. Decoders written for older
|
|
100
|
+
versions of the spec cannot read it: `@blackwell-systems/gcf` 2.6.1, the
|
|
101
|
+
latest on npm at the time of writing, predates spec 3.6.0 and fails with
|
|
102
|
+
`invalid field name: name=Ada`. Check the version of the decoder at the
|
|
103
|
+
other end.
|
|
104
|
+
* **JS values are normalised like `JSON.stringify`.** `encode(value)`
|
|
105
|
+
serialises with `JSON.stringify` semantics, the host-value normalisation
|
|
106
|
+
that SPEC §2.3.2 allows: `undefined` and functions are omitted, `NaN` and
|
|
107
|
+
`±Infinity` become null, array holes become null, `toJSON` is honoured
|
|
108
|
+
(so a `Date` becomes its ISO string), and a `Map` or `Set` becomes `{}`.
|
|
109
|
+
Normalise such values yourself if they matter. For exact key order
|
|
110
|
+
(numeric-looking keys), encode JSON text with `encodeJson`.
|
|
111
|
+
* **Memory: streaming decode, released above 64 MiB.** Decoding writes a
|
|
112
|
+
document while it parses it, so it never holds the whole document as a
|
|
113
|
+
tree: it peaks at about 1.2–1.6 times its input plus its output (for
|
|
114
|
+
typical tables, about 3.5 times the GCF input; `decodeLazy` about 4.5
|
|
115
|
+
times). Encoding peaks at about 4.5 times the JSON input. Each codec has
|
|
116
|
+
one WebAssembly memory that grows to the largest payload it has handled,
|
|
117
|
+
and WebAssembly memory cannot shrink. By default, a call that leaves more
|
|
118
|
+
than 64 MiB reserved is followed by a fresh instance, switched in as soon
|
|
119
|
+
as the call returns, and the old memory is garbage collected. Decoding up
|
|
120
|
+
to about 15 MB of GCF, or encoding up to about 15 MB of JSON, stays below
|
|
121
|
+
the limit. Above it, the next large call is up to about 25% slower while
|
|
122
|
+
the memory is requested again; if you handle such payloads continuously,
|
|
123
|
+
raise the limit with `init(undefined, { maxRetainedMemory: 512 << 20 })`
|
|
124
|
+
(bytes) or pass `Infinity` to keep the memory. `codec.memoryBytes`
|
|
125
|
+
reports the current size and `await codec.recycle()` releases it on
|
|
126
|
+
demand. The limit bounds what stays reserved between calls, not the peak
|
|
127
|
+
during a call.
|
|
128
|
+
* **Streaming encoders run in JavaScript** (see above).
|
|
129
|
+
|
|
95
130
|
## CLI
|
|
96
131
|
|
|
97
132
|
```sh
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ class GcfError extends Error {
|
|
|
16
16
|
this.line = line;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
var DEFAULT_MAX_RETAINED_MEMORY = 64 * 1024 * 1024;
|
|
19
20
|
var utf8 = new TextEncoder;
|
|
20
21
|
var fromUtf8 = new TextDecoder;
|
|
21
22
|
function isPlainGraph(text) {
|
|
@@ -153,13 +154,35 @@ function parseLargeInts(text, mode) {
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
class GcfCodec {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
x;
|
|
158
|
+
module;
|
|
159
|
+
maxRetained;
|
|
160
|
+
pending;
|
|
161
|
+
constructor(module, exports, opts) {
|
|
162
|
+
this.module = module;
|
|
163
|
+
this.x = exports;
|
|
164
|
+
const max = opts.maxRetainedMemory ?? DEFAULT_MAX_RETAINED_MEMORY;
|
|
165
|
+
if (!(max > 0))
|
|
166
|
+
throw new RangeError(`maxRetainedMemory must be a positive number of bytes or Infinity, got ${max}`);
|
|
167
|
+
this.maxRetained = max;
|
|
168
|
+
}
|
|
169
|
+
static async instantiate(wasm, opts = {}) {
|
|
170
|
+
const module = wasm instanceof WebAssembly.Module ? wasm : await WebAssembly.compile(wasm);
|
|
171
|
+
const instance = await WebAssembly.instantiate(module, {});
|
|
172
|
+
return new GcfCodec(module, instance.exports, opts);
|
|
173
|
+
}
|
|
174
|
+
get exports() {
|
|
175
|
+
return this.x;
|
|
176
|
+
}
|
|
177
|
+
get memoryBytes() {
|
|
178
|
+
return this.x.memory.buffer.byteLength;
|
|
179
|
+
}
|
|
180
|
+
recycle() {
|
|
181
|
+
this.pending ??= WebAssembly.instantiate(this.module, {}).then((instance) => {
|
|
182
|
+
this.x = instance.exports;
|
|
183
|
+
this.pending = undefined;
|
|
184
|
+
});
|
|
185
|
+
return this.pending;
|
|
163
186
|
}
|
|
164
187
|
load(input) {
|
|
165
188
|
const x = this.exports;
|
|
@@ -172,7 +195,12 @@ class GcfCodec {
|
|
|
172
195
|
const ptr = x.gcf_input_alloc(cap);
|
|
173
196
|
return utf8.encodeInto(input, new Uint8Array(x.memory.buffer, ptr, cap)).written;
|
|
174
197
|
}
|
|
198
|
+
releaseIfLarge() {
|
|
199
|
+
if (!this.pending && this.x.memory.buffer.byteLength > this.maxRetained)
|
|
200
|
+
this.recycle();
|
|
201
|
+
}
|
|
175
202
|
output(status) {
|
|
203
|
+
this.releaseIfLarge();
|
|
176
204
|
const x = this.exports;
|
|
177
205
|
const text = (p, n) => fromUtf8.decode(new Uint8Array(x.memory.buffer, p, n));
|
|
178
206
|
if (status !== 0) {
|
|
@@ -181,6 +209,7 @@ class GcfCodec {
|
|
|
181
209
|
return text(x.gcf_output_ptr(), x.gcf_output_len());
|
|
182
210
|
}
|
|
183
211
|
outputBytes(status, view) {
|
|
212
|
+
this.releaseIfLarge();
|
|
184
213
|
if (status !== 0)
|
|
185
214
|
this.output(status);
|
|
186
215
|
const x = this.exports;
|
|
@@ -682,6 +711,7 @@ function main(argv, gcf) {
|
|
|
682
711
|
}
|
|
683
712
|
}
|
|
684
713
|
// src/index.ts
|
|
714
|
+
var bundled;
|
|
685
715
|
var shared;
|
|
686
716
|
async function wasmBytes(source) {
|
|
687
717
|
if (source instanceof URL || typeof source === "string") {
|
|
@@ -697,15 +727,18 @@ async function wasmBytes(source) {
|
|
|
697
727
|
throw new Error(`failed to load gcf.wasm: HTTP ${res.status}`);
|
|
698
728
|
return res.arrayBuffer();
|
|
699
729
|
}
|
|
700
|
-
function init(source) {
|
|
730
|
+
function init(source, options) {
|
|
701
731
|
if (source === undefined) {
|
|
702
|
-
|
|
732
|
+
bundled ??= wasmBytes(new URL("./gcf.wasm", import.meta.url)).then((b) => WebAssembly.compile(b));
|
|
733
|
+
if (options !== undefined)
|
|
734
|
+
return bundled.then((m) => GcfCodec.instantiate(m, options));
|
|
735
|
+
shared ??= bundled.then((m) => GcfCodec.instantiate(m));
|
|
703
736
|
return shared;
|
|
704
737
|
}
|
|
705
738
|
if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
|
|
706
|
-
return GcfCodec.instantiate(source);
|
|
739
|
+
return GcfCodec.instantiate(source, options);
|
|
707
740
|
}
|
|
708
|
-
return wasmBytes(source).then((b) => GcfCodec.instantiate(b));
|
|
741
|
+
return wasmBytes(source).then((b) => GcfCodec.instantiate(b, options));
|
|
709
742
|
}
|
|
710
743
|
|
|
711
744
|
// src/cli-npm.ts
|
package/dist/gcf.wasm
CHANGED
|
Binary file
|
package/dist/host.d.ts
CHANGED
|
@@ -28,6 +28,28 @@ export interface BytesOptions {
|
|
|
28
28
|
*/
|
|
29
29
|
view?: boolean;
|
|
30
30
|
}
|
|
31
|
+
/** Default `maxRetainedMemory`: 64 MiB. */
|
|
32
|
+
export declare const DEFAULT_MAX_RETAINED_MEMORY: number;
|
|
33
|
+
export interface CodecOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Bytes of WebAssembly memory a codec may keep reserved between calls.
|
|
36
|
+
* A codec's memory grows to the largest payload it has handled and
|
|
37
|
+
* WebAssembly memory cannot shrink, so when a call leaves more than this,
|
|
38
|
+
* the codec prepares a fresh instance in the background and switches to
|
|
39
|
+
* it as soon as the call has returned (asynchronously, never during a
|
|
40
|
+
* call); the old memory is then garbage collected. Calls can still use
|
|
41
|
+
* more while they run: this bounds what stays reserved afterwards, not
|
|
42
|
+
* the peak.
|
|
43
|
+
*
|
|
44
|
+
* Default: 64 MiB (`DEFAULT_MAX_RETAINED_MEMORY`), which decoding up to
|
|
45
|
+
* about 15 MB of GCF, or encoding up to about 15 MB of JSON, does not
|
|
46
|
+
* reach. Above it, the first large call on the fresh instance is up to
|
|
47
|
+
* about 25% slower while the memory is requested again, so if you handle
|
|
48
|
+
* such payloads continuously, raise the limit or pass `Infinity` to keep
|
|
49
|
+
* the memory.
|
|
50
|
+
*/
|
|
51
|
+
maxRetainedMemory?: number;
|
|
52
|
+
}
|
|
31
53
|
export interface GraphSymbol {
|
|
32
54
|
qualifiedName: string;
|
|
33
55
|
kind: string;
|
|
@@ -135,11 +157,31 @@ type Columns = {
|
|
|
135
157
|
references?: number[];
|
|
136
158
|
};
|
|
137
159
|
export declare class GcfCodec {
|
|
138
|
-
|
|
160
|
+
private x;
|
|
161
|
+
private readonly module;
|
|
162
|
+
private readonly maxRetained;
|
|
163
|
+
/** A fresh instance being prepared; it replaces the current one when ready. */
|
|
164
|
+
private pending;
|
|
139
165
|
private constructor();
|
|
140
|
-
static instantiate(wasm: BufferSource | WebAssembly.Module): Promise<GcfCodec>;
|
|
166
|
+
static instantiate(wasm: BufferSource | WebAssembly.Module, opts?: CodecOptions): Promise<GcfCodec>;
|
|
167
|
+
/** The current instance's exports (the raw ABI, docs/WASM_ABI.md). */
|
|
168
|
+
get exports(): Exports;
|
|
169
|
+
/** Bytes of WebAssembly memory the codec currently holds. */
|
|
170
|
+
get memoryBytes(): number;
|
|
171
|
+
/**
|
|
172
|
+
* Replace the instance with a fresh one, releasing the memory it holds
|
|
173
|
+
* (see `maxRetainedMemory`, which does this automatically). Results of
|
|
174
|
+
* earlier calls stay valid, including `view: true` byte views.
|
|
175
|
+
*/
|
|
176
|
+
recycle(): Promise<void>;
|
|
141
177
|
/** Copy the input into the engine's arena and return its byte length. */
|
|
142
178
|
private load;
|
|
179
|
+
/**
|
|
180
|
+
* Every call ends by reading its result through output() or outputBytes().
|
|
181
|
+
* When the call left more memory than `maxRetainedMemory`, a fresh
|
|
182
|
+
* instance is prepared; it takes over once the call has returned.
|
|
183
|
+
*/
|
|
184
|
+
private releaseIfLarge;
|
|
143
185
|
private output;
|
|
144
186
|
/** Raw output bytes: an owned copy, or a view valid until the next call. */
|
|
145
187
|
private outputBytes;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export * from "./host.js";
|
|
2
|
-
import { GcfCodec } from "./host.js";
|
|
2
|
+
import { type CodecOptions, GcfCodec } from "./host.js";
|
|
3
3
|
export type WasmSource = BufferSource | WebAssembly.Module | URL | string | Response | Promise<Response>;
|
|
4
4
|
/**
|
|
5
5
|
* Create a codec. Without arguments, returns a shared instance backed by the
|
|
6
|
-
* `gcf.wasm` shipped with this package (loaded once).
|
|
7
|
-
*
|
|
6
|
+
* `gcf.wasm` shipped with this package (loaded and compiled once). With
|
|
7
|
+
* `options` (e.g. `init(undefined, { maxRetainedMemory: 512 << 20 })`), returns
|
|
8
|
+
* a new codec from the same compiled module. Each codec owns its own
|
|
9
|
+
* WebAssembly memory; create more than one only for parallel use or
|
|
10
|
+
* different options.
|
|
8
11
|
*/
|
|
9
|
-
export declare function init(source?: WasmSource): Promise<GcfCodec>;
|
|
12
|
+
export declare function init(source?: WasmSource, options?: CodecOptions): Promise<GcfCodec>;
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ class GcfError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
var GcfDecodeError = GcfError;
|
|
15
|
+
var DEFAULT_MAX_RETAINED_MEMORY = 64 * 1024 * 1024;
|
|
15
16
|
var utf8 = new TextEncoder;
|
|
16
17
|
var fromUtf8 = new TextDecoder;
|
|
17
18
|
function isPlainGraph(text) {
|
|
@@ -149,13 +150,35 @@ function parseLargeInts(text, mode) {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
class GcfCodec {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
x;
|
|
154
|
+
module;
|
|
155
|
+
maxRetained;
|
|
156
|
+
pending;
|
|
157
|
+
constructor(module, exports, opts) {
|
|
158
|
+
this.module = module;
|
|
159
|
+
this.x = exports;
|
|
160
|
+
const max = opts.maxRetainedMemory ?? DEFAULT_MAX_RETAINED_MEMORY;
|
|
161
|
+
if (!(max > 0))
|
|
162
|
+
throw new RangeError(`maxRetainedMemory must be a positive number of bytes or Infinity, got ${max}`);
|
|
163
|
+
this.maxRetained = max;
|
|
164
|
+
}
|
|
165
|
+
static async instantiate(wasm, opts = {}) {
|
|
166
|
+
const module = wasm instanceof WebAssembly.Module ? wasm : await WebAssembly.compile(wasm);
|
|
167
|
+
const instance = await WebAssembly.instantiate(module, {});
|
|
168
|
+
return new GcfCodec(module, instance.exports, opts);
|
|
169
|
+
}
|
|
170
|
+
get exports() {
|
|
171
|
+
return this.x;
|
|
172
|
+
}
|
|
173
|
+
get memoryBytes() {
|
|
174
|
+
return this.x.memory.buffer.byteLength;
|
|
175
|
+
}
|
|
176
|
+
recycle() {
|
|
177
|
+
this.pending ??= WebAssembly.instantiate(this.module, {}).then((instance) => {
|
|
178
|
+
this.x = instance.exports;
|
|
179
|
+
this.pending = undefined;
|
|
180
|
+
});
|
|
181
|
+
return this.pending;
|
|
159
182
|
}
|
|
160
183
|
load(input) {
|
|
161
184
|
const x = this.exports;
|
|
@@ -168,7 +191,12 @@ class GcfCodec {
|
|
|
168
191
|
const ptr = x.gcf_input_alloc(cap);
|
|
169
192
|
return utf8.encodeInto(input, new Uint8Array(x.memory.buffer, ptr, cap)).written;
|
|
170
193
|
}
|
|
194
|
+
releaseIfLarge() {
|
|
195
|
+
if (!this.pending && this.x.memory.buffer.byteLength > this.maxRetained)
|
|
196
|
+
this.recycle();
|
|
197
|
+
}
|
|
171
198
|
output(status) {
|
|
199
|
+
this.releaseIfLarge();
|
|
172
200
|
const x = this.exports;
|
|
173
201
|
const text = (p, n) => fromUtf8.decode(new Uint8Array(x.memory.buffer, p, n));
|
|
174
202
|
if (status !== 0) {
|
|
@@ -177,6 +205,7 @@ class GcfCodec {
|
|
|
177
205
|
return text(x.gcf_output_ptr(), x.gcf_output_len());
|
|
178
206
|
}
|
|
179
207
|
outputBytes(status, view) {
|
|
208
|
+
this.releaseIfLarge();
|
|
180
209
|
if (status !== 0)
|
|
181
210
|
this.output(status);
|
|
182
211
|
const x = this.exports;
|
|
@@ -960,6 +989,7 @@ class GenericStreamEncoder {
|
|
|
960
989
|
}
|
|
961
990
|
}
|
|
962
991
|
// src/index.ts
|
|
992
|
+
var bundled;
|
|
963
993
|
var shared;
|
|
964
994
|
async function wasmBytes(source) {
|
|
965
995
|
if (source instanceof URL || typeof source === "string") {
|
|
@@ -975,17 +1005,21 @@ async function wasmBytes(source) {
|
|
|
975
1005
|
throw new Error(`failed to load gcf.wasm: HTTP ${res.status}`);
|
|
976
1006
|
return res.arrayBuffer();
|
|
977
1007
|
}
|
|
978
|
-
function init(source) {
|
|
1008
|
+
function init(source, options) {
|
|
979
1009
|
if (source === undefined) {
|
|
980
|
-
|
|
1010
|
+
bundled ??= wasmBytes(new URL("./gcf.wasm", import.meta.url)).then((b) => WebAssembly.compile(b));
|
|
1011
|
+
if (options !== undefined)
|
|
1012
|
+
return bundled.then((m) => GcfCodec.instantiate(m, options));
|
|
1013
|
+
shared ??= bundled.then((m) => GcfCodec.instantiate(m));
|
|
981
1014
|
return shared;
|
|
982
1015
|
}
|
|
983
1016
|
if (source instanceof WebAssembly.Module || source instanceof ArrayBuffer || ArrayBuffer.isView(source)) {
|
|
984
|
-
return GcfCodec.instantiate(source);
|
|
1017
|
+
return GcfCodec.instantiate(source, options);
|
|
985
1018
|
}
|
|
986
|
-
return wasmBytes(source).then((b) => GcfCodec.instantiate(b));
|
|
1019
|
+
return wasmBytes(source).then((b) => GcfCodec.instantiate(b, options));
|
|
987
1020
|
}
|
|
988
1021
|
export {
|
|
1022
|
+
DEFAULT_MAX_RETAINED_MEMORY,
|
|
989
1023
|
DEFAULT_REANCHOR_N,
|
|
990
1024
|
GcfCodec,
|
|
991
1025
|
GcfDecodeError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@felixdrp/gcf-wasm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fast, strict codec for the Graph Compact Format (GCF v3.6.0): an 88 KB WebAssembly SIMD engine written by hand in WAT, with a zero-dependency JS API for Node, Bun, Deno, browsers and edge runtimes.",
|
|
5
5
|
"keywords": ["gcf", "graph-compact-format", "llm", "tokens", "token-efficient", "serialization", "json-alternative", "mcp", "webassembly", "wasm", "simd", "wat"],
|
|
6
6
|
"license": "Apache-2.0",
|