From 9eb48a366f9d3c28f18590d30df96fb2b5227a23 Mon Sep 17 00:00:00 2001 From: Iwo Plaza Date: Wed, 11 Sep 2024 13:33:16 +0200 Subject: [PATCH] Initial implementation of TypedArray support. (#11) --- apps/typed-binary-docs/astro.config.mjs | 1 + .../src/content/docs/guides/typed-arrays.mdx | 37 ++++++ cypress/e2e/bufferReader.cy.ts | 27 ++++ cypress/e2e/bufferWriter.cy.ts | 34 ++++- packages/typed-binary/src/describe/index.ts | 28 ++++ packages/typed-binary/src/io/bufferIOBase.ts | 12 +- packages/typed-binary/src/io/bufferReader.ts | 17 +++ .../src/io/bufferReaderWriter.test.ts | 124 ++++++++++++++++++ packages/typed-binary/src/io/bufferWriter.ts | 9 ++ packages/typed-binary/src/io/types.ts | 4 + .../typed-binary/src/io/unwrapBuffer.test.ts | 33 +++++ packages/typed-binary/src/io/unwrapBuffer.ts | 15 +++ .../typed-binary/src/structure/typedArray.ts | 46 +++++++ .../typed-binary/src/test/typedArray.test.ts | 94 +++++++++++++ 14 files changed, 470 insertions(+), 11 deletions(-) create mode 100644 apps/typed-binary-docs/src/content/docs/guides/typed-arrays.mdx create mode 100644 cypress/e2e/bufferReader.cy.ts create mode 100644 packages/typed-binary/src/io/unwrapBuffer.test.ts create mode 100644 packages/typed-binary/src/io/unwrapBuffer.ts create mode 100644 packages/typed-binary/src/structure/typedArray.ts create mode 100644 packages/typed-binary/src/test/typedArray.test.ts diff --git a/apps/typed-binary-docs/astro.config.mjs b/apps/typed-binary-docs/astro.config.mjs index 82944bb..704a626 100644 --- a/apps/typed-binary-docs/astro.config.mjs +++ b/apps/typed-binary-docs/astro.config.mjs @@ -30,6 +30,7 @@ export default defineConfig({ { label: 'Objects', slug: 'guides/objects' }, { label: 'Arrays and Tuples', slug: 'guides/arrays-and-tuples' }, { label: 'Optionals', slug: 'guides/optionals' }, + { label: 'Typed Arrays', slug: 'guides/typed-arrays' }, { label: 'Recursive Types', slug: 'guides/recursive-types' }, { label: 'Custom Schema Types', diff --git a/apps/typed-binary-docs/src/content/docs/guides/typed-arrays.mdx b/apps/typed-binary-docs/src/content/docs/guides/typed-arrays.mdx new file mode 100644 index 0000000..e78d21d --- /dev/null +++ b/apps/typed-binary-docs/src/content/docs/guides/typed-arrays.mdx @@ -0,0 +1,37 @@ +--- +title: Typed Arrays +description: A guide on how typed arrays can be represented in Typed Binary +--- + +Sometimes binary is the format that we want to work with directly. [Typed array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays) schemas +allow that data to be nested in plain objects or arrays. + +```ts +const Image = object({ + size: tupleOf([u32, u32]), + bitmap: u8Array(256 * 256), +}); + +// { +// size: [number, number]; +// bitmap: Uint8Array; +// } +const image = Image.read(...); + +image.size // [number, number] +image.bitmap // Uint8Array +``` + +Below is the list of available typed array schemas. + +Schema constructor | Encoded as | JavaScript value +---|---|--- +`u8Array` | consecutive 8-bit unsigned integers | `Uint8Array` +`u8ClampedArray` | consecutive 8-bit unsigned integers | `Uint8ClampedArray` +`u16Array` | consecutive 16-bit unsigned integers | `Uint16Array` +`u32Array` | consecutive 32-bit unsigned integers | `Uint32Array` +`i8Array` | consecutive 8-bit signed integers | `Int8Array` +`i16Array` | consecutive 16-bit signed integers | `Int16Array` +`i32Array` | consecutive 32-bit signed integers | `Int32Array` +`f32Array` | consecutive 32-bit floats | `Float32Array` +`f64Array` | consecutive 64-bit floats | `Float64Array` diff --git a/cypress/e2e/bufferReader.cy.ts b/cypress/e2e/bufferReader.cy.ts new file mode 100644 index 0000000..69b7b27 --- /dev/null +++ b/cypress/e2e/bufferReader.cy.ts @@ -0,0 +1,27 @@ +import { BufferReader } from 'typed-binary'; + +describe('BufferReader', () => { + it('reads a uint32 from an ArrayBuffer', () => { + const buffer = new ArrayBuffer(64); + const u32Array = new Uint32Array(buffer); + u32Array[0] = 256; + + const reader = new BufferReader(buffer); + expect(reader.readUint32()).to.equal(256); + }); + + it('reads an int32 array from an ArrayBuffer', () => { + const buffer = new ArrayBuffer(3 * 4); + const i32View = new Int32Array(buffer); + + i32View[0] = 1; + i32View[1] = 2; + i32View[2] = 3; + + const i32Array = new Int32Array(3); + const reader = new BufferReader(buffer); + reader.readSlice(i32Array, 0, i32Array.byteLength); + + expect([...i32Array]).to.deep.eq([1, 2, 3]); + }); +}); diff --git a/cypress/e2e/bufferWriter.cy.ts b/cypress/e2e/bufferWriter.cy.ts index e191227..d326803 100644 --- a/cypress/e2e/bufferWriter.cy.ts +++ b/cypress/e2e/bufferWriter.cy.ts @@ -1,13 +1,39 @@ -import { BufferReader, BufferWriter } from 'typed-binary'; +import { BufferWriter } from 'typed-binary'; describe('BufferWriter', () => { - it('writes and reads from an ArrayBuffer', () => { + it('writes a uint32 from an ArrayBuffer', () => { const buffer = new ArrayBuffer(64); const writer = new BufferWriter(buffer); writer.writeUint32(256); - const reader = new BufferReader(buffer); - expect(reader.readUint32()).to.equal(256); + const u32View = new Uint32Array(buffer); + expect(u32View[0]).to.equal(256); + }); + + it('writes an int32 array to an ArrayBuffer', () => { + const buffer = new ArrayBuffer(64); + const writer = new BufferWriter(buffer); + + const i32Array = new Int32Array([1, 2, 3]); + writer.writeSlice(i32Array); + + const i32View = new Int32Array(buffer); + expect(i32View[0]).to.equal(1); + expect(i32View[1]).to.equal(2); + expect(i32View[2]).to.equal(3); + }); + + it('writes a uint32 array to an ArrayBuffer', () => { + const buffer = new ArrayBuffer(64); + const writer = new BufferWriter(buffer); + + const u32Array = new Uint32Array([1, 2, 3]); + writer.writeSlice(u32Array); + + const u32View = new Uint32Array(buffer); + expect(u32View[0]).to.equal(1); + expect(u32View[1]).to.equal(2); + expect(u32View[2]).to.equal(3); }); }); diff --git a/packages/typed-binary/src/describe/index.ts b/packages/typed-binary/src/describe/index.ts index 3abf450..92ac68d 100644 --- a/packages/typed-binary/src/describe/index.ts +++ b/packages/typed-binary/src/describe/index.ts @@ -9,6 +9,7 @@ import { DynamicArraySchema } from '../structure/dynamicArray'; import { KeyedSchema } from '../structure/keyed'; import { type AnyObjectSchema, GenericObjectSchema } from '../structure/object'; import { OptionalSchema } from '../structure/optional'; +import { TypedArraySchema } from '../structure/typedArray'; import type { AnySchema, AnySchemaWithProperties, @@ -56,6 +57,33 @@ export const tupleOf = ( schemas: TSchema, ) => new TupleSchema(schemas); +export const u8Array = (length: number) => + new TypedArraySchema(length, Uint8Array); + +export const u8ClampedArray = (length: number) => + new TypedArraySchema(length, Uint8ClampedArray); + +export const u16Array = (length: number) => + new TypedArraySchema(length, Uint16Array); + +export const u32Array = (length: number) => + new TypedArraySchema(length, Uint32Array); + +export const i8Array = (length: number) => + new TypedArraySchema(length, Int8Array); + +export const i16Array = (length: number) => + new TypedArraySchema(length, Int16Array); + +export const i32Array = (length: number) => + new TypedArraySchema(length, Int32Array); + +export const f32Array = (length: number) => + new TypedArraySchema(length, Float32Array); + +export const f64Array = (length: number) => + new TypedArraySchema(length, Float64Array); + export const optional = (innerType: TSchema) => new OptionalSchema(innerType); diff --git a/packages/typed-binary/src/io/bufferIOBase.ts b/packages/typed-binary/src/io/bufferIOBase.ts index 692912a..9362e1c 100644 --- a/packages/typed-binary/src/io/bufferIOBase.ts +++ b/packages/typed-binary/src/io/bufferIOBase.ts @@ -1,5 +1,6 @@ import { getSystemEndianness } from '../util'; import type { Endianness } from './types'; +import { unwrapBuffer } from './unwrapBuffer'; export type BufferIOOptions = { /** @@ -33,14 +34,11 @@ export class BufferIOBase { this.endianness = endianness === 'system' ? systemEndianness : endianness; this.switchEndianness = this.endianness !== systemEndianness; - let innerBuffer = buffer; - if (typeof Buffer !== 'undefined' && innerBuffer instanceof Buffer) { - // Getting rid of the outer shell, which causes the Uint8Array line to create a copy, instead of a view. - this.byteOffset += innerBuffer.byteOffset; - innerBuffer = innerBuffer.buffer; - } + // Getting rid of the outer shell, which causes the Uint8Array line to create a copy, instead of a view. + const unwrapped = unwrapBuffer(buffer); + this.byteOffset += unwrapped.byteOffset; - this.uint8View = new Uint8Array(innerBuffer, 0); + this.uint8View = new Uint8Array(unwrapped.buffer, 0); const helperBuffer = new ArrayBuffer(4); this.helperInt32View = new Int32Array(helperBuffer); diff --git a/packages/typed-binary/src/io/bufferReader.ts b/packages/typed-binary/src/io/bufferReader.ts index f497a97..a814f96 100644 --- a/packages/typed-binary/src/io/bufferReader.ts +++ b/packages/typed-binary/src/io/bufferReader.ts @@ -1,5 +1,6 @@ import { BufferIOBase } from './bufferIOBase'; import type { ISerialInput } from './types'; +import { unwrapBuffer } from './unwrapBuffer'; export class BufferReader extends BufferIOBase implements ISerialInput { private copyInputToHelper(bytes: number) { @@ -46,4 +47,20 @@ export class BufferReader extends BufferIOBase implements ISerialInput { return contents; } + + readSlice( + bufferView: ArrayLike & ArrayBufferView, + offset: number, + byteLength: number, + ): void { + const unwrapped = unwrapBuffer(bufferView); + const destU8 = new Uint8Array( + unwrapped.buffer, + unwrapped.byteOffset + offset, + ); + + for (let i = 0; i < byteLength; ++i) { + destU8[i] = this.uint8View[this.byteOffset++]; + } + } } diff --git a/packages/typed-binary/src/io/bufferReaderWriter.test.ts b/packages/typed-binary/src/io/bufferReaderWriter.test.ts index 80868a8..1ab7f07 100644 --- a/packages/typed-binary/src/io/bufferReaderWriter.test.ts +++ b/packages/typed-binary/src/io/bufferReaderWriter.test.ts @@ -4,6 +4,130 @@ import { getSystemEndianness } from '../util'; import { BufferReader } from './bufferReader'; import { BufferWriter } from './bufferWriter'; +describe('BufferWriter', () => { + it('should encode a Uint8Array', () => { + const uint8s = new Uint8Array([0, 1, 1, 2, 3, 5, 8, 255]); + + const buffer = Buffer.alloc(8); + const writer = new BufferWriter(buffer); + writer.writeSlice(uint8s); + + const reader = new BufferReader(buffer); + expect(reader.readByte()).to.eq(0); + expect(reader.readByte()).to.eq(1); + expect(reader.readByte()).to.eq(1); + expect(reader.readByte()).to.eq(2); + expect(reader.readByte()).to.eq(3); + expect(reader.readByte()).to.eq(5); + expect(reader.readByte()).to.eq(8); + expect(reader.readByte()).to.eq(255); + }); + + it('should encode a Int32Array', () => { + const int32s = new Int32Array([ + 0, + 1, + 1, + 2, + 3, + 5, + -2_147_483_648, // min signed 32-bit integer value + 2_147_483_647, // max signed 32-bit integer value + ]); + + const buffer = Buffer.alloc(8 * 4); + const writer = new BufferWriter(buffer); + writer.writeSlice(int32s); + + const reader = new BufferReader(buffer); + expect(reader.readInt32()).to.eq(0); + expect(reader.readInt32()).to.eq(1); + expect(reader.readInt32()).to.eq(1); + expect(reader.readInt32()).to.eq(2); + expect(reader.readInt32()).to.eq(3); + expect(reader.readInt32()).to.eq(5); + expect(reader.readInt32()).to.eq(-2_147_483_648); + expect(reader.readInt32()).to.eq(2_147_483_647); + }); + + it('should encode a Uint32Array', () => { + const uint32s = new Uint32Array([ + 0, + 1, + 1, + 2, + 3, + 5, + 8, + 4_294_967_295, // max unsigned 32-bit integer value + ]); + + const buffer = Buffer.alloc(8 * 4); + const writer = new BufferWriter(buffer); + writer.writeSlice(uint32s); + + const reader = new BufferReader(buffer); + expect(reader.readUint32()).to.eq(0); + expect(reader.readUint32()).to.eq(1); + expect(reader.readUint32()).to.eq(1); + expect(reader.readUint32()).to.eq(2); + expect(reader.readUint32()).to.eq(3); + expect(reader.readUint32()).to.eq(5); + expect(reader.readUint32()).to.eq(8); + expect(reader.readUint32()).to.eq(4_294_967_295); + }); +}); + +describe('BufferReader', () => { + it('should decode a Uint8Array', () => { + const buffer = Buffer.alloc(4); + const writer = new BufferWriter(buffer); + writer.writeByte(0); + writer.writeByte(15); + writer.writeByte(64); + writer.writeByte(255); + + const destBuffer = new ArrayBuffer(4); + const destU8 = new Uint8Array(destBuffer); + const reader = new BufferReader(buffer); + reader.readSlice(destU8, 0, 4); + + expect([...destU8]).toEqual([0, 15, 64, 255]); + }); + + it('should decode a Uint32Array', () => { + const buffer = Buffer.alloc(4 * 4); + const writer = new BufferWriter(buffer); + writer.writeUint32(0); + writer.writeUint32(15); + writer.writeUint32(255); + writer.writeUint32(4_294_967_295); + + const destBuffer = new ArrayBuffer(4 * 4); + const destU32 = new Uint32Array(destBuffer); + const reader = new BufferReader(buffer); + reader.readSlice(destU32, 0, destBuffer.byteLength); + + expect([...destU32]).toEqual([0, 15, 255, 4_294_967_295]); + }); + + it('should decode a Int32Array', () => { + const buffer = Buffer.alloc(4 * 4); + const writer = new BufferWriter(buffer); + writer.writeInt32(0); + writer.writeInt32(15); + writer.writeInt32(-2_147_483_648); + writer.writeInt32(2_147_483_647); + + const destBuffer = new ArrayBuffer(4 * 4); + const destI32 = new Int32Array(destBuffer); + const reader = new BufferReader(buffer); + reader.readSlice(destI32, 0, destBuffer.byteLength); + + expect([...destI32]).toEqual([0, 15, -2_147_483_648, 2_147_483_647]); + }); +}); + describe('BufferWriter/BufferReader', () => { it('parses options correctly', () => { const buffer = Buffer.alloc(16); diff --git a/packages/typed-binary/src/io/bufferWriter.ts b/packages/typed-binary/src/io/bufferWriter.ts index ae1ced7..21d0a9b 100644 --- a/packages/typed-binary/src/io/bufferWriter.ts +++ b/packages/typed-binary/src/io/bufferWriter.ts @@ -1,5 +1,6 @@ import { BufferIOBase } from './bufferIOBase'; import type { ISerialOutput } from './types'; +import { unwrapBuffer } from './unwrapBuffer'; export class BufferWriter extends BufferIOBase implements ISerialOutput { private copyHelperToOutput(bytes: number) { @@ -42,4 +43,12 @@ export class BufferWriter extends BufferIOBase implements ISerialOutput { // Extra null character this.uint8View[this.byteOffset++] = 0; } + + writeSlice(bufferView: ArrayLike & ArrayBufferView): void { + const unwrapped = unwrapBuffer(bufferView); + const srcU8 = new Uint8Array(unwrapped.buffer, unwrapped.byteOffset); + for (const srcByte of srcU8) { + this.uint8View[this.byteOffset++] = srcByte; + } + } } diff --git a/packages/typed-binary/src/io/types.ts b/packages/typed-binary/src/io/types.ts index dae453f..9a91402 100644 --- a/packages/typed-binary/src/io/types.ts +++ b/packages/typed-binary/src/io/types.ts @@ -1,5 +1,7 @@ export type Endianness = 'big' | 'little'; +export type BufferView = ArrayLike & ArrayBufferView; + export interface ISerialInput { readBool(): boolean; readByte(): number; @@ -7,6 +9,7 @@ export interface ISerialInput { readUint32(): number; readFloat32(): number; readString(): string; + readSlice(bufferView: BufferView, offset: number, byteLength: number): void; seekTo(offset: number): void; skipBytes(bytes: number): void; readonly endianness: Endianness; @@ -20,6 +23,7 @@ export interface ISerialOutput { writeUint32(value: number): void; writeFloat32(value: number): void; writeString(value: string): void; + writeSlice(bufferView: BufferView): void; seekTo(offset: number): void; skipBytes(bytes: number): void; readonly endianness: Endianness; diff --git a/packages/typed-binary/src/io/unwrapBuffer.test.ts b/packages/typed-binary/src/io/unwrapBuffer.test.ts new file mode 100644 index 0000000..08df521 --- /dev/null +++ b/packages/typed-binary/src/io/unwrapBuffer.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { unwrapBuffer } from './unwrapBuffer'; + +describe('unwrapBuffer', () => { + it('returns the input if given an array buffer', () => { + const arrayBuffer = new ArrayBuffer(4 * 4); + + const result = unwrapBuffer(arrayBuffer); + + expect(result.byteOffset).toEqual(0); + expect(result.buffer).toBe(arrayBuffer); + }); + + it('unwraps typed array and returns underlying buffer', () => { + const arrayBuffer = new ArrayBuffer(4 * 4); + const u32View = new Uint32Array(arrayBuffer); + + const result = unwrapBuffer(u32View); + + expect(result.byteOffset).toEqual(0); + expect(result.buffer).toBe(arrayBuffer); + }); + + it('correctly calculates the offset that the view had on the buffer', () => { + const arrayBuffer = new ArrayBuffer(4 * 4); + const u32View = new Uint32Array(arrayBuffer, 4); + + const result = unwrapBuffer(u32View); + + expect(result.byteOffset).toEqual(4); + expect(result.buffer).toBe(arrayBuffer); + }); +}); diff --git a/packages/typed-binary/src/io/unwrapBuffer.ts b/packages/typed-binary/src/io/unwrapBuffer.ts new file mode 100644 index 0000000..1ad374c --- /dev/null +++ b/packages/typed-binary/src/io/unwrapBuffer.ts @@ -0,0 +1,15 @@ +/** + * Removes up to one layer of view over a buffer. + */ +export function unwrapBuffer(buffer: ArrayBufferLike | ArrayBufferView) { + let byteOffset = 0; + let innerBuffer = buffer; + + if (!!innerBuffer && 'buffer' in innerBuffer && 'byteOffset' in innerBuffer) { + // Getting rid of the outer shell, which allow us to create new views on the buffer instead of creating copies of it. + byteOffset += innerBuffer.byteOffset; + innerBuffer = innerBuffer.buffer; + } + + return { buffer: innerBuffer, byteOffset }; +} diff --git a/packages/typed-binary/src/structure/typedArray.ts b/packages/typed-binary/src/structure/typedArray.ts new file mode 100644 index 0000000..2b47a76 --- /dev/null +++ b/packages/typed-binary/src/structure/typedArray.ts @@ -0,0 +1,46 @@ +import { + type IMeasurer, + type ISerialInput, + type ISerialOutput, + Measurer, +} from '../io'; +import type { Parsed } from '../utilityTypes'; +import { type MaxValue, Schema } from './types'; + +type TypedArrayConstructor = { + readonly BYTES_PER_ELEMENT: number; + new (buffer: ArrayBufferLike, offset?: number, length?: number): T; +}; + +export class TypedArraySchema< + TTypedArray extends ArrayLike & ArrayBufferView, +> extends Schema { + public readonly byteLength: number; + + constructor( + public readonly length: number, + private readonly _arrayConstructor: TypedArrayConstructor, + ) { + super(); + + this.byteLength = length * _arrayConstructor.BYTES_PER_ELEMENT; + } + + write(output: ISerialOutput, value: Parsed): void { + output.writeSlice(value); + } + + read(input: ISerialInput): Parsed { + const buffer = new ArrayBuffer(this.byteLength); + const view = new this._arrayConstructor(buffer, 0, this.length); + input.readSlice(view, 0, this.byteLength); + return view as Parsed; + } + + measure( + _value: Parsed | typeof MaxValue, + measurer: IMeasurer = new Measurer(), + ): IMeasurer { + return measurer.add(this.byteLength); + } +} diff --git a/packages/typed-binary/src/test/typedArray.test.ts b/packages/typed-binary/src/test/typedArray.test.ts new file mode 100644 index 0000000..a53b14b --- /dev/null +++ b/packages/typed-binary/src/test/typedArray.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from 'vitest'; +import { + f32Array, + f64Array, + i8Array, + i16Array, + i32Array, + u8Array, + u8ClampedArray, + u16Array, + u32Array, +} from '../describe'; +import { encodeAndDecode } from './helpers/mock'; + +describe('u8Array', () => { + it('encodes and decodes a uint8 array', () => { + const value = new Uint8Array([0, 1, 15, 255, 256]); + + expect(value[4]).toEqual(0); // not-clamping + expect([...encodeAndDecode(u8Array(5), value)]).toEqual([...value]); + }); +}); + +describe('u8ClampedArray', () => { + it('encodes and decodes a clamped uint8 array', () => { + const value = new Uint8ClampedArray([0, 1, 15, 255, 256]); + + expect(value[4]).toEqual(255); // clamping + expect([...encodeAndDecode(u8ClampedArray(5), value)]).toEqual([...value]); + }); +}); + +describe('u16Array', () => { + it('encodes and decodes a uint16 array', () => { + const value = new Uint16Array([0, 1, 15, 65_535, 65_536]); + + expect(value[3]).toEqual(65_535); + expect(value[4]).toEqual(0); // not-clamping + expect([...encodeAndDecode(u16Array(5), value)]).toEqual([...value]); + }); +}); + +describe('u32Array', () => { + it('encodes and decodes a uint32 array', () => { + const value = new Uint32Array([0, 1, 15, 4_294_967_295, 4_294_967_296]); + + expect(value[3]).toEqual(4_294_967_295); + expect(value[4]).toEqual(0); // not-clamping + expect([...encodeAndDecode(u32Array(5), value)]).toEqual([...value]); + }); +}); + +describe('i8Array', () => { + it('encodes and decodes a int8 array', () => { + const value = new Int8Array([0, 1, 15, 127, 128]); + + expect(value[3]).toEqual(127); + expect(value[4]).toEqual(-128); // one-over max interpreted as negative + expect([...encodeAndDecode(i8Array(5), value)]).toEqual([...value]); + }); +}); + +describe('i16Array', () => { + it('encodes and decodes a int16 array', () => { + const value = new Int16Array([0, 1, 15, 32_767, 32_768]); + + expect(value[3]).toEqual(32_767); + expect(value[4]).toEqual(-32_768); // one-over max interpreted as negative + expect([...encodeAndDecode(i16Array(5), value)]).toEqual([...value]); + }); +}); + +describe('i32Array', () => { + it('encodes and decodes a int32 array', () => { + const value = new Int32Array([ + 16, -2_147_483_648, 2_147_483_647, 2_147_483_648, + ]); + expect([...encodeAndDecode(i32Array(4), value)]).toEqual([...value]); + }); +}); + +describe('f32Array', () => { + it('encodes and decodes a float32 array', () => { + const value = new Float32Array([0.1, 0.2, 0.3]); + expect([...encodeAndDecode(f32Array(3), value)]).toEqual([...value]); + }); +}); + +describe('f64Array', () => { + it('encodes and decodes a float64 array', () => { + const value = new Float64Array([0.1, 0.2, 0.3]); + expect([...encodeAndDecode(f64Array(3), value)]).toEqual([...value]); + }); +});