@ovok/core 0.4.38 → 0.4.40
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.
- package/dist/client/auth/session-fetch.js +5 -6
- package/dist/client/observation/measurement-fields.d.ts +10 -0
- package/dist/client/observation/measurement-fields.js +12 -0
- package/dist/client/observation/services/base-observation-service.d.ts +2 -0
- package/dist/client/observation/services/base-observation-service.js +9 -0
- package/dist/client/observation/services/creator.d.ts +2 -0
- package/dist/client/observation/services/creator.js +4 -0
- package/dist/client/observation/types/IObservationService.d.ts +3 -0
- package/dist/client/observation/types/service/MeasurementField.d.ts +11 -0
- package/dist/client/observation/types/service/MeasurementField.js +2 -0
- package/dist/client/ovok-client.d.ts +8 -0
- package/dist/client/ovok-client.js +50 -4
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -71,7 +71,7 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
71
71
|
: (0, session_http_1.withHttpErrorMetadata)(response);
|
|
72
72
|
};
|
|
73
73
|
const fetchWithRefreshRetry = async (url, options) => {
|
|
74
|
-
var _a, _b, _c, _d;
|
|
74
|
+
var _a, _b, _c, _d, _e;
|
|
75
75
|
const requestUrl = new URL(url, normalizedBaseUrl);
|
|
76
76
|
if (!isTokenRefresh(url, options)) {
|
|
77
77
|
const response = await fetchOnce(url, options);
|
|
@@ -166,10 +166,9 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
166
166
|
};
|
|
167
167
|
if (config.keepSessionOnRefreshUnavailable) {
|
|
168
168
|
refreshState.unavailable = true;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
throw lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : (0, session_http_1.refreshUnavailableError)(undefined);
|
|
169
|
+
// Medplum clears the login on any token response that isn't OK, so a 429 or 5xx goes back as a network error.
|
|
170
|
+
void ((_d = lastResponse === null || lastResponse === void 0 ? void 0 : lastResponse.body) === null || _d === void 0 ? void 0 : _d.cancel().catch(() => undefined));
|
|
171
|
+
throw lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : (0, session_http_1.refreshUnavailableError)(lastResponse);
|
|
173
172
|
}
|
|
174
173
|
unauthenticatedReason = reason;
|
|
175
174
|
markActiveAccountExpired();
|
|
@@ -179,7 +178,7 @@ function createSessionFetch({ config, storage, fetchImplementation, normalizedBa
|
|
|
179
178
|
terminalRefreshToken = login === null || login === void 0 ? void 0 : login.accessToken;
|
|
180
179
|
terminalRefreshError = lastNetworkError !== null && lastNetworkError !== void 0 ? lastNetworkError : (0, session_http_1.refreshUnavailableError)(undefined);
|
|
181
180
|
clearActiveLogin();
|
|
182
|
-
(
|
|
181
|
+
(_e = config.onUnauthenticated) === null || _e === void 0 ? void 0 : _e.call(config, reason);
|
|
183
182
|
unauthenticatedReason = undefined;
|
|
184
183
|
throw terminalRefreshError;
|
|
185
184
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { MeasurementTypeKey } from "../../types";
|
|
2
|
+
import { MeasurementField } from "./types/service/MeasurementField";
|
|
3
|
+
/**
|
|
4
|
+
* For each measurement type the SDK stores, its properties as their Observations store them: the
|
|
5
|
+
* code (LOINC), unit and decimals of each, the main one first. Apps read codes and units here rather
|
|
6
|
+
* than keeping their own table. Lipid panels, respiratory therapy and baby sleep list none: their
|
|
7
|
+
* values are components rather than fixed properties (see `URINE_ANALYTE_DEFINITIONS` for urine).
|
|
8
|
+
*/
|
|
9
|
+
export declare const MEASUREMENT_FIELDS: Readonly<Partial<Record<MeasurementTypeKey, readonly MeasurementField[]>>>;
|
|
10
|
+
export type { MeasurementField };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MEASUREMENT_FIELDS = void 0;
|
|
4
|
+
const creator_1 = require("./services/creator");
|
|
5
|
+
const services = creator_1.ObservationServiceCreator.getInstance();
|
|
6
|
+
/**
|
|
7
|
+
* For each measurement type the SDK stores, its properties as their Observations store them: the
|
|
8
|
+
* code (LOINC), unit and decimals of each, the main one first. Apps read codes and units here rather
|
|
9
|
+
* than keeping their own table. Lipid panels, respiratory therapy and baby sleep list none: their
|
|
10
|
+
* values are components rather than fixed properties (see `URINE_ANALYTE_DEFINITIONS` for urine).
|
|
11
|
+
*/
|
|
12
|
+
exports.MEASUREMENT_FIELDS = Object.fromEntries(services.getTypes().map((type) => [type, services.getService(type).getFields()]));
|
|
@@ -5,6 +5,7 @@ import { Sample } from "../types/generate-measurement/Sample";
|
|
|
5
5
|
import { ObservationValue } from "../types/generate-observation-body/ObservationValue";
|
|
6
6
|
import { IObservationService } from "../types/IObservationService";
|
|
7
7
|
import { Measurement } from "../types/measurement/Measurement";
|
|
8
|
+
import { MeasurementField } from "../types/service/MeasurementField";
|
|
8
9
|
import { ObservationMetadata } from "../types/service/ObservationMetadata";
|
|
9
10
|
export declare abstract class BaseObservationService<T extends Measurement> implements IObservationService {
|
|
10
11
|
protected abstract observationMetadata: ObservationMetadata<T>[];
|
|
@@ -12,6 +13,7 @@ export declare abstract class BaseObservationService<T extends Measurement> impl
|
|
|
12
13
|
protected abstract measurementTypeName: string;
|
|
13
14
|
abstract convertSamplesToMeasurement(samples: Sample[]): T;
|
|
14
15
|
abstract convertRecordToMeasurement(record: RecordResult<RecordType>): T;
|
|
16
|
+
getFields(): MeasurementField[];
|
|
15
17
|
protected getCastedMeasurement(measurement: Measurement): T;
|
|
16
18
|
generateValues(measurement: Measurement): ObservationValue[];
|
|
17
19
|
getObservationCodes(): CodeableConcept[];
|
|
@@ -3,6 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BaseObservationService = void 0;
|
|
4
4
|
const fhir_systems_1 = require("../fhir-systems");
|
|
5
5
|
class BaseObservationService {
|
|
6
|
+
getFields() {
|
|
7
|
+
return this.observationMetadata.map(({ valueKey, primaryCode, unit, toFixed, display }) => ({
|
|
8
|
+
key: String(valueKey),
|
|
9
|
+
code: primaryCode,
|
|
10
|
+
unit,
|
|
11
|
+
decimals: toFixed,
|
|
12
|
+
display,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
6
15
|
getCastedMeasurement(measurement) {
|
|
7
16
|
if (measurement.measurementTypeKey !== this.measurementTypeKey) {
|
|
8
17
|
throw new Error(`Measurement is not a ${this.measurementTypeName} measurement`);
|
|
@@ -5,5 +5,7 @@ export declare class ObservationServiceCreator {
|
|
|
5
5
|
private readonly services;
|
|
6
6
|
private constructor();
|
|
7
7
|
static getInstance(): ObservationServiceCreator;
|
|
8
|
+
/** The measurement types the SDK turns into Observations. */
|
|
9
|
+
getTypes(): MeasurementTypeKey[];
|
|
8
10
|
getService(type: MeasurementTypeKey): IObservationService;
|
|
9
11
|
}
|
|
@@ -32,6 +32,10 @@ class ObservationServiceCreator {
|
|
|
32
32
|
}
|
|
33
33
|
return ObservationServiceCreator.instance;
|
|
34
34
|
}
|
|
35
|
+
/** The measurement types the SDK turns into Observations. */
|
|
36
|
+
getTypes() {
|
|
37
|
+
return [...this.services.keys()];
|
|
38
|
+
}
|
|
35
39
|
getService(type) {
|
|
36
40
|
const service = this.services.get(type);
|
|
37
41
|
if (!service) {
|
|
@@ -4,12 +4,15 @@ import { Sample } from "./generate-measurement/Sample";
|
|
|
4
4
|
import { ObservationFragment } from "./generate-observation-body/ObservationFragment";
|
|
5
5
|
import { ObservationValue } from "./generate-observation-body/ObservationValue";
|
|
6
6
|
import { Measurement } from "./measurement/Measurement";
|
|
7
|
+
import { MeasurementField } from "./service/MeasurementField";
|
|
7
8
|
export interface IObservationService {
|
|
8
9
|
generateValues: (measurement: Measurement) => ObservationValue[];
|
|
9
10
|
generateObservationFragments?: (measurement: Measurement) => ObservationFragment[];
|
|
10
11
|
getObservationCodes: () => CodeableConcept[];
|
|
11
12
|
getAdditionalProperties: (measurement: Measurement) => Partial<Observation>[];
|
|
12
13
|
getUnits: () => string[];
|
|
14
|
+
/** The measurement's properties as their Observations store them; the first is the main one. */
|
|
15
|
+
getFields: () => MeasurementField[];
|
|
13
16
|
getMeasurementDisplay: (measurement: Measurement) => string;
|
|
14
17
|
getObservationDisplay: (observations: Observation[]) => string | undefined;
|
|
15
18
|
convertSamplesToMeasurement: (samples: Sample[]) => Measurement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** One property of a measurement, as its Observation stores it. */
|
|
2
|
+
export type MeasurementField = {
|
|
3
|
+
/** The property on the measurement: `systolic`, `bloodGlucose`, … */
|
|
4
|
+
key: string;
|
|
5
|
+
/** The code (LOINC) of the Observation that carries it. */
|
|
6
|
+
code: string;
|
|
7
|
+
unit: string;
|
|
8
|
+
/** The decimal places the SDK keeps when it writes the value. */
|
|
9
|
+
decimals: number;
|
|
10
|
+
display: string;
|
|
11
|
+
};
|
|
@@ -50,6 +50,8 @@ export declare class OvokClient extends MedplumClient {
|
|
|
50
50
|
private readonly keepSessionOnRefreshUnavailable;
|
|
51
51
|
private readonly refreshState;
|
|
52
52
|
private offlineMeasurementQueue;
|
|
53
|
+
private profileRetry;
|
|
54
|
+
private profileRetryMs;
|
|
53
55
|
/**
|
|
54
56
|
* @param config - The configuration for the client.
|
|
55
57
|
* @param config.socialLoginClientId - The social login client id to use for the client.
|
|
@@ -57,6 +59,12 @@ export declare class OvokClient extends MedplumClient {
|
|
|
57
59
|
constructor(config: OvokClientOptions);
|
|
58
60
|
get<T = unknown>(url: URL | string, options?: MedplumRequestOptions): ReadablePromise<T>;
|
|
59
61
|
refreshIfExpired(gracePeriod?: number): Promise<void>;
|
|
62
|
+
/** A profile load that failed is tried again now, rather than answering with its old error. */
|
|
63
|
+
getProfileAsync(): ReturnType<MedplumClient["getProfileAsync"]>;
|
|
64
|
+
clearActiveLogin(): void;
|
|
65
|
+
/** While signed in without a profile, a failed load is tried again: after 2 s, doubling to a minute. */
|
|
66
|
+
private retryProfileOnFailure;
|
|
67
|
+
private cancelProfileRetry;
|
|
60
68
|
post<T = unknown>(url: URL | string, body?: unknown, contentType?: string, options?: MedplumRequestOptions): Promise<T>;
|
|
61
69
|
/** Lists saved accounts without exposing their access or refresh tokens. */
|
|
62
70
|
getSavedAccounts(): OvokSavedAccount[];
|
|
@@ -69,6 +69,9 @@ var observation_bundle_utils_2 = require("./observation/observation-bundle-utils
|
|
|
69
69
|
Object.defineProperty(exports, "MAX_CONDITIONAL_OBSERVATION_ENTRIES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_CONDITIONAL_OBSERVATION_ENTRIES; } });
|
|
70
70
|
Object.defineProperty(exports, "MAX_OBSERVATION_BUNDLE_BYTES", { enumerable: true, get: function () { return observation_bundle_utils_2.MAX_OBSERVATION_BUNDLE_BYTES; } });
|
|
71
71
|
const isRetrySafePost = (pathname) => pathname === "/ai/fhir/search";
|
|
72
|
+
/** A failed profile load is tried again after this long, doubling up to the maximum. */
|
|
73
|
+
const PROFILE_RETRY_MS = 2000;
|
|
74
|
+
const PROFILE_RETRY_MAX_MS = 60000;
|
|
72
75
|
class OvokClient extends core_1.MedplumClient {
|
|
73
76
|
/**
|
|
74
77
|
* @param config - The configuration for the client.
|
|
@@ -92,7 +95,22 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
92
95
|
fetch: session.fetch,
|
|
93
96
|
});
|
|
94
97
|
this.socialLoginClientId = "";
|
|
98
|
+
this.profileRetryMs = PROFILE_RETRY_MS;
|
|
95
99
|
session.setClearActiveLogin(this.clearActiveLogin.bind(this));
|
|
100
|
+
// Medplum keeps a failed `auth/me` as the profile for good: `isLoading()` stays true and `getProfileAsync()`
|
|
101
|
+
// keeps rejecting. So a failed load is tried again while signed in.
|
|
102
|
+
const loader = this;
|
|
103
|
+
const refreshProfile = loader.refreshProfile.bind(this);
|
|
104
|
+
loader.refreshProfile = () => {
|
|
105
|
+
this.cancelProfileRetry();
|
|
106
|
+
const loading = refreshProfile();
|
|
107
|
+
this.retryProfileOnFailure(loading);
|
|
108
|
+
return loading;
|
|
109
|
+
};
|
|
110
|
+
// A saved session resumed inside super() started loading before the retry existed.
|
|
111
|
+
if (loader.profilePromise) {
|
|
112
|
+
this.retryProfileOnFailure(loader.profilePromise);
|
|
113
|
+
}
|
|
96
114
|
this.hasExplicitStorageAdapter = config.storage !== undefined;
|
|
97
115
|
this.clientOptions = config;
|
|
98
116
|
this.keepSessionOnRefreshUnavailable = config.keepSessionOnRefreshUnavailable === true;
|
|
@@ -131,7 +149,6 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
131
149
|
return request;
|
|
132
150
|
}
|
|
133
151
|
async refreshIfExpired(gracePeriod) {
|
|
134
|
-
var _a;
|
|
135
152
|
const activeLogin = this.getActiveLogin();
|
|
136
153
|
const accessToken = this.getAccessToken();
|
|
137
154
|
const expiresAt = accessToken ? (0, core_1.tryGetJwtExpiration)(accessToken) : undefined;
|
|
@@ -147,9 +164,7 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
147
164
|
!(activeAccount === null || activeAccount === void 0 ? void 0 : activeAccount.expired) &&
|
|
148
165
|
expiresAt !== undefined &&
|
|
149
166
|
expiresAt > Date.now()) {
|
|
150
|
-
|
|
151
|
-
this.setAccessToken(accessToken, activeLogin.refreshToken);
|
|
152
|
-
return;
|
|
167
|
+
return; // the still-valid access token carries the request
|
|
153
168
|
}
|
|
154
169
|
throw error;
|
|
155
170
|
}
|
|
@@ -157,6 +172,37 @@ class OvokClient extends core_1.MedplumClient {
|
|
|
157
172
|
this.refreshState.unavailable = false;
|
|
158
173
|
}
|
|
159
174
|
}
|
|
175
|
+
/** A profile load that failed is tried again now, rather than answering with its old error. */
|
|
176
|
+
getProfileAsync() {
|
|
177
|
+
return this.profileRetry
|
|
178
|
+
? this.refreshProfile()
|
|
179
|
+
: super.getProfileAsync();
|
|
180
|
+
}
|
|
181
|
+
clearActiveLogin() {
|
|
182
|
+
this.cancelProfileRetry();
|
|
183
|
+
super.clearActiveLogin();
|
|
184
|
+
}
|
|
185
|
+
/** While signed in without a profile, a failed load is tried again: after 2 s, doubling to a minute. */
|
|
186
|
+
retryProfileOnFailure(loading) {
|
|
187
|
+
loading.then(() => {
|
|
188
|
+
this.profileRetryMs = PROFILE_RETRY_MS;
|
|
189
|
+
}, () => {
|
|
190
|
+
var _a, _b;
|
|
191
|
+
if (!this.getActiveLogin() || this.getProfile()) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const retry = setTimeout(() => {
|
|
195
|
+
void this.refreshProfile().catch(() => undefined);
|
|
196
|
+
}, this.profileRetryMs);
|
|
197
|
+
(_b = (_a = retry).unref) === null || _b === void 0 ? void 0 : _b.call(_a); // never keeps a Node process alive
|
|
198
|
+
this.profileRetry = retry;
|
|
199
|
+
this.profileRetryMs = Math.min(this.profileRetryMs * 2, PROFILE_RETRY_MAX_MS);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
cancelProfileRetry() {
|
|
203
|
+
clearTimeout(this.profileRetry);
|
|
204
|
+
this.profileRetry = undefined;
|
|
205
|
+
}
|
|
160
206
|
post(url, body, contentType, options) {
|
|
161
207
|
const pathname = new URL(url.toString(), this.getBaseUrl()).pathname;
|
|
162
208
|
if (!isRetrySafePost(pathname)) {
|
|
@@ -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.
|
|
51
|
+
version: "0.4.40",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|
package/dist/index.d.ts
CHANGED
|
@@ -940,6 +940,7 @@ export * from "./hooks";
|
|
|
940
940
|
export * from "./utils";
|
|
941
941
|
export * from "./client/observation/services";
|
|
942
942
|
export * from "./client/observation/methods";
|
|
943
|
+
export * from "./client/observation/measurement-fields";
|
|
943
944
|
export { saveMeasurement } from "./client/observation/methods/saveMeasurement";
|
|
944
945
|
export * from "./conformance/capability-requirements";
|
|
945
946
|
export * from "./conformance/api-requirements";
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,7 @@ __exportStar(require("./hooks"), exports);
|
|
|
58
58
|
__exportStar(require("./utils"), exports);
|
|
59
59
|
__exportStar(require("./client/observation/services"), exports);
|
|
60
60
|
__exportStar(require("./client/observation/methods"), exports);
|
|
61
|
+
__exportStar(require("./client/observation/measurement-fields"), exports);
|
|
61
62
|
var saveMeasurement_1 = require("./client/observation/methods/saveMeasurement");
|
|
62
63
|
Object.defineProperty(exports, "saveMeasurement", { enumerable: true, get: function () { return saveMeasurement_1.saveMeasurement; } });
|
|
63
64
|
__exportStar(require("./conformance/capability-requirements"), exports);
|