Synchronization
The synchronization system keeps client data in sync with the PHP backend through a long-poll mechanism. It is implemented in libs/upp-data/src/modules/sync.ts and coordinates with the server's connection/sync endpoints.
Architecture Overview
Key Classes
syncService
The Angular injectable (providedIn: 'root') that orchestrates everything. Key responsibilities:
- Session lifecycle:
Start(session)/Stop() - Entity association:
SetUser(user),SetPlace(place),SetQrCode(qrcode) - Change flushing:
Flush(changes, force)sends committed changes to the server - Observables:
OnChange,OnExpired,OnReleased,IsReady
@Injectable({ providedIn: 'root' })
export class syncService implements OnDestroy {
constructor(
public lang: languageService,
public store: storeService,
public cache: cacheService,
public adhoc: adhocService,
public state: stateService,
public clock: clockService,
public toast: toastService,
private deviceid: identificationService,
private alertCtrl: alertService
) { ... }
}
Connection
Manages the server connection lifecycle and periodic refresh:
| Method | Purpose |
|---|---|
CreateConnection() | Sends connection/create with triggers, then starts periodic refresh |
RefreshConnection() | Re-sends current triggers to connection/update |
ReleaseConnection() | Sends connection/delete, runs final sync, stops refresh |
SetUser(user) | Updates triggers with user info |
SetPlace(place) | Updates triggers with place info |
SetQrCode(qrcode) | Updates triggers with QR code info |
CancelRefresh() | Sends connection/cancel to interrupt a long-poll |
Connection status: RUNNING | STOPPED | EXPIRED.
Syncronizator
The core sync engine. Manages:
- Change queues:
_syncchanges(pending send),_waitchanges(waiting for next cycle),_sentchanges(in-flight) - Timestamps: Per-target (
SESSION,PLACE,QRCODE) to enable incremental sync - Server reference: Adjusts local timing to server clock
- Dependency resolution:
CheckDepends()ensures changes are ordered correctly - UUID → objid mapping:
ResolvedMaptracks server-assigned IDs for locally-created objects
PendingQueue
Manages incoming server updates with priority-based processing:
- Updates are assigned numeric priorities based on their table type
- Lower priority numbers are processed first (synchronously when possible)
- Processing is batched (up to 5000 items for high-priority, 500 for low) with a 50ms time limit per batch
- Progress tracking via
OnRefreshStageandOnRefreshPercent
CacheManager
Persists sync data to IndexedDB for faster startup:
- Session cache: Stores session-level updates keyed by
(deviceid, sessionid) - Place cache: Stores place-level updates keyed by
placeid - Validates cache freshness against
AppConstants.CacheExpiration - On startup, loads cached data into
PendingQueuebefore querying the server
Update Triggers
When a connection is created or updated, the client sends triggers that tell the server what data this connection cares about:
interface UpdateTriggers {
SESSION: string; // always required
QRCODE: string | null;
USER: string | null;
PLACE: string | null;
}
The server's CONNECTED table stores these triggers. When changes are committed that affect matching triggers, the server marks those connections for refresh.
Sync Flow
1. Starting a Session
await syncService.Start(sessionId);
This:
- Sets
state.session - Recovers any pending changes from previous sessions (via
storeService) - Loads cached session data from IndexedDB
- Creates a server connection with triggers
- Begins periodic refresh
2. Periodic Refresh Cycle
Every AppConstants.MainRefresh milliseconds:
- Prepare changes: Collects uncommitted changes, resolves UUID→objid for previously inserted items, assigns server time reference
- Send request:
POST /syncwith{ access, timestamp, target }as query params and changes as body - Process response:
- Store newly resolved UUID→objid mappings
- Save updates to IndexedDB cache
- Filter out volatile responses and already-pending items
- Enqueue updates into
PendingQueuefor prioritized processing
- Handle expiration: If
response.expired, reconnect automatically - Handle errors: Retry with backoff (2s for HTTP errors, 10s for server errors)
3. Committing Changes
When the client modifies data:
// DataObject collects changes recursively
const changes = ticket.Commit;
// Queue changes for next sync cycle
await dataService.Commit(changes);
await dataService.Flush(force);
The Syncronizator.FlushChanges() method:
- Deduplicates changes (same object replaces previous pending change)
- Validates dependency ordering via
CheckChanges() - Persists changes to local storage as a safety net
- If
force = true, cancels any running refresh and triggers an immediate one
4. Conflict Resolution
The system uses a last-write-wins strategy with merge:
private _MergeChange(chng1: any, chng2: any): any {
const updated1 = new Date(chng1['updated']);
const updated2 = new Date(chng2['updated']);
const source = updated1 > updated2 ? chng1 : chng2;
const target = updated1 > updated2 ? chng2 : chng1;
return { ...target, ...source };
}
When the same object has both a pending local change and a server update:
- The entry with the newer
updatedtimestamp takes precedence - Fields from both are merged, with the newer version's fields overriding
5. Dependency Ordering
Before sending changes, CheckChanges() ensures correct ordering:
- Topological sort: If change A depends on change B (references B's UUID), B must come first
- Relation map validation: Every UUID reference must either already have an
objidor be present earlier in the change list - Throws a descriptive error if ordering cannot be satisfied
Priority System
The PendingQueue assigns priorities to incoming updates based on table type. Lower numbers are processed first:
| Priority | Tables | Rationale |
|---|---|---|
| 1.1–1.5 | SESSION, FCM, USER, STAFF, PLACE | Critical session/identity data |
| 2.1–2.7 | ADDRESS, STRIPE, PLACEOPT, AUDIT, etc. | Infrastructure without UI components |
| 3.1–3.8 | PRODUCT, CATEGORY, OFFER, EXTRA, DISCOUNT, etc. | Catalog data (needed before tickets) |
| 4.1–4.2 | FAMILY, FAMILYPRODUCT, FAMILYPERIOD | Product groupings |
| 5.1–5.6 | TICKET*, SESSION | Active tickets and their components |
| 6.1–6.3 | QRCODE, PRINTER, PAYMENT, PLACE | Place configuration updates |
| 8.1–8.6 | (same tables, lower status) | Deferred items (e.g., deleted QR codes) |
Items with _actn == 'do_insert' get a -10 offset, ensuring inserts are processed before updates.
Stages 1–3 are processed synchronously (blocking the UI clock), while stages 4+ are processed asynchronously in 50ms batches with yields to the event loop.
Server-Side Sync
On the PHP backend, the sync flow (model/synchronize.php) works as follows:
- Validate session and commit POST body changes in a DB transaction
- Call
connection_notifyto mark affected connections asREFRESH - Long-poll: If
timestamp > 0and status isUPDATED, poll in a loop (~20s) waiting forREFRESH,EXPIRED, orCANCELLED - When refresh is due, load updates from the correct database:
- Central DB for user-target data (SESSION, USER, PLACE, STAFF)
- Tenant DB for place-target data (PRODUCT, TICKET, QRCODE, etc.)
- Return JSON:
{ errorcode, timestamp, reference, target, updated, expired, updates[] }
Connection State
- Stored in the
CONNECTEDtable (central DB) or in APCu (with 60s TTL) - All reads/writes are serialized under a mutex lock to prevent concurrent corruption
- Fields:
session,status(UPDATED/REFRESH/CANCELLED),synchronized,config(JSON triggers)
Triggers and Notification
Each connection's config holds triggers mapping tables to object IDs. When changes are committed:
- The server builds a list of
(table, objid)updates connection_notifymatches these against all active connections' triggers- Matching connections are marked
REFRESH - Only those clients receive data on their next poll
Session Expiration
When the server returns errorcode == 2 or errorcode == 3:
syncService.OnSessionExpired()is called- The synchronizer status is set to
EXPIRED - The connection is stopped
- A modal alert notifies the user
OnExpiredobservable emits, allowing the app to redirect to login
Stored Changes (Persistence)
Changes awaiting sync are persisted to storeService (Ionic Storage) under the key upp-stored-syncdata. This ensures that if the browser is closed or crashes:
- On next startup,
GetStoredChanges()recovers them - They are sent with the next sync cycle
- After successful sync,
DelStoredChanges()clears the stored copy
Retry limits prevent infinite loops: 50 retries for HTTP errors, 5 for server errors. If exhausted, changes are discarded and the user is notified.