@ovok/core 0.4.21 → 0.4.22

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.
@@ -1,6 +1,6 @@
1
1
  import { IClientStorage } from "@medplum/core";
2
2
  import { Bundle, BundleEntry, Observation } from "@medplum/fhirtypes";
3
- declare const DEFAULT_STORAGE_KEY = "ovok.core.offline-measurements";
3
+ export declare const OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY = "ovok.core.offline-measurements";
4
4
  declare const IDENTIFIER_SYSTEM = "https://api.ovok.com/fhir/StructureDefinition/offline-measurement-id";
5
5
  export type OfflineMeasurementQueueOptions = {
6
6
  enabled?: boolean;
@@ -8,6 +8,8 @@ export type OfflineMeasurementQueueOptions = {
8
8
  maxRetries?: number;
9
9
  retryBackoffMs?: number;
10
10
  maxRetryBackoffMs?: number;
11
+ /** Returns the active account key. Queue items are delivered only to their owner. */
12
+ getOwnerKey?: () => string | undefined;
11
13
  };
12
14
  export type OfflineMeasurementFlushOptions = {
13
15
  includeExhausted?: boolean;
@@ -30,6 +32,7 @@ export declare class OfflineMeasurementQueue {
30
32
  private readonly maxRetries;
31
33
  private readonly retryBackoffMs;
32
34
  private readonly maxRetryBackoffMs;
35
+ private readonly getOwnerKey?;
33
36
  private operation;
34
37
  constructor(storage: IClientStorage, transport: MeasurementTransport, options?: OfflineMeasurementQueueOptions);
35
38
  enqueueAndFlush: (observations: Observation[]) => Promise<{
@@ -42,4 +45,4 @@ export declare class OfflineMeasurementQueue {
42
45
  private writeQueue;
43
46
  private flushQueue;
44
47
  }
45
- export { DEFAULT_STORAGE_KEY as DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY, IDENTIFIER_SYSTEM as OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM, entryForObservation, stableMeasurementId, };
48
+ export { OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY as DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY, IDENTIFIER_SYSTEM as OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM, entryForObservation, stableMeasurementId, };
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.stableMeasurementId = exports.entryForObservation = exports.OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM = exports.DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY = exports.OfflineMeasurementQueue = void 0;
4
- const DEFAULT_STORAGE_KEY = "ovok.core.offline-measurements";
5
- exports.DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY = DEFAULT_STORAGE_KEY;
3
+ exports.stableMeasurementId = exports.entryForObservation = exports.OFFLINE_MEASUREMENT_IDENTIFIER_SYSTEM = exports.DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY = exports.OfflineMeasurementQueue = exports.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY = void 0;
4
+ exports.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY = "ovok.core.offline-measurements";
5
+ exports.DEFAULT_OFFLINE_MEASUREMENT_QUEUE_KEY = exports.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
6
6
  const DEFAULT_MAX_RETRIES = 8;
7
7
  const DEFAULT_RETRY_BACKOFF_MS = 5000;
8
8
  const DEFAULT_MAX_RETRY_BACKOFF_MS = 60 * 60 * 1000;
@@ -82,14 +82,19 @@ class OfflineMeasurementQueue {
82
82
  this.transport = transport;
83
83
  this.operation = Promise.resolve();
84
84
  this.enqueueAndFlush = (observations) => this.runExclusive(async () => {
85
- var _a, _b;
85
+ var _a, _b, _c;
86
86
  await ((_b = (_a = this.storage).getInitPromise) === null || _b === void 0 ? void 0 : _b.call(_a));
87
87
  const queue = this.readQueue();
88
88
  const entries = observations.map(entryForObservation);
89
- const id = stableValueId(entries);
89
+ const ownerKey = (_c = this.getOwnerKey) === null || _c === void 0 ? void 0 : _c.call(this);
90
+ if (this.getOwnerKey && !ownerKey) {
91
+ throw new Error("Offline measurements require an active account to prevent cross-account delivery.");
92
+ }
93
+ const id = stableValueId({ ownerKey, entries });
90
94
  if (!queue.some((item) => item.id === id)) {
91
95
  queue.push({
92
96
  id,
97
+ ownerKey,
93
98
  queuedAt: new Date().toISOString(),
94
99
  attempt: 0,
95
100
  nextAttemptAt: 0,
@@ -130,6 +135,7 @@ class OfflineMeasurementQueue {
130
135
  }
131
136
  };
132
137
  this.flushQueue = async (queue, options = {}) => {
138
+ var _a;
133
139
  const result = {
134
140
  attempted: 0,
135
141
  saved: 0,
@@ -138,6 +144,7 @@ class OfflineMeasurementQueue {
138
144
  responses: {},
139
145
  };
140
146
  const now = Date.now();
147
+ const ownerKey = (_a = this.getOwnerKey) === null || _a === void 0 ? void 0 : _a.call(this);
141
148
  let changed = false;
142
149
  for (const item of [...queue]) {
143
150
  if (item.exhausted && options.resetExhausted) {
@@ -156,6 +163,10 @@ class OfflineMeasurementQueue {
156
163
  }
157
164
  continue;
158
165
  }
166
+ if (this.getOwnerKey && (!ownerKey || item.ownerKey !== ownerKey)) {
167
+ result.queued++;
168
+ continue;
169
+ }
159
170
  result.attempted++;
160
171
  const attempt = item.attempt + 1;
161
172
  try {
@@ -172,7 +183,7 @@ class OfflineMeasurementQueue {
172
183
  result.responses[item.id] = response;
173
184
  changed = true;
174
185
  }
175
- catch (_a) {
186
+ catch (_b) {
176
187
  item.attempt = attempt;
177
188
  item.exhausted = attempt >= this.maxRetries;
178
189
  item.nextAttemptAt = item.exhausted
@@ -193,10 +204,12 @@ class OfflineMeasurementQueue {
193
204
  }
194
205
  return result;
195
206
  };
196
- this.storageKey = (_a = options.storageKey) !== null && _a !== void 0 ? _a : DEFAULT_STORAGE_KEY;
207
+ this.storageKey =
208
+ (_a = options.storageKey) !== null && _a !== void 0 ? _a : exports.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
197
209
  this.maxRetries = Math.max(1, (_b = options.maxRetries) !== null && _b !== void 0 ? _b : DEFAULT_MAX_RETRIES);
198
210
  this.retryBackoffMs = Math.max(1, (_c = options.retryBackoffMs) !== null && _c !== void 0 ? _c : DEFAULT_RETRY_BACKOFF_MS);
199
211
  this.maxRetryBackoffMs = Math.max(this.retryBackoffMs, (_d = options.maxRetryBackoffMs) !== null && _d !== void 0 ? _d : DEFAULT_MAX_RETRY_BACKOFF_MS);
212
+ this.getOwnerKey = options.getOwnerKey;
200
213
  }
201
214
  }
202
215
  exports.OfflineMeasurementQueue = OfflineMeasurementQueue;
@@ -55,6 +55,11 @@ export declare class OvokClient extends MedplumClient {
55
55
  /** Flushes measurements queued while offline. Call this on connectivity/app-resume events. */
56
56
  flushOfflineMeasurementQueue(options?: OfflineMeasurementFlushOptions): Promise<OfflineMeasurementFlushResult>;
57
57
  private getOfflineMeasurementQueue;
58
+ /**
59
+ * Medplum clears its entire client storage after an unrecoverable 401. Keep
60
+ * queued measurements so they can be retried only after their owner signs in.
61
+ */
62
+ clear(): void;
58
63
  private executeConditionalObservationCreates;
59
64
  private executeObservationBundle;
60
65
  private bindMethods;
@@ -148,9 +148,38 @@ class OvokClient extends core_1.MedplumClient {
148
148
  if (!this.clientStorage) {
149
149
  throw new Error("Offline measurement queue requires a durable client storage adapter.");
150
150
  }
151
- (_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle) => this.executeObservationBundle(bundle), this.offlineQueueOptions));
151
+ (_a = this.offlineMeasurementQueue) !== null && _a !== void 0 ? _a : (this.offlineMeasurementQueue = new offline_1.OfflineMeasurementQueue(this.clientStorage, (bundle) => this.executeObservationBundle(bundle), {
152
+ ...this.offlineQueueOptions,
153
+ getOwnerKey: () => {
154
+ var _a, _b, _c;
155
+ const login = this.getActiveLogin();
156
+ const profileReference = (_a = login === null || login === void 0 ? void 0 : login.profile) === null || _a === void 0 ? void 0 : _a.reference;
157
+ if (!profileReference) {
158
+ return undefined;
159
+ }
160
+ return [
161
+ this.getBaseUrl(),
162
+ (_c = (_b = login === null || login === void 0 ? void 0 : login.project) === null || _b === void 0 ? void 0 : _b.reference) !== null && _c !== void 0 ? _c : "",
163
+ profileReference,
164
+ ].join("|");
165
+ },
166
+ }));
152
167
  return this.offlineMeasurementQueue;
153
168
  }
169
+ /**
170
+ * Medplum clears its entire client storage after an unrecoverable 401. Keep
171
+ * queued measurements so they can be retried only after their owner signs in.
172
+ */
173
+ clear() {
174
+ var _a;
175
+ const storage = this.clientStorage;
176
+ const queueKey = (_a = this.offlineQueueOptions.storageKey) !== null && _a !== void 0 ? _a : offline_1.OFFLINE_MEASUREMENT_QUEUE_STORAGE_KEY;
177
+ const queuedMeasurements = storage === null || storage === void 0 ? void 0 : storage.getObject(queueKey);
178
+ super.clear();
179
+ if (Array.isArray(queuedMeasurements)) {
180
+ storage === null || storage === void 0 ? void 0 : storage.setObject(queueKey, queuedMeasurements);
181
+ }
182
+ }
154
183
  async executeConditionalObservationCreates(observations) {
155
184
  const entries = observations.map(offline_1.entryForObservation);
156
185
  return this.executeObservationBundle({
@@ -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.21",
51
+ version: "0.4.22",
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.21",
3
+ "version": "0.4.22",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",