@stream-mdx/tui 0.4.0 → 1.0.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 CHANGED
@@ -1,4 +1,130 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 45b29fb: Native list and blockquote streaming preserves current nested children in open dumps and Core cold exports. Nested code, table, and paragraph leaves update through existing owner patch families and generic child operations; authored ranges remain physical UTF-16 coordinates in the canonical input document.
8
+
9
+ Primary list paragraphs use native paragraph deltas while cold exports preserve the existing item representation. Empty tasks retain their authored markers without rendering duplicate marker text, including after finalization.
10
+
11
+ Keep provisional inline HTML and allowlisted MDX repair separate from authored source. Rejected HTML stays literal even inside formatted inline content; unclosed previews become literal at finalization without consuming later paragraphs.
12
+
13
+ Preserve the canonical nested hierarchy during late link and footnote resolution. Publish targeted meaning updates with stable authored ranges and historical values, including generic heading metadata and links. Footnote numbering and occurrence anchors follow document reference order across nested paragraphs and table cells.
14
+
15
+ Preserve table headers and alignments when CRLF arrives one UTF-16 unit at a time. Parse lone CR as a line ending without changing physical source offsets, invalidate affected incremental fragments when LF follows, and advance retired root paragraph generations across source-kind changes.
16
+
17
+ Carry canonical generic ranges and metadata through existing property patches. Core and React apply metadata replacement, omission and explicit clearing consistently, without resurrecting stale block metadata or mutating historical exports. Preserve a list item's canonical source extent when retained marker context is shorter than its managed primary paragraph.
18
+
19
+ Preserve completed paragraph leases when retained native marker context omits their body. Publish current HTML source ranges alongside sanitized content, and keep type-1 HTML, comments and processing instructions outside type-6/7 continuation recovery.
20
+
21
+ Remove invalidated suffix reference definitions as titles arrive, and retire paragraph ownership when a footnote definition becomes complete. Native list child identity carries its canonical ordinal so ordered markers remain correct across bounded windows.
22
+
23
+ Native code rows remain in their owned code index rather than a duplicate generic child tree. Native line ranges subscribe only to changed aligned ranges, including late decoration and provisional boundary completion.
24
+
25
+ Keep completed list-item DOM mounted when list looseness changes; inherited presentation updates through the parent layout context. Current ancestor ordering epochs remain observable without restamping unchanged descendants, while stale updates are still rejected.
26
+
27
+ Reuse typed container markers and list looseness in bounded native windows without reading completed item gaps. Preserve quote markers inside the changing paragraph and refresh marker-only opening lines before deriving their retained context.
28
+
29
+ Explicit renderer resets and worker replacement start a fresh document baseline. Keep retired owner generations within that baseline, and invalidate native code-range demand and source actions across resets even when worker-local IDs and generations repeat. Ordinary appends retain their bounded range subscriptions.
30
+
31
+ Normalize CRLF and lone CR for explicit cold container parsing and native fallback semantic extraction from canonical source. Retain untouched authored source, legitimate spaces, and physical UTF-16 child ranges, including nonzero source origins and astral characters.
32
+
33
+ Apply sparse paragraph meaning updates only to affected index branches, preserving earlier snapshots and unchanged authored source. Plaintext projection accepts read-only prepared inline nodes without copying them.
34
+
35
+ Keep MDX components finalized over their complete authored source. Retire absorbed paragraph and container leases before emitting further native updates, preserving unrelated batched changes.
36
+
37
+ Honor configured lazy tokenization for native nested code. Advance grammar state through skipped source without coloring unrequested rows, and retain requested tokens through append, finalize and cold JSON reset.
38
+
39
+ Do not republish a canonical root snapshot containing an already-published live nested code lease after its semantic owner updates. Preserve native lifetime guards and the unmanaged replacement path.
40
+
41
+ Retire the `appendLines` wire operation, generic code-line producers and reducers, and obsolete coalescing telemetry. Native source and decoration changes use their typed code owner. Static version-1 cold artifacts remain unchanged.
42
+
43
+ - 159a237: Use source-owned records for root code fences, tables, and paragraphs, with bounded line, row, and span updates and on-demand full-source reads. Patch consumers must accept wire protocol 2.0, replace `appendLines` with `appendCode`, adopt `appendParagraph` and `updateParagraphSpans`, and handle native table operations; the retired operation has no compatibility alias. Plugins must use the source snapshot and on-demand `readBlocks` document context instead of a retained `blocks` array.
44
+
45
+ Live consumers now initialize one canonical root input document from the
46
+ `INITIALIZED` snapshot baseline and retain later admitted bytes with ordered
47
+ semantic `appendInputSource` root patches. Missing live baselines are
48
+ incompatible; cold `Block[]` and standalone snapshots keep only their honest
49
+ per-block source slices. Native code, table, and paragraph windows share the
50
+ consumer input document while retaining independent owner generations and
51
+ exact newly owned `sourceAppend` suffixes. Empty baselines are valid, and static
52
+ artifact version 1 is unchanged.
53
+
54
+ Preserve owner identity and retirement guards when replacing active snapshots. Reject regressing revisions and source frontiers, while allowing valid source-stable table and paragraph dependency meaning updates after finalization. Same-generation finalized source cannot grow or reopen. Keep final code highlighting and fallback presentation intact without retransmitting source in presentation-only patches.
55
+
56
+ For paragraph owners after raw Markdown EOF, reopen only the actual trailing paragraph under a fresh owner generation. Do not fuse intervening definition or root source into an earlier paragraph. Keep genuinely unowned provisional following roots eligible for continuation.
57
+
58
+ Do not re-finalize completed paragraph owners when later roots extend the shared input. Preserve their original parser frontier and captured source while still publishing valid late dependency changes.
59
+
60
+ Queue imperative append and finalize calls behind their matching initialization acknowledgement. Ignore errors from detached or superseded initializations without changing public worker attachment readiness, and keep detached snapshot callbacks callable.
61
+
62
+ Render managed paragraph children through the native span view while preserving registered source props and finalized block overrides. Default appends avoid global block/MDX scans; legacy full-source custom props use an explicit cold projection. Cold Block exports also retain admitted open native tables.
63
+
64
+ Blocks whose rendering depends on HTML stay on the base HTML grouping path. Genuine inline-HTML paragraphs and HTML-bearing containers retire native owners instead of adopting or rehoming them; tail-local auto-close and native ownership for non-HTML blocks remain.
65
+
66
+ - Release the coordinated 1.0.0 package line. Upgrade all installed StreamMDX packages together and recopy the hosted worker from the matching version. Internal runtime dependencies remain exact versions; the optional unified adapter's StreamMDX peers require the 1.x line.
67
+
68
+ Migration from 0.5.0:
69
+
70
+ - Remove imports of `@stream-mdx/plugins/registry`, `/base`, and `/math`, their `stream-mdx/plugins/*` convenience re-exports, and `@stream-mdx/worker/streaming/custom-matcher`. `PluginRegistry`, `globalPluginRegistry`, legacy match handlers, and legacy math plugin factories are no longer public. Use native Lezer extensions, serializable `SyntaxRuleSpec` rules, and per-engine `DocumentPluginRegistry` document plugins. Native extension functions belong in the engine or custom worker bundle, not in structured-clone `INIT` messages.
71
+ - `MarkdownEngine` owns its parser, queue, document state, plugins, and caches. Construct it with `{ post, extensions? }`, send configuration and document resets through `handle({ type: "INIT", ... })`, and call terminal `dispose()` when finished. Use public `serveMarkdownWorker` and `createInlineEngineWorker` factories instead of old global runtime plumbing. Worker URLs and actual Worker factories create renderer-owned workers that terminate on detach; an existing Worker is caller-owned. Caller-created inline adapters also need caller-owned termination. See `docs/PUBLIC_API.md` and `docs/PLUGIN_ABI.md` for supported wiring.
72
+ `managedWorker={true}` requires a supplied Worker instance or caller-accessible factory and caller-owned initialization/document commands. URL/default workers and `engine="main-thread"` are rejected, even with a supplied worker source. Managed auto cannot silently fall back inline. Borrowed instances remain caller-owned; factory-created workers remain component-owned and terminate on detach.
73
+ - Provisional lookahead inputs over 512 characters now return raw text before completeness scanning, including closed strings. Oversized finalized/nonprovisional complete content is preserved; within-budget finalized projection retains its behavior. Internal defaults are `maxScanChars: 512`, `maxNewlines: 2`, `maxSyntheticOps: 6`, `maxNestingDepth: 8`, `maxValidationFailures: 1`, and `maxProviderMs: 5`. Provider timers start before scanning; these caps are not React props or latency guarantees.
74
+ - Core root exports no longer expose Lezer converter, math extension factory, link-label normalization, entity decoder, or default/regex plugin adapter internals. Use retained parser/preparation APIs and public worker/compiler entries. Owning parser settings remain authoritative, including disabled math in nested snapshot fallbacks.
75
+ - `ComponentRegistry` treats a block component as custom only when it differs from the built-in default. Custom paragraph and quote wrappers must render the supplied `children` projection to retain native inline, HTML, and nested-block ownership. Do not reconstruct those children from raw source.
76
+ - `CodeFenceRendererProps.renderCode` is a React node, not a callback. Return it directly as a fallback. `language`, fence metadata, and emitted `language-*` classes preserve the authored info word, such as `js`, rather than the canonical Shiki grammar name. Renderer lookups still fall back to canonical names and accept aliases. Unlabeled and indented code has no language class or `data-language` attribute.
77
+ - `restart()` cancels the current async iterator generation; it does not replay a consumed iterator. Supply a fresh iterator for a new stream. Renderer-owned text replacements reset the engine document, while caller-initialized managed workers retain their document ownership.
78
+ - URL transformation and HTML/style sanitization now apply across native client rendering, SSR, and the optional unified adapter. Rejected URLs are omitted; custom components must honor the supplied sanitized attributes and children. `allowedElements` and `disallowedElements` are mutually exclusive.
79
+
80
+ The remaining consumed changesets describe the new opt-in renderers, controls, animation, table virtualization, themes, native extension APIs, and correctness/security fixes. These features do not enable themselves when upgrading. See the root README migration section and the generated package changelogs before upgrading custom integrations.
81
+
82
+ ### Patch Changes
83
+
84
+ - 3f24715: Require each live worker PATCH and every schema-2 protocol event to carry its native root document generation. Renderers and TUI event stores reject stale-generation work against the current validated INITIALIZED baseline, including after queueing; TUI replacement also clears old transaction/status metadata. Caller-managed worker attachment stays pending until an observed baseline, then drains accepted input once. Node and direct snapshot compilers bind patch/finalization handling to that baseline. Static snapshot artifacts remain version 1.
85
+
86
+ Explicit cold TUI replacement retires the old native lease, so delayed snapshots and lifecycle events cannot reclaim the replacement. A validated newer same-stream baseline can resume delivery.
87
+
88
+ Bind same-turn restart capacity waits to the new session's admitting renderer, retaining their requested capacity and cancellation options without dropping accepted source.
89
+
90
+ Use the task-packed protocol dependency in installed Node, Next, and Vite smoke consumers instead of allowing a registry fallback.
91
+
92
+ Generate JavaScript for both installed runtime loaders and replay a replacement root's actual finalization patches before checking completed source.
93
+
94
+ Render the terminal example from authored cold block source without adding duplicate heading, quote, or code-fence markers.
95
+
96
+ - Updated dependencies [1669abb]
97
+ - Updated dependencies [0e96940]
98
+ - Updated dependencies [5833985]
99
+ - Updated dependencies [9c482bd]
100
+ - Updated dependencies [150a0ac]
101
+ - Updated dependencies [7879ed5]
102
+ - Updated dependencies [73bdb36]
103
+ - Updated dependencies [fdaa309]
104
+ - Updated dependencies [e02e7cf]
105
+ - Updated dependencies
106
+ - @stream-mdx/core@1.0.0
107
+ - @stream-mdx/protocol@1.0.0
108
+
109
+ ## 0.5.0
110
+
111
+ ### Minor Changes
112
+
113
+ - Promote the current hardening and product-surface tranche as `0.5.0`.
114
+
115
+ This release rolls up:
116
+
117
+ - renderer/store correctness hardening
118
+ - seeded smoke and scheduler-parity release discipline
119
+ - benchmark methodology and comparison cleanup
120
+ - docs, showcase, and TUI/product surface maturation
121
+
122
+ ### Patch Changes
123
+
124
+ - Updated dependencies
125
+ - @stream-mdx/core@0.5.0
126
+ - @stream-mdx/protocol@0.5.0
127
+
3
128
  ## 0.1.1
