Sync Component
Introductionβ
The Sync component is a non-visual SDUI component that enables offline-first capabilities for mobile applications. It defines what data to pre-load and cache locally, how to queue operations (workflow executions, file uploads) when offline, and how to synchronize changes when connectivity is restored.
The Sync component acts as a data layer adapter β other SDUI components (datasource, form, button) interact with it transparently, without needing to know whether data comes from the network or local cache.
Key Capabilitiesβ
- Data pre-loading: Cache configurable datasets for offline access
- Operation queuing: Persist workflow executions and mutations when offline
- Background sync: Process queued operations automatically via expo-task-manager
- Attachment uploads: Queue and upload files in the background with compression
- Status visibility: Expose sync state for UI indicators and an operations screen
- Conflict resolution: Last-write-wins strategy for offline changes
Design Principlesβ
- Declarative: Sync behavior is defined in YAML, consistent with the SDUI pattern
- Transparent: Consuming components don't distinguish between online and offline
- Resilient: Queue persists across app restarts and survives app kills
- Configurable: Each workflow and model can specify its own sync strategy
YAML Structureβ
component: sync
name: <syncInstanceName>
props:
models:
- name: <string>
query: <graphql>
variables: <object>
keys: [<string>]
syncInterval: <number>
syncOn: [<trigger>]
maxAge: <number>
queue:
maxRetries: <number>
retryInterval: <number>
retryStrategy: <strategy>
persistQueue: <boolean>
backgroundSync: <boolean>
backgroundInterval: <number>
maxQueueSize: <number>
workflows:
default: <mode>
items:
- workflowId: <string>
mode: <mode>
optimisticResult: <object>
rollbackAction: <action[]>
dependsOn: [<string>]
attachments:
strategy: <strategy>
maxConcurrent: <number>
retryOnFailure: <boolean>
compress:
enabled: <boolean>
quality: <number>
maxDimension: <number>
status:
showIndicator: <boolean>
position: <position>
showPendingCount: <boolean>
notifyOnComplete: <boolean>
notifyOnError: <boolean>
Attribute Descriptionβ
Top-Level Propsβ
| Attribute | Type | Required | Description |
|---|---|---|---|
models | array | No | List of data models to pre-load and cache for offline access |
queue | object | No | Configuration for the offline operation queue |
workflows | object | No | Workflow execution mode configuration (queue vs optimistic) |
attachments | object | No | File upload queue configuration |
status | object | No | Sync status UI indicator configuration |
Modelsβ
Each model defines a dataset to cache locally for offline access.
| Attribute | Type | Default | Description |
|---|---|---|---|
name | string | β | Required. Unique identifier for this model within the sync instance |
query | string | β | Required. GraphQL query string to fetch the data |
variables | object | {} | Template-parsed variables passed to the query. Supports {{ }} expressions |
keys | string[] | β | Required. Primary key field(s) for cache identity and deduplication |
syncInterval | number | 300 | Seconds between automatic background refreshes when online |
syncOn | string[] | ["networkReconnect"] | Additional triggers for syncing this model |
maxAge | number | 86400 | Maximum seconds before cached data is considered stale (default: 24h) |
filter | string | β | Optional default filter applied to cached results |
maxItems | number | 1000 | Maximum number of items to cache for this model |
syncOn Triggersβ
| Value | Description |
|---|---|
appForeground | Sync when the app returns to the foreground |
networkReconnect | Sync when network connectivity is restored |
manual | Only sync when explicitly triggered by user action or refresh action |
intervalOnly | Only sync on the defined syncInterval, no event-based triggers |
Queueβ
Configuration for the offline operation queue that persists and processes mutations/workflows.
| Attribute | Type | Default | Description |
|---|---|---|---|
maxRetries | number | 5 | Maximum retry attempts for a failed operation |
retryInterval | number | 30 | Base retry delay in seconds |
retryStrategy | string | "exponential" | Backoff strategy: "exponential" or "fixed" |
persistQueue | boolean | true | Whether the queue persists to device storage (survives app restart) |
backgroundSync | boolean | true | Whether to use expo-task-manager for background queue processing |
backgroundInterval | number | 60 | Minimum seconds between background sync attempts |
maxQueueSize | number | 100 | Maximum pending operations before rejecting new entries |
Workflowsβ
Defines how each workflow action behaves when executed offline.
| Attribute | Type | Default | Description |
|---|---|---|---|
default | string | "queue" | Default mode for workflows not explicitly listed: "queue" or "optimistic" |
items | array | [] | Per-workflow configuration overrides |
Workflow Itemβ
| Attribute | Type | Default | Description |
|---|---|---|---|
workflowId | string | β | Required. The workflow identifier to configure |
mode | string | inherited | Execution mode: "queue" or "optimistic" |
optimisticResult | object | β | Expected result shape applied locally when in optimistic mode |
rollbackAction | action[] | β | Actions executed if the operation fails permanently after all retries |
dependsOn | string[] | β | Queue item types that must complete before this workflow executes (e.g., ["upload"]) |
Execution Modesβ
queue: The workflow call (workflowId + inputs) is stored in the queue. It executes server-side when connectivity is available. The user receives a "queued" confirmation but no immediate result.optimistic: TheoptimisticResultis applied to local state immediately, giving the user instant feedback. When online, the actual workflow executes. If it fails permanently,rollbackActionis triggered and the optimistic state is reverted.
Attachmentsβ
Configuration for the file upload queue.
| Attribute | Type | Default | Description |
|---|---|---|---|
strategy | string | "background" | Upload strategy: "background" (queue for later) or "immediate" (block until done) |
maxConcurrent | number | 2 | Maximum simultaneous upload operations |
retryOnFailure | boolean | true | Whether failed uploads are retried automatically |
compress.enabled | boolean | false | Whether to compress images before upload |
compress.quality | number | 0.8 | Image compression quality (0.0β1.0) |
compress.maxDimension | number | 2048 | Maximum width or height in pixels (aspect ratio preserved) |
Statusβ
Configuration for the sync status UI indicator.
| Attribute | Type | Default | Description |
|---|---|---|---|
showIndicator | boolean | true | Whether to show a sync status badge/icon |
position | string | "header" | Where to display the indicator: "top", "bottom", or "header" |
showPendingCount | boolean | true | Whether to show the count of pending operations as a badge |
notifyOnComplete | boolean | true | Show notification when all queued operations sync successfully |
notifyOnError | boolean | true | Show notification when an operation fails permanently |
Adapter Interfaceβ
Other SDUI components interact with the Sync component via the syncAdapter prop. This provides transparent offline access without the consuming component needing to handle online/offline logic.
Usage in DataSourceβ
- component: datasource
name: vehicleData
props:
syncAdapter: receivingSync
model: vehicles
filter: "trackingNumber:{{ trackingNumber }}"
When syncAdapter is specified, the datasource reads from the local cache first. If online and data is stale, it fetches fresh data in the background.
Usage in Workflow Actionsβ
- component: button
props:
label:
en-US: Submit
onClick:
- workflow:
workflowId: "6bda41d8-..."
inputs:
data: "{{ form.values }}"
syncAdapter: receivingSync
onQueued:
- notification:
message:
en-US: "Saved offline. Will sync when connected."
type: info
onSynced:
- notification:
message:
en-US: "Successfully synced to server."
type: success
Adapter Actionsβ
The sync adapter exposes the following actions that can be triggered from YAML:
| Action | Description |
|---|---|
syncNow | Force immediate sync of all models and process queue |
syncModel | Force sync of a specific model by name |
clearCache | Clear all cached data for this sync instance |
clearQueue | Discard all pending queue operations |
retryFailed | Retry all permanently failed operations |
- component: button
props:
label:
en-US: Refresh Data
onClick:
- syncAction:
adapter: receivingSync
action: syncModel
model: vehicles
New Action Eventsβ
When syncAdapter is specified on a workflow action, additional events become available:
| Event | Description |
|---|---|
onQueued | Fired when the operation is added to the offline queue (offline path) |
onSynced | Fired when a previously queued operation completes successfully |
onSyncFailed | Fired when a queued operation fails permanently after all retries |
Examplesβ
Basic: Cache a Single Modelβ
Pre-load customer contacts for offline access with refresh every 10 minutes:
component: sync
name: contactsSync
props:
models:
- name: customers
query: |
query($organizationId: Int!) {
contacts(organizationId: $organizationId, filter: "contactType:Customer") {
items {
contactId
name
email
phone
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [contactId]
syncInterval: 600
syncOn: [appForeground, networkReconnect]
Intermediate: Multiple Models with Workflow Queueβ
A receiving module that caches vehicles and locations, with offline workflow support:
component: sync
name: receivingSync
props:
models:
- name: vehicles
query: |
query($organizationId: Int!, $filter: String) {
orders(organizationId: $organizationId, filter: $filter, limit: 500) {
items {
orderId
trackingNumber
orderStatus { orderStatusName }
orderCommodities {
commodity { customValues }
}
billToContact { contactId name }
}
}
}
variables:
organizationId: "{{ number organizationId }}"
filter: "orderType:Freight"
keys: [orderId]
syncInterval: 300
syncOn: [appForeground, networkReconnect]
- name: warehouseLocations
query: |
query($organizationId: Int!) {
warehouseLocations(organizationId: $organizationId) {
items {
warehouseLocationId
name
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [warehouseLocationId]
syncInterval: 3600
queue:
maxRetries: 5
retryStrategy: exponential
backgroundSync: true
backgroundInterval: 60
workflows:
default: queue
items:
- workflowId: "6bda41d8-28af-42ac-9975-0f7adc0dc45e"
mode: optimistic
optimisticResult:
status: completed
rollbackAction:
- notification:
message:
en-US: "Receiving failed to sync. Check Sync Operations."
type: error
dependsOn: [upload]
- workflowId: "2ff3c10b-d2d9-418f-a41a-8834a58abf2e"
mode: queue
Advanced: Full Offline Module with Attachmentsβ
Complete sync configuration for a field inspection module with photo capture and background upload:
component: sync
name: inspectionSync
props:
models:
- name: assignments
query: |
query($organizationId: Int!, $assignedTo: String!) {
inspectionAssignments(
organizationId: $organizationId
filter: "assignedTo:{{ assignedTo }} AND status:Pending"
) {
items {
assignmentId
location { name address }
scheduledDate
inspectionType
notes
}
}
}
variables:
organizationId: "{{ number organizationId }}"
assignedTo: "{{ user.userId }}"
keys: [assignmentId]
syncInterval: 300
syncOn: [appForeground, networkReconnect]
maxItems: 200
- name: inspectionTemplates
query: |
query($organizationId: Int!) {
inspectionTemplates(organizationId: $organizationId) {
items {
templateId
name
sections { sectionId title fields { fieldId label type required } }
}
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [templateId]
syncInterval: 86400
syncOn: [manual]
queue:
maxRetries: 5
retryStrategy: exponential
retryInterval: 30
backgroundSync: true
backgroundInterval: 60
maxQueueSize: 200
persistQueue: true
workflows:
default: optimistic
items:
- workflowId: "a1b2c3d4-complete-inspection"
mode: optimistic
optimisticResult:
status: Completed
completedAt: "{{ format now() ISO }}"
rollbackAction:
- notification:
message:
en-US: "Inspection submission failed. Your data is saved locally."
type: warning
dependsOn: [upload]
attachments:
strategy: background
maxConcurrent: 2
retryOnFailure: true
compress:
enabled: true
quality: 0.8
maxDimension: 2048
status:
showIndicator: true
position: header
showPendingCount: true
notifyOnComplete: true
notifyOnError: true
Using the Adapter in a Formβ
How other components consume the sync adapter for seamless offline operation:
- component: form
name: inspectionForm
props:
validationSchema: {}
children:
# Load assignment data from cache
- component: datasource
name: assignmentData
props:
syncAdapter: inspectionSync
model: assignments
filter: "assignmentId:{{ assignmentId }}"
# Form fields...
- component: field
name: notes
props:
type: text
label:
en-US: Inspection Notes
# Photo attachment with offline queue
- component: field
name: photos
props:
type: attachment
label:
en-US: Take Photos
options:
allowMultiple: true
displayAs: image
parentType: Inspection
parentId: "{{ assignmentId }}"
# Submit with offline support
- component: button
name: submitInspection
props:
label:
en-US: Complete Inspection
options:
variant: primary
onClick:
- validateForm:
validationSchema:
notes:
required:
message: Notes are required
- workflow:
workflowId: "a1b2c3d4-complete-inspection"
inputs:
assignmentId: "{{ assignmentId }}"
notes: "{{ inspectionForm.notes }}"
photos: "{{ inspectionForm.photos }}"
syncAdapter: inspectionSync
onQueued:
- notification:
message:
en-US: "Inspection saved. Will sync when online."
type: info
- navigateBack:
onSynced:
- notification:
message:
en-US: "Inspection synced successfully."
type: success
Global Sync (System Module)β
A system-level sync component that caches shared data used across all modules:
component: sync
name: globalSync
props:
models:
- name: contacts
query: |
query($organizationId: Int!) {
contacts(organizationId: $organizationId, limit: 1000) {
items { contactId name contactType email phone }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [contactId]
syncInterval: 600
syncOn: [appForeground, networkReconnect]
- name: warehouseLocations
query: |
query($organizationId: Int!) {
warehouseLocations(organizationId: $organizationId) {
items { warehouseLocationId name code }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [warehouseLocationId]
syncInterval: 3600
- name: users
query: |
query($organizationId: Int!) {
users(organizationId: $organizationId) {
items { userId displayName email }
}
}
variables:
organizationId: "{{ number organizationId }}"
keys: [userId]
syncInterval: 3600
syncOn: [appForeground]
status:
showIndicator: true
position: header
showPendingCount: false
Sync Operations Screenβ
The Sync component automatically registers a system screen at the route sync-operations that displays all sync instances, their cached models, and the operation queue.
Screen Sectionsβ
- Status Bar: Current network state (Online/Offline), last sync timestamp, manual Sync Now button
- Sync Instances: Grouped by sync name, showing:
- Each cached model with item count and freshness indicator
- Pending queue items with operation type, timestamp, and retry count
- Queue Actions: Per-item Retry and Discard buttons
- History: Completed and failed operations with timestamps
Accessing the Screenβ
- component: button
props:
label:
en-US: View Sync Status
onClick:
- navigate: "sync-operations"
The screen is also accessible via the sync status indicator when tapped.
Architectureβ
Data Flowβ
βββββββββββββββββββββββββββββββββββββββββββ
β YAML Sync Definition β
β (models, queue, workflows, attachments) β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β parsed at module load
ββββββββββββββββββββΌβββββββββββββββββββββββ
β Sync Engine β
β ββββββββββββββ ββββββββββββββββββββ β
β βModel Store β β Operation Queue β β
β β(TanStack Q)β β (Zustand+persist)β β
β βββββββ¬βββββββ ββββββββββ¬ββββββββββ β
β β β β
β βββββββΌβββββββ ββββββββββΌββββββββββ β
β β Scheduler β βBackground Worker β β
β β(refetch) β β(expo-task-mgr) β β
β ββββββββββββββ ββββββββββββββββββββ β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββ
β Sync Adapter Interface β
β read() | queue() | upload() | status() β
ββββββββββββββββββββ¬βββββββββββββββββββββββ
β consumed by
ββββββββββββββββββββΌβββββββββββββββββββββββ
β SDUI Components β
β datasource | form | button | dataGrid β
βββββββββββββββββββββββββββββββββββββββββββ
Storage Strategyβ
| Data Type | Storage | Persistence |
|---|---|---|
| Cached model data | TanStack Query + AsyncStorage persister | Survives app restart |
| Operation queue | Zustand store + AsyncStorage | Survives app restart |
| Attachment queue | Zustand store + file system | Survives app restart |
| Sync configuration | In-memory (parsed from YAML) | Re-parsed on module load |
| Network state | In-memory (NetInfo) | Real-time only |
Queue Processing Orderβ
When connectivity is restored, the background worker processes operations in this order:
- Attachment uploads (to obtain real
attachmentIdvalues) - Workflows with
dependsOn: [upload](use real attachment IDs) - Independent workflows (no dependencies, processed in FIFO order)
- Model refresh (pull latest data after mutations complete)
Best Practicesβ
- Scope sync instances narrowly: Define per-module sync with only the data that module needs, rather than caching everything globally. This reduces storage usage and sync time.
- Use
dependsOnfor attachment workflows: Always declaredependsOn: [upload]for workflows that reference uploaded file IDs. This prevents submitting workflows with placeholder attachment references. - Set appropriate
syncInterval: Frequently changing data (vehicles, orders) benefits from shorter intervals (300s). Reference data (locations, templates) can use longer intervals (3600s+). - Prefer
queuemode for data-creation workflows: Use optimistic mode only when you can reliably predict the server result. For complex workflows with server-side validation,queuemode with clear "saved offline" messaging is safer. - Configure
maxItemsfor large datasets: Prevent excessive storage usage by limiting cached items to what a field worker reasonably needs in a session. - Use
syncOn: [manual]for large reference data: Inspection templates or configuration data that rarely changes should only sync on demand to avoid unnecessary network traffic. - Handle
onSyncFailedgracefully: Provide clear user messaging and a path to the Sync Operations screen where they can retry or discard failed operations. - Test with airplane mode: Validate the complete offline flow by enabling airplane mode, performing all operations, then re-enabling connectivity to verify queue processing.
Related Topicsβ
- DataSource Component β Primary consumer of sync adapter for data loading
- Button Component β Workflow actions with
syncAdapterprop - Form Component β Form submissions with offline queue support
- Field Component (Attachment) β File upload integration with sync queue