Check out Handling CRUD Operations for a guide on supporting:

  • Create: Insert One, Insert Many
  • Read: Find One, Find Many, Find All, Observe One, Observe Many
  • Update: Update One, Update Many, Upsert One, Upsert Many
  • Delete: Delete One, Delete Many

Purpose of Mutable Store

  • Data Mutations: Supports creating, updating, and deleting data, in addition to reading.
  • Synchronization: Manages syncing local changes with remote data sources, handling network failures and retries.
  • Conflict Resolution: Works with the Bookkeeper and Updater to resolve conflicts that arise from concrruent modifications or offline changes.
  • Offline Support: Allows for local data modifications while offline, ensuring changes are eventually synchronized when connectivity is restored.

APIs

MutableStore

MutableStore has the following structure:

interface MutableStore<Key : Any, Output : Any> {
    fun <Response : Any> stream(
        request: StoreReadRequest<Key>,
    ): Flow<StoreReadResponse<Output>>

    suspend fun <Response : Any> write(
        request: StoreWriteRequest<Key, Output, Response>,
    ): StoreWriteResponse

    suspend fun clear(key: Key)

    suspend fun clearAll()
}
Key
Any
required

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

Output
Any
required

The type representing the domain data model representation of the item being retrieved.

Response
Any
required

The type representing the response received after updating the remote data source.

stream

A function that returns a Flow of StoreReadResponse.

request
StoreReadRequest<Key>
required

The request configuration for the data retrieval.

write

A function that returns a StoreWriteResponse.

request
StoreWriteRequest<Key, Output, Response>
required

The request configuration for the data mutation.

clear

A function that clears the data item identified by the given key.

This method only removes data from the delegate Store. It will not update the remote data source.

key
Key
required

The key identifying the data item to be cleared.

clearAll

A function that clears all data items.

This method only removes data from the delegate Store. It will not update the remote data source.

Key Components

The RealMutableStore is the default implementation of the MutableStore interface. It’s composed of the following components:

  1. Delegate Store: Read operations are delegated to an instance of RealStore. This delegation allows RealMutableStore to leverage the efficient caching and data retrieval mechanisms already implemented in RealStore.

  2. Updater: Handles applying local changes to the remote data source. It defines the logic for how data mutations are synchronized with the server.

  3. Bookkeeper: Rracks failed synchronization attempts. It ensures that any local changes that could not be synced due to network issues or other failures are retried, maintaining data consistency.

  4. Write Request Queue: For each key, RealMutableStore maintains a write request queue (WriteRequestQueue<Key, Output, *>). This queue holds pending write operations that need to be synchronized with the remote data source. New write requests are added to the queue. Upon successful synchronization, processed requests are removed.

  5. Thread Safety Mechanisms: To handle concurrent operations safely, RealMutableStore uses:

    • A global Mutex to synchronize access to per-key ThreadSafety objects.

    • ThreadSafety objects that manage fine-grained locking mechanisms for each key.

    • Lightswitch mechanisms managing read-write locks efficiently.

  6. Conflict Resolution Logic: Conflict resolution is integral to RealMutableStore. At a high level:

    • Checks for conflicts using the Bookkeeper and pending write requests.
    • Attempts to synchronize with the remote source to resolve conflicts.
    • Updates the local state based on the outcome.

Data Flow

Reading Data

1

Initialization

Ensures that thead safety mechanisms and write request queues are initialized.

safeInitStore(request.key)
2

Eager Conflict Resolution

Attempts to resolve any conflicts before proceeding with the read operation.

tryEagerlyResolveConflicts<Response>(request.key)
  • If conflicts exist, tries to synchronize local changes with the remote source.
  • If no conflicts, proceeds to data retrieval.
3

Data Retrieval

Delegates the read operation to the RealStore instance.

delegate
    .stream(request)
    .collect { storeReadResponse ->
        emit(storeReadResponse)
    }

Writing Data

1

Request Queueing

The write request is added to the per-key write request queue.

addWriteRequestToQueue(writeRequest)
2

Local Update

The value is immediately written to the local data source.

delegate.write(writeRequest.key, writeRequest.value)
3

Server Synchronization Attempt

Attempts to update the remote data source using the Updater.

val updaterResult = tryUpdateServer(writeRequest)
4

Handling the Updater Result

A

Success

  • Updates the write request queue, removing processed requests.
  • Clears any failed sync records from the Bookkeeper.
updateWriteRequestQueue<Response>(
    key = request.key,
    created = request.created,
    updaterResult = updaterResult
)

bookkeeper?.clear(request.key)
B

Failure

  • Records the failed attempt using the Bookkeeper.
bookkeeper?.setLastFailedSync(request.key)
5

Response Emission

Emits a StoreWriteResponse indicating success or failure.

emit(storeWriteResponse)