129
+
4
130
  - Initial release with NDJSON helpers and snapshot store.
package/README.md CHANGED
@@ -1,40 +1,51 @@
1
- # @stream-mdx/tui
1
+ # `@stream-mdx/tui`
2
2
 
3
- Reference utilities for terminal and CLI integrations.
4
-
5
- ## Features
6
- - NDJSON encoder/decoder helpers for StreamMDX protocol events.
7
- - Lightweight snapshot store that applies patches and yields blocks.
3
+ `@stream-mdx/tui` provides terminal-oriented helpers for StreamMDX protocol streams: NDJSON encoding/decoding and a lightweight snapshot store that can apply events and materialize blocks outside React.
8
4
 
9
5
  ## Install
6
+
10
7
  ```bash
11
- npm install @stream-mdx/tui @stream-mdx/core @stream-mdx/protocol
8
+ npm install @stream-mdx/tui @stream-mdx/protocol @stream-mdx/core
12
9
  ```
13
10
 
14
- ## NDJSON
15
- ```ts
16
- import { NdjsonDecoder, encodeNdjsonEvent } from "@stream-mdx/tui";
11
+ ## Main Capabilities
17
12
 
18
- const decoder = new NdjsonDecoder();
19
- const chunk = encodeNdjsonEvent({ event: "done" } as any);
20
- const events = decoder.push(chunk);
21
- ```
13
+ | Capability | Notes |
14
+ | --- | --- |
15
+ | NDJSON helpers | Encode/decode protocol events for line-oriented transports |
16
+ | Snapshot store | Apply patch streams and recover block state |
17
+ | Non-React usage | Useful in TUIs, CLIs, log viewers, and replay tools |
18
+
19
+ ## NDJSON Snapshot Store
22
20
 
