@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.
@@ -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
+ }