Skip to content

Commit 19164cb

Browse files
authored
Merge pull request #499 from streamich/resp-improvements
Adds RESP v2 support
2 parents 71b72a4 + 5a60649 commit 19164cb

File tree

6 files changed

+195
-26
lines changed

6 files changed

+195
-26
lines changed

src/json-pack/resp/RespEncoder.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import {Writer} from '../../util/buffers/Writer';
22
import {RESP} from './constants';
33
import {utf8Size} from '../../util/strings/utf8';
4-
import {RespAttributes, RespPush} from './extensions';
4+
import {RespAttributes, RespPush, RespVerbatimString} from './extensions';
5+
import {JsonPackExtension} from '../JsonPackExtension';
56
import type {IWriter, IWriterGrowable} from '../../util/buffers';
67
import type {BinaryJsonEncoder, StreamingBinaryJsonEncoder, TlvBinaryJsonEncoder} from '../types';
78
import type {Slice} from '../../util/buffers/Slice';
89

910
const REG_RN = /[\r\n]/;
1011
const isSafeInteger = Number.isSafeInteger;
1112

13+
/**
14+
* Implements RESP3 encoding.
15+
*/
1216
export class RespEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriterGrowable>
1317
implements BinaryJsonEncoder, StreamingBinaryJsonEncoder, TlvBinaryJsonEncoder
1418
{
@@ -38,8 +42,11 @@ export class RespEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriter
3842
if (value instanceof Uint8Array) return this.writeBin(value);
3943
if (value instanceof Error) return this.writeErr(value.message);
4044
if (value instanceof Set) return this.writeSet(value);
41-
if (value instanceof RespPush) return this.writePush(value.val);
42-
if (value instanceof RespAttributes) return this.writeAttr(value.val);
45+
if (value instanceof JsonPackExtension) {
46+
if (value instanceof RespPush) return this.writePush(value.val);
47+
if (value instanceof RespVerbatimString) return this.writeVerbatimStr('txt', value.val);
48+
if (value instanceof RespAttributes) return this.writeAttr(value.val);
49+
}
4350
return this.writeObj(value as Record<string, unknown>);
4451
}
4552
case 'undefined':
@@ -52,31 +59,23 @@ export class RespEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriter
5259
}
5360

5461
protected writeLength(length: number): void {
55-
let digits = 1;
56-
if (length < 10000) {
57-
if (length < 100) {
58-
if (length < 10) digits = 1;
59-
else digits = 2;
60-
} else {
61-
if (length < 1000) digits = 3;
62-
else digits = 4;
63-
}
64-
} else if (length < 100000000) {
65-
if (length < 1000000) {
66-
if (length < 100000) digits = 5;
67-
else digits = 6;
68-
} else {
69-
if (length < 10000000) digits = 7;
70-
else digits = 8;
71-
}
72-
} else {
73-
let pow = 10;
74-
while (length >= pow) {
75-
digits++;
76-
pow *= 10;
62+
const writer = this.writer;
63+
if (length < 100) {
64+
if (length < 10) {
65+
writer.u8(length + 48);
66+
return;
7767
}
68+
const octet1 = length % 10;
69+
const octet2 = (length - octet1) / 10;
70+
writer.u16(((octet2 + 48) << 8) + octet1 + 48);
71+
return;
72+
}
73+
let digits = 1;
74+
let pow = 10;
75+
while (length >= pow) {
76+
digits++;
77+
pow *= 10;
7878
}
79-
const writer = this.writer;
8079
writer.ensureCapacity(digits);
8180
const uint8 = writer.uint8;
8281
const x = writer.x;
@@ -248,6 +247,13 @@ export class RespEncoder<W extends IWriter & IWriterGrowable = IWriter & IWriter
248247
writer.u16(RESP.RN); // \r\n
249248
}
250249

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+
251257
public writeBulkStr(str: string): void {
252258
const writer = this.writer;
253259
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+
}

src/json-pack/resp/__tests__/RespEncoder.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {bufferToUint8Array} from '../../../util/buffers/bufferToUint8Array';
22
import {RespEncoder} from '../RespEncoder';
3+
import {RespVerbatimString} from '../extensions';
34
const Parser = require('redis-parser');
45

56
const parse = (uint8: Uint8Array): unknown => {
@@ -76,6 +77,12 @@ describe('strings', () => {
7677
const encoded = encoder.writer.flush();
7778
expect(toStr(encoded)).toBe('=8\r\ntxt:asdf\r\n');
7879
});
80+
81+
test('can encode verbatim string using RespVerbatimString', () => {
82+
const encoder = new RespEncoder();
83+
const encoded = encoder.encode(new RespVerbatimString('asdf'));
84+
expect(toStr(encoded)).toBe('=8\r\ntxt:asdf\r\n');
85+
});
7986
});
8087
});
8188

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/extensions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ export class RespAttributes extends JsonPackExtension<Record<string, unknown>> {
1111
super(2, val);
1212
}
1313
}
14+
15+
export class RespVerbatimString extends JsonPackExtension<string> {
16+
constructor(public readonly val: string) {
17+
super(3, val);
18+
}
19+
}

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)