flux-md 0.20.0 → 0.20.2
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 +9 -4
- package/dist/client.d.ts +57 -1
- package/dist/client.js +157 -7
- package/dist/wasm/flux_md_core_bg.wasm +0 -0
- package/dist/worker-core.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,7 +156,12 @@ use it from any binding.
|
|
|
156
156
|
> append-only so parsing stays incremental. Re-transforming the *whole* string
|
|
157
157
|
> each token (so earlier bytes change) forces `setContent` to reparse every tick
|
|
158
158
|
> (O(n²)); that's what render-time overrides avoid. `setContent`'s reset path is
|
|
159
|
-
> for the **once**-at-the-end reprocess swap, not per-token rewrites.
|
|
159
|
+
> for the **once**-at-the-end reprocess swap, not per-token rewrites. That swap
|
|
160
|
+
> is seamless: the current view stays on screen while the new string reparses —
|
|
161
|
+
> the document never blanks, scroll never moves, and blocks whose rendered
|
|
162
|
+
> content is unchanged keep their identity (and React keys), so only genuinely
|
|
163
|
+
> changed blocks re-render. (`setContent("")` is an explicit clear and resets
|
|
164
|
+
> immediately.)
|
|
160
165
|
|
|
161
166
|
<details>
|
|
162
167
|
<summary>Full manual control (caller-owned client)</summary>
|
|
@@ -552,9 +557,9 @@ class FluxClient {
|
|
|
552
557
|
): Promise<void>;
|
|
553
558
|
finalize(): void; // mark stream complete
|
|
554
559
|
setContent( // drive from a controlled full string
|
|
555
|
-
full: string, // diffs vs last: prefix → append delta; else
|
|
556
|
-
opts?: { done?: boolean }, //
|
|
557
|
-
): void;
|
|
560
|
+
full: string, // diffs vs last: prefix → append delta; else seamless
|
|
561
|
+
opts?: { done?: boolean }, // reset+reparse (view held, unchanged blocks keep identity)
|
|
562
|
+
): void; // done:true → finalize
|
|
558
563
|
reset(): void; // wipe and reuse
|
|
559
564
|
destroy(): void; // free this stream's parser
|
|
560
565
|
whenReady(): Promise<void>; // resolves once WASM loaded; rejects on init failure
|
package/dist/client.d.ts
CHANGED
|
@@ -133,6 +133,10 @@ export declare class FluxClient {
|
|
|
133
133
|
private rafHandle;
|
|
134
134
|
private finalizePending;
|
|
135
135
|
private epoch;
|
|
136
|
+
private staleSnapshot;
|
|
137
|
+
private staleTrimmed;
|
|
138
|
+
private idNamespace;
|
|
139
|
+
private mergeCache;
|
|
136
140
|
private appendedBytes;
|
|
137
141
|
private patchCount;
|
|
138
142
|
private totalParseMicros;
|
|
@@ -154,6 +158,10 @@ export declare class FluxClient {
|
|
|
154
158
|
* order, after the store updates) — for side effects like lazily
|
|
155
159
|
* highlighting a finished code block or analytics. A committed block never
|
|
156
160
|
* re-fires; the streaming tail does not (subscribe for live tail updates).
|
|
161
|
+
* NOTE: this is a PARSER-commit hook — the block carries the parser's raw
|
|
162
|
+
* id. During a setContent divergence swap the rendered view may show that
|
|
163
|
+
* block under a different id (an adopted old id, or a namespaced one), so
|
|
164
|
+
* correlate with rendered blocks via subscribe()+getSnapshot(), not this id.
|
|
157
165
|
* @param options.coalesce opt-in (default `false`): collapse multiple
|
|
158
166
|
* intra-frame patch notifications into ONE `requestAnimationFrame`-scheduled
|
|
159
167
|
* flush to subscribers, so a React `useSyncExternalStore` consumer renders at
|
|
@@ -221,7 +229,12 @@ export declare class FluxClient {
|
|
|
221
229
|
* - **prefix-extension** (the streaming-growth case) → append only the new
|
|
222
230
|
* suffix, so committed blocks stay put and only the active tail re-parses;
|
|
223
231
|
* - **any other change** (e.g. a finished stream swapped for a re-processed
|
|
224
|
-
* final string) →
|
|
232
|
+
* final string) → reset + reparse the whole new string, keeping the
|
|
233
|
+
* current view on screen until the reparse lands: the document never
|
|
234
|
+
* blanks, scroll never moves, and blocks whose rendered content is
|
|
235
|
+
* unchanged keep their identity (and React keys) so only genuinely
|
|
236
|
+
* changed blocks re-render. An empty new string is an explicit clear and
|
|
237
|
+
* hard-resets immediately.
|
|
225
238
|
*
|
|
226
239
|
* This is the first-class bridge for UIs that hold a streaming message as a
|
|
227
240
|
* single growing string prop (the common React shape) — no hand-rolled diff,
|
|
@@ -243,6 +256,27 @@ export declare class FluxClient {
|
|
|
243
256
|
done?: boolean;
|
|
244
257
|
}): void;
|
|
245
258
|
reset(): void;
|
|
259
|
+
/**
|
|
260
|
+
* setContent's divergence reset: rebuild the parser exactly like {@link reset},
|
|
261
|
+
* but keep `preserve` (the currently displayed view) on screen while the new
|
|
262
|
+
* content reparses. No notify fires here — subscribers keep reading the same
|
|
263
|
+
* snapshot reference until the first new-generation patch merges over it, so
|
|
264
|
+
* the swap is seamless: no empty frame, no container collapse, no scroll
|
|
265
|
+
* clamp, and blocks whose content survives the reprocess never re-render.
|
|
266
|
+
*/
|
|
267
|
+
private softReset;
|
|
268
|
+
/**
|
|
269
|
+
* Finish a preserved-view divergence swap by making the merged view THE
|
|
270
|
+
* store: adopted ids become the committed keys, the merged array becomes the
|
|
271
|
+
* snapshot, and every scrap of merge state drops. From here getSnapshot() is
|
|
272
|
+
* a plain field read again (zero steady-state overhead) and the superseded
|
|
273
|
+
* generation's blocks are garbage — only one document stays in memory.
|
|
274
|
+
* Runs on the terminal (final) patch, which commits everything — if anything
|
|
275
|
+
* is somehow still open, the lazy merge simply stays live instead (the
|
|
276
|
+
* incremental reuse keeps it linear).
|
|
277
|
+
*/
|
|
278
|
+
private collapseStale;
|
|
279
|
+
private resetParser;
|
|
246
280
|
destroy(): void;
|
|
247
281
|
/**
|
|
248
282
|
* Re-register with the pool after {@link destroy} so the client receives
|
|
@@ -253,6 +287,28 @@ export declare class FluxClient {
|
|
|
253
287
|
reattach(): void;
|
|
254
288
|
subscribe: (fn: () => void) => () => boolean;
|
|
255
289
|
getSnapshot: () => Block[];
|
|
290
|
+
/**
|
|
291
|
+
* Positional merge of the preserved pre-divergence view over the rebuilding
|
|
292
|
+
* store (see {@link softReset}). Per position:
|
|
293
|
+
* - identical committed block (html + kind + open + speculative) → the OLD
|
|
294
|
+
* block object, so its id and reference survive the swap and the block
|
|
295
|
+
* never re-renders (blocksEqual / the DOM keyed reconcile hold);
|
|
296
|
+
* - changed committed block → the new block, its id offset into this
|
|
297
|
+
* generation's namespace (adopted old ids and raw new ids could collide);
|
|
298
|
+
* - still-open block over old content → the old block (never a shrinking
|
|
299
|
+
* partial where complete content was already on screen); past the old
|
|
300
|
+
* document's end the live tail streams in as-is;
|
|
301
|
+
* - position the reparse hasn't reached → the old block, until the terminal
|
|
302
|
+
* patch sets `staleTrimmed` and the view clamps to the new length.
|
|
303
|
+
*
|
|
304
|
+
* LINEARITY: committed blocks are reference-stable across patches (the store
|
|
305
|
+
* contract), so a base entry pointer-equal to the previous merge's reproduces
|
|
306
|
+
* its previous decision without re-running the O(html) equality compare. Each
|
|
307
|
+
* block is string-compared exactly once — when it first commits — keeping a
|
|
308
|
+
* long post-divergence stream linear instead of quadratic. Only the active
|
|
309
|
+
* tail (fresh references each patch, small by design) re-compares per patch.
|
|
310
|
+
*/
|
|
311
|
+
private mergeStale;
|
|
256
312
|
/**
|
|
257
313
|
* Internal: a renderer with an `onRenderMetrics` hook calls this once per
|
|
258
314
|
* actual React block render so `getMetrics().renderCount` aggregates churn.
|
package/dist/client.js
CHANGED
|
@@ -162,6 +162,7 @@ class FluxPool {
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
const ID_NAMESPACE_STRIDE = 4294967296;
|
|
165
166
|
function poolCap() {
|
|
166
167
|
const hc = typeof navigator !== "undefined" ? navigator.hardwareConcurrency : 0;
|
|
167
168
|
return Math.min(hc || 4, 8);
|
|
@@ -213,6 +214,29 @@ class FluxClient {
|
|
|
213
214
|
// echoed back on each patch; a patch whose epoch is older than this is a
|
|
214
215
|
// pre-reset straggler and is dropped before it can repopulate the cleared store.
|
|
215
216
|
epoch = 0;
|
|
217
|
+
// --- Preserved-view divergence swap (setContent's reset+reparse path) ---
|
|
218
|
+
// The displayed snapshot captured by softReset(). While set, getSnapshot()
|
|
219
|
+
// returns a positional merge of this view over the rebuilding store, so the
|
|
220
|
+
// document never blanks out during a divergence reparse and unchanged blocks
|
|
221
|
+
// keep their object identity AND id (React key / DOM node key) across the
|
|
222
|
+
// swap. It persists after the reparse completes — dropping it would revert
|
|
223
|
+
// the adopted ids and remount every block one notify later.
|
|
224
|
+
staleSnapshot = null;
|
|
225
|
+
// Set when the reparse's terminal (final) patch lands: the merge stops
|
|
226
|
+
// padding with the old document's tail, so a shorter replacement trims to the
|
|
227
|
+
// new length on that very notify.
|
|
228
|
+
staleTrimmed = false;
|
|
229
|
+
// Id offset applied to non-adopted new blocks in a merged view. A divergence
|
|
230
|
+
// reparse restarts core block ids at 0 (fresh parser), and streamed ids are
|
|
231
|
+
// chunk-dependent (tail reparses burn ids) — so a changed block's new id can
|
|
232
|
+
// collide with a retained old block's id in the same merged snapshot.
|
|
233
|
+
// Bumped by ID_NAMESPACE_STRIDE per generation, which keeps every merged id
|
|
234
|
+
// provably unique: adopted ids are all below the current namespace.
|
|
235
|
+
idNamespace = 0;
|
|
236
|
+
// getSnapshot() cache: (store.snapshot ref, trimmed) → merged array. Repeated
|
|
237
|
+
// reads between notifies must return the SAME reference — the
|
|
238
|
+
// useSyncExternalStore cached-snapshot contract.
|
|
239
|
+
mergeCache = null;
|
|
216
240
|
// Perf
|
|
217
241
|
appendedBytes = 0;
|
|
218
242
|
patchCount = 0;
|
|
@@ -238,6 +262,10 @@ class FluxClient {
|
|
|
238
262
|
* order, after the store updates) — for side effects like lazily
|
|
239
263
|
* highlighting a finished code block or analytics. A committed block never
|
|
240
264
|
* re-fires; the streaming tail does not (subscribe for live tail updates).
|
|
265
|
+
* NOTE: this is a PARSER-commit hook — the block carries the parser's raw
|
|
266
|
+
* id. During a setContent divergence swap the rendered view may show that
|
|
267
|
+
* block under a different id (an adopted old id, or a namespaced one), so
|
|
268
|
+
* correlate with rendered blocks via subscribe()+getSnapshot(), not this id.
|
|
241
269
|
* @param options.coalesce opt-in (default `false`): collapse multiple
|
|
242
270
|
* intra-frame patch notifications into ONE `requestAnimationFrame`-scheduled
|
|
243
271
|
* flush to subscribers, so a React `useSyncExternalStore` consumer renders at
|
|
@@ -366,7 +394,12 @@ class FluxClient {
|
|
|
366
394
|
* - **prefix-extension** (the streaming-growth case) → append only the new
|
|
367
395
|
* suffix, so committed blocks stay put and only the active tail re-parses;
|
|
368
396
|
* - **any other change** (e.g. a finished stream swapped for a re-processed
|
|
369
|
-
* final string) →
|
|
397
|
+
* final string) → reset + reparse the whole new string, keeping the
|
|
398
|
+
* current view on screen until the reparse lands: the document never
|
|
399
|
+
* blanks, scroll never moves, and blocks whose rendered content is
|
|
400
|
+
* unchanged keep their identity (and React keys) so only genuinely
|
|
401
|
+
* changed blocks re-render. An empty new string is an explicit clear and
|
|
402
|
+
* hard-resets immediately.
|
|
370
403
|
*
|
|
371
404
|
* This is the first-class bridge for UIs that hold a streaming message as a
|
|
372
405
|
* single growing string prop (the common React shape) — no hand-rolled diff,
|
|
@@ -387,9 +420,14 @@ class FluxClient {
|
|
|
387
420
|
setContent(content, opts) {
|
|
388
421
|
if (content !== this.lastContent) {
|
|
389
422
|
if (!this.contentDone && content.startsWith(this.lastContent)) {
|
|
423
|
+
if (this.lastContent === "" && content.length > 0 && this.getSnapshot().length > 0) {
|
|
424
|
+
this.softReset(this.getSnapshot());
|
|
425
|
+
}
|
|
390
426
|
this.append(content.slice(this.lastContent.length));
|
|
391
427
|
} else {
|
|
392
|
-
this.
|
|
428
|
+
const displayed = this.getSnapshot();
|
|
429
|
+
if (content.length > 0 && displayed.length > 0) this.softReset(displayed);
|
|
430
|
+
else this.reset();
|
|
393
431
|
this.append(content);
|
|
394
432
|
}
|
|
395
433
|
this.lastContent = content;
|
|
@@ -401,7 +439,57 @@ class FluxClient {
|
|
|
401
439
|
}
|
|
402
440
|
}
|
|
403
441
|
reset() {
|
|
404
|
-
const hadContent = this.
|
|
442
|
+
const hadContent = this.getSnapshot().length > 0;
|
|
443
|
+
this.staleSnapshot = null;
|
|
444
|
+
this.staleTrimmed = false;
|
|
445
|
+
this.mergeCache = null;
|
|
446
|
+
this.resetParser();
|
|
447
|
+
if (hadContent) this.emit(true);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* setContent's divergence reset: rebuild the parser exactly like {@link reset},
|
|
451
|
+
* but keep `preserve` (the currently displayed view) on screen while the new
|
|
452
|
+
* content reparses. No notify fires here — subscribers keep reading the same
|
|
453
|
+
* snapshot reference until the first new-generation patch merges over it, so
|
|
454
|
+
* the swap is seamless: no empty frame, no container collapse, no scroll
|
|
455
|
+
* clamp, and blocks whose content survives the reprocess never re-render.
|
|
456
|
+
*/
|
|
457
|
+
softReset(preserve) {
|
|
458
|
+
this.staleSnapshot = preserve;
|
|
459
|
+
this.staleTrimmed = false;
|
|
460
|
+
this.idNamespace += ID_NAMESPACE_STRIDE;
|
|
461
|
+
this.resetParser();
|
|
462
|
+
this.mergeCache = { base: this.store.snapshot, trimmed: false, view: preserve };
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Finish a preserved-view divergence swap by making the merged view THE
|
|
466
|
+
* store: adopted ids become the committed keys, the merged array becomes the
|
|
467
|
+
* snapshot, and every scrap of merge state drops. From here getSnapshot() is
|
|
468
|
+
* a plain field read again (zero steady-state overhead) and the superseded
|
|
469
|
+
* generation's blocks are garbage — only one document stays in memory.
|
|
470
|
+
* Runs on the terminal (final) patch, which commits everything — if anything
|
|
471
|
+
* is somehow still open, the lazy merge simply stays live instead (the
|
|
472
|
+
* incremental reuse keeps it linear).
|
|
473
|
+
*/
|
|
474
|
+
collapseStale() {
|
|
475
|
+
if (this.store.active.length > 0) return;
|
|
476
|
+
const view = this.getSnapshot();
|
|
477
|
+
const committed = /* @__PURE__ */ new Map();
|
|
478
|
+
const committedOrder = new Array(view.length);
|
|
479
|
+
for (let i = 0; i < view.length; i++) {
|
|
480
|
+
committedOrder[i] = view[i].id;
|
|
481
|
+
committed.set(view[i].id, view[i]);
|
|
482
|
+
}
|
|
483
|
+
this.store = { committed, committedOrder, active: [], snapshot: view };
|
|
484
|
+
this.staleSnapshot = null;
|
|
485
|
+
this.staleTrimmed = false;
|
|
486
|
+
this.mergeCache = null;
|
|
487
|
+
}
|
|
488
|
+
// Shared generation teardown: swap in an empty store, zero the metrics and
|
|
489
|
+
// the setContent baseline, invalidate any coalesced frame, bump the epoch,
|
|
490
|
+
// and tell the worker to drop the parser. View concerns (stale preservation,
|
|
491
|
+
// subscriber notify) belong to the callers — reset() and softReset().
|
|
492
|
+
resetParser() {
|
|
405
493
|
this.store = emptyBlockStore();
|
|
406
494
|
this.appendedBytes = 0;
|
|
407
495
|
this.patchCount = 0;
|
|
@@ -417,7 +505,6 @@ class FluxClient {
|
|
|
417
505
|
this.epoch += 1;
|
|
418
506
|
const pw = this.ensureAcquired();
|
|
419
507
|
this.pool.send(pw, { type: "reset", streamId: this.streamId, epoch: this.epoch });
|
|
420
|
-
if (hadContent) this.emit(true);
|
|
421
508
|
}
|
|
422
509
|
destroy() {
|
|
423
510
|
if (!this.attached) return;
|
|
@@ -449,7 +536,65 @@ class FluxClient {
|
|
|
449
536
|
this.listeners.add(fn);
|
|
450
537
|
return () => this.listeners.delete(fn);
|
|
451
538
|
};
|
|
452
|
-
getSnapshot = () =>
|
|
539
|
+
getSnapshot = () => {
|
|
540
|
+
const base = this.store.snapshot;
|
|
541
|
+
if (!this.staleSnapshot) return base;
|
|
542
|
+
const cache = this.mergeCache;
|
|
543
|
+
if (cache && cache.base === base && cache.trimmed === this.staleTrimmed) return cache.view;
|
|
544
|
+
const view = this.mergeStale(base, cache && cache.trimmed === this.staleTrimmed ? cache : null);
|
|
545
|
+
this.mergeCache = { base, trimmed: this.staleTrimmed, view };
|
|
546
|
+
return view;
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* Positional merge of the preserved pre-divergence view over the rebuilding
|
|
550
|
+
* store (see {@link softReset}). Per position:
|
|
551
|
+
* - identical committed block (html + kind + open + speculative) → the OLD
|
|
552
|
+
* block object, so its id and reference survive the swap and the block
|
|
553
|
+
* never re-renders (blocksEqual / the DOM keyed reconcile hold);
|
|
554
|
+
* - changed committed block → the new block, its id offset into this
|
|
555
|
+
* generation's namespace (adopted old ids and raw new ids could collide);
|
|
556
|
+
* - still-open block over old content → the old block (never a shrinking
|
|
557
|
+
* partial where complete content was already on screen); past the old
|
|
558
|
+
* document's end the live tail streams in as-is;
|
|
559
|
+
* - position the reparse hasn't reached → the old block, until the terminal
|
|
560
|
+
* patch sets `staleTrimmed` and the view clamps to the new length.
|
|
561
|
+
*
|
|
562
|
+
* LINEARITY: committed blocks are reference-stable across patches (the store
|
|
563
|
+
* contract), so a base entry pointer-equal to the previous merge's reproduces
|
|
564
|
+
* its previous decision without re-running the O(html) equality compare. Each
|
|
565
|
+
* block is string-compared exactly once — when it first commits — keeping a
|
|
566
|
+
* long post-divergence stream linear instead of quadratic. Only the active
|
|
567
|
+
* tail (fresh references each patch, small by design) re-compares per patch.
|
|
568
|
+
*/
|
|
569
|
+
mergeStale(base, prev) {
|
|
570
|
+
const stale = this.staleSnapshot;
|
|
571
|
+
const len = this.staleTrimmed ? base.length : Math.max(base.length, stale.length);
|
|
572
|
+
const view = new Array(len);
|
|
573
|
+
for (let i = 0; i < len; i++) {
|
|
574
|
+
const nb = i < base.length ? base[i] : void 0;
|
|
575
|
+
if (nb !== void 0 && prev !== null && i < prev.base.length && prev.base[i] === nb) {
|
|
576
|
+
view[i] = prev.view[i];
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const ob = i < stale.length ? stale[i] : void 0;
|
|
580
|
+
if (!nb) {
|
|
581
|
+
view[i] = ob;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (ob) {
|
|
585
|
+
if (nb.open && !this.staleTrimmed) {
|
|
586
|
+
view[i] = ob;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (nb.html === ob.html && nb.kind.type === ob.kind.type && nb.open === ob.open && nb.speculative === ob.speculative) {
|
|
590
|
+
view[i] = ob;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
view[i] = { ...nb, id: this.idNamespace + nb.id };
|
|
595
|
+
}
|
|
596
|
+
return view;
|
|
597
|
+
}
|
|
453
598
|
/**
|
|
454
599
|
* Internal: a renderer with an `onRenderMetrics` hook calls this once per
|
|
455
600
|
* actual React block render so `getMetrics().renderCount` aggregates churn.
|
|
@@ -497,7 +642,7 @@ class FluxClient {
|
|
|
497
642
|
*/
|
|
498
643
|
outline() {
|
|
499
644
|
const out = [];
|
|
500
|
-
for (const b of this.
|
|
645
|
+
for (const b of this.getSnapshot()) {
|
|
501
646
|
if (b.kind.type === "Heading") {
|
|
502
647
|
const d = b.kind.data;
|
|
503
648
|
const level = typeof d === "number" ? d : d?.level ?? 1;
|
|
@@ -515,7 +660,7 @@ class FluxClient {
|
|
|
515
660
|
*/
|
|
516
661
|
toPlaintext() {
|
|
517
662
|
const parts = [];
|
|
518
|
-
for (const b of this.
|
|
663
|
+
for (const b of this.getSnapshot()) {
|
|
519
664
|
const t = htmlToText(b.html);
|
|
520
665
|
if (t) parts.push(t);
|
|
521
666
|
}
|
|
@@ -535,6 +680,11 @@ class FluxClient {
|
|
|
535
680
|
break;
|
|
536
681
|
}
|
|
537
682
|
applyPatch(this.store, patch);
|
|
683
|
+
if (msg.final === true && this.staleSnapshot) {
|
|
684
|
+
this.staleTrimmed = true;
|
|
685
|
+
this.mergeCache = null;
|
|
686
|
+
this.collapseStale();
|
|
687
|
+
}
|
|
538
688
|
this.appendedBytes = msg.appendedBytes;
|
|
539
689
|
this.totalParseMicros += msg.parseMicros;
|
|
540
690
|
this.retainedBytes = msg.retainedBytes;
|
|
Binary file
|
package/dist/worker-core.js
CHANGED
|
@@ -125,8 +125,9 @@ class WorkerCore {
|
|
|
125
125
|
try {
|
|
126
126
|
const parser = this.getOrCreate(streamId);
|
|
127
127
|
if (buffered && buffered.length > 0) {
|
|
128
|
-
parser.append(buffered);
|
|
128
|
+
const drained = parser.append(buffered);
|
|
129
129
|
this.totalAppended.set(streamId, (this.totalAppended.get(streamId) ?? 0) + buffered.length);
|
|
130
|
+
this.emitPatch(streamId, drained, parser, 0, false);
|
|
130
131
|
}
|
|
131
132
|
const patch = parser.finalize();
|
|
132
133
|
this.emitPatch(streamId, patch, parser, 0, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flux-md",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
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"],
|