@ovok/core 0.4.38 → 0.4.39

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.
@@ -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
- if (lastResponse) {
170
- return lastResponse;
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
- (_d = config.onUnauthenticated) === null || _d === void 0 ? void 0 : _d.call(config, reason);
181
+ (_e = config.onUnauthenticated) === null || _e === void 0 ? void 0 : _e.call(config, reason);
183
182
  unauthenticatedReason = undefined;
184
183
  throw terminalRefreshError;
185
184
  };
@@ -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
- (_a = this.clientStorage) === null || _a === void 0 ? void 0 : _a.setObject(account_storage_1.ACTIVE_LOGIN_STORAGE_KEY, activeLogin);
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.38",
51
+ version: "0.4.39",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovok/core",
3
- "version": "0.4.38",
3
+ "version": "0.4.39",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",