@ovok/core 0.4.49-dev.497 → 0.4.49-dev.499

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,7 +48,7 @@ exports.OVOK_CORE_REQUIREMENTS = {
48
48
  description: "The FHIR surface a server must provide for the @ovok/core client library's own methods to function.",
49
49
  software: {
50
50
  name: "@ovok/core",
51
- version: "0.4.49-dev.497+7201066",
51
+ version: "0.4.49-dev.499+db0e927",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
@@ -17,6 +17,7 @@ export type { HealthCheck } from "./resources/health-resource";
17
17
  export type { PatientRegistration, PractitionerRegistration, RegisterResult } from "./resources/auth/registration";
18
18
  export type { SocialCredentials, SocialProvider } from "./resources/auth/social-login";
19
19
  export type { LastObservationsQuery, LatestObservationsParams, MeasurementCodeTypes, SaveObservationsOptions, SaveObservationsResult, } from "./resources/measurements/measurements-resource";
20
+ export type { OfflineMeasurementFailure, OfflineMeasurementFlushOptions, OfflineMeasurementFlushResult, OfflineMeasurementQueueApi, } from "./resources/measurements/offline-measurement-queue";
20
21
  export type { BuildObservationsParams } from "./resources/measurements/observation-builder";
21
22
  export type { SignalsDeliveryRepair, SignalsDeliveryRepairResult, SignalsUndeliverable, SignalsUndeliverableResident, } from "./resources/signals/undeliverable-resource";
22
23
  export type { ProjectListQuery } from "./resources/projects/projects-resource";
package/dist/next/ovok.js CHANGED
@@ -24,6 +24,7 @@ const invites_resource_1 = require("./resources/invites/invites-resource");
24
24
  const locales_resource_1 = require("./resources/localization/locales-resource");
25
25
  const localizations_resource_1 = require("./resources/localization/localizations-resource");
26
26
  const measurements_resource_1 = require("./resources/measurements/measurements-resource");
27
+ const offline_measurement_queue_1 = require("./resources/measurements/offline-measurement-queue");
27
28
  const patient_resource_1 = require("./resources/patient/patient-resource");
28
29
  const projects_resource_1 = require("./resources/projects/projects-resource");
29
30
  const questionnaires_resource_1 = require("./resources/questionnaires/questionnaires-resource");
@@ -36,6 +37,7 @@ const wearables_resource_1 = require("./resources/wearables/wearables-resource")
36
37
  /** One client per tenant; each namespace mirrors an area of the Ovok API. */
37
38
  class Ovok {
38
39
  constructor(config) {
40
+ var _a, _b;
39
41
  const settings = (0, settings_1.readSettings)(config, (0, settings_1.currentEnvironment)());
40
42
  const credentials = credentialsFor(settings);
41
43
  const http = new http_client_1.HttpClient({ settings, credentials, retryPolicy: new retry_policy_1.RetryPolicy(settings.maxNetworkRetries) });
@@ -46,7 +48,11 @@ class Ovok {
46
48
  this.fhir = fhir;
47
49
  this.health = new health_resource_1.HealthResource(http.anonymous());
48
50
  this.patient = new patient_resource_1.PatientResource(http, fhir);
49
- this.measurements = new measurements_resource_1.MeasurementsResource(http, fhir, session);
51
+ this.measurements = new measurements_resource_1.MeasurementsResource(http, fhir, session, {
52
+ storage: settings.storage,
53
+ queueKey: (0, offline_measurement_queue_1.offlineMeasurementStorageKey)(settings.tenantCode, settings.baseUrl),
54
+ machineClientId: (_b = (_a = settings.machineClient) === null || _a === void 0 ? void 0 : _a.clientId) !== null && _b !== void 0 ? _b : null,
55
+ });
50
56
  this.questionnaires = new questionnaires_resource_1.QuestionnairesResource(http);
51
57
  this.questionnaireResponses = new questionnaires_resource_1.QuestionnaireResponsesResource(http, fhir);
52
58
  this.consent = new consent_resource_1.ConsentResource(http);
@@ -0,0 +1,4 @@
1
+ import * as React from "react";
2
+ import type { Ovok } from "../ovok";
3
+ export declare const OvokContext: React.Context<Ovok | null>;
4
+ export declare const useOvok: () => Ovok;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.useOvok = exports.OvokContext = void 0;
37
+ const React = __importStar(require("react"));
38
+ exports.OvokContext = React.createContext(null);
39
+ const useOvok = () => {
40
+ const client = React.useContext(exports.OvokContext);
41
+ if (client === null) {
42
+ throw new Error("useOvok must be used inside an OvokProvider.");
43
+ }
44
+ return client;
45
+ };
46
+ exports.useOvok = useOvok;
@@ -0,0 +1,14 @@
1
+ import * as React from "react";
2
+ import type { Session } from "../auth/session";
3
+ import type { Ovok } from "../ovok";
4
+ import type { OvokQueryState } from "./query-hooks";
5
+ import { useOvok } from "./context";
6
+ export type OvokProviderProps = React.PropsWithChildren<{
7
+ client: Ovok;
8
+ }>;
9
+ export declare const OvokProvider: ({ client, children }: OvokProviderProps) => React.ReactElement;
10
+ export declare const useSession: () => OvokQueryState<Session | null>;
11
+ export declare const useProfile: () => OvokQueryState<Session["profile"] | null>;
12
+ export { useOvokMutation, useOvokQuery, useResource, useSearch, useSubscription } from "./query-hooks";
13
+ export { useOvok };
14
+ export type { OvokMutationState, OvokQueryOptions, OvokQueryState } from "./query-hooks";
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ "use client";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.useOvok = exports.useSubscription = exports.useSearch = exports.useResource = exports.useOvokQuery = exports.useOvokMutation = exports.useProfile = exports.useSession = exports.OvokProvider = void 0;
38
+ const React = __importStar(require("react"));
39
+ const context_1 = require("./context");
40
+ Object.defineProperty(exports, "useOvok", { enumerable: true, get: function () { return context_1.useOvok; } });
41
+ const query_hooks_1 = require("./query-hooks");
42
+ const OvokProvider = ({ client, children }) => (React.createElement(context_1.OvokContext.Provider, { value: client }, children));
43
+ exports.OvokProvider = OvokProvider;
44
+ const useSession = () => {
45
+ const client = (0, context_1.useOvok)();
46
+ const subscribe = React.useCallback((listener) => client.session.onChange(listener), [client]);
47
+ const changedSession = (0, query_hooks_1.useSubscription)(subscribe);
48
+ const state = (0, query_hooks_1.useOvokQuery)((ovok) => ovok.session.current(), { queryKey: ["session"] });
49
+ return changedSession === undefined
50
+ ? state
51
+ : { ...state, data: changedSession, loading: false, isRefreshing: false, isStale: false, error: undefined };
52
+ };
53
+ exports.useSession = useSession;
54
+ const useProfile = () => {
55
+ var _a, _b;
56
+ const session = (0, exports.useSession)();
57
+ return {
58
+ ...session,
59
+ data: session.data === undefined ? undefined : (_b = (_a = session.data) === null || _a === void 0 ? void 0 : _a.profile) !== null && _b !== void 0 ? _b : null,
60
+ };
61
+ };
62
+ exports.useProfile = useProfile;
63
+ var query_hooks_2 = require("./query-hooks");
64
+ Object.defineProperty(exports, "useOvokMutation", { enumerable: true, get: function () { return query_hooks_2.useOvokMutation; } });
65
+ Object.defineProperty(exports, "useOvokQuery", { enumerable: true, get: function () { return query_hooks_2.useOvokQuery; } });
66
+ Object.defineProperty(exports, "useResource", { enumerable: true, get: function () { return query_hooks_2.useResource; } });
67
+ Object.defineProperty(exports, "useSearch", { enumerable: true, get: function () { return query_hooks_2.useSearch; } });
68
+ Object.defineProperty(exports, "useSubscription", { enumerable: true, get: function () { return query_hooks_2.useSubscription; } });
@@ -0,0 +1,31 @@
1
+ import type { QueryParams } from "../http/url";
2
+ import type { Ovok } from "../ovok";
3
+ import type { Page } from "../paging/page";
4
+ import type { ExtractResource, ResourceType } from "@medplum/fhirtypes";
5
+ export type OvokQueryOptions = {
6
+ queryKey: readonly unknown[];
7
+ enabled?: boolean;
8
+ };
9
+ export type OvokQueryState<T> = {
10
+ data: T | undefined;
11
+ loading: boolean;
12
+ isRefreshing: boolean;
13
+ isStale: boolean;
14
+ error: Error | undefined;
15
+ refetch: () => void;
16
+ };
17
+ export type OvokMutationState<TVariables, TResult> = {
18
+ data: TResult | undefined;
19
+ loading: boolean;
20
+ error: Error | undefined;
21
+ mutate: (variables: TVariables) => Promise<TResult>;
22
+ reset: () => void;
23
+ };
24
+ type OvokQuery<T> = (client: Ovok, signal: AbortSignal) => Promise<T>;
25
+ type OvokMutation<TVariables, TResult> = (client: Ovok, variables: TVariables) => Promise<TResult>;
26
+ export declare const useOvokQuery: <T>(query: OvokQuery<T>, options: OvokQueryOptions) => OvokQueryState<T>;
27
+ export declare const useOvokMutation: <TVariables, TResult>(mutation: OvokMutation<TVariables, TResult>) => OvokMutationState<TVariables, TResult>;
28
+ export declare const useResource: <K extends ResourceType>(type: K, id: string | undefined, options?: Pick<OvokQueryOptions, "enabled">) => OvokQueryState<ExtractResource<K>>;
29
+ export declare const useSearch: <K extends ResourceType>(type: K, params?: QueryParams, options?: Pick<OvokQueryOptions, "enabled">) => OvokQueryState<Page<ExtractResource<K>>>;
30
+ export declare const useSubscription: <T>(subscribe: (listener: (value: T) => void) => () => void) => T | undefined;
31
+ export {};
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.useSubscription = exports.useSearch = exports.useResource = exports.useOvokMutation = exports.useOvokQuery = void 0;
37
+ const React = __importStar(require("react"));
38
+ const context_1 = require("./context");
39
+ const useOvokQuery = (query, options) => {
40
+ var _a, _b;
41
+ const client = (0, context_1.useOvok)();
42
+ const key = (_a = JSON.stringify(options.queryKey)) !== null && _a !== void 0 ? _a : "[]";
43
+ const enabled = (_b = options.enabled) !== null && _b !== void 0 ? _b : true;
44
+ const queryRef = React.useRef(query);
45
+ const previousQuery = React.useRef(null);
46
+ const [data, setData] = React.useState();
47
+ const [loading, setLoading] = React.useState(enabled);
48
+ const [error, setError] = React.useState();
49
+ const [revision, setRevision] = React.useState(0);
50
+ queryRef.current = query;
51
+ React.useEffect(() => {
52
+ const previous = previousQuery.current;
53
+ const changed = previous === null || previous.client !== client || previous.key !== key;
54
+ previousQuery.current = { client, key };
55
+ if (changed) {
56
+ setData(undefined);
57
+ }
58
+ if (!enabled) {
59
+ setError(undefined);
60
+ setLoading(false);
61
+ return undefined;
62
+ }
63
+ const controller = new AbortController();
64
+ let disposed = false;
65
+ setLoading(true);
66
+ setError(undefined);
67
+ void Promise.resolve()
68
+ .then(() => queryRef.current(client, controller.signal))
69
+ .then((result) => {
70
+ if (!disposed) {
71
+ setData(result);
72
+ setLoading(false);
73
+ }
74
+ })
75
+ .catch((reason) => {
76
+ if (!disposed) {
77
+ setError(toError(reason));
78
+ setLoading(false);
79
+ }
80
+ });
81
+ return () => {
82
+ disposed = true;
83
+ controller.abort();
84
+ };
85
+ }, [client, enabled, key, revision]);
86
+ const refetch = React.useCallback(() => setRevision((value) => value + 1), []);
87
+ return {
88
+ data,
89
+ loading,
90
+ isRefreshing: data !== undefined && loading,
91
+ isStale: data !== undefined && (loading || error !== undefined),
92
+ error,
93
+ refetch,
94
+ };
95
+ };
96
+ exports.useOvokQuery = useOvokQuery;
97
+ const useOvokMutation = (mutation) => {
98
+ const client = (0, context_1.useOvok)();
99
+ const mutationRef = React.useRef(mutation);
100
+ const pendingCount = React.useRef(0);
101
+ const [data, setData] = React.useState();
102
+ const [loading, setLoading] = React.useState(false);
103
+ const [error, setError] = React.useState();
104
+ mutationRef.current = mutation;
105
+ const mutate = React.useCallback(async (variables) => {
106
+ pendingCount.current += 1;
107
+ setLoading(true);
108
+ setError(undefined);
109
+ try {
110
+ const result = await mutationRef.current(client, variables);
111
+ setData(result);
112
+ return result;
113
+ }
114
+ catch (reason) {
115
+ const mutationError = toError(reason);
116
+ setError(mutationError);
117
+ throw mutationError;
118
+ }
119
+ finally {
120
+ pendingCount.current -= 1;
121
+ setLoading(pendingCount.current > 0);
122
+ }
123
+ }, [client]);
124
+ const reset = React.useCallback(() => {
125
+ setData(undefined);
126
+ setError(undefined);
127
+ }, []);
128
+ return { data, loading, error, mutate, reset };
129
+ };
130
+ exports.useOvokMutation = useOvokMutation;
131
+ const useResource = (type, id, options = {}) => {
132
+ var _a;
133
+ return (0, exports.useOvokQuery)((client, signal) => {
134
+ if (!id) {
135
+ return Promise.reject(new Error("A resource id is required."));
136
+ }
137
+ return client.fhir.read(type, id, { signal });
138
+ }, { queryKey: [type, id], enabled: (_a = options.enabled) !== null && _a !== void 0 ? _a : Boolean(id) });
139
+ };
140
+ exports.useResource = useResource;
141
+ const useSearch = (type, params = {}, options = {}) => (0, exports.useOvokQuery)((client, signal) => client.fhir.search(type, params, { signal }), { queryKey: [type, params], enabled: options.enabled });
142
+ exports.useSearch = useSearch;
143
+ const useSubscription = (subscribe) => {
144
+ const [subscription, setSubscription] = React.useState();
145
+ React.useEffect(() => {
146
+ let active = true;
147
+ const unsubscribe = subscribe((value) => {
148
+ if (active) {
149
+ setSubscription({ subscribe, value });
150
+ }
151
+ });
152
+ return () => {
153
+ active = false;
154
+ unsubscribe();
155
+ };
156
+ }, [subscribe]);
157
+ return (subscription === null || subscription === void 0 ? void 0 : subscription.subscribe) === subscribe ? subscription.value : undefined;
158
+ };
159
+ exports.useSubscription = useSubscription;
160
+ const toError = (reason) => (reason instanceof Error ? reason : new Error(String(reason)));
@@ -1,4 +1,6 @@
1
+ import type { OfflineMeasurementQueueApi } from "./offline-measurement-queue";
1
2
  import type { GetLatestObservationsByCodeParams } from "../../../client/observation/types/GetLatestObservationsByCodeParams";
3
+ import type { OvokStorage } from "../../storage/ovok-storage";
2
4
  import type { Bundle, Observation } from "@medplum/fhirtypes";
3
5
  import { observationCodesForTypes } from "../../../client/observation/methods/observationsToMeasurements";
4
6
  import { HttpClient } from "../../http/http-client";
@@ -15,6 +17,14 @@ export type SaveObservationsOptions = RequestOptions & {
15
17
  export type SaveObservationsResult = {
16
18
  status: "saved";
17
19
  response: Bundle;
20
+ } | {
21
+ status: "queued";
22
+ queueId: string;
23
+ };
24
+ type MeasurementStorage = {
25
+ storage: OvokStorage;
26
+ queueKey: string;
27
+ machineClientId: string | null;
18
28
  };
19
29
  export type LatestObservationsParams = GetLatestObservationsByCodeParams;
20
30
  export type LastObservationsQuery = {
@@ -26,7 +36,11 @@ export declare class MeasurementsResource {
26
36
  private readonly http;
27
37
  private readonly fhir;
28
38
  private readonly account;
29
- constructor(http: HttpClient, fhir: FhirResource, session: SessionSource | null);
39
+ private readonly offlineMeasurementQueue;
40
+ private readonly machineClientId;
41
+ private readonly session;
42
+ readonly offlineQueue: OfflineMeasurementQueueApi;
43
+ constructor(http: HttpClient, fhir: FhirResource, session: SessionSource | null, storageOptions: MeasurementStorage);
30
44
  save(params: BuildObservationsParams, options?: SaveObservationsOptions): Promise<SaveObservationsResult>;
31
45
  /** One reading, main observation first, created conditionally so a retry cannot duplicate it. */
32
46
  saveObservations(observations: Observation[], options?: SaveObservationsOptions): Promise<SaveObservationsResult>;
@@ -38,5 +52,7 @@ export declare class MeasurementsResource {
38
52
  private latestPatientId;
39
53
  /** A reading split over several requests stops once another account signs in, so none of it lands elsewhere. */
40
54
  private send;
55
+ private currentOwnerKey;
41
56
  private requireOwner;
42
57
  }
58
+ export {};
@@ -8,15 +8,22 @@ const query_string_1 = require("../../../client/query-string");
8
8
  const ovok_errors_1 = require("../../errors/ovok-errors");
9
9
  const url_1 = require("../../http/url");
10
10
  const observation_builder_1 = require("./observation-builder");
11
+ const offline_measurement_queue_2 = require("./offline-measurement-queue");
11
12
  const save_error_1 = require("./save-error");
12
13
  const signed_in_account_1 = require("./signed-in-account");
13
14
  const LATEST_OBSERVATIONS_PATH = "fhir/Observation/$lastn";
14
15
  const LATEST_PER_CODE = 1;
15
16
  class MeasurementsResource {
16
- constructor(http, fhir, session) {
17
+ constructor(http, fhir, session, storageOptions) {
17
18
  this.http = http;
18
19
  this.fhir = fhir;
20
+ this.session = session;
21
+ this.machineClientId = storageOptions.machineClientId;
19
22
  this.account = new signed_in_account_1.SignedInAccount(session);
23
+ this.offlineMeasurementQueue = new offline_measurement_queue_2.OfflineMeasurementQueue(storageOptions.storage, storageOptions.queueKey, () => this.currentOwnerKey(), (bundle, ownerKey, requestOptions) => this.send(bundle, requestOptions, ownerKey));
24
+ this.offlineQueue = {
25
+ flush: (options, requestOptions) => this.offlineMeasurementQueue.flush(options, requestOptions),
26
+ };
20
27
  }
21
28
  async save(params, options) {
22
29
  return this.saveObservations(await this.build(params), options);
@@ -25,8 +32,16 @@ class MeasurementsResource {
25
32
  async saveObservations(observations, options = {}) {
26
33
  const { identifierSystems, replace, ...requestOptions } = options;
27
34
  const entry = (0, observation_bundle_utils_1.linkObservationMembers)(observations).map((observation) => (0, offline_measurement_queue_1.entryForObservation)(observation, { identifierSystems, replace }));
28
- const response = await this.send({ resourceType: "Bundle", type: "transaction", entry }, requestOptions);
29
- return { status: "saved", response };
35
+ const ownerKey = await this.currentOwnerKey();
36
+ if (entry.length === 0 || ownerKey === null) {
37
+ const response = await this.send({ resourceType: "Bundle", type: "transaction", entry }, requestOptions, ownerKey);
38
+ return { status: "saved", response };
39
+ }
40
+ const queued = await this.offlineMeasurementQueue.enqueueAndFlush(entry, ownerKey, requestOptions);
41
+ const response = queued.flush.responses[queued.id];
42
+ return response
43
+ ? { status: "saved", response }
44
+ : { status: "queued", queueId: queued.id };
30
45
  }
31
46
  async build(params) {
32
47
  const subject = params.patientId ? `Patient/${params.patientId}` : await this.account.profileReference();
@@ -55,8 +70,7 @@ class MeasurementsResource {
55
70
  return params.patientId;
56
71
  }
57
72
  /** A reading split over several requests stops once another account signs in, so none of it lands elsewhere. */
58
- async send(bundle, options) {
59
- const owner = await this.account.key();
73
+ async send(bundle, options, owner) {
60
74
  try {
61
75
  return await (0, observation_bundle_utils_1.executeObservationBundle)(bundle, async (request) => {
62
76
  await this.requireOwner(owner);
@@ -67,8 +81,15 @@ class MeasurementsResource {
67
81
  throw (0, save_error_1.isBundle)(error) ? (0, save_error_1.saveErrorFrom)(error) : error;
68
82
  }
69
83
  }
84
+ async currentOwnerKey() {
85
+ const accountKey = await this.account.key();
86
+ if (accountKey) {
87
+ return `user:${accountKey}`;
88
+ }
89
+ return this.session === null && this.machineClientId ? `machine:${this.machineClientId}` : null;
90
+ }
70
91
  async requireOwner(owner) {
71
- if (owner !== (await this.account.key())) {
92
+ if (owner !== (await this.currentOwnerKey())) {
72
93
  throw new ovok_errors_1.OvokConflictError((0, ovok_errors_1.detailsFor)("The signed-in account changed during the save; the rest was not sent."));
73
94
  }
74
95
  }
@@ -0,0 +1,22 @@
1
+ import type { RequestOptions } from "../../http/transmit";
2
+ import type { Bundle } from "@medplum/fhirtypes";
3
+ export type OfflineMeasurementFlushOptions = {
4
+ includeExhausted?: boolean;
5
+ resetExhausted?: boolean;
6
+ };
7
+ export type OfflineMeasurementFailure = {
8
+ name: string;
9
+ code?: string;
10
+ status?: number;
11
+ };
12
+ export type OfflineMeasurementFlushResult = {
13
+ attempted: number;
14
+ saved: number;
15
+ queued: number;
16
+ exhausted: number;
17
+ responses: Record<string, Bundle>;
18
+ failures: Record<string, OfflineMeasurementFailure>;
19
+ };
20
+ export type OfflineMeasurementQueueApi = {
21
+ flush: (options?: OfflineMeasurementFlushOptions, requestOptions?: RequestOptions) => Promise<OfflineMeasurementFlushResult>;
22
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,25 @@
1
+ import type { OfflineMeasurementFlushOptions, OfflineMeasurementFlushResult } from "./offline-measurement-queue-types";
2
+ import type { RequestOptions } from "../../http/transmit";
3
+ import type { OvokStorage } from "../../storage/ovok-storage";
4
+ import type { Bundle, BundleEntry, Observation } from "@medplum/fhirtypes";
5
+ export type { OfflineMeasurementFailure, OfflineMeasurementFlushOptions, OfflineMeasurementFlushResult, OfflineMeasurementQueueApi, } from "./offline-measurement-queue-types";
6
+ type MeasurementTransport = (bundle: Bundle, ownerKey: string, requestOptions?: RequestOptions) => Promise<Bundle>;
7
+ type OwnerKey = () => Promise<string | null>;
8
+ export declare const offlineMeasurementStorageKey: (tenantCode: string, baseUrl: string) => string;
9
+ export declare class OfflineMeasurementQueue {
10
+ private readonly storage;
11
+ private readonly storageKey;
12
+ private readonly getOwnerKey;
13
+ private readonly transport;
14
+ private operation;
15
+ constructor(storage: OvokStorage, storageKey: string, getOwnerKey: OwnerKey, transport: MeasurementTransport);
16
+ enqueueAndFlush: (entries: BundleEntry<Observation>[], ownerKey: string, requestOptions?: RequestOptions) => Promise<{
17
+ id: string;
18
+ flush: OfflineMeasurementFlushResult;
19
+ }>;
20
+ flush: (options?: OfflineMeasurementFlushOptions, requestOptions?: RequestOptions) => Promise<OfflineMeasurementFlushResult>;
21
+ private runExclusive;
22
+ private readQueue;
23
+ private writeQueue;
24
+ private flushQueue;
25
+ }
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OfflineMeasurementQueue = exports.offlineMeasurementStorageKey = void 0;
4
+ const ovok_errors_1 = require("../../errors/ovok-errors");
5
+ const MAX_RETRIES = 8;
6
+ const RETRY_BACKOFF_MS = 5000;
7
+ const MAX_RETRY_BACKOFF_MS = 60 * 60 * 1000;
8
+ const offlineMeasurementStorageKey = (tenantCode, baseUrl) => {
9
+ const base = new URL(baseUrl);
10
+ const path = base.pathname.replace(/\/+$/g, "");
11
+ return `ovok:${tenantCode}:${base.origin}${path}:offline-measurements`;
12
+ };
13
+ exports.offlineMeasurementStorageKey = offlineMeasurementStorageKey;
14
+ const queueItemId = (entries) => entries.map(({ fullUrl }) => fullUrl !== null && fullUrl !== void 0 ? fullUrl : "").join("|");
15
+ const retryable = (error) => error instanceof ovok_errors_1.OvokConnectionError ||
16
+ error instanceof ovok_errors_1.OvokRateLimitError ||
17
+ error instanceof ovok_errors_1.OvokServiceError;
18
+ const summarizeFailure = (error) => {
19
+ const failure = error;
20
+ return {
21
+ name: typeof (failure === null || failure === void 0 ? void 0 : failure.name) === "string" ? failure.name : "Error",
22
+ ...(typeof (failure === null || failure === void 0 ? void 0 : failure.code) === "string" ? { code: failure.code } : {}),
23
+ ...(typeof (failure === null || failure === void 0 ? void 0 : failure.status) === "number" ? { status: failure.status } : {}),
24
+ };
25
+ };
26
+ class OfflineMeasurementQueue {
27
+ constructor(storage, storageKey, getOwnerKey, transport) {
28
+ this.storage = storage;
29
+ this.storageKey = storageKey;
30
+ this.getOwnerKey = getOwnerKey;
31
+ this.transport = transport;
32
+ this.operation = Promise.resolve();
33
+ this.enqueueAndFlush = (entries, ownerKey, requestOptions) => this.runExclusive(async () => {
34
+ const queue = await this.readQueue();
35
+ const id = queueItemId(entries);
36
+ const exists = queue.some((item) => item.id === id && item.ownerKey === ownerKey);
37
+ if (!exists) {
38
+ queue.push({
39
+ id,
40
+ ownerKey,
41
+ queuedAt: new Date().toISOString(),
42
+ attempt: 0,
43
+ nextAttemptAt: 0,
44
+ entries,
45
+ });
46
+ await this.writeQueue(queue);
47
+ }
48
+ return {
49
+ id,
50
+ flush: await this.flushQueue(queue, {}, requestOptions, true),
51
+ };
52
+ });
53
+ this.flush = (options = {}, requestOptions) => this.runExclusive(async () => this.flushQueue(await this.readQueue(), options, requestOptions, false));
54
+ this.runExclusive = (operation) => {
55
+ const next = this.operation.then(operation, operation);
56
+ this.operation = next.catch(() => undefined);
57
+ return next;
58
+ };
59
+ this.readQueue = async () => {
60
+ const serialized = await this.storage.getItem(this.storageKey);
61
+ if (serialized === null || serialized === "") {
62
+ return [];
63
+ }
64
+ const queue = JSON.parse(serialized);
65
+ if (!Array.isArray(queue)) {
66
+ throw new Error("Offline measurement queue is not an array.");
67
+ }
68
+ return queue;
69
+ };
70
+ this.writeQueue = async (queue) => {
71
+ if (queue.length === 0) {
72
+ await this.storage.removeItem(this.storageKey);
73
+ return;
74
+ }
75
+ const serialized = JSON.stringify(queue);
76
+ await this.storage.setItem(this.storageKey, serialized);
77
+ if ((await this.storage.getItem(this.storageKey)) !== serialized) {
78
+ throw new Error("Offline measurement queue could not be persisted.");
79
+ }
80
+ };
81
+ this.flushQueue = async (queue, options, requestOptions, throwNonRetryable) => {
82
+ const result = {
83
+ attempted: 0,
84
+ saved: 0,
85
+ queued: 0,
86
+ exhausted: 0,
87
+ responses: {},
88
+ failures: {},
89
+ };
90
+ let changed = false;
91
+ const now = Date.now();
92
+ for (const item of [...queue]) {
93
+ const ownerKey = await this.getOwnerKey();
94
+ if (!ownerKey || item.ownerKey !== ownerKey) {
95
+ result.queued++;
96
+ continue;
97
+ }
98
+ if (item.lastFailure) {
99
+ result.failures[item.id] = item.lastFailure;
100
+ }
101
+ if (item.exhausted && options.resetExhausted) {
102
+ item.attempt = 0;
103
+ item.exhausted = false;
104
+ item.nextAttemptAt = 0;
105
+ changed = true;
106
+ }
107
+ if ((item.exhausted && !options.includeExhausted) || item.nextAttemptAt > now) {
108
+ if (item.exhausted) {
109
+ result.exhausted++;
110
+ }
111
+ else {
112
+ result.queued++;
113
+ }
114
+ continue;
115
+ }
116
+ result.attempted++;
117
+ try {
118
+ const response = await this.transport({ resourceType: "Bundle", type: "transaction", entry: item.entries }, item.ownerKey, requestOptions);
119
+ queue.splice(queue.indexOf(item), 1);
120
+ result.saved++;
121
+ result.responses[item.id] = response;
122
+ delete result.failures[item.id];
123
+ changed = true;
124
+ }
125
+ catch (error) {
126
+ if (throwNonRetryable && !retryable(error)) {
127
+ queue.splice(queue.indexOf(item), 1);
128
+ await this.writeQueue(queue);
129
+ throw error;
130
+ }
131
+ item.attempt++;
132
+ item.exhausted = item.attempt >= MAX_RETRIES;
133
+ item.nextAttemptAt = item.exhausted
134
+ ? Number.MAX_SAFE_INTEGER
135
+ : Date.now() + Math.min(MAX_RETRY_BACKOFF_MS, RETRY_BACKOFF_MS * 2 ** (item.attempt - 1));
136
+ item.lastFailure = summarizeFailure(error);
137
+ result.failures[item.id] = item.lastFailure;
138
+ if (item.exhausted) {
139
+ result.exhausted++;
140
+ }
141
+ else {
142
+ result.queued++;
143
+ }
144
+ changed = true;
145
+ }
146
+ }
147
+ if (changed) {
148
+ await this.writeQueue(queue);
149
+ }
150
+ return result;
151
+ };
152
+ }
153
+ }
154
+ exports.OfflineMeasurementQueue = OfflineMeasurementQueue;
@@ -7,5 +7,6 @@ export declare class SessionResource {
7
7
  readonly savedAccounts: SavedAccountsResource;
8
8
  constructor(http: HttpClient, session: UserSession | null);
9
9
  onChange(listener: SessionChangeListener): () => void;
10
+ current(): Promise<Session | null>;
10
11
  }
11
12
  export type { Session };
@@ -11,5 +11,9 @@ class SessionResource {
11
11
  var _a, _b;
12
12
  return (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.onChange(listener)) !== null && _b !== void 0 ? _b : (() => undefined);
13
13
  }
14
+ current() {
15
+ var _a, _b;
16
+ return (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.current()) !== null && _b !== void 0 ? _b : Promise.resolve(null);
17
+ }
14
18
  }
15
19
  exports.SessionResource = SessionResource;
@@ -0,0 +1 @@
1
+ export * from "../../dist/next/react";
@@ -0,0 +1 @@
1
+ module.exports = require("../../dist/next/react/index.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovok/core",
3
- "version": "0.4.49-dev.497+7201066",
3
+ "version": "0.4.49-dev.499+db0e927",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -59,5 +59,16 @@
59
59
  "@medplum/fhirtypes": ">=3.2.22",
60
60
  "@medplum/react-hooks": ">=3.2.19",
61
61
  "react": "*"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "react": {
65
+ "optional": true
66
+ },
67
+ "@medplum/core": {
68
+ "optional": true
69
+ },
70
+ "@medplum/react-hooks": {
71
+ "optional": true
72
+ }
62
73
  }
63
74
  }