-
Notifications
You must be signed in to change notification settings - Fork 800
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(experimental): add resizeCodec helper to @solana/codecs-core (…
…#2293) This PR adds a new `resizeCodec` helper function. See copy/pasted documentation below: --- ## Adjusting the size of codecs The `resizeCodec` helper re-defines the size of a given codec by accepting a function that takes the current size of the codec and returns a new size. This works for both fixed-size and variable-size codecs. ```ts // Fixed-size codec. const getBiggerU32Codec = () => resizeCodec(getU32Codec(), size => size + 4); getBiggerU32Codec().encode(42); // 0x2a00000000000000 // | └-- Empty buffer space caused by the resizeCodec function. // └-- Our encoded u32 number. // Variable-size codec. const getBiggerStringCodec = () => resizeCodec(getStringCodec(), size => size + 4); getBiggerStringCodec().encode('ABC'); // 0x0300000041424300000000 // | └-- Empty buffer space caused by the resizeCodec function. // └-- Our encoded string with a 4-byte size prefix. ``` Note that the `resizeCodec` function doesn't change any encoded or decoded bytes, it merely tells the `encode` and `decode` functions how big the `Uint8Array` should be before delegating to their respective `write` and `read` functions. In fact, this is completely bypassed when using the `write` and `read` functions directly. For instance: ```ts const getBiggerU32Codec = () => resizeCodec(getU32Codec(), size => size + 4); // Using the encode function. getBiggerU32Codec().encode(42); // 0x2a00000000000000 // Using the lower-level write function. const myCustomBytes = new Uint8Array(4); getBiggerU32Codec().write(42, myCustomBytes, 0); // 0x2a000000 ``` So when would it make sense to use the `resizeCodec` function? This function is particularly useful when combined with the `offsetCodec` function described below. Whilst the `offsetCodec` may help us push the offset forward — e.g. to skip some padding — it won't change the size of the encoded data which means the last bytes will be truncated by how much we pushed the offset forward. The `resizeCodec` function can be used to fix that. For instance, here's how we can use the `resizeCodec` and the `offsetCodec` functions together to create a struct codec that includes some padding. ```ts const personCodec = getStructCodec([ ['name', getStringCodec({ size: 8 })], // There is a 4-byte padding between name and age. [ 'age', offsetCodec( resizeCodec(getU32Codec(), size => size + 4), ({ preOffset }) => preOffset + 4, ), ], ]); personCodec.encode({ name: 'Alice', age: 42 }); // 0x416c696365000000000000002a000000 // | | └-- Our encoded u32 (42). // | └-- The 4-bytes of padding we are skipping. // └-- Our 8-byte encoded string ("Alice"). ``` As usual, the `resizeEncoder` and `resizeDecoder` functions can also be used to achieve that. ```ts const getBiggerU32Encoder = () => resizeEncoder(getU32Codec(), size => size + 4); const getBiggerU32Decoder = () => resizeDecoder(getU32Codec(), size => size + 4); const getBiggerU32Codec = () => combineCodec(getBiggerU32Encoder(), getBiggerU32Decoder()); ```
- Loading branch information
1 parent
650707b
commit 606de63
Showing
9 changed files
with
301 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, SolanaError } from '@solana/errors'; | ||
|
||
import { FixedSizeCodec } from '../codec'; | ||
import { resizeCodec } from '../resize-codec'; | ||
import { getMockCodec } from './__setup__'; | ||
|
||
describe('resizeCodec', () => { | ||
it('resizes fixed-size codecs', () => { | ||
const mockCodec = getMockCodec({ size: 42 }) as FixedSizeCodec<unknown, 42>; | ||
expect(resizeCodec(mockCodec, size => size + 1).fixedSize).toBe(43); | ||
expect(resizeCodec(mockCodec, size => size * 2).fixedSize).toBe(84); | ||
expect(resizeCodec(mockCodec, () => 0).fixedSize).toBe(0); | ||
}); | ||
|
||
it('resizes variable-size codecs', () => { | ||
const mockCodec = getMockCodec(); | ||
mockCodec.getSizeFromValue.mockReturnValue(42); | ||
expect(resizeCodec(mockCodec, size => size + 1).getSizeFromValue(null)).toBe(43); | ||
expect(resizeCodec(mockCodec, size => size * 2).getSizeFromValue(null)).toBe(84); | ||
expect(resizeCodec(mockCodec, () => 0).getSizeFromValue(null)).toBe(0); | ||
}); | ||
|
||
it('throws when fixed-size codecs have negative sizes', () => { | ||
const mockCodec = getMockCodec({ size: 42 }) as FixedSizeCodec<unknown, 42>; | ||
expect(() => resizeCodec(mockCodec, size => size - 100).fixedSize).toThrow( | ||
new SolanaError(SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, { | ||
bytesLength: -58, | ||
codecDescription: 'resizeEncoder', | ||
}), | ||
); | ||
}); | ||
|
||
it('throws when variable-size codecs have negative sizes', () => { | ||
const mockCodec = getMockCodec(); | ||
mockCodec.getSizeFromValue.mockReturnValue(42); | ||
expect(() => resizeCodec(mockCodec, size => size - 100).getSizeFromValue(null)).toThrow( | ||
new SolanaError(SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, { | ||
bytesLength: -58, | ||
codecDescription: 'resizeEncoder', | ||
}), | ||
); | ||
}); | ||
}); |
77 changes: 77 additions & 0 deletions
77
packages/codecs-core/src/__typetests__/resize-codec-typetest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { | ||
Codec, | ||
Decoder, | ||
Encoder, | ||
FixedSizeCodec, | ||
FixedSizeDecoder, | ||
FixedSizeEncoder, | ||
VariableSizeCodec, | ||
VariableSizeDecoder, | ||
VariableSizeEncoder, | ||
} from '../codec'; | ||
import { resizeCodec, resizeDecoder, resizeEncoder } from '../resize-codec'; | ||
|
||
type NumberToArray<N extends number, T extends unknown[] = []> = T['length'] extends N | ||
? T | ||
: NumberToArray<N, [...T, unknown]>; | ||
type Increment<N extends number> = [...NumberToArray<N>, unknown]['length']; | ||
|
||
{ | ||
// [resizeEncoder]: It returns the same encoder type as the one provided for non-fixed size encoders. | ||
type BrandedEncoder = Encoder<42> & { readonly __brand: unique symbol }; | ||
const resize = (size: number) => size * 2; | ||
resizeEncoder({} as BrandedEncoder, resize) satisfies BrandedEncoder; | ||
resizeEncoder({} as VariableSizeEncoder<string>, resize) satisfies VariableSizeEncoder<string>; | ||
resizeEncoder({} as Encoder<string>, resize) satisfies Encoder<string>; | ||
} | ||
|
||
{ | ||
// [resizeEncoder]: It uses the resize ReturnType as size for fixed-size encoders. | ||
const doubleResize = (size: number): number => size * 2; | ||
const encoder = {} as FixedSizeEncoder<string, 42>; | ||
resizeEncoder(encoder, doubleResize) satisfies FixedSizeEncoder<string, number>; | ||
// @ts-expect-error We no longer know if the fixed size is 42. | ||
resizeEncoder(encoder, doubleResize) satisfies FixedSizeEncoder<string, 42>; | ||
const incrementResize = <TSize extends number>(size: TSize) => (size + 1) as Increment<TSize>; | ||
resizeEncoder(encoder, incrementResize) satisfies FixedSizeEncoder<string, 43>; | ||
} | ||
|
||
{ | ||
// [resizeDecoder]: It returns the same decoder type as the one provided for non-fixed size decoders. | ||
type BrandedDecoder = Decoder<42> & { readonly __brand: unique symbol }; | ||
const resize = (size: number) => size * 2; | ||
resizeDecoder({} as BrandedDecoder, resize) satisfies BrandedDecoder; | ||
resizeDecoder({} as VariableSizeDecoder<string>, resize) satisfies VariableSizeDecoder<string>; | ||
resizeDecoder({} as Decoder<string>, resize) satisfies Decoder<string>; | ||
} | ||
|
||
{ | ||
// [resizeDecoder]: It uses the resize ReturnType as size for fixed-size decoders. | ||
const doubleResize = (size: number): number => size * 2; | ||
const decoder = {} as FixedSizeDecoder<string, 42>; | ||
resizeDecoder(decoder, doubleResize) satisfies FixedSizeDecoder<string, number>; | ||
// @ts-expect-error We no longer know if the fixed size is 42. | ||
resizeDecoder(decoder, doubleResize) satisfies FixedSizeDecoder<string, 42>; | ||
const incrementResize = <TSize extends number>(size: TSize) => (size + 1) as Increment<TSize>; | ||
resizeDecoder(decoder, incrementResize) satisfies FixedSizeDecoder<string, 43>; | ||
} | ||
|
||
{ | ||
// [resizeCodec]: It returns the same codec type as the one provided for non-fixed size codecs. | ||
type BrandedCodec = Codec<42> & { readonly __brand: unique symbol }; | ||
const resize = (size: number) => size * 2; | ||
resizeCodec({} as BrandedCodec, resize) satisfies BrandedCodec; | ||
resizeCodec({} as VariableSizeCodec<string>, resize) satisfies VariableSizeCodec<string>; | ||
resizeCodec({} as Codec<string>, resize) satisfies Codec<string>; | ||
} | ||
|
||
{ | ||
// [resizeCodec]: It uses the resize ReturnType as size for fixed-size codecs. | ||
const doubleResize = (size: number): number => size * 2; | ||
const codec = {} as FixedSizeCodec<string, string, 42>; | ||
resizeCodec(codec, doubleResize) satisfies FixedSizeCodec<string, string, number>; | ||
// @ts-expect-error We no longer know if the fixed size is 42. | ||
resizeCodec(codec, doubleResize) satisfies FixedSizeCodec<string, 42>; | ||
const incrementResize = <TSize extends number>(size: TSize) => (size + 1) as Increment<TSize>; | ||
resizeCodec(codec, incrementResize) satisfies FixedSizeCodec<string, string, 43>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, SolanaError } from '@solana/errors'; | ||
|
||
import { | ||
Codec, | ||
createDecoder, | ||
createEncoder, | ||
Decoder, | ||
Encoder, | ||
FixedSizeCodec, | ||
FixedSizeDecoder, | ||
FixedSizeEncoder, | ||
isFixedSize, | ||
} from './codec'; | ||
import { combineCodec } from './combine-codec'; | ||
|
||
/** | ||
* Updates the size of a given encoder. | ||
*/ | ||
export function resizeEncoder<TFrom, TSize extends number, TNewSize extends number>( | ||
encoder: FixedSizeEncoder<TFrom, TSize>, | ||
resize: (size: TSize) => TNewSize, | ||
): FixedSizeEncoder<TFrom, TNewSize>; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeEncoder<TEncoder extends Encoder<any>>( | ||
encoder: TEncoder, | ||
resize: (size: number) => number, | ||
): TEncoder; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeEncoder<TEncoder extends Encoder<any>>( | ||
encoder: TEncoder, | ||
resize: (size: number) => number, | ||
): TEncoder { | ||
if (isFixedSize(encoder)) { | ||
const fixedSize = resize(encoder.fixedSize); | ||
if (fixedSize < 0) { | ||
throw new SolanaError(SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, { | ||
bytesLength: fixedSize, | ||
codecDescription: 'resizeEncoder', | ||
}); | ||
} | ||
return createEncoder({ ...encoder, fixedSize }) as TEncoder; | ||
} | ||
return createEncoder({ | ||
...encoder, | ||
getSizeFromValue: value => { | ||
const newSize = resize(encoder.getSizeFromValue(value)); | ||
if (newSize < 0) { | ||
throw new SolanaError(SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, { | ||
bytesLength: newSize, | ||
codecDescription: 'resizeEncoder', | ||
}); | ||
} | ||
return newSize; | ||
}, | ||
}) as TEncoder; | ||
} | ||
|
||
/** | ||
* Updates the size of a given decoder. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
|
||
export function resizeDecoder<TFrom, TSize extends number, TNewSize extends number>( | ||
decoder: FixedSizeDecoder<TFrom, TSize>, | ||
resize: (size: TSize) => TNewSize, | ||
): FixedSizeDecoder<TFrom, TNewSize>; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeDecoder<TDecoder extends Decoder<any>>( | ||
decoder: TDecoder, | ||
resize: (size: number) => number, | ||
): TDecoder; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeDecoder<TDecoder extends Decoder<any>>( | ||
decoder: TDecoder, | ||
resize: (size: number) => number, | ||
): TDecoder { | ||
if (isFixedSize(decoder)) { | ||
const fixedSize = resize(decoder.fixedSize); | ||
if (fixedSize < 0) { | ||
throw new SolanaError(SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH, { | ||
bytesLength: fixedSize, | ||
codecDescription: 'resizeDecoder', | ||
}); | ||
} | ||
return createDecoder({ ...decoder, fixedSize }) as TDecoder; | ||
} | ||
return decoder; | ||
} | ||
|
||
/** | ||
* Updates the size of a given codec. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeCodec<TFrom, TTo extends TFrom, TSize extends number, TNewSize extends number>( | ||
codec: FixedSizeCodec<TFrom, TTo, TSize>, | ||
resize: (size: TSize) => TNewSize, | ||
): FixedSizeCodec<TFrom, TTo, TNewSize>; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeCodec<TCodec extends Codec<any>>(codec: TCodec, resize: (size: number) => number): TCodec; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function resizeCodec<TCodec extends Codec<any>>(codec: TCodec, resize: (size: number) => number): TCodec { | ||
return combineCodec(resizeEncoder(codec, resize), resizeDecoder(codec, resize)) as TCodec; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters