@effect.healthcare/messenger 0.3.1

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * @module Component
3
+ */
4
+ // Base Component
5
+ import { Component } from "@scalable.software/component";
6
+ // Component Metadata
7
+ import { Tag, Attributes, Connection, Event, Gesture } from "./messenger.meta.js";
8
+ import { Session, Protocol, Message } from "./messenger.meta.js";
9
+ // Component Validation
10
+ import { Validate } from "./messenger.validation.js";
11
+ /**
12
+ * Configuration for the headless component — no template or stylesheet.
13
+ * @category Configuration
14
+ */
15
+ export const configuration = {
16
+ url: import.meta.url,
17
+ };
18
+ /**
19
+ * @category Component
20
+ */
21
+ export class Messenger extends Component {
22
+ /**
23
+ * The tag name of the component
24
+ * @category Configuration
25
+ */
26
+ static get Tag() {
27
+ return Tag;
28
+ }
29
+ /**
30
+ * Only attributes defined the Attributes object will be observed in DOM
31
+ * @category State
32
+ */
33
+ static get Attributes() {
34
+ return Attributes;
35
+ }
36
+ /**
37
+ * The live connection status of the messenger. Internally owned.
38
+ * @category State
39
+ * @hidden
40
+ */
41
+ _connection = Connection.DISCONNECTED;
42
+ /**
43
+ * The negotiate endpoint URL. When absent the messenger stays disconnected.
44
+ * @category State
45
+ * @hidden
46
+ */
47
+ _endpoint = null;
48
+ /**
49
+ * The live Web PubSub socket. Held so it can be closed and replaced.
50
+ * @category State
51
+ * @hidden
52
+ */
53
+ _socket = null;
54
+ /**
55
+ * Event subscription handler slots.
56
+ * @category Event
57
+ * @hidden
58
+ */
59
+ _onendpointchange = null;
60
+ _onconnecting = null;
61
+ _onconnected = null;
62
+ _ondisconnected = null;
63
+ _onmasterdatachange = null;
64
+ /**
65
+ * Cache element references to improve performance
66
+ * @category State
67
+ * @hidden
68
+ */
69
+ elements = {};
70
+ /**
71
+ * @hidden
72
+ */
73
+ constructor() {
74
+ super(configuration);
75
+ }
76
+ /**
77
+ * The live connection status of the messenger. Internally owned — readable
78
+ * externally, but only mutable from within (protected setter).
79
+ * @category State
80
+ */
81
+ get connection() {
82
+ return this._connection;
83
+ }
84
+ set connection(connection) {
85
+ // Guard
86
+ if (this._connection === connection)
87
+ return;
88
+ // Mutate
89
+ this._connection = connection;
90
+ this.setAttribute(Attributes.CONNECTION, connection);
91
+ // Dispatch
92
+ const event = { detail: { connection } };
93
+ connection === Connection.CONNECTING &&
94
+ this._dispatchEvent(Event.ON_CONNECTING, event);
95
+ connection === Connection.CONNECTED &&
96
+ this._dispatchEvent(Event.ON_CONNECTED, event);
97
+ connection === Connection.DISCONNECTED &&
98
+ this._dispatchEvent(Event.ON_DISCONNECTED, event);
99
+ }
100
+ /**
101
+ * The negotiate endpoint URL. When absent the messenger stays disconnected.
102
+ * @category State
103
+ */
104
+ get endpoint() {
105
+ return this._endpoint;
106
+ }
107
+ set endpoint(endpoint) {
108
+ // Normalize
109
+ endpoint = endpoint ?? null;
110
+ // Validate
111
+ endpoint = endpoint ? Validate.endpoint(endpoint) : null;
112
+ // Guard
113
+ if (this._endpoint === endpoint)
114
+ return;
115
+ // Mutate
116
+ this._endpoint = endpoint;
117
+ endpoint === null
118
+ ? this.removeAttribute(Attributes.ENDPOINT)
119
+ : this.setAttribute(Attributes.ENDPOINT, endpoint);
120
+ // Dispatch
121
+ this._dispatchEvent(Event.ON_ENDPOINT_CHANGE, { detail: { endpoint } });
122
+ }
123
+ /**
124
+ * The session token, read fresh from local storage on every access — never
125
+ * cached — because the License component renews and rewrites the key.
126
+ * @category Operation
127
+ * @hidden
128
+ */
129
+ get _token() {
130
+ return localStorage.getItem(Session);
131
+ }
132
+ /**
133
+ * Negotiates a client access URL from the configured endpoint, authenticating
134
+ * with the session token, then opens the live connection. No-op when the
135
+ * endpoint or token is unavailable, or when already connecting or connected.
136
+ * @category Operation
137
+ */
138
+ connect = async () => {
139
+ const token = this._token;
140
+ // Gate + guard
141
+ if (!this.endpoint || !token)
142
+ return;
143
+ if (this._connection !== Connection.DISCONNECTED)
144
+ return;
145
+ // Establish
146
+ this.connection = Connection.CONNECTING;
147
+ const url = await this._negotiate(token);
148
+ this._socket = this._open(url);
149
+ };
150
+ /**
151
+ * Closes the live connection.
152
+ * @category Operation
153
+ */
154
+ disconnect = () => {
155
+ this._socket?.close();
156
+ this._socket = null;
157
+ this.connection = Connection.DISCONNECTED;
158
+ };
159
+ /**
160
+ * Subscribes to the `onendpointchange` event. Assigning replaces the previous
161
+ * handler; assigning `null` removes it.
162
+ * @category Event
163
+ */
164
+ set onendpointchange(handler) {
165
+ this._onendpointchange &&
166
+ this.removeEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
167
+ this._onendpointchange = handler;
168
+ this._onendpointchange &&
169
+ this.addEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
170
+ }
171
+ /**
172
+ * Subscribes to the `onconnecting` event. Assigning replaces the previous
173
+ * handler; assigning `null` removes it.
174
+ * @category Event
175
+ */
176
+ set onconnecting(handler) {
177
+ this._onconnecting &&
178
+ this.removeEventListener(Event.ON_CONNECTING, this._onconnecting);
179
+ this._onconnecting = handler;
180
+ this._onconnecting &&
181
+ this.addEventListener(Event.ON_CONNECTING, this._onconnecting);
182
+ }
183
+ /**
184
+ * Subscribes to the `onconnected` event. Assigning replaces the previous
185
+ * handler; assigning `null` removes it.
186
+ * @category Event
187
+ */
188
+ set onconnected(handler) {
189
+ this._onconnected &&
190
+ this.removeEventListener(Event.ON_CONNECTED, this._onconnected);
191
+ this._onconnected = handler;
192
+ this._onconnected &&
193
+ this.addEventListener(Event.ON_CONNECTED, this._onconnected);
194
+ }
195
+ /**
196
+ * Subscribes to the `ondisconnected` event. Assigning replaces the previous
197
+ * handler; assigning `null` removes it.
198
+ * @category Event
199
+ */
200
+ set ondisconnected(handler) {
201
+ this._ondisconnected &&
202
+ this.removeEventListener(Event.ON_DISCONNECTED, this._ondisconnected);
203
+ this._ondisconnected = handler;
204
+ this._ondisconnected &&
205
+ this.addEventListener(Event.ON_DISCONNECTED, this._ondisconnected);
206
+ }
207
+ /**
208
+ * Subscribes to the `onmasterdatachange` event. Assigning replaces the
209
+ * previous handler; assigning `null` removes it.
210
+ * @category Event
211
+ */
212
+ set onmasterdatachange(handler) {
213
+ this._onmasterdatachange &&
214
+ this.removeEventListener(Event.ON_MASTERDATACHANGE, this._onmasterdatachange);
215
+ this._onmasterdatachange = handler;
216
+ this._onmasterdatachange &&
217
+ this.addEventListener(Event.ON_MASTERDATACHANGE, this._onmasterdatachange);
218
+ }
219
+ /**
220
+ * List operations to perform for selected attributes being observed in the DOM.
221
+ * @category Configuration
222
+ * @hidden
223
+ */
224
+ _attributeHandlers = {
225
+ [Attributes.CONNECTION]: (value) => value !== this._connection &&
226
+ this.setAttribute(Attributes.CONNECTION, this._connection),
227
+ [Attributes.ENDPOINT]: (value) => (this.endpoint = value),
228
+ };
229
+ /**
230
+ * Cache element references to improve performance
231
+ * @category Configuration
232
+ * @hidden
233
+ */
234
+ _cache = () => { };
235
+ /**
236
+ * Initialize component attributes with default values
237
+ * @category Configuration
238
+ * @hidden
239
+ */
240
+ _initialize = () => {
241
+ !this.hasAttribute(Attributes.CONNECTION) &&
242
+ this.setAttribute(Attributes.CONNECTION, this._connection);
243
+ };
244
+ /**
245
+ * Called by the connectedCallback prototypical method
246
+ * @category Configuration
247
+ * @hidden
248
+ */
249
+ _addEventListeners = () => { };
250
+ /**
251
+ * Called by the disconnectedCallback prototypical method
252
+ * @category Configuration
253
+ * @hidden
254
+ */
255
+ _removeEventListeners = () => { };
256
+ /**
257
+ * Negotiates a short-lived Web PubSub client access URL from the endpoint,
258
+ * authenticating the request with the session token.
259
+ * @category Operation
260
+ * @hidden
261
+ */
262
+ _negotiate = async (token) => {
263
+ const response = await fetch(this.endpoint, {
264
+ headers: { Authorization: `Bearer ${token}` },
265
+ });
266
+ const { url } = await response.json();
267
+ return url;
268
+ };
269
+ /**
270
+ * Opens a Web PubSub WebSocket with the required subprotocol and wires the
271
+ * connection lifecycle handlers.
272
+ * @category Operation
273
+ * @hidden
274
+ */
275
+ _open = (url) => {
276
+ const socket = new WebSocket(url, Protocol);
277
+ socket.onopen = () => (this.connection = Connection.CONNECTED);
278
+ socket.onclose = () => (this.connection = Connection.DISCONNECTED);
279
+ socket.addEventListener(Gesture.MESSAGE, this._handleMessage);
280
+ return socket;
281
+ };
282
+ /**
283
+ * Handles an inbound Web PubSub message (the `message` gesture). Dispatches
284
+ * `onmasterdatachange` when the payload's `data.type` is `masterData.changed`.
285
+ * @category Gesture
286
+ * @hidden
287
+ */
288
+ _handleMessage = (event) => {
289
+ const message = JSON.parse(event.data);
290
+ message.data?.type === Message.MASTER_DATA_CHANGED &&
291
+ this._dispatchEvent(Event.ON_MASTERDATACHANGE, {
292
+ detail: { datetime: new Date().toISOString() },
293
+ });
294
+ };
295
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @module Component
3
+ */
4
+ /**
5
+ * @category Configuration
6
+ */
7
+ export declare const Tag: "app-messenger";
8
+ /**
9
+ * Local storage key under which the session token is stored (and renewed) by
10
+ * the License component. Externalized so the contract lives in one place.
11
+ * @category Configuration
12
+ */
13
+ export declare const Session: "app-license.session";
14
+ /**
15
+ * The Azure Web PubSub subprotocol required for structured group messaging.
16
+ * @category Configuration
17
+ */
18
+ export declare const Protocol: "json.webpubsub.azure.v1";
19
+ /**
20
+ * Inbound message types the messenger reacts to (the `data.type` of a frame).
21
+ * @category Metadata
22
+ * @enum
23
+ */
24
+ export declare const Message: {
25
+ readonly MASTER_DATA_CHANGED: "masterData.changed";
26
+ };
27
+ /**
28
+ * Inbound message types the messenger reacts to.
29
+ * @category Metadata
30
+ */
31
+ export type Message = (typeof Message)[keyof typeof Message];
32
+ /**
33
+ * HTML Attributes available to set
34
+ * @category Metadata
35
+ * @enum
36
+ */
37
+ export declare const Attributes: {
38
+ readonly CONNECTION: "connection";
39
+ readonly ENDPOINT: "endpoint";
40
+ };
41
+ /**
42
+ * HTML Attributes available to set
43
+ * @category Metadata
44
+ */
45
+ export type Attributes = typeof Attributes;
46
+ /**
47
+ * HTML Attributes available to set
48
+ * @category Metadata
49
+ * @enum
50
+ */
51
+ export declare const State: {
52
+ readonly CONNECTION: "connection";
53
+ readonly ENDPOINT: "endpoint";
54
+ };
55
+ /**
56
+ * HTML Attributes available to set
57
+ * @category Metadata
58
+ */
59
+ export type State = (typeof State)[keyof typeof State];
60
+ /**
61
+ * Live connection status value domain
62
+ * @category Metadata
63
+ * @enum
64
+ */
65
+ export declare const Connection: {
66
+ readonly DISCONNECTED: "disconnected";
67
+ readonly CONNECTING: "connecting";
68
+ readonly CONNECTED: "connected";
69
+ };
70
+ /**
71
+ * Live connection status value domain
72
+ * @category Metadata
73
+ */
74
+ export type Connection = (typeof Connection)[keyof typeof Connection];
75
+ /**
76
+ * The negotiate endpoint URL value domain
77
+ * @category Metadata
78
+ */
79
+ export type Endpoint = string;
80
+ /**
81
+ * @category Metadata
82
+ * @enum
83
+ */
84
+ export declare const Operation: {
85
+ readonly CONNECT: "connect";
86
+ readonly DISCONNECT: "disconnect";
87
+ };
88
+ /**
89
+ * @category Metadata
90
+ */
91
+ export type Operation = (typeof Operation)[keyof typeof Operation];
92
+ /**
93
+ * @category Metadata
94
+ * @enum
95
+ */
96
+ export declare const Event: {
97
+ readonly ON_ENDPOINT_CHANGE: "onendpointchange";
98
+ readonly ON_CONNECTING: "onconnecting";
99
+ readonly ON_CONNECTED: "onconnected";
100
+ readonly ON_DISCONNECTED: "ondisconnected";
101
+ readonly ON_MASTERDATACHANGE: "onmasterdatachange";
102
+ };
103
+ /**
104
+ * @category Metadata
105
+ */
106
+ export type Event = (typeof Event)[keyof typeof Event];
107
+ /**
108
+ * @category Metadata
109
+ * @enum
110
+ */
111
+ export declare const Gesture: {
112
+ readonly MESSAGE: "message";
113
+ };
114
+ /**
115
+ * @category Metadata
116
+ */
117
+ export type Gesture = (typeof Gesture)[keyof typeof Gesture];
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @module Component
3
+ */
4
+ /**
5
+ * @category Configuration
6
+ */
7
+ export const Tag = "app-messenger";
8
+ /**
9
+ * Local storage key under which the session token is stored (and renewed) by
10
+ * the License component. Externalized so the contract lives in one place.
11
+ * @category Configuration
12
+ */
13
+ export const Session = "app-license.session";
14
+ /**
15
+ * The Azure Web PubSub subprotocol required for structured group messaging.
16
+ * @category Configuration
17
+ */
18
+ export const Protocol = "json.webpubsub.azure.v1";
19
+ /**
20
+ * Inbound message types the messenger reacts to (the `data.type` of a frame).
21
+ * @category Metadata
22
+ * @enum
23
+ */
24
+ export const Message = {
25
+ MASTER_DATA_CHANGED: "masterData.changed",
26
+ };
27
+ /**
28
+ * HTML Attributes available to set
29
+ * @category Metadata
30
+ * @enum
31
+ */
32
+ export const Attributes = {
33
+ CONNECTION: "connection",
34
+ ENDPOINT: "endpoint",
35
+ };
36
+ /**
37
+ * HTML Attributes available to set
38
+ * @category Metadata
39
+ * @enum
40
+ */
41
+ export const State = {
42
+ ...Attributes,
43
+ };
44
+ /**
45
+ * Live connection status value domain
46
+ * @category Metadata
47
+ * @enum
48
+ */
49
+ export const Connection = {
50
+ DISCONNECTED: "disconnected",
51
+ CONNECTING: "connecting",
52
+ CONNECTED: "connected",
53
+ };
54
+ /**
55
+ * @category Metadata
56
+ * @enum
57
+ */
58
+ export const Operation = {
59
+ CONNECT: "connect",
60
+ DISCONNECT: "disconnect",
61
+ };
62
+ /**
63
+ * @category Metadata
64
+ * @enum
65
+ */
66
+ export const Event = {
67
+ ON_ENDPOINT_CHANGE: "onendpointchange",
68
+ ON_CONNECTING: "onconnecting",
69
+ ON_CONNECTED: "onconnected",
70
+ ON_DISCONNECTED: "ondisconnected",
71
+ ON_MASTERDATACHANGE: "onmasterdatachange",
72
+ };
73
+ /**
74
+ * @category Metadata
75
+ * @enum
76
+ */
77
+ export const Gesture = {
78
+ MESSAGE: "message",
79
+ };
@@ -0,0 +1,6 @@
1
+ /* =======================
2
+ Host Element Styles
3
+ ======================= */
4
+ :host {
5
+ display: inline-block;
6
+ }
@@ -0,0 +1 @@
1
+ <template id="app-messenger"> </template>
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module Component
3
+ */
4
+ import { type Endpoint } from "./messenger.meta.js";
5
+ /**
6
+ * Runtime enforcement of declared value domains.
7
+ * @category Validation
8
+ */
9
+ export declare class Validate {
10
+ /**
11
+ * Validates the negotiate endpoint URL value domain.
12
+ * @category Validation
13
+ */
14
+ static endpoint: (value: string) => Endpoint;
15
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module Component
3
+ */
4
+ /**
5
+ * Runtime enforcement of declared value domains.
6
+ * @category Validation
7
+ */
8
+ export class Validate {
9
+ /**
10
+ * Validates the negotiate endpoint URL value domain.
11
+ * @category Validation
12
+ */
13
+ static endpoint = (value) => {
14
+ try {
15
+ new URL(value);
16
+ }
17
+ catch {
18
+ throw new Error(`Invalid endpoint value: ${value}`);
19
+ }
20
+ return value;
21
+ };
22
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@effect.healthcare/messenger",
3
+ "version": "0.3.1",
4
+ "description": "Azure Pub Sub Messenger Web Component",
5
+ "keywords": [
6
+ "web component"
7
+ ],
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "types": "dist/index.d.ts",
14
+ "scripts": {
15
+ "clean.test": "node ./tasks/clean.test.mjs",
16
+ "pretest": "npm run clean.test && tsc --project tsconfig.test.json",
17
+ "test": "karma start",
18
+ "posttest": "npm run clean.test",
19
+ "prebuild": "node ./tasks/clean.build.mjs",
20
+ "build": "tsc --project tsconfig.build.json",
21
+ "postbuild": "node ./tasks/copy.build.mjs",
22
+ "preserve": "npm run build",
23
+ "serve": "web-dev-server",
24
+ "document": "npx typedoc --tsconfig tsconfig.test.json",
25
+ "prepublish": "npm run build"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/effect-healthcare/messenger.git"
30
+ },
31
+ "author": "Scalable.Software",
32
+ "license": "CC-BY-NC-SA-4.0",
33
+ "bugs": {
34
+ "url": "https://github.com/effect-healthcare/messenger/issues"
35
+ },
36
+ "homepage": "https://github.com/effect-healthcare/messenger#readme",
37
+ "devDependencies": {
38
+ "@types/jasmine": "^5.1.4",
39
+ "@types/node": "^26.0.1",
40
+ "@web/dev-server": "^0.4.6",
41
+ "jasmine": "^5.1.0",
42
+ "karma": "^6.4.3",
43
+ "karma-chrome-launcher": "^3.2.0",
44
+ "karma-coverage-istanbul-instrumenter": "^1.0.4",
45
+ "karma-coverage-istanbul-reporter": "^3.0.3",
46
+ "karma-jasmine": "^5.1.0",
47
+ "karma-spec-reporter": "^0.0.36",
48
+ "typedoc": "^0.28.19",
49
+ "typedoc-plugin-merge-modules": "^7.0.0",
50
+ "typescript": "^6.0.3"
51
+ },
52
+ "dependencies": {
53
+ "@scalable.software/component": "^0.3.0"
54
+ }
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export {
2
+ Tag,
3
+ Session,
4
+ Protocol,
5
+ Message,
6
+ Attributes,
7
+ State,
8
+ Connection,
9
+ Operation,
10
+ Event,
11
+ Gesture,
12
+ } from "./messenger.meta.js";
13
+
14
+ export { Validate } from "./messenger.validation.js";
15
+
16
+ export { Messenger } from "./messenger.js";