@ovok/core 0.4.49-dev.496 → 0.4.49-dev.498

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.
Files changed (32) hide show
  1. package/dist/conformance/capability-requirements.js +1 -1
  2. package/dist/next/auth/saved-accounts-store.d.ts +26 -0
  3. package/dist/next/auth/saved-accounts-store.js +93 -0
  4. package/dist/next/auth/user-session.d.ts +13 -1
  5. package/dist/next/auth/user-session.js +106 -5
  6. package/dist/next/errors/ovok-account-error.d.ts +5 -0
  7. package/dist/next/errors/ovok-account-error.js +13 -0
  8. package/dist/next/http/http-client.d.ts +1 -0
  9. package/dist/next/http/http-client.js +3 -0
  10. package/dist/next/index.d.ts +6 -0
  11. package/dist/next/index.js +3 -1
  12. package/dist/next/ovok.d.ts +2 -0
  13. package/dist/next/ovok.js +13 -4
  14. package/dist/next/resources/invites/invites-resource.d.ts +6 -1
  15. package/dist/next/resources/invites/invites-resource.js +16 -1
  16. package/dist/next/resources/measurements/measurements-resource.d.ts +17 -1
  17. package/dist/next/resources/measurements/measurements-resource.js +27 -6
  18. package/dist/next/resources/measurements/offline-measurement-queue-types.d.ts +22 -0
  19. package/dist/next/resources/measurements/offline-measurement-queue-types.js +2 -0
  20. package/dist/next/resources/measurements/offline-measurement-queue.d.ts +25 -0
  21. package/dist/next/resources/measurements/offline-measurement-queue.js +154 -0
  22. package/dist/next/resources/session/saved-accounts-resource.d.ts +13 -0
  23. package/dist/next/resources/session/saved-accounts-resource.js +44 -0
  24. package/dist/next/resources/session/session-resource.d.ts +11 -0
  25. package/dist/next/resources/session/session-resource.js +15 -0
  26. package/dist/next/resources/slim/live-reads.d.ts +1 -0
  27. package/dist/next/resources/slim/live-reads.js +13 -2
  28. package/dist/next/resources/slim/slim-devices-resource.d.ts +2 -1
  29. package/dist/next/resources/slim/slim-devices-resource.js +2 -1
  30. package/dist/next/resources/slim/slim-resource.d.ts +2 -1
  31. package/dist/next/resources/slim/slim-resource.js +2 -2
  32. package/package.json +1 -1
@@ -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.496+c48b1bd",
51
+ version: "0.4.49-dev.498+92fd957",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
@@ -0,0 +1,26 @@
1
+ import { OvokStorage } from "../storage/ovok-storage";
2
+ import { OvokReference, Session } from "./session";
3
+ export type SavedAccount = {
4
+ id: string;
5
+ profile: OvokReference;
6
+ project: OvokReference;
7
+ active: boolean;
8
+ expired: boolean;
9
+ };
10
+ export type StoredSavedAccount = {
11
+ session: Session;
12
+ expired: boolean;
13
+ };
14
+ export declare class SavedAccountsStore {
15
+ private readonly storage;
16
+ private readonly key;
17
+ constructor(storage: OvokStorage, key: string);
18
+ list(activeAccountId: string | null): Promise<SavedAccount[]>;
19
+ get(accountId: string): Promise<StoredSavedAccount | null>;
20
+ save(session: Session): Promise<void>;
21
+ markExpired(accountId: string): Promise<void>;
22
+ remove(accountId: string): Promise<void>;
23
+ private read;
24
+ private write;
25
+ }
26
+ export declare const savedAccountsStoreFor: (storage: OvokStorage, tenantCode: string) => SavedAccountsStore;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.savedAccountsStoreFor = exports.SavedAccountsStore = void 0;
4
+ class SavedAccountsStore {
5
+ constructor(storage, key) {
6
+ this.storage = storage;
7
+ this.key = key;
8
+ }
9
+ async list(activeAccountId) {
10
+ return (await this.read()).map(({ session, expired }) => ({
11
+ id: session.profile.reference,
12
+ profile: session.profile,
13
+ project: session.project,
14
+ active: session.profile.reference === activeAccountId,
15
+ expired,
16
+ }));
17
+ }
18
+ async get(accountId) {
19
+ var _a;
20
+ return (_a = (await this.read()).find(({ session }) => session.profile.reference === accountId)) !== null && _a !== void 0 ? _a : null;
21
+ }
22
+ async save(session) {
23
+ const accounts = await this.read();
24
+ const saved = { session, expired: false };
25
+ const existingIndex = accounts.findIndex((account) => account.session.profile.reference === session.profile.reference);
26
+ if (existingIndex === -1) {
27
+ accounts.unshift(saved);
28
+ }
29
+ else {
30
+ accounts[existingIndex] = saved;
31
+ }
32
+ await this.write(accounts);
33
+ }
34
+ async markExpired(accountId) {
35
+ const accounts = await this.read();
36
+ const account = accounts.find(({ session }) => session.profile.reference === accountId);
37
+ if (!account) {
38
+ return;
39
+ }
40
+ account.session = { ...account.session, accessToken: "", refreshToken: null };
41
+ account.expired = true;
42
+ await this.write(accounts);
43
+ }
44
+ async remove(accountId) {
45
+ const accounts = await this.read();
46
+ await this.write(accounts.filter(({ session }) => session.profile.reference !== accountId));
47
+ }
48
+ async read() {
49
+ const raw = await this.storage.getItem(this.key);
50
+ if (raw === null) {
51
+ return [];
52
+ }
53
+ try {
54
+ const value = JSON.parse(raw);
55
+ return Array.isArray(value) ? value.filter(isStoredSavedAccount) : [];
56
+ }
57
+ catch (_a) {
58
+ return [];
59
+ }
60
+ }
61
+ async write(accounts) {
62
+ if (accounts.length === 0) {
63
+ await this.storage.removeItem(this.key);
64
+ return;
65
+ }
66
+ await this.storage.setItem(this.key, JSON.stringify(accounts));
67
+ }
68
+ }
69
+ exports.SavedAccountsStore = SavedAccountsStore;
70
+ const savedAccountsStoreFor = (storage, tenantCode) => new SavedAccountsStore(storage, `ovok:${tenantCode}:saved-accounts`);
71
+ exports.savedAccountsStoreFor = savedAccountsStoreFor;
72
+ const isStoredSavedAccount = (value) => {
73
+ if (typeof value !== "object" || value === null || !("session" in value)) {
74
+ return false;
75
+ }
76
+ const record = value;
77
+ const session = record.session;
78
+ if (typeof session !== "object" || session === null || typeof record.expired !== "boolean") {
79
+ return false;
80
+ }
81
+ const candidate = session;
82
+ return (typeof candidate.accessToken === "string" &&
83
+ (candidate.refreshToken === null || typeof candidate.refreshToken === "string") &&
84
+ typeof candidate.expiresAt === "number" &&
85
+ isReference(candidate.profile) &&
86
+ isReference(candidate.project));
87
+ };
88
+ const isReference = (value) => typeof value === "object" &&
89
+ value !== null &&
90
+ "reference" in value &&
91
+ typeof value.reference === "string" &&
92
+ "display" in value &&
93
+ (typeof value.display === "string" || value.display === null);
@@ -1,21 +1,32 @@
1
1
  import { OvokSettings } from "../config/ovok-config";
