@dxfeed/dxlink-console-rpc 0.0.0-stage → 0.10.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.js ADDED
@@ -0,0 +1,806 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ rpcChannelPlugin: () => rpcChannelPlugin
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/plugin.tsx
38
+ var import_dxlink_console_core5 = require("@dxfeed/dxlink-console-core");
39
+ var import_SwapHoriz2 = __toESM(require("@mui/icons-material/SwapHoriz"));
40
+
41
+ // src/descriptors.ts
42
+ var import_protobuf = require("@bufbuild/protobuf");
43
+ var import_reflect = require("@bufbuild/protobuf/reflect");
44
+ var import_wkt = require("@bufbuild/protobuf/wkt");
45
+ var METHOD_MODELS = {
46
+ unary: {
47
+ label: "REQUEST_RESPONSE",
48
+ supported: true,
49
+ description: "One request, one response."
50
+ },
51
+ server_streaming: {
52
+ label: "REQUEST_STREAM",
53
+ supported: true,
54
+ description: "One request, a stream of responses."
55
+ },
56
+ bidi_streaming: {
57
+ label: "STREAM_STREAM",
58
+ supported: true,
59
+ description: "A stream of requests, a stream of responses."
60
+ },
61
+ client_streaming: {
62
+ label: "STREAM_RESPONSE",
63
+ supported: false,
64
+ description: "Not supported \u2014 the dxLink v1.0 wire has no graceful request half-close, so the server is never told the requests ended."
65
+ }
66
+ };
67
+ var methodModel = (method) => METHOD_MODELS[method.methodKind];
68
+ var isMethodSupported = (method) => methodModel(method).supported;
69
+ var decodeDescriptorSet = (bytes) => {
70
+ const firstByte = bytes.find((byte) => byte > 32);
71
+ if (firstByte === 123) {
72
+ return (0, import_protobuf.fromJson)(import_wkt.FileDescriptorSetSchema, JSON.parse(new TextDecoder().decode(bytes)));
73
+ }
74
+ return (0, import_protobuf.fromBinary)(import_wkt.FileDescriptorSetSchema, bytes);
75
+ };
76
+ var deduceMissingJsonNames = (registry) => {
77
+ for (const desc of registry) {
78
+ if (desc.kind !== "message") continue;
79
+ for (const field of desc.fields) {
80
+ if (field.jsonName === "") {
81
+ ;
82
+ field.jsonName = (0, import_reflect.protoCamelCase)(field.name);
83
+ }
84
+ }
85
+ }
86
+ return registry;
87
+ };
88
+ var parseDescriptorSet = (bytes) => deduceMissingJsonNames((0, import_protobuf.createFileRegistry)(decodeDescriptorSet(bytes)));
89
+ var BINARY_ACCEPT = "application/protobuf, application/x-protobuf, application/octet-stream";
90
+ var fetchDescriptorSet = async (url) => {
91
+ let response;
92
+ try {
93
+ response = await fetch(url, { headers: { Accept: BINARY_ACCEPT } });
94
+ } catch (error) {
95
+ throw new Error(
96
+ "the endpoint must be reachable, and one on another origin must allow cross-origin requests including the Accept header",
97
+ { cause: error }
98
+ );
99
+ }
100
+ if (!response.ok) {
101
+ throw new Error(`${response.status} ${response.statusText}`);
102
+ }
103
+ return parseDescriptorSet(new Uint8Array(await response.arrayBuffer()));
104
+ };
105
+ var readDescriptorSet = async (file) => parseDescriptorSet(new Uint8Array(await file.arrayBuffer()));
106
+ var listServices = (registry) => {
107
+ const services = [];
108
+ for (const desc of registry) {
109
+ if (desc.kind === "service") {
110
+ services.push(desc);
111
+ }
112
+ }
113
+ return services.sort((a, b) => a.typeName.localeCompare(b.typeName));
114
+ };
115
+ var fieldTemplate = (field) => {
116
+ switch (field.fieldKind) {
117
+ case "list":
118
+ return [];
119
+ case "map":
120
+ return {};
121
+ case "message":
122
+ return {};
123
+ case "enum":
124
+ return field.enum.values[0]?.name ?? 0;
125
+ case "scalar":
126
+ switch (field.scalar) {
127
+ case import_protobuf.ScalarType.BOOL:
128
+ return false;
129
+ case import_protobuf.ScalarType.STRING:
130
+ case import_protobuf.ScalarType.BYTES:
131
+ return "";
132
+ case import_protobuf.ScalarType.INT64:
133
+ case import_protobuf.ScalarType.UINT64:
134
+ case import_protobuf.ScalarType.FIXED64:
135
+ case import_protobuf.ScalarType.SFIXED64:
136
+ case import_protobuf.ScalarType.SINT64:
137
+ return "0";
138
+ default:
139
+ return 0;
140
+ }
141
+ }
142
+ };
143
+ var createRequestTemplate = (message) => Object.fromEntries(message.fields.map((field) => [field.jsonName, fieldTemplate(field)]));
144
+ var parseRequest = (message, json) => {
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(json);
148
+ } catch (error) {
149
+ return { error: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}` };
150
+ }
151
+ try {
152
+ return { message: (0, import_protobuf.fromJson)(message, parsed) };
153
+ } catch (error) {
154
+ return { error: error instanceof Error ? error.message : String(error) };
155
+ }
156
+ };
157
+ var formatMessage = (schema, message) => (0, import_protobuf.toJson)(schema, message);
158
+
159
+ // src/rpc-channel.tsx
160
+ var import_dxlink_console_core2 = require("@dxfeed/dxlink-console-core");
161
+ var import_dxlink_console_core3 = require("@dxfeed/dxlink-console-core");
162
+ var import_dxlink_console_core4 = require("@dxfeed/dxlink-console-core");
163
+ var import_Send = __toESM(require("@mui/icons-material/Send"));
164
+ var import_SwapHoriz = __toESM(require("@mui/icons-material/SwapHoriz"));
165
+ var import_Alert = __toESM(require("@mui/material/Alert"));
166
+ var import_Box = __toESM(require("@mui/material/Box"));
167
+ var import_Button = __toESM(require("@mui/material/Button"));
168
+ var import_Chip = __toESM(require("@mui/material/Chip"));
169
+ var import_Paper = __toESM(require("@mui/material/Paper"));
170
+ var import_Stack = __toESM(require("@mui/material/Stack"));
171
+ var import_TextField = __toESM(require("@mui/material/TextField"));
172
+ var import_Typography = __toESM(require("@mui/material/Typography"));
173
+ var import_react = require("react");
174
+
175
+ // src/rpc-view-model.ts
176
+ var import_dxlink_api = require("@dxfeed/dxlink-api");
177
+ var import_dxlink_console_core = require("@dxfeed/dxlink-console-core");
178
+ var import_dxlink_protobuf_es = require("@dxfeed/dxlink-protobuf-es");
179
+ var import_rxjs = require("rxjs");
180
+ var import_vanilla = require("zustand/vanilla");
181
+ var FLUSH_INTERVAL_MS = 100;
182
+ var MAX_RESPONSES = 200;
183
+ var lastEntryId = 0;
184
+ var entry = (json) => {
185
+ lastEntryId += 1;
186
+ return { id: lastEntryId, time: (/* @__PURE__ */ new Date()).toLocaleTimeString(), json };
187
+ };
188
+ var toDXLinkError = (error) => {
189
+ if (typeof error === "object" && error !== null && "type" in error && "message" in error && typeof error.message === "string") {
190
+ return error;
191
+ }
192
+ return { type: "UNKNOWN", message: error instanceof Error ? error.message : String(error) };
193
+ };
194
+ var RpcViewModel = class {
195
+ constructor(client, params) {
196
+ this.store = (0, import_vanilla.createStore)(() => ({
197
+ callState: "active",
198
+ responses: [],
199
+ requests: [],
200
+ errors: []
201
+ }));
202
+ this.subscription = null;
203
+ /** Request stream for bidirectional methods; null for the other models. */
204
+ this.input = null;
205
+ this.pending = [];
206
+ this.flushHandle = null;
207
+ this.start = () => {
208
+ if (this.subscription !== null) return;
209
+ this.store.setState({ callState: "active", responses: [], requests: [], errors: [] });
210
+ let responses;
211
+ try {
212
+ const service = (0, import_dxlink_protobuf_es.createDXLinkDynamicService)(this.client, this.service, {
213
+ // A descriptor chosen at runtime may declare methods the wire cannot carry; the
214
+ // picker never offers them, and the rest of the service stays callable.
215
+ skipUnsupportedMethods: true,
216
+ // A debug console wants the protocol traffic in the browser log.
217
+ logLevel: import_dxlink_api.DXLinkLogLevel.DEBUG
218
+ });
219
+ const call = service[this.method.localName];
220
+ if (call === void 0) {
221
+ throw new Error(`${this.service.typeName} does not expose ${this.method.name}`);
222
+ }
223
+ if (this.isBidirectional) {
224
+ this.input = new import_rxjs.ReplaySubject();
225
+ responses = call(this.input);
226
+ this.emit(this.request);
227
+ } else {
228
+ responses = call(this.request);
229
+ this.record("requests", formatMessage(this.method.input, this.request));
230
+ }
231
+ } catch (error) {
232
+ this.fail(error);
233
+ return;
234
+ }
235
+ this.subscription = responses.subscribe({
236
+ next: (message) => this.handleResponse(message),
237
+ error: (error) => this.fail(error),
238
+ complete: () => this.store.setState({ callState: "completed" })
239
+ });
240
+ };
241
+ this.stop = () => {
242
+ if (this.flushHandle !== null) {
243
+ clearTimeout(this.flushHandle);
244
+ this.flushHandle = null;
245
+ }
246
+ this.pending = [];
247
+ this.input?.complete();
248
+ this.input = null;
249
+ this.subscription?.unsubscribe();
250
+ this.subscription = null;
251
+ };
252
+ /**
253
+ * Send another request on a bidirectional call. Rejected messages are reported as errors
254
+ * on this channel rather than thrown, so a typo in the editor cannot take the card down.
255
+ */
256
+ this.send = (message) => {
257
+ if (!this.isBidirectional || this.input === null) return;
258
+ this.emit(message);
259
+ };
260
+ this.clearErrors = () => {
261
+ this.store.setState({ errors: [] });
262
+ };
263
+ this.close = () => {
264
+ this.stop();
265
+ };
266
+ this.dispose = () => {
267
+ this.stop();
268
+ };
269
+ this.emit = (message) => {
270
+ try {
271
+ this.input?.next(message);
272
+ this.record("requests", formatMessage(this.method.input, message));
273
+ } catch (error) {
274
+ this.fail(error);
275
+ }
276
+ };
277
+ this.fail = (error) => {
278
+ this.store.setState((state) => ({
279
+ callState: "failed",
280
+ errors: (0, import_dxlink_console_core.prependError)(state.errors, toDXLinkError(error))
281
+ }));
282
+ };
283
+ this.handleResponse = (message) => {
284
+ let json;
285
+ try {
286
+ json = formatMessage(this.method.output, message);
287
+ } catch (error) {
288
+ this.fail(error);
289
+ return;
290
+ }
291
+ this.pending = [entry(json), ...this.pending];
292
+ if (this.flushHandle === null) {
293
+ this.flushHandle = setTimeout(this.flush, FLUSH_INTERVAL_MS);
294
+ }
295
+ };
296
+ this.flush = () => {
297
+ this.flushHandle = null;
298
+ const pending = this.pending;
299
+ if (pending.length === 0) return;
300
+ this.pending = [];
301
+ this.store.setState((state) => ({
302
+ responses: [...pending, ...state.responses].slice(0, MAX_RESPONSES)
303
+ }));
304
+ };
305
+ this.record = (key, json) => {
306
+ this.store.setState((state) => ({
307
+ [key]: [entry(json), ...state[key]].slice(0, MAX_RESPONSES)
308
+ }));
309
+ };
310
+ this.client = client;
311
+ this.service = params.service;
312
+ this.method = params.method;
313
+ this.request = params.request;
314
+ }
315
+ get isBidirectional() {
316
+ return this.method.methodKind === "bidi_streaming";
317
+ }
318
+ };
319
+
320
+ // src/rpc-channel.tsx
321
+ var import_jsx_runtime = require("react/jsx-runtime");
322
+ var RpcStatusChip = ({ state, responses }) => {
323
+ if (state === "failed") {
324
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Chip.default, { size: "small", color: "error", variant: "outlined", label: "failed" });
325
+ }
326
+ if (state === "completed") {
327
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Chip.default, { size: "small", variant: "outlined", label: "completed" });
328
+ }
329
+ return responses === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Chip.default, { size: "small", color: "warning", variant: "outlined", label: "waiting" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Chip.default, { size: "small", color: "success", variant: "outlined", label: "streaming" });
330
+ };
331
+ var MessageEntry = ({ entry: entry2 }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_Paper.default, { variant: "outlined", sx: { p: 1.5 }, children: [
332
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Typography.default, { variant: "caption", color: "text.secondary", children: entry2.time }),
333
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
334
+ import_Box.default,
335
+ {
336
+ component: "pre",
337
+ sx: { m: 0, mt: 0.5, fontSize: 13, whiteSpace: "pre-wrap", wordBreak: "break-word" },
338
+ children: JSON.stringify(entry2.json, null, 2)
339
+ }
340
+ )
341
+ ] });
342
+ var MessageList = ({
343
+ title,
344
+ entries,
345
+ empty
346
+ }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_Stack.default, { spacing: 1, children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_Stack.default, { direction: "row", spacing: 1, sx: { alignItems: "baseline" }, children: [
348
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Typography.default, { variant: "subtitle2", children: title }),
349
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Typography.default, { variant: "caption", color: "text.secondary", children: entries.length })
350
+ ] }),
351
+ entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Typography.default, { variant: "body2", color: "text.secondary", children: empty }) : entries.map((entry2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageEntry, { entry: entry2 }, entry2.id))
352
+ ] });
353
+ var SendRequest = ({
354
+ vm,
355
+ config,
356
+ active
357
+ }) => {
358
+ const [json, setJson] = (0, import_react.useState)(
359
+ () => JSON.stringify(createRequestTemplate(config.method.input), null, 2)
360
+ );
361
+ const parsed = parseRequest(config.method.input, json);
362
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_Stack.default, { spacing: 1, children: [
363
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Typography.default, { variant: "subtitle2", children: "Send another request" }),
364
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
365
+ import_TextField.default,
366
+ {
367
+ value: json,
368
+ onChange: (e) => setJson(e.target.value),
369
+ multiline: true,
370
+ minRows: 4,
371
+ fullWidth: true,
372
+ size: "small",
373
+ slotProps: { input: { sx: { fontFamily: "monospace", fontSize: 13 } } }
374
+ }
375
+ ),
376
+ "error" in parsed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Alert.default, { severity: "error", children: parsed.error }),
377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Box.default, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
378
+ import_Button.default,
379
+ {
380
+ variant: "contained",
381
+ startIcon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Send.default, {}),
382
+ disabled: !active || "error" in parsed,
383
+ onClick: () => {
384
+ if ("message" in parsed) vm.send(parsed.message);
385
+ },
386
+ children: "Send"
387
+ }
388
+ ) })
389
+ ] });
390
+ };
391
+ var RpcChannel = ({ title, config }) => {
392
+ const connectionVM = (0, import_dxlink_console_core4.useConnectionVM)();
393
+ const [vm] = (0, import_react.useState)(() => {
394
+ const client = connectionVM.getClient();
395
+ if (client === null) {
396
+ throw new Error("RPC channel opened without an active connection");
397
+ }
398
+ return new RpcViewModel(client, {
399
+ service: config.service,
400
+ method: config.method,
401
+ request: config.request
402
+ });
403
+ });
404
+ (0, import_react.useEffect)(() => {
405
+ vm.start();
406
+ return () => vm.stop();
407
+ }, [vm]);
408
+ const callState = (0, import_dxlink_console_core2.useVM)(vm, (s) => s.callState);
409
+ const responses = (0, import_dxlink_console_core2.useVM)(vm, (s) => s.responses);
410
+ const requests = (0, import_dxlink_console_core2.useVM)(vm, (s) => s.requests);
411
+ const errors = (0, import_dxlink_console_core2.useVM)(vm, (s) => s.errors);
412
+ const model = methodModel(config.method);
413
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
414
+ import_dxlink_console_core3.ChannelWidget,
415
+ {
416
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_SwapHoriz.default, {}),
417
+ title,
418
+ subtitle: `${config.service.typeName} \xB7 ${config.method.name}`,
419
+ onClose: vm.close,
420
+ status: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RpcStatusChip, { state: callState, responses: responses.length }),
421
+ parameters: { service: config.service.typeName, methodName: config.method.name },
422
+ errors,
423
+ onClearErrors: vm.clearErrors,
424
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_Stack.default, { spacing: 2, children: [
425
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_Stack.default, { direction: "row", spacing: 0.5, useFlexGap: true, sx: { flexWrap: "wrap" }, children: [
426
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Chip.default, { size: "small", variant: "outlined", label: model.label }),
427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Chip.default, { size: "small", variant: "outlined", label: `in: ${config.method.input.typeName}` }),
428
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Chip.default, { size: "small", variant: "outlined", label: `out: ${config.method.output.typeName}` })
429
+ ] }),
430
+ vm.isBidirectional && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SendRequest, { vm, config, active: callState === "active" }),
431
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageList, { title: "Sent", entries: requests, empty: "Nothing sent yet." }),
432
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
433
+ MessageList,
434
+ {
435
+ title: "Received",
436
+ entries: responses,
437
+ empty: callState === "completed" ? "The server closed the channel without a response." : "Waiting for the first response\u2026"
438
+ }
439
+ )
440
+ ] })
441
+ }
442
+ );
443
+ };
444
+
445
+ // src/rpc-channel-request.tsx
446
+ var import_protobuf2 = require("@bufbuild/protobuf");
447
+ var import_ExpandMore = __toESM(require("@mui/icons-material/ExpandMore"));
448
+ var import_UploadFile = __toESM(require("@mui/icons-material/UploadFile"));
449
+ var import_Accordion = __toESM(require("@mui/material/Accordion"));
450
+ var import_AccordionDetails = __toESM(require("@mui/material/AccordionDetails"));
451
+ var import_AccordionSummary = __toESM(require("@mui/material/AccordionSummary"));
452
+ var import_Alert2 = __toESM(require("@mui/material/Alert"));
453
+ var import_Box2 = __toESM(require("@mui/material/Box"));
454
+ var import_Button2 = __toESM(require("@mui/material/Button"));
455
+ var import_Chip2 = __toESM(require("@mui/material/Chip"));
456
+ var import_FormControlLabel = __toESM(require("@mui/material/FormControlLabel"));
457
+ var import_MenuItem = __toESM(require("@mui/material/MenuItem"));
458
+ var import_Stack2 = __toESM(require("@mui/material/Stack"));
459
+ var import_Switch = __toESM(require("@mui/material/Switch"));
460
+ var import_TextField2 = __toESM(require("@mui/material/TextField"));
461
+ var import_Typography2 = __toESM(require("@mui/material/Typography"));
462
+ var import_react2 = require("react");
463
+ var import_jsx_runtime2 = require("react/jsx-runtime");
464
+ var stringify = (json) => JSON.stringify(json, null, 2);
465
+ var isEditableField = (field) => (field.fieldKind === "scalar" || field.fieldKind === "enum") && field.oneof === void 0;
466
+ var STRING_SCALARS = /* @__PURE__ */ new Set([
467
+ import_protobuf2.ScalarType.STRING,
468
+ import_protobuf2.ScalarType.BYTES,
469
+ import_protobuf2.ScalarType.INT64,
470
+ import_protobuf2.ScalarType.UINT64,
471
+ import_protobuf2.ScalarType.FIXED64,
472
+ import_protobuf2.ScalarType.SFIXED64,
473
+ import_protobuf2.ScalarType.SINT64
474
+ ]);
475
+ var scalarHelp = (scalar) => {
476
+ if (scalar === import_protobuf2.ScalarType.BYTES) return "bytes \u2014 base64";
477
+ if (STRING_SCALARS.has(scalar) && scalar !== import_protobuf2.ScalarType.STRING) return "64-bit \u2014 sent as a string";
478
+ return void 0;
479
+ };
480
+ var RequestFields = ({
481
+ message,
482
+ json,
483
+ onChange
484
+ }) => {
485
+ const parsed = (0, import_react2.useMemo)(() => {
486
+ try {
487
+ const value = JSON.parse(json);
488
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
489
+ } catch {
490
+ return null;
491
+ }
492
+ }, [json]);
493
+ const fields = message.fields.filter(isEditableField);
494
+ if (fields.length === 0) {
495
+ return null;
496
+ }
497
+ if (parsed === null) {
498
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Typography2.default, { variant: "body2", color: "text.secondary", children: "Fields are editable again once the request below is a JSON object." });
499
+ }
500
+ const set = (field, fieldValue) => onChange(stringify({ ...parsed, [field.jsonName]: fieldValue }));
501
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Box2.default, { sx: { display: "grid", gap: 2, gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" } }, children: fields.map((field) => {
502
+ const current = parsed[field.jsonName];
503
+ if (field.fieldKind === "enum") {
504
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
505
+ import_TextField2.default,
506
+ {
507
+ select: true,
508
+ label: field.name,
509
+ value: typeof current === "string" ? current : "",
510
+ onChange: (e) => set(field, e.target.value),
511
+ size: "small",
512
+ helperText: field.enum.typeName,
513
+ children: field.enum.values.map((enumValue) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_MenuItem.default, { value: enumValue.name, children: enumValue.name }, enumValue.number))
514
+ },
515
+ field.number
516
+ );
517
+ }
518
+ if (field.fieldKind !== "scalar") {
519
+ return null;
520
+ }
521
+ if (field.scalar === import_protobuf2.ScalarType.BOOL) {
522
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
523
+ import_FormControlLabel.default,
524
+ {
525
+ control: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
526
+ import_Switch.default,
527
+ {
528
+ checked: current === true,
529
+ onChange: (e) => set(field, e.target.checked),
530
+ size: "small"
531
+ }
532
+ ),
533
+ label: field.name
534
+ },
535
+ field.number
536
+ );
537
+ }
538
+ if (STRING_SCALARS.has(field.scalar)) {
539
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
540
+ import_TextField2.default,
541
+ {
542
+ label: field.name,
543
+ value: typeof current === "string" ? current : "",
544
+ onChange: (e) => set(field, e.target.value),
545
+ size: "small",
546
+ helperText: scalarHelp(field.scalar)
547
+ },
548
+ field.number
549
+ );
550
+ }
551
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
552
+ import_TextField2.default,
553
+ {
554
+ label: field.name,
555
+ type: "number",
556
+ value: typeof current === "number" ? String(current) : "",
557
+ onChange: (e) => set(field, e.target.value === "" ? 0 : Number(e.target.value)),
558
+ size: "small"
559
+ },
560
+ field.number
561
+ );
562
+ }) });
563
+ };
564
+ var RpcChannelRequest = ({ value, onChange, urlLocked }) => {
565
+ const [loading, setLoading] = (0, import_react2.useState)(false);
566
+ const [loadError, setLoadError] = (0, import_react2.useState)(null);
567
+ const fileInput = (0, import_react2.useRef)(null);
568
+ const services = (0, import_react2.useMemo)(
569
+ () => value.registry === null ? [] : listServices(value.registry),
570
+ [value.registry]
571
+ );
572
+ const service = services.find((s) => s.typeName === value.serviceName);
573
+ const method = service?.method[value.methodName];
574
+ const parsedRequest = (0, import_react2.useMemo)(
575
+ () => method === void 0 ? null : parseRequest(method.input, value.json),
576
+ [method, value.json]
577
+ );
578
+ const selectMethod = (next, patch = {}) => onChange({
579
+ ...value,
580
+ ...patch,
581
+ methodName: next?.localName ?? "",
582
+ json: next === void 0 ? "{}" : stringify(createRequestTemplate(next.input))
583
+ });
584
+ const selectService = (next) => selectMethod(next?.methods.find(isMethodSupported), { serviceName: next?.typeName ?? "" });
585
+ const load = async (source, loader) => {
586
+ setLoading(true);
587
+ setLoadError(null);
588
+ try {
589
+ await loader();
590
+ } catch (error) {
591
+ setLoadError(
592
+ `Could not load definitions from ${source}: ${error instanceof Error ? error.message : String(error)}`
593
+ );
594
+ } finally {
595
+ setLoading(false);
596
+ }
597
+ };
598
+ const applyRegistry = (registry, source) => {
599
+ const firstService = listServices(registry)[0];
600
+ const firstMethod = firstService?.methods.find(isMethodSupported);
601
+ onChange({
602
+ ...value,
603
+ registry,
604
+ source,
605
+ serviceName: firstService?.typeName ?? "",
606
+ methodName: firstMethod?.localName ?? "",
607
+ json: firstMethod === void 0 ? "{}" : stringify(createRequestTemplate(firstMethod.input))
608
+ });
609
+ };
610
+ const loadUrl = () => load(value.url, async () => {
611
+ applyRegistry(await fetchDescriptorSet(value.url), value.url);
612
+ });
613
+ const loadFile = (file) => load(file.name, async () => {
614
+ applyRegistry(await readDescriptorSet(file), file.name);
615
+ });
616
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Stack2.default, { spacing: 2.5, sx: { pt: 1 }, children: [
617
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Stack2.default, { spacing: 1.5, children: [
618
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Typography2.default, { variant: "subtitle2", children: "Service definitions" }),
619
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Stack2.default, { direction: "row", spacing: 1, sx: { alignItems: "flex-start" }, children: [
620
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
621
+ import_TextField2.default,
622
+ {
623
+ label: "Descriptor set URL",
624
+ value: value.url,
625
+ onChange: (e) => onChange({ ...value, url: e.target.value }),
626
+ disabled: urlLocked,
627
+ size: "small",
628
+ fullWidth: true,
629
+ helperText: urlLocked ? "Fixed by this deployment." : "An endpoint serving a FileDescriptorSet, binary or protobuf-JSON."
630
+ }
631
+ ),
632
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
633
+ import_Button2.default,
634
+ {
635
+ variant: "outlined",
636
+ onClick: () => void loadUrl(),
637
+ disabled: loading || value.url.trim() === "",
638
+ sx: { mt: 0.25 },
639
+ children: "Load"
640
+ }
641
+ )
642
+ ] }),
643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Box2.default, { children: [
644
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
645
+ import_Button2.default,
646
+ {
647
+ size: "small",
648
+ startIcon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_UploadFile.default, {}),
649
+ onClick: () => fileInput.current?.click(),
650
+ disabled: loading,
651
+ children: "Load from file"
652
+ }
653
+ ),
654
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
655
+ "input",
656
+ {
657
+ ref: fileInput,
658
+ type: "file",
659
+ hidden: true,
660
+ accept: ".binpb,.bin,.pb,.json",
661
+ onChange: (e) => {
662
+ const file = e.target.files?.[0];
663
+ e.target.value = "";
664
+ if (file !== void 0) void loadFile(file);
665
+ }
666
+ }
667
+ )
668
+ ] }),
669
+ loadError !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Alert2.default, { severity: "error", children: loadError }),
670
+ value.registry !== null && loadError === null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Typography2.default, { variant: "caption", color: "text.secondary", children: [
671
+ services.length,
672
+ " service",
673
+ services.length === 1 ? "" : "s",
674
+ " from ",
675
+ value.source
676
+ ] })
677
+ ] }),
678
+ value.registry !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
679
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
680
+ import_TextField2.default,
681
+ {
682
+ select: true,
683
+ label: "Service",
684
+ value: service?.typeName ?? "",
685
+ onChange: (e) => selectService(services.find((s) => s.typeName === e.target.value)),
686
+ size: "small",
687
+ fullWidth: true,
688
+ disabled: services.length === 0,
689
+ helperText: services.length === 0 ? "The descriptor set declares no services." : " ",
690
+ children: services.map((option) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_MenuItem.default, { value: option.typeName, children: option.typeName }, option.typeName))
691
+ }
692
+ ),
693
+ service !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
694
+ import_TextField2.default,
695
+ {
696
+ select: true,
697
+ label: "Method",
698
+ value: method?.localName ?? "",
699
+ onChange: (e) => selectMethod(service.methods.find((m) => m.localName === e.target.value)),
700
+ size: "small",
701
+ fullWidth: true,
702
+ helperText: method === void 0 ? " " : methodModel(method).description,
703
+ children: service.methods.map((option) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
704
+ import_MenuItem.default,
705
+ {
706
+ value: option.localName,
707
+ disabled: !isMethodSupported(option),
708
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Stack2.default, { direction: "row", spacing: 1, sx: { alignItems: "center" }, children: [
709
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: option.name }),
710
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Chip2.default, { size: "small", variant: "outlined", label: methodModel(option).label })
711
+ ] })
712
+ },
713
+ option.name
714
+ ))
715
+ }
716
+ ),
717
+ method !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Stack2.default, { spacing: 1.5, children: [
718
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_Typography2.default, { variant: "subtitle2", children: [
719
+ "Request \xB7 ",
720
+ method.input.typeName
721
+ ] }),
722
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
723
+ RequestFields,
724
+ {
725
+ message: method.input,
726
+ json: value.json,
727
+ onChange: (json) => onChange({ ...value, json })
728
+ }
729
+ ),
730
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
731
+ import_Accordion.default,
732
+ {
733
+ disableGutters: true,
734
+ variant: "outlined",
735
+ defaultExpanded: true,
736
+ sx: { "&::before": { display: "none" } },
737
+ children: [
738
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_AccordionSummary.default, { expandIcon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_ExpandMore.default, {}), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Typography2.default, { sx: { fontWeight: 600 }, children: "Request JSON" }) }),
739
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_AccordionDetails.default, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
740
+ import_TextField2.default,
741
+ {
742
+ value: value.json,
743
+ onChange: (e) => onChange({ ...value, json: e.target.value }),
744
+ multiline: true,
745
+ minRows: 6,
746
+ fullWidth: true,
747
+ size: "small",
748
+ slotProps: { input: { sx: { fontFamily: "monospace", fontSize: 13 } } },
749
+ helperText: "Canonical protobuf-JSON \u2014 exactly what is sent on the channel."
750
+ }
751
+ ) })
752
+ ]
753
+ }
754
+ ),
755
+ parsedRequest !== null && "error" in parsedRequest && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_Alert2.default, { severity: "error", children: parsedRequest.error })
756
+ ] })
757
+ ] })
758
+ ] });
759
+ };
760
+ var canOpenRpcChannel = (value) => {
761
+ if (value.registry === null) return false;
762
+ const service = value.registry.getService(value.serviceName);
763
+ const method = service?.method[value.methodName];
764
+ if (method === void 0 || !isMethodSupported(method)) return false;
765
+ return !("error" in parseRequest(method.input, value.json));
766
+ };
767
+
768
+ // src/plugin.tsx
769
+ var import_jsx_runtime3 = require("react/jsx-runtime");
770
+ var rpcChannelPlugin = ({
771
+ descriptorSetUrl = "",
772
+ locked = false
773
+ } = {}) => (0, import_dxlink_console_core5.defineChannelPlugin)({
774
+ kind: "rpc",
775
+ label: "RPC",
776
+ icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_SwapHoriz2.default, {}),
777
+ dialogTitle: "New RPC channel",
778
+ // A service picker, a method picker and a JSON editor; the other three forms are a
779
+ // handful of fields.
780
+ dialogMaxWidth: "md",
781
+ createRequest: () => ({
782
+ url: descriptorSetUrl,
783
+ registry: null,
784
+ source: null,
785
+ serviceName: "",
786
+ methodName: "",
787
+ json: "{}"
788
+ }),
789
+ RequestForm: ({ value, onChange }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RpcChannelRequest, { value, onChange, urlLocked: locked }),
790
+ canOpen: canOpenRpcChannel,
791
+ buildConfig: (request) => {
792
+ const service = request.registry?.getService(request.serviceName);
793
+ const method = service?.method[request.methodName];
794
+ if (service === void 0 || method === void 0) {
795
+ return null;
796
+ }
797
+ const parsed = parseRequest(method.input, request.json);
798
+ return "error" in parsed ? null : { service, method, request: parsed.message };
799
+ },
800
+ Channel: RpcChannel
801
+ });
802
+ // Annotate the CommonJS export names for ESM import in node:
803
+ 0 && (module.exports = {
804
+ rpcChannelPlugin
805
+ });
806
+ //# sourceMappingURL=index.js.map