23
- ## Snapshot Store
24
21
  ```ts
25
- import { createSnapshotStore } from "@stream-mdx/tui";
22
+ import { NdjsonDecoder, createSnapshotStore } from "@stream-mdx/tui";
23
+ import type { StreamMdxEvent } from "@stream-mdx/protocol";
26
24
 
25
+ const decoder = new NdjsonDecoder<StreamMdxEvent>();
27
26
  const store = createSnapshotStore();
28
- store.applyEvents(events);
29
- const blocks = store.getBlocks();
27
+
28
+ export function receiveChunk(chunk: string | Uint8Array) {
29
+ store.applyEvents(decoder.push(chunk));
30
+ return store.getBlocks();
31
+ }
30
32
  ```
31
33
 
34
+ Every schema-2 event carries the producer's native document `generation`. Publish `init` and the matching `INITIALIZED` snapshot before patches; the snapshot's canonical input generation must match its event envelope. Do not infer a baseline from the first patch. The store rejects stale or foreign-stream events, clears generation-local transaction/status metadata on replacement, and accepts `done` only for the bound generation.
35
+
36
+ `setSnapshot(Block[])` is a cold replacement: it retires the previous native transport generation without inventing a new one. A later validated, newer baseline can resume live events. Direct `applyPatches()` remains caller-owned and does not infer transport ownership.
37
+
32
38
  ## Notes
33
- - Token output is gated by the worker output mode ("tokens" or "both").
34
- - The store is intended for non-React environments (TUIs, CLIs).
35
39
 
36
- ## Docs
40
+ - Token output depends on the worker output mode (`tokens` or `both`).
41
+ - The store is intentionally lightweight and aimed at non-React consumers.
42
+
43
+ ## Documentation
37
44
 
38
- - CLI usage: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/CLI_USAGE.md
39
- - Protocol spec: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/STREAMMDX_JSON_DIFF_SPEC.md
40
- - Public API: https://github.com/kmccleary3301/stream-mdx/blob/main/docs/PUBLIC_API.md
45
+ - Run the example locally with `npm run example:tui-minimal` from the repo root.
46
+ - [`../../examples/tui-minimal/README.md`](../../examples/tui-minimal/README.md)
47
+ - [`../../docs/TUI_MINIMAL_EXAMPLE.md`](../../docs/TUI_MINIMAL_EXAMPLE.md)
48
+ - [`../../docs/TUI_GUIDE.md`](../../docs/TUI_GUIDE.md)
49
+ - [`../../docs/CLI_USAGE.md`](../../docs/CLI_USAGE.md)
50
+ - [`../../docs/STREAMMDX_JSON_DIFF_SPEC.md`](../../docs/STREAMMDX_JSON_DIFF_SPEC.md)
51
+ - [`../../docs/PUBLIC_API.md`](../../docs/PUBLIC_API.md)
package/dist/index.cjs CHANGED
@@ -28,56 +28,112 @@ __export(index_exports, {
28
28
  });
29
29
  module.exports = __toCommonJS(index_exports);
30
30
  var import_core = require("@stream-mdx/core");
31
- function createSnapshotStore(initialBlocks = []) {
32
- let snapshot = (0, import_core.createInitialSnapshot)(initialBlocks);
31
+ var import_protocol = require("@stream-mdx/protocol");
32
+ function nativeGenerationFrom(input) {
33
+ return !Array.isArray(input) && input.inputSource ? input.inputSource.generation : null;
34
+ }
35
+ function clearGenerationMetadata(meta) {
36
+ delete meta.lastTx;
37
+ delete meta.metrics;
38
+ delete meta.error;
39
+ delete meta.done;
40
+ }
41
+ function createSnapshotStore(initial = []) {
42
+ let snapshot = Array.isArray(initial) ? (0, import_core.createInitialSnapshot)(initial) : (0, import_core.createInitialSnapshot)(initial);
43
+ let sourceGeneration = nativeGenerationFrom(initial);
44
+ let generationFloor = sourceGeneration;
45
+ let retiredNativeGeneration = null;
46
+ let streamId = null;
33
47
  const meta = {};
34
- const setSnapshot = (blocks) => {
35
- snapshot = (0, import_core.createInitialSnapshot)(blocks ?? []);
36
- return snapshot.blocks;
48
+ const isAdmissibleNativeGeneration = (generation) => (generationFloor === null || generation >= generationFloor) && (retiredNativeGeneration === null || generation > retiredNativeGeneration);
49
+ const replaceSnapshot = (input, nextGeneration = nativeGenerationFrom(input)) => {
50
+ if (nextGeneration !== null && !isAdmissibleNativeGeneration(nextGeneration)) return false;
51
+ const nextSnapshot = Array.isArray(input) ? (0, import_core.createInitialSnapshot)(input) : (0, import_core.createInitialSnapshot)(input);
52
+ if (nextGeneration === null) {
53
+ if (sourceGeneration !== null && (retiredNativeGeneration === null || sourceGeneration > retiredNativeGeneration)) {
54
+ retiredNativeGeneration = sourceGeneration;
55
+ }
56
+ sourceGeneration = null;
57
+ clearGenerationMetadata(meta);
58
+ } else {
59
+ if (sourceGeneration !== nextGeneration) clearGenerationMetadata(meta);
60
+ sourceGeneration = nextGeneration;
61
+ if (generationFloor === null || nextGeneration > generationFloor) generationFloor = nextGeneration;
62
+ }
63
+ snapshot = nextSnapshot;
64
+ return true;
65
+ };
66
+ const setSnapshot = (input) => {
67
+ replaceSnapshot(input);
37
68
  };
38
69
  const applyPatches = (patches) => {
39
- return (0, import_core.applyPatchBatch)(snapshot, patches ?? []);
70
+ (0, import_core.applyPatchBatch)(snapshot, patches);
71
+ };
72
+ const isAdmissibleEvent = (event) => {
73
+ if (!Number.isSafeInteger(event.generation) || event.generation < 0) {
74
+ throw new Error("Incompatible stream-mdx event: native generation is missing or invalid");
75
+ }
76
+ if (typeof event.streamId !== "string") {
77
+ throw new Error("Incompatible stream-mdx event: stream ID is missing or invalid");
78
+ }
79
+ return (streamId === null || event.streamId === streamId) && isAdmissibleNativeGeneration(event.generation);
80
+ };
81
+ const acceptStream = (event) => {
82
+ if (streamId === null) streamId = event.streamId;
40
83
  };
41
84
  const applyEvent = (event) => {
42
- if (!event || typeof event !== "object") {
43
- return snapshot.blocks;
85
+ if (event.protocol !== import_protocol.STREAM_MDX_PROTOCOL_ID || event.schemaVersion !== import_protocol.STREAM_MDX_SCHEMA_VERSION) {
86
+ throw new Error(`Unsupported stream-mdx wire identity: ${event.protocol}/${event.schemaVersion}`);
44
87
  }
88
+ if (!isAdmissibleEvent(event)) return;
45
89
  switch (event.event) {
46
90
  case "init":
91
+ if (generationFloor === null || event.generation > generationFloor) {
92
+ clearGenerationMetadata(meta);
93
+ generationFloor = event.generation;
94
+ }
95
+ acceptStream(event);
47
96
  meta.init = event;
48
- return snapshot.blocks;
49
- case "snapshot":
50
- return setSnapshot(event.blocks ?? []);
51
- case "patch": {
52
- const patchEvent = event;
53
- if (typeof patchEvent.tx === "number") {
54
- meta.lastTx = patchEvent.tx;
97
+ break;
98
+ case "snapshot": {
99
+ const nativeGeneration = nativeGenerationFrom(event.snapshot);
100
+ if (nativeGeneration === null) {
101
+ throw new Error("Incompatible stream-mdx snapshot: canonical input baseline is missing or invalid");
55
102
  }
56
- return applyPatches(patchEvent.patches ?? []);
103
+ if (nativeGeneration !== event.generation) return;
104
+ if (!replaceSnapshot(event.snapshot, nativeGeneration)) return;
105
+ acceptStream(event);
106
+ break;
57
107
  }
108
+ case "patch":
109
+ if (event.generation !== sourceGeneration) return;
110
+ acceptStream(event);
111
+ applyPatches(event.patches);
112
+ meta.lastTx = event.tx;
113
+ break;
58
114
  case "metrics":
115
+ if (event.generation !== sourceGeneration) return;
116
+ acceptStream(event);
59
117
  meta.metrics = event.metrics;
60
- return snapshot.blocks;
118
+ break;
61
119
  case "error":
120
+ if (event.generation !== sourceGeneration) return;
121
+ acceptStream(event);
62
122
  meta.error = event;
63
- return snapshot.blocks;
123
+ break;
64
124
  case "done":
125
+ if (event.generation !== sourceGeneration) return;
126
+ acceptStream(event);
65
127
  meta.done = event;
66
- return snapshot.blocks;
67
- default:
68
- return snapshot.blocks;
128
+ break;
69
129
  }
70
130
  };
71
131
  const applyEvents = (events) => {
72
- let blocks = snapshot.blocks;
73
- for (const event of events) {
74
- blocks = applyEvent(event);
75
- }
76
- return blocks;
132
+ for (const event of events) applyEvent(event);
77
133
  };
78
134
  return {
79
135
  getSnapshot: () => snapshot,
80
- getBlocks: () => snapshot.blocks,
136
+ getBlocks: () => snapshot.getBlocks(),
81
137
  getMeta: () => ({ ...meta }),
82
138
  setSnapshot,
83
139
  applyPatches,
package/dist/index.d.cts CHANGED
@@ -1,27 +1,27 @@
1
- import { PatchMetrics, DocumentSnapshot, Block, Patch } from '@stream-mdx/core';
2
- import { StreamMdxInitEventV1, StreamMdxErrorEventV1, StreamMdxDoneEventV1, StreamMdxEventV1 } from '@stream-mdx/protocol';
1
+ import { PatchMetrics, DocumentSnapshot, Block, NodeSnapshot, Patch } from '@stream-mdx/core';
2
+ import { StreamMdxInitEvent, StreamMdxErrorEvent, StreamMdxDoneEvent, StreamMdxEvent } from '@stream-mdx/protocol';
3
3
 
4
4
  type StreamMdxStoreMeta = {
5
- init?: StreamMdxInitEventV1;
5
+ init?: StreamMdxInitEvent;
6
6
  lastTx?: number;
7
7
  metrics?: PatchMetrics;
8
- error?: StreamMdxErrorEventV1;
9
- done?: StreamMdxDoneEventV1;
8
+ error?: StreamMdxErrorEvent;
9
+ done?: StreamMdxDoneEvent;
10
10
  };
11
11
  type StreamMdxSnapshotStore = {
12
12
  getSnapshot(): DocumentSnapshot;
13
13
  getBlocks(): Block[];
14
14
  getMeta(): StreamMdxStoreMeta;
15
- setSnapshot(blocks: Block[]): Block[];
16
- applyPatches(patches: Patch[]): Block[];
17
- applyEvent(event: StreamMdxEventV1): Block[];
18
- applyEvents(events: StreamMdxEventV1[]): Block[];
15
+ setSnapshot(input: Block[] | NodeSnapshot): void;
16
+ applyPatches(patches: Patch[]): void;
17
+ applyEvent(event: StreamMdxEvent): void;
18
+ applyEvents(events: StreamMdxEvent[]): void;
19
19
  };
20
- declare function createSnapshotStore(initialBlocks?: Block[]): StreamMdxSnapshotStore;
20
+ declare function createSnapshotStore(initial?: Block[] | NodeSnapshot): StreamMdxSnapshotStore;
21
21
  declare function encodeNdjsonEvent<T>(event: T): string;
22
22
  declare function encodeNdjsonEvents<T>(events: Iterable<T>): string;
23
- declare function decodeNdjson<T = StreamMdxEventV1>(input: string): T[];
24
- declare class NdjsonDecoder<T = StreamMdxEventV1> {
23
+ declare function decodeNdjson<T = StreamMdxEvent>(input: string): T[];
24
+ declare class NdjsonDecoder<T = StreamMdxEvent> {
25
25
  private buffer;
26
26
  private decoder;
27
27
  constructor();
package/dist/index.d.ts CHANGED
@@ -1,27 +1,27 @@
1
- import { PatchMetrics, DocumentSnapshot, Block, Patch } from '@stream-mdx/core';
2
- import { StreamMdxInitEventV1, StreamMdxErrorEventV1, StreamMdxDoneEventV1, StreamMdxEventV1 } from '@stream-mdx/protocol';
1
+ import { PatchMetrics, DocumentSnapshot, Block, NodeSnapshot, Patch } from '@stream-mdx/core';
2
+ import { StreamMdxInitEvent, StreamMdxErrorEvent, StreamMdxDoneEvent, StreamMdxEvent } from '@stream-mdx/protocol';
3
3
 
4
4
  type StreamMdxStoreMeta = {
5
- init?: StreamMdxInitEventV1;
5
+ init?: StreamMdxInitEvent;
6
6
  lastTx?: number;
7
7
  metrics?: PatchMetrics;
8
- error?: StreamMdxErrorEventV1;
9
- done?: StreamMdxDoneEventV1;
8
+ error?: StreamMdxErrorEvent;
9
+ done?: StreamMdxDoneEvent;
10
10
  };
11
11
  type StreamMdxSnapshotStore = {
12
12
  getSnapshot(): DocumentSnapshot;
13
13
  getBlocks(): Block[];
14
14
  getMeta(): StreamMdxStoreMeta;
15
- setSnapshot(blocks: Block[]): Block[];
16
- applyPatches(patches: Patch[]): Block[];
17
- applyEvent(event: StreamMdxEventV1): Block[];
18
- applyEvents(events: StreamMdxEventV1[]): Block[];
15
+ setSnapshot(input: Block[] | NodeSnapshot): void;
16
+ applyPatches(patches: Patch[]): void;
17
+ applyEvent(event: StreamMdxEvent): void;
18
+ applyEvents(events: StreamMdxEvent[]): void;
19
19
  };
20
- declare function createSnapshotStore(initialBlocks?: Block[]): StreamMdxSnapshotStore;
20
+ declare function createSnapshotStore(initial?: Block[] | NodeSnapshot): StreamMdxSnapshotStore;
21
21
  declare function encodeNdjsonEvent<T>(event: T): string;
22
22
  declare function encodeNdjsonEvents<T>(events: Iterable<T>): string;
23
- declare function decodeNdjson<T = StreamMdxEventV1>(input: string): T[];
24
- declare class NdjsonDecoder<T = StreamMdxEventV1> {
23
+ declare function decodeNdjson<T = StreamMdxEvent>(input: string): T[];
24
+ declare class NdjsonDecoder<T = StreamMdxEvent> {
25
25
  private buffer;
26
26
  private decoder;
27
27
  constructor();
package/dist/index.mjs CHANGED
@@ -1,55 +1,111 @@
1
1
  // src/index.ts
2
2
  import { applyPatchBatch, createInitialSnapshot } from "@stream-mdx/core";
3
- function createSnapshotStore(initialBlocks = []) {
4
- let snapshot = createInitialSnapshot(initialBlocks);
3
+ import { STREAM_MDX_PROTOCOL_ID, STREAM_MDX_SCHEMA_VERSION } from "@stream-mdx/protocol";
4
+ function nativeGenerationFrom(input) {
5
+ return !Array.isArray(input) && input.inputSource ? input.inputSource.generation : null;
6
+ }
7
+ function clearGenerationMetadata(meta) {
8
+ delete meta.lastTx;
9
+ delete meta.metrics;
10
+ delete meta.error;
11
+ delete meta.done;
12
+ }
13
+ function createSnapshotStore(initial = []) {
14
+ let snapshot = Array.isArray(initial) ? createInitialSnapshot(initial) : createInitialSnapshot(initial);
15
+ let sourceGeneration = nativeGenerationFrom(initial);
16
+ let generationFloor = sourceGeneration;
17
+ let retiredNativeGeneration = null;
18
+ let streamId = null;
5
19
  const meta = {};
6
- const setSnapshot = (blocks) => {
7
- snapshot = createInitialSnapshot(blocks ?? []);
8
- return snapshot.blocks;
20
+ const isAdmissibleNativeGeneration = (generation) => (generationFloor === null || generation >= generationFloor) && (retiredNativeGeneration === null || generation > retiredNativeGeneration);
21
+ const replaceSnapshot = (input, nextGeneration = nativeGenerationFrom(input)) => {
22
+ if (nextGeneration !== null && !isAdmissibleNativeGeneration(nextGeneration)) return false;
23
+ const nextSnapshot = Array.isArray(input) ? createInitialSnapshot(input) : createInitialSnapshot(input);
24
+ if (nextGeneration === null) {
25
+ if (sourceGeneration !== null && (retiredNativeGeneration === null || sourceGeneration > retiredNativeGeneration)) {
26
+ retiredNativeGeneration = sourceGeneration;
27
+ }
28
+ sourceGeneration = null;
29
+ clearGenerationMetadata(meta);
30
+ } else {
31
+ if (sourceGeneration !== nextGeneration) clearGenerationMetadata(meta);
32
+ sourceGeneration = nextGeneration;
33
+ if (generationFloor === null || nextGeneration > generationFloor) generationFloor = nextGeneration;
34
+ }
35
+ snapshot = nextSnapshot;
36
+ return true;
37
+ };
38
+ const setSnapshot = (input) => {
39
+ replaceSnapshot(input);
9
40
  };
10
41
  const applyPatches = (patches) => {
11
- return applyPatchBatch(snapshot, patches ?? []);
42
+ applyPatchBatch(snapshot, patches);
43
+ };
44
+ const isAdmissibleEvent = (event) => {
45
+ if (!Number.isSafeInteger(event.generation) || event.generation < 0) {
46
+ throw new Error("Incompatible stream-mdx event: native generation is missing or invalid");
47
+ }
48
+ if (typeof event.streamId !== "string") {
49
+ throw new Error("Incompatible stream-mdx event: stream ID is missing or invalid");
50
+ }
51
+ return (streamId === null || event.streamId === streamId) && isAdmissibleNativeGeneration(event.generation);
52
+ };
53
+ const acceptStream = (event) => {
54
+ if (streamId === null) streamId = event.streamId;
12
55
  };
13
56
  const applyEvent = (event) => {
14
- if (!event || typeof event !== "object") {
15
- return snapshot.blocks;
57
+ if (event.protocol !== STREAM_MDX_PROTOCOL_ID || event.schemaVersion !== STREAM_MDX_SCHEMA_VERSION) {
58
+ throw new Error(`Unsupported stream-mdx wire identity: ${event.protocol}/${event.schemaVersion}`);
16
59
  }
60
+ if (!isAdmissibleEvent(event)) return;
17
61
  switch (event.event) {
18
62
  case "init":
63
+ if (generationFloor === null || event.generation > generationFloor) {
64
+ clearGenerationMetadata(meta);
65
+ generationFloor = event.generation;
66
+ }
67
+ acceptStream(event);
19
68
  meta.init = event;
20
- return snapshot.blocks;
21
- case "snapshot":
22
- return setSnapshot(event.blocks ?? []);
23
- case "patch": {
24
- const patchEvent = event;
25
- if (typeof patchEvent.tx === "number") {
26
- meta.lastTx = patchEvent.tx;
69
+ break;
70
+ case "snapshot": {
71
+ const nativeGeneration = nativeGenerationFrom(event.snapshot);
72
+ if (nativeGeneration === null) {
73
+ throw new Error("Incompatible stream-mdx snapshot: canonical input baseline is missing or invalid");
27
74
  }
28
- return applyPatches(patchEvent.patches ?? []);
75
+ if (nativeGeneration !== event.generation) return;
76
+ if (!replaceSnapshot(event.snapshot, nativeGeneration)) return;
77
+ acceptStream(event);
78
+ break;
29
79
  }
80
+ case "patch":
81
+ if (event.generation !== sourceGeneration) return;
82
+ acceptStream(event);
83
+ applyPatches(event.patches);
84
+ meta.lastTx = event.tx;
85
+ break;
30
86
  case "metrics":
87
+ if (event.generation !== sourceGeneration) return;
88
+ acceptStream(event);
31
89
  meta.metrics = event.metrics;
32
- return snapshot.blocks;
90
+ break;
33
91
  case "error":
92
+ if (event.generation !== sourceGeneration) return;
93
+ acceptStream(event);
34
94
  meta.error = event;
35
- return snapshot.blocks;
95
+ break;
36
96
  case "done":
97
+ if (event.generation !== sourceGeneration) return;
98
+ acceptStream(event);
37
99
  meta.done = event;
38
- return snapshot.blocks;
39
- default:
40
- return snapshot.blocks;
100
+ break;
41
101
  }
42
102
  };
43
103
  const applyEvents = (events) => {
44
- let blocks = snapshot.blocks;
45
- for (const event of events) {
46
- blocks = applyEvent(event);
47
- }
48
- return blocks;
104
+ for (const event of events) applyEvent(event);
49
105
  };
50
106
  return {
51
107
  getSnapshot: () => snapshot,
52
- getBlocks: () => snapshot.blocks,
108
+ getBlocks: () => snapshot.getBlocks(),
53
109
  getMeta: () => ({ ...meta }),
54
110
  setSnapshot,
55
111
  applyPatches,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-mdx/tui",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "StreamMDX TUI helpers (NDJSON parsing and snapshot store)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,11 +30,12 @@
30
30
  "sideEffects": false,
31
31
  "scripts": {
32
32
  "build": "tsup",
33
+ "test": "tsx ../../scripts/run-tests.ts __tests__",
33
34
  "clean": "rm -rf dist",
34
35
  "prepack": "npm run build"
35
36
  },
36
37
  "dependencies": {
37
- "@stream-mdx/core": "0.4.0",
38
- "@stream-mdx/protocol": "0.4.0"
38
+ "@stream-mdx/core": "1.0.0",
39
+ "@stream-mdx/protocol": "1.0.0"
39
40
  }
40
41
  }