From 25cb29f523716c926a304cd8cd275c4c564c0066 Mon Sep 17 00:00:00 2001 From: Githena Date: Sat, 22 Aug 2026 02:50:58 +0000 Subject: [PATCH] fix(pglite): use normalized channel name in LISTEN/UNLISTEN Fixes pg.listen() not receiving notifications from pg_notify() when channel name has mixed case (e.g., 'TinyBase'). listen(): use toPostgresName(channel) in LISTEN SQL command. unlisten(): use toPostgresName(channel) in UNLISTEN SQL command. Fix unsubscribe function: was passing normalized key as callback. Add case-insensitive fallback in notification lookup. Closes #642 --- packages/pglite/src/pglite.ts | 17 +++++++++++++---- packages/pglite/tests/notify.test.ts | 11 +++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/pglite/src/pglite.ts b/packages/pglite/src/pglite.ts index 517a4376a..9403166fd 100644 --- a/packages/pglite/src/pglite.ts +++ b/packages/pglite/src/pglite.ts @@ -1108,7 +1108,16 @@ export class PGlite } } else if (msg instanceof NotificationResponseMessage) { // We've received a notification, call the listeners - const listeners = this.#notifyListeners.get(msg.channel) + // Listeners are stored with their original channel name. Use the + // normalized form (lowercased for unquoted identifiers) as a fallback + // so that pg.listen('TinyBase') + pg_notify('TinyBase') match. + let listeners = this.#notifyListeners.get(msg.channel) + if (!listeners) { + const pgChannel = pglUtils.toPostgresName(msg.channel) + if (pgChannel !== msg.channel) { + listeners = this.#notifyListeners.get(pgChannel) + } + } if (listeners) { listeners.forEach((cb) => { // We use queueMicrotask so that the callback is called after any @@ -1192,7 +1201,7 @@ export class PGlite } this.#notifyListeners.get(pgChannel)!.add(callback) try { - await pg.exec(`LISTEN ${channel}`) + await pg.exec(`LISTEN ${pgChannel}`) } catch (e) { this.#notifyListeners.get(pgChannel)!.delete(callback) if (this.#notifyListeners.get(pgChannel)?.size === 0) { @@ -1201,7 +1210,7 @@ export class PGlite throw e } return async (tx?: Transaction) => { - await this.unlisten(pgChannel, callback, tx) + await this.unlisten(channel, callback, tx) } } @@ -1226,7 +1235,7 @@ export class PGlite const pgChannel = pglUtils.toPostgresName(channel) const pg = tx ?? this const cleanUp = async () => { - await pg.exec(`UNLISTEN ${channel}`) + await pg.exec(`UNLISTEN ${pgChannel}`) // While that query was running, another query might have subscribed // so we need to check again if (this.#notifyListeners.get(pgChannel)?.size === 0) { diff --git a/packages/pglite/tests/notify.test.ts b/packages/pglite/tests/notify.test.ts index 29b9260a7..22eee2640 100644 --- a/packages/pglite/tests/notify.test.ts +++ b/packages/pglite/tests/notify.test.ts @@ -58,6 +58,14 @@ describe('notify API', () => { await pg.listen('postgresdefaultlower', allLower1) await pg.exec(`NOTIFY postgresdefaultlower, 'payload1'`) + // TinyBase: mixed-case channel names with pg_notify should match + // Regression test for https://github.com/electric-sql/pglite/issues/642 + const tinyBaseListener = vi.fn() + await pg.listen('TinyBase', tinyBaseListener) + // pg_notify sends the string as-is; LISTEN uses the normalized (lowercased) name. + // The notification lookup now falls back to the normalized key, so TinyBase works. + await pg.exec(`SELECT pg_notify('TinyBase', 'hello-tinybase')`) + const autoLowerTest1 = vi.fn() await pg.listen('PostgresDefaultLower', autoLowerTest1) await pg.exec(`NOTIFY PostgresDefaultLower, 'payload1'`) @@ -116,6 +124,9 @@ describe('notify API', () => { expect(otherCharsWithQuotes).toHaveBeenCalledOnce() expect(quotedWithSpaces).toHaveBeenCalledOnce() expect(unquotedWithSpaces).not.toHaveBeenCalled() + // TinyBase regression test: pg_notify('TinyBase') must reach pg.listen('TinyBase') + expect(tinyBaseListener).toHaveBeenCalledTimes(1) + expect(tinyBaseListener).toHaveBeenCalledWith('hello-tinybase') }) it('check unlisten case sensitivity + special chars as Postgresql', async () => {