Offline Queue¶
Overview¶
The Offline Queue enables the application to capture clinical data entries when the device has no internet connectivity, then automatically (or manually) synchronise them to the server once connectivity is restored. It uses the browser's IndexedDB as a persistent local store, with a 24-hour time-to-live (TTL) per entry so stale data is not silently submitted after prolonged disconnection.
Five data types are supported in the queue:
| Type Key | Feature |
|---|---|
assessment |
BGL Assessment |
incident |
Diabetes Incident |
meal-entry |
Meal Entry |
blood-pressure |
Blood Pressure Session |
bp-incident |
Blood Pressure Incident |
Architecture¶
User submits form (offline)
→ Feature component calls OfflineQueueService.enqueue(type, payload)
→ Stored in IndexedDB store "offlineQueue" with timestamp
→ Toast: "Saved offline. Will sync when back online."
Connectivity restored
→ App detects "online" event OR user opens Offline Queue page
→ SyncService.syncPending() replays each entry to the correct API
→ On success: entry removed from IndexedDB
→ SW posts OFFLINE_SYNC_COMPLETE event to all clients
→ Toast: "X entries synced successfully."
Frontend — OfflineQueueService¶
File: client/src/app/core/services/offline-queue.service.ts
IndexedDB Store¶
- Database name:
vitaraDb(or similar app-wide name). - Store name:
offlineQueue. - Key: auto-increment integer.
Entry Schema¶
interface OfflineQueueEntry {
id?: number; // Auto-assigned by IndexedDB
type: 'assessment' | 'incident' | 'meal-entry' | 'blood-pressure' | 'bp-incident';
payload: unknown; // The full DTO that would have been POSTed
createdAt: number; // Unix timestamp (ms) — used for TTL checks
retryCount: number; // Incremented on each failed sync attempt
}
24-Hour TTL¶
On every syncPending() call (and optionally on page load), entries older than 24 hours are moved to "Expired" status and are no longer auto-synced. They remain visible in the UI until the user explicitly discards them.
TTL check: Date.now() - entry.createdAt > 24 * 60 * 60 * 1000
Key Methods¶
| Method | Description |
|---|---|
enqueue(type, payload) |
Adds an entry to IndexedDB |
getPending() |
Returns all entries where createdAt is within 24 hours |
getExpired() |
Returns all entries older than 24 hours |
remove(id) |
Deletes a specific entry by IndexedDB key |
clearExpired() |
Bulk-deletes all expired entries |
getCount() |
Returns total pending entry count (used for badge) |
Frontend — SyncService¶
File: client/src/app/core/services/sync.service.ts
The SyncService orchestrates the replay of queued entries against the live API.
syncPending()¶
- Calls
OfflineQueueService.getPending()to retrieve all non-expired entries. - For each entry, dispatches to the correct HTTP endpoint based on
type:
| Type | Endpoint |
|---|---|
assessment |
POST /api/assessment |
incident |
POST /api/incident |
meal-entry |
POST /api/meal-entry |
blood-pressure |
POST /api/blood-pressure |
bp-incident |
POST /api/bp-incident |
- On success (HTTP 2xx): calls
OfflineQueueService.remove(entry.id). - On failure: increments
retryCount. IfretryCount >= 3, marks the entry as permanenly failed (left in expired state rather than retried again automatically). - After processing all entries, broadcasts an
OFFLINE_SYNC_COMPLETEcustom event (or service workerpostMessage) to all active clients. - Triggers a toast notification summarising synced / failed counts.
Auto-Sync on Reconnect¶
AppComponent (or the service itself) listens for the global online event:
This means every time network connectivity is restored, pending entries are automatically replayed without any user action.
Frontend — Service Worker Integration¶
The service worker (custom-sw.js) intercepts failed POST requests to the API when offline and posts a QUEUE_ITEM message to the Angular app, which then calls OfflineQueueService.enqueue(). This allows network errors at the HTTP level to be captured even outside the normal form submission flow.
When SyncService.syncPending() completes, it dispatches a postMessage to the service worker with { type: 'OFFLINE_SYNC_COMPLETE' }. The service worker can then broadcast this to all open clients, allowing every tab/window to show the sync completion notification.
Frontend — OfflineQueueComponent¶
File: client/src/app/features/offline-queue/offline-queue.component.ts
Route: /offline-queue (accessible from Dashboard)
Layout¶
Two sections:
Pending Section¶
Lists all non-expired entries: - Icon for data type. - Data type label (e.g., "BGL Assessment"). - Time queued (formatted as "X minutes ago" or specific date-time). - Care Recipient name (if available in payload). - Discard button (removes single entry with confirmation).
"Sync Now" button at the top:
- Calls SyncService.syncPending().
- Shows a spinner while in progress.
- Shows per-entry success/failure feedback.
Expired Section¶
Lists all entries older than 24 hours:
- Same display as pending but with a red "Expired" badge.
- Individual discard buttons.
- "Clear All Expired" button — calls OfflineQueueService.clearExpired().
Queue Badge¶
AppComponent (header / sidenav) shows a badge on the offline-queue nav item with the count of pending entries, refreshed whenever the count changes.
Feature Component Integration¶
Each form component that supports offline submission follows this pattern:
async onSubmit() {
try {
await this.featureService.create(formValue).toPromise();
this.showSuccess();
} catch (err) {
if (!navigator.onLine) {
await this.offlineQueueService.enqueue('incident', formValue);
this.showOfflineSaveToast();
} else {
this.showError(err);
}
}
}
Components with offline support:
- BglReadingComponent (assessment)
- IncidentComponent (incident)
- MealEntryComponent (meal-entry)
- BpEntryComponent (blood-pressure)
- BpIncidentFormComponent (bp-incident)
Data Integrity Considerations¶
- TTL enforcement: The 24-hour TTL prevents submission of clinically stale data (e.g., a BGL reading from yesterday being submitted as "now").
- Idempotency: The backend does not have idempotency guards on POST endpoints; if a sync is interrupted midway, a duplicate entry may appear. Users are expected to review and discard any duplicates.
- Sensitive data at rest: Payloads stored in IndexedDB are not encrypted. IndexedDB data is origin-scoped and not accessible by other origins, but the device must be considered trusted (shared or stolen device is a risk vector).
- Token expiry: If the JWT expires while entries are queued, the sync will fail with 401. The
AuthInterceptorhandles token refresh, but if the refresh token is also expired, entries will remain pending until the user logs in again.
End-to-End Data Flow¶
Form submitted while offline
→ OfflineQueueService.enqueue(type, payload)
→ IndexedDB write: { type, payload, createdAt: now, retryCount: 0 }
→ Feature component shows "Saved offline" toast
→ Queue badge count increments in header
Device comes back online
→ window "online" event fires
→ SyncService.syncPending()
→ getPending() → filter by TTL < 24h
→ For each: POST /api/{endpoint} with payload + auth header
→ 2xx: OfflineQueueService.remove(id)
→ Error: increment retryCount, skip if >= 3
→ SW postMessage: OFFLINE_SYNC_COMPLETE
→ Toast: "3 entries synced"
→ Queue badge count decrements
User opens /offline-queue
→ Sees Pending list and Expired list
→ Can manually trigger "Sync Now"
→ Can discard individual items
→ Can clear all expired items