@yoltra/devtools-server 0.2.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/README.md +160 -0
- package/bin/devtools-server.js +4 -0
- package/dist/devtools-server.cjs.js +9 -0
- package/dist/devtools-server.cjs.js.map +1 -0
- package/dist/devtools-server.esm.js +498 -0
- package/dist/devtools-server.esm.js.map +1 -0
- package/dist/types/cli.d.ts +28 -0
- package/dist/types/connection.d.ts +43 -0
- package/dist/types/hub.d.ts +164 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/ring-buffer.d.ts +58 -0
- package/dist/types/router.d.ts +118 -0
- package/package.json +75 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central WebSocket hub that brokers DevTools protocol traffic.
|
|
3
|
+
*
|
|
4
|
+
* @module @yoltra/devtools-server
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for the DevTools hub server.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* All fields are optional; sensible defaults are applied when omitted.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export interface DevtoolsHubOptions {
|
|
15
|
+
/** Port to bind on. @default 9800 */
|
|
16
|
+
port?: number;
|
|
17
|
+
/** Host to bind on. @default "127.0.0.1" (localhost only for v1 security) */
|
|
18
|
+
host?: string;
|
|
19
|
+
/** Maximum events retained in the ring buffer for late-connecting extensions. @default 1000 */
|
|
20
|
+
historySize?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Extra WebSocket `Origin` values to accept, beyond the always-allowed set
|
|
23
|
+
* (no Origin, browser-extension origins, and loopback origins). Use this only
|
|
24
|
+
* for a non-loopback local dev host (e.g. a custom `.local` domain). Adding a
|
|
25
|
+
* remote origin re-opens the cross-site hijack surface — don't.
|
|
26
|
+
*/
|
|
27
|
+
allowedOrigins?: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Central WebSocket hub that brokers messages between Yoltra stores and DevTools extensions.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* - Accepts WS connections, validates protocol handshakes, and routes messages.
|
|
34
|
+
* - Store events are fan-out to all extension clients.
|
|
35
|
+
* - Extension commands are routed to the target store by `storeId`.
|
|
36
|
+
* - Maintains a ring buffer of recent events for late-connecting extensions.
|
|
37
|
+
* - Binds to localhost only (v1 security).
|
|
38
|
+
*
|
|
39
|
+
* @example Embeddable usage
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { DevtoolsHub } from '@yoltra/devtools-server';
|
|
42
|
+
*
|
|
43
|
+
* const hub = new DevtoolsHub({ port: 9800 });
|
|
44
|
+
* await hub.start();
|
|
45
|
+
* // ... later
|
|
46
|
+
* await hub.stop();
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export declare class DevtoolsHub {
|
|
52
|
+
private readonly port;
|
|
53
|
+
private readonly host;
|
|
54
|
+
private readonly allowedOrigins;
|
|
55
|
+
private readonly router;
|
|
56
|
+
private readonly history;
|
|
57
|
+
private wss;
|
|
58
|
+
/**
|
|
59
|
+
* Create a new DevTools hub instance.
|
|
60
|
+
*
|
|
61
|
+
* @param opts - Hub configuration. All fields are optional.
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
constructor(opts?: DevtoolsHubOptions);
|
|
66
|
+
/**
|
|
67
|
+
* Start the WebSocket server and begin accepting connections.
|
|
68
|
+
*
|
|
69
|
+
* @returns Resolves once the server is bound and listening.
|
|
70
|
+
* @throws If the underlying `WebSocketServer` emits an error during
|
|
71
|
+
* startup (e.g. port already in use).
|
|
72
|
+
*
|
|
73
|
+
* @public
|
|
74
|
+
*/
|
|
75
|
+
start(): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Stop the server and close all connections.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Existing client sockets are closed with code `1001` ("Going Away")
|
|
81
|
+
* before the server socket is torn down.
|
|
82
|
+
*
|
|
83
|
+
* @returns Resolves once the server has fully shut down.
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
stop(): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Check if a DevTools hub is already running on the given port.
|
|
90
|
+
*
|
|
91
|
+
* @param port - Port to probe.
|
|
92
|
+
* @returns `true` if a hub is listening and responds to handshake.
|
|
93
|
+
*
|
|
94
|
+
* @public
|
|
95
|
+
*/
|
|
96
|
+
static probe(port: number): Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* Handle a new WebSocket connection: wait for handshake, then route messages.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Starts a handshake timeout timer. If the first valid message is a
|
|
102
|
+
* `HANDSHAKE_REQUEST` the connection is promoted to a routed client;
|
|
103
|
+
* otherwise it is closed after {@link HANDSHAKE_TIMEOUT_MS}.
|
|
104
|
+
*
|
|
105
|
+
* @param ws - Newly accepted WebSocket.
|
|
106
|
+
*/
|
|
107
|
+
private handleConnection;
|
|
108
|
+
/**
|
|
109
|
+
* Process a handshake request: validate, register, and respond.
|
|
110
|
+
*
|
|
111
|
+
* @remarks
|
|
112
|
+
* Performs a major-version compatibility check against
|
|
113
|
+
* {@link PROTOCOL_VERSION}. On success the connection is registered with
|
|
114
|
+
* the {@link Router} and post-handshake side-effects are triggered
|
|
115
|
+
* (store-connected broadcast or registry + history replay).
|
|
116
|
+
*
|
|
117
|
+
* @param ws - The client WebSocket.
|
|
118
|
+
* @param req - Parsed handshake request payload.
|
|
119
|
+
* @returns The new {@link ConnectionInfo} on success, or `null` if the
|
|
120
|
+
* handshake was rejected.
|
|
121
|
+
*/
|
|
122
|
+
private handleHandshake;
|
|
123
|
+
/**
|
|
124
|
+
* Route a post-handshake message based on the sender's role.
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* Store messages are fanned-out to all extensions and, if the message
|
|
128
|
+
* type is `STORE_EVENT`, buffered in the ring buffer for replay.
|
|
129
|
+
* Extension messages are forwarded to the store identified by
|
|
130
|
+
* `msg.storeId`.
|
|
131
|
+
*
|
|
132
|
+
* @param sender - Connection info of the sending client.
|
|
133
|
+
* @param msg - Parsed message payload (untyped; serialized internally).
|
|
134
|
+
*/
|
|
135
|
+
private routeMessage;
|
|
136
|
+
/**
|
|
137
|
+
* Handle a client disconnection.
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Unregisters the client from the {@link Router}. If the client was a
|
|
141
|
+
* store, a `STORE_DISCONNECTED` event is broadcast to all extensions.
|
|
142
|
+
*
|
|
143
|
+
* @param info - Connection info of the disconnected client.
|
|
144
|
+
*/
|
|
145
|
+
private handleDisconnect;
|
|
146
|
+
/**
|
|
147
|
+
* Current number of connected stores.
|
|
148
|
+
*
|
|
149
|
+
* @public
|
|
150
|
+
*/
|
|
151
|
+
get storeCount(): number;
|
|
152
|
+
/**
|
|
153
|
+
* Current number of connected extensions.
|
|
154
|
+
*
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
get extensionCount(): number;
|
|
158
|
+
/**
|
|
159
|
+
* Number of events in the history ring buffer.
|
|
160
|
+
*
|
|
161
|
+
* @public
|
|
162
|
+
*/
|
|
163
|
+
get historySize(): number;
|
|
164
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @yoltra/devtools-server
|
|
3
|
+
*
|
|
4
|
+
* Central WebSocket hub for the Yoltra DevTools suite.
|
|
5
|
+
* Can be used as an embeddable library or a standalone CLI server.
|
|
6
|
+
*/
|
|
7
|
+
export { main as startCli } from './cli';
|
|
8
|
+
export { DevtoolsHub } from './hub';
|
|
9
|
+
export type { DevtoolsHubOptions } from './hub';
|
|
10
|
+
export { RingBuffer } from './ring-buffer';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed-size circular buffer for bounded event retention.
|
|
3
|
+
*
|
|
4
|
+
* @module @yoltra/devtools-server
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Fixed-size circular buffer that overwrites the oldest entry on overflow.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam T - Item type stored in the buffer.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Used by the hub to retain event history for late-connecting extensions.
|
|
13
|
+
* The buffer pre-allocates an array of the given capacity and uses modular
|
|
14
|
+
* arithmetic to track insertion position, making {@link push} an O(1)
|
|
15
|
+
* operation with no memory allocation after construction.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export declare class RingBuffer<T> {
|
|
20
|
+
readonly capacity: number;
|
|
21
|
+
private readonly items;
|
|
22
|
+
private head;
|
|
23
|
+
private count;
|
|
24
|
+
/**
|
|
25
|
+
* @param capacity - Maximum number of items. Must be at least 1.
|
|
26
|
+
*/
|
|
27
|
+
constructor(capacity: number);
|
|
28
|
+
/**
|
|
29
|
+
* Push an item. Overwrites the oldest if at capacity.
|
|
30
|
+
*
|
|
31
|
+
* @param item - Item to add.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
push(item: T): void;
|
|
36
|
+
/**
|
|
37
|
+
* Returns all items in insertion order (oldest first).
|
|
38
|
+
*
|
|
39
|
+
* @returns A new array containing buffered items from oldest to newest.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
toArray(): T[];
|
|
44
|
+
/**
|
|
45
|
+
* Current number of items stored in the buffer.
|
|
46
|
+
*
|
|
47
|
+
* @returns A value between `0` and {@link capacity} inclusive.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
get size(): number;
|
|
52
|
+
/**
|
|
53
|
+
* Remove all items.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
clear(): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { DevtoolsRole } from '@yoltra/devtools-protocol';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import { ConnectionInfo } from './connection';
|
|
4
|
+
/**
|
|
5
|
+
* Routes DevTools protocol messages between stores and extensions.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The router maintains two parallel maps -- one for store connections and
|
|
9
|
+
* one for extension connections -- and exposes helpers that implement the
|
|
10
|
+
* three core routing patterns of the DevTools protocol:
|
|
11
|
+
*
|
|
12
|
+
* - **Fan-out**: Store messages are forwarded to every connected extension.
|
|
13
|
+
* - **Targeted delivery**: Extension commands are routed to a specific
|
|
14
|
+
* store identified by `storeId`.
|
|
15
|
+
* - **Lifecycle broadcast**: `STORE_CONNECTED` / `STORE_DISCONNECTED`
|
|
16
|
+
* events are broadcast to all extensions whenever a store joins or
|
|
17
|
+
* leaves.
|
|
18
|
+
*
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export declare class Router {
|
|
22
|
+
/** All store connections, keyed by store ID. */
|
|
23
|
+
private readonly stores;
|
|
24
|
+
/** All extension connections, keyed by extension ID. */
|
|
25
|
+
private readonly extensions;
|
|
26
|
+
/**
|
|
27
|
+
* Register a newly handshaked connection.
|
|
28
|
+
*
|
|
29
|
+
* @param info - Connection info from the completed handshake.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
register(info: ConnectionInfo): void;
|
|
34
|
+
/**
|
|
35
|
+
* Remove a connection by ID.
|
|
36
|
+
*
|
|
37
|
+
* @param id - Client ID to remove.
|
|
38
|
+
* @param role - Client role (`STORE` or `EXTENSION`).
|
|
39
|
+
*
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
unregister(id: string, role: DevtoolsRole): void;
|
|
43
|
+
/**
|
|
44
|
+
* Get the WebSocket for a specific store.
|
|
45
|
+
*
|
|
46
|
+
* @param storeId - Store UUID.
|
|
47
|
+
* @returns The store's WebSocket, or `undefined` if not connected.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
getStoreSocket(storeId: string): WebSocket | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Route a message from a store to all extensions (fan-out).
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* Only sends to extensions whose WebSocket is in the `OPEN` ready-state;
|
|
57
|
+
* connections in a closing or closed state are silently skipped.
|
|
58
|
+
*
|
|
59
|
+
* @param message - Serialized JSON message string.
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
fanOutToExtensions(message: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Route a message from an extension to a specific store.
|
|
66
|
+
*
|
|
67
|
+
* @param storeId - Target store UUID.
|
|
68
|
+
* @param message - Serialized JSON message string.
|
|
69
|
+
* @returns `true` if the message was sent, `false` if the store was
|
|
70
|
+
* not found or its socket was not open.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
sendToStore(storeId: string, message: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Build a `STORE_CONNECTED` broadcast message.
|
|
77
|
+
*
|
|
78
|
+
* @param info - Store connection info (must have {@link ConnectionInfo.storeInfo}).
|
|
79
|
+
* @returns Serialized {@link StoreConnected} JSON string.
|
|
80
|
+
*
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
buildStoreConnectedMessage(info: ConnectionInfo): string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Build a `STORE_DISCONNECTED` broadcast message.
|
|
86
|
+
*
|
|
87
|
+
* @param storeId - Disconnected store ID.
|
|
88
|
+
* @param reason - Optional human-readable disconnect reason.
|
|
89
|
+
* @returns Serialized {@link StoreDisconnected} JSON string.
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
buildStoreDisconnectedMessage(storeId: string, reason?: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Build a `STORE_REGISTRY` message listing all connected stores.
|
|
96
|
+
*
|
|
97
|
+
* @returns Serialized {@link StoreRegistry} JSON string.
|
|
98
|
+
*
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
buildRegistryMessage(): string;
|
|
102
|
+
/**
|
|
103
|
+
* Number of connected stores.
|
|
104
|
+
*
|
|
105
|
+
* @returns Current store connection count.
|
|
106
|
+
*
|
|
107
|
+
* @public
|
|
108
|
+
*/
|
|
109
|
+
get storeCount(): number;
|
|
110
|
+
/**
|
|
111
|
+
* Number of connected extensions.
|
|
112
|
+
*
|
|
113
|
+
* @returns Current extension connection count.
|
|
114
|
+
*
|
|
115
|
+
* @public
|
|
116
|
+
*/
|
|
117
|
+
get extensionCount(): number;
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yoltra/devtools-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Hub WebSocket server for Yoltra DevTools — standalone CLI and embeddable library",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Manu Ramirez <@pixerael>",
|
|
8
|
+
"email": "manu@yoltra.dev"
|
|
9
|
+
},
|
|
10
|
+
"maintainers": [],
|
|
11
|
+
"homepage": "https://yoltra.dev",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"yoltra",
|
|
14
|
+
"devtools",
|
|
15
|
+
"server"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/yoltra/yoltra.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/yoltra/yoltra/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/devtools-server.cjs.js",
|
|
26
|
+
"module": "dist/devtools-server.esm.js",
|
|
27
|
+
"types": "dist/types/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/types/index.d.ts",
|
|
31
|
+
"import": "./dist/devtools-server.esm.js",
|
|
32
|
+
"require": "./dist/devtools-server.cjs.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"bin": {
|
|
36
|
+
"devtools-server": "./bin/devtools-server.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"bin"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"ws": "^8.19.0",
|
|
45
|
+
"@yoltra/devtools-protocol": "0.2.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/ws": "^8.18.1",
|
|
49
|
+
"@types/node": "^24.0.12",
|
|
50
|
+
"typedoc": "^0.28.13",
|
|
51
|
+
"typedoc-plugin-markdown": "4.9.0",
|
|
52
|
+
"typedoc-plugin-localization": "3.0.6",
|
|
53
|
+
"typescript": "5.9.3",
|
|
54
|
+
"vite": "^7.1.11",
|
|
55
|
+
"vite-plugin-dts": "^4.5.4",
|
|
56
|
+
"vite-plugin-banner": "0.8.1",
|
|
57
|
+
"vitest": "3.2.4"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.18"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "vite build",
|
|
67
|
+
"test": "vitest --watch=false",
|
|
68
|
+
"lint": "node ../../tools/repo-tools/bin/repo-eslint.cjs --report-unused-disable-directives --max-warnings 0",
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"docs": "rushx docs:js && rushx docs:md",
|
|
71
|
+
"docs:md": "pnpm typedoc --options ./typedoc.json",
|
|
72
|
+
"docs:js": "pnpm typedoc --options ./typedoc.json --json ./.typedoc/devtools-server-en.json",
|
|
73
|
+
"start": "node bin/devtools-server.js"
|
|
74
|
+
}
|
|
75
|
+
}
|