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