@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 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
- exports;
157
- constructor(exports) {
158
- this.exports = exports;
159
- }
160
- static async instantiate(wasm) {
161
- const instance = wasm instanceof WebAssembly.Module ? await WebAssembly.instantiate(wasm, {}) : (await WebAssembly.instantiate(wasm, {})).instance;
162
- return new GcfCodec(instance.exports);
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
- shared ??= wasmBytes(new URL("./gcf.wasm", import.meta.url)).then((b) => GcfCodec.instantiate(b));
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
- readonly exports: Exports;
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). Each codec owns its
7
- * own WebAssembly memory; create more than one only for parallel use.
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
- exports;
153
- constructor(exports) {
154
- this.exports = exports;
155
- }
156
- static async instantiate(wasm) {
157
- const instance = wasm instanceof WebAssembly.Module ? await WebAssembly.instantiate(wasm, {}) : (await WebAssembly.instantiate(wasm, {})).instance;
158
- return new GcfCodec(instance.exports);
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
- shared ??= wasmBytes(new URL("./gcf.wasm", import.meta.url)).then((b) => GcfCodec.instantiate(b));
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.1.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",