first commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user