brookmd 0.25.0 → 0.25.2

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,138 @@ 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.25.2 — 2026-07-27
8
+
9
+ Performance only. Rendered output is unchanged — byte-identical mid-stream, not
10
+ just at finalize.
11
+
12
+ ### Fixed
13
+
14
+ - **An open component block no longer re-scans its whole body on every blank
15
+ line — O(n²) → O(n).** `try_incremental_component` bailed whenever the fed
16
+ buffer ended on a blank line, and because the cache had already been taken the
17
+ bail dropped it, forcing a full tail re-scan, a full re-render, and a fresh
18
+ nested parser over the entire body. Blank lines are legal component-body
19
+ content, so that fired once per body paragraph.
20
+
21
+ The bail existed for a real reason: when the buffer ends blank the full rescan
22
+ renders the component and all its sub-blocks with `open_tail = false`, which
23
+ the nested parser's `force_open_tail = true` commits can never match. Rather
24
+ than work around it with a trigger-byte heuristic (measured: still 3.9×/doubling
25
+ on any body containing a backtick or bracket — i.e. all real ones), the cache
26
+ now carries a **settled twin**: a second nested parser with `force_open_tail`
27
+ off, fed lazily only on the appends that read it, so each catch-up spans just
28
+ the bytes since the previous blank line. A body with no blank lines never
29
+ allocates it. The cache now arms once per stream instead of once per paragraph.
30
+
31
+ Streaming a 64 KB `<Thinking>` body, scan work and wall time:
32
+
33
+ | body | before | after |
34
+ | --- | --- | --- |
35
+ | plain paragraphs | 117,333,095 B / 11,954 ms | 229,837 B / **269 ms** |
36
+ | with inline code | 101,046,887 B / 9,316 ms | 224,977 B / **226 ms** |
37
+ | with links | 86,362,310 B / 6,837 ms | 220,835 B / **180 ms** |
38
+ | with `$` and `<` | 117,333,095 B / 7,333 ms | 229,837 B / **388 ms** |
39
+
40
+ Scan and render work are now 2.00×/doubling (dead linear); the residual
41
+ 3.3–3.6×/doubling in wall time is a separate, pre-existing cliff —
42
+ `assemble_wrapped_body` re-materializes an open block's HTML every append —
43
+ which the already-registered `open-block-html-reemit` shape sits in too.
44
+
45
+ **Trade-off:** the twin holds a second copy of an open component's body bytes
46
+ plus one rendered-HTML set, freed when the block closes. Roughly 2× transient
47
+ memory for open component blocks *that contain blank lines*; blank-free bodies
48
+ are unaffected.
49
+
50
+ ### Added
51
+
52
+ - `tests/scaling.rs` gains `component_multi_para` and `component_multi_para_rich`
53
+ (backticks, brackets, `$`, `<`), both gated `Linear`. The existing
54
+ `component_block_open` shape generates no blank lines and so was structurally
55
+ blind to this: its `scanned` is a flat 249 B from 8 KB to 64 KB. The new shapes
56
+ measure 252.4× on the pre-fix parser (gate fails) and 16.0× over a 16× span
57
+ after.
58
+
59
+ ## 0.25.1 — 2026-07-27
60
+
61
+ A correction release. 0.25.0 shipped one user-visible regression and a dev-gate
62
+ that failed open in production; both are fixed here, along with a pre-existing
63
+ memory leak on the opt-in `childMemo` path. Upgrade from 0.25.0.
64
+
65
+ ### Fixed
66
+
67
+ - **WITHDRAWN: the streaming component-tag deferral from 0.25.0.** That change
68
+ gated component rendering on `open_tail`, which a container propagates
69
+ UNCHANGED to every sub-block of every list item. A component tag alone in a
70
+ **non-last list item** — whose one-line body carries no trailing newline of its
71
+ own — therefore never satisfied the gate again, and rendered as the literal
72
+ text `<Thinking>` for the entire rest of the stream instead of mounting the
73
+ consumer's component. Measured 201/201 ticks affected in a 200-item list, and
74
+ the deferral bought exactly one tick across the three shapes it was written
75
+ for. `brookmd-core` is reverted to its pre-0.24.0 behavior here (0.24.1); the
76
+ convergence property 0.25.0 claimed is withdrawn with it. The one-tick issue it
77
+ was chasing is already contained by the per-block error boundary.
78
+ - **Dev-only warnings ran in production.** The gate read `globalThis.process?.env`,
79
+ which is a different member path from the `process.env.NODE_ENV` free
80
+ identifier that esbuild / Vite / Next substitute — so in every bundled browser
81
+ production build it evaluated to "development", printed to real users, and
82
+ prevented dead-code elimination of the message strings. Every gate is now at
83
+ the call site in the literal form bundlers fold. Verified against a real Vite
84
+ build: 11 dev-warning strings survived a production bundle before, 0 now.
85
+ (Note the trade: in a realm with no `process` at all — an unbundled CDN
86
+ consumer — these six development warnings no longer fire. Error reporting via
87
+ `onBlockError` / `console.error` is NOT gated and is unaffected.)
88
+ - **`childMemo` retained every intermediate tree on a single-element block**
89
+ (pre-existing, opt-in path). The cache key embeds the segment text, so a block
90
+ whose HTML is ONE top-level element — which is every core-emitted block kind —
91
+ could never hit: each patch wrote an entry that was never read again, and
92
+ `CHILD_MEMO_CAP` bounds entry count, not bytes. Peak RSS grew 154 MB → 1.65 GB
93
+ over 800 patches of a streaming table. Such blocks now take the ordinary
94
+ whole-block walk.
95
+
96
+ ### Changed
97
+
98
+ - Shipped bundles are smaller than 0.25.0: gzipped, minified, production —
99
+ `index` 14,232 → 13,407 B, `react` 14,185 → 13,365 B, `client` 4,651 → 4,283 B.
100
+ The boundary's `console.error` keeps everything actionable in production (block
101
+ id, kind, override keys, HTML excerpt, the error) and moves only the
102
+ explanatory prose behind the dev gate.
103
+
104
+ ## brookmd-react-native 0.1.6 — 2026-07-27
105
+
106
+ ### Fixed
107
+
108
+ - Vendored native binaries rebuilt against `brookmd-core` 0.24.2, which makes an
109
+ open component block's streaming cost linear in its body. This matters more
110
+ on-device than in the browser: the pre-fix path spent ~24 s of native CPU on a
111
+ 64 KB token-streamed `<Thinking>` body. No JS changes.
112
+
113
+ ## brookmd-react-native 0.1.5 — 2026-07-27
114
+
115
+ ### Fixed
116
+
117
+ - Vendored native binaries rebuilt against `brookmd-core` 0.24.1, so the on-device
118
+ parser no longer carries the component-tag deferral regression withdrawn in
119
+ brookmd 0.25.1 (a component tag alone in a non-last list item rendered as
120
+ literal text for the rest of the stream). No JS changes.
121
+
122
+ ## brookmd-react-native 0.1.4 — 2026-07-26
123
+
124
+ ### Fixed
125
+
126
+ - **The native transport shim routes worker listeners by event type.** brookmd
127
+ 0.24.0 taught `BrookPool` to listen on `error` / `messageerror` (the browser's
128
+ out-of-band worker-failure channels). `NativeWorker.addEventListener` ignored
129
+ its `type` argument and registered every listener in one set, so the pool's
130
+ fatal handler received the ordinary `ready` / `patch` envelopes and read the
131
+ first as `brookmd worker failed to load`, killing every stream before it
132
+ started. Neither channel can fire for an in-process shim — there is no script
133
+ URL to fetch and no structured-clone step — so they are now accepted and never
134
+ fired. This was latent: the package pinned `brookmd` to `^0.23.0`, which
135
+ predates those listeners, so it only surfaced when the range moved.
136
+ - `brookmd` dependency range corrected `^0.23.0` → `^0.25.0`; vendored native
137
+ binaries rebuilt against `brookmd-core` 0.24.0.
138
+
7
139
  ## 0.25.0 — 2026-07-26
8
140
 
9
141
  Fixes a crash class where a `components` override could be invoked with the
package/dist/client.js CHANGED
@@ -29,10 +29,12 @@ function applyPatch(store, patch) {
29
29
  for (let i = 0; i < store.committedOrder.length; i++) {
30
30
  const b = store.committed.get(store.committedOrder[i]);
31
31
  if (b === void 0) {
32
- warnOnce(
33
- "orphan-committed",
34
- `brookmd: committed block ${store.committedOrder[i]} is missing from the store and was dropped. This is a bug in brookmd \u2014 please report it.`
35
- );
32
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
33
+ warnOnce(
34
+ "orphan-committed",
35
+ `brookmd: committed block ${store.committedOrder[i]} is missing from the store and was dropped. This is a bug in brookmd \u2014 please report it.`
36
+ );
37
+ }
36
38
  continue;
37
39
  }
38
40
  next[w++] = b;
@@ -40,7 +42,9 @@ function applyPatch(store, patch) {
40
42
  for (let i = 0; i < store.active.length; i++) {
41
43
  const b = store.active[i];
42
44
  if (b === void 0) {
43
- warnOnce("orphan-active", `brookmd: active block at index ${i} is undefined and was dropped.`);
45
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
46
+ warnOnce("orphan-active", `brookmd: active block at index ${i} is undefined and was dropped.`);
47
+ }
44
48
  continue;
45
49
  }
46
50
  next[w++] = b;
@@ -476,10 +480,12 @@ class BrookClient {
476
480
  if (this.failedError === null) return;
477
481
  this.pendingRebind = false;
478
482
  if (this.store.snapshot.length === 0 && !this.staleSnapshot) return;
479
- warnOnce(
480
- "rebind-reset",
481
- "brookmd: the parser was rebuilt on a new worker after a fatal failure, so the document restarted. Blocks rendered before the failure were dropped because the fresh parser renumbers from zero. Enable `recovery` (the default) to re-feed and heal invisibly instead."
482
- );
483
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
484
+ warnOnce(
485
+ "rebind-reset",
486
+ "brookmd: the parser was rebuilt on a new worker after a fatal failure, so the document restarted. Blocks rendered before the failure were dropped because the fresh parser renumbers from zero. Enable `recovery` (the default) to re-feed and heal invisibly instead."
487
+ );
488
+ }
483
489
  this.resetParser();
484
490
  }
485
491
  get ready() {
@@ -816,7 +822,9 @@ class BrookClient {
816
822
  view[i] = { ...nb, id: this.idNamespace + nb.id };
817
823
  }
818
824
  if (dropped) {
819
- warnOnce("merge-hole", "brookmd: the stale-merge produced an empty position \u2014 compacting.");
825
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
826
+ warnOnce("merge-hole", "brookmd: the stale-merge produced an empty position \u2014 compacting.");
827
+ }
820
828
  this.mergeDropped = true;
821
829
  return view.filter((b) => b !== void 0);
822
830
  }
@@ -238,7 +238,7 @@ function pushDecoratedText(out, text, decorators, ancestors, keyBase) {
238
238
  function resolveTagType(tag, components) {
239
239
  const c = tag.charCodeAt(0);
240
240
  if (c >= 65 && c <= 90 && BLOCK_KIND_KEYS.has(tag)) {
241
- if (components[tag] !== void 0) {
241
+ if (components[tag] !== void 0 && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
242
242
  warnOnce(
243
243
  "kind-key-as-tag:" + tag,
244
244
  `brookmd: raw <${tag}> in the rendered HTML collides with the block-kind override key "${tag}". A block-kind override receives \`BlockComponentProps\` (with \`block\`), which an inline element cannot supply, so the raw tag is rendered as a plain element instead. Rename the raw tag or the override key.`
@@ -335,6 +335,7 @@ function htmlToReact(html, components, childMemoMap, opts) {
335
335
  const ctx = opts && (opts.decorators || opts.urlTransform) ? { decorators: opts.decorators, urlTransform: opts.urlTransform } : void 0;
336
336
  if (!childMemoMap) return nodesToReact(parseTrustedHtml(html), components, "", ctx, []);
337
337
  const segs = topLevelSegments(html);
338
+ if (segs.length < 2) return nodesToReact(parseTrustedHtml(html), components, "", ctx, []);
338
339
  const out = [];
339
340
  for (let idx = 0; idx < segs.length; idx++) {
340
341
  const seg = segs[idx];
package/dist/react.js CHANGED
@@ -19,10 +19,12 @@ import { warnOnce } from "./warn.js";
19
19
  const NO_DEFER_BLOCKS = [];
20
20
  const EMPTY_KEYS = [];
21
21
  function skipBadBlock(index) {
22
- warnOnce(
23
- "bad-block",
24
- `brookmd: snapshot position ${index} has no block kind and was skipped. This indicates a corrupted block store \u2014 please report it.`
25
- );
22
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
23
+ warnOnce(
24
+ "bad-block",
25
+ `brookmd: snapshot position ${index} has no block kind and was skipped. This indicates a corrupted block store \u2014 please report it.`
26
+ );
27
+ }
26
28
  return null;
27
29
  }
28
30
  const warnedUnstable = /* @__PURE__ */ new Set();
@@ -32,8 +34,7 @@ function useUnstablePropWarning(name, value) {
32
34
  const prevDefined = ref.current !== void 0 && ref.current !== null;
33
35
  const nextDefined = value !== void 0 && value !== null;
34
36
  ref.current = value;
35
- const env = globalThis.process?.env;
36
- if (prevDefined && nextDefined && !warnedUnstable.has(name) && (!env || env.NODE_ENV !== "production")) {
37
+ if (prevDefined && nextDefined && !warnedUnstable.has(name) && typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
37
38
  warnedUnstable.add(name);
38
39
  console.warn(
39
40
  `<BrookMarkdown>: the \`${name}\` prop changed identity between renders. Hoist it to module scope or wrap it in useMemo \u2014 a fresh identity each render busts the per-block memo and re-parses every block on every patch.`
@@ -625,8 +626,9 @@ class BlockBoundary extends Component {
625
626
  this.props.onBlockError(error, info);
626
627
  return;
627
628
  }
629
+ const hint = typeof process !== "undefined" && process.env.NODE_ENV !== "production" ? ` This is almost always a \`components\` override throwing. If it is registered for a block-kind or component-tag key, note that the SAME key is also dispatched for a matching element nested inside a block's HTML \u2014 that call gets attributes + children only, with no \`block\` prop. Guard with \`if (!block) return <>{children}</>\`.` : "";
628
630
  console.error(
629
- `brookmd: block ${info.blockId} (${info.kind}) failed to render and was skipped. This is almost always a \`components\` override throwing. If the override is registered for a block-kind or component-tag key, note that the SAME key is also dispatched for a matching element nested inside a block's HTML \u2014 that call gets attributes + children only, with no \`block\` prop. Guard with \`if (!block) return <>{children}</>\`.`,
631
+ `brookmd: block ${info.blockId} (${info.kind}) failed to render and was skipped.${hint}`,
630
632
  { componentKeys: info.componentKeys, html: info.html },
631
633
  error
632
634
  );
package/dist/warn.d.ts CHANGED
@@ -1,9 +1,4 @@
1
- /** True unless the bundler/runtime says NODE_ENV === "production". Read off
2
- * `globalThis` so there is no @types/node dependency; bundlers inline
3
- * `process.env.NODE_ENV`, and absence is treated as dev (same rule as the
4
- * unstable-prop tripwire in react.tsx). */
5
- export declare function isDev(): boolean;
6
- /** Warn once per `id` (dev only). Returns true if it actually warned. */
1
+ /** Warn once per `id`. Callers own the production gate see the contract above. */
7
2
  export declare function warnOnce(id: string, message: string): boolean;
8
3
  /** Test-only: clear the latch so a test can assert a warning fires. */
9
4
  export declare function __resetWarnOnce(): void;
package/dist/warn.js CHANGED
@@ -1,10 +1,6 @@
1
1
  const warned = /* @__PURE__ */ new Set();
2
- function isDev() {
3
- const env = globalThis.process?.env;
4
- return !env || env.NODE_ENV !== "production";
5
- }
6
2
  function warnOnce(id, message) {
7
- if (!isDev() || warned.has(id)) return false;
3
+ if (warned.has(id)) return false;
8
4
  warned.add(id);
9
5
  console.warn(message);
10
6
  return true;
@@ -14,6 +10,5 @@ function __resetWarnOnce() {
14
10
  }
15
11
  export {
16
12
  __resetWarnOnce,
17
- isDev,
18
13
  warnOnce
19
14
  };
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brookmd",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
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"],