brookmd 0.22.2 → 0.23.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/CHANGELOG.md CHANGED
@@ -4,6 +4,48 @@ Notable changes to brookmd (formerly `flux-md`). Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/); this project aims to follow
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## 0.23.1 — 2026-07-21
8
+
9
+ ### Changed
10
+
11
+ - Documentation only: the README now covers wire delta mode (the 0.23.0
12
+ headline) — measured numbers, the automatic worker↔client behavior, and the
13
+ raw-boundary opt-in — plus refreshed platform-status notes across the
14
+ repository. No code changes; behavior identical to 0.23.0.
15
+
16
+ ## 0.23.0 — 2026-07-21
17
+
18
+ ### Added
19
+
20
+ - **Wire delta mode — the streaming re-emit floor is retired** (wire contract
21
+ **v1.2.0**, additive). Previously, every `append` re-emitted each open
22
+ block's full HTML across the WASM→worker→main-thread boundary, making total
23
+ emitted bytes O(n²/chunk) for a block that grows across many chunks — the
24
+ documented "re-emit floor". Now the parser can emit a **verified splice**
25
+ (`html_delta: { keep_bytes, keep_units, append }`) against the block's
26
+ previous emit instead; splices are established by byte comparison, never by
27
+ structural inference, so reconstruction is byte-exact by construction.
28
+
29
+ Measured at 200 KB / 256-byte chunks: a streaming list's total patch JSON
30
+ drops **119.6 MB → 0.78 MB (153× less)** with **2.8× faster** end-to-end
31
+ parse+serialize; an unclosed code fence **80.1 MB → 0.58 MB (137×, 4.9×
32
+ faster)**. Fast-committing shapes are unchanged (no regression). The curve
33
+ is now linear, so the gap keeps widening with document size.
34
+
35
+ - The npm package enables this **automatically and invisibly** — the worker
36
+ opts in, and the client reconstructs full blocks before anything else sees
37
+ them. No API change; `Block.html` is always complete.
38
+ - `brookmd-react-native` does the same across the JSI boundary.
39
+ - Raw-boundary consumers (`BrookParser`, native `BrookSession`, C ABI): the
40
+ mode is **opt-in** (`setWireDelta(true)` / `wire_delta: true`) and the
41
+ default wire is byte-identical to contract v1.1.0. Enabling it obliges you
42
+ to reconstruct per [WIRE.md §11](../../crates/brookmd-core/WIRE.md).
43
+ - Pinned by: reconstruction-parity suites (Rust corpus × chunkings, dual
44
+ UTF-8/UTF-16 offset verification), wire goldens in all five language
45
+ surfaces (core, FFI, C-ABI, Kotlin, Swift), a fuzzer extension asserting
46
+ per-patch reconstruction on arbitrary inputs, and a new scaling-gate test
47
+ that *fails CI* if delta-mode emitted bytes regress from linear.
48
+
7
49
  ## 0.22.2 — 2026-07-21
8
50
 
9
51
  ### Performance
package/README.md CHANGED
@@ -4,7 +4,7 @@ Zero-dep streaming markdown for the browser. Rust→WASM core, one Web Worker pe
4
4
 
5
5
  Drop in a streaming-aware renderer — **React, Vue, Svelte, Solid, a framework-agnostic `<brook-markdown>` Web Component, or the vanilla DOM mount** — wire each LLM stream to a `BrookClient`, and the markdown renders incrementally off the main thread, block by block, with stable identities so unchanged blocks never re-reconcile.
6
6
 
