@cnstra/devtools-transport-ws 1.1.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/dist/index.cjs ADDED
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var CNSDevToolsTransportWs = class {
5
+ constructor(opts) {
6
+ this.opts = opts;
7
+ this.connecting = false;
8
+ this.closed = false;
9
+ this.buffer = [];
10
+ this.hasConnectedOnce = false;
11
+ this.reconnectAttempts = 0;
12
+ }
13
+ get reconnectDelayMs() {
14
+ return this.opts.reconnectDelayMs ?? 1e3;
15
+ }
16
+ get bufferMaxSize() {
17
+ return this.opts.bufferMaxSize ?? 100;
18
+ }
19
+ get maxReconnectAttempts() {
20
+ return this.opts.maxReconnectAttempts ?? Infinity;
21
+ }
22
+ ensureSocket() {
23
+ const WS = this.opts.webSocketImpl || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
24
+ if (!WS)
25
+ return Promise.reject(
26
+ new Error(
27
+ "WebSocket implementation is not available. Provide webSocketImpl."
28
+ )
29
+ );
30
+ if (this.ws && this.ws.readyState === 1)
31
+ return Promise.resolve();
32
+ if (this.connecting) {
33
+ return new Promise((resolve) => {
34
+ const check = () => {
35
+ if (this.ws && this.ws.readyState === 1)
36
+ return resolve();
37
+ setTimeout(check, 50);
38
+ };
39
+ check();
40
+ });
41
+ }
42
+ this.connecting = true;
43
+ return new Promise((resolve) => {
44
+ const ws = new WS(this.opts.url, this.opts.protocols);
45
+ this.ws = ws;
46
+ ws.onopen = () => {
47
+ if (this.opts.consoleLogEnabled) {
48
+ console.log("DevTools connected to server");
49
+ }
50
+ this.connecting = false;
51
+ this.reconnectAttempts = 0;
52
+ if (this.hasConnectedOnce && this.lastInitMessage) {
53
+ if (this.opts.consoleLogEnabled) {
54
+ console.log("DevTools reconnected - resending init message");
55
+ }
56
+ this.buffer.unshift({
57
+ type: "init",
58
+ payload: this.lastInitMessage
59
+ });
60
+ }
61
+ this.hasConnectedOnce = true;
62
+ this.flush();
63
+ resolve();
64
+ };
65
+ ws.onclose = () => {
66
+ this.connecting = false;
67
+ if (!this.closed && this.reconnectAttempts < this.maxReconnectAttempts) {
68
+ this.reconnectAttempts++;
69
+ if (this.opts.consoleLogEnabled) {
70
+ console.log(
71
+ `DevTools disconnected, reconnecting... (attempt ${this.reconnectAttempts})`
72
+ );
73
+ }
74
+ setTimeout(
75
+ () => this.ensureSocket().catch(() => {
76
+ }),
77
+ this.reconnectDelayMs
78
+ );
79
+ } else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
80
+ if (this.opts.consoleLogEnabled) {
81
+ console.log("DevTools max reconnect attempts reached");
82
+ }
83
+ }
84
+ };
85
+ ws.onerror = () => {
86
+ try {
87
+ ws.close();
88
+ } catch {
89
+ }
90
+ };
91
+ ws.onmessage = (ev) => {
92
+ try {
93
+ const data = typeof ev.data === "string" ? ev.data : "" + ev.data;
94
+ const msg = JSON.parse(data);
95
+ if (msg && msg.type === "stimulate") {
96
+ const cmd = msg;
97
+ const ack = {
98
+ type: "stimulate-accepted",
99
+ stimulationCommandId: cmd.stimulationCommandId,
100
+ stimulationId: cmd.stimulationCommandId,
101
+ appId: cmd.appId
102
+ };
103
+ try {
104
+ this.ws?.send(JSON.stringify(ack));
105
+ } catch {
106
+ }
107
+ this.onStimulate?.(cmd);
108
+ }
109
+ } catch {
110
+ }
111
+ };
112
+ });
113
+ }
114
+ async sendInitMessage(message) {
115
+ this.lastInitMessage = message;
116
+ this.buffer.push({ type: "init", payload: message });
117
+ if (this.buffer.length >= this.bufferMaxSize) await this.flush();
118
+ else void this.flush();
119
+ }
120
+ async sendNeuronResponseMessage(message) {
121
+ this.buffer.push({ type: "response", payload: message });
122
+ if (this.buffer.length >= this.bufferMaxSize) await this.flush();
123
+ else void this.flush();
124
+ }
125
+ async sendStimulationMessage(message) {
126
+ await this.ensureSocket();
127
+ if (!this.ws || this.ws.readyState !== 1) return;
128
+ try {
129
+ this.ws.send(JSON.stringify(message));
130
+ } catch {
131
+ }
132
+ }
133
+ async flush() {
134
+ if (this.buffer.length === 0) return;
135
+ await this.ensureSocket();
136
+ if (!this.ws || this.ws.readyState !== 1) return;
137
+ const items = this.buffer.splice(0, this.buffer.length);
138
+ const replayResponses = items.filter((item) => item.type === "response").filter((item) => {
139
+ const stimId = item.payload?.stimulationId || "";
140
+ return typeof stimId === "string" && stimId.includes("-replay-");
141
+ });
142
+ if (replayResponses.length > 0 && this.opts.consoleLogEnabled) {
143
+ console.log("[Transport] Flushing REPLAY responses batch:", {
144
+ totalItems: items.length,
145
+ replayResponsesCount: replayResponses.length,
146
+ replayStimIds: replayResponses.slice(0, 3).map((r) => r.payload?.stimulationId),
147
+ replayAppIds: replayResponses.slice(0, 3).map((r) => r.payload?.appId)
148
+ });
149
+ }
150
+ try {
151
+ this.ws.send(JSON.stringify({ type: "batch", items }));
152
+ } catch {
153
+ this.buffer.unshift(...items);
154
+ }
155
+ }
156
+ onStimulateCommand(handler) {
157
+ this.onStimulate = handler;
158
+ return () => {
159
+ this.onStimulate = void 0;
160
+ };
161
+ }
162
+ /** Close the connection and stop reconnecting */
163
+ close() {
164
+ this.closed = true;
165
+ try {
166
+ this.ws?.close();
167
+ } catch {
168
+ }
169
+ this.ws = void 0;
170
+ }
171
+ /** Get connection status */
172
+ get isConnected() {
173
+ return this.ws?.readyState === 1;
174
+ }
175
+ /** Get buffered message count */
176
+ get bufferSize() {
177
+ return this.buffer.length;
178
+ }
179
+ };
180
+
181
+ exports.CNSDevToolsTransportWs = CNSDevToolsTransportWs;
182
+ //# sourceMappingURL=index.cjs.map
183
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ICNSDevToolsTransport } from '@cnstra/devtools';\nimport type {\n InitMessage,\n NeuronResponseMessage,\n StimulateCommand,\n StimulateAccepted,\n StimulateRejected,\n StimulationMessage,\n} from '@cnstra/devtools-dto';\n\nexport type TWSOpts = {\n /** ws://localhost:7777 or wss://... */\n url: string;\n /** Optional protocols */\n protocols?: string | string[];\n /** Reconnect delay in ms (default 1000) */\n reconnectDelayMs?: number;\n /** Max buffered messages before force flush (default 100) */\n bufferMaxSize?: number;\n /** Optional WebSocket implementation for Node (e.g., globalThis.WebSocket or ws) */\n webSocketImpl?: typeof WebSocket;\n /** Auto-connect on first message (default true) */\n autoConnect?: boolean;\n /** Max reconnect attempts (default Infinity) */\n maxReconnectAttempts?: number;\n /** When true, print transport-level console logs */\n consoleLogEnabled?: boolean;\n};\n\ntype WSLike = WebSocket;\n\nexport class CNSDevToolsTransportWs implements ICNSDevToolsTransport {\n private ws?: WSLike;\n private connecting = false;\n private closed = false;\n private readonly buffer: Array<{\n type: 'init' | 'response';\n payload: any;\n }> = [];\n\n // Store the latest init message for reconnect\n private lastInitMessage?: InitMessage;\n private hasConnectedOnce = false;\n private reconnectAttempts = 0;\n\n constructor(private readonly opts: TWSOpts) {}\n\n private get reconnectDelayMs() {\n return this.opts.reconnectDelayMs ?? 1000;\n }\n private get bufferMaxSize() {\n return this.opts.bufferMaxSize ?? 100;\n }\n private get maxReconnectAttempts() {\n return this.opts.maxReconnectAttempts ?? Infinity;\n }\n\n private ensureSocket(): Promise<void> {\n const WS: typeof WebSocket | undefined =\n this.opts.webSocketImpl ||\n (typeof WebSocket !== 'undefined' ? WebSocket : undefined);\n if (!WS)\n return Promise.reject(\n new Error(\n 'WebSocket implementation is not available. Provide webSocketImpl.'\n )\n );\n if (this.ws && this.ws.readyState === 1)\n // OPEN = 1\n return Promise.resolve();\n if (this.connecting) {\n return new Promise(resolve => {\n const check = () => {\n if (this.ws && this.ws.readyState === 1)\n // OPEN = 1\n return resolve();\n setTimeout(check, 50);\n };\n check();\n });\n }\n this.connecting = true;\n return new Promise(resolve => {\n const ws = new WS(this.opts.url, this.opts.protocols) as WSLike;\n this.ws = ws;\n ws.onopen = () => {\n if (this.opts.consoleLogEnabled) {\n console.log('DevTools connected to server');\n }\n this.connecting = false;\n this.reconnectAttempts = 0; // Reset on successful connection\n\n // On reconnect, resend init message first\n if (this.hasConnectedOnce && this.lastInitMessage) {\n if (this.opts.consoleLogEnabled) {\n console.log('DevTools reconnected - resending init message');\n }\n this.buffer.unshift({\n type: 'init',\n payload: this.lastInitMessage,\n });\n }\n\n this.hasConnectedOnce = true;\n this.flush();\n resolve();\n };\n ws.onclose = () => {\n this.connecting = false;\n if (\n !this.closed &&\n this.reconnectAttempts < this.maxReconnectAttempts\n ) {\n this.reconnectAttempts++;\n if (this.opts.consoleLogEnabled) {\n console.log(\n `DevTools disconnected, reconnecting... (attempt ${this.reconnectAttempts})`\n );\n }\n setTimeout(\n () => this.ensureSocket().catch(() => {}),\n this.reconnectDelayMs\n );\n } else if (\n this.reconnectAttempts >= this.maxReconnectAttempts\n ) {\n if (this.opts.consoleLogEnabled) {\n console.log('DevTools max reconnect attempts reached');\n }\n }\n };\n ws.onerror = () => {\n try {\n ws.close();\n } catch {}\n };\n ws.onmessage = (ev: MessageEvent) => {\n try {\n const data =\n typeof ev.data === 'string' ? ev.data : '' + ev.data;\n const msg = JSON.parse(data);\n if (msg && msg.type === 'stimulate') {\n const cmd = msg as StimulateCommand & {\n appId?: string;\n cnsId?: string;\n };\n const ack: StimulateAccepted = {\n type: 'stimulate-accepted',\n stimulationCommandId: cmd.stimulationCommandId,\n stimulationId: cmd.stimulationCommandId,\n appId: cmd.appId,\n };\n try {\n this.ws?.send(JSON.stringify(ack));\n } catch {}\n this.onStimulate?.(cmd);\n }\n } catch {\n // ignore invalid messages\n }\n };\n });\n }\n\n async sendInitMessage(message: InitMessage): Promise<void> {\n // Store the latest init message for reconnects\n this.lastInitMessage = message;\n\n this.buffer.push({ type: 'init', payload: message });\n if (this.buffer.length >= this.bufferMaxSize) await this.flush();\n else void this.flush();\n }\n\n async sendNeuronResponseMessage(\n message: NeuronResponseMessage\n ): Promise<void> {\n this.buffer.push({ type: 'response', payload: message });\n if (this.buffer.length >= this.bufferMaxSize) await this.flush();\n else void this.flush();\n }\n\n async sendStimulationMessage(message: StimulationMessage): Promise<void> {\n await this.ensureSocket();\n if (!this.ws || this.ws.readyState !== 1) return; // OPEN = 1\n try {\n this.ws.send(JSON.stringify(message));\n } catch {\n // ignore send errors\n }\n }\n\n private async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n await this.ensureSocket();\n /* istanbul ignore next -- ts-jest source-map occasionally misattributes coverage for this guard */\n if (!this.ws || this.ws.readyState !== 1) return; // OPEN = 1\n /* istanbul ignore next -- see note above */\n const items = this.buffer.splice(0, this.buffer.length);\n\n // Check for replay responses in the batch\n /* istanbul ignore next -- see note above */\n const replayResponses = items\n .filter(item => item.type === 'response')\n .filter(item => {\n const stimId = item.payload?.stimulationId || '';\n return (\n typeof stimId === 'string' && stimId.includes('-replay-')\n );\n });\n\n if (replayResponses.length > 0 && this.opts.consoleLogEnabled) {\n console.log('[Transport] Flushing REPLAY responses batch:', {\n totalItems: items.length,\n replayResponsesCount: replayResponses.length,\n replayStimIds: replayResponses\n .slice(0, 3)\n .map(r => r.payload?.stimulationId),\n replayAppIds: replayResponses\n .slice(0, 3)\n .map(r => r.payload?.appId),\n });\n }\n\n try {\n this.ws.send(JSON.stringify({ type: 'batch', items }));\n } catch {\n // requeue if send fails\n this.buffer.unshift(...items);\n }\n }\n\n private onStimulate?: (cmd: StimulateCommand) => void;\n onStimulateCommand(handler: (cmd: StimulateCommand) => void): () => void {\n this.onStimulate = handler;\n return () => {\n this.onStimulate = undefined;\n };\n }\n\n /** Close the connection and stop reconnecting */\n close(): void {\n this.closed = true;\n try {\n this.ws?.close();\n } catch {}\n this.ws = undefined;\n }\n\n /** Get connection status */\n get isConnected(): boolean {\n return this.ws?.readyState === 1; // OPEN = 1\n }\n\n /** Get buffered message count */\n get bufferSize(): number {\n return this.buffer.length;\n }\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import type { ICNSDevToolsTransport } from '@cnstra/devtools';
2
+ import type { InitMessage, NeuronResponseMessage, StimulateCommand, StimulationMessage } from '@cnstra/devtools-dto';
3
+ export type TWSOpts = {
4
+ /** ws://localhost:7777 or wss://... */
5
+ url: string;
6
+ /** Optional protocols */
7
+ protocols?: string | string[];
8
+ /** Reconnect delay in ms (default 1000) */
9
+ reconnectDelayMs?: number;
10
+ /** Max buffered messages before force flush (default 100) */
11
+ bufferMaxSize?: number;
12
+ /** Optional WebSocket implementation for Node (e.g., globalThis.WebSocket or ws) */
13
+ webSocketImpl?: typeof WebSocket;
14
+ /** Auto-connect on first message (default true) */
15
+ autoConnect?: boolean;
16
+ /** Max reconnect attempts (default Infinity) */
17
+ maxReconnectAttempts?: number;
18
+ /** When true, print transport-level console logs */
19
+ consoleLogEnabled?: boolean;
20
+ };
21
+ export declare class CNSDevToolsTransportWs implements ICNSDevToolsTransport {
22
+ private readonly opts;
23
+ private ws?;
24
+ private connecting;
25
+ private closed;
26
+ private readonly buffer;
27
+ private lastInitMessage?;
28
+ private hasConnectedOnce;
29
+ private reconnectAttempts;
30
+ constructor(opts: TWSOpts);
31
+ private get reconnectDelayMs();
32
+ private get bufferMaxSize();
33
+ private get maxReconnectAttempts();
34
+ private ensureSocket;
35
+ sendInitMessage(message: InitMessage): Promise<void>;
36
+ sendNeuronResponseMessage(message: NeuronResponseMessage): Promise<void>;
37
+ sendStimulationMessage(message: StimulationMessage): Promise<void>;
38
+ private flush;
39
+ private onStimulate?;
40
+ onStimulateCommand(handler: (cmd: StimulateCommand) => void): () => void;
41
+ /** Close the connection and stop reconnecting */
42
+ close(): void;
43
+ /** Get connection status */
44
+ get isConnected(): boolean;
45
+ /** Get buffered message count */
46
+ get bufferSize(): number;
47
+ }
48
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,KAAK,EACR,WAAW,EACX,qBAAqB,EACrB,gBAAgB,EAGhB,kBAAkB,EACrB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,MAAM,OAAO,GAAG;IAClB,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC9B,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,aAAa,CAAC,EAAE,OAAO,SAAS,CAAC;IACjC,mDAAmD;IACnD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAIF,qBAAa,sBAAuB,YAAW,qBAAqB;IAcpD,OAAO,CAAC,QAAQ,CAAC,IAAI;IAbjC,OAAO,CAAC,EAAE,CAAC,CAAS;IACpB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGf;IAGR,OAAO,CAAC,eAAe,CAAC,CAAc;IACtC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,iBAAiB,CAAK;gBAED,IAAI,EAAE,OAAO;IAE1C,OAAO,KAAK,gBAAgB,GAE3B;IACD,OAAO,KAAK,aAAa,GAExB;IACD,OAAO,KAAK,oBAAoB,GAE/B;IAED,OAAO,CAAC,YAAY;IA2Gd,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IASpD,yBAAyB,CAC3B,OAAO,EAAE,qBAAqB,GAC/B,OAAO,CAAC,IAAI,CAAC;IAMV,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;YAU1D,KAAK;IAwCnB,OAAO,CAAC,WAAW,CAAC,CAAkC;IACtD,kBAAkB,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI;IAOxE,iDAAiD;IACjD,KAAK,IAAI,IAAI;IAQb,4BAA4B;IAC5B,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,iCAAiC;IACjC,IAAI,UAAU,IAAI,MAAM,CAEvB;CACJ"}
package/dist/index.js ADDED
@@ -0,0 +1,181 @@
1
+ // src/index.ts
2
+ var CNSDevToolsTransportWs = class {
3
+ constructor(opts) {
4
+ this.opts = opts;
5
+ this.connecting = false;
6
+ this.closed = false;
7
+ this.buffer = [];
8
+ this.hasConnectedOnce = false;
9
+ this.reconnectAttempts = 0;
10
+ }
11
+ get reconnectDelayMs() {
12
+ return this.opts.reconnectDelayMs ?? 1e3;
13
+ }
14
+ get bufferMaxSize() {
15
+ return this.opts.bufferMaxSize ?? 100;
16
+ }
17
+ get maxReconnectAttempts() {
18
+ return this.opts.maxReconnectAttempts ?? Infinity;
19
+ }
20
+ ensureSocket() {
21
+ const WS = this.opts.webSocketImpl || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
22
+ if (!WS)
23
+ return Promise.reject(
24
+ new Error(
25
+ "WebSocket implementation is not available. Provide webSocketImpl."
26
+ )
27
+ );
28
+ if (this.ws && this.ws.readyState === 1)
29
+ return Promise.resolve();
30
+ if (this.connecting) {
31
+ return new Promise((resolve) => {
32
+ const check = () => {
33
+ if (this.ws && this.ws.readyState === 1)
34
+ return resolve();
35
+ setTimeout(check, 50);
36
+ };
37
+ check();
38
+ });
39
+ }
40
+ this.connecting = true;
41
+ return new Promise((resolve) => {
42
+ const ws = new WS(this.opts.url, this.opts.protocols);
43
+ this.ws = ws;
44
+ ws.onopen = () => {
45
+ if (this.opts.consoleLogEnabled) {
46
+ console.log("DevTools connected to server");
47
+ }
48
+ this.connecting = false;
49
+ this.reconnectAttempts = 0;
50
+ if (this.hasConnectedOnce && this.lastInitMessage) {
51
+ if (this.opts.consoleLogEnabled) {
52
+ console.log("DevTools reconnected - resending init message");
53
+ }
54
+ this.buffer.unshift({
55
+ type: "init",
56
+ payload: this.lastInitMessage
57
+ });
58
+ }
59
+ this.hasConnectedOnce = true;
60
+ this.flush();
61
+ resolve();
62
+ };
63
+ ws.onclose = () => {
64
+ this.connecting = false;
65
+ if (!this.closed && this.reconnectAttempts < this.maxReconnectAttempts) {
66
+ this.reconnectAttempts++;
67
+ if (this.opts.consoleLogEnabled) {
68
+ console.log(
69
+ `DevTools disconnected, reconnecting... (attempt ${this.reconnectAttempts})`
70
+ );
71
+ }
72
+ setTimeout(
73
+ () => this.ensureSocket().catch(() => {
74
+ }),
75
+ this.reconnectDelayMs
76
+ );
77
+ } else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
78
+ if (this.opts.consoleLogEnabled) {
79
+ console.log("DevTools max reconnect attempts reached");
80
+ }
81
+ }
82
+ };
83
+ ws.onerror = () => {
84
+ try {
85
+ ws.close();
86
+ } catch {
87
+ }
88
+ };
89
+ ws.onmessage = (ev) => {
90
+ try {
91
+ const data = typeof ev.data === "string" ? ev.data : "" + ev.data;
92
+ const msg = JSON.parse(data);
93
+ if (msg && msg.type === "stimulate") {
94
+ const cmd = msg;
95
+ const ack = {
96
+ type: "stimulate-accepted",
97
+ stimulationCommandId: cmd.stimulationCommandId,
98
+ stimulationId: cmd.stimulationCommandId,
99
+ appId: cmd.appId
100
+ };
101
+ try {
102
+ this.ws?.send(JSON.stringify(ack));
103
+ } catch {
104
+ }
105
+ this.onStimulate?.(cmd);
106
+ }
107
+ } catch {
108
+ }
109
+ };
110
+ });
111
+ }
112
+ async sendInitMessage(message) {
113
+ this.lastInitMessage = message;
114
+ this.buffer.push({ type: "init", payload: message });
115
+ if (this.buffer.length >= this.bufferMaxSize) await this.flush();
116
+ else void this.flush();
117
+ }
118
+ async sendNeuronResponseMessage(message) {
119
+ this.buffer.push({ type: "response", payload: message });
120
+ if (this.buffer.length >= this.bufferMaxSize) await this.flush();
121
+ else void this.flush();
122
+ }
123
+ async sendStimulationMessage(message) {
124
+ await this.ensureSocket();
125
+ if (!this.ws || this.ws.readyState !== 1) return;
126
+ try {
127
+ this.ws.send(JSON.stringify(message));
128
+ } catch {
129
+ }
130
+ }
131
+ async flush() {
132
+ if (this.buffer.length === 0) return;
133
+ await this.ensureSocket();
134
+ if (!this.ws || this.ws.readyState !== 1) return;
135
+ const items = this.buffer.splice(0, this.buffer.length);
136
+ const replayResponses = items.filter((item) => item.type === "response").filter((item) => {
137
+ const stimId = item.payload?.stimulationId || "";
138
+ return typeof stimId === "string" && stimId.includes("-replay-");
139
+ });
140
+ if (replayResponses.length > 0 && this.opts.consoleLogEnabled) {
141
+ console.log("[Transport] Flushing REPLAY responses batch:", {
142
+ totalItems: items.length,
143
+ replayResponsesCount: replayResponses.length,
144
+ replayStimIds: replayResponses.slice(0, 3).map((r) => r.payload?.stimulationId),
145
+ replayAppIds: replayResponses.slice(0, 3).map((r) => r.payload?.appId)
146
+ });
147
+ }
148
+ try {
149
+ this.ws.send(JSON.stringify({ type: "batch", items }));
150
+ } catch {
151
+ this.buffer.unshift(...items);
152
+ }
153
+ }
154
+ onStimulateCommand(handler) {
155
+ this.onStimulate = handler;
156
+ return () => {
157
+ this.onStimulate = void 0;
158
+ };
159
+ }
160
+ /** Close the connection and stop reconnecting */
161
+ close() {
162
+ this.closed = true;
163
+ try {
164
+ this.ws?.close();
165
+ } catch {
166
+ }
167
+ this.ws = void 0;
168
+ }
169
+ /** Get connection status */
170
+ get isConnected() {
171
+ return this.ws?.readyState === 1;
172
+ }
173
+ /** Get buffered message count */
174
+ get bufferSize() {
175
+ return this.buffer.length;
176
+ }
177
+ };
178
+
179
+ export { CNSDevToolsTransportWs };
180
+ //# sourceMappingURL=index.js.map
181
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ICNSDevToolsTransport } from '@cnstra/devtools';\nimport type {\n InitMessage,\n NeuronResponseMessage,\n StimulateCommand,\n StimulateAccepted,\n StimulateRejected,\n StimulationMessage,\n} from '@cnstra/devtools-dto';\n\nexport type TWSOpts = {\n /** ws://localhost:7777 or wss://... */\n url: string;\n /** Optional protocols */\n protocols?: string | string[];\n /** Reconnect delay in ms (default 1000) */\n reconnectDelayMs?: number;\n /** Max buffered messages before force flush (default 100) */\n bufferMaxSize?: number;\n /** Optional WebSocket implementation for Node (e.g., globalThis.WebSocket or ws) */\n webSocketImpl?: typeof WebSocket;\n /** Auto-connect on first message (default true) */\n autoConnect?: boolean;\n /** Max reconnect attempts (default Infinity) */\n maxReconnectAttempts?: number;\n /** When true, print transport-level console logs */\n consoleLogEnabled?: boolean;\n};\n\ntype WSLike = WebSocket;\n\nexport class CNSDevToolsTransportWs implements ICNSDevToolsTransport {\n private ws?: WSLike;\n private connecting = false;\n private closed = false;\n private readonly buffer: Array<{\n type: 'init' | 'response';\n payload: any;\n }> = [];\n\n // Store the latest init message for reconnect\n private lastInitMessage?: InitMessage;\n private hasConnectedOnce = false;\n private reconnectAttempts = 0;\n\n constructor(private readonly opts: TWSOpts) {}\n\n private get reconnectDelayMs() {\n return this.opts.reconnectDelayMs ?? 1000;\n }\n private get bufferMaxSize() {\n return this.opts.bufferMaxSize ?? 100;\n }\n private get maxReconnectAttempts() {\n return this.opts.maxReconnectAttempts ?? Infinity;\n }\n\n private ensureSocket(): Promise<void> {\n const WS: typeof WebSocket | undefined =\n this.opts.webSocketImpl ||\n (typeof WebSocket !== 'undefined' ? WebSocket : undefined);\n if (!WS)\n return Promise.reject(\n new Error(\n 'WebSocket implementation is not available. Provide webSocketImpl.'\n )\n );\n if (this.ws && this.ws.readyState === 1)\n // OPEN = 1\n return Promise.resolve();\n if (this.connecting) {\n return new Promise(resolve => {\n const check = () => {\n if (this.ws && this.ws.readyState === 1)\n // OPEN = 1\n return resolve();\n setTimeout(check, 50);\n };\n check();\n });\n }\n this.connecting = true;\n return new Promise(resolve => {\n const ws = new WS(this.opts.url, this.opts.protocols) as WSLike;\n this.ws = ws;\n ws.onopen = () => {\n if (this.opts.consoleLogEnabled) {\n console.log('DevTools connected to server');\n }\n this.connecting = false;\n this.reconnectAttempts = 0; // Reset on successful connection\n\n // On reconnect, resend init message first\n if (this.hasConnectedOnce && this.lastInitMessage) {\n if (this.opts.consoleLogEnabled) {\n console.log('DevTools reconnected - resending init message');\n }\n this.buffer.unshift({\n type: 'init',\n payload: this.lastInitMessage,\n });\n }\n\n this.hasConnectedOnce = true;\n this.flush();\n resolve();\n };\n ws.onclose = () => {\n this.connecting = false;\n if (\n !this.closed &&\n this.reconnectAttempts < this.maxReconnectAttempts\n ) {\n this.reconnectAttempts++;\n if (this.opts.consoleLogEnabled) {\n console.log(\n `DevTools disconnected, reconnecting... (attempt ${this.reconnectAttempts})`\n );\n }\n setTimeout(\n () => this.ensureSocket().catch(() => {}),\n this.reconnectDelayMs\n );\n } else if (\n this.reconnectAttempts >= this.maxReconnectAttempts\n ) {\n if (this.opts.consoleLogEnabled) {\n console.log('DevTools max reconnect attempts reached');\n }\n }\n };\n ws.onerror = () => {\n try {\n ws.close();\n } catch {}\n };\n ws.onmessage = (ev: MessageEvent) => {\n try {\n const data =\n typeof ev.data === 'string' ? ev.data : '' + ev.data;\n const msg = JSON.parse(data);\n if (msg && msg.type === 'stimulate') {\n const cmd = msg as StimulateCommand & {\n appId?: string;\n cnsId?: string;\n };\n const ack: StimulateAccepted = {\n type: 'stimulate-accepted',\n stimulationCommandId: cmd.stimulationCommandId,\n stimulationId: cmd.stimulationCommandId,\n appId: cmd.appId,\n };\n try {\n this.ws?.send(JSON.stringify(ack));\n } catch {}\n this.onStimulate?.(cmd);\n }\n } catch {\n // ignore invalid messages\n }\n };\n });\n }\n\n async sendInitMessage(message: InitMessage): Promise<void> {\n // Store the latest init message for reconnects\n this.lastInitMessage = message;\n\n this.buffer.push({ type: 'init', payload: message });\n if (this.buffer.length >= this.bufferMaxSize) await this.flush();\n else void this.flush();\n }\n\n async sendNeuronResponseMessage(\n message: NeuronResponseMessage\n ): Promise<void> {\n this.buffer.push({ type: 'response', payload: message });\n if (this.buffer.length >= this.bufferMaxSize) await this.flush();\n else void this.flush();\n }\n\n async sendStimulationMessage(message: StimulationMessage): Promise<void> {\n await this.ensureSocket();\n if (!this.ws || this.ws.readyState !== 1) return; // OPEN = 1\n try {\n this.ws.send(JSON.stringify(message));\n } catch {\n // ignore send errors\n }\n }\n\n private async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n await this.ensureSocket();\n /* istanbul ignore next -- ts-jest source-map occasionally misattributes coverage for this guard */\n if (!this.ws || this.ws.readyState !== 1) return; // OPEN = 1\n /* istanbul ignore next -- see note above */\n const items = this.buffer.splice(0, this.buffer.length);\n\n // Check for replay responses in the batch\n /* istanbul ignore next -- see note above */\n const replayResponses = items\n .filter(item => item.type === 'response')\n .filter(item => {\n const stimId = item.payload?.stimulationId || '';\n return (\n typeof stimId === 'string' && stimId.includes('-replay-')\n );\n });\n\n if (replayResponses.length > 0 && this.opts.consoleLogEnabled) {\n console.log('[Transport] Flushing REPLAY responses batch:', {\n totalItems: items.length,\n replayResponsesCount: replayResponses.length,\n replayStimIds: replayResponses\n .slice(0, 3)\n .map(r => r.payload?.stimulationId),\n replayAppIds: replayResponses\n .slice(0, 3)\n .map(r => r.payload?.appId),\n });\n }\n\n try {\n this.ws.send(JSON.stringify({ type: 'batch', items }));\n } catch {\n // requeue if send fails\n this.buffer.unshift(...items);\n }\n }\n\n private onStimulate?: (cmd: StimulateCommand) => void;\n onStimulateCommand(handler: (cmd: StimulateCommand) => void): () => void {\n this.onStimulate = handler;\n return () => {\n this.onStimulate = undefined;\n };\n }\n\n /** Close the connection and stop reconnecting */\n close(): void {\n this.closed = true;\n try {\n this.ws?.close();\n } catch {}\n this.ws = undefined;\n }\n\n /** Get connection status */\n get isConnected(): boolean {\n return this.ws?.readyState === 1; // OPEN = 1\n }\n\n /** Get buffered message count */\n get bufferSize(): number {\n return this.buffer.length;\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@cnstra/devtools-transport-ws",
3
+ "version": "1.1.0",
4
+ "description": "WebSocket transport for CNStra DevTools",
5
+ "keywords": [
6
+ "devtools",
7
+ "cnstra",
8
+ "transport",
9
+ "websocket",
10
+ "ws"
11
+ ],
12
+ "type": "module",
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsup && tsc --emitDeclarationOnly",
26
+ "build:types": "tsc --emitDeclarationOnly",
27
+ "test": "jest",
28
+ "test:watch": "jest --watch",
29
+ "test:coverage": "jest --coverage",
30
+ "prepublishOnly": "npm run test && npm run build"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "dependencies": {
36
+ "@cnstra/devtools-dto": "1.0.0"
37
+ },
38
+ "peerDependencies": {
39
+ "@cnstra/devtools": "^1.0.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/jest": "^29.5.5",
43
+ "jest": "^29.7.0",
44
+ "jest-environment-jsdom": "^29.7.0",
45
+ "ts-jest": "^29.1.1",
46
+ "tsup": "8.5.1",
47
+ "typescript": "^5.8.3"
48
+ },
49
+ "author": "Andrei Baikov <abaikov23@gmail.com>",
50
+ "license": "MIT",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }