Skip to content

Bookkeeper

Designed to track local data changes that haven’t been successfully synchronized with the remote data source. It plays a crucial role in conflict resolution, especially in applications that support offline operations or have intermittent network connectivity.

Relevant Context

Conflict resolution is critical when your app makes local changes while offline or when there are discrepancies between the client and server data. The Bookkeeper helps by:

  • Versioning: Recording timestamps of failed syncs allows you to determine when changes occurred, which is essential for resolving conflicts.
  • Strategies: Depending on your application’s needs, you might implement different conflict resolution strategies. A common approach in mobile apps is “last write wins,” where the most recent change overwrites previous ones.

Purpose of the Bookkeeper

  • Tracking Failed Synchronizations: The Bookkeeper records instances when local updates fail to sync with the remote source.
  • Conflict Resolution: By keeping a record of failed syncs, the Bookkeeper enables the Store to identify and resolve conflicts between local and remote data upon the next synchronization attempt.
  • Data Consistency: Helps maintain consistency between the client’s local data and the server’s data by ensuring that unsynced changes are not forgotten and are eventually synchronized.

APIs

Bookkeeper

Bookkeeper has the following structure:

kotlin
interface Bookkeeper<Key : Any> {
    suspend fun getLastFailedSync(key: Key): Long?

    suspend fun setLastFailedSync(
        key: Key,
        timestamp: Long = now(),
    ): Boolean

    suspend fun clear(key: Key): Boolean

    suspend fun clearAll(): Boolean
}
Parameter
Key
Type
Any
Required
Required
Description

The type representing the key used to identify the data item.

Parameter
getLastFailedSync(key: Key)
Type
Long?
Required
Optional
Description

Returns the timestamp of the last failed sync attempt for the given key.

Parameter
setLastFailedSync(key: Key, timestamp: Long = now())
Type
Boolean
Required
Optional
Description

Records a failed sync attempt with the provided timestamp.

Parameter
clear(key: Key)
Type
Boolean
Required
Optional
Description

Clears the record of failed syncs for the given key.

Parameter
clearAll()
Type
Boolean
Required
Optional
Description

Clears all records of failed syncs.

Data Flow

Implementing a Bookkeeper

You can create a Bookkeeper using the Bookkeeper.by factory method:

kotlin
val bookkeeper = Bookkeeper.by(
    getLastFailedSync = { key ->
        // Retrieve timestamp from storage
    },
    setLastFailedSync = { key, timestamp ->
        // Save timestamp to storage
        true
    },
    clear = { key ->
        // Remove entry from storage
        true
    },
    clearAll = {
        // Clear all entries from storage
        true
    }
)

Example

From the Trails app:

kotlin
package org.mobilenativefoundation.trails.xplat.lib.market.post.impl.store

private fun createBookkeeper(): Bookkeeper<Int> =
  Bookkeeper.by(
      getLastFailedSync = { id ->
          trailsDatabase.postBookkeepingQueries
              .selectMostRecentFailedSync(id).executeAsOneOrNull()?.let { failedSync ->
                  timestampToEpochMilliseconds(timestamp = failedSync.timestamp)
              }
      },
      setLastFailedSync = { id, timestamp ->
          try {
              trailsDatabase.postBookkeepingQueries.insertFailedSync(
                  PostFailedSync(
                      post_id = id,
                      timestamp = epochMillisecondsToTimestamp(timestamp)
                  )
              )
              true
          } catch (e: SQLException) {
              // Handle the exception
              false
          }
      },
      clear = { id ->
          try {
              trailsDatabase.postBookkeepingQueries.clearByPostId(id)
              true
          } catch (e: SQLException) {
              // Handle the exception
              false
          }
      },
      clearAll = {
          try {
              trailsDatabase.postBookkeepingQueries.clearAll()
              true
          } catch (e: SQLException) {
              // Handle the exception
              false
          }
      }
  )

Best Practices

  • Persistent Storage: Use persistent storage (e.g., a local database) for the Bookkeeper to ensure that failed sync records are not lost between app sessions.
  • Error Handling: Ensure that exceptions are properly caught and handled when performing sync operations.