@ovok/core 0.4.49-dev.499 → 0.4.49-dev.501
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conformance/capability-requirements.js +1 -1
- package/dist/next/index.d.ts +2 -0
- package/dist/next/ovok.d.ts +2 -0
- package/dist/next/ovok.js +4 -1
- package/dist/next/resources/realtime/realtime-binding.d.ts +7 -0
- package/dist/next/resources/realtime/realtime-binding.js +44 -0
- package/dist/next/resources/realtime/realtime-connection.d.ts +38 -0
- package/dist/next/resources/realtime/realtime-connection.js +167 -0
- package/dist/next/resources/realtime/realtime-resource.d.ts +13 -0
- package/dist/next/resources/realtime/realtime-resource.js +39 -0
- package/dist/next/resources/realtime/realtime-socket.d.ts +23 -0
- package/dist/next/resources/realtime/realtime-socket.js +152 -0
- package/dist/next/resources/realtime/realtime-types.d.ts +6 -0
- package/dist/next/resources/realtime/realtime-types.js +2 -0
- 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.
|
|
51
|
+
version: "0.4.49-dev.501+330a19b",
|
|
52
52
|
},
|
|
53
53
|
fhirVersion: "4.0.1",
|
|
54
54
|
format: ["json"],
|
package/dist/next/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/next/ovok.d.ts
CHANGED
|
@@ -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
|
|
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,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>;
|