flux-md 0.17.0 → 0.18.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 +950 -0
- package/README.md +41 -43
- package/dist/client.d.ts +1 -0
- package/dist/client.js +20 -6
- package/dist/server-react.d.ts +32 -0
- package/dist/server-react.js +48 -0
- package/dist/server.d.ts +3 -33
- package/dist/server.js +0 -45
- package/dist/types-core.d.ts +5 -0
- package/dist/wasm/flux_md_core_bg.wasm +0 -0
- package/dist/worker-core.d.ts +5 -0
- package/dist/worker-core.js +41 -8
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -25,8 +25,10 @@ constructs Web Workers). For **server-side / static rendering of finished
|
|
|
25
25
|
content** — SSR, React Server Components, build steps — use the worker-free,
|
|
26
26
|
synchronous [`flux-md/server`](#server-side-rendering) entry. The framework packages — `react`,
|
|
27
27
|
`vue`, `svelte`, `solid-js` — are all **optional** peer dependencies; you only
|
|
28
|
-
need the one whose binding you import. The
|
|
29
|
-
`flux-md/dom`, `flux-md/element`)
|
|
28
|
+
need the one whose binding you import. The framework-free entries
|
|
29
|
+
(`flux-md/client`, `flux-md/dom`, `flux-md/element`, and `flux-md/server`) need
|
|
30
|
+
none. (The bare `flux-md` entry re-exports the React component surface, so it
|
|
31
|
+
pulls `react` — import from `flux-md/client` if you want a framework-free core.)
|
|
30
32
|
|
|
31
33
|
> **Vite — one-line config.** Vite's dependency pre-bundling (esbuild) hoists
|
|
32
34
|
> the wasm-bindgen glue into `.vite/deps/`, which breaks the relative
|
|
@@ -44,43 +46,35 @@ need the one whose binding you import. The core (`flux-md`, `flux-md/client`,
|
|
|
44
46
|
|
|
45
47
|
<a id="nextjs"></a>
|
|
46
48
|
|
|
47
|
-
> **Next.js (App Router) —
|
|
48
|
-
> **Turbopack** (the default for both `next dev` and `next build`)
|
|
49
|
-
>
|
|
49
|
+
> **Next.js (App Router) — one requirement.** Works on **Next.js** with
|
|
50
|
+
> **Turbopack** (the default for both `next dev` and `next build`) or webpack.
|
|
51
|
+
> Since 0.17.0 flux-md ships **compiled ESM**, so **no `transpilePackages` or
|
|
52
|
+
> other build config is needed** — earlier versions required it only because the
|
|
53
|
+
> package shipped raw TypeScript, which Next does not compile inside
|
|
54
|
+
> `node_modules`. That no longer applies.
|
|
50
55
|
>
|
|
51
|
-
>
|
|
52
|
-
>
|
|
53
|
-
>
|
|
56
|
+
> **Use it from a Client Component.** `<FluxMarkdown>` uses React hooks (and
|
|
57
|
+
> spawns a Web Worker on mount), so it must carry `"use client"` — it can't be
|
|
58
|
+
> a Server Component. (It is still SSR-safe: on the server it renders an empty
|
|
59
|
+
> shell and only starts streaming after hydration, so there's no SSR crash —
|
|
60
|
+
> the constraint is hooks, not the worker.)
|
|
54
61
|
>
|
|
55
|
-
>
|
|
56
|
-
>
|
|
57
|
-
>
|
|
58
|
-
> const nextConfig: NextConfig = { transpilePackages: ["flux-md"] };
|
|
59
|
-
> export default nextConfig;
|
|
60
|
-
> ```
|
|
62
|
+
> ```tsx
|
|
63
|
+
> "use client";
|
|
64
|
+
> import { FluxMarkdown } from "flux-md/react";
|
|
61
65
|
>
|
|
62
|
-
>
|
|
63
|
-
>
|
|
64
|
-
>
|
|
65
|
-
>
|
|
66
|
-
> the constraint is hooks, not the worker.)
|
|
67
|
-
>
|
|
68
|
-
> ```tsx
|
|
69
|
-
> "use client";
|
|
70
|
-
> import { FluxMarkdown } from "flux-md/react";
|
|
71
|
-
>
|
|
72
|
-
> export default function Answer({ stream }: { stream: AsyncIterable<string> }) {
|
|
73
|
-
> return <FluxMarkdown stream={stream} />;
|
|
74
|
-
> }
|
|
75
|
-
> ```
|
|
66
|
+
> export default function Answer({ stream }: { stream: AsyncIterable<string> }) {
|
|
67
|
+
> return <FluxMarkdown stream={stream} />;
|
|
68
|
+
> }
|
|
69
|
+
> ```
|
|
76
70
|
>
|
|
77
|
-
>
|
|
78
|
-
>
|
|
79
|
-
>
|
|
80
|
-
>
|
|
81
|
-
>
|
|
82
|
-
>
|
|
83
|
-
>
|
|
71
|
+
> **Create the `stream` in Client Component code, not in a Server Component.**
|
|
72
|
+
> A `Response` / `ReadableStream` / `AsyncIterable` isn't serializable, so it
|
|
73
|
+
> can't be passed as a prop from a Server Component (e.g. `page.tsx`) — that
|
|
74
|
+
> throws *"Only plain objects can be passed to Client Components."* Pass a
|
|
75
|
+
> serializable prop (a URL, the chat messages) from the server and open the
|
|
76
|
+
> stream on the client — e.g. `stream={await fetch("/api/chat")}` from a client
|
|
77
|
+
> effect, or the `useFluxStream` hook (see [Quick start](#quick-start)).
|
|
84
78
|
>
|
|
85
79
|
> That's it — Turbopack bundles the worker and emits the `.wasm` to
|
|
86
80
|
> `_next/static/media` itself, so no extra asset/loader config is needed (and the
|
|
@@ -427,12 +421,15 @@ const html = renderToString("# Hello\n\n**world**"); // sync HTML string, no w
|
|
|
427
421
|
```
|
|
428
422
|
|
|
429
423
|
For React server rendering (RSC, static generation, or SSR), use
|
|
430
|
-
`<FluxMarkdownStatic>` — a hookless, RSC-safe
|
|
431
|
-
content with the same `components` overrides
|
|
432
|
-
dispatch on the server too)
|
|
424
|
+
`<FluxMarkdownStatic>` from **`flux-md/server/react`** — a hookless, RSC-safe
|
|
425
|
+
component that renders finished content with the same `components` overrides
|
|
426
|
+
(inline/block component tags dispatch on the server too). It lives in its own
|
|
427
|
+
subpath so the core `flux-md/server` above stays importable with no `react`
|
|
428
|
+
installed:
|
|
433
429
|
|
|
434
430
|
```tsx
|
|
435
|
-
import { initFlux
|
|
431
|
+
import { initFlux } from "flux-md/server";
|
|
432
|
+
import { FluxMarkdownStatic } from "flux-md/server/react";
|
|
436
433
|
|
|
437
434
|
await initFlux();
|
|
438
435
|
export default function Doc({ md }: { md: string }) {
|
|
@@ -451,10 +448,11 @@ export default function Doc({ md }: { md: string }) {
|
|
|
451
448
|
bundler-resolved asset. On edge runtimes pass bytes yourself:
|
|
452
449
|
`initFluxSync(wasmBytes)`.
|
|
453
450
|
- **`renderToString(md, { config })`** — synchronous HTML string, **zero React
|
|
454
|
-
dependency
|
|
451
|
+
dependency** (imports cleanly with no `react` installed).
|
|
455
452
|
- **`parseToBlocks(md, { config })`** — the block array, for custom rendering.
|
|
456
|
-
- **`<FluxMarkdownStatic content config components />`**
|
|
457
|
-
for **render-once** contexts;
|
|
453
|
+
- **`<FluxMarkdownStatic content config components />`** (from
|
|
454
|
+
`flux-md/server/react`) — synchronous React tree for **render-once** contexts;
|
|
455
|
+
render it with your framework's server renderer
|
|
458
456
|
(`renderToStaticMarkup`, RSC, …). For live streaming, client-side code
|
|
459
457
|
highlighting, or Mermaid, render `<FluxMarkdown>` on the client instead — it's a
|
|
460
458
|
separate component. (If you SSR-then-hydrate, use the *same* component on both
|
|
@@ -1161,7 +1159,7 @@ re-snapping during streaming, so treat smooth following there as best-effort.
|
|
|
1161
1159
|
│ microtask, calls WASM │
|
|
1162
1160
|
└──┬──── ffi ───────────────────────────┘
|
|
1163
1161
|
▼
|
|
1164
|
-
┌── Rust → WASM (~
|
|
1162
|
+
┌── Rust → WASM (~170 KB after opt) ────┐
|
|
1165
1163
|
│ StreamParser: │
|
|
1166
1164
|
│ buffer: append-only │
|
|
1167
1165
|
│ committed_offset │
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -141,6 +141,12 @@ class FluxPool {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
for (const sid of pw.streamIds) this.dispatch(sid, msg);
|
|
144
|
+
try {
|
|
145
|
+
pw.worker.terminate();
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
const idx = this.workers.indexOf(pw);
|
|
149
|
+
if (idx !== -1) this.workers.splice(idx, 1);
|
|
144
150
|
return;
|
|
145
151
|
}
|
|
146
152
|
this.dispatch(msg.streamId, msg);
|
|
@@ -200,8 +206,13 @@ class FluxClient {
|
|
|
200
206
|
coalesce = false;
|
|
201
207
|
rafHandle = null;
|
|
202
208
|
// Set by finalize(); the next patch's emit flushes synchronously (a 'done'
|
|
203
|
-
// notification must not be deferred a frame) and clears it.
|
|
209
|
+
// notification must not be deferred a frame) and clears it. Belt-and-suspenders
|
|
210
|
+
// alongside the per-patch `final` flag, which is the authoritative signal.
|
|
204
211
|
finalizePending = false;
|
|
212
|
+
// Stream generation, bumped on reset(). Stamped on every worker message and
|
|
213
|
+
// echoed back on each patch; a patch whose epoch is older than this is a
|
|
214
|
+
// pre-reset straggler and is dropped before it can repopulate the cleared store.
|
|
215
|
+
epoch = 0;
|
|
205
216
|
// Perf
|
|
206
217
|
appendedBytes = 0;
|
|
207
218
|
patchCount = 0;
|
|
@@ -259,7 +270,8 @@ class FluxClient {
|
|
|
259
270
|
* multiplexing (pick() is unchanged and remains the only path to create()).
|
|
260
271
|
*/
|
|
261
272
|
ensureAcquired() {
|
|
262
|
-
if (this.pw) return this.pw;
|
|
273
|
+
if (this.pw && !this.pw.failed) return this.pw;
|
|
274
|
+
this.pw = null;
|
|
263
275
|
const { streamId, pw } = this.pool.acquire((msg) => this.onMessage(msg));
|
|
264
276
|
this.streamId = streamId;
|
|
265
277
|
this.pw = pw;
|
|
@@ -283,12 +295,12 @@ class FluxClient {
|
|
|
283
295
|
append(chunk) {
|
|
284
296
|
const pw = this.ensureAcquired();
|
|
285
297
|
if (this.firstAppendMs === 0) this.firstAppendMs = performance.now();
|
|
286
|
-
this.pool.send(pw, { type: "append", streamId: this.streamId, chunk, config: this.firstConfig() });
|
|
298
|
+
this.pool.send(pw, { type: "append", streamId: this.streamId, chunk, config: this.firstConfig(), epoch: this.epoch });
|
|
287
299
|
}
|
|
288
300
|
finalize() {
|
|
289
301
|
const pw = this.ensureAcquired();
|
|
290
302
|
this.finalizePending = true;
|
|
291
|
-
this.pool.send(pw, { type: "finalize", streamId: this.streamId, config: this.firstConfig() });
|
|
303
|
+
this.pool.send(pw, { type: "finalize", streamId: this.streamId, config: this.firstConfig(), epoch: this.epoch });
|
|
292
304
|
}
|
|
293
305
|
/**
|
|
294
306
|
* Pipe a source straight in: read it to completion, `append()` each chunk,
|
|
@@ -402,8 +414,9 @@ class FluxClient {
|
|
|
402
414
|
this.contentDone = false;
|
|
403
415
|
this.cancelFrame();
|
|
404
416
|
this.finalizePending = false;
|
|
417
|
+
this.epoch += 1;
|
|
405
418
|
const pw = this.ensureAcquired();
|
|
406
|
-
this.pool.send(pw, { type: "reset", streamId: this.streamId });
|
|
419
|
+
this.pool.send(pw, { type: "reset", streamId: this.streamId, epoch: this.epoch });
|
|
407
420
|
if (hadContent) this.emit(true);
|
|
408
421
|
}
|
|
409
422
|
destroy() {
|
|
@@ -511,6 +524,7 @@ class FluxClient {
|
|
|
511
524
|
onMessage(msg) {
|
|
512
525
|
switch (msg.type) {
|
|
513
526
|
case "patch": {
|
|
527
|
+
if (msg.epoch !== void 0 && msg.epoch < this.epoch) break;
|
|
514
528
|
let patch;
|
|
515
529
|
try {
|
|
516
530
|
patch = JSON.parse(msg.patch);
|
|
@@ -527,7 +541,7 @@ class FluxClient {
|
|
|
527
541
|
this.wasmMemoryBytes = msg.wasmMemoryBytes;
|
|
528
542
|
this.patchCount += 1;
|
|
529
543
|
this.lastPatchMs = performance.now();
|
|
530
|
-
const sync = this.finalizePending;
|
|
544
|
+
const sync = this.finalizePending || msg.final === true;
|
|
531
545
|
this.finalizePending = false;
|
|
532
546
|
this.emit(sync);
|
|
533
547
|
if (this.onBlock) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Components, ParserConfig } from "./types.js";
|
|
3
|
+
interface FluxMarkdownStaticProps {
|
|
4
|
+
/** The complete markdown to render (server/static use is for finished content). */
|
|
5
|
+
content: string;
|
|
6
|
+
/** Parser config (same shape as the streaming client's). */
|
|
7
|
+
config?: ParserConfig;
|
|
8
|
+
/** Tag-level / block-kind / component-tag overrides (see {@link Components}). */
|
|
9
|
+
components?: Components;
|
|
10
|
+
/** Appended to the root's `className` (the `flux-md` class is always present). */
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Set on the root element. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** Set on the root element (e.g. `"article"`). */
|
|
15
|
+
role?: string;
|
|
16
|
+
/** Make the root a live region (parity with `<FluxMarkdown>` if you hydrate). */
|
|
17
|
+
"aria-live"?: "off" | "polite" | "assertive";
|
|
18
|
+
/** Live-region atomicity; pair with `aria-live`. */
|
|
19
|
+
"aria-atomic"?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Synchronous, worker-free React rendering of finished markdown — a React Server
|
|
23
|
+
* Component, or any one-shot SSR / static render. Emits the `flux-md` root +
|
|
24
|
+
* per-block structure with the same `components` overrides (inline/block
|
|
25
|
+
* component tags dispatch here too). Requires `initFlux` (or `initFluxSync`)
|
|
26
|
+
* from `flux-md/server` to have run. Uses no hooks (RSC-safe). A **render-once**
|
|
27
|
+
* component: for live streaming, client-side code highlighting, or Mermaid use
|
|
28
|
+
* the client `<FluxMarkdown>` instead (and if you SSR-then-hydrate, render the
|
|
29
|
+
* *same* component on both sides).
|
|
30
|
+
*/
|
|
31
|
+
export declare function FluxMarkdownStatic({ content, config, components, className, id, role, "aria-live": ariaLive, "aria-atomic": ariaAtomic, }: FluxMarkdownStaticProps): ReactNode;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { htmlToReact } from "./html-to-react.js";
|
|
3
|
+
import { blockKindProps } from "./react.js";
|
|
4
|
+
import { parseToBlocks } from "./server.js";
|
|
5
|
+
function renderStaticBlock(block, components) {
|
|
6
|
+
const kind = block.kind.type;
|
|
7
|
+
if (components) {
|
|
8
|
+
if (kind === "Component") {
|
|
9
|
+
const tag = block.kind.data?.tag;
|
|
10
|
+
const override = tag && components[tag] || components.Component;
|
|
11
|
+
if (override) return createElement(override, { key: block.id, ...blockKindProps(block, components) });
|
|
12
|
+
}
|
|
13
|
+
const blockOverride = components[kind];
|
|
14
|
+
if (blockOverride) return createElement(blockOverride, { key: block.id, ...blockKindProps(block, components) });
|
|
15
|
+
}
|
|
16
|
+
const className = "flux-block flux-block-" + kind.toLowerCase() + (block.open ? " flux-open" : "") + (block.speculative ? " flux-speculative" : "");
|
|
17
|
+
if (components) {
|
|
18
|
+
return createElement("div", { key: block.id, className }, htmlToReact(block.html, components));
|
|
19
|
+
}
|
|
20
|
+
return createElement("div", { key: block.id, className, dangerouslySetInnerHTML: { __html: block.html } });
|
|
21
|
+
}
|
|
22
|
+
function FluxMarkdownStatic({
|
|
23
|
+
content,
|
|
24
|
+
config,
|
|
25
|
+
components,
|
|
26
|
+
className,
|
|
27
|
+
id,
|
|
28
|
+
role,
|
|
29
|
+
"aria-live": ariaLive,
|
|
30
|
+
"aria-atomic": ariaAtomic
|
|
31
|
+
}) {
|
|
32
|
+
const blocks = parseToBlocks(content, { config });
|
|
33
|
+
const comps = components && Object.keys(components).length > 0 ? components : void 0;
|
|
34
|
+
return createElement(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
className: className ? `flux-md ${className}` : "flux-md",
|
|
38
|
+
id,
|
|
39
|
+
role,
|
|
40
|
+
"aria-live": ariaLive,
|
|
41
|
+
"aria-atomic": ariaAtomic
|
|
42
|
+
},
|
|
43
|
+
blocks.map((b) => renderStaticBlock(b, comps))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
FluxMarkdownStatic
|
|
48
|
+
};
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Block, Components, ParserConfig } from "./types.js";
|
|
1
|
+
import type { Block, ParserConfig } from "./types.js";
|
|
3
2
|
/** Has the sync WASM core been initialized in this process? */
|
|
4
3
|
export declare function isFluxReady(): boolean;
|
|
5
4
|
/** Initialize the sync core from compiled WASM bytes (or a `WebAssembly.Module`).
|
|
@@ -24,38 +23,9 @@ export declare function parseToBlocks(markdown: string, opts?: {
|
|
|
24
23
|
* Render a complete markdown string to an HTML string synchronously — no worker,
|
|
25
24
|
* no React. The concatenated per-block HTML (XSS-safe with `unsafeHtml` off).
|
|
26
25
|
* For component dispatch / a `<FluxMarkdown>`-matching React tree, use
|
|
27
|
-
*
|
|
26
|
+
* `FluxMarkdownStatic` from `flux-md/server/react` with your framework's server
|
|
27
|
+
* renderer instead.
|
|
28
28
|
*/
|
|
29
29
|
export declare function renderToString(markdown: string, opts?: {
|
|
30
30
|
config?: ParserConfig;
|
|
31
31
|
}): string;
|
|
32
|
-
interface FluxMarkdownStaticProps {
|
|
33
|
-
/** The complete markdown to render (server/static use is for finished content). */
|
|
34
|
-
content: string;
|
|
35
|
-
/** Parser config (same shape as the streaming client's). */
|
|
36
|
-
config?: ParserConfig;
|
|
37
|
-
/** Tag-level / block-kind / component-tag overrides (see {@link Components}). */
|
|
38
|
-
components?: Components;
|
|
39
|
-
/** Appended to the root's `className` (the `flux-md` class is always present). */
|
|
40
|
-
className?: string;
|
|
41
|
-
/** Set on the root element. */
|
|
42
|
-
id?: string;
|
|
43
|
-
/** Set on the root element (e.g. `"article"`). */
|
|
44
|
-
role?: string;
|
|
45
|
-
/** Make the root a live region (parity with `<FluxMarkdown>` if you hydrate). */
|
|
46
|
-
"aria-live"?: "off" | "polite" | "assertive";
|
|
47
|
-
/** Live-region atomicity; pair with `aria-live`. */
|
|
48
|
-
"aria-atomic"?: boolean;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Synchronous, worker-free React rendering of finished markdown — a React Server
|
|
52
|
-
* Component, or any one-shot SSR / static render. Emits the `flux-md` root +
|
|
53
|
-
* per-block structure with the same `components` overrides (inline/block
|
|
54
|
-
* component tags dispatch here too). Requires {@link initFlux} (or
|
|
55
|
-
* {@link initFluxSync}) to have run. Uses no hooks (RSC-safe). A **render-once**
|
|
56
|
-
* component: for live streaming, client-side code highlighting, or Mermaid use
|
|
57
|
-
* the client `<FluxMarkdown>` instead (and if you SSR-then-hydrate, render the
|
|
58
|
-
* *same* component on both sides).
|
|
59
|
-
*/
|
|
60
|
-
export declare function FluxMarkdownStatic({ content, config, components, className, id, role, "aria-live": ariaLive, "aria-atomic": ariaAtomic, }: FluxMarkdownStaticProps): ReactNode;
|
|
61
|
-
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import { createElement } from "react";
|
|
2
1
|
import initWasmAsync, { FluxParser, initSync } from "./wasm/flux_md_core.js";
|
|
3
|
-
import { htmlToReact } from "./html-to-react.js";
|
|
4
|
-
import { blockKindProps } from "./react.js";
|
|
5
2
|
let ready = false;
|
|
6
3
|
function isFluxReady() {
|
|
7
4
|
return ready;
|
|
@@ -75,49 +72,7 @@ function parseToBlocks(markdown, opts) {
|
|
|
75
72
|
function renderToString(markdown, opts) {
|
|
76
73
|
return parseToBlocks(markdown, opts).map((b) => b.html).join("");
|
|
77
74
|
}
|
|
78
|
-
function renderStaticBlock(block, components) {
|
|
79
|
-
const kind = block.kind.type;
|
|
80
|
-
if (components) {
|
|
81
|
-
if (kind === "Component") {
|
|
82
|
-
const tag = block.kind.data?.tag;
|
|
83
|
-
const override = tag && components[tag] || components.Component;
|
|
84
|
-
if (override) return createElement(override, { key: block.id, ...blockKindProps(block, components) });
|
|
85
|
-
}
|
|
86
|
-
const blockOverride = components[kind];
|
|
87
|
-
if (blockOverride) return createElement(blockOverride, { key: block.id, ...blockKindProps(block, components) });
|
|
88
|
-
}
|
|
89
|
-
const className = "flux-block flux-block-" + kind.toLowerCase() + (block.open ? " flux-open" : "") + (block.speculative ? " flux-speculative" : "");
|
|
90
|
-
if (components) {
|
|
91
|
-
return createElement("div", { key: block.id, className }, htmlToReact(block.html, components));
|
|
92
|
-
}
|
|
93
|
-
return createElement("div", { key: block.id, className, dangerouslySetInnerHTML: { __html: block.html } });
|
|
94
|
-
}
|
|
95
|
-
function FluxMarkdownStatic({
|
|
96
|
-
content,
|
|
97
|
-
config,
|
|
98
|
-
components,
|
|
99
|
-
className,
|
|
100
|
-
id,
|
|
101
|
-
role,
|
|
102
|
-
"aria-live": ariaLive,
|
|
103
|
-
"aria-atomic": ariaAtomic
|
|
104
|
-
}) {
|
|
105
|
-
const blocks = parseToBlocks(content, { config });
|
|
106
|
-
const comps = components && Object.keys(components).length > 0 ? components : void 0;
|
|
107
|
-
return createElement(
|
|
108
|
-
"div",
|
|
109
|
-
{
|
|
110
|
-
className: className ? `flux-md ${className}` : "flux-md",
|
|
111
|
-
id,
|
|
112
|
-
role,
|
|
113
|
-
"aria-live": ariaLive,
|
|
114
|
-
"aria-atomic": ariaAtomic
|
|
115
|
-
},
|
|
116
|
-
blocks.map((b) => renderStaticBlock(b, comps))
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
75
|
export {
|
|
120
|
-
FluxMarkdownStatic,
|
|
121
76
|
initFlux,
|
|
122
77
|
initFluxSync,
|
|
123
78
|
isFluxReady,
|
package/dist/types-core.d.ts
CHANGED
|
@@ -336,13 +336,16 @@ export type ToWorker = {
|
|
|
336
336
|
streamId: number;
|
|
337
337
|
chunk: string;
|
|
338
338
|
config?: ParserConfig;
|
|
339
|
+
epoch?: number;
|
|
339
340
|
} | {
|
|
340
341
|
type: "finalize";
|
|
341
342
|
streamId: number;
|
|
342
343
|
config?: ParserConfig;
|
|
344
|
+
epoch?: number;
|
|
343
345
|
} | {
|
|
344
346
|
type: "reset";
|
|
345
347
|
streamId: number;
|
|
348
|
+
epoch?: number;
|
|
346
349
|
} | {
|
|
347
350
|
type: "dispose";
|
|
348
351
|
streamId: number;
|
|
@@ -357,6 +360,8 @@ export type FromWorker = {
|
|
|
357
360
|
parseMicros: number;
|
|
358
361
|
retainedBytes: number;
|
|
359
362
|
wasmMemoryBytes: number;
|
|
363
|
+
final?: boolean;
|
|
364
|
+
epoch?: number;
|
|
360
365
|
} | {
|
|
361
366
|
type: "error";
|
|
362
367
|
streamId: number;
|
|
Binary file
|
package/dist/worker-core.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare class WorkerCore {
|
|
|
44
44
|
private pending;
|
|
45
45
|
private totalAppended;
|
|
46
46
|
private finalizePending;
|
|
47
|
+
private epochs;
|
|
47
48
|
private flushScheduled;
|
|
48
49
|
private ready;
|
|
49
50
|
constructor(deps: WorkerCoreDeps);
|
|
@@ -53,8 +54,12 @@ export declare class WorkerCore {
|
|
|
53
54
|
markReady(): void;
|
|
54
55
|
private getOrCreate;
|
|
55
56
|
private dispose;
|
|
57
|
+
/** free() a stream's parser, swallowing a throw from a poisoned WASM instance
|
|
58
|
+
* so teardown can never escape the message loop. */
|
|
59
|
+
private safeFree;
|
|
56
60
|
private emitPatch;
|
|
57
61
|
private scheduleFlush;
|
|
58
62
|
private flush;
|
|
59
63
|
private doFinalize;
|
|
64
|
+
private postParseError;
|
|
60
65
|
}
|
package/dist/worker-core.js
CHANGED
|
@@ -9,6 +9,11 @@ class WorkerCore {
|
|
|
9
9
|
pending = /* @__PURE__ */ new Map();
|
|
10
10
|
totalAppended = /* @__PURE__ */ new Map();
|
|
11
11
|
finalizePending = /* @__PURE__ */ new Set();
|
|
12
|
+
// Per-stream generation, echoed on emitted patches so the client can drop a
|
|
13
|
+
// patch produced before a reset (see FromWorker.epoch). A microtask flush
|
|
14
|
+
// drains `pending` between messages, so the epoch at emit time is always the
|
|
15
|
+
// epoch the buffered input was appended under.
|
|
16
|
+
epochs = /* @__PURE__ */ new Map();
|
|
12
17
|
flushScheduled = false;
|
|
13
18
|
ready = false;
|
|
14
19
|
/** Handle one message from the main thread (append/finalize/reset/dispose). */
|
|
@@ -17,6 +22,9 @@ class WorkerCore {
|
|
|
17
22
|
if ((msg.type === "append" || msg.type === "finalize") && msg.config) {
|
|
18
23
|
this.config.set(id, msg.config);
|
|
19
24
|
}
|
|
25
|
+
if (msg.type !== "dispose" && msg.epoch !== void 0) {
|
|
26
|
+
this.epochs.set(id, msg.epoch);
|
|
27
|
+
}
|
|
20
28
|
switch (msg.type) {
|
|
21
29
|
case "append":
|
|
22
30
|
this.pending.set(id, (this.pending.get(id) ?? "") + msg.chunk);
|
|
@@ -27,7 +35,7 @@ class WorkerCore {
|
|
|
27
35
|
else this.doFinalize(id);
|
|
28
36
|
break;
|
|
29
37
|
case "reset":
|
|
30
|
-
this.
|
|
38
|
+
this.safeFree(id);
|
|
31
39
|
this.parsers.delete(id);
|
|
32
40
|
this.pending.delete(id);
|
|
33
41
|
this.finalizePending.delete(id);
|
|
@@ -57,14 +65,23 @@ class WorkerCore {
|
|
|
57
65
|
return p;
|
|
58
66
|
}
|
|
59
67
|
dispose(streamId) {
|
|
60
|
-
this.
|
|
68
|
+
this.safeFree(streamId);
|
|
61
69
|
this.parsers.delete(streamId);
|
|
62
70
|
this.config.delete(streamId);
|
|
63
71
|
this.pending.delete(streamId);
|
|
64
72
|
this.finalizePending.delete(streamId);
|
|
65
73
|
this.totalAppended.delete(streamId);
|
|
74
|
+
this.epochs.delete(streamId);
|
|
75
|
+
}
|
|
76
|
+
/** free() a stream's parser, swallowing a throw from a poisoned WASM instance
|
|
77
|
+
* so teardown can never escape the message loop. */
|
|
78
|
+
safeFree(streamId) {
|
|
79
|
+
try {
|
|
80
|
+
this.parsers.get(streamId)?.free();
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
66
83
|
}
|
|
67
|
-
emitPatch(streamId, patch, parser, parseMicros) {
|
|
84
|
+
emitPatch(streamId, patch, parser, parseMicros, final) {
|
|
68
85
|
this.deps.post({
|
|
69
86
|
type: "patch",
|
|
70
87
|
streamId,
|
|
@@ -72,7 +89,9 @@ class WorkerCore {
|
|
|
72
89
|
appendedBytes: this.totalAppended.get(streamId) ?? 0,
|
|
73
90
|
parseMicros,
|
|
74
91
|
retainedBytes: parser.retainedBytes(),
|
|
75
|
-
wasmMemoryBytes: this.deps.memBytes()
|
|
92
|
+
wasmMemoryBytes: this.deps.memBytes(),
|
|
93
|
+
final,
|
|
94
|
+
epoch: this.epochs.get(streamId)
|
|
76
95
|
});
|
|
77
96
|
}
|
|
78
97
|
scheduleFlush() {
|
|
@@ -92,9 +111,9 @@ class WorkerCore {
|
|
|
92
111
|
const patch = parser.append(chunk);
|
|
93
112
|
const dt = (performance.now() - t0) * 1e3;
|
|
94
113
|
this.totalAppended.set(streamId, (this.totalAppended.get(streamId) ?? 0) + chunk.length);
|
|
95
|
-
this.emitPatch(streamId, patch, parser, dt);
|
|
114
|
+
this.emitPatch(streamId, patch, parser, dt, false);
|
|
96
115
|
} catch (e) {
|
|
97
|
-
this.
|
|
116
|
+
this.postParseError(streamId, e);
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
}
|
|
@@ -110,11 +129,25 @@ class WorkerCore {
|
|
|
110
129
|
this.totalAppended.set(streamId, (this.totalAppended.get(streamId) ?? 0) + buffered.length);
|
|
111
130
|
}
|
|
112
131
|
const patch = parser.finalize();
|
|
113
|
-
this.emitPatch(streamId, patch, parser, 0);
|
|
132
|
+
this.emitPatch(streamId, patch, parser, 0, true);
|
|
114
133
|
} catch (e) {
|
|
115
|
-
this.
|
|
134
|
+
this.postParseError(streamId, e);
|
|
116
135
|
}
|
|
117
136
|
}
|
|
137
|
+
// A WASM trap (stack overflow / unreachable / OOM) throws a
|
|
138
|
+
// WebAssembly.RuntimeError and POISONS the singleton instance shared by every
|
|
139
|
+
// stream on this worker — so it is escalated to `fatal`, which the pool treats
|
|
140
|
+
// as a worker-level failure (eviction + terminate). A plain Error (e.g. a
|
|
141
|
+
// parser-construction failure) stays a recoverable per-stream error.
|
|
142
|
+
postParseError(streamId, e) {
|
|
143
|
+
const fatal = typeof WebAssembly !== "undefined" && typeof WebAssembly.RuntimeError === "function" && e instanceof WebAssembly.RuntimeError;
|
|
144
|
+
this.deps.post({
|
|
145
|
+
type: "error",
|
|
146
|
+
streamId,
|
|
147
|
+
message: e instanceof Error ? e.message : String(e),
|
|
148
|
+
fatal
|
|
149
|
+
});
|
|
150
|
+
}
|
|
118
151
|
}
|
|
119
152
|
export {
|
|
120
153
|
WorkerCore
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flux-md",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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"],
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
|
|
12
12
|
"./react": { "types": "./dist/react.d.ts", "import": "./dist/react.js" },
|
|
13
13
|
"./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
|
|
14
|
+
"./server/react": { "types": "./dist/server-react.d.ts", "import": "./dist/server-react.js" },
|
|
14
15
|
"./dom": { "types": "./dist/dom.d.ts", "import": "./dist/dom.js" },
|
|
15
16
|
"./element": { "types": "./dist/element.d.ts", "import": "./dist/element.js" },
|
|
16
17
|
"./vue": { "types": "./dist/vue.d.ts", "import": "./dist/vue.js" },
|
|
@@ -23,7 +24,8 @@
|
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"dist",
|
|
26
|
-
"README.md"
|
|
27
|
+
"README.md",
|
|
28
|
+
"CHANGELOG.md"
|
|
27
29
|
],
|
|
28
30
|
"peerDependencies": {
|
|
29
31
|
"react": ">=18",
|