Skip to main content

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:

MethodPurpose
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: ResolvedMap tracks 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 OnRefreshStage and OnRefreshPercent

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 PendingQueue before 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:

  1. Sets state.session
  2. Recovers any pending changes from previous sessions (via storeService)
  3. Loads cached session data from IndexedDB
  4. Creates a server connection with triggers
  5. Begins periodic refresh

2. Periodic Refresh Cycle

Every AppConstants.MainRefresh milliseconds:

  1. Prepare changes: Collects uncommitted changes, resolves UUID→objid for previously inserted items, assigns server time reference
  2. Send request: POST /sync with { access, timestamp, target } as query params and changes as body
  3. Process response:
    • Store newly resolved UUID→objid mappings
    • Save updates to IndexedDB cache
    • Filter out volatile responses and already-pending items
    • Enqueue updates into PendingQueue for prioritized processing
  4. Handle expiration: If response.expired, reconnect automatically
  5. 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:

  1. Deduplicates changes (same object replaces previous pending change)
  2. Validates dependency ordering via CheckChanges()
  3. Persists changes to local storage as a safety net
  4. 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 updated timestamp takes precedence
  • Fields from both are merged, with the newer version's fields overriding

5. Dependency Ordering

Before sending changes, CheckChanges() ensures correct ordering:

  1. Topological sort: If change A depends on change B (references B's UUID), B must come first
  2. Relation map validation: Every UUID reference must either already have an objid or be present earlier in the change list
  3. 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:

PriorityTablesRationale
1.1–1.5SESSION, FCM, USER, STAFF, PLACECritical session/identity data
2.1–2.7ADDRESS, STRIPE, PLACEOPT, AUDIT, etc.Infrastructure without UI components
3.1–3.8PRODUCT, CATEGORY, OFFER, EXTRA, DISCOUNT, etc.Catalog data (needed before tickets)
4.1–4.2FAMILY, FAMILYPRODUCT, FAMILYPERIODProduct groupings
5.1–5.6TICKET*, SESSIONActive tickets and their components
6.1–6.3QRCODE, PRINTER, PAYMENT, PLACEPlace 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:

  1. Validate session and commit POST body changes in a DB transaction
  2. Call connection_notify to mark affected connections as REFRESH
  3. Long-poll: If timestamp > 0 and status is UPDATED, poll in a loop (~20s) waiting for REFRESH, EXPIRED, or CANCELLED
  4. 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.)
  5. Return JSON: { errorcode, timestamp, reference, target, updated, expired, updates[] }

Connection State

  • Stored in the CONNECTED table (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:

  1. The server builds a list of (table, objid) updates
  2. connection_notify matches these against all active connections' triggers
  3. Matching connections are marked REFRESH
  4. Only those clients receive data on their next poll

Session Expiration

When the server returns errorcode == 2 or errorcode == 3:

  1. syncService.OnSessionExpired() is called
  2. The synchronizer status is set to EXPIRED
  3. The connection is stopped
  4. A modal alert notifies the user
  5. OnExpired observable 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.