flux-md 0.3.1

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/src/worker.ts ADDED
@@ -0,0 +1,151 @@
1
+ /// <reference lib="webworker" />
2
+ import init, { FluxParser } from "./wasm/flux_md_core.js";
3
+ import type { FromWorker, ParserConfig, Patch, ToWorker } from "./types";
4
+
5
+ // Resolve the WASM asset with the *web-standard* `new URL(asset,
6
+ // import.meta.url)` pattern (not Vite's `?url` suffix), so the package works in
7
+ // any bundler with asset-module support — Vite, webpack 5, Rollup, Parcel.
8
+ // wasm-bindgen's init() fetches a URL instance directly.
9
+ const wasmUrl = new URL("./wasm/flux_md_core_bg.wasm", import.meta.url);
10
+
11
+ // One worker multiplexes many streams: a parser per stream id (the worker
12
+ // pool). WASM is loaded once for the whole worker, shared by every parser.
13
+ const parsers = new Map<number, FluxParser>();
14
+ const config = new Map<number, ParserConfig>();
15
+ const pending = new Map<number, string>();
16
+ const totalAppended = new Map<number, number>();
17
+ let flushScheduled = false;
18
+ let wasmExports: any = null;
19
+
20
+ const ctx: DedicatedWorkerGlobalScope = self as unknown as DedicatedWorkerGlobalScope;
21
+
22
+ function post(msg: FromWorker) {
23
+ ctx.postMessage(msg);
24
+ }
25
+
26
+ async function setup() {
27
+ // init() returns the wasm-bindgen instance; capture its `.memory` export so
28
+ // we can report WASM-side memory usage on every patch. No parser yet — they
29
+ // are created per stream, on demand.
30
+ wasmExports = await init({ module_or_path: wasmUrl });
31
+ post({ type: "ready" });
32
+ }
33
+
34
+ function getOrCreate(streamId: number): FluxParser {
35
+ let p = parsers.get(streamId);
36
+ if (!p) {
37
+ p = new FluxParser();
38
+ // Per-stream config (sent on the stream's first message); omitted fields
39
+ // fall back to the library defaults — autolinks + alerts on (LLM output is
40
+ // full of bare URLs and `> [!NOTE]` blocks), raw HTML escaped, footnotes off.
41
+ const c = config.get(streamId);
42
+ p.setGfmAutolinks(c?.gfmAutolinks ?? true);
43
+ p.setGfmAlerts(c?.gfmAlerts ?? true);
44
+ p.setGfmFootnotes(c?.gfmFootnotes ?? false);
45
+ p.setGfmMath(c?.gfmMath ?? false);
46
+ p.setDirAuto(c?.dirAuto ?? false);
47
+ p.setUnsafeHtml(c?.unsafeHtml ?? false);
48
+ parsers.set(streamId, p);
49
+ }
50
+ return p;
51
+ }
52
+
53
+ function dispose(streamId: number) {
54
+ parsers.get(streamId)?.free();
55
+ parsers.delete(streamId);
56
+ config.delete(streamId);
57
+ pending.delete(streamId);
58
+ totalAppended.delete(streamId);
59
+ }
60
+
61
+ function wasmMemBytes(): number {
62
+ try {
63
+ return (wasmExports?.memory?.buffer?.byteLength as number) ?? 0;
64
+ } catch {
65
+ return 0;
66
+ }
67
+ }
68
+
69
+ function emitPatch(streamId: number, patch: Patch, parser: FluxParser, parseMicros: number) {
70
+ post({
71
+ type: "patch",
72
+ streamId,
73
+ patch,
74
+ appendedBytes: totalAppended.get(streamId) ?? 0,
75
+ parseMicros,
76
+ retainedBytes: parser.retainedBytes(),
77
+ wasmMemoryBytes: wasmMemBytes(),
78
+ });
79
+ }
80
+
81
+ function flush() {
82
+ flushScheduled = false;
83
+ if (pending.size === 0) return;
84
+ // Process every stream with buffered input this microtask.
85
+ for (const [streamId, chunk] of pending) {
86
+ pending.delete(streamId);
87
+ if (chunk.length === 0) continue;
88
+ const parser = getOrCreate(streamId);
89
+ const t0 = performance.now();
90
+ try {
91
+ const patch = parser.append(chunk) as Patch;
92
+ const dt = (performance.now() - t0) * 1000;
93
+ totalAppended.set(streamId, (totalAppended.get(streamId) ?? 0) + chunk.length);
94
+ emitPatch(streamId, patch, parser, dt);
95
+ } catch (e: unknown) {
96
+ post({ type: "error", streamId, message: e instanceof Error ? e.message : String(e) });
97
+ }
98
+ }
99
+ }
100
+
101
+ function scheduleFlush() {
102
+ if (flushScheduled) return;
103
+ flushScheduled = true;
104
+ queueMicrotask(flush);
105
+ }
106
+
107
+ ctx.addEventListener("message", (ev: MessageEvent<ToWorker>) => {
108
+ const msg = ev.data;
109
+ const id = msg.streamId;
110
+ // Stash any per-stream config carried on the first message (FIFO guarantees
111
+ // it arrives before the parser is created in flush/finalize).
112
+ if ((msg.type === "append" || msg.type === "finalize") && msg.config) {
113
+ config.set(id, msg.config);
114
+ }
115
+ switch (msg.type) {
116
+ case "append":
117
+ pending.set(id, (pending.get(id) ?? "") + msg.chunk);
118
+ scheduleFlush();
119
+ break;
120
+ case "finalize": {
121
+ // Drain any buffered input for this stream, then finalize.
122
+ const buffered = pending.get(id);
123
+ pending.delete(id);
124
+ const parser = getOrCreate(id);
125
+ try {
126
+ if (buffered && buffered.length > 0) {
127
+ parser.append(buffered);
128
+ totalAppended.set(id, (totalAppended.get(id) ?? 0) + buffered.length);
129
+ }
130
+ const patch = parser.finalize() as Patch;
131
+ emitPatch(id, patch, parser, 0);
132
+ } catch (e: unknown) {
133
+ post({ type: "error", streamId: id, message: e instanceof Error ? e.message : String(e) });
134
+ }
135
+ break;
136
+ }
137
+ case "reset":
138
+ // Free and recreate lazily on the next append — same stream id, **same
139
+ // config** (kept). The client resets its local state synchronously.
140
+ parsers.get(id)?.free();
141
+ parsers.delete(id);
142
+ pending.delete(id);
143
+ totalAppended.delete(id);
144
+ break;
145
+ case "dispose":
146
+ dispose(id);
147
+ break;
148
+ }
149
+ });
150
+
151
+ setup();