2
2
  import { Clock } from "./access-token";
3
3
  import { Credentials } from "./credentials";
4
+ import { SavedAccount, SavedAccountsStore, StoredSavedAccount } from "./saved-accounts-store";
4
5
  import { LoginTokens, Session } from "./session";
5
6
  import { SessionStore } from "./session-store";
6
7
  import { TokenEndpoint } from "./token-endpoint";
7
8
  type SessionSettings = Pick<OvokSettings, "clientId" | "onUnauthenticated">;
9
+ export type SessionChangeListener = (session: Session | null) => void;
8
10
  export declare class UserSession implements Credentials {
9
11
  private readonly store;
10
12
  private readonly endpoint;
11
13
  private readonly settings;
12
14
  private readonly now;
15
+ private readonly savedAccounts;
13
16
  private loaded;
14
17
  private pendingRefresh;
15
- constructor(store: SessionStore, endpoint: TokenEndpoint, settings: SessionSettings, now?: Clock);
18
+ private readonly changeListeners;
19
+ constructor(store: SessionStore, endpoint: TokenEndpoint, settings: SessionSettings, now?: Clock, savedAccounts?: SavedAccountsStore | null);
16
20
  current(): Promise<Session | null>;
21
+ onChange(listener: SessionChangeListener): () => void;
17
22
  begin(tokens: LoginTokens): Promise<Session>;
18
23
  end(): Promise<void>;
24
+ listSavedAccounts(): Promise<SavedAccount[]>;
25
+ getSavedAccount(accountId: string): Promise<StoredSavedAccount | null>;
26
+ switchSavedAccount(accountId: string): Promise<void>;
27
+ removeSavedAccount(accountId: string): Promise<void>;
28
+ markSavedAccountExpired(accountId: string): Promise<void>;
29
+ forSavedAccount(session: Session): UserSession;
19
30
  accessToken(): Promise<string | null>;
20
31
  refresh(): Promise<boolean>;
21
32
  private exchangeRefreshToken;
@@ -23,5 +34,6 @@ export declare class UserSession implements Credentials {
23
34
  private signOutIfRejected;
24
35
  private refreshForm;
25
36
  private save;
37
+ private notifyChange;
26
38
  }
27
39
  export {};
@@ -1,31 +1,120 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UserSession = void 0;
4
+ const ovok_account_error_1 = require("../errors/ovok-account-error");
4
5
  const ovok_errors_1 = require("../errors/ovok-errors");
6
+ const ovok_storage_1 = require("../storage/ovok-storage");
5
7
  const session_1 = require("./session");
8
+ const session_store_1 = require("./session-store");
6
9
  const REJECTED_REFRESH_STATUSES = new Set([400, 401, 403]);
7
10
  class UserSession {
8
- constructor(store, endpoint, settings, now = Date.now) {
11
+ constructor(store, endpoint, settings, now = Date.now, savedAccounts = null) {
9
12
  this.store = store;
10
13
  this.endpoint = endpoint;
11
14
  this.settings = settings;
12
15
  this.now = now;
16
+ this.savedAccounts = savedAccounts;
13
17
  this.loaded = null;
14
18
  this.pendingRefresh = null;
19
+ this.changeListeners = new Set();
15
20
  }
16
21
  current() {
17
- var _a;
18
- (_a = this.loaded) !== null && _a !== void 0 ? _a : (this.loaded = this.store.load());
22
+ if (this.loaded === null) {
23
+ const pending = this.store.load().catch((error) => {
24
+ if (this.loaded === pending) {
25
+ this.loaded = null;
26
+ }
27
+ throw error;
28
+ });
29
+ this.loaded = pending;
30
+ }
19
31
  return this.loaded;
20
32
  }
33
+ onChange(listener) {
34
+ this.changeListeners.add(listener);
35
+ return () => this.changeListeners.delete(listener);
36
+ }
21
37
  async begin(tokens) {
22
38
  const session = (0, session_1.sessionFromLogin)(tokens, this.now());
23
39
  await this.save(session);
40
+ this.notifyChange(session);
24
41
  return session;
25
42
  }
26
43
  async end() {
44
+ var _a;
45
+ const current = await this.current().catch(() => null);
27
46
  this.loaded = Promise.resolve(null);
28
47
  await this.store.clear();
48
+ if (current) {
49
+ await ((_a = this.savedAccounts) === null || _a === void 0 ? void 0 : _a.remove(current.profile.reference));
50
+ }
51
+ this.notifyChange(null);
52
+ }
53
+ async listSavedAccounts() {
54
+ var _a;
55
+ const current = await this.current();
56
+ const currentAccountId = (_a = current === null || current === void 0 ? void 0 : current.profile.reference) !== null && _a !== void 0 ? _a : null;
57
+ if (this.savedAccounts === null) {
58
+ return [];
59
+ }
60
+ const accounts = await this.savedAccounts.list(currentAccountId);
61
+ if (current && !accounts.some((account) => account.active)) {
62
+ await this.savedAccounts.save(current);
63
+ return this.savedAccounts.list(currentAccountId);
64
+ }
65
+ return accounts;
66
+ }
67
+ async getSavedAccount(accountId) {
68
+ var _a, _b;
69
+ return (_b = (_a = this.savedAccounts) === null || _a === void 0 ? void 0 : _a.get(accountId)) !== null && _b !== void 0 ? _b : null;
70
+ }
71
+ async switchSavedAccount(accountId) {
72
+ const account = await this.getSavedAccount(accountId);
73
+ if (account === null) {
74
+ throw new ovok_account_error_1.OvokAccountError("account_not_found");
75
+ }
76
+ if (account.expired) {
77
+ throw new ovok_account_error_1.OvokAccountError("account_expired");
78
+ }
79
+ const current = await this.current();
80
+ if ((current === null || current === void 0 ? void 0 : current.profile.reference) === accountId) {
81
+ return;
82
+ }
83
+ await this.store.save(account.session);
84
+ this.loaded = Promise.resolve(account.session);
85
+ this.notifyChange(account.session);
86
+ }
87
+ async removeSavedAccount(accountId) {
88
+ var _a, _b, _c;
89
+ const account = await this.getSavedAccount(accountId);
90
+ if (account === null) {
91
+ throw new ovok_account_error_1.OvokAccountError("account_not_found");
92
+ }
93
+ const current = await this.current();
94
+ const wasActive = (current === null || current === void 0 ? void 0 : current.profile.reference) === accountId;
95
+ if (wasActive) {
96
+ await this.store.clear();
97
+ this.loaded = Promise.resolve(null);
98
+ }
99
+ await ((_a = this.savedAccounts) === null || _a === void 0 ? void 0 : _a.remove(accountId));
100
+ if (wasActive) {
101
+ const nextAccount = ((_c = await ((_b = this.savedAccounts) === null || _b === void 0 ? void 0 : _b.list(null))) !== null && _c !== void 0 ? _c : []).find((saved) => !saved.expired);
102
+ if (nextAccount) {
103
+ await this.switchSavedAccount(nextAccount.id);
104
+ }
105
+ else {
106
+ this.notifyChange(null);
107
+ }
108
+ }
109
+ }
110
+ async markSavedAccountExpired(accountId) {
111
+ var _a;
112
+ await ((_a = this.savedAccounts) === null || _a === void 0 ? void 0 : _a.markExpired(accountId));
113
+ }
114
+ forSavedAccount(session) {
115
+ const isolated = new UserSession(new session_store_1.SessionStore(new ovok_storage_1.MemoryStorage(), "ovok:isolated-session"), this.endpoint, { ...this.settings, onUnauthenticated: () => undefined }, this.now);
116
+ isolated.loaded = Promise.resolve(session);
117
+ return isolated;
29
118
  }
30
119
  async accessToken() {
31
120
  var _a, _b;
@@ -61,10 +150,17 @@ class UserSession {
61
150
  }
62
151
  /** Only a refused refresh token signs the user out; an outage keeps the session for a later retry. */
63
152
  async signOutIfRejected(error) {
153
+ var _a;
64
154
  const status = error instanceof ovok_errors_1.OvokError ? error.status : null;
65
155
  const rejected = status !== null && REJECTED_REFRESH_STATUSES.has(status);
66
156
  if (rejected) {
67
- await this.end();
157
+ const current = await this.current();
158
+ if (current) {
159
+ await ((_a = this.savedAccounts) === null || _a === void 0 ? void 0 : _a.markExpired(current.profile.reference));
160
+ }
161
+ this.loaded = Promise.resolve(null);
162
+ await this.store.clear();
163
+ this.notifyChange(null);
68
164
  this.settings.onUnauthenticated({ type: "refresh-rejected", status });
69
165
  return false;
70
166
  }
@@ -75,8 +171,13 @@ class UserSession {
75
171
  return { grant_type: "refresh_token", refresh_token: refreshToken, ...client };
76
172
  }
77
173
  async save(session) {
78
- this.loaded = Promise.resolve(session);
174
+ var _a;
79
175
  await this.store.save(session);
176
+ await ((_a = this.savedAccounts) === null || _a === void 0 ? void 0 : _a.save(session));
177
+ this.loaded = Promise.resolve(session);
178
+ }
179
+ notifyChange(session) {
180
+ this.changeListeners.forEach((listener) => listener(session));
80
181
  }
81
182
  }
82
183
  exports.UserSession = UserSession;
@@ -0,0 +1,5 @@
1
+ export type OvokAccountErrorCode = "account_not_found" | "account_expired";
2
+ export declare class OvokAccountError extends Error {
3
+ readonly code: OvokAccountErrorCode;
4
+ constructor(code: OvokAccountErrorCode);
5
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OvokAccountError = void 0;
4
+ class OvokAccountError extends Error {
5
+ constructor(code) {
6
+ super(code === "account_not_found"
7
+ ? "Saved account was not found."
8
+ : "This account's session has expired. Sign in again.");
9
+ this.code = code;
10
+ this.name = "OvokAccountError";
11
+ }
12
+ }
13
+ exports.OvokAccountError = OvokAccountError;
@@ -21,6 +21,7 @@ export declare class HttpClient {
21
21
  constructor(context: HttpContext, basePath?: string);
22
22
  under(path: string): HttpClient;
23
23
  anonymous(): HttpClient;
24
+ withCredentials(credentials: Credentials): HttpClient;
24
25
  get<T>(path: string, options?: RequestOptions): Promise<T>;
25
26
  post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
26
27
  put<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
@@ -20,6 +20,9 @@ class HttpClient {
20
20
  anonymous() {
21
21
  return new HttpClient({ ...this.context, credentials: credentials_1.noCredentials }, this.basePath);
22
22
  }
23
+ withCredentials(credentials) {
24
+ return new HttpClient({ ...this.context, credentials }, this.basePath);
25
+ }
23
26
  get(path, options) {
24
27
  return this.send({ method: "GET", path }, options);
25
28
  }
@@ -3,17 +3,21 @@ export type { OvokConfig, OvokFetch, OvokUnauthenticatedReason } from "./config/
3
3
  export { MemoryStorage } from "./storage/ovok-storage";
4
4
  export type { OvokStorage } from "./storage/ovok-storage";
5
5
  export { OvokAuthenticationError, OvokConfigError, OvokConflictError, OvokConnectionError, OvokError, OvokNotFoundError, OvokPermissionError, OvokRateLimitError, OvokServiceError, OvokValidationError, } from "./errors/ovok-errors";
6
+ export { OvokAccountError } from "./errors/ovok-account-error";
6
7
  export type { OvokErrorDetails } from "./errors/ovok-errors";
7
8
  export type { RequestOptions } from "./http/transmit";
8
9
  export type { QueryParams, QueryValue } from "./http/url";
9
10
  export type { Page, PagePromise } from "./paging/page";
10
11
  export type { OvokReference, Session } from "./auth/session";
12
+ export type { SavedAccount } from "./auth/saved-accounts-store";
13
+ export type { SessionChangeListener } from "./auth/user-session";
11
14
  export type { LoginCredentials, LoginResult, MfaChallenge, ProfileType } from "./resources/auth-resource";
12
15
  export type { CreateOptions, PatchOperation } from "./resources/fhir-resource";
13
16
  export type { HealthCheck } from "./resources/health-resource";
14
17
  export type { PatientRegistration, PractitionerRegistration, RegisterResult } from "./resources/auth/registration";
15
18
  export type { SocialCredentials, SocialProvider } from "./resources/auth/social-login";
16
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";
17
21
  export type { BuildObservationsParams } from "./resources/measurements/observation-builder";
18
22
  export type { SignalsDeliveryRepair, SignalsDeliveryRepairResult, SignalsUndeliverable, SignalsUndeliverableResident, } from "./resources/signals/undeliverable-resource";
19
23
  export type { ProjectListQuery } from "./resources/projects/projects-resource";
@@ -47,6 +51,8 @@ export type { PatientResource } from "./resources/patient/patient-resource";
47
51
  export type { ProjectsResource } from "./resources/projects/projects-resource";
48
52
  export type { QuestionnaireResponsesResource, QuestionnairesResource, } from "./resources/questionnaires/questionnaires-resource";
49
53
  export type { SharingResource } from "./resources/sharing/sharing-resource";
54
+ export type { SavedAccountsResource } from "./resources/session/saved-accounts-resource";
55
+ export type { SessionResource } from "./resources/session/session-resource";
50
56
  export type { SignalsResource } from "./resources/signals/signals-resource";
51
57
  export type { SignalsGatewayRequest } from "./resources/signals/signals-gateway-resource";
52
58
  export type { SlimResource } from "./resources/slim/slim-resource";
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.OvokValidationError = exports.OvokServiceError = exports.OvokRateLimitError = exports.OvokPermissionError = exports.OvokNotFoundError = exports.OvokError = exports.OvokConnectionError = exports.OvokConflictError = exports.OvokConfigError = exports.OvokAuthenticationError = exports.MemoryStorage = exports.Ovok = void 0;
17
+ exports.OvokAccountError = exports.OvokValidationError = exports.OvokServiceError = exports.OvokRateLimitError = exports.OvokPermissionError = exports.OvokNotFoundError = exports.OvokError = exports.OvokConnectionError = exports.OvokConflictError = exports.OvokConfigError = exports.OvokAuthenticationError = exports.MemoryStorage = exports.Ovok = void 0;
18
18
  var ovok_1 = require("./ovok");
19
19
  Object.defineProperty(exports, "Ovok", { enumerable: true, get: function () { return ovok_1.Ovok; } });
20
20
  var ovok_storage_1 = require("./storage/ovok-storage");
@@ -30,4 +30,6 @@ Object.defineProperty(exports, "OvokPermissionError", { enumerable: true, get: f
30
30
  Object.defineProperty(exports, "OvokRateLimitError", { enumerable: true, get: function () { return ovok_errors_1.OvokRateLimitError; } });
31
31
  Object.defineProperty(exports, "OvokServiceError", { enumerable: true, get: function () { return ovok_errors_1.OvokServiceError; } });
32
32
  Object.defineProperty(exports, "OvokValidationError", { enumerable: true, get: function () { return ovok_errors_1.OvokValidationError; } });
33
+ var ovok_account_error_1 = require("./errors/ovok-account-error");
34
+ Object.defineProperty(exports, "OvokAccountError", { enumerable: true, get: function () { return ovok_account_error_1.OvokAccountError; } });
33
35
  __exportStar(require("../types/MeasurementTypes"), exports);
@@ -17,6 +17,7 @@ import { MeasurementsResource } from "./resources/measurements/measurements-reso
17
17
  import { PatientResource } from "./resources/patient/patient-resource";
18
18
  import { ProjectsResource } from "./resources/projects/projects-resource";
19
19
  import { QuestionnaireResponsesResource, QuestionnairesResource } from "./resources/questionnaires/questionnaires-resource";
20
+ import { SessionResource } from "./resources/session/session-resource";
20
21
  import { SharingResource } from "./resources/sharing/sharing-resource";
21
22
  import { SignalsResource } from "./resources/signals/signals-resource";
22
23
  import { SlimResource } from "./resources/slim/slim-resource";
@@ -25,6 +26,7 @@ import { WearablesResource } from "./resources/wearables/wearables-resource";
25
26
  /** One client per tenant; each namespace mirrors an area of the Ovok API. */
26
27
  export declare class Ovok {
27
28
  readonly auth: AuthResource;
29
+ readonly session: SessionResource;
28
30
  readonly fhir: FhirResource;
29
31
  readonly health: HealthResource;
30
32
  readonly patient: PatientResource;
package/dist/next/ovok.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Ovok = void 0;
4
4
  const client_credentials_1 = require("./auth/client-credentials");
5
+ const saved_accounts_store_1 = require("./auth/saved-accounts-store");
5
6
  const session_store_1 = require("./auth/session-store");
6
7
  const token_endpoint_1 = require("./auth/token-endpoint");
7
8
  const user_session_1 = require("./auth/user-session");
@@ -23,9 +24,11 @@ const invites_resource_1 = require("./resources/invites/invites-resource");
23
24
  const locales_resource_1 = require("./resources/localization/locales-resource");
24
25
  const localizations_resource_1 = require("./resources/localization/localizations-resource");
25
26
  const measurements_resource_1 = require("./resources/measurements/measurements-resource");
27
+ const offline_measurement_queue_1 = require("./resources/measurements/offline-measurement-queue");
26
28
  const patient_resource_1 = require("./resources/patient/patient-resource");
27
29
  const projects_resource_1 = require("./resources/projects/projects-resource");
28
30
  const questionnaires_resource_1 = require("./resources/questionnaires/questionnaires-resource");
31
+ const session_resource_1 = require("./resources/session/session-resource");
29
32
  const sharing_resource_1 = require("./resources/sharing/sharing-resource");
30
33
  const signals_resource_1 = require("./resources/signals/signals-resource");
31
34
  const slim_resource_1 = require("./resources/slim/slim-resource");
@@ -34,23 +37,29 @@ const wearables_resource_1 = require("./resources/wearables/wearables-resource")
34
37
  /** One client per tenant; each namespace mirrors an area of the Ovok API. */
35
38
  class Ovok {
36
39
  constructor(config) {
40
+ var _a, _b;
37
41
  const settings = (0, settings_1.readSettings)(config, (0, settings_1.currentEnvironment)());
38
42
  const credentials = credentialsFor(settings);
39
43
  const http = new http_client_1.HttpClient({ settings, credentials, retryPolicy: new retry_policy_1.RetryPolicy(settings.maxNetworkRetries) });
40
44
  const session = credentials instanceof user_session_1.UserSession ? credentials : null;
41
45
  const fhir = new fhir_resource_1.FhirResource(http.under(settings.fhirPath));
42
46
  this.auth = new auth_resource_1.AuthResource(http, settings.tenantCode, session, settings.clientId);
47
+ this.session = new session_resource_1.SessionResource(http, session);
43
48
  this.fhir = fhir;
44
49
  this.health = new health_resource_1.HealthResource(http.anonymous());
45
50
  this.patient = new patient_resource_1.PatientResource(http, fhir);
46
- 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
+ });
47
56
  this.questionnaires = new questionnaires_resource_1.QuestionnairesResource(http);
48
57
  this.questionnaireResponses = new questionnaires_resource_1.QuestionnaireResponsesResource(http, fhir);
49
58
  this.consent = new consent_resource_1.ConsentResource(http);
50
- this.invites = new invites_resource_1.InvitesResource(http);
59
+ this.invites = new invites_resource_1.InvitesResource(http, session);
51
60
  this.sharing = new sharing_resource_1.SharingResource(http);
52
61
  this.signals = new signals_resource_1.SignalsResource(http);
53
- this.slim = new slim_resource_1.SlimResource(http);
62
+ this.slim = new slim_resource_1.SlimResource(http, session);
54
63
  this.projects = new projects_resource_1.ProjectsResource(http);
55
64
  this.accounts = new accounts_resource_1.AccountsResource(http);
56
65
  this.locales = new locales_resource_1.LocalesResource(http);
@@ -70,5 +79,5 @@ const credentialsFor = (settings) => {
70
79
  const tokenEndpoint = new token_endpoint_1.TokenEndpoint(settings);
71
80
  return settings.machineClient
72
81
  ? new client_credentials_1.ClientCredentials(tokenEndpoint, settings.machineClient)
73
- : new user_session_1.UserSession((0, session_store_1.sessionStoreFor)(settings.storage, settings.tenantCode), tokenEndpoint, settings);
82
+ : new user_session_1.UserSession((0, session_store_1.sessionStoreFor)(settings.storage, settings.tenantCode), tokenEndpoint, settings, Date.now, (0, saved_accounts_store_1.savedAccountsStoreFor)(settings.storage, settings.tenantCode));
74
83
  };
@@ -1,10 +1,13 @@
1
1
  import type { AcceptInviteBody, AcceptRecordShareBody, InviteBody, RecordShareAccepted, RecordShareBody } from "../../../client/invites/types";
2
2
  import type { PatientSharingPractitionerShare } from "../../../client/patient-sharing/types";
3
+ import type { UserSession } from "../../auth/user-session";
3
4
  import { HttpClient } from "../../http/http-client";
5
+ import { RequestOptions } from "../../http/transmit";
4
6
  import { InviteFlowResource } from "./invite-flow-resource";
5
7
  export type RecordShareFlow = InviteFlowResource<RecordShareBody, AcceptRecordShareBody, RecordShareAccepted>;
6
8
  export type PersonInviteFlow = InviteFlowResource<InviteBody, AcceptInviteBody, PatientSharingPractitionerShare>;
7
9
  export declare class InvitesResource {
10
+ private readonly session;
8
11
  /** A patient shares their record with a practitioner, or some codes with a carer. */
9
12
  readonly recordShares: RecordShareFlow;
10
13
  /** A practitioner invites a patient into their care. */
@@ -13,5 +16,7 @@ export declare class InvitesResource {
13
16
  readonly practitioners: PersonInviteFlow;
14
17
  /** @deprecated Use `care`; the route sunsets on 2026-12-01. */
15
18
  readonly patients: PersonInviteFlow;
16
- constructor(http: HttpClient);
19
+ constructor(http: HttpClient, session?: UserSession | null);
20
+ /** Accepts a record share as a practitioner or a care invite as a patient. */
21
+ accept(body: AcceptInviteBody, options?: RequestOptions): Promise<PatientSharingPractitionerShare>;
17
22
  }
@@ -1,14 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InvitesResource = void 0;
4
+ const ovok_errors_1 = require("../../errors/ovok-errors");
4
5
  const invite_flow_resource_1 = require("./invite-flow-resource");
5
6
  const INVITES_PATH = "v1/invites";
6
7
  class InvitesResource {
7
- constructor(http) {
8
+ constructor(http, session = null) {
9
+ this.session = session;
8
10
  this.recordShares = new invite_flow_resource_1.InviteFlowResource(http, `${INVITES_PATH}/record-share`);
9
11
  this.care = new invite_flow_resource_1.InviteFlowResource(http, `${INVITES_PATH}/care`);
10
12
  this.practitioners = new invite_flow_resource_1.InviteFlowResource(http, `${INVITES_PATH}/practitioner`);
11
13
  this.patients = new invite_flow_resource_1.InviteFlowResource(http, `${INVITES_PATH}/patient`);
12
14
  }
15
+ /** Accepts a record share as a practitioner or a care invite as a patient. */
16
+ async accept(body, options) {
17
+ var _a, _b;
18
+ const profileReference = (_b = (await ((_a = this.session) === null || _a === void 0 ? void 0 : _a.current()))) === null || _b === void 0 ? void 0 : _b.profile.reference;
19
+ const profileType = profileReference === null || profileReference === void 0 ? void 0 : profileReference.split("/", 1)[0];
20
+ if (profileType === "Practitioner") {
21
+ return this.recordShares.accept(body, options);
22
+ }
23
+ if (profileType === "Patient") {
24
+ return this.care.accept(body, options);
25
+ }
26
+ throw new ovok_errors_1.OvokConfigError((0, ovok_errors_1.detailsFor)("Accepting an invite requires a signed-in Patient or Practitioner profile."));
27
+ }
13
28
  }
14
29
  exports.InvitesResource = InvitesResource;
@@ -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;
@@ -0,0 +1,13 @@
1
+ import { SavedAccount } from "../../auth/saved-accounts-store";
2
+ import { UserSession } from "../../auth/user-session";
3
+ import { HttpClient } from "../../http/http-client";
4
+ import { RequestOptions } from "../../http/transmit";
5
+ export declare class SavedAccountsResource {
6
+ private readonly http;
7
+ private readonly session;
8
+ constructor(http: HttpClient, session: UserSession | null);
9
+ list(): Promise<SavedAccount[]>;
10
+ switch(accountId: string): Promise<void>;
11
+ remove(accountId: string, options?: RequestOptions): Promise<void>;
12
+ private userSession;
13
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SavedAccountsResource = void 0;
4
+ const ovok_account_error_1 = require("../../errors/ovok-account-error");
5
+ const ovok_errors_1 = require("../../errors/ovok-errors");
6
+ const CURRENT_SESSION_PATH = "auth/session/current";
7
+ class SavedAccountsResource {
8
+ constructor(http, session) {
9
+ this.http = http;
10
+ this.session = session;
11
+ }
12
+ list() {
13
+ return this.userSession().listSavedAccounts();
14
+ }
15
+ switch(accountId) {
16
+ return this.userSession().switchSavedAccount(accountId);
17
+ }
18
+ async remove(accountId, options) {
19
+ const session = this.userSession();
20
+ const account = await session.getSavedAccount(accountId);
21
+ if (account === null) {
22
+ throw new ovok_account_error_1.OvokAccountError("account_not_found");
23
+ }
24
+ if (!account.expired) {
25
+ const isolatedSession = session.forSavedAccount(account.session);
26
+ try {
27
+ await this.http.withCredentials(isolatedSession).delete(CURRENT_SESSION_PATH, options);
28
+ }
29
+ catch (error) {
30
+ if (!(error instanceof ovok_errors_1.OvokError && error.status === 401) && await isolatedSession.current() !== null) {
31
+ throw error;
32
+ }
33
+ }
34
+ }
35
+ await session.removeSavedAccount(accountId);
36
+ }
37
+ userSession() {
38
+ if (this.session === null) {
39
+ throw new ovok_errors_1.OvokConfigError((0, ovok_errors_1.detailsFor)("Saved accounts need a user session."));
40
+ }
41
+ return this.session;
42
+ }
43
+ }
44
+ exports.SavedAccountsResource = SavedAccountsResource;
@@ -0,0 +1,11 @@
1
+ import type { Session } from "../../auth/session";
2
+ import type { SessionChangeListener, UserSession } from "../../auth/user-session";
3
+ import { HttpClient } from "../../http/http-client";
4
+ import { SavedAccountsResource } from "./saved-accounts-resource";
5
+ export declare class SessionResource {
6
+ private readonly session;
7
+ readonly savedAccounts: SavedAccountsResource;
8
+ constructor(http: HttpClient, session: UserSession | null);
9
+ onChange(listener: SessionChangeListener): () => void;
10
+ }
11
+ export type { Session };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionResource = void 0;
4
+ const saved_accounts_resource_1 = require("./saved-accounts-resource");
5
+ class SessionResource {
6
+ constructor(http, session) {
7
+ this.session = session;
8
+ this.savedAccounts = new saved_accounts_resource_1.SavedAccountsResource(http, session);
9
+ }
10
+ onChange(listener) {
11
+ var _a, _b;
12
+ return (_b = (_a = this.session) === null || _a === void 0 ? void 0 : _a.onChange(listener)) !== null && _b !== void 0 ? _b : (() => undefined);
13
+ }
14
+ }
15
+ exports.SessionResource = SessionResource;
@@ -5,6 +5,7 @@ export declare class LiveReads {
5
5
  private readonly http;
6
6
  private readonly inFlight;
7
7
  constructor(http: HttpClient);
8
+ clear(): void;
8
9
  get<T>(path: string, options?: RequestOptions): Promise<T>;
9
10
  private share;
10
11
  private start;
@@ -20,11 +20,17 @@ class SharedRead {
20
20
  this.subscribers -= 1;
21
21
  const abandoned = this.pending && this.subscribers === 0;
22
22
  if (abandoned) {
23
- this.finish();
24
- this.controller.abort();
23
+ this.abort();
25
24
  }
26
25
  }
26
+ abort() {
27
+ this.finish();
28
+ this.controller.abort();
29
+ }
27
30
  finish() {
31
+ if (!this.pending) {
32
+ return;
33
+ }
28
34
  this.pending = false;
29
35
  this.onDone();
30
36
  }
@@ -35,6 +41,11 @@ class LiveReads {
35
41
  this.http = http;
36
42
  this.inFlight = new Map();
37
43
  }
44
+ clear() {
45
+ const reads = [...this.inFlight.values()];
46
+ this.inFlight.clear();
47
+ reads.forEach((read) => read.abort());
48
+ }
38
49
  get(path, options = {}) {
39
50
  const shareable = Object.keys(options).every((key) => key === SIGNAL_OPTION);
40
51
  return shareable ? this.share(path, options.signal) : this.http.get(path, options);
@@ -1,4 +1,5 @@
1
1
  import type { SlimAssignDevice, SlimCreateDevice, SlimDevice, SlimDeviceListItem, SlimDeviceListQuery, SlimDeviceTelemetry, SlimLiveQuery, SlimLiveTelemetry, SlimObservationIds, SlimUpdateDevice } from "../../../client/slim/types";
2
+ import type { UserSession } from "../../auth/user-session";
2
3
  import { HttpClient } from "../../http/http-client";
3
4
  import { RequestOptions } from "../../http/transmit";
4
5
  import { PagePromise } from "../../paging/page";
@@ -8,7 +9,7 @@ export type SlimObservationIdsByDevice = Record<string, SlimObservationIds>;
8
9
  export declare class SlimDevicesResource {
9
10
  private readonly http;
10
11
  private readonly liveReads;
11
- constructor(http: HttpClient);
12
+ constructor(http: HttpClient, session?: UserSession | null);
12
13
  list(params?: SlimDeviceListQuery, options?: RequestOptions): PagePromise<SlimDeviceListItem>;
13
14
  retrieve(deviceId: string, options?: RequestOptions): Promise<SlimDevice>;
14
15
  /** 409 when another customer holds the serial; 422 when this project already uses it. */
@@ -20,9 +20,10 @@ const LIVE_COUNT_MESSAGE = `count must be an integer from ${MIN_LIVE_COUNT} to $
20
20
  const devicePath = (deviceId) => `${DEVICES_PATH}/${(0, url_1.pathSegment)(deviceId)}`;
21
21
  const telemetryPath = (deviceId) => `${TELEMETRY_PATH}/${(0, url_1.pathSegment)(deviceId)}`;
22
22
  class SlimDevicesResource {
23
- constructor(http) {
23
+ constructor(http, session = null) {
24
24
  this.http = http;
25
25
  this.liveReads = new live_reads_1.LiveReads(http);
26
+ session === null || session === void 0 ? void 0 : session.onChange(() => this.liveReads.clear());
26
27
  }
27
28
  list(params = {}, options) {
28
29
  return (0, numbered_pages_1.numberedRoutePages)((path) => this.http.get(path, options), DEVICES_PATH, params);
@@ -1,4 +1,5 @@
1
1
  import type { SlimHealth } from "../../../client/slim/types";
2
+ import type { UserSession } from "../../auth/user-session";
2
3
  import { HttpClient } from "../../http/http-client";
3
4
  import { RequestOptions } from "../../http/transmit";
4
5
  import { SlimChangeHistoryResource } from "./slim-change-history-resource";
@@ -33,6 +34,6 @@ export declare class SlimResource {
33
34
  readonly organizations: SlimOrganizationsResource;
34
35
  readonly practitioners: SlimPractitionersResource;
35
36
  readonly users: SlimUsersResource;
36
- constructor(http: HttpClient);
37
+ constructor(http: HttpClient, session?: UserSession | null);
37
38
  health(options?: RequestOptions): Promise<SlimHealth>;
38
39
  }
@@ -19,10 +19,10 @@ const slim_users_resource_1 = require("./slim-users-resource");
19
19
  const slim_zones_resource_1 = require("./slim-zones-resource");
20
20
  const HEALTH_PATH = `${slim_paths_1.SLIM_PATH}/health`;
21
21
  class SlimResource {
22
- constructor(http) {
22
+ constructor(http, session = null) {
23
23
  this.http = http;
24
24
  this.patients = new slim_patients_resource_1.SlimPatientsResource(http);
25
- this.devices = new slim_devices_resource_1.SlimDevicesResource(http);
25
+ this.devices = new slim_devices_resource_1.SlimDevicesResource(http, session);
26
26
  this.notifications = new slim_notifications_resource_1.SlimNotificationsResource(http);
27
27
  this.thresholds = new slim_thresholds_resource_1.SlimThresholdsResource(http);
28
28
  this.trends = new slim_trends_resource_1.SlimTrendsResource(http);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovok/core",
3
- "version": "0.4.49-dev.496+c48b1bd",
3
+ "version": "0.4.49-dev.498+92fd957",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",