From 0e3f3c52060f76d0bf0db9f071840d1aba5740b6 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Wed, 14 May 2025 22:34:12 +0530 Subject: [PATCH 01/15] add api-audit that list down some api need to handle while offline --- .../care/offline-support-project/api-audit.md | 148 ++++++++++++++++++ .../offline-support-cep.md | 1 + 2 files changed, 149 insertions(+) create mode 100644 docs/care/offline-support-project/api-audit.md create mode 100644 docs/care/offline-support-project/offline-support-cep.md diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md new file mode 100644 index 0000000..60b2aa0 --- /dev/null +++ b/docs/care/offline-support-project/api-audit.md @@ -0,0 +1,148 @@ +# Backend API audit for offline support + +This document lists the standard workflows that will be supported offline. +Each workflow has an associated markdown table that defines the structure, HTTP method, and other important information about the API endpoints involved. + +In the tables, backend API endpoints are clearly distinguished based on whether they will be **cached at the Service Worker level** or **stored and later synced via IndexedDB (for write operations).** + +These tables give us idea about which dynamic data need to be cached for offlien support and will helps us for route registration in workbox. + +--- + +## Standard Workflows + +- [1. Patient Registration and Search Patient Workflow](#1-patient-registration-and-search-patient-workflow) +- [2. Encounter Management and Questionnaire Filling](#2-encounter-management-and-questionnaire-filling) +- [3. Patient Profile management](#3-patient-profile-management) +- [4. Appointments Management](#4-appointments-management) + +--- + +## 1. Patient Registration and Search Patient Workflow +Patient registration and search patient workflow include add new patient and search patient by its mobile number.After click on one of the search patient ,user go to page from where they can create encounter,schedule appointments,see encounters list of that patient.This table mainly include endpoints regarding add patient and search patient only . create encounter,list encounter come under the encounter management and schedule appointment come under the appointment management. + + pattern of Read only Api that going to be cached using workbox: + 1. URL starts with `/api/v1/getcurrentuser` + 2. URL starts with `/api/v1/organization` + 3. URL matches `/api/v1/facility/:facilityId/` + + + Note: (3,4) are the post type read only api so we need to store them into indexed when user hit them during online. and (8) is the write operation api. + + +| # | API Endpoint | Method | Purpose | Offline Handling | Notes | +|:--:|:------------------------------------------------------------------|:------:|:-----------------------------------------------------------|:----------------------------------|:--------------------------------------------------------------| +| 1 | `/api/v1/users/getcurrentuser/` | GET | Fetch current logged-in user info (global) | Cache (SW) | Used in every workflow. precaching will be best for this | +| 2 | `/api/v1/facility/{facility_id}/` | GET | Fetch details for a selected facility | Cache (SW) | Cache per facility. workbox on-demand caching will be used | +| 3 | `/api/v1/patient/search/` | POST | Search patients by phone number | cache in IndexedDB whenever user's hit it .so that data coming from this can load on the ui during offline | Keyed by phone; store results + timestamp | +| 4 | `/api/v1/patient/search_retrieve/` | POST | Retrieve full patient record after selecting from search | cache in IndexedDB during online so that data coming from this can load on the ui during offline | Keyed by patient.id; | +| 5 | `/api/v1/encounter/?patient={patient_id}&live=false` | GET | List all encounters for a specific patient | Cache (SW) | workbox on-demand caching will be used | +| 6 | `/api/v1/organization/?org_type=govt&parent=&limit=200` | GET | Fetch top-level government organization list | Cache (SW) |workbox on-demand caching will be used | +| 7 | `/api/v1/organization/?org_type=govt&parent={parent_id}&limit=200`| GET | Fetch child organizations under a given parent (dynamic) | Cache (SW) | Same pattern as #6; cache per parent query. | +| 8 | `/api/v1/patient/` | POST | Create a new patient record |store in IndexedDB and will sync on reconnect to internet | Store new record with `dirty=true`; sync and clear flag later | + + + +## 2. Encounter Management and Questionnaire Filling + +This workflow include list down encounters on the encounter page , create/update encounter. It include filling questionnair available on encounter page . user can see allregies,symptoms,diagnones and medication statements. + +pattern of Read only Api that going to be cached using workbox: + 1. URL starts with `/api/v1/encounter` + 2. URL matches `/api/v1/facility/:facilityId/organizations` + 3. URL starts with `/api/v1/valueset` + 4. URL starts with `/api/v1/questionnaire` + 5. URL matches `/api/v1/patient/:patientId/symptom/` + 6. URL matches `/api/v1/patient/:patientId/diagnosis/` + 7. URL matches `/api/v1/patient/:patientId/allergy_intolerance/` + 8. URL matches `/api/v1/patient/:patientId/medication-statement/` + 9. URL matches `/api/v1/patient/:patientId/questionnaire_response/` + 10. URL starts with `/api/v1/role/` + + + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +|:----:|:------------------------------------------------------|:-------|:------------------------------------|:-----------------------------|:--------------------------------------------------------------| +| 1 | List recent encounters for a facility | GET | `/api/v1/encounter?facility={facilityId}&...` | Cache (Service Worker) | Main encounter list | +| 2 | Create a new encounter | POST | `/api/v1/encounter/` | Store & Sync (IndexedDB) | Adds a new encounter | +| 3 | Fetch specific encounter detail | GET | `/api/v1/encounter/{encounterId}/?facility={facilityId}` | Cache (Service Worker) | View full encounter | +| 5 | Load facility organizations by parent | GET | `/api/v1/facility/{facilityId}/organizations?parent={orgParentId}&...`| Cache (Service Worker) | Child organizations dropdown | +| 6 | Load root facility organizations | GET | `/api/v1/facility/{facilityId}/organizations?parent=&...` | Cache (Service Worker) | Root organizations list | +| 7 | Load patient allergy history | GET | `/api/v1/patient/{patientId}/allergy_intolerance?&...` | Cache (Service Worker) | Allergy records | +| 8 | Load current-encounter symptoms | GET | `/api/v1/patient/{patientId}/symptom?encounter={encounterId}&...` | Cache (Service Worker) | Symptoms tied to this encounter | +| 9 | Load full symptom history | GET | `/api/v1/patient/{patientId}/symptom?limit=100&...` | Cache (Service Worker) | “History” view | +| 10 | Load current-encounter diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?encounter={encounterId}&...` | Cache (Service Worker) | Diagnosis for this encounter | +| 11 | Load full diagnosis history | GET | `/api/v1/patient/{patientId}/diagnosis?limit=100&...` | Cache (Service Worker) | “History” view | +| 12 | List questionnaire responses for this encounter | GET | `/api/v1/patient/{patientId}/questionnaire_response?encounter={encounterId}&...` | Cache (Service Worker) | Already-filled forms | +| 13 | Fetch encounter-specific questionnaires | GET | `/api/v1/questionnaire?tag_slug=encounter_actions&...` | Cache (Service Worker) | Which forms can be added | +| 14 | Fetch all active questionnaires | GET | `/api/v1/questionnaire?status=active&...` | Cache (Service Worker) | General form listings | +| 15 | Fetch questionnaires with subject type 'encounter' | GET | `/api/v1/questionnaire?subject_type=encounter&status=active&...` | Cache (Service Worker) | Encounter-scoped forms | +| 16 | Load a questionnaire definition | GET | `/api/v1/questionnaire/{questionnaireSlug}/` | Cache (Service Worker) | Full form schema | +| 17 | Load valueset favourites | GET | `/api/v1/valueset/{slug}/favourites/` | Cache (Service Worker) | Speeds up include/exclude lookups | +| 18 | Load valueset recent views | GET | `/api/v1/valueset/{slug}/recent_views/` | Cache (Service Worker) | Speeds up include/exclude lookups | +| 19 | Expand valueset to retrieve items | POST | `/api/v1/valueset/{slug}/expand/` | Cache results (Service Worker) | Lookup codes for pick-lists | +| 20 | Batch create/update encounter-related data | POST | `/api/v1/batch_requests/` | Store & Sync (IndexedDB) | Batch create or update of symptoms, diagnoses, allergies, questionnaire responses, etc. | + +## 3. Patient Profile Management +This workflow include access patient profile and including appointments, encounters, health data, resources, users. + + +pattern of Read only Api that going to be cached using workbox: + 1. URL starts with `/api/v1/users/` + 2. URL matches `/api/v1/patient/:patientId/` + 3. URL matches `/api/v1/patient/:patientId/get_users/` + 4. URL matches `/api/v1/patient/:externalId/get_appointments/` + 5. URL starts with `/api/v1/patient` + 6. URL starts with `/api/v1/resource` + 7. URL starts with `/api/v1/getallfacilities` + + + + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +|:----:|:----------------------------------------------|:-------|:-------------------------------------------------------------------|:-----------------------------|:----------------------------------------------------| +| 1 | View patient detail | GET | `/api/v1/patient/{patientId}/` | Cache (Service Worker) | Load full patient record | +| 2 | List patients in context | GET | `/api/v1/patient?limit...` | Cache (Service Worker) | Patient cards on Patient tab | +| 3 | View organization detail | GET | `/api/v1/organization/{organizationId}/` | Cache (Service Worker) | Parent organization | +| 4 | List child organizations | GET | `/api/v1/organization?parent={organizationId}&...` | Cache (Service Worker) | Fetch children from parent organization | +| 7 | Search patient by phone | POST | `/api/v1/patient/search/` | Store in IndexedDB | Indexed by phone number | +| 8 | Update patient details | PUT | `/api/v1/patient/{patientId}/` | Store & Sync (IndexedDB) | Save edited profile | +| 9 | List patient appointments | GET | `/api/v1/appointments?patient={patientId}&...` | Cache (Service Worker) | Appointments tab | +| 10 | List recent encounters for patient | GET | `/api/v1/encounter?patient={patientId}&...` | Cache (Service Worker) | Encounter tab | +| 11 | List health profile: medications | GET | `/api/v1/patient/{patientId}/medication/statement?limit...` | Cache (Service Worker) | Medications history | +| 12 | List health profile: allergies | GET | `/api/v1/patient/{patientId}/allergy_intolerance?limit...` | Cache (Service Worker) | Allergy history | +| 13 | List health profile: symptoms | GET | `/api/v1/patient/{patientId}/symptom?limit...` | Cache (Service Worker) | Symptom history | +| 14 | List health profile: diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?category...&limit...` | Cache (Service Worker) | Diagnosis history | +| 15 | List patient questionnaire responses | GET | `/api/v1/patient/{patientId}/questionnaire_response?subject_type=patient&...` | Cache (Service Worker) | Patient forms | +| 16 | List resources related to patient | GET | `/api/v1/resource?related_patient={patientId}&...` | Cache (Service Worker) | Request tab | +| 17 | List all facilities | GET | `/api/v1/getallfacilities?limit...` | Cache (Service Worker) | Facility lookup | +| 18 | Create resource | POST | `/api/v1/resource/` | Store & Sync (IndexedDB) | Add new resource | +| 19 | Get users list | GET | `/api/v1/users?limit...&search_text...` | Cache (Service Worker) | For user assignment | +| 20 | List resources | GET | `/api/v1/resource?status=pending&...` | Cache (Service Worker) | Filtered resource list | +| 21 | Fetch specific resource detail | GET | `/api/v1/resource/{resourceid}/` | Cache (Service Worker) | Resource detail view | +| 22 | List assignable users | GET | `/api/v1/patient/{patientId}/get_users/` | Cache (Service Worker) | Add user to patient | +| 23 | List roles | GET | `/api/v1/role/` | Cache (Service Worker) | For user assignment | +| 24 | Assign user to patient | POST | `/api/v1/patient/{patientId}/add_user/` | Store & Sync (IndexedDB) | Add user to patient | + +## 4. Appointments Management +It include View, create, and manage appointments for a facility. +1. url start with (`api/v1/facility/{fac:id}/appointments`) +2. url start with (`api/v1/facility/{fac:id}/slots`) + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +|:----:|:------------------------------------------------------|:-------|:----------------------------------------------------------------|:-----------------------------|:------------------------------------------------------------| +| 1 | Fetch details for a selected facility | GET | `/api/v1/facility/{facilityId}/` | Cache (Service Worker) | Cache per facility. workbox on-demand caching will be | +| 2 | List “in consultation” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=in_consultation&...` | Cache (Service Worker) | data coming from these api will be cached on req from server by sw(workbox) during online and then this data will available ofline | +| 3 | List “fulfilled” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=fulfilled&...` | Cache (Service Worker) | same as 2 | +| 4 | List “no-show” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=noshow&...` | Cache (Service Worker) | same as 2 | +| 5 | List “booked” (upcoming) appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=booked&...` | Cache (Service Worker) | same as 2 | +| 6 | List “checked in” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=checked_in&...` | Cache (Service Worker) | same as 2 | +| 7 | Fetch available users(practitioner) for appointments | GET | `/api/v1/facility/{facilityId}/appointments/available_users/` | Cache (Service Worker) | sw(workbox) will handle caching of dynamic data comig from this api | +| 8 | Get aggregated slot availability stats | POST | `/api/v1/facility/{facilityId}/slots/availability_stats/` | Store & Sync (IndexedDB) | Keyed by `{from_date,to_date,user}`; for charts | +| 9 | Get detailed slots for a specific day | POST | `/api/v1/facility/{facilityId}/slots/get_slots_for_day/` | Store & Sync (IndexedDB) | Keyed by `{day,user}`; for slot picker | +| 10 | Create a new appointment on a slot | POST | `/api/v1/facility/{facilityId}/slots/{slotId}/create_appointment/` | Store & Sync (IndexedDB) | Stored locally and sync later | +| 11 | Fetch details of a specific appointment | GET | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Cache (Service Worker) | Keyed by `appointmentId`; for detail view | +| 12 | Reschedule an existing appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/reschedule/` | Store & Sync (IndexedDB) | Queue reschedule request locally and will sync later | +| 13 | Update appointment details (status, notes, etc.) | PUT | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Store & Sync (IndexedDB) | Queue update locally and will sync later | +| 14 | Cancel an appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/cancel/` | Store & Sync (IndexedDB) | Queue cancel request locally and will sync later | +| 15 | Get list of appointments of a patient | GET | `/api/v1/facility/{facilityid}/appointments/?patient={patientid}&limit=100` | cache(SW) | sw(workbox) will handle caching of dynamic data comig from this api | diff --git a/docs/care/offline-support-project/offline-support-cep.md b/docs/care/offline-support-project/offline-support-cep.md new file mode 100644 index 0000000..43eb36c --- /dev/null +++ b/docs/care/offline-support-project/offline-support-cep.md @@ -0,0 +1 @@ +## CEP for offline support project \ No newline at end of file From 8b5523ac1a4749ef559ce54d39d13f99949b3871 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Thu, 15 May 2025 12:21:34 +0530 Subject: [PATCH 02/15] minor fix --- docs/care/offline-support-project/api-audit.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md index 60b2aa0..c0f5484 100644 --- a/docs/care/offline-support-project/api-audit.md +++ b/docs/care/offline-support-project/api-audit.md @@ -3,10 +3,9 @@ This document lists the standard workflows that will be supported offline. Each workflow has an associated markdown table that defines the structure, HTTP method, and other important information about the API endpoints involved. -In the tables, backend API endpoints are clearly distinguished based on whether they will be **cached at the Service Worker level** or **stored and later synced via IndexedDB (for write operations).** - -These tables give us idea about which dynamic data need to be cached for offlien support and will helps us for route registration in workbox. +In the tables, backend API endpoints are clearly distinguished based on whether they will be **cached** or **stored and later synced via IndexedDB (for write operations).** +These tables give us idea about which dynamic data need to be cached for offlien support and help to avoid store unnecessary data. --- ## Standard Workflows @@ -21,7 +20,7 @@ These tables give us idea about which dynamic data need to be cached for offli ## 1. Patient Registration and Search Patient Workflow Patient registration and search patient workflow include add new patient and search patient by its mobile number.After click on one of the search patient ,user go to page from where they can create encounter,schedule appointments,see encounters list of that patient.This table mainly include endpoints regarding add patient and search patient only . create encounter,list encounter come under the encounter management and schedule appointment come under the appointment management. - pattern of Read only Api that going to be cached using workbox: + pattern of Read only Api that going to be cached : 1. URL starts with `/api/v1/getcurrentuser` 2. URL starts with `/api/v1/organization` 3. URL matches `/api/v1/facility/:facilityId/` @@ -47,7 +46,7 @@ Patient registration and search patient workflow include add new patient and sea This workflow include list down encounters on the encounter page , create/update encounter. It include filling questionnair available on encounter page . user can see allregies,symptoms,diagnones and medication statements. -pattern of Read only Api that going to be cached using workbox: +pattern of Read only Api that going to be cached: 1. URL starts with `/api/v1/encounter` 2. URL matches `/api/v1/facility/:facilityId/organizations` 3. URL starts with `/api/v1/valueset` @@ -87,7 +86,7 @@ pattern of Read only Api that going to be cached using workbox: This workflow include access patient profile and including appointments, encounters, health data, resources, users. -pattern of Read only Api that going to be cached using workbox: +pattern of Read only Api that going to be cached: 1. URL starts with `/api/v1/users/` 2. URL matches `/api/v1/patient/:patientId/` 3. URL matches `/api/v1/patient/:patientId/get_users/` From a4e80530ff4e4b031c8377947e272c502d22c693 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Fri, 16 May 2025 17:20:40 +0530 Subject: [PATCH 03/15] add methods to achieve offline functionality --- .../care/offline-support-project/api-audit.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md index c0f5484..9c9b23b 100644 --- a/docs/care/offline-support-project/api-audit.md +++ b/docs/care/offline-support-project/api-audit.md @@ -145,3 +145,75 @@ It include View, create, and manage appointments for a facility. | 13 | Update appointment details (status, notes, etc.) | PUT | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Store & Sync (IndexedDB) | Queue update locally and will sync later | | 14 | Cancel an appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/cancel/` | Store & Sync (IndexedDB) | Queue cancel request locally and will sync later | | 15 | Get list of appointments of a patient | GET | `/api/v1/facility/{facilityid}/appointments/?patient={patientid}&limit=100` | cache(SW) | sw(workbox) will handle caching of dynamic data comig from this api | + + + +# Approaches for Caching Read-Only APIs and Syncing Write Operations + +When we talk about adding offline functionality to our apps, there are several methods to achieve this. Each approach is implemented differently, but they all share one common goal: caching read-only API data locally so the UI can operate offline. + +The approaches discussed here focus primarily on caching API responses. Our UI shell is already cached by default since CARE is a PWA. + +Below, we outline some of the approaches we can use to achieve offline functionality for the workflows we’ve discussed. + +--- + +### 1. Using TanStack Query Cache with Persistence + +This approach plays around using TanStack Query to cache the API responses and sync write operations once the app comes back online. Persistence, in this context, here means to store the cached data into some local DB (e.g., IndexedDB). Since CARE already uses TanStack Query for fetch and write operations, integrating persistence and offline syncing becomes relatively straightforward. This approach leverages existing infrastructure, reducing the need for additional caching logic or custom data handling. Now let's discuss what configuration needs to be implemented for this approach. + +#### 1. Persister: +It is the mechanism that defines how and where the data is saved/restored. TanStack provides some persisters like `createSyncStoragePersister` for mainly localStorage and `createAsyncStoragePersister` for AsyncStorage. It also provides a method to create a custom persister. We will create a custom persister for IndexedDB(via dexie.js) for our use case. + +#### 2. cacheTime: +For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cacheTime` should be set as the same value or higher than `persistQueryClient`'s `maxAge` option. + +#### 3. Cache Busting: +It is a mechanism to forcefully invalidate old cached data, typically after app updates or changes to data structure. By setting a unique buster string (like a version ID), previously persisted caches without the matching string are discarded. This ensures users always get fresh data after critical changes. + +#### 4. PersistQueryClientProvider: +`PersistQueryClientProvider` is a React wrapper around our normal `QueryClientProvider` that automatically restores persisted cache on mount and keeps it in sync through subscribe/unsubscribe. It prevents our queries from fetching until the cache is hydrated, ensuring a smooth offline‑first experience. + + +Reference for technical implementation: [TanStack Docs](https://tanstack.com/query/v4/docs/framework/react/plugins/persistQueryClient) + +--- + +### 2. Using Service Worker (Workbox) with Manual Sync for Offline Functionality + +This approach uses a Service Worker powered by Workbox to cache GET requests and store write operations locally (e.g., in IndexedDB). Write operations are manually synced only when the web application is active and the network is available. This gives full control over when syncs are triggered, avoiding unnecessary background processes. Now let's discuss what configuration needs to be implemented for this approach. + +#### 1. Workbox Routing: +It provides utilities to intercept and handle network requests in the service worker. We have to define routing logic based on request methods and URLs. Above API tables provide info about which types of URLs we have to intercept to store them in cache storage. GET responses are stored in cache storage instead of storage like IndexedDB. +Each route should be registered with an appropriate caching strategy using `registerRoute()`. + +#### 2. Caching Strategies: +Workbox provides many strategies that define how responses should be handled for intercepted requests. A few common strategies that Workbox provides are: + +- **Stale-while-revalidate:** + The stale-while-revalidate pattern allows you to respond to the request as quickly as possible with a cached response if available, falling back to the network request if it's not cached. The network request is then used to update the cache. + +- **Cache first:** + If there is a Response in the cache, the Request will be fulfilled using the cached response and the network will not be used at all. If there isn't a cached response, the Request will be fulfilled by a network request and the response will be cached so that the next request is served directly from the cache. + +- **Network first:** + For requests that are updating frequently, the network first strategy is the ideal solution. By default, it will try to fetch the latest response from the network. If the request is successful, it'll put the response in the cache. If the network fails to return a response, the cached response will be used. + +#### 3. IndexedDB (via Dexie.js): +It will be used to store write operations (like form submissions) that fail due to network issues. We will manually store these requests and later retrieve them for syncing. And we are going to use the Dexie.js wrapper that is built on top of IndexedDB to reduce complexity. Each queued request should include all relevant data (URL, method, headers, body) needed to retry the operation later. + +#### 4. Manual Sync Logic: +Instead of using Workbox’s `BackgroundSyncPlugin`, write sync logic that checks `navigator.onLine` and retries queued requests only when: + +- The app is open (user is active and logged in) +- The network is restored + +If we use manual sync logic, we can use existing API routes that are already defined in the codebase using TanStack Query. + + + + + + + + From 8ea86671c2f76c0ed0d3a1c2eaa0c5aa769500bb Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Sat, 17 May 2025 15:23:30 +0530 Subject: [PATCH 04/15] minor change in approches --- docs/care/offline-support-project/api-audit.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md index 9c9b23b..9a27ff2 100644 --- a/docs/care/offline-support-project/api-audit.md +++ b/docs/care/offline-support-project/api-audit.md @@ -211,7 +211,11 @@ Instead of using Workbox’s `BackgroundSyncPlugin`, write sync logic that check If we use manual sync logic, we can use existing API routes that are already defined in the codebase using TanStack Query. +Few point to be consider in these approches : +- For read-only POST APIs, TanStack Query automatically caches responses by treating them as queries, while Workbox requires manual implementation since it doesn't cache POST requests by default (unlike GET requests which are cached automatically) + +- In TanStack Query’s cache, we must carefully configure staleTime. While online, staleTime should remain its normal (short) value, but when offline it needs to be set to nearly the same value as cacheTime so that cached data doesn’t become stale during offline use. However, staleTime is only read once—when a query is initialized—so simply changing our network status won’t update it. That means if we set staleTime to match cacheTime to preserve offline behavior, our online behavior will no longer work as it did before. From 5ff15f8f8769dcd1ab0be3e30b813bc19825b4fd Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Wed, 21 May 2025 21:38:41 +0530 Subject: [PATCH 05/15] add tanstack approch --- .../care/offline-support-project/api-audit.md | 99 +++++++++++-------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md index 9a27ff2..3527998 100644 --- a/docs/care/offline-support-project/api-audit.md +++ b/docs/care/offline-support-project/api-audit.md @@ -146,76 +146,95 @@ It include View, create, and manage appointments for a facility. | 14 | Cancel an appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/cancel/` | Store & Sync (IndexedDB) | Queue cancel request locally and will sync later | | 15 | Get list of appointments of a patient | GET | `/api/v1/facility/{facilityid}/appointments/?patient={patientid}&limit=100` | cache(SW) | sw(workbox) will handle caching of dynamic data comig from this api | +## Key Assumptions and Design Considerations for Offline Mode : + + Before implementing offline support, it’s essential to define how the application determines internet connectivity and how it behaves in various online/offline scenarios. This document outlines the design assumptions and expected behavior around connection status handling. + We use a real server connectivity check instead of navigator.onLine. This involves pinging a known backend endpoint (e.g., /ping) and marking the user as offline if the server is unreachable. -# Approaches for Caching Read-Only APIs and Syncing Write Operations + We maintain a global state variable isOnline to represent the app’s understanding of the user’s connectivity. This value can be: + - set based on real server checks during page reload and websites open . + - Manually overridden by the user via a toggle. -When we talk about adding offline functionality to our apps, there are several methods to achieve this. Each approach is implemented differently, but they all share one common goal: caching read-only API data locally so the UI can operate offline. + ### Offline Mode Toggle Button + The app provides an "Enable Offline Mode" toggle button in the UI.This gives users control when internet conditions are unstable, allowing them to continue working in offline mode without interruptions. + + we Use an Explicit `isOnline` State Instead of Just Runtime Checks because Automatic checks can flicker rapidly when internet connectivity is poor (e.g., intermittent mobile data). This can cause unpredictable app behavior, like failed fetches or UI state inconsistencies. Manual offline mode offers a **clear and consistent user experience**. Users understand when they're offline and what features are expected to work -The approaches discussed here focus primarily on caching API responses. Our UI shell is already cached by default since CARE is a PWA. + - Enabled = isOnline = false: User manually forces the app into offline mode to prevent unstable behavior during intermittent connectivity. + - Disabled = isOnline = true: The app will attempt to connect to the server normally. -Below, we outline some of the approaches we can use to achieve offline functionality for the workflows we’ve discussed. +### Behavior on App Load and Connectivity Events +#### 1. App Launch (Fresh Tab Open) +- On opening the app, `checkRealServer()` is called. +- If the server is reachable → `isOnline = true` or If unreachable → `isOnline = false`.. +- Ensures `isOnline` reflects real connectivity at startup. --- -### 1. Using TanStack Query Cache with Persistence +#### 2. Login Attempt During Poor Connectivity +- If the user is **not logged in** and connectivity is poor,then Login may fail: +- App offers **fallback login** using last cached profile (if available). +- It Allows access even when login fails due to network issues. +--- -This approach plays around using TanStack Query to cache the API responses and sync write operations once the app comes back online. Persistence, in this context, here means to store the cached data into some local DB (e.g., IndexedDB). Since CARE already uses TanStack Query for fetch and write operations, integrating persistence and offline syncing becomes relatively straightforward. This approach leverages existing infrastructure, reducing the need for additional caching logic or custom data handling. Now let's discuss what configuration needs to be implemented for this approach. +#### 3. User Goes Offline During Active Session + - If already logged in and internet becomes unstable: + - User can toggle **offline mode manually** → `isOnline = false`. + - Enables offline features (e.g., cached reads, local writes). +--- -#### 1. Persister: -It is the mechanism that defines how and where the data is saved/restored. TanStack provides some persisters like `createSyncStoragePersister` for mainly localStorage and `createAsyncStoragePersister` for AsyncStorage. It also provides a method to create a custom persister. We will create a custom persister for IndexedDB(via dexie.js) for our use case. +#### 4. Page Reload or Tab Reopen +- On reload or reopening , `checkRealServer()` runs again and Updates `isOnline` state.: +- Notifies user of current connection status. +- Even if user previously forced offline mode, connection is revalidated. -#### 2. cacheTime: -For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cacheTime` should be set as the same value or higher than `persistQueryClient`'s `maxAge` option. +### Role based caching , when multiple users access the app on the same device +- As Care have role based access control, we have to ensure that if multiple user access the app on same device then their data of one user does not override the other user data while caching. Its necessary because permissions are come from backend via api based on user. For example, the getCurrentUser API returns a list of permissions specific to the logged-in user. If this response is cached and a different user logs in, their permissions could overwrite the previous user's data. Later, if the first user tries to use the app offline, they might see incorrect permissions or data from the second user. -#### 3. Cache Busting: -It is a mechanism to forcefully invalidate old cached data, typically after app updates or changes to data structure. By setting a unique buster string (like a version ID), previously persisted caches without the matching string are discarded. This ensures users always get fresh data after critical changes. +so to ovrcome this problem we have to cache data per user. we will discussed its solution in approache's section. -#### 4. PersistQueryClientProvider: -`PersistQueryClientProvider` is a React wrapper around our normal `QueryClientProvider` that automatically restores persisted cache on mount and keeps it in sync through subscribe/unsubscribe. It prevents our queries from fetching until the cache is hydrated, ensuring a smooth offline‑first experience. - -Reference for technical implementation: [TanStack Docs](https://tanstack.com/query/v4/docs/framework/react/plugins/persistQueryClient) +# Now Lets Discussed Approches to achive offline Support : + The approaches discussed here focus primarily on caching API responses. Our UI shell is already cached by default since CARE is a PWA. Below are some of the approaches we can use to achieve offline functionality for the workflows we’ve discussed. --- -### 2. Using Service Worker (Workbox) with Manual Sync for Offline Functionality - -This approach uses a Service Worker powered by Workbox to cache GET requests and store write operations locally (e.g., in IndexedDB). Write operations are manually synced only when the web application is active and the network is available. This gives full control over when syncs are triggered, avoiding unnecessary background processes. Now let's discuss what configuration needs to be implemented for this approach. +### 1. Using TanStack Query Cache with Persistence +This approach plays around using TanStack Query to cache the API responses and sync write operations once the app comes back online. Persistence, in this context, here means to store the cached data into some local DB (e.g., IndexedDB). Since CARE already uses TanStack Query for fetch and write operations, integrating persistence and offline syncing becomes relatively straightforward. This approach leverages existing infrastructure, reducing the need for additional caching logic or custom data handling. Now let's discuss what configuration needs to be implemented for this approach. -#### 1. Workbox Routing: -It provides utilities to intercept and handle network requests in the service worker. We have to define routing logic based on request methods and URLs. Above API tables provide info about which types of URLs we have to intercept to store them in cache storage. GET responses are stored in cache storage instead of storage like IndexedDB. -Each route should be registered with an appropriate caching strategy using `registerRoute()`. +#### 1. Persister: +It is the mechanism that defines how and where the data is saved/restored. TanStack provides some persisters like `createSyncStoragePersister` for mainly localStorage and `createAsyncStoragePersister` for AsyncStorage. It also provides a method to create a custom persister. We will create a custom persister for IndexedDB(via dexie.js) for our use case. -#### 2. Caching Strategies: -Workbox provides many strategies that define how responses should be handled for intercepted requests. A few common strategies that Workbox provides are: + We just need to create a createDbPersister() function that returns our custom persister. Inside this function, we define a single cache key under which the entire React Query cache will be stored. The function should implement three methods — `persistClient()`, `restoreClient()`, and `removeClient()` — following the rules to create a custom persister for React Query. -- **Stale-while-revalidate:** - The stale-while-revalidate pattern allows you to respond to the request as quickly as possible with a cached response if available, falling back to the network request if it's not cached. The network request is then used to update the cache. +When online, data fetched using useQuery is stored in the in-memory cache and also persisted to IndexedDB via this custom persister. When offline, the cached data stored in IndexedDB (using Dexie.js) is automatically hydrated back into the in-memory cache. TanStack Query then manages the cache seamlessly, providing offline support without extra effort. -- **Cache first:** - If there is a Response in the cache, the Request will be fulfilled using the cached response and the network will not be used at all. If there isn't a cached response, the Request will be fulfilled by a network request and the response will be cached so that the next request is served directly from the cache. +#### 2. cacheTime(or gcTime) and maxage: +For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cacheTime` should be set as the same value or higher than `persistQueryClient`'s `maxAge` option. -- **Network first:** - For requests that are updating frequently, the network first strategy is the ideal solution. By default, it will try to fetch the latest response from the network. If the request is successful, it'll put the response in the cache. If the network fails to return a response, the cached response will be used. +- `cacheTime` defines how long inactive cached data stays in memory before being garbage collected. This means the data remains available for queries without refetching during this time, even if not actively used. +- `maxAge` (used in persistQueryClient) determines how long the persisted cache data remains valid and can be restored from storage. -#### 3. IndexedDB (via Dexie.js): -It will be used to store write operations (like form submissions) that fail due to network issues. We will manually store these requests and later retrieve them for syncing. And we are going to use the Dexie.js wrapper that is built on top of IndexedDB to reduce complexity. Each queued request should include all relevant data (URL, method, headers, body) needed to retry the operation later. +#### 4. PersistQueryClientProvider: +`PersistQueryClientProvider` is a React wrapper around our normal `QueryClientProvider` that automatically restores persisted cache on mount and keeps it in sync through subscribe/unsubscribe. It prevents our queries from fetching until the cache is hydrated, ensuring a smooth offline‑first experience. -#### 4. Manual Sync Logic: -Instead of using Workbox’s `BackgroundSyncPlugin`, write sync logic that checks `navigator.onLine` and retries queued requests only when: + **lets discuss how to overcome the problem of cache mixing when multiple user use same device :** TanStack Query caches all API data fetched via useQuery. To avoid mixing cached data between different users, include the userId as part of the queryKey along with other parameters. Since TanStack Query stores cache data based on the queryKey (not just the URL), adding userId creates distinct cache entries even if the API endpoint (URL) is the same. This ensures each user’s data is cached separately. + (Unlike Workbox, which caches based on URL, TanStack Query relies on the uniqueness of the query key.) + + **Note:** To avoid unnecessary refetch attempts when offline, configure useQuery with enabled: isOnline === true. This way, during online mode, staleTime can remain 0 to always fetch fresh data. When offline, the query is disabled (enabled: false), so cached data is used without triggering refetches that would fail. + This approach keeps the default online behavior unchanged while ensuring smooth offline caching without forced stale data refetches. -- The app is open (user is active and logged in) -- The network is restored + Main advantages of this approches is : + - It will cache data coming from api that was fetch using useQuery irrespective of operation we use(eg.GET,POST).But Approches like workbox does not provide such option's they will cache only get api responses not read only post responses. + - As Care is already using tanstack query it become easy to implement this approch in the CARE. we dont have to write + - Give option to separate cached data based on user by using just single userid in query key.But if we use aproches like workbox we have to add userid in url to store data in cache based on url, it can increase complexity. -If we use manual sync logic, we can use existing API routes that are already defined in the codebase using TanStack Query. + #### sync and write operation in this approah : -Few point to be consider in these approches : -- For read-only POST APIs, TanStack Query automatically caches responses by treating them as queries, while Workbox requires manual implementation since it doesn't cache POST requests by default (unlike GET requests which are cached automatically) -- In TanStack Query’s cache, we must carefully configure staleTime. While online, staleTime should remain its normal (short) value, but when offline it needs to be set to nearly the same value as cacheTime so that cached data doesn’t become stale during offline use. However, staleTime is only read once—when a query is initialized—so simply changing our network status won’t update it. That means if we set staleTime to match cacheTime to preserve offline behavior, our online behavior will no longer work as it did before. From d2c62194564d678d0a0d0468c53f9320a2686f81 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Wed, 21 May 2025 21:43:10 +0530 Subject: [PATCH 06/15] minor fix --- .../care/offline-support-project/api-audit.md | 242 ----------------- .../offline-support-cep.md | 243 +++++++++++++++++- 2 files changed, 242 insertions(+), 243 deletions(-) delete mode 100644 docs/care/offline-support-project/api-audit.md diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md deleted file mode 100644 index 3527998..0000000 --- a/docs/care/offline-support-project/api-audit.md +++ /dev/null @@ -1,242 +0,0 @@ -# Backend API audit for offline support - -This document lists the standard workflows that will be supported offline. -Each workflow has an associated markdown table that defines the structure, HTTP method, and other important information about the API endpoints involved. - -In the tables, backend API endpoints are clearly distinguished based on whether they will be **cached** or **stored and later synced via IndexedDB (for write operations).** - -These tables give us idea about which dynamic data need to be cached for offlien support and help to avoid store unnecessary data. ---- - -## Standard Workflows - -- [1. Patient Registration and Search Patient Workflow](#1-patient-registration-and-search-patient-workflow) -- [2. Encounter Management and Questionnaire Filling](#2-encounter-management-and-questionnaire-filling) -- [3. Patient Profile management](#3-patient-profile-management) -- [4. Appointments Management](#4-appointments-management) - ---- - -## 1. Patient Registration and Search Patient Workflow -Patient registration and search patient workflow include add new patient and search patient by its mobile number.After click on one of the search patient ,user go to page from where they can create encounter,schedule appointments,see encounters list of that patient.This table mainly include endpoints regarding add patient and search patient only . create encounter,list encounter come under the encounter management and schedule appointment come under the appointment management. - - pattern of Read only Api that going to be cached : - 1. URL starts with `/api/v1/getcurrentuser` - 2. URL starts with `/api/v1/organization` - 3. URL matches `/api/v1/facility/:facilityId/` - - - Note: (3,4) are the post type read only api so we need to store them into indexed when user hit them during online. and (8) is the write operation api. - - -| # | API Endpoint | Method | Purpose | Offline Handling | Notes | -|:--:|:------------------------------------------------------------------|:------:|:-----------------------------------------------------------|:----------------------------------|:--------------------------------------------------------------| -| 1 | `/api/v1/users/getcurrentuser/` | GET | Fetch current logged-in user info (global) | Cache (SW) | Used in every workflow. precaching will be best for this | -| 2 | `/api/v1/facility/{facility_id}/` | GET | Fetch details for a selected facility | Cache (SW) | Cache per facility. workbox on-demand caching will be used | -| 3 | `/api/v1/patient/search/` | POST | Search patients by phone number | cache in IndexedDB whenever user's hit it .so that data coming from this can load on the ui during offline | Keyed by phone; store results + timestamp | -| 4 | `/api/v1/patient/search_retrieve/` | POST | Retrieve full patient record after selecting from search | cache in IndexedDB during online so that data coming from this can load on the ui during offline | Keyed by patient.id; | -| 5 | `/api/v1/encounter/?patient={patient_id}&live=false` | GET | List all encounters for a specific patient | Cache (SW) | workbox on-demand caching will be used | -| 6 | `/api/v1/organization/?org_type=govt&parent=&limit=200` | GET | Fetch top-level government organization list | Cache (SW) |workbox on-demand caching will be used | -| 7 | `/api/v1/organization/?org_type=govt&parent={parent_id}&limit=200`| GET | Fetch child organizations under a given parent (dynamic) | Cache (SW) | Same pattern as #6; cache per parent query. | -| 8 | `/api/v1/patient/` | POST | Create a new patient record |store in IndexedDB and will sync on reconnect to internet | Store new record with `dirty=true`; sync and clear flag later | - - - -## 2. Encounter Management and Questionnaire Filling - -This workflow include list down encounters on the encounter page , create/update encounter. It include filling questionnair available on encounter page . user can see allregies,symptoms,diagnones and medication statements. - -pattern of Read only Api that going to be cached: - 1. URL starts with `/api/v1/encounter` - 2. URL matches `/api/v1/facility/:facilityId/organizations` - 3. URL starts with `/api/v1/valueset` - 4. URL starts with `/api/v1/questionnaire` - 5. URL matches `/api/v1/patient/:patientId/symptom/` - 6. URL matches `/api/v1/patient/:patientId/diagnosis/` - 7. URL matches `/api/v1/patient/:patientId/allergy_intolerance/` - 8. URL matches `/api/v1/patient/:patientId/medication-statement/` - 9. URL matches `/api/v1/patient/:patientId/questionnaire_response/` - 10. URL starts with `/api/v1/role/` - - - -| Step | User Action | Method | Endpoint | Offline Handling | Notes | -|:----:|:------------------------------------------------------|:-------|:------------------------------------|:-----------------------------|:--------------------------------------------------------------| -| 1 | List recent encounters for a facility | GET | `/api/v1/encounter?facility={facilityId}&...` | Cache (Service Worker) | Main encounter list | -| 2 | Create a new encounter | POST | `/api/v1/encounter/` | Store & Sync (IndexedDB) | Adds a new encounter | -| 3 | Fetch specific encounter detail | GET | `/api/v1/encounter/{encounterId}/?facility={facilityId}` | Cache (Service Worker) | View full encounter | -| 5 | Load facility organizations by parent | GET | `/api/v1/facility/{facilityId}/organizations?parent={orgParentId}&...`| Cache (Service Worker) | Child organizations dropdown | -| 6 | Load root facility organizations | GET | `/api/v1/facility/{facilityId}/organizations?parent=&...` | Cache (Service Worker) | Root organizations list | -| 7 | Load patient allergy history | GET | `/api/v1/patient/{patientId}/allergy_intolerance?&...` | Cache (Service Worker) | Allergy records | -| 8 | Load current-encounter symptoms | GET | `/api/v1/patient/{patientId}/symptom?encounter={encounterId}&...` | Cache (Service Worker) | Symptoms tied to this encounter | -| 9 | Load full symptom history | GET | `/api/v1/patient/{patientId}/symptom?limit=100&...` | Cache (Service Worker) | “History” view | -| 10 | Load current-encounter diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?encounter={encounterId}&...` | Cache (Service Worker) | Diagnosis for this encounter | -| 11 | Load full diagnosis history | GET | `/api/v1/patient/{patientId}/diagnosis?limit=100&...` | Cache (Service Worker) | “History” view | -| 12 | List questionnaire responses for this encounter | GET | `/api/v1/patient/{patientId}/questionnaire_response?encounter={encounterId}&...` | Cache (Service Worker) | Already-filled forms | -| 13 | Fetch encounter-specific questionnaires | GET | `/api/v1/questionnaire?tag_slug=encounter_actions&...` | Cache (Service Worker) | Which forms can be added | -| 14 | Fetch all active questionnaires | GET | `/api/v1/questionnaire?status=active&...` | Cache (Service Worker) | General form listings | -| 15 | Fetch questionnaires with subject type 'encounter' | GET | `/api/v1/questionnaire?subject_type=encounter&status=active&...` | Cache (Service Worker) | Encounter-scoped forms | -| 16 | Load a questionnaire definition | GET | `/api/v1/questionnaire/{questionnaireSlug}/` | Cache (Service Worker) | Full form schema | -| 17 | Load valueset favourites | GET | `/api/v1/valueset/{slug}/favourites/` | Cache (Service Worker) | Speeds up include/exclude lookups | -| 18 | Load valueset recent views | GET | `/api/v1/valueset/{slug}/recent_views/` | Cache (Service Worker) | Speeds up include/exclude lookups | -| 19 | Expand valueset to retrieve items | POST | `/api/v1/valueset/{slug}/expand/` | Cache results (Service Worker) | Lookup codes for pick-lists | -| 20 | Batch create/update encounter-related data | POST | `/api/v1/batch_requests/` | Store & Sync (IndexedDB) | Batch create or update of symptoms, diagnoses, allergies, questionnaire responses, etc. | - -## 3. Patient Profile Management -This workflow include access patient profile and including appointments, encounters, health data, resources, users. - - -pattern of Read only Api that going to be cached: - 1. URL starts with `/api/v1/users/` - 2. URL matches `/api/v1/patient/:patientId/` - 3. URL matches `/api/v1/patient/:patientId/get_users/` - 4. URL matches `/api/v1/patient/:externalId/get_appointments/` - 5. URL starts with `/api/v1/patient` - 6. URL starts with `/api/v1/resource` - 7. URL starts with `/api/v1/getallfacilities` - - - - -| Step | User Action | Method | Endpoint | Offline Handling | Notes | -|:----:|:----------------------------------------------|:-------|:-------------------------------------------------------------------|:-----------------------------|:----------------------------------------------------| -| 1 | View patient detail | GET | `/api/v1/patient/{patientId}/` | Cache (Service Worker) | Load full patient record | -| 2 | List patients in context | GET | `/api/v1/patient?limit...` | Cache (Service Worker) | Patient cards on Patient tab | -| 3 | View organization detail | GET | `/api/v1/organization/{organizationId}/` | Cache (Service Worker) | Parent organization | -| 4 | List child organizations | GET | `/api/v1/organization?parent={organizationId}&...` | Cache (Service Worker) | Fetch children from parent organization | -| 7 | Search patient by phone | POST | `/api/v1/patient/search/` | Store in IndexedDB | Indexed by phone number | -| 8 | Update patient details | PUT | `/api/v1/patient/{patientId}/` | Store & Sync (IndexedDB) | Save edited profile | -| 9 | List patient appointments | GET | `/api/v1/appointments?patient={patientId}&...` | Cache (Service Worker) | Appointments tab | -| 10 | List recent encounters for patient | GET | `/api/v1/encounter?patient={patientId}&...` | Cache (Service Worker) | Encounter tab | -| 11 | List health profile: medications | GET | `/api/v1/patient/{patientId}/medication/statement?limit...` | Cache (Service Worker) | Medications history | -| 12 | List health profile: allergies | GET | `/api/v1/patient/{patientId}/allergy_intolerance?limit...` | Cache (Service Worker) | Allergy history | -| 13 | List health profile: symptoms | GET | `/api/v1/patient/{patientId}/symptom?limit...` | Cache (Service Worker) | Symptom history | -| 14 | List health profile: diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?category...&limit...` | Cache (Service Worker) | Diagnosis history | -| 15 | List patient questionnaire responses | GET | `/api/v1/patient/{patientId}/questionnaire_response?subject_type=patient&...` | Cache (Service Worker) | Patient forms | -| 16 | List resources related to patient | GET | `/api/v1/resource?related_patient={patientId}&...` | Cache (Service Worker) | Request tab | -| 17 | List all facilities | GET | `/api/v1/getallfacilities?limit...` | Cache (Service Worker) | Facility lookup | -| 18 | Create resource | POST | `/api/v1/resource/` | Store & Sync (IndexedDB) | Add new resource | -| 19 | Get users list | GET | `/api/v1/users?limit...&search_text...` | Cache (Service Worker) | For user assignment | -| 20 | List resources | GET | `/api/v1/resource?status=pending&...` | Cache (Service Worker) | Filtered resource list | -| 21 | Fetch specific resource detail | GET | `/api/v1/resource/{resourceid}/` | Cache (Service Worker) | Resource detail view | -| 22 | List assignable users | GET | `/api/v1/patient/{patientId}/get_users/` | Cache (Service Worker) | Add user to patient | -| 23 | List roles | GET | `/api/v1/role/` | Cache (Service Worker) | For user assignment | -| 24 | Assign user to patient | POST | `/api/v1/patient/{patientId}/add_user/` | Store & Sync (IndexedDB) | Add user to patient | - -## 4. Appointments Management -It include View, create, and manage appointments for a facility. -1. url start with (`api/v1/facility/{fac:id}/appointments`) -2. url start with (`api/v1/facility/{fac:id}/slots`) - -| Step | User Action | Method | Endpoint | Offline Handling | Notes | -|:----:|:------------------------------------------------------|:-------|:----------------------------------------------------------------|:-----------------------------|:------------------------------------------------------------| -| 1 | Fetch details for a selected facility | GET | `/api/v1/facility/{facilityId}/` | Cache (Service Worker) | Cache per facility. workbox on-demand caching will be | -| 2 | List “in consultation” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=in_consultation&...` | Cache (Service Worker) | data coming from these api will be cached on req from server by sw(workbox) during online and then this data will available ofline | -| 3 | List “fulfilled” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=fulfilled&...` | Cache (Service Worker) | same as 2 | -| 4 | List “no-show” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=noshow&...` | Cache (Service Worker) | same as 2 | -| 5 | List “booked” (upcoming) appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=booked&...` | Cache (Service Worker) | same as 2 | -| 6 | List “checked in” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=checked_in&...` | Cache (Service Worker) | same as 2 | -| 7 | Fetch available users(practitioner) for appointments | GET | `/api/v1/facility/{facilityId}/appointments/available_users/` | Cache (Service Worker) | sw(workbox) will handle caching of dynamic data comig from this api | -| 8 | Get aggregated slot availability stats | POST | `/api/v1/facility/{facilityId}/slots/availability_stats/` | Store & Sync (IndexedDB) | Keyed by `{from_date,to_date,user}`; for charts | -| 9 | Get detailed slots for a specific day | POST | `/api/v1/facility/{facilityId}/slots/get_slots_for_day/` | Store & Sync (IndexedDB) | Keyed by `{day,user}`; for slot picker | -| 10 | Create a new appointment on a slot | POST | `/api/v1/facility/{facilityId}/slots/{slotId}/create_appointment/` | Store & Sync (IndexedDB) | Stored locally and sync later | -| 11 | Fetch details of a specific appointment | GET | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Cache (Service Worker) | Keyed by `appointmentId`; for detail view | -| 12 | Reschedule an existing appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/reschedule/` | Store & Sync (IndexedDB) | Queue reschedule request locally and will sync later | -| 13 | Update appointment details (status, notes, etc.) | PUT | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Store & Sync (IndexedDB) | Queue update locally and will sync later | -| 14 | Cancel an appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/cancel/` | Store & Sync (IndexedDB) | Queue cancel request locally and will sync later | -| 15 | Get list of appointments of a patient | GET | `/api/v1/facility/{facilityid}/appointments/?patient={patientid}&limit=100` | cache(SW) | sw(workbox) will handle caching of dynamic data comig from this api | - -## Key Assumptions and Design Considerations for Offline Mode : - - Before implementing offline support, it’s essential to define how the application determines internet connectivity and how it behaves in various online/offline scenarios. This document outlines the design assumptions and expected behavior around connection status handling. - - We use a real server connectivity check instead of navigator.onLine. This involves pinging a known backend endpoint (e.g., /ping) and marking the user as offline if the server is unreachable. - - We maintain a global state variable isOnline to represent the app’s understanding of the user’s connectivity. This value can be: - - set based on real server checks during page reload and websites open . - - Manually overridden by the user via a toggle. - - ### Offline Mode Toggle Button - The app provides an "Enable Offline Mode" toggle button in the UI.This gives users control when internet conditions are unstable, allowing them to continue working in offline mode without interruptions. - - we Use an Explicit `isOnline` State Instead of Just Runtime Checks because Automatic checks can flicker rapidly when internet connectivity is poor (e.g., intermittent mobile data). This can cause unpredictable app behavior, like failed fetches or UI state inconsistencies. Manual offline mode offers a **clear and consistent user experience**. Users understand when they're offline and what features are expected to work - - - Enabled = isOnline = false: User manually forces the app into offline mode to prevent unstable behavior during intermittent connectivity. - - Disabled = isOnline = true: The app will attempt to connect to the server normally. - -### Behavior on App Load and Connectivity Events - -#### 1. App Launch (Fresh Tab Open) -- On opening the app, `checkRealServer()` is called. -- If the server is reachable → `isOnline = true` or If unreachable → `isOnline = false`.. -- Ensures `isOnline` reflects real connectivity at startup. ---- - -#### 2. Login Attempt During Poor Connectivity -- If the user is **not logged in** and connectivity is poor,then Login may fail: -- App offers **fallback login** using last cached profile (if available). -- It Allows access even when login fails due to network issues. ---- - -#### 3. User Goes Offline During Active Session - - If already logged in and internet becomes unstable: - - User can toggle **offline mode manually** → `isOnline = false`. - - Enables offline features (e.g., cached reads, local writes). ---- - -#### 4. Page Reload or Tab Reopen -- On reload or reopening , `checkRealServer()` runs again and Updates `isOnline` state.: -- Notifies user of current connection status. -- Even if user previously forced offline mode, connection is revalidated. - -### Role based caching , when multiple users access the app on the same device -- As Care have role based access control, we have to ensure that if multiple user access the app on same device then their data of one user does not override the other user data while caching. Its necessary because permissions are come from backend via api based on user. For example, the getCurrentUser API returns a list of permissions specific to the logged-in user. If this response is cached and a different user logs in, their permissions could overwrite the previous user's data. Later, if the first user tries to use the app offline, they might see incorrect permissions or data from the second user. - -so to ovrcome this problem we have to cache data per user. we will discussed its solution in approache's section. - - -# Now Lets Discussed Approches to achive offline Support : - The approaches discussed here focus primarily on caching API responses. Our UI shell is already cached by default since CARE is a PWA. Below are some of the approaches we can use to achieve offline functionality for the workflows we’ve discussed. - ---- - -### 1. Using TanStack Query Cache with Persistence -This approach plays around using TanStack Query to cache the API responses and sync write operations once the app comes back online. Persistence, in this context, here means to store the cached data into some local DB (e.g., IndexedDB). Since CARE already uses TanStack Query for fetch and write operations, integrating persistence and offline syncing becomes relatively straightforward. This approach leverages existing infrastructure, reducing the need for additional caching logic or custom data handling. Now let's discuss what configuration needs to be implemented for this approach. - -#### 1. Persister: -It is the mechanism that defines how and where the data is saved/restored. TanStack provides some persisters like `createSyncStoragePersister` for mainly localStorage and `createAsyncStoragePersister` for AsyncStorage. It also provides a method to create a custom persister. We will create a custom persister for IndexedDB(via dexie.js) for our use case. - - We just need to create a createDbPersister() function that returns our custom persister. Inside this function, we define a single cache key under which the entire React Query cache will be stored. The function should implement three methods — `persistClient()`, `restoreClient()`, and `removeClient()` — following the rules to create a custom persister for React Query. - -When online, data fetched using useQuery is stored in the in-memory cache and also persisted to IndexedDB via this custom persister. When offline, the cached data stored in IndexedDB (using Dexie.js) is automatically hydrated back into the in-memory cache. TanStack Query then manages the cache seamlessly, providing offline support without extra effort. - -#### 2. cacheTime(or gcTime) and maxage: -For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cacheTime` should be set as the same value or higher than `persistQueryClient`'s `maxAge` option. - -- `cacheTime` defines how long inactive cached data stays in memory before being garbage collected. This means the data remains available for queries without refetching during this time, even if not actively used. -- `maxAge` (used in persistQueryClient) determines how long the persisted cache data remains valid and can be restored from storage. - -#### 4. PersistQueryClientProvider: -`PersistQueryClientProvider` is a React wrapper around our normal `QueryClientProvider` that automatically restores persisted cache on mount and keeps it in sync through subscribe/unsubscribe. It prevents our queries from fetching until the cache is hydrated, ensuring a smooth offline‑first experience. - - **lets discuss how to overcome the problem of cache mixing when multiple user use same device :** TanStack Query caches all API data fetched via useQuery. To avoid mixing cached data between different users, include the userId as part of the queryKey along with other parameters. Since TanStack Query stores cache data based on the queryKey (not just the URL), adding userId creates distinct cache entries even if the API endpoint (URL) is the same. This ensures each user’s data is cached separately. - (Unlike Workbox, which caches based on URL, TanStack Query relies on the uniqueness of the query key.) - - **Note:** To avoid unnecessary refetch attempts when offline, configure useQuery with enabled: isOnline === true. This way, during online mode, staleTime can remain 0 to always fetch fresh data. When offline, the query is disabled (enabled: false), so cached data is used without triggering refetches that would fail. - This approach keeps the default online behavior unchanged while ensuring smooth offline caching without forced stale data refetches. - - Main advantages of this approches is : - - It will cache data coming from api that was fetch using useQuery irrespective of operation we use(eg.GET,POST).But Approches like workbox does not provide such option's they will cache only get api responses not read only post responses. - - As Care is already using tanstack query it become easy to implement this approch in the CARE. we dont have to write - - Give option to separate cached data based on user by using just single userid in query key.But if we use aproches like workbox we have to add userid in url to store data in cache based on url, it can increase complexity. - - #### sync and write operation in this approah : - - - - - - - - - diff --git a/docs/care/offline-support-project/offline-support-cep.md b/docs/care/offline-support-project/offline-support-cep.md index 43eb36c..50c08e8 100644 --- a/docs/care/offline-support-project/offline-support-cep.md +++ b/docs/care/offline-support-project/offline-support-cep.md @@ -1 +1,242 @@ -## CEP for offline support project \ No newline at end of file +# CEP for offline support project + +This document lists the standard workflows that will be supported offline and how we can achive this. +Each workflow has an associated markdown table that defines the structure, HTTP method, and other important information about the API endpoints involved. + +In the tables, backend API endpoints are clearly distinguished based on whether they will be **cached** or **stored and later synced via IndexedDB (for write operations).** + +These tables give us idea about which dynamic data need to be cached for offlien support and help to avoid store unnecessary data. +--- + +## Standard Workflows + +- [1. Patient Registration and Search Patient Workflow](#1-patient-registration-and-search-patient-workflow) +- [2. Encounter Management and Questionnaire Filling](#2-encounter-management-and-questionnaire-filling) +- [3. Patient Profile management](#3-patient-profile-management) +- [4. Appointments Management](#4-appointments-management) + +--- + +## 1. Patient Registration and Search Patient Workflow +Patient registration and search patient workflow include add new patient and search patient by its mobile number.After click on one of the search patient ,user go to page from where they can create encounter,schedule appointments,see encounters list of that patient.This table mainly include endpoints regarding add patient and search patient only . create encounter,list encounter come under the encounter management and schedule appointment come under the appointment management. + + pattern of Read only Api that going to be cached : + 1. URL starts with `/api/v1/getcurrentuser` + 2. URL starts with `/api/v1/organization` + 3. URL matches `/api/v1/facility/:facilityId/` + + + Note: (3,4) are the post type read only api so we need to store them into indexed when user hit them during online. and (8) is the write operation api. + + +| # | API Endpoint | Method | Purpose | Offline Handling | Notes | +|:--:|:------------------------------------------------------------------|:------:|:-----------------------------------------------------------|:----------------------------------|:--------------------------------------------------------------| +| 1 | `/api/v1/users/getcurrentuser/` | GET | Fetch current logged-in user info (global) | Cache (SW) | Used in every workflow. precaching will be best for this | +| 2 | `/api/v1/facility/{facility_id}/` | GET | Fetch details for a selected facility | Cache (SW) | Cache per facility. workbox on-demand caching will be used | +| 3 | `/api/v1/patient/search/` | POST | Search patients by phone number | cache in IndexedDB whenever user's hit it .so that data coming from this can load on the ui during offline | Keyed by phone; store results + timestamp | +| 4 | `/api/v1/patient/search_retrieve/` | POST | Retrieve full patient record after selecting from search | cache in IndexedDB during online so that data coming from this can load on the ui during offline | Keyed by patient.id; | +| 5 | `/api/v1/encounter/?patient={patient_id}&live=false` | GET | List all encounters for a specific patient | Cache (SW) | workbox on-demand caching will be used | +| 6 | `/api/v1/organization/?org_type=govt&parent=&limit=200` | GET | Fetch top-level government organization list | Cache (SW) |workbox on-demand caching will be used | +| 7 | `/api/v1/organization/?org_type=govt&parent={parent_id}&limit=200`| GET | Fetch child organizations under a given parent (dynamic) | Cache (SW) | Same pattern as #6; cache per parent query. | +| 8 | `/api/v1/patient/` | POST | Create a new patient record |store in IndexedDB and will sync on reconnect to internet | Store new record with `dirty=true`; sync and clear flag later | + + + +## 2. Encounter Management and Questionnaire Filling + +This workflow include list down encounters on the encounter page , create/update encounter. It include filling questionnair available on encounter page . user can see allregies,symptoms,diagnones and medication statements. + +pattern of Read only Api that going to be cached: + 1. URL starts with `/api/v1/encounter` + 2. URL matches `/api/v1/facility/:facilityId/organizations` + 3. URL starts with `/api/v1/valueset` + 4. URL starts with `/api/v1/questionnaire` + 5. URL matches `/api/v1/patient/:patientId/symptom/` + 6. URL matches `/api/v1/patient/:patientId/diagnosis/` + 7. URL matches `/api/v1/patient/:patientId/allergy_intolerance/` + 8. URL matches `/api/v1/patient/:patientId/medication-statement/` + 9. URL matches `/api/v1/patient/:patientId/questionnaire_response/` + 10. URL starts with `/api/v1/role/` + + + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +|:----:|:------------------------------------------------------|:-------|:------------------------------------|:-----------------------------|:--------------------------------------------------------------| +| 1 | List recent encounters for a facility | GET | `/api/v1/encounter?facility={facilityId}&...` | Cache (Service Worker) | Main encounter list | +| 2 | Create a new encounter | POST | `/api/v1/encounter/` | Store & Sync (IndexedDB) | Adds a new encounter | +| 3 | Fetch specific encounter detail | GET | `/api/v1/encounter/{encounterId}/?facility={facilityId}` | Cache (Service Worker) | View full encounter | +| 5 | Load facility organizations by parent | GET | `/api/v1/facility/{facilityId}/organizations?parent={orgParentId}&...`| Cache (Service Worker) | Child organizations dropdown | +| 6 | Load root facility organizations | GET | `/api/v1/facility/{facilityId}/organizations?parent=&...` | Cache (Service Worker) | Root organizations list | +| 7 | Load patient allergy history | GET | `/api/v1/patient/{patientId}/allergy_intolerance?&...` | Cache (Service Worker) | Allergy records | +| 8 | Load current-encounter symptoms | GET | `/api/v1/patient/{patientId}/symptom?encounter={encounterId}&...` | Cache (Service Worker) | Symptoms tied to this encounter | +| 9 | Load full symptom history | GET | `/api/v1/patient/{patientId}/symptom?limit=100&...` | Cache (Service Worker) | “History” view | +| 10 | Load current-encounter diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?encounter={encounterId}&...` | Cache (Service Worker) | Diagnosis for this encounter | +| 11 | Load full diagnosis history | GET | `/api/v1/patient/{patientId}/diagnosis?limit=100&...` | Cache (Service Worker) | “History” view | +| 12 | List questionnaire responses for this encounter | GET | `/api/v1/patient/{patientId}/questionnaire_response?encounter={encounterId}&...` | Cache (Service Worker) | Already-filled forms | +| 13 | Fetch encounter-specific questionnaires | GET | `/api/v1/questionnaire?tag_slug=encounter_actions&...` | Cache (Service Worker) | Which forms can be added | +| 14 | Fetch all active questionnaires | GET | `/api/v1/questionnaire?status=active&...` | Cache (Service Worker) | General form listings | +| 15 | Fetch questionnaires with subject type 'encounter' | GET | `/api/v1/questionnaire?subject_type=encounter&status=active&...` | Cache (Service Worker) | Encounter-scoped forms | +| 16 | Load a questionnaire definition | GET | `/api/v1/questionnaire/{questionnaireSlug}/` | Cache (Service Worker) | Full form schema | +| 17 | Load valueset favourites | GET | `/api/v1/valueset/{slug}/favourites/` | Cache (Service Worker) | Speeds up include/exclude lookups | +| 18 | Load valueset recent views | GET | `/api/v1/valueset/{slug}/recent_views/` | Cache (Service Worker) | Speeds up include/exclude lookups | +| 19 | Expand valueset to retrieve items | POST | `/api/v1/valueset/{slug}/expand/` | Cache results (Service Worker) | Lookup codes for pick-lists | +| 20 | Batch create/update encounter-related data | POST | `/api/v1/batch_requests/` | Store & Sync (IndexedDB) | Batch create or update of symptoms, diagnoses, allergies, questionnaire responses, etc. | + +## 3. Patient Profile Management +This workflow include access patient profile and including appointments, encounters, health data, resources, users. + + +pattern of Read only Api that going to be cached: + 1. URL starts with `/api/v1/users/` + 2. URL matches `/api/v1/patient/:patientId/` + 3. URL matches `/api/v1/patient/:patientId/get_users/` + 4. URL matches `/api/v1/patient/:externalId/get_appointments/` + 5. URL starts with `/api/v1/patient` + 6. URL starts with `/api/v1/resource` + 7. URL starts with `/api/v1/getallfacilities` + + + + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +|:----:|:----------------------------------------------|:-------|:-------------------------------------------------------------------|:-----------------------------|:----------------------------------------------------| +| 1 | View patient detail | GET | `/api/v1/patient/{patientId}/` | Cache (Service Worker) | Load full patient record | +| 2 | List patients in context | GET | `/api/v1/patient?limit...` | Cache (Service Worker) | Patient cards on Patient tab | +| 3 | View organization detail | GET | `/api/v1/organization/{organizationId}/` | Cache (Service Worker) | Parent organization | +| 4 | List child organizations | GET | `/api/v1/organization?parent={organizationId}&...` | Cache (Service Worker) | Fetch children from parent organization | +| 7 | Search patient by phone | POST | `/api/v1/patient/search/` | Store in IndexedDB | Indexed by phone number | +| 8 | Update patient details | PUT | `/api/v1/patient/{patientId}/` | Store & Sync (IndexedDB) | Save edited profile | +| 9 | List patient appointments | GET | `/api/v1/appointments?patient={patientId}&...` | Cache (Service Worker) | Appointments tab | +| 10 | List recent encounters for patient | GET | `/api/v1/encounter?patient={patientId}&...` | Cache (Service Worker) | Encounter tab | +| 11 | List health profile: medications | GET | `/api/v1/patient/{patientId}/medication/statement?limit...` | Cache (Service Worker) | Medications history | +| 12 | List health profile: allergies | GET | `/api/v1/patient/{patientId}/allergy_intolerance?limit...` | Cache (Service Worker) | Allergy history | +| 13 | List health profile: symptoms | GET | `/api/v1/patient/{patientId}/symptom?limit...` | Cache (Service Worker) | Symptom history | +| 14 | List health profile: diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?category...&limit...` | Cache (Service Worker) | Diagnosis history | +| 15 | List patient questionnaire responses | GET | `/api/v1/patient/{patientId}/questionnaire_response?subject_type=patient&...` | Cache (Service Worker) | Patient forms | +| 16 | List resources related to patient | GET | `/api/v1/resource?related_patient={patientId}&...` | Cache (Service Worker) | Request tab | +| 17 | List all facilities | GET | `/api/v1/getallfacilities?limit...` | Cache (Service Worker) | Facility lookup | +| 18 | Create resource | POST | `/api/v1/resource/` | Store & Sync (IndexedDB) | Add new resource | +| 19 | Get users list | GET | `/api/v1/users?limit...&search_text...` | Cache (Service Worker) | For user assignment | +| 20 | List resources | GET | `/api/v1/resource?status=pending&...` | Cache (Service Worker) | Filtered resource list | +| 21 | Fetch specific resource detail | GET | `/api/v1/resource/{resourceid}/` | Cache (Service Worker) | Resource detail view | +| 22 | List assignable users | GET | `/api/v1/patient/{patientId}/get_users/` | Cache (Service Worker) | Add user to patient | +| 23 | List roles | GET | `/api/v1/role/` | Cache (Service Worker) | For user assignment | +| 24 | Assign user to patient | POST | `/api/v1/patient/{patientId}/add_user/` | Store & Sync (IndexedDB) | Add user to patient | + +## 4. Appointments Management +It include View, create, and manage appointments for a facility. +1. url start with (`api/v1/facility/{fac:id}/appointments`) +2. url start with (`api/v1/facility/{fac:id}/slots`) + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +|:----:|:------------------------------------------------------|:-------|:----------------------------------------------------------------|:-----------------------------|:------------------------------------------------------------| +| 1 | Fetch details for a selected facility | GET | `/api/v1/facility/{facilityId}/` | Cache (Service Worker) | Cache per facility. workbox on-demand caching will be | +| 2 | List “in consultation” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=in_consultation&...` | Cache (Service Worker) | data coming from these api will be cached on req from server by sw(workbox) during online and then this data will available ofline | +| 3 | List “fulfilled” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=fulfilled&...` | Cache (Service Worker) | same as 2 | +| 4 | List “no-show” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=noshow&...` | Cache (Service Worker) | same as 2 | +| 5 | List “booked” (upcoming) appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=booked&...` | Cache (Service Worker) | same as 2 | +| 6 | List “checked in” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=checked_in&...` | Cache (Service Worker) | same as 2 | +| 7 | Fetch available users(practitioner) for appointments | GET | `/api/v1/facility/{facilityId}/appointments/available_users/` | Cache (Service Worker) | sw(workbox) will handle caching of dynamic data comig from this api | +| 8 | Get aggregated slot availability stats | POST | `/api/v1/facility/{facilityId}/slots/availability_stats/` | Store & Sync (IndexedDB) | Keyed by `{from_date,to_date,user}`; for charts | +| 9 | Get detailed slots for a specific day | POST | `/api/v1/facility/{facilityId}/slots/get_slots_for_day/` | Store & Sync (IndexedDB) | Keyed by `{day,user}`; for slot picker | +| 10 | Create a new appointment on a slot | POST | `/api/v1/facility/{facilityId}/slots/{slotId}/create_appointment/` | Store & Sync (IndexedDB) | Stored locally and sync later | +| 11 | Fetch details of a specific appointment | GET | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Cache (Service Worker) | Keyed by `appointmentId`; for detail view | +| 12 | Reschedule an existing appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/reschedule/` | Store & Sync (IndexedDB) | Queue reschedule request locally and will sync later | +| 13 | Update appointment details (status, notes, etc.) | PUT | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Store & Sync (IndexedDB) | Queue update locally and will sync later | +| 14 | Cancel an appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/cancel/` | Store & Sync (IndexedDB) | Queue cancel request locally and will sync later | +| 15 | Get list of appointments of a patient | GET | `/api/v1/facility/{facilityid}/appointments/?patient={patientid}&limit=100` | cache(SW) | sw(workbox) will handle caching of dynamic data comig from this api | + +## Key Assumptions and Design Considerations for Offline Mode : + + Before implementing offline support, it’s essential to define how the application determines internet connectivity and how it behaves in various online/offline scenarios. This document outlines the design assumptions and expected behavior around connection status handling. + + We use a real server connectivity check instead of navigator.onLine. This involves pinging a known backend endpoint (e.g., /ping) and marking the user as offline if the server is unreachable. + + We maintain a global state variable isOnline to represent the app’s understanding of the user’s connectivity. This value can be: + - set based on real server checks during page reload and websites open . + - Manually overridden by the user via a toggle. + + ### Offline Mode Toggle Button + The app provides an "Enable Offline Mode" toggle button in the UI.This gives users control when internet conditions are unstable, allowing them to continue working in offline mode without interruptions. + + we Use an Explicit `isOnline` State Instead of Just Runtime Checks because Automatic checks can flicker rapidly when internet connectivity is poor (e.g., intermittent mobile data). This can cause unpredictable app behavior, like failed fetches or UI state inconsistencies. Manual offline mode offers a **clear and consistent user experience**. Users understand when they're offline and what features are expected to work + + - Enabled = isOnline = false: User manually forces the app into offline mode to prevent unstable behavior during intermittent connectivity. + - Disabled = isOnline = true: The app will attempt to connect to the server normally. + +### Behavior on App Load and Connectivity Events + +#### 1. App Launch (Fresh Tab Open) +- On opening the app, `checkRealServer()` is called. +- If the server is reachable → `isOnline = true` or If unreachable → `isOnline = false`.. +- Ensures `isOnline` reflects real connectivity at startup. +--- + +#### 2. Login Attempt During Poor Connectivity +- If the user is **not logged in** and connectivity is poor,then Login may fail: +- App offers **fallback login** using last cached profile (if available). +- It Allows access even when login fails due to network issues. +--- + +#### 3. User Goes Offline During Active Session + - If already logged in and internet becomes unstable: + - User can toggle **offline mode manually** → `isOnline = false`. + - Enables offline features (e.g., cached reads, local writes). +--- + +#### 4. Page Reload or Tab Reopen +- On reload or reopening , `checkRealServer()` runs again and Updates `isOnline` state.: +- Notifies user of current connection status. +- Even if user previously forced offline mode, connection is revalidated. + +### Role based caching , when multiple users access the app on the same device +- As Care have role based access control, we have to ensure that if multiple user access the app on same device then their data of one user does not override the other user data while caching. Its necessary because permissions are come from backend via api based on user. For example, the getCurrentUser API returns a list of permissions specific to the logged-in user. If this response is cached and a different user logs in, their permissions could overwrite the previous user's data. Later, if the first user tries to use the app offline, they might see incorrect permissions or data from the second user. + +so to ovrcome this problem we have to cache data per user. we will discussed its solution in approache's section. + + +# Now Lets Discussed Approches to achive offline Support : + The approaches discussed here focus primarily on caching API responses. Our UI shell is already cached by default since CARE is a PWA. Below are some of the approaches we can use to achieve offline functionality for the workflows we’ve discussed. + +--- + +### 1. Using TanStack Query Cache with Persistence +This approach plays around using TanStack Query to cache the API responses and sync write operations once the app comes back online. Persistence, in this context, here means to store the cached data into some local DB (e.g., IndexedDB). Since CARE already uses TanStack Query for fetch and write operations, integrating persistence and offline syncing becomes relatively straightforward. This approach leverages existing infrastructure, reducing the need for additional caching logic or custom data handling. Now let's discuss what configuration needs to be implemented for this approach. + +#### 1. Persister: +It is the mechanism that defines how and where the data is saved/restored. TanStack provides some persisters like `createSyncStoragePersister` for mainly localStorage and `createAsyncStoragePersister` for AsyncStorage. It also provides a method to create a custom persister. We will create a custom persister for IndexedDB(via dexie.js) for our use case. + + We just need to create a createDbPersister() function that returns our custom persister. Inside this function, we define a single cache key under which the entire React Query cache will be stored. The function should implement three methods — `persistClient()`, `restoreClient()`, and `removeClient()` — following the rules to create a custom persister for React Query. + +When online, data fetched using useQuery is stored in the in-memory cache and also persisted to IndexedDB via this custom persister. When offline, the cached data stored in IndexedDB (using Dexie.js) is automatically hydrated back into the in-memory cache. TanStack Query then manages the cache seamlessly, providing offline support without extra effort. + +#### 2. cacheTime(or gcTime) and maxage: +For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cacheTime` should be set as the same value or higher than `persistQueryClient`'s `maxAge` option. + +- `cacheTime` defines how long inactive cached data stays in memory before being garbage collected. This means the data remains available for queries without refetching during this time, even if not actively used. +- `maxAge` (used in persistQueryClient) determines how long the persisted cache data remains valid and can be restored from storage. + +#### 4. PersistQueryClientProvider: +`PersistQueryClientProvider` is a React wrapper around our normal `QueryClientProvider` that automatically restores persisted cache on mount and keeps it in sync through subscribe/unsubscribe. It prevents our queries from fetching until the cache is hydrated, ensuring a smooth offline‑first experience. + + **lets discuss how to overcome the problem of cache mixing when multiple user use same device :** TanStack Query caches all API data fetched via useQuery. To avoid mixing cached data between different users, include the userId as part of the queryKey along with other parameters. Since TanStack Query stores cache data based on the queryKey (not just the URL), adding userId creates distinct cache entries even if the API endpoint (URL) is the same. This ensures each user’s data is cached separately. + (Unlike Workbox, which caches based on URL, TanStack Query relies on the uniqueness of the query key.) + + **Note:** To avoid unnecessary refetch attempts when offline, configure useQuery with enabled: isOnline === true. This way, during online mode, staleTime can remain 0 to always fetch fresh data. When offline, the query is disabled (enabled: false), so cached data is used without triggering refetches that would fail. + This approach keeps the default online behavior unchanged while ensuring smooth offline caching without forced stale data refetches. + + Main advantages of this approches is : + - It will cache data coming from api that was fetch using useQuery irrespective of operation we use(eg.GET,POST).But Approches like workbox does not provide such option's they will cache only get api responses not read only post responses. + - As Care is already using tanstack query it become easy to implement this approch in the CARE. we dont have to write + - Give option to separate cached data based on user by using just single userid in query key.But if we use aproches like workbox we have to add userid in url to store data in cache based on url, it can increase complexity. + + #### sync and write operation in this approah : + + + + + + + + + From 8ef6aa6126e29501fce3b6fd36a2aa202d432149 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Sun, 25 May 2025 08:54:28 +0530 Subject: [PATCH 07/15] minor change --- docs/care/offline-support-project/offline-support-cep.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/care/offline-support-project/offline-support-cep.md b/docs/care/offline-support-project/offline-support-cep.md index 50c08e8..b851a10 100644 --- a/docs/care/offline-support-project/offline-support-cep.md +++ b/docs/care/offline-support-project/offline-support-cep.md @@ -228,7 +228,7 @@ For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `ca Main advantages of this approches is : - It will cache data coming from api that was fetch using useQuery irrespective of operation we use(eg.GET,POST).But Approches like workbox does not provide such option's they will cache only get api responses not read only post responses. - As Care is already using tanstack query it become easy to implement this approch in the CARE. we dont have to write - - Give option to separate cached data based on user by using just single userid in query key.But if we use aproches like workbox we have to add userid in url to store data in cache based on url, it can increase complexity. + - Give option to separate cached data based on user by using just single userid in query key. #### sync and write operation in this approah : From cfb30c958776c87233e9f2e8c63f1575f3c44695 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Sun, 25 May 2025 09:40:28 +0530 Subject: [PATCH 08/15] update doc --- .../offline-support-cep.md | 371 +++++++++++------- 1 file changed, 221 insertions(+), 150 deletions(-) diff --git a/docs/care/offline-support-project/offline-support-cep.md b/docs/care/offline-support-project/offline-support-cep.md index b851a10..b2a71b3 100644 --- a/docs/care/offline-support-project/offline-support-cep.md +++ b/docs/care/offline-support-project/offline-support-cep.md @@ -1,242 +1,313 @@ # CEP for offline support project -This document lists the standard workflows that will be supported offline and how we can achive this. +This document lists the standard workflows that will be supported offline and how we can achive this. Each workflow has an associated markdown table that defines the structure, HTTP method, and other important information about the API endpoints involved. In the tables, backend API endpoints are clearly distinguished based on whether they will be **cached** or **stored and later synced via IndexedDB (for write operations).** -These tables give us idea about which dynamic data need to be cached for offlien support and help to avoid store unnecessary data. ---- +## These tables give us idea about which dynamic data need to be cached for offlien support and help to avoid store unnecessary data. ## Standard Workflows -- [1. Patient Registration and Search Patient Workflow](#1-patient-registration-and-search-patient-workflow) -- [2. Encounter Management and Questionnaire Filling](#2-encounter-management-and-questionnaire-filling) -- [3. Patient Profile management](#3-patient-profile-management) +- [1. Patient Registration and Search Patient Workflow](#1-patient-registration-and-search-patient-workflow) +- [2. Encounter Management and Questionnaire Filling](#2-encounter-management-and-questionnaire-filling) +- [3. Patient Profile management](#3-patient-profile-management) - [4. Appointments Management](#4-appointments-management) --- ## 1. Patient Registration and Search Patient Workflow + Patient registration and search patient workflow include add new patient and search patient by its mobile number.After click on one of the search patient ,user go to page from where they can create encounter,schedule appointments,see encounters list of that patient.This table mainly include endpoints regarding add patient and search patient only . create encounter,list encounter come under the encounter management and schedule appointment come under the appointment management. - pattern of Read only Api that going to be cached : - 1. URL starts with `/api/v1/getcurrentuser` - 2. URL starts with `/api/v1/organization` - 3. URL matches `/api/v1/facility/:facilityId/` +pattern of Read only Api that going to be cached : +1. URL starts with `/api/v1/getcurrentuser` +2. URL starts with `/api/v1/organization` +3. URL matches `/api/v1/facility/:facilityId/` - Note: (3,4) are the post type read only api so we need to store them into indexed when user hit them during online. and (8) is the write operation api. - +Note: (3,4) are the post type read only api so we need to store them into indexed when user hit them during online. and (8) is the write operation api. -| # | API Endpoint | Method | Purpose | Offline Handling | Notes | -|:--:|:------------------------------------------------------------------|:------:|:-----------------------------------------------------------|:----------------------------------|:--------------------------------------------------------------| -| 1 | `/api/v1/users/getcurrentuser/` | GET | Fetch current logged-in user info (global) | Cache (SW) | Used in every workflow. precaching will be best for this | -| 2 | `/api/v1/facility/{facility_id}/` | GET | Fetch details for a selected facility | Cache (SW) | Cache per facility. workbox on-demand caching will be used | -| 3 | `/api/v1/patient/search/` | POST | Search patients by phone number | cache in IndexedDB whenever user's hit it .so that data coming from this can load on the ui during offline | Keyed by phone; store results + timestamp | -| 4 | `/api/v1/patient/search_retrieve/` | POST | Retrieve full patient record after selecting from search | cache in IndexedDB during online so that data coming from this can load on the ui during offline | Keyed by patient.id; | -| 5 | `/api/v1/encounter/?patient={patient_id}&live=false` | GET | List all encounters for a specific patient | Cache (SW) | workbox on-demand caching will be used | -| 6 | `/api/v1/organization/?org_type=govt&parent=&limit=200` | GET | Fetch top-level government organization list | Cache (SW) |workbox on-demand caching will be used | -| 7 | `/api/v1/organization/?org_type=govt&parent={parent_id}&limit=200`| GET | Fetch child organizations under a given parent (dynamic) | Cache (SW) | Same pattern as #6; cache per parent query. | -| 8 | `/api/v1/patient/` | POST | Create a new patient record |store in IndexedDB and will sync on reconnect to internet | Store new record with `dirty=true`; sync and clear flag later | +| # | API Endpoint | Method | Purpose | Offline Handling | Notes | +| :-: | :----------------------------------------------------------------- | :----: | :------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | +| 1 | `/api/v1/users/getcurrentuser/` | GET | Fetch current logged-in user info (global) | Cache (SW) | Used in every workflow. precaching will be best for this | +| 2 | `/api/v1/facility/{facility_id}/` | GET | Fetch details for a selected facility | Cache (SW) | Cache per facility. workbox on-demand caching will be used | +| 3 | `/api/v1/patient/search/` | POST | Search patients by phone number | cache in IndexedDB whenever user's hit it .so that data coming from this can load on the ui during offline | Keyed by phone; store results + timestamp | +| 4 | `/api/v1/patient/search_retrieve/` | POST | Retrieve full patient record after selecting from search | cache in IndexedDB during online so that data coming from this can load on the ui during offline | Keyed by patient.id; | +| 5 | `/api/v1/encounter/?patient={patient_id}&live=false` | GET | List all encounters for a specific patient | Cache (SW) | workbox on-demand caching will be used | +| 6 | `/api/v1/organization/?org_type=govt&parent=&limit=200` | GET | Fetch top-level government organization list | Cache (SW) | workbox on-demand caching will be used | +| 7 | `/api/v1/organization/?org_type=govt&parent={parent_id}&limit=200` | GET | Fetch child organizations under a given parent (dynamic) | Cache (SW) | Same pattern as #6; cache per parent query. | +| 8 | `/api/v1/patient/` | POST | Create a new patient record | store in IndexedDB and will sync on reconnect to internet | Store new record with `dirty=true`; sync and clear flag later | +## 2. Encounter Management and Questionnaire Filling +This workflow include list down encounters on the encounter page , create/update encounter. It include filling questionnair available on encounter page . user can see allregies,symptoms,diagnones and medication statements. -## 2. Encounter Management and Questionnaire Filling +pattern of Read only Api that going to be cached: -This workflow include list down encounters on the encounter page , create/update encounter. It include filling questionnair available on encounter page . user can see allregies,symptoms,diagnones and medication statements. +1. URL starts with `/api/v1/encounter` +2. URL matches `/api/v1/facility/:facilityId/organizations` +3. URL starts with `/api/v1/valueset` +4. URL starts with `/api/v1/questionnaire` +5. URL matches `/api/v1/patient/:patientId/symptom/` +6. URL matches `/api/v1/patient/:patientId/diagnosis/` +7. URL matches `/api/v1/patient/:patientId/allergy_intolerance/` +8. URL matches `/api/v1/patient/:patientId/medication-statement/` +9. URL matches `/api/v1/patient/:patientId/questionnaire_response/` +10. URL starts with `/api/v1/role/` + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +| :--: | :------------------------------------------------- | :----- | :------------------------------------------------------------------------------- | :----------------------------- | :-------------------------------------------------------------------------------------- | +| 1 | List recent encounters for a facility | GET | `/api/v1/encounter?facility={facilityId}&...` | Cache (Service Worker) | Main encounter list | +| 2 | Create a new encounter | POST | `/api/v1/encounter/` | Store & Sync (IndexedDB) | Adds a new encounter | +| 3 | Fetch specific encounter detail | GET | `/api/v1/encounter/{encounterId}/?facility={facilityId}` | Cache (Service Worker) | View full encounter | +| 5 | Load facility organizations by parent | GET | `/api/v1/facility/{facilityId}/organizations?parent={orgParentId}&...` | Cache (Service Worker) | Child organizations dropdown | +| 6 | Load root facility organizations | GET | `/api/v1/facility/{facilityId}/organizations?parent=&...` | Cache (Service Worker) | Root organizations list | +| 7 | Load patient allergy history | GET | `/api/v1/patient/{patientId}/allergy_intolerance?&...` | Cache (Service Worker) | Allergy records | +| 8 | Load current-encounter symptoms | GET | `/api/v1/patient/{patientId}/symptom?encounter={encounterId}&...` | Cache (Service Worker) | Symptoms tied to this encounter | +| 9 | Load full symptom history | GET | `/api/v1/patient/{patientId}/symptom?limit=100&...` | Cache (Service Worker) | “History” view | +| 10 | Load current-encounter diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?encounter={encounterId}&...` | Cache (Service Worker) | Diagnosis for this encounter | +| 11 | Load full diagnosis history | GET | `/api/v1/patient/{patientId}/diagnosis?limit=100&...` | Cache (Service Worker) | “History” view | +| 12 | List questionnaire responses for this encounter | GET | `/api/v1/patient/{patientId}/questionnaire_response?encounter={encounterId}&...` | Cache (Service Worker) | Already-filled forms | +| 13 | Fetch encounter-specific questionnaires | GET | `/api/v1/questionnaire?tag_slug=encounter_actions&...` | Cache (Service Worker) | Which forms can be added | +| 14 | Fetch all active questionnaires | GET | `/api/v1/questionnaire?status=active&...` | Cache (Service Worker) | General form listings | +| 15 | Fetch questionnaires with subject type 'encounter' | GET | `/api/v1/questionnaire?subject_type=encounter&status=active&...` | Cache (Service Worker) | Encounter-scoped forms | +| 16 | Load a questionnaire definition | GET | `/api/v1/questionnaire/{questionnaireSlug}/` | Cache (Service Worker) | Full form schema | +| 17 | Load valueset favourites | GET | `/api/v1/valueset/{slug}/favourites/` | Cache (Service Worker) | Speeds up include/exclude lookups | +| 18 | Load valueset recent views | GET | `/api/v1/valueset/{slug}/recent_views/` | Cache (Service Worker) | Speeds up include/exclude lookups | +| 19 | Expand valueset to retrieve items | POST | `/api/v1/valueset/{slug}/expand/` | Cache results (Service Worker) | Lookup codes for pick-lists | +| 20 | Batch create/update encounter-related data | POST | `/api/v1/batch_requests/` | Store & Sync (IndexedDB) | Batch create or update of symptoms, diagnoses, allergies, questionnaire responses, etc. | + +## 3. Patient Profile Management + +This workflow include access patient profile and including appointments, encounters, health data, resources, users. pattern of Read only Api that going to be cached: - 1. URL starts with `/api/v1/encounter` - 2. URL matches `/api/v1/facility/:facilityId/organizations` - 3. URL starts with `/api/v1/valueset` - 4. URL starts with `/api/v1/questionnaire` - 5. URL matches `/api/v1/patient/:patientId/symptom/` - 6. URL matches `/api/v1/patient/:patientId/diagnosis/` - 7. URL matches `/api/v1/patient/:patientId/allergy_intolerance/` - 8. URL matches `/api/v1/patient/:patientId/medication-statement/` - 9. URL matches `/api/v1/patient/:patientId/questionnaire_response/` - 10. URL starts with `/api/v1/role/` - - - -| Step | User Action | Method | Endpoint | Offline Handling | Notes | -|:----:|:------------------------------------------------------|:-------|:------------------------------------|:-----------------------------|:--------------------------------------------------------------| -| 1 | List recent encounters for a facility | GET | `/api/v1/encounter?facility={facilityId}&...` | Cache (Service Worker) | Main encounter list | -| 2 | Create a new encounter | POST | `/api/v1/encounter/` | Store & Sync (IndexedDB) | Adds a new encounter | -| 3 | Fetch specific encounter detail | GET | `/api/v1/encounter/{encounterId}/?facility={facilityId}` | Cache (Service Worker) | View full encounter | -| 5 | Load facility organizations by parent | GET | `/api/v1/facility/{facilityId}/organizations?parent={orgParentId}&...`| Cache (Service Worker) | Child organizations dropdown | -| 6 | Load root facility organizations | GET | `/api/v1/facility/{facilityId}/organizations?parent=&...` | Cache (Service Worker) | Root organizations list | -| 7 | Load patient allergy history | GET | `/api/v1/patient/{patientId}/allergy_intolerance?&...` | Cache (Service Worker) | Allergy records | -| 8 | Load current-encounter symptoms | GET | `/api/v1/patient/{patientId}/symptom?encounter={encounterId}&...` | Cache (Service Worker) | Symptoms tied to this encounter | -| 9 | Load full symptom history | GET | `/api/v1/patient/{patientId}/symptom?limit=100&...` | Cache (Service Worker) | “History” view | -| 10 | Load current-encounter diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?encounter={encounterId}&...` | Cache (Service Worker) | Diagnosis for this encounter | -| 11 | Load full diagnosis history | GET | `/api/v1/patient/{patientId}/diagnosis?limit=100&...` | Cache (Service Worker) | “History” view | -| 12 | List questionnaire responses for this encounter | GET | `/api/v1/patient/{patientId}/questionnaire_response?encounter={encounterId}&...` | Cache (Service Worker) | Already-filled forms | -| 13 | Fetch encounter-specific questionnaires | GET | `/api/v1/questionnaire?tag_slug=encounter_actions&...` | Cache (Service Worker) | Which forms can be added | -| 14 | Fetch all active questionnaires | GET | `/api/v1/questionnaire?status=active&...` | Cache (Service Worker) | General form listings | -| 15 | Fetch questionnaires with subject type 'encounter' | GET | `/api/v1/questionnaire?subject_type=encounter&status=active&...` | Cache (Service Worker) | Encounter-scoped forms | -| 16 | Load a questionnaire definition | GET | `/api/v1/questionnaire/{questionnaireSlug}/` | Cache (Service Worker) | Full form schema | -| 17 | Load valueset favourites | GET | `/api/v1/valueset/{slug}/favourites/` | Cache (Service Worker) | Speeds up include/exclude lookups | -| 18 | Load valueset recent views | GET | `/api/v1/valueset/{slug}/recent_views/` | Cache (Service Worker) | Speeds up include/exclude lookups | -| 19 | Expand valueset to retrieve items | POST | `/api/v1/valueset/{slug}/expand/` | Cache results (Service Worker) | Lookup codes for pick-lists | -| 20 | Batch create/update encounter-related data | POST | `/api/v1/batch_requests/` | Store & Sync (IndexedDB) | Batch create or update of symptoms, diagnoses, allergies, questionnaire responses, etc. | - -## 3. Patient Profile Management -This workflow include access patient profile and including appointments, encounters, health data, resources, users. +1. URL starts with `/api/v1/users/` +2. URL matches `/api/v1/patient/:patientId/` +3. URL matches `/api/v1/patient/:patientId/get_users/` +4. URL matches `/api/v1/patient/:externalId/get_appointments/` +5. URL starts with `/api/v1/patient` +6. URL starts with `/api/v1/resource` +7. URL starts with `/api/v1/getallfacilities` + +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +| :--: | :----------------------------------- | :----- | :---------------------------------------------------------------------------- | :----------------------- | :-------------------------------------- | +| 1 | View patient detail | GET | `/api/v1/patient/{patientId}/` | Cache (Service Worker) | Load full patient record | +| 2 | List patients in context | GET | `/api/v1/patient?limit...` | Cache (Service Worker) | Patient cards on Patient tab | +| 3 | View organization detail | GET | `/api/v1/organization/{organizationId}/` | Cache (Service Worker) | Parent organization | +| 4 | List child organizations | GET | `/api/v1/organization?parent={organizationId}&...` | Cache (Service Worker) | Fetch children from parent organization | +| 7 | Search patient by phone | POST | `/api/v1/patient/search/` | Store in IndexedDB | Indexed by phone number | +| 8 | Update patient details | PUT | `/api/v1/patient/{patientId}/` | Store & Sync (IndexedDB) | Save edited profile | +| 9 | List patient appointments | GET | `/api/v1/appointments?patient={patientId}&...` | Cache (Service Worker) | Appointments tab | +| 10 | List recent encounters for patient | GET | `/api/v1/encounter?patient={patientId}&...` | Cache (Service Worker) | Encounter tab | +| 11 | List health profile: medications | GET | `/api/v1/patient/{patientId}/medication/statement?limit...` | Cache (Service Worker) | Medications history | +| 12 | List health profile: allergies | GET | `/api/v1/patient/{patientId}/allergy_intolerance?limit...` | Cache (Service Worker) | Allergy history | +| 13 | List health profile: symptoms | GET | `/api/v1/patient/{patientId}/symptom?limit...` | Cache (Service Worker) | Symptom history | +| 14 | List health profile: diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?category...&limit...` | Cache (Service Worker) | Diagnosis history | +| 15 | List patient questionnaire responses | GET | `/api/v1/patient/{patientId}/questionnaire_response?subject_type=patient&...` | Cache (Service Worker) | Patient forms | +| 16 | List resources related to patient | GET | `/api/v1/resource?related_patient={patientId}&...` | Cache (Service Worker) | Request tab | +| 17 | List all facilities | GET | `/api/v1/getallfacilities?limit...` | Cache (Service Worker) | Facility lookup | +| 18 | Create resource | POST | `/api/v1/resource/` | Store & Sync (IndexedDB) | Add new resource | +| 19 | Get users list | GET | `/api/v1/users?limit...&search_text...` | Cache (Service Worker) | For user assignment | +| 20 | List resources | GET | `/api/v1/resource?status=pending&...` | Cache (Service Worker) | Filtered resource list | +| 21 | Fetch specific resource detail | GET | `/api/v1/resource/{resourceid}/` | Cache (Service Worker) | Resource detail view | +| 22 | List assignable users | GET | `/api/v1/patient/{patientId}/get_users/` | Cache (Service Worker) | Add user to patient | +| 23 | List roles | GET | `/api/v1/role/` | Cache (Service Worker) | For user assignment | +| 24 | Assign user to patient | POST | `/api/v1/patient/{patientId}/add_user/` | Store & Sync (IndexedDB) | Add user to patient | + +## 4. Appointments Management -pattern of Read only Api that going to be cached: - 1. URL starts with `/api/v1/users/` - 2. URL matches `/api/v1/patient/:patientId/` - 3. URL matches `/api/v1/patient/:patientId/get_users/` - 4. URL matches `/api/v1/patient/:externalId/get_appointments/` - 5. URL starts with `/api/v1/patient` - 6. URL starts with `/api/v1/resource` - 7. URL starts with `/api/v1/getallfacilities` - - - - -| Step | User Action | Method | Endpoint | Offline Handling | Notes | -|:----:|:----------------------------------------------|:-------|:-------------------------------------------------------------------|:-----------------------------|:----------------------------------------------------| -| 1 | View patient detail | GET | `/api/v1/patient/{patientId}/` | Cache (Service Worker) | Load full patient record | -| 2 | List patients in context | GET | `/api/v1/patient?limit...` | Cache (Service Worker) | Patient cards on Patient tab | -| 3 | View organization detail | GET | `/api/v1/organization/{organizationId}/` | Cache (Service Worker) | Parent organization | -| 4 | List child organizations | GET | `/api/v1/organization?parent={organizationId}&...` | Cache (Service Worker) | Fetch children from parent organization | -| 7 | Search patient by phone | POST | `/api/v1/patient/search/` | Store in IndexedDB | Indexed by phone number | -| 8 | Update patient details | PUT | `/api/v1/patient/{patientId}/` | Store & Sync (IndexedDB) | Save edited profile | -| 9 | List patient appointments | GET | `/api/v1/appointments?patient={patientId}&...` | Cache (Service Worker) | Appointments tab | -| 10 | List recent encounters for patient | GET | `/api/v1/encounter?patient={patientId}&...` | Cache (Service Worker) | Encounter tab | -| 11 | List health profile: medications | GET | `/api/v1/patient/{patientId}/medication/statement?limit...` | Cache (Service Worker) | Medications history | -| 12 | List health profile: allergies | GET | `/api/v1/patient/{patientId}/allergy_intolerance?limit...` | Cache (Service Worker) | Allergy history | -| 13 | List health profile: symptoms | GET | `/api/v1/patient/{patientId}/symptom?limit...` | Cache (Service Worker) | Symptom history | -| 14 | List health profile: diagnoses | GET | `/api/v1/patient/{patientId}/diagnosis?category...&limit...` | Cache (Service Worker) | Diagnosis history | -| 15 | List patient questionnaire responses | GET | `/api/v1/patient/{patientId}/questionnaire_response?subject_type=patient&...` | Cache (Service Worker) | Patient forms | -| 16 | List resources related to patient | GET | `/api/v1/resource?related_patient={patientId}&...` | Cache (Service Worker) | Request tab | -| 17 | List all facilities | GET | `/api/v1/getallfacilities?limit...` | Cache (Service Worker) | Facility lookup | -| 18 | Create resource | POST | `/api/v1/resource/` | Store & Sync (IndexedDB) | Add new resource | -| 19 | Get users list | GET | `/api/v1/users?limit...&search_text...` | Cache (Service Worker) | For user assignment | -| 20 | List resources | GET | `/api/v1/resource?status=pending&...` | Cache (Service Worker) | Filtered resource list | -| 21 | Fetch specific resource detail | GET | `/api/v1/resource/{resourceid}/` | Cache (Service Worker) | Resource detail view | -| 22 | List assignable users | GET | `/api/v1/patient/{patientId}/get_users/` | Cache (Service Worker) | Add user to patient | -| 23 | List roles | GET | `/api/v1/role/` | Cache (Service Worker) | For user assignment | -| 24 | Assign user to patient | POST | `/api/v1/patient/{patientId}/add_user/` | Store & Sync (IndexedDB) | Add user to patient | - -## 4. Appointments Management It include View, create, and manage appointments for a facility. + 1. url start with (`api/v1/facility/{fac:id}/appointments`) 2. url start with (`api/v1/facility/{fac:id}/slots`) -| Step | User Action | Method | Endpoint | Offline Handling | Notes | -|:----:|:------------------------------------------------------|:-------|:----------------------------------------------------------------|:-----------------------------|:------------------------------------------------------------| -| 1 | Fetch details for a selected facility | GET | `/api/v1/facility/{facilityId}/` | Cache (Service Worker) | Cache per facility. workbox on-demand caching will be | -| 2 | List “in consultation” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=in_consultation&...` | Cache (Service Worker) | data coming from these api will be cached on req from server by sw(workbox) during online and then this data will available ofline | -| 3 | List “fulfilled” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=fulfilled&...` | Cache (Service Worker) | same as 2 | -| 4 | List “no-show” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=noshow&...` | Cache (Service Worker) | same as 2 | -| 5 | List “booked” (upcoming) appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=booked&...` | Cache (Service Worker) | same as 2 | -| 6 | List “checked in” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=checked_in&...` | Cache (Service Worker) | same as 2 | -| 7 | Fetch available users(practitioner) for appointments | GET | `/api/v1/facility/{facilityId}/appointments/available_users/` | Cache (Service Worker) | sw(workbox) will handle caching of dynamic data comig from this api | -| 8 | Get aggregated slot availability stats | POST | `/api/v1/facility/{facilityId}/slots/availability_stats/` | Store & Sync (IndexedDB) | Keyed by `{from_date,to_date,user}`; for charts | -| 9 | Get detailed slots for a specific day | POST | `/api/v1/facility/{facilityId}/slots/get_slots_for_day/` | Store & Sync (IndexedDB) | Keyed by `{day,user}`; for slot picker | -| 10 | Create a new appointment on a slot | POST | `/api/v1/facility/{facilityId}/slots/{slotId}/create_appointment/` | Store & Sync (IndexedDB) | Stored locally and sync later | -| 11 | Fetch details of a specific appointment | GET | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Cache (Service Worker) | Keyed by `appointmentId`; for detail view | -| 12 | Reschedule an existing appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/reschedule/` | Store & Sync (IndexedDB) | Queue reschedule request locally and will sync later | -| 13 | Update appointment details (status, notes, etc.) | PUT | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Store & Sync (IndexedDB) | Queue update locally and will sync later | -| 14 | Cancel an appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/cancel/` | Store & Sync (IndexedDB) | Queue cancel request locally and will sync later | -| 15 | Get list of appointments of a patient | GET | `/api/v1/facility/{facilityid}/appointments/?patient={patientid}&limit=100` | cache(SW) | sw(workbox) will handle caching of dynamic data comig from this api | +| Step | User Action | Method | Endpoint | Offline Handling | Notes | +| :--: | :--------------------------------------------------- | :----- | :-------------------------------------------------------------------------- | :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Fetch details for a selected facility | GET | `/api/v1/facility/{facilityId}/` | Cache (Service Worker) | Cache per facility. workbox on-demand caching will be | +| 2 | List “in consultation” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=in_consultation&...` | Cache (Service Worker) | data coming from these api will be cached on req from server by sw(workbox) during online and then this data will available ofline | +| 3 | List “fulfilled” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=fulfilled&...` | Cache (Service Worker) | same as 2 | +| 4 | List “no-show” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=noshow&...` | Cache (Service Worker) | same as 2 | +| 5 | List “booked” (upcoming) appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=booked&...` | Cache (Service Worker) | same as 2 | +| 6 | List “checked in” appointments | GET | `/api/v1/facility/{facilityId}/appointments?status=checked_in&...` | Cache (Service Worker) | same as 2 | +| 7 | Fetch available users(practitioner) for appointments | GET | `/api/v1/facility/{facilityId}/appointments/available_users/` | Cache (Service Worker) | sw(workbox) will handle caching of dynamic data comig from this api | +| 8 | Get aggregated slot availability stats | POST | `/api/v1/facility/{facilityId}/slots/availability_stats/` | Store & Sync (IndexedDB) | Keyed by `{from_date,to_date,user}`; for charts | +| 9 | Get detailed slots for a specific day | POST | `/api/v1/facility/{facilityId}/slots/get_slots_for_day/` | Store & Sync (IndexedDB) | Keyed by `{day,user}`; for slot picker | +| 10 | Create a new appointment on a slot | POST | `/api/v1/facility/{facilityId}/slots/{slotId}/create_appointment/` | Store & Sync (IndexedDB) | Stored locally and sync later | +| 11 | Fetch details of a specific appointment | GET | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Cache (Service Worker) | Keyed by `appointmentId`; for detail view | +| 12 | Reschedule an existing appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/reschedule/` | Store & Sync (IndexedDB) | Queue reschedule request locally and will sync later | +| 13 | Update appointment details (status, notes, etc.) | PUT | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/` | Store & Sync (IndexedDB) | Queue update locally and will sync later | +| 14 | Cancel an appointment | POST | `/api/v1/facility/{facilityId}/appointments/{appointmentId}/cancel/` | Store & Sync (IndexedDB) | Queue cancel request locally and will sync later | +| 15 | Get list of appointments of a patient | GET | `/api/v1/facility/{facilityid}/appointments/?patient={patientid}&limit=100` | cache(SW) | sw(workbox) will handle caching of dynamic data comig from this api | ## Key Assumptions and Design Considerations for Offline Mode : - - Before implementing offline support, it’s essential to define how the application determines internet connectivity and how it behaves in various online/offline scenarios. This document outlines the design assumptions and expected behavior around connection status handling. - We use a real server connectivity check instead of navigator.onLine. This involves pinging a known backend endpoint (e.g., /ping) and marking the user as offline if the server is unreachable. +Before implementing offline support, it’s essential to define how the application determines internet connectivity and how it behaves in various online/offline scenarios. This document outlines the design assumptions and expected behavior around connection status handling. + +We use a real server connectivity check instead of navigator.onLine. This involves pinging a known backend endpoint (e.g., /ping) and marking the user as offline if the server is unreachable. - We maintain a global state variable isOnline to represent the app’s understanding of the user’s connectivity. This value can be: - - set based on real server checks during page reload and websites open . - - Manually overridden by the user via a toggle. +We maintain a global state variable isOnline to represent the app’s understanding of the user’s connectivity. This value can be: - ### Offline Mode Toggle Button - The app provides an "Enable Offline Mode" toggle button in the UI.This gives users control when internet conditions are unstable, allowing them to continue working in offline mode without interruptions. - - we Use an Explicit `isOnline` State Instead of Just Runtime Checks because Automatic checks can flicker rapidly when internet connectivity is poor (e.g., intermittent mobile data). This can cause unpredictable app behavior, like failed fetches or UI state inconsistencies. Manual offline mode offers a **clear and consistent user experience**. Users understand when they're offline and what features are expected to work +- set based on real server checks during page reload and websites open . +- Manually overridden by the user via a toggle. - - Enabled = isOnline = false: User manually forces the app into offline mode to prevent unstable behavior during intermittent connectivity. - - Disabled = isOnline = true: The app will attempt to connect to the server normally. +### Offline Mode Toggle Button + +The app provides an "Enable Offline Mode" toggle button in the UI.This gives users control when internet conditions are unstable, allowing them to continue working in offline mode without interruptions. + +we Use an Explicit `isOnline` State Instead of Just Runtime Checks because Automatic checks can flicker rapidly when internet connectivity is poor (e.g., intermittent mobile data). This can cause unpredictable app behavior, like failed fetches or UI state inconsistencies. Manual offline mode offers a **clear and consistent user experience**. Users understand when they're offline and what features are expected to work + +- Enabled = isOnline = false: User manually forces the app into offline mode to prevent unstable behavior during intermittent connectivity. +- Disabled = isOnline = true: The app will attempt to connect to the server normally. ### Behavior on App Load and Connectivity Events #### 1. App Launch (Fresh Tab Open) + - On opening the app, `checkRealServer()` is called. - If the server is reachable → `isOnline = true` or If unreachable → `isOnline = false`.. - Ensures `isOnline` reflects real connectivity at startup. + --- #### 2. Login Attempt During Poor Connectivity + - If the user is **not logged in** and connectivity is poor,then Login may fail: - App offers **fallback login** using last cached profile (if available). -- It Allows access even when login fails due to network issues. +- It Allows access even when login fails due to network issues. + --- #### 3. User Goes Offline During Active Session - - If already logged in and internet becomes unstable: - - User can toggle **offline mode manually** → `isOnline = false`. - - Enables offline features (e.g., cached reads, local writes). + +- If already logged in and internet becomes unstable: +- User can toggle **offline mode manually** → `isOnline = false`. +- Enables offline features (e.g., cached reads, local writes). + --- #### 4. Page Reload or Tab Reopen + - On reload or reopening , `checkRealServer()` runs again and Updates `isOnline` state.: - Notifies user of current connection status. - Even if user previously forced offline mode, connection is revalidated. ### Role based caching , when multiple users access the app on the same device -- As Care have role based access control, we have to ensure that if multiple user access the app on same device then their data of one user does not override the other user data while caching. Its necessary because permissions are come from backend via api based on user. For example, the getCurrentUser API returns a list of permissions specific to the logged-in user. If this response is cached and a different user logs in, their permissions could overwrite the previous user's data. Later, if the first user tries to use the app offline, they might see incorrect permissions or data from the second user. -so to ovrcome this problem we have to cache data per user. we will discussed its solution in approache's section. +- As Care have role based access control, we have to ensure that if multiple user access the app on same device then their data of one user does not override the other user data while caching. Its necessary because permissions are come from backend via api based on user. For example, the getCurrentUser API returns a list of permissions specific to the logged-in user. If this response is cached and a different user logs in, their permissions could overwrite the previous user's data. Later, if the first user tries to use the app offline, they might see incorrect permissions or data from the second user. +so to ovrcome this problem we have to cache data per user. we will discussed its solution in approache's section. # Now Lets Discussed Approches to achive offline Support : - The approaches discussed here focus primarily on caching API responses. Our UI shell is already cached by default since CARE is a PWA. Below are some of the approaches we can use to achieve offline functionality for the workflows we’ve discussed. + +The approaches discussed here focus primarily on caching API responses. Our UI shell is already cached by default since CARE is a PWA. Below are some of the approaches we can use to achieve offline functionality for the workflows we’ve discussed. --- ### 1. Using TanStack Query Cache with Persistence + This approach plays around using TanStack Query to cache the API responses and sync write operations once the app comes back online. Persistence, in this context, here means to store the cached data into some local DB (e.g., IndexedDB). Since CARE already uses TanStack Query for fetch and write operations, integrating persistence and offline syncing becomes relatively straightforward. This approach leverages existing infrastructure, reducing the need for additional caching logic or custom data handling. Now let's discuss what configuration needs to be implemented for this approach. #### 1. Persister: + It is the mechanism that defines how and where the data is saved/restored. TanStack provides some persisters like `createSyncStoragePersister` for mainly localStorage and `createAsyncStoragePersister` for AsyncStorage. It also provides a method to create a custom persister. We will create a custom persister for IndexedDB(via dexie.js) for our use case. - We just need to create a createDbPersister() function that returns our custom persister. Inside this function, we define a single cache key under which the entire React Query cache will be stored. The function should implement three methods — `persistClient()`, `restoreClient()`, and `removeClient()` — following the rules to create a custom persister for React Query. +We just need to create a createDbPersister() function that returns our custom persister. Inside this function, we define a single cache key under which the entire React Query cache will be stored. The function should implement three methods — `persistClient()`, `restoreClient()`, and `removeClient()` — following the rules to create a custom persister for React Query. When online, data fetched using useQuery is stored in the in-memory cache and also persisted to IndexedDB via this custom persister. When offline, the cached data stored in IndexedDB (using Dexie.js) is automatically hydrated back into the in-memory cache. TanStack Query then manages the cache seamlessly, providing offline support without extra effort. -#### 2. cacheTime(or gcTime) and maxage: -For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cacheTime` should be set as the same value or higher than `persistQueryClient`'s `maxAge` option. +#### 2. cacheTime(or gcTime) and maxage: + +For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cacheTime` should be set as the same value or higher than `persistQueryClient`'s `maxAge` option. -- `cacheTime` defines how long inactive cached data stays in memory before being garbage collected. This means the data remains available for queries without refetching during this time, even if not actively used. +- `cacheTime` defines how long inactive cached data stays in memory before being garbage collected. This means the data remains available for queries without refetching during this time, even if not actively used. - `maxAge` (used in persistQueryClient) determines how long the persisted cache data remains valid and can be restored from storage. #### 4. PersistQueryClientProvider: + `PersistQueryClientProvider` is a React wrapper around our normal `QueryClientProvider` that automatically restores persisted cache on mount and keeps it in sync through subscribe/unsubscribe. It prevents our queries from fetching until the cache is hydrated, ensuring a smooth offline‑first experience. - **lets discuss how to overcome the problem of cache mixing when multiple user use same device :** TanStack Query caches all API data fetched via useQuery. To avoid mixing cached data between different users, include the userId as part of the queryKey along with other parameters. Since TanStack Query stores cache data based on the queryKey (not just the URL), adding userId creates distinct cache entries even if the API endpoint (URL) is the same. This ensures each user’s data is cached separately. - (Unlike Workbox, which caches based on URL, TanStack Query relies on the uniqueness of the query key.) - - **Note:** To avoid unnecessary refetch attempts when offline, configure useQuery with enabled: isOnline === true. This way, during online mode, staleTime can remain 0 to always fetch fresh data. When offline, the query is disabled (enabled: false), so cached data is used without triggering refetches that would fail. - This approach keeps the default online behavior unchanged while ensuring smooth offline caching without forced stale data refetches. +**lets discuss how to overcome the problem of cache mixing when multiple user use same device :** TanStack Query caches all API data fetched via useQuery. To avoid mixing cached data between different users, include the userId as part of the queryKey along with other parameters. Since TanStack Query stores cache data based on the queryKey (not just the URL), adding userId creates distinct cache entries even if the API endpoint (URL) is the same. This ensures each user’s data is cached separately. +(Unlike Workbox, which caches based on URL, TanStack Query relies on the uniqueness of the query key.) + +**Note:** To avoid unnecessary refetch attempts when offline, configure useQuery with enabled: isOnline === true. This way, during online mode, staleTime can remain 0 to always fetch fresh data. When offline, the query is disabled (enabled: false), so cached data is used without triggering refetches that would fail. +This approach keeps the default online behavior unchanged while ensuring smooth offline caching without forced stale data refetches. + +Main advantages of this approches is : + +- It will cache data coming from api that was fetch using useQuery irrespective of operation we use(eg.GET,POST).But Approches like workbox does not provide such option's they will cache only get api responses not read only post responses. +- As Care is already using tanstack query it become easy to implement this approch in the CARE. we dont have to write +- Give option to separate cached data based on user by using just single userid in query key. + +**Note** : write and sync logic will going to be same in other approch as well so we will discussed it later in the doc. The focus is on the caching of api responses using different-different approaches. + +### 2. Using Workbased approach : + +In the Workbox-based approach, Workbox caches API responses based on network requests. It stores the API data using the request URL as the key. During offline access, when the same URL is used to fetch data, Workbox returns the cached response. In this approach, we typically write our service worker code in a separate file, outside the React application code. +Now let's discuss what configuration needs to be implemented for this approach. + +#### 1. Caching Strategy + +Workbox offers multiple caching strategies depending on the freshness and criticality of the API data. The most relevant ones are: + +- **StaleWhileRevalidate**: Returns cached data immediately, then fetches new data in the background. Useful for fast loads with eventual consistency. +- **NetworkFirst**: Tries to fetch fresh data first, falls back to cache if offline. Ideal for dynamic or frequently updated data. +- **CacheFirst**: Serves cached data if available, fetches from the network only if not cached. Best for static or rarely changing data. + +#### 2. Custom Service Worker + +A custom service worker file must be created to define how caching works. In this file, we register routes and attach caching strategies to them using Workbox’s utilities like `registerRoute`, `StaleWhileRevalidate`, or `NetworkFirst`. For API caching: + +- we typically match all API requests to a specific domain or pattern (e.g., https://api.care.org/). +- Responses are cached using a named cache (api-cache) and configured with expiration rules. +- Plugins like `ExpirationPlugin` and `CacheableResponsePlugin` help manage cache size and validity based on status codes and TTLs. + +#### 3.Cache Invalidation and Expiration + +To prevent the cache from growing indefinitely or serving outdated data, Workbox provides: + +- `ExpirationPlugin`: Limits the number of cached entries and the maximum time they remain valid. +- `CacheableResponsePlugin`: Filters which responses should be cached (e.g., only HTTP 200 responses). +- These plugins are essential for maintaining a healthy cache lifecycle, especially for API data that may become stale over time. + +**Avoiding Cache Mixing Between Multiple Users**: Since Workbox caches data based on request URLs (not request bodies or headers), it's important to prevent data leakage between users. We can override Workbox's default behavior using a plugin like cacheKeyWillBeUsed. For example, we can fetch the userId from the authentication token and prepend it to the API URL during caching. This way, the modified URL becomes user-specific, allowing Workbox to distinguish and store data separately for each user. As a result, cache data is isolated per user, effectively preventing mixing or accidental data exposure. + +So far we have discussed about different approaches for caching. Now come to write and sync part. it will be going to same for approches. + +### write and sync operation logic : + +To enable offline support for write operations, we implement a system that temporarily stores unsent writes in the browser using Dexie.js (IndexedDB) and later syncs them to the server once the device regains connectivity. This approach ensures that user actions like form submissions are never lost and are reliably retried when online. + +#### Dexie Table for Offline Writes - Main advantages of this approches is : - - It will cache data coming from api that was fetch using useQuery irrespective of operation we use(eg.GET,POST).But Approches like workbox does not provide such option's they will cache only get api responses not read only post responses. - - As Care is already using tanstack query it become easy to implement this approch in the CARE. we dont have to write - - Give option to separate cached data based on user by using just single userid in query key. +We create a Dexie-backed IndexedDB table called offlineWrites, which acts as a local queue for pending write operations. Each entry includes important metadata such as: - #### sync and write operation in this approah : +- `userId`: Identifies the user to isolate caches and avoid data mixing. +- `syncrouteKey`: A key corresponding to the mutation route or function (e.g., "updatePatient"). It is used to dynamically identify and invoke the exact mutation function during sync. +- `payload`: The form data or mutation body that needs to be submitted. +- `pathParams`: Route parameters (like IDs) required to correctly construct the mutation request path or API call. +- `queryrouteKey` and `queryParams`: These identify the read query associated with the mutated resource. They help fetch the latest server data and update the local cache accordingly. +- `serverTimestamp`: The last known modification timestamp from the server at the time of the offline write. This is crucial for detecting conflicts before syncing. +- Additional fields: `syncStatus`, `retries`, `lastError`, etc., to manage syncing states and error handling. +#### Conditional Mutation Handling Based on Connectivity +- When offline, submitting a form triggers saving the mutation data to the Dexie table instead of performing the actual network mutation. +- When online, the mutation function is invoked directly using TanStack Query’s `useMutation` hook. +**Using syncrouteKey and pathParams to Replay Mutations** +During sync, the system uses the stored syncrouteKey to identify the exact mutation function to call. The pathParams are used to reconstruct the mutation endpoint or parameters, ensuring the request matches what would have been sent originally. - +**For example:** If syncrouteKey is "updatePatient", the sync logic calls the corresponding mutation function in CARE’s existing codebase, passing payload and pathParams. This dynamic approach leverages the existing mutation infrastructure without requiring code duplication or special-case logic. +**Using queryrouteKey and queryParams for Cache Updates and Conflict Detection**: The stored queryrouteKey and queryParams point to the query that fetches the latest resource state. +This allows the sync process to: +- Retrieve the current server state and timestamp. +- Compare it with the stored serverTimestamp to detect any conflicts (e.g., data updated elsewhere). +- Update the local TanStack Query cache after a successful sync by invalidating or refetching this query. +This reuse of existing query keys ensures consistent cache updates and helps maintain cache integrity without manual cache manipulation. From f2aefdbafb5c23e52b2ff0fee86c6e5ecc6257cf Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Sun, 1 Jun 2025 18:28:36 +0530 Subject: [PATCH 09/15] Conclude the final CEP for offline support --- ...fline-support-cep.md => AdditionalInfo.md} | 55 +- .../offline-support-project/OfflineSupport.md | 501 ++++++++++++++++++ 2 files changed, 514 insertions(+), 42 deletions(-) rename docs/care/offline-support-project/{offline-support-cep.md => AdditionalInfo.md} (91%) create mode 100644 docs/care/offline-support-project/OfflineSupport.md diff --git a/docs/care/offline-support-project/offline-support-cep.md b/docs/care/offline-support-project/AdditionalInfo.md similarity index 91% rename from docs/care/offline-support-project/offline-support-cep.md rename to docs/care/offline-support-project/AdditionalInfo.md index b2a71b3..346e501 100644 --- a/docs/care/offline-support-project/offline-support-cep.md +++ b/docs/care/offline-support-project/AdditionalInfo.md @@ -1,6 +1,6 @@ # CEP for offline support project -This document lists the standard workflows that will be supported offline and how we can achive this. +This document lists the standard workflows that will be supported offline and how we can achive this.it aslo contain theoritical info about alternative to achive offline functionality. Each workflow has an associated markdown table that defines the structure, HTTP method, and other important information about the API endpoints involved. In the tables, backend API endpoints are clearly distinguished based on whether they will be **cached** or **stored and later synced via IndexedDB (for write operations).** @@ -146,59 +146,27 @@ It include View, create, and manage appointments for a facility. Before implementing offline support, it’s essential to define how the application determines internet connectivity and how it behaves in various online/offline scenarios. This document outlines the design assumptions and expected behavior around connection status handling. -We use a real server connectivity check instead of navigator.onLine. This involves pinging a known backend endpoint (e.g., /ping) and marking the user as offline if the server is unreachable. - -We maintain a global state variable isOnline to represent the app’s understanding of the user’s connectivity. This value can be: - -- set based on real server checks during page reload and websites open . -- Manually overridden by the user via a toggle. - -### Offline Mode Toggle Button - -The app provides an "Enable Offline Mode" toggle button in the UI.This gives users control when internet conditions are unstable, allowing them to continue working in offline mode without interruptions. - -we Use an Explicit `isOnline` State Instead of Just Runtime Checks because Automatic checks can flicker rapidly when internet connectivity is poor (e.g., intermittent mobile data). This can cause unpredictable app behavior, like failed fetches or UI state inconsistencies. Manual offline mode offers a **clear and consistent user experience**. Users understand when they're offline and what features are expected to work - -- Enabled = isOnline = false: User manually forces the app into offline mode to prevent unstable behavior during intermittent connectivity. -- Disabled = isOnline = true: The app will attempt to connect to the server normally. ### Behavior on App Load and Connectivity Events -#### 1. App Launch (Fresh Tab Open) -- On opening the app, `checkRealServer()` is called. -- If the server is reachable → `isOnline = true` or If unreachable → `isOnline = false`.. -- Ensures `isOnline` reflects real connectivity at startup. +#### 1. Login Attempt During Poor Connectivity + If user is explicity logout or session expired , we will removed all cached data also at that instant. it will increase security and also handle the case where one user cached data overiding other. so user cannot able to login and hence cannot acess website offline if user explicity logout or session expired. --- -#### 2. Login Attempt During Poor Connectivity - -- If the user is **not logged in** and connectivity is poor,then Login may fail: -- App offers **fallback login** using last cached profile (if available). -- It Allows access even when login fails due to network issues. - ---- - -#### 3. User Goes Offline During Active Session +#### 2. User Goes Offline During Active Session - If already logged in and internet becomes unstable: -- User can toggle **offline mode manually** → `isOnline = false`. -- Enables offline features (e.g., cached reads, local writes). +- Enables offline features (e.g., cached reads, local writes). --- -#### 4. Page Reload or Tab Reopen - -- On reload or reopening , `checkRealServer()` runs again and Updates `isOnline` state.: -- Notifies user of current connection status. -- Even if user previously forced offline mode, connection is revalidated. - ### Role based caching , when multiple users access the app on the same device - As Care have role based access control, we have to ensure that if multiple user access the app on same device then their data of one user does not override the other user data while caching. Its necessary because permissions are come from backend via api based on user. For example, the getCurrentUser API returns a list of permissions specific to the logged-in user. If this response is cached and a different user logs in, their permissions could overwrite the previous user's data. Later, if the first user tries to use the app offline, they might see incorrect permissions or data from the second user. -so to ovrcome this problem we have to cache data per user. we will discussed its solution in approache's section. +so to overcome this problem we have to cache data per user. we will discussed its solution in approache's section. # Now Lets Discussed Approches to achive offline Support : @@ -229,17 +197,16 @@ For persist to work properly, we have to pass `QueryClient` a `cacheTime`. `cach `PersistQueryClientProvider` is a React wrapper around our normal `QueryClientProvider` that automatically restores persisted cache on mount and keeps it in sync through subscribe/unsubscribe. It prevents our queries from fetching until the cache is hydrated, ensuring a smooth offline‑first experience. -**lets discuss how to overcome the problem of cache mixing when multiple user use same device :** TanStack Query caches all API data fetched via useQuery. To avoid mixing cached data between different users, include the userId as part of the queryKey along with other parameters. Since TanStack Query stores cache data based on the queryKey (not just the URL), adding userId creates distinct cache entries even if the API endpoint (URL) is the same. This ensures each user’s data is cached separately. -(Unlike Workbox, which caches based on URL, TanStack Query relies on the uniqueness of the query key.) +**lets discuss how to overcome the problem of cache mixing when multiple user use same device :** TanStack Query caches all API data fetched via useQuery. To avoid mixing cached data between different users, we always cleared out cached data whenever user logout or session expired. it help to prevent data mixing as we are clearing previous user data. But here there is one point that to be come , which is after logout if we go offline and try to access website then it will not accessible offline as we already cleared out cache data during logout and session expired. -**Note:** To avoid unnecessary refetch attempts when offline, configure useQuery with enabled: isOnline === true. This way, during online mode, staleTime can remain 0 to always fetch fresh data. When offline, the query is disabled (enabled: false), so cached data is used without triggering refetches that would fail. +**Note:** To avoid unnecessary refetch attempts when offline, configure useQuery with enabled: navigator.online === true. This way, during online mode, staleTime can remain 0 to always fetch fresh data. When offline, the query is disabled (enabled: false), so cached data is used without triggering refetches that would fail. This approach keeps the default online behavior unchanged while ensuring smooth offline caching without forced stale data refetches. Main advantages of this approches is : - It will cache data coming from api that was fetch using useQuery irrespective of operation we use(eg.GET,POST).But Approches like workbox does not provide such option's they will cache only get api responses not read only post responses. - As Care is already using tanstack query it become easy to implement this approch in the CARE. we dont have to write -- Give option to separate cached data based on user by using just single userid in query key. + **Note** : write and sync logic will going to be same in other approch as well so we will discussed it later in the doc. The focus is on the caching of api responses using different-different approaches. @@ -311,3 +278,7 @@ This allows the sync process to: - Update the local TanStack Query cache after a successful sync by invalidating or refetching this query. This reuse of existing query keys ensures consistent cache updates and helps maintain cache integrity without manual cache manipulation. + + + + diff --git a/docs/care/offline-support-project/OfflineSupport.md b/docs/care/offline-support-project/OfflineSupport.md new file mode 100644 index 0000000..6bc29c8 --- /dev/null +++ b/docs/care/offline-support-project/OfflineSupport.md @@ -0,0 +1,501 @@ +# Care Enhancement Proposal (CEP): Offline Support Implementation + +## Overview + +This proposal details the implementation of offline capabilities in CARE using **TanStack Query with IndexedDB persistence**. The solution enables healthcare workers to perform critical operations during internet outages with automatic synchronization when connectivity is restored. + +We evaluated multiple methods to achieve this functionality. Information about these alternative approaches is documented in: +[Additional Approaches Audit](./docs/care/offline-support-project/OfflineSupport.md) + +The current proposal focuses specifically on our selected production-ready solution. Comprehensive implementation details for the chosen approach will be maintained in this document. + +The implementation will focus on supporting specific critical workflows that require offline functionality. All workflows designated for offline support are documented in: [Additional Info Audit](./docs/care/offline-support-project/OfflineSupport.md) + +## Implementation Phases + +1. **[Phase 1: Caching ](#phase-1-caching)** +2. **[Phase 2: Offline Writes](#phase-2-offline-writes)** +3. **[Phase 3: Synchronization](#phase-3-synchronization)** +4. **[Phase 4: Notifications](#phase-4-notifications)** + +### Phase 1: Caching + +Phase 1 will include adding caching logic in Care for the workflows(mention in [Additional Info Audit](./docs/care/offline-support-project/OfflineSupport.md)) so that data will be cached locally and avaible when ofline. + +### Scope + +- **Primary Focus**: API response caching for offline-supported workflows +- **Secondary**: Asset caching (handled automatically via PWA capabilities) +- **Selective Caching**: Only endpoints marked with `meta.persist` will be cached + +**Note**: Configuration details needed for caching using tanstack query are available in [Additional Approaches Audit](./docs/care/offline-support-project/OfflineSupport.md). This section focuses on their technical implementation. + +### Technical Design: + +#### 1. Persistence Implementation + +We will implement a custom persister using **IndexedDB** for local data storage, with **Dexie.js** as our wrapper library for simplified IndexedDB operations. + +**Database Schema Definition**: + +```typescript +export class AppCacheDB extends Dexie { + queryCache!: Dexie.Table< + { + cacheKey: string; + data: unknown; + timestamp: number; + }, + string + >; + + constructor() { + super("AppQueryCache"); + this.version(1).stores({ + queryCache: "cacheKey, timestamp", + }); + } +} +``` + +**createUserPersister funtion** : It create a custom persister function that handles IndexedDB operations via Dexie.js. It will passed in persistOptions in the `PersistQueryClientProvider`. This persister will implement three core methods required by TanStack Query: + +- `persistClient`: Saves the current query cache state +- `restoreClient`: Retrieves cached data when offline +- `removeClient`: Clears cached data (e.g., on logout) + +```typescript +export const createUserPersister = () => { + const db = new AppCacheDB(); + const CACHE_KEY = `REACT_QUERY`; + + return { + async persistClient(client: unknown) { + await db.queryCache.put({ + cacheKey: CACHE_KEY, + data: client, + timestamp: Date.now(), + }); + }, + + async restoreClient(): Promise { + try { + const entry = await db.queryCache.get(CACHE_KEY); + return entry?.data as PersistedClient; + } catch { + return undefined; + } + }, + + async removeClient() { + await db.queryCache.delete(CACHE_KEY); + }, + } satisfies PersistQueryClientOptions["persister"]; +}; +``` + +**PersistQueryClientProvider**: We'll wrap our application with the `PersistQueryClientProvider` to enable offline persistence. This requires: + +1. QueryClient Configuration: + +```typescript +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 24 * 60 * 60 * 1000, + }, + }, +}); +``` + +2. PersistQueryClientProvider + +```typescript + + Boolean(query.meta?.persist) + } + }} + }} + > + +``` + +#### Configuration Notes: + +- `gcTime` (Garbage Collection Time) Duration queries stay cached in memory (default: 5 minutes). It will set using `env` . + +- `maxAge` How long persisted data remains valid in storage. It Must be ≤ `gcTime` .It will set using `env`. + +- `shouldDehydrateQuery: (query) => + Boolean(query.meta?.persist)` Filters only those queries which have meta.persist `true`.It ensure only those query will store locally which we want . Its because we have to cache data for specfic workflows only. + +**useQuery configuration**: In all the `usequery` of the discussed workflows will be updated with `networkMode: "online"` and `meta: {persist: true}` in their config. + +- `networkMode: "online"` ensures the query will execute only when online. When offline, the query will not execute and cached data will be shown. + + > **Note**: Tanstack use their Onlinemanger.online() for network status which sometime can give false positive's. for eg . wifi but no internet access + +- `meta: { persist: true }` helps to dehydrate only those APIs that have `persist: true`, preventing excessive storage usage. + +API data is cached per `queryKey`. When fetching fresh data with the same `queryKey`, the cached data will automatically update with the new response in both: + +- In-memory cache +- Persistent local storage (IndexedDB) + +```typescript +const { data: user, isLoading } = useQuery({ + queryKey: ["currentUser", accessToken], + queryFn: query(routes.currentUser, { silent: true }), + networkMode: "online", + retry: false, + enabled: !!localStorage.getItem(LocalStorageKeys.accessToken), + meta: { persist: true }, +}); +``` + +#### Important Cache Behavior Notes + +- **`User Session Isolation`** + + - Cache data is cleared during logout/session expiration + - Prevents one user's data from mixing with another's + - Consequence: Offline access unavailable after logout + +- **`Dynamic Data Availability`** + - Only data fetched while online is cached for offline use + - Example: + - Patient1 (accessed online) → Available offline + - Patient2 (not accessed) → Unavailable offline + +### Phase 2: Offline Writes + +This phase implements saving form data to IndexedDB (via Dexie.js) when offline, for later synchronization when connectivity returns. + +**Implementation Flow**: + +1. On form submission, check network status +2. If offline: + - Save form data to IndexedDB + - Queue for later synchronization +3. If online: + - Process normally via `useMutation` + +#### 1. Dexie.js Table Schema + +so first see the table schema and then we will discusse about each entrie of the table schema. we will use same indexdDB database instant we use for caching. W just create another Table in it for offline writes. + +```ts +offlineWrites!: Dexie.Table< + { + id: string; + userId: string; + syncrouteKey: string; + resourceType?: string; + pathParams?: Record; + payload: unknown; + clientTimestamp: number; + serverTimestamp?: string; + lastAttemptAt?: number; + syncStatus: "pending" | "success" | "failed" | "conflict"; + lastError?: string; + retries?: number; + conflictData?: unknown; + queryrouteKey?: string; + queryParams?: Record; + }, + string + >; + constructor() { + this.version(2).stores({ + queryCache: "cacheKey, timestamp", + offlineWrites: "id, userId, timestamp", + }); + } +``` + +Below is a concise description of each field in the offlineWrites Dexie table :- + +1. **`id: string`** :- Unique identifier for this offline‑write record (usually a UUID). +2. **`userId: string`** :- The ID of the current user (e.g. `user.external_id`) who initiated this write. Help to prevent syncing data of one user by another user if there is same device. +3. **`syncrouteKey: string`** :- Key/name of the mutation route to replay when syncing (e.g. `"updatePatient"`), allowing reuse of existing mutation functions. +4. **`resourceType?: string`** :- Human‑readable tag for the type of resource being written (e.g. `"patient"`, `"form"`), useful for grouping or logging. +5. **`pathParams?: Record`** :- Route parameters (e.g. `{ id: patientId }`) required by the `syncrouteKey` mutation, so the same API function signature can be called offline. +6. **`payload: unknown`** :- The actual data object (e.g. updated form fields) that will be sent to the server when back online. +7. **`clientTimestamp: number`** :- Timestamp (e.g. `Date.now()`) when the user saved this record offline—used to order writes or detect staleness. +8. **`serverTimestamp?: string`** :- Optional server‑provided timestamp of the last known server version (e.g. `patientQuery.data.modified_date`) for conflict detection at frontend level. +9. **`lastAttemptAt?: number`** :- Timestamp of the last time you tried syncing this record—used to implement retry/backoff logic. +10. **`syncStatus: "pending" | "success" | "failed" | "conflict"`** :- Current sync state: + - `"pending"` = not yet retried + - `"success"` = synced successfully + - `"failed"` = last sync attempt errored + - `"conflict"` = server data has diverged from local payload +11. **`lastError?: string`** :- Error message (e.g. HTTP status or validation error) from the most recent sync attempt, for debugging or user notification. +12. **`retries?: number`** :- Number of times this record has been retried so far—used to limit retry attempts or escalate conflicts. +13. **`conflictData?: unknown`** :- If a conflict is detected (e.g. server has newer data), this holds the server’s latest version so you can show a merge UI. +14. **`queryrouteKey?: string`** :- Key/name of the “fetch” route (e.g. `"getPatient"`) that can be called before syncing, in order to retrieve the current server state for conflict checks. +15. **`queryParams?: Record`** :- Route parameters (e.g. `{ id: patientId }`) required by `queryrouteKey` to fetch the current form/patient data before submitting the offline write. + +#### 2. **SaveofflineWrites** function + +We have a generalized `saveOfflineWrites` function that should be called when submitting form data while offline. We will pass all necessary information to it so that it can save the data locally in IndexedDB. + +```ts +export const saveOfflineWrite = async ({ + userId, + syncrouteKey, + payload, + pathParams, + resourceType, + serverTimestamp, + queryParams, + queryrouteKey, +}: SaveOfflineWriteParams) => { + const writeEntry = { + id: uuidv4(), + userId, + syncrouteKey, + payload, + pathParams, + resourceType, + clientTimestamp: Date.now(), + serverTimestamp, + syncStatus: "pending" as const, + retries: 0, + queryParams, + queryrouteKey, + }; + + try { + await db.offlineWrites.add(writeEntry); + console.log("Offline write saved successfully:", writeEntry); + } catch (error) { + console.error(" Failed to save offline write:", error); + } +}; +``` + +> Note : Here again a critical point that is using navigator.online or tanstack network provider for checking network status, both can sometime giving wrong network status value in some cases. + +### Phase 3: Synchronization + +Now this is one of the important phase of offline support functionality. Here we will synchronize the data that we save during offline. so for that we need a sync manage . A production‑grade sync manager should include: + +#### 1. fetching pending writes (`getPendingWrites`) +- Use `getPendingAndRetryableWrites(userId)` to retrieve all records where syncStatus is either "pending" or "failed" (with retries < MAX_RETRIES). + +```ts +export async function getPendingAndRetryableWrites( + userId: string +): Promise { + return db.offlineWrites + .where("userId") + .equals(userId) + .and((w) => { + const isPending = w.syncStatus === "pending"; + const isFailedButRetryable = + w.syncStatus === "failed" && (w.retries || 0) < MAX_RETRIES; + return isPending || isFailedButRetryable; + }) + .toArray(); +} +``` + +#### 2. Processing each write (`syncOneWrite`) with proper error/409 handling and Retry/backoff logic (`shouldRetry`, `computeBackoffDelay`, scheduleRetry). +- For every item in that list, call `syncOneWrite(write)`, which: + Runs conflict detection first (`detectAndMarkConflict`). If the server’s `modified_date` differs from the cached `serverTimestamp`, it immediately marks that record as "conflict" and skips any further mutation. +- If no `conflict`, attempts the API mutation `(mutate(route, payload))`. +- On success, updates syncStatus = "success" and timestamps the write. +  • On error: If HTTP 409, marks `syncStatus = "conflict"` and stores the server’s data in conflictData. Otherwise, marks syncStatus = "failed", increments retries, saves the error message, and—if `retries < MAX_RETRIES—schedules` a retry via `computeBackoffDelay(retries) + scheduleRetry`. + +```ts +export async function syncOneWrite(write: OfflineWriteRecord): Promise { + const now = Date.now(); + + // 1) Detect (and mark) conflict up front + const didConflict = await detectAndMarkConflict(write); + if (didConflict) { + return; + } + + try { + // 2) Execute the mutation route + const route = offlineRoutes[write.syncrouteKey as OfflineRouteKey]; + const mutationFn = mutate(route, { pathParams: write.pathParams as any }); + await mutationFn(write.payload); + + // 3) On success, mark as synced + await db.offlineWrites.update(write.id, { + syncStatus: "success", + lastAttemptAt: now, + }); + + toast.success(`Sync succeeded for write ${write.id}`); + } catch (error: any) { + const nextRetries = (write.retries || 0) + 1; + const isConflict = error?.response?.status === 409; + + const updateFields: Partial = { + lastAttemptAt: now, + retries: nextRetries, + lastError: error?.message || "Unknown error", + }; + + if (isConflict) { + // 4.a) On HTTP 409, mark as conflict + updateFields.syncStatus = "conflict"; + updateFields.conflictData = error.response?.data; + } else { + // 4.b) On other errors, mark as failed + updateFields.syncStatus = "failed"; + } + + await db.offlineWrites.update(write.id, updateFields); + toast.error(`Sync failed for write ${write.id}: ${updateFields.lastError}`); + + // 5) Retry/backoff logic (only for non-conflict) + if (!isConflict && shouldRetry(nextRetries)) { + const delay = computeBackoffDelay(nextRetries); + scheduleRetry(write.userId, delay); + } + } +} + +export function shouldRetry(currentRetries: number): boolean { + return currentRetries < MAX_RETRIES; +} + +export function computeBackoffDelay(attempt: number): number { + return BASE_BACKOFF_MS * 2 ** (attempt - 1); +} + +export function scheduleRetry(userId: string, delayMs: number): void { + setTimeout(() => { + processSyncQueue(userId); + }, delayMs); +} +``` + +#### 3. Conflict detection & resolution (`detectAndMarkConflict`) :- If server’s modified_date has changed since we cached it, that’s a conflict. +- `detectAndMarkConflict`(write) fetches the current server record (using queryrouteKey/queryParams) and compares `serverData.modified_date` with `write.serverTimestamp`. +- If they differ, it updates that write’s `syncStatus = "conflict"` and saves conflictData so the UI can prompt the user. +```ts +async function detectAndMarkConflict( + write: OfflineWriteRecord +): Promise { + if (!write.queryrouteKey || !write.queryParams) { + return false; + } + + try { + const fetchFn = query( + offlineRoutes[write.queryrouteKey as OfflineRouteKey] + ); + const serverData = await fetchFn({ pathParams: write.queryParams as any }); + + if (serverData.modified_date !== write.serverTimestamp) { + await db.offlineWrites.update(write.id, { + syncStatus: "conflict", + conflictData: serverData, + lastAttemptAt: Date.now(), + }); + return true; + } + } catch (err) { + console.warn(`detectAndMarkConflict: failed for write ${write.id}`, err); + } + + return false; +} +``` + +#### 4. Queue orchestration (`processSyncQueue`) triggered by network or timed events. +- `processSyncQueue(userId)` is triggered whenever the browser fires "online" (via onNetworkStatusChange) . +- Each pass fetches the current pending/retryable writes and calls syncOneWrite on each in series. +```ts +export async function processSyncQueue(userId: string): Promise { + if (!navigator.onLine || !userId) return; + + const writesToProcess = await getPendingAndRetryableWrites(userId); + + for (const write of writesToProcess) { + await syncOneWrite(write); + } +} +``` + +#### 5. cleanup (`cleanupSuccessfulWrites`). +- `cleanupSuccessfulWrites(userId, olderThanMs)` deletes any records where `syncStatus === "success"` and `clientTimestamp < Date.now() − olderThanMs` preventing the offline‐write table from growing indefinitely. + +```ts +export async function cleanupSuccessfulWrites( + userId: string, + olderThanMs: number +): Promise { + const cutoff = Date.now() - olderThanMs; + await db.offlineWrites + .where("userId") + .equals(userId) + .and((w) => w.syncStatus === "success" && w.clientTimestamp < cutoff) + .delete(); +} +``` + +#### 6. Event listeners (`onNetworkStatusChange`). +- `onNetworkStatusChange(userId)` listens for window.addEventListener("online") and calls processSyncQueue(userId) immediately when connectivity returns. +> Note : We can also have `periodicsync` that try o sync data after specific intervel. But for now we are just syncing on only on network status change. + +```ts +export function onNetworkStatusChange(userId: string): void { + window.addEventListener("online", () => { + processSyncQueue(userId); + }); +} +``` + +### Phase 4: Notifications + + + + + + + + + + +## Limitations and Known Issues + +Before discussing limitation and known Issue , let say go through how CARE work offline : + +- User logged-in and run website, Whenever user go throught he workflows for which offline functionality is added there API-data will caached and save locally +- now suppose user goes offline( during Active session), Now it will can access those pages and data that is present in cache or user previosly visit. +- now if user fill forms , it will be saved offline if navigator.online==false(means we are offline). +- Now when network status switch to online , syncing start and all our data will be sync to backend. +- user can see their conflict and faild data in notification part. + +Now lets take a look om limitation and known issue now : + + - **Reliance on navigator.onLine** : Browsers often report “online” when a device is connected to a local network but actually has no Internet access (e.g. captive portals or firewalls). This can cause the sync manager to attempt writes even though the server is unreachable + - **No Background Sync Outside Active Tab**: All synchronization occurs only while the app is open and the user is logged-in. If the user closes the tab or the device sleeps, pending writes remain unsynced until the app is re‐opened. +- **Conflict Resolution Assumes Timestamp Accuracy**: We rely on comparing serverData.modified_date to write.serverTimestamp. If the server’s clock drifts or the timestamp field is not updated reliably, conflicts may be missed or falsely detected. +- **No offline support after Log-out**: when a user logout , its cached data will be cleared and it cannot access website offline. But clearing cache during logout increase security and also prevent mixing of data of two or more user if their is same device. +- **No child offline write until parent offline write syncs**: In many cases, a user should not be able to save a child record offline if it depends on a parent record that has not yet been synced. For example, after saving an offline write to create a patient, you cannot save an offline write for an encounter of that patient until the “create-patient” write has successfully synced. +> Note : For some simple cases we can handle dependency enforcement of data during online. Simply means for those child write that not depend heavily on parent record. + + +## Implemenation Timeline and Milestone + +As project implementation is divided into four phases , we will complete phase 1-2 from week 1-6 (till mid evaluation) and phase 3-4 from week 7-12. + +| **Phase** | **Timeline** | **Key Deliverables** | +|-------------------------|-------------------|-------------------------------------------------------------------------------------| +| **Phase 1: Caching** | Week 1 – Week 3 | • Dexie schema & IndexedDB setup
• `PersistQueryClientProvider` integrated
• Selective caching (`meta.persist`) verified
• Implementation of all tech config mention in phase 1
• Offline reads tested | +| **Phase 2: Offline Writes**(mid-evaluation) | Week 4 – Week 6 | • `offlineWrites` table created
• `saveOfflineWrite` hooked into forms| +| **Phase 3: Synchronization** | Week 7 – Week 9 | • `syncOneWrite`, retry/backoff, and `computeBackoffDelay` complete
• `detectAndMarkConflict` implemented
• Implementation of all tech config mention in phase 3
• End-to-end sync (offline → online) tested | +| **Phase 4: Notifications** | Week 10 – Week 12 | • Conflict notification event/UI built
• `resolveConflict` helper added
•Optimization & Performance
•User Testing & Documentation From d8ab1e08030f6616745c3f21edaeaede5df9722d Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Tue, 22 Jul 2025 10:08:04 +0530 Subject: [PATCH 10/15] update the doc --- .../offline-support-project/OfflineSupport.md | 305 ++++++++---------- 1 file changed, 139 insertions(+), 166 deletions(-) diff --git a/docs/care/offline-support-project/OfflineSupport.md b/docs/care/offline-support-project/OfflineSupport.md index 6bc29c8..c86375f 100644 --- a/docs/care/offline-support-project/OfflineSupport.md +++ b/docs/care/offline-support-project/OfflineSupport.md @@ -5,11 +5,11 @@ This proposal details the implementation of offline capabilities in CARE using **TanStack Query with IndexedDB persistence**. The solution enables healthcare workers to perform critical operations during internet outages with automatic synchronization when connectivity is restored. We evaluated multiple methods to achieve this functionality. Information about these alternative approaches is documented in: -[Additional Approaches Audit](./docs/care/offline-support-project/OfflineSupport.md) +[Additional Approaches Audit](AdditionalInfo.md) The current proposal focuses specifically on our selected production-ready solution. Comprehensive implementation details for the chosen approach will be maintained in this document. -The implementation will focus on supporting specific critical workflows that require offline functionality. All workflows designated for offline support are documented in: [Additional Info Audit](./docs/care/offline-support-project/OfflineSupport.md) +The implementation will focus on supporting specific critical workflows that require offline functionality. All workflows designated for offline support are documented in: [Additional Info Audit](AdditionalInfo.md) ## Implementation Phases @@ -20,133 +20,65 @@ The implementation will focus on supporting specific critical workflows that req ### Phase 1: Caching -Phase 1 will include adding caching logic in Care for the workflows(mention in [Additional Info Audit](./docs/care/offline-support-project/OfflineSupport.md)) so that data will be cached locally and avaible when ofline. +#### What Are We Caching? -### Scope +- **API Responses:** We cache the results of API calls for workflows that require offline support (such as patient data, forms, and other critical resources). Only endpoints explicitly marked for offline use (`meta.persist: true`) are cached. +- **Selective Data:** Not all API responses are cached—only those relevant to offline workflows, as determined by the application's business logic and user needs. +- **Granularity:** Data is cached per `queryKey`, ensuring that only the specific data a user has accessed is available offline. For example, if a user views Patient A's record, only that record is cached for offline use. -- **Primary Focus**: API response caching for offline-supported workflows -- **Secondary**: Asset caching (handled automatically via PWA capabilities) -- **Selective Caching**: Only endpoints marked with `meta.persist` will be cached +#### Why Do We Need Caching? -**Note**: Configuration details needed for caching using tanstack query are available in [Additional Approaches Audit](./docs/care/offline-support-project/OfflineSupport.md). This section focuses on their technical implementation. +- **Offline Access:** Caching enables users to access previously viewed data even when there is no internet connection, ensuring continuity of care and workflow. +- **Performance:** By serving data from the local cache, the application reduces redundant network requests and improves data retrieval speed. +- **User Experience:** Users can continue to view and interact with critical information during connectivity interruptions, minimizing workflow disruptions. +- **Reliability:** Caching prevents data loss and enables seamless transitions between online and offline states, which is crucial for healthcare environments where connectivity may be unreliable. -### Technical Design: +**Note**: Configuration details needed for caching using tanstack query are +available in [Additional Approaches Audit](AdditionalInfo.md). This section focuses on their +technical implementation. -#### 1. Persistence Implementation +#### How Are We Caching? -We will implement a custom persister using **IndexedDB** for local data storage, with **Dexie.js** as our wrapper library for simplified IndexedDB operations. +- **Technology Stack:** + - **TanStack Query (React Query):** Used for data fetching, caching, and state management. + - **IndexedDB (via Dexie.js):** Provides persistent local storage for cached data, allowing it to survive page reloads and browser restarts. +- **Custom Persister:** + - A custom persister is implemented using Dexie.js to store and retrieve TanStack Query's cache from IndexedDB. + - The persister implements three core methods: `persistClient` (save cache), `restoreClient` (load cache), and `removeClient` (clear cache on logout). +- **Configuration:** + - Only queries with `meta.persist: true` are persisted to IndexedDB, ensuring that only essential data is stored. + - Cached data is stored with a configurable expiration (`maxAge`), and in-memory cache is managed with `gcTime`. + - The application is wrapped in `PersistQueryClientProvider` to enable persistence and manage cache lifecycle. +- **Cache Invalidation:** + - Cached data is cleared on logout or session expiration to prevent data leakage between users and ensure privacy. -**Database Schema Definition**: +#### How Does Caching Help in Offline Support? -```typescript -export class AppCacheDB extends Dexie { - queryCache!: Dexie.Table< - { - cacheKey: string; - data: unknown; - timestamp: number; - }, - string - >; - - constructor() { - super("AppQueryCache"); - this.version(1).stores({ - queryCache: "cacheKey, timestamp", - }); - } -} -``` - -**createUserPersister funtion** : It create a custom persister function that handles IndexedDB operations via Dexie.js. It will passed in persistOptions in the `PersistQueryClientProvider`. This persister will implement three core methods required by TanStack Query: - -- `persistClient`: Saves the current query cache state -- `restoreClient`: Retrieves cached data when offline -- `removeClient`: Clears cached data (e.g., on logout) - -```typescript -export const createUserPersister = () => { - const db = new AppCacheDB(); - const CACHE_KEY = `REACT_QUERY`; - - return { - async persistClient(client: unknown) { - await db.queryCache.put({ - cacheKey: CACHE_KEY, - data: client, - timestamp: Date.now(), - }); - }, - - async restoreClient(): Promise { - try { - const entry = await db.queryCache.get(CACHE_KEY); - return entry?.data as PersistedClient; - } catch { - return undefined; - } - }, - - async removeClient() { - await db.queryCache.delete(CACHE_KEY); - }, - } satisfies PersistQueryClientOptions["persister"]; -}; -``` - -**PersistQueryClientProvider**: We'll wrap our application with the `PersistQueryClientProvider` to enable offline persistence. This requires: - -1. QueryClient Configuration: - -```typescript -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - gcTime: 24 * 60 * 60 * 1000, - }, - }, -}); -``` - -2. PersistQueryClientProvider - -```typescript - - Boolean(query.meta?.persist) - } - }} - }} - > - -``` - -#### Configuration Notes: - -- `gcTime` (Garbage Collection Time) Duration queries stay cached in memory (default: 5 minutes). It will set using `env` . +- **Data Availability:** Users can access cached data for supported workflows even when offline, allowing them to continue working without interruption. +- **Seamless Experience:** When the app detects offline status, it serves data from the cache instead of making network requests, providing a smooth user experience. +- **Automatic Updates:** When connectivity is restored, fresh data is fetched from the server and the cache is updated automatically. +- **Security:** Cache is user-isolated and cleared on logout, ensuring that data from one user is not accessible to another. -- `maxAge` How long persisted data remains valid in storage. It Must be ≤ `gcTime` .It will set using `env`. +#### Practical Example -- `shouldDehydrateQuery: (query) => - Boolean(query.meta?.persist)` Filters only those queries which have meta.persist `true`.It ensure only those query will store locally which we want . Its because we have to cache data for specfic workflows only. +Suppose a healthcare worker accesses a patient's record while online. The data is fetched from the server and cached locally. If the worker later loses connectivity, they can still view the cached patient record. When connectivity returns, the app fetches the latest data and updates the cache. -**useQuery configuration**: In all the `usequery` of the discussed workflows will be updated with `networkMode: "online"` and `meta: {persist: true}` in their config. +#### Additional Details and Limitations -- `networkMode: "online"` ensures the query will execute only when online. When offline, the query will not execute and cached data will be shown. +- **Only Accessed Data is Cached:** Data must be accessed at least once while online to be available offline. New or unvisited records will not be available until accessed online. +- **Selective Caching:** Mark only essential queries with `meta.persist: true` to avoid excessive storage usage and ensure optimal performance. +- **Network Detection Caveats:** TanStack Query’s network detection may sometimes give false positives (e.g., connected to Wi-Fi without internet). This can affect when cached data is served versus when a network request is attempted. +- **Session Isolation:** Cache is cleared on logout to prevent data mixing between users, which is especially important on shared devices. - > **Note**: Tanstack use their Onlinemanger.online() for network status which sometime can give false positive's. for eg . wifi but no internet access - -- `meta: { persist: true }` helps to dehydrate only those APIs that have `persist: true`, preventing excessive storage usage. +#### Technical Implementation Summary -API data is cached per `queryKey`. When fetching fresh data with the same `queryKey`, the cached data will automatically update with the new response in both: - -- In-memory cache -- Persistent local storage (IndexedDB) +- **Dexie.js Schema:** + - A table named `queryCache` is used to store cached query data, keyed by a unique cache key and timestamped for expiration management. +- **PersistQueryClientProvider:** + - The app is wrapped in this provider, which manages the persistence and hydration of the query cache. +- **Query Configuration:** + - Queries that should be cached for offline use are configured with `meta: { persist: true }` and `networkMode: "online"`. + - Example: ```typescript const { data: user, isLoading } = useQuery({ @@ -159,19 +91,11 @@ const { data: user, isLoading } = useQuery({ }); ``` -#### Important Cache Behavior Notes - -- **`User Session Isolation`** +- **Cache Lifecycle:** + - Data is cached when fetched online, served from cache when offline, and updated when connectivity is restored. + - Cache is cleared on logout or session expiration. - - Cache data is cleared during logout/session expiration - - Prevents one user's data from mixing with another's - - Consequence: Offline access unavailable after logout - -- **`Dynamic Data Availability`** - - Only data fetched while online is cached for offline use - - Example: - - Patient1 (accessed online) → Available offline - - Patient2 (not accessed) → Unavailable offline +This caching strategy forms the foundation for robust offline support, ensuring that users have access to critical data regardless of network conditions, while maintaining security, performance, and data integrity. ### Phase 2: Offline Writes @@ -182,10 +106,47 @@ This phase implements saving form data to IndexedDB (via Dexie.js) when offline, 1. On form submission, check network status 2. If offline: - Save form data to IndexedDB + - **Normalize the form payload to match the API response structure** + - **Update the cache using setQueryData to reflect the new/updated record** + - **Update all relevant cached API responses that relate to this form (e.g., detail and list endpoints)** - Queue for later synchronization 3. If online: - Process normally via `useMutation` +#### Normalizing and Caching Offline Writes + +After saving a record offline, it is important to ensure the local cache reflects the new or updated data, so the user experience remains consistent and the UI shows the latest state—even while offline. This is achieved by: + +- **Normalizing the Payload:** The form data is transformed to match the structure of the API response (as if it had come from the backend). This ensures consistency between online and offline data representations. +- **Updating the Cache:** + - Use TanStack Query's `setQueryData` to update the cache for the relevant query key(s) with the normalized data. + - Update not only the detail endpoint (e.g., the specific record) but also any related list endpoints (e.g., the list of all encounters) to ensure the new or updated record appears in all relevant places in the UI. + +**Example: Creating an Encounter Offline** + +Suppose a user creates a new encounter while offline: + +1. The form payload is saved to IndexedDB as an offline write. +2. The payload is normalized to match the encounter API response structure (e.g., adding default fields, IDs, or computed values as needed). +3. The cache is updated: + - `setQueryData(["encounter", newEncounterId], normalizedEncounter)` updates the cache for the encounter detail endpoint. + - `setQueryData(["encounter-list", patientId], prevList => [...prevList, normalizedEncounter])` updates the cache for the encounter list endpoint, ensuring the new encounter appears in the list view. + +This approach ensures that after creating an encounter offline, the user immediately sees the new encounter in both the detail and list views, providing a seamless and consistent experience. + +#### Additional Details on Offline Write Handling + +- **Form Validation:** All forms are validated client-side before saving to IndexedDB. Users are notified immediately if any required fields are missing or invalid, ensuring only valid data is stored for later sync. +- **Temporary IDs:** When creating new records offline (such as a new encounter or patient), temporary UUIDs are generated. These IDs are used to maintain relationships between records (e.g., linking a new encounter to a new patient) until real IDs are assigned after successful sync with the backend. +- **Payload Normalization:** The form data is normalized to match the API response structure. This ensures that the UI can display the new or updated record as if it had come from the backend, maintaining consistency between online and offline states. +- **Immediate Cache Update:** After saving the offline write, the cache is updated using TanStack Query's `setQueryData` for both detail and list endpoints. This allows the UI to reflect the new or updated record instantly, even before it is synced to the backend. +- **Atomicity and Consistency:** The offline write and cache update are performed together to prevent UI inconsistencies. If the cache update fails, the system can roll back the IndexedDB write or notify the user of the issue. +- **Metadata Storage:** Each offline write includes metadata such as timestamp, operation type (create/update), and the target route. This metadata is useful for later synchronization, debugging, and providing UI indicators (such as "pending" badges). +- **Developer Extensibility:** Developers can hook into the offline write process to trigger custom logic, such as analytics, local notifications, or additional cache updates, making the system flexible and extensible. +- **Example UI Feedback:** + - "Your data has been saved locally and will be synced when you’re back online." + - "This record is pending sync." + #### 1. Dexie.js Table Schema so first see the table schema and then we will discusse about each entrie of the table schema. we will use same indexdDB database instant we use for caching. W just create another Table in it for offline writes. @@ -196,9 +157,12 @@ offlineWrites!: Dexie.Table< id: string; userId: string; syncrouteKey: string; + type?:string; resourceType?: string; pathParams?: Record; payload: unknown; + response?: unknown; + parentMutationIds?: string[]; clientTimestamp: number; serverTimestamp?: string; lastAttemptAt?: number; @@ -240,6 +204,10 @@ Below is a concise description of each field in the offlineWrites Dexie table :- 13. **`conflictData?: unknown`** :- If a conflict is detected (e.g. server has newer data), this holds the server’s latest version so you can show a merge UI. 14. **`queryrouteKey?: string`** :- Key/name of the “fetch” route (e.g. `"getPatient"`) that can be called before syncing, in order to retrieve the current server state for conflict checks. 15. **`queryParams?: Record`** :- Route parameters (e.g. `{ id: patientId }`) required by `queryrouteKey` to fetch the current form/patient data before submitting the offline write. +16. **`parentMutationIds`** :- array of parent mutations id It help us during syncing of parent-child dependent mutations. +17. **`response`** :- store response after successful sync. +18. **`type`** :- store the the type of mutation like 'createpatient,'updatepatient' + #### 2. **SaveofflineWrites** function @@ -251,17 +219,21 @@ export const saveOfflineWrite = async ({ syncrouteKey, payload, pathParams, + type, resourceType, serverTimestamp, queryParams, queryrouteKey, + parentMutationIds + dependentFields, }: SaveOfflineWriteParams) => { const writeEntry = { - id: uuidv4(), + id, userId, syncrouteKey, payload, pathParams, + type, resourceType, clientTimestamp: Date.now(), serverTimestamp, @@ -269,6 +241,8 @@ export const saveOfflineWrite = async ({ retries: 0, queryParams, queryrouteKey, + parentMutationIds, + dependentFields, }; try { @@ -287,6 +261,7 @@ export const saveOfflineWrite = async ({ Now this is one of the important phase of offline support functionality. Here we will synchronize the data that we save during offline. so for that we need a sync manage . A production‑grade sync manager should include: #### 1. fetching pending writes (`getPendingWrites`) + - Use `getPendingAndRetryableWrites(userId)` to retrieve all records where syncStatus is either "pending" or "failed" (with retries < MAX_RETRIES). ```ts @@ -307,11 +282,12 @@ export async function getPendingAndRetryableWrites( ``` #### 2. Processing each write (`syncOneWrite`) with proper error/409 handling and Retry/backoff logic (`shouldRetry`, `computeBackoffDelay`, scheduleRetry). + - For every item in that list, call `syncOneWrite(write)`, which: - Runs conflict detection first (`detectAndMarkConflict`). If the server’s `modified_date` differs from the cached `serverTimestamp`, it immediately marks that record as "conflict" and skips any further mutation. -- If no `conflict`, attempts the API mutation `(mutate(route, payload))`. + Runs conflict detection first (`detectAndMarkConflict`). If the server’s `modified_date` differs from the cached `serverTimestamp`, it immediately marks that record as "conflict" and skips any further mutation. +- If no `conflict`, attempts the API mutation `(mutate(route, payload))`. - On success, updates syncStatus = "success" and timestamps the write. -  • On error: If HTTP 409, marks `syncStatus = "conflict"` and stores the server’s data in conflictData. Otherwise, marks syncStatus = "failed", increments retries, saves the error message, and—if `retries < MAX_RETRIES—schedules` a retry via `computeBackoffDelay(retries) + scheduleRetry`. +   • On error: If HTTP 409, marks `syncStatus = "conflict"` and stores the server’s data in conflictData. Otherwise, marks syncStatus = "failed", increments retries, saves the error message, and—if `retries < MAX_RETRIES—schedules` a retry via `computeBackoffDelay(retries) + scheduleRetry`. ```ts export async function syncOneWrite(write: OfflineWriteRecord): Promise { @@ -382,8 +358,10 @@ export function scheduleRetry(userId: string, delayMs: number): void { ``` #### 3. Conflict detection & resolution (`detectAndMarkConflict`) :- If server’s modified_date has changed since we cached it, that’s a conflict. -- `detectAndMarkConflict`(write) fetches the current server record (using queryrouteKey/queryParams) and compares `serverData.modified_date` with `write.serverTimestamp`. -- If they differ, it updates that write’s `syncStatus = "conflict"` and saves conflictData so the UI can prompt the user. + +- `detectAndMarkConflict`(write) fetches the current server record (using queryrouteKey/queryParams) and compares `serverData.modified_date` with `write.serverTimestamp`. +- If they differ, it updates that write’s `syncStatus = "conflict"` and saves conflictData so the UI can prompt the user. + ```ts async function detectAndMarkConflict( write: OfflineWriteRecord @@ -415,8 +393,10 @@ async function detectAndMarkConflict( ``` #### 4. Queue orchestration (`processSyncQueue`) triggered by network or timed events. + - `processSyncQueue(userId)` is triggered whenever the browser fires "online" (via onNetworkStatusChange) . -- Each pass fetches the current pending/retryable writes and calls syncOneWrite on each in series. +- Each pass fetches the current pending/retryable writes and calls syncOneWrite on each in series. + ```ts export async function processSyncQueue(userId: string): Promise { if (!navigator.onLine || !userId) return; @@ -430,43 +410,38 @@ export async function processSyncQueue(userId: string): Promise { ``` #### 5. cleanup (`cleanupSuccessfulWrites`). -- `cleanupSuccessfulWrites(userId, olderThanMs)` deletes any records where `syncStatus === "success"` and `clientTimestamp < Date.now() − olderThanMs` preventing the offline‐write table from growing indefinitely. - -```ts -export async function cleanupSuccessfulWrites( - userId: string, - olderThanMs: number -): Promise { - const cutoff = Date.now() - olderThanMs; - await db.offlineWrites - .where("userId") - .equals(userId) - .and((w) => w.syncStatus === "success" && w.clientTimestamp < cutoff) - .delete(); -} -``` -#### 6. Event listeners (`onNetworkStatusChange`). -- `onNetworkStatusChange(userId)` listens for window.addEventListener("online") and calls processSyncQueue(userId) immediately when connectivity returns. -> Note : We can also have `periodicsync` that try o sync data after specific intervel. But for now we are just syncing on only on network status change. - -```ts -export function onNetworkStatusChange(userId: string): void { - window.addEventListener("online", () => { - processSyncQueue(userId); - }); -} -``` - -### Phase 4: Notifications +- `cleanupSuccessfulWrites(userId, olderThanMs)` deletes any records where ` +### Phase 4: Notifications (Sync Status Page) +The Notifications phase is implemented as a **Sync Status Page**—a dedicated interface where users can monitor and manage the status of all their offline writes and sync operations. +#### Purpose +- Provide a clear, centralized view of all data that is pending sync, has failed to sync, has conflicts, or has been successfully synced. +- Empower users to take action on failed or conflicted records directly from the page. +#### What the Sync Status Page Shows +- **Conflicts Queue:** List of records where a conflict was detected during sync. Each entry shows details of the conflict and provides an action to resolve it (e.g., open a merge dialog or accept/reject changes). +- **Failed Queue:** List of records that failed to sync due to network/server errors. Each entry includes error details and a "Retry" button to attempt sync again. +- **Success Queue:** (Optional) List or count of records that have been successfully synced, for audit or reassurance. +- **Pending Queue:** (Optional) Records that are saved locally and waiting for the next sync attempt. +#### Example UI Elements +- **Tables or Lists:** Each queue is displayed as a table or list with columns for record type, timestamp, status, and actions. +- **Badges:** Status badges (e.g., "conflict", "failed", "pending", "success") for quick visual identification. +- **Action Buttons:** + - **Retry:** Manually retry syncing a failed or conflicted record. + - **Resolve:** Open a conflict resolution dialog for conflicted records. + - **View Details:** Inspect the payload, error, or conflict data for any record. +#### User Actions +- Users can manually retry failed/conflicted syncs. +- Users can resolve conflicts through a dedicated UI. +- Users can view details of any record in the sync queues. +This Sync Status Page ensures transparency and gives users control over their offline data, making it easy to track, troubleshoot, and resolve sync issues as they arise. ## Limitations and Known Issues @@ -480,14 +455,13 @@ Before discussing limitation and known Issue , let say go through how CARE work - user can see their conflict and faild data in notification part. Now lets take a look om limitation and known issue now : - + - **Reliance on navigator.onLine** : Browsers often report “online” when a device is connected to a local network but actually has no Internet access (e.g. captive portals or firewalls). This can cause the sync manager to attempt writes even though the server is unreachable - **No Background Sync Outside Active Tab**: All synchronization occurs only while the app is open and the user is logged-in. If the user closes the tab or the device sleeps, pending writes remain unsynced until the app is re‐opened. - **Conflict Resolution Assumes Timestamp Accuracy**: We rely on comparing serverData.modified_date to write.serverTimestamp. If the server’s clock drifts or the timestamp field is not updated reliably, conflicts may be missed or falsely detected. - **No offline support after Log-out**: when a user logout , its cached data will be cleared and it cannot access website offline. But clearing cache during logout increase security and also prevent mixing of data of two or more user if their is same device. -- **No child offline write until parent offline write syncs**: In many cases, a user should not be able to save a child record offline if it depends on a parent record that has not yet been synced. For example, after saving an offline write to create a patient, you cannot save an offline write for an encounter of that patient until the “create-patient” write has successfully synced. -> Note : For some simple cases we can handle dependency enforcement of data during online. Simply means for those child write that not depend heavily on parent record. +> Note : For some simple cases we can handle dependency enforcement of data during online. Simply means for those child write that not depend heavily on parent record. ## Implemenation Timeline and Milestone @@ -498,4 +472,3 @@ As project implementation is divided into four phases , we will complete phase | **Phase 1: Caching** | Week 1 – Week 3 | • Dexie schema & IndexedDB setup
• `PersistQueryClientProvider` integrated
• Selective caching (`meta.persist`) verified
• Implementation of all tech config mention in phase 1
• Offline reads tested | | **Phase 2: Offline Writes**(mid-evaluation) | Week 4 – Week 6 | • `offlineWrites` table created
• `saveOfflineWrite` hooked into forms| | **Phase 3: Synchronization** | Week 7 – Week 9 | • `syncOneWrite`, retry/backoff, and `computeBackoffDelay` complete
• `detectAndMarkConflict` implemented
• Implementation of all tech config mention in phase 3
• End-to-end sync (offline → online) tested | -| **Phase 4: Notifications** | Week 10 – Week 12 | • Conflict notification event/UI built
• `resolveConflict` helper added
•Optimization & Performance
•User Testing & Documentation From d994a8620bee311a02dc08b0f64bc19e72df4d69 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Tue, 22 Jul 2025 10:15:09 +0530 Subject: [PATCH 11/15] minor fix --- .../offline-support-project/OfflineSupport.md | 6 +++- .../care/offline-support-project/api-audit.md | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 docs/care/offline-support-project/api-audit.md diff --git a/docs/care/offline-support-project/OfflineSupport.md b/docs/care/offline-support-project/OfflineSupport.md index c86375f..9a7a3cd 100644 --- a/docs/care/offline-support-project/OfflineSupport.md +++ b/docs/care/offline-support-project/OfflineSupport.md @@ -262,7 +262,11 @@ Now this is one of the important phase of offline support functionality. Here we #### 1. fetching pending writes (`getPendingWrites`) -- Use `getPendingAndRetryableWrites(userId)` to retrieve all records where syncStatus is either "pending" or "failed" (with retries < MAX_RETRIES). +- Use the following method to retrieve all records: + + ```ts + getPendingAndRetryableWrites(userId) +``` ```ts export async function getPendingAndRetryableWrites( diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md new file mode 100644 index 0000000..4a3457d --- /dev/null +++ b/docs/care/offline-support-project/api-audit.md @@ -0,0 +1,31 @@ +# API Audit for Offline Support + +This document lists some of the key backend APIs and workflows that need to be handled for robust offline support in the CARE application. The tables below provide an overview of standard workflows and the dynamic data that should be cached to ensure seamless user experience during connectivity interruptions. + +These tables are intended to: +- Identify critical workflows and their associated APIs for offline operation +- Guide route registration in service workers (e.g., workbox) +- Inform caching and persistence strategies for dynamic data + +## Standard Workflows and Associated APIs + +| Workflow | API Endpoint(s) | Description / Notes | +|-------------------------|-----------------------------------------|-----------------------------------------------------| +| Patient Registration | `/api/v1/patient/` (POST, GET) | Create and fetch patient records | +| Patient Details | `/api/v1/patient/{id}/` (GET, PUT) | View and update patient details | +| Encounter Creation | `/api/v1/encounter/` (POST, GET) | Create and list encounters | +| Encounter Details | `/api/v1/encounter/{id}/` (GET, PUT) | View and update encounter details | +| Form Submission | `/api/v1/form/` (POST, GET) | Submit and fetch forms | +| Form Details | `/api/v1/form/{id}/` (GET, PUT) | View and update form details | +| Visit List | `/api/v1/visit/` (GET) | List all visits for a patient | +| Visit Details | `/api/v1/visit/{id}/` (GET, PUT) | View and update visit details | +| User Profile | `/api/v1/user/profile/` (GET, PUT) | Fetch and update user profile | + +## Notes +- The above APIs are examples; actual endpoints may vary based on backend implementation. +- These APIs should be registered for caching and persistence in the service worker to support offline workflows. +- Additional APIs may be added as new workflows are identified for offline support. + +## How to Use This Audit +- Use this list to inform which routes should be registered in the workbox/service worker for caching. +- Reference this document when planning or updating offline support features and CEPs. \ No newline at end of file From 6665635471c7e69e3c59610bd29e3885b58a2b37 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Tue, 22 Jul 2025 10:17:06 +0530 Subject: [PATCH 12/15] minor fix --- .../care/offline-support-project/api-audit.md | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 docs/care/offline-support-project/api-audit.md diff --git a/docs/care/offline-support-project/api-audit.md b/docs/care/offline-support-project/api-audit.md deleted file mode 100644 index 4a3457d..0000000 --- a/docs/care/offline-support-project/api-audit.md +++ /dev/null @@ -1,31 +0,0 @@ -# API Audit for Offline Support - -This document lists some of the key backend APIs and workflows that need to be handled for robust offline support in the CARE application. The tables below provide an overview of standard workflows and the dynamic data that should be cached to ensure seamless user experience during connectivity interruptions. - -These tables are intended to: -- Identify critical workflows and their associated APIs for offline operation -- Guide route registration in service workers (e.g., workbox) -- Inform caching and persistence strategies for dynamic data - -## Standard Workflows and Associated APIs - -| Workflow | API Endpoint(s) | Description / Notes | -|-------------------------|-----------------------------------------|-----------------------------------------------------| -| Patient Registration | `/api/v1/patient/` (POST, GET) | Create and fetch patient records | -| Patient Details | `/api/v1/patient/{id}/` (GET, PUT) | View and update patient details | -| Encounter Creation | `/api/v1/encounter/` (POST, GET) | Create and list encounters | -| Encounter Details | `/api/v1/encounter/{id}/` (GET, PUT) | View and update encounter details | -| Form Submission | `/api/v1/form/` (POST, GET) | Submit and fetch forms | -| Form Details | `/api/v1/form/{id}/` (GET, PUT) | View and update form details | -| Visit List | `/api/v1/visit/` (GET) | List all visits for a patient | -| Visit Details | `/api/v1/visit/{id}/` (GET, PUT) | View and update visit details | -| User Profile | `/api/v1/user/profile/` (GET, PUT) | Fetch and update user profile | - -## Notes -- The above APIs are examples; actual endpoints may vary based on backend implementation. -- These APIs should be registered for caching and persistence in the service worker to support offline workflows. -- Additional APIs may be added as new workflows are identified for offline support. - -## How to Use This Audit -- Use this list to inform which routes should be registered in the workbox/service worker for caching. -- Reference this document when planning or updating offline support features and CEPs. \ No newline at end of file From e01a7ec4d5cbf3226350827abfc4dc478f291301 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Tue, 22 Jul 2025 10:26:06 +0530 Subject: [PATCH 13/15] minor fix --- docs/care/offline-support-project/OfflineSupport.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/care/offline-support-project/OfflineSupport.md b/docs/care/offline-support-project/OfflineSupport.md index 9a7a3cd..ad06b20 100644 --- a/docs/care/offline-support-project/OfflineSupport.md +++ b/docs/care/offline-support-project/OfflineSupport.md @@ -471,8 +471,10 @@ Now lets take a look om limitation and known issue now : As project implementation is divided into four phases , we will complete phase 1-2 from week 1-6 (till mid evaluation) and phase 3-4 from week 7-12. -| **Phase** | **Timeline** | **Key Deliverables** | -|-------------------------|-------------------|-------------------------------------------------------------------------------------| -| **Phase 1: Caching** | Week 1 – Week 3 | • Dexie schema & IndexedDB setup
• `PersistQueryClientProvider` integrated
• Selective caching (`meta.persist`) verified
• Implementation of all tech config mention in phase 1
• Offline reads tested | -| **Phase 2: Offline Writes**(mid-evaluation) | Week 4 – Week 6 | • `offlineWrites` table created
• `saveOfflineWrite` hooked into forms| -| **Phase 3: Synchronization** | Week 7 – Week 9 | • `syncOneWrite`, retry/backoff, and `computeBackoffDelay` complete
• `detectAndMarkConflict` implemented
• Implementation of all tech config mention in phase 3
• End-to-end sync (offline → online) tested | + +| **Phase** | **Timeline** | **Key Deliverables** | +|--------------------------------------------|---------------------|--------------------------------------------------------------------------------------| +| **Phase 1: Caching** | Week 1 – Week 3 | • Dexie schema & IndexedDB setup
• `PersistQueryClientProvider` integrated
• Selective caching using `meta.persist`
• Implementation of all tech configs mentioned in Phase 1
• Offline reads tested | +| **Phase 2: Offline Writes** (Mid-evaluation)| Week 4 – Week 6 | • `offlineWrites` table created
• `saveOfflineWrite` hooked into form submissions
• Basic retry metadata stored
• Test coverage for offline write logic | +| **Phase 3: Synchronization** | Week 7 – Week 9 | • `syncOneWrite`, `computeBackoffDelay`, and retry mechanism implemented
• `detectAndMarkConflict` for conflict detection
• Sync trigger on reconnect
• Implementation of all tech configs mentioned in Phase 3
• End-to-end sync (offline → online) tested | +| **Phase 4: Conflict Resolution + User Feedback** | Week 10 – Week 12 | • Conflict resolution UI implemented
• Sync status page for success/failed/pending writes
• Manual discard/overwrite support for conflicts
• User notifications (toasts, alerts) for sync outcomes
• Cleanup of stale writes
• Final testing & polishing
• **Build errors resolved and CI passes** | From 1363f1900a0f619d6c98c68efeb875b5e483c4f4 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Tue, 22 Jul 2025 10:33:50 +0530 Subject: [PATCH 14/15] minor bild fix --- docs/care/offline-support-project/OfflineSupport.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/care/offline-support-project/OfflineSupport.md b/docs/care/offline-support-project/OfflineSupport.md index ad06b20..a3aff1d 100644 --- a/docs/care/offline-support-project/OfflineSupport.md +++ b/docs/care/offline-support-project/OfflineSupport.md @@ -474,7 +474,7 @@ As project implementation is divided into four phases , we will complete phase | **Phase** | **Timeline** | **Key Deliverables** | |--------------------------------------------|---------------------|--------------------------------------------------------------------------------------| -| **Phase 1: Caching** | Week 1 – Week 3 | • Dexie schema & IndexedDB setup
• `PersistQueryClientProvider` integrated
• Selective caching using `meta.persist`
• Implementation of all tech configs mentioned in Phase 1
• Offline reads tested | -| **Phase 2: Offline Writes** (Mid-evaluation)| Week 4 – Week 6 | • `offlineWrites` table created
• `saveOfflineWrite` hooked into form submissions
• Basic retry metadata stored
• Test coverage for offline write logic | -| **Phase 3: Synchronization** | Week 7 – Week 9 | • `syncOneWrite`, `computeBackoffDelay`, and retry mechanism implemented
• `detectAndMarkConflict` for conflict detection
• Sync trigger on reconnect
• Implementation of all tech configs mentioned in Phase 3
• End-to-end sync (offline → online) tested | -| **Phase 4: Conflict Resolution + User Feedback** | Week 10 – Week 12 | • Conflict resolution UI implemented
• Sync status page for success/failed/pending writes
• Manual discard/overwrite support for conflicts
• User notifications (toasts, alerts) for sync outcomes
• Cleanup of stale writes
• Final testing & polishing
• **Build errors resolved and CI passes** | +| **Phase 1: Caching** | Week 1 – Week 3 | • Dexie schema & IndexedDB setup
• `PersistQueryClientProvider` integrated
• Selective caching using `meta.persist`
• Implementation of all tech configs mentioned in Phase 1
• Offline reads tested | +| **Phase 2: Offline Writes** (Mid-evaluation)| Week 4 – Week 6 | • `offlineWrites` table created
• `saveOfflineWrite` hooked into form submissions
• Basic retry metadata stored
• Test coverage for offline write logic | +| **Phase 3: Synchronization** | Week 7 – Week 9 | • `syncOneWrite`, `computeBackoffDelay`, and retry mechanism implemented
• `detectAndMarkConflict` for conflict detection
• Sync trigger on reconnect
• Implementation of all tech configs mentioned in Phase 3
• End-to-end sync (offline → online) tested | +| **Phase 4: Conflict Resolution + User Feedback** | Week 10 – Week 12 | • Conflict resolution UI implemented
• Sync status page for success/failed/pending writes
• Manual discard/overwrite support for conflicts
• User notifications (toasts, alerts) for sync outcomes
• Cleanup of stale writes
• Final testing & polishing
• **Build errors resolved and CI passes** | From 6a86eba8caa1c4c01190053dda623c89201e8b15 Mon Sep 17 00:00:00 2001 From: vikaspal8923 Date: Sun, 3 Aug 2025 10:47:33 +0530 Subject: [PATCH 15/15] update doc --- .../offline-support-project/OfflineSupport.md | 422 +++++++++++++----- 1 file changed, 307 insertions(+), 115 deletions(-) diff --git a/docs/care/offline-support-project/OfflineSupport.md b/docs/care/offline-support-project/OfflineSupport.md index a3aff1d..bd46380 100644 --- a/docs/care/offline-support-project/OfflineSupport.md +++ b/docs/care/offline-support-project/OfflineSupport.md @@ -258,164 +258,356 @@ export const saveOfflineWrite = async ({ ### Phase 3: Synchronization -Now this is one of the important phase of offline support functionality. Here we will synchronize the data that we save during offline. so for that we need a sync manage . A production‑grade sync manager should include: +## Core Components -#### 1. fetching pending writes (`getPendingWrites`) +### 1. SyncManager Class -- Use the following method to retrieve all records: +The main orchestrator for offline synchronization. - ```ts - getPendingAndRetryableWrites(userId) +**Location:** `src/OfflineSupport/syncmanger.ts` + +**Purpose:** Coordinates the entire sync process, manages dependencies, handles errors, and ensures data consistency across offline and online states. + +**Key Features:** +- Dependency-aware sync processing +- ID mapping persistence +- Error classification and retry logic +- Progress tracking and abort control +- Conflict detection + +**Usage:** +```typescript +const syncManager = new SyncManager({ + userId: "user-id", + maxRetries: 3, + retryDelayMs: 1000, + enableConflictDetection: true, + onProgress: (synced, total) => console.log(`${synced}/${total}`), + onSyncStart: (total) => console.log(`Starting sync of ${total} items`), + onSyncComplete: () => console.log("Sync completed"), +}); + +const result = await syncManager.sync(); ``` -```ts -export async function getPendingAndRetryableWrites( - userId: string -): Promise { - return db.offlineWrites - .where("userId") - .equals(userId) - .and((w) => { - const isPending = w.syncStatus === "pending"; - const isFailedButRetryable = - w.syncStatus === "failed" && (w.retries || 0) < MAX_RETRIES; - return isPending || isFailedButRetryable; - }) - .toArray(); +### 2. IdMap Class + +Manages offline-to-server ID mappings across sync sessions. + +**Location:** `src/OfflineSupport/idMap.ts` + +**Purpose:** Maintains a mapping between offline-generated IDs and server-generated IDs, ensuring that dependent writes can reference the correct server IDs even across multiple sync sessions. + +**Features:** +- Pre-population from successful sync records +- In-memory caching with IndexedDB persistence +- Automatic mapping updates + +**Usage:** +```typescript +const idMap = new IdMap(); + +// Pre-populate with existing mappings +idMap.prePopulateFromSuccessfulSyncs(successfulWrites); + +// Add new mapping +idMap.addMapping("offline-123", "server-456"); + +// Get server ID +const serverId = idMap.getServerId("offline-123"); +``` + +### 3. Write Queue Management + +Handles the lifecycle of offline writes. + +**Location:** `src/OfflineSupport/writeQueue.ts` + +**Purpose:** Manages the state and lifecycle of offline writes, including retry logic, status updates, and dependency management between parent and child writes. + +**Key Functions:** +- `getPendingAndRetryableWrites()` - Retrieves writes ready for sync +- `markWriteStatus()` - Updates write status and metadata +- `unblockDependentWrites()` - Unblocks writes when parent succeeds + +**Write States:** +- `pending` - Ready for sync +- `success` - Successfully synced +- `failed` - Failed to sync (may be retried) +- `blocked` - Blocked by failed parent +- `conflict` - Has conflicts + +## Sync Process Flow + +### 1. Initialization +**Purpose:** Ensures sync can only run when appropriate conditions are met and prevents multiple concurrent sync sessions. + +```typescript +// Check if sync is already running +if (this.isRunning) { + throw new Error("Sync is already running"); +} + +// Verify online status +if (!onlineManager.isOnline()) { + throw new Error("Cannot sync while offline"); } ``` -#### 2. Processing each write (`syncOneWrite`) with proper error/409 handling and Retry/backoff logic (`shouldRetry`, `computeBackoffDelay`, scheduleRetry). +### 2. Write Collection +**Purpose:** Gathers all writes that need to be synced, including pending, failed (retryable), and blocked writes that have been unblocked. -- For every item in that list, call `syncOneWrite(write)`, which: - Runs conflict detection first (`detectAndMarkConflict`). If the server’s `modified_date` differs from the cached `serverTimestamp`, it immediately marks that record as "conflict" and skips any further mutation. -- If no `conflict`, attempts the API mutation `(mutate(route, payload))`. -- On success, updates syncStatus = "success" and timestamps the write. -   • On error: If HTTP 409, marks `syncStatus = "conflict"` and stores the server’s data in conflictData. Otherwise, marks syncStatus = "failed", increments retries, saves the error message, and—if `retries < MAX_RETRIES—schedules` a retry via `computeBackoffDelay(retries) + scheduleRetry`. +```typescript +// Get all writes that need syncing +const pendingWrites = await getPendingAndRetryableWrites(userId); -```ts -export async function syncOneWrite(write: OfflineWriteRecord): Promise { - const now = Date.now(); +// Includes: +// - pending writes +// - failed writes (retries < MAX_RETRIES) +// - blocked writes (unblocked when parent succeeds) +``` - // 1) Detect (and mark) conflict up front - const didConflict = await detectAndMarkConflict(write); - if (didConflict) { - return; - } +### 3. ID Mapping Pre-population +**Purpose:** Builds a complete mapping of offline-to-server IDs from all successful writes, ensuring that dependent writes can reference the correct server IDs even if they were created in different sync sessions. - try { - // 2) Execute the mutation route - const route = offlineRoutes[write.syncrouteKey as OfflineRouteKey]; - const mutationFn = mutate(route, { pathParams: write.pathParams as any }); - await mutationFn(write.payload); - - // 3) On success, mark as synced - await db.offlineWrites.update(write.id, { - syncStatus: "success", - lastAttemptAt: now, - }); +```typescript +// Get all writes to build complete ID mapping +const allWrites = await db.OfflineWrites + .where("userId") + .equals(userId) + .toArray(); + +const successfulWrites = allWrites.filter( + write => write.syncStatus === "success" +); + +// Pre-populate ID map with historical mappings +this.idMap.prePopulateFromSuccessfulSyncs(successfulWrites); +``` - toast.success(`Sync succeeded for write ${write.id}`); - } catch (error: any) { - const nextRetries = (write.retries || 0) + 1; - const isConflict = error?.response?.status === 409; - - const updateFields: Partial = { - lastAttemptAt: now, - retries: nextRetries, - lastError: error?.message || "Unknown error", - }; - - if (isConflict) { - // 4.a) On HTTP 409, mark as conflict - updateFields.syncStatus = "conflict"; - updateFields.conflictData = error.response?.data; - } else { - // 4.b) On other errors, mark as failed - updateFields.syncStatus = "failed"; - } +### 4. Dependency Resolution +**Purpose:** Ensures that writes are processed in the correct order based on their dependencies, preventing errors where child writes reference parent writes that haven't been synced yet. - await db.offlineWrites.update(write.id, updateFields); - toast.error(`Sync failed for write ${write.id}: ${updateFields.lastError}`); +```typescript +// Sort writes by dependencies (topological sort) +const sortedWrites = topologicalSort(pendingWrites); - // 5) Retry/backoff logic (only for non-conflict) - if (!isConflict && shouldRetry(nextRetries)) { - const delay = computeBackoffDelay(nextRetries); - scheduleRetry(write.userId, delay); - } +// Ensures parent writes are processed before children +// Example: Patient → Encounter → Questionnaire +``` + +### 5. Write Processing +For each write in dependency order: + +#### Step 1: Parent Check +**Purpose:** Verifies that all parent writes are not permanently failed, preventing child writes from attempting to sync when their dependencies cannot be satisfied. + +```typescript +// Check if parent writes are blocked +if (write.parentMutationIds?.length > 0) { + const blockedParents = await this.checkBlockedParents( + write.parentMutationIds + ); + if (blockedParents.length > 0) { + return { status: "blocked" }; } } +``` + +#### Step 2: Conflict Detection +**Purpose:** Detects and handles conflicts between offline changes and server state, ensuring data consistency and preventing data corruption. -export function shouldRetry(currentRetries: number): boolean { - return currentRetries < MAX_RETRIES; +```typescript +// Optional conflict detection +if (this.options.enableConflictDetection && write.useQueryRouteKey) { + const hasConflict = await detectAndMarkConflict(write); + if (hasConflict) { + return { status: "conflict" }; + } } +``` -export function computeBackoffDelay(attempt: number): number { - return BASE_BACKOFF_MS * 2 ** (attempt - 1); +#### Step 3: ID Replacement +**Purpose:** Replaces all offline-generated IDs in the write payload with their corresponding server IDs, ensuring the API receives valid server references. + +```typescript +// Replace offline IDs with server IDs +const processedWrite = replaceOfflineIdsInWrite( + write, + dependencySchema, + this.idMap +); +``` + +#### Step 4: API Execution +**Purpose:** Executes the actual API mutation using the processed write data, communicating with the server to persist the offline changes. + +```typescript +// Execute the actual API mutation +const response = await this.executeMutation(processedWrite); +``` + +#### Step 5: Success Handling +**Purpose:** Updates the write status, stores the server response, creates ID mappings for future reference, and unblocks any dependent writes that were waiting for this write to succeed. + +```typescript +// Update write status +await markWriteStatus(write.id, "success", { + response, + lastAttemptAt: Date.now(), +}); + +// Store ID mapping +if (response?.id && write.id.startsWith("offline-")) { + this.idMap.addMapping(write.id, response.id); } -export function scheduleRetry(userId: string, delayMs: number): void { - setTimeout(() => { - processSyncQueue(userId); - }, delayMs); +// Unblock dependent writes +await unblockDependentWrites(write.id); +``` + +#### Step 6: Error Handling +**Purpose:** Classifies errors as permanent or temporary, updates write status with detailed error information, and blocks dependent writes if the failure is permanent to prevent cascading failures. + +```typescript +// Classify error as permanent or temporary +const isPermanentFailure = this.isPermanentFailure(error); + +// Update write status with error details +await markWriteStatus(write.id, "failed", { + lastError: errorMessage, + lastErrorDetails: errorDetails, + lastAttemptAt: Date.now(), + retries: (write.retries || 0) + 1, + isPermanentFailure, +}); + +// Block dependent writes if permanent failure +if (isPermanentFailure) { + await this.markDependentWritesAsBlocked(write.id); } ``` -#### 3. Conflict detection & resolution (`detectAndMarkConflict`) :- If server’s modified_date has changed since we cached it, that’s a conflict. +## Error Classification -- `detectAndMarkConflict`(write) fetches the current server record (using queryrouteKey/queryParams) and compares `serverData.modified_date` with `write.serverTimestamp`. -- If they differ, it updates that write’s `syncStatus = "conflict"` and saves conflictData so the UI can prompt the user. +The sync manager intelligently classifies errors: -```ts -async function detectAndMarkConflict( - write: OfflineWriteRecord -): Promise { - if (!write.queryrouteKey || !write.queryParams) { - return false; - } +### Permanent Failures (No Retry) +- **4xx Errors** (except 429): Validation errors, not found, etc. +- **Most 5xx Errors**: Server configuration issues - try { - const fetchFn = query( - offlineRoutes[write.queryrouteKey as OfflineRouteKey] - ); - const serverData = await fetchFn({ pathParams: write.queryParams as any }); - - if (serverData.modified_date !== write.serverTimestamp) { - await db.offlineWrites.update(write.id, { - syncStatus: "conflict", - conflictData: serverData, - lastAttemptAt: Date.now(), - }); +### Temporary Failures (Retry) +- **429 Rate Limit**: Too many requests +- **501, 502, 503**: Service unavailable, gateway errors +- **Network Errors**: Timeouts, connection issues + +```typescript +private isPermanentFailure(error: any): boolean { + if (error instanceof HTTPError) { + const statusCode = error.status; + + // 4xx errors are permanent (except 429) + if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) { return true; } - } catch (err) { - console.warn(`detectAndMarkConflict: failed for write ${write.id}`, err); + + // 5xx errors: 501, 502, 503 are temporary + if (statusCode >= 500) { + return ![501, 502, 503].includes(statusCode); + } } - return false; } ``` -#### 4. Queue orchestration (`processSyncQueue`) triggered by network or timed events. +## Retry Strategy -- `processSyncQueue(userId)` is triggered whenever the browser fires "online" (via onNetworkStatusChange) . -- Each pass fetches the current pending/retryable writes and calls syncOneWrite on each in series. +### Retry Logic +- **Max Retries**: 5 attempts per write +- **Retry Condition**: `retries < MAX_RETRIES && !isPermanentFailure` +- **Retry Timing**: Immediate retry in next sync session -```ts -export async function processSyncQueue(userId: string): Promise { - if (!navigator.onLine || !userId) return; +### Retry Flow +1. Write fails → Status: `failed`, `retries++` +2. Next sync → Include in `pendingWrites` if retries < 5 +3. Retry attempt → Execute mutation again +4. Success → Status: `success`, unblock dependents +5. Failure → Repeat until max retries reached + +## Dependency Management + +### Dependency Schema +Defined in `src/OfflineSupport/dependencySchema.ts`: + +**Purpose:** Defines the relationships between different types of writes, specifying which writes depend on others and how to resolve these dependencies during sync. - const writesToProcess = await getPendingAndRetryableWrites(userId); +```typescript +export const dependencySchema: DependencySchema = { + create_patient: [], // No dependencies + create_encounter: [ + { location: "payload", path: ["patient"], resourceType: "patient" }, + ], + create_appointment: [ + { location: "payload", path: ["patient"], resourceType: "patient" }, + ], + // ... more dependencies +}; +``` - for (const write of writesToProcess) { - await syncOneWrite(write); +### Dependency Resolution +1. **Topological Sort**: Ensures correct processing order +2. **Parent Check**: Verifies parents are not blocked +3. **Blocking Logic**: Blocks children when parent fails permanently +4. **Unblocking Logic**: Unblocks children when parent succeeds + +## Integration Points + +### 1. React Context Integration +**Location:** `src/context/SyncContext.tsx` + +**Purpose:** Provides sync state and controls to React components, enabling UI components to display sync progress and allow users to trigger manual syncs. + +Provides sync state and controls to React components: + +```typescript +const { isSyncing, syncedCount, totalCount, startSync, resetSync } = useSync(); +``` + +### 2. AuthUserProvider Integration +**Location:** `src/Providers/AuthUserProvider.tsx` + +**Purpose:** Automatically triggers sync when users come online and are authenticated, with smart conditions to prevent sync on login pages or when users are not properly authenticated. + +Automatic sync triggers with smart conditions: + +```typescript +useEffect(() => { + const isOnSessionExpiredPage = location.pathname === "/session-expired"; + + if ( + !onlineManager.isOnline() || + !user?.external_id || + isSyncing || + localStorage.getItem(LocalStorageKeys.accessToken) === null || + isOnSessionExpiredPage + ) { + return; } -} + + const timeout = setTimeout(() => { + startSync(user.external_id); + }, 3000); + + return () => clearTimeout(timeout); +}, [user?.external_id, onlineManager.isOnline(), startSync, isSyncing, location.pathname]); ``` -#### 5. cleanup (`cleanupSuccessfulWrites`). +### 3. UI Components +**Location:** `src/components/Common/SyncBanner.tsx` + +**Purpose:** Displays real-time sync progress to users, providing visual feedback about the sync process and completion status. + -- `cleanupSuccessfulWrites(userId, olderThanMs)` deletes any records where ` ### Phase 4: Notifications (Sync Status Page)