How Loco Sync Works
Sync Actions
The main building block of the sync engine is the sync action. A sync action is an action on a particular instance of a model. In particular, an action is "insert"
, "update"
, or "delete"
, and the model instance is recognized by the combination of modelName
, and modelId
. The order of sync actions is defined by its syncId
, which you can think of as an auto-incrementing sequence.
For example, a sync action corresponding to an insert of a Todo
might be:
const syncAction: SyncAction<M, 'Todo'> = {
syncId: 123,
action: 'insert',
modelName: 'Todo',
modelId: '1',
data: { id: '1', text: 'hello', authorId: '2' },
};
Sync actions are created by an authoritative backend. Every change to synced data in your application should be reflected in a corresponding sync action.
Sync actions must applied in the same order across all clients to ensure all of them to converge on the same state. The next section describes how this is guaranteed.
Syncing Protocol
The following steps are performed to sync the client with changes from the backend, by ensuring sync actions are applied in the correct order:
- Check if Loco Sync data exists locally via the storage adapter
- If so, load the data into memory and read the
lastSyncId
from the metadata - Else, call
bootstrap
, which returns both a snapshot of the data but also the associatedsyncId
- If so, load the data into memory and read the
- Call
initSync
to begin receiving sync actions- Do not apply these sync actions immediately - there could be a gap between the
lastSyncId
from step 1 and the first sync action received (especially when loading from storage)
- Do not apply these sync actions immediately - there could be a gap between the
- Call
deltaSync
to fetch all of the sync actions between thelastSyncId
from step 1 and the first- Apply the sync actions returned, followed by those accrued from
initSync
in the mean time
- Apply the sync actions returned, followed by those accrued from
- Start applying sync actions to storage and to in-memory values, receive changes from server
- If
initSync
disconnects, return to step 2, but use thelastSyncId
from the last sync action received rather than step 1
- If
Applying a sync action means updating the storage and in-memory representations of the associated data.
Mutations
When mutating data, three important things happen:
- A transaction is save in storage
- This can facilitate retries when coming back online
- Optimistic changes are applied to the data for instant updates
- The transaction is sent to the server to mutation
- If successful, save the
lastSyncId
of the sync actions produced by the transaction (returned viasendTransaction
network adapter method) - Otherwise, rollback the transaction locally
- If successful, save the
Though mutations are applied optimistically on the client, they may be rejected by the server. In that case, they are be rolled back. For this reason, optimistic changes are not applied to data that has been confirmed from the server (via one of the network adapter methods), but rather combined before being shown.
Besides failed mutations, pending transactions are actually also rolled back when applying a sync action with a syncId
greater than the lastSyncId
of that transaction (from 3 above). This means the sync actions from the server for that transaction have already been applied to the client, so the client can be safely drop the transaction.
Conflicts
On the client, optimistic changes are rebased with confirmed data. If multiple actions affect the same data, conflicts are handled with a last-write-wins convention by default. When applied at the field level with models that are not highly nested, collisions can be minimal in most applications. However, the server can implement any desired conflict resolution pattern. Most cases can be handled via last-write-wins and additional models to track conflicts, but Loco Sync also support custom conflict merging on the client via custom mutations.