@effect.healthcare/messenger 0.4.1 → 0.5.0

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/.claude/CLAUDE.md CHANGED
@@ -78,11 +78,11 @@ When a new component's shape conflicts with a rule, classify the rule before rec
78
78
 
79
79
  For advanced testing or debugging, the live web component can be served and driven through the `chrome-devtools` MCP. This gives access to the rendered component, its shadow DOM, the console, and DOM manipulation.
80
80
 
81
- 1. **Start the dev server** — `npm run serve`. The `preserve` hook builds first (`src/` → `dist/`), and `web-dev-server.config.mjs` is auto-loaded (`rootDir: "."`, `port: 8000`, `watch: true`). Run it in the background. The server serves the repo root because the demo loads `../importmap` and `./dist` via the import map.
82
- 2. **Navigate the controlled browser** — `mcp__chrome-devtools__navigate_page` to `http://localhost:8000/demo/index.html`. Always use the explicit `/demo/index.html` path; the demo uses relative asset paths and will not resolve at `/`.
81
+ 1. **Start the dev server** — `npm run serve`. The `preserve` hook builds first (`src/` → `dist/`), and `web-dev-server.config.mjs` is auto-loaded (`rootDir: "."`, `port: 3000`, `watch: true`). Run it in the background. The server serves the repo root because the demo loads `../importmap` and `./dist` via the import map. Port 3000 is the origin the backend CORS-whitelists, so do not change it.
82
+ 2. **Navigate the controlled browser** — `mcp__chrome-devtools__navigate_page` to `http://localhost:3000/demo/index.html`. Always use the explicit `/demo/index.html` path; the demo uses relative asset paths and will not resolve at `/`.
83
83
  3. **Verify** — `mcp__chrome-devtools__evaluate_script` to inspect the component (e.g. `customElements.get("app-messenger")` is defined; the base framework exposes the shadow root as `el.root`); `mcp__chrome-devtools__list_console_messages` to check for errors.
84
- 4. **Clean up when done** — navigate the page to `about:blank` (the MCP keeps one page alive, so the last page cannot be closed), then stop the dev server process on port 8000.
84
+ 4. **Clean up when done** — navigate the page to `about:blank` (the MCP keeps one page alive, so the last page cannot be closed), then stop the dev server process on port 3000.
85
85
 
86
86
  Notes:
87
87
  - MCP tool schemas must be loaded via `ToolSearch` (`select:<name>`) before first use each session.
88
- - If a stale server already holds port 8000, the new one fails to bind — kill the straggler rather than letting it drift to another port.
88
+ - If a stale server already holds port 3000, the new one fails to bind — kill the straggler rather than letting it drift to another port.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { Tag, Session, Protocol, Message, Attributes, State, Connection, Operation, Event, Gesture, } from "./messenger.meta.js";
1
+ export { Tag, Session, Endpoint, Protocol, Message, Attributes, State, Connection, Operation, Event, Gesture, } from "./messenger.meta.js";
2
+ export { type Path } from "./messenger.meta.js";
2
3
  export { Validate } from "./messenger.validation.js";
3
4
  export { Messenger } from "./messenger.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Tag, Session, Protocol, Message, Attributes, State, Connection, Operation, Event, Gesture, } from "./messenger.meta.js";
1
+ export { Tag, Session, Endpoint, Protocol, Message, Attributes, State, Connection, Operation, Event, Gesture, } from "./messenger.meta.js";
2
2
  export { Validate } from "./messenger.validation.js";
3
3
  export { Messenger } from "./messenger.js";
@@ -4,7 +4,7 @@
4
4
  import { Component } from "@scalable.software/component";
5
5
  import { type Configuration } from "@scalable.software/component";
6
6
  import { Attributes, Connection } from "./messenger.meta.js";
7
- import { type Endpoint } from "./messenger.meta.js";
7
+ import { type Path } from "./messenger.meta.js";
8
8
  /**
9
9
  * Configuration for the headless component — no template or stylesheet.
10
10
  * @category Configuration
@@ -31,11 +31,11 @@ export declare class Messenger extends Component {
31
31
  */
