Offline measurement saves
Offline queueing lets the SDK retain a measurement when its Observation bundle cannot be saved immediately. It is opt-in, uses the client’s configured storage adapter, and requires the application to trigger later flushes.
How the queue behaves
When queueing is disabled, saveMeasurement() sends the generated Observations immediately. The call returns a saved result or rejects if the save fails.
When queueing is enabled, a measurement save:
- Converts the measurement to Observation resources.
- Writes the request to the configured storage adapter.
- Tries to send the queued request immediately.
- Returns
status: 'saved'if that attempt succeeds, orstatus: 'queued'with aqueueIdif it remains pending.
A queued result means the server has not confirmed the save. Keep the related UI state pending until a later flush returns that queue ID in responses.
Configure queueing
Pass a durable storage adapter through storage and set offlineQueue.enabled to true. The queue stores generated Observation data in that adapter. Select storage and a queue key that match your app’s persistence and account boundaries.
import { OvokClient } from '@ovok/core';
const client = new OvokClient({
baseUrl: 'https://api.sandbox.ovok.com',
fhirUrlPath: '/fhir/R4/',
storage: appStorage, // Durable storage adapter configured by your app.
offlineQueue: {
enabled: true,
storageKey: 'my-app:production:account-scope:measurements',
},
});
The SDK uses ovok.core.offline-measurements as the default queue key. Set storageKey when a storage adapter is shared across applications, environments, or account scopes. The client checks that a storage adapter is configured; persistence across suspension or restart depends on the adapter you provide. Do not flush queued measurements through a client authenticated for a different account context.
Queue storage must survive the periods when the app is closed or suspended if those queued measurements need to remain available. Treat the stored health data according to your app’s retention and protection requirements.
Save and represent pending state
A queue-enabled save tries to flush the new item right away. It may still return queued when the request fails or its retry time has not arrived.
import { MeasurementTypeKey } from '@ovok/core';
async function recordHeartRate() {
const result = await client.saveMeasurement({
measurement: {
measurementTypeKey: MeasurementTypeKey.heartRate,
heartRate: 72,
},
effectiveDateTime: new Date(),
});
if (result.status === 'queued') {
return { state: 'pending' as const, queueId: result.queueId };
}
return { state: 'saved' as const, response: result.response };
}
Keep the returned queueId with the app’s pending UI state. The SDK does not expose a separate queue-inspection method. A later flush returns successful responses keyed by queue ID, which lets the app reconcile pending items.
Flush queued measurements
Call flushOfflineMeasurementQueue() from an app lifecycle or connectivity event, such as when the app resumes or the browser returns online. The SDK does not install event listeners or schedule background flushes for the application.
async function flushPendingMeasurements() {
const result = await client.flushOfflineMeasurementQueue();
for (const [queueId, response] of Object.entries(result.responses)) {
await markMeasurementSaved(queueId, response);
}
updateQueueSummary({
attempted: result.attempted,
saved: result.saved,
queued: result.queued,
exhausted: result.exhausted,
});
return result;
}
The example’s markMeasurementSaved() and updateQueueSummary() are application functions. Use the response map to mark individual queue IDs as saved. Use the counters for aggregate pending and retry UI.
| Flush result field | Meaning |
|---|---|
attempted | Number of queue items sent during this call. |
saved | Number of queue items successfully saved during this call. |
queued | Number of items still pending, including items whose retry time has not arrived. |
exhausted | Number of exhausted items left in the queue. |
responses | Successful transaction responses keyed by queue ID. |
Calling flushOfflineMeasurementQueue() when queueing is disabled returns zero counts and an empty response map.
Retry behavior
Each queued item is retried with exponential backoff. By default, the queue allows eight attempts total, including the initial attempt made during the save call. The retry delay starts at five seconds and doubles after each failed attempt, up to a one-hour cap.
| Option | Default | Purpose |
|---|---|---|
enabled | false | Turns queueing on when set to true. |
maxRetries | 8 | Maximum attempts for each queue item, including its first attempt. |
retryBackoffMs | 5000 | Starting delay before a retry, in milliseconds. |
maxRetryBackoffMs | 3600000 | Maximum backoff delay, in milliseconds. |
storageKey | @ovok/core/offline-measurements | Key used to persist the queue in the configured storage adapter. |
An item becomes exhausted after it reaches maxRetries. A normal flush leaves exhausted items in storage and does not attempt them again.
Use the flush options to retry exhausted items deliberately:
// Attempt exhausted items once without resetting their retry history.
await client.flushOfflineMeasurementQueue({ includeExhausted: true });
// Reset exhausted items and start their retry count again.
await client.flushOfflineMeasurementQueue({ resetExhausted: true });
includeExhausted includes exhausted items in that flush. resetExhausted clears their exhausted state and retry timing before the flush. Review your application’s error state before retrying repeatedly.
Failure handling and duplicate protection
With queueing enabled, unsuccessful network calls and transaction responses containing a 4xx or 5xx status are treated as failed attempts. They stay queued for retry until they save or become exhausted. The flush result reports counts; it does not include the failed response details. Validate measurement inputs and authentication before queueing, and use your application’s server or client diagnostics to investigate items that remain pending or become exhausted.
The client uses conditional creates for Observation entries. Retrying the same generated Observation does not create a second copy. A changed measurement or effective time produces different content and can be saved as a separate reading.
React applications
The queue belongs to the configured OvokClient; React hooks use that client from OvokProvider. useSaveMeasurement() returns the same saved-or-queued result as the client method. Use useClient() to call the flush method from an app lifecycle or connectivity handler.
import {
MeasurementTypeKey,
useClient,
useSaveMeasurement,
} from '@ovok/core';
import { useState } from 'react';
export function OfflineSaveExample() {
const client = useClient();
const saveMeasurement = useSaveMeasurement();
const [pendingQueueId, setPendingQueueId] = useState<string>();
const [status, setStatus] = useState('idle');
async function saveReading() {
const result = await saveMeasurement({
measurement: {
measurementTypeKey: MeasurementTypeKey.heartRate,
heartRate: 72,
},
effectiveDateTime: new Date(),
});
if (result.status === 'queued') {
setPendingQueueId(result.queueId);
setStatus('pending');
return;
}
setStatus('saved');
}
async function flushPending() {
const result = await client.flushOfflineMeasurementQueue();
if (pendingQueueId && result.responses[pendingQueueId]) {
setPendingQueueId(undefined);
setStatus('saved');
}
return result;
}
return (
<div>
<p>Save status: {status}</p>
<button onClick={() => void saveReading()}>Save reading</button>
<button onClick={() => void flushPending()}>Retry pending saves</button>
</div>
);
}
See useSaveMeasurement for the hook contract, React applications for provider setup, and Measurements and health data for measurement-specific fields and units.