Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/db/src/collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,58 @@ export class CollectionImpl<
return this._events.waitFor(event, timeout)
}

/**
* Get the synced (server-side) value for a key, without any optimistic overlays.
*
* This returns the authoritative server state for an entity, ignoring any
* pending optimistic mutations. Useful for detecting server-side changes
* during paced mutation persistence — compare `mutation.original` against
* the current synced value to decide whether to persist, merge, or skip.
*
* @param key - The key to look up
* @returns The synced value, or undefined if no synced data exists for this key
* @example
* // In a paced mutation's mutationFn, check for server-side conflicts
* mutationFn: async ({ transaction }) => {
* for (const mutation of transaction.mutations) {
* const serverValue = collection.getSyncedValue(mutation.key)
* if (serverValue && !deepEquals(serverValue, mutation.original)) {
* // Server data changed since this mutation was created — reconcile
* return
* }
* }
* await api.save(transaction.mutations)
* }
*/
public getSyncedValue(key: TKey): TOutput | undefined {
return this._state.syncedData.get(key)
}

/**
* Get the sync metadata for a key.
*
* Sync metadata is set by the sync layer via write operations and can
* contain information like revision numbers, timestamps, or ETags that
* are useful for conflict detection.
*
* @param key - The key to look up
* @returns The sync metadata, or undefined if none exists for this key
* @example
* // Check revision number before persisting
* mutationFn: async ({ transaction }) => {
* const mutation = transaction.mutations[0]
* const meta = collection.getSyncedMetadata(mutation.key)
* if (meta?.revision !== mutation.syncMetadata.revision) {
* // Server revision changed — skip or merge
* return
* }
* await api.save(transaction.mutations)
* }
*/
public getSyncedMetadata(key: TKey): unknown {
return this._state.syncedMetadata.get(key)
}

/**
* Clean up the collection by stopping sync and clearing data
* This can be called manually or automatically by garbage collection
Expand Down
17 changes: 7 additions & 10 deletions packages/db/src/paced-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,13 @@ export function createPacedMutations<

// Commit callback that the strategy will call when it's time to persist
const commitCallback = () => {
if (!activeTransaction) {
throw new Error(
`Strategy callback called but no active transaction exists. This indicates a bug in the strategy implementation.`,
)
}

if (activeTransaction.state !== `pending`) {
throw new Error(
`Strategy callback called but active transaction is in state "${activeTransaction.state}". Expected "pending".`,
)
if (!activeTransaction || activeTransaction.state !== `pending`) {
// Transaction was externally cancelled (e.g., rolled back due to a
// server-side update via collection.rollbackOptimisticUpdates(), or
// via cascade conflict rollback). Clear the reference so the next
// mutate() call creates a fresh transaction.
activeTransaction = null
return
}

const txToCommit = activeTransaction
Expand Down
Loading
Loading