An application may use a literal marker to stop displaying generated AI text. Searching each incoming chunk independently fails when one chunk ends with <EN and the next begins with D>. By the time the second chunk arrives, part of the marker may already have been displayed.
Prerequisites: Node.js with built-in ES modules and node:assert/strict; the example was executed on Node.js 26.13.0. There are no external packages. The input is an iterable of already decoded text deltas from a hypothetical AI adapter. HTTP events, UTF-8 decoding and provider response envelopes are outside this example.
Retain a possible marker prefix
For a marker of length m, retain up to m minus one trailing UTF-16 code units after every unsuccessful search. Earlier text cannot begin a future complete match, so it can be appended to the output. Search the pending text before releasing its safe prefix. The implementation uses the standard String.prototype.indexOf semantics and stops at the first complete literal marker.
import assert from 'node:assert/strict';
function collect(chunks, marker) {
if (typeof marker !== 'string' || marker.length === 0) {
throw new TypeError('nonempty stop marker required');
}
let pending = '', output = '';
for (const chunk of chunks) {
if (typeof chunk !== 'string') throw new TypeError('text chunk required');
pending += chunk;
const found = pending.indexOf(marker);
if (found !== -1) return output + pending.slice(0, found);
// Keep enough trailing code units for a marker split across chunks.
const safe = Math.max(0, pending.length - marker.length + 1);
output += pending.slice(0, safe);
pending = pending.slice(safe);
}
return output + pending;
}
const text = 'Answer<END>discard';
for (let i = 0; i <= text.length; i++) {
assert.equal(collect([text.slice(0, i), text.slice(i)], '<END>'), 'Answer');
}
assert.equal(collect([...text], '<END>'), 'Answer');
assert.equal(collect(['ok<EN'], '<END>'), 'ok<EN');
assert.equal(collect(['x', 'y'], '!'), 'xy');
assert.equal(collect(['<END>extra'], '<END>'), '');
assert.equal(collect(['aENDEND'], 'END'), 'a');
assert.throws(() => collect([], ''), TypeError);
assert.throws(() => collect([3], 'END'), TypeError);
console.log('stop checks passed: all split points, EOF, first match, invalid input');
Run the fixture tests
Save as stop.mjs and run:
node stop.mjs
Expected output:
stop checks passed: all split points, EOF, first match, invalid input
The split-point loop tests every division of the same response into two chunks, including empty chunks at either end. A second test supplies one character per chunk, which exercises a marker spanning more than two deliveries. The incomplete marker at end of input is preserved as ordinary text because no complete stop marker occurred.
Edge cases and integration boundaries
A one-character marker needs no retained suffix. An empty marker is rejected because its stopping meaning would otherwise be ambiguous. Matching is case-sensitive and performs no Unicode normalization. Choose a marker that is unlikely to appear in ordinary output, and document whether a literal occurrence is allowed to terminate an answer.
This collector stores the complete returned answer, so total memory is not bounded by marker length. It demonstrates matching state rather than a production streaming writer. If you adapt it to emit visible pieces, decide how to avoid splitting surrogate pairs in those pieces. A live transport must also cancel or close its upstream request when appropriate; returning from this function does not demonstrate cancellation of a network request.
These synthetic tests validate text-boundary handling only. They do not establish that a model obeys a requested stop policy. Find more examples in Javascript and AI.