flux-md 0.16.2 → 0.17.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.
Files changed (66) hide show
  1. package/README.md +70 -39
  2. package/dist/block-props.d.ts +18 -0
  3. package/dist/block-props.js +75 -0
  4. package/dist/client.d.ts +310 -0
  5. package/{src/client.ts → dist/client.js} +123 -319
  6. package/dist/dom.d.ts +110 -0
  7. package/dist/dom.js +576 -0
  8. package/dist/element.d.ts +20 -0
  9. package/dist/element.js +287 -0
  10. package/dist/hi.d.ts +12 -0
  11. package/{src/hi.ts → dist/hi.js} +42 -74
  12. package/dist/html-to-react.d.ts +40 -0
  13. package/dist/html-to-react.js +344 -0
  14. package/{src/index.ts → dist/index.d.ts} +5 -25
  15. package/dist/index.js +16 -0
  16. package/dist/morph.d.ts +28 -0
  17. package/dist/morph.js +166 -0
  18. package/dist/react.d.ts +204 -0
  19. package/dist/react.js +490 -0
  20. package/dist/renderers/CodeBlock.d.ts +7 -0
  21. package/dist/renderers/CodeBlock.js +75 -0
  22. package/dist/renderers/Math.d.ts +14 -0
  23. package/dist/renderers/Math.js +15 -0
  24. package/dist/renderers/Mermaid.d.ts +13 -0
  25. package/dist/renderers/Mermaid.js +15 -0
  26. package/dist/server.d.ts +61 -0
  27. package/dist/server.js +126 -0
  28. package/{src/solid.tsx → dist/solid.d.ts} +19 -95
  29. package/dist/solid.js +54 -0
  30. package/dist/svelte.d.ts +80 -0
  31. package/dist/svelte.js +59 -0
  32. package/dist/types-core.d.ts +377 -0
  33. package/dist/types-core.js +0 -0
  34. package/{src/types-react.ts → dist/types-react.d.ts} +0 -1
  35. package/dist/types-react.js +0 -0
  36. package/dist/types.d.ts +2 -0
  37. package/dist/types.js +2 -0
  38. package/dist/vue.d.ts +94 -0
  39. package/dist/vue.js +79 -0
  40. package/{src → dist}/wasm/flux_md_core.d.ts +18 -6
  41. package/{src → dist}/wasm/flux_md_core.js +50 -55
  42. package/dist/wasm/flux_md_core_bg.wasm +0 -0
  43. package/{src → dist}/wasm/flux_md_core_bg.wasm.d.ts +1 -0
  44. package/dist/worker-core.d.ts +60 -0
  45. package/dist/worker-core.js +121 -0
  46. package/dist/worker.d.ts +1 -0
  47. package/dist/worker.js +48 -0
  48. package/package.json +22 -18
  49. package/src/block-props.ts +0 -141
  50. package/src/dom.ts +0 -946
  51. package/src/element.ts +0 -400
  52. package/src/html-to-react.ts +0 -455
  53. package/src/morph.ts +0 -253
  54. package/src/react.tsx +0 -1020
  55. package/src/renderers/CodeBlock.tsx +0 -116
  56. package/src/renderers/Math.tsx +0 -28
  57. package/src/renderers/Mermaid.tsx +0 -27
  58. package/src/server.tsx +0 -221
  59. package/src/svelte.ts +0 -179
  60. package/src/types-core.ts +0 -398
  61. package/src/types.ts +0 -7
  62. package/src/vue.ts +0 -184
  63. package/src/wasm/flux_md_core_bg.wasm +0 -0
  64. package/src/worker-core.ts +0 -174
  65. package/src/worker.ts +0 -72
  66. /package/{src → dist}/styles.css +0 -0
package/README.md CHANGED
@@ -12,12 +12,14 @@ Parsing runs entirely **off the main thread** — each stream gets its own poole
12
12
  bun add flux-md # or: npm i flux-md / pnpm add flux-md
13
13
  ```
14
14
 
15
- flux-md ships as **source** (TypeScript + the compiled WASM). The worker and
16
- WASM asset are referenced with the **web-standard `new URL(asset,
15
+ flux-md ships **compiled, non-minified ESM** (`dist/*.js` + `.d.ts` types) plus
16
+ the compiled WASM no raw `.ts`/`.tsx` source. The worker and WASM asset are
17
+ referenced with the **web-standard `new URL(asset,
17
18
  import.meta.url)`** pattern, so any bundler with asset-module support resolves
18
19
  them: **Vite** (the reference setup), **webpack 5**, **Rollup** (with asset
19
20
  modules), **Parcel**, and **Next.js** (App Router — Turbopack *and* webpack;
20
- **verified on Next.js 16**, see the [Next.js callout](#nextjs) below). It is
21
+ **verified on Next.js 16**, see the [Next.js callout](#nextjs) below).
22
+
21
23
  The streaming client (`<FluxMarkdown>` / `FluxClient`) is **browser-only** (it
22
24
  constructs Web Workers). For **server-side / static rendering of finished
23
25
  content** — SSR, React Server Components, build steps — use the worker-free,
@@ -999,42 +1001,71 @@ genuinely hostile content where CSS-overlay/clickjacking matters, render inside
999
1001
  a sandboxed `<iframe>` instead — sanitization stops injection, not every
1000
1002
  visual-overlay trick.
1001
1003
 
1002
- ### Supply-chain transparency
1003
-
1004
- flux-md is **zero runtime dependency** — no third-party packages are pulled in
1005
- at runtime. The parsing core is Rust compiled to WebAssembly, reproducibly
1006
- buildable from `crates/flux-md-core/` via `bun run build:wasm`.
1007
-
1008
- **Native code (WebAssembly).** The shipped `flux_md_core_bg.wasm` (~200 KB) is
1009
- first-party, built from the Rust source in this repo, and runs inside a sandboxed
1010
- Web Worker (browser) or Node worker thread. Supply-chain scanners such as
1011
- [Socket.dev](https://socket.dev) will flag it as `nativeCode` — this is accurate
1012
- and expected. The WASM is not a vendored third-party binary; it is reproducible
1013
- from source.
1014
-
1015
- **Network access.** flux-md performs network I/O in exactly two scenarios, both
1016
- caller-driven:
1017
-
1018
- - `<flux-markdown src="URL">` — the Web Component fetches the URL you supply and
1019
- streams the response. No URL is ever chosen by flux-md itself.
1020
- - The wasm-bindgen glue (`wasm/flux_md_core.js`) loads the co-located `.wasm`
1021
- asset via `fetch(new URL("…_bg.wasm", import.meta.url))`bundlers resolve
1022
- this to a local build artifact, not a remote endpoint.
1023
-
1024
- flux-md has no telemetry, no analytics, and no first-party remote endpoints.
1025
- Socket will flag the `networkAccess` signal — it is accurate and expected. In
1026
- privileged contexts (browser extensions, Electron, environments where the
1027
- same-origin policy may not apply), treat the `src` attribute value as you would
1028
- any external URL and allowlist it in your CSP / security policy.
1029
-
1030
- **Filesystem access (Node/SSR only).** `flux-md/server` reads the package's
1031
- own `.wasm` file off disk on Node.js (Node's `fetch` cannot load `file://`
1032
- URLs). This is a Node-only path; it reads only the package-internal asset and
1033
- never touches caller-supplied paths. Socket will flag `filesystemAccess` — also
1034
- accurate and expected.
1035
-
1036
- The `socket.yml` at the repository root documents these signals with their
1037
- justifications for Socket's GitHub app.
1004
+ ### Supply chain & security posture
1005
+
1006
+ flux-md ships **zero runtime dependencies** — `dependencies` and
1007
+ `optionalDependencies` in `package.json` are both empty. The parsing core is Rust
1008
+ compiled to WebAssembly, reproducibly buildable from `crates/flux-md-core/` via
1009
+ `bun run build:wasm`. The package publishes **compiled, non-minified ESM**
1010
+ (`dist/*.js` + `.d.ts`); it does not ship raw `.ts`/`.tsx` source.
1011
+
1012
+ **Frameworks are optional peers, by design.** `react`, `vue`, `svelte`, and
1013
+ `solid-js` are declared as `peerDependencies` with
1014
+ `peerDependenciesMeta.optional: true`. You install only the one you use or none
1015
+ (the `flux-md/dom` and `flux-md/element` entries need no framework at all). This
1016
+ is the most important supply-chain property of the package: **a React-only
1017
+ consumer never installs `vue` or `solid-js`, so those frameworks' transitive
1018
+ internals never enter that consumer's lockfile.** `npm i flux-md` on its own pulls
1019
+ in nothing else.
1020
+
1021
+ **Why a registry scan may flag `seroval` and `@vue/compiler-*`.** When a scanner
1022
+ resolves *all* declared peers, it surfaces alerts on framework internals reachable
1023
+ only through the optional peers**none of which is flux-md code, and none of
1024
+ which is installed unless you opt into that framework:**
1025
+
1026
+ - `seroval` (transitive of **solid-js**) its `deserialize()` uses
1027
+ `(0, eval)(source)` and touches the network. This is Solid's SSR serialization
1028
+ layer; it is also the package some scanners label a "potential vulnerability".
1029
+ - `@vue/compiler-core` (transitive of **vue**) uses the `Function` constructor
1030
+ for template codegen.
1031
+ - `@vue/compiler-sfc` (transitive of **vue**) — references `globalThis["fetch"]`.
1032
+ - minified esm-bundler builds of those compilers read as "obfuscated code".
1033
+
1034
+ flux-md's own source contains **no `eval` and no `Function(...)` constructor**
1035
+ (`grep -rnE '\beval\s*\(|\bnew Function\b|\bFunction\s*\(' packages/flux-md/src`
1036
+ returns nothing). The
1037
+ repository's [`socket.yml`](https://github.com/siinghd/flux-md/blob/main/socket.yml)
1038
+ documents this and disables those upstream-framework alert types for flux-md's own
1039
+ CI (which installs every framework as a devDependency for cross-framework tests).
1040
+ If you prefer surgical handling, ignore the specific transitive packages instead
1041
+ (e.g. `@SocketSecurity ignore seroval@<version>`).
1042
+
1043
+ **flux-md is browser-oriented (Web Worker + WASM).** The default path runs the
1044
+ WASM parser inside a Web Worker — ideal for browsers and modern Node
1045
+ (`worker_threads`), but **not** intended for non-browser or older environments
1046
+ that lack Workers/WASM. If you need a worker-free, synchronous path (Node SSR /
1047
+ React Server Components), use **`flux-md/server`** — it loads the same WASM
1048
+ synchronously off disk and renders to a string without spawning a worker.
1049
+
1050
+ **First-party signals a scanner will (correctly) show.** These describe flux-md
1051
+ itself and are kept *visible* rather than silenced:
1052
+
1053
+ - **Native code (`hasNativeCode`).** The first-party `dist/wasm/flux_md_core_bg.wasm`
1054
+ (~180 KB) is built from the Rust source in this repo and runs inside a sandboxed
1055
+ Web Worker (browser) or Node worker thread. It is reproducible from source, not a
1056
+ vendored third-party binary.
1057
+ - **Network access (`networkAccess`).** Only `<flux-markdown src="URL">` (the URL
1058
+ *you* supply) and the wasm-bindgen glue loading the co-located `.wasm` via
1059
+ `fetch(new URL("…_bg.wasm", import.meta.url))` — which bundlers resolve to a
1060
+ local build artifact. No telemetry, no analytics, no first-party remote
1061
+ endpoints. In privileged contexts (browser extensions, Electron) treat the `src`
1062
+ value as any external URL and allowlist it in your CSP.
1063
+ - **Filesystem access (`filesystemAccess`).** Node/SSR only: `flux-md/server` reads
1064
+ the package's own `.wasm` off disk (Node's `fetch` cannot load `file://` URLs).
1065
+ It reads only the package-internal asset, never a caller-supplied path.
1066
+
1067
+ The `socket.yml` at the repository root documents every signal with its
1068
+ justification for Socket's GitHub app.
1038
1069
 
1039
1070
  ## Scaling
1040
1071
 
@@ -0,0 +1,18 @@
1
+ import type { Block, BlockComponentProps } from "./types-core.js";
2
+ /** Info-string language from a code block's `data-lang="…"`. */
3
+ export declare function extractLang(html: string): string;
4
+ /**
5
+ * Convert sanitized HTML attribute pairs into a spreadable object, keeping the
6
+ * HTML-form names (`class`, `for`) verbatim. This is the deliberate divergence
7
+ * from the JSX renderer (which renames to `className`/`htmlFor` for a prop
8
+ * spread): the DOM renderer applies them via `el.setAttribute(name, value)`,
9
+ * which wants the literal HTML names.
10
+ */
11
+ export declare function htmlAttrs(pairs: [string, string][]): Record<string, string>;
12
+ /**
13
+ * Build the props a block-kind / component-tag override receives — the same
14
+ * shape the JSX renderer's block-kind props carry, with ONE deliberate
15
+ * divergence: for `Component` blocks `attrs` stay in HTML form (`class`/`for`)
16
+ * because DOM overrides apply them via `setAttribute` (see {@link htmlAttrs}).
17
+ */
18
+ export declare function blockProps(block: Block): BlockComponentProps;
@@ -0,0 +1,75 @@
1
+ function decodeEntities(s) {
2
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
3
+ }
4
+ function decodeCodeText(html) {
5
+ const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
6
+ return m ? decodeEntities(m[1]) : "";
7
+ }
8
+ function decodeMathText(html) {
9
+ const d = html.match(/<div class="math math-display">([\s\S]*?)<\/div>/);
10
+ if (d) return decodeEntities(d[1]);
11
+ return decodeCodeText(html);
12
+ }
13
+ function extractLang(html) {
14
+ const m = html.match(/data-lang="([^"]+)"/);
15
+ return m ? m[1] : "";
16
+ }
17
+ function componentInnerHtml(html, tag) {
18
+ const gt = html.indexOf(">");
19
+ if (gt < 0) return "";
20
+ let inner = html.slice(gt + 1);
21
+ const close = `</${tag}>`;
22
+ if (inner.endsWith(close)) inner = inner.slice(0, -close.length);
23
+ return inner.replace(/^\n/, "").replace(/\n$/, "");
24
+ }
25
+ function htmlAttrs(pairs) {
26
+ const out = {};
27
+ for (const [k, v] of pairs) out[k] = v;
28
+ return out;
29
+ }
30
+ function blockProps(block) {
31
+ const props = {
32
+ block,
33
+ html: block.html,
34
+ open: block.open,
35
+ speculative: block.speculative
36
+ };
37
+ const data = block.kind.data;
38
+ if (block.kind.type === "CodeBlock") {
39
+ props.text = data?.code ?? decodeCodeText(block.html);
40
+ props.language = data?.lang ?? "";
41
+ if (typeof data?.code === "string") {
42
+ props.code = { lang: data.lang ?? null, code: data.code };
43
+ }
44
+ } else if (block.kind.type === "MathBlock") {
45
+ props.text = data?.latex ?? decodeMathText(block.html);
46
+ if (typeof data?.latex === "string") {
47
+ props.math = { latex: data.latex };
48
+ }
49
+ } else if (block.kind.type === "List") {
50
+ if (data && typeof data.start === "number") {
51
+ props.list = { ordered: !!data.ordered, start: data.start, items: data.items };
52
+ }
53
+ } else if (block.kind.type === "Component") {
54
+ props.tag = data?.tag ?? "";
55
+ props.attrs = htmlAttrs(data?.attrs ?? []);
56
+ props.html = componentInnerHtml(block.html, props.tag);
57
+ } else if (block.kind.type === "Table") {
58
+ props.table = block.kind.data;
59
+ } else if (block.kind.type === "Heading") {
60
+ if (typeof block.kind.data === "object" && block.kind.data !== null) {
61
+ props.heading = block.kind.data;
62
+ }
63
+ } else if (block.kind.type === "Blockquote" || block.kind.type === "Alert") {
64
+ const cd = block.kind.data;
65
+ if (cd && Array.isArray(cd.nested)) {
66
+ props.container = { nested: cd.nested };
67
+ }
68
+ }
69
+ return props;
70
+ }
71
+ export {
72
+ blockProps,
73
+ extractLang,
74
+ htmlAttrs
75
+ };
@@ -0,0 +1,310 @@
1
+ import type { Block, FromWorker, ParserConfig, Patch, ToWorker, WorkerLike } from "./types-core.js";
2
+ /**
3
+ * The ordered-block store backing a stream, extracted as a pure function so
4
+ * its reference-stability contract is testable without a Worker.
5
+ *
6
+ * **The contract that prevents extra React re-renders:** a block, once
7
+ * committed, is never re-sent by the parser, so `applyPatch` never replaces it
8
+ * in the map. Its object reference stays identical across every later patch —
9
+ * which is exactly what `blocksEqual` (the BlockView memo) checks, so committed
10
+ * blocks never re-render (and never re-parse) as the stream grows. Only the
11
+ * `active` tail gets fresh references each patch, and only it re-renders.
12
+ */
13
+ export interface BlockStore {
14
+ committed: Map<number, Block>;
15
+ committedOrder: number[];
16
+ active: Block[];
17
+ snapshot: Block[];
18
+ }
19
+ export declare function emptyBlockStore(): BlockStore;
20
+ /** A heading entry for building a table of contents — see {@link FluxClient.outline}. */
21
+ export interface OutlineEntry {
22
+ /** Heading level 1–6. */
23
+ level: number;
24
+ /** Plain-text heading content (tags stripped, entities decoded). */
25
+ text: string;
26
+ /** Stable block id — usable as a scroll target / React key. */
27
+ id: number;
28
+ }
29
+ export declare function applyPatch(store: BlockStore, patch: Patch): void;
30
+ interface PoolWorker {
31
+ worker: WorkerLike;
32
+ ready: boolean;
33
+ /** Set once WASM init fails; whenWorkerReady rejects with this thereafter. */
34
+ failed: Error | null;
35
+ streamCount: number;
36
+ /** Live stream ids on this worker — so a fatal failure can notify each one. */
37
+ streamIds: Set<number>;
38
+ readyWaiters: Array<{
39
+ resolve: () => void;
40
+ reject: (e: Error) => void;
41
+ }>;
42
+ }
43
+ /**
44
+ * A pool of Web Workers, each multiplexing many `FluxParser`s keyed by stream
45
+ * id. This is what lets flux-md scale past `hardwareConcurrency` concurrent
46
+ * streams without oversubscribing OS threads: 50 streams share (at most) the
47
+ * cap's worth of workers instead of spawning 50.
48
+ *
49
+ * Worker creation is **lazy and load-aware**: while under the cap, each new
50
+ * stream gets its own worker (so 1 stream = 1 worker, identical to the old
51
+ * behavior); once at the cap, new streams attach to the least-loaded worker.
52
+ *
53
+ * The constructor injects a `WorkerLike` factory so the routing and lifecycle
54
+ * logic is unit-testable with a fake worker — no real Worker or WASM needed.
55
+ */
56
+ export declare class FluxPool {
57
+ private factory;
58
+ private cap;
59
+ private workers;
60
+ private handlers;
61
+ private nextStreamId;
62
+ constructor(factory: () => WorkerLike, cap: number);
63
+ /** Reserve a stream id and assign a worker, registering its message handler. */
64
+ acquire(handler: (msg: FromWorker) => void): {
65
+ streamId: number;
66
+ pw: PoolWorker;
67
+ };
68
+ /** Free a stream's parser in its worker; keep the worker warm for siblings. */
69
+ release(streamId: number, pw: PoolWorker): void;
70
+ /** Inverse of {@link release}: re-register a stream's handler so it receives
71
+ * patches again. For React StrictMode's dev double-mount, which destroys a
72
+ * client on the simulated unmount and remounts the SAME instance. The worker
73
+ * lazily recreates the disposed parser on the next append. */
74
+ reattach(streamId: number, pw: PoolWorker, handler: (msg: FromWorker) => void): void;
75
+ send(pw: PoolWorker, msg: ToWorker): void;
76
+ /** Resolves when the given worker has finished WASM init; rejects if it failed. */
77
+ whenWorkerReady(pw: PoolWorker): Promise<void>;
78
+ /**
79
+ * Eagerly spin up one worker so WASM init starts BEFORE the first stream —
80
+ * taking the one-time init off the first-token critical path (e.g. call
81
+ * `getDefaultPool().warm()` on app load / route entry). Reuses a live worker
82
+ * if one exists; the warm worker is the one the first stream attaches to (it
83
+ * has spare capacity), so the work is not wasted. Resolves when that worker has
84
+ * finished initializing WASM; rejects if init fails fatally. Browser-only (it
85
+ * constructs a `Worker`).
86
+ */
87
+ warm(): Promise<void>;
88
+ /** Terminate every worker (test teardown / full shutdown). */
89
+ disposeAll(): void;
90
+ get workerCount(): number;
91
+ private pick;
92
+ private create;
93
+ private onMessage;
94
+ private dispatch;
95
+ }
96
+ /** The process-wide default pool every `FluxClient` shares unless given one. */
97
+ export declare function getDefaultPool(): FluxPool;
98
+ /** TEST-ONLY: drop the process-wide default pool so the next {@link getDefaultPool}
99
+ * rebuilds it (lazily, with the current global `Worker`). Lets a test file that
100
+ * drives the default pool start from a clean, deterministic state regardless of
101
+ * which other file warmed it first in bun's shared test process. Not part of the
102
+ * public API and a no-op for normal runtime use. */
103
+ export declare function __resetDefaultPool(): void;
104
+ /**
105
+ * Subscriber-driven store backing a single streaming parser. Each client owns
106
+ * one stream within a shared {@link FluxPool}; many clients multiplex over a
107
+ * small set of workers (see the pool for the scaling story).
108
+ *
109
+ * The store exposes:
110
+ * - subscribe(listener): for React's useSyncExternalStore
111
+ * - getSnapshot(): the current ordered list of blocks
112
+ * - getMetrics(): per-stream perf metrics
113
+ *
114
+ * Mutation methods:
115
+ * - append(chunk): forward to the worker
116
+ * - finalize(): mark the stream done
117
+ * - reset(): start fresh
118
+ */
119
+ export declare class FluxClient {
120
+ private pool;
121
+ private pw;
122
+ private streamId;
123
+ private config?;
124
+ private configSent;
125
+ private listeners;
126
+ private store;
127
+ private onError?;
128
+ private onBlock?;
129
+ private attached;
130
+ private lastContent;
131
+ private contentDone;
132
+ private coalesce;
133
+ private rafHandle;
134
+ private finalizePending;
135
+ private appendedBytes;
136
+ private patchCount;
137
+ private totalParseMicros;
138
+ private lastPatchMs;
139
+ private firstAppendMs;
140
+ private retainedBytes;
141
+ private wasmMemoryBytes;
142
+ private renderCount;
143
+ private rebuildCount;
144
+ /**
145
+ * @param options.pool worker pool to join (defaults to the shared
146
+ * process-wide pool — pass a dedicated `FluxPool` only for isolation).
147
+ * @param options.config per-stream parser flags (see {@link ParserConfig});
148
+ * omitted fields use library defaults. Applied once, immutable thereafter.
149
+ * @param options.onError invoked on a worker/parse error or a fatal WASM-init
150
+ * failure (`fatal: true`). Without it, errors are only `console.error`d and
151
+ * a load failure surfaces solely as a rejected {@link FluxClient.whenReady}.
152
+ * @param options.onBlock invoked once per block as it commits (in document
153
+ * order, after the store updates) — for side effects like lazily
154
+ * highlighting a finished code block or analytics. A committed block never
155
+ * re-fires; the streaming tail does not (subscribe for live tail updates).
156
+ * @param options.coalesce opt-in (default `false`): collapse multiple
157
+ * intra-frame patch notifications into ONE `requestAnimationFrame`-scheduled
158
+ * flush to subscribers, so a React `useSyncExternalStore` consumer renders at
159
+ * most once per frame instead of once per patch. Lossless — committed blocks
160
+ * are reference-stable, so only superseded tail-only renders are skipped. The
161
+ * stream-completion (finalize) patch always flushes synchronously, and a
162
+ * pending frame is cancelled on `reset()`/`destroy()`. No effect when
163
+ * `requestAnimationFrame` is unavailable (e.g. SSR) — emits stay synchronous.
164
+ */
165
+ constructor(options?: {
166
+ pool?: FluxPool;
167
+ config?: ParserConfig;
168
+ onError?: (err: {
169
+ message: string;
170
+ fatal?: boolean;
171
+ }) => void;
172
+ onBlock?: (block: Block) => void;
173
+ coalesce?: boolean;
174
+ });
175
+ /**
176
+ * Lazily reserve this client's stream id and bind it to a pool worker. The
177
+ * SOLE place that calls pool.acquire() — so the worker is created on the FIRST
178
+ * worker-bound operation (append/finalize/reset/pipeFrom/whenReady), never at
179
+ * construct time. This is what makes `new FluxClient()` SSR-safe: nothing here
180
+ * runs during an SSR render (which only subscribes + reads the snapshot).
181
+ *
182
+ * Idempotent: once this.pw is set it returns it immediately and never
183
+ * re-acquires — this.pw is never nulled (destroy() deliberately keeps it so
184
+ * StrictMode's destroy()→reattach() on the SAME instance re-registers the same
185
+ * slot). Note: streamId/worker assignment now follows first-worker-bound-op
186
+ * order, not construction order — a client constructed first no longer
187
+ * necessarily owns the lowest streamId. This affects neither the pool cap nor
188
+ * multiplexing (pick() is unchanged and remains the only path to create()).
189
+ */
190
+ private ensureAcquired;
191
+ get ready(): boolean;
192
+ whenReady(): Promise<void>;
193
+ private firstConfig;
194
+ append(chunk: string): void;
195
+ finalize(): void;
196
+ /**
197
+ * Pipe a source straight in: read it to completion, `append()` each chunk,
198
+ * then `finalize()`. The LLM-native path — e.g.
199
+ * `await client.pipeFrom(await fetch("/api/chat"))`. Accepts:
200
+ * - a `Response` or its `ReadableStream<Uint8Array>` body (bytes; decoded
201
+ * with `TextDecoder({ stream: true })` so a multibyte sequence straddling
202
+ * a chunk boundary carries into the next read), or
203
+ * - an `AsyncIterable<string>` (e.g. an SSE delta generator) — string chunks
204
+ * appended verbatim.
205
+ *
206
+ * Pass `opts.signal` to supersede/cancel: the signal is checked on every
207
+ * iteration, so once aborted no further chunk is appended and **finalize is
208
+ * skipped** (a superseded stream must not finalize). For a byte source the
209
+ * reader is also `cancel()`'d to tear down the upstream. Resolves once
210
+ * finalized (or cleanly on abort); rejects if the source itself errors.
211
+ * Browser-only for byte sources (uses `TextDecoder`).
212
+ */
213
+ pipeFrom(source: ReadableStream<Uint8Array> | Response | AsyncIterable<string>, opts?: {
214
+ signal?: AbortSignal;
215
+ }): Promise<void>;
216
+ /**
217
+ * Drive the parser from a CONTROLLED full string instead of manual appends.
218
+ * Pass the whole document-so-far each time; setContent diffs it against the
219
+ * last value and does the minimal work:
220
+ * - **prefix-extension** (the streaming-growth case) → append only the new
221
+ * suffix, so committed blocks stay put and only the active tail re-parses;
222
+ * - **any other change** (e.g. a finished stream swapped for a re-processed
223
+ * final string) → `reset()` + reparse the whole new string.
224
+ *
225
+ * This is the first-class bridge for UIs that hold a streaming message as a
226
+ * single growing string prop (the common React shape) — no hand-rolled diff,
227
+ * no readiness gate (appends before WASM is ready are buffered). Pass
228
+ * `{ done: true }` once the content is final to `finalize()` (idempotent within
229
+ * a generation; a content change *after* done reopens the stream via a fresh
230
+ * reparse, since a finalized parser is terminal and can't be appended to).
231
+ * Drive a given client with `setContent` *or* manual `append()`/`finalize()`,
232
+ * not both — they share the internal diff baseline.
233
+ *
234
+ * v1 note: the non-prefix path is a full reparse, not a partial rewind —
235
+ * committed blocks are frozen, so there is no truncate-to-offset. For the
236
+ * common case (append-growth + one end-of-stream swap) that is optimal. A
237
+ * transform that rewrites *earlier* bytes on every update is an anti-pattern
238
+ * here (it forces a reparse each tick); do that enrichment at render time via
239
+ * `components` instead, keeping the source append-only.
240
+ */
241
+ setContent(content: string, opts?: {
242
+ done?: boolean;
243
+ }): void;
244
+ reset(): void;
245
+ destroy(): void;
246
+ /**
247
+ * Re-register with the pool after {@link destroy} so the client receives
248
+ * patches again. Needed only for React StrictMode's dev double-mount, where
249
+ * the renderer destroys on the simulated unmount then remounts the SAME
250
+ * client instance; apps don't normally call this. No-op if still attached.
251
+ */
252
+ reattach(): void;
253
+ subscribe: (fn: () => void) => () => boolean;
254
+ getSnapshot: () => Block[];
255
+ /**
256
+ * Internal: a renderer with an `onRenderMetrics` hook calls this once per
257
+ * actual React block render so `getMetrics().renderCount` aggregates churn.
258
+ * No-op cost when no hook is wired (it is simply never called). Not part of
259
+ * the public API surface — the underscore marks it renderer-internal.
260
+ */
261
+ __noteRender(): void;
262
+ /**
263
+ * Internal: the DOM renderer calls this once per actual node rebuild (the
264
+ * changed-block branch) when an `onRenderMetrics` hook is wired, so
265
+ * `getMetrics().rebuildCount` aggregates churn. Never called without a hook.
266
+ */
267
+ __noteRebuild(): void;
268
+ getMetrics(): {
269
+ bytes: number;
270
+ patches: number;
271
+ meanParseMicros: number;
272
+ totalParseMs: number;
273
+ throughputKBs: number;
274
+ committedBlocks: number;
275
+ activeBlocks: number;
276
+ lastPatchAgoMs: number;
277
+ retainedBytes: number;
278
+ wasmMemoryBytes: number;
279
+ renderCount: number;
280
+ rebuildCount: number;
281
+ };
282
+ /**
283
+ * A heading outline of the current snapshot (committed + active), in document
284
+ * order — for a table of contents. Works mid-stream; entries appear as their
285
+ * headings stream in. The `id` is stable, so a built ToC won't re-key.
286
+ */
287
+ outline(): OutlineEntry[];
288
+ /**
289
+ * The rendered document as plain text — tags stripped, entities decoded,
290
+ * blocks separated by blank lines. Derived from the rendered HTML (the source
291
+ * markdown is parsed away in WASM and not retained client-side), so it is a
292
+ * readable approximation for search indexing / summaries, not a round-trip of
293
+ * the original source.
294
+ */
295
+ toPlaintext(): string;
296
+ private onMessage;
297
+ /**
298
+ * Notify subscribers of a new snapshot.
299
+ *
300
+ * With `coalesce` off (default) this is fully synchronous, exactly as before.
301
+ * With it on and `requestAnimationFrame` available, a normal emit only
302
+ * *schedules* a single per-frame flush — repeated intra-frame emits collapse
303
+ * into one notify. `sync` forces an immediate flush (stream completion / reset)
304
+ * and cancels any frame already pending so the snapshot is delivered once.
305
+ */
306
+ private emit;
307
+ private flushNow;
308
+ private cancelFrame;
309
+ }
310
+ export {};