diff --git a/packages/bot-runner/tests/bot-runner-test.ts b/packages/bot-runner/tests/bot-runner-test.ts index 872b8bd400a..2560eda7815 100644 --- a/packages/bot-runner/tests/bot-runner-test.ts +++ b/packages/bot-runner/tests/bot-runner-test.ts @@ -112,6 +112,7 @@ module('timeline handler', () => { dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_registrations br')) { diff --git a/packages/bot-runner/tests/command-runner-test.ts b/packages/bot-runner/tests/command-runner-test.ts index 7e9d82ca861..ab8193f7afe 100644 --- a/packages/bot-runner/tests/command-runner-test.ts +++ b/packages/bot-runner/tests/command-runner-test.ts @@ -84,6 +84,7 @@ module('command runner', () => { ]); let dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_commands WHERE bot_id =')) { @@ -228,6 +229,7 @@ module('command runner', () => { ]); let dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_commands WHERE bot_id =')) { @@ -440,6 +442,7 @@ module('command runner', () => { ]); let dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_commands WHERE bot_id =')) { @@ -534,6 +537,7 @@ module('command runner', () => { ]); let dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_commands WHERE bot_id =')) { @@ -671,6 +675,7 @@ module('command runner', () => { ]); let dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_commands WHERE bot_id =')) { @@ -883,6 +888,7 @@ module('command runner', () => { ]); let dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_commands WHERE bot_id =')) { @@ -1002,6 +1008,7 @@ module('command runner', () => { ]); let dbAdapter = { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { if (sql.includes('FROM bot_commands WHERE bot_id =')) { diff --git a/packages/host/app/lib/sqlite-adapter.ts b/packages/host/app/lib/sqlite-adapter.ts index e611786a8ff..df8e599d1aa 100644 --- a/packages/host/app/lib/sqlite-adapter.ts +++ b/packages/host/app/lib/sqlite-adapter.ts @@ -53,6 +53,10 @@ export default class SQLiteAdapter implements DBAdapter { return await this.internalExecute(sql, opts); } + // SQLite has no pub/sub primitive and the host runs a single in-process + // realm with no peers to notify, so this is intentionally a no-op. + async notify(_channel: string, _payload: string): Promise {} + private async internalExecute(sql: string, opts?: ExecuteOptions) { sql = this.adjustSQL(sql); return await this.query(sql, opts); diff --git a/packages/postgres/pg-adapter.ts b/packages/postgres/pg-adapter.ts index f9256b9fa98..3e87102efe9 100644 --- a/packages/postgres/pg-adapter.ts +++ b/packages/postgres/pg-adapter.ts @@ -291,6 +291,12 @@ export class PgAdapter implements DBAdapter { } } + async notify(channel: string, payload: string): Promise { + await this.execute('SELECT pg_notify($1, $2)', { + bind: [channel, payload], + }); + } + // @deprecated — prefer `subscribe(channel, handler)`. Each call to listen() // opens its own dedicated Client connection for the duration of `fn`, which // doesn't scale as the number of LISTEN-using callers grows. subscribe() diff --git a/packages/realm-server/tests/prerender-proxy-test.ts b/packages/realm-server/tests/prerender-proxy-test.ts index 2fe79955a57..a739a359b1e 100644 --- a/packages/realm-server/tests/prerender-proxy-test.ts +++ b/packages/realm-server/tests/prerender-proxy-test.ts @@ -20,6 +20,7 @@ module(basename(__filename), function () { function makeDbAdapter(rows: any[]): DBAdapter { return { kind: 'pg', + async notify() {}, isClosed: false, async execute() { return rows; diff --git a/packages/realm-server/tests/realm-file-changes-listener-test.ts b/packages/realm-server/tests/realm-file-changes-listener-test.ts index 07ac461c844..9007d5d323a 100644 --- a/packages/realm-server/tests/realm-file-changes-listener-test.ts +++ b/packages/realm-server/tests/realm-file-changes-listener-test.ts @@ -2,7 +2,6 @@ import { module, test } from 'qunit'; import { basename } from 'path'; import type { PgAdapter } from '@cardstack/postgres'; import type { Realm } from '@cardstack/runtime-common'; -import { query, param } from '@cardstack/runtime-common'; import { setupDB } from './helpers'; import { RealmFileChangesListener, @@ -164,13 +163,10 @@ module(basename(__filename), function () { }); await listener.start(); try { - await query(dbAdapter, [ - `SELECT pg_notify(`, - param('realm_file_changes'), - `,`, - param(`${realmUrl}:src/greeting.gts`), - `)`, - ]); + await dbAdapter.notify( + 'realm_file_changes', + `${realmUrl}:src/greeting.gts`, + ); const received = await waitFor(() => invalidations.length > 0 ? invalidations : undefined, @@ -194,13 +190,10 @@ module(basename(__filename), function () { }); await listener.start(); try { - await query(dbAdapter, [ - `SELECT pg_notify(`, - param('realm_file_changes'), - `,`, - param(`http://x.test/not-mounted/:file.gts`), - `)`, - ]); + await dbAdapter.notify( + 'realm_file_changes', + `http://x.test/not-mounted/:file.gts`, + ); // Wait for the lookup to be recorded (proves the NOTIFY was received // and dispatched; the lookup miss then silently drops). diff --git a/packages/realm-server/tests/screenshot-card-test.ts b/packages/realm-server/tests/screenshot-card-test.ts index 7e8958a73fc..fd29f312f1f 100644 --- a/packages/realm-server/tests/screenshot-card-test.ts +++ b/packages/realm-server/tests/screenshot-card-test.ts @@ -24,6 +24,7 @@ module(basename(__filename), function () { function makeDbAdapter(): DBAdapter { return { kind: 'pg', + async notify() {}, isClosed: false, async execute() { return []; diff --git a/packages/runtime-common/db.ts b/packages/runtime-common/db.ts index 7e92381cff7..9f327683a72 100644 --- a/packages/runtime-common/db.ts +++ b/packages/runtime-common/db.ts @@ -20,4 +20,9 @@ export interface DBAdapter { ) => Promise[]>; close: () => Promise; getColumnNames: (tableName: string) => Promise; + // Best-effort cross-instance broadcast on a named channel. Backends that + // don't support pub/sub (e.g. in-process SQLite) implement this as a no-op: + // the caller must treat it as fire-and-forget cache-coherency, never as + // delivery-guaranteed messaging. + notify: (channel: string, payload: string) => Promise; } diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index ec3c5aea1ab..ffa8d83c5b1 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -1125,19 +1125,17 @@ export class Realm { // same path. Best-effort — failures are logged and swallowed because the // local write already succeeded and a missed NOTIFY is a bounded cache- // staleness window (see docs §9 "Cache-invalidation NOTIFY missed"), not - // a correctness failure. + // a correctness failure. Adapters without pub/sub (e.g. SQLite in the + // host/browser context) implement notify as a no-op. async #notifyFileChange(path: LocalPath): Promise { try { - await query(this.#dbAdapter, [ - `SELECT pg_notify(`, - param(REALM_FILE_CHANGES_CHANNEL), - `,`, - param(`${this.url}:${path}`), - `)`, - ]); + await this.#dbAdapter.notify( + REALM_FILE_CHANGES_CHANNEL, + `${this.url}:${path}`, + ); } catch (err: unknown) { this.#log.warn( - `pg_notify ${REALM_FILE_CHANGES_CHANNEL} failed for ${this.url}:${path}: ${String(err)}`, + `notify ${REALM_FILE_CHANGES_CHANNEL} failed for ${this.url}:${path}: ${String(err)}`, ); } } diff --git a/packages/runtime-common/tests/run-command-task-shared-tests.ts b/packages/runtime-common/tests/run-command-task-shared-tests.ts index 824eaaaab8e..3784a2f5f5b 100644 --- a/packages/runtime-common/tests/run-command-task-shared-tests.ts +++ b/packages/runtime-common/tests/run-command-task-shared-tests.ts @@ -15,6 +15,7 @@ function makeDBAdapter( ): DBAdapter { return { kind: 'pg', + notify: async () => {}, isClosed: false, execute: async (sql: string, opts?: ExecuteOptions) => { assertion?.(sql, opts);