IPC & Sync Triggers
We do not stream granular keystrokes or ephemeral memory state over the NDJSON IPC bridge. The bridge is reserved for meaningful state synchronization.
Sync Boundaries
C# holds the active paragraph in its local memory. It only sends an updated block payload to Nim at deliberate moments to keep the backend lightweight.
- Triggers: A sync boundary occurs when the user stops typing for a few seconds, presses Enter, clicks away, or triggers a structural command.
- Block-Level Splicing: Standard typing/editing is contained within a single block. For structural mutations (bulk deletions/pastes), an array splice command minimizes the payload. By abandoning nested DOM trees, edits become simple string replacements or array splices.
Optimistic UI & Eventual Consistency
To prevent state drift without sacrificing typing latency (for things like live word counts):
- Optimistic UI: C# calculates the live word count instantly on every keystroke and updates the screen.
- Authoritative Reconciliation: When a sync boundary is reached, C# sends the block to Nim. Nim updates the master document and replies with an ACK containing the true, canonical document statistics.
- Silent Correction: C# silently updates its UI counter with Nim's true count. If there was a drift, it snaps to the correct number instantly.
IPC Protocol: Envelope vs. Letter
To maintain a clean bridge between statically typed C# and Nim, the NDJSON IPC structure strictly separates the "envelope" from the "letter".
- The Split: The IPC bridge is divided into the "Envelope" (the router/dispatcher that parses
message_idandaction) and the "Letter" (the strongly-typed domain structs likeBlockUpdate). - Payload Separation: To future-proof for automated code generation, IPC payload structures must live in isolated files (e.g.,
ipc_payloads.nimorCore/IPC/Payloads/) and remain pure, flat data containers with zero business logic.
The Engine Dispatcher (engine.nim / dispatcher.nim)
- The core engine file perfectly implements the Envelope/Letter separation.
- The
dispatcher()loop reads stdin and parses the generic envelope. It uses templates (adapt*[T]) to parse the inner JSON into strongly typed domain structs before passing them to the domain handlers. - Because of this design, the dispatcher itself doesn't need to change much when adding new features.