@wavecraft/core 0.7.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 +67 -0
- package/dist/index.d.ts +573 -0
- package/dist/index.js +782 -0
- package/dist/index.js.map +1 -0
- package/dist/meters.d.ts +41 -0
- package/dist/meters.js +15 -0
- package/dist/meters.js.map +1 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
2
|
+
import { dbToLinear, linearToDb } from "./meters.js";
|
|
3
|
+
function isWebViewEnvironment() {
|
|
4
|
+
return globalThis.__WAVECRAFT_IPC__ !== void 0;
|
|
5
|
+
}
|
|
6
|
+
function isBrowserEnvironment() {
|
|
7
|
+
return !isWebViewEnvironment();
|
|
8
|
+
}
|
|
9
|
+
const ERROR_PARSE = -32700;
|
|
10
|
+
const ERROR_INVALID_REQUEST = -32600;
|
|
11
|
+
const ERROR_METHOD_NOT_FOUND = -32601;
|
|
12
|
+
const ERROR_INVALID_PARAMS = -32602;
|
|
13
|
+
const ERROR_INTERNAL = -32603;
|
|
14
|
+
const ERROR_PARAM_NOT_FOUND = -32e3;
|
|
15
|
+
const ERROR_PARAM_OUT_OF_RANGE = -32001;
|
|
16
|
+
const METHOD_GET_PARAMETER = "getParameter";
|
|
17
|
+
const METHOD_SET_PARAMETER = "setParameter";
|
|
18
|
+
const METHOD_GET_ALL_PARAMETERS = "getAllParameters";
|
|
19
|
+
const NOTIFICATION_PARAMETER_CHANGED = "parameterChanged";
|
|
20
|
+
function isIpcResponse(obj) {
|
|
21
|
+
return typeof obj === "object" && obj !== null && "jsonrpc" in obj && "id" in obj && ("result" in obj || "error" in obj);
|
|
22
|
+
}
|
|
23
|
+
function isIpcNotification(obj) {
|
|
24
|
+
return typeof obj === "object" && obj !== null && "jsonrpc" in obj && "method" in obj && !("id" in obj);
|
|
25
|
+
}
|
|
26
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
27
|
+
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
|
|
28
|
+
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
|
|
29
|
+
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
|
|
30
|
+
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
|
|
31
|
+
return LogLevel2;
|
|
32
|
+
})(LogLevel || {});
|
|
33
|
+
class Logger {
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
this.minLevel = options.minLevel ?? 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Set the minimum log level at runtime.
|
|
39
|
+
*/
|
|
40
|
+
setMinLevel(level) {
|
|
41
|
+
this.minLevel = level;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Log debug message (verbose tracing).
|
|
45
|
+
*/
|
|
46
|
+
debug(message, context) {
|
|
47
|
+
if (this.minLevel <= 0) {
|
|
48
|
+
console.debug(`[DEBUG] ${message}`, context ?? {});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Log informational message.
|
|
53
|
+
*/
|
|
54
|
+
info(message, context) {
|
|
55
|
+
if (this.minLevel <= 1) {
|
|
56
|
+
console.info(`[INFO] ${message}`, context ?? {});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Log warning message.
|
|
61
|
+
*/
|
|
62
|
+
warn(message, context) {
|
|
63
|
+
if (this.minLevel <= 2) {
|
|
64
|
+
console.warn(`[WARN] ${message}`, context ?? {});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Log error message.
|
|
69
|
+
*/
|
|
70
|
+
error(message, context) {
|
|
71
|
+
if (this.minLevel <= 3) {
|
|
72
|
+
console.error(`[ERROR] ${message}`, context ?? {});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const logger = new Logger({
|
|
77
|
+
minLevel: 1
|
|
78
|
+
/* INFO */
|
|
79
|
+
});
|
|
80
|
+
class NativeTransport {
|
|
81
|
+
constructor() {
|
|
82
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
83
|
+
this.notificationCallbacks = /* @__PURE__ */ new Set();
|
|
84
|
+
this.primitives = globalThis.__WAVECRAFT_IPC__;
|
|
85
|
+
if (!this.primitives) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"NativeTransport: __WAVECRAFT_IPC__ primitives not found. Ensure this runs in a WKWebView with injected IPC."
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
this.primitives.setReceiveCallback((message) => {
|
|
91
|
+
this.handleIncomingMessage(message);
|
|
92
|
+
});
|
|
93
|
+
if (this.primitives.onParamUpdate) {
|
|
94
|
+
this.primitives.onParamUpdate((notification) => {
|
|
95
|
+
if (isIpcNotification(notification)) {
|
|
96
|
+
this.handleNotification(notification);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Send a JSON-RPC request and wait for response
|
|
103
|
+
*/
|
|
104
|
+
async send(request) {
|
|
105
|
+
if (!this.primitives) {
|
|
106
|
+
throw new Error("NativeTransport: Primitives not available");
|
|
107
|
+
}
|
|
108
|
+
const parsedRequest = JSON.parse(request);
|
|
109
|
+
const id = parsedRequest.id;
|
|
110
|
+
if (id === void 0 || id === null) {
|
|
111
|
+
throw new Error("NativeTransport: Request must have an id");
|
|
112
|
+
}
|
|
113
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
114
|
+
const timeoutId = setTimeout(() => {
|
|
115
|
+
this.pendingRequests.delete(id);
|
|
116
|
+
reject(new Error(`Request timeout: ${parsedRequest.method}`));
|
|
117
|
+
}, 5e3);
|
|
118
|
+
this.pendingRequests.set(id, { resolve, reject, timeoutId });
|
|
119
|
+
});
|
|
120
|
+
this.primitives.postMessage(request);
|
|
121
|
+
return responsePromise;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Register a callback for incoming notifications
|
|
125
|
+
*/
|
|
126
|
+
onNotification(callback) {
|
|
127
|
+
this.notificationCallbacks.add(callback);
|
|
128
|
+
return () => {
|
|
129
|
+
this.notificationCallbacks.delete(callback);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if transport is connected (native is always connected)
|
|
134
|
+
*/
|
|
135
|
+
isConnected() {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Clean up resources
|
|
140
|
+
*/
|
|
141
|
+
dispose() {
|
|
142
|
+
for (const [id, { reject, timeoutId }] of this.pendingRequests.entries()) {
|
|
143
|
+
clearTimeout(timeoutId);
|
|
144
|
+
reject(new Error("Transport disposed"));
|
|
145
|
+
this.pendingRequests.delete(id);
|
|
146
|
+
}
|
|
147
|
+
this.notificationCallbacks.clear();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Handle incoming message (response or notification)
|
|
151
|
+
*/
|
|
152
|
+
handleIncomingMessage(message) {
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(message);
|
|
155
|
+
if (isIpcResponse(parsed)) {
|
|
156
|
+
this.handleResponse(parsed);
|
|
157
|
+
} else if (isIpcNotification(parsed)) {
|
|
158
|
+
this.handleNotification(parsed);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
logger.error("Failed to parse incoming message", { error });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Handle JSON-RPC response
|
|
166
|
+
*/
|
|
167
|
+
handleResponse(response) {
|
|
168
|
+
const pending = this.pendingRequests.get(response.id);
|
|
169
|
+
if (pending) {
|
|
170
|
+
clearTimeout(pending.timeoutId);
|
|
171
|
+
this.pendingRequests.delete(response.id);
|
|
172
|
+
pending.resolve(JSON.stringify(response));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Handle notification and dispatch to listeners
|
|
177
|
+
*/
|
|
178
|
+
handleNotification(notification) {
|
|
179
|
+
const notificationJson = JSON.stringify(notification);
|
|
180
|
+
for (const callback of this.notificationCallbacks) {
|
|
181
|
+
try {
|
|
182
|
+
callback(notificationJson);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
logger.error("Error in notification callback", { error });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
class WebSocketTransport {
|
|
190
|
+
constructor(options) {
|
|
191
|
+
this.ws = null;
|
|
192
|
+
this.isConnecting = false;
|
|
193
|
+
this.reconnectAttempts = 0;
|
|
194
|
+
this.reconnectTimeoutId = null;
|
|
195
|
+
this.isDisposed = false;
|
|
196
|
+
this.maxAttemptsReached = false;
|
|
197
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
198
|
+
this.notificationCallbacks = /* @__PURE__ */ new Set();
|
|
199
|
+
this.url = options.url;
|
|
200
|
+
this.reconnectDelayMs = options.reconnectDelayMs ?? 1e3;
|
|
201
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
202
|
+
this.connect();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Send a JSON-RPC request and wait for response
|
|
206
|
+
*/
|
|
207
|
+
async send(request) {
|
|
208
|
+
if (!this.isConnected()) {
|
|
209
|
+
throw new Error("WebSocketTransport: Not connected");
|
|
210
|
+
}
|
|
211
|
+
const parsedRequest = JSON.parse(request);
|
|
212
|
+
const id = parsedRequest.id;
|
|
213
|
+
if (id === void 0 || id === null) {
|
|
214
|
+
throw new Error("WebSocketTransport: Request must have an id");
|
|
215
|
+
}
|
|
216
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
217
|
+
const timeoutId = setTimeout(() => {
|
|
218
|
+
this.pendingRequests.delete(id);
|
|
219
|
+
reject(new Error(`Request timeout: ${parsedRequest.method}`));
|
|
220
|
+
}, 5e3);
|
|
221
|
+
this.pendingRequests.set(id, { resolve, reject, timeoutId });
|
|
222
|
+
});
|
|
223
|
+
if (!this.ws) {
|
|
224
|
+
throw new Error("WebSocketTransport: Connection lost");
|
|
225
|
+
}
|
|
226
|
+
this.ws.send(request);
|
|
227
|
+
return responsePromise;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Register a callback for incoming notifications
|
|
231
|
+
*/
|
|
232
|
+
onNotification(callback) {
|
|
233
|
+
this.notificationCallbacks.add(callback);
|
|
234
|
+
return () => {
|
|
235
|
+
this.notificationCallbacks.delete(callback);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Check if transport is connected
|
|
240
|
+
*/
|
|
241
|
+
isConnected() {
|
|
242
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Clean up resources and close connection
|
|
246
|
+
*/
|
|
247
|
+
dispose() {
|
|
248
|
+
this.isDisposed = true;
|
|
249
|
+
if (this.reconnectTimeoutId) {
|
|
250
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
251
|
+
this.reconnectTimeoutId = null;
|
|
252
|
+
}
|
|
253
|
+
if (this.ws) {
|
|
254
|
+
this.ws.close();
|
|
255
|
+
this.ws = null;
|
|
256
|
+
}
|
|
257
|
+
for (const [id, { reject, timeoutId }] of this.pendingRequests.entries()) {
|
|
258
|
+
clearTimeout(timeoutId);
|
|
259
|
+
reject(new Error("Transport disposed"));
|
|
260
|
+
this.pendingRequests.delete(id);
|
|
261
|
+
}
|
|
262
|
+
this.notificationCallbacks.clear();
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Attempt to connect to WebSocket server
|
|
266
|
+
*/
|
|
267
|
+
connect() {
|
|
268
|
+
if (this.isDisposed || this.isConnecting || this.isConnected()) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.isConnecting = true;
|
|
272
|
+
try {
|
|
273
|
+
this.ws = new WebSocket(this.url);
|
|
274
|
+
this.ws.onopen = () => {
|
|
275
|
+
this.isConnecting = false;
|
|
276
|
+
this.reconnectAttempts = 0;
|
|
277
|
+
logger.info("WebSocketTransport connected", { url: this.url });
|
|
278
|
+
};
|
|
279
|
+
this.ws.onmessage = (event) => {
|
|
280
|
+
this.handleIncomingMessage(event.data);
|
|
281
|
+
};
|
|
282
|
+
this.ws.onerror = (error) => {
|
|
283
|
+
logger.error("WebSocketTransport connection error", { error });
|
|
284
|
+
};
|
|
285
|
+
this.ws.onclose = () => {
|
|
286
|
+
this.isConnecting = false;
|
|
287
|
+
this.ws = null;
|
|
288
|
+
if (!this.isDisposed && !this.maxAttemptsReached) {
|
|
289
|
+
this.scheduleReconnect();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
this.isConnecting = false;
|
|
294
|
+
logger.error("WebSocketTransport failed to create WebSocket", { error, url: this.url });
|
|
295
|
+
this.scheduleReconnect();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Schedule reconnection attempt with exponential backoff
|
|
300
|
+
*/
|
|
301
|
+
scheduleReconnect() {
|
|
302
|
+
if (this.isDisposed || this.maxAttemptsReached) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
306
|
+
this.maxAttemptsReached = true;
|
|
307
|
+
logger.error("WebSocketTransport max reconnect attempts reached", {
|
|
308
|
+
maxAttempts: this.maxReconnectAttempts
|
|
309
|
+
});
|
|
310
|
+
if (this.ws) {
|
|
311
|
+
this.ws.close();
|
|
312
|
+
this.ws = null;
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.reconnectAttempts++;
|
|
317
|
+
const delay = this.reconnectDelayMs * Math.pow(2, this.reconnectAttempts - 1);
|
|
318
|
+
logger.debug("WebSocketTransport reconnecting", {
|
|
319
|
+
delayMs: delay,
|
|
320
|
+
attempt: this.reconnectAttempts,
|
|
321
|
+
maxAttempts: this.maxReconnectAttempts
|
|
322
|
+
});
|
|
323
|
+
this.reconnectTimeoutId = setTimeout(() => {
|
|
324
|
+
this.reconnectTimeoutId = null;
|
|
325
|
+
this.connect();
|
|
326
|
+
}, delay);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Handle incoming message (response or notification)
|
|
330
|
+
*/
|
|
331
|
+
handleIncomingMessage(message) {
|
|
332
|
+
try {
|
|
333
|
+
const parsed = JSON.parse(message);
|
|
334
|
+
if (isIpcResponse(parsed)) {
|
|
335
|
+
this.handleResponse(parsed);
|
|
336
|
+
} else if (isIpcNotification(parsed)) {
|
|
337
|
+
this.handleNotification(parsed);
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
logger.error("WebSocketTransport failed to parse incoming message", { error, message });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Handle JSON-RPC response
|
|
345
|
+
*/
|
|
346
|
+
handleResponse(response) {
|
|
347
|
+
const pending = this.pendingRequests.get(response.id);
|
|
348
|
+
if (pending) {
|
|
349
|
+
clearTimeout(pending.timeoutId);
|
|
350
|
+
this.pendingRequests.delete(response.id);
|
|
351
|
+
pending.resolve(JSON.stringify(response));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Handle notification and dispatch to listeners
|
|
356
|
+
*/
|
|
357
|
+
handleNotification(notification) {
|
|
358
|
+
const notificationJson = JSON.stringify(notification);
|
|
359
|
+
for (const callback of this.notificationCallbacks) {
|
|
360
|
+
try {
|
|
361
|
+
callback(notificationJson);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
logger.error("WebSocketTransport notification callback error", {
|
|
364
|
+
error,
|
|
365
|
+
method: notification.method
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const IS_WEBVIEW = isWebViewEnvironment();
|
|
372
|
+
let transportInstance = null;
|
|
373
|
+
function getTransport() {
|
|
374
|
+
if (transportInstance) {
|
|
375
|
+
return transportInstance;
|
|
376
|
+
}
|
|
377
|
+
if (IS_WEBVIEW) {
|
|
378
|
+
transportInstance = new NativeTransport();
|
|
379
|
+
} else {
|
|
380
|
+
const wsUrl = "ws://127.0.0.1:9000";
|
|
381
|
+
transportInstance = new WebSocketTransport({ url: wsUrl });
|
|
382
|
+
}
|
|
383
|
+
return transportInstance;
|
|
384
|
+
}
|
|
385
|
+
const _IpcBridge = class _IpcBridge {
|
|
386
|
+
// Log warning max once per 5s
|
|
387
|
+
constructor() {
|
|
388
|
+
this.nextId = 1;
|
|
389
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
390
|
+
this.transport = null;
|
|
391
|
+
this.isInitialized = false;
|
|
392
|
+
this.lastDisconnectWarning = 0;
|
|
393
|
+
this.DISCONNECT_WARNING_INTERVAL_MS = 5e3;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Initialize the IPC bridge (lazy)
|
|
397
|
+
*/
|
|
398
|
+
initialize() {
|
|
399
|
+
if (this.isInitialized) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.transport = getTransport();
|
|
403
|
+
this.transport.onNotification((notificationJson) => {
|
|
404
|
+
try {
|
|
405
|
+
const parsed = JSON.parse(notificationJson);
|
|
406
|
+
if (isIpcNotification(parsed)) {
|
|
407
|
+
this.handleNotification(parsed);
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
logger.error("Failed to parse notification", { error });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
this.isInitialized = true;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get singleton instance
|
|
417
|
+
*/
|
|
418
|
+
static getInstance() {
|
|
419
|
+
_IpcBridge.instance ?? (_IpcBridge.instance = new _IpcBridge());
|
|
420
|
+
return _IpcBridge.instance;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Check if the bridge is connected
|
|
424
|
+
*/
|
|
425
|
+
isConnected() {
|
|
426
|
+
var _a;
|
|
427
|
+
this.initialize();
|
|
428
|
+
return ((_a = this.transport) == null ? void 0 : _a.isConnected()) ?? false;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Invoke a method and wait for response
|
|
432
|
+
*/
|
|
433
|
+
async invoke(method, params) {
|
|
434
|
+
var _a;
|
|
435
|
+
this.initialize();
|
|
436
|
+
if (!((_a = this.transport) == null ? void 0 : _a.isConnected())) {
|
|
437
|
+
const now = Date.now();
|
|
438
|
+
if (now - this.lastDisconnectWarning > this.DISCONNECT_WARNING_INTERVAL_MS) {
|
|
439
|
+
logger.warn("Transport not connected, call will fail. Waiting for reconnection...");
|
|
440
|
+
this.lastDisconnectWarning = now;
|
|
441
|
+
}
|
|
442
|
+
throw new Error("IpcBridge: Transport not connected");
|
|
443
|
+
}
|
|
444
|
+
const id = this.nextId++;
|
|
445
|
+
const request = {
|
|
446
|
+
jsonrpc: "2.0",
|
|
447
|
+
id,
|
|
448
|
+
method,
|
|
449
|
+
params
|
|
450
|
+
};
|
|
451
|
+
const requestJson = JSON.stringify(request);
|
|
452
|
+
const responseJson = await this.transport.send(requestJson);
|
|
453
|
+
const response = JSON.parse(responseJson);
|
|
454
|
+
if (response.error) {
|
|
455
|
+
throw new Error(`IPC Error ${response.error.code}: ${response.error.message}`);
|
|
456
|
+
}
|
|
457
|
+
return response.result;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Subscribe to notification events
|
|
461
|
+
*/
|
|
462
|
+
on(event, callback) {
|
|
463
|
+
this.initialize();
|
|
464
|
+
if (!this.eventListeners.has(event)) {
|
|
465
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
466
|
+
}
|
|
467
|
+
const listeners = this.eventListeners.get(event);
|
|
468
|
+
if (!listeners) {
|
|
469
|
+
throw new Error(`Event listener set not found for event: ${event}`);
|
|
470
|
+
}
|
|
471
|
+
listeners.add(callback);
|
|
472
|
+
return () => {
|
|
473
|
+
listeners.delete(callback);
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Handle notification and dispatch to listeners
|
|
478
|
+
*/
|
|
479
|
+
handleNotification(notification) {
|
|
480
|
+
const listeners = this.eventListeners.get(notification.method);
|
|
481
|
+
if (!listeners || listeners.size === 0) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
for (const listener of listeners) {
|
|
485
|
+
try {
|
|
486
|
+
listener(notification.params);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
logger.error("Error in event listener", { event: notification.method, error });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
_IpcBridge.instance = null;
|
|
494
|
+
let IpcBridge = _IpcBridge;
|
|
495
|
+
const _ParameterClient = class _ParameterClient {
|
|
496
|
+
constructor() {
|
|
497
|
+
this.bridge = IpcBridge.getInstance();
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get singleton instance
|
|
501
|
+
*/
|
|
502
|
+
static getInstance() {
|
|
503
|
+
if (!_ParameterClient.instance) {
|
|
504
|
+
_ParameterClient.instance = new _ParameterClient();
|
|
505
|
+
}
|
|
506
|
+
return _ParameterClient.instance;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get a single parameter's current value and metadata
|
|
510
|
+
*/
|
|
511
|
+
async getParameter(id) {
|
|
512
|
+
return this.bridge.invoke(METHOD_GET_PARAMETER, { id });
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Set a parameter's value
|
|
516
|
+
* @param id Parameter ID
|
|
517
|
+
* @param value Normalized value [0.0, 1.0]
|
|
518
|
+
*/
|
|
519
|
+
async setParameter(id, value) {
|
|
520
|
+
await this.bridge.invoke(METHOD_SET_PARAMETER, {
|
|
521
|
+
id,
|
|
522
|
+
value
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get all parameters with their current values and metadata
|
|
527
|
+
*/
|
|
528
|
+
async getAllParameters() {
|
|
529
|
+
const result = await this.bridge.invoke(METHOD_GET_ALL_PARAMETERS);
|
|
530
|
+
return result.parameters;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Test connectivity with Rust backend
|
|
534
|
+
* @returns Roundtrip time in milliseconds
|
|
535
|
+
*/
|
|
536
|
+
async ping() {
|
|
537
|
+
const start = performance.now();
|
|
538
|
+
await this.bridge.invoke("ping");
|
|
539
|
+
const end = performance.now();
|
|
540
|
+
return end - start;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Subscribe to parameter change notifications
|
|
544
|
+
* @returns Unsubscribe function
|
|
545
|
+
*/
|
|
546
|
+
onParameterChanged(callback) {
|
|
547
|
+
return this.bridge.on(NOTIFICATION_PARAMETER_CHANGED, (data) => {
|
|
548
|
+
if (data && typeof data === "object" && "id" in data && "value" in data) {
|
|
549
|
+
callback(data.id, data.value);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
_ParameterClient.instance = null;
|
|
555
|
+
let ParameterClient = _ParameterClient;
|
|
556
|
+
let client = null;
|
|
557
|
+
function getClient() {
|
|
558
|
+
client ?? (client = ParameterClient.getInstance());
|
|
559
|
+
return client;
|
|
560
|
+
}
|
|
561
|
+
function useParameter(id) {
|
|
562
|
+
const [param, setParam] = useState(null);
|
|
563
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
564
|
+
const [error, setError] = useState(null);
|
|
565
|
+
useEffect(() => {
|
|
566
|
+
let isMounted = true;
|
|
567
|
+
async function loadParameter() {
|
|
568
|
+
try {
|
|
569
|
+
setIsLoading(true);
|
|
570
|
+
setError(null);
|
|
571
|
+
const allParams = await getClient().getAllParameters();
|
|
572
|
+
const foundParam = allParams.find((p) => p.id === id);
|
|
573
|
+
if (isMounted) {
|
|
574
|
+
if (foundParam) {
|
|
575
|
+
setParam(foundParam);
|
|
576
|
+
} else {
|
|
577
|
+
setError(new Error(`Parameter not found: ${id}`));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
if (isMounted) {
|
|
582
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
583
|
+
}
|
|
584
|
+
} finally {
|
|
585
|
+
if (isMounted) {
|
|
586
|
+
setIsLoading(false);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
loadParameter();
|
|
591
|
+
return () => {
|
|
592
|
+
isMounted = false;
|
|
593
|
+
};
|
|
594
|
+
}, [id]);
|
|
595
|
+
useEffect(() => {
|
|
596
|
+
const unsubscribe = getClient().onParameterChanged((changedId, value) => {
|
|
597
|
+
if (changedId === id) {
|
|
598
|
+
setParam((prev) => prev ? { ...prev, value } : null);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
return unsubscribe;
|
|
602
|
+
}, [id]);
|
|
603
|
+
const setValue = useCallback(
|
|
604
|
+
async (value) => {
|
|
605
|
+
try {
|
|
606
|
+
await getClient().setParameter(id, value);
|
|
607
|
+
setParam((prev) => prev ? { ...prev, value } : null);
|
|
608
|
+
} catch (err) {
|
|
609
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
610
|
+
throw err;
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
[id]
|
|
614
|
+
);
|
|
615
|
+
return { param, setValue, isLoading, error };
|
|
616
|
+
}
|
|
617
|
+
function useAllParameters() {
|
|
618
|
+
const [params, setParams] = useState([]);
|
|
619
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
620
|
+
const [error, setError] = useState(null);
|
|
621
|
+
const reload = useCallback(async () => {
|
|
622
|
+
try {
|
|
623
|
+
setIsLoading(true);
|
|
624
|
+
setError(null);
|
|
625
|
+
const allParams = await getClient().getAllParameters();
|
|
626
|
+
setParams(allParams);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
629
|
+
} finally {
|
|
630
|
+
setIsLoading(false);
|
|
631
|
+
}
|
|
632
|
+
}, []);
|
|
633
|
+
useEffect(() => {
|
|
634
|
+
reload();
|
|
635
|
+
}, [reload]);
|
|
636
|
+
useEffect(() => {
|
|
637
|
+
const handleParamChange = (changedId, value) => {
|
|
638
|
+
setParams((prev) => prev.map((p) => p.id === changedId ? { ...p, value } : p));
|
|
639
|
+
};
|
|
640
|
+
const unsubscribe = getClient().onParameterChanged(handleParamChange);
|
|
641
|
+
return unsubscribe;
|
|
642
|
+
}, []);
|
|
643
|
+
return { params, isLoading, error, reload };
|
|
644
|
+
}
|
|
645
|
+
function useLatencyMonitor(intervalMs = 1e3) {
|
|
646
|
+
const [latency, setLatency] = useState(null);
|
|
647
|
+
const [measurements, setMeasurements] = useState([]);
|
|
648
|
+
const bridge = IpcBridge.getInstance();
|
|
649
|
+
useEffect(() => {
|
|
650
|
+
let isMounted = true;
|
|
651
|
+
async function measure() {
|
|
652
|
+
if (!bridge.isConnected()) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const ms = await getClient().ping();
|
|
657
|
+
if (isMounted) {
|
|
658
|
+
setLatency(ms);
|
|
659
|
+
setMeasurements((prev) => [...prev.slice(-99), ms]);
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
logger.debug("Ping failed", { error: err });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
measure();
|
|
666
|
+
const intervalId = setInterval(measure, intervalMs);
|
|
667
|
+
return () => {
|
|
668
|
+
isMounted = false;
|
|
669
|
+
clearInterval(intervalId);
|
|
670
|
+
};
|
|
671
|
+
}, [intervalMs, bridge]);
|
|
672
|
+
const avg = measurements.length > 0 ? measurements.reduce((sum, val) => sum + val, 0) / measurements.length : 0;
|
|
673
|
+
const max = measurements.length > 0 ? Math.max(...measurements) : 0;
|
|
674
|
+
return {
|
|
675
|
+
latency,
|
|
676
|
+
avg,
|
|
677
|
+
max,
|
|
678
|
+
count: measurements.length
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function useParameterGroups(parameters) {
|
|
682
|
+
return useMemo(() => {
|
|
683
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
684
|
+
for (const param of parameters) {
|
|
685
|
+
const groupName = param.group ?? "Parameters";
|
|
686
|
+
const existing = grouped.get(groupName) ?? [];
|
|
687
|
+
existing.push(param);
|
|
688
|
+
grouped.set(groupName, existing);
|
|
689
|
+
}
|
|
690
|
+
const groups = Array.from(grouped.entries()).map(([name, parameters2]) => ({ name, parameters: parameters2 })).sort((a, b) => {
|
|
691
|
+
if (a.name === "Parameters") return -1;
|
|
692
|
+
if (b.name === "Parameters") return 1;
|
|
693
|
+
return a.name.localeCompare(b.name);
|
|
694
|
+
});
|
|
695
|
+
return groups;
|
|
696
|
+
}, [parameters]);
|
|
697
|
+
}
|
|
698
|
+
function useConnectionStatus() {
|
|
699
|
+
const [status, setStatus] = useState(() => {
|
|
700
|
+
const bridge = IpcBridge.getInstance();
|
|
701
|
+
const connected = bridge.isConnected();
|
|
702
|
+
let transport;
|
|
703
|
+
if (isWebViewEnvironment()) {
|
|
704
|
+
transport = "native";
|
|
705
|
+
} else if (connected) {
|
|
706
|
+
transport = "websocket";
|
|
707
|
+
} else {
|
|
708
|
+
transport = "none";
|
|
709
|
+
}
|
|
710
|
+
return { connected, transport };
|
|
711
|
+
});
|
|
712
|
+
useEffect(() => {
|
|
713
|
+
const bridge = IpcBridge.getInstance();
|
|
714
|
+
const intervalId = setInterval(() => {
|
|
715
|
+
const connected = bridge.isConnected();
|
|
716
|
+
let transport;
|
|
717
|
+
if (isWebViewEnvironment()) {
|
|
718
|
+
transport = "native";
|
|
719
|
+
} else if (connected) {
|
|
720
|
+
transport = "websocket";
|
|
721
|
+
} else {
|
|
722
|
+
transport = "none";
|
|
723
|
+
}
|
|
724
|
+
setStatus((prevStatus) => {
|
|
725
|
+
if (prevStatus.connected !== connected || prevStatus.transport !== transport) {
|
|
726
|
+
return { connected, transport };
|
|
727
|
+
}
|
|
728
|
+
return prevStatus;
|
|
729
|
+
});
|
|
730
|
+
}, 1e3);
|
|
731
|
+
return () => {
|
|
732
|
+
clearInterval(intervalId);
|
|
733
|
+
};
|
|
734
|
+
}, []);
|
|
735
|
+
return status;
|
|
736
|
+
}
|
|
737
|
+
async function requestResize(width, height) {
|
|
738
|
+
const bridge = IpcBridge.getInstance();
|
|
739
|
+
const result = await bridge.invoke("requestResize", { width, height });
|
|
740
|
+
return result.accepted;
|
|
741
|
+
}
|
|
742
|
+
function useRequestResize() {
|
|
743
|
+
return requestResize;
|
|
744
|
+
}
|
|
745
|
+
async function getMeterFrame() {
|
|
746
|
+
const bridge = IpcBridge.getInstance();
|
|
747
|
+
const result = await bridge.invoke("getMeterFrame");
|
|
748
|
+
return result.frame;
|
|
749
|
+
}
|
|
750
|
+
export {
|
|
751
|
+
ERROR_INTERNAL,
|
|
752
|
+
ERROR_INVALID_PARAMS,
|
|
753
|
+
ERROR_INVALID_REQUEST,
|
|
754
|
+
ERROR_METHOD_NOT_FOUND,
|
|
755
|
+
ERROR_PARAM_NOT_FOUND,
|
|
756
|
+
ERROR_PARAM_OUT_OF_RANGE,
|
|
757
|
+
ERROR_PARSE,
|
|
758
|
+
IpcBridge,
|
|
759
|
+
LogLevel,
|
|
760
|
+
Logger,
|
|
761
|
+
METHOD_GET_ALL_PARAMETERS,
|
|
762
|
+
METHOD_GET_PARAMETER,
|
|
763
|
+
METHOD_SET_PARAMETER,
|
|
764
|
+
NOTIFICATION_PARAMETER_CHANGED,
|
|
765
|
+
NativeTransport,
|
|
766
|
+
ParameterClient,
|
|
767
|
+
WebSocketTransport,
|
|
768
|
+
dbToLinear,
|
|
769
|
+
getMeterFrame,
|
|
770
|
+
isBrowserEnvironment,
|
|
771
|
+
isWebViewEnvironment,
|
|
772
|
+
linearToDb,
|
|
773
|
+
logger,
|
|
774
|
+
requestResize,
|
|
775
|
+
useAllParameters,
|
|
776
|
+
useConnectionStatus,
|
|
777
|
+
useLatencyMonitor,
|
|
778
|
+
useParameter,
|
|
779
|
+
useParameterGroups,
|
|
780
|
+
useRequestResize
|
|
781
|
+
};
|
|
782
|
+
//# sourceMappingURL=index.js.map
|