ohzi-core 13.2.2 → 14.0.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/build/index.mjs +1 -1
- package/build/index.mjs.map +1 -1
- package/package.json +7 -4
- package/src/camera_controller/CameraController.ts +6 -4
- package/src/dev_bridge/CameraBridge.ts +363 -0
- package/src/dev_bridge/CaptureService.ts +256 -0
- package/src/dev_bridge/CommandDispatcher.ts +114 -0
- package/src/dev_bridge/ConsoleBuffer.ts +286 -0
- package/src/dev_bridge/DevBridge.ts +195 -0
- package/src/dev_bridge/InputSynthesizer.ts +287 -0
- package/src/dev_bridge/PerformanceProbe.ts +118 -0
- package/src/dev_bridge/RenderModeRegistry.ts +66 -0
- package/src/dev_bridge/SceneEditor.ts +288 -0
- package/src/dev_bridge/SceneInspector.ts +388 -0
- package/src/dev_bridge/ViewNavigator.ts +101 -0
- package/src/dev_bridge/protocol.ts +31 -0
- package/src/index.ts +15 -5
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Command, Reply } from './protocol';
|
|
2
|
+
|
|
3
|
+
// `unknown` already absorbs Promise<unknown>; run() normalises both via Promise.resolve.
|
|
4
|
+
export type CommandHandler = (args: Record<string, unknown>) => unknown;
|
|
5
|
+
export type HandlerTiming = 'immediate' | 'frame_end';
|
|
6
|
+
|
|
7
|
+
interface Registration
|
|
8
|
+
{
|
|
9
|
+
timing: HandlerTiming;
|
|
10
|
+
handler: CommandHandler;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface QueuedCommand
|
|
14
|
+
{
|
|
15
|
+
id: string;
|
|
16
|
+
args: Record<string, unknown>;
|
|
17
|
+
handler: CommandHandler;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class CommandDispatcher
|
|
21
|
+
{
|
|
22
|
+
handlers: Record<string, Registration>;
|
|
23
|
+
queue: QueuedCommand[];
|
|
24
|
+
|
|
25
|
+
private reply: (reply: Reply) => void;
|
|
26
|
+
|
|
27
|
+
constructor(reply: (reply: Reply) => void)
|
|
28
|
+
{
|
|
29
|
+
this.reply = reply;
|
|
30
|
+
this.handlers = {};
|
|
31
|
+
this.queue = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
register(cmd: string, timing: HandlerTiming, handler: CommandHandler)
|
|
35
|
+
{
|
|
36
|
+
this.handlers[cmd] = { timing, handler };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handle(command: Command)
|
|
40
|
+
{
|
|
41
|
+
const registration = this.handlers[command.cmd];
|
|
42
|
+
const args = command.args === undefined ? {} : command.args;
|
|
43
|
+
|
|
44
|
+
if (registration === undefined)
|
|
45
|
+
{
|
|
46
|
+
this.reply({
|
|
47
|
+
id: command.id,
|
|
48
|
+
ok: false,
|
|
49
|
+
error: { code: 'unknown_command', message: `Unknown command '${command.cmd}'` }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (registration.timing === 'frame_end')
|
|
56
|
+
{
|
|
57
|
+
this.queue.push({ id: command.id, args, handler: registration.handler });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.run(command.id, args, registration.handler);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Called from MainApplication.on_frame_end() so rendering and mutation
|
|
65
|
+
// never happen part-way through a frame.
|
|
66
|
+
drain()
|
|
67
|
+
{
|
|
68
|
+
const queued = this.queue;
|
|
69
|
+
this.queue = [];
|
|
70
|
+
|
|
71
|
+
for (const command of queued)
|
|
72
|
+
{
|
|
73
|
+
this.run(command.id, command.args, command.handler);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private run(id: string, args: Record<string, unknown>, handler: CommandHandler)
|
|
78
|
+
{
|
|
79
|
+
try
|
|
80
|
+
{
|
|
81
|
+
Promise.resolve(handler(args))
|
|
82
|
+
.then((result) => this.reply({ id, ok: true, result }))
|
|
83
|
+
.catch((error) => this.reply({ id, ok: false, error: this.to_error(error) }));
|
|
84
|
+
}
|
|
85
|
+
catch (error)
|
|
86
|
+
{
|
|
87
|
+
this.reply({ id, ok: false, error: this.to_error(error) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private to_error(error: unknown): { code: string; message: string }
|
|
92
|
+
{
|
|
93
|
+
const code = (error as { code?: string })?.code;
|
|
94
|
+
const message = (error as { message?: string })?.message;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
code: code === undefined ? 'handler_failed' : code,
|
|
98
|
+
message: typeof message === 'string' ? message : this.describe(error)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Avoids '[object Object]' reaching the developer for a thrown non-Error.
|
|
103
|
+
private describe(error: unknown): string
|
|
104
|
+
{
|
|
105
|
+
if (typeof error === 'string')
|
|
106
|
+
{
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `Handler failed with a non-Error value of type ${typeof error}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { CommandDispatcher };
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const DEFAULT_CAPACITY = 200;
|
|
2
|
+
const MAX_MESSAGE_LENGTH = 2000;
|
|
3
|
+
|
|
4
|
+
export type ConsoleLevel = 'log' | 'info' | 'warn' | 'error';
|
|
5
|
+
|
|
6
|
+
const LEVELS: ConsoleLevel[] = ['log', 'info', 'warn', 'error'];
|
|
7
|
+
|
|
8
|
+
export interface ConsoleEntry
|
|
9
|
+
{
|
|
10
|
+
seq: number;
|
|
11
|
+
level: ConsoleLevel;
|
|
12
|
+
message: string;
|
|
13
|
+
at: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ConsoleLike
|
|
17
|
+
{
|
|
18
|
+
log: (...args: unknown[]) => void;
|
|
19
|
+
info: (...args: unknown[]) => void;
|
|
20
|
+
warn: (...args: unknown[]) => void;
|
|
21
|
+
error: (...args: unknown[]) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ErrorSource
|
|
25
|
+
{
|
|
26
|
+
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
27
|
+
removeEventListener(type: string, listener: (event: unknown) => void): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Read options arrive off the dev-bridge wire and are therefore untrusted.
|
|
31
|
+
export interface ConsoleReadOptions
|
|
32
|
+
{
|
|
33
|
+
level?: unknown;
|
|
34
|
+
limit?: unknown;
|
|
35
|
+
since?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConsoleReadResult
|
|
39
|
+
{
|
|
40
|
+
entries: ConsoleEntry[];
|
|
41
|
+
dropped: number;
|
|
42
|
+
total: number;
|
|
43
|
+
capacity: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A bounded log of console output and uncaught failures, so the bridge can
|
|
47
|
+
// report what a screenshot cannot show. Injected console and error source keep
|
|
48
|
+
// this testable outside a browser.
|
|
49
|
+
class ConsoleBuffer
|
|
50
|
+
{
|
|
51
|
+
private capacity: number;
|
|
52
|
+
private entries: ConsoleEntry[];
|
|
53
|
+
private seq: number;
|
|
54
|
+
private dropped: number;
|
|
55
|
+
private clock: () => number;
|
|
56
|
+
|
|
57
|
+
private target: ConsoleLike | null;
|
|
58
|
+
private source: ErrorSource | null;
|
|
59
|
+
private originals: Partial<Record<ConsoleLevel, (...args: unknown[]) => void>>;
|
|
60
|
+
private on_error: ((event: unknown) => void) | null;
|
|
61
|
+
private on_rejection: ((event: unknown) => void) | null;
|
|
62
|
+
|
|
63
|
+
constructor(capacity: number = DEFAULT_CAPACITY, clock: () => number = () => Date.now())
|
|
64
|
+
{
|
|
65
|
+
this.capacity = capacity > 0 ? Math.floor(capacity) : DEFAULT_CAPACITY;
|
|
66
|
+
this.entries = [];
|
|
67
|
+
this.seq = 0;
|
|
68
|
+
this.dropped = 0;
|
|
69
|
+
this.clock = clock;
|
|
70
|
+
this.target = null;
|
|
71
|
+
this.source = null;
|
|
72
|
+
this.originals = {};
|
|
73
|
+
this.on_error = null;
|
|
74
|
+
this.on_rejection = null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
install(target: ConsoleLike, source?: ErrorSource)
|
|
78
|
+
{
|
|
79
|
+
this.dispose();
|
|
80
|
+
this.target = target;
|
|
81
|
+
|
|
82
|
+
for (const level of LEVELS)
|
|
83
|
+
{
|
|
84
|
+
// Explicitly typed: indexing with a union then calling .bind() widens to
|
|
85
|
+
// any, which the lint rules reject.
|
|
86
|
+
const original: (...args: unknown[]) => void = target[level];
|
|
87
|
+
this.originals[level] = original;
|
|
88
|
+
|
|
89
|
+
target[level] = (...args: unknown[]) =>
|
|
90
|
+
{
|
|
91
|
+
this.push(level, this.format(args));
|
|
92
|
+
original.apply(target, args);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (source !== undefined)
|
|
97
|
+
{
|
|
98
|
+
this.source = source;
|
|
99
|
+
|
|
100
|
+
this.on_error = (event) => this.push('error', this.event_message(event));
|
|
101
|
+
this.on_rejection = (event) => this.push('error', `Unhandled rejection: ${this.event_reason(event)}`);
|
|
102
|
+
|
|
103
|
+
source.addEventListener('error', this.on_error);
|
|
104
|
+
source.addEventListener('unhandledrejection', this.on_rejection);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
dispose()
|
|
109
|
+
{
|
|
110
|
+
const target = this.target;
|
|
111
|
+
|
|
112
|
+
if (target !== null)
|
|
113
|
+
{
|
|
114
|
+
for (const level of LEVELS)
|
|
115
|
+
{
|
|
116
|
+
const original = this.originals[level];
|
|
117
|
+
|
|
118
|
+
if (original !== undefined)
|
|
119
|
+
{
|
|
120
|
+
target[level] = original;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const source = this.source;
|
|
126
|
+
|
|
127
|
+
if (source !== null)
|
|
128
|
+
{
|
|
129
|
+
if (this.on_error !== null)
|
|
130
|
+
{
|
|
131
|
+
source.removeEventListener('error', this.on_error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.on_rejection !== null)
|
|
135
|
+
{
|
|
136
|
+
source.removeEventListener('unhandledrejection', this.on_rejection);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.originals = {};
|
|
141
|
+
this.target = null;
|
|
142
|
+
this.source = null;
|
|
143
|
+
this.on_error = null;
|
|
144
|
+
this.on_rejection = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
read(options: ConsoleReadOptions): ConsoleReadResult
|
|
148
|
+
{
|
|
149
|
+
const level = typeof options.level === 'string' ? options.level : null;
|
|
150
|
+
const since = typeof options.since === 'number' ? options.since : 0;
|
|
151
|
+
const limit = typeof options.limit === 'number' && options.limit > 0 ? Math.floor(options.limit) : 0;
|
|
152
|
+
|
|
153
|
+
let entries = this.entries.filter((entry) => entry.seq > since);
|
|
154
|
+
|
|
155
|
+
if (level !== null)
|
|
156
|
+
{
|
|
157
|
+
entries = entries.filter((entry) => entry.level === level);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (limit > 0 && entries.length > limit)
|
|
161
|
+
{
|
|
162
|
+
entries = entries.slice(entries.length - limit);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { entries, dropped: this.dropped, total: this.seq, capacity: this.capacity };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private push(level: ConsoleLevel, message: string)
|
|
169
|
+
{
|
|
170
|
+
this.seq++;
|
|
171
|
+
this.entries.push({ seq: this.seq, level, message, at: this.clock() });
|
|
172
|
+
|
|
173
|
+
while (this.entries.length > this.capacity)
|
|
174
|
+
{
|
|
175
|
+
this.entries.shift();
|
|
176
|
+
this.dropped++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private format(args: unknown[]): string
|
|
181
|
+
{
|
|
182
|
+
const message = args.map((arg) => this.describe(arg)).join(' ');
|
|
183
|
+
|
|
184
|
+
if (message.length <= MAX_MESSAGE_LENGTH)
|
|
185
|
+
{
|
|
186
|
+
return message;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const overflow = message.length - MAX_MESSAGE_LENGTH;
|
|
190
|
+
|
|
191
|
+
return `${message.slice(0, MAX_MESSAGE_LENGTH)} ... [truncated ${overflow} chars]`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Deliberately exhaustive: a log line rendered as '[object Object]' is worse
|
|
195
|
+
// than no log line at all.
|
|
196
|
+
private describe(value: unknown): string
|
|
197
|
+
{
|
|
198
|
+
if (typeof value === 'string')
|
|
199
|
+
{
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (value === null)
|
|
204
|
+
{
|
|
205
|
+
return 'null';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (value === undefined)
|
|
209
|
+
{
|
|
210
|
+
return 'undefined';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
214
|
+
{
|
|
215
|
+
return String(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof value === 'function')
|
|
219
|
+
{
|
|
220
|
+
return '[function]';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (typeof value === 'symbol')
|
|
224
|
+
{
|
|
225
|
+
return '[symbol]';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof value === 'bigint')
|
|
229
|
+
{
|
|
230
|
+
return `${value.toString()}n`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (value instanceof Error)
|
|
234
|
+
{
|
|
235
|
+
return `${value.name}: ${value.message}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try
|
|
239
|
+
{
|
|
240
|
+
const json = JSON.stringify(value);
|
|
241
|
+
|
|
242
|
+
return json === undefined ? '[unserialisable]' : json;
|
|
243
|
+
}
|
|
244
|
+
catch
|
|
245
|
+
{
|
|
246
|
+
return '[unserialisable]';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private event_message(event: unknown): string
|
|
251
|
+
{
|
|
252
|
+
if (typeof event === 'object' && event !== null)
|
|
253
|
+
{
|
|
254
|
+
const record = event as Record<string, unknown>;
|
|
255
|
+
|
|
256
|
+
if (typeof record.message === 'string')
|
|
257
|
+
{
|
|
258
|
+
return record.message;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (record.error !== undefined)
|
|
262
|
+
{
|
|
263
|
+
return this.describe(record.error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this.describe(event);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private event_reason(event: unknown): string
|
|
271
|
+
{
|
|
272
|
+
if (typeof event === 'object' && event !== null)
|
|
273
|
+
{
|
|
274
|
+
const record = event as Record<string, unknown>;
|
|
275
|
+
|
|
276
|
+
if (record.reason !== undefined)
|
|
277
|
+
{
|
|
278
|
+
return this.describe(record.reason);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return this.describe(event);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export { ConsoleBuffer };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { CommandHandler, HandlerTiming } from './CommandDispatcher';
|
|
2
|
+
import { CommandDispatcher } from './CommandDispatcher';
|
|
3
|
+
import type { AppInfo, Command, Reply } from './protocol';
|
|
4
|
+
import { PROTOCOL_VERSION } from './protocol';
|
|
5
|
+
|
|
6
|
+
const RETRY_DELAY_MS = 2000;
|
|
7
|
+
|
|
8
|
+
interface DevBridgeOptions
|
|
9
|
+
{
|
|
10
|
+
port: number;
|
|
11
|
+
app_info: () => AppInfo;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class DevBridge
|
|
15
|
+
{
|
|
16
|
+
dispatcher: CommandDispatcher;
|
|
17
|
+
|
|
18
|
+
private socket: WebSocket;
|
|
19
|
+
private options: DevBridgeOptions;
|
|
20
|
+
private retry_timer: number;
|
|
21
|
+
private disposed: boolean;
|
|
22
|
+
|
|
23
|
+
constructor()
|
|
24
|
+
{
|
|
25
|
+
this.socket = null;
|
|
26
|
+
this.options = null;
|
|
27
|
+
this.retry_timer = 0;
|
|
28
|
+
this.disposed = false;
|
|
29
|
+
this.dispatcher = new CommandDispatcher((reply) => this.send_reply(reply));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
init(options: DevBridgeOptions)
|
|
33
|
+
{
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.disposed = false;
|
|
36
|
+
this.connect();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
register(cmd: string, timing: HandlerTiming, handler: CommandHandler)
|
|
40
|
+
{
|
|
41
|
+
this.dispatcher.register(cmd, timing, handler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Called from the application's on_frame_end so queued commands run at a
|
|
45
|
+
// safe frame boundary instead of part-way through a frame.
|
|
46
|
+
on_frame_end()
|
|
47
|
+
{
|
|
48
|
+
this.dispatcher.drain();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
dispose()
|
|
52
|
+
{
|
|
53
|
+
this.disposed = true;
|
|
54
|
+
window.clearTimeout(this.retry_timer);
|
|
55
|
+
|
|
56
|
+
if (this.socket !== null)
|
|
57
|
+
{
|
|
58
|
+
this.socket.onclose = null;
|
|
59
|
+
this.socket.close();
|
|
60
|
+
this.socket = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private connect()
|
|
65
|
+
{
|
|
66
|
+
const options = this.options;
|
|
67
|
+
|
|
68
|
+
if (this.disposed || options === null)
|
|
69
|
+
{
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const socket = new WebSocket(`ws://127.0.0.1:${options.port}`);
|
|
74
|
+
this.socket = socket;
|
|
75
|
+
|
|
76
|
+
socket.onopen = () =>
|
|
77
|
+
{
|
|
78
|
+
socket.send(JSON.stringify({
|
|
79
|
+
event: 'hello',
|
|
80
|
+
protocol: PROTOCOL_VERSION,
|
|
81
|
+
app: options.app_info()
|
|
82
|
+
}));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
socket.onmessage = (message) => this.on_message(message);
|
|
86
|
+
|
|
87
|
+
socket.onclose = () =>
|
|
88
|
+
{
|
|
89
|
+
if (this.socket === socket)
|
|
90
|
+
{
|
|
91
|
+
this.socket = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.schedule_retry();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// A closed host raises both error and close. Retry is driven by close only.
|
|
98
|
+
socket.onerror = () => {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private on_message(message: { data: unknown })
|
|
102
|
+
{
|
|
103
|
+
const raw = message.data;
|
|
104
|
+
|
|
105
|
+
if (typeof raw !== 'string')
|
|
106
|
+
{
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const frame = this.parse_frame(raw);
|
|
111
|
+
|
|
112
|
+
if (frame === null)
|
|
113
|
+
{
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (frame.event === 'welcome')
|
|
118
|
+
{
|
|
119
|
+
console.info('[ohzi-mcp] bridge connected');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (frame.event === 'refused')
|
|
124
|
+
{
|
|
125
|
+
const reason = this.as_text(frame.reason);
|
|
126
|
+
const server_protocol = this.as_text(frame.server_protocol);
|
|
127
|
+
|
|
128
|
+
console.warn(`[ohzi-mcp] bridge refused: ${reason} (server protocol ${server_protocol}, this app speaks ${PROTOCOL_VERSION})`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof frame.id === 'string' && typeof frame.cmd === 'string')
|
|
133
|
+
{
|
|
134
|
+
this.dispatcher.handle(frame as unknown as Command);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private parse_frame(raw: string): Record<string, unknown> | null
|
|
139
|
+
{
|
|
140
|
+
try
|
|
141
|
+
{
|
|
142
|
+
const value: unknown = JSON.parse(raw);
|
|
143
|
+
|
|
144
|
+
if (typeof value !== 'object' || value === null)
|
|
145
|
+
{
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return value as Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
catch
|
|
152
|
+
{
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Never let an object reach a template literal as '[object Object]'.
|
|
158
|
+
private as_text(value: unknown): string
|
|
159
|
+
{
|
|
160
|
+
if (typeof value === 'string')
|
|
161
|
+
{
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
166
|
+
{
|
|
167
|
+
return String(value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return '(unknown)';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private send_reply(reply: Reply)
|
|
174
|
+
{
|
|
175
|
+
if (this.socket === null || this.socket.readyState !== WebSocket.OPEN)
|
|
176
|
+
{
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.socket.send(JSON.stringify(reply));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private schedule_retry()
|
|
184
|
+
{
|
|
185
|
+
if (this.disposed)
|
|
186
|
+
{
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
window.clearTimeout(this.retry_timer);
|
|
191
|
+
this.retry_timer = window.setTimeout(() => this.connect(), RETRY_DELAY_MS);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { DevBridge };
|