Skip to content

Commit e168922

Browse files
authored
fix(realtime): handle null values in postgres changes filter comparison (#1918)
1 parent eae712d commit e168922

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

packages/core/realtime-js/src/RealtimeChannel.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,9 @@ export default class RealtimeChannel {
330330
if (
331331
serverPostgresFilter &&
332332
serverPostgresFilter.event === event &&
333-
serverPostgresFilter.schema === schema &&
334-
serverPostgresFilter.table === table &&
335-
serverPostgresFilter.filter === filter
333+
RealtimeChannel.isFilterValueEqual(serverPostgresFilter.schema, schema) &&
334+
RealtimeChannel.isFilterValueEqual(serverPostgresFilter.table, table) &&
335+
RealtimeChannel.isFilterValueEqual(serverPostgresFilter.filter, filter)
336336
) {
337337
newPostgresBindings.push({
338338
...clientPostgresBinding,
@@ -952,6 +952,20 @@ export default class RealtimeChannel {
952952
return true
953953
}
954954

955+
/**
956+
* Compares two optional filter values for equality.
957+
* Treats undefined, null, and empty string as equivalent empty values.
958+
* @internal
959+
*/
960+
private static isFilterValueEqual(
961+
serverValue: string | undefined | null,
962+
clientValue: string | undefined
963+
): boolean {
964+
const normalizedServer = serverValue ?? undefined
965+
const normalizedClient = clientValue ?? undefined
966+
return normalizedServer === normalizedClient
967+
}
968+
955969
/** @internal */
956970
private _rejoinUntilConnected() {
957971
this.rejoinTimer.scheduleTimeout()

packages/core/realtime-js/test/RealtimeChannel.postgres.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,77 @@ describe('PostgreSQL binding matching behavior', () => {
247247
assert.equal(channel.bindings.postgres_changes[0].id, 'server-id-1')
248248
})
249249

250+
test('should match postgres changes when server returns null for optional fields', () => {
251+
const callbackSpy = vi.fn()
252+
253+
channel.on(
254+
'postgres_changes',
255+
{
256+
event: 'INSERT',
257+
schema: 'public',
258+
table: 'notifications',
259+
},
260+
callbackSpy
261+
)
262+
263+
channel.subscribe()
264+
265+
const mockServerResponse = {
266+
postgres_changes: [
267+
{
268+
event: 'INSERT',
269+
schema: 'public',
270+
table: 'notifications',
271+
filter: null,
272+
id: 'server-id-1',
273+
},
274+
],
275+
}
276+
277+
channel.joinPush._matchReceive({
278+
status: 'ok',
279+
response: mockServerResponse,
280+
})
281+
282+
assert.equal(channel.state, CHANNEL_STATES.joined)
283+
assert.equal(channel.bindings.postgres_changes[0].id, 'server-id-1')
284+
})
285+
286+
test('should match postgres changes when server omits optional filter field', () => {
287+
const callbackSpy = vi.fn()
288+
289+
channel.on(
290+
'postgres_changes',
291+
{
292+
event: '*',
293+
schema: 'public',
294+
table: 'notifications',
295+
},
296+
callbackSpy
297+
)
298+
299+
channel.subscribe()
300+
301+
const mockServerResponse = {
302+
postgres_changes: [
303+
{
304+
event: '*',
305+
schema: 'public',
306+
table: 'notifications',
307+
id: 'server-id-1',
308+
},
309+
],
310+
}
311+
312+
channel.joinPush._matchReceive({
313+
status: 'ok',
314+
response: mockServerResponse,
315+
})
316+
317+
assert.equal(channel.state, CHANNEL_STATES.joined)
318+
assert.equal(channel.bindings.postgres_changes[0].id, 'server-id-1')
319+
})
320+
250321
test.each([
251322
{
252323
description: 'should fail when event differs',

0 commit comments

Comments
 (0)