Skip to content

fix(dexie-cloud): decode private key in changeSpecs for 'update' mutations#2277

Merged
dfahlander merged 1 commit into
masterfrom
liz/fix-update-mut-private-key-decode
Mar 26, 2026
Merged

fix(dexie-cloud): decode private key in changeSpecs for 'update' mutations#2277
dfahlander merged 1 commit into
masterfrom
liz/fix-update-mut-private-key-decode

Conversation

@liz709
Copy link
Copy Markdown
Contributor

@liz709 liz709 commented Mar 26, 2026

Problem

When the server sends an update-type mutation for a record with an inbound private ID primary key (e.g. #ical-calendars-data), sync fails permanently with Error: Cannot change primary key.

Root cause: In applyServerChanges.ts, the update case decodes mut.keys via keyDecoder (#key:userId#key) but does not update the primary key field inside mut.changeSpecs. When bulkUpdate() compares them, they mismatch:

cmp("#ical-calendars-data:[email protected]", "#ical-calendars-data") !== 0
→ throw new Error('Cannot change primary key')

The insert and upsert cases already handle this correctly via Dexie.setByKeyPath.

Fix

Before calling bulkUpdate(), check if the primary key field is present in each changeSpec (using Dexie.getByKeyPath to correctly handle compound/nested keyPaths) and overwrite it with the decoded key.

Testing

Reproduction:

  1. Table with inbound private ID primary key (e.g. setting table with id: "#ical-calendars-data")
  2. Write to record from one client
  3. Server responds with update-type mutation (observed with large blob references)
  4. Before fix: sync stuck permanently. After fix: sync proceeds normally.

Reported by: @bennie.forss

Summary by CodeRabbit

  • Bug Fixes
    • Fixed handling of server updates in cloud synchronization to prevent incorrect primary key data from being included in bulk update operations.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0fd8566b-7cc2-4415-99e2-cf2e7be428bf

📥 Commits

Reviewing files that changed from the base of the PR and between 8b682b1 and 2d0eacd.

📒 Files selected for processing (1)
  • addons/dexie-cloud/src/sync/applyServerChanges.ts

📝 Walkthrough

Walkthrough

The change modifies the 'update' mutation type handling in applyServerChanges.ts to remove primary key field values from each change spec before executing bulk updates. When applying inbound updates with a keyPath present, the code now iterates through change specs and deletes the primary-key field at the specified keyPath using Dexie.delByKeyPath before the bulkUpdate call.

Changes

Cohort / File(s) Summary
Primary Key Handling in Sync Updates
addons/dexie-cloud/src/sync/applyServerChanges.ts
Added iteration loop that removes the primary-key field from each change spec (via Dexie.delByKeyPath) before bulk update operation for inbound 'update' mutations with a defined keyPath.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #2279: Directly related modification to the same 'update' path in applyServerChanges to handle primary-key values within mut.changeSpecs.

Possibly related PRs

  • #2277: Directly related change stripping the primary-key field from each mut.changeSpecs element using Dexie.delByKeyPath before bulkUpdate in the same code path.

Suggested reviewers

  • dfahlander

Poem

🐰 A key that shouldn't stay,
Gets whisked away before the fray,
Sync paths now clean and bright,
Primary keys removed just right! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing how private keys in changeSpecs are handled for 'update' mutations in dexie-cloud, which directly addresses the primary modification in applyServerChanges.ts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch liz/fix-update-mut-private-key-decode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@addons/dexie-cloud/src/sync/connectWebSocket.ts`:
- Around line 64-69: The current switchMap waits on
db.cloud.persistedSyncState.take(1) which can replay a stale BehaviorSubject
value; change it to first attempt a fresh read via liveQuery(() =>
db.getPersistedSyncState()) and only if that fresh value has no serverRevision
fall back to subscribing to db.cloud.persistedSyncState as a backup waiter. In
other words, in the switchMap replace the single
persistedSyncState.pipe(filter(...), take(1)) with logic that (a) uses
liveQuery(() => db.getPersistedSyncState()).pipe(filter(sync => !!(sync &&
sync.serverRevision)), take(1)) and (b) if that completes without a valid
revision, subscribes to db.cloud.persistedSyncState.pipe(filter(...), take(1))
so the fresh DB read is preferred and the BehaviorSubject is only a fallback.

In `@addons/dexie-cloud/src/sync/LocalSyncWorker.ts`:
- Around line 22-24: The code clears pullSignalled and pushSignalled before
scheduling a backoff, which loses the original purpose and causes a failed pull
to be retried as a push; update the retry logic in syncAndRetry() so the
original purpose (derived from pullSignalled/pushSignalled or the local variable
purpose) is captured and passed into the backoff callback (or stored on the
retry context) instead of relying on the flags, and ensure syncIfPossible() is
invoked with options.purpose set to that captured purpose; apply the same change
to the other occurrence that clears flags (the block around the second instance
mentioned, lines 58–61) so pull retries remain pull retries until success or a
new signal overrides them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 374b873d-af53-4155-8ca7-1aa2d81ec6e5

📥 Commits

Reviewing files that changed from the base of the PR and between 9899e0c and 3d60548.

⛔ Files ignored due to path filters (2)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • samples/dexie-cloud-todo-app/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • addons/dexie-cloud/src/sync/LocalSyncWorker.ts
  • addons/dexie-cloud/src/sync/applyServerChanges.ts
  • addons/dexie-cloud/src/sync/connectWebSocket.ts
  • samples/dexie-cloud-todo-app/package.json

Comment thread addons/dexie-cloud/src/sync/connectWebSocket.ts
Comment thread addons/dexie-cloud/src/sync/LocalSyncWorker.ts
…tions

The primary key should never be part of an updateSpec — it cannot change
and is already communicated via the operation's keys array.

For private singleton IDs (e.g. '#key:userId' on server, '#key' on client),
the encoded server-side key may leak into the changeSpec via getObjectDiff().
This causes bulkUpdate() to throw 'Cannot change primary key' since the
changeSpec key mismatches the decoded key from mut.keys.

Strip the primary key unconditionally from all changeSpecs as a defensive
measure, mirroring how insert/upsert already use Dexie.setByKeyPath to
keep inbound keys consistent.

Fixes #2279
@liz709 liz709 force-pushed the liz/fix-update-mut-private-key-decode branch from 8b682b1 to 2d0eacd Compare March 26, 2026 14:35
@dfahlander dfahlander merged commit 75d544b into master Mar 26, 2026
6 checks passed
@dfahlander dfahlander deleted the liz/fix-update-mut-private-key-decode branch March 26, 2026 14:38
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.

2 participants