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 +132 -0
- package/dist/client.js +18 -10
- package/dist/html-to-react.js +2 -1
- package/dist/react.js +9 -7
- package/dist/warn.d.ts +1 -6
- package/dist/warn.js +1 -6
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/package.json +1 -1
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/html-to-react.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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 (
|
|
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.
|
|
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"],
|