Skip to content

Use shallowRef for database instance to prevent DataCloneError in worker communication#895

Merged
khawarizmus merged 5 commits into
mainfrom
fix-comlink-vue
Mar 26, 2026
Merged

Use shallowRef for database instance to prevent DataCloneError in worker communication#895
khawarizmus merged 5 commits into
mainfrom
fix-comlink-vue

Conversation

@khawarizmus
Copy link
Copy Markdown
Contributor

Problem

When using @powersync/vue with enableMultiTabs: true and useWebWorker: true, the sync system fails to initialize with:

Uncaught (in promise) DataCloneError: Failed to execute 'postMessage' on 'MessagePort': [object Array] could not be cloned.
    at comlink.mjs:347:12
    at new Promise (<anonymous>)
    at requestResponseMessage (comlink.mjs:341:12)
    at Object.apply (comlink.mjs:269:20)
    at SharedWebStreamingSyncImplementation._init (SharedWebStreamingSyncImplementation.js:179:32)

This causes waitForFirstSync() and waitForStatus() to hang indefinitely, as the _init() promise never resolves and waitForReady() blocks all subsequent calls including connect().

Root Cause

In @powersync/vue, both createPowerSyncPlugin and providePowerSync wrap the database instance in Vue's ref():

// createPowerSyncPlugin
app.provide(POWERSYNC_KEY, ref(powerSyncPluginOptions.database));

// providePowerSync
provide(key || POWERSYNC_KEY, ref(database));

Vue's ref() creates a deep reactive Proxy over the database instance. Every property accessed through powerSync.value (including internal arrays like subscriptions) is returned as a Proxy(Array) instead of a plain Array.

When SharedWebStreamingSyncImplementation._init() calls:

await this.syncManager.setParams({
    dbParams: this.dbAdapter.getConfiguration(),
    streamOptions: { ... }
}, this.options.subscriptions);  // ← Proxy(Array) — not cloneable

Comlink's postMessage attempts to clone the arguments for the SharedWorker. structuredClone (used internally by postMessage) cannot serialize Vue Proxy objects, so it throws DataCloneError. Since this error occurs inside _init(), the isInitialized promise rejects, waitForReady() never resolves, and the entire sync pipeline stalls.

Impact

This affects any @powersync/vue user with shared workers enabled (enableMultiTabs: true + useWebWorker: true). The symptoms are:

  • DataCloneError in console on connection
  • waitForFirstSync() hangs indefinitely
  • waitForStatus() hangs indefinitely
  • connect() hangs indefinitely (it awaits waitForReady())

Steps to Reproduce

  1. Create a Vue/Nuxt app using @powersync/vue
  2. Initialize PowerSyncDatabase with enableMultiTabs: true and useWebWorker: true
  3. Install the plugin with createPowerSyncPlugin({ database: db })
  4. In a component, use usePowerSync() to get the database ref
  5. Call powerSync.value.connect(connector) followed by await powerSync.value.waitForFirstSync()
  6. Observe DataCloneError in console and waitForFirstSync hanging

Workaround

Users can work around this by unwrapping the Proxy before calling connect, disconnect, disconnectAndClear, and subscribe:

import { toRaw } from "vue";

const powerSync = usePowerSync();
const rawDb = toRaw(powerSync.value);
await rawDb?.connect(connector, { params: { ... } });

Disabling workers "fixes" the issue. With useWebWorker: false and enableMultiTabs: false, the SharedWebStreamingSyncImplementation code path is skipped entirely.

Fix

Replace ref() with shallowRef() when providing the database instance. shallowRef only tracks .value reassignment — it does not deep-proxy the object, so internal arrays and properties remain plain JavaScript objects that postMessage can clone.

Backwards Compatibility

ShallowRef extends Ref, so existing code that accesses .value works unchanged. The only behavioral difference is that properties on the database instance are no longer deep-reactive.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 15, 2026

🦋 Changeset detected

Latest commit: 0877ecb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@powersync/vue Patch
@powersync/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@khawarizmus khawarizmus changed the title fix: use shallowRef for database instance to prevent DataCloneError in worker communication use shallowRef for database instance to prevent DataCloneError in worker communication Mar 15, 2026
@khawarizmus khawarizmus changed the title use shallowRef for database instance to prevent DataCloneError in worker communication Use shallowRef for database instance to prevent DataCloneError in worker communication Mar 15, 2026
@khawarizmus khawarizmus marked this pull request as ready for review March 25, 2026 07:02
Copy link
Copy Markdown
Contributor

@Chriztiaan Chriztiaan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a reasonable approach. My suggestion up till now to users encountering this issue was to mark the instance with markraw. This should also resolve the issue I believe.

@khawarizmus khawarizmus merged commit a465f6f into main Mar 26, 2026
9 checks passed
@khawarizmus khawarizmus deleted the fix-comlink-vue branch March 26, 2026 00:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants