/** * Minimal RESP (REdis Serialization Protocol) codec — just enough to drive * Redis pub/sub over a raw TCP socket, with no external client dependency. * * Encodes commands as arrays of bulk strings, and streaming-parses the replies * we care about: simple strings (+), errors (-), integers (:), bulk strings ($), * and arrays (*) — which is what SUBSCRIBE/PSUBSCRIBE confirmations and * message/pmessage pushes are made of. */ export type RespValue = string | number | null | RespValue[]; const encoder = new TextEncoder(); const decoder = new TextDecoder(); /** Encode a command (`["PUBLISH", topic, payload]`) as a RESP array of bulk strings. */ export function encodeCommand(args: string[]): Uint8Array { let head = `*${args.length}\r\n`; const parts: Uint8Array[] = []; for (const arg of args) { const bytes = encoder.encode(arg); head += `$${bytes.length}\r\n`; parts.push(encoder.encode(head), bytes, encoder.encode("\r\n")); head = ""; } return concat(parts); } /** Concatenate byte arrays. */ export function concat(parts: Uint8Array[]): Uint8Array { let len = 0; for (const p of parts) len += p.length; const out = new Uint8Array(len); let off = 0; for (const p of parts) { out.set(p, off); off += p.length; } return out; } function indexOfCRLF(buf: Uint8Array, from: number): number { for (let i = from; i + 1 < buf.length; i++) { if (buf[i] === 13 && buf[i + 1] === 10) return i; } return -1; } /** * Parse one RESP reply from `buf` at `off`. Returns the value and the offset just * past it, or `null` if the buffer doesn't yet hold a complete reply (caller * should wait for more bytes). */ export function parseReply(buf: Uint8Array, off = 0): { value: RespValue; next: number } | null { if (off >= buf.length) return null; const type = buf[off]; const lineEnd = indexOfCRLF(buf, off + 1); if (lineEnd === -1) return null; const line = decoder.decode(buf.subarray(off + 1, lineEnd)); const after = lineEnd + 2; switch (type) { case 43: // '+' simple string case 45: // '-' error return { value: line, next: after }; case 58: // ':' integer return { value: Number(line), next: after }; case 36: { // '$' bulk string const len = Number(line); if (len === -1) return { value: null, next: after }; if (after + len + 2 > buf.length) return null; return { value: decoder.decode(buf.subarray(after, after + len)), next: after + len + 2 }; } case 42: { // '*' array const count = Number(line); if (count === -1) return { value: null, next: after }; const arr: RespValue[] = []; let cur = after; for (let i = 0; i < count; i++) { const r = parseReply(buf, cur); if (!r) return null; arr.push(r.value); cur = r.next; } return { value: arr, next: cur }; } default: return null; } }