Skip to content

Mutating listener deletions during sync don't propagate back to source #280

@bitmage

Description

@bitmage

Description

When using a mutating listener to reject/delete incoming synced data, the deletion doesn't propagate back to the originating client. However, explicit delRow calls after sync completes do propagate correctly.

Use Case

Server-side validation: A server wants to reject invalid data that clients attempt to sync. The pattern is to use a mutating listener that deletes rows with invalid values.

Client side validation satisfies the requirements for UX, but does not satisfy the security requirements - that needs to happen server side.

The ideal implementation would have the same validation run on both client and server. But in this case we omit the client validation to demonstrate that the server does not behave as expected.

Reproduction

import {createMergeableStore} from 'tinybase'
import {createLocalSynchronizer} from 'tinybase/synchronizers/synchronizer-local'

const clientStore = createMergeableStore()
const serverStore = createMergeableStore()

// Server rejects rows with name "Delete Me"
serverStore.addRowListener('jobs', null, (_store, _tableId, rowId) => {
  const row = serverStore.getRow('jobs', rowId)
  if (row.name === 'Delete Me') {
    serverStore.delRow('jobs', rowId)
  }
}, true) // mutator = true

const clientSync = await createLocalSynchronizer(clientStore)
const serverSync = await createLocalSynchronizer(serverStore)
await clientSync.startSync()
await serverSync.startSync()

clientStore.setRow('jobs', 'job-1', {name: 'Delete Me'})

await new Promise(r => setTimeout(r, 300))

console.log(serverStore.getRow('jobs', 'job-1')) // {} ✓ deleted
console.log(clientStore.getRow('jobs', 'job-1')) // {name: 'Delete Me'} ✗ still exists

Expected Behavior

The server's deletion should sync back to the client, removing the row from the client store.

Actual Behavior

  • Server correctly deletes the row locally
  • Client retains the original data
  • The tombstone doesn't appear to propagate

Working Case

If I wait for sync to complete, then delete in a separate operation, it works:

clientStore.setRow('jobs', 'job-1', {name: 'Test'})
await waitForSync()
serverStore.delRow('jobs', 'job-1') // This DOES sync back correctly

Questions

  1. Is this expected behavior for mutating listeners during sync?
  2. Is there a recommended pattern for server-side validation that rejects incoming sync data?

Environment

  • TinyBase 7.3.1
  • Node.js (tested with createLocalSynchronizer)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions