From 7800e3b22ff59fa1bb8a80226e44337d1f2920d0 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Fri, 1 Dec 2023 15:16:59 +0000 Subject: [PATCH] refactor(experimental): pre-build the byte array before encoding with codecs (#1865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Note this is a draft PR focusing on `codecs-core` to gather feedback, I will continue to update the other libraries afterwards._ This PR updates the codecs API such that both encoding and decoding function **have access to the entire byte array**. Let’s first see the change this PR introduce and then see why this is a valuable change. ## API Changes - **Encode**: The `Encoder` type contains a new function `write`. Contrary to the `encode` function which creates a new `Uint8Array` and returns it directly, the `write function` updates the provided `bytes` argument at the provided `offset`. It then returns the next offset that should be written to. ```ts // Before type Encoder = { encode: (value: T) => Uint8Array; // ... }; // After type Encoder = { encode: (value: T) => Uint8Array; write: (value: T, bytes: Uint8Array, offset: Offset) => Offset; // ... }; ``` A new `createEncoder` function was provided to automatically fill the `encode` function from the `write` function. ```ts const myU8Encoder = createEncoder({ fixedSize: 1, write: (value: number, bytes: Uint8Array, offset: Offset) { bytes.set(value, offset); return offset + 1; }; }); ``` - **Decode**: The `decode` function already following a similar approach by using offsets. The newly added function `read` takes over this responsibility. The only difference is we now make the offset a mandatory argument to stay consistent with the `write` function. The `decode` function now becomes syntactic sugar for accessing the value directly. ```ts // Before type Decoder = { decode: (bytes: Uint8Array, offset?: Offset) => [T, Offset]; // ... }; // After type Decoder = { decode: (bytes: Uint8Array, offset?: Offset) => T; read: (bytes: Uint8Array, offset: Offset) => [T, Offset]; // ... }; ``` Similarly to the `Encoder` changes, a new `createDecoder` function is provided to fill the `decode` function using the `read` function. ```ts const myU8Decoder = createDecoder({ fixedSize: 1, read: (bytes: Uint8Array, offset: Offset) { return [bytes[offset], offset + 1]; }; }); ``` - **Sizes**: Because we now need to pre-build the entire byte array that will be encoded, we need a way to find the variable size of a given value. We introduce a new `variableSize` function and narrow the types so that it can only be provided when `fixedSize` is `null`. ```ts // Before type Encoder = { fixedSize: number | null; maxSize: number | null; } // After type Encoder = { ... } & ( | { fixedSize: number; } | { fixedSize: null; variableSize: (value: T) => number; maxSize?: number } ) ``` We do something similar the `Decoder` except that this one doesn’t need to know about the variable size (it would make no sense as the type parameter `T` for decoder refers to the decoded type and not the type to encode). ```ts // Before type Decoder = { fixedSize: number | null; maxSize: number | null; } // After type Decoder = { ... } & ( | { fixedSize: number; } | { fixedSize: null; maxSize?: number } ) ``` - **Description**: This PR takes this refactoring opportunity to remove the `description` attribute from the codecs API which brings little value to the end-user. ## Why? - **Consistent API**: The implementation of the `encode` / `decode` and `write` / `read` functions are now consistent with each other. Before one was using offsets to navigate through an entire byte array and the other was returning and merge byte arrays together. Now they both use offsets to navigate the byte array to encode or decode. - **Performant API**: By pre-building the byte array once, we’re avoiding creating multiple instance of byte arrays and merging them together. - **Non-linear serialisation**: The main reason why it’s important for the `encode` method to have access to the entire encoded byte array is that it allows us to offer more complex codec primitives that are able to jump back and forth within the buffer. Without it, we are locking ourselves to only supporting serialisation strategies that are read linearly which isn’t always the case. For instance, imagine we have the size of an array being stored at the very beginning of the account whereas the items themselves are being stored at the end. Because we now have the full byte array when encoding, we can push the size at the beginning whilst inserting all items at the requested offset. We could even offer a `getOffsetCodec` (or similar) that allows us to shift the offset forward or backward to compose more complex, non-linear data structures. This would be simply impossible with the current format of the `encode` function. --- .../codecs-core/src/__tests__/__setup__.ts | 40 +++--- .../codecs-core/src/__tests__/codec-test.ts | 66 +++++----- .../src/__tests__/combine-codec.ts | 84 +++++------- .../src/__tests__/fix-codec-test.ts | 107 ++++++++------- .../src/__tests__/map-codec-test.ts | 94 +++++++------ .../src/__tests__/reverse-codec-test.ts | 39 +++--- packages/codecs-core/src/assertions.ts | 8 +- packages/codecs-core/src/codec.ts | 123 ++++++++++++++---- packages/codecs-core/src/combine-codec.ts | 25 +--- packages/codecs-core/src/fix-codec.ts | 49 ++++--- packages/codecs-core/src/map-codec.ts | 39 +++--- packages/codecs-core/src/reverse-codec.ts | 31 ++--- 12 files changed, 368 insertions(+), 337 deletions(-) diff --git a/packages/codecs-core/src/__tests__/__setup__.ts b/packages/codecs-core/src/__tests__/__setup__.ts index 73f5043b916..059fd33159d 100644 --- a/packages/codecs-core/src/__tests__/__setup__.ts +++ b/packages/codecs-core/src/__tests__/__setup__.ts @@ -1,31 +1,37 @@ -import { Codec } from '../codec'; +import { Codec, createCodec } from '../codec'; export const b = (s: string) => base16.encode(s); -export const base16: Codec = { - decode(bytes, offset = 0) { +export const base16: Codec = createCodec({ + fixedSize: null, + read(bytes, offset) { const value = bytes.slice(offset).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); return [value, bytes.length]; }, - description: 'base16', - encode(value: string) { + variableSize: (value: string) => Math.ceil(value.length / 2), + write(value: string, bytes, offset) { const matches = value.toLowerCase().match(/.{1,2}/g); - return Uint8Array.from(matches ? matches.map((byte: string) => parseInt(byte, 16)) : []); + const hexBytes = matches ? matches.map((byte: string) => parseInt(byte, 16)) : []; + bytes.set(hexBytes, offset); + return offset + hexBytes.length; }, - fixedSize: null, - maxSize: null, -}; +}); export const getMockCodec = ( config: { defaultValue?: string; description?: string; size?: number | null; - } = {}, -) => ({ - decode: jest.fn().mockReturnValue([config.defaultValue ?? '', 0]), - description: config.description ?? 'mock', - encode: jest.fn().mockReturnValue(new Uint8Array()), - fixedSize: config.size ?? null, - maxSize: config.size ?? null, -}); + } = {} +) => + createCodec({ + fixedSize: config.size ?? null, + maxSize: config.size ?? undefined, + read: jest.fn().mockReturnValue([config.defaultValue ?? '', 0]), + variableSize: jest.fn().mockReturnValue(config.size ?? 0), + write: jest.fn().mockReturnValue(0), + }) as Codec & { + readonly read: jest.Mock; + readonly variableSize: jest.Mock; + readonly write: jest.Mock; + }; diff --git a/packages/codecs-core/src/__tests__/codec-test.ts b/packages/codecs-core/src/__tests__/codec-test.ts index 912757ab2a9..a89f943ef33 100644 --- a/packages/codecs-core/src/__tests__/codec-test.ts +++ b/packages/codecs-core/src/__tests__/codec-test.ts @@ -1,75 +1,73 @@ -import { Codec, Decoder, Encoder } from '../codec'; +import { Codec, createCodec, createDecoder, createEncoder, Decoder, Encoder } from '../codec'; describe('Encoder', () => { it('can define Encoder instances', () => { - const myEncoder: Encoder = { - description: 'myEncoder', - encode: (value: string) => { - const bytes = new Uint8Array(32).fill(0); + const myEncoder: Encoder = createEncoder({ + fixedSize: 32, + write: (value: string, bytes, offset) => { const charCodes = [...value.slice(0, 32)].map(char => Math.min(char.charCodeAt(0), 255)); - bytes.set(new Uint8Array(charCodes)); - return bytes; + bytes.set(charCodes, offset); + return offset + 32; }, - fixedSize: 32, - maxSize: 32, - }; + }); - expect(myEncoder.description).toBe('myEncoder'); expect(myEncoder.fixedSize).toBe(32); - expect(myEncoder.maxSize).toBe(32); const expectedBytes = new Uint8Array(32).fill(0); expectedBytes.set(new Uint8Array([104, 101, 108, 108, 111])); expect(myEncoder.encode('hello')).toStrictEqual(expectedBytes); + + const writtenBytes = new Uint8Array(32).fill(0); + expect(myEncoder.write('hello', writtenBytes, 0)).toBe(32); + expect(writtenBytes).toStrictEqual(expectedBytes); }); }); describe('Decoder', () => { it('can define Decoder instances', () => { - const myDecoder: Decoder = { - decode: (bytes: Uint8Array, offset = 0) => { + const myDecoder: Decoder = createDecoder({ + fixedSize: 32, + read: (bytes: Uint8Array, offset) => { const slice = bytes.slice(offset, offset + 32); const str = [...slice].map(charCode => String.fromCharCode(charCode)).join(''); return [str, offset + 32]; }, - description: 'myDecoder', - fixedSize: 32, - maxSize: 32, - }; + }); - expect(myDecoder.description).toBe('myDecoder'); expect(myDecoder.fixedSize).toBe(32); - expect(myDecoder.maxSize).toBe(32); - expect(myDecoder.decode(new Uint8Array([104, 101, 108, 108, 111]))).toStrictEqual(['hello', 32]); + + expect(myDecoder.decode(new Uint8Array([104, 101, 108, 108, 111]))).toBe('hello'); + expect(myDecoder.read(new Uint8Array([104, 101, 108, 108, 111]), 0)).toStrictEqual(['hello', 32]); }); }); describe('Codec', () => { it('can define Codec instances', () => { - const myCodec: Codec = { - decode: (bytes: Uint8Array, offset = 0) => { + const myCodec: Codec = createCodec({ + fixedSize: 32, + read: (bytes: Uint8Array, offset) => { const slice = bytes.slice(offset, offset + 32); const str = [...slice].map(charCode => String.fromCharCode(charCode)).join(''); return [str, offset + 32]; }, - description: 'myCodec', - encode: (value: string) => { - const bytes = new Uint8Array(32).fill(0); + write: (value: string, bytes, offset) => { const charCodes = [...value.slice(0, 32)].map(char => Math.min(char.charCodeAt(0), 255)); - bytes.set(new Uint8Array(charCodes)); - return bytes; + bytes.set(charCodes, offset); + return offset + 32; }, - fixedSize: 32, - maxSize: 32, - }; + }); - expect(myCodec.description).toBe('myCodec'); expect(myCodec.fixedSize).toBe(32); - expect(myCodec.maxSize).toBe(32); const expectedBytes = new Uint8Array(32).fill(0); expectedBytes.set(new Uint8Array([104, 101, 108, 108, 111])); expect(myCodec.encode('hello')).toStrictEqual(expectedBytes); - expect(myCodec.decode(new Uint8Array([104, 101, 108, 108, 111]))).toStrictEqual(['hello', 32]); + + const writtenBytes = new Uint8Array(32).fill(0); + expect(myCodec.write('hello', writtenBytes, 0)).toBe(32); + expect(writtenBytes).toStrictEqual(expectedBytes); + + expect(myCodec.decode(new Uint8Array([104, 101, 108, 108, 111]))).toBe('hello'); + expect(myCodec.read(new Uint8Array([104, 101, 108, 108, 111]), 0)).toStrictEqual(['hello', 32]); }); }); diff --git a/packages/codecs-core/src/__tests__/combine-codec.ts b/packages/codecs-core/src/__tests__/combine-codec.ts index 08aa4ec6a2d..25bd023e303 100644 --- a/packages/codecs-core/src/__tests__/combine-codec.ts +++ b/packages/codecs-core/src/__tests__/combine-codec.ts @@ -1,89 +1,63 @@ -import { Codec, Decoder, Encoder } from '../codec'; +import { Codec, createDecoder, createEncoder, Decoder, Encoder } from '../codec'; import { combineCodec } from '../combine-codec'; describe('combineCodec', () => { - const mockEncode: Encoder['encode'] = () => new Uint8Array([]); - const mockDecode: Decoder['decode'] = (_bytes: Uint8Array, offset = 0) => [42, offset]; - it('can join encoders and decoders with the same type', () => { - const u8Encoder: Encoder = { - description: 'u8', - encode: (value: number) => new Uint8Array([value]), + const u8Encoder: Encoder = createEncoder({ fixedSize: 1, - maxSize: 1, - }; + write: (value: number, buffer, offset) => { + buffer.set([value], offset); + return offset + 1; + }, + }); - const u8Decoder: Decoder = { - decode: (bytes: Uint8Array, offset = 0) => [bytes[offset], offset + 1], - description: 'u8', + const u8Decoder: Decoder = createDecoder({ fixedSize: 1, - maxSize: 1, - }; + read: (bytes: Uint8Array, offset = 0) => [bytes[offset], offset + 1], + }); const u8Codec: Codec = combineCodec(u8Encoder, u8Decoder); - expect(u8Codec.description).toBe('u8'); expect(u8Codec.fixedSize).toBe(1); - expect(u8Codec.maxSize).toBe(1); expect(u8Codec.encode(42)).toStrictEqual(new Uint8Array([42])); - expect(u8Codec.decode(new Uint8Array([42]))).toStrictEqual([42, 1]); + expect(u8Codec.decode(new Uint8Array([42]))).toBe(42); }); it('can join encoders and decoders with different but matching types', () => { - const u8Encoder: Encoder = { - description: 'u8', - encode: (value: number | bigint) => new Uint8Array([Number(value)]), + const u8Encoder: Encoder = createEncoder({ fixedSize: 1, - maxSize: 1, - }; + write: (value: number | bigint, buffer, offset) => { + buffer.set([Number(value)], offset); + return offset + 1; + }, + }); - const u8Decoder: Decoder = { - decode: (bytes: Uint8Array, offset = 0) => [BigInt(bytes[offset]), offset + 1], - description: 'u8', + const u8Decoder: Decoder = createDecoder({ fixedSize: 1, - maxSize: 1, - }; + read: (bytes: Uint8Array, offset = 0) => [BigInt(bytes[offset]), offset + 1], + }); const u8Codec: Codec = combineCodec(u8Encoder, u8Decoder); - expect(u8Codec.description).toBe('u8'); expect(u8Codec.fixedSize).toBe(1); - expect(u8Codec.maxSize).toBe(1); expect(u8Codec.encode(42)).toStrictEqual(new Uint8Array([42])); expect(u8Codec.encode(42n)).toStrictEqual(new Uint8Array([42])); - expect(u8Codec.decode(new Uint8Array([42]))).toStrictEqual([42n, 1]); + expect(u8Codec.decode(new Uint8Array([42]))).toBe(42n); }); - it('cannot join encoders and decoders with sizes or descriptions', () => { + it('cannot join encoders and decoders with different sizes', () => { expect(() => combineCodec( - { description: 'u8', encode: mockEncode, fixedSize: 1, maxSize: 1 }, - { decode: mockDecode, description: 'u8', fixedSize: 2, maxSize: 1 }, - ), + createEncoder({ fixedSize: 1, write: jest.fn() }), + createDecoder({ fixedSize: 2, read: jest.fn() }) + ) ).toThrow('Encoder and decoder must have the same fixed size, got [1] and [2]'); expect(() => combineCodec( - { description: 'u8', encode: mockEncode, fixedSize: 1, maxSize: 1 }, - { decode: mockDecode, description: 'u8', fixedSize: 1, maxSize: null }, - ), - ).toThrow('Encoder and decoder must have the same max size, got [1] and [null]'); - - expect(() => - combineCodec( - { description: 'u8', encode: mockEncode, fixedSize: 1, maxSize: 1 }, - { decode: mockDecode, description: 'u16', fixedSize: 1, maxSize: 1 }, - ), - ).toThrow('Encoder and decoder must have the same description, got [u8] and [u16]'); - }); - - it('can override the description of the joined codec', () => { - const myCodec = combineCodec( - { description: 'u8', encode: mockEncode, fixedSize: 1, maxSize: 1 }, - { decode: mockDecode, description: 'u16', fixedSize: 1, maxSize: 1 }, - 'myCustomDescription', - ); - - expect(myCodec.description).toBe('myCustomDescription'); + createEncoder({ fixedSize: null, maxSize: 1, variableSize: jest.fn(), write: jest.fn() }), + createDecoder({ fixedSize: null, read: jest.fn() }) + ) + ).toThrow('Encoder and decoder must have the same max size, got [1] and [undefined]'); }); }); diff --git a/packages/codecs-core/src/__tests__/fix-codec-test.ts b/packages/codecs-core/src/__tests__/fix-codec-test.ts index 9cec7d66e40..e8980036333 100644 --- a/packages/codecs-core/src/__tests__/fix-codec-test.ts +++ b/packages/codecs-core/src/__tests__/fix-codec-test.ts @@ -1,4 +1,4 @@ -import { Codec } from '../codec'; +import { createCodec } from '../codec'; import { fixCodec, fixDecoder, fixEncoder } from '../fix-codec'; import { b, getMockCodec } from './__setup__'; @@ -6,92 +6,93 @@ describe('fixCodec', () => { it('keeps same-sized byte arrays as-is', () => { const mockCodec = getMockCodec(); - mockCodec.encode.mockReturnValueOnce(b('08050c0c0f170f120c04')); + mockCodec.variableSize.mockReturnValueOnce(10); + mockCodec.write.mockImplementation((_, bytes: Uint8Array, offset: number) => { + bytes.set(b('08050c0c0f170f120c04'), offset); + return offset + 10; + }); expect(fixCodec(mockCodec, 10).encode('helloworld')).toStrictEqual(b('08050c0c0f170f120c04')); - expect(mockCodec.encode).toHaveBeenCalledWith('helloworld'); + expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0); fixCodec(mockCodec, 10).decode(b('08050c0c0f170f120c04')); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0); - fixCodec(mockCodec, 10).decode(b('ffff08050c0c0f170f120c04'), 2); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0); + fixCodec(mockCodec, 10).read(b('ffff08050c0c0f170f120c04'), 2); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0); }); it('truncates over-sized byte arrays', () => { const mockCodec = getMockCodec(); - mockCodec.encode.mockReturnValueOnce(b('08050c0c0f170f120c04')); + mockCodec.variableSize.mockReturnValueOnce(10); + mockCodec.write.mockImplementation((_, bytes: Uint8Array, offset: number) => { + bytes.set(b('08050c0c0f170f120c04'), offset); + return offset + 10; + }); expect(fixCodec(mockCodec, 5).encode('helloworld')).toStrictEqual(b('08050c0c0f')); - expect(mockCodec.encode).toHaveBeenCalledWith('helloworld'); + expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0); fixCodec(mockCodec, 5).decode(b('08050c0c0f170f120c04')); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f'), 0); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f'), 0); - fixCodec(mockCodec, 5).decode(b('ffff08050c0c0f170f120c04'), 2); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f'), 0); + fixCodec(mockCodec, 5).read(b('ffff08050c0c0f170f120c04'), 2); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f'), 0); }); it('pads under-sized byte arrays', () => { const mockCodec = getMockCodec(); - mockCodec.encode.mockReturnValueOnce(b('08050c0c0f')); + mockCodec.variableSize.mockReturnValueOnce(5); + mockCodec.write.mockImplementation((_, bytes: Uint8Array, offset: number) => { + bytes.set(b('08050c0c0f'), offset); + return offset + 5; + }); expect(fixCodec(mockCodec, 10).encode('hello')).toStrictEqual(b('08050c0c0f0000000000')); - expect(mockCodec.encode).toHaveBeenCalledWith('hello'); + expect(mockCodec.write).toHaveBeenCalledWith('hello', expect.any(Uint8Array), 0); fixCodec(mockCodec, 10).decode(b('08050c0c0f0000000000')); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0); - fixCodec(mockCodec, 10).decode(b('ffff08050c0c0f0000000000'), 2); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0); + fixCodec(mockCodec, 10).read(b('ffff08050c0c0f0000000000'), 2); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0); expect(() => fixCodec(mockCodec, 10).decode(b('08050c0c0f'))).toThrow( 'Codec [fixCodec] expected 10 bytes, got 5.', ); }); - it('has the right description', () => { - const mockCodec = getMockCodec({ description: 'mock' }); - - // Description matches the fixed definition. - expect(fixCodec(mockCodec, 42).description).toBe('fixed(42, mock)'); - - // Description can be overridden. - expect(fixCodec(mockCodec, 42, 'my fixed').description).toBe('my fixed'); - }); - it('has the right sizes', () => { const mockCodec = getMockCodec({ size: null }); expect(fixCodec(mockCodec, 12).fixedSize).toBe(12); - expect(fixCodec(mockCodec, 12).maxSize).toBe(12); expect(fixCodec(mockCodec, 42).fixedSize).toBe(42); - expect(fixCodec(mockCodec, 42).maxSize).toBe(42); }); it('can fix a codec that requires a minimum amount of bytes', () => { // Given a mock `u32` codec that ensures the byte array is 4 bytes long. - const u32: Codec = { - decode(bytes, offset = 0): [number, number] { + const u32 = createCodec({ + fixedSize: 4, + read(bytes, offset = 0): [number, number] { // eslint-disable-next-line jest/no-conditional-in-test if (bytes.slice(offset).length < offset + 4) { throw new Error('Not enough bytes to decode a u32.'); } return [bytes.slice(offset)[0], offset + 4]; }, - description: 'u32', - encode: (value: number) => new Uint8Array([value, 0, 0, 0]), - fixedSize: 4, - maxSize: 4, - }; + write: (value: number, bytes, offset) => { + bytes.set([value], offset); + return offset + 4; + }, + }); // When we synthesize a `u24` from that `u32` using `fixCodec`. const u24 = fixCodec(u32, 3); // Then we can encode a `u24`. - const buf = u24.encode(42); - expect(buf).toStrictEqual(new Uint8Array([42, 0, 0])); + const bytes = u24.encode(42); + expect(bytes).toStrictEqual(new Uint8Array([42, 0, 0])); // And we can decode it back. - const hydrated = u24.decode(buf); + const hydrated = u24.read(bytes, 0); expect(hydrated).toStrictEqual([42, 3]); }); }); @@ -100,17 +101,29 @@ describe('fixEncoder', () => { it('can fix an encoder to a given amount of bytes', () => { const mockCodec = getMockCodec(); - mockCodec.encode.mockReturnValueOnce(b('08050c0c0f170f120c04')); + mockCodec.variableSize.mockReturnValueOnce(10); + mockCodec.write.mockImplementationOnce((_, bytes: Uint8Array, offset: number) => { + bytes.set(b('08050c0c0f170f120c04'), offset); + return offset + 10; + }); expect(fixEncoder(mockCodec, 10).encode('helloworld')).toStrictEqual(b('08050c0c0f170f120c04')); - expect(mockCodec.encode).toHaveBeenCalledWith('helloworld'); + expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0); - mockCodec.encode.mockReturnValueOnce(b('08050c0c0f170f120c04')); + mockCodec.variableSize.mockReturnValueOnce(10); + mockCodec.write.mockImplementationOnce((_, bytes: Uint8Array, offset: number) => { + bytes.set(b('08050c0c0f170f120c04'), offset); + return offset + 10; + }); expect(fixEncoder(mockCodec, 5).encode('helloworld')).toStrictEqual(b('08050c0c0f')); - expect(mockCodec.encode).toHaveBeenCalledWith('helloworld'); + expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0); - mockCodec.encode.mockReturnValueOnce(b('08050c0c0f')); + mockCodec.variableSize.mockReturnValueOnce(5); + mockCodec.write.mockImplementationOnce((_, bytes: Uint8Array, offset: number) => { + bytes.set(b('08050c0c0f'), offset); + return offset + 5; + }); expect(fixEncoder(mockCodec, 10).encode('hello')).toStrictEqual(b('08050c0c0f0000000000')); - expect(mockCodec.encode).toHaveBeenCalledWith('hello'); + expect(mockCodec.write).toHaveBeenCalledWith('hello', expect.any(Uint8Array), 0); }); }); @@ -119,13 +132,13 @@ describe('fixDecoder', () => { const mockCodec = getMockCodec(); fixDecoder(mockCodec, 10).decode(b('08050c0c0f170f120c04')); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f170f120c04'), 0); fixDecoder(mockCodec, 5).decode(b('08050c0c0f170f120c04')); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f'), 0); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f'), 0); fixDecoder(mockCodec, 10).decode(b('08050c0c0f0000000000')); - expect(mockCodec.decode).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0); + expect(mockCodec.read).toHaveBeenCalledWith(b('08050c0c0f0000000000'), 0); expect(() => fixDecoder(mockCodec, 10).decode(b('08050c0c0f'))).toThrow( 'Codec [fixCodec] expected 10 bytes, got 5.', diff --git a/packages/codecs-core/src/__tests__/map-codec-test.ts b/packages/codecs-core/src/__tests__/map-codec-test.ts index 8972305f987..dc30c1495bb 100644 --- a/packages/codecs-core/src/__tests__/map-codec-test.ts +++ b/packages/codecs-core/src/__tests__/map-codec-test.ts @@ -1,13 +1,14 @@ -import { Codec, Decoder, Encoder } from '../codec'; +import { Codec, createCodec, createDecoder, createEncoder, Decoder, Encoder } from '../codec'; import { mapCodec, mapDecoder, mapEncoder } from '../map-codec'; -const numberCodec: Codec = { - decode: (bytes: Uint8Array): [number, number] => [bytes[0], 1], - description: 'number', - encode: (value: number) => new Uint8Array([value]), +const numberCodec: Codec = createCodec({ fixedSize: 1, - maxSize: 1, -}; + read: (bytes: Uint8Array): [number, number] => [bytes[0], 1], + write: (value: number, bytes, offset) => { + bytes.set([value], offset); + return offset + 1; + }, +}); describe('mapCodec', () => { it('can loosen the codec input with a map', () => { @@ -18,10 +19,10 @@ describe('mapCodec', () => { ); const bytesA = mappedCodec.encode(42); - expect(mappedCodec.decode(bytesA)[0]).toBe(42); + expect(mappedCodec.decode(bytesA)).toBe(42); const bytesB = mappedCodec.encode('Hello world'); - expect(mappedCodec.decode(bytesB)[0]).toBe(11); + expect(mappedCodec.decode(bytesB)).toBe(11); }); it('can map both the input and output of a codec', () => { @@ -34,10 +35,10 @@ describe('mapCodec', () => { ); const bytesA = mappedCodec.encode(42); - expect(mappedCodec.decode(bytesA)[0]).toBe('x'.repeat(42)); + expect(mappedCodec.decode(bytesA)).toBe('x'.repeat(42)); const bytesB = mappedCodec.encode('Hello world'); - expect(mappedCodec.decode(bytesB)[0]).toBe('x'.repeat(11)); + expect(mappedCodec.decode(bytesB)).toBe('x'.repeat(11)); }); it('can map the input and output of a codec to the same type', () => { @@ -49,10 +50,10 @@ describe('mapCodec', () => { ); const bytesA = mappedCodec.encode('42'); - expect(mappedCodec.decode(bytesA)[0]).toBe('xx'); + expect(mappedCodec.decode(bytesA)).toBe('xx'); const bytesB = mappedCodec.encode('Hello world'); - expect(mappedCodec.decode(bytesB)[0]).toBe('xxxxxxxxxxx'); + expect(mappedCodec.decode(bytesB)).toBe('xxxxxxxxxxx'); }); it('can wrap a codec type in an object using a map', () => { @@ -65,25 +66,26 @@ describe('mapCodec', () => { ); const bytes = mappedCodec.encode({ value: 42 }); - expect(mappedCodec.decode(bytes)[0]).toStrictEqual({ value: 42 }); + expect(mappedCodec.decode(bytes)).toStrictEqual({ value: 42 }); }); it('map a codec to loosen its input by providing default values', () => { // Create Codec. type Strict = { discriminator: number; label: string }; - const strictCodec: Codec = { - decode: (bytes: Uint8Array): [Strict, number] => [ + const strictCodec: Codec = createCodec({ + fixedSize: 2, + read: (bytes: Uint8Array): [Strict, number] => [ { discriminator: bytes[0], label: 'x'.repeat(bytes[1]) }, 1, ], - description: 'Strict', - encode: (value: Strict) => new Uint8Array([value.discriminator, value.label.length]), - fixedSize: 2, - maxSize: 2, - }; + write: (value: Strict, bytes, offset) => { + bytes.set([value.discriminator, value.label.length], offset); + return offset + 2; + }, + }); const bytesA = strictCodec.encode({ discriminator: 5, label: 'Hello world' }); - expect(strictCodec.decode(bytesA)[0]).toStrictEqual({ + expect(strictCodec.decode(bytesA)).toStrictEqual({ discriminator: 5, label: 'xxxxxxxxxxx', }); @@ -100,30 +102,31 @@ describe('mapCodec', () => { // With explicit discriminator. const bytesB = looseCodec.encode({ discriminator: 5, label: 'Hello world' }); - expect(looseCodec.decode(bytesB)[0]).toStrictEqual({ + expect(looseCodec.decode(bytesB)).toStrictEqual({ discriminator: 5, label: 'xxxxxxxxxxx', }); // With implicit discriminator. const bytesC = looseCodec.encode({ label: 'Hello world' }); - expect(looseCodec.decode(bytesC)[0]).toStrictEqual({ + expect(looseCodec.decode(bytesC)).toStrictEqual({ discriminator: 42, label: 'xxxxxxxxxxx', }); }); it('can loosen a tuple codec', () => { - const codec: Codec<[number, string]> = { - decode: (bytes: Uint8Array): [[number, string], number] => [[bytes[0], 'x'.repeat(bytes[1])], 2], - description: 'Tuple', - encode: (value: [number, string]) => new Uint8Array([value[0], value[1].length]), + const codec: Codec<[number, string]> = createCodec({ fixedSize: 2, - maxSize: 2, - }; + read: (bytes: Uint8Array): [[number, string], number] => [[bytes[0], 'x'.repeat(bytes[1])], 2], + write: (value: [number, string], bytes, offset) => { + bytes.set([value[0], value[1].length], offset); + return offset + 2; + }, + }); const bytesA = codec.encode([42, 'Hello world']); - expect(codec.decode(bytesA)[0]).toStrictEqual([42, 'xxxxxxxxxxx']); + expect(codec.decode(bytesA)).toStrictEqual([42, 'xxxxxxxxxxx']); const mappedCodec = mapCodec(codec, (value: [number | null, string]): [number, string] => [ // eslint-disable-next-line jest/no-conditional-in-test @@ -132,45 +135,40 @@ describe('mapCodec', () => { ]); const bytesB = mappedCodec.encode([null, 'Hello world']); - expect(mappedCodec.decode(bytesB)[0]).toStrictEqual([11, 'xxxxxxxxxxx']); + expect(mappedCodec.decode(bytesB)).toStrictEqual([11, 'xxxxxxxxxxx']); const bytesC = mappedCodec.encode([42, 'Hello world']); - expect(mappedCodec.decode(bytesC)[0]).toStrictEqual([42, 'xxxxxxxxxxx']); + expect(mappedCodec.decode(bytesC)).toStrictEqual([42, 'xxxxxxxxxxx']); }); }); describe('mapEncoder', () => { it('can map an encoder to another encoder', () => { - const encoderA: Encoder = { - description: 'A', - encode: (value: number) => new Uint8Array([value]), + const encoderA: Encoder = createEncoder({ fixedSize: 1, - maxSize: 1, - }; + write: (value: number, bytes, offset) => { + bytes.set([value], offset); + return offset + 1; + }, + }); const encoderB = mapEncoder(encoderA, (value: string): number => value.length); - expect(encoderB.description).toBe('A'); expect(encoderB.fixedSize).toBe(1); - expect(encoderB.maxSize).toBe(1); expect(encoderB.encode('helloworld')).toStrictEqual(new Uint8Array([10])); }); }); describe('mapDecoder', () => { it('can map an encoder to another encoder', () => { - const decoder: Decoder = { - decode: (bytes: Uint8Array, offset = 0) => [bytes[offset], offset + 1], - description: 'A', + const decoder: Decoder = createDecoder({ fixedSize: 1, - maxSize: 1, - }; + read: (bytes: Uint8Array, offset = 0) => [bytes[offset], offset + 1], + }); const decoderB = mapDecoder(decoder, (value: number): string => 'x'.repeat(value)); - expect(decoderB.description).toBe('A'); expect(decoderB.fixedSize).toBe(1); - expect(decoderB.maxSize).toBe(1); - expect(decoderB.decode(new Uint8Array([10]))).toStrictEqual(['xxxxxxxxxx', 1]); + expect(decoderB.decode(new Uint8Array([10]))).toBe('xxxxxxxxxx'); }); }); diff --git a/packages/codecs-core/src/__tests__/reverse-codec-test.ts b/packages/codecs-core/src/__tests__/reverse-codec-test.ts index c850bae2194..67d70617a92 100644 --- a/packages/codecs-core/src/__tests__/reverse-codec-test.ts +++ b/packages/codecs-core/src/__tests__/reverse-codec-test.ts @@ -1,4 +1,4 @@ -import { Decoder, Encoder } from '../codec'; +import { createDecoder, createEncoder, Decoder, Encoder } from '../codec'; import { fixCodec } from '../fix-codec'; import { reverseCodec, reverseDecoder, reverseEncoder } from '../reverse-codec'; import { b, base16 } from './__setup__'; @@ -19,12 +19,12 @@ describe('reverseCodec', () => { expect(s(32).encode(`${'00'.repeat(31)}01`)).toStrictEqual(b(`01${'00'.repeat(31)}`)); // Decode. - expect(s(2).decode(b('ff00'))).toStrictEqual(['00ff', 2]); - expect(s(2).decode(b('00ff'))).toStrictEqual(['ff00', 2]); - expect(s(4).decode(b('00000001'))).toStrictEqual(['01000000', 4]); - expect(s(4).decode(b('01000000'))).toStrictEqual(['00000001', 4]); - expect(s(4).decode(b('aaaa01000000bbbb'), 2)).toStrictEqual(['00000001', 6]); - expect(s(4).decode(b('aaaa00000001bbbb'), 2)).toStrictEqual(['01000000', 6]); + expect(s(2).decode(b('ff00'))).toBe('00ff'); + expect(s(2).decode(b('00ff'))).toBe('ff00'); + expect(s(4).decode(b('00000001'))).toBe('01000000'); + expect(s(4).decode(b('01000000'))).toBe('00000001'); + expect(s(4).read(b('aaaa01000000bbbb'), 2)).toStrictEqual(['00000001', 6]); + expect(s(4).read(b('aaaa00000001bbbb'), 2)).toStrictEqual(['01000000', 6]); // Variable-size codec. expect(() => reverseCodec(base16)).toThrow('Cannot reverse a codec of variable size'); @@ -33,17 +33,16 @@ describe('reverseCodec', () => { describe('reverseEncoder', () => { it('can reverse the bytes of a fixed-size encoder', () => { - const encoder: Encoder = { - description: 'u16', - encode: (value: number) => new Uint8Array([value, 0]), + const encoder: Encoder = createEncoder({ fixedSize: 2, - maxSize: 2, - }; + write: (value: number, bytes, offset) => { + bytes.set([value, 0], offset); + return offset + 2; + }, + }); const reversedEncoder = reverseEncoder(encoder); - expect(reversedEncoder.description).toBe('u16'); expect(reversedEncoder.fixedSize).toBe(2); - expect(reversedEncoder.maxSize).toBe(2); expect(reversedEncoder.encode(42)).toStrictEqual(new Uint8Array([0, 42])); expect(() => reverseEncoder(base16)).toThrow('Cannot reverse a codec of variable size'); }); @@ -51,18 +50,14 @@ describe('reverseEncoder', () => { describe('reverseDecoder', () => { it('can reverse the bytes of a fixed-size decoder', () => { - const decoder: Decoder = { - decode: (bytes: Uint8Array, offset = 0) => [`${bytes[offset]}-${bytes[offset + 1]}`, offset + 2], - description: 'u16', + const decoder: Decoder = createDecoder({ fixedSize: 2, - maxSize: 2, - }; + read: (bytes: Uint8Array, offset = 0) => [`${bytes[offset]}-${bytes[offset + 1]}`, offset + 2], + }); const reversedDecoder = reverseDecoder(decoder); - expect(reversedDecoder.description).toBe('u16'); expect(reversedDecoder.fixedSize).toBe(2); - expect(reversedDecoder.maxSize).toBe(2); - expect(reversedDecoder.decode(new Uint8Array([42, 0]))).toStrictEqual(['0-42', 2]); + expect(reversedDecoder.read(new Uint8Array([42, 0]), 0)).toStrictEqual(['0-42', 2]); expect(() => reverseDecoder(base16)).toThrow('Cannot reverse a codec of variable size'); }); }); diff --git a/packages/codecs-core/src/assertions.ts b/packages/codecs-core/src/assertions.ts index 5a6cac7e8d0..f8e7e2aa578 100644 --- a/packages/codecs-core/src/assertions.ts +++ b/packages/codecs-core/src/assertions.ts @@ -1,5 +1,3 @@ -import { CodecData } from './codec'; - /** * Asserts that a given byte array is not empty. */ @@ -17,7 +15,7 @@ export function assertByteArrayHasEnoughBytesForCodec( codecDescription: string, expected: number, bytes: Uint8Array, - offset = 0, + offset = 0 ) { const bytesLength = bytes.length - offset; if (bytesLength < expected) { @@ -30,8 +28,8 @@ export function assertByteArrayHasEnoughBytesForCodec( * Asserts that a given codec is fixed-size codec. */ export function assertFixedSizeCodec( - data: Pick, - message?: string, + data: { fixedSize: number | null }, + message?: string ): asserts data is { fixedSize: number } { if (data.fixedSize === null) { // TODO: Coded error. diff --git a/packages/codecs-core/src/codec.ts b/packages/codecs-core/src/codec.ts index 2e0d02ff8f8..791c463ead9 100644 --- a/packages/codecs-core/src/codec.ts +++ b/packages/codecs-core/src/codec.ts @@ -4,36 +4,63 @@ export type Offset = number; /** - * The shared attributes between codecs, encoders and decoders. - */ -export type CodecData = { - /** A description for the codec. */ - description: string; - /** The fixed size of the encoded value in bytes, or `null` if it is variable. */ - fixedSize: number | null; - /** The maximum size an encoded value can be in bytes, or `null` if it is variable. */ - maxSize: number | null; + * An object that can encode a value to a `Uint8Array`. + */ +export type Encoder = EncoderSize & { + /** Encode the provided value and return the encoded bytes directly. */ + readonly encode: (value: T) => Uint8Array; + /** + * Writes the encoded value into the provided byte array at the given offset. + * Returns the offset of the next byte after the encoded value. + */ + readonly write: (value: T, bytes: Uint8Array, offset: Offset) => Offset; }; /** - * An object that can encode a value to a `Uint8Array`. + * Describes the size of an Encoder, either fixed or variable. */ -export type Encoder = CodecData & { - /** The function that encodes a value into bytes. */ - encode: (value: T) => Uint8Array; -}; +export type EncoderSize = + | { + /** The fixed size of the encoded value in bytes, if applicable. */ + readonly fixedSize: number; + } + | { + /** Otherwise, a null fixedSize indicates it's a variable size encoder. */ + readonly fixedSize: null; + /** The maximum size an encoded value can be in bytes, if applicable. */ + readonly maxSize?: number; + /** The total size of the encoded value in bytes. */ + readonly variableSize: (value: T) => number; + }; /** * An object that can decode a value from a `Uint8Array`. */ -export type Decoder = CodecData & { +export type Decoder = DecoderSize & { + /** Decodes the provided byte array at the given offset (or zero) and returns the value directly. */ + readonly decode: (bytes: Uint8Array, offset?: Offset) => T; /** - * The function that decodes a value from bytes. - * It returns the decoded value and the number of bytes read. + * Reads the encoded value from the provided byte array at the given offset. + * Returns the decoded value and the offset of the next byte after the encoded value. */ - decode: (bytes: Uint8Array, offset?: Offset) => [T, Offset]; + readonly read: (bytes: Uint8Array, offset: Offset) => [T, Offset]; }; +/** + * Describes the size of an Decoder, either fixed or variable. + */ +export type DecoderSize = + | { + /** The fixed size of the encoded value in bytes, if applicable. */ + readonly fixedSize: number; + } + | { + /** Otherwise, a null fixedSize indicates it's a variable size encoder. */ + readonly fixedSize: null; + /** The maximum size an encoded value can be in bytes, if applicable. */ + readonly maxSize?: number; + }; + /** * An object that can encode and decode a value to and from a `Uint8Array`. * It supports encoding looser types than it decodes for convenience. @@ -45,17 +72,61 @@ export type Decoder = CodecData & { */ export type Codec = Encoder & Decoder; -/** - * Defines common configurations for codec factories. - */ -export type BaseCodecConfig = { - /** A custom description for the Codec. */ - description?: string; -}; - /** * Wraps all the attributes of an object in Codecs. */ export type WrapInCodec = { [P in keyof T]: Codec; }; + +/** + * Get the encoded size of a given value in bytes. + */ +export function getEncodedSize(value: T, encoder: EncoderSize): number { + return encoder.fixedSize !== null ? encoder.fixedSize : encoder.variableSize(value); +} + +/** Fills the missing `encode` function using the existing `write` function. */ +export function createEncoder(encoder: EncoderSize & Omit, 'encode'>): Encoder { + return Object.freeze({ + ...(encoder.fixedSize === null + ? { fixedSize: null, maxSize: encoder.maxSize, variableSize: encoder.variableSize } + : { fixedSize: encoder.fixedSize }), + encode: (value: T) => { + const bytes = new Uint8Array(getEncodedSize(value, encoder)); + encoder.write(value, bytes, 0); + return bytes; + }, + write: encoder.write, + }); +} + +/** Fills the missing `decode` function using the existing `read` function. */ +export function createDecoder(decoder: DecoderSize & Omit, 'decode'>): Decoder { + return Object.freeze({ + ...(decoder.fixedSize === null + ? { fixedSize: null, maxSize: decoder.maxSize } + : { fixedSize: decoder.fixedSize }), + decode: (bytes: Uint8Array, offset?: Offset) => decoder.read(bytes, offset ?? 0)[0], + read: decoder.read, + }); +} + +/** Fills the missing `encode` and `decode` function using the existing `write` and `read` functions. */ +export function createCodec( + codec: EncoderSize & Omit, 'encode' | 'decode'> +): Codec { + return Object.freeze({ + ...(codec.fixedSize === null + ? { fixedSize: null, maxSize: codec.maxSize, variableSize: codec.variableSize } + : { fixedSize: codec.fixedSize }), + decode: (bytes: Uint8Array, offset?: Offset) => codec.read(bytes, offset ?? 0)[0], + encode: (value: From) => { + const bytes = new Uint8Array(getEncodedSize(value, codec)); + codec.write(value, bytes, 0); + return bytes; + }, + read: codec.read, + write: codec.write, + }); +} diff --git a/packages/codecs-core/src/combine-codec.ts b/packages/codecs-core/src/combine-codec.ts index 313d2f24d7c..2f10d658c0a 100644 --- a/packages/codecs-core/src/combine-codec.ts +++ b/packages/codecs-core/src/combine-codec.ts @@ -7,36 +7,21 @@ import { Codec, Decoder, Encoder } from './codec'; */ export function combineCodec( encoder: Encoder, - decoder: Decoder, - description?: string, + decoder: Decoder ): Codec { if (encoder.fixedSize !== decoder.fixedSize) { // TODO: Coded error. throw new Error( - `Encoder and decoder must have the same fixed size, got [${encoder.fixedSize}] and [${decoder.fixedSize}].`, + `Encoder and decoder must have the same fixed size, got [${encoder.fixedSize}] and [${decoder.fixedSize}].` ); } - if (encoder.maxSize !== decoder.maxSize) { + if (encoder.fixedSize === null && decoder.fixedSize === null && encoder.maxSize !== decoder.maxSize) { // TODO: Coded error. throw new Error( - `Encoder and decoder must have the same max size, got [${encoder.maxSize}] and [${decoder.maxSize}].`, + `Encoder and decoder must have the same max size, got [${encoder.maxSize}] and [${decoder.maxSize}].` ); } - if (description === undefined && encoder.description !== decoder.description) { - // TODO: Coded error. - throw new Error( - `Encoder and decoder must have the same description, got [${encoder.description}] and [${decoder.description}]. ` + - `Pass a custom description as a third argument if you want to override the description and bypass this error.`, - ); - } - - return { - decode: decoder.decode, - description: description ?? encoder.description, - encode: encoder.encode, - fixedSize: encoder.fixedSize, - maxSize: encoder.maxSize, - }; + return { ...decoder, ...encoder }; } diff --git a/packages/codecs-core/src/fix-codec.ts b/packages/codecs-core/src/fix-codec.ts index 0db3d8f9207..5ffa0881a23 100644 --- a/packages/codecs-core/src/fix-codec.ts +++ b/packages/codecs-core/src/fix-codec.ts @@ -1,16 +1,8 @@ import { assertByteArrayHasEnoughBytesForCodec } from './assertions'; import { fixBytes } from './bytes'; -import { Codec, CodecData, Decoder, Encoder } from './codec'; +import { Codec, createDecoder, createEncoder, Decoder, Encoder, Offset } from './codec'; import { combineCodec } from './combine-codec'; -function fixCodecHelper(data: CodecData, fixedBytes: number, description?: string): CodecData { - return { - description: description ?? `fixed(${fixedBytes}, ${data.description})`, - fixedSize: fixedBytes, - maxSize: fixedBytes, - }; -} - /** * Creates a fixed-size encoder from a given encoder. * @@ -18,11 +10,20 @@ function fixCodecHelper(data: CodecData, fixedBytes: number, description?: strin * @param fixedBytes - The fixed number of bytes to write. * @param description - A custom description for the encoder. */ -export function fixEncoder(encoder: Encoder, fixedBytes: number, description?: string): Encoder { - return { - ...fixCodecHelper(encoder, fixedBytes, description), - encode: (value: T) => fixBytes(encoder.encode(value), fixedBytes), - }; +export function fixEncoder(encoder: Encoder, fixedBytes: number): Encoder { + return createEncoder({ + fixedSize: fixedBytes, + write: (value: T, bytes: Uint8Array, offset: Offset) => { + // Here we exceptionally use the `encode` function instead of the `write` + // function as using the nested `write` function on a fixed-sized byte + // array may result in a out-of-bounds error on the nested encoder. + const variableByteArray = encoder.encode(value); + const fixedByteArray = + variableByteArray.length > fixedBytes ? variableByteArray.slice(0, fixedBytes) : variableByteArray; + bytes.set(fixedByteArray, offset); + return offset + fixedBytes; + }, + }); } /** @@ -32,10 +33,10 @@ export function fixEncoder(encoder: Encoder, fixedBytes: number, descripti * @param fixedBytes - The fixed number of bytes to read. * @param description - A custom description for the decoder. */ -export function fixDecoder(decoder: Decoder, fixedBytes: number, description?: string): Decoder { - return { - ...fixCodecHelper(decoder, fixedBytes, description), - decode: (bytes: Uint8Array, offset = 0) => { +export function fixDecoder(decoder: Decoder, fixedBytes: number): Decoder { + return createDecoder({ + fixedSize: fixedBytes, + read: (bytes: Uint8Array, offset: Offset) => { assertByteArrayHasEnoughBytesForCodec('fixCodec', fixedBytes, bytes, offset); // Slice the byte array to the fixed size if necessary. if (offset > 0 || bytes.length > fixedBytes) { @@ -46,10 +47,10 @@ export function fixDecoder(decoder: Decoder, fixedBytes: number, descripti bytes = fixBytes(bytes, decoder.fixedSize); } // Decode the value using the nested decoder. - const [value] = decoder.decode(bytes, 0); + const [value] = decoder.read(bytes, 0); return [value, offset + fixedBytes]; }, - }; + }); } /** @@ -59,10 +60,6 @@ export function fixDecoder(decoder: Decoder, fixedBytes: number, descripti * @param fixedBytes - The fixed number of bytes to read/write. * @param description - A custom description for the codec. */ -export function fixCodec( - codec: Codec, - fixedBytes: number, - description?: string, -): Codec { - return combineCodec(fixEncoder(codec, fixedBytes, description), fixDecoder(codec, fixedBytes, description)); +export function fixCodec(codec: Codec, fixedBytes: number): Codec { + return combineCodec(fixEncoder(codec, fixedBytes), fixDecoder(codec, fixedBytes)); } diff --git a/packages/codecs-core/src/map-codec.ts b/packages/codecs-core/src/map-codec.ts index 0fcdf82e253..87489fee005 100644 --- a/packages/codecs-core/src/map-codec.ts +++ b/packages/codecs-core/src/map-codec.ts @@ -1,15 +1,15 @@ -import { Codec, Decoder, Encoder } from './codec'; +import { Codec, createCodec, createDecoder, createEncoder, Decoder, Encoder } from './codec'; /** * Converts an encoder A to a encoder B by mapping their values. */ export function mapEncoder(encoder: Encoder, unmap: (value: U) => T): Encoder { - return { - description: encoder.description, - encode: (value: U) => encoder.encode(unmap(value)), - fixedSize: encoder.fixedSize, - maxSize: encoder.maxSize, - }; + return createEncoder({ + ...(encoder.fixedSize === null + ? { ...encoder, variableSize: (value: U) => encoder.variableSize(unmap(value)) } + : encoder), + write: (value: U, bytes, offset) => encoder.write(unmap(value), bytes, offset), + }); } /** @@ -19,15 +19,13 @@ export function mapDecoder( decoder: Decoder, map: (value: T, bytes: Uint8Array, offset: number) => U, ): Decoder { - return { - decode: (bytes: Uint8Array, offset = 0) => { - const [value, length] = decoder.decode(bytes, offset); - return [map(value, bytes, offset), length]; + return createDecoder({ + ...decoder, + read: (bytes: Uint8Array, offset = 0) => { + const [value, newOffset] = decoder.read(bytes, offset); + return [map(value, bytes, offset), newOffset]; }, - description: decoder.description, - fixedSize: decoder.fixedSize, - maxSize: decoder.maxSize, - }; + }); } /** @@ -47,11 +45,8 @@ export function mapCodec OldFrom, map?: (value: OldTo, bytes: Uint8Array, offset: number) => NewTo, ): Codec { - return { - decode: map ? mapDecoder(codec, map).decode : (codec.decode as unknown as Decoder['decode']), - description: codec.description, - encode: mapEncoder(codec, unmap).encode, - fixedSize: codec.fixedSize, - maxSize: codec.maxSize, - }; + return createCodec({ + ...mapEncoder(codec, unmap), + read: map ? mapDecoder(codec, map).read : (codec.read as unknown as Decoder['read']), + }); } diff --git a/packages/codecs-core/src/reverse-codec.ts b/packages/codecs-core/src/reverse-codec.ts index 41f15e1424e..27f8946ed2d 100644 --- a/packages/codecs-core/src/reverse-codec.ts +++ b/packages/codecs-core/src/reverse-codec.ts @@ -1,6 +1,5 @@ import { assertFixedSizeCodec } from './assertions'; -import { mergeBytes } from './bytes'; -import { Codec, Decoder, Encoder } from './codec'; +import { Codec, createDecoder, createEncoder, Decoder, Encoder } from './codec'; import { combineCodec } from './combine-codec'; /** @@ -8,10 +7,15 @@ import { combineCodec } from './combine-codec'; */ export function reverseEncoder(encoder: Encoder): Encoder { assertFixedSizeCodec(encoder, 'Cannot reverse a codec of variable size.'); - return { + return createEncoder({ ...encoder, - encode: (value: T) => encoder.encode(value).reverse(), - }; + write: (value: T, bytes, offset) => { + const newOffset = encoder.write(value, bytes, offset); + const slice = bytes.slice(offset, offset + encoder.fixedSize).reverse(); + bytes.set(slice, offset); + return newOffset; + }, + }); } /** @@ -19,21 +23,18 @@ export function reverseEncoder(encoder: Encoder): Encoder { */ export function reverseDecoder(decoder: Decoder): Decoder { assertFixedSizeCodec(decoder, 'Cannot reverse a codec of variable size.'); - return { + return createDecoder({ ...decoder, - decode: (bytes: Uint8Array, offset = 0) => { + read: (bytes, offset) => { const reverseEnd = offset + decoder.fixedSize; if (offset === 0 && bytes.length === reverseEnd) { - return decoder.decode(bytes.reverse(), offset); + return decoder.read(bytes.reverse(), offset); } - const newBytes = mergeBytes([ - ...(offset === 0 ? [] : [bytes.slice(0, offset)]), - bytes.slice(offset, reverseEnd).reverse(), - ...(bytes.length === reverseEnd ? [] : [bytes.slice(reverseEnd)]), - ]); - return decoder.decode(newBytes, offset); + const reversedBytes = bytes.slice(); + reversedBytes.set(bytes.slice(offset, reverseEnd).reverse(), offset); + return decoder.read(reversedBytes, offset); }, - }; + }); } /**