---
title: Offline Sync Patterns
aliases: [sync, offline, STATE, syncStatus, replication, tracked object]
sources: [sources/sessions/2026-04-20-offline-sync-patterns.md]
last_updated: 2026-05-04
status: draft
---

# Offline Sync Patterns

The CG Mobile App operates offline-first. All data lives in local SQLite; background sync handles Salesforce communication.

## STATE Flags (Object Lifecycle)

Bitwise flags track where an object is in its lifecycle:

| Constant          | Value | Meaning                       |
| ----------------- | ----- | ----------------------------- |
| `STATE.NEW`       | 1     | Created locally, never synced |
| `STATE.DIRTY`     | 2     | Modified locally, needs sync  |
| `STATE.PERSISTED` | 4     | Synced to Salesforce          |
| `STATE.DELETED`   | 8     | Marked for deletion           |

### Combining Flags (Bitwise OR)

```javascript
// New object with local changes (most common for creates)
me.setObjectStatus(STATE.NEW | STATE.DIRTY);

// Existing object modified locally
me.setObjectStatus(STATE.DIRTY | STATE.PERSISTED);

// State transition item
liRecentState.objectStatus = STATE.NEW | STATE.DIRTY;
```

### Checking Flags (Bitwise AND)

```javascript
// Check if deleted
if ((objectStatus & STATE.DELETED) > 0) { ... }

// Check if new AND deleted (created then deleted locally — skip sync)
var stateNewDeleted = STATE.NEW | STATE.DELETED;
if ((objectStatus & stateNewDeleted) == stateNewDeleted) {
    // Never existed in Salesforce, no need to sync delete
}
```

### Clearing Flags (Bitwise AND NOT)

```javascript
// Un-delete: clear the DELETED flag
item.setObjectStatus(item.getObjectStatus() & ~STATE.DELETED);
```

## syncStatus (Sync Queue State)

Database column `_syncStatus` tracks sync progress:

| Value | Constant                            | Meaning  | Icon              |
| ----- | ----------------------------------- | -------- | ----------------- |
| 0     | `BLConstants.Order.SYNC_SUCCESSFUL` | Synced   | (none)            |
| 1     | `BLConstants.Order.SYNC_AWAITING`   | In queue | OrderAwaitingSync |
| 2     | `BLConstants.Order.SYNC_ERROR`      | Failed   | OrderSyncError    |
| 3     | `BLConstants.Order.NOT_SYNCABLE`    | Locked   | OrderNotSyncable  |

Used in DS for icon display:

```sql
CASE WHEN _syncStatus = 1 THEN 'OrderAwaitingSync'
     WHEN _syncStatus = 2 THEN 'OrderSyncError'
     WHEN _syncStatus = 3 THEN 'OrderNotSyncable'
     ELSE 'EmptyImage' END AS syncStatusIcon
```

## Save & Sync Flow

```
User modifies BO
    ↓
me.setObjectStatus(STATE.DIRTY | STATE.PERSISTED)
    ↓
Process SAVE action → saveAsync()
    ↓
BoSfHelper.saveTrackedObject(bo, mapping)
    ↓
Determines changeType from objectStatus:
  STATE.NEW → "N" (insert)
  STATE.DIRTY → "U" (update)
  STATE.DELETED → "D" (delete)
    ↓
Facade.putTrackedObjectInTransaction({
  tableName: editableEntity,    // e.g., "Order__c"
  idAttribute: "Id",
  mapping: property→column,
  changeType: "N"/"U"/"D",
  data: serialized object
})
    ↓
Transaction stored in local sync queue
    ↓
Background replication (_syncStatus = 1 → 0 on success, 2 on error)
```

## Soft Delete Pattern (IsDeleted)

Salesforce objects use `IsDeleted` flag for soft deletion:

```sql
-- All queries filter out deleted records
WHERE Visit.IsDeleted = '0'
AND Task.IsDeleted = '0'
```

When `STATE.DELETED` is set:

1. `saveTrackedObject` sends changeType "D"
2. Salesforce sets `IsDeleted = '1'`
3. Record remains in DB but invisible to queries

## On-Demand Data (Named Fetch Trees)

For data not synced via Tracked Objects (large datasets, customer-specific):

