diff --git a/.changeset/fix-comlink-dispose-listener.md b/.changeset/fix-comlink-dispose-listener.md new file mode 100644 index 000000000..c407afdc2 --- /dev/null +++ b/.changeset/fix-comlink-dispose-listener.md @@ -0,0 +1,5 @@ +--- +"@powersync/web": patch +--- + +Avoid binding `this` when disposing table change listeners in the web adapter to prevent Comlink serialization errors on close. diff --git a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts index 2b1f2221a..4a46e3a9c 100644 --- a/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts +++ b/packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts @@ -170,7 +170,10 @@ export class LockedAsyncDatabaseAdapter */ async close() { this.closing = true; - this._disposeTableChangeListener?.(); + const dispose = this._disposeTableChangeListener; + if (dispose) { + dispose(); + } this.pendingAbortControllers.forEach((controller) => controller.abort('Closed')); await this.baseDB?.close?.(); this.closed = true; diff --git a/packages/web/tests/locked_adapter_close.test.ts b/packages/web/tests/locked_adapter_close.test.ts new file mode 100644 index 000000000..e7bafa7cd --- /dev/null +++ b/packages/web/tests/locked_adapter_close.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { LockedAsyncDatabaseAdapter } from '../src/db/adapters/LockedAsyncDatabaseAdapter'; +import { AsyncDatabaseConnection } from '../src/db/adapters/AsyncDatabaseConnection'; +import { DEFAULT_CACHE_SIZE_KB, TemporaryStorageOption } from '../src/db/adapters/web-sql-flags'; +import { ResolvedWASQLiteOpenFactoryOptions } from '../src/db/adapters/wa-sqlite/WASQLiteOpenFactory'; +import { WASQLiteVFS } from '../src/db/adapters/wa-sqlite/WASQLiteConnection'; + +describe('LockedAsyncDatabaseAdapter.close', () => { + it('calls the table change disposer without binding this', async () => { + let thisValue: unknown; + let called = false; + + const disposer = function (this: unknown) { + thisValue = this; + called = true; + }; + + const config: ResolvedWASQLiteOpenFactoryOptions = { + dbFilename: 'test.db', + flags: { + broadcastLogs: true, + disableSSRWarning: false, + enableMultiTabs: false, + useWebWorker: false, + ssrMode: false + }, + temporaryStorage: TemporaryStorageOption.MEMORY, + cacheSizeKb: DEFAULT_CACHE_SIZE_KB, + vfs: WASQLiteVFS.OPFSCoopSyncVFS + }; + + const db: AsyncDatabaseConnection = { + init: async () => {}, + close: async () => {}, + markHold: async () => 'hold', + releaseHold: async () => {}, + isAutoCommit: async () => true, + execute: async () => ({ rows: { _array: [], length: 0 } } as any), + executeRaw: async () => [], + executeBatch: async () => ({ rows: { _array: [], length: 0 } } as any), + registerOnTableChange: async () => disposer, + getConfig: async () => config + }; + + const adapter = new LockedAsyncDatabaseAdapter({ + name: 'test-close', + openConnection: async () => db + }); + + await adapter.init(); + await adapter.close(); + + expect(called).toBe(true); + expect(thisValue).toBeUndefined(); + }); +});