@xstate-devtools/adapter 0.1.3 → 0.1.4

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/src/server.ts DELETED
@@ -1,310 +0,0 @@
1
- // Server entrypoint — exposes a WebSocket bridge so the DevTools panel
2
- // can connect to actors running in Node.
3
- import { createServer as createTcpServer } from 'node:net'
4
- import type {
5
- ExtensionToPageMessage,
6
- PageToExtensionMessage,
7
- } from '../../extension/src/shared/types.js'
8
- import { createInspector, type Transport } from './core.js'
9
- import {
10
- debugLog as baseDebugLog,
11
- infoLog as baseInfoLog,
12
- warnLog as baseWarnLog,
13
- } from './logging.js'
14
- import { sanitize } from './sanitize.js'
15
-
16
- /**
17
- * Find the lowest TCP port >= `start` that is not currently in use.
18
- * Uses a temporary TCP server to probe; the port is released before resolving.
19
- */
20
- function getAvailablePort(start: number): Promise<number> {
21
- return new Promise((resolve, reject) => {
22
- const probe = createTcpServer()
23
- probe.unref()
24
- probe.on('error', (err: NodeJS.ErrnoException) => {
25
- if (err.code === 'EADDRINUSE') {
26
- resolve(getAvailablePort(start + 1))
27
- } else {
28
- reject(err)
29
- }
30
- })
31
- probe.listen(start, '127.0.0.1', () => {
32
- const port = (probe.address() as { port: number }).port
33
- probe.close(() => resolve(port))
34
- })
35
- })
36
- }
37
-
38
- export interface ServerAdapterOptions {
39
- /** Port to listen on. Defaults to env XSTATE_DEVTOOLS_PORT or 9301. */
40
- port?: number
41
- /** Host to bind. Defaults to '127.0.0.1'. */
42
- host?: string
43
- /** Max messages to buffer while no panel is connected. Default 200. */
44
- bufferSize?: number
45
- }
46
-
47
- interface ClientLike {
48
- send(data: string): void
49
- on(event: string, listener: (...args: unknown[]) => void): void
50
- readyState: number
51
- }
52
-
53
- const OPEN_STATE = 1
54
-
55
- function summarizeMessage(message: ExtensionToPageMessage | PageToExtensionMessage) {
56
- const summary: Record<string, unknown> = { type: message.type }
57
- if ('sessionId' in message) summary.sessionId = message.sessionId
58
- if ('parentSessionId' in message && message.parentSessionId) {
59
- summary.parentSessionId = message.parentSessionId
60
- }
61
- if ('globalSeq' in message) summary.globalSeq = message.globalSeq
62
- if ('timestamp' in message) summary.timestamp = message.timestamp
63
- if (
64
- 'event' in message &&
65
- message.event &&
66
- typeof message.event === 'object' &&
67
- 'type' in message.event
68
- ) {
69
- summary.eventType = message.event.type
70
- }
71
- return summary
72
- }
73
-
74
- function debugLog(message: string, details?: unknown) {
75
- baseDebugLog('server', message, details)
76
- }
77
-
78
- function infoLog(message: string, details?: unknown) {
79
- baseInfoLog('server', message, details)
80
- }
81
-
82
- function warnLog(message: string, details?: unknown) {
83
- baseWarnLog('server', message, details)
84
- }
85
-
86
- function stringifyOutgoingMessage(message: PageToExtensionMessage): string | null {
87
- const payload = { ...message, __xstateDevtools: true as const }
88
-
89
- try {
90
- return JSON.stringify(payload)
91
- } catch (error) {
92
- warnLog('failed to stringify adapter message; retrying with sanitized payload', {
93
- error,
94
- message: summarizeMessage(message),
95
- })
96
- }
97
-
98
- try {
99
- return JSON.stringify(sanitize(payload))
100
- } catch (error) {
101
- warnLog('dropping adapter message that could not be stringified', {
102
- error,
103
- message: summarizeMessage(message),
104
- })
105
- return null
106
- }
107
- }
108
-
109
- interface CachedServer {
110
- clients: Set<ClientLike>
111
- dispatchHandlers: Set<(msg: ExtensionToPageMessage) => void>
112
- buffer: string[]
113
- bufferSize: number
114
- activated: boolean
115
- /** Resolves with the actual TCP port once the WS server is listening. */
116
- port: Promise<number>
117
- close: () => void
118
- }
119
-
120
- /**
121
- * Start a local WebSocket server that the DevTools panel can connect to.
122
- * Returns the inspector callback. Multiple panels can connect simultaneously.
123
- *
124
- * The WS server, connected clients, dispatch handlers, and pre-connection
125
- * buffer are all stashed on globalThis keyed by port. This makes the function
126
- * idempotent across HMR re-evaluation: subsequent calls reuse the existing
127
- * server and only register new inspector hooks.
128
- *
129
- * Inspection events emitted before the first panel connects are buffered (up
130
- * to `bufferSize`, default 200) and flushed to the first connecting client so
131
- * actors registered at boot are visible.
132
- */
133
- export function createServerAdapter(options: ServerAdapterOptions = {}) {
134
- const port = options.port ?? (Number(process.env.XSTATE_DEVTOOLS_PORT) || 9301)
135
- const host = options.host ?? '127.0.0.1'
136
- const bufferSize = options.bufferSize ?? 200
137
- infoLog('createServerAdapter called', { host, port, bufferSize })
138
-
139
- const key = `__xstate_devtools_server_${port}__`
140
- const cache = (globalThis as Record<string, unknown>)[key] as CachedServer | undefined
141
-
142
- let server: CachedServer
143
- if (cache) {
144
- server = cache
145
- infoLog('reusing cached WebSocket server', {
146
- host,
147
- port,
148
- clientCount: server.clients.size,
149
- bufferedMessages: server.buffer.length,
150
- })
151
- // honour the most recent caller's buffer size if larger
152
- if (bufferSize > server.bufferSize) server.bufferSize = bufferSize
153
- } else {
154
- const clients = new Set<ClientLike>()
155
- const dispatchHandlers = new Set<(msg: ExtensionToPageMessage) => void>()
156
- const buffer: string[] = []
157
- let wss: any = null
158
- let closed = false
159
-
160
- let portResolve!: (port: number) => void
161
- let portReject!: (err: unknown) => void
162
- const portPromise = new Promise<number>((res, rej) => {
163
- portResolve = res
164
- portReject = rej
165
- })
166
-
167
- server = {
168
- clients,
169
- dispatchHandlers,
170
- buffer,
171
- bufferSize,
172
- activated: false,
173
- port: portPromise,
174
- close: () => {
175
- closed = true
176
- infoLog('closing WebSocket server', { host, port, clientCount: clients.size })
177
- try {
178
- wss?.close()
179
- } catch {
180
- /* noop */
181
- }
182
- clients.clear()
183
- dispatchHandlers.clear()
184
- buffer.length = 0
185
- delete (globalThis as Record<string, unknown>)[key]
186
- },
187
- }
188
-
189
- // Lazily import ws so this module is import-safe in environments that
190
- // never use the server entrypoint (or where ws isn't installed).
191
- void (async () => {
192
- try {
193
- const mod = await import('ws')
194
- const WSServer = (mod as any).WebSocketServer ?? (mod as any).Server
195
- if (closed) return
196
- const actualPort = await getAvailablePort(port)
197
- if (closed) return
198
- wss = new WSServer({ port: actualPort, host })
199
- process.env.XSTATE_DEVTOOLS_PORT = String(actualPort)
200
- portResolve(actualPort)
201
- infoLog('WebSocket server listening', { host, port: actualPort })
202
- wss.on('connection', (ws: ClientLike) => {
203
- infoLog('panel connected to WebSocket server', {
204
- host,
205
- port,
206
- activated: server.activated,
207
- bufferedMessages: server.buffer.length,
208
- })
209
- // Drain bootstrap buffer to the first client only.
210
- if (!server.activated) {
211
- server.activated = true
212
- infoLog('flushing bootstrap buffer to first panel', {
213
- host,
214
- port,
215
- bufferedMessages: server.buffer.length,
216
- })
217
- for (const payload of server.buffer) {
218
- try {
219
- ws.send(payload)
220
- } catch {
221
- /* ignore */
222
- }
223
- }
224
- server.buffer.length = 0
225
- }
226
- server.clients.add(ws)
227
- ws.on('message', (raw: unknown) => {
228
- try {
229
- const text = typeof raw === 'string' ? raw : (raw as Buffer).toString('utf8')
230
- const msg = JSON.parse(text) as ExtensionToPageMessage
231
- debugLog('received dispatch from panel', summarizeMessage(msg))
232
- for (const cb of server.dispatchHandlers) cb(msg)
233
- } catch (error) {
234
- warnLog('failed to parse panel message', { error })
235
- }
236
- })
237
- ws.on('close', () => {
238
- server.clients.delete(ws)
239
- infoLog('panel disconnected from WebSocket server', {
240
- host,
241
- port,
242
- clientCount: server.clients.size,
243
- })
244
- })
245
- ws.on('error', (error: unknown) => {
246
- server.clients.delete(ws)
247
- warnLog('WebSocket client error', { error })
248
- })
249
- })
250
- wss.on('error', (err: Error) => {
251
- warnLog('WS server error', { host, port, message: err.message })
252
- })
253
- } catch (e) {
254
- portReject(e)
255
- warnLog('could not start server adapter — install `ws` to enable', {
256
- host,
257
- port,
258
- message: (e as Error).message,
259
- })
260
- }
261
- })()
262
-
263
- ;(globalThis as Record<string, unknown>)[key] = server
264
- }
265
-
266
- const transport: Transport = {
267
- send(message: PageToExtensionMessage) {
268
- const payload = stringifyOutgoingMessage(message)
269
- if (payload === null) return
270
-
271
- if (!server.activated) {
272
- // No panel has connected yet — buffer for the first one.
273
- if (server.buffer.length >= server.bufferSize) server.buffer.shift()
274
- server.buffer.push(payload)
275
- debugLog('buffered outgoing adapter message; no panel connected yet', {
276
- bufferedMessages: server.buffer.length,
277
- message: summarizeMessage(message),
278
- })
279
- return
280
- }
281
- let sentCount = 0
282
- for (const ws of server.clients) {
283
- if (ws.readyState === OPEN_STATE) {
284
- try {
285
- ws.send(payload)
286
- sentCount += 1
287
- } catch {
288
- /* ignore */
289
- }
290
- }
291
- }
292
- debugLog('sent adapter message to connected panels', {
293
- sentCount,
294
- clientCount: server.clients.size,
295
- message: summarizeMessage(message),
296
- })
297
- },
298
- subscribe(handler) {
299
- server.dispatchHandlers.add(handler)
300
- debugLog('registered dispatch handler', { handlerCount: server.dispatchHandlers.size })
301
- return () => {
302
- server.dispatchHandlers.delete(handler)
303
- debugLog('removed dispatch handler', { handlerCount: server.dispatchHandlers.size })
304
- }
305
- },
306
- }
307
-
308
- const inspector = createInspector(transport, 'srv')
309
- return { ...inspector, close: server.close, port: server.port }
310
- }
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "jsx": "react-jsx",
8
- "outDir": "./dist",
9
- "rootDir": "..",
10
- "declaration": true,
11
- "skipLibCheck": true
12
- },
13
- "include": ["src"]
14
- }