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

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.498+92fd957",
51
+ version: "0.4.49-dev.500+07ba725",
52
52
  },
53
53
  fhirVersion: "4.0.1",
54
54
  format: ["json"],
@@ -8,6 +8,7 @@ export type { OvokErrorDetails } from "./errors/ovok-errors";
8
8
  export type { RequestOptions } from "./http/transmit";
9
9
  export type { QueryParams, QueryValue } from "./http/url";
10
10
  export type { Page, PagePromise } from "./paging/page";
11
+ export type { RealtimeNotification, RealtimeNotificationHandler, RealtimeUnsubscribe } from "./resources/realtime/realtime-resource";
11
12
  export type { OvokReference, Session } from "./auth/session";
12
13
  export type { SavedAccount } from "./auth/saved-accounts-store";
13
14
  export type { SessionChangeListener } from "./auth/user-session";
@@ -42,6 +43,7 @@ export type { ContentResource } from "./resources/content/content-resource";
42
43
  export type { DocumentsResource } from "./resources/documents/documents-resource";
43
44
  export type { EmailResource } from "./resources/email/email-resource";
44
45
  export type { FhirResource } from "./resources/fhir-resource";
46
+ export type { RealtimeResource } from "./resources/realtime/realtime-resource";
45
47
  export type { HealthResource } from "./resources/health-resource";
46
48
  export type { InvitesResource } from "./resources/invites/invites-resource";
47
49
  export type { LocalesResource } from "./resources/localization/locales-resource";
@@ -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 { RealtimeResource } from "./resources/realtime/realtime-resource";
20
21
  import { SessionResource } from "./resources/session/session-resource";
21
22
  import { SharingResource } from "./resources/sharing/sharing-resource";
22
23
  import { SignalsResource } from "./resources/signals/signals-resource";