7
- Parsing runs entirely **off the main thread** — each stream gets its own pooled Web Worker, so many concurrent LLM responses render without contending for the UI thread. On each token the parser re-parses only the **active tail**, not the whole document, and heavy renderers (syntax highlighting, math, mermaid) are **deferred until a block closes**. The result is low retained memory and a main thread that stays responsive while streaming. See [the live demo](https://md.hsingh.app/).
7
+ Parsing runs entirely **off the main thread** — each stream gets its own pooled Web Worker, so many concurrent LLM responses render without contending for the UI thread. On each token the parser re-parses only the **active tail**, not the whole document; patches cross the worker boundary as **verified splices** (not full re-sends, so emitted bytes stay O(n) even for one giant growing block); and heavy renderers (syntax highlighting, math, mermaid) are **deferred until a block closes**. The result is low retained memory and a main thread that stays responsive while streaming. See [the live demo](https://md.hsingh.app/).
8
8
 
9
9
  > **Beyond the browser:** the same Rust core also powers experimental React
10
10
  > Native, Swift (iOS/macOS), Kotlin/Android, Flutter, and C-ABI bindings —
@@ -1044,6 +1044,26 @@ Headline numbers are not durable across machines, but the curve is: chunk size
1044
1044
  shouldn't change the order of magnitude for any shape. If you hit one that does,
1045
1045
  file an issue with the input and chunking — that's the next bench scenario.
1046
1046
 
1047
+ ### Wire delta mode (automatic)
1048
+
1049
+ Parse work was already O(n), but until 0.23.0 the **bytes crossing the
1050
+ worker→main-thread boundary** were not: every append re-emitted the open
1051
+ block's full HTML, O(n²/chunk) total for one block that grows across many
1052
+ chunks (a long streaming list, a big code fence). Since 0.23.0 the parser
1053
+ emits **verified splices** instead — `{keep, append}` deltas against the
1054
+ block's previous emit, established by byte comparison so reconstruction is
1055
+ byte-exact by construction — and the client reassembles full blocks before
1056
+ anything else sees them. Zero API change; `Block.html` is always complete.
1057
+
1058
+ Measured at a 200 KB document in 256-byte chunks, a streaming list's total
1059
+ patch traffic drops from **119.6 MB to 0.78 MB (153× less, 2.8× faster
1060
+ end-to-end)**; an unclosed code fence from **80.1 MB to 0.58 MB (137×, 4.9×
1061
+ faster)**. Fast-committing prose is unchanged. Emitted bytes are now gated
1062
+ linear in CI alongside the parse-work counters. Raw-boundary consumers (the
1063
+ WASM `BrookParser`, native bindings, C ABI) keep byte-identical v1 wire by
1064
+ default and can opt in with `setWireDelta(true)` — see
1065
+ [`WIRE.md` §11](https://github.com/siinghd/brookmd/blob/main/crates/brookmd-core/WIRE.md).
1066
+
1047
1067
  ## Security
1048
1068
 
1049
1069
  brookmd is XSS-safe by default — its HTML output is meant to be injected via
package/dist/client.js CHANGED
@@ -10,7 +10,19 @@ function applyPatch(store, patch) {
10
10
  if (!store.committed.has(b.id)) store.committedOrder.push(b.id);
11
11
  store.committed.set(b.id, b);
12
12
  }
13
- store.active = patch.active;
13
+ const active = new Array(patch.active.length);
14
+ for (let i = 0; i < patch.active.length; i++) {
15
+ const entry = patch.active[i];
16
+ if ("html_delta" in entry) {
17
+ const { html_delta, ...rest } = entry;
18
+ const prev = store.active.find((b) => b.id === entry.id);
19
+ if (!prev) throw new Error(`brookmd: html_delta for block ${entry.id} without a base`);
20
+ active[i] = { ...rest, html: prev.html.slice(0, html_delta.keep_units) + html_delta.append };
21
+ } else {
22
+ active[i] = entry;
23
+ }
24
+ }
25
+ store.active = active;
14
26
  const next = new Array(store.committedOrder.length + store.active.length);
15
27
  for (let i = 0; i < store.committedOrder.length; i++) {
16
28
  next[i] = store.committed.get(store.committedOrder[i]);
@@ -679,13 +691,13 @@ class BrookClient {
679
691
  let patch;
680
692
  try {
681
693
  patch = JSON.parse(msg.patch);
694
+ applyPatch(this.store, patch);
682
695
  } catch (e) {
683
696
  const message = e instanceof Error ? e.message : String(e);
684
697
  if (this.onError) this.onError({ message: `brookmd: malformed patch (${message})` });
685
698
  else console.error("brookmd: malformed patch:", message);
686
699
  break;
687
700
  }
688
- applyPatch(this.store, patch);
689
701
  if (msg.final === true && this.staleSnapshot) {
690
702
  this.staleTrimmed = true;
691
703
  this.mergeCache = null;
@@ -164,9 +164,30 @@ export interface Block {
164
164
  open: boolean;
165
165
  speculative: boolean;
166
166
  }
167
+ /**
168
+ * Wire delta mode (WIRE.md §11): the splice an active block carries in place
169
+ * of `html` when it was already emitted in the previous patch. Reconstruct
170
+ * with `prev.slice(0, keep_units) + append` (JS strings are UTF-16, so
171
+ * `keep_units` is the right offset here; `keep_bytes` is the same prefix for
172
+ * byte-oriented consumers).
173
+ */
174
+ export interface WireHtmlDelta {
175
+ keep_bytes: number;
176
+ keep_units: number;
177
+ append: string;
178
+ }
179
+ /**
180
+ * An `active` array entry as serialized: a full {@link Block}, or (wire delta
181
+ * mode only) every Block field except `html` plus an `html_delta` splice.
182
+ * {@link applyPatch} reconstructs deltas into full Blocks — nothing past the
183
+ * store ever sees this union.
184
+ */
185
+ export type WireActiveBlock = Block | (Omit<Block, "html"> & {
186
+ html_delta: WireHtmlDelta;
187
+ });
167
188
  export interface Patch {
168
189
  newly_committed: Block[];
169
- active: Block[];
190
+ active: WireActiveBlock[];
170
191
  }
171
192
  /**
172
193
  * Per-block render-churn sample passed to an {@link RenderMetricsHook}. Lets you
@@ -52,15 +52,24 @@ entirely:
52
52
 
53
53
  ```toml
54
54
  [dependencies]
55
- brookmd-core = { version = "0.20", default-features = false }
55
+ brookmd-core = { version = "0.23", default-features = false }
56
56
  ```
57
57
 
58
58
  ## Wire format
59
59
 
60
60
  Blocks and patches serialize to a stable, language-agnostic JSON wire format —
61
- see [WIRE.md](WIRE.md) (wire contract v1.1.0). Native consumers can produce the
61
+ see [WIRE.md](WIRE.md) (wire contract v1.2.0). Native consumers can produce the
62
62
  same bytes as the WASM/JS boundary via `wire::patch_to_json` / `wire::blocks_to_json`.
63
63
 
64
+ Contract v1.2.0 adds the opt-in **wire delta mode**
65
+ (`StreamParser::set_wire_delta`): active blocks re-emitted across appends
66
+ serialize as verified `html_delta` splices against their previous emit instead
67
+ of full `html`, making total emitted bytes O(n) for a block that grows across
68
+ many appends (WIRE.md §11). Off by default — the default wire stays
69
+ byte-identical to v1.1.0. A consumer that enables it reconstructs
70
+ `prev[..keep] + append` per patch; the npm and React Native packages do this
71
+ transparently.
72
+
64
73
  ## Links
65
74
 
66
75
  - npm package: <https://www.npmjs.com/package/brookmd>
@@ -112,6 +112,15 @@ export class BrookParser {
112
112
  * when rendering untrusted input — bypasses XSS protection.
113
113
  */
114
114
  setUnsafeHtml(on: boolean): void;
115
+ /**
116
+ * Opt-in wire delta mode (WIRE.md §11): active blocks re-emitted across
117
+ * appends serialize as verified `html_delta` splices against their previous
118
+ * emit instead of full `html`, making total emitted bytes O(n) for a block
119
+ * that grows across many appends. Off by default (wire byte-identical to
120
+ * pre-delta releases). A consumer that enables this must reconstruct
121
+ * active `html` per WIRE.md §11 — the npm package's client does.
122
+ */
123
+ setWireDelta(on: boolean): void;
115
124
  }
116
125
 
117
126
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
@@ -137,6 +146,7 @@ export interface InitOutput {
137
146
  readonly brookparser_setHtmlSanitize: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
138
147
  readonly brookparser_setInlineComponentTags: (a: number, b: number, c: number) => void;
139
148
  readonly brookparser_setUnsafeHtml: (a: number, b: number) => void;
149
+ readonly brookparser_setWireDelta: (a: number, b: number) => void;
140
150
  readonly __wbindgen_export: (a: number, b: number) => number;
141
151
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
142
152
  readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
@@ -255,6 +255,18 @@ export class BrookParser {
255
255
  setUnsafeHtml(on) {
256
256
  wasm.brookparser_setUnsafeHtml(this.__wbg_ptr, on);
257
257
  }
258
+ /**
259
+ * Opt-in wire delta mode (WIRE.md §11): active blocks re-emitted across
260
+ * appends serialize as verified `html_delta` splices against their previous
261
+ * emit instead of full `html`, making total emitted bytes O(n) for a block
262
+ * that grows across many appends. Off by default (wire byte-identical to
263
+ * pre-delta releases). A consumer that enables this must reconstruct
264
+ * active `html` per WIRE.md §11 — the npm package's client does.
265
+ * @param {boolean} on
266
+ */
267
+ setWireDelta(on) {
268
+ wasm.brookparser_setWireDelta(this.__wbg_ptr, on);
269
+ }
258
270
  }
259
271
  if (Symbol.dispose) BrookParser.prototype[Symbol.dispose] = BrookParser.prototype.free;
260
272
  function __wbg_get_imports() {
Binary file
@@ -20,6 +20,7 @@ export const brookparser_setGfmTagfilter: (a: number, b: number) => void;
20
20
  export const brookparser_setHtmlSanitize: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
21
21
  export const brookparser_setInlineComponentTags: (a: number, b: number, c: number) => void;
22
22
  export const brookparser_setUnsafeHtml: (a: number, b: number) => void;
23
+ export const brookparser_setWireDelta: (a: number, b: number) => void;
23
24
  export const __wbindgen_export: (a: number, b: number) => number;
24
25
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
25
26
  export const __wbindgen_add_to_stack_pointer: (a: number) => number;
package/dist/worker.js CHANGED
@@ -25,6 +25,7 @@ const core = new WorkerCore({
25
25
  c?.dropHtmlTags ?? []
26
26
  );
27
27
  p.setBlockData(c?.blockData ?? false);
28
+ p.setWireDelta(true);
28
29
  return p;
29
30
  },
30
31
  post: (msg) => ctx.postMessage(msg),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brookmd",
3
- "version": "0.22.2",
3
+ "version": "0.23.1",
4
4
  "description": "Zero-dep streaming markdown for the browser. Rust→WASM core, Web Worker per stream, incremental parse with speculative closure.",
5
5
  "type": "module",
6
6
  "sideEffects": ["./dist/worker.js", "./dist/styles.css"],