UNPKG

@solana/codecs-data-structures

Version:

Codecs for various data structures

232 lines (222 loc) • 10.5 kB
import { createDecoder, Decoder, FixedSizeDecoder, ReadonlyUint8Array, VariableSizeDecoder } from '@solana/codecs-core'; import { DrainOuterGeneric, getFixedSize, sumCodecSizes } from './utils'; /** * A function that builds a {@link Decoder} for a struct field whose shape * depends on the values of previously decoded fields in the same struct. * * The function receives a frozen snapshot of all fields that have been decoded * so far, in declaration order, and must return the {@link Decoder} that should * be used to read the current field from the byte stream. * * @typeParam TPriorFields - The shape of the fields that have already been * decoded by the time this factory is invoked. * @typeParam TValue - The type of the value produced by the returned decoder. * * @see {@link createDependentStructDecoder} */ export type DependentStructDecoderFieldFactory<TPriorFields extends Record<string, unknown>, TValue> = ( fields: Readonly<TPriorFields>, ) => Decoder<TValue>; /** * A fluent builder that accumulates field decoders for a struct whose later * fields may depend on the values of earlier ones. * * Each call to {@link DependentStructDecoderBuilder.field | `field`} returns a * new builder whose accumulated field type is widened by the newly added field. * Call {@link DependentStructDecoderBuilder.build | `build`} to obtain the * final {@link Decoder} once every field has been declared. * * The builder tracks at the type level whether the struct decoded so far has a * fixed size. Adding a {@link FixedSizeDecoder} preserves that property, while * adding a {@link VariableSizeDecoder} or a {@link DependentStructDecoderFieldFactory | field factory} * drops the builder to variable size and it stays variable thereafter. * * Instances of this type are immutable. Calling `field` does not mutate the * receiver; it returns a new builder. * * @typeParam TFields - The shape of the struct that has been accumulated so far. * @typeParam TIsFixedSize - `true` while every field added so far is a * {@link FixedSizeDecoder}, `false` once any variable size decoder or * factory has been added. * * @see {@link createDependentStructDecoder} */ export type DependentStructDecoderBuilder<TFields extends Record<string, unknown>, TIsFixedSize extends boolean> = { /** * Finalizes the builder and returns a {@link Decoder} that decodes each * declared field in turn, in the order they were added. * * Returns a {@link FixedSizeDecoder} when every field has been added with a * fixed size decoder, and a {@link VariableSizeDecoder} otherwise. */ build(): TIsFixedSize extends true ? FixedSizeDecoder<DrainOuterGeneric<TFields>> : VariableSizeDecoder<DrainOuterGeneric<TFields>>; /** * Adds a field decoded by a {@link VariableSizeDecoder}. Drops the builder * to variable size; subsequent {@link field} calls cannot raise it back to * fixed size. * * Adding a field that has already been declared on this builder is a * compile time error. */ field<TName extends string, TValue>( name: TName extends keyof TFields ? never : TName, decoder: VariableSizeDecoder<TValue>, ): DependentStructDecoderBuilder<DrainOuterGeneric<TFields & { [K in TName]: TValue }>, false>; /** * Adds a field whose decoder is built from a frozen snapshot of the fields * that precede it. Drops the builder to variable size since the byte * length of the produced decoder cannot be known at type time. * * Adding a field that has already been declared on this builder is a * compile time error. */ field<TName extends string, TValue>( name: TName extends keyof TFields ? never : TName, factory: DependentStructDecoderFieldFactory<TFields, TValue>, ): DependentStructDecoderBuilder<DrainOuterGeneric<TFields & { [K in TName]: TValue }>, false>; /** * Adds a field decoded by a {@link FixedSizeDecoder}. Preserves the fixed * size property of the builder. * * Adding a field that has already been declared on this builder is a * compile time error. */ field<TName extends string, TValue>( name: TName extends keyof TFields ? never : TName, decoder: FixedSizeDecoder<TValue>, ): DependentStructDecoderBuilder<DrainOuterGeneric<TFields & { [K in TName]: TValue }>, TIsFixedSize>; }; /** * Creates a fluent builder for a struct decoder whose later fields may depend * on the decoded values of earlier ones. * * Unlike {@link getStructDecoder}, which accepts a fixed array of named * decoders, this builder lets each field provide a factory that receives the * values that have already been decoded. This is useful for binary formats * where a count, version, or discriminator decoded near the start of the * struct controls how a later field must be parsed. * * The builder mirrors the fixed vs variable size behaviour of * {@link getStructDecoder}. The empty builder finishes to a * {@link FixedSizeDecoder} of size zero. Adding a {@link FixedSizeDecoder} * preserves the fixed size property and the sizes accumulate. Adding a * {@link VariableSizeDecoder} or a {@link DependentStructDecoderFieldFactory | field factory} * drops the builder to variable size, which is then preserved by every * subsequent {@link DependentStructDecoderBuilder.field | `field`} call. * * The returned builder is immutable; each {@link DependentStructDecoderBuilder.field | `field`} * call returns a new builder whose accumulated field type is widened by the * newly added field. Call {@link DependentStructDecoderBuilder.build | `build`} * to produce the final decoder. * * @remarks * Prefer {@link getStructDecoder} when every field's decoder is independent of * the values that precede it. Reach for this builder only when at least one * field needs to be parameterised by another. * * The encoder direction does not need a dependent variant. An encoder already * has access to the entire value when serialising, so the existing * {@link getStructEncoder} can be paired with the decoder returned by this * builder and combined with `combineCodec` to obtain a full codec. * * @example * Decoding a struct whose array length is read from an earlier field. * ```ts * import { getArrayDecoder } from '@solana/codecs-data-structures'; * import { getU8Decoder, getU32Decoder } from '@solana/codecs-numbers'; * * const decoder = createDependentStructDecoder() * .field('count', getU8Decoder()) * .field('values', fields => getArrayDecoder(getU32Decoder(), { size: fields.count })) * .build(); * * decoder.decode(new Uint8Array([0x02, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00])); * // { count: 2, values: [1, 2] } * ``` * * @example * Mixing static and dependent fields, with a discriminator selecting the payload decoder. * ```ts * const decoder = createDependentStructDecoder() * .field('version', getU8Decoder()) * .field('header', fields => fields.version === 0 ? getU16Decoder() : getU32Decoder()) * .build(); * ``` * * @example * Combining the dependent decoder with a static encoder to obtain a full codec. * ```ts * import { combineCodec } from '@solana/codecs-core'; * * const encoder = getStructEncoder([ * ['count', getU8Encoder()], * ['values', getArrayEncoder(getU32Encoder())], * ]); * const decoder = createDependentStructDecoder() * .field('count', getU8Decoder()) * .field('values', fields => getArrayDecoder(getU32Decoder(), { size: fields.count })) * .build(); * const codec = combineCodec(encoder, decoder); * ``` * * @see {@link getStructDecoder} * @see {@link getStructEncoder} */ export function createDependentStructDecoder(): DependentStructDecoderBuilder<Record<never, never>, true> { return buildFromEntries([]); } type InternalEntry = Readonly<{ name: string; resolveDecoder: (fields: Record<string, unknown>) => Decoder<unknown>; /** * The static decoder for this entry, if the entry was added with one * directly. Absent when the entry was added with a factory, in which case * the entry is treated as variable size for the purposes of computing the * struct's total fixed size. */ staticDecoder: Decoder<unknown> | undefined; }>; function buildFromEntries<TFields extends Record<string, unknown>, TIsFixedSize extends boolean>( entries: readonly InternalEntry[], ): DependentStructDecoderBuilder<TFields, TIsFixedSize> { return { build() { const staticDecoders = entries.map(e => e.staticDecoder); const everyStatic = staticDecoders.every((d): d is Decoder<unknown> => d !== undefined); const fixedSize = everyStatic ? sumCodecSizes(staticDecoders.map(getFixedSize)) : null; const read = ( bytes: ReadonlyUint8Array | Uint8Array, offset: number, ): [DrainOuterGeneric<TFields>, number] => { const decoded: Record<string, unknown> = {}; for (const { name, resolveDecoder } of entries) { const decoder = resolveDecoder(decoded); const [value, newOffset] = decoder.read(bytes, offset); decoded[name] = value; offset = newOffset; } return [decoded as DrainOuterGeneric<TFields>, offset]; }; const decoder = fixedSize === null ? createDecoder({ read }) : createDecoder({ fixedSize, read }); return decoder as never; }, field<TName extends string, TValue>( name: TName, decoderOrFactory: Decoder<TValue> | DependentStructDecoderFieldFactory<TFields, TValue>, ): DependentStructDecoderBuilder<Record<string, unknown>, boolean> { const isFactory = typeof decoderOrFactory === 'function'; const resolveDecoder = isFactory ? (fields: Record<string, unknown>) => decoderOrFactory(Object.freeze({ ...fields }) as Readonly<TFields>) : () => decoderOrFactory; const nextEntry: InternalEntry = { name, resolveDecoder, staticDecoder: isFactory ? undefined : decoderOrFactory, }; return buildFromEntries([...entries, nextEntry]); }, } as DependentStructDecoderBuilder<TFields, TIsFixedSize>; }