UNPKG

@supabase/realtime-js

Version:
204 lines (174 loc) 7.07 kB
// This file draws heavily from https://github.com/phoenixframework/phoenix/commit/cf098e9cf7a44ee6479d31d911a97d3c7430c6fe // License: https://github.com/phoenixframework/phoenix/blob/master/LICENSE.md export type Msg<T> = { join_ref?: string | null ref?: string | null topic: string event: string payload: T } export default class Serializer { HEADER_LENGTH = 1 USER_BROADCAST_PUSH_META_LENGTH = 6 KINDS = { userBroadcastPush: 3, userBroadcast: 4 } BINARY_ENCODING = 0 JSON_ENCODING = 1 BROADCAST_EVENT = 'broadcast' allowedMetadataKeys: string[] = [] constructor(allowedMetadataKeys?: string[] | null) { this.allowedMetadataKeys = allowedMetadataKeys ?? [] } encode(msg: Msg<{ [key: string]: any }>, callback: (result: ArrayBuffer | string) => any) { if ( msg.event === this.BROADCAST_EVENT && !(msg.payload instanceof ArrayBuffer) && typeof msg.payload.event === 'string' ) { return callback( this._binaryEncodeUserBroadcastPush(msg as Msg<{ event: string } & { [key: string]: any }>) ) } let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload] return callback(JSON.stringify(payload)) } private _binaryEncodeUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) { if (this._isArrayBuffer(message.payload?.payload)) { return this._encodeBinaryUserBroadcastPush(message) } else { return this._encodeJsonUserBroadcastPush(message) } } private _encodeBinaryUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) { const userPayload = message.payload?.payload ?? new ArrayBuffer(0) return this._encodeUserBroadcastPush(message, this.BINARY_ENCODING, userPayload) } private _encodeJsonUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) { const userPayload = message.payload?.payload ?? {} const encoder = new TextEncoder() const encodedUserPayload = encoder.encode(JSON.stringify(userPayload)).buffer return this._encodeUserBroadcastPush(message, this.JSON_ENCODING, encodedUserPayload) } private _encodeUserBroadcastPush( message: Msg<{ event: string } & { [key: string]: any }>, encodingType: number, encodedPayload: ArrayBuffer ) { const topic = message.topic const ref = message.ref ?? '' const joinRef = message.join_ref ?? '' const userEvent = message.payload.event // Filter metadata based on allowed keys const rest = this.allowedMetadataKeys ? this._pick(message.payload, this.allowedMetadataKeys) : {} const metadata = Object.keys(rest).length === 0 ? '' : JSON.stringify(rest) // Validate lengths don't exceed uint8 max value (255) if (joinRef.length > 255) { throw new Error(`joinRef length ${joinRef.length} exceeds maximum of 255`) } if (ref.length > 255) { throw new Error(`ref length ${ref.length} exceeds maximum of 255`) } if (topic.length > 255) { throw new Error(`topic length ${topic.length} exceeds maximum of 255`) } if (userEvent.length > 255) { throw new Error(`userEvent length ${userEvent.length} exceeds maximum of 255`) } if (metadata.length > 255) { throw new Error(`metadata length ${metadata.length} exceeds maximum of 255`) } const metaLength = this.USER_BROADCAST_PUSH_META_LENGTH + joinRef.length + ref.length + topic.length + userEvent.length + metadata.length const header = new ArrayBuffer(this.HEADER_LENGTH + metaLength) let view = new DataView(header) let offset = 0 view.setUint8(offset++, this.KINDS.userBroadcastPush) // kind view.setUint8(offset++, joinRef.length) view.setUint8(offset++, ref.length) view.setUint8(offset++, topic.length) view.setUint8(offset++, userEvent.length) view.setUint8(offset++, metadata.length) view.setUint8(offset++, encodingType) Array.from(joinRef, (char) => view.setUint8(offset++, char.charCodeAt(0))) Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0))) Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0))) Array.from(userEvent, (char) => view.setUint8(offset++, char.charCodeAt(0))) Array.from(metadata, (char) => view.setUint8(offset++, char.charCodeAt(0))) var combined = new Uint8Array(header.byteLength + encodedPayload.byteLength) combined.set(new Uint8Array(header), 0) combined.set(new Uint8Array(encodedPayload), header.byteLength) return combined.buffer } decode(rawPayload: ArrayBuffer | string, callback: Function) { if (this._isArrayBuffer(rawPayload)) { let result = this._binaryDecode(rawPayload as ArrayBuffer) return callback(result) } if (typeof rawPayload === 'string') { const jsonPayload = JSON.parse(rawPayload) const [join_ref, ref, topic, event, payload] = jsonPayload return callback({ join_ref, ref, topic, event, payload }) } return callback({}) } private _binaryDecode(buffer: ArrayBuffer) { const view = new DataView(buffer) const kind = view.getUint8(0) const decoder = new TextDecoder() switch (kind) { case this.KINDS.userBroadcast: return this._decodeUserBroadcast(buffer, view, decoder) } } private _decodeUserBroadcast( buffer: ArrayBuffer, view: DataView, decoder: TextDecoder ): { join_ref: null ref: null topic: string event: string payload: { [key: string]: any } } { const topicSize = view.getUint8(1) const userEventSize = view.getUint8(2) const metadataSize = view.getUint8(3) const payloadEncoding = view.getUint8(4) let offset = this.HEADER_LENGTH + 4 const topic = decoder.decode(buffer.slice(offset, offset + topicSize)) offset = offset + topicSize const userEvent = decoder.decode(buffer.slice(offset, offset + userEventSize)) offset = offset + userEventSize const metadata = decoder.decode(buffer.slice(offset, offset + metadataSize)) offset = offset + metadataSize const payload = buffer.slice(offset, buffer.byteLength) const parsedPayload = payloadEncoding === this.JSON_ENCODING ? JSON.parse(decoder.decode(payload)) : payload const data: { [key: string]: any } = { type: this.BROADCAST_EVENT, event: userEvent, payload: parsedPayload, } // Metadata is optional and always JSON encoded if (metadataSize > 0) { data['meta'] = JSON.parse(metadata) } return { join_ref: null, ref: null, topic: topic, event: this.BROADCAST_EVENT, payload: data } } private _isArrayBuffer(buffer: any): boolean { return buffer instanceof ArrayBuffer || buffer?.constructor?.name === 'ArrayBuffer' } private _pick(obj: Record<string, any> | null | undefined, keys: string[]): Record<string, any> { if (!obj || typeof obj !== 'object') { return {} } return Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key))) } }