modality-kit 0.8.4 → 0.8.6

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/index.js CHANGED
@@ -5644,9 +5644,429 @@ class JSONRPCManager extends JSONRPCCall {
5644
5644
  this.methods.clear();
5645
5645
  }
5646
5646
  }
5647
+ // src/websocket-client.ts
5648
+ var logger3 = getLoggerInstance("WebSocket-Client");
5649
+
5650
+ class WebSocketClient {
5651
+ ws = null;
5652
+ url;
5653
+ config = {
5654
+ initialReconnectDelay: 1000,
5655
+ maxReconnectDelay: 30000,
5656
+ maxReconnectAttempts: 10,
5657
+ callTimeout: 5000,
5658
+ heartbeatInterval: 30000,
5659
+ enableKeepAlive: true,
5660
+ handleMessage: (_validMessage, _ws) => {}
5661
+ };
5662
+ connectionId = null;
5663
+ cleanupInterval = null;
5664
+ isManualDisconnect = false;
5665
+ reconnectAttempts = 0;
5666
+ reconnectDelay;
5667
+ heartbeatInterval = null;
5668
+ constructor(url, config) {
5669
+ if (!this.isValidWebSocketUrl(url)) {
5670
+ throw new Error(`Invalid WebSocket URL: ${url}. Must use ws:// or wss:// protocol.`);
5671
+ }
5672
+ this.config = { ...this.config, ...config };
5673
+ this.url = url;
5674
+ this.reconnectDelay = this.config.initialReconnectDelay;
5675
+ }
5676
+ isValidWebSocketUrl(url) {
5677
+ try {
5678
+ const parsed = new URL(url);
5679
+ return parsed.protocol === "ws:" || parsed.protocol === "wss:";
5680
+ } catch {
5681
+ return false;
5682
+ }
5683
+ }
5684
+ stopCleanupInterval() {
5685
+ if (this.cleanupInterval !== null) {
5686
+ clearInterval(this.cleanupInterval);
5687
+ this.cleanupInterval = null;
5688
+ }
5689
+ }
5690
+ stopHeartbeat() {
5691
+ if (this.heartbeatInterval !== null) {
5692
+ clearInterval(this.heartbeatInterval);
5693
+ this.heartbeatInterval = null;
5694
+ }
5695
+ }
5696
+ startHeartbeat() {
5697
+ if (!this.config.enableKeepAlive)
5698
+ return;
5699
+ this.stopHeartbeat();
5700
+ this.heartbeatInterval = setInterval(() => {
5701
+ if (this.isConnected()) {
5702
+ this.send({ method: "ping" });
5703
+ }
5704
+ }, this.config.heartbeatInterval);
5705
+ }
5706
+ attemptReconnect() {
5707
+ if (this.reconnectAttempts < this.config.maxReconnectAttempts) {
5708
+ this.reconnectAttempts++;
5709
+ logger3.info(`Attempting to reconnect (${this.reconnectAttempts}/${this.config.maxReconnectAttempts}) in ${this.reconnectDelay}ms`);
5710
+ setTimeout(() => {
5711
+ this.isManualDisconnect = false;
5712
+ this.connect();
5713
+ }, this.reconnectDelay);
5714
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.config.maxReconnectDelay);
5715
+ } else {
5716
+ logger3.error("Max reconnection attempts reached, will try again in 60 seconds");
5717
+ setTimeout(() => {
5718
+ this.reconnectAttempts = 0;
5719
+ this.reconnectDelay = this.config.initialReconnectDelay;
5720
+ this.attemptReconnect();
5721
+ }, 300000);
5722
+ }
5723
+ }
5724
+ onOpen(event) {
5725
+ logger3.info("WebSocket connection opened:", event);
5726
+ }
5727
+ onClose(event) {
5728
+ logger3.info("WebSocket connection closed:", event);
5729
+ }
5730
+ getClientId() {
5731
+ const url = new URL(this.url);
5732
+ return url.searchParams.get("clientId") ?? (this.connectionId ? String(this.connectionId) : "");
5733
+ }
5734
+ send(data) {
5735
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
5736
+ try {
5737
+ const message = JSON.stringify({
5738
+ ...data,
5739
+ jsonrpc: "2.0",
5740
+ timestamp: new Date().toISOString()
5741
+ });
5742
+ this.ws.send(message);
5743
+ logger3.info("Message sent:", message);
5744
+ return true;
5745
+ } catch (error) {
5746
+ logger3.error("Error sending message:", error);
5747
+ return false;
5748
+ }
5749
+ } else {
5750
+ logger3.warn("WebSocket is not connected during send operation.");
5751
+ return false;
5752
+ }
5753
+ }
5754
+ isConnected() {
5755
+ return this.ws?.readyState === WebSocket.OPEN;
5756
+ }
5757
+ getInfo() {
5758
+ return {
5759
+ url: this.url,
5760
+ connected: this.isConnected(),
5761
+ connectionId: this.connectionId,
5762
+ clientId: this.getClientId()
5763
+ };
5764
+ }
5765
+ forceReconnect() {
5766
+ logger3.info("Forcing reconnection...");
5767
+ this.isManualDisconnect = false;
5768
+ this.reconnectAttempts = 0;
5769
+ this.reconnectDelay = this.config.initialReconnectDelay;
5770
+ if (this.ws) {
5771
+ this.ws.close(1000, "Force reconnect");
5772
+ } else {
5773
+ this.connect();
5774
+ }
5775
+ }
5776
+ getHeartbeatInterval() {
5777
+ return this.config.heartbeatInterval;
5778
+ }
5779
+ getEnableKeepAlive() {
5780
+ return this.config.enableKeepAlive;
5781
+ }
5782
+ disconnect() {
5783
+ this.isManualDisconnect = true;
5784
+ this.stopCleanupInterval();
5785
+ this.stopHeartbeat();
5786
+ if (this.ws) {
5787
+ this.ws.close(1000, "Manual disconnect");
5788
+ this.ws = null;
5789
+ this.connectionId = null;
5790
+ }
5791
+ }
5792
+ connect() {
5793
+ try {
5794
+ this.isManualDisconnect = false;
5795
+ this.ws = new WebSocket(this.url);
5796
+ this.ws.onopen = (event) => {
5797
+ logger3.info("WebSocket connected:", event);
5798
+ this.reconnectAttempts = 0;
5799
+ this.reconnectDelay = this.config.initialReconnectDelay;
5800
+ this.startHeartbeat();
5801
+ this.onOpen(event);
5802
+ };
5803
+ this.ws.onclose = (event) => {
5804
+ this.connectionId = null;
5805
+ this.stopHeartbeat();
5806
+ this.onClose(event);
5807
+ if (!this.isManualDisconnect) {
5808
+ this.attemptReconnect();
5809
+ }
5810
+ };
5811
+ this.ws.onmessage = (event) => {
5812
+ logger3.info("WebSocket message received:", event.data);
5813
+ try {
5814
+ const message = JSONRPCUtils.deserialize(event.data);
5815
+ if (!message) {
5816
+ throw new Error(`deserialize returned null or undefined ${event.data}`);
5817
+ }
5818
+ const validMessage = JSONRPCUtils.validateMessage(message);
5819
+ if (validMessage.valid) {
5820
+ const message2 = validMessage.message;
5821
+ if (message2.method === "server.connected") {
5822
+ this.connectionId = (message2.params || {}).connectionId;
5823
+ } else {
5824
+ this.config.handleMessage(validMessage, this);
5825
+ }
5826
+ } else {
5827
+ throw new Error(`Invalid message: ${validMessage.error}`);
5828
+ }
5829
+ } catch (error) {
5830
+ logger3.error("Error deserializing WebSocket message:", error);
5831
+ return;
5832
+ }
5833
+ };
5834
+ } catch (error) {
5835
+ logger3.error("Error creating WebSocket connection:", error);
5836
+ }
5837
+ }
5838
+ }
5839
+ // src/ReactiveComponent.ts
5840
+ class ReactiveComponent extends HTMLElement {
5841
+ #state;
5842
+ #isRendering = false;
5843
+ #pendingUpdate = false;
5844
+ #hasRendered = false;
5845
+ #shadow;
5846
+ #options;
5847
+ #delegatedEventListeners = {};
5848
+ #eventTypes = [
5849
+ "click",
5850
+ "dblclick",
5851
+ "mousedown",
5852
+ "mouseup",
5853
+ "mousemove",
5854
+ "mouseover",
5855
+ "mouseout",
5856
+ "focus",
5857
+ "blur",
5858
+ "change",
5859
+ "input",
5860
+ "submit",
5861
+ "keydown",
5862
+ "keyup",
5863
+ "keypress"
5864
+ ];
5865
+ _stores;
5866
+ _storeListeners;
5867
+ constructor(options = {}) {
5868
+ super();
5869
+ this.#options = options;
5870
+ this.#state = options.initialState || {};
5871
+ }
5872
+ get state() {
5873
+ return Object.freeze({ ...this.#state });
5874
+ }
5875
+ setState(updator, _action, prevState) {
5876
+ const oldState = prevState || { ...this.#state };
5877
+ let newUpdates;
5878
+ if (typeof updator === "function") {
5879
+ newUpdates = updator(this.#state);
5880
+ } else {
5881
+ newUpdates = updator;
5882
+ }
5883
+ const newState = { ...this.#state, ...newUpdates };
5884
+ if (this.#options.shouldUpdate && !this.#options.shouldUpdate(newState, oldState)) {
5885
+ return this.#state;
5886
+ } else {
5887
+ this.#state = newState;
5888
+ this.#scheduleUpdate(oldState);
5889
+ return newState;
5890
+ }
5891
+ }
5892
+ forceUpdate() {
5893
+ this.#scheduleUpdate({ ...this.#state });
5894
+ }
5895
+ #scheduleUpdate(previousState) {
5896
+ if (this.#isRendering || this.#pendingUpdate) {
5897
+ return;
5898
+ }
5899
+ this.#pendingUpdate = true;
5900
+ queueMicrotask(() => {
5901
+ if (this.#pendingUpdate && this.isConnected) {
5902
+ this.#performUpdate(previousState);
5903
+ }
5904
+ });
5905
+ }
5906
+ #performUpdate(previousState) {
5907
+ this.#isRendering = true;
5908
+ this.#pendingUpdate = false;
5909
+ const wasFirstRender = !this.#hasRendered;
5910
+ const prevState = previousState || { ...this.#state };
5911
+ try {
5912
+ this.#updateDOM();
5913
+ this.#hasRendered = true;
5914
+ if (!wasFirstRender && typeof this.componentDidUpdate === "function") {
5915
+ this.componentDidUpdate(this.#state, prevState);
5916
+ }
5917
+ } catch (error) {
5918
+ console.error("Error during component update:", error);
5919
+ } finally {
5920
+ this.#isRendering = false;
5921
+ }
5922
+ }
5923
+ static #trustedTypesPolicy = (() => {
5924
+ if (typeof window !== "undefined" && window.trustedTypes && window.trustedTypes.createPolicy) {
5925
+ try {
5926
+ return window.trustedTypes.createPolicy("reactive-component", {
5927
+ createHTML: (string) => string
5928
+ });
5929
+ } catch (e) {
5930
+ console.warn("Failed to create trusted types policy:", e);
5931
+ return null;
5932
+ }
5933
+ }
5934
+ return null;
5935
+ })();
5936
+ #safeSetInnerHTML(element, html) {
5937
+ if (ReactiveComponent.#trustedTypesPolicy) {
5938
+ element.innerHTML = ReactiveComponent.#trustedTypesPolicy.createHTML(html);
5939
+ } else {
5940
+ element.innerHTML = html;
5941
+ }
5942
+ }
5943
+ #updateDOM() {
5944
+ if (!this.#shadow) {
5945
+ this.#shadow = this.shadowRoot || this.attachShadow({ mode: "open" });
5946
+ this.#setupEventDelegation();
5947
+ }
5948
+ if (typeof window !== "undefined" && window.trustedTypes && window.trustedTypes.emptyHTML) {
5949
+ this.#shadow.innerHTML = window.trustedTypes.emptyHTML;
5950
+ } else {
5951
+ while (this.#shadow.firstChild) {
5952
+ this.#shadow.removeChild(this.#shadow.firstChild);
5953
+ }
5954
+ }
5955
+ const renderResult = this.render();
5956
+ if (typeof renderResult === "string") {
5957
+ const template = document.createElement("template");
5958
+ this.#safeSetInnerHTML(template, renderResult);
5959
+ this.#shadow.appendChild(template.content.cloneNode(true));
5960
+ } else if (renderResult instanceof DocumentFragment || renderResult instanceof Element) {
5961
+ this.#shadow.appendChild(renderResult);
5962
+ }
5963
+ }
5964
+ #setupEventDelegation() {
5965
+ this.#eventTypes.forEach((eventType) => {
5966
+ const listener = (e) => {
5967
+ this.#handleDelegatedEvent(e);
5968
+ };
5969
+ this.#delegatedEventListeners[eventType] = listener;
5970
+ const usePassive = !["mousedown", "keydown", "submit"].includes(eventType);
5971
+ this.#shadow.addEventListener(eventType, listener, {
5972
+ passive: usePassive
5973
+ });
5974
+ });
5975
+ }
5976
+ #handleDelegatedEvent(e) {
5977
+ const eventType = e.type;
5978
+ const target = e.target;
5979
+ if (!target)
5980
+ return;
5981
+ const handlerAttribute = `data-${eventType}`;
5982
+ const elementsWithHandlers = this.#shadow.querySelectorAll(`[${handlerAttribute}]`);
5983
+ Array.from(elementsWithHandlers).forEach((element) => {
5984
+ if (target.isSameNode(element) || element.contains(target)) {
5985
+ const handlerName = element.getAttribute(handlerAttribute);
5986
+ if (handlerName && typeof this[handlerName] === "function") {
5987
+ this[handlerName](e);
5988
+ }
5989
+ }
5990
+ });
5991
+ }
5992
+ connectedCallback() {
5993
+ if (this._stores && this._stores.length > 0) {
5994
+ this._storeListeners = [];
5995
+ this._stores.forEach((store) => {
5996
+ const boundSetState = this.setState.bind(this);
5997
+ store.addListener(boundSetState);
5998
+ this._storeListeners.push({ store, listener: boundSetState });
5999
+ const initialState = store.getState();
6000
+ this.setState(initialState);
6001
+ });
6002
+ }
6003
+ this.#performUpdate();
6004
+ if (typeof this.componentDidMount === "function") {
6005
+ this.componentDidMount();
6006
+ }
6007
+ }
6008
+ disconnectedCallback() {
6009
+ if (this.#shadow && Object.keys(this.#delegatedEventListeners).length > 0) {
6010
+ this.#eventTypes.forEach((eventType) => {
6011
+ const listener = this.#delegatedEventListeners[eventType];
6012
+ if (listener) {
6013
+ this.#shadow.removeEventListener(eventType, listener);
6014
+ }
6015
+ });
6016
+ this.#delegatedEventListeners = {};
6017
+ }
6018
+ if (this._storeListeners && this._storeListeners.length > 0) {
6019
+ this._storeListeners.forEach(({ store, listener }) => {
6020
+ store.removeListener(listener);
6021
+ });
6022
+ this._storeListeners = [];
6023
+ }
6024
+ }
6025
+ }
6026
+ function render(componentName, props = {}, stores) {
6027
+ const element = document.createElement(componentName);
6028
+ const { appendTo = document.body, ...restProps } = props;
6029
+ Object.keys(restProps).forEach((key) => {
6030
+ const value = restProps[key];
6031
+ if (value != null) {
6032
+ const stringValue = typeof value === "object" ? JSON.stringify(value) : String(value);
6033
+ element.setAttribute(key, stringValue);
6034
+ }
6035
+ });
6036
+ if (stores && stores.length > 0) {
6037
+ element._stores = stores;
6038
+ }
6039
+ if (appendTo instanceof HTMLElement) {
6040
+ appendTo.appendChild(element);
6041
+ }
6042
+ return element;
6043
+ }
6044
+ var lazyStores = { current: {} };
6045
+ function registerStore(componentName, stores) {
6046
+ const connectionSectionElement = document.querySelector("connection-section");
6047
+ if (connectionSectionElement) {
6048
+ connectionSectionElement._stores = stores;
6049
+ } else {
6050
+ lazyStores.current[componentName] = stores;
6051
+ }
6052
+ }
6053
+ if (typeof document !== "undefined") {
6054
+ document.addEventListener("DOMContentLoaded", () => {
6055
+ const promise = Object.keys(lazyStores.current).map(async (componentName) => {
6056
+ const connectionSectionElement = document.querySelector(componentName);
6057
+ if (connectionSectionElement) {
6058
+ connectionSectionElement._stores = lazyStores.current[componentName];
6059
+ delete lazyStores.current[componentName];
6060
+ }
6061
+ });
6062
+ Promise.all(promise);
6063
+ });
6064
+ }
5647
6065
  export {
5648
6066
  withErrorHandling,
5649
6067
  setupAITools,
6068
+ render,
6069
+ registerStore,
5650
6070
  loadVersion,
5651
6071
  getLoggerInstance,
5652
6072
  formatSuccessResponse,
@@ -5654,7 +6074,9 @@ export {
5654
6074
  emptySchema,
5655
6075
  createDataPendingOperations,
5656
6076
  compressWithLanguageDetection as compressText,
6077
+ WebSocketClient,
5657
6078
  exports_schemas_symbol as SymbolTypes,
6079
+ ReactiveComponent,
5658
6080
  JSONRPCUtils,
5659
6081
  JSONRPCManager,
5660
6082
  JSONRPCErrorCode,
@@ -11,7 +11,7 @@ type StateType<TState> = Partial<TState> | StateCallbackHandler<TState> | TState
11
11
  /**
12
12
  * Base class for reactive web components
13
13
  */
14
- export declare abstract class ReactiveHTMLElement<TState = any> extends HTMLElement {
14
+ export declare abstract class ReactiveComponent<TState = any> extends HTMLElement {
15
15
  #private;
16
16
  _stores?: Array<any>;
17
17
  _storeListeners?: Array<{
@@ -60,5 +60,5 @@ export type { ReactiveComponentOptions };
60
60
  * Generic render function for ReactiveHTMLElement components
61
61
  * Creates and displays a component with specified componentName and optional stores
62
62
  */
63
- export declare function render<T extends ReactiveHTMLElement>(componentName: string, props?: Record<string, any>, stores?: Array<any>): T;
63
+ export declare function render<T extends ReactiveComponent>(componentName: string, props?: Record<string, any>, stores?: Array<any>): T;
64
64
  export declare function registerStore(componentName: string, stores: Array<any>): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -14,3 +14,5 @@ export { JSONRPCUtils, JSONRPCErrorCode } from "./schemas/jsonrpc";
14
14
  export type { JSONRPCMessage, JSONRPCRequest, JSONRPCNotification, JSONRPCResponse, JSONRPCBatchRequest, JSONRPCBatchResponse, JSONRPCErrorResponse, JSONRPCValidationResult, JSONRPCError, JSONRPCParams, CommandExecuteParams, NotificationSendParams, } from "./schemas/jsonrpc";
15
15
  export { JSONRPCManager } from "./jsonrpc-manager";
16
16
  export type { JSONRPCManagerEvents, JSONRPCManagerConfig, } from "./jsonrpc-manager";
17
+ export { WebSocketClient } from "./websocket-client";
18
+ export { ReactiveComponent, render, registerStore } from "./ReactiveComponent";
@@ -0,0 +1,55 @@
1
+ import type { JSONRPCValidationResult } from "./schemas/jsonrpc";
2
+ interface WebSocketConfig {
3
+ maxReconnectAttempts: number;
4
+ initialReconnectDelay: number;
5
+ maxReconnectDelay: number;
6
+ callTimeout: number;
7
+ heartbeatInterval: number;
8
+ enableKeepAlive: boolean;
9
+ handleMessage: (validMessage: JSONRPCValidationResult, ws: WebSocketClient) => void;
10
+ }
11
+ interface WebSocketInfo {
12
+ url: string;
13
+ connected: boolean;
14
+ clientId: string;
15
+ connectionId: number | null;
16
+ pendingCalls?: number;
17
+ }
18
+ export declare class WebSocketClient {
19
+ private ws;
20
+ private url;
21
+ private config;
22
+ private connectionId;
23
+ private cleanupInterval;
24
+ private isManualDisconnect;
25
+ private reconnectAttempts;
26
+ private reconnectDelay;
27
+ private heartbeatInterval;
28
+ constructor(url: string, config?: Record<keyof WebSocketConfig, any>);
29
+ private isValidWebSocketUrl;
30
+ private stopCleanupInterval;
31
+ private stopHeartbeat;
32
+ private startHeartbeat;
33
+ private attemptReconnect;
34
+ private onOpen;
35
+ private onClose;
36
+ private getClientId;
37
+ send(data: any): boolean;
38
+ isConnected(): boolean;
39
+ getInfo(): WebSocketInfo;
40
+ /**
41
+ * Force reconnection even if currently connected
42
+ */
43
+ forceReconnect(): void;
44
+ /**
45
+ * Get heartbeat interval in milliseconds (for testing)
46
+ */
47
+ getHeartbeatInterval(): number;
48
+ /**
49
+ * Get keep-alive enabled status (for testing)
50
+ */
51
+ getEnableKeepAlive(): boolean;
52
+ disconnect(): void;
53
+ connect(): void;
54
+ }
55
+ export {};
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.8.4",
2
+ "version": "0.8.6",
3
3
  "name": "modality-kit",
4
4
  "repository": {
5
5
  "type": "git",