Message framing
Buffer partial lines
Accumulate chunks, extract every complete line, and keep the unfinished remainder for the next event.
10 minute lesson
The newline protocol is defined. Now the parser has to survive TCP’s chunking: a line can arrive in pieces, and several lines can arrive together.
The parser keeps per-socket state, because each TCP connection can stop in the middle of a line. That state is one string: the bytes received so far that don’t yet end in \n.
Add a line buffer:
let pending = ''
socket.on('data', chunk => {
pending += chunk.toString('utf8')
const lines = pending.split('\n')
pending = lines.pop()
for (const line of lines) handleLine(line)
})
Walk through it. Each chunk is appended to pending. split('\n') breaks the accumulated text into pieces; every piece except the last was terminated by a newline, so it’s a complete line. The last piece is either an empty string (the chunk ended exactly at \n) or a partial line — lines.pop() puts it back into pending to wait for the rest.
Prove both cases work
Interactive netcat sends whole lines, so use a small script to control the chunking:
import net from 'node:net'
const c = net.createConnection({ host: '127.0.0.1', port: 4000 })
c.write('PI')
setTimeout(() => c.write('NG\nECHO hi\nEC'), 300)
setTimeout(() => c.write('HO bye\n'), 600)
c.on('data', data => process.stdout.write(data))
Send half a command, pause, then finish it. Send two commands together. The first write parks PI in pending. The second completes PING, delivers a whole ECHO hi, and parks EC. The third completes ECHO bye. Both cases must produce exactly the intended replies — three, no extras, none missing.
Keep the buffer honest
Enforce the maximum line length here, before retaining more input:
if (pending.length > 4096) return socket.destroy()
Validate UTF-8 and length before retaining untrusted input. One subtlety: chunk.toString('utf8') can mangle a multi-byte character split across two chunks; StringDecoder from node:string_decoder handles that boundary if your protocol carries non-ASCII text.
And never share pending between sockets. Declare it inside the connection callback so each client gets its own. A module-level buffer mixes two clients’ bytes into one garbled stream — a bug that only appears with concurrent connections, which is exactly when you least want to debug it.
Lesson completed