```javascript
// Framework calls this on sync cycle
function requestOnDemandDataAsync(syncContext) {
    // 1. Determine user role (DSD vs Retail)
    var roles = me.getCurrentUserRoles();

    // 2. Query local DB to identify needed data
    var sqlBulkJsonArray = [
        {
            dataSource: 'DsBoSfReplicationCallbacks',
            dataSourceMethod: 'CustomersWithCallsForSync',
            jsonParams: workingDayRange,
            uniqueReturnKey: 'CustomersWithCallsForSync',
        },
    ];

    // 3. Execute bulk queries
    return me.executeRequestsAsync(sqlBulkJsonArray).then(function (result) {
        // 4. Build NFT requests based on results
        me.addCallNftsToRequest(request, customerIds);
        // 5. Pull from Salesforce
        return Facade.requestBatchNftDataAndWait(request);
    });
}
```

Availability checks before each NFT:

-   `isCallOnDemandDataAvailable()`
-   `isOrderOnDemandDataAvailable()`
-   `isDSDVisitOnDemandDataAvailable()`

## BLConstants (Sync-Related)

```javascript
// Sync options
BLConstants.SYNC.SYNC_OPTION_RELEASE = 'Release';
BLConstants.SYNC.SYNC_OPTION_CANCEL = 'Cancel';

// Order phases (affect sync eligibility)
BLConstants.Order.PHASE_INITIAL = 'Initial';
BLConstants.Order.PHASE_RELEASED = 'Released'; // Can sync
BLConstants.Order.PHASE_CANCELED = 'Canceled';

// Visit statuses
BLConstants.VISIT.STATUS_PLANNED = 'Planned';
BLConstants.VISIT.STATUS_INPROGRESS = 'InProgress';
```

## Sync Module Location

| Path                                    | Purpose                                   |
| --------------------------------------- | ----------------------------------------- |
| `src/Sync/BO/BoSfReplicationCallbacks/` | Main sync callbacks (requestOnDemandData) |
| `src/Sync/BO/LoSync/`                   | Sync tracking list                        |
| `src/Sync/PR/Sync_Overview/`            | Sync status UI                            |
| `src/Utilities/BO/BoSfHelper/`          | SaveTrackedObject utilities               |
| `src/Plugins/BLConstants/`              | Sync constants                            |

## Known Issue — `Tracked_Relationship__c = "false"` breaks sync

**Symptom (simulator / server log):**

```
Configured tracked relation field "false" not found in object: <SObject>
Exception: cgc_sync.SYNCException — Line Number: 676
```

**Cause:** In the connected org, `cgc_sync__Sync_Tracked_Object_Config__c` holds one row per tracked SObject. Its `cgc_sync__Tracked_Relationship__c` is `Text(255)` and should hold **either a relationship-field API name** (e.g. `cgcloud_dev__Order__c`) **or be blank**. A seed flow can write the literal string `"false"` into it (column misalignment with the adjacent `Tracking_enabled__c` boolean), and the sync engine then tries to look up a field named `false`.

**Debug:**

```bash
# Count offenders — any non-zero means the bug is present
sf data query --target-org <alias> \
  -q "SELECT COUNT() FROM cgc_sync__Sync_Tracked_Object_Config__c
      WHERE cgc_sync__Tracked_Relationship__c IN ('true','false')"
```

**Fix (null out the bad values — `Tracked_Relationship__c` should be either a lookup field API name or null):**

```bash
sf data query --target-org <alias> \
  -q "SELECT Id FROM cgc_sync__Sync_Tracked_Object_Config__c
      WHERE cgc_sync__Tracked_Relationship__c = 'false'" \
  -r csv > /tmp/ids.csv

awk 'NR==1 {print "Id,cgc_sync__Tracked_Relationship__c"; next}
     {print $1 ",#N/A"}' /tmp/ids.csv > /tmp/null.csv   # #N/A = null in bulk API

sf data update bulk --target-org <alias> \
  --sobject cgc_sync__Sync_Tracked_Object_Config__c \
  --file /tmp/null.csv --wait 10
```

Legitimate rows (a real relationship field API name like `cgcloud_dev__Order__c`) are not matched by the `= 'false'` filter and are preserved.

## Cross-References

-   [[business-objects]] — objectStatus set on BO save lifecycle
-   [[datasource]] — editableEntity determines sync target
-   [[business-logic]] — BL manages STATE flags, calls Facade.putTrackedObjectInTransaction
-   [[architecture-overview]] — Offline-first principle