32
32
  private _connection;
33
33
  /**
34
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
34
+ * The negotiate request path. When absent the messenger stays disconnected.
35
35
  * @category State
36
36
  * @hidden
37
37
  */
38
- private _endpoint;
38
+ private _path;
39
39
  /**
40
40
  * The live Web PubSub socket. Held so it can be closed and replaced.
41
41
  * @category State
@@ -47,7 +47,7 @@ export declare class Messenger extends Component {
47
47
  * @category Event
48
48
  * @hidden
49
49
  */
50
- private _onendpointchange;
50
+ private _onpathchange;
51
51
  private _onconnecting;
52
52
  private _onconnected;
53
53
  private _ondisconnected;
@@ -70,11 +70,11 @@ export declare class Messenger extends Component {
70
70
  get connection(): Connection;
71
71
  protected set connection(connection: Connection);
72
72
  /**
73
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
73
+ * The negotiate request path. When absent the messenger stays disconnected.
74
74
  * @category State
75
75
  */
76
- get endpoint(): Endpoint | null;
77
- set endpoint(endpoint: Endpoint | null);
76
+ get path(): Path | null;
77
+ set path(path: Path | null);
78
78
  /**
79
79
  * The session token, read fresh from local storage on every access — never
80
80
  * cached — because the License component renews and rewrites the key.
@@ -83,9 +83,18 @@ export declare class Messenger extends Component {
83
83
  */
84
84
  private get _token();
85
85
  /**
86
- * Negotiates a client access URL from the configured endpoint, authenticating
86
+ * The endpoint host base URL, read fresh from local storage on every access —
87
+ * never cached — mirroring the session token. Composed with the `path` state
88
+ * to form the negotiate URL. When absent the messenger stays disconnected.
89
+ * @category State
90
+ * @hidden
91
+ */
92
+ private get _host();
93
+ /**
94
+ * Negotiates a client access URL from the configured path, authenticating
87
95
  * with the session token, then opens the live connection. No-op when the
88
- * endpoint or token is unavailable, or when already connecting or connected.
96
+ * path or token is unavailable, or when already connecting or connected.
97
+ * When negotiate fails the connection returns to `disconnected`.
89
98
  * @category Operation
90
99
  */
91
100
  connect: () => Promise<void>;
@@ -103,11 +112,11 @@ export declare class Messenger extends Component {
103
112
  */
104
113
  send: (message: object) => boolean;
105
114
  /**
106
- * Subscribes to the `onendpointchange` event. Assigning replaces the previous
115
+ * Subscribes to the `onpathchange` event. Assigning replaces the previous
107
116
  * handler; assigning `null` removes it.
108
117
  * @category Event
109
118
  */
110
- set onendpointchange(handler: EventListener | null);
119
+ set onpathchange(handler: EventListener | null);
111
120
  /**
112
121
  * Subscribes to the `onconnecting` event. Assigning replaces the previous
113
122
  * handler; assigning `null` removes it.
@@ -139,7 +148,7 @@ export declare class Messenger extends Component {
139
148
  */
140
149
  protected _attributeHandlers: {
141
150
  connection: (value: Connection) => false | void;
142
- endpoint: (value: Endpoint) => string;
151
+ path: (value: Path) => string;
143
152
  };
144
153
  /**
145
154
  * Cache element references to improve performance
@@ -160,13 +169,14 @@ export declare class Messenger extends Component {
160
169
  */
161
170
  protected _addEventListeners: () => void;
162
171
  /**
163
- * Called by the disconnectedCallback prototypical method
172
+ * Called by the disconnectedCallback prototypical method. Closes the live
173
+ * connection so a removed element does not keep a socket open.
164
174
  * @category Configuration
165
175
  * @hidden
166
176
  */
167
177
  protected _removeEventListeners: () => void;
168
178
  /**
169
- * Negotiates a short-lived Web PubSub client access URL from the endpoint,
179
+ * Negotiates a short-lived Web PubSub client access URL from the path,
170
180
  * authenticating the request with the session token.
171
181
  * @category Operation
172
182
  * @hidden
package/dist/messenger.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // Base Component
5
5
  import { Component } from "@scalable.software/component";
6
6
  // Component Metadata
7
- import { Tag, Attributes, Connection, Event, Gesture, Session, Protocol, Message, } from "./messenger.meta.js";
7
+ import { Tag, Attributes, Connection, Event, Gesture, Session, Endpoint, Protocol, Message, } from "./messenger.meta.js";
8
8
  // Component Validation
9
9
  import { Validate } from "./messenger.validation.js";
10
10
  /**
@@ -39,11 +39,11 @@ export class Messenger extends Component {
39
39
  */
40
40
  _connection = Connection.DISCONNECTED;
41
41
  /**
42
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
42
+ * The negotiate request path. When absent the messenger stays disconnected.
43
43
  * @category State
44
44
  * @hidden
45
45
  */
46
- _endpoint = null;
46
+ _path = null;
47
47
  /**
48
48
  * The live Web PubSub socket. Held so it can be closed and replaced.
49
49
  * @category State
@@ -55,7 +55,7 @@ export class Messenger extends Component {
55
55
  * @category Event
56
56
  * @hidden
57
57
  */
58
- _onendpointchange = null;
58
+ _onpathchange = null;
59
59
  _onconnecting = null;
60
60
  _onconnected = null;
61
61
  _ondisconnected = null;
@@ -97,27 +97,27 @@ export class Messenger extends Component {
97
97
  this._dispatchEvent(Event.ON_DISCONNECTED, event);
98
98
  }
99
99
  /**
100
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
100
+ * The negotiate request path. When absent the messenger stays disconnected.
101
101
  * @category State
102
102
  */
103
- get endpoint() {
104
- return this._endpoint;
103
+ get path() {
104
+ return this._path;
105
105
  }
106
- set endpoint(endpoint) {
106
+ set path(path) {
107
107
  // Normalize
108
- endpoint = endpoint ?? null;
108
+ path = path ?? null;
109
109
  // Validate
110
- endpoint = endpoint ? Validate.endpoint(endpoint) : null;
110
+ path = path ? Validate.path(path) : null;
111
111
  // Guard
112
- if (this._endpoint === endpoint)
112
+ if (this._path === path)
113
113
  return;
114
114
  // Mutate
115
- this._endpoint = endpoint;
116
- endpoint === null
117
- ? this.removeAttribute(Attributes.ENDPOINT)
118
- : this.setAttribute(Attributes.ENDPOINT, endpoint);
115
+ this._path = path;
116
+ path === null
117
+ ? this.removeAttribute(Attributes.PATH)
118
+ : this.setAttribute(Attributes.PATH, path);
119
119
  // Dispatch
120
- this._dispatchEvent(Event.ON_ENDPOINT_CHANGE, { detail: { endpoint } });
120
+ this._dispatchEvent(Event.ON_PATH_CHANGE, { detail: { path } });
121
121
  }
122
122
  /**
123
123
  * The session token, read fresh from local storage on every access — never
@@ -129,22 +129,40 @@ export class Messenger extends Component {
129
129
  return localStorage.getItem(Session);
130
130
  }
131
131
  /**
132
- * Negotiates a client access URL from the configured endpoint, authenticating
132
+ * The endpoint host base URL, read fresh from local storage on every access —
133
+ * never cached — mirroring the session token. Composed with the `path` state
134
+ * to form the negotiate URL. When absent the messenger stays disconnected.
135
+ * @category State
136
+ * @hidden
137
+ */
138
+ get _host() {
139
+ return localStorage.getItem(Endpoint);
140
+ }
141
+ /**
142
+ * Negotiates a client access URL from the configured path, authenticating
133
143
  * with the session token, then opens the live connection. No-op when the
134
- * endpoint or token is unavailable, or when already connecting or connected.
144
+ * path or token is unavailable, or when already connecting or connected.
145
+ * When negotiate fails the connection returns to `disconnected`.
135
146
  * @category Operation
136
147
  */
137
148
  connect = async () => {
138
149
  const token = this._token;
139
150
  // Gate + guard
140
- if (!this.endpoint || !token)
151
+ if (!this._host || !this.path || !token)
141
152
  return;
142
153
  if (this._connection !== Connection.DISCONNECTED)
143
154
  return;
144
- // Establish
155
+ // Establish — on negotiate failure, recover to disconnected
145
156
  this.connection = Connection.CONNECTING;
146
- const url = await this._negotiate(token);
147
- this._socket = this._open(url);
157
+ try {
158
+ const url = await this._negotiate(token);
159
+ if (this.connection !== Connection.CONNECTING)
160
+ return;
161
+ this._socket = this._open(url);
162
+ }
163
+ catch {
164
+ this.connection = Connection.DISCONNECTED;
165
+ }
148
166
  };
149
167
  /**
150
168
  * Closes the live connection.
@@ -176,16 +194,16 @@ export class Messenger extends Component {
176
194
  return true;
177
195
  };
178
196
  /**
179
- * Subscribes to the `onendpointchange` event. Assigning replaces the previous
197
+ * Subscribes to the `onpathchange` event. Assigning replaces the previous
180
198
  * handler; assigning `null` removes it.
181
199
  * @category Event
182
200
  */
183
- set onendpointchange(handler) {
184
- this._onendpointchange &&
185
- this.removeEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
186
- this._onendpointchange = handler;
187
- this._onendpointchange &&
188
- this.addEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
201
+ set onpathchange(handler) {
202
+ this._onpathchange &&
203
+ this.removeEventListener(Event.ON_PATH_CHANGE, this._onpathchange);
204
+ this._onpathchange = handler;
205
+ this._onpathchange &&
206
+ this.addEventListener(Event.ON_PATH_CHANGE, this._onpathchange);
189
207
  }
190
208
  /**
191
209
  * Subscribes to the `onconnecting` event. Assigning replaces the previous
@@ -243,7 +261,7 @@ export class Messenger extends Component {
243
261
  _attributeHandlers = {
244
262
  [Attributes.CONNECTION]: (value) => value !== this._connection &&
245
263
  this.setAttribute(Attributes.CONNECTION, this._connection),
246
- [Attributes.ENDPOINT]: (value) => (this.endpoint = value),
264
+ [Attributes.PATH]: (value) => (this.path = value),
247
265
  };
248
266
  /**
249
267
  * Cache element references to improve performance
@@ -267,22 +285,25 @@ export class Messenger extends Component {
267
285
  */
268
286
  _addEventListeners = () => { };
269
287
  /**
270
- * Called by the disconnectedCallback prototypical method
288
+ * Called by the disconnectedCallback prototypical method. Closes the live
289
+ * connection so a removed element does not keep a socket open.
271
290
  * @category Configuration
272
291
  * @hidden
273
292
  */
274
- _removeEventListeners = () => { };
293
+ _removeEventListeners = () => this.disconnect();
275
294
  /**
276
- * Negotiates a short-lived Web PubSub client access URL from the endpoint,
295
+ * Negotiates a short-lived Web PubSub client access URL from the path,
277
296
  * authenticating the request with the session token.
278
297
  * @category Operation
279
298
  * @hidden
280
299
  */
281
300
  _negotiate = async (token) => {
282
- const response = await fetch(this.endpoint, {
283
- headers: { Authorization: `JWT ${token}` },
301
+ const response = await fetch(`${this._host}${this.path}`, {
302
+ headers: { Authorization: `Bearer ${token}` },
284
303
  });
285
304
  const { url } = await response.json();
305
+ if (!url)
306
+ throw new Error("Invalid negotiate response: missing url");
286
307
  return url;
287
308
  };
288
309
  /**
@@ -11,6 +11,12 @@ export declare const Tag: "app-messenger";
11
11
  * @category Configuration
12
12
  */
13
13
  export declare const Session: "app-license.session";
14
+ /**
15
+ * Local storage key under which the endpoint host base URL is cached by the
16
+ * Configurator component. Composed with the `path` state to form the negotiate URL.
17
+ * @category Configuration
18
+ */
19
+ export declare const Endpoint: "data";
14
20
  /**
15
21
  * The Azure Web PubSub subprotocol required for structured group messaging.
16
22
  * @category Configuration
@@ -36,7 +42,7 @@ export type Message = (typeof Message)[keyof typeof Message];
36
42
  */
37
43
  export declare const Attributes: {
38
44
  readonly CONNECTION: "connection";
39
- readonly ENDPOINT: "endpoint";
45
+ readonly PATH: "path";
40
46
  };
41
47
  /**
42
48
  * HTML Attributes available to set
@@ -50,7 +56,7 @@ export type Attributes = typeof Attributes;
50
56
  */
51
57
  export declare const State: {
52
58
  readonly CONNECTION: "connection";
53
- readonly ENDPOINT: "endpoint";
59
+ readonly PATH: "path";
54
60
  };
55
61
  /**
56
62
  * HTML Attributes available to set
@@ -73,10 +79,10 @@ export declare const Connection: {
73
79
  */
74
80
  export type Connection = (typeof Connection)[keyof typeof Connection];
75
81
  /**
76
- * The negotiate endpoint URL value domain
82
+ * The negotiate request path value domain
77
83
  * @category Metadata
78
84
  */
79
- export type Endpoint = string;
85
+ export type Path = string;
80
86
  /**
81
87
  * @category Metadata
82
88
  * @enum
@@ -95,7 +101,7 @@ export type Operation = (typeof Operation)[keyof typeof Operation];
95
101
  * @enum
96
102
  */
97
103
  export declare const Event: {
98
- readonly ON_ENDPOINT_CHANGE: "onendpointchange";
104
+ readonly ON_PATH_CHANGE: "onpathchange";
99
105
  readonly ON_CONNECTING: "onconnecting";
100
106
  readonly ON_CONNECTED: "onconnected";
101
107
  readonly ON_DISCONNECTED: "ondisconnected";
@@ -11,6 +11,12 @@ export const Tag = "app-messenger";
11
11
  * @category Configuration
12
12
  */
13
13
  export const Session = "app-license.session";
14
+ /**
15
+ * Local storage key under which the endpoint host base URL is cached by the
16
+ * Configurator component. Composed with the `path` state to form the negotiate URL.
17
+ * @category Configuration
18
+ */
19
+ export const Endpoint = "data";
14
20
  /**
15
21
  * The Azure Web PubSub subprotocol required for structured group messaging.
16
22
  * @category Configuration
@@ -31,7 +37,7 @@ export const Message = {
31
37
  */
32
38
  export const Attributes = {
33
39
  CONNECTION: "connection",
34
- ENDPOINT: "endpoint",
40
+ PATH: "path",
35
41
  };
36
42
  /**
37
43
  * HTML Attributes available to set
@@ -65,7 +71,7 @@ export const Operation = {
65
71
  * @enum
66
72
  */
67
73
  export const Event = {
68
- ON_ENDPOINT_CHANGE: "onendpointchange",
74
+ ON_PATH_CHANGE: "onpathchange",
69
75
  ON_CONNECTING: "onconnecting",
70
76
  ON_CONNECTED: "onconnected",
71
77
  ON_DISCONNECTED: "ondisconnected",
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * @module Component
3
3
  */
4
- import { type Endpoint } from "./messenger.meta.js";
4
+ import { type Path } from "./messenger.meta.js";
5
5
  /**
6
6
  * Runtime enforcement of declared value domains.
7
7
  * @category Validation
8
8
  */
9
9
  export declare class Validate {
10
10
  /**
11
- * Validates the negotiate endpoint URL value domain.
11
+ * Validates the negotiate request path value domain.
12
12
  * @category Validation
13
13
  */
14
- static endpoint: (value: string) => Endpoint;
14
+ static path: (value: string) => Path;
15
15
  }
@@ -7,15 +7,12 @@
7
7
  */
8
8
  export class Validate {
9
9
  /**
10
- * Validates the negotiate endpoint URL value domain.
10
+ * Validates the negotiate request path value domain.
11
11
  * @category Validation
12
12
  */
13
- static endpoint = (value) => {
14
- try {
15
- new URL(value);
16
- }
17
- catch {
18
- throw new Error(`Invalid endpoint value: ${value}`);
13
+ static path = (value) => {
14
+ if (!value.startsWith("/")) {
15
+ throw new Error(`Invalid path value: ${value}`);
19
16
  }
20
17
  return value;
21
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect.healthcare/messenger",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Azure Pub Sub Messenger Web Component",
5
5
  "keywords": [
6
6
  "web component"
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  Tag,
3
3
  Session,
4
+ Endpoint,
4
5
  Protocol,
5
6
  Message,
6
7
  Attributes,
@@ -11,6 +12,8 @@ export {
11
12
  Gesture,
12
13
  } from "./messenger.meta.js";
13
14
 
15
+ export { type Path } from "./messenger.meta.js";
16
+
14
17
  export { Validate } from "./messenger.validation.js";
15
18
 
16
19
  export { Messenger } from "./messenger.js";
@@ -14,6 +14,13 @@ export const Tag = "app-messenger" as const;
14
14
  */
15
15
  export const Session = "app-license.session" as const;
16
16
 
17
+ /**
18
+ * Local storage key under which the endpoint host base URL is cached by the
19
+ * Configurator component. Composed with the `path` state to form the negotiate URL.
20
+ * @category Configuration
21
+ */
22
+ export const Endpoint = "data" as const;
23
+
17
24
  /**
18
25
  * The Azure Web PubSub subprotocol required for structured group messaging.
19
26
  * @category Configuration
@@ -42,7 +49,7 @@ export type Message = (typeof Message)[keyof typeof Message];
42
49
  */
43
50
  export const Attributes = {
44
51
  CONNECTION: "connection",
45
- ENDPOINT: "endpoint",
52
+ PATH: "path",
46
53
  } as const;
47
54
  /**
48
55
  * HTML Attributes available to set
@@ -83,10 +90,10 @@ export const Connection = {
83
90
  export type Connection = (typeof Connection)[keyof typeof Connection];
84
91
 
85
92
  /**
86
- * The negotiate endpoint URL value domain
93
+ * The negotiate request path value domain
87
94
  * @category Metadata
88
95
  */
89
- export type Endpoint = string;
96
+ export type Path = string;
90
97
 
91
98
  /**
92
99
  * @category Metadata
@@ -108,7 +115,7 @@ export type Operation = (typeof Operation)[keyof typeof Operation];
108
115
  * @enum
109
116
  */
110
117
  export const Event = {
111
- ON_ENDPOINT_CHANGE: "onendpointchange",
118
+ ON_PATH_CHANGE: "onpathchange",
112
119
  ON_CONNECTING: "onconnecting",
113
120
  ON_CONNECTED: "onconnected",
114
121
  ON_DISCONNECTED: "ondisconnected",
package/src/messenger.ts CHANGED
@@ -14,11 +14,12 @@ import {
14
14
  Event,
15
15
  Gesture,
16
16
  Session,
17
+ Endpoint,
17
18
  Protocol,
18
19
  Message,
19
20
  } from "./messenger.meta.js";
20
21
 
21
- import { type Endpoint } from "./messenger.meta.js";
22
+ import { type Path } from "./messenger.meta.js";
22
23
 
23
24
  // Component Validation
24
25
  import { Validate } from "./messenger.validation.js";
@@ -59,11 +60,11 @@ export class Messenger extends Component {
59
60
  private _connection: Connection = Connection.DISCONNECTED;
60
61
 
61
62
  /**
62
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
63
+ * The negotiate request path. When absent the messenger stays disconnected.
63
64
  * @category State
64
65
  * @hidden
65
66
  */
66
- private _endpoint: Endpoint | null = null;
67
+ private _path: Path | null = null;
67
68
 
68
69
  /**
69
70
  * The live Web PubSub socket. Held so it can be closed and replaced.
@@ -77,7 +78,7 @@ export class Messenger extends Component {
77
78
  * @category Event
78
79
  * @hidden
79
80
  */
80
- private _onendpointchange: EventListener | null = null;
81
+ private _onpathchange: EventListener | null = null;
81
82
  private _onconnecting: EventListener | null = null;
82
83
  private _onconnected: EventListener | null = null;
83
84
  private _ondisconnected: EventListener | null = null;
@@ -125,31 +126,31 @@ export class Messenger extends Component {
125
126
  }
126
127
 
127
128
  /**
128
- * The negotiate endpoint URL. When absent the messenger stays disconnected.
129
+ * The negotiate request path. When absent the messenger stays disconnected.
129
130
  * @category State
130
131
  */
131
- public get endpoint(): Endpoint | null {
132
- return this._endpoint;
132
+ public get path(): Path | null {
133
+ return this._path;
133
134
  }
134
135
 
135
- public set endpoint(endpoint: Endpoint | null) {
136
+ public set path(path: Path | null) {
136
137
  // Normalize
137
- endpoint = endpoint ?? null;
138
+ path = path ?? null;
138
139
 
139
140
  // Validate
140
- endpoint = endpoint ? Validate.endpoint(endpoint) : null;
141
+ path = path ? Validate.path(path) : null;
141
142
 
142
143
  // Guard
143
- if (this._endpoint === endpoint) return;
144
+ if (this._path === path) return;
144
145
 
145
146
  // Mutate
146
- this._endpoint = endpoint;
147
- endpoint === null
148
- ? this.removeAttribute(Attributes.ENDPOINT)
149
- : this.setAttribute(Attributes.ENDPOINT, endpoint);
147
+ this._path = path;
148
+ path === null
149
+ ? this.removeAttribute(Attributes.PATH)
150
+ : this.setAttribute(Attributes.PATH, path);
150
151
 
151
152
  // Dispatch
152
- this._dispatchEvent(Event.ON_ENDPOINT_CHANGE, { detail: { endpoint } });
153
+ this._dispatchEvent(Event.ON_PATH_CHANGE, { detail: { path } });
153
154
  }
154
155
 
155
156
  /**
@@ -163,22 +164,39 @@ export class Messenger extends Component {
163
164
  }
164
165
 
165
166
  /**
166
- * Negotiates a client access URL from the configured endpoint, authenticating
167
+ * The endpoint host base URL, read fresh from local storage on every access —
168
+ * never cached — mirroring the session token. Composed with the `path` state
169
+ * to form the negotiate URL. When absent the messenger stays disconnected.
170
+ * @category State
171
+ * @hidden
172
+ */
173
+ private get _host(): string | null {
174
+ return localStorage.getItem(Endpoint);
175
+ }
176
+
177
+ /**
178
+ * Negotiates a client access URL from the configured path, authenticating
167
179
  * 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.
180
+ * path or token is unavailable, or when already connecting or connected.
181
+ * When negotiate fails the connection returns to `disconnected`.
169
182
  * @category Operation
170
183
  */
171
184
  public connect = async (): Promise<void> => {
172
185
  const token = this._token;
173
186
 
174
187
  // Gate + guard
175
- if (!this.endpoint || !token) return;
188
+ if (!this._host || !this.path || !token) return;
176
189
  if (this._connection !== Connection.DISCONNECTED) return;
177
190
 
178
- // Establish
191
+ // Establish — on negotiate failure, recover to disconnected
179
192
  this.connection = Connection.CONNECTING;
180
- const url = await this._negotiate(token);
181
- this._socket = this._open(url);
193
+ try {
194
+ const url = await this._negotiate(token);
195
+ if (this.connection !== Connection.CONNECTING) return;
196
+ this._socket = this._open(url);
197
+ } catch {
198
+ this.connection = Connection.DISCONNECTED;
199
+ }
182
200
  };
183
201
 
184
202
  /**
@@ -216,21 +234,18 @@ export class Messenger extends Component {
216
234
  };
217
235
 
218
236
  /**
219
- * Subscribes to the `onendpointchange` event. Assigning replaces the previous
237
+ * Subscribes to the `onpathchange` event. Assigning replaces the previous
220
238
  * handler; assigning `null` removes it.
221
239
  * @category Event
222
240
  */
223
- public set onendpointchange(handler: EventListener | null) {
224
- this._onendpointchange &&
225
- this.removeEventListener(
226
- Event.ON_ENDPOINT_CHANGE,
227
- this._onendpointchange,
228
- );
241
+ public set onpathchange(handler: EventListener | null) {
242
+ this._onpathchange &&
243
+ this.removeEventListener(Event.ON_PATH_CHANGE, this._onpathchange);
229
244
 
230
- this._onendpointchange = handler;
245
+ this._onpathchange = handler;
231
246
 
232
- this._onendpointchange &&
233
- this.addEventListener(Event.ON_ENDPOINT_CHANGE, this._onendpointchange);
247
+ this._onpathchange &&
248
+ this.addEventListener(Event.ON_PATH_CHANGE, this._onpathchange);
234
249
  }
235
250
 
236
251
  /**
@@ -308,7 +323,7 @@ export class Messenger extends Component {
308
323
  [Attributes.CONNECTION]: (value: Connection) =>
309
324
  value !== this._connection &&
310
325
  this.setAttribute(Attributes.CONNECTION, this._connection),
311
- [Attributes.ENDPOINT]: (value: Endpoint) => (this.endpoint = value),
326
+ [Attributes.PATH]: (value: Path) => (this.path = value),
312
327
  };
313
328
 
314
329
  /**
@@ -336,23 +351,25 @@ export class Messenger extends Component {
336
351
  protected _addEventListeners = () => {};
337
352
 
338
353
  /**
339
- * Called by the disconnectedCallback prototypical method
354
+ * Called by the disconnectedCallback prototypical method. Closes the live
355
+ * connection so a removed element does not keep a socket open.
340
356
  * @category Configuration
341
357
  * @hidden
342
358
  */
343
- protected _removeEventListeners = () => {};
359
+ protected _removeEventListeners = () => this.disconnect();
344
360
 
345
361
  /**
346
- * Negotiates a short-lived Web PubSub client access URL from the endpoint,
362
+ * Negotiates a short-lived Web PubSub client access URL from the path,
347
363
  * authenticating the request with the session token.
348
364
  * @category Operation
349
365
  * @hidden
350
366
  */
351
367
  private _negotiate = async (token: string): Promise<string> => {
352
- const response = await fetch(this.endpoint as string, {
353
- headers: { Authorization: `JWT ${token}` },
368
+ const response = await fetch(`${this._host}${this.path}`, {
369
+ headers: { Authorization: `Bearer ${token}` },
354
370
  });
355
371
  const { url } = await response.json();
372
+ if (!url) throw new Error("Invalid negotiate response: missing url");
356
373
  return url;
357
374
  };
358
375
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  // Component Metadata
6
- import { type Endpoint } from "./messenger.meta.js";
6
+ import { type Path } from "./messenger.meta.js";
7
7
 
8
8
  /**
9
9
  * Runtime enforcement of declared value domains.
@@ -11,15 +11,13 @@ import { type Endpoint } from "./messenger.meta.js";
11
11
  */
12
12
  export class Validate {
13
13
  /**
14
- * Validates the negotiate endpoint URL value domain.
14
+ * Validates the negotiate request path value domain.
15
15
  * @category Validation
16
16
  */
17
- public static endpoint = (value: string) => {
18
- try {
19
- new URL(value);
20
- } catch {
21
- throw new Error(`Invalid endpoint value: ${value}`);
17
+ public static path = (value: string) => {
18
+ if (!value.startsWith("/")) {
19
+ throw new Error(`Invalid path value: ${value}`);
22
20
  }
23
- return value as Endpoint;
21
+ return value as Path;
24
22
  };
25
23
  }