Skip to content

Commit d03bb03

Browse files
committed
feat(json-pack): 🎸 add RESP v2 encoding ability
1 parent 655614e commit d03bb03

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

src/json-pack/resp/RespEncoder.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import type {Slice} from '../../util/buffers/Slice';
1010
const REG_RN = /[\r\n]/;
1111
const isSafeInteger = Number.isSafeInteger;
1212

13+
/**
14+
* Implements RESP3 encoding.
15+
*/
1316
export class RespEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriterGrowable>
1417
implements BinaryJsonEncoder, StreamingBinaryJsonEncoder, TlvBinaryJsonEncoder
1518
{
@@ -244,6 +247,13 @@ export class RespEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriter
244247
writer.u16(RESP.RN); // \r\n
245248
}
246249

250+
public writeSimpleStrAscii(str: string): void {
251+
const writer = this.writer;
252+
writer.u8(RESP.STR_SIMPLE); // +
253+
writer.ascii(str);
254+
writer.u16(RESP.RN); // \r\n
255+
}
256+
247257
public writeBulkStr(str: string): void {
248258
const writer = this.writer;
249259
const size = utf8Size(str);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {RESP} from './constants';
2+
import {RespAttributes, RespPush, RespVerbatimString} from './extensions';
3+
import {JsonPackExtension} from '../JsonPackExtension';
4+
import {RespEncoder} from './RespEncoder';
5+
import type {IWriter, IWriterGrowable} from '../../util/buffers';
6+
7+
const REG_RN = /[\r\n]/;
8+
const isSafeInteger = Number.isSafeInteger;
9+
10+
/**
11+
* Implements RESP v2 encoding.
12+
*/
13+
export class RespEncoderLegacy<W extends IWriter & IWriterGrowable = IWriter & IWriterGrowable> extends RespEncoder<W> {
14+
public writeAny(value: unknown): void {
15+
switch (typeof value) {
16+
case 'number':
17+
return this.writeNumber(value as number);
18+
case 'string':
19+
return this.writeStr(value);
20+
case 'boolean':
21+
return this.writeSimpleStr(value ? 'TRUE' : 'FALSE');
22+
case 'object': {
23+
if (!value) return this.writeNull();
24+
if (value instanceof Array) return this.writeArr(value);
25+
if (value instanceof Uint8Array) return this.writeBin(value);
26+
if (value instanceof Error) return this.writeErr(value.message);
27+
if (value instanceof Set) return this.writeSet(value);
28+
if (value instanceof JsonPackExtension) {
29+
if (value instanceof RespPush) return this.writeArr(value.val);
30+
if (value instanceof RespVerbatimString) return this.writeStr(value.val);
31+
if (value instanceof RespAttributes) return this.writeObj(value.val);
32+
}
33+
return this.writeObj(value as Record<string, unknown>);
34+
}
35+
case 'undefined':
36+
return this.writeUndef();
37+
case 'bigint':
38+
return this.writeSimpleStrAscii(value + '');
39+
default:
40+
return this.writeUnknown(value);
41+
}
42+
}
43+
44+
public writeNumber(num: number): void {
45+
if (isSafeInteger(num)) this.writeInteger(num);
46+
else this.writeSimpleStrAscii(num + '');
47+
}
48+
49+
public writeStr(str: string): void {
50+
const length = str.length;
51+
if (length < 64 && !REG_RN.test(str)) this.writeSimpleStr(str);
52+
else this.writeBulkStr(str);
53+
}
54+
55+
public writeNull(): void {
56+
this.writeNullArr();
57+
}
58+
59+
public writeErr(str: string): void {
60+
if (str.length < 64 && !REG_RN.test(str)) this.writeSimpleErr(str);
61+
else this.writeBulkStr(str);
62+
}
63+
64+
public writeSet(set: Set<unknown>): void {
65+
this.writeArr([...set]);
66+
}
67+
68+
public writeArr(arr: unknown[]): void {
69+
const writer = this.writer;
70+
const length = arr.length;
71+
writer.u8(RESP.ARR); // *
72+
this.writeLength(length);
73+
writer.u16(RESP.RN); // \r\n
74+
for (let i = 0; i < length; i++) {
75+
const val = arr[i];
76+
if (val === null) this.writeNullStr();
77+
else this.writeAny(val);
78+
};
79+
}
80+
81+
public writeObj(obj: Record<string, unknown>): void {
82+
const writer = this.writer;
83+
const keys = Object.keys(obj);
84+
const length = keys.length;
85+
writer.u8(RESP.ARR); // %
86+
this.writeLength(length << 1);
87+
writer.u16(RESP.RN); // \r\n
88+
for (let i = 0; i < length; i++) {
89+
const key = keys[i];
90+
this.writeStr(key);
91+
const val = obj[key];
92+
if (val === null) this.writeNullStr();
93+
else this.writeAny(val);
94+
}
95+
}
96+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {RespEncoderLegacy} from '../RespEncoderLegacy';
2+
3+
const encode = (value: unknown): string => {
4+
const encoder = new RespEncoderLegacy();
5+
const encoded = encoder.encode(value);
6+
return Buffer.from(encoded).toString();
7+
};
8+
9+
test('can encode simple strings', () => {
10+
expect(encode('')).toBe('+\r\n');
11+
expect(encode('asdf')).toBe('+asdf\r\n');
12+
});
13+
14+
test('can encode simple errors', () => {
15+
expect(encode(new Error('asdf'))).toBe('-asdf\r\n');
16+
});
17+
18+
test('can encode integers', () => {
19+
expect(encode(0)).toBe(':0\r\n');
20+
expect(encode(123)).toBe(':123\r\n');
21+
expect(encode(-422469777)).toBe(':-422469777\r\n');
22+
});
23+
24+
test('can encode bulk strings', () => {
25+
expect(encode('ab\nc')).toBe('$4\r\nab\nc\r\n');
26+
expect(encode(new Uint8Array([65]))).toBe('$1\r\nA\r\n');
27+
});
28+
29+
test('can encode arrays', () => {
30+
expect(encode(['a', 1])).toBe('*2\r\n+a\r\n:1\r\n');
31+
});
32+
33+
test('encodes null as nullable array', () => {
34+
expect(encode(null)).toBe('*-1\r\n');
35+
});
36+
37+
test('encodes null in nested structure as nullable string', () => {
38+
expect(encode(['a', 'b', null])).toBe('*3\r\n+a\r\n+b\r\n$-1\r\n');
39+
});
40+
41+
test('encodes booleans as strings', () => {
42+
expect(encode(true)).toBe('+TRUE\r\n');
43+
expect(encode(false)).toBe('+FALSE\r\n');
44+
});
45+
46+
test('encodes floats as strings', () => {
47+
expect(encode(1.23)).toBe('+1.23\r\n');
48+
});
49+
50+
test('encodes objects as 2-tuple arrays', () => {
51+
expect(encode({foo: 'bar'})).toBe('*2\r\n+foo\r\n+bar\r\n');
52+
});

src/json-pack/resp/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export * from './constants';
22
export * from './extensions';
33
export * from './RespEncoder';
4+
export * from './RespEncoderLegacy';
45
export * from './RespDecoder';
6+
export * from './RespStreamingDecoder';

0 commit comments

Comments
 (0)