flux-md 0.16.2 → 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 +111 -82
- package/dist/block-props.d.ts +18 -0
- package/dist/block-props.js +75 -0
- package/dist/client.d.ts +311 -0
- package/{src/client.ts → dist/client.js} +143 -325
- 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-react.d.ts +32 -0
- package/dist/server-react.js +48 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +81 -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 +382 -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 +65 -0
- package/dist/worker-core.js +154 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +48 -0
- package/package.json +25 -19
- 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
|
@@ -1,113 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The ordered-block store backing a stream, extracted as a pure function so
|
|
5
|
-
* its reference-stability contract is testable without a Worker.
|
|
6
|
-
*
|
|
7
|
-
* **The contract that prevents extra React re-renders:** a block, once
|
|
8
|
-
* committed, is never re-sent by the parser, so `applyPatch` never replaces it
|
|
9
|
-
* in the map. Its object reference stays identical across every later patch —
|
|
10
|
-
* which is exactly what `blocksEqual` (the BlockView memo) checks, so committed
|
|
11
|
-
* blocks never re-render (and never re-parse) as the stream grows. Only the
|
|
12
|
-
* `active` tail gets fresh references each patch, and only it re-renders.
|
|
13
|
-
*/
|
|
14
|
-
export interface BlockStore {
|
|
15
|
-
committed: Map<number, Block>;
|
|
16
|
-
committedOrder: number[];
|
|
17
|
-
active: Block[];
|
|
18
|
-
snapshot: Block[];
|
|
1
|
+
function emptyBlockStore() {
|
|
2
|
+
return { committed: /* @__PURE__ */ new Map(), committedOrder: [], active: [], snapshot: [] };
|
|
19
3
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return { committed: new Map(), committedOrder: [], active: [], snapshot: [] };
|
|
4
|
+
function htmlToText(html) {
|
|
5
|
+
return html.replace(/<[^>]*>/g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").replace(/\s+/g, " ").trim();
|
|
23
6
|
}
|
|
24
|
-
|
|
25
|
-
/** A heading entry for building a table of contents — see {@link FluxClient.outline}. */
|
|
26
|
-
export interface OutlineEntry {
|
|
27
|
-
/** Heading level 1–6. */
|
|
28
|
-
level: number;
|
|
29
|
-
/** Plain-text heading content (tags stripped, entities decoded). */
|
|
30
|
-
text: string;
|
|
31
|
-
/** Stable block id — usable as a scroll target / React key. */
|
|
32
|
-
id: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Strip tags (→ space) and decode the small entity set the core emits, then
|
|
36
|
-
* collapse whitespace. INVARIANT: the simple `<[^>]*>` strip is only safe
|
|
37
|
-
* because every input here is HTML the Rust core produced via escape_html /
|
|
38
|
-
* escape_attr — which escape `>` inside attribute values, so no `>` ever
|
|
39
|
-
* appears except as a real tag close. This must NOT be fed externally-authored
|
|
40
|
-
* HTML. `&` decodes last so `&lt;` → `<`, not `<`. */
|
|
41
|
-
function htmlToText(html: string): string {
|
|
42
|
-
return html
|
|
43
|
-
.replace(/<[^>]*>/g, " ")
|
|
44
|
-
.replace(/</g, "<")
|
|
45
|
-
.replace(/>/g, ">")
|
|
46
|
-
.replace(/"/g, '"')
|
|
47
|
-
.replace(/'/g, "'")
|
|
48
|
-
.replace(/&/g, "&")
|
|
49
|
-
.replace(/\s+/g, " ")
|
|
50
|
-
.trim();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function applyPatch(store: BlockStore, patch: Patch): void {
|
|
7
|
+
function applyPatch(store, patch) {
|
|
54
8
|
for (const b of patch.newly_committed) {
|
|
55
9
|
if (!store.committed.has(b.id)) store.committedOrder.push(b.id);
|
|
56
10
|
store.committed.set(b.id, b);
|
|
57
11
|
}
|
|
58
12
|
store.active = patch.active;
|
|
59
|
-
|
|
60
|
-
// committed entries inside it are the same object references as before.
|
|
61
|
-
const next: Block[] = new Array(store.committedOrder.length + store.active.length);
|
|
13
|
+
const next = new Array(store.committedOrder.length + store.active.length);
|
|
62
14
|
for (let i = 0; i < store.committedOrder.length; i++) {
|
|
63
|
-
next[i] = store.committed.get(store.committedOrder[i])
|
|
15
|
+
next[i] = store.committed.get(store.committedOrder[i]);
|
|
64
16
|
}
|
|
65
17
|
for (let i = 0; i < store.active.length; i++) {
|
|
66
18
|
next[store.committedOrder.length + i] = store.active[i];
|
|
67
19
|
}
|
|
68
20
|
store.snapshot = next;
|
|
69
21
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
streamCount: number;
|
|
81
|
-
/** Live stream ids on this worker — so a fatal failure can notify each one. */
|
|
82
|
-
streamIds: Set<number>;
|
|
83
|
-
readyWaiters: Array<{ resolve: () => void; reject: (e: Error) => void }>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* A pool of Web Workers, each multiplexing many `FluxParser`s keyed by stream
|
|
88
|
-
* id. This is what lets flux-md scale past `hardwareConcurrency` concurrent
|
|
89
|
-
* streams without oversubscribing OS threads: 50 streams share (at most) the
|
|
90
|
-
* cap's worth of workers instead of spawning 50.
|
|
91
|
-
*
|
|
92
|
-
* Worker creation is **lazy and load-aware**: while under the cap, each new
|
|
93
|
-
* stream gets its own worker (so 1 stream = 1 worker, identical to the old
|
|
94
|
-
* behavior); once at the cap, new streams attach to the least-loaded worker.
|
|
95
|
-
*
|
|
96
|
-
* The constructor injects a `WorkerLike` factory so the routing and lifecycle
|
|
97
|
-
* logic is unit-testable with a fake worker — no real Worker or WASM needed.
|
|
98
|
-
*/
|
|
99
|
-
export class FluxPool {
|
|
100
|
-
private workers: PoolWorker[] = [];
|
|
101
|
-
private handlers = new Map<number, (msg: FromWorker) => void>();
|
|
102
|
-
private nextStreamId = 1;
|
|
103
|
-
|
|
104
|
-
constructor(
|
|
105
|
-
private factory: () => WorkerLike,
|
|
106
|
-
private cap: number,
|
|
107
|
-
) {}
|
|
108
|
-
|
|
22
|
+
class FluxPool {
|
|
23
|
+
constructor(factory, cap) {
|
|
24
|
+
this.factory = factory;
|
|
25
|
+
this.cap = cap;
|
|
26
|
+
}
|
|
27
|
+
factory;
|
|
28
|
+
cap;
|
|
29
|
+
workers = [];
|
|
30
|
+
handlers = /* @__PURE__ */ new Map();
|
|
31
|
+
nextStreamId = 1;
|
|
109
32
|
/** Reserve a stream id and assign a worker, registering its message handler. */
|
|
110
|
-
acquire(handler
|
|
33
|
+
acquire(handler) {
|
|
111
34
|
const streamId = this.nextStreamId++;
|
|
112
35
|
const pw = this.pick();
|
|
113
36
|
pw.streamCount++;
|
|
@@ -115,42 +38,36 @@ export class FluxPool {
|
|
|
115
38
|
this.handlers.set(streamId, handler);
|
|
116
39
|
return { streamId, pw };
|
|
117
40
|
}
|
|
118
|
-
|
|
119
41
|
/** Free a stream's parser in its worker; keep the worker warm for siblings. */
|
|
120
|
-
release(streamId
|
|
42
|
+
release(streamId, pw) {
|
|
121
43
|
this.handlers.delete(streamId);
|
|
122
44
|
pw.streamIds.delete(streamId);
|
|
123
45
|
pw.streamCount = Math.max(0, pw.streamCount - 1);
|
|
124
46
|
try {
|
|
125
47
|
pw.worker.postMessage({ type: "dispose", streamId });
|
|
126
48
|
} catch {
|
|
127
|
-
/* worker already gone */
|
|
128
49
|
}
|
|
129
50
|
}
|
|
130
|
-
|
|
131
51
|
/** Inverse of {@link release}: re-register a stream's handler so it receives
|
|
132
52
|
* patches again. For React StrictMode's dev double-mount, which destroys a
|
|
133
53
|
* client on the simulated unmount and remounts the SAME instance. The worker
|
|
134
54
|
* lazily recreates the disposed parser on the next append. */
|
|
135
|
-
reattach(streamId
|
|
55
|
+
reattach(streamId, pw, handler) {
|
|
136
56
|
if (!this.handlers.has(streamId)) {
|
|
137
57
|
pw.streamCount++;
|
|
138
58
|
pw.streamIds.add(streamId);
|
|
139
59
|
}
|
|
140
60
|
this.handlers.set(streamId, handler);
|
|
141
61
|
}
|
|
142
|
-
|
|
143
|
-
send(pw: PoolWorker, msg: ToWorker): void {
|
|
62
|
+
send(pw, msg) {
|
|
144
63
|
pw.worker.postMessage(msg);
|
|
145
64
|
}
|
|
146
|
-
|
|
147
65
|
/** Resolves when the given worker has finished WASM init; rejects if it failed. */
|
|
148
|
-
whenWorkerReady(pw
|
|
66
|
+
whenWorkerReady(pw) {
|
|
149
67
|
if (pw.ready) return Promise.resolve();
|
|
150
68
|
if (pw.failed) return Promise.reject(pw.failed);
|
|
151
69
|
return new Promise((resolve, reject) => pw.readyWaiters.push({ resolve, reject }));
|
|
152
70
|
}
|
|
153
|
-
|
|
154
71
|
/**
|
|
155
72
|
* Eagerly spin up one worker so WASM init starts BEFORE the first stream —
|
|
156
73
|
* taking the one-time init off the first-token critical path (e.g. call
|
|
@@ -160,57 +77,51 @@ export class FluxPool {
|
|
|
160
77
|
* finished initializing WASM; rejects if init fails fatally. Browser-only (it
|
|
161
78
|
* constructs a `Worker`).
|
|
162
79
|
*/
|
|
163
|
-
warm()
|
|
80
|
+
warm() {
|
|
164
81
|
const live = this.workers.filter((w) => !w.failed);
|
|
165
82
|
const pw = live[0] ?? this.create();
|
|
166
83
|
return this.whenWorkerReady(pw);
|
|
167
84
|
}
|
|
168
|
-
|
|
169
85
|
/** Terminate every worker (test teardown / full shutdown). */
|
|
170
|
-
disposeAll()
|
|
86
|
+
disposeAll() {
|
|
171
87
|
for (const pw of this.workers) {
|
|
172
88
|
try {
|
|
173
89
|
pw.worker.terminate();
|
|
174
90
|
} catch {
|
|
175
|
-
/* ignore */
|
|
176
91
|
}
|
|
177
92
|
}
|
|
178
93
|
this.workers = [];
|
|
179
94
|
this.handlers.clear();
|
|
180
95
|
}
|
|
181
|
-
|
|
182
|
-
get workerCount(): number {
|
|
96
|
+
get workerCount() {
|
|
183
97
|
return this.workers.length;
|
|
184
98
|
}
|
|
185
|
-
|
|
186
99
|
// Create a new worker while under cap and every live worker is busy; otherwise
|
|
187
100
|
// attach to the least-loaded LIVE worker. A fatally-failed worker is never
|
|
188
101
|
// handed out (a stream on it would post into a dead worker and hang) — it is
|
|
189
102
|
// retained only to reject outstanding whenWorkerReady waiters.
|
|
190
|
-
|
|
103
|
+
pick() {
|
|
191
104
|
const live = this.workers.filter((w) => !w.failed);
|
|
192
105
|
if (this.workers.length < this.cap && live.every((w) => w.streamCount > 0)) {
|
|
193
106
|
return this.create();
|
|
194
107
|
}
|
|
195
108
|
if (live.length === 0) return this.create();
|
|
196
|
-
return live.reduce((a, b) =>
|
|
109
|
+
return live.reduce((a, b) => b.streamCount < a.streamCount ? b : a);
|
|
197
110
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const pw: PoolWorker = {
|
|
111
|
+
create() {
|
|
112
|
+
const pw = {
|
|
201
113
|
worker: this.factory(),
|
|
202
114
|
ready: false,
|
|
203
115
|
failed: null,
|
|
204
116
|
streamCount: 0,
|
|
205
|
-
streamIds: new Set(),
|
|
206
|
-
readyWaiters: []
|
|
117
|
+
streamIds: /* @__PURE__ */ new Set(),
|
|
118
|
+
readyWaiters: []
|
|
207
119
|
};
|
|
208
120
|
pw.worker.addEventListener("message", (ev) => this.onMessage(pw, ev.data));
|
|
209
121
|
this.workers.push(pw);
|
|
210
122
|
return pw;
|
|
211
123
|
}
|
|
212
|
-
|
|
213
|
-
private onMessage(pw: PoolWorker, msg: FromWorker): void {
|
|
124
|
+
onMessage(pw, msg) {
|
|
214
125
|
if (msg.type === "ready") {
|
|
215
126
|
pw.ready = true;
|
|
216
127
|
const waiters = pw.readyWaiters;
|
|
@@ -219,10 +130,6 @@ export class FluxPool {
|
|
|
219
130
|
return;
|
|
220
131
|
}
|
|
221
132
|
if (msg.type === "error" && msg.fatal) {
|
|
222
|
-
// A fatal (WASM-init) failure dooms every stream on this worker. Reject
|
|
223
|
-
// anyone awaiting readiness, then notify each live stream's client so its
|
|
224
|
-
// onError fires — the message carries no real streamId to route by. The
|
|
225
|
-
// worker is kept only to reject those waiters; pick() never reuses it.
|
|
226
133
|
const err = new Error(msg.message);
|
|
227
134
|
pw.failed = err;
|
|
228
135
|
const waiters = pw.readyWaiters;
|
|
@@ -231,91 +138,63 @@ export class FluxPool {
|
|
|
231
138
|
try {
|
|
232
139
|
w.reject(err);
|
|
233
140
|
} catch {
|
|
234
|
-
/* a waiter's rejection handler is the caller's problem, not ours */
|
|
235
141
|
}
|
|
236
142
|
}
|
|
237
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);
|
|
238
150
|
return;
|
|
239
151
|
}
|
|
240
152
|
this.dispatch(msg.streamId, msg);
|
|
241
153
|
}
|
|
242
|
-
|
|
243
154
|
// Route a message to a stream's handler, isolating a throwing client callback
|
|
244
155
|
// (e.g. a user-supplied onError) so it can neither break the worker message
|
|
245
156
|
// loop nor starve sibling streams sharing this worker.
|
|
246
|
-
|
|
157
|
+
dispatch(streamId, msg) {
|
|
247
158
|
try {
|
|
248
159
|
this.handlers.get(streamId)?.(msg);
|
|
249
160
|
} catch (e) {
|
|
250
|
-
// eslint-disable-next-line no-console
|
|
251
161
|
console.error("flux: stream message handler threw", e);
|
|
252
162
|
}
|
|
253
163
|
}
|
|
254
164
|
}
|
|
255
|
-
|
|
256
|
-
function poolCap(): number {
|
|
165
|
+
function poolCap() {
|
|
257
166
|
const hc = typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 0;
|
|
258
167
|
return Math.min(hc || 4, 8);
|
|
259
168
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
/** The process-wide default pool every `FluxClient` shares unless given one. */
|
|
264
|
-
export function getDefaultPool(): FluxPool {
|
|
169
|
+
let defaultPool = null;
|
|
170
|
+
function getDefaultPool() {
|
|
265
171
|
if (!defaultPool) {
|
|
266
172
|
defaultPool = new FluxPool(
|
|
267
|
-
() => new Worker(new URL("./worker.
|
|
268
|
-
poolCap()
|
|
173
|
+
() => new Worker(new URL("./worker.js", import.meta.url), { type: "module" }),
|
|
174
|
+
poolCap()
|
|
269
175
|
);
|
|
270
176
|
}
|
|
271
177
|
return defaultPool;
|
|
272
178
|
}
|
|
273
|
-
|
|
274
|
-
/** TEST-ONLY: drop the process-wide default pool so the next {@link getDefaultPool}
|
|
275
|
-
* rebuilds it (lazily, with the current global `Worker`). Lets a test file that
|
|
276
|
-
* drives the default pool start from a clean, deterministic state regardless of
|
|
277
|
-
* which other file warmed it first in bun's shared test process. Not part of the
|
|
278
|
-
* public API and a no-op for normal runtime use. */
|
|
279
|
-
export function __resetDefaultPool(): void {
|
|
179
|
+
function __resetDefaultPool() {
|
|
280
180
|
defaultPool = null;
|
|
281
181
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
* - subscribe(listener): for React's useSyncExternalStore
|
|
294
|
-
* - getSnapshot(): the current ordered list of blocks
|
|
295
|
-
* - getMetrics(): per-stream perf metrics
|
|
296
|
-
*
|
|
297
|
-
* Mutation methods:
|
|
298
|
-
* - append(chunk): forward to the worker
|
|
299
|
-
* - finalize(): mark the stream done
|
|
300
|
-
* - reset(): start fresh
|
|
301
|
-
*/
|
|
302
|
-
export class FluxClient {
|
|
303
|
-
private pool: FluxPool;
|
|
304
|
-
private pw: PoolWorker | null = null;
|
|
305
|
-
private streamId = 0;
|
|
306
|
-
private config?: ParserConfig;
|
|
307
|
-
private configSent = false;
|
|
308
|
-
private listeners = new Set<() => void>();
|
|
309
|
-
private store: BlockStore = emptyBlockStore();
|
|
310
|
-
private onError?: (err: { message: string; fatal?: boolean }) => void;
|
|
311
|
-
private onBlock?: (block: Block) => void;
|
|
312
|
-
private attached = true;
|
|
182
|
+
class FluxClient {
|
|
183
|
+
pool;
|
|
184
|
+
pw = null;
|
|
185
|
+
streamId = 0;
|
|
186
|
+
config;
|
|
187
|
+
configSent = false;
|
|
188
|
+
listeners = /* @__PURE__ */ new Set();
|
|
189
|
+
store = emptyBlockStore();
|
|
190
|
+
onError;
|
|
191
|
+
onBlock;
|
|
192
|
+
attached = true;
|
|
313
193
|
// Diff baseline for setContent(): the full string fed in so far, and whether
|
|
314
194
|
// it has been finalized. Cleared by reset()/reattach() (the worker drops the
|
|
315
195
|
// parser there, so the baseline is stale and the document must be re-fed).
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
196
|
+
lastContent = "";
|
|
197
|
+
contentDone = false;
|
|
319
198
|
// Opt-in rAF coalescing (see constructor `coalesce`). When on AND
|
|
320
199
|
// requestAnimationFrame exists, intra-frame emit()s collapse into ONE
|
|
321
200
|
// rAF-scheduled flush to listeners — the React useSyncExternalStore path then
|
|
@@ -324,26 +203,29 @@ export class FluxClient {
|
|
|
324
203
|
// reference-stable, so a dropped intermediate notify only skips a tail-only
|
|
325
204
|
// render that the next flush supersedes. The finalize/done patch is exempt and
|
|
326
205
|
// flushes synchronously, never deferred to the next frame.
|
|
327
|
-
|
|
328
|
-
|
|
206
|
+
coalesce = false;
|
|
207
|
+
rafHandle = null;
|
|
329
208
|
// Set by finalize(); the next patch's emit flushes synchronously (a 'done'
|
|
330
|
-
// notification must not be deferred a frame) and clears it.
|
|
331
|
-
|
|
332
|
-
|
|
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.
|
|
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;
|
|
333
216
|
// Perf
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
217
|
+
appendedBytes = 0;
|
|
218
|
+
patchCount = 0;
|
|
219
|
+
totalParseMicros = 0;
|
|
220
|
+
lastPatchMs = 0;
|
|
221
|
+
firstAppendMs = 0;
|
|
222
|
+
retainedBytes = 0;
|
|
223
|
+
wasmMemoryBytes = 0;
|
|
341
224
|
// Render-path observability (advanced ONLY when an onRenderMetrics hook is
|
|
342
225
|
// wired into a renderer; zero-cost otherwise). renderCount = React BlockView
|
|
343
226
|
// body renders; rebuildCount = DOM node rebuilds.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
227
|
+
renderCount = 0;
|
|
228
|
+
rebuildCount = 0;
|
|
347
229
|
/**
|
|
348
230
|
* @param options.pool worker pool to join (defaults to the shared
|
|
349
231
|
* process-wide pool — pass a dedicated `FluxPool` only for isolation).
|
|
@@ -365,22 +247,13 @@ export class FluxClient {
|
|
|
365
247
|
* pending frame is cancelled on `reset()`/`destroy()`. No effect when
|
|
366
248
|
* `requestAnimationFrame` is unavailable (e.g. SSR) — emits stay synchronous.
|
|
367
249
|
*/
|
|
368
|
-
constructor(
|
|
369
|
-
options: {
|
|
370
|
-
pool?: FluxPool;
|
|
371
|
-
config?: ParserConfig;
|
|
372
|
-
onError?: (err: { message: string; fatal?: boolean }) => void;
|
|
373
|
-
onBlock?: (block: Block) => void;
|
|
374
|
-
coalesce?: boolean;
|
|
375
|
-
} = {},
|
|
376
|
-
) {
|
|
250
|
+
constructor(options = {}) {
|
|
377
251
|
this.pool = options.pool ?? getDefaultPool();
|
|
378
252
|
this.config = options.config;
|
|
379
253
|
this.onError = options.onError;
|
|
380
254
|
this.onBlock = options.onBlock;
|
|
381
255
|
this.coalesce = options.coalesce ?? false;
|
|
382
256
|
}
|
|
383
|
-
|
|
384
257
|
/**
|
|
385
258
|
* Lazily reserve this client's stream id and bind it to a pool worker. The
|
|
386
259
|
* SOLE place that calls pool.acquire() — so the worker is created on the FIRST
|
|
@@ -396,46 +269,39 @@ export class FluxClient {
|
|
|
396
269
|
* necessarily owns the lowest streamId. This affects neither the pool cap nor
|
|
397
270
|
* multiplexing (pick() is unchanged and remains the only path to create()).
|
|
398
271
|
*/
|
|
399
|
-
|
|
400
|
-
if (this.pw) return this.pw;
|
|
272
|
+
ensureAcquired() {
|
|
273
|
+
if (this.pw && !this.pw.failed) return this.pw;
|
|
274
|
+
this.pw = null;
|
|
401
275
|
const { streamId, pw } = this.pool.acquire((msg) => this.onMessage(msg));
|
|
402
276
|
this.streamId = streamId;
|
|
403
277
|
this.pw = pw;
|
|
404
278
|
return pw;
|
|
405
279
|
}
|
|
406
|
-
|
|
407
|
-
get ready(): boolean {
|
|
280
|
+
get ready() {
|
|
408
281
|
return this.pw?.ready ?? false;
|
|
409
282
|
}
|
|
410
|
-
|
|
411
|
-
whenReady(): Promise<void> {
|
|
283
|
+
whenReady() {
|
|
412
284
|
const pw = this.ensureAcquired();
|
|
413
285
|
return this.pool.whenWorkerReady(pw);
|
|
414
286
|
}
|
|
415
|
-
|
|
416
287
|
// The config rides on the first message a stream sends; the worker applies it
|
|
417
288
|
// when it creates the parser. postMessage is FIFO per worker, so it always
|
|
418
289
|
// lands before any append is processed. Returns undefined after the first use.
|
|
419
|
-
|
|
420
|
-
if (this.configSent || !this.config) return
|
|
290
|
+
firstConfig() {
|
|
291
|
+
if (this.configSent || !this.config) return void 0;
|
|
421
292
|
this.configSent = true;
|
|
422
293
|
return this.config;
|
|
423
294
|
}
|
|
424
|
-
|
|
425
|
-
append(chunk: string) {
|
|
295
|
+
append(chunk) {
|
|
426
296
|
const pw = this.ensureAcquired();
|
|
427
297
|
if (this.firstAppendMs === 0) this.firstAppendMs = performance.now();
|
|
428
|
-
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 });
|
|
429
299
|
}
|
|
430
|
-
|
|
431
300
|
finalize() {
|
|
432
301
|
const pw = this.ensureAcquired();
|
|
433
|
-
// The terminal patch this triggers must reach subscribers synchronously, not
|
|
434
|
-
// a frame late — mark it so the next emit flushes now even under coalescing.
|
|
435
302
|
this.finalizePending = true;
|
|
436
|
-
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 });
|
|
437
304
|
}
|
|
438
|
-
|
|
439
305
|
/**
|
|
440
306
|
* Pipe a source straight in: read it to completion, `append()` each chunk,
|
|
441
307
|
* then `finalize()`. The LLM-native path — e.g.
|
|
@@ -453,60 +319,46 @@ export class FluxClient {
|
|
|
453
319
|
* finalized (or cleanly on abort); rejects if the source itself errors.
|
|
454
320
|
* Browser-only for byte sources (uses `TextDecoder`).
|
|
455
321
|
*/
|
|
456
|
-
async pipeFrom(
|
|
457
|
-
source: ReadableStream<Uint8Array> | Response | AsyncIterable<string>,
|
|
458
|
-
opts?: { signal?: AbortSignal },
|
|
459
|
-
): Promise<void> {
|
|
322
|
+
async pipeFrom(source, opts) {
|
|
460
323
|
const signal = opts?.signal;
|
|
461
|
-
|
|
462
|
-
if (signal?.aborted) return; // already superseded before we started
|
|
463
|
-
|
|
464
|
-
// AsyncIterable<string> (SSE deltas, generators). Detected by elimination:
|
|
465
|
-
// a ReadableStream has `getReader`, a Response has `body` — neither here.
|
|
324
|
+
if (signal?.aborted) return;
|
|
466
325
|
if (!("getReader" in source) && !("body" in source)) {
|
|
467
|
-
for await (const chunk of source
|
|
468
|
-
if (signal?.aborted) return;
|
|
326
|
+
for await (const chunk of source) {
|
|
327
|
+
if (signal?.aborted) return;
|
|
469
328
|
this.append(chunk);
|
|
470
329
|
}
|
|
471
330
|
if (!signal?.aborted) this.finalize();
|
|
472
331
|
return;
|
|
473
332
|
}
|
|
474
|
-
|
|
475
|
-
// Byte source: a Response (use its body) or a ReadableStream directly.
|
|
476
333
|
const body = "body" in source ? source.body : source;
|
|
477
334
|
if (!body) {
|
|
478
|
-
// An empty Response body (e.g. 204) is a completed, empty stream.
|
|
479
335
|
this.finalize();
|
|
480
336
|
return;
|
|
481
337
|
}
|
|
482
338
|
const reader = body.getReader();
|
|
483
|
-
// A pending read() can't observe `aborted` until the next chunk; cancel()
|
|
484
|
-
// on abort tears down the upstream and resolves the pending read so the
|
|
485
|
-
// loop's post-read check fires and bails without finalizing.
|
|
486
339
|
const onAbort = () => {
|
|
487
|
-
reader.cancel().catch(() => {
|
|
340
|
+
reader.cancel().catch(() => {
|
|
341
|
+
});
|
|
488
342
|
};
|
|
489
343
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
490
344
|
const decoder = new TextDecoder();
|
|
491
345
|
try {
|
|
492
|
-
for (
|
|
346
|
+
for (; ; ) {
|
|
493
347
|
const { done, value } = await reader.read();
|
|
494
|
-
if (signal?.aborted) return;
|
|
348
|
+
if (signal?.aborted) return;
|
|
495
349
|
if (done) break;
|
|
496
350
|
if (value) this.append(decoder.decode(value, { stream: true }));
|
|
497
351
|
}
|
|
498
|
-
this.append(decoder.decode());
|
|
352
|
+
this.append(decoder.decode());
|
|
499
353
|
this.finalize();
|
|
500
354
|
} finally {
|
|
501
355
|
signal?.removeEventListener("abort", onAbort);
|
|
502
356
|
try {
|
|
503
357
|
reader.releaseLock();
|
|
504
358
|
} catch {
|
|
505
|
-
/* already released (e.g. by cancel) */
|
|
506
359
|
}
|
|
507
360
|
}
|
|
508
361
|
}
|
|
509
|
-
|
|
510
362
|
/**
|
|
511
363
|
* Drive the parser from a CONTROLLED full string instead of manual appends.
|
|
512
364
|
* Pass the whole document-so-far each time; setContent diffs it against the
|
|
@@ -532,17 +384,12 @@ export class FluxClient {
|
|
|
532
384
|
* here (it forces a reparse each tick); do that enrichment at render time via
|
|
533
385
|
* `components` instead, keeping the source append-only.
|
|
534
386
|
*/
|
|
535
|
-
setContent(content
|
|
387
|
+
setContent(content, opts) {
|
|
536
388
|
if (content !== this.lastContent) {
|
|
537
|
-
// Fast path appends the delta into the EXISTING parser — but a parser that
|
|
538
|
-
// was already finalized ({ done: true }) is terminal: the core drops any
|
|
539
|
-
// further append. So gate the fast path on !contentDone; reopening a
|
|
540
|
-
// finalized stream (or any divergence) falls through to reset()+reparse,
|
|
541
|
-
// which frees the dead parser and rebuilds a fresh one.
|
|
542
389
|
if (!this.contentDone && content.startsWith(this.lastContent)) {
|
|
543
390
|
this.append(content.slice(this.lastContent.length));
|
|
544
391
|
} else {
|
|
545
|
-
this.reset();
|
|
392
|
+
this.reset();
|
|
546
393
|
this.append(content);
|
|
547
394
|
}
|
|
548
395
|
this.lastContent = content;
|
|
@@ -553,12 +400,7 @@ export class FluxClient {
|
|
|
553
400
|
this.contentDone = true;
|
|
554
401
|
}
|
|
555
402
|
}
|
|
556
|
-
|
|
557
403
|
reset() {
|
|
558
|
-
// Only notify subscribers if there was content to clear: resetting an
|
|
559
|
-
// already-empty store leaves the view empty either way, so skip the no-op
|
|
560
|
-
// emit (which would otherwise drive every subscriber through a wasted,
|
|
561
|
-
// output-identical render pass).
|
|
562
404
|
const hadContent = this.store.snapshot.length > 0;
|
|
563
405
|
this.store = emptyBlockStore();
|
|
564
406
|
this.appendedBytes = 0;
|
|
@@ -568,33 +410,23 @@ export class FluxClient {
|
|
|
568
410
|
this.firstAppendMs = 0;
|
|
569
411
|
this.retainedBytes = 0;
|
|
570
412
|
this.wasmMemoryBytes = 0;
|
|
571
|
-
this.lastContent = "";
|
|
413
|
+
this.lastContent = "";
|
|
572
414
|
this.contentDone = false;
|
|
573
|
-
// A frame coalesced from the just-cleared stream is now stale — cancel it so
|
|
574
|
-
// it can't fire a notify referencing content reset() just dropped.
|
|
575
415
|
this.cancelFrame();
|
|
576
416
|
this.finalizePending = false;
|
|
577
|
-
|
|
417
|
+
this.epoch += 1;
|
|
578
418
|
const pw = this.ensureAcquired();
|
|
579
|
-
this.pool.send(pw, { type: "reset", streamId: this.streamId });
|
|
580
|
-
if (hadContent) this.emit(true);
|
|
419
|
+
this.pool.send(pw, { type: "reset", streamId: this.streamId, epoch: this.epoch });
|
|
420
|
+
if (hadContent) this.emit(true);
|
|
581
421
|
}
|
|
582
|
-
|
|
583
422
|
destroy() {
|
|
584
|
-
if (!this.attached) return;
|
|
585
|
-
// Free this stream's parser; the shared worker stays warm for siblings.
|
|
586
|
-
// Only release a real slot — a never-acquired client (constructed during an
|
|
587
|
-
// SSR render then unmounted) has no pool slot to free, so skip the call.
|
|
588
|
-
// We deliberately do NOT null this.pw here: StrictMode's destroy()→reattach()
|
|
589
|
-
// on the SAME instance needs the same pw/streamId to re-register.
|
|
423
|
+
if (!this.attached) return;
|
|
590
424
|
if (this.pw) this.pool.release(this.streamId, this.pw);
|
|
591
|
-
// Drop any pending coalesced frame so it can't fire into cleared listeners.
|
|
592
425
|
this.cancelFrame();
|
|
593
426
|
this.finalizePending = false;
|
|
594
427
|
this.listeners.clear();
|
|
595
428
|
this.attached = false;
|
|
596
429
|
}
|
|
597
|
-
|
|
598
430
|
/**
|
|
599
431
|
* Re-register with the pool after {@link destroy} so the client receives
|
|
600
432
|
* patches again. Needed only for React StrictMode's dev double-mount, where
|
|
@@ -603,61 +435,46 @@ export class FluxClient {
|
|
|
603
435
|
*/
|
|
604
436
|
reattach() {
|
|
605
437
|
if (this.attached) return;
|
|
606
|
-
// The prior destroy()→dispose dropped this stream's parser, so setContent's
|
|
607
|
-
// diff baseline is stale — clear it so the next setContent re-feeds the whole
|
|
608
|
-
// document (StrictMode dev double-mount on the SAME instance).
|
|
609
438
|
this.lastContent = "";
|
|
610
439
|
this.contentDone = false;
|
|
611
440
|
if (!this.pw) {
|
|
612
|
-
// Never acquired (e.g. constructed during SSR, first real mount on client).
|
|
613
|
-
// No prior pool slot to re-register; just mark attached. The next
|
|
614
|
-
// worker-bound op acquires lazily. configSent is already false, so the
|
|
615
|
-
// first append will carry config exactly as a brand-new client would.
|
|
616
441
|
this.attached = true;
|
|
617
442
|
return;
|
|
618
443
|
}
|
|
619
444
|
this.pool.reattach(this.streamId, this.pw, (msg) => this.onMessage(msg));
|
|
620
445
|
this.attached = true;
|
|
621
|
-
// The worker discarded this stream's config on `dispose` (unlike `reset`,
|
|
622
|
-
// which keeps it), so re-send it on the next message — otherwise the parser
|
|
623
|
-
// would be rebuilt with library defaults (gfmMath / componentTags / … lost).
|
|
624
446
|
this.configSent = false;
|
|
625
447
|
}
|
|
626
|
-
|
|
627
|
-
subscribe = (fn: () => void) => {
|
|
448
|
+
subscribe = (fn) => {
|
|
628
449
|
this.listeners.add(fn);
|
|
629
450
|
return () => this.listeners.delete(fn);
|
|
630
451
|
};
|
|
631
|
-
|
|
632
|
-
getSnapshot = (): Block[] => this.store.snapshot;
|
|
633
|
-
|
|
452
|
+
getSnapshot = () => this.store.snapshot;
|
|
634
453
|
/**
|
|
635
454
|
* Internal: a renderer with an `onRenderMetrics` hook calls this once per
|
|
636
455
|
* actual React block render so `getMetrics().renderCount` aggregates churn.
|
|
637
456
|
* No-op cost when no hook is wired (it is simply never called). Not part of
|
|
638
457
|
* the public API surface — the underscore marks it renderer-internal.
|
|
639
458
|
*/
|
|
640
|
-
__noteRender()
|
|
459
|
+
__noteRender() {
|
|
641
460
|
this.renderCount++;
|
|
642
461
|
}
|
|
643
|
-
|
|
644
462
|
/**
|
|
645
463
|
* Internal: the DOM renderer calls this once per actual node rebuild (the
|
|
646
464
|
* changed-block branch) when an `onRenderMetrics` hook is wired, so
|
|
647
465
|
* `getMetrics().rebuildCount` aggregates churn. Never called without a hook.
|
|
648
466
|
*/
|
|
649
|
-
__noteRebuild()
|
|
467
|
+
__noteRebuild() {
|
|
650
468
|
this.rebuildCount++;
|
|
651
469
|
}
|
|
652
|
-
|
|
653
470
|
getMetrics() {
|
|
654
471
|
const elapsed = this.firstAppendMs ? Math.max(1, performance.now() - this.firstAppendMs) : 1;
|
|
655
472
|
return {
|
|
656
473
|
bytes: this.appendedBytes,
|
|
657
474
|
patches: this.patchCount,
|
|
658
475
|
meanParseMicros: this.patchCount > 0 ? this.totalParseMicros / this.patchCount : 0,
|
|
659
|
-
totalParseMs: this.totalParseMicros /
|
|
660
|
-
throughputKBs:
|
|
476
|
+
totalParseMs: this.totalParseMicros / 1e3,
|
|
477
|
+
throughputKBs: this.appendedBytes / 1024 / (elapsed / 1e3),
|
|
661
478
|
committedBlocks: this.store.committed.size,
|
|
662
479
|
activeBlocks: this.store.active.length,
|
|
663
480
|
lastPatchAgoMs: this.lastPatchMs === 0 ? 0 : performance.now() - this.lastPatchMs,
|
|
@@ -670,31 +487,25 @@ export class FluxClient {
|
|
|
670
487
|
// renderer): renderCount = React block-body renders, rebuildCount = DOM
|
|
671
488
|
// node rebuilds. Committed blocks memo-skip, so they contribute once.
|
|
672
489
|
renderCount: this.renderCount,
|
|
673
|
-
rebuildCount: this.rebuildCount
|
|
490
|
+
rebuildCount: this.rebuildCount
|
|
674
491
|
};
|
|
675
492
|
}
|
|
676
|
-
|
|
677
493
|
/**
|
|
678
494
|
* A heading outline of the current snapshot (committed + active), in document
|
|
679
495
|
* order — for a table of contents. Works mid-stream; entries appear as their
|
|
680
496
|
* headings stream in. The `id` is stable, so a built ToC won't re-key.
|
|
681
497
|
*/
|
|
682
|
-
outline()
|
|
683
|
-
const out
|
|
498
|
+
outline() {
|
|
499
|
+
const out = [];
|
|
684
500
|
for (const b of this.store.snapshot) {
|
|
685
501
|
if (b.kind.type === "Heading") {
|
|
686
|
-
|
|
687
|
-
// `{ level, text, id }` object when on — accept both. `OutlineEntry.id`
|
|
688
|
-
// stays the numeric block id (stable, non-breaking); the anchor slug is
|
|
689
|
-
// reachable additively via `kind.data.id` for consumers who want it.
|
|
690
|
-
const d = b.kind.data as number | { level?: number } | undefined;
|
|
502
|
+
const d = b.kind.data;
|
|
691
503
|
const level = typeof d === "number" ? d : d?.level ?? 1;
|
|
692
504
|
out.push({ level, text: htmlToText(b.html), id: b.id });
|
|
693
505
|
}
|
|
694
506
|
}
|
|
695
507
|
return out;
|
|
696
508
|
}
|
|
697
|
-
|
|
698
509
|
/**
|
|
699
510
|
* The rendered document as plain text — tags stripped, entities decoded,
|
|
700
511
|
* blocks separated by blank lines. Derived from the rendered HTML (the source
|
|
@@ -702,48 +513,51 @@ export class FluxClient {
|
|
|
702
513
|
* readable approximation for search indexing / summaries, not a round-trip of
|
|
703
514
|
* the original source.
|
|
704
515
|
*/
|
|
705
|
-
toPlaintext()
|
|
706
|
-
const parts
|
|
516
|
+
toPlaintext() {
|
|
517
|
+
const parts = [];
|
|
707
518
|
for (const b of this.store.snapshot) {
|
|
708
519
|
const t = htmlToText(b.html);
|
|
709
520
|
if (t) parts.push(t);
|
|
710
521
|
}
|
|
711
522
|
return parts.join("\n\n");
|
|
712
523
|
}
|
|
713
|
-
|
|
714
|
-
private onMessage(msg: FromWorker) {
|
|
524
|
+
onMessage(msg) {
|
|
715
525
|
switch (msg.type) {
|
|
716
|
-
case "patch":
|
|
717
|
-
|
|
526
|
+
case "patch": {
|
|
527
|
+
if (msg.epoch !== void 0 && msg.epoch < this.epoch) break;
|
|
528
|
+
let patch;
|
|
529
|
+
try {
|
|
530
|
+
patch = JSON.parse(msg.patch);
|
|
531
|
+
} catch (e) {
|
|
532
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
533
|
+
if (this.onError) this.onError({ message: `flux: malformed patch (${message})` });
|
|
534
|
+
else console.error("flux: malformed patch:", message);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
applyPatch(this.store, patch);
|
|
718
538
|
this.appendedBytes = msg.appendedBytes;
|
|
719
539
|
this.totalParseMicros += msg.parseMicros;
|
|
720
540
|
this.retainedBytes = msg.retainedBytes;
|
|
721
541
|
this.wasmMemoryBytes = msg.wasmMemoryBytes;
|
|
722
542
|
this.patchCount += 1;
|
|
723
543
|
this.lastPatchMs = performance.now();
|
|
724
|
-
|
|
725
|
-
// stream completion to the next frame. One-shot: cleared after use.
|
|
726
|
-
const sync = this.finalizePending;
|
|
544
|
+
const sync = this.finalizePending || msg.final === true;
|
|
727
545
|
this.finalizePending = false;
|
|
728
546
|
this.emit(sync);
|
|
729
|
-
// After subscribers see the new snapshot, fire the per-block hook for
|
|
730
|
-
// anything that just committed (document order). A throw here is
|
|
731
|
-
// isolated by the pool's dispatch boundary and won't skip emit().
|
|
732
547
|
if (this.onBlock) {
|
|
733
|
-
for (const b of
|
|
548
|
+
for (const b of patch.newly_committed) this.onBlock(b);
|
|
734
549
|
}
|
|
735
550
|
break;
|
|
551
|
+
}
|
|
736
552
|
case "error":
|
|
737
553
|
if (this.onError) {
|
|
738
554
|
this.onError({ message: msg.message, fatal: msg.fatal });
|
|
739
555
|
} else {
|
|
740
|
-
// eslint-disable-next-line no-console
|
|
741
556
|
console.error("flux worker error:", msg.message);
|
|
742
557
|
}
|
|
743
558
|
break;
|
|
744
559
|
}
|
|
745
560
|
}
|
|
746
|
-
|
|
747
561
|
/**
|
|
748
562
|
* Notify subscribers of a new snapshot.
|
|
749
563
|
*
|
|
@@ -753,29 +567,33 @@ export class FluxClient {
|
|
|
753
567
|
* into one notify. `sync` forces an immediate flush (stream completion / reset)
|
|
754
568
|
* and cancels any frame already pending so the snapshot is delivered once.
|
|
755
569
|
*/
|
|
756
|
-
|
|
570
|
+
emit(sync = false) {
|
|
757
571
|
if (this.coalesce && !sync && typeof requestAnimationFrame === "function") {
|
|
758
|
-
if (this.rafHandle !== null) return;
|
|
572
|
+
if (this.rafHandle !== null) return;
|
|
759
573
|
this.rafHandle = requestAnimationFrame(() => {
|
|
760
574
|
this.rafHandle = null;
|
|
761
575
|
this.flushNow();
|
|
762
576
|
});
|
|
763
577
|
return;
|
|
764
578
|
}
|
|
765
|
-
// Synchronous path: drop any pending frame so its notify can't double-fire
|
|
766
|
-
// after this one, then flush immediately.
|
|
767
579
|
this.cancelFrame();
|
|
768
580
|
this.flushNow();
|
|
769
581
|
}
|
|
770
|
-
|
|
771
|
-
private flushNow() {
|
|
582
|
+
flushNow() {
|
|
772
583
|
for (const fn of this.listeners) fn();
|
|
773
584
|
}
|
|
774
|
-
|
|
775
|
-
private cancelFrame() {
|
|
585
|
+
cancelFrame() {
|
|
776
586
|
if (this.rafHandle !== null) {
|
|
777
587
|
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.rafHandle);
|
|
778
588
|
this.rafHandle = null;
|
|
779
589
|
}
|
|
780
590
|
}
|
|
781
591
|
}
|
|
592
|
+
export {
|
|
593
|
+
FluxClient,
|
|
594
|
+
FluxPool,
|
|
595
|
+
__resetDefaultPool,
|
|
596
|
+
applyPatch,
|
|
597
|
+
emptyBlockStore,
|
|
598
|
+
getDefaultPool
|
|
599
|
+
};
|