@effect.healthcare/messenger 0.3.1 → 0.4.2

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/src/messenger.ts CHANGED
@@ -1,347 +1,386 @@
1
- /**
2
- * @module Component
3
- */
4
-
5
- // Base Component
6
- import { Component } from "@scalable.software/component";
7
- import { type Configuration } from "@scalable.software/component";
8
-
9
- // Component Metadata
10
- import { Tag, Attributes, Connection, Event, Gesture } from "./messenger.meta.js";
11
- import { Session, Protocol, Message } from "./messenger.meta.js";
12
- import { type Endpoint } from "./messenger.meta.js";
13
-
14
- // Component Validation
15
- import { Validate } from "./messenger.validation.js";
16
-
17
- /**
18
- * Configuration for the headless component — no template or stylesheet.
19
- * @category Configuration
20
- */
21
- export const configuration: Configuration = {
22
- url: import.meta.url,
23
- } as const;
24
-
25
- /**
26
- * @category Component
27
- */
28
- export class Messenger extends Component {
29
- /**
30
- * The tag name of the component
31
- * @category Configuration
32
- */
33
- public static get Tag() {
34
- return Tag;
35
- }
36
-
37
- /**
38
- * Only attributes defined the Attributes object will be observed in DOM
39
- * @category State
40
- */
41
- public static get Attributes(): Attributes {
42
- return Attributes;
43
- }
44
-
45
- /**
46
- * The live connection status of the messenger. Internally owned.
47
- * @category State
48
- * @hidden
49
- */
50
- private _connection: Connection = Connection.DISCONNECTED;
51
-
52
- /**
53
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
54
- * @category State
55
- * @hidden
56
- */
57
- private _endpoint: Endpoint | null = null;
58
-
59
- /**
60
- * The live Web PubSub socket. Held so it can be closed and replaced.
61
- * @category State
62
- * @hidden
63
- */
64
- private _socket: WebSocket | null = null;
65
-
66
- /**
67
- * Event subscription handler slots.
68
- * @category Event
69
- * @hidden
70
- */
71
- private _onendpointchange: EventListener | null = null;
72
- private _onconnecting: EventListener | null = null;
73
- private _onconnected: EventListener | null = null;
74
- private _ondisconnected: EventListener | null = null;
75
- private _onmasterdatachange: EventListener | null = null;
76
-
77
- /**
78
- * Cache element references to improve performance
79
- * @category State
80
- * @hidden
81
- */
82
- protected elements: {} = {};
83
-
84
- /**
85
- * @hidden
86
- */
87
- constructor() {
88
- super(configuration);
89
- }
90
-
91
- /**
92
- * The live connection status of the messenger. Internally owned — readable
93
- * externally, but only mutable from within (protected setter).
94
- * @category State
95
- */
96
- public get connection(): Connection {
97
- return this._connection;
98
- }
99
-
100
- protected set connection(connection: Connection) {
101
- // Guard
102
- if (this._connection === connection) return;
103
-
104
- // Mutate
105
- this._connection = connection;
106
- this.setAttribute(Attributes.CONNECTION, connection);
107
-
108
- // Dispatch
109
- const event = { detail: { connection } };
110
- connection === Connection.CONNECTING &&
111
- this._dispatchEvent(Event.ON_CONNECTING, event);
112
- connection === Connection.CONNECTED &&
113
- this._dispatchEvent(Event.ON_CONNECTED, event);
114
- connection === Connection.DISCONNECTED &&
115
- this._dispatchEvent(Event.ON_DISCONNECTED, event);
116
- }
117
-
118
- /**
119
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
120
- * @category State
121
- */
122
- public get endpoint(): Endpoint | null {
123
- return this._endpoint;
124
- }
125
-
126
- public set endpoint(endpoint: Endpoint | null) {
127
- // Normalize
128
- endpoint = endpoint ?? null;
129
-
130
- // Validate
131
- endpoint = endpoint ? Validate.endpoint(endpoint) : null;
132
-
133
- // Guard
134
- if (this._endpoint === endpoint) return;
135
-
136
- // Mutate
137
- this._endpoint = endpoint;
138
- endpoint === null
139
- ? this.removeAttribute(Attributes.ENDPOINT)
140
- : this.setAttribute(Attributes.ENDPOINT, endpoint);
141
-
142
- // Dispatch
143
- this._dispatchEvent(Event.ON_ENDPOINT_CHANGE, { detail: { endpoint } });
144
- }
145
-
146
- /**
147
- * The session token, read fresh from local storage on every access — never
148
- * cached — because the License component renews and rewrites the key.
149
- * @category Operation
150
- * @hidden
151
- */
152
- private get _token(): string | null {
153
- return localStorage.getItem(Session);
154
- }
155
-
156
- /**
157
- * Negotiates a client access URL from the configured endpoint, authenticating
158
- * with the session token, then opens the live connection. No-op when the
159
- * endpoint or token is unavailable, or when already connecting or connected.
160
- * @category Operation
161
- */
162
- public connect = async (): Promise<void> => {
163
- const token = this._token;
164
-
165
- // Gate + guard
166
- if (!this.endpoint || !token) return;
167
- if (this._connection !== Connection.DISCONNECTED) return;
168
-
169
- // Establish
170
- this.connection = Connection.CONNECTING;
171
- const url = await this._negotiate(token);
172
- this._socket = this._open(url);
173
- };
174
-
175
- /**
176
- * Closes the live connection.
177
- * @category Operation
178
- */
179
- public disconnect = (): void => {
180
- this._socket?.close();
181
- this._socket = null;
182
- this.connection = Connection.DISCONNECTED;
183
- };
184
-
185
- /**
186
- * Subscribes to the `onendpointchange` event. Assigning replaces the previous
187
- * handler; assigning `null` removes it.
188
- * @category Event
189
- */
190
- public set onendpointchange(handler: EventListener | null) {
191
- this._onendpointchange &&
192
- this.removeEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
193
-
194
- this._onendpointchange = handler;
195
-
196
- this._onendpointchange &&
197
- this.addEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
198
- }
199
-
200
- /**
201
- * Subscribes to the `onconnecting` event. Assigning replaces the previous
202
- * handler; assigning `null` removes it.
203
- * @category Event
204
- */
205
- public set onconnecting(handler: EventListener | null) {
206
- this._onconnecting &&
207
- this.removeEventListener(Event.ON_CONNECTING, this._onconnecting);
208
-
209
- this._onconnecting = handler;
210
-
211
- this._onconnecting &&
212
- this.addEventListener(Event.ON_CONNECTING, this._onconnecting);
213
- }
214
-
215
- /**
216
- * Subscribes to the `onconnected` event. Assigning replaces the previous
217
- * handler; assigning `null` removes it.
218
- * @category Event
219
- */
220
- public set onconnected(handler: EventListener | null) {
221
- this._onconnected &&
222
- this.removeEventListener(Event.ON_CONNECTED, this._onconnected);
223
-
224
- this._onconnected = handler;
225
-
226
- this._onconnected &&
227
- this.addEventListener(Event.ON_CONNECTED, this._onconnected);
228
- }
229
-
230
- /**
231
- * Subscribes to the `ondisconnected` event. Assigning replaces the previous
232
- * handler; assigning `null` removes it.
233
- * @category Event
234
- */
235
- public set ondisconnected(handler: EventListener | null) {
236
- this._ondisconnected &&
237
- this.removeEventListener(Event.ON_DISCONNECTED, this._ondisconnected);
238
-
239
- this._ondisconnected = handler;
240
-
241
- this._ondisconnected &&
242
- this.addEventListener(Event.ON_DISCONNECTED, this._ondisconnected);
243
- }
244
-
245
- /**
246
- * Subscribes to the `onmasterdatachange` event. Assigning replaces the
247
- * previous handler; assigning `null` removes it.
248
- * @category Event
249
- */
250
- public set onmasterdatachange(handler: EventListener | null) {
251
- this._onmasterdatachange &&
252
- this.removeEventListener(
253
- Event.ON_MASTERDATACHANGE,
254
- this._onmasterdatachange,
255
- );
256
-
257
- this._onmasterdatachange = handler;
258
-
259
- this._onmasterdatachange &&
260
- this.addEventListener(Event.ON_MASTERDATACHANGE, this._onmasterdatachange);
261
- }
262
-
263
- /**
264
- * List operations to perform for selected attributes being observed in the DOM.
265
- * @category Configuration
266
- * @hidden
267
- */
268
- protected _attributeHandlers = {
269
- [Attributes.CONNECTION]: (value: Connection) =>
270
- value !== this._connection &&
271
- this.setAttribute(Attributes.CONNECTION, this._connection),
272
- [Attributes.ENDPOINT]: (value: Endpoint) => (this.endpoint = value),
273
- };
274
-
275
- /**
276
- * Cache element references to improve performance
277
- * @category Configuration
278
- * @hidden
279
- */
280
- protected _cache = () => {};
281
-
282
- /**
283
- * Initialize component attributes with default values
284
- * @category Configuration
285
- * @hidden
286
- */
287
- protected _initialize = () => {
288
- !this.hasAttribute(Attributes.CONNECTION) &&
289
- this.setAttribute(Attributes.CONNECTION, this._connection);
290
- };
291
-
292
- /**
293
- * Called by the connectedCallback prototypical method
294
- * @category Configuration
295
- * @hidden
296
- */
297
- protected _addEventListeners = () => {};
298
-
299
- /**
300
- * Called by the disconnectedCallback prototypical method
301
- * @category Configuration
302
- * @hidden
303
- */
304
- protected _removeEventListeners = () => {};
305
-
306
- /**
307
- * Negotiates a short-lived Web PubSub client access URL from the endpoint,
308
- * authenticating the request with the session token.
309
- * @category Operation
310
- * @hidden
311
- */
312
- private _negotiate = async (token: string): Promise<string> => {
313
- const response = await fetch(this.endpoint as string, {
314
- headers: { Authorization: `Bearer ${token}` },
315
- });
316
- const { url } = await response.json();
317
- return url;
318
- };
319
-
320
- /**
321
- * Opens a Web PubSub WebSocket with the required subprotocol and wires the
322
- * connection lifecycle handlers.
323
- * @category Operation
324
- * @hidden
325
- */
326
- private _open = (url: string): WebSocket => {
327
- const socket = new WebSocket(url, Protocol);
328
- socket.onopen = () => (this.connection = Connection.CONNECTED);
329
- socket.onclose = () => (this.connection = Connection.DISCONNECTED);
330
- socket.addEventListener(Gesture.MESSAGE, this._handleMessage);
331
- return socket;
332
- };
333
-
334
- /**
335
- * Handles an inbound Web PubSub message (the `message` gesture). Dispatches
336
- * `onmasterdatachange` when the payload's `data.type` is `masterData.changed`.
337
- * @category Gesture
338
- * @hidden
339
- */
340
- private _handleMessage = (event: MessageEvent) => {
341
- const message = JSON.parse(event.data);
342
- message.data?.type === Message.MASTER_DATA_CHANGED &&
343
- this._dispatchEvent(Event.ON_MASTERDATACHANGE, {
344
- detail: { datetime: new Date().toISOString() },
345
- });
346
- };
347
- }
1
+ /**
2
+ * @module Component
3
+ */
4
+
5
+ // Base Component
6
+ import { Component } from "@scalable.software/component";
7
+ import { type Configuration } from "@scalable.software/component";
8
+
9
+ // Component Metadata
10
+ import {
11
+ Tag,
12
+ Attributes,
13
+ Connection,
14
+ Event,
15
+ Gesture,
16
+ Session,
17
+ Protocol,
18
+ Message,
19
+ } from "./messenger.meta.js";
20
+
21
+ import { type Endpoint } from "./messenger.meta.js";
22
+
23
+ // Component Validation
24
+ import { Validate } from "./messenger.validation.js";
25
+
26
+ /**
27
+ * Configuration for the headless component — no template or stylesheet.
28
+ * @category Configuration
29
+ */
30
+ export const configuration: Configuration = {
31
+ url: import.meta.url,
32
+ } as const;
33
+
34
+ /**
35
+ * @category Component
36
+ */
37
+ export class Messenger extends Component {
38
+ /**
39
+ * The tag name of the component
40
+ * @category Configuration
41
+ */
42
+ public static get Tag() {
43
+ return Tag;
44
+ }
45
+
46
+ /**
47
+ * Only attributes defined the Attributes object will be observed in DOM
48
+ * @category State
49
+ */
50
+ public static get Attributes(): Attributes {
51
+ return Attributes;
52
+ }
53
+
54
+ /**
55
+ * The live connection status of the messenger. Internally owned.
56
+ * @category State
57
+ * @hidden
58
+ */
59
+ private _connection: Connection = Connection.DISCONNECTED;
60
+
61
+ /**
62
+ * The negotiate endpoint URL. When absent the messenger stays disconnected.
63
+ * @category State
64
+ * @hidden
65
+ */
66
+ private _endpoint: Endpoint | null = null;
67
+
68
+ /**
69
+ * The live Web PubSub socket. Held so it can be closed and replaced.
70
+ * @category State
71
+ * @hidden
72
+ */
73
+ private _socket: WebSocket | null = null;
74
+
75
+ /**
76
+ * Event subscription handler slots.
77
+ * @category Event
78
+ * @hidden
79
+ */
80
+ private _onendpointchange: EventListener | null = null;
81
+ private _onconnecting: EventListener | null = null;
82
+ private _onconnected: EventListener | null = null;
83
+ private _ondisconnected: EventListener | null = null;
84
+ private _onmasterdatachange: EventListener | null = null;
85
+
86
+ /**
87
+ * Cache element references to improve performance
88
+ * @category State
89
+ * @hidden
90
+ */
91
+ protected elements: {} = {};
92
+
93
+ /**
94
+ * @hidden
95
+ */
96
+ constructor() {
97
+ super(configuration);
98
+ }
99
+
100
+ /**
101
+ * The live connection status of the messenger. Internally owned — readable
102
+ * externally, but only mutable from within (protected setter).
103
+ * @category State
104
+ */
105
+ public get connection(): Connection {
106
+ return this._connection;
107
+ }
108
+
109
+ protected set connection(connection: Connection) {
110
+ // Guard
111
+ if (this._connection === connection) return;
112
+
113
+ // Mutate
114
+ this._connection = connection;
115
+ this.setAttribute(Attributes.CONNECTION, connection);
116
+
117
+ // Dispatch
118
+ const event = { detail: { connection } };
119
+ connection === Connection.CONNECTING &&
120
+ this._dispatchEvent(Event.ON_CONNECTING, event);
121
+ connection === Connection.CONNECTED &&
122
+ this._dispatchEvent(Event.ON_CONNECTED, event);
123
+ connection === Connection.DISCONNECTED &&
124
+ this._dispatchEvent(Event.ON_DISCONNECTED, event);
125
+ }
126
+
127
+ /**
128
+ * The negotiate endpoint URL. When absent the messenger stays disconnected.
129
+ * @category State
130
+ */
131
+ public get endpoint(): Endpoint | null {
132
+ return this._endpoint;
133
+ }
134
+
135
+ public set endpoint(endpoint: Endpoint | null) {
136
+ // Normalize
137
+ endpoint = endpoint ?? null;
138
+
139
+ // Validate
140
+ endpoint = endpoint ? Validate.endpoint(endpoint) : null;
141
+
142
+ // Guard
143
+ if (this._endpoint === endpoint) return;
144
+
145
+ // Mutate
146
+ this._endpoint = endpoint;
147
+ endpoint === null
148
+ ? this.removeAttribute(Attributes.ENDPOINT)
149
+ : this.setAttribute(Attributes.ENDPOINT, endpoint);
150
+
151
+ // Dispatch
152
+ this._dispatchEvent(Event.ON_ENDPOINT_CHANGE, { detail: { endpoint } });
153
+ }
154
+
155
+ /**
156
+ * The session token, read fresh from local storage on every access — never
157
+ * cached — because the License component renews and rewrites the key.
158
+ * @category State
159
+ * @hidden
160
+ */
161
+ private get _token(): string | null {
162
+ return localStorage.getItem(Session);
163
+ }
164
+
165
+ /**
166
+ * Negotiates a client access URL from the configured endpoint, authenticating
167
+ * with the session token, then opens the live connection. No-op when the
168
+ * endpoint or token is unavailable, or when already connecting or connected.
169
+ * @category Operation
170
+ */
171
+ public connect = async (): Promise<void> => {
172
+ const token = this._token;
173
+
174
+ // Gate + guard
175
+ if (!this.endpoint || !token) return;
176
+ if (this._connection !== Connection.DISCONNECTED) return;
177
+
178
+ // Establish
179
+ this.connection = Connection.CONNECTING;
180
+ const url = await this._negotiate(token);
181
+ this._socket = this._open(url);
182
+ };
183
+
184
+ /**
185
+ * Closes the live connection.
186
+ * @category Operation
187
+ */
188
+ public disconnect = (): void => {
189
+ this._socket?.close();
190
+ this._socket = null;
191
+ this.connection = Connection.DISCONNECTED;
192
+ };
193
+
194
+ /**
195
+ * Sends a JSON message to the server over the open Web PubSub connection by
196
+ * wrapping it in an `event` frame — the client-to-server mirror of the inbound
197
+ * `masterData.changed` notification. No-op returning `false` when there is no
198
+ * open connection; returns `true` when a frame is sent.
199
+ * @category Operation
200
+ */
201
+ public send = (message: object): boolean => {
202
+ // Gate
203
+ if (!this._socket || this._connection !== Connection.CONNECTED)
204
+ return false;
205
+
206
+ // Send — wrap in an Azure Web PubSub `event` frame, message rides as `data`
207
+ this._socket.send(
208
+ JSON.stringify({
209
+ type: "event",
210
+ event: "message",
211
+ dataType: "json",
212
+ data: message,
213
+ }),
214
+ );
215
+ return true;
216
+ };
217
+
218
+ /**
219
+ * Subscribes to the `onendpointchange` event. Assigning replaces the previous
220
+ * handler; assigning `null` removes it.
221
+ * @category Event
222
+ */
223
+ public set onendpointchange(handler: EventListener | null) {
224
+ this._onendpointchange &&
225
+ this.removeEventListener(
226
+ Event.ON_ENDPOINT_CHANGE,
227
+ this._onendpointchange,
228
+ );
229
+
230
+ this._onendpointchange = handler;
231
+
232
+ this._onendpointchange &&
233
+ this.addEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
234
+ }
235
+
236
+ /**
237
+ * Subscribes to the `onconnecting` event. Assigning replaces the previous
238
+ * handler; assigning `null` removes it.
239
+ * @category Event
240
+ */
241
+ public set onconnecting(handler: EventListener | null) {
242
+ this._onconnecting &&
243
+ this.removeEventListener(Event.ON_CONNECTING, this._onconnecting);
244
+
245
+ this._onconnecting = handler;
246
+
247
+ this._onconnecting &&
248
+ this.addEventListener(Event.ON_CONNECTING, this._onconnecting);
249
+ }
250
+
251
+ /**
252
+ * Subscribes to the `onconnected` event. Assigning replaces the previous
253
+ * handler; assigning `null` removes it.
254
+ * @category Event
255
+ */
256
+ public set onconnected(handler: EventListener | null) {
257
+ this._onconnected &&
258
+ this.removeEventListener(Event.ON_CONNECTED, this._onconnected);
259
+
260
+ this._onconnected = handler;
261
+
262
+ this._onconnected &&
263
+ this.addEventListener(Event.ON_CONNECTED, this._onconnected);
264
+ }
265
+
266
+ /**
267
+ * Subscribes to the `ondisconnected` event. Assigning replaces the previous
268
+ * handler; assigning `null` removes it.
269
+ * @category Event
270
+ */
271
+ public set ondisconnected(handler: EventListener | null) {
272
+ this._ondisconnected &&
273
+ this.removeEventListener(Event.ON_DISCONNECTED, this._ondisconnected);
274
+
275
+ this._ondisconnected = handler;
276
+
277
+ this._ondisconnected &&
278
+ this.addEventListener(Event.ON_DISCONNECTED, this._ondisconnected);
279
+ }
280
+
281
+ /**
282
+ * Subscribes to the `onmasterdatachange` event. Assigning replaces the
283
+ * previous handler; assigning `null` removes it.
284
+ * @category Event
285
+ */
286
+ public set onmasterdatachange(handler: EventListener | null) {
287
+ this._onmasterdatachange &&
288
+ this.removeEventListener(
289
+ Event.ON_MASTERDATACHANGE,
290
+ this._onmasterdatachange,
291
+ );
292
+
293
+ this._onmasterdatachange = handler;
294
+
295
+ this._onmasterdatachange &&
296
+ this.addEventListener(
297
+ Event.ON_MASTERDATACHANGE,
298
+ this._onmasterdatachange,
299
+ );
300
+ }
301
+
302
+ /**
303
+ * List operations to perform for selected attributes being observed in the DOM.
304
+ * @category Configuration
305
+ * @hidden
306
+ */
307
+ protected _attributeHandlers = {
308
+ [Attributes.CONNECTION]: (value: Connection) =>
309
+ value !== this._connection &&
310
+ this.setAttribute(Attributes.CONNECTION, this._connection),
311
+ [Attributes.ENDPOINT]: (value: Endpoint) => (this.endpoint = value),
312
+ };
313
+
314
+ /**
315
+ * Cache element references to improve performance
316
+ * @category Configuration
317
+ * @hidden
318
+ */
319
+ protected _cache = () => {};
320
+
321
+ /**
322
+ * Initialize component attributes with default values
323
+ * @category Configuration
324
+ * @hidden
325
+ */
326
+ protected _initialize = () => {
327
+ !this.hasAttribute(Attributes.CONNECTION) &&
328
+ this.setAttribute(Attributes.CONNECTION, this._connection);
329
+ };
330
+
331
+ /**
332
+ * Called by the connectedCallback prototypical method
333
+ * @category Configuration
334
+ * @hidden
335
+ */
336
+ protected _addEventListeners = () => {};
337
+
338
+ /**
339
+ * Called by the disconnectedCallback prototypical method
340
+ * @category Configuration
341
+ * @hidden
342
+ */
343
+ protected _removeEventListeners = () => {};
344
+
345
+ /**
346
+ * Negotiates a short-lived Web PubSub client access URL from the endpoint,
347
+ * authenticating the request with the session token.
348
+ * @category Operation
349
+ * @hidden
350
+ */
351
+ private _negotiate = async (token: string): Promise<string> => {
352
+ const response = await fetch(this.endpoint as string, {
353
+ headers: { Authorization: `Bearer ${token}` },
354
+ });
355
+ const { url } = await response.json();
356
+ return url;
357
+ };
358
+
359
+ /**
360
+ * Opens a Web PubSub WebSocket with the required subprotocol and wires the
361
+ * connection lifecycle handlers.
362
+ * @category Operation
363
+ * @hidden
364
+ */
365
+ private _open = (url: string): WebSocket => {
366
+ const socket = new WebSocket(url, Protocol);
367
+ socket.onopen = () => (this.connection = Connection.CONNECTED);
368
+ socket.onclose = () => (this.connection = Connection.DISCONNECTED);
369
+ socket.addEventListener(Gesture.MESSAGE, this._handleMessage);
370
+ return socket;
371
+ };
372
+
373
+ /**
374
+ * Handles an inbound Web PubSub message (the `message` gesture). Dispatches
375
+ * `onmasterdatachange` when the payload's `data.type` is `masterData.changed`.
376
+ * @category Gesture
377
+ * @hidden
378
+ */
379
+ private _handleMessage = (event: MessageEvent) => {
380
+ const message = JSON.parse(event.data);
381
+ message.data?.type === Message.MASTER_DATA_CHANGED &&
382
+ this._dispatchEvent(Event.ON_MASTERDATACHANGE, {
383
+ detail: { datetime: new Date().toISOString() },
384
+ });
385
+ };
386
+ }