@@ -33,6 +34,7 @@ export declare class Ovok {
33
34
  readonly measurements: MeasurementsResource;
34
35
  readonly questionnaires: QuestionnairesResource;
35
36
  readonly questionnaireResponses: QuestionnaireResponsesResource;
37
+ readonly realtime: RealtimeResource;
36
38
  readonly consent: ConsentResource;
37
39
  readonly invites: InvitesResource;
38
40
  readonly sharing: SharingResource;
package/dist/next/ovok.js CHANGED
@@ -28,6 +28,7 @@ const offline_measurement_queue_1 = require("./resources/measurements/offline-me
28
28
  const patient_resource_1 = require("./resources/patient/patient-resource");
29
29
  const projects_resource_1 = require("./resources/projects/projects-resource");
30
30
  const questionnaires_resource_1 = require("./resources/questionnaires/questionnaires-resource");
31
+ const realtime_resource_1 = require("./resources/realtime/realtime-resource");
31
32
  const session_resource_1 = require("./resources/session/session-resource");
32
33
  const sharing_resource_1 = require("./resources/sharing/sharing-resource");
33
34
  const signals_resource_1 = require("./resources/signals/signals-resource");
@@ -42,10 +43,12 @@ class Ovok {
42
43
  const credentials = credentialsFor(settings);
43
44
  const http = new http_client_1.HttpClient({ settings, credentials, retryPolicy: new retry_policy_1.RetryPolicy(settings.maxNetworkRetries) });
44
45
  const session = credentials instanceof user_session_1.UserSession ? credentials : null;
45
- const fhir = new fhir_resource_1.FhirResource(http.under(settings.fhirPath));
46
+ const fhirHttp = http.under(settings.fhirPath);
47
+ const fhir = new fhir_resource_1.FhirResource(fhirHttp);
46
48
  this.auth = new auth_resource_1.AuthResource(http, settings.tenantCode, session, settings.clientId);
47
49
  this.session = new session_resource_1.SessionResource(http, session);
48
50
  this.fhir = fhir;
51
+ this.realtime = new realtime_resource_1.RealtimeResource(fhirHttp, fhir, settings.baseUrl, this.session);
49
52
  this.health = new health_resource_1.HealthResource(http.anonymous());
50
53
  this.patient = new patient_resource_1.PatientResource(http, fhir);
51
54
  this.measurements = new measurements_resource_1.MeasurementsResource(http, fhir, session, {
@@ -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)));
@@ -0,0 +1,7 @@
1
+ import { HttpClient } from "../../http/http-client";
2
+ export type RealtimeBinding = {
3
+ token: string;
4
+ url: string;
5
+ expiresAt: number;
6
+ };
7
+ export declare const fetchRealtimeBinding: (http: HttpClient, subscriptionId: string, baseUrl: string) => Promise<RealtimeBinding>;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchRealtimeBinding = void 0;
4
+ const ovok_errors_1 = require("../../errors/ovok-errors");
5
+ const url_1 = require("../../http/url");
6
+ const SOCKET_PATH = "/ws/subscriptions-r4";
7
+ const fetchRealtimeBinding = async (http, subscriptionId, baseUrl) => {
8
+ var _a, _b, _c, _d, _e, _f;
9
+ const parameters = await http.send({
10
+ method: "GET",
11
+ path: `Subscription/${(0, url_1.pathSegment)(subscriptionId)}/$get-ws-binding-token`,
12
+ headers: { Accept: "application/fhir+json", "Cache-Control": "no-cache" },
13
+ });
14
+ const token = (_b = (_a = parameters.parameter) === null || _a === void 0 ? void 0 : _a.find((parameter) => parameter.name === "token")) === null || _b === void 0 ? void 0 : _b.valueString;
15
+ const url = (_d = (_c = parameters.parameter) === null || _c === void 0 ? void 0 : _c.find((parameter) => parameter.name === "websocket-url")) === null || _d === void 0 ? void 0 : _d.valueUrl;
16
+ const expires = (_f = (_e = parameters.parameter) === null || _e === void 0 ? void 0 : _e.find((parameter) => parameter.name === "expiration")) === null || _f === void 0 ? void 0 : _f.valueDateTime;
17
+ if (!token || !url || !expires) {
18
+ throw new ovok_errors_1.OvokServiceError((0, ovok_errors_1.detailsFor)("The realtime binding response is missing its token, URL, or expiry."));
19
+ }
20
+ const socketUrl = apiSocketUrl(url, baseUrl);
21
+ const expiresAt = Date.parse(expires);
22
+ if (!Number.isFinite(expiresAt)) {
23
+ throw new ovok_errors_1.OvokServiceError((0, ovok_errors_1.detailsFor)("The realtime binding response has an invalid expiry."));
24
+ }
25
+ return { token, url: socketUrl.toString(), expiresAt };
26
+ };
27
+ exports.fetchRealtimeBinding = fetchRealtimeBinding;
28
+ const apiSocketUrl = (value, baseUrl) => {
29
+ let socketUrl;
30
+ try {
31
+ socketUrl = new URL(value);
32
+ }
33
+ catch (_a) {
34
+ throw new ovok_errors_1.OvokServiceError((0, ovok_errors_1.detailsFor)("The realtime binding response has an invalid WebSocket URL."));
35
+ }
36
+ const apiUrl = new URL(baseUrl);
37
+ const expectedProtocol = apiUrl.protocol === "https:" ? "wss:" : "ws:";
38
+ if (socketUrl.protocol !== expectedProtocol ||
39
+ socketUrl.host !== apiUrl.host ||
40
+ socketUrl.pathname !== SOCKET_PATH) {
41
+ throw new ovok_errors_1.OvokServiceError((0, ovok_errors_1.detailsFor)("The realtime WebSocket URL must use the configured API host."));
42
+ }
43
+ return socketUrl;
44
+ };
@@ -0,0 +1,38 @@
1
+ import { HttpClient } from "../../http/http-client";
2
+ import { FhirResource } from "../fhir-resource";
3
+ import { SessionResource } from "../session/session-resource";
4
+ import { RealtimeNotificationHandler } from "./realtime-types";
5
+ export declare class RealtimeConnection {
6
+ private readonly http;
7
+ private readonly fhir;
8
+ private readonly baseUrl;
9
+ private readonly session;
10
+ private readonly subscriptionId;
11
+ private readonly handler;
12
+ private socket;
13
+ private binding;
14
+ private profileReference;
15
+ private accessToken;
16
+ private reconnectDelayMs;
17
+ private reconnectTimer;
18
+ private refreshTimer;
19
+ private refreshInFlight;
20
+ private removeSessionListener;
21
+ private connecting;
22
+ private paused;
23
+ private stopped;
24
+ private deleted;
25
+ constructor(http: HttpClient, fhir: FhirResource, baseUrl: string, session: SessionResource, subscriptionId: string, handler: RealtimeNotificationHandler);
26
+ start(): Promise<void>;
27
+ unsubscribe(): Promise<void>;
28
+ private connect;
29
+ private openSocket;
30
+ private socketClosed;
31
+ private sessionChanged;
32
+ private refreshBinding;
33
+ private scheduleBindingRefresh;
34
+ private scheduleReconnect;
35
+ private closeSocket;
36
+ private clearTimers;
37
+ private clearRefreshTimer;
38
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RealtimeConnection = void 0;
4
+ const realtime_binding_1 = require("./realtime-binding");
5
+ const realtime_socket_1 = require("./realtime-socket");
6
+ const TOKEN_REFRESH_MARGIN_MS = 5 * 60000;
7
+ const TOKEN_REFRESH_MIN_DELAY_MS = 60000;
8
+ const INITIAL_RECONNECT_DELAY_MS = 1000;
9
+ const MAX_RECONNECT_DELAY_MS = 30000;
10
+ class RealtimeConnection {
11
+ constructor(http, fhir, baseUrl, session, subscriptionId, handler) {
12
+ this.http = http;
13
+ this.fhir = fhir;
14
+ this.baseUrl = baseUrl;
15
+ this.session = session;
16
+ this.subscriptionId = subscriptionId;
17
+ this.handler = handler;
18
+ this.socket = null;
19
+ this.binding = null;
20
+ this.profileReference = null;
21
+ this.accessToken = null;
22
+ this.reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
23
+ this.reconnectTimer = null;
24
+ this.refreshTimer = null;
25
+ this.refreshInFlight = null;
26
+ this.removeSessionListener = null;
27
+ this.connecting = false;
28
+ this.paused = false;
29
+ this.stopped = false;
30
+ this.deleted = false;
31
+ }
32
+ async start() {
33
+ var _a, _b;
34
+ const session = await this.session.current();
35
+ this.profileReference = (_a = session === null || session === void 0 ? void 0 : session.profile.reference) !== null && _a !== void 0 ? _a : null;
36
+ this.accessToken = (_b = session === null || session === void 0 ? void 0 : session.accessToken) !== null && _b !== void 0 ? _b : null;
37
+ this.removeSessionListener = this.session.onChange((nextSession) => this.sessionChanged(nextSession));
38
+ await this.connect();
39
+ }
40
+ async unsubscribe() {
41
+ var _a, _b, _c;
42
+ if (this.deleted) {
43
+ return;
44
+ }
45
+ this.stopped = true;
46
+ (_a = this.removeSessionListener) === null || _a === void 0 ? void 0 : _a.call(this);
47
+ this.removeSessionListener = null;
48
+ this.clearTimers();
49
+ (_b = this.socket) === null || _b === void 0 ? void 0 : _b.close((_c = this.binding) === null || _c === void 0 ? void 0 : _c.token);
50
+ this.socket = null;
51
+ await this.fhir.delete("Subscription", this.subscriptionId);
52
+ this.deleted = true;
53
+ }
54
+ async connect() {
55
+ if (this.stopped || this.paused || this.connecting || this.socket) {
56
+ return;
57
+ }
58
+ this.connecting = true;
59
+ try {
60
+ const binding = await (0, realtime_binding_1.fetchRealtimeBinding)(this.http, this.subscriptionId, this.baseUrl);
61
+ if (this.stopped || this.paused) {
62
+ return;
63
+ }
64
+ this.binding = binding;
65
+ this.scheduleBindingRefresh(binding);
66
+ this.openSocket(binding);
67
+ }
68
+ finally {
69
+ this.connecting = false;
70
+ }
71
+ }
72
+ openSocket(binding) {
73
+ const socket = new realtime_socket_1.RealtimeSocket(binding.url, binding.token, this.handler, () => { this.reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS; }, (closedSocket) => this.socketClosed(closedSocket));
74
+ this.socket = socket;
75
+ socket.open();
76
+ }
77
+ socketClosed(socket) {
78
+ if (socket !== this.socket) {
79
+ return;
80
+ }
81
+ this.socket = null;
82
+ this.clearRefreshTimer();
83
+ this.scheduleReconnect();
84
+ }
85
+ sessionChanged(session) {
86
+ if (session === null) {
87
+ this.paused = true;
88
+ this.clearTimers();
89
+ this.closeSocket();
90
+ return;
91
+ }
92
+ if (this.profileReference !== null && session.profile.reference !== this.profileReference) {
93
+ void this.unsubscribe().catch(() => undefined);
94
+ return;
95
+ }
96
+ this.profileReference = session.profile.reference;
97
+ if (this.paused) {
98
+ this.paused = false;
99
+ void this.connect().catch(() => this.scheduleReconnect());
100
+ return;
101
+ }
102
+ if (session.accessToken === this.accessToken) {
103
+ return;
104
+ }
105
+ this.accessToken = session.accessToken;
106
+ if (this.connecting) {
107
+ return;
108
+ }
109
+ void this.refreshBinding().catch(() => { var _a; return (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close(); });
110
+ }
111
+ refreshBinding() {
112
+ if (this.refreshInFlight) {
113
+ return this.refreshInFlight;
114
+ }
115
+ this.refreshInFlight = (0, realtime_binding_1.fetchRealtimeBinding)(this.http, this.subscriptionId, this.baseUrl)
116
+ .then((binding) => {
117
+ var _a;
118
+ if (this.stopped || this.paused) {
119
+ return;
120
+ }
121
+ this.binding = binding;
122
+ this.scheduleBindingRefresh(binding);
123
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.bind(binding.token);
124
+ })
125
+ .finally(() => {
126
+ this.refreshInFlight = null;
127
+ });
128
+ return this.refreshInFlight;
129
+ }
130
+ scheduleBindingRefresh(binding) {
131
+ this.clearRefreshTimer();
132
+ const delay = Math.max(TOKEN_REFRESH_MIN_DELAY_MS, binding.expiresAt - Date.now() - TOKEN_REFRESH_MARGIN_MS);
133
+ this.refreshTimer = setTimeout(() => {
134
+ void this.refreshBinding().catch(() => { var _a; return (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close(); });
135
+ }, delay);
136
+ }
137
+ scheduleReconnect() {
138
+ if (this.stopped || this.paused || this.reconnectTimer !== null) {
139
+ return;
140
+ }
141
+ const delay = this.reconnectDelayMs;
142
+ this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, MAX_RECONNECT_DELAY_MS);
143
+ this.reconnectTimer = setTimeout(() => {
144
+ this.reconnectTimer = null;
145
+ void this.connect().catch(() => this.scheduleReconnect());
146
+ }, delay);
147
+ }
148
+ closeSocket() {
149
+ const socket = this.socket;
150
+ this.socket = null;
151
+ socket === null || socket === void 0 ? void 0 : socket.close();
152
+ }
153
+ clearTimers() {
154
+ if (this.reconnectTimer !== null) {
155
+ clearTimeout(this.reconnectTimer);
156
+ }
157
+ this.reconnectTimer = null;
158
+ this.clearRefreshTimer();
159
+ }
160
+ clearRefreshTimer() {
161
+ if (this.refreshTimer !== null) {
162
+ clearTimeout(this.refreshTimer);
163
+ }
164
+ this.refreshTimer = null;
165
+ }
166
+ }
167
+ exports.RealtimeConnection = RealtimeConnection;
@@ -0,0 +1,13 @@
1
+ import { HttpClient } from "../../http/http-client";
2
+ import { FhirResource } from "../fhir-resource";
3
+ import { SessionResource } from "../session/session-resource";
4
+ import { RealtimeNotificationHandler, RealtimeUnsubscribe } from "./realtime-types";
5
+ export type { RealtimeNotification, RealtimeNotificationHandler, RealtimeUnsubscribe, } from "./realtime-types";
6
+ export declare class RealtimeResource {
7
+ private readonly http;
8
+ private readonly fhir;
9
+ private readonly baseUrl;
10
+ private readonly session;
11
+ constructor(http: HttpClient, fhir: FhirResource, baseUrl: string, session: SessionResource);
12
+ subscribe(criteria: string, handler: RealtimeNotificationHandler): Promise<RealtimeUnsubscribe>;
13
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RealtimeResource = void 0;
4
+ const ovok_errors_1 = require("../../errors/ovok-errors");
5
+ const realtime_connection_1 = require("./realtime-connection");
6
+ class RealtimeResource {
7
+ constructor(http, fhir, baseUrl, session) {
8
+ this.http = http;
9
+ this.fhir = fhir;
10
+ this.baseUrl = baseUrl;
11
+ this.session = session;
12
+ }
13
+ async subscribe(criteria, handler) {
14
+ if (criteria.trim() === "") {
15
+ throw new ovok_errors_1.OvokValidationError((0, ovok_errors_1.detailsFor)("A realtime subscription criteria string is required."));
16
+ }
17
+ const subscription = {
18
+ resourceType: "Subscription",
19
+ status: "active",
20
+ reason: "Ovok SDK realtime subscription",
21
+ criteria,
22
+ channel: { type: "websocket" },
23
+ };
24
+ const created = await this.fhir.create(subscription);
25
+ if (!created.id) {
26
+ throw new ovok_errors_1.OvokServiceError((0, ovok_errors_1.detailsFor)("The realtime subscription response did not include an id."));
27
+ }
28
+ const connection = new realtime_connection_1.RealtimeConnection(this.http, this.fhir, this.baseUrl, this.session, created.id, handler);
29
+ try {
30
+ await connection.start();
31
+ }
32
+ catch (error) {
33
+ await connection.unsubscribe().catch(() => undefined);
34
+ throw error;
35
+ }
36
+ return () => connection.unsubscribe();
37
+ }
38
+ }
39
+ exports.RealtimeResource = RealtimeResource;
@@ -0,0 +1,23 @@
1
+ import { RealtimeNotificationHandler } from "./realtime-types";
2
+ export declare class RealtimeSocket {
3
+ private readonly url;
4
+ private token;
5
+ private readonly handler;
6
+ private readonly onHandshake;
7
+ private readonly onClose;
8
+ private socket;
9
+ private handshakeTimer;
10
+ private pingTimer;
11
+ private waitingForPong;
12
+ constructor(url: string, token: string, handler: RealtimeNotificationHandler, onHandshake: () => void, onClose: (socket: RealtimeSocket) => void);
13
+ open(): void;
14
+ bind(token: string): void;
15
+ close(unbindToken?: string): void;
16
+ private socketOpened;
17
+ private socketMessage;
18
+ private socketClosed;
19
+ private sendPing;
20
+ private sendBinding;
21
+ private clearTimers;
22
+ private clearHandshakeTimer;
23
+ }
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RealtimeSocket = void 0;
4
+ const ovok_errors_1 = require("../../errors/ovok-errors");
5
+ const SOCKET_OPEN = 1;
6
+ const PING_INTERVAL_MS = 5000;
7
+ const SOCKET_HANDSHAKE_TIMEOUT_MS = 10000;
8
+ class RealtimeSocket {
9
+ constructor(url, token, handler, onHandshake, onClose) {
10
+ this.url = url;
11
+ this.token = token;
12
+ this.handler = handler;
13
+ this.onHandshake = onHandshake;
14
+ this.onClose = onClose;
15
+ this.socket = null;
16
+ this.handshakeTimer = null;
17
+ this.pingTimer = null;
18
+ this.waitingForPong = false;
19
+ }
20
+ open() {
21
+ if (typeof WebSocket === "undefined") {
22
+ throw new ovok_errors_1.OvokConfigError((0, ovok_errors_1.detailsFor)("This runtime does not provide WebSocket support."));
23
+ }
24
+ const socket = new WebSocket(this.url);
25
+ this.socket = socket;
26
+ socket.addEventListener("open", () => this.socketOpened(socket));
27
+ socket.addEventListener("message", (event) => this.socketMessage(socket, event));
28
+ socket.addEventListener("error", () => socket.close());
29
+ socket.addEventListener("close", () => this.socketClosed(socket));
30
+ }
31
+ bind(token) {
32
+ var _a;
33
+ this.token = token;
34
+ if (((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === SOCKET_OPEN) {
35
+ this.sendBinding(this.socket);
36
+ }
37
+ }
38
+ close(unbindToken) {
39
+ const socket = this.socket;
40
+ if ((socket === null || socket === void 0 ? void 0 : socket.readyState) === SOCKET_OPEN && unbindToken) {
41
+ try {
42
+ socket.send(JSON.stringify({ type: "unbind-from-token", payload: { token: unbindToken } }));
43
+ }
44
+ catch (_a) {
45
+ socket.close();
46
+ }
47
+ }
48
+ this.socket = null;
49
+ this.clearTimers();
50
+ socket === null || socket === void 0 ? void 0 : socket.close();
51
+ }
52
+ socketOpened(socket) {
53
+ if (socket !== this.socket) {
54
+ return;
55
+ }
56
+ this.sendBinding(socket);
57
+ this.handshakeTimer = setTimeout(() => socket.close(), SOCKET_HANDSHAKE_TIMEOUT_MS);
58
+ this.pingTimer = setInterval(() => this.sendPing(socket), PING_INTERVAL_MS);
59
+ }
60
+ socketMessage(socket, event) {
61
+ if (socket !== this.socket || typeof event.data !== "string") {
62
+ return;
63
+ }
64
+ let message;
65
+ try {
66
+ message = JSON.parse(event.data);
67
+ }
68
+ catch (_a) {
69
+ socket.close();
70
+ return;
71
+ }
72
+ if (!isRecord(message)) {
73
+ socket.close();
74
+ return;
75
+ }
76
+ if (message.type === "pong") {
77
+ this.waitingForPong = false;
78
+ return;
79
+ }
80
+ if (!isSubscriptionStatus(message)) {
81
+ socket.close();
82
+ return;
83
+ }
84
+ if (message.type === "handshake") {
85
+ this.clearHandshakeTimer();
86
+ this.onHandshake();
87
+ return;
88
+ }
89
+ if (message.type === "heartbeat") {
90
+ return;
91
+ }
92
+ if (message.type === "event-notification") {
93
+ this.handler({ ...message, type: "event-notification" });
94
+ }
95
+ }
96
+ socketClosed(socket) {
97
+ if (socket !== this.socket) {
98
+ return;
99
+ }
100
+ this.socket = null;
101
+ this.clearTimers();
102
+ this.onClose(this);
103
+ }
104
+ sendPing(socket) {
105
+ if (socket !== this.socket || socket.readyState !== SOCKET_OPEN) {
106
+ return;
107
+ }
108
+ if (this.waitingForPong) {
109
+ socket.close();
110
+ return;
111
+ }
112
+ try {
113
+ socket.send(JSON.stringify({ type: "ping" }));
114
+ this.waitingForPong = true;
115
+ }
116
+ catch (_a) {
117
+ socket.close();
118
+ }
119
+ }
120
+ sendBinding(socket) {
121
+ try {
122
+ socket.send(JSON.stringify({ type: "bind-with-token", payload: { token: this.token } }));
123
+ }
124
+ catch (_a) {
125
+ socket.close();
126
+ }
127
+ }
128
+ clearTimers() {
129
+ this.clearHandshakeTimer();
130
+ if (this.pingTimer !== null) {
131
+ clearInterval(this.pingTimer);
132
+ }
133
+ this.pingTimer = null;
134
+ this.waitingForPong = false;
135
+ }
136
+ clearHandshakeTimer() {
137
+ if (this.handshakeTimer !== null) {
138
+ clearTimeout(this.handshakeTimer);
139
+ }
140
+ this.handshakeTimer = null;
141
+ }
142
+ }
143
+ exports.RealtimeSocket = RealtimeSocket;
144
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
145
+ const isSubscriptionStatus = (value) => {
146
+ if (!isRecord(value) || !isRecord(value.subscription)) {
147
+ return false;
148
+ }
149
+ return (value.resourceType === "SubscriptionStatus" &&
150
+ typeof value.type === "string" &&
151
+ typeof value.subscription.reference === "string");
152
+ };
@@ -0,0 +1,6 @@
1
+ import type { SubscriptionStatus } from "@medplum/fhirtypes";
2
+ export type RealtimeNotification = SubscriptionStatus & {
3
+ type: "event-notification";
4
+ };
5
+ export type RealtimeNotificationHandler = (notification: RealtimeNotification) => void;
6
+ export type RealtimeUnsubscribe = () => Promise<void>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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.498+92fd957",
3
+ "version": "0.4.49-dev.500+07ba725",
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
  }