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.
- package/README.md +70 -39
- package/dist/block-props.d.ts +18 -0
- package/dist/block-props.js +75 -0
- package/dist/client.d.ts +310 -0
- package/{src/client.ts → dist/client.js} +123 -319
- package/dist/dom.d.ts +110 -0
- package/dist/dom.js +576 -0
- package/dist/element.d.ts +20 -0
- package/dist/element.js +287 -0
- package/dist/hi.d.ts +12 -0
- package/{src/hi.ts → dist/hi.js} +42 -74
- package/dist/html-to-react.d.ts +40 -0
- package/dist/html-to-react.js +344 -0
- package/{src/index.ts → dist/index.d.ts} +5 -25
- package/dist/index.js +16 -0
- package/dist/morph.d.ts +28 -0
- package/dist/morph.js +166 -0
- package/dist/react.d.ts +204 -0
- package/dist/react.js +490 -0
- package/dist/renderers/CodeBlock.d.ts +7 -0
- package/dist/renderers/CodeBlock.js +75 -0
- package/dist/renderers/Math.d.ts +14 -0
- package/dist/renderers/Math.js +15 -0
- package/dist/renderers/Mermaid.d.ts +13 -0
- package/dist/renderers/Mermaid.js +15 -0
- package/dist/server.d.ts +61 -0
- package/dist/server.js +126 -0
- package/{src/solid.tsx → dist/solid.d.ts} +19 -95
- package/dist/solid.js +54 -0
- package/dist/svelte.d.ts +80 -0
- package/dist/svelte.js +59 -0
- package/dist/types-core.d.ts +377 -0
- package/dist/types-core.js +0 -0
- package/{src/types-react.ts → dist/types-react.d.ts} +0 -1
- package/dist/types-react.js +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/vue.d.ts +94 -0
- package/dist/vue.js +79 -0
- package/{src → dist}/wasm/flux_md_core.d.ts +18 -6
- package/{src → dist}/wasm/flux_md_core.js +50 -55
- package/dist/wasm/flux_md_core_bg.wasm +0 -0
- package/{src → dist}/wasm/flux_md_core_bg.wasm.d.ts +1 -0
- package/dist/worker-core.d.ts +60 -0
- package/dist/worker-core.js +121 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +48 -0
- package/package.json +22 -18
- package/src/block-props.ts +0 -141
- package/src/dom.ts +0 -946
- package/src/element.ts +0 -400
- package/src/html-to-react.ts +0 -455
- package/src/morph.ts +0 -253
- package/src/react.tsx +0 -1020
- package/src/renderers/CodeBlock.tsx +0 -116
- package/src/renderers/Math.tsx +0 -28
- package/src/renderers/Mermaid.tsx +0 -27
- package/src/server.tsx +0 -221
- package/src/svelte.ts +0 -179
- package/src/types-core.ts +0 -398
- package/src/types.ts +0 -7
- package/src/vue.ts +0 -184
- package/src/wasm/flux_md_core_bg.wasm +0 -0
- package/src/worker-core.ts +0 -174
- package/src/worker.ts +0 -72
- /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
|
|
16
|
-
WASM
|
|
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).
|
|
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
|
|
1003
|
-
|
|
1004
|
-
flux-md
|
|
1005
|
-
|
|
1006
|
-
buildable from `crates/flux-md-core/` via
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/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
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -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 {};
|