@adcops/autocore-react 3.3.54 → 3.3.57

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.
@@ -1 +1 @@
1
- {"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,EASV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAQf;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;gDACgD;AAChD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,OAAO,EAAE,GAAG,CAAC;IACb,OAAO,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,cAAc,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IAEvB,KAAK,EAAE,YAAY,CAAC;IAEpB,SAAS,EAAE,YAAY,CAAC;IACxB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAEjD;;qCAEiC;IACjC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACpE;uEACmE;IACnE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnG,QAAQ,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAA;KAAE,CAAC;CACnD;AAeD,QAAA,MAAM,UAAU,gCAUd,CAAC;AAmCH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAkLlD,CAAC;AAMF,eAAO,MAAM,MAAM,uBAAyC,CAAC;AAC7D,eAAO,MAAM,aAAa,sBAA0C,CAAC;AACrE,eAAO,MAAM,WAAW,oBAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,wCA3QF,iBAAiB,KAAK,IAAI,CA8QnD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,YAAY,MAAM,EAAE,WAAW,MAAM;;;;CAY/D,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;;;;;;CAuBvC,CAAC;AAIF,OAAO,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,EASV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAQf;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAAA;CAAE,CAAC;AAErE,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IAEvB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED;gDACgD;AAChD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,GAAG,GAAG,IAAI,CAAC;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC;IACd,OAAO,EAAE,GAAG,CAAC;IACb,OAAO,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,cAAc,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IAEvB,KAAK,EAAE,YAAY,CAAC;IAEpB,SAAS,EAAE,YAAY,CAAC;IACxB,YAAY,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAEjD;;qCAEiC;IACjC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACpE;uEACmE;IACnE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACnG,QAAQ,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAA;KAAE,CAAC;CACnD;AAeD,QAAA,MAAM,UAAU,gCAUd,CAAC;AAmCH,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAmLlD,CAAC;AAMF,eAAO,MAAM,MAAM,uBAAyC,CAAC;AAC7D,eAAO,MAAM,aAAa,sBAA0C,CAAC;AACrE,eAAO,MAAM,WAAW,oBAA0C,CAAC;AACnE,eAAO,MAAM,eAAe,wCA5QF,iBAAiB,KAAK,IAAI,CA+QnD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,YAAY,MAAM,EAAE,WAAW,MAAM;;;;CAY/D,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;;;;;;CAuBvC,CAAC;AAIF,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -30,6 +30,19 @@ export declare class HubWebSocket extends HubBase {
30
30
  * Used to match asynchronous WebSocket responses to their original requests.
31
31
  */
32
32
  private pendingRequests;
33
+ /**
34
+ * Messages queued while the WebSocket is still in CONNECTING state.
35
+ * Flushed in `socket.onopen`. Without this queue, an `invoke()` fired
36
+ * from a React `useEffect` on first mount races the WS handshake —
37
+ * `socket.send()` on a non-OPEN socket throws a synchronous
38
+ * DOMException ("object not, or is no longer, usable"), which the
39
+ * caller sees as an opaque rejection and the underlying request is
40
+ * never sent.
41
+ *
42
+ * Each entry carries the request id so we can reject the matching
43
+ * pending Promise if the WS closes before we ever flush.
44
+ */
45
+ private sendQueue;
33
46
  /**
34
47
  * Initializes the WebSocket connection immediately.
35
48
  */
@@ -1 +1 @@
1
- {"version":3,"file":"HubWebSocket.d.ts","sourceRoot":"","sources":["../../src/hub/HubWebSocket.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA+DpE;;GAEG;AACH,qBAAa,YAAa,SAAQ,OAAO;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,SAAS,CAAK;IAEtB;;;OAGG;IACH,OAAO,CAAC,eAAe,CAAoC;IAE3D;;OAEG;;IA4FH;;;;;OAKG;IACH,MAAM,GAAI,OAAO,MAAM,EAAE,aAAa,WAAW,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,cAAc,CAAC,CAqB5F;IAED;;;OAGG;IACH,wBAAwB,GAAI,KAAK,cAAc,UAO9C;IAED,UAAU,QAAO,IAAI,CAEpB;IAED,SAAS,CAAC,IAAI,GAAI,WAAW,MAAM,EAAE,UAAU,MAAM,KAAG,IAAI,CAE3D;CACJ"}
1
+ {"version":3,"file":"HubWebSocket.d.ts","sourceRoot":"","sources":["../../src/hub/HubWebSocket.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA+DpE;;GAEG;AACH,qBAAa,YAAa,SAAQ,OAAO;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,SAAS,CAAK;IAEtB;;;OAGG;IACH,OAAO,CAAC,eAAe,CAAoC;IAE3D;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,SAAS,CAA2C;IAE5D;;OAEG;;IAqHH;;;;;OAKG;IACH,MAAM,GAAI,OAAO,MAAM,EAAE,aAAa,WAAW,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,cAAc,CAAC,CA6C5F;IAED;;;OAGG;IACH,wBAAwB,GAAI,KAAK,cAAc,UAO9C;IAED,UAAU,QAAO,IAAI,CAEpB;IAED,SAAS,CAAC,IAAI,GAAI,WAAW,MAAM,EAAE,UAAU,MAAM,KAAG,IAAI,CAE3D;CACJ"}
@@ -1 +1 @@
1
- import{HubBase}from"./HubBase";import{MessageType}from"./CommandMessage";import{getDebugPanel}from"./DebugPanel";function b64toBlob(e,t="application/octet-stream",s=512){const n=atob(e),o=[];for(let e=0;e<n.length;e+=s){const t=n.slice(e,e+s),a=new Array(t.length);for(let e=0;e<t.length;e++)a[e]=t.charCodeAt(e);const i=new Uint8Array(a);o.push(i.buffer)}return new Blob(o,{type:t})}function downloadBlob(e,t){const s=window.URL.createObjectURL(e),n=document.createElement("a");n.href=s,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),window.URL.revokeObjectURL(s)}export class HubWebSocket extends HubBase{constructor(){super(),Object.defineProperty(this,"socket",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"requestId",{enumerable:!0,configurable:!0,writable:!0,value:0}),Object.defineProperty(this,"pendingRequests",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"invoke",{enumerable:!0,configurable:!0,writable:!0,value:(e,t,s)=>new Promise((n,o)=>{const a=++this.requestId;this.pendingRequests.set(a,{resolve:n,reject:o});const i={transaction_id:a,topic:e,data:s||{},timecode:Date.now(),message_type:t,success:!1,error_message:""};getDebugPanel().messageSent(MessageType[t],e,a),this.socket.send(JSON.stringify(i))})}),Object.defineProperty(this,"handleUnsolicitedMessage",{enumerable:!0,configurable:!0,writable:!0,value:e=>{e.message_type==MessageType.Broadcast&&e.topic.length>1&&this.publish(e.topic,e.data)}}),Object.defineProperty(this,"disconnect",{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.socket.close()}}),Object.defineProperty(this,"emit",{enumerable:!0,configurable:!0,writable:!0,value:(e,t)=>{this.socket.send(JSON.stringify({eventName:e,payload:t}))}}),getDebugPanel().hubCreated();const e=window.location.hostname,t=window.location.port,s=`${"https:"===window.location.protocol?"wss://":"ws://"}${e}${t?":"+t:""}/ws/`;this.socket=new WebSocket(s);let n=this;this.socket.onopen=function(){getDebugPanel().wsOpened(s),n.publish("HUB/connected",!0),n.setIsConnected(!0)},this.socket.onmessage=e=>{const t=JSON.parse(e.data);if(getDebugPanel().messageReceived(MessageType[t.message_type]||String(t.message_type),t.topic,t.transaction_id),t.transaction_id&&this.pendingRequests.has(t.transaction_id)){const{resolve:e,reject:s}=this.pendingRequests.get(t.transaction_id);if("FILE_DOWNLOAD"===t.topic&&void 0!==t.data.file_name&&null!==t.data.file_name&&t.data.file_name.length>0){let n=t.data.file_name.split("/");const o=n[n.length-1];if(t.success){downloadBlob(b64toBlob(t.data),o),t.data=o,e(t)}else s(new Error(t.error_message))}else t.success?e(t):s(new Error(t.error_message));this.pendingRequests.delete(t.transaction_id)}else this.handleUnsolicitedMessage(t)},this.socket.onerror=e=>{getDebugPanel().wsError(String(e))},this.socket.onclose=e=>{n.setIsConnected(!1),getDebugPanel().wsClosed(e.code,e.reason||"No reason")}}}
1
+ import{HubBase}from"./HubBase";import{MessageType}from"./CommandMessage";import{getDebugPanel}from"./DebugPanel";function b64toBlob(e,t="application/octet-stream",s=512){const n=atob(e),o=[];for(let e=0;e<n.length;e+=s){const t=n.slice(e,e+s),a=new Array(t.length);for(let e=0;e<t.length;e++)a[e]=t.charCodeAt(e);const i=new Uint8Array(a);o.push(i.buffer)}return new Blob(o,{type:t})}function downloadBlob(e,t){const s=window.URL.createObjectURL(e),n=document.createElement("a");n.href=s,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),window.URL.revokeObjectURL(s)}export class HubWebSocket extends HubBase{constructor(){super(),Object.defineProperty(this,"socket",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"requestId",{enumerable:!0,configurable:!0,writable:!0,value:0}),Object.defineProperty(this,"pendingRequests",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"sendQueue",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"invoke",{enumerable:!0,configurable:!0,writable:!0,value:(e,t,s)=>new Promise((n,o)=>{const a=++this.requestId;this.pendingRequests.set(a,{resolve:n,reject:o});const i={transaction_id:a,topic:e,data:s||{},timecode:Date.now(),message_type:t,success:!1,error_message:""};getDebugPanel().messageSent(MessageType[t],e,a);const r=JSON.stringify(i);switch(this.socket.readyState){case WebSocket.OPEN:this.socket.send(r);break;case WebSocket.CONNECTING:this.sendQueue.push({id:a,json:r});break;default:this.pendingRequests.delete(a),o(new Error(`WebSocket not open (readyState=${this.socket.readyState}); cannot send '${e}'`))}})}),Object.defineProperty(this,"handleUnsolicitedMessage",{enumerable:!0,configurable:!0,writable:!0,value:e=>{e.message_type==MessageType.Broadcast&&e.topic.length>1&&this.publish(e.topic,e.data)}}),Object.defineProperty(this,"disconnect",{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.socket.close()}}),Object.defineProperty(this,"emit",{enumerable:!0,configurable:!0,writable:!0,value:(e,t)=>{this.socket.send(JSON.stringify({eventName:e,payload:t}))}}),getDebugPanel().hubCreated();const e=window.location.hostname,t=window.location.port,s=`${"https:"===window.location.protocol?"wss://":"ws://"}${e}${t?":"+t:""}/ws/`;this.socket=new WebSocket(s);let n=this;this.socket.onopen=function(){if(getDebugPanel().wsOpened(s),n.sendQueue.length>0){for(const{json:e}of n.sendQueue)n.socket.send(e);n.sendQueue=[]}n.publish("HUB/connected",!0),n.setIsConnected(!0)},this.socket.onmessage=e=>{const t=JSON.parse(e.data);if(getDebugPanel().messageReceived(MessageType[t.message_type]||String(t.message_type),t.topic,t.transaction_id),t.transaction_id&&this.pendingRequests.has(t.transaction_id)){const{resolve:e,reject:s}=this.pendingRequests.get(t.transaction_id);if("FILE_DOWNLOAD"===t.topic&&void 0!==t.data.file_name&&null!==t.data.file_name&&t.data.file_name.length>0){let n=t.data.file_name.split("/");const o=n[n.length-1];if(t.success){downloadBlob(b64toBlob(t.data),o),t.data=o,e(t)}else s(new Error(t.error_message))}else t.success?e(t):s(new Error(t.error_message));this.pendingRequests.delete(t.transaction_id)}else this.handleUnsolicitedMessage(t)},this.socket.onerror=e=>{getDebugPanel().wsError(String(e))},this.socket.onclose=e=>{if(n.setIsConnected(!1),getDebugPanel().wsClosed(e.code,e.reason||"No reason"),n.sendQueue.length>0){for(const{id:e}of n.sendQueue){const t=n.pendingRequests.get(e);t&&(t.reject(new Error("WebSocket closed before message could be sent")),n.pendingRequests.delete(e))}n.sendQueue=[]}}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.54",
3
+ "version": "3.3.57",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -191,7 +191,9 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
191
191
  });
192
192
 
193
193
  // -----------------------------------------------------------------
194
- // Schema load — once on mount
194
+ // Schema load — once on mount. The Hub's `invoke()` queues sends
195
+ // while the WS is still CONNECTING and flushes them on `onopen`,
196
+ // so this works whether or not the handshake has finished yet.
195
197
  // -----------------------------------------------------------------
196
198
  useEffect(() => {
197
199
  let cancelled = false;
@@ -213,7 +215,6 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
213
215
  }
214
216
  })();
215
217
  return () => { cancelled = true; };
216
- // We intentionally only run once on mount — schemas are stable.
217
218
  // eslint-disable-next-line react-hooks/exhaustive-deps
218
219
  }, []);
219
220
 
@@ -98,13 +98,27 @@ function downloadBlob(blob: Blob, filename: string): void {
98
98
  export class HubWebSocket extends HubBase {
99
99
  private socket: WebSocket;
100
100
  private requestId = 0;
101
-
102
- /**
101
+
102
+ /**
103
103
  * Map of pending Transaction ID -> Promise Resolvers.
104
104
  * Used to match asynchronous WebSocket responses to their original requests.
105
105
  */
106
106
  private pendingRequests = new Map<number, RequestRecord>();
107
107
 
108
+ /**
109
+ * Messages queued while the WebSocket is still in CONNECTING state.
110
+ * Flushed in `socket.onopen`. Without this queue, an `invoke()` fired
111
+ * from a React `useEffect` on first mount races the WS handshake —
112
+ * `socket.send()` on a non-OPEN socket throws a synchronous
113
+ * DOMException ("object not, or is no longer, usable"), which the
114
+ * caller sees as an opaque rejection and the underlying request is
115
+ * never sent.
116
+ *
117
+ * Each entry carries the request id so we can reject the matching
118
+ * pending Promise if the WS closes before we ever flush.
119
+ */
120
+ private sendQueue: Array<{ id: number; json: string }> = [];
121
+
108
122
  /**
109
123
  * Initializes the WebSocket connection immediately.
110
124
  */
@@ -127,6 +141,18 @@ export class HubWebSocket extends HubBase {
127
141
  this.socket.onopen = function () {
128
142
  console.log("WebSocket connection established.");
129
143
  getDebugPanel().wsOpened(wsUrl);
144
+ // Flush any messages that `invoke()` queued while the WS
145
+ // was still in CONNECTING. Order is preserved by Array push
146
+ // / for-of iteration. Done BEFORE publishing HUB/connected
147
+ // and setIsConnected(true) so any subscriber that fires its
148
+ // own invoke() in response sees the queue already drained.
149
+ if (self.sendQueue.length > 0) {
150
+ console.log(`[HubWebSocket] Flushing ${self.sendQueue.length} queued message(s)`);
151
+ for (const { json } of self.sendQueue) {
152
+ self.socket.send(json);
153
+ }
154
+ self.sendQueue = [];
155
+ }
130
156
  // Notify app that we are online
131
157
  self.publish("HUB/connected", true);
132
158
  self.setIsConnected(true);
@@ -195,6 +221,19 @@ export class HubWebSocket extends HubBase {
195
221
  self.setIsConnected(false);
196
222
  console.log('WebSocket connection closed.');
197
223
  getDebugPanel().wsClosed(event.code, event.reason || 'No reason');
224
+
225
+ // Anything queued but never sent must reject — its Promise
226
+ // is in pendingRequests and would otherwise hang forever.
227
+ if (self.sendQueue.length > 0) {
228
+ for (const { id } of self.sendQueue) {
229
+ const req = self.pendingRequests.get(id);
230
+ if (req) {
231
+ req.reject(new Error('WebSocket closed before message could be sent'));
232
+ self.pendingRequests.delete(id);
233
+ }
234
+ }
235
+ self.sendQueue = [];
236
+ }
198
237
  };
199
238
  }
200
239
 
@@ -224,7 +263,31 @@ export class HubWebSocket extends HubBase {
224
263
  };
225
264
 
226
265
  getDebugPanel().messageSent(MessageType[messageType], topic, id);
227
- this.socket.send(JSON.stringify(cm));
266
+ const json = JSON.stringify(cm);
267
+
268
+ // Branch on the WS state. The browser throws a synchronous
269
+ // DOMException if you send() on a socket that isn't OPEN, so
270
+ // every consumer would otherwise need its own
271
+ // `subscribe('HUB/connected', ...)` guard. Centralising it
272
+ // here means useEffect-on-mount calls "just work" — they
273
+ // get queued and flushed when the handshake completes.
274
+ switch (this.socket.readyState) {
275
+ case WebSocket.OPEN:
276
+ this.socket.send(json);
277
+ break;
278
+ case WebSocket.CONNECTING:
279
+ this.sendQueue.push({ id, json });
280
+ break;
281
+ default:
282
+ // CLOSING / CLOSED — the WS is gone, no point queuing.
283
+ // Fail fast so the caller sees a real error instead
284
+ // of hanging.
285
+ this.pendingRequests.delete(id);
286
+ reject(new Error(
287
+ `WebSocket not open (readyState=${this.socket.readyState}); ` +
288
+ `cannot send '${topic}'`
289
+ ));
290
+ }
228
291
  });
229
292
  }
230
293