From 482281e217d194385e5c27c03036d037ce0ebd64 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 18 Dec 2023 21:33:43 +0100 Subject: [PATCH 1/3] =?UTF-8?q?feat(json-pack):=20=F0=9F=8E=B8=20implement?= =?UTF-8?q?=20command=20decoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-pack/resp/RespDecoder.ts | 19 ++++++++++++++++++ .../resp/__tests__/RespDecoder.spec.ts | 20 ++++++++++++++++++- src/util/buffers/strings.ts | 16 +++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/util/buffers/strings.ts diff --git a/src/json-pack/resp/RespDecoder.ts b/src/json-pack/resp/RespDecoder.ts index c63c337cd9..54cb42997a 100644 --- a/src/json-pack/resp/RespDecoder.ts +++ b/src/json-pack/resp/RespDecoder.ts @@ -81,6 +81,25 @@ export class RespDecoder { const decoder = new RespDecoder(); @@ -215,3 +215,21 @@ describe('nulls', () => { expect(decoded).toBe(null); }); }); + +describe('commands', () => { + test('can decode a PING command', () => { + const encoded = encoder.encodeCmd(['PING']); + const decoder = new RespDecoder(); + decoder.reader.reset(encoded); + const decoded = decoder.readCmd(); + expect(decoded).toEqual(['PING']); + }); + + test('can decode a SET command', () => { + const encoded = encoder.encodeCmd(['SET', 'foo', 'bar']); + const decoder = new RespDecoder(); + decoder.reader.reset(encoded); + const decoded = decoder.readCmd(); + expect(decoded).toEqual(['SET', utf8`foo`, utf8`bar`]); + }); +}); diff --git a/src/util/buffers/strings.ts b/src/util/buffers/strings.ts new file mode 100644 index 0000000000..23816c8f80 --- /dev/null +++ b/src/util/buffers/strings.ts @@ -0,0 +1,16 @@ +import {bufferToUint8Array} from "./bufferToUint8Array"; + +export const ascii = (txt: TemplateStringsArray | string | [string]): Uint8Array => { + if (typeof txt === 'string') return ascii([txt]); + [txt] = txt; + const len = txt.length; + const res = new Uint8Array(len); + for (let i = 0; i < len; i++) res[i] = txt.charCodeAt(i); + return res; +}; + +export const utf8 = (txt: TemplateStringsArray | [string] | string): Uint8Array => { + if (typeof txt === 'string') return utf8([txt]); + [txt] = txt; + return bufferToUint8Array(Buffer.from(txt, 'utf8')); +}; From 3b29cab6a1fe2e3085218fc8525f25296e624674 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 18 Dec 2023 21:36:43 +0100 Subject: [PATCH 2/3] =?UTF-8?q?style(util):=20=F0=9F=92=84=20run=20Prettie?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/buffers/strings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/buffers/strings.ts b/src/util/buffers/strings.ts index 23816c8f80..f4ac5b7378 100644 --- a/src/util/buffers/strings.ts +++ b/src/util/buffers/strings.ts @@ -1,4 +1,4 @@ -import {bufferToUint8Array} from "./bufferToUint8Array"; +import {bufferToUint8Array} from './bufferToUint8Array'; export const ascii = (txt: TemplateStringsArray | string | [string]): Uint8Array => { if (typeof txt === 'string') return ascii([txt]); From 4e7c44bc5ce3ff8934308aafe8dc911a4c3c4295 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 18 Dec 2023 21:40:44 +0100 Subject: [PATCH 3/3] =?UTF-8?q?feat(json-pack):=20=F0=9F=8E=B8=20decode=20?= =?UTF-8?q?command=20in=20streaming=20decoder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-pack/resp/RespStreamingDecoder.ts | 23 +++++++++++++++++++ .../__tests__/RespStreamingDecoder.spec.ts | 9 ++++++++ 2 files changed, 32 insertions(+) diff --git a/src/json-pack/resp/RespStreamingDecoder.ts b/src/json-pack/resp/RespStreamingDecoder.ts index 4dac79aab4..c9116f2dce 100644 --- a/src/json-pack/resp/RespStreamingDecoder.ts +++ b/src/json-pack/resp/RespStreamingDecoder.ts @@ -67,6 +67,29 @@ export class RespStreamingDecoder { } } + /** + * Decode only one RESP command from the stream, if the value is not a + * command, an error will be thrown. + * + * @returns Redis command and its arguments or `undefined` if there is + * not enough data to decode. + */ + public readCmd(): [cmd: string, ...args: Uint8Array[]] | undefined { + const reader = this.reader; + if (reader.size() === 0) return undefined; + const x = reader.x; + try { + const args = this.decoder.readCmd(); + reader.consume(); + return args; + } catch (error) { + if (error instanceof RangeError) { + reader.x = x; + return undefined; + } else throw error; + } + } + /** * Skips one value from the stream. If `undefined` is returned, then * there is not enough data to skip or the stream is finished. diff --git a/src/json-pack/resp/__tests__/RespStreamingDecoder.spec.ts b/src/json-pack/resp/__tests__/RespStreamingDecoder.spec.ts index ec185ffcc2..112fd3a054 100644 --- a/src/json-pack/resp/__tests__/RespStreamingDecoder.spec.ts +++ b/src/json-pack/resp/__tests__/RespStreamingDecoder.spec.ts @@ -2,6 +2,7 @@ import {RespStreamingDecoder} from '../RespStreamingDecoder'; import {RespEncoder} from '../RespEncoder'; import {concatList} from '../../../util/buffers/concat'; import {documents} from '../../../__tests__/json-documents'; +import {utf8} from '../../../util/buffers/strings'; const encoder = new RespEncoder(); @@ -66,3 +67,11 @@ test('can stream 49 bytes at a time', () => { } expect(decoded).toEqual(docs); }); + +test('can decode a command', () => { + const encoded = encoder.encodeCmd(['SET', 'foo', 'bar']); + const decoder = new RespStreamingDecoder(); + decoder.push(encoded); + const decoded = decoder.readCmd(); + expect(decoded).toEqual(['SET', utf8`foo`, utf8`bar`]); +});