reading json before it is valid
JSON.parse refuses everything up to the final character, which is a problem when the interesting part arrives first. So I wrote a scanner that reads what has closed so far.
A LaSu lookup asks for up to four languages in one model response. The old version waited for the closing brace and then rendered everything, which meant the reader watched a spinner for as long as the slowest of the four took — even though the first one had been sitting complete in the buffer for seconds.
The fix sounds like it should be a library call and is not, because of one property of the format: JSON is only valid at the very end.
{"translations":{"japanese":"こんに
JSON.parse → SyntaxError
and yet a human can read it perfectly well:
there is a key, it is japanese, and it starts こんにEverything you need is right there. JSON.parse will not give you any of it, and it is correct not to — it is a parser for documents, and this is not a document yet. What I wanted was not a parser. It was a reader that reports what is currently knowable.
What "knowable" means
The rule is: emit any value whose meaning cannot change any more.
A string that has not closed yet still emits, because appending to it can only make it longer — こんに is a true prefix of whatever it becomes. A number does not emit until something terminates it, because 1 can still become 19 and then 1900. A key with no colon after it is not a key yet. An object emits the pairs that have completed and says nothing about the ones it has not seen.
That distinction is the whole design. It is also why this is a scanner and not a parser: a parser's job is to reject an incomplete document, and this one's job is to describe it.
Two details that will bite
Escape state has to be tracked properly, not approximated by looking at the previous character. A buffer ending in \\ has a closed escape and the string can end; a buffer ending in \ does not. Get this wrong and a quotation mark inside a translation terminates the string early, and the reader watches half a sentence get truncated and then jump.
The other one is Unicode escapes. A \u sequence needs four hex digits, and the chunk boundary does not care where you wanted it. A buffer ending in \u12 is a value that does not exist yet — decoding it produces a wrong character that the next chunk will not correct, because by then it has already been painted.
// A dangling \u12 is not a character. Drop it and let the next
// chunk deliver the whole escape — one frame late is invisible,
// one wrong glyph is not.
const dangling = /\\u[0-9a-fA-F]{0,3}$/;
return partial.replace(dangling, "");The other half is not parsing at all
Once deltas land as fast as the network delivers them, the bottleneck moves. A hundred tokens a second is a hundred state updates a second, and React will happily attempt a hundred renders to match.
So the parser and the view are decoupled: deltas accumulate into a buffer as they arrive, and the view reads that buffer once per animation frame. A hundred tokens a second costs sixty renders — and on a slower frame it costs fewer, automatically, because the loop is driven by the display rather than by the network.
The visible result is that each language gets its row immediately, and the row fills in as its translation is written, with a caret on the one currently being typed and a pulse on the ones still queued. Nothing waits for the slowest.
The part I would do the same again
This parser exists twice — once in TypeScript in the web app, once in dependency-free JavaScript in the extension — against one wire format. Two implementations of the same thing is normally a smell, and here it is the point: the in-page card and the web result must never read the model differently.
What makes it safe is that the duplication is declared rather than accidental. The extension's README carries a contract table mapping every mirrored constant to the server symbol that owns it, and the test suite fails when one side drifts from the other.