UNPKG

expo

Version:
236 lines (210 loc) • 8.35 kB
/** * A message frame packer that serializes a messageKey and a payload into either a JSON string * (fast path) or a binary format (for complex payloads). * * Fast Path (JSON.stringify/JSON.parse): * - For simple payloads (e.g., strings, numbers, null, undefined, or plain objects), the packer * uses `JSON.stringify` for serialization and `JSON.parse` for deserialization, ensuring * optimal performance. * * Binary Format: * - For more complex payloads (e.g., Uint8Array, ArrayBuffer, Blob), the packer uses a binary * format with the following structure: * * +------------------+-------------------+----------------------------+--------------------------+ * | 4 bytes (Uint32) | Variable length | 1 byte (Uint8) | Variable length | * | MessageKeyLength | MessageKey (JSON) | PayloadTypeIndicator (enum)| Payload (binary data) | * +------------------+-------------------+----------------------------+--------------------------+ * * 1. MessageKeyLength (4 bytes): * - A 4-byte unsigned integer indicating the length of the MessageKey JSON string. * * 2. MessageKey (Variable length): * - The JSON string representing the message key, encoded as UTF-8. * * 3. PayloadTypeIndicator (1 byte): * - A single byte enum value representing the type of the payload (e.g., Uint8Array, String, * Object, ArrayBuffer, Blob). * * 4. Payload (Variable length): * - The actual payload data, which can vary in type and length depending on the PayloadType. */ import { blobToArrayBufferAsync } from '../utils/blobUtils'; type MessageKeyTypeBase = string | object; type PayloadType = Uint8Array | string | number | null | undefined | object | ArrayBuffer | Blob; enum PayloadTypeIndicator { Uint8Array = 1, String = 2, Number = 3, Null = 4, Undefined = 5, Object = 6, ArrayBuffer = 7, Blob = 8, } interface MessageFrame<T extends MessageKeyTypeBase> { messageKey: T; payload?: PayloadType; } export class MessageFramePacker<T extends MessageKeyTypeBase> { private textEncoder = new TextEncoder(); private textDecoder = new TextDecoder(); public pack({ messageKey, payload }: MessageFrame<T>): string | Uint8Array | Promise<Uint8Array> { // Fast path to pack as string given `JSON.stringify` is fast. if (this.isFastPathPayload(payload)) { return JSON.stringify({ messageKey, payload }); } // Slowest path for Blob returns a promise. if (payload instanceof Blob) { return new Promise(async (resolve, reject) => { try { const arrayBuffer = await blobToArrayBufferAsync(payload); resolve( this.packImpl( { messageKey, payload: new Uint8Array(arrayBuffer) }, PayloadTypeIndicator.Blob ) ); } catch (error) { reject(error); } }); } // Slow path for other types returns a Uint8Array. return this.packImpl({ messageKey, payload }, undefined); } public unpack(packedData: string | ArrayBuffer): MessageFrame<T> { // Fast path to unpack as string given `JSON.parse` is fast. if (typeof packedData === 'string') { return JSON.parse(packedData); } // [0] messageKeyLength (4 bytes) const messageKeyLengthView = new DataView(packedData, 0, 4); const messageKeyLength = messageKeyLengthView.getUint32(0, false); // [1] messageKey (variable length) const messageKeyBytes = packedData.slice(4, 4 + messageKeyLength); const messageKeyString = this.textDecoder.decode(messageKeyBytes); const messageKey = JSON.parse(messageKeyString); // [2] payloadTypeIndicator (1 byte) const payloadTypeView = new DataView(packedData, 4 + messageKeyLength, 1); const payloadType = payloadTypeView.getUint8(0); // [3] payload (variable length) const payloadBinary = packedData.slice(4 + messageKeyLength + 1); const payload = this.deserializePayload(payloadBinary, payloadType); return { messageKey, payload }; } private isFastPathPayload(payload: PayloadType): boolean { if (payload == null) { return true; } const payloadType = typeof payload; if (payloadType === 'string' || payloadType === 'number') { return true; } if (payloadType === 'object' && payload.constructor === Object) { return true; } return false; } private payloadToUint8Array(payload: PayloadType): Uint8Array { if (payload instanceof Uint8Array) { return payload; } else if (typeof payload === 'string') { return this.textEncoder.encode(payload); } else if (typeof payload === 'number') { const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setFloat64(0, payload, false); return new Uint8Array(buffer); } else if (payload === null) { return new Uint8Array(0); } else if (payload === undefined) { return new Uint8Array(0); } else if (payload instanceof ArrayBuffer) { return new Uint8Array(payload); } else if (payload instanceof Blob) { throw new Error('Blob is not supported in this callsite.'); } else { return this.textEncoder.encode(JSON.stringify(payload)); } } private packImpl( { messageKey, payload }: MessageFrame<T>, payloadType: PayloadTypeIndicator | undefined ): Promise<Uint8Array> | Uint8Array { const messageKeyString = JSON.stringify(messageKey); const messageKeyBytes = this.textEncoder.encode(messageKeyString); const messageKeyLength = messageKeyBytes.length; const payloadBinary = this.payloadToUint8Array(payload); const totalLength = 4 + messageKeyLength + 1 + payloadBinary.byteLength; const buffer = new ArrayBuffer(totalLength); const packedArray = new Uint8Array(buffer); // [0] messageKeyLength (4 bytes) const messageKeyLengthView = new DataView(buffer, 0, 4); messageKeyLengthView.setUint32(0, messageKeyLength, false); // [1] messageKey (variable length) packedArray.set(messageKeyBytes, 4); // [2] payloadTypeIndicator (1 byte) const payloadTypeView = new DataView(buffer, 4 + messageKeyLength, 1); payloadTypeView.setUint8(0, payloadType ?? MessageFramePacker.getPayloadTypeIndicator(payload)); // [3] payload (variable length) packedArray.set(payloadBinary, 4 + messageKeyLength + 1); return packedArray; } private deserializePayload( payloadBinary: ArrayBuffer, payloadTypeIndicator: PayloadTypeIndicator ): PayloadType { switch (payloadTypeIndicator) { case PayloadTypeIndicator.Uint8Array: { return new Uint8Array(payloadBinary); } case PayloadTypeIndicator.String: { return this.textDecoder.decode(payloadBinary); } case PayloadTypeIndicator.Number: { const view = new DataView(payloadBinary); return view.getFloat64(0, false); } case PayloadTypeIndicator.Null: { return null; } case PayloadTypeIndicator.Undefined: { return undefined; } case PayloadTypeIndicator.Object: { const jsonString = this.textDecoder.decode(payloadBinary); return JSON.parse(jsonString); } case PayloadTypeIndicator.ArrayBuffer: { return payloadBinary; } case PayloadTypeIndicator.Blob: { return new Blob([payloadBinary]); } default: throw new Error('Unsupported payload type'); } } private static getPayloadTypeIndicator(payload: PayloadType): PayloadTypeIndicator { if (payload instanceof Uint8Array) { return PayloadTypeIndicator.Uint8Array; } else if (typeof payload === 'string') { return PayloadTypeIndicator.String; } else if (typeof payload === 'number') { return PayloadTypeIndicator.Number; } else if (payload === null) { return PayloadTypeIndicator.Null; } else if (payload === undefined) { return PayloadTypeIndicator.Undefined; } else if (payload instanceof ArrayBuffer) { return PayloadTypeIndicator.ArrayBuffer; } else if (payload instanceof Blob) { return PayloadTypeIndicator.Blob; } else if (typeof payload === 'object') { return PayloadTypeIndicator.Object; } else { throw new Error('Unsupported payload type'); } } }