From 197bc13ade85f80bd7073a8e4a401c61af45d4e5 Mon Sep 17 00:00:00 2001 From: Eduardo Gurgel Pinho Date: Mon, 17 Nov 2025 13:09:24 +1300 Subject: [PATCH] fix(realtime): account for null refs when encoding messages ref and join_ref can be null/undefined in certain scenarios like when publishing heartbeaats. Currently using JSON to send heartbeats but in the future this can be changed and we want to be ready --- .../core/realtime-js/src/lib/serializer.ts | 35 ++++--- .../core/realtime-js/test/serializer.test.ts | 93 +++++++++++++++++++ 2 files changed, 114 insertions(+), 14 deletions(-) diff --git a/packages/core/realtime-js/src/lib/serializer.ts b/packages/core/realtime-js/src/lib/serializer.ts index c88a3c1f6..af9bfa16e 100644 --- a/packages/core/realtime-js/src/lib/serializer.ts +++ b/packages/core/realtime-js/src/lib/serializer.ts @@ -3,8 +3,8 @@ import { CHANNEL_EVENTS } from '../lib/constants' export type Msg = { - join_ref: string - ref: string + join_ref?: string | null + ref?: string | null topic: string event: string payload: T @@ -42,19 +42,22 @@ export default class Serializer { } private _binaryEncodePush(message: Msg) { - const { join_ref, ref, event, topic, payload } = message - const metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length + const { event, topic, payload } = message + const ref = message.ref ?? '' + const joinRef = message.join_ref ?? '' + + const metaLength = this.META_LENGTH + joinRef.length + ref.length + topic.length + event.length const header = new ArrayBuffer(this.HEADER_LENGTH + metaLength) let view = new DataView(header) let offset = 0 view.setUint8(offset++, this.KINDS.push) // kind - view.setUint8(offset++, join_ref.length) + view.setUint8(offset++, joinRef.length) view.setUint8(offset++, ref.length) view.setUint8(offset++, topic.length) view.setUint8(offset++, event.length) - Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0))) + 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(event, (char) => view.setUint8(offset++, char.charCodeAt(0))) @@ -75,13 +78,15 @@ export default class Serializer { } private _encodeBinaryUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) { - const { join_ref, ref, topic } = message + const topic = message.topic + const ref = message.ref ?? '' + const joinRef = message.join_ref ?? '' const userEvent = message.payload.event const userPayload = message.payload?.payload ?? new ArrayBuffer(0) const metaLength = this.USER_BROADCAST_PUSH_META_LENGTH + - join_ref.length + + joinRef.length + ref.length + topic.length + userEvent.length @@ -91,12 +96,12 @@ export default class Serializer { let offset = 0 view.setUint8(offset++, this.KINDS.userBroadcastPush) // kind - view.setUint8(offset++, join_ref.length) + view.setUint8(offset++, joinRef.length) view.setUint8(offset++, ref.length) view.setUint8(offset++, topic.length) view.setUint8(offset++, userEvent.length) view.setUint8(offset++, this.BINARY_ENCODING) - Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0))) + 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))) @@ -109,7 +114,9 @@ export default class Serializer { } private _encodeJsonUserBroadcastPush(message: Msg<{ event: string } & { [key: string]: any }>) { - const { join_ref, ref, topic } = message + const topic = message.topic + const ref = message.ref ?? '' + const joinRef = message.join_ref ?? '' const userEvent = message.payload.event const userPayload = message.payload?.payload ?? {} @@ -118,7 +125,7 @@ export default class Serializer { const metaLength = this.USER_BROADCAST_PUSH_META_LENGTH + - join_ref.length + + joinRef.length + ref.length + topic.length + userEvent.length @@ -128,12 +135,12 @@ export default class Serializer { let offset = 0 view.setUint8(offset++, this.KINDS.userBroadcastPush) // kind - view.setUint8(offset++, join_ref.length) + view.setUint8(offset++, joinRef.length) view.setUint8(offset++, ref.length) view.setUint8(offset++, topic.length) view.setUint8(offset++, userEvent.length) view.setUint8(offset++, this.JSON_ENCODING) - Array.from(join_ref, (char) => view.setUint8(offset++, char.charCodeAt(0))) + 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))) diff --git a/packages/core/realtime-js/test/serializer.test.ts b/packages/core/realtime-js/test/serializer.test.ts index c75415ee6..1eef5f52c 100644 --- a/packages/core/realtime-js/test/serializer.test.ts +++ b/packages/core/realtime-js/test/serializer.test.ts @@ -28,6 +28,13 @@ const decodeAsync = ( } let exampleMsg = { join_ref: '0', ref: '1', topic: 't', event: 'e', payload: { foo: 1 } } +let missingRefExampleMsg = { + join_ref: null, + ref: null, + topic: 't', + event: 'e', + payload: { foo: 1 }, +} // \x01\x04 let binPayload = () => { @@ -43,10 +50,20 @@ describe('JSON', () => { expect(result).toBe('["0","1","t","e",{"foo":1}]') }) + it('encodes missing refs', async () => { + const result = await encodeAsync(serializer, missingRefExampleMsg) + expect(result).toBe('[null,null,"t","e",{"foo":1}]') + }) + it('decodes', async () => { const result = await decodeAsync(serializer, '["0","1","t","e",{"foo":1}]') expect(result).toEqual(exampleMsg) }) + + it('decodes missing refs', async () => { + const result = await decodeAsync(serializer, '[null,null,"t","e",{"foo":1}]') + expect(result).toEqual(missingRefExampleMsg) + }) }) describe('binary', () => { @@ -63,6 +80,30 @@ describe('binary', () => { expect(decoder.decode(result as ArrayBuffer)).toBe(bin) }) + it('encodes push with undefined join_ref and ref', async () => { + let buffer = binPayload() + let bin = '\0\x00\x00\x01\x01te\x01\x04' + const result = await encodeAsync(serializer, { + join_ref: undefined, + ref: undefined, + topic: 't', + event: 'e', + payload: buffer, + }) + expect(decoder.decode(result as ArrayBuffer)).toBe(bin) + }) + + it('encodes push with no join_ref no ref', async () => { + let buffer = binPayload() + let bin = '\0\x00\x00\x01\x01te\x01\x04' + const result = await encodeAsync(serializer, { + topic: 't', + event: 'e', + payload: buffer, + }) + expect(decoder.decode(result as ArrayBuffer)).toBe(bin) + }) + it('encodes variable length segments', async () => { let buffer = binPayload() let bin = '\0\x02\x01\x03\x02101topev\x01\x04' @@ -106,6 +147,33 @@ describe('binary', () => { expect(decoder.decode(result as ArrayBuffer)).toBe(bin) }) + it('encodes user broadcast push with JSON payload no refs', async () => { + // 3 -> user_broadcast_push + // 0 join_ref length + // 0 for ref length + // 3 for topic length + // 10 for user event length + // 1 for JSON encoding + // actual join ref + // actual ref + // actual topic + // actual user event + // actual payload + let bin = '\x03\x00\x00\x03\x0a\x01topuser-event{"a":"b"}' + + const result = await encodeAsync(serializer, { + topic: 'top', + event: 'broadcast', + payload: { + event: 'user-event', + payload: { + a: 'b', + }, + }, + }) + expect(decoder.decode(result as ArrayBuffer)).toBe(bin) + }) + it('encodes user broadcast push with Binary payload', async () => { // 3 -> user_broadcast_push // 2 join_ref length @@ -133,6 +201,31 @@ describe('binary', () => { expect(decoder.decode(result as ArrayBuffer)).toBe(bin) }) + it('encodes user broadcast push with Binary payload no refs', async () => { + // 3 -> user_broadcast_push + // 0 join_ref length + // 0 for ref length + // 3 for topic length + // 10 for user event length + // 0 for Binary encoding + // actual join ref + // actual ref + // actual topic + // actual user event + // actual payload + let bin = '\x03\x00\x00\x03\x0a\x00topuser-event\x01\x04' + + const result = await encodeAsync(serializer, { + topic: 'top', + event: 'broadcast', + payload: { + event: 'user-event', + payload: binPayload(), + }, + }) + expect(decoder.decode(result as ArrayBuffer)).toBe(bin) + }) + it('decodes push payload as JSON', async () => { let bin = '\0\x03\x03\n123topsome-event{"a":"b"}' let buffer = new TextEncoder().encode(bin).buffer