brookmd 0.27.0 → 0.29.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/CHANGELOG.md +96 -0
- package/README.md +100 -0
- package/dist/client.d.ts +139 -0
- package/dist/client.js +251 -2
- package/dist/dom.d.ts +17 -0
- package/dist/dom.js +196 -18
- package/dist/element.js +3 -1
- package/dist/hi-inc.d.ts +25 -0
- package/dist/hi-inc.js +6 -0
- package/dist/html-to-react.d.ts +2 -0
- package/dist/html-to-react.js +7 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/react-splice.d.ts +32 -0
- package/dist/react-splice.js +33 -0
- package/dist/react.d.ts +11 -0
- package/dist/react.js +29 -8
- package/dist/renderers/CodeBlock.d.ts +13 -1
- package/dist/renderers/CodeBlock.js +51 -8
- package/dist/server.js +1 -0
- package/dist/splice.d.ts +126 -0
- package/dist/splice.js +212 -0
- package/dist/types-core.d.ts +16 -0
- package/dist/wasm/brook_md_core.d.ts +13 -0
- package/dist/wasm/brook_md_core.js +15 -0
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/dist/wasm/brook_md_core_bg.wasm.d.ts +1 -0
- package/dist/worker.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,102 @@ 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.29.0 — 2026-07-31
|
|
8
|
+
|
|
9
|
+
**Sublinear resources.** Total parse work has an Ω(n) floor and stays exactly
|
|
10
|
+
there — but retained memory and reopen latency don't have to be O(n), and now
|
|
11
|
+
they aren't. Requires `brookmd-core` 0.26.0. Both features are **web-path**
|
|
12
|
+
(WASM + TS client); the native bindings (React Native / Kotlin / Swift /
|
|
13
|
+
Flutter) share every parser-core improvement but consume `allBlocks()` and so
|
|
14
|
+
keep full retention — see the platform matrix in the root README.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Instant thread reopen — `getPersistable()` / `hydrate()` /
|
|
19
|
+
`sourceFingerprint()`.** A finished (or in-flight) stream can be persisted
|
|
20
|
+
as a small JSON envelope — the blocks `getSnapshot()` showed plus a source
|
|
21
|
+
length/hash and a `done` flag — and a fresh `BrookClient` hydrates from it
|
|
22
|
+
with **no worker, no WASM, no parsing**: 1 MB / 2,250 blocks re-streams in
|
|
23
|
+
~110 ms, hydrates in **~5.5 ms** (and a real browser reopen also skips
|
|
24
|
+
worker spin-up and WASM init). To *continue* a non-finalized thread, pass
|
|
25
|
+
the original source to `beginResume()`: the UI keeps showing hydrated
|
|
26
|
+
blocks while a background re-parse catches up — reusing the existing
|
|
27
|
+
divergence/adoption machinery, so every hydrated block keeps its exact id
|
|
28
|
+
across the swap and nothing visible remounts; appends stream live once
|
|
29
|
+
caught up. The envelope is a versioned **package** format
|
|
30
|
+
(`hydrateVersion`), deliberately not the wire contract; the README's
|
|
31
|
+
"Instant thread reopen" section documents the persistence and invalidation
|
|
32
|
+
contract.
|
|
33
|
+
- **`retainCommittedHtml` — the parser no longer hoards the rendered
|
|
34
|
+
document.** The core kept every committed block's HTML alive for
|
|
35
|
+
`allBlocks()` — which the web worker path never calls. The worker now
|
|
36
|
+
defaults the flag **off**: retained memory beyond the source buffer drops
|
|
37
|
+
to the open tail only (measured: **53–70% less total retention** on 1 MB
|
|
38
|
+
streams, with the flag-off overhead above the raw source staying under
|
|
39
|
+
300 bytes at every point). Patches are byte-identical either way — committed
|
|
40
|
+
blocks are emitted exactly once and never re-read. Rust-side default stays
|
|
41
|
+
ON, so native bindings and `allBlocks()` consumers are unaffected;
|
|
42
|
+
`renderToString` pins it on (it assembles from `allBlocks`). Set
|
|
43
|
+
`retainCommittedHtml: true` in `ParserConfig` if you drive the worker and
|
|
44
|
+
want `allBlocks` fidelity.
|
|
45
|
+
|
|
46
|
+
## 0.28.0 — 2026-07-31
|
|
47
|
+
|
|
48
|
+
**Browser-side linearity.** The parser and wire have been O(new bytes) per
|
|
49
|
+
append for a while; this release makes the *DOM application* match. Before, an
|
|
50
|
+
open (streaming) block was fully rebuilt on every animation frame — a 20 KB
|
|
51
|
+
highlighted code block wrote ~44 million characters of `innerHTML` over its
|
|
52
|
+
lifetime (≈2,150× the wire bytes), and a per-frame `replaceWith` destroyed any
|
|
53
|
+
text selection or `<pre>` scroll position inside the block. Every open-block
|
|
54
|
+
path now applies patches incrementally, measured at **2–3.4× the block's final
|
|
55
|
+
markup, flat across sizes** (1× is the write-it-once floor), and enforced by a
|
|
56
|
+
chars-written regression gate so browser-side linearity is CI-pinned like the
|
|
57
|
+
parser's scaling shapes.
|
|
58
|
+
|
|
59
|
+
### Performance
|
|
60
|
+
|
|
61
|
+
- **Open blocks apply the wire's splice instead of rebuilding.** The delta
|
|
62
|
+
signal (`keep_units`) the wire already computed was being discarded at the
|
|
63
|
+
client; it now reaches both renderers, which rebuild only the element the
|
|
64
|
+
splice lands in (fast path fires ~94% of syncs; any ambiguity falls back to
|
|
65
|
+
a full rebuild, so correctness never depends on the fast path).
|
|
66
|
+
- **Open code blocks reuse the streaming highlighter's frozen prefix**: the
|
|
67
|
+
frozen markup is appended once and never rewritten; only the bounded
|
|
68
|
+
speculative tail repaints. 20 KB highlighted stream: ~44 MB written → ~370 KB
|
|
69
|
+
(**~120× less**).
|
|
70
|
+
- **Keyed list/container sync in the DOM renderer** (`blockData` on): settled
|
|
71
|
+
`<li>`s / nested container children are never re-rendered — new items append,
|
|
72
|
+
only the open last item repaints; a tight→loose flip resyncs once, keyed by
|
|
73
|
+
`(index, html)` so it cannot be silently missed. Streamed 20 KB list: 284× →
|
|
74
|
+
**2.9×**; blockquote: 318× → **3.4×**.
|
|
75
|
+
- **React's keyed container path now engages by default.** It was accidentally
|
|
76
|
+
gated behind a `components` map (git history shows the gate was incidental);
|
|
77
|
+
a default-config streaming blockquote/alert now renders keyed — 22.6× and
|
|
78
|
+
growing → **3.4× flat**. React and DOM implementations were cross-checked to
|
|
79
|
+
byte-identical work counts.
|
|
80
|
+
- Known documented fallback: a streamed table with `blockData: false` still
|
|
81
|
+
takes the full-rebuild path (the splice refuses table tag chains — foster
|
|
82
|
+
parenting). `blockData: true` tables were already incremental (1.7×). A
|
|
83
|
+
table-scoped splice was measured (15% improvement) and rejected as not worth
|
|
84
|
+
the parser-divergence risk.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- **Text selection and `<pre>` scroll survive streaming.** Selecting text in
|
|
89
|
+
the already-settled part of a streaming block no longer collapses on the
|
|
90
|
+
next token; a code block's horizontal scroll position is preserved. Both are
|
|
91
|
+
pinned by tests with negative controls.
|
|
92
|
+
|
|
93
|
+
### Changed
|
|
94
|
+
|
|
95
|
+
- The DOM inside an open, streaming `<code>` element is now two spans (frozen +
|
|
96
|
+
speculative tail) while streaming; it settles to the same single-markup form
|
|
97
|
+
as before when the block closes. CSS that targets `code > *` structurally
|
|
98
|
+
may observe the difference mid-stream only. Settled output is unchanged.
|
|
99
|
+
- React's default open blockquote/alert omits inter-block whitespace text
|
|
100
|
+
nodes mid-stream (matching the DOM renderer's long-standing keyed behavior);
|
|
101
|
+
layout is unaffected and settle output is byte-identical.
|
|
102
|
+
|
|
7
103
|
## 0.27.0 — 2026-07-31
|
|
8
104
|
|
|
9
105
|
### Added
|
package/README.md
CHANGED
|
@@ -586,6 +586,11 @@ class BrookClient {
|
|
|
586
586
|
whenReady(): Promise<void>; // resolves once WASM loaded; rejects on init failure
|
|
587
587
|
subscribe(listener: () => void): () => void; // React-friendly store
|
|
588
588
|
getSnapshot(): Block[]; // ordered current blocks
|
|
589
|
+
getPersistable(source?: string): PersistableSnapshot; // capture the rendered doc as JSON
|
|
590
|
+
hydrate( // restore it: no worker, no parse
|
|
591
|
+
snapshot: PersistableSnapshot,
|
|
592
|
+
opts?: { source?: string }, // source ⇒ a live thread can resume
|
|
593
|
+
): void;
|
|
589
594
|
outline(): { level: number; text: string; id: number }[]; // heading table-of-contents (works mid-stream)
|
|
590
595
|
toPlaintext(): string; // rendered document as plain text (search / summaries)
|
|
591
596
|
getMetrics(): { bytes, patches, totalParseMs, throughputKBs,
|
|
@@ -1284,6 +1289,101 @@ WASM `BrookParser`, native bindings, C ABI) keep byte-identical v1 wire by
|
|
|
1284
1289
|
default and can opt in with `setWireDelta(true)` — see
|
|
1285
1290
|
[`WIRE.md` §11](https://github.com/siinghd/brookmd/blob/main/crates/brookmd-core/WIRE.md).
|
|
1286
1291
|
|
|
1292
|
+
## Instant thread reopen — persist and hydrate
|
|
1293
|
+
|
|
1294
|
+
Reopening a long thread normally means re-feeding its whole source through the
|
|
1295
|
+
parser before the first paint: O(history) work between the click and the pixels,
|
|
1296
|
+
which is exactly when a chat UI feels frozen.
|
|
1297
|
+
|
|
1298
|
+
It is also unnecessary. A committed block is emitted **once** and is **final**,
|
|
1299
|
+
so the blocks a stream already produced are a complete description of the
|
|
1300
|
+
document — there is nothing to re-derive. Capture them, store them, restore them:
|
|
1301
|
+
|
|
1302
|
+
```ts
|
|
1303
|
+
// When the thread closes (or at any checkpoint):
|
|
1304
|
+
await db.put(threadId, JSON.stringify(client.getPersistable()));
|
|
1305
|
+
|
|
1306
|
+
// When it reopens:
|
|
1307
|
+
const client = new BrookClient();
|
|
1308
|
+
client.hydrate(JSON.parse(await db.get(threadId))); // no worker, no WASM, no parse
|
|
1309
|
+
// <BrookMarkdown client={client} /> paints the whole thread on the first frame.
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
Measured on a 1 MB / ~2250-block thread: **~5 ms to hydrate vs ~110 ms to
|
|
1313
|
+
re-stream** it in 2 KB chunks (~51 ms even for a single-shot re-parse) — before
|
|
1314
|
+
counting the worker spin-up and WASM init a re-stream also pays. Hydration is
|
|
1315
|
+
O(blocks) of JSON handling; nothing parses.
|
|
1316
|
+
|
|
1317
|
+
Hydrated blocks are **ordinary committed blocks**: both renderers mount them
|
|
1318
|
+
through the same path as live ones, so a reopened thread renders byte-identical
|
|
1319
|
+
to the one the user closed.
|
|
1320
|
+
|
|
1321
|
+
### What to store, and when to throw it away
|
|
1322
|
+
|
|
1323
|
+
```ts
|
|
1324
|
+
interface PersistableSnapshot {
|
|
1325
|
+
hydrateVersion: number; // envelope version; hydrate() refuses what it can't read
|
|
1326
|
+
blocks: Block[]; // exactly what getSnapshot() showed
|
|
1327
|
+
sourceLength: number; // UTF-16 length of the markdown behind them
|
|
1328
|
+
sourceHash: string; // fingerprint of that markdown — a staleness check
|
|
1329
|
+
done: boolean; // had the stream been finalized?
|
|
1330
|
+
}
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
Store it as JSON beside the thread. **Invalidate it when your source moves on**:
|
|
1334
|
+
hydrate deliberately does *not* verify the blocks against the source, so a stale
|
|
1335
|
+
snapshot would paint stale HTML. That is what `sourceHash` is for:
|
|
1336
|
+
|
|
1337
|
+
```ts
|
|
1338
|
+
import { sourceFingerprint } from "brookmd";
|
|
1339
|
+
|
|
1340
|
+
if (snapshot.sourceHash === sourceFingerprint(mySource)) {
|
|
1341
|
+
client.hydrate(snapshot); // instant
|
|
1342
|
+
} else {
|
|
1343
|
+
client.setContent(mySource, { done: true }); // the text changed — re-stream
|
|
1344
|
+
}
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
Discard a snapshot whose `hydrateVersion` your build doesn't read, too. Either
|
|
1348
|
+
way a failed hydrate is safe to fall back from: a malformed or unreadable
|
|
1349
|
+
snapshot is rejected **whole**, with nothing written until every block has
|
|
1350
|
+
validated.
|
|
1351
|
+
|
|
1352
|
+
> This envelope is a brookmd **package** format, *not* the parser wire contract
|
|
1353
|
+
> ([`WIRE.md`](https://github.com/siinghd/brookmd/blob/main/crates/brookmd-core/WIRE.md)).
|
|
1354
|
+
> The two are versioned independently.
|
|
1355
|
+
|
|
1356
|
+
### Resuming a thread that was still streaming
|
|
1357
|
+
|
|
1358
|
+
A `done: false` snapshot was captured mid-stream. Pass the original markdown as
|
|
1359
|
+
`source` and the thread simply continues:
|
|
1360
|
+
|
|
1361
|
+
```ts
|
|
1362
|
+
client.hydrate(snapshot, { source: markdownSoFar });
|
|
1363
|
+
client.append(nextDelta); // or client.pipeFrom(await fetch("/api/chat"))
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
The parser's internal state cannot be serialized (it is an `Rc` graph, not
|
|
1367
|
+
data), so continuing a document genuinely requires re-parsing what came before
|
|
1368
|
+
it — but that cost moves **off the critical path**. On the first `append`
|
|
1369
|
+
brookmd spins up the worker, re-feeds the source in the background, and keeps
|
|
1370
|
+
the hydrated blocks on screen throughout: the reader scrolls and reads while the
|
|
1371
|
+
parse catches up. New chunks need no buffering — they queue behind the re-feed
|
|
1372
|
+
and land the moment it completes (they arrive at model token speed; the parser
|
|
1373
|
+
catches up far faster). When the re-parse lands, unchanged blocks are adopted
|
|
1374
|
+
**by reference and by id**, so nothing re-renders and nothing remounts at the
|
|
1375
|
+
swap.
|
|
1376
|
+
|
|
1377
|
+
A `done: true` snapshot is terminal: it needs no `source`, no worker is ever
|
|
1378
|
+
created for it, and `append()` throws — a finished thread cannot be extended
|
|
1379
|
+
(`reset()` to start a new one). A `done: false` snapshot hydrated *without*
|
|
1380
|
+
`source` is view-only: it paints, but appending throws, because a document
|
|
1381
|
+
cannot be continued correctly without the text preceding it.
|
|
1382
|
+
|
|
1383
|
+
Nothing on the hydrate path touches a worker: `whenReady()` resolves immediately
|
|
1384
|
+
and `client.ready` is `true`, so a readiness gate never sits over a thread that
|
|
1385
|
+
is already on screen.
|
|
1386
|
+
|
|
1287
1387
|
## Security
|
|
1288
1388
|
|
|
1289
1389
|
brookmd is XSS-safe by default — its HTML output is meant to be injected via
|
package/dist/client.d.ts
CHANGED
|
@@ -26,6 +26,50 @@ export interface OutlineEntry {
|
|
|
26
26
|
/** Stable block id — usable as a scroll target / React key. */
|
|
27
27
|
id: number;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A self-contained, JSON-serializable capture of everything a stream currently
|
|
31
|
+
* renders — produced by {@link BrookClient.getPersistable}, restored by
|
|
32
|
+
* {@link BrookClient.hydrate}.
|
|
33
|
+
*
|
|
34
|
+
* Reopening a long thread normally re-feeds its whole source through the parser
|
|
35
|
+
* before the first paint (O(source), and it blocks the reopen). Persist this
|
|
36
|
+
* next to the thread instead and restoring it is pure JSON handling: the blocks
|
|
37
|
+
* go straight into the store and both renderers mount them as ordinary
|
|
38
|
+
* committed blocks — **no worker, no WASM, no parse**.
|
|
39
|
+
*
|
|
40
|
+
* NOT the wire contract. The parser↔renderer wire (WIRE.md) is a separate,
|
|
41
|
+
* separately-versioned boundary; this envelope is a brookmd *package* format
|
|
42
|
+
* that only {@link BrookClient.hydrate} consumes.
|
|
43
|
+
*/
|
|
44
|
+
export interface PersistableSnapshot {
|
|
45
|
+
/** Envelope version. {@link BrookClient.hydrate} refuses anything it does not
|
|
46
|
+
* know how to read, so a stored snapshot never half-restores. */
|
|
47
|
+
hydrateVersion: number;
|
|
48
|
+
/**
|
|
49
|
+
* The blocks exactly as {@link BrookClient.getSnapshot} showed them at
|
|
50
|
+
* capture: the committed history plus whatever tail was on screen. Plain
|
|
51
|
+
* JSON — a `Block` carries no hidden state (the renderers' splice bookkeeping
|
|
52
|
+
* lives in a WeakMap, never on the block), so `JSON.stringify` round-trips it.
|
|
53
|
+
*/
|
|
54
|
+
blocks: Block[];
|
|
55
|
+
/** UTF-16 length of the markdown that produced `blocks`. */
|
|
56
|
+
sourceLength: number;
|
|
57
|
+
/** {@link sourceFingerprint} of that markdown. A staleness check — "is the
|
|
58
|
+
* source I stored still the one these blocks came from?" — and deliberately
|
|
59
|
+
* nothing more: not a checksum, not security. */
|
|
60
|
+
sourceHash: string;
|
|
61
|
+
/** Whether the captured stream had been finalized. A `done` snapshot is a
|
|
62
|
+
* terminal document: it needs no source and can never be resumed. */
|
|
63
|
+
done: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 32-bit FNV-1a over a string's UTF-16 code units, as 8 hex digits. Backs
|
|
67
|
+
* {@link PersistableSnapshot.sourceHash}: cheap enough to run over a megabyte
|
|
68
|
+
* of markdown, sharp enough to notice that a stored source changed. Exported so
|
|
69
|
+
* a caller can make the same staleness decision brookmd does —
|
|
70
|
+
* `snap.sourceHash === sourceFingerprint(mySource)`. Not a security primitive.
|
|
71
|
+
*/
|
|
72
|
+
export declare function sourceFingerprint(source: string): string;
|
|
29
73
|
export declare function applyPatch(store: BlockStore, patch: Patch): void;
|
|
30
74
|
interface PoolWorker {
|
|
31
75
|
worker: WorkerLike;
|
|
@@ -171,6 +215,7 @@ export declare class BrookClient {
|
|
|
171
215
|
private recoveryBuffer;
|
|
172
216
|
private recoveredLen;
|
|
173
217
|
private recoveryAttempted;
|
|
218
|
+
private appendedAny;
|
|
174
219
|
private coalesce;
|
|
175
220
|
private rafHandle;
|
|
176
221
|
private finalizePending;
|
|
@@ -182,6 +227,12 @@ export declare class BrookClient {
|
|
|
182
227
|
/** Set by mergeStale when it had to compact a hole out of the view, so
|
|
183
228
|
* getSnapshot skips caching a view whose indices no longer track `base`. */
|
|
184
229
|
private mergeDropped;
|
|
230
|
+
private hydrated;
|
|
231
|
+
private hydratedDone;
|
|
232
|
+
private hydratedLength;
|
|
233
|
+
private hydratedHash;
|
|
234
|
+
private hydratedSource;
|
|
235
|
+
private resumed;
|
|
185
236
|
private appendedBytes;
|
|
186
237
|
private patchCount;
|
|
187
238
|
private totalParseMicros;
|
|
@@ -261,6 +312,10 @@ export declare class BrookClient {
|
|
|
261
312
|
*/
|
|
262
313
|
private settleRebind;
|
|
263
314
|
get ready(): boolean;
|
|
315
|
+
/** True while a hydrated document has never been handed to a parser: the
|
|
316
|
+
* store holds restored blocks and no worker exists. Cleared once the resume
|
|
317
|
+
* re-feed is issued (or the caller re-drives the whole document itself). */
|
|
318
|
+
private get hydrationPending();
|
|
264
319
|
/**
|
|
265
320
|
* The fatal error that killed this client's worker, or `null` if healthy.
|
|
266
321
|
*
|
|
@@ -329,6 +384,90 @@ export declare class BrookClient {
|
|
|
329
384
|
setContent(content: string, opts?: {
|
|
330
385
|
done?: boolean;
|
|
331
386
|
}): void;
|
|
387
|
+
/**
|
|
388
|
+
* Capture everything this client currently renders as a plain-JSON
|
|
389
|
+
* {@link PersistableSnapshot}: `JSON.stringify` it, store it beside the
|
|
390
|
+
* thread, and {@link hydrate} it back later to repaint with **no parse at
|
|
391
|
+
* all**. This is the persistence half of instant thread reopen.
|
|
392
|
+
*
|
|
393
|
+
* The committed wire is already a complete serialization — a committed block
|
|
394
|
+
* is emitted exactly once and is final (WIRE.md §2) — so there is nothing to
|
|
395
|
+
* re-derive: the snapshot IS the document. Cost here is one pass to
|
|
396
|
+
* fingerprint the source; cost on the way back in is a `Map` fill.
|
|
397
|
+
*
|
|
398
|
+
* @param source the markdown driven into this stream, used ONLY to compute
|
|
399
|
+
* {@link PersistableSnapshot.sourceHash}. Optional, because a client with
|
|
400
|
+
* `recovery` on (the default) already retains it, as does a
|
|
401
|
+
* `setContent`-driven one; required in the single configuration that holds
|
|
402
|
+
* neither — `recovery: false` plus manual `append()` — where omitting it
|
|
403
|
+
* throws rather than persisting a snapshot no one can check for staleness.
|
|
404
|
+
*/
|
|
405
|
+
getPersistable(source?: string): PersistableSnapshot;
|
|
406
|
+
/** The full driven document when this client happens to hold it: the recovery
|
|
407
|
+
* buffer is exactly that whenever `recovery` is on (the default), and
|
|
408
|
+
* setContent's baseline covers the recovery-off controlled-string mode.
|
|
409
|
+
* `null` means genuinely unknown — reachable only with recovery off AND
|
|
410
|
+
* manual appends. */
|
|
411
|
+
private retainedSource;
|
|
412
|
+
/**
|
|
413
|
+
* Restore a {@link PersistableSnapshot} into an untouched client. The blocks
|
|
414
|
+
* land in the store as ordinary committed blocks and `getSnapshot()` returns
|
|
415
|
+
* them immediately, so the first paint already has the whole document —
|
|
416
|
+
* **no worker is created, no WASM loads, nothing is parsed**. Reopening a
|
|
417
|
+
* thread costs O(blocks) of JSON handling instead of O(source) of parsing.
|
|
418
|
+
*
|
|
419
|
+
* Call it on a fresh client before anything is appended and, ideally, before
|
|
420
|
+
* the renderer mounts (hydrating an already-mounted client works — it
|
|
421
|
+
* notifies subscribers — but costs an extra render). Hydrating a client that
|
|
422
|
+
* already holds content throws.
|
|
423
|
+
*
|
|
424
|
+
* **Resuming a live thread.** A snapshot with `done: false` was still
|
|
425
|
+
* streaming. Pass `source` — the markdown behind the snapshot — and the first
|
|
426
|
+
* {@link append} rebuilds parser state in the background: the parser's
|
|
427
|
+
* internals are an `Rc` graph with no serialized form, so continuing a
|
|
428
|
+
* document genuinely requires re-parsing what came before it, but that cost
|
|
429
|
+
* moves OFF the critical path. The hydrated blocks stay on screen and the
|
|
430
|
+
* reader scrolls them while the worker catches up; new chunks queue behind the
|
|
431
|
+
* re-feed and land the moment it does. Without `source` the thread is
|
|
432
|
+
* view-only and appending throws — continuing a document correctly is not
|
|
433
|
+
* possible without the text that precedes it.
|
|
434
|
+
*
|
|
435
|
+
* **Hydration does not verify the blocks against the source.** The snapshot is
|
|
436
|
+
* trusted as produced; `sourceHash` exists so the CALLER can notice its stored
|
|
437
|
+
* source moved on and re-stream instead of painting stale HTML. The resume
|
|
438
|
+
* path re-checks it, lets the fresh parse win, and warns in dev.
|
|
439
|
+
*
|
|
440
|
+
* @throws if the envelope version is unknown, the snapshot is malformed, or
|
|
441
|
+
* this client already holds content. Validation completes before anything is
|
|
442
|
+
* written, so a rejected snapshot leaves the client exactly as it was.
|
|
443
|
+
*/
|
|
444
|
+
hydrate(snapshot: PersistableSnapshot, opts?: {
|
|
445
|
+
source?: string;
|
|
446
|
+
}): void;
|
|
447
|
+
/**
|
|
448
|
+
* The first content-bearing op on a hydrated thread: give the parser back the
|
|
449
|
+
* state it could not be handed.
|
|
450
|
+
*
|
|
451
|
+
* There is no way around re-parsing — the core keeps `Rc` graphs with no
|
|
452
|
+
* `Deserialize` — but every part of that cost sits off the critical path. The
|
|
453
|
+
* hydrated blocks are already painted and STAY painted: `softReset` preserves
|
|
454
|
+
* them exactly as the setContent divergence swap does, so the reader keeps
|
|
455
|
+
* reading and scrolling while the worker chews through the history on its own
|
|
456
|
+
* thread. The caller's new chunks need no buffer of ours — `postMessage` is
|
|
457
|
+
* FIFO per worker, so they queue behind the re-feed and are parsed the instant
|
|
458
|
+
* it catches up. When the re-parse's patch lands, `mergeStale` adopts every
|
|
459
|
+
* unchanged block BY REFERENCE (same object, same id), so the swap re-renders
|
|
460
|
+
* and remounts nothing, and the live tail streams on from there.
|
|
461
|
+
*/
|
|
462
|
+
private beginResume;
|
|
463
|
+
/** The resume re-feed is no longer wanted: either it has just been issued, or
|
|
464
|
+
* the caller took over by driving the whole document itself. Releases the
|
|
465
|
+
* retained source so one document, not two, stays in memory. */
|
|
466
|
+
private retireHydration;
|
|
467
|
+
/** Drop every trace of a hydrate. An explicit reset() is a brand-new
|
|
468
|
+
* document, so a restored view must not keep intercepting appends or re-feed
|
|
469
|
+
* a source that is no longer the one being streamed. */
|
|
470
|
+
private clearHydration;
|
|
332
471
|
reset(): void;
|
|
333
472
|
/**
|
|
334
473
|
* setContent's divergence reset: rebuild the parser exactly like {@link reset},
|