From 36fc43b134150bcc1c0cd5033edcf994ec3b03af Mon Sep 17 00:00:00 2001 From: Eric Daniels Date: Sun, 22 Sep 2024 02:14:47 -0400 Subject: [PATCH 1/7] Migrate to connect-es --- etc/buf.web.gen.yaml | 15 +- rpc/js/package-lock.json | 56 +- rpc/js/package.json | 6 +- rpc/js/src/BaseChannel.ts | 6 +- rpc/js/src/BaseStream.ts | 27 +- rpc/js/src/ClientChannel.ts | 201 +++++--- rpc/js/src/ClientStream.ts | 297 ++++++----- rpc/js/src/SignalingExchange.ts | 363 +++++++++++++ rpc/js/src/StreamClientStream.ts | 142 ++++++ rpc/js/src/UnaryClientStream.ts | 116 +++++ rpc/js/src/dial.ts | 843 ++++++++++++------------------- rpc/js/src/main.ts | 9 +- rpc/js/tsconfig.json | 2 +- rpc/js/vite.config.ts | 12 - 14 files changed, 1270 insertions(+), 825 deletions(-) create mode 100644 rpc/js/src/SignalingExchange.ts create mode 100644 rpc/js/src/StreamClientStream.ts create mode 100644 rpc/js/src/UnaryClientStream.ts diff --git a/etc/buf.web.gen.yaml b/etc/buf.web.gen.yaml index 215ea5cf..0aa7b927 100644 --- a/etc/buf.web.gen.yaml +++ b/etc/buf.web.gen.yaml @@ -1,15 +1,8 @@ version: v1 +managed: + enabled: true plugins: - - name: js + - plugin: buf.build/connectrpc/es:v1.5.0 out: dist/js - opt: - - import_style=commonjs - - name: grpc-web + - plugin: buf.build/bufbuild/es:v1.10.0 out: dist/js - opt: - - import_style=commonjs - - mode=grpcwebtext - - name: ts - out: dist/js - opt: - - service=grpc-web diff --git a/rpc/js/package-lock.json b/rpc/js/package-lock.json index 72ae6cab..6b3b6ba1 100644 --- a/rpc/js/package-lock.json +++ b/rpc/js/package-lock.json @@ -9,11 +9,11 @@ "version": "0.2.6", "license": "Apache-2.0", "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0" }, "devDependencies": { - "@types/google-protobuf": "^3.7.4", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "@viamrobotics/eslint-config": "^0.2.6", @@ -219,6 +219,31 @@ "node": ">=4" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.5.0.tgz", + "integrity": "sha512-1gGg0M6c2Y3lnr5itis9dNj9r8hbOIuBMqoGSbUy7L7Vjw4MAttjJzJfj9HCDgytGCJkGanYEYI6MQVDijdVQw==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.5.0.tgz", + "integrity": "sha512-xjiiQ932Kibddaka18fGZ6yQL7xjXuLcYFYh/cU+q1WWEIrFPkZfViG/Ee6yrZbrlZkjcBuDibng+q7baTndfg==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.5.0" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", @@ -324,17 +349,6 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, - "node_modules/@improbable-eng/grpc-web": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", - "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", - "dependencies": { - "browser-headers": "^0.4.0" - }, - "peerDependencies": { - "google-protobuf": "^3.2.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -463,12 +477,6 @@ "dev": true, "peer": true }, - "node_modules/@types/google-protobuf": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.2.tgz", - "integrity": "sha512-ubeqvw7sl6CdgeiIilsXB2jIFoD/D0F+/LIEp7xEBEXRNtDJcf05FRINybsJtL7GlkWOUVn6gJs2W9OF+xI6lg==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1040,11 +1048,6 @@ "node": ">=8" } }, - "node_modules/browser-headers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" - }, "node_modules/browserslist": { "version": "4.22.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", @@ -1844,7 +1847,8 @@ "node_modules/google-protobuf": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", - "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==" + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", + "dev": true }, "node_modules/graceful-fs": { "version": "4.2.10", diff --git a/rpc/js/package.json b/rpc/js/package.json index 5f2c1c9c..2e040f3c 100644 --- a/rpc/js/package.json +++ b/rpc/js/package.json @@ -3,11 +3,11 @@ "version": "0.2.6", "license": "Apache-2.0", "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0" }, "devDependencies": { - "@types/google-protobuf": "^3.7.4", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "@viamrobotics/eslint-config": "^0.2.6", diff --git a/rpc/js/src/BaseChannel.ts b/rpc/js/src/BaseChannel.ts index 6eef1c29..a9c3df13 100644 --- a/rpc/js/src/BaseChannel.ts +++ b/rpc/js/src/BaseChannel.ts @@ -1,4 +1,4 @@ -import type { ProtobufMessage } from '@improbable-eng/grpc-web/dist/typings/message'; +import { Message } from '@bufbuild/protobuf'; import { ConnectionClosedError } from './errors'; export class BaseChannel { @@ -75,7 +75,7 @@ export class BaseChannel { this.closeWithReason(new Error(ev)); } - protected write(msg: ProtobufMessage) { - this.dataChannel.send(msg.serializeBinary()); + protected write(msg: Message) { + this.dataChannel.send(msg.toBinary()); } } diff --git a/rpc/js/src/BaseStream.ts b/rpc/js/src/BaseStream.ts index 3c24d5ad..df06d8d3 100644 --- a/rpc/js/src/BaseStream.ts +++ b/rpc/js/src/BaseStream.ts @@ -1,41 +1,30 @@ -import type { grpc } from '@improbable-eng/grpc-web'; import type { PacketMessage, Stream } from './gen/proto/rpc/webrtc/v1/grpc_pb'; // MaxMessageSize is the maximum size a gRPC message can be. let MaxMessageSize = 1 << 25; export class BaseStream { - protected readonly stream: Stream; - private readonly onDone: (id: number) => void; - protected readonly opts: grpc.TransportOptions; + protected readonly grpcStream: Stream; + private readonly onDone: (id: bigint) => void; protected closed: boolean = false; private readonly packetBuf: Array = []; private packetBufSize = 0; - private err: Error | undefined; - constructor( - stream: Stream, - onDone: (id: number) => void, - opts: grpc.TransportOptions - ) { - this.stream = stream; + constructor(grpcStream: Stream, onDone: (id: bigint) => void) { + this.grpcStream = grpcStream; this.onDone = onDone; - this.opts = opts; } - public closeWithRecvError(err?: Error) { + public closeWithRecvError() { if (this.closed) { return; } this.closed = true; - this.err = err; - this.onDone(this.stream.getId()); - // pretty sure passing the error does nothing. - this.opts.onEnd(this.err); + this.onDone(this.grpcStream.id); } protected processPacketMessage(msg: PacketMessage): Uint8Array | undefined { - const data = msg.getData_asU8(); + const { data } = msg; if (data.length + this.packetBufSize > MaxMessageSize) { this.packetBuf.length = 0; this.packetBufSize = 0; @@ -46,7 +35,7 @@ export class BaseStream { } this.packetBuf.push(data); this.packetBufSize += data.length; - if (msg.getEom()) { + if (msg.eom) { const data = new Uint8Array(this.packetBufSize); let position = 0; for (let i = 0; i < this.packetBuf.length; i++) { diff --git a/rpc/js/src/ClientChannel.ts b/rpc/js/src/ClientChannel.ts index 043921c9..2b50eeea 100644 --- a/rpc/js/src/ClientChannel.ts +++ b/rpc/js/src/ClientChannel.ts @@ -1,6 +1,18 @@ -import type { grpc } from '@improbable-eng/grpc-web'; +import { + AnyMessage, + Message, + MethodInfo, + PartialMessage, + ServiceType, +} from '@bufbuild/protobuf'; +import { + ContextValues, + StreamResponse, + Transport, + UnaryResponse, +} from '@connectrpc/connect'; import { BaseChannel } from './BaseChannel'; -import { ClientStream } from './ClientStream'; +import { ClientStream, ClientStreamConstructor } from './ClientStream'; import { ConnectionClosedError } from './errors'; import { Request, @@ -9,6 +21,8 @@ import { Response, Stream, } from './gen/proto/rpc/webrtc/v1/grpc_pb'; +import { StreamClientStream } from './StreamClientStream'; +import { UnaryClientStream } from './UnaryClientStream'; // MaxStreamCount is the max number of streams a channel can have. let MaxStreamCount = 256; @@ -17,9 +31,9 @@ interface activeClienStream { cs: ClientStream; } -export class ClientChannel extends BaseChannel { +export class ClientChannel extends BaseChannel implements Transport { private streamIDCounter = 0; - private readonly streams: Record = {}; + private readonly streams: Record = {}; constructor(pc: RTCPeerConnection, dc: RTCDataChannel) { super(pc, dc); @@ -38,41 +52,26 @@ export class ClientChannel extends BaseChannel { dc.addEventListener('close', () => this.onConnectionTerminated()); } - public transportFactory(): grpc.TransportFactory { - return (opts: grpc.TransportOptions) => { - return this.newStream(this.nextStreamID(), opts); - }; - } - private onConnectionTerminated() { // we may call this twice but we know closed will be true at this point. this.closeWithReason(new ConnectionClosedError('data channel closed')); - const err = new ConnectionClosedError('connection terminated'); for (const streamId in this.streams) { const stream = this.streams[streamId]!; - stream.cs.closeWithRecvError(err); + stream.cs.closeWithRecvError(); } } private onChannelMessage(event: MessageEvent) { - let resp: Response; - try { - resp = Response.deserializeBinary( - new Uint8Array(event.data as ArrayBuffer) - ); - } catch (e) { - console.error('error deserializing message', e); - return; - } + let resp = Response.fromBinary(new Uint8Array(event.data as ArrayBuffer)); - const stream = resp.getStream(); + const { stream } = resp; if (stream === undefined) { console.error('no stream id; discarding'); return; } - const id = stream.getId(); - const activeStream = this.streams[id]; + const { id } = stream; + const activeStream = this.streams[id.toString()]; if (activeStream === undefined) { console.error('no stream for id; discarding', 'id', id); return; @@ -81,82 +80,124 @@ export class ClientChannel extends BaseChannel { } private nextStreamID(): Stream { - const stream = new Stream(); - stream.setId(this.streamIDCounter++); - return stream; + return new Stream({ + id: BigInt(this.streamIDCounter++), + }); } - private newStream( + private newStream< + T extends ClientStream, + I extends Message, + O extends Message, + >( + clientCtor: ClientStreamConstructor, stream: Stream, - opts: grpc.TransportOptions - ): grpc.Transport { + service: ServiceType, + method: MethodInfo, + header: HeadersInit | undefined + ): T { if (this.isClosed()) { - return new FailingClientStream( - new ConnectionClosedError('connection closed'), - opts - ); + throw new ConnectionClosedError('connection closed'); } - let activeStream = this.streams[stream.getId()]; - if (activeStream === undefined) { - if (Object.keys(this.streams).length > MaxStreamCount) { - return new FailingClientStream(new Error('stream limit hit'), opts); - } - const clientStream = new ClientStream( - this, - stream, - (id: number) => this.removeStreamByID(id), - opts - ); - activeStream = { cs: clientStream }; - this.streams[stream.getId()] = activeStream; + let activeStream = this.streams[stream.id.toString()]; + if (activeStream !== undefined) { + throw new Error('invariant: stream should not exist yet'); } - return activeStream.cs; + if (Object.keys(this.streams).length > MaxStreamCount) { + throw new Error('stream limit hit'); + } + const clientStream = new clientCtor( + this, + stream, + (id: bigint) => this.removeStreamByID(id), + service, + method, + header + ); + activeStream = { cs: clientStream }; + this.streams[stream.id.toString()] = activeStream; + return clientStream; } - private removeStreamByID(id: number) { - delete this.streams[id]; + private removeStreamByID(id: bigint) { + delete this.streams[id.toString()]; } public writeHeaders(stream: Stream, headers: RequestHeaders) { - const request = new Request(); - request.setStream(stream); - request.setHeaders(headers); - this.write(request); + this.write( + new Request({ + stream, + type: { + case: 'headers', + value: headers, + }, + }) + ); } public writeMessage(stream: Stream, msg: RequestMessage) { - const request = new Request(); - request.setStream(stream); - request.setMessage(msg); - this.write(request); + this.write( + new Request({ + stream, + type: { + case: 'message', + value: msg, + }, + }) + ); } public writeReset(stream: Stream) { - const request = new Request(); - request.setStream(stream); - request.setRstStream(true); - this.write(request); + this.write( + new Request({ + stream, + type: { + case: 'rstStream', + value: true, + }, + }) + ); } -} -class FailingClientStream implements grpc.Transport { - private readonly err: Error; - private readonly opts: grpc.TransportOptions; - - constructor(err: Error, opts: grpc.TransportOptions) { - this.err = err; - this.opts = opts; + public async unary< + I extends Message = AnyMessage, + O extends Message = AnyMessage, + >( + service: ServiceType, + method: MethodInfo, + signal: AbortSignal | undefined, + timeoutMs: number | undefined, + header: HeadersInit | undefined, + message: PartialMessage, + contextValues?: ContextValues + ): Promise> { + return this.newStream, I, O>( + UnaryClientStream, + this.nextStreamID(), + service, + method, + header + ).run(signal, timeoutMs, message, contextValues); } - public start() { - if (this.opts.onEnd) { - setTimeout(() => this.opts.onEnd(this.err)); - } + public async stream< + I extends Message = AnyMessage, + O extends Message = AnyMessage, + >( + service: ServiceType, + method: MethodInfo, + signal: AbortSignal | undefined, + timeoutMs: number | undefined, + header: HeadersInit | undefined, + input: AsyncIterable>, + contextValues?: ContextValues + ): Promise> { + return this.newStream, I, O>( + StreamClientStream, + this.nextStreamID(), + service, + method, + header + ).run(signal, timeoutMs, input, contextValues); } - - public sendMessage() {} - - public finishSend() {} - - public cancel() {} } diff --git a/rpc/js/src/ClientStream.ts b/rpc/js/src/ClientStream.ts index 68746ef8..ff58cbd9 100644 --- a/rpc/js/src/ClientStream.ts +++ b/rpc/js/src/ClientStream.ts @@ -1,7 +1,13 @@ -import { grpc } from '@improbable-eng/grpc-web'; +import { + AnyMessage, + Message, + MethodInfo, + ServiceType, +} from '@bufbuild/protobuf'; +import { createClientMethodSerializers } from '@connectrpc/connect/protocol'; import { BaseStream } from './BaseStream'; import type { ClientChannel } from './ClientChannel'; -import { GRPCError } from './errors'; +import { cloneHeaders } from './dial'; import { Metadata, PacketMessage, @@ -18,77 +24,120 @@ import { // see golang/client_stream.go const maxRequestMessagePacketDataSize = 16373; -export class ClientStream extends BaseStream implements grpc.Transport { - private readonly channel: ClientChannel; +export interface ClientStreamConstructor< + T extends ClientStream, + I extends Message = AnyMessage, + O extends Message = AnyMessage, +> { + // eslint-disable-next-line @typescript-eslint/prefer-function-type -- this works better with ClientChannel + new ( + channel: ClientChannel, + stream: Stream, + onDone: (id: bigint) => void, + service: ServiceType, + method: MethodInfo, + header: HeadersInit | undefined + ): T; +} + +/** A ClientStream provides all the facilities needed to invoke and manage a + * gRPC stream at a low-level. Implementors like UnaryClientStream and StreamClientStream + * handle the method specific flow of unary/stream operations. + */ +export abstract class ClientStream< + I extends Message = AnyMessage, + O extends Message = AnyMessage, +> extends BaseStream { + protected readonly channel: ClientChannel; + protected readonly service: ServiceType; + protected readonly method: MethodInfo; + protected readonly parseMessage: (data: Uint8Array) => O; + protected readonly requestHeaders: RequestHeaders; + private headersReceived: boolean = false; private trailersReceived: boolean = false; + protected abstract onHeaders(headers: ResponseHeaders): void; + protected abstract onTrailers(trailers: ResponseTrailers): void; + protected abstract onMessage(msgBytes: Uint8Array): void; + constructor( channel: ClientChannel, stream: Stream, - onDone: (id: number) => void, - opts: grpc.TransportOptions + onDone: (id: bigint) => void, + service: ServiceType, + method: MethodInfo, + header: HeadersInit | undefined ) { - super(stream, onDone, opts); + super(stream, onDone); this.channel = channel; + this.service = service; + this.method = method; + + const { parse } = createClientMethodSerializers( + method, + true, + undefined, + undefined + ); + this.parseMessage = parse; + const svcMethod = `/${service.typeName}/${method.name}`; + this.requestHeaders = new RequestHeaders({ + method: svcMethod, + }); + const metadataProto = fromGRPCMetadata(cloneHeaders(header)); + if (metadataProto) { + this.requestHeaders.metadata = metadataProto; + } } - public start(metadata: grpc.Metadata) { - const method = `/${this.opts.methodDefinition.service.serviceName}/${this.opts.methodDefinition.methodName}`; - const requestHeaders = new RequestHeaders(); - requestHeaders.setMethod(method); - requestHeaders.setMetadata(fromGRPCMetadata(metadata)); + protected startRequest(signal?: AbortSignal) { + if (signal) { + signal.onabort = () => { + this.resetStream(); + }; + } try { - this.channel.writeHeaders(this.stream, requestHeaders); + this.channel.writeHeaders(this.grpcStream, this.requestHeaders); } catch (error) { console.error('error writing headers', error); - this.closeWithRecvError(error as Error); + this.closeWithRecvError(); } } - public sendMessage(msgBytes?: Uint8Array) { - // skip frame header bytes + protected sendMessage(msgBytes?: Uint8Array) { if (msgBytes) { - this.writeMessage(false, msgBytes.slice(5)); + this.writeMessage(false, msgBytes); return; } this.writeMessage(false, undefined); } - public resetStream() { + protected resetStream() { + if (this.closed) { + return; + } try { - this.channel.writeReset(this.stream); + this.channel.writeReset(this.grpcStream); } catch (error) { console.error('error writing reset', error); - this.closeWithRecvError(error as Error); - } - } - - public finishSend() { - if (!this.opts.methodDefinition.requestStream) { - return; - } - this.writeMessage(true, undefined); - } - - public cancel() { - if (this.closed) { - return; + this.closeWithRecvError(); } - this.resetStream(); } - private writeMessage(eos: boolean, msgBytes?: Uint8Array) { + protected writeMessage(eos: boolean, msgBytes?: Uint8Array) { try { if (!msgBytes || msgBytes.length == 0) { - const packet = new PacketMessage(); - packet.setEom(true); - const requestMessage = new RequestMessage(); - requestMessage.setHasMessage(!!msgBytes); - requestMessage.setPacketMessage(packet); - requestMessage.setEos(eos); - this.channel.writeMessage(this.stream, requestMessage); + const packetMessage = new PacketMessage({ + eom: true, + }); + const requestMessage = new RequestMessage({ + hasMessage: !!msgBytes, + packetMessage, + eos, + }); + this.channel.writeMessage(this.grpcStream, requestMessage); return; } @@ -97,159 +146,133 @@ export class ClientStream extends BaseStream implements grpc.Transport { msgBytes.length, maxRequestMessagePacketDataSize ); - const packet = new PacketMessage(); - packet.setData(msgBytes.slice(0, amountToSend)); + const packetMessage = new PacketMessage(); + packetMessage.data = msgBytes.slice(0, amountToSend); msgBytes = msgBytes.slice(amountToSend); if (msgBytes.length === 0) { - packet.setEom(true); + packetMessage.eom = true; } - const requestMessage = new RequestMessage(); - requestMessage.setHasMessage(!!msgBytes); - requestMessage.setPacketMessage(packet); - requestMessage.setEos(eos); - this.channel.writeMessage(this.stream, requestMessage); + const requestMessage = new RequestMessage({ + hasMessage: !!msgBytes, + packetMessage, + eos, + }); + this.channel.writeMessage(this.grpcStream, requestMessage); } } catch (error) { console.error('error writing message', error); - this.closeWithRecvError(error as Error); + this.closeWithRecvError(); } } public onResponse(resp: Response) { - switch (resp.getTypeCase()) { - case Response.TypeCase.HEADERS: + switch (resp.type.case) { + case 'headers': if (this.headersReceived) { - this.closeWithRecvError(new Error('headers already received')); + console.error( + `invariant: headers already received for ${this.grpcStream.id}` + ); return; } if (this.trailersReceived) { - this.closeWithRecvError(new Error('headers received after trailers')); + console.error( + `invariant: headers received after trailers for ${this.grpcStream.id}` + ); return; } - this.processHeaders(resp.getHeaders()!); + this.processHeaders(resp.type.value); break; - case Response.TypeCase.MESSAGE: + case 'message': if (!this.headersReceived) { - this.closeWithRecvError(new Error('headers not yet received')); + console.error( + `invariant: headers not yet received for ${this.grpcStream.id}` + ); return; } if (this.trailersReceived) { - this.closeWithRecvError(new Error('headers received after trailers')); + console.error( + `invariant: headers received after trailers for ${this.grpcStream.id}` + ); return; } - this.processMessage(resp.getMessage()!); + this.processMessage(resp.type.value); break; - case Response.TypeCase.TRAILERS: - this.processTrailers(resp.getTrailers()!); + case 'trailers': + this.processTrailers(resp.type.value); break; default: - console.error('unknown response type', resp.getTypeCase()); + console.error('unknown response type', resp.type.case); break; } } private processHeaders(headers: ResponseHeaders) { this.headersReceived = true; - this.opts.onHeaders(toGRPCMetadata(headers.getMetadata()), 200); + if (!this.onHeaders) { + throw new Error('invariant: onHeaders unset'); + } + this.onHeaders(headers); } private processMessage(msg: ResponseMessage) { - const result = super.processPacketMessage(msg.getPacketMessage()!); + if (!this.onMessage) { + throw new Error('invariant: onMessage unset'); + } + if (!msg.packetMessage) { + return; + } + const result = super.processPacketMessage(msg.packetMessage); if (!result) { return; } - const chunk = new ArrayBuffer(result.length + 5); - new DataView(chunk, 1, 4).setUint32(0, result.length, false); - new Uint8Array(chunk, 5).set(result); - this.opts.onChunk(new Uint8Array(chunk)); + this.onMessage(result); } private processTrailers(trailers: ResponseTrailers) { this.trailersReceived = true; - const headers = toGRPCMetadata(trailers.getMetadata()); - let statusCode, statusMessage; - const status = trailers.getStatus(); + if (!this.onTrailers) { + throw new Error('invariant: onTrailers unset'); + } + + let statusCode; + const { status } = trailers; if (status) { - statusCode = status.getCode(); - statusMessage = status.getMessage(); - headers.set('grpc-status', `${status.getCode()}`); - if (statusMessage !== undefined) { - headers.set('grpc-message', status.getMessage()); - } + statusCode = status.code; } else { statusCode = 0; - headers.set('grpc-status', '0'); - statusMessage = ''; } - const headerBytes = headersToBytes(headers); - const chunk = new ArrayBuffer(headerBytes.length + 5); - new DataView(chunk, 0, 1).setUint8(0, 1 << 7); - new DataView(chunk, 1, 4).setUint32(0, headerBytes.length, false); - new Uint8Array(chunk, 5).set(headerBytes); - this.opts.onChunk(new Uint8Array(chunk)); + this.onTrailers(trailers); if (statusCode === 0) { this.closeWithRecvError(); return; } - this.closeWithRecvError(new GRPCError(statusCode, statusMessage)); + this.closeWithRecvError(); } } -// from https://github.com/improbable-eng/grpc-web/blob/6fb683f067bd56862c3a510bc5590b955ce46d2a/ts/src/ChunkParser.ts#L22 -export function encodeASCII(input: string): Uint8Array { - const encoded = new Uint8Array(input.length); - for (let i = 0; i !== input.length; ++i) { - const charCode = input.charCodeAt(i); - if (!isValidHeaderAscii(charCode)) { - throw new Error('Metadata contains invalid ASCII'); - } - encoded[i] = charCode; - } - return encoded; -} - -const isAllowedControlChars = (char: number) => - char === 0x9 || char === 0xa || char === 0xd; - -function isValidHeaderAscii(val: number): boolean { - return isAllowedControlChars(val) || (val >= 0x20 && val <= 0x7e); -} - -function headersToBytes(headers: grpc.Metadata): Uint8Array { - let asString = ''; - headers.forEach((key, values) => { - asString += `${key}: ${values.join(', ')}\r\n`; - }); - return encodeASCII(asString); -} - +// Needs testing // from https://github.com/jsmouret/grpc-over-webrtc/blob/45cd6d6cf516e78b1e262ea7aa741bc7a7a93dbc/client-improbable/src/grtc/webrtcclient.ts#L7 -const fromGRPCMetadata = (metadata?: grpc.Metadata): Metadata | undefined => { - if (!metadata) { +const fromGRPCMetadata = (headers?: Headers): Metadata | undefined => { + if (!headers) { return undefined; } - const result = new Metadata(); - const md = result.getMdMap(); - metadata.forEach((key, values) => { - const strings = new Strings(); - strings.setValuesList(values); - md.set(key, strings); + const result = new Metadata({ + md: Object.fromEntries( + [...headers.entries()].map(([key, value]) => [ + key, + new Strings({ values: [value] }), + ]) + ), }); - if (result.getMdMap().getLength() === 0) { - return undefined; - } - return result; + + return Object.keys(result.md).length > 0 ? result : undefined; }; -const toGRPCMetadata = (metadata?: Metadata): grpc.Metadata => { - const result = new grpc.Metadata(); - if (metadata) { - metadata - .getMdMap() - .forEach((entry: any, key: any) => - result.append(key, entry.getValuesList()) - ); - } - return result; +// Needs testing +export const toGRPCMetadata = (metadata?: Metadata): Headers => { + const headers = Object.entries(metadata?.md ?? {}).flatMap( + ([key, { values }]) => values.map<[string, string]>((value) => [key, value]) + ); + return new Headers(headers); }; diff --git a/rpc/js/src/SignalingExchange.ts b/rpc/js/src/SignalingExchange.ts new file mode 100644 index 00000000..e4a9fc68 --- /dev/null +++ b/rpc/js/src/SignalingExchange.ts @@ -0,0 +1,363 @@ +import { + CallOptions, + Code, + ConnectError, + PromiseClient, +} from '@connectrpc/connect'; +import { ClientChannel } from './ClientChannel'; +import { DialWebRTCOptions } from './dial'; +import { ConnectionClosedError } from './errors'; +import { Status } from './gen/google/rpc/status_pb'; +import { SignalingService } from './gen/proto/rpc/webrtc/v1/signaling_connect'; +import { + CallRequest, + CallResponse, + CallResponseInitStage, + CallResponseUpdateStage, + CallUpdateRequest, + ICECandidate, +} from './gen/proto/rpc/webrtc/v1/signaling_pb'; +import { addSdpFields } from './peer'; + +const callUUIDUnset = 'invariant: call uuid unset'; + +export class SignalingExchange { + private readonly clientChannel: ClientChannel; + private callUuid?: string; + // only send once since exchange may end or ICE may end + private sentDoneOrErrorOnce = false; + private exchangeDone = false; + private iceComplete = false; + private awaitingRemoteDescription?: { + success: (value: unknown) => void; + failure: (reason?: any) => void; + }; + + private remoteDescriptionSet?: Promise; + + // stats + private numCallUpdates = 0; + private maxCallUpdateDuration = 0; + private totalCallUpdateDuration = 0; + + constructor( + private readonly signalingClient: PromiseClient, + private readonly callOpts: CallOptions, + private readonly pc: RTCPeerConnection, + private readonly dc: RTCDataChannel, + private readonly dialOpts?: DialWebRTCOptions + ) { + this.clientChannel = new ClientChannel(this.pc, this.dc); + } + + public async doExchange(): Promise { + // Setup our handlers before starting the signaling call. + await this.setup(); + + const description = addSdpFields( + this.pc.localDescription, + this.dialOpts?.additionalSdpFields + ); + const encodedSDP = btoa(JSON.stringify(description)); + const callRequest = new CallRequest({ + sdp: encodedSDP, + }); + if (this.dialOpts && this.dialOpts.disableTrickleICE) { + callRequest.disableTrickle = this.dialOpts.disableTrickleICE; + } + + // As long as we establish a connection (i.e. we are ready), then + // we will make it clear across the exchange that we are done + // and no more work should be done nor should any errors be emitted. + this.clientChannel.ready + .then(() => { + this.exchangeDone = true; + }) + .catch(console.error); + + // Initiate now the call now that all of our handlers are setup. + const callResponses = this.signalingClient.call(callRequest, this.callOpts); + + // Start processing the responses asynchronously. + const responsesProcessed = this.processCallResponses(callResponses); + + await Promise.all([this.clientChannel.ready, responsesProcessed]); + return this.clientChannel; + } + + private async setup() { + this.remoteDescriptionSet = new Promise((resolve, reject) => { + this.awaitingRemoteDescription = { + success: resolve, + failure: reject, + }; + }); + if (!this.dialOpts?.disableTrickleICE) { + // set up offer + const offerDesc = await this.pc.createOffer({}); + + this.pc.addEventListener('iceconnectionstatechange', () => { + if ( + this.pc.iceConnectionState !== 'completed' || + this.numCallUpdates === 0 + ) { + return; + } + let averageCallUpdateDuration = + this.totalCallUpdateDuration / this.numCallUpdates; + console.groupCollapsed('Caller update statistics'); + console.table({ + num_updates: this.numCallUpdates, + average_duration: `${averageCallUpdateDuration}ms`, + max_duration: `${this.maxCallUpdateDuration}ms`, + }); + console.groupEnd(); + }); + this.pc.addEventListener( + 'icecandidate', + async (event: { candidate: RTCIceCandidateInit | null }) => + this.onLocalICECandidate(event) + ); + + await this.pc.setLocalDescription(offerDesc); + } + } + + public terminate() { + this.clientChannel.close(); + } + + private async processCallResponses( + callResponses: AsyncIterable + ) { + let haveInit = false; + try { + for await (const response of callResponses) { + if (response.stage.case == 'init') { + if (haveInit) { + await this.sendError('got init stage more than once'); + return; + } + haveInit = true; + if (!this.handleInitResponse(response.uuid, response.stage.value)) { + return; + } + } else if (response.stage.case == 'update') { + if (!haveInit) { + await this.sendError('got update stage before init stage'); + return; + } + if (!this.handleUpdateResponse(response.uuid, response.stage.value)) { + return; + } + } else { + await this.sendError('unknown CallResponse stage'); + return; + } + } + } catch (err) { + if (this.exchangeDone || this.pc.iceConnectionState === 'connected') { + // There's nothing to do with these errors, our connection is established. + return; + } + if (err instanceof ConnectError && err.code == Code.Unimplemented) { + if (err.message === 'Response closed without headers') { + throw new ConnectionClosedError('failed to dial'); + } + if (this.clientChannel?.isClosed()) { + throw new ConnectionClosedError('client channel is closed'); + } + console.error(err.message); + } + throw err; + } + } + + private async handleInitResponse( + uuid: string, + response: CallResponseInitStage + ): Promise { + this.callUuid = uuid; + + const remoteSDP = new RTCSessionDescription(JSON.parse(atob(response.sdp))); + if (this.clientChannel.isClosed()) { + await this.sendError('client channel is closed'); + return false; + } + await this.pc.setRemoteDescription(remoteSDP); + this.awaitingRemoteDescription?.success(true); + + if (this.dialOpts?.disableTrickleICE) { + this.exchangeDone = true; + await this.sendDone(); + return false; + } + + return true; + } + + private async handleUpdateResponse( + uuid: string, + response: CallResponseUpdateStage + ): Promise { + if (uuid !== this.callUuid) { + await this.sendError(`uuid mismatch; have=${uuid} want=${this.callUuid}`); + return false; + } + const cand = iceCandidateFromProto(response.candidate!); + if (cand.candidate !== null) { + console.debug(`received remote ICE ${cand.candidate}`); + } + try { + await this.pc.addIceCandidate(cand); + } catch (error) { + await this.sendError(JSON.stringify(error)); + return false; + } + + return true; + } + + private async onLocalICECandidate(event: { + candidate: RTCIceCandidateInit | null; + }) { + await this.remoteDescriptionSet; + if (this.exchangeDone || this.pc.iceConnectionState === 'connected') { + return; + } + + if (event.candidate === null) { + this.iceComplete = true; + await this.sendDone(); + return; + } + + if (!this.callUuid) { + throw new Error(callUUIDUnset); + } + + if (event.candidate.candidate !== null) { + console.debug(`gathered local ICE ${event.candidate.candidate}`); + } + const iProto = iceCandidateToProto(event.candidate); + const callRequestUpdate = new CallUpdateRequest({ + uuid: this.callUuid, + update: { + case: 'candidate', + value: iProto, + }, + }); + const callUpdateStart = new Date(); + try { + await this.signalingClient.callUpdate(callRequestUpdate, this.callOpts); + this.numCallUpdates++; + let callUpdateEnd = new Date(); + let callUpdateDuration = + callUpdateEnd.getTime() - callUpdateStart.getTime(); + if (callUpdateDuration > this.maxCallUpdateDuration) { + this.maxCallUpdateDuration = callUpdateDuration; + } + this.totalCallUpdateDuration += callUpdateDuration; + return; + } catch (err) { + if ( + this.exchangeDone || + this.iceComplete || + // @ts-expect-error tsc is unaware that iceConnectionState can change + // after we've inspected it before. + this.pc.iceConnectionState === 'connected' + ) { + return; + } + console.error(err); + } + } + + private async sendError(err: string) { + if (this.sentDoneOrErrorOnce) { + return; + } + if (!this.callUuid) { + throw new Error(callUUIDUnset); + } + this.sentDoneOrErrorOnce = true; + const callRequestUpdate = new CallUpdateRequest({ + uuid: this.callUuid, + update: { + case: 'error', + value: new Status({ + code: Code.Unknown, + message: err, + }), + }, + }); + try { + await this.signalingClient.callUpdate(callRequestUpdate, this.callOpts); + } catch (err) { + // even though this call update fails, there's a chance another + // attempt with another ICE candidate(s) will make the connection + // work. In the future it may be better to figure out if this + // error is fatal or not. + console.error('failed to send call update; continuing', err); + } + } + + private async sendDone() { + if (this.sentDoneOrErrorOnce) { + return; + } + if (!this.callUuid) { + throw new Error(callUUIDUnset); + } + this.sentDoneOrErrorOnce = true; + const callRequestUpdate = new CallUpdateRequest({ + uuid: this.callUuid, + update: { + case: 'done', + value: true, + }, + }); + try { + await this.signalingClient.callUpdate(callRequestUpdate, this.callOpts); + } catch (err) { + // even though this call update fails, there's a chance another + // attempt with another ICE candidate(s) will make the connection + // work. In the future it may be better to figure out if this + // error is fatal or not. + console.error(err); + } + } +} + +function iceCandidateFromProto(i: ICECandidate): RTCIceCandidateInit { + let candidate: RTCIceCandidateInit = { + candidate: i.candidate, + }; + if (i.sdpMid) { + candidate.sdpMid = i.sdpMid; + } + if (i.sdpmLineIndex) { + candidate.sdpMLineIndex = i.sdpmLineIndex; + } + if (i.usernameFragment) { + candidate.usernameFragment = i.usernameFragment; + } + return candidate; +} + +function iceCandidateToProto(i: RTCIceCandidateInit): ICECandidate { + let candidate = new ICECandidate({ + candidate: i.candidate!, + }); + candidate.candidate = i.candidate!; + if (i.sdpMid) { + candidate.sdpMid = i.sdpMid; + } + if (i.sdpMLineIndex) { + candidate.sdpmLineIndex = i.sdpMLineIndex; + } + if (i.usernameFragment) { + candidate.usernameFragment = i.usernameFragment; + } + return candidate; +} diff --git a/rpc/js/src/StreamClientStream.ts b/rpc/js/src/StreamClientStream.ts new file mode 100644 index 00000000..bdddd3bc --- /dev/null +++ b/rpc/js/src/StreamClientStream.ts @@ -0,0 +1,142 @@ +import { Message, PartialMessage } from '@bufbuild/protobuf'; +import { + ContextValues, + StreamRequest, + StreamResponse, + createContextValues, +} from '@connectrpc/connect'; +import { + createWritableIterable, + runStreamingCall, +} from '@connectrpc/connect/protocol'; +import { ClientStream, toGRPCMetadata } from './ClientStream'; +import { + ResponseHeaders, + ResponseTrailers, +} from './gen/proto/rpc/webrtc/v1/grpc_pb'; + +export class StreamClientStream< + I extends Message, + O extends Message, +> extends ClientStream { + private awaitingHeadersResult?: { + success: (value: Headers) => void; + failure: (reason?: any) => void; + }; + + private gotHeaders = false; + + // trailers will be written to later + private readonly respStream = createWritableIterable(); + private readonly trailers: Headers = new Headers(); + private respStreamQueue?: Promise; + + public async run( + signal: AbortSignal | undefined, + timeoutMs: number | undefined, + input: AsyncIterable>, + contextValues?: ContextValues + ): Promise> { + let req = { + stream: true as const, + url: '', + init: {}, + service: this.service, + method: this.method, + header: new Headers(), + contextValues: contextValues ?? createContextValues(), + message: input, + }; + type optParams = Parameters>[0]; + let opt: optParams = { + req, + // next is what actually kicks off the request. The run call below will + // ultimately call this for us. + next: async (req: StreamRequest): Promise> => { + let startRequest = new Promise((resolve, reject) => { + this.awaitingHeadersResult = { + success: resolve, + failure: reject, + }; + this.startRequest(); + this.sendMessages(req.message).catch((err) => { + console.error('error sending streaming message', err); + this.closeWithRecvError(); + }); + }); + + const headers = await startRequest; + + return { + ...req, + header: headers, + trailer: this.trailers, + message: this.respStream, + } satisfies StreamResponse; + }, + }; + if (signal) { + opt.signal = signal; + } + if (timeoutMs) { + opt.timeoutMs = timeoutMs; + } + + return runStreamingCall(opt); + } + + protected async sendMessages(messages: AsyncIterable) { + for await (const msg of messages) { + this.sendMessage(msg.toBinary()); + } + // end of messages + this.writeMessage(true, undefined); + } + + protected onHeaders(respHeaders: ResponseHeaders): void { + this.gotHeaders = true; + this.awaitingHeadersResult?.success(toGRPCMetadata(respHeaders.metadata)); + } + + protected onTrailers(respTrailers: ResponseTrailers): void { + if (respTrailers.metadata?.md) { + for (let key in respTrailers.metadata.md) { + let value = respTrailers.metadata.md[key]; + for (let val in value?.values) { + this.trailers.append(key, val); + } + } + } + this.respStream.close(); + + if (!respTrailers.status || respTrailers.status.code == 0) { + if (this.gotHeaders) { + return; + } + this.awaitingHeadersResult?.success(new Headers()); + return; + } + if (this.gotHeaders) { + // nothing to fail here + return; + } + this.awaitingHeadersResult?.failure(respTrailers.status.message); + } + + protected onMessage(msgBytes: Uint8Array) { + let msg = this.parseMessage(msgBytes); + if (this.respStreamQueue) { + this.respStreamQueue = this.respStreamQueue.then(async () => + this.respStream.write(msg) + ); + } else { + this.respStreamQueue = this.respStream.write(msg); + } + this.respStreamQueue.catch((err) => { + console.error( + `error pushing received message into stream; failing: ${err}` + ); + this.resetStream(); + }); + } +} diff --git a/rpc/js/src/UnaryClientStream.ts b/rpc/js/src/UnaryClientStream.ts new file mode 100644 index 00000000..1511c7f5 --- /dev/null +++ b/rpc/js/src/UnaryClientStream.ts @@ -0,0 +1,116 @@ +import { Message, PartialMessage } from '@bufbuild/protobuf'; +import { + ContextValues, + UnaryRequest, + UnaryResponse, + createContextValues, +} from '@connectrpc/connect'; +import { runUnaryCall } from '@connectrpc/connect/protocol'; +import { ClientStream, toGRPCMetadata } from './ClientStream'; +import { + ResponseHeaders, + ResponseTrailers, +} from './gen/proto/rpc/webrtc/v1/grpc_pb'; + +export class UnaryClientStream< + I extends Message, + O extends Message, +> extends ClientStream { + private result?: { + success: (value: UnaryResponse) => void; + failure: (reason?: any) => void; + }; + + private headers?: Headers; + private message?: O; + + public async run( + signal: AbortSignal | undefined, + timeoutMs: number | undefined, + message: PartialMessage, + contextValues?: ContextValues + ): Promise> { + let req = { + stream: false as const, + url: '', + init: {}, + service: this.service, + method: this.method, + header: new Headers(), + contextValues: contextValues ?? createContextValues(), + message, + }; + type optParams = Parameters>[0]; + let opt: optParams = { + req, + // next is what actually kicks off the request. The run call below will + // ultimately call this for us. + next: async (req: UnaryRequest): Promise> => { + return new Promise((resolve, reject) => { + this.result = { success: resolve, failure: reject }; + this.startRequest(); + this.sendMessage(req.message.toBinary()); + }); + }, + }; + if (signal) { + opt.signal = signal; + } + if (timeoutMs) { + opt.timeoutMs = timeoutMs; + } + return runUnaryCall(opt); + } + + protected onHeaders(headers: ResponseHeaders): void { + if (this.headers !== undefined) { + this.result?.failure( + new Error('invariant: received headers more than once') + ); + return; + } + this.headers = toGRPCMetadata(headers.metadata); + } + + protected onTrailers(respTrailers: ResponseTrailers): void { + let trailers = toGRPCMetadata(respTrailers.metadata); + if (!respTrailers.status || respTrailers.status.code == 0) { + if (!this.headers) { + this.result?.failure( + new Error( + 'invariant: received trailers for successful unary request without headers' + ) + ); + return; + } + if (this.message === undefined) { + this.result?.failure( + new Error( + 'invariant: received trailers for successful unary request without message' + ) + ); + return; + } + this.result?.success({ + stream: false, + header: this.headers, + message: this.message, + trailer: trailers, + service: this.service, + method: this.method, + } satisfies UnaryResponse); + return; + } + this.result?.failure(respTrailers.status.message); + } + + protected onMessage(msgBytes: Uint8Array): void { + if (this.message !== undefined) { + this.result?.failure( + new Error('invariant: received two response messages for unary request') + ); + return; + } + this.message = this.parseMessage(msgBytes); + } +} diff --git a/rpc/js/src/dial.ts b/rpc/js/src/dial.ts index 169d7655..604d100b 100644 --- a/rpc/js/src/dial.ts +++ b/rpc/js/src/dial.ts @@ -1,35 +1,34 @@ -import { grpc } from '@improbable-eng/grpc-web'; -import type { ProtobufMessage } from '@improbable-eng/grpc-web/dist/typings/message'; -import { ClientChannel } from './ClientChannel'; -import { ConnectionClosedError } from './errors'; -import { Code } from './gen/google/rpc/code_pb'; -import { Status } from './gen/google/rpc/status_pb'; -import { - AuthenticateRequest, - AuthenticateResponse, - AuthenticateToRequest, - AuthenticateToResponse, - Credentials as PBCredentials, -} from './gen/proto/rpc/v1/auth_pb'; +import { Message } from '@bufbuild/protobuf'; + +import type { + AnyMessage, + MethodInfo, + PartialMessage, + ServiceType, +} from '@bufbuild/protobuf'; + +import type { + CallOptions, + ContextValues, + StreamResponse, + Transport, + UnaryResponse, +} from '@connectrpc/connect'; +import { Code, ConnectError, createPromiseClient } from '@connectrpc/connect'; import { AuthService, ExternalAuthService, -} from './gen/proto/rpc/v1/auth_pb_service'; +} from './gen/proto/rpc/v1/auth_connect'; import { - CallRequest, - CallResponse, - CallUpdateRequest, - CallUpdateResponse, - ICECandidate, - OptionalWebRTCConfigRequest, - OptionalWebRTCConfigResponse, - WebRTCConfig, -} from './gen/proto/rpc/webrtc/v1/signaling_pb'; -import { SignalingService } from './gen/proto/rpc/webrtc/v1/signaling_pb_service'; -import { addSdpFields, newPeerConnectionForClient } from './peer'; - -import { atob, btoa } from './polyfills'; -import { CrossBrowserHttpTransportInit } from '@improbable-eng/grpc-web/dist/typings/transports/http/http'; + AuthenticateRequest, + Credentials as PBCredentials, +} from './gen/proto/rpc/v1/auth_pb'; +import { SignalingService } from './gen/proto/rpc/webrtc/v1/signaling_connect'; +import { WebRTCConfig } from './gen/proto/rpc/webrtc/v1/signaling_pb'; +import { newPeerConnectionForClient } from './peer'; + +import { createGrpcWebTransport } from '@connectrpc/connect-web'; +import { SignalingExchange } from './SignalingExchange'; export interface DialOptions { authEntity?: string | undefined; @@ -90,21 +89,27 @@ export interface Credentials { payload: string; } +export type TransportFactory = ( + // platform specific + init: any +) => Transport; + +interface TransportInitOptions { + baseUrl: string; +} + export async function dialDirect( address: string, opts?: DialOptions -): Promise { +): Promise { validateDialOptions(opts); - const defaultFactory = (opts: grpc.TransportOptions): grpc.Transport => { - let TransFact: ( - init: CrossBrowserHttpTransportInit - ) => grpc.TransportFactory; - try { - TransFact = window.VIAM.GRPC_TRANSPORT_FACTORY; - } catch { - TransFact = grpc.CrossBrowserHttpTransport; - } - return TransFact({ withCredentials: false })(opts); + const createTransport = + typeof globalThis.VIAM?.GRPC_TRANSPORT_FACTORY === 'function' + ? globalThis.VIAM.GRPC_TRANSPORT_FACTORY + : createGrpcWebTransport; + + const transportOpts = { + baseUrl: address, }; // Client already has access token with no external auth, skip Authenticate process. @@ -112,70 +117,55 @@ export async function dialDirect( opts?.accessToken && !(opts?.externalAuthAddress && opts?.externalAuthToEntity) ) { - const md = new grpc.Metadata(); - md.set('authorization', `Bearer ${opts.accessToken}`); - return (opts: grpc.TransportOptions): grpc.Transport => { - return new authenticatedTransport(opts, defaultFactory, md); - }; + const headers = new Headers(); + headers.set('authorization', `Bearer ${opts.accessToken}`); + return new AuthenticatedTransport(transportOpts, createTransport, headers); } if (!opts || (!opts?.credentials && !opts?.accessToken)) { - return defaultFactory; + return createTransport(transportOpts); } - return makeAuthenticatedTransportFactory(address, defaultFactory, opts); + const authFact = await makeAuthenticatedTransportFactory( + address, + createTransport, + opts + ); + return authFact(transportOpts); } +const addressCleanupRegex = /^(.*:\/\/)/; + async function makeAuthenticatedTransportFactory( address: string, - defaultFactory: grpc.TransportFactory, + defaultFactory: TransportFactory, opts: DialOptions -): Promise { +): Promise { let accessToken = ''; - const getExtraMetadata = async (): Promise => { - const md = new grpc.Metadata(); + const getExtraHeaders = async (): Promise => { + const headers = new Headers(); // TODO(GOUT-10): handle expiration if (accessToken == '') { let thisAccessToken = ''; - let pResolve: (value: grpc.Metadata) => void; - let pReject: (reason?: unknown) => void; - if (!opts.accessToken || opts.accessToken === '') { - const request = new AuthenticateRequest(); - request.setEntity( - opts.authEntity ? opts.authEntity : address.replace(/^(.*:\/\/)/, '') - ); - const creds = new PBCredentials(); - creds.setType(opts.credentials?.type!); - creds.setPayload(opts.credentials?.payload!); - request.setCredentials(creds); - - let done = new Promise((resolve, reject) => { - pResolve = resolve; - pReject = reject; + const request = new AuthenticateRequest({ + entity: opts.authEntity + ? opts.authEntity + : address.replace(/^(.*:\/\/)/, ''), + credentials: new PBCredentials({ + type: opts.credentials?.type!, + payload: opts.credentials?.payload!, + }), }); - grpc.invoke(AuthService.Authenticate, { - request: request, - host: opts.externalAuthAddress ? opts.externalAuthAddress : address, - transport: defaultFactory, - onMessage: (message: AuthenticateResponse) => { - thisAccessToken = message.getAccessToken(); - }, - onEnd: ( - code: grpc.Code, - msg: string | undefined, - _trailers: grpc.Metadata - ) => { - if (code == grpc.Code.OK) { - pResolve(md); - } else { - pReject(msg); - } - }, - }); - await done; + const resolvedAddress = opts.externalAuthAddress + ? opts.externalAuthAddress + : address; + const transport = defaultFactory({ baseUrl: resolvedAddress }); + const authClient = createPromiseClient(AuthService, transport); + const resp = await authClient.authenticate(request); + thisAccessToken = resp.accessToken; } else { thisAccessToken = opts.accessToken; } @@ -183,142 +173,152 @@ async function makeAuthenticatedTransportFactory( accessToken = thisAccessToken; if (opts.externalAuthAddress && opts.externalAuthToEntity) { - const md = new grpc.Metadata(); - md.set('authorization', `Bearer ${accessToken}`); + const headers = new Headers(); + headers.set('authorization', `Bearer ${accessToken}`); - let done = new Promise((resolve, reject) => { - pResolve = resolve; - pReject = reject; - }); thisAccessToken = ''; - const request = new AuthenticateToRequest(); - request.setEntity(opts.externalAuthToEntity); - grpc.invoke(ExternalAuthService.AuthenticateTo, { - request: request, - host: opts.externalAuthAddress!, - transport: defaultFactory, - metadata: md, - onMessage: (message: AuthenticateToResponse) => { - thisAccessToken = message.getAccessToken(); - }, - onEnd: ( - code: grpc.Code, - msg: string | undefined, - _trailers: grpc.Metadata - ) => { - if (code == grpc.Code.OK) { - pResolve(md); - } else { - pReject(msg); - } - }, + const request = new AuthenticateRequest({ + entity: opts.externalAuthToEntity, + }); + const transport = defaultFactory({ + baseUrl: opts.externalAuthAddress!, }); - await done; + const externalAuthClient = createPromiseClient( + ExternalAuthService, + transport + ); + const resp = await externalAuthClient.authenticateTo(request); + thisAccessToken = resp.accessToken; accessToken = thisAccessToken; } } - md.set('authorization', `Bearer ${accessToken}`); - return md; + headers.set('authorization', `Bearer ${accessToken}`); + return headers; }; - const extraMd = await getExtraMetadata(); - return (opts: grpc.TransportOptions): grpc.Transport => { - return new authenticatedTransport(opts, defaultFactory, extraMd); + const extraMd = await getExtraHeaders(); + return (opts: TransportInitOptions): Transport => { + return new AuthenticatedTransport(opts, defaultFactory, extraMd); }; } -class authenticatedTransport implements grpc.Transport { - protected readonly opts: grpc.TransportOptions; - protected readonly transport: grpc.Transport; - protected readonly extraMetadata: grpc.Metadata; +class AuthenticatedTransport implements Transport { + protected readonly transport: Transport; + protected readonly extraHeaders: Headers; constructor( - opts: grpc.TransportOptions, - defaultFactory: grpc.TransportFactory, - extraMetadata: grpc.Metadata + opts: TransportInitOptions, + defaultFactory: TransportFactory, + extraHeaders: Headers ) { - this.opts = opts; - this.extraMetadata = extraMetadata; + this.extraHeaders = extraHeaders; this.transport = defaultFactory(opts); } - public start(metadata: grpc.Metadata) { - this.extraMetadata.forEach((key: string, values: string | string[]) => { - metadata.set(key, values); + public async unary< + I extends Message = AnyMessage, + O extends Message = AnyMessage, + >( + service: ServiceType, + method: MethodInfo, + signal: AbortSignal | undefined, + timeoutMs: number | undefined, + header: HeadersInit | undefined, + message: PartialMessage, + contextValues?: ContextValues + ): Promise> { + const newHeaders = cloneHeaders(header); + this.extraHeaders.forEach((value: string, key: string) => { + newHeaders.set(key, value); }); - this.transport.start(metadata); - } - - public sendMessage(msgBytes: Uint8Array) { - this.transport.sendMessage(msgBytes); + return this.transport.unary( + service, + method, + signal, + timeoutMs, + newHeaders, + message, + contextValues + ); } - public finishSend() { - this.transport.finishSend(); + public async stream< + I extends Message = AnyMessage, + O extends Message = AnyMessage, + >( + service: ServiceType, + method: MethodInfo, + signal: AbortSignal | undefined, + timeoutMs: number | undefined, + header: HeadersInit | undefined, + input: AsyncIterable>, + contextValues?: ContextValues + ): Promise> { + const newHeaders = cloneHeaders(header); + this.extraHeaders.forEach((value: string, key: string) => { + newHeaders.set(key, value); + }); + return this.transport.stream( + service, + method, + signal, + timeoutMs, + newHeaders, + input, + contextValues + ); } +} - public cancel() { - this.transport.cancel(); +export function cloneHeaders(headers: HeadersInit | undefined): Headers { + let cloned = new Headers(); + if (headers && headers !== undefined) { + if (Array.isArray(headers)) { + for (const [key, value] of headers) { + cloned.append(key, value); + } + } else if ('forEach' in headers) { + if (typeof headers.forEach == 'function') { + headers.forEach((value, key) => { + cloned.append(key, value); + }); + } + } else { + for (const [key, value] of Object.entries(headers)) { + cloned.append(key, value); + } + } } + return cloned; } export interface WebRTCConnection { - transportFactory: grpc.TransportFactory; + transport: Transport; peerConnection: RTCPeerConnection; dataChannel: RTCDataChannel; } async function getOptionalWebRTCConfig( signalingAddress: string, - host: string, - opts?: DialOptions + callOpts: CallOptions, + dialOpts?: DialOptions ): Promise { - const optsCopy = { ...opts } as DialOptions; + const optsCopy = { ...dialOpts } as DialOptions; const directTransport = await dialDirect(signalingAddress, optsCopy); - let pResolve: (value: WebRTCConfig) => void; - let pReject: (reason?: unknown) => void; - - let result: WebRTCConfig | undefined; - let done = new Promise((resolve, reject) => { - pResolve = resolve; - pReject = reject; - }); - - grpc.unary(SignalingService.OptionalWebRTCConfig, { - request: new OptionalWebRTCConfigRequest(), - metadata: { - 'rpc-host': host, - }, - host: signalingAddress, - transport: directTransport, - onEnd: (resp: grpc.UnaryOutput) => { - const { status, statusMessage, message } = resp; - if (status === grpc.Code.OK && message) { - result = message.getConfig(); - if (!result) { - pResolve(new WebRTCConfig()); - return; - } - pResolve(result); - // In some cases the `OptionalWebRTCConfig` method seems to be unimplemented, even - // when building `viam-server` from latest. Falling back to a default config seems - // harmless in these cases, and allows connection to continue. - } else if (status === grpc.Code.Unimplemented) { - pResolve(new WebRTCConfig()); - return; - } else { - pReject(statusMessage); - } - }, - }); - - await done; - - if (!result) { - throw new Error('no config'); + const signalingClient = createPromiseClient( + SignalingService, + directTransport + ); + try { + const resp = await signalingClient.optionalWebRTCConfig({}, callOpts); + return resp.config ?? new WebRTCConfig(); + } catch (err) { + if (err instanceof ConnectError && err.code == Code.Unimplemented) { + return new WebRTCConfig(); + } + throw err; } - return result; } // dialWebRTC makes a connection to given host by signaling with the address provided. A Promise is returned @@ -331,50 +331,28 @@ async function getOptionalWebRTCConfig( export async function dialWebRTC( signalingAddress: string, host: string, - opts?: DialOptions + dialOpts?: DialOptions ): Promise { signalingAddress = signalingAddress.replace(/(\/)$/, ''); - validateDialOptions(opts); + validateDialOptions(dialOpts); // TODO(RSDK-2836): In general, this logic should be in parity with the golang implementation. // https://github.com/viamrobotics/goutils/blob/main/rpc/wrtc_client.go#L160-L175 - const config = await getOptionalWebRTCConfig(signalingAddress, host, opts); - const additionalIceServers: RTCIceServer[] = config - .toObject() - .additionalIceServersList.map((ice) => { - return { - urls: ice.urlsList, - credential: ice.credential, - username: ice.username, - }; - }); - - if (!opts) { - opts = {}; - } + const callOpts = { + headers: { + 'rpc-host': host, + }, + }; - let webrtcOpts: DialWebRTCOptions; - if (!opts.webrtcOptions) { - // use additional webrtc config as default - webrtcOpts = { - disableTrickleICE: config.getDisableTrickle(), - rtcConfig: { - iceServers: additionalIceServers, - }, - }; - } else { - // RSDK-8715: We deep copy here to avoid mutating the input config's `rtcConfig.iceServers` - // list. - webrtcOpts = JSON.parse(JSON.stringify(opts.webrtcOptions)); - if (!webrtcOpts.rtcConfig) { - webrtcOpts.rtcConfig = { iceServers: additionalIceServers }; - } else { - webrtcOpts.rtcConfig.iceServers = [ - ...(webrtcOpts.rtcConfig.iceServers || []), - ...additionalIceServers, - ]; - } - } + // first complete our WebRTC options, gathering any extra information like + // TURN servers from a cloud server. + const webrtcOpts = await processWebRTCOpts( + signalingAddress, + callOpts, + dialOpts + ); + // then derive options specifically for signaling against our target. + const exchangeOpts = processSignalingExchangeOpts(signalingAddress, dialOpts); const { pc, dc } = await newPeerConnectionForClient( webrtcOpts !== undefined && webrtcOpts.disableTrickleICE, @@ -383,309 +361,58 @@ export async function dialWebRTC( ); let successful = false; + let directTransport: Transport; try { - // replace auth entity and creds - let optsCopy = opts; - if (opts) { - optsCopy = { ...opts } as DialOptions; - - if (!opts.accessToken) { - optsCopy.authEntity = opts?.webrtcOptions?.signalingAuthEntity; - if (!optsCopy.authEntity) { - if (optsCopy.externalAuthAddress) { - optsCopy.authEntity = opts.externalAuthAddress?.replace( - /^(.*:\/\/)/, - '' - ); - } else { - optsCopy.authEntity = signalingAddress.replace(/^(.*:\/\/)/, ''); - } - } - optsCopy.credentials = opts?.webrtcOptions?.signalingCredentials; - optsCopy.accessToken = opts?.webrtcOptions?.signalingAccessToken; - } - - optsCopy.externalAuthAddress = - opts?.webrtcOptions?.signalingExternalAuthAddress; - optsCopy.externalAuthToEntity = - opts?.webrtcOptions?.signalingExternalAuthToEntity; - } + directTransport = await dialDirect(signalingAddress, exchangeOpts); + } catch (err) { + pc.close(); + throw err; + } - const directTransport = await dialDirect(signalingAddress, optsCopy); - const client = grpc.client(SignalingService.Call, { - host: signalingAddress, - transport: directTransport, - }); + const signalingClient = createPromiseClient( + SignalingService, + directTransport + ); - let uuid = ''; - // only send once since exchange may end or ICE may end - let sentDoneOrErrorOnce = false; - const sendError = (err: string) => { - if (sentDoneOrErrorOnce) { - return; - } - sentDoneOrErrorOnce = true; - const callRequestUpdate = new CallUpdateRequest(); - callRequestUpdate.setUuid(uuid); - const status = new Status(); - status.setCode(Code.UNKNOWN); - status.setMessage(err); - callRequestUpdate.setError(status); - grpc.unary(SignalingService.CallUpdate, { - request: callRequestUpdate, - metadata: { - 'rpc-host': host, - }, - host: signalingAddress, - transport: directTransport, - onEnd: (output: grpc.UnaryOutput) => { - const { status, statusMessage, message } = output; - if (status === grpc.Code.OK && message) { - return; - } - console.error(statusMessage); - }, - }); - }; - const sendDone = () => { - if (sentDoneOrErrorOnce) { - return; - } - sentDoneOrErrorOnce = true; - const callRequestUpdate = new CallUpdateRequest(); - callRequestUpdate.setUuid(uuid); - callRequestUpdate.setDone(true); - grpc.unary(SignalingService.CallUpdate, { - request: callRequestUpdate, - metadata: { - 'rpc-host': host, - }, - host: signalingAddress, - transport: directTransport, - onEnd: (output: grpc.UnaryOutput) => { - const { status, statusMessage, message } = output; - if (status === grpc.Code.OK && message) { - return; + const exchange = new SignalingExchange( + signalingClient, + callOpts, + pc, + dc, + webrtcOpts + ); + try { + // set timeout for dial attempt if a timeout is specified + if (dialOpts?.dialTimeout) { + setTimeout( + () => { + if (!successful) { + exchange.terminate(); } - console.error(statusMessage); }, - }); - }; - - let pResolve: (value: unknown) => void; - let remoteDescSet = new Promise((resolve) => { - pResolve = resolve; - }); - let exchangeDone = false; - if (!webrtcOpts?.disableTrickleICE) { - // set up offer - const offerDesc = await pc.createOffer({}); - - let iceComplete = false; - let numCallUpdates = 0; - let maxCallUpdateDuration = 0; - let totalCallUpdateDuration = 0; - - pc.addEventListener('iceconnectionstatechange', () => { - if (pc.iceConnectionState !== 'completed' || numCallUpdates === 0) { - return; - } - let averageCallUpdateDuration = - totalCallUpdateDuration / numCallUpdates; - console.groupCollapsed('Caller update statistics'); - console.table({ - num_updates: numCallUpdates, - average_duration: `${averageCallUpdateDuration}ms`, - max_duration: `${maxCallUpdateDuration}ms`, - }); - console.groupEnd(); - }); - pc.addEventListener( - 'icecandidate', - async (event: { candidate: RTCIceCandidateInit | null }) => { - await remoteDescSet; - if (exchangeDone) { - return; - } - - if (event.candidate === null) { - iceComplete = true; - sendDone(); - return; - } - - if (event.candidate.candidate !== null) { - console.debug(`gathered local ICE ${event.candidate.candidate}`); - } - const iProto = iceCandidateToProto(event.candidate); - const callRequestUpdate = new CallUpdateRequest(); - callRequestUpdate.setUuid(uuid); - callRequestUpdate.setCandidate(iProto); - const callUpdateStart = new Date(); - grpc.unary(SignalingService.CallUpdate, { - request: callRequestUpdate, - metadata: { - 'rpc-host': host, - }, - host: signalingAddress, - transport: directTransport, - onEnd: (output: grpc.UnaryOutput) => { - const { status, statusMessage, message } = output; - if (status === grpc.Code.OK && message) { - numCallUpdates++; - let callUpdateEnd = new Date(); - let callUpdateDuration = - callUpdateEnd.getTime() - callUpdateStart.getTime(); - if (callUpdateDuration > maxCallUpdateDuration) { - maxCallUpdateDuration = callUpdateDuration; - } - totalCallUpdateDuration += callUpdateDuration; - return; - } - if (exchangeDone || iceComplete) { - return; - } - console.error('error sending candidate', statusMessage); - }, - }); - } + dialOpts?.dialTimeout ); - - await pc.setLocalDescription(offerDesc); - } - - // initialize cc here so we can use it in the callbacks - let cc: ClientChannel; - - let haveInit = false; - // TS says that CallResponse isn't a valid type here. More investigation required. - client.onMessage(async (message: ProtobufMessage) => { - const response = message as CallResponse; - - if (response.hasInit()) { - if (haveInit) { - sendError('got init stage more than once'); - return; - } - const init = response.getInit()!; - haveInit = true; - uuid = response.getUuid(); - - const remoteSDP = new RTCSessionDescription( - JSON.parse(atob(init.getSdp())) - ); - if (cc?.isClosed()) { - sendError('client channel is closed'); - return; - } - await pc.setRemoteDescription(remoteSDP); - - pResolve(true); - - if (webrtcOpts?.disableTrickleICE) { - exchangeDone = true; - sendDone(); - return; - } - } else if (response.hasUpdate()) { - if (!haveInit) { - sendError('got update stage before init stage'); - return; - } - if (response.getUuid() !== uuid) { - sendError(`uuid mismatch; have=${response.getUuid()} want=${uuid}`); - return; - } - const update = response.getUpdate()!; - const cand = iceCandidateFromProto(update.getCandidate()!); - if (cand.candidate !== null) { - console.debug(`received remote ICE ${cand.candidate}`); - } - try { - await pc.addIceCandidate(cand); - } catch (error) { - sendError(JSON.stringify(error)); - return; - } - } else { - sendError('unknown CallResponse stage'); - return; - } - }); - - let clientEndResolve: () => void; - let clientEndReject: (reason?: unknown) => void; - let clientEnd = new Promise((resolve, reject) => { - clientEndResolve = resolve; - clientEndReject = reject; - }); - client.onEnd( - (status: grpc.Code, statusMessage: string, _trailers: grpc.Metadata) => { - if (status === grpc.Code.OK) { - clientEndResolve(); - return; - } - if (statusMessage === 'Response closed without headers') { - clientEndReject(new ConnectionClosedError('failed to dial')); - return; - } - if (cc?.isClosed()) { - clientEndReject( - new ConnectionClosedError('client channel is closed') - ); - return; - } - console.error(statusMessage); - clientEndReject(statusMessage); - } - ); - client.start({ 'rpc-host': host }); - - const callRequest = new CallRequest(); - const description = addSdpFields( - pc.localDescription, - opts.webrtcOptions?.additionalSdpFields - ); - const encodedSDP = btoa(JSON.stringify(description)); - callRequest.setSdp(encodedSDP); - if (webrtcOpts && webrtcOpts.disableTrickleICE) { - callRequest.setDisableTrickle(webrtcOpts.disableTrickleICE); - } - client.send(callRequest); - - cc = new ClientChannel(pc, dc); - - // set timeout for dial attempt if a timeout is specified - if (opts.dialTimeout) { - setTimeout(() => { - if (!successful) { - cc.close(); - } - }, opts.dialTimeout); } - cc.ready - .then(() => clientEndResolve()) - .catch((err) => clientEndReject(err)); - await clientEnd; - await cc.ready; - exchangeDone = true; - sendDone(); + const cc = await exchange.doExchange(); - if (opts?.externalAuthAddress) { + if (dialOpts?.externalAuthAddress) { // TODO(GOUT-11): prepare AuthenticateTo here // for client channel. - } else if (opts?.credentials?.type) { + } else if (dialOpts?.credentials?.type) { // TODO(GOUT-11): prepare Authenticate here // for client channel } successful = true; return { - transportFactory: cc.transportFactory(), + transport: cc, peerConnection: pc, dataChannel: dc, }; + } catch (err) { + console.error('error dialing', err); + throw err; } finally { if (!successful) { pc.close(); @@ -693,35 +420,91 @@ export async function dialWebRTC( } } -function iceCandidateFromProto(i: ICECandidate): RTCIceCandidateInit { - let candidate: RTCIceCandidateInit = { - candidate: i.getCandidate(), - }; - if (i.hasSdpMid()) { - candidate.sdpMid = i.getSdpMid(); - } - if (i.hasSdpmLineIndex()) { - candidate.sdpMLineIndex = i.getSdpmLineIndex(); +async function processWebRTCOpts( + signalingAddress: string, + callOpts: CallOptions, + dialOpts?: DialOptions +): Promise { + // Get TURN servers, if any. + const config = await getOptionalWebRTCConfig( + signalingAddress, + callOpts, + dialOpts + ); + const additionalIceServers: RTCIceServer[] = config.additionalIceServers.map( + (ice) => { + return { + urls: ice.urls, + credential: ice.credential, + username: ice.username, + }; + } + ); + + if (!dialOpts) { + dialOpts = {}; } - if (i.hasUsernameFragment()) { - candidate.usernameFragment = i.getUsernameFragment(); + + let webrtcOpts: DialWebRTCOptions; + if (!dialOpts.webrtcOptions) { + // use additional webrtc config as default + webrtcOpts = { + disableTrickleICE: config.disableTrickle, + rtcConfig: { + iceServers: additionalIceServers, + }, + }; + } else { + // RSDK-8715: We deep copy here to avoid mutating the input config's `rtcConfig.iceServers` + // list. + webrtcOpts = JSON.parse(JSON.stringify(dialOpts.webrtcOptions)); + if (!webrtcOpts.rtcConfig) { + webrtcOpts.rtcConfig = { iceServers: additionalIceServers }; + } else { + webrtcOpts.rtcConfig.iceServers = [ + ...(webrtcOpts.rtcConfig.iceServers || []), + ...additionalIceServers, + ]; + } } - return candidate; + + return webrtcOpts; } -function iceCandidateToProto(i: RTCIceCandidateInit): ICECandidate { - let candidate = new ICECandidate(); - candidate.setCandidate(i.candidate!); - if (i.sdpMid) { - candidate.setSdpMid(i.sdpMid); - } - if (i.sdpMLineIndex) { - candidate.setSdpmLineIndex(i.sdpMLineIndex); - } - if (i.usernameFragment) { - candidate.setUsernameFragment(i.usernameFragment); +function processSignalingExchangeOpts( + signalingAddress: string, + dialOpts?: DialOptions +) { + // replace auth entity and creds + let optsCopy = dialOpts; + if (dialOpts) { + optsCopy = { ...dialOpts } as DialOptions; + + if (!dialOpts.accessToken) { + optsCopy.authEntity = dialOpts?.webrtcOptions?.signalingAuthEntity; + if (!optsCopy.authEntity) { + if (optsCopy.externalAuthAddress) { + optsCopy.authEntity = dialOpts.externalAuthAddress?.replace( + addressCleanupRegex, + '' + ); + } else { + optsCopy.authEntity = signalingAddress.replace( + addressCleanupRegex, + '' + ); + } + } + optsCopy.credentials = dialOpts?.webrtcOptions?.signalingCredentials; + optsCopy.accessToken = dialOpts?.webrtcOptions?.signalingAccessToken; + } + + optsCopy.externalAuthAddress = + dialOpts?.webrtcOptions?.signalingExternalAuthAddress; + optsCopy.externalAuthToEntity = + dialOpts?.webrtcOptions?.signalingExternalAuthToEntity; } - return candidate; + return optsCopy; } function validateDialOptions(opts?: DialOptions) { diff --git a/rpc/js/src/main.ts b/rpc/js/src/main.ts index 2569d99d..582a64b3 100644 --- a/rpc/js/src/main.ts +++ b/rpc/js/src/main.ts @@ -1,7 +1,10 @@ +import { Transport } from '@connectrpc/connect'; + declare global { - interface Window { - VIAM: any; - } + // eslint-disable-next-line vars-on-top,no-var + var VIAM: { + GRPC_TRANSPORT_FACTORY: (opts: any) => Transport; + }; } export { diff --git a/rpc/js/tsconfig.json b/rpc/js/tsconfig.json index 00bb11ce..e719e96e 100644 --- a/rpc/js/tsconfig.json +++ b/rpc/js/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@viamrobotics/typescript-config/base.json", "compilerOptions": { - "target": "ES5", + "target": "esnext", "useDefineForClassFields": true, "importHelpers": true, "module": "ESNext", diff --git a/rpc/js/vite.config.ts b/rpc/js/vite.config.ts index 0d5058d4..f48d71bb 100644 --- a/rpc/js/vite.config.ts +++ b/rpc/js/vite.config.ts @@ -9,18 +9,6 @@ export default defineConfig({ __VERSION__: JSON.stringify(pkg.version), }, build: { - // This config is necessary to transform libraries on the list into ES modules. - // This can be removed if protobuf-es or a code generating tool that has good - // support for ES modules is used. - commonjsOptions: { - transformMixedEsModules: true, - include: [ - /exponential-backoff/u, - /google-protobuf/u, - /@improbable-eng\/grpc-web/u, - /gen\//u, - ], - }, lib: { entry: 'src/main.ts', formats: ['es'], From cf8b5a956530e5e67eaffddeae5e0b48ca0c9970 Mon Sep 17 00:00:00 2001 From: Eric Daniels Date: Sun, 22 Sep 2024 02:15:14 -0400 Subject: [PATCH 2/7] Update echo example for connect-es --- rpc/examples/echo/frontend/package-lock.json | 109 ++++---- rpc/examples/echo/frontend/package.json | 8 +- rpc/examples/echo/frontend/src/index.ts | 246 +++++++++---------- rpc/examples/echo/frontend/vite.config.ts | 7 - rpc/examples/echo/tests/echo.spec.ts | 6 +- 5 files changed, 177 insertions(+), 199 deletions(-) diff --git a/rpc/examples/echo/frontend/package-lock.json b/rpc/examples/echo/frontend/package-lock.json index bd0243cc..291fba1a 100644 --- a/rpc/examples/echo/frontend/package-lock.json +++ b/rpc/examples/echo/frontend/package-lock.json @@ -9,12 +9,12 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "@viamrobotics/rpc": "file:../../../js", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@viamrobotics/rpc": "file:../../../js" }, "devDependencies": { - "@types/google-protobuf": "^3.7.4", "ansi-regex": ">=5.0.1", "concurrently": "^5.3.0", "set-value": ">=4.0.1", @@ -29,11 +29,11 @@ "version": "0.2.6", "license": "Apache-2.0", "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0" }, "devDependencies": { - "@types/google-protobuf": "^3.7.4", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "@viamrobotics/eslint-config": "^0.2.6", @@ -52,6 +52,31 @@ "vite": "^4.0.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.5.0.tgz", + "integrity": "sha512-1gGg0M6c2Y3lnr5itis9dNj9r8hbOIuBMqoGSbUy7L7Vjw4MAttjJzJfj9HCDgytGCJkGanYEYI6MQVDijdVQw==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.5.0.tgz", + "integrity": "sha512-xjiiQ932Kibddaka18fGZ6yQL7xjXuLcYFYh/cU+q1WWEIrFPkZfViG/Ee6yrZbrlZkjcBuDibng+q7baTndfg==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.5.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -404,17 +429,6 @@ "node": ">=12" } }, - "node_modules/@improbable-eng/grpc-web": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", - "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", - "dependencies": { - "browser-headers": "^0.4.0" - }, - "peerDependencies": { - "google-protobuf": "^3.2.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -508,12 +522,6 @@ "dev": true, "peer": true }, - "node_modules/@types/google-protobuf": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.2.tgz", - "integrity": "sha512-ubeqvw7sl6CdgeiIilsXB2jIFoD/D0F+/LIEp7xEBEXRNtDJcf05FRINybsJtL7GlkWOUVn6gJs2W9OF+xI6lg==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -802,11 +810,6 @@ "node": ">=8" } }, - "node_modules/browser-headers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" - }, "node_modules/browserslist": { "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", @@ -1243,7 +1246,8 @@ "node_modules/google-protobuf": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", - "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==" + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", + "dev": true }, "node_modules/graceful-fs": { "version": "4.2.10", @@ -2498,6 +2502,23 @@ } }, "dependencies": { + "@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==" + }, + "@connectrpc/connect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.5.0.tgz", + "integrity": "sha512-1gGg0M6c2Y3lnr5itis9dNj9r8hbOIuBMqoGSbUy7L7Vjw4MAttjJzJfj9HCDgytGCJkGanYEYI6MQVDijdVQw==", + "requires": {} + }, + "@connectrpc/connect-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.5.0.tgz", + "integrity": "sha512-xjiiQ932Kibddaka18fGZ6yQL7xjXuLcYFYh/cU+q1WWEIrFPkZfViG/Ee6yrZbrlZkjcBuDibng+q7baTndfg==", + "requires": {} + }, "@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -2652,14 +2673,6 @@ "dev": true, "optional": true }, - "@improbable-eng/grpc-web": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", - "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", - "requires": { - "browser-headers": "^0.4.0" - } - }, "@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -2744,12 +2757,6 @@ "dev": true, "peer": true }, - "@types/google-protobuf": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.2.tgz", - "integrity": "sha512-ubeqvw7sl6CdgeiIilsXB2jIFoD/D0F+/LIEp7xEBEXRNtDJcf05FRINybsJtL7GlkWOUVn6gJs2W9OF+xI6lg==", - "dev": true - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2767,8 +2774,9 @@ "@viamrobotics/rpc": { "version": "file:../../../js", "requires": { - "@improbable-eng/grpc-web": "^0.13.0", - "@types/google-protobuf": "^3.7.4", + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "@viamrobotics/eslint-config": "^0.2.6", @@ -2778,7 +2786,6 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-sonarjs": "^0.21.0", "eslint-plugin-unicorn": "^48.0.1", - "google-protobuf": "^3.14.0", "prettier": "^3.0.3", "set-value": ">=4.0.1", "ts-loader": "^9.4.1", @@ -3029,11 +3036,6 @@ "fill-range": "^7.0.1" } }, - "browser-headers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" - }, "browserslist": { "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", @@ -3367,7 +3369,8 @@ "google-protobuf": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", - "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==" + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", + "dev": true }, "graceful-fs": { "version": "4.2.10", diff --git a/rpc/examples/echo/frontend/package.json b/rpc/examples/echo/frontend/package.json index a801847b..31bb0bcd 100644 --- a/rpc/examples/echo/frontend/package.json +++ b/rpc/examples/echo/frontend/package.json @@ -3,14 +3,14 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "@viamrobotics/rpc": "file:../../../js", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@viamrobotics/rpc": "file:../../../js" }, "devDependencies": { "ansi-regex": ">=5.0.1", "set-value": ">=4.0.1", - "@types/google-protobuf": "^3.7.4", "concurrently": "^5.3.0", "ts-loader": "^8.0.14", "ts-protoc-gen": "^0.14.0", diff --git a/rpc/examples/echo/frontend/src/index.ts b/rpc/examples/echo/frontend/src/index.ts index ee67c1fe..5d7dedc9 100644 --- a/rpc/examples/echo/frontend/src/index.ts +++ b/rpc/examples/echo/frontend/src/index.ts @@ -1,19 +1,22 @@ -import { grpc } from "@improbable-eng/grpc-web"; +import type { PartialMessage } from "@bufbuild/protobuf"; +import { ConnectError, createPromiseClient, PromiseClient } from "@connectrpc/connect"; +import { createWritableIterable } from "@connectrpc/connect/protocol"; import { Credentials, dialDirect, dialWebRTC } from "@viamrobotics/rpc"; import { DialOptions } from "@viamrobotics/rpc/src/dial"; -import { EchoBiDiRequest, EchoMultipleRequest, EchoMultipleResponse, EchoRequest, EchoResponse } from "./gen/proto/rpc/examples/echo/v1/echo_pb"; -import { EchoServiceClient, ServiceError } from "./gen/proto/rpc/examples/echo/v1/echo_pb_service"; +import { createWriteStream } from "fs"; +import { EchoService } from "./gen/proto/rpc/examples/echo/v1/echo_connect"; +import { EchoBiDiRequest, EchoBiDiResponse, EchoMultipleRequest, EchoMultipleResponse, EchoRequest, EchoResponse } from "./gen/proto/rpc/examples/echo/v1/echo_pb"; const thisHost = `${window.location.protocol}//${window.location.host}`; declare global { - interface Window { - webrtcHost: string; - creds?: Credentials; - externalAuthAddr?: string; - externalAuthToEntity?: string; - accessToken?: string; - } + interface Window { + webrtcHost: string; + creds?: Credentials; + externalAuthAddr?: string; + externalAuthToEntity?: string; + accessToken?: string; + } } function createElemForResponse(text: string, method: string, type: string) { @@ -29,129 +32,110 @@ function createElemForResponse(text: string, method: string, type: string) { } async function getClients() { - const webrtcHost = window.webrtcHost; - const opts: DialOptions = { - externalAuthAddress: window.externalAuthAddr, - externalAuthToEntity: window.externalAuthToEntity, - webrtcOptions: { - disableTrickleICE: false, - } - }; - - if (!window.accessToken) { - opts.credentials = window.creds; - opts.webrtcOptions!.signalingCredentials = window.creds; - } else { - opts.accessToken = window.accessToken; - } - - if (opts.externalAuthAddress) { - if (!window.accessToken) { - // we are authenticating against the external address and then - // we will authenticate for externalAuthToEntity. - opts.authEntity = opts.externalAuthAddress.replace(/^(.*:\/\/)/, ''); - } - - // do similar for WebRTC - opts.webrtcOptions!.signalingExternalAuthAddress = opts.externalAuthAddress; - opts.webrtcOptions!.signalingExternalAuthToEntity = opts.externalAuthToEntity; - } - - const webRTCConn = await dialWebRTC(thisHost, webrtcHost, opts); - const webrtcClient = new EchoServiceClient(webrtcHost, { transport: webRTCConn.transportFactory }); - await renderResponses(webrtcClient, "wrtc"); - - const directTransport = await dialDirect(thisHost, opts); - const directClient = new EchoServiceClient(thisHost, { transport: directTransport }); - await renderResponses(directClient, "direct"); + const webrtcHost = window.webrtcHost; + const opts: DialOptions = { + externalAuthAddress: window.externalAuthAddr, + externalAuthToEntity: window.externalAuthToEntity, + webrtcOptions: { + disableTrickleICE: false, + } + }; + + if (!window.accessToken) { + opts.credentials = window.creds; + opts.webrtcOptions!.signalingCredentials = window.creds; + } else { + opts.accessToken = window.accessToken; + } + + if (opts.externalAuthAddress) { + if (!window.accessToken) { + // we are authenticating against the external address and then + // we will authenticate for externalAuthToEntity. + opts.authEntity = opts.externalAuthAddress.replace(/^(.*:\/\/)/, ''); + } + + // do similar for WebRTC + opts.webrtcOptions!.signalingExternalAuthAddress = opts.externalAuthAddress; + opts.webrtcOptions!.signalingExternalAuthToEntity = opts.externalAuthToEntity; + } + + const webRTCConn = await dialWebRTC(thisHost, webrtcHost, opts); + const webrtcClient = createPromiseClient(EchoService, webRTCConn.transport); + await renderResponses(webrtcClient, "wrtc"); + + const directTransport = await dialDirect(thisHost, opts); + const directClient = createPromiseClient(EchoService, directTransport); + await renderResponses(directClient, "direct"); } + getClients().catch(e => { - console.error("error getting clients", e); + console.error("error getting clients", e); }); -async function renderResponses(client: EchoServiceClient, method: string) { - const echoRequest = new EchoRequest(); - echoRequest.setMessage("hello"); - - let pResolve: (value: any) => void; - let pReject: (reason?: any) => void; - let done = new Promise((resolve, reject) => { - pResolve = resolve; - pReject = reject; - }); - client.echo(echoRequest, (err: ServiceError, resp: EchoResponse) => { - if (err) { - console.error(err); - pReject(err); - return - } - createElemForResponse(resp.getMessage(), method, "unary"); - pResolve(resp); - }); - await done; - - const echoMultipleRequest = new EchoMultipleRequest(); - echoMultipleRequest.setMessage("hello?"); - - done = new Promise((resolve, reject) => { - pResolve = resolve; - pReject = reject; - }); - const multiStream = client.echoMultiple(echoMultipleRequest); - multiStream.on("data", (message: EchoMultipleResponse) => { - createElemForResponse(message.getMessage(), method, "multi"); - }); - multiStream.on("end", ({ code, details }: { code: number, details: string, metadata: grpc.Metadata }) => { - if (code !== 0) { - console.log(code); - console.log(details); - pReject(code); - return; - } - pResolve(undefined); - }); - await done; - - const bidiStream = client.echoBiDi(); - - let echoBiDiRequest = new EchoBiDiRequest(); - echoBiDiRequest.setMessage("one"); - - done = new Promise((resolve, reject) => { - pResolve = resolve; - pReject = reject; - }); - - let msgCount = 0; - bidiStream.on("data", (message: EchoMultipleResponse) => { - msgCount++ - createElemForResponse(message.getMessage(), method, "bidi"); - if (msgCount == 3) { - pResolve(undefined); - } - }); - bidiStream.on("end", ({ code, details }: { code: number, details: string, metadata: grpc.Metadata }) => { - if (code !== 0) { - console.log(code); - console.log(details); - pReject(code); - return; - } - }); - - bidiStream.write(echoBiDiRequest); - await done; - - done = new Promise((resolve, reject) => { - pResolve = resolve; - pReject = reject; - }); - msgCount = 0; - - echoBiDiRequest = new EchoBiDiRequest(); - echoBiDiRequest.setMessage("two"); - bidiStream.write(echoBiDiRequest); - - await done; - bidiStream.end(); +async function renderResponses(client: PromiseClient, method: string) { + const echoRequest = new EchoRequest(); + echoRequest.message = "hello"; + + const response = await client.echo(echoRequest); + createElemForResponse(response.message, method, "unary"); + + const echoMultipleRequest = new EchoMultipleRequest(); + echoMultipleRequest.message = "hello?"; + + const multiStream = client.echoMultiple(echoMultipleRequest); + try { + for await (const response of multiStream) { + createElemForResponse(response.message, method, "multi"); + } + } catch (err) { + if (err instanceof ConnectError) { + console.log(err.code); + console.log(err.details); + } + throw err; + } + + if (method === "direct") { + // grpc-web cannot do bidi correctly. Previous versions + // of this code just stalled after the first message was received. + return; + } + + const clientStream = createWritableIterable>(); + const bidiStream = client.echoBiDi(clientStream); + + let msgCount = 0; + const processResponses = async () => { + try { + for await (const response of bidiStream) { + msgCount++ + createElemForResponse(response.message, method, "bidi"); + if (msgCount == 3) { + return; + } + } + } catch (err) { + if (err instanceof ConnectError) { + console.log(err.code); + console.log(err.details); + } + throw err; + } + } + + let echoBiDiRequest = new EchoBiDiRequest(); + echoBiDiRequest.message = "one"; + + clientStream.write(echoBiDiRequest); + await processResponses(); + + msgCount = 0; + + echoBiDiRequest = new EchoBiDiRequest(); + echoBiDiRequest.message = "two"; + clientStream.write(echoBiDiRequest); + + await processResponses(); + clientStream.close(); } diff --git a/rpc/examples/echo/frontend/vite.config.ts b/rpc/examples/echo/frontend/vite.config.ts index 9b6133df..4aa1a0ac 100644 --- a/rpc/examples/echo/frontend/vite.config.ts +++ b/rpc/examples/echo/frontend/vite.config.ts @@ -9,13 +9,6 @@ export default defineConfig({ __VERSION__: JSON.stringify(pkg.version), }, build: { - // This config is necessary to transform libraries on the list into ES modules. - // This can be removed if protobuf-es or a code generating tool that has good - // support for ES modules is used. - commonjsOptions: { - transformMixedEsModules: true, - include: [/google-protobuf/u, /@improbable-eng\/grpc-web/u, /gen\//u], - }, target: "esnext", lib: { entry: "src/index.ts", diff --git a/rpc/examples/echo/tests/echo.spec.ts b/rpc/examples/echo/tests/echo.spec.ts index 78a86cc2..42667243 100644 --- a/rpc/examples/echo/tests/echo.spec.ts +++ b/rpc/examples/echo/tests/echo.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; test("receives responses", async ({ page }) => { await page.goto("/"); @@ -8,9 +8,7 @@ test("receives responses", async ({ page }) => { ["bidi-wrtc", ["o", "n", "e", "t", "w", "o"]], ["unary-direct", ["hello"]], ["multi-direct", ["h", "e", "l", "l", "o", "?"]], - // gRPC-web does not yet support bidirectional streaming so we expect to - // only receive a response to our first request. - ["bidi-direct", ["o", "n", "e"]], + ["bidi-direct", []], ]; for (const [testID, expected] of table) { From 4f6a851479452cfaa2caa71d1537f0c095752b21 Mon Sep 17 00:00:00 2001 From: Eric Daniels Date: Sun, 22 Sep 2024 02:15:33 -0400 Subject: [PATCH 3/7] Update fileupload example for connect-es --- .../examples/fileupload/v1/fileupload.pb.go | 14 +- .../fileupload/v1/fileupload.pb.gw.go | 43 ++--- .../examples/fileupload/v1/fileupload.proto | 4 +- .../fileupload/v1/fileupload_grpc.pb.go | 16 +- .../fileupload/frontend/package-lock.json | 159 +++++++++++------- rpc/examples/fileupload/frontend/package.json | 8 +- rpc/examples/fileupload/frontend/src/index.ts | 44 ++--- .../fileupload/frontend/vite.config.ts | 7 - rpc/examples/fileupload/server/server.go | 2 +- 9 files changed, 160 insertions(+), 137 deletions(-) diff --git a/proto/rpc/examples/fileupload/v1/fileupload.pb.go b/proto/rpc/examples/fileupload/v1/fileupload.pb.go index 63c5ddde..39b6e198 100644 --- a/proto/rpc/examples/fileupload/v1/fileupload.pb.go +++ b/proto/rpc/examples/fileupload/v1/fileupload.pb.go @@ -173,8 +173,8 @@ var file_proto_rpc_examples_fileupload_v1_fileupload_proto_rawDesc = []byte{ 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x32, 0x92, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6c, 0x65, - 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7d, 0x0a, + 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x32, 0x90, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6c, 0x65, + 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a, 0x0a, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x33, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, @@ -182,11 +182,11 @@ var file_proto_rpc_examples_fileupload_v1_fileupload_proto_rawDesc = []byte{ 0x1a, 0x34, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x72, 0x70, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x34, 0x5a, 0x32, - 0x67, 0x6f, 0x2e, 0x76, 0x69, 0x61, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x74, 0x69, 0x6c, - 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x2f, - 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x6f, + 0x2e, 0x76, 0x69, 0x61, 0x6d, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x73, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x2f, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/rpc/examples/fileupload/v1/fileupload.pb.gw.go b/proto/rpc/examples/fileupload/v1/fileupload.pb.gw.go index 5d8dc05a..41eb8ff7 100644 --- a/proto/rpc/examples/fileupload/v1/fileupload.pb.gw.go +++ b/proto/rpc/examples/fileupload/v1/fileupload.pb.gw.go @@ -31,7 +31,7 @@ var _ = runtime.String var _ = utilities.NewDoubleArray var _ = metadata.Join -func request_FileUploadService_UploadFile_0(ctx context.Context, marshaler runtime.Marshaler, client FileUploadServiceClient, req *http.Request, pathParams map[string]string) (FileUploadService_UploadFileClient, runtime.ServerMetadata, error) { +func request_FileUploadService_UploadFile_0(ctx context.Context, marshaler runtime.Marshaler, client FileUploadServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var metadata runtime.ServerMetadata stream, err := client.UploadFile(ctx) if err != nil { @@ -39,39 +39,40 @@ func request_FileUploadService_UploadFile_0(ctx context.Context, marshaler runti return nil, metadata, err } dec := marshaler.NewDecoder(req.Body) - handleSend := func() error { + for { var protoReq UploadFileRequest - err := dec.Decode(&protoReq) + err = dec.Decode(&protoReq) if err == io.EOF { - return err + break } if err != nil { grpclog.Infof("Failed to decode request: %v", err) - return err + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } - if err := stream.Send(&protoReq); err != nil { - grpclog.Infof("Failed to send request: %v", err) - return err - } - return nil - } - go func() { - for { - if err := handleSend(); err != nil { + if err = stream.Send(&protoReq); err != nil { + if err == io.EOF { break } + grpclog.Infof("Failed to send request: %v", err) + return nil, metadata, err } - if err := stream.CloseSend(); err != nil { - grpclog.Infof("Failed to terminate client stream: %v", err) - } - }() + } + + if err := stream.CloseSend(); err != nil { + grpclog.Infof("Failed to terminate client stream: %v", err) + return nil, metadata, err + } header, err := stream.Header() if err != nil { grpclog.Infof("Failed to get header from client: %v", err) return nil, metadata, err } metadata.HeaderMD = header - return stream, metadata, nil + + msg, err := stream.CloseAndRecv() + metadata.TrailerMD = stream.Trailer() + return msg, metadata, err + } // RegisterFileUploadServiceHandlerServer registers the http handlers for service FileUploadService to "mux". @@ -146,7 +147,7 @@ func RegisterFileUploadServiceHandlerClient(ctx context.Context, mux *runtime.Se return } - forward_FileUploadService_UploadFile_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + forward_FileUploadService_UploadFile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -158,5 +159,5 @@ var ( ) var ( - forward_FileUploadService_UploadFile_0 = runtime.ForwardResponseStream + forward_FileUploadService_UploadFile_0 = runtime.ForwardResponseMessage ) diff --git a/proto/rpc/examples/fileupload/v1/fileupload.proto b/proto/rpc/examples/fileupload/v1/fileupload.proto index d9546ebf..5127b7d7 100644 --- a/proto/rpc/examples/fileupload/v1/fileupload.proto +++ b/proto/rpc/examples/fileupload/v1/fileupload.proto @@ -16,7 +16,5 @@ message UploadFileResponse { } service FileUploadService { - // Due to an issue described by https://github.com/improbable-eng/ts-protoc-gen/pull/264 - // we use a streaming response but only expect one response. - rpc UploadFile(stream UploadFileRequest) returns (stream UploadFileResponse) {} + rpc UploadFile(stream UploadFileRequest) returns (UploadFileResponse) {} } diff --git a/proto/rpc/examples/fileupload/v1/fileupload_grpc.pb.go b/proto/rpc/examples/fileupload/v1/fileupload_grpc.pb.go index e02fce14..7babeaa7 100644 --- a/proto/rpc/examples/fileupload/v1/fileupload_grpc.pb.go +++ b/proto/rpc/examples/fileupload/v1/fileupload_grpc.pb.go @@ -18,8 +18,6 @@ const _ = grpc.SupportPackageIsVersion7 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type FileUploadServiceClient interface { - // Due to an issue described by https://github.com/improbable-eng/ts-protoc-gen/pull/264 - // we use a streaming response but only expect one response. UploadFile(ctx context.Context, opts ...grpc.CallOption) (FileUploadService_UploadFileClient, error) } @@ -42,7 +40,7 @@ func (c *fileUploadServiceClient) UploadFile(ctx context.Context, opts ...grpc.C type FileUploadService_UploadFileClient interface { Send(*UploadFileRequest) error - Recv() (*UploadFileResponse, error) + CloseAndRecv() (*UploadFileResponse, error) grpc.ClientStream } @@ -54,7 +52,10 @@ func (x *fileUploadServiceUploadFileClient) Send(m *UploadFileRequest) error { return x.ClientStream.SendMsg(m) } -func (x *fileUploadServiceUploadFileClient) Recv() (*UploadFileResponse, error) { +func (x *fileUploadServiceUploadFileClient) CloseAndRecv() (*UploadFileResponse, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } m := new(UploadFileResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err @@ -66,8 +67,6 @@ func (x *fileUploadServiceUploadFileClient) Recv() (*UploadFileResponse, error) // All implementations must embed UnimplementedFileUploadServiceServer // for forward compatibility type FileUploadServiceServer interface { - // Due to an issue described by https://github.com/improbable-eng/ts-protoc-gen/pull/264 - // we use a streaming response but only expect one response. UploadFile(FileUploadService_UploadFileServer) error mustEmbedUnimplementedFileUploadServiceServer() } @@ -97,7 +96,7 @@ func _FileUploadService_UploadFile_Handler(srv interface{}, stream grpc.ServerSt } type FileUploadService_UploadFileServer interface { - Send(*UploadFileResponse) error + SendAndClose(*UploadFileResponse) error Recv() (*UploadFileRequest, error) grpc.ServerStream } @@ -106,7 +105,7 @@ type fileUploadServiceUploadFileServer struct { grpc.ServerStream } -func (x *fileUploadServiceUploadFileServer) Send(m *UploadFileResponse) error { +func (x *fileUploadServiceUploadFileServer) SendAndClose(m *UploadFileResponse) error { return x.ServerStream.SendMsg(m) } @@ -129,7 +128,6 @@ var FileUploadService_ServiceDesc = grpc.ServiceDesc{ { StreamName: "UploadFile", Handler: _FileUploadService_UploadFile_Handler, - ServerStreams: true, ClientStreams: true, }, }, diff --git a/rpc/examples/fileupload/frontend/package-lock.json b/rpc/examples/fileupload/frontend/package-lock.json index c82f234d..291fba1a 100644 --- a/rpc/examples/fileupload/frontend/package-lock.json +++ b/rpc/examples/fileupload/frontend/package-lock.json @@ -9,12 +9,12 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "@viamrobotics/rpc": "^0.1.25", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@viamrobotics/rpc": "file:../../../js" }, "devDependencies": { - "@types/google-protobuf": "^3.7.4", "ansi-regex": ">=5.0.1", "concurrently": "^5.3.0", "set-value": ">=4.0.1", @@ -24,6 +24,59 @@ "vite": "^4.0.0" } }, + "../../../js": { + "name": "@viamrobotics/rpc", + "version": "0.2.6", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@viamrobotics/eslint-config": "^0.2.6", + "@viamrobotics/prettier-config": "^0.3.4", + "@viamrobotics/typescript-config": "^0.1.0", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-sonarjs": "^0.21.0", + "eslint-plugin-unicorn": "^48.0.1", + "prettier": "^3.0.3", + "set-value": ">=4.0.1", + "ts-loader": "^9.4.1", + "ts-protoc-gen": "^0.14.0", + "tslib": "^2.4.1", + "typescript": "^5.2.2", + "vite": "^4.0.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.5.0.tgz", + "integrity": "sha512-1gGg0M6c2Y3lnr5itis9dNj9r8hbOIuBMqoGSbUy7L7Vjw4MAttjJzJfj9HCDgytGCJkGanYEYI6MQVDijdVQw==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.5.0.tgz", + "integrity": "sha512-xjiiQ932Kibddaka18fGZ6yQL7xjXuLcYFYh/cU+q1WWEIrFPkZfViG/Ee6yrZbrlZkjcBuDibng+q7baTndfg==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.5.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -376,17 +429,6 @@ "node": ">=12" } }, - "node_modules/@improbable-eng/grpc-web": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", - "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", - "dependencies": { - "browser-headers": "^0.4.0" - }, - "peerDependencies": { - "google-protobuf": "^3.2.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -480,12 +522,6 @@ "dev": true, "peer": true }, - "node_modules/@types/google-protobuf": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.2.tgz", - "integrity": "sha512-ubeqvw7sl6CdgeiIilsXB2jIFoD/D0F+/LIEp7xEBEXRNtDJcf05FRINybsJtL7GlkWOUVn6gJs2W9OF+xI6lg==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -501,13 +537,8 @@ "peer": true }, "node_modules/@viamrobotics/rpc": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@viamrobotics/rpc/-/rpc-0.1.25.tgz", - "integrity": "sha512-2n7JQhaUsrnRpo98D/b8eDT01UXkpc4Jb4I65g9T3XQ+Oyh6c5yCYxKQ0yXAJt18coM8FZKMsoCBYtqRWqjOpA==", - "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "google-protobuf": "^3.14.0" - } + "resolved": "../../../js", + "link": true }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", @@ -779,11 +810,6 @@ "node": ">=8" } }, - "node_modules/browser-headers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" - }, "node_modules/browserslist": { "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", @@ -1220,7 +1246,8 @@ "node_modules/google-protobuf": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", - "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==" + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", + "dev": true }, "node_modules/graceful-fs": { "version": "4.2.10", @@ -2475,6 +2502,23 @@ } }, "dependencies": { + "@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==" + }, + "@connectrpc/connect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.5.0.tgz", + "integrity": "sha512-1gGg0M6c2Y3lnr5itis9dNj9r8hbOIuBMqoGSbUy7L7Vjw4MAttjJzJfj9HCDgytGCJkGanYEYI6MQVDijdVQw==", + "requires": {} + }, + "@connectrpc/connect-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.5.0.tgz", + "integrity": "sha512-xjiiQ932Kibddaka18fGZ6yQL7xjXuLcYFYh/cU+q1WWEIrFPkZfViG/Ee6yrZbrlZkjcBuDibng+q7baTndfg==", + "requires": {} + }, "@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -2629,14 +2673,6 @@ "dev": true, "optional": true }, - "@improbable-eng/grpc-web": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", - "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", - "requires": { - "browser-headers": "^0.4.0" - } - }, "@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -2721,12 +2757,6 @@ "dev": true, "peer": true }, - "@types/google-protobuf": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.2.tgz", - "integrity": "sha512-ubeqvw7sl6CdgeiIilsXB2jIFoD/D0F+/LIEp7xEBEXRNtDJcf05FRINybsJtL7GlkWOUVn6gJs2W9OF+xI6lg==", - "dev": true - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -2742,12 +2772,27 @@ "peer": true }, "@viamrobotics/rpc": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@viamrobotics/rpc/-/rpc-0.1.25.tgz", - "integrity": "sha512-2n7JQhaUsrnRpo98D/b8eDT01UXkpc4Jb4I65g9T3XQ+Oyh6c5yCYxKQ0yXAJt18coM8FZKMsoCBYtqRWqjOpA==", + "version": "file:../../../js", "requires": { - "@improbable-eng/grpc-web": "^0.13.0", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@viamrobotics/eslint-config": "^0.2.6", + "@viamrobotics/prettier-config": "^0.3.4", + "@viamrobotics/typescript-config": "^0.1.0", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-sonarjs": "^0.21.0", + "eslint-plugin-unicorn": "^48.0.1", + "prettier": "^3.0.3", + "set-value": ">=4.0.1", + "ts-loader": "^9.4.1", + "ts-protoc-gen": "^0.14.0", + "tslib": "^2.4.1", + "typescript": "^5.2.2", + "vite": "^4.0.0" } }, "@webassemblyjs/ast": { @@ -2991,11 +3036,6 @@ "fill-range": "^7.0.1" } }, - "browser-headers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" - }, "browserslist": { "version": "4.21.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", @@ -3329,7 +3369,8 @@ "google-protobuf": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", - "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==" + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", + "dev": true }, "graceful-fs": { "version": "4.2.10", diff --git a/rpc/examples/fileupload/frontend/package.json b/rpc/examples/fileupload/frontend/package.json index c6e7d5c9..31bb0bcd 100644 --- a/rpc/examples/fileupload/frontend/package.json +++ b/rpc/examples/fileupload/frontend/package.json @@ -3,14 +3,14 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@improbable-eng/grpc-web": "^0.13.0", - "@viamrobotics/rpc": "^0.1.25", - "google-protobuf": "^3.14.0" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@viamrobotics/rpc": "file:../../../js" }, "devDependencies": { "ansi-regex": ">=5.0.1", "set-value": ">=4.0.1", - "@types/google-protobuf": "^3.7.4", "concurrently": "^5.3.0", "ts-loader": "^8.0.14", "ts-protoc-gen": "^0.14.0", diff --git a/rpc/examples/fileupload/frontend/src/index.ts b/rpc/examples/fileupload/frontend/src/index.ts index db71e8d6..e9caee35 100644 --- a/rpc/examples/fileupload/frontend/src/index.ts +++ b/rpc/examples/fileupload/frontend/src/index.ts @@ -1,8 +1,10 @@ -import { grpc } from "@improbable-eng/grpc-web"; +import { PartialMessage } from "@bufbuild/protobuf"; +import { createPromiseClient, PromiseClient } from "@connectrpc/connect"; +import { createWritableIterable } from "@connectrpc/connect/protocol"; import { Credentials, dialWebRTC } from "@viamrobotics/rpc"; import { DialOptions } from "@viamrobotics/rpc/src/dial"; +import { FileUploadService } from "./gen/proto/rpc/examples/fileupload/v1/fileupload_connect"; import { UploadFileRequest, UploadFileResponse } from "./gen/proto/rpc/examples/fileupload/v1/fileupload_pb"; -import { FileUploadServiceClient } from "./gen/proto/rpc/examples/fileupload/v1/fileupload_pb_service"; const thisHost = `${window.location.protocol}//${window.location.host}`; @@ -15,9 +17,9 @@ declare global { } } -let clientResolve: (value: FileUploadServiceClient) => void; +let clientResolve: (value: PromiseClient) => void; let clientReject: (reason?: any) => void; -let clientProm = new Promise((resolve, reject) => { +let clientProm = new Promise>((resolve, reject) => { clientResolve = resolve; clientReject = reject; }); @@ -41,10 +43,9 @@ if (opts.externalAuthAddress) { opts.webrtcOptions!.signalingExternalAuthAddress = opts.externalAuthAddress; opts.webrtcOptions!.signalingExternalAuthToEntity = opts.externalAuthToEntity; } -dialWebRTC(thisHost, webrtcHost, opts).then(async ({ transportFactory }) => { +dialWebRTC(thisHost, webrtcHost, opts).then(async ({ transport }) => { console.log("WebRTC connection established") - const webrtcClient = new FileUploadServiceClient(webrtcHost, { transport: transportFactory }); - clientResolve(webrtcClient); + clientResolve(createPromiseClient(FileUploadService, transport)); }).catch((e: any) => clientReject(e)); window.onload = async (event) => { @@ -63,7 +64,7 @@ window.onload = async (event) => { }); }; -async function doUpload(client: FileUploadServiceClient, name: string, data: Uint8Array) { +async function doUpload(client: PromiseClient, name: string, data: Uint8Array) { let pResolve: (value: UploadFileResponse) => void; let pReject: (reason?: any) => void; let done = new Promise((resolve, reject) => { @@ -71,32 +72,23 @@ async function doUpload(client: FileUploadServiceClient, name: string, data: Uin pReject = reject; }); - const uploadStream = client.uploadFile(); + + const uploadStream = createWritableIterable>(); + const resp = client.uploadFile(uploadStream); let uploadFileRequest = new UploadFileRequest(); - uploadFileRequest.setName(name); + uploadFileRequest.data.case = "name"; + uploadFileRequest.data.value = name; uploadStream.write(uploadFileRequest); - uploadStream.on("data", (message: UploadFileResponse) => { - pResolve(message); - }); - uploadStream.on("end", ({ code, details }: { code: number, details: string, metadata: grpc.Metadata }) => { - if (code !== 0) { - console.log(code); - console.log(details); - pReject(code); - return; - } - }); - for (let i = 0; i < data.byteLength; i += 1024) { uploadFileRequest = new UploadFileRequest(); - uploadFileRequest.setChunkData(data.slice(i, i + 1024)); + uploadFileRequest.data.case = "chunkData"; + uploadFileRequest.data.value = data.slice(i, i + 1024); uploadStream.write(uploadFileRequest); } - uploadStream.end(); - const resp = await done; - console.log("upload complete", resp.toObject()); + uploadStream.close(); + console.log("upload complete", (await resp).toJson()); } diff --git a/rpc/examples/fileupload/frontend/vite.config.ts b/rpc/examples/fileupload/frontend/vite.config.ts index 9b6133df..4aa1a0ac 100644 --- a/rpc/examples/fileupload/frontend/vite.config.ts +++ b/rpc/examples/fileupload/frontend/vite.config.ts @@ -9,13 +9,6 @@ export default defineConfig({ __VERSION__: JSON.stringify(pkg.version), }, build: { - // This config is necessary to transform libraries on the list into ES modules. - // This can be removed if protobuf-es or a code generating tool that has good - // support for ES modules is used. - commonjsOptions: { - transformMixedEsModules: true, - include: [/google-protobuf/u, /@improbable-eng\/grpc-web/u, /gen\//u], - }, target: "esnext", lib: { entry: "src/index.ts", diff --git a/rpc/examples/fileupload/server/server.go b/rpc/examples/fileupload/server/server.go index 63ec8cfa..095b23fa 100644 --- a/rpc/examples/fileupload/server/server.go +++ b/rpc/examples/fileupload/server/server.go @@ -39,7 +39,7 @@ func (srv *Server) UploadFile(server pb.FileUploadService_UploadFileServer) erro if err != nil { if errors.Is(err, io.EOF) { // at this point, you can do something with the file - return server.Send(&pb.UploadFileResponse{Name: fileName, Size: int64(len(data))}) + return server.SendAndClose(&pb.UploadFileResponse{Name: fileName, Size: int64(len(data))}) } return err } From 797c554322ce4a9df4286dfd9607cc7ebb725ba0 Mon Sep 17 00:00:00 2001 From: Eric Daniels Date: Sun, 22 Sep 2024 02:15:40 -0400 Subject: [PATCH 4/7] Add node client echo example --- buf.yaml | 1 + rpc/examples/echo/Makefile | 3 + rpc/examples/echo/node-client/.gitignore | 4 + rpc/examples/echo/node-client/etc/prepare.sh | 11 + .../echo/node-client/package-lock.json | 4297 +++++++++++++++++ rpc/examples/echo/node-client/package.json | 29 + rpc/examples/echo/node-client/src/index.ts | 104 + rpc/examples/echo/node-client/tsconfig.json | 15 + 8 files changed, 4464 insertions(+) create mode 100644 rpc/examples/echo/node-client/.gitignore create mode 100755 rpc/examples/echo/node-client/etc/prepare.sh create mode 100644 rpc/examples/echo/node-client/package-lock.json create mode 100644 rpc/examples/echo/node-client/package.json create mode 100644 rpc/examples/echo/node-client/src/index.ts create mode 100644 rpc/examples/echo/node-client/tsconfig.json diff --git a/buf.yaml b/buf.yaml index 61f4a0e5..c4ec4848 100644 --- a/buf.yaml +++ b/buf.yaml @@ -6,6 +6,7 @@ build: excludes: - node_modules - rpc/examples/echo/frontend/node_modules + - rpc/examples/echo/node-client/node_modules - rpc/examples/fileupload/frontend/node_modules - rpc/js/node_modules - vendor diff --git a/rpc/examples/echo/Makefile b/rpc/examples/echo/Makefile index e2bef11c..95f48871 100644 --- a/rpc/examples/echo/Makefile +++ b/rpc/examples/echo/Makefile @@ -20,6 +20,9 @@ test-run-server: build install-playwright run-client: go run client/main.go -host=echo-server +run-node-client: + cd node-client && npm start + setup-auth: cd ${ROOT_DIR} && make setup-cert setup-priv-key diff --git a/rpc/examples/echo/node-client/.gitignore b/rpc/examples/echo/node-client/.gitignore new file mode 100644 index 00000000..85b3d9c4 --- /dev/null +++ b/rpc/examples/echo/node-client/.gitignore @@ -0,0 +1,4 @@ +node_modules +src/gen +src/index.js +dist diff --git a/rpc/examples/echo/node-client/etc/prepare.sh b/rpc/examples/echo/node-client/etc/prepare.sh new file mode 100755 index 00000000..042892c2 --- /dev/null +++ b/rpc/examples/echo/node-client/etc/prepare.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +rm -rf src/gen + +# Ours +mkdir -p src/gen/proto/rpc/examples +cp -R ../../../../dist/js/proto/rpc/examples/echo src/gen/proto/rpc/examples + +# Third-Party +mkdir -p src/gen/google +cp -R ../../../../dist/js/google/api src/gen/google diff --git a/rpc/examples/echo/node-client/package-lock.json b/rpc/examples/echo/node-client/package-lock.json new file mode 100644 index 00000000..574bfd97 --- /dev/null +++ b/rpc/examples/echo/node-client/package-lock.json @@ -0,0 +1,4297 @@ +{ + "name": "@viamrobotics/rpc-echo-example", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@viamrobotics/rpc-echo-example", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-node": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@viamrobotics/rpc": "file:../../../js", + "node-datachannel": "^0.11.0" + }, + "devDependencies": { + "@types/node": "^22.5.5", + "@viamrobotics/typescript-config": "^0.1.0", + "ansi-regex": ">=5.0.1", + "concurrently": "^5.3.0", + "set-value": ">=4.0.1", + "ts-loader": "^8.0.14", + "ts-protoc-gen": "^0.14.0", + "typescript": "^5.2.2" + } + }, + "../../../js": { + "name": "@viamrobotics/rpc", + "version": "0.2.6", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@viamrobotics/eslint-config": "^0.2.6", + "@viamrobotics/prettier-config": "^0.3.4", + "@viamrobotics/typescript-config": "^0.1.0", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-sonarjs": "^0.21.0", + "eslint-plugin-unicorn": "^48.0.1", + "prettier": "^3.0.3", + "set-value": ">=4.0.1", + "ts-loader": "^9.4.1", + "ts-protoc-gen": "^0.14.0", + "tslib": "^2.4.1", + "typescript": "^5.2.2", + "vite": "^4.0.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.5.0.tgz", + "integrity": "sha512-1gGg0M6c2Y3lnr5itis9dNj9r8hbOIuBMqoGSbUy7L7Vjw4MAttjJzJfj9HCDgytGCJkGanYEYI6MQVDijdVQw==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@connectrpc/connect-node": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.5.0.tgz", + "integrity": "sha512-go27+V6c6/YOGMXUs7d1NbXnGpMJl5fKYfMtbBKDs9azZTg/tW9q5S0iw9upZs43bOPV8YnJAWmTW9n1Te9NAw==", + "license": "Apache-2.0", + "dependencies": { + "undici": "^5.28.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.5.0" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.5.0.tgz", + "integrity": "sha512-xjiiQ932Kibddaka18fGZ6yQL7xjXuLcYFYh/cU+q1WWEIrFPkZfViG/Ee6yrZbrlZkjcBuDibng+q7baTndfg==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.5.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true, + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "8.4.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz", + "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true, + "peer": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true, + "peer": true + }, + "node_modules/@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@viamrobotics/rpc": { + "resolved": "../../../js", + "link": true + }, + "node_modules/@viamrobotics/typescript-config": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@viamrobotics/typescript-config/-/typescript-config-0.1.0.tgz", + "integrity": "sha512-yzcTIJ7O7znHVTwM8OLLeaPWkZYSntCwpjkcdYJz+YCEaW4VaV3kmMNOfQ4FsxnKfQngVcNUkxZMnjybfXKJ4g==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "typescript": ">=5 <6" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "peer": true + }, + "node_modules/acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001429", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", + "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ], + "peer": true + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "peer": true + }, + "node_modules/concurrently": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2", + "date-fns": "^2.0.1", + "lodash": "^4.17.15", + "read-pkg": "^4.0.1", + "rxjs": "^6.5.2", + "spawn-command": "^0.0.2-1", + "supports-color": "^6.1.0", + "tree-kill": "^1.2.2", + "yargs": "^13.3.0" + }, + "bin": { + "concurrently": "bin/concurrently.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/date-fns": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.22.1.tgz", + "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==", + "dev": true, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "dev": true, + "peer": true + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true, + "peer": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "peer": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "peer": true + }, + "node_modules/google-protobuf": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", + "dev": true + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "peer": true + }, + "node_modules/node-abi": { + "version": "3.67.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", + "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-datachannel": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.11.0.tgz", + "integrity": "sha512-8/vAMms32XxgJ9FIRDXbfmmH1ROm0HBdsa/XteIcUWN4VTQN38UITTkuu6YsfQzN/CQp8YhhnfAEzEadQJ2c6Q==", + "hasInstallScript": true, + "license": "MPL 2.0", + "dependencies": { + "node-domexception": "^2.0.1", + "prebuild-install": "^7.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/node-domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-2.0.1.tgz", + "integrity": "sha512-M85rnSC7WQ7wnfQTARPT4LrK7nwCHLdDFOCcItZMhTQjyCebJH8GciKqYJNgaOFZs9nFmTmd/VMyi3OW5jA47w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true, + "peer": true + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dev": true, + "dependencies": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "node_modules/set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/jonschlinkert", + "https://paypal.me/jonathanschlinkert", + "https://jonschlinkert.dev/sponsor" + ], + "dependencies": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + }, + "engines": { + "node": ">=11.0" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terser": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.3.0.tgz", + "integrity": "sha512-MgGly4I6cStsJy27ViE32UoqxPTN9Xly4anxxVyaIWR+9BGxboV4EyJBGfR3RePV7Ksjj3rHmPZJeIt+7o4Vag==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "*" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/ts-protoc-gen": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/ts-protoc-gen/-/ts-protoc-gen-0.14.0.tgz", + "integrity": "sha512-2z6w2HioMCMVNcgNHBcEvudmQfzrn+3BjAlz+xgYZ9L0o8n8UG8WUiTJcbXHFiEg2SU8IltwH2pm1otLoMSKwg==", + "dev": true, + "dependencies": { + "google-protobuf": "^3.6.1" + }, + "bin": { + "protoc-gen-ts": "bin/protoc-gen-ts" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "peer": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.74.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", + "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/enhanced-resolve": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", + "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + }, + "dependencies": { + "@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==" + }, + "@connectrpc/connect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.5.0.tgz", + "integrity": "sha512-1gGg0M6c2Y3lnr5itis9dNj9r8hbOIuBMqoGSbUy7L7Vjw4MAttjJzJfj9HCDgytGCJkGanYEYI6MQVDijdVQw==", + "requires": {} + }, + "@connectrpc/connect-node": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.5.0.tgz", + "integrity": "sha512-go27+V6c6/YOGMXUs7d1NbXnGpMJl5fKYfMtbBKDs9azZTg/tW9q5S0iw9upZs43bOPV8YnJAWmTW9n1Te9NAw==", + "requires": { + "undici": "^5.28.4" + } + }, + "@connectrpc/connect-web": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.5.0.tgz", + "integrity": "sha512-xjiiQ932Kibddaka18fGZ6yQL7xjXuLcYFYh/cU+q1WWEIrFPkZfViG/Ee6yrZbrlZkjcBuDibng+q7baTndfg==", + "requires": {} + }, + "@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==" + }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "peer": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "peer": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true, + "peer": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@types/eslint": { + "version": "8.4.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.9.tgz", + "integrity": "sha512-jFCSo4wJzlHQLCpceUhUnXdrPuCNOjGFMQ8Eg6JXxlz3QaCKOb7eGi2cephQdM4XTYsNej69P9JDJ1zqNIbncQ==", + "dev": true, + "peer": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "peer": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true, + "peer": true + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true, + "peer": true + }, + "@types/node": { + "version": "22.5.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", + "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "dev": true, + "requires": { + "undici-types": "~6.19.2" + } + }, + "@viamrobotics/rpc": { + "version": "file:../../../js", + "requires": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@viamrobotics/eslint-config": "^0.2.6", + "@viamrobotics/prettier-config": "^0.3.4", + "@viamrobotics/typescript-config": "^0.1.0", + "eslint": "^8.53.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-sonarjs": "^0.21.0", + "eslint-plugin-unicorn": "^48.0.1", + "prettier": "^3.0.3", + "set-value": ">=4.0.1", + "ts-loader": "^9.4.1", + "ts-protoc-gen": "^0.14.0", + "tslib": "^2.4.1", + "typescript": "^5.2.2", + "vite": "^4.0.0" + } + }, + "@viamrobotics/typescript-config": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@viamrobotics/typescript-config/-/typescript-config-0.1.0.tgz", + "integrity": "sha512-yzcTIJ7O7znHVTwM8OLLeaPWkZYSntCwpjkcdYJz+YCEaW4VaV3kmMNOfQ4FsxnKfQngVcNUkxZMnjybfXKJ4g==", + "dev": true, + "requires": {} + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "peer": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "peer": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true, + "peer": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "peer": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "peer": true + }, + "acorn": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "dev": true, + "peer": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peer": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peer": true, + "requires": {} + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "peer": true, + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001429", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", + "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", + "dev": true, + "peer": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "peer": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "peer": true + }, + "concurrently": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", + "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "date-fns": "^2.0.1", + "lodash": "^4.17.15", + "read-pkg": "^4.0.1", + "rxjs": "^6.5.2", + "spawn-command": "^0.0.2-1", + "supports-color": "^6.1.0", + "tree-kill": "^1.2.2", + "yargs": "^13.3.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "date-fns": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.22.1.tgz", + "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" + }, + "electron-to-chromium": { + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "dev": true, + "peer": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true, + "peer": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "peer": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "peer": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "peer": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "peer": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "peer": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "peer": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "peer": true + }, + "google-protobuf": { + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-primitive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", + "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "peer": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "peer": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "peer": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "peer": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "peer": true + }, + "node-abi": { + "version": "3.67.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", + "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", + "requires": { + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + } + } + }, + "node-datachannel": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-datachannel/-/node-datachannel-0.11.0.tgz", + "integrity": "sha512-8/vAMms32XxgJ9FIRDXbfmmH1ROm0HBdsa/XteIcUWN4VTQN38UITTkuu6YsfQzN/CQp8YhhnfAEzEadQJ2c6Q==", + "requires": { + "node-domexception": "^2.0.1", + "prebuild-install": "^7.0.1" + } + }, + "node-domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-2.0.1.tgz", + "integrity": "sha512-M85rnSC7WQ7wnfQTARPT4LrK7nwCHLdDFOCcItZMhTQjyCebJH8GciKqYJNgaOFZs9nFmTmd/VMyi3OW5jA47w==" + }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true, + "peer": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "peer": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "peer": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "peer": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dev": true, + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "peer": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "peer": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz", + "integrity": "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "is-primitive": "^3.0.1" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "peer": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "terser": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", + "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "terser-webpack-plugin": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", + "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.14", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.14.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "ts-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.3.0.tgz", + "integrity": "sha512-MgGly4I6cStsJy27ViE32UoqxPTN9Xly4anxxVyaIWR+9BGxboV4EyJBGfR3RePV7Ksjj3rHmPZJeIt+7o4Vag==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "ts-protoc-gen": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/ts-protoc-gen/-/ts-protoc-gen-0.14.0.tgz", + "integrity": "sha512-2z6w2HioMCMVNcgNHBcEvudmQfzrn+3BjAlz+xgYZ9L0o8n8UG8WUiTJcbXHFiEg2SU8IltwH2pm1otLoMSKwg==", + "dev": true, + "requires": { + "google-protobuf": "^3.6.1" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true + }, + "undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "requires": { + "@fastify/busboy": "^2.0.0" + } + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "peer": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "peer": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "peer": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.74.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", + "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "dev": true, + "peer": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "enhanced-resolve": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", + "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", + "dev": true, + "peer": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "peer": true + } + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "peer": true + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/rpc/examples/echo/node-client/package.json b/rpc/examples/echo/node-client/package.json new file mode 100644 index 00000000..34bf43e2 --- /dev/null +++ b/rpc/examples/echo/node-client/package.json @@ -0,0 +1,29 @@ +{ + "name": "@viamrobotics/rpc-echo-example", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.5.0", + "@connectrpc/connect-node": "^1.5.0", + "@connectrpc/connect-web": "^1.5.0", + "@viamrobotics/rpc": "file:../../../js", + "node-datachannel": "^0.11.0" + }, + "devDependencies": { + "@types/node": "^22.5.5", + "@viamrobotics/typescript-config": "^0.1.0", + "ansi-regex": ">=5.0.1", + "concurrently": "^5.3.0", + "set-value": ">=4.0.1", + "ts-loader": "^8.0.14", + "ts-protoc-gen": "^0.14.0", + "typescript": "^5.2.2" + }, + "type": "module", + "scripts": { + "prepare": "./etc/prepare.sh", + "test": "echo \"Error: no test specified\" && exit 1", + "start": "npx tsc && node src/index.js" + } +} diff --git a/rpc/examples/echo/node-client/src/index.ts b/rpc/examples/echo/node-client/src/index.ts new file mode 100644 index 00000000..fe724442 --- /dev/null +++ b/rpc/examples/echo/node-client/src/index.ts @@ -0,0 +1,104 @@ +import type { PartialMessage } from "@bufbuild/protobuf"; +import { ConnectError, createPromiseClient, PromiseClient, Transport } from "@connectrpc/connect"; +import { createWritableIterable } from "@connectrpc/connect/protocol"; +import { dialDirect, dialWebRTC } from "@viamrobotics/rpc"; +import { EchoService } from "./gen/proto/rpc/examples/echo/v1/echo_connect.js"; +import { EchoBiDiRequest, EchoMultipleRequest, EchoRequest } from "./gen/proto/rpc/examples/echo/v1/echo_pb.js"; + +import { createGrpcTransport } from '@connectrpc/connect-node'; +import wrtc from "node-datachannel/polyfill"; +declare global { + var VIAM: { + GRPC_TRANSPORT_FACTORY: (opts: any) => Transport; + }; +} +globalThis.VIAM = { + GRPC_TRANSPORT_FACTORY: (opts: any) => createGrpcTransport({ httpVersion: "2", ...opts }), +}; +for (const key in wrtc) { + (global as any)[key] = (wrtc as any)[key]; +} + +const thisHost = "http://localhost:8080"; + + +async function getClients() { + const webRTCConn = await dialWebRTC(thisHost, "echo-server", {}); + const webrtcClient = createPromiseClient(EchoService, webRTCConn.transport); + await renderResponses(webrtcClient, "wrtc"); + webRTCConn.peerConnection.close(); + + const directTransport = await dialDirect(thisHost, {}); + const directClient = createPromiseClient(EchoService, directTransport); + await renderResponses(directClient, "direct"); +} + +getClients().catch(e => { + console.error("error getting clients", e); +}); + +async function renderResponses(client: PromiseClient, method: string) { + const echoRequest = new EchoRequest(); + echoRequest.message = "hello"; + + console.log(`\n+++${method}+++`); + console.log("---unary---") + const response = await client.echo(echoRequest); + console.log(response.message); + + const echoMultipleRequest = new EchoMultipleRequest(); + echoMultipleRequest.message = "hello?"; + + console.log("---multi---") + const multiStream = client.echoMultiple(echoMultipleRequest); + try { + for await (const response of multiStream) { + console.log(response.message); + } + } catch (err) { + if (err instanceof ConnectError) { + console.log(err.code); + console.log(err.details); + } + throw err; + } + + console.log("---bidi---") + const clientStream = createWritableIterable>(); + const bidiStream = client.echoBiDi(clientStream); + + let msgCount = 0; + const processResponses = async () => { + try { + for await (const response of bidiStream) { + msgCount++ + console.log(response.message); + if (msgCount == 3) { + return; + } + } + } catch (err) { + if (err instanceof ConnectError) { + console.log(err.code); + console.log(err.details); + } + throw err; + } + } + + let echoBiDiRequest = new EchoBiDiRequest(); + echoBiDiRequest.message = "one"; + + clientStream.write(echoBiDiRequest); + await processResponses(); + + msgCount = 0; + + echoBiDiRequest = new EchoBiDiRequest(); + echoBiDiRequest.message = "two"; + clientStream.write(echoBiDiRequest); + + await processResponses(); + clientStream.close(); + await processResponses(); +} diff --git a/rpc/examples/echo/node-client/tsconfig.json b/rpc/examples/echo/node-client/tsconfig.json new file mode 100644 index 00000000..ad5aa3a5 --- /dev/null +++ b/rpc/examples/echo/node-client/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@viamrobotics/typescript-config/base.json", + "compilerOptions": { + "target": "ES5", + "useDefineForClassFields": true, + "importHelpers": true, + "module": "ESNext", + "moduleResolution": "node", + "verbatimModuleSyntax": false, + "lib": ["ESNext", "DOM", "DOM.iterable"], + "types": ["node"] + }, + "include": ["src", ".*.cjs"], + "exclude": ["node_modules", "src/gen"] +} From e4cd0ea0d02215ad0ab480c9a648a949e12f8570 Mon Sep 17 00:00:00 2001 From: Eric Daniels Date: Mon, 23 Sep 2024 13:18:01 -0400 Subject: [PATCH 5/7] lint pass --- rpc/js/package.json | 4 +- .../src/{BaseChannel.ts => base-channel.ts} | 10 +- rpc/js/src/{BaseStream.ts => base-stream.ts} | 17 +- .../{ClientChannel.ts => client-channel.ts} | 43 +-- .../src/{ClientStream.ts => client-stream.ts} | 72 ++-- .../{errors.ts => connection-closed-error.ts} | 16 +- rpc/js/src/dial.ts | 320 +++++++++--------- rpc/js/src/grpc-error.ts | 12 + rpc/js/src/main.ts | 13 +- rpc/js/src/peer.ts | 85 ++--- rpc/js/src/polyfills.ts | 12 +- ...alingExchange.ts => signaling-exchange.ts} | 176 ++++++---- ...lientStream.ts => stream-client-stream.ts} | 60 ++-- ...ClientStream.ts => unary-client-stream.ts} | 30 +- rpc/js/tsconfig.build.json | 3 +- rpc/js/tsconfig.json | 6 +- 16 files changed, 449 insertions(+), 430 deletions(-) rename rpc/js/src/{BaseChannel.ts => base-channel.ts} (88%) rename rpc/js/src/{BaseStream.ts => base-stream.ts} (73%) rename rpc/js/src/{ClientChannel.ts => client-channel.ts} (79%) rename rpc/js/src/{ClientStream.ts => client-stream.ts} (83%) rename rpc/js/src/{errors.ts => connection-closed-error.ts} (56%) create mode 100644 rpc/js/src/grpc-error.ts rename rpc/js/src/{SignalingExchange.ts => signaling-exchange.ts} (68%) rename rpc/js/src/{StreamClientStream.ts => stream-client-stream.ts} (68%) rename rpc/js/src/{UnaryClientStream.ts => unary-client-stream.ts} (79%) diff --git a/rpc/js/package.json b/rpc/js/package.json index 2e040f3c..84265f1c 100644 --- a/rpc/js/package.json +++ b/rpc/js/package.json @@ -36,8 +36,8 @@ }, "scripts": { "prepare": "./etc/prepare.sh", - "typecheck": "tsc --noEmit", - "lint": "eslint '**/*.{ts,js,cjs}' --quiet", + "typecheck": "tsc", + "lint": "eslint '**/*.{ts,js,cjs}'", "format": "prettier --write '**/*.{ts,js,cjs}'", "build": "vite build && tsc --project tsconfig.build.json", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/rpc/js/src/BaseChannel.ts b/rpc/js/src/base-channel.ts similarity index 88% rename from rpc/js/src/BaseChannel.ts rename to rpc/js/src/base-channel.ts index a9c3df13..bc75e463 100644 --- a/rpc/js/src/BaseChannel.ts +++ b/rpc/js/src/base-channel.ts @@ -1,5 +1,5 @@ -import { Message } from '@bufbuild/protobuf'; -import { ConnectionClosedError } from './errors'; +import type { Message } from '@bufbuild/protobuf'; +import { ConnectionClosedError } from './connection-closed-error'; export class BaseChannel { public readonly ready: Promise; @@ -12,7 +12,7 @@ export class BaseChannel { private closed = false; private closedReason: Error | undefined; - protected maxDataChannelSize = 65535; + protected maxDataChannelSize = 65_535; constructor(peerConn: RTCPeerConnection, dataChannel: RTCDataChannel) { this.peerConn = peerConn; @@ -70,9 +70,9 @@ export class BaseChannel { this.closeWithReason(new ConnectionClosedError('data channel closed')); } - private onChannelError(ev: any) { + private onChannelError(ev: Event) { console.error('channel error', ev); - this.closeWithReason(new Error(ev)); + this.closeWithReason(new Error(ev.toString())); } protected write(msg: Message) { diff --git a/rpc/js/src/BaseStream.ts b/rpc/js/src/base-stream.ts similarity index 73% rename from rpc/js/src/BaseStream.ts rename to rpc/js/src/base-stream.ts index df06d8d3..69f39399 100644 --- a/rpc/js/src/BaseStream.ts +++ b/rpc/js/src/base-stream.ts @@ -1,13 +1,13 @@ import type { PacketMessage, Stream } from './gen/proto/rpc/webrtc/v1/grpc_pb'; -// MaxMessageSize is the maximum size a gRPC message can be. -let MaxMessageSize = 1 << 25; +// MaxMessageSize (2^25) is the maximum size a gRPC message can be. +const MaxMessageSize = 33_554_432; export class BaseStream { protected readonly grpcStream: Stream; private readonly onDone: (id: bigint) => void; - protected closed: boolean = false; - private readonly packetBuf: Array = []; + protected closed = false; + private readonly packetBuf: Uint8Array[] = []; private packetBufSize = 0; constructor(grpcStream: Stream, onDone: (id: bigint) => void) { @@ -36,16 +36,15 @@ export class BaseStream { this.packetBuf.push(data); this.packetBufSize += data.length; if (msg.eom) { - const data = new Uint8Array(this.packetBufSize); + const pktData = new Uint8Array(this.packetBufSize); let position = 0; - for (let i = 0; i < this.packetBuf.length; i++) { - const partialData = this.packetBuf[i]!; - data.set(partialData, position); + for (const partialData of this.packetBuf) { + pktData.set(partialData, position); position += partialData.length; } this.packetBuf.length = 0; this.packetBufSize = 0; - return data; + return pktData; } return undefined; } diff --git a/rpc/js/src/ClientChannel.ts b/rpc/js/src/client-channel.ts similarity index 79% rename from rpc/js/src/ClientChannel.ts rename to rpc/js/src/client-channel.ts index 2b50eeea..2fdd2f68 100644 --- a/rpc/js/src/ClientChannel.ts +++ b/rpc/js/src/client-channel.ts @@ -1,19 +1,19 @@ -import { +import type { AnyMessage, Message, MethodInfo, PartialMessage, ServiceType, } from '@bufbuild/protobuf'; -import { +import type { ContextValues, StreamResponse, Transport, UnaryResponse, } from '@connectrpc/connect'; -import { BaseChannel } from './BaseChannel'; -import { ClientStream, ClientStreamConstructor } from './ClientStream'; -import { ConnectionClosedError } from './errors'; +import { BaseChannel } from './base-channel'; +import type { ClientStream, ClientStreamConstructor } from './client-stream'; +import { ConnectionClosedError } from './connection-closed-error'; import { Request, RequestHeaders, @@ -21,11 +21,11 @@ import { Response, Stream, } from './gen/proto/rpc/webrtc/v1/grpc_pb'; -import { StreamClientStream } from './StreamClientStream'; -import { UnaryClientStream } from './UnaryClientStream'; +import { StreamClientStream } from './stream-client-stream'; +import { UnaryClientStream } from './unary-client-stream'; // MaxStreamCount is the max number of streams a channel can have. -let MaxStreamCount = 256; +const MaxStreamCount = 256; interface activeClienStream { cs: ClientStream; @@ -33,11 +33,11 @@ interface activeClienStream { export class ClientChannel extends BaseChannel implements Transport { private streamIDCounter = 0; - private readonly streams: Record = {}; + private readonly streams = new Map(); constructor(pc: RTCPeerConnection, dc: RTCDataChannel) { super(pc, dc); - dc.addEventListener('message', (event: MessageEvent<'message'>) => { + dc.addEventListener('message', (event: MessageEvent) => { this.onChannelMessage(event); }); pc.addEventListener('iceconnectionstatechange', () => { @@ -55,14 +55,13 @@ export class ClientChannel extends BaseChannel implements Transport { private onConnectionTerminated() { // we may call this twice but we know closed will be true at this point. this.closeWithReason(new ConnectionClosedError('data channel closed')); - for (const streamId in this.streams) { - const stream = this.streams[streamId]!; + for (const stream of this.streams.values()) { stream.cs.closeWithRecvError(); } } - private onChannelMessage(event: MessageEvent) { - let resp = Response.fromBinary(new Uint8Array(event.data as ArrayBuffer)); + private onChannelMessage(event: MessageEvent) { + const resp = Response.fromBinary(new Uint8Array(event.data)); const { stream } = resp; if (stream === undefined) { @@ -71,7 +70,7 @@ export class ClientChannel extends BaseChannel implements Transport { } const { id } = stream; - const activeStream = this.streams[id.toString()]; + const activeStream = this.streams.get(id.toString()); if (activeStream === undefined) { console.error('no stream for id; discarding', 'id', id); return; @@ -80,8 +79,10 @@ export class ClientChannel extends BaseChannel implements Transport { } private nextStreamID(): Stream { + const thisId = this.streamIDCounter; + this.streamIDCounter += 1; return new Stream({ - id: BigInt(this.streamIDCounter++), + id: BigInt(thisId), }); } @@ -90,7 +91,7 @@ export class ClientChannel extends BaseChannel implements Transport { I extends Message, O extends Message, >( - clientCtor: ClientStreamConstructor, + ClientCtor: ClientStreamConstructor, stream: Stream, service: ServiceType, method: MethodInfo, @@ -99,14 +100,14 @@ export class ClientChannel extends BaseChannel implements Transport { if (this.isClosed()) { throw new ConnectionClosedError('connection closed'); } - let activeStream = this.streams[stream.id.toString()]; + let activeStream = this.streams.get(stream.id.toString()); if (activeStream !== undefined) { throw new Error('invariant: stream should not exist yet'); } if (Object.keys(this.streams).length > MaxStreamCount) { throw new Error('stream limit hit'); } - const clientStream = new clientCtor( + const clientStream = new ClientCtor( this, stream, (id: bigint) => this.removeStreamByID(id), @@ -115,12 +116,12 @@ export class ClientChannel extends BaseChannel implements Transport { header ); activeStream = { cs: clientStream }; - this.streams[stream.id.toString()] = activeStream; + this.streams.set(stream.id.toString(), activeStream); return clientStream; } private removeStreamByID(id: bigint) { - delete this.streams[id.toString()]; + this.streams.delete(id.toString()); } public writeHeaders(stream: Stream, headers: RequestHeaders) { diff --git a/rpc/js/src/ClientStream.ts b/rpc/js/src/client-stream.ts similarity index 83% rename from rpc/js/src/ClientStream.ts rename to rpc/js/src/client-stream.ts index ff58cbd9..982955db 100644 --- a/rpc/js/src/ClientStream.ts +++ b/rpc/js/src/client-stream.ts @@ -1,12 +1,12 @@ -import { +import type { AnyMessage, Message, MethodInfo, ServiceType, } from '@bufbuild/protobuf'; import { createClientMethodSerializers } from '@connectrpc/connect/protocol'; -import { BaseStream } from './BaseStream'; -import type { ClientChannel } from './ClientChannel'; +import { BaseStream } from './base-stream'; +import type { ClientChannel } from './client-channel'; import { cloneHeaders } from './dial'; import { Metadata, @@ -22,7 +22,7 @@ import { } from './gen/proto/rpc/webrtc/v1/grpc_pb'; // see golang/client_stream.go -const maxRequestMessagePacketDataSize = 16373; +const maxRequestMessagePacketDataSize = 16_373; export interface ClientStreamConstructor< T extends ClientStream, @@ -40,7 +40,8 @@ export interface ClientStreamConstructor< ): T; } -/** A ClientStream provides all the facilities needed to invoke and manage a +/** + * A ClientStream provides all the facilities needed to invoke and manage a * gRPC stream at a low-level. Implementors like UnaryClientStream and StreamClientStream * handle the method specific flow of unary/stream operations. */ @@ -54,8 +55,8 @@ export abstract class ClientStream< protected readonly parseMessage: (data: Uint8Array) => O; protected readonly requestHeaders: RequestHeaders; - private headersReceived: boolean = false; - private trailersReceived: boolean = false; + private headersReceived = false; + private trailersReceived = false; protected abstract onHeaders(headers: ResponseHeaders): void; protected abstract onTrailers(trailers: ResponseTrailers): void; @@ -93,9 +94,9 @@ export abstract class ClientStream< protected startRequest(signal?: AbortSignal) { if (signal) { - signal.onabort = () => { + signal.addEventListener('abort', () => { this.resetStream(); - }; + }); } try { @@ -128,12 +129,12 @@ export abstract class ClientStream< protected writeMessage(eos: boolean, msgBytes?: Uint8Array) { try { - if (!msgBytes || msgBytes.length == 0) { + if (!msgBytes || msgBytes.length === 0) { const packetMessage = new PacketMessage({ eom: true, }); const requestMessage = new RequestMessage({ - hasMessage: !!msgBytes, + hasMessage: Boolean(msgBytes), packetMessage, eos, }); @@ -141,19 +142,20 @@ export abstract class ClientStream< return; } - while (msgBytes.length !== 0) { + let remMsgBytes = msgBytes; + while (remMsgBytes.length > 0) { const amountToSend = Math.min( - msgBytes.length, + remMsgBytes.length, maxRequestMessagePacketDataSize ); const packetMessage = new PacketMessage(); - packetMessage.data = msgBytes.slice(0, amountToSend); - msgBytes = msgBytes.slice(amountToSend); - if (msgBytes.length === 0) { + packetMessage.data = remMsgBytes.slice(0, amountToSend); + remMsgBytes = remMsgBytes.slice(amountToSend); + if (remMsgBytes.length === 0) { packetMessage.eom = true; } const requestMessage = new RequestMessage({ - hasMessage: !!msgBytes, + hasMessage: Boolean(remMsgBytes), packetMessage, eos, }); @@ -167,7 +169,7 @@ export abstract class ClientStream< public onResponse(resp: Response) { switch (resp.type.case) { - case 'headers': + case 'headers': { if (this.headersReceived) { console.error( `invariant: headers already received for ${this.grpcStream.id}` @@ -182,7 +184,8 @@ export abstract class ClientStream< } this.processHeaders(resp.type.value); break; - case 'message': + } + case 'message': { if (!this.headersReceived) { console.error( `invariant: headers not yet received for ${this.grpcStream.id}` @@ -197,27 +200,24 @@ export abstract class ClientStream< } this.processMessage(resp.type.value); break; - case 'trailers': + } + case 'trailers': { this.processTrailers(resp.type.value); break; - default: + } + default: { console.error('unknown response type', resp.type.case); break; + } } } private processHeaders(headers: ResponseHeaders) { this.headersReceived = true; - if (!this.onHeaders) { - throw new Error('invariant: onHeaders unset'); - } this.onHeaders(headers); } private processMessage(msg: ResponseMessage) { - if (!this.onMessage) { - throw new Error('invariant: onMessage unset'); - } if (!msg.packetMessage) { return; } @@ -230,18 +230,8 @@ export abstract class ClientStream< private processTrailers(trailers: ResponseTrailers) { this.trailersReceived = true; - if (!this.onTrailers) { - throw new Error('invariant: onTrailers unset'); - } - - let statusCode; const { status } = trailers; - if (status) { - statusCode = status.code; - } else { - statusCode = 0; - } - + const statusCode = status ? status.code : 0; this.onTrailers(trailers); if (statusCode === 0) { this.closeWithRecvError(); @@ -251,8 +241,10 @@ export abstract class ClientStream< } } -// Needs testing -// from https://github.com/jsmouret/grpc-over-webrtc/blob/45cd6d6cf516e78b1e262ea7aa741bc7a7a93dbc/client-improbable/src/grtc/webrtcclient.ts#L7 +/** + * Needs testing + * from https://github.com/jsmouret/grpc-over-webrtc/blob/45cd6d6cf516e78b1e262ea7aa741bc7a7a93dbc/client-improbable/src/grtc/webrtcclient.ts#L7 + */ const fromGRPCMetadata = (headers?: Headers): Metadata | undefined => { if (!headers) { return undefined; diff --git a/rpc/js/src/errors.ts b/rpc/js/src/connection-closed-error.ts similarity index 56% rename from rpc/js/src/errors.ts rename to rpc/js/src/connection-closed-error.ts index 2e0b7788..b0c866b8 100644 --- a/rpc/js/src/errors.ts +++ b/rpc/js/src/connection-closed-error.ts @@ -1,10 +1,12 @@ export class ConnectionClosedError extends Error { + public override readonly name = 'ConnectionClosedError'; + constructor(msg: string) { super(msg); Object.setPrototypeOf(this, ConnectionClosedError.prototype); } - static isError(error: any): boolean { + static isError(error: unknown): boolean { if (error instanceof ConnectionClosedError) { return true; } @@ -17,15 +19,3 @@ export class ConnectionClosedError extends Error { return false; } } - -export class GRPCError extends Error { - public readonly code: number; - public readonly grpcMessage: string; - - constructor(code: number, message: string) { - super(`Code=${code} Message=${message}`); - this.code = code; - this.grpcMessage = message; - Object.setPrototypeOf(this, GRPCError.prototype); - } -} diff --git a/rpc/js/src/dial.ts b/rpc/js/src/dial.ts index 604d100b..5dbfd167 100644 --- a/rpc/js/src/dial.ts +++ b/rpc/js/src/dial.ts @@ -28,7 +28,7 @@ import { WebRTCConfig } from './gen/proto/rpc/webrtc/v1/signaling_pb'; import { newPeerConnectionForClient } from './peer'; import { createGrpcWebTransport } from '@connectrpc/connect-web'; -import { SignalingExchange } from './SignalingExchange'; +import { SignalingExchange } from './signaling-exchange'; export interface DialOptions { authEntity?: string | undefined; @@ -37,13 +37,15 @@ export interface DialOptions { externalAuthAddress?: string | undefined; externalAuthToEntity?: string | undefined; - // `accessToken` allows a pre-authenticated client to dial with - // an authorization header. Direct dial will have the access token - // appended to the "Authorization: Bearer" header. WebRTC dial will - // appened it to the signaling server communication - // - // If enabled, other auth options have no affect. Eg. authEntity, credentials, - // externalAuthAddress, externalAuthToEntity, webrtcOptions.signalingAccessToken + /** + * `accessToken` allows a pre-authenticated client to dial with + * an authorization header. Direct dial will have the access token + * appended to the "Authorization: Bearer" header. WebRTC dial will + * appened it to the signaling server communication + * + * If enabled, other auth options have no affect. Eg. authEntity, credentials, + * externalAuthAddress, externalAuthToEntity, webrtcOptions.signalingAccessToken + */ accessToken?: string | undefined; // set timeout in milliseconds for dialing. @@ -57,27 +59,33 @@ export interface DialWebRTCOptions { // signalingAuthEntity is the entity to authenticate as to the signaler. signalingAuthEntity?: string; - // signalingExternalAuthAddress is the address to perform external auth yet. - // This is unlikely to be needed since the signaler is typically in the same - // place where authentication happens. + /** + * signalingExternalAuthAddress is the address to perform external auth yet. + * This is unlikely to be needed since the signaler is typically in the same + * place where authentication happens. + */ signalingExternalAuthAddress?: string; - // signalingExternalAuthToEntity is the entity to authenticate for after - // externally authenticating. - // This is unlikely to be needed since the signaler is typically in the same - // place where authentication happens. + /** + * signalingExternalAuthToEntity is the entity to authenticate for after + * externally authenticating. + * This is unlikely to be needed since the signaler is typically in the same + * place where authentication happens. + */ signalingExternalAuthToEntity?: string; // signalingCredentials are used to authenticate the request to the signaling server. signalingCredentials?: Credentials; - // `signalingAccessToken` allows a pre-authenticated client to dial with - // an authorization header to the signaling server. This skips the Authenticate() - // request to the singaling server or external auth but does not skip the - // AuthenticateTo() request to retrieve the credentials at the external auth - // endpoint. - // - // If enabled, other auth options have no affect. Eg. authEntity, credentials, signalingAuthEntity, signalingCredentials. + /** + * `signalingAccessToken` allows a pre-authenticated client to dial with + * an authorization header to the signaling server. This skips the Authenticate() + * request to the singaling server or external auth but does not skip the + * AuthenticateTo() request to retrieve the credentials at the external auth + * endpoint. + * + * If enabled, other auth options have no affect. Eg. authEntity, credentials, signalingAuthEntity, signalingCredentials. + */ signalingAccessToken?: string; // `additionalSDPValues` is a collection of additional SDP values that we want to pass into the connection's call request. @@ -91,22 +99,20 @@ export interface Credentials { export type TransportFactory = ( // platform specific - init: any + init: TransportInitOptions ) => Transport; interface TransportInitOptions { baseUrl: string; } -export async function dialDirect( +export const dialDirect = async ( address: string, opts?: DialOptions -): Promise { +): Promise => { validateDialOptions(opts); const createTransport = - typeof globalThis.VIAM?.GRPC_TRANSPORT_FACTORY === 'function' - ? globalThis.VIAM.GRPC_TRANSPORT_FACTORY - : createGrpcWebTransport; + globalThis.VIAM?.GRPC_TRANSPORT_FACTORY ?? createGrpcWebTransport; const transportOpts = { baseUrl: address, @@ -115,14 +121,14 @@ export async function dialDirect( // Client already has access token with no external auth, skip Authenticate process. if ( opts?.accessToken && - !(opts?.externalAuthAddress && opts?.externalAuthToEntity) + !(opts.externalAuthAddress && opts.externalAuthToEntity) ) { const headers = new Headers(); headers.set('authorization', `Bearer ${opts.accessToken}`); return new AuthenticatedTransport(transportOpts, createTransport, headers); } - if (!opts || (!opts?.credentials && !opts?.accessToken)) { + if (!opts || (!opts.credentials && !opts.accessToken)) { return createTransport(transportOpts); } @@ -132,36 +138,34 @@ export async function dialDirect( opts ); return authFact(transportOpts); -} +}; -const addressCleanupRegex = /^(.*:\/\/)/; +const addressCleanupRegex = /^.*:\/\//u; -async function makeAuthenticatedTransportFactory( +const makeAuthenticatedTransportFactory = async ( address: string, defaultFactory: TransportFactory, opts: DialOptions -): Promise { +): Promise => { let accessToken = ''; const getExtraHeaders = async (): Promise => { const headers = new Headers(); // TODO(GOUT-10): handle expiration - if (accessToken == '') { + if (accessToken === '') { let thisAccessToken = ''; if (!opts.accessToken || opts.accessToken === '') { const request = new AuthenticateRequest({ - entity: opts.authEntity - ? opts.authEntity - : address.replace(/^(.*:\/\/)/, ''), - credentials: new PBCredentials({ - type: opts.credentials?.type!, - payload: opts.credentials?.payload!, - }), + entity: opts.authEntity ?? address.replace(addressCleanupRegex, ''), }); + if (opts.credentials) { + request.credentials = new PBCredentials({ + type: opts.credentials.type, + payload: opts.credentials.payload, + }); + } - const resolvedAddress = opts.externalAuthAddress - ? opts.externalAuthAddress - : address; + const resolvedAddress = opts.externalAuthAddress ?? address; const transport = defaultFactory({ baseUrl: resolvedAddress }); const authClient = createPromiseClient(AuthService, transport); const resp = await authClient.authenticate(request); @@ -170,37 +174,45 @@ async function makeAuthenticatedTransportFactory( thisAccessToken = opts.accessToken; } - accessToken = thisAccessToken; - - if (opts.externalAuthAddress && opts.externalAuthToEntity) { - const headers = new Headers(); - headers.set('authorization', `Bearer ${accessToken}`); - - thisAccessToken = ''; - - const request = new AuthenticateRequest({ - entity: opts.externalAuthToEntity, - }); - const transport = defaultFactory({ - baseUrl: opts.externalAuthAddress!, - }); - const externalAuthClient = createPromiseClient( - ExternalAuthService, - transport - ); - const resp = await externalAuthClient.authenticateTo(request); - thisAccessToken = resp.accessToken; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- await race + if (accessToken === '') { accessToken = thisAccessToken; + + if (opts.externalAuthAddress && opts.externalAuthToEntity) { + const authHeaders = new Headers(); + authHeaders.set('authorization', `Bearer ${accessToken}`); + + thisAccessToken = ''; + + const request = new AuthenticateRequest({ + entity: opts.externalAuthToEntity, + }); + const transport = defaultFactory({ + baseUrl: opts.externalAuthAddress, + }); + const externalAuthClient = createPromiseClient( + ExternalAuthService, + transport + ); + const resp = await externalAuthClient.authenticateTo(request, { + headers: authHeaders, + }); + thisAccessToken = resp.accessToken; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- await race + if (accessToken === '') { + accessToken = thisAccessToken; + } + } } } headers.set('authorization', `Bearer ${accessToken}`); return headers; }; const extraMd = await getExtraHeaders(); - return (opts: TransportInitOptions): Transport => { - return new AuthenticatedTransport(opts, defaultFactory, extraMd); + return (transportOpts: TransportInitOptions): Transport => { + return new AuthenticatedTransport(transportOpts, defaultFactory, extraMd); }; -} +}; class AuthenticatedTransport implements Transport { protected readonly transport: Transport; @@ -228,9 +240,9 @@ class AuthenticatedTransport implements Transport { contextValues?: ContextValues ): Promise> { const newHeaders = cloneHeaders(header); - this.extraHeaders.forEach((value: string, key: string) => { + for (const [key, value] of this.extraHeaders) { newHeaders.set(key, value); - }); + } return this.transport.unary( service, method, @@ -255,9 +267,9 @@ class AuthenticatedTransport implements Transport { contextValues?: ContextValues ): Promise> { const newHeaders = cloneHeaders(header); - this.extraHeaders.forEach((value: string, key: string) => { + for (const [key, value] of this.extraHeaders) { newHeaders.set(key, value); - }); + } return this.transport.stream( service, method, @@ -270,15 +282,16 @@ class AuthenticatedTransport implements Transport { } } -export function cloneHeaders(headers: HeadersInit | undefined): Headers { - let cloned = new Headers(); - if (headers && headers !== undefined) { +export const cloneHeaders = (headers: HeadersInit | undefined): Headers => { + const cloned = new Headers(); + if (headers !== undefined) { if (Array.isArray(headers)) { for (const [key, value] of headers) { cloned.append(key, value); } } else if ('forEach' in headers) { - if (typeof headers.forEach == 'function') { + if (typeof headers.forEach === 'function') { + // eslint-disable-next-line unicorn/no-array-for-each headers.forEach((value, key) => { cloned.append(key, value); }); @@ -290,7 +303,7 @@ export function cloneHeaders(headers: HeadersInit | undefined): Headers { } } return cloned; -} +}; export interface WebRTCConnection { transport: Transport; @@ -298,11 +311,11 @@ export interface WebRTCConnection { dataChannel: RTCDataChannel; } -async function getOptionalWebRTCConfig( +const getOptionalWebRTCConfig = async ( signalingAddress: string, callOpts: CallOptions, dialOpts?: DialOptions -): Promise { +): Promise => { const optsCopy = { ...dialOpts } as DialOptions; const directTransport = await dialDirect(signalingAddress, optsCopy); @@ -313,60 +326,67 @@ async function getOptionalWebRTCConfig( try { const resp = await signalingClient.optionalWebRTCConfig({}, callOpts); return resp.config ?? new WebRTCConfig(); - } catch (err) { - if (err instanceof ConnectError && err.code == Code.Unimplemented) { + } catch (error) { + if (error instanceof ConnectError && error.code === Code.Unimplemented) { return new WebRTCConfig(); } - throw err; + throw error; } -} - -// dialWebRTC makes a connection to given host by signaling with the address provided. A Promise is returned -// upon successful connection that contains a transport factory to use with gRPC client as well as the WebRTC -// PeerConnection itself. Care should be taken with the PeerConnection and is currently returned for experimental -// use. -// TODO(GOUT-7): figure out decent way to handle reconnect on connection termination -// eslint-disable-next-line sonarjs/cognitive-complexity -// eslint-disable-next-line func-style -export async function dialWebRTC( +}; + +/** + * dialWebRTC makes a connection to given host by signaling with the address provided. A Promise is returned + * upon successful connection that contains a transport factory to use with gRPC client as well as the WebRTC + * PeerConnection itself. Care should be taken with the PeerConnection and is currently returned for experimental + * use. + * TODO(GOUT-7): figure out decent way to handle reconnect on connection termination + */ +export const dialWebRTC = async ( signalingAddress: string, host: string, dialOpts?: DialOptions -): Promise { - signalingAddress = signalingAddress.replace(/(\/)$/, ''); +): Promise => { + const usableSignalingAddress = signalingAddress.replace(/\/$/u, ''); validateDialOptions(dialOpts); - // TODO(RSDK-2836): In general, this logic should be in parity with the golang implementation. - // https://github.com/viamrobotics/goutils/blob/main/rpc/wrtc_client.go#L160-L175 + /** + * TODO(RSDK-2836): In general, this logic should be in parity with the golang implementation. + * https://github.com/viamrobotics/goutils/blob/main/rpc/wrtc_client.go#L160-L175 + */ const callOpts = { headers: { 'rpc-host': host, }, }; - // first complete our WebRTC options, gathering any extra information like - // TURN servers from a cloud server. + /** + * first complete our WebRTC options, gathering any extra information like + * TURN servers from a cloud server. + */ const webrtcOpts = await processWebRTCOpts( - signalingAddress, + usableSignalingAddress, callOpts, dialOpts ); // then derive options specifically for signaling against our target. - const exchangeOpts = processSignalingExchangeOpts(signalingAddress, dialOpts); + const exchangeOpts = processSignalingExchangeOpts( + usableSignalingAddress, + dialOpts + ); const { pc, dc } = await newPeerConnectionForClient( - webrtcOpts !== undefined && webrtcOpts.disableTrickleICE, - webrtcOpts?.rtcConfig, - webrtcOpts?.additionalSdpFields + webrtcOpts.disableTrickleICE, + webrtcOpts.rtcConfig, + webrtcOpts.additionalSdpFields ); let successful = false; let directTransport: Transport; try { - directTransport = await dialDirect(signalingAddress, exchangeOpts); - } catch (err) { + directTransport = await dialDirect(usableSignalingAddress, exchangeOpts); + } catch (error) { pc.close(); - throw err; + throw error; } const signalingClient = createPromiseClient( @@ -384,24 +404,20 @@ export async function dialWebRTC( try { // set timeout for dial attempt if a timeout is specified if (dialOpts?.dialTimeout) { - setTimeout( - () => { - if (!successful) { - exchange.terminate(); - } - }, - dialOpts?.dialTimeout - ); + setTimeout(() => { + if (!successful) { + exchange.terminate(); + } + }, dialOpts.dialTimeout); } const cc = await exchange.doExchange(); if (dialOpts?.externalAuthAddress) { - // TODO(GOUT-11): prepare AuthenticateTo here - // for client channel. + // TODO(GOUT-11): prepare AuthenticateTo here for client channel. + // eslint-disable-next-line sonarjs/no-duplicated-branches } else if (dialOpts?.credentials?.type) { - // TODO(GOUT-11): prepare Authenticate here - // for client channel + // TODO(GOUT-11): prepare Authenticate here for client channel } successful = true; @@ -410,21 +426,21 @@ export async function dialWebRTC( peerConnection: pc, dataChannel: dc, }; - } catch (err) { - console.error('error dialing', err); - throw err; + } catch (error) { + console.error('error dialing', error); + throw error; } finally { if (!successful) { pc.close(); } } -} +}; -async function processWebRTCOpts( +const processWebRTCOpts = async ( signalingAddress: string, callOpts: CallOptions, dialOpts?: DialOptions -): Promise { +): Promise => { // Get TURN servers, if any. const config = await getOptionalWebRTCConfig( signalingAddress, @@ -441,12 +457,10 @@ async function processWebRTCOpts( } ); - if (!dialOpts) { - dialOpts = {}; - } + const usableDialOpts = dialOpts ?? {}; let webrtcOpts: DialWebRTCOptions; - if (!dialOpts.webrtcOptions) { + if (usableDialOpts.webrtcOptions === undefined) { // use additional webrtc config as default webrtcOpts = { disableTrickleICE: config.disableTrickle, @@ -455,59 +469,53 @@ async function processWebRTCOpts( }, }; } else { - // RSDK-8715: We deep copy here to avoid mutating the input config's `rtcConfig.iceServers` - // list. - webrtcOpts = JSON.parse(JSON.stringify(dialOpts.webrtcOptions)); - if (!webrtcOpts.rtcConfig) { + // RSDK-8715: We deep copy here to avoid mutating the input config's `rtcConfig.iceServers` list. + webrtcOpts = JSON.parse( + JSON.stringify(usableDialOpts.webrtcOptions) + ) as DialWebRTCOptions; + if (webrtcOpts.rtcConfig === undefined) { webrtcOpts.rtcConfig = { iceServers: additionalIceServers }; } else { webrtcOpts.rtcConfig.iceServers = [ - ...(webrtcOpts.rtcConfig.iceServers || []), + ...(webrtcOpts.rtcConfig.iceServers ?? []), ...additionalIceServers, ]; } } return webrtcOpts; -} +}; -function processSignalingExchangeOpts( +const processSignalingExchangeOpts = ( signalingAddress: string, dialOpts?: DialOptions -) { +) => { // replace auth entity and creds let optsCopy = dialOpts; if (dialOpts) { optsCopy = { ...dialOpts } as DialOptions; if (!dialOpts.accessToken) { - optsCopy.authEntity = dialOpts?.webrtcOptions?.signalingAuthEntity; + optsCopy.authEntity = dialOpts.webrtcOptions?.signalingAuthEntity; if (!optsCopy.authEntity) { - if (optsCopy.externalAuthAddress) { - optsCopy.authEntity = dialOpts.externalAuthAddress?.replace( - addressCleanupRegex, - '' - ); - } else { - optsCopy.authEntity = signalingAddress.replace( - addressCleanupRegex, - '' - ); - } + optsCopy.authEntity = optsCopy.externalAuthAddress + ? dialOpts.externalAuthAddress?.replace(addressCleanupRegex, '') + : signalingAddress.replace(addressCleanupRegex, ''); } - optsCopy.credentials = dialOpts?.webrtcOptions?.signalingCredentials; - optsCopy.accessToken = dialOpts?.webrtcOptions?.signalingAccessToken; + optsCopy.credentials = dialOpts.webrtcOptions?.signalingCredentials; + optsCopy.accessToken = dialOpts.webrtcOptions?.signalingAccessToken; } optsCopy.externalAuthAddress = - dialOpts?.webrtcOptions?.signalingExternalAuthAddress; + dialOpts.webrtcOptions?.signalingExternalAuthAddress; optsCopy.externalAuthToEntity = - dialOpts?.webrtcOptions?.signalingExternalAuthToEntity; + dialOpts.webrtcOptions?.signalingExternalAuthToEntity; } return optsCopy; -} +}; -function validateDialOptions(opts?: DialOptions) { +// eslint-disable-next-line sonarjs/cognitive-complexity -- it is not complex +const validateDialOptions = (opts?: DialOptions) => { if (!opts) { return; } @@ -541,7 +549,7 @@ function validateDialOptions(opts?: DialOptions) { } if ( - opts?.webrtcOptions?.signalingAccessToken && + opts.webrtcOptions?.signalingAccessToken && opts.webrtcOptions.signalingAccessToken.length > 0 ) { if (opts.webrtcOptions.signalingAuthEntity) { @@ -555,4 +563,4 @@ function validateDialOptions(opts?: DialOptions) { ); } } -} +}; diff --git a/rpc/js/src/grpc-error.ts b/rpc/js/src/grpc-error.ts new file mode 100644 index 00000000..3c572304 --- /dev/null +++ b/rpc/js/src/grpc-error.ts @@ -0,0 +1,12 @@ +export class GRPCError extends Error { + public override readonly name = 'GRPCError'; + public readonly code: number; + public readonly grpcMessage: string; + + constructor(code: number, message: string) { + super(`Code=${code} Message=${message}`); + this.code = code; + this.grpcMessage = message; + Object.setPrototypeOf(this, GRPCError.prototype); + } +} diff --git a/rpc/js/src/main.ts b/rpc/js/src/main.ts index 582a64b3..7f7ceb7b 100644 --- a/rpc/js/src/main.ts +++ b/rpc/js/src/main.ts @@ -1,10 +1,12 @@ -import { Transport } from '@connectrpc/connect'; +import type { Transport } from '@connectrpc/connect'; declare global { // eslint-disable-next-line vars-on-top,no-var - var VIAM: { - GRPC_TRANSPORT_FACTORY: (opts: any) => Transport; - }; + var VIAM: + | { + GRPC_TRANSPORT_FACTORY?: (opts: unknown) => Transport; + } + | undefined; } export { @@ -16,4 +18,5 @@ export { type WebRTCConnection, } from './dial'; -export { ConnectionClosedError, GRPCError } from './errors'; +export { ConnectionClosedError } from './connection-closed-error'; +export { GRPCError } from './grpc-error'; diff --git a/rpc/js/src/peer.ts b/rpc/js/src/peer.ts index aafb5071..cbdc88ee 100644 --- a/rpc/js/src/peer.ts +++ b/rpc/js/src/peer.ts @@ -5,40 +5,35 @@ interface ReadyPeer { dc: RTCDataChannel; } -export function addSdpFields( +export const addSdpFields = ( localDescription?: RTCSessionDescription | null, sdpFields?: Record -) { - let description = { +) => { + const description = { sdp: localDescription?.sdp, type: localDescription?.type, }; if (sdpFields) { - Object.keys(sdpFields).forEach((key) => { - description.sdp = [ - description.sdp, - `a=${key}:${sdpFields[key as keyof typeof sdpFields]}\r\n`, - ].join(''); - }); + for (const [key, value] of Object.entries(sdpFields)) { + description.sdp = [description.sdp, `a=${key}:${value}\r\n`].join(''); + } } return description; -} +}; -export async function newPeerConnectionForClient( +export const newPeerConnectionForClient = async ( disableTrickle: boolean, rtcConfig?: RTCConfiguration, additionalSdpFields?: Record -): Promise { - if (!rtcConfig) { - rtcConfig = { - iceServers: [ - { - urls: 'stun:global.stun.twilio.com:3478', - }, - ], - }; - } - const peerConnection = new RTCPeerConnection(rtcConfig); +): Promise => { + const usableRTCConfig = rtcConfig ?? { + iceServers: [ + { + urls: 'stun:global.stun.twilio.com:3478', + }, + ], + }; + const peerConnection = new RTCPeerConnection(usableRTCConfig); let pResolve: (value: ReadyPeer) => void; const result = new Promise((resolve) => { @@ -58,27 +53,19 @@ export async function newPeerConnectionForClient( }); negotiationChannel.binaryType = 'arraybuffer'; - let ignoreOffer = false; - const polite = true; let negOpen = false; negotiationChannel.addEventListener('open', () => { negOpen = true; }); negotiationChannel.addEventListener( 'message', - async (event: MessageEvent) => { - try { + (event: MessageEvent) => { + (async () => { const description = new RTCSessionDescription( - JSON.parse(atob(event.data.toString())) + JSON.parse(atob(event.data)) as RTCSessionDescriptionInit ); - const offerCollision = - description.type === 'offer' && - (description || peerConnection.signalingState !== 'stable'); - ignoreOffer = !polite && offerCollision; - if (ignoreOffer) { - return; - } + // we are always polite and will never ignore an offer await peerConnection.setRemoteDescription(description); @@ -90,40 +77,32 @@ export async function newPeerConnectionForClient( ); negotiationChannel.send(btoa(JSON.stringify(newDescription))); } - } catch (e) { - console.error(e); - } + })().catch(console.error); } ); - peerConnection.addEventListener('negotiationneeded', async () => { - if (!negOpen) { - return; - } - try { + peerConnection.addEventListener('negotiationneeded', () => { + (async () => { + if (!negOpen) { + return; + } await peerConnection.setLocalDescription(); const newDescription = addSdpFields( peerConnection.localDescription, additionalSdpFields ); negotiationChannel.send(btoa(JSON.stringify(newDescription))); - } catch (e) { - console.error(e); - } + })().catch(console.error); }); if (!disableTrickle) { - return Promise.resolve({ pc: peerConnection, dc: dataChannel }); + return { pc: peerConnection, dc: dataChannel }; } // set up offer const offerDesc = await peerConnection.createOffer({}); - try { - await peerConnection.setLocalDescription(offerDesc); - } catch (e) { - return Promise.reject(e); - } + await peerConnection.setLocalDescription(offerDesc); - peerConnection.addEventListener('icecandidate', async (event) => { + peerConnection.addEventListener('icecandidate', (event) => { if (event.candidate !== null) { return; } @@ -131,4 +110,4 @@ export async function newPeerConnectionForClient( }); return result; -} +}; diff --git a/rpc/js/src/polyfills.ts b/rpc/js/src/polyfills.ts index 472d5326..0fc0f3fe 100644 --- a/rpc/js/src/polyfills.ts +++ b/rpc/js/src/polyfills.ts @@ -1,8 +1,9 @@ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; -export const btoa = (input: string = '') => { - let str = input; +/* eslint-disable no-bitwise, unicorn/prefer-code-point, no-plusplus, require-unicode-regexp */ +export const btoa = (input = '') => { + const str = input; let output = ''; for ( @@ -24,11 +25,11 @@ export const btoa = (input: string = '') => { return output; }; -export const atob = (input: string = '') => { - let str = input.replace(/=+$/, ''); // eslint-disable-line no-div-regex +export const atob = (input = '') => { + const str = input.replace(/=+$/, ''); // eslint-disable-line no-div-regex let output = ''; - if (str.length % 4 == 1) { + if (str.length % 4 === 1) { throw new Error( "'atob' failed: The string to be decoded is not correctly encoded." ); @@ -45,3 +46,4 @@ export const atob = (input: string = '') => { return output; }; +/* eslint-enable no-bitwise, unicorn/prefer-code-point, no-plusplus, require-unicode-regexp */ diff --git a/rpc/js/src/SignalingExchange.ts b/rpc/js/src/signaling-exchange.ts similarity index 68% rename from rpc/js/src/SignalingExchange.ts rename to rpc/js/src/signaling-exchange.ts index e4a9fc68..84a6bac0 100644 --- a/rpc/js/src/SignalingExchange.ts +++ b/rpc/js/src/signaling-exchange.ts @@ -1,12 +1,8 @@ -import { - CallOptions, - Code, - ConnectError, - PromiseClient, -} from '@connectrpc/connect'; -import { ClientChannel } from './ClientChannel'; -import { DialWebRTCOptions } from './dial'; -import { ConnectionClosedError } from './errors'; +import type { CallOptions, PromiseClient } from '@connectrpc/connect'; +import { Code, ConnectError } from '@connectrpc/connect'; +import { ClientChannel } from './client-channel'; +import { ConnectionClosedError } from './connection-closed-error'; +import type { DialWebRTCOptions } from './dial'; import { Status } from './gen/google/rpc/status_pb'; import { SignalingService } from './gen/proto/rpc/webrtc/v1/signaling_connect'; import { @@ -28,9 +24,10 @@ export class SignalingExchange { private sentDoneOrErrorOnce = false; private exchangeDone = false; private iceComplete = false; + private haveInitResponse = false; private awaitingRemoteDescription?: { success: (value: unknown) => void; - failure: (reason?: any) => void; + failure: (reason?: unknown) => void; }; private remoteDescriptionSet?: Promise; @@ -66,9 +63,11 @@ export class SignalingExchange { callRequest.disableTrickle = this.dialOpts.disableTrickleICE; } - // As long as we establish a connection (i.e. we are ready), then - // we will make it clear across the exchange that we are done - // and no more work should be done nor should any errors be emitted. + /** + * As long as we establish a connection (i.e. we are ready), then + * we will make it clear across the exchange that we are done + * and no more work should be done nor should any errors be emitted. + */ this.clientChannel.ready .then(() => { this.exchangeDone = true; @@ -103,7 +102,7 @@ export class SignalingExchange { ) { return; } - let averageCallUpdateDuration = + const averageCallUpdateDuration = this.totalCallUpdateDuration / this.numCallUpdates; console.groupCollapsed('Caller update statistics'); console.table({ @@ -115,8 +114,11 @@ export class SignalingExchange { }); this.pc.addEventListener( 'icecandidate', - async (event: { candidate: RTCIceCandidateInit | null }) => - this.onLocalICECandidate(event) + (event: { candidate: RTCIceCandidateInit | null }) => { + this.onLocalICECandidate(event).catch((error) => { + console.error(`error processing local ICE candidate ${error}`); + }); + } ); await this.pc.setLocalDescription(offerDesc); @@ -130,56 +132,74 @@ export class SignalingExchange { private async processCallResponses( callResponses: AsyncIterable ) { - let haveInit = false; try { for await (const response of callResponses) { - if (response.stage.case == 'init') { - if (haveInit) { - await this.sendError('got init stage more than once'); - return; - } - haveInit = true; - if (!this.handleInitResponse(response.uuid, response.stage.value)) { - return; + switch (response.stage.case) { + case 'init': { + if ( + !(await this.handleInitResponse( + response.uuid, + response.stage.value + )) + ) { + return; + } + break; } - } else if (response.stage.case == 'update') { - if (!haveInit) { - await this.sendError('got update stage before init stage'); - return; + case 'update': { + if ( + !(await this.handleUpdateResponse( + response.uuid, + response.stage.value + )) + ) { + return; + } + break; } - if (!this.handleUpdateResponse(response.uuid, response.stage.value)) { + default: { + await this.sendError('unknown CallResponse stage'); return; } - } else { - await this.sendError('unknown CallResponse stage'); - return; } } - } catch (err) { - if (this.exchangeDone || this.pc.iceConnectionState === 'connected') { - // There's nothing to do with these errors, our connection is established. - return; + } catch (error) { + this.handleInitError(error); + } + } + + private handleInitError(error: unknown) { + if (this.exchangeDone || this.pc.iceConnectionState === 'connected') { + // There's nothing to do with these errors, our connection is established. + return; + } + if (error instanceof ConnectError && error.code === Code.Unimplemented) { + if (error.message === 'Response closed without headers') { + throw new ConnectionClosedError('failed to dial'); } - if (err instanceof ConnectError && err.code == Code.Unimplemented) { - if (err.message === 'Response closed without headers') { - throw new ConnectionClosedError('failed to dial'); - } - if (this.clientChannel?.isClosed()) { - throw new ConnectionClosedError('client channel is closed'); - } - console.error(err.message); + if (this.clientChannel.isClosed()) { + throw new ConnectionClosedError('client channel is closed'); } - throw err; + console.error(error.message); } + throw error; } private async handleInitResponse( uuid: string, response: CallResponseInitStage ): Promise { + if (this.haveInitResponse) { + await this.sendError('got init stage more than once'); + return false; + } + this.haveInitResponse = true; + this.callUuid = uuid; - const remoteSDP = new RTCSessionDescription(JSON.parse(atob(response.sdp))); + const remoteSDP = new RTCSessionDescription( + JSON.parse(atob(response.sdp)) as RTCSessionDescriptionInit + ); if (this.clientChannel.isClosed()) { await this.sendError('client channel is closed'); return false; @@ -200,12 +220,17 @@ export class SignalingExchange { uuid: string, response: CallResponseUpdateStage ): Promise { + if (!this.haveInitResponse) { + await this.sendError('got update stage before init stage'); + return false; + } + if (uuid !== this.callUuid) { await this.sendError(`uuid mismatch; have=${uuid} want=${this.callUuid}`); return false; } const cand = iceCandidateFromProto(response.candidate!); - if (cand.candidate !== null) { + if (cand.candidate !== undefined) { console.debug(`received remote ICE ${cand.candidate}`); } try { @@ -236,7 +261,7 @@ export class SignalingExchange { throw new Error(callUUIDUnset); } - if (event.candidate.candidate !== null) { + if (event.candidate.candidate !== undefined) { console.debug(`gathered local ICE ${event.candidate.candidate}`); } const iProto = iceCandidateToProto(event.candidate); @@ -250,26 +275,25 @@ export class SignalingExchange { const callUpdateStart = new Date(); try { await this.signalingClient.callUpdate(callRequestUpdate, this.callOpts); - this.numCallUpdates++; - let callUpdateEnd = new Date(); - let callUpdateDuration = + this.numCallUpdates += 1; + const callUpdateEnd = new Date(); + const callUpdateDuration = callUpdateEnd.getTime() - callUpdateStart.getTime(); if (callUpdateDuration > this.maxCallUpdateDuration) { this.maxCallUpdateDuration = callUpdateDuration; } this.totalCallUpdateDuration += callUpdateDuration; - return; - } catch (err) { + } catch (error) { if ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.exchangeDone || this.iceComplete || // @ts-expect-error tsc is unaware that iceConnectionState can change - // after we've inspected it before. this.pc.iceConnectionState === 'connected' ) { return; } - console.error(err); + console.error(error); } } @@ -293,12 +317,14 @@ export class SignalingExchange { }); try { await this.signalingClient.callUpdate(callRequestUpdate, this.callOpts); - } catch (err) { - // even though this call update fails, there's a chance another - // attempt with another ICE candidate(s) will make the connection - // work. In the future it may be better to figure out if this - // error is fatal or not. - console.error('failed to send call update; continuing', err); + } catch (error) { + /** + * even though this call update fails, there's a chance another + * attempt with another ICE candidate(s) will make the connection + * work. In the future it may be better to figure out if this + * error is fatal or not. + */ + console.error('failed to send call update; continuing', error); } } @@ -319,18 +345,20 @@ export class SignalingExchange { }); try { await this.signalingClient.callUpdate(callRequestUpdate, this.callOpts); - } catch (err) { - // even though this call update fails, there's a chance another - // attempt with another ICE candidate(s) will make the connection - // work. In the future it may be better to figure out if this - // error is fatal or not. - console.error(err); + } catch (error) { + /** + * even though this call update fails, there's a chance another + * attempt with another ICE candidate(s) will make the connection + * work. In the future it may be better to figure out if this + * error is fatal or not. + */ + console.error(error); } } } -function iceCandidateFromProto(i: ICECandidate): RTCIceCandidateInit { - let candidate: RTCIceCandidateInit = { +const iceCandidateFromProto = (i: ICECandidate) => { + const candidate: RTCIceCandidateInit = { candidate: i.candidate, }; if (i.sdpMid) { @@ -343,10 +371,10 @@ function iceCandidateFromProto(i: ICECandidate): RTCIceCandidateInit { candidate.usernameFragment = i.usernameFragment; } return candidate; -} +}; -function iceCandidateToProto(i: RTCIceCandidateInit): ICECandidate { - let candidate = new ICECandidate({ +const iceCandidateToProto = (i: RTCIceCandidateInit) => { + const candidate = new ICECandidate({ candidate: i.candidate!, }); candidate.candidate = i.candidate!; @@ -360,4 +388,4 @@ function iceCandidateToProto(i: RTCIceCandidateInit): ICECandidate { candidate.usernameFragment = i.usernameFragment; } return candidate; -} +}; diff --git a/rpc/js/src/StreamClientStream.ts b/rpc/js/src/stream-client-stream.ts similarity index 68% rename from rpc/js/src/StreamClientStream.ts rename to rpc/js/src/stream-client-stream.ts index bdddd3bc..18eee557 100644 --- a/rpc/js/src/StreamClientStream.ts +++ b/rpc/js/src/stream-client-stream.ts @@ -1,15 +1,15 @@ -import { Message, PartialMessage } from '@bufbuild/protobuf'; -import { +import type { Message, PartialMessage } from '@bufbuild/protobuf'; +import type { ContextValues, StreamRequest, StreamResponse, - createContextValues, } from '@connectrpc/connect'; +import { createContextValues } from '@connectrpc/connect'; import { createWritableIterable, runStreamingCall, } from '@connectrpc/connect/protocol'; -import { ClientStream, toGRPCMetadata } from './ClientStream'; +import { ClientStream, toGRPCMetadata } from './client-stream'; import { ResponseHeaders, ResponseTrailers, @@ -21,7 +21,7 @@ export class StreamClientStream< > extends ClientStream { private awaitingHeadersResult?: { success: (value: Headers) => void; - failure: (reason?: any) => void; + failure: (reason?: unknown) => void; }; private gotHeaders = false; @@ -37,7 +37,7 @@ export class StreamClientStream< input: AsyncIterable>, contextValues?: ContextValues ): Promise> { - let req = { + const req = { stream: true as const, url: '', init: {}, @@ -48,19 +48,23 @@ export class StreamClientStream< message: input, }; type optParams = Parameters>[0]; - let opt: optParams = { + const opt: optParams = { req, - // next is what actually kicks off the request. The run call below will - // ultimately call this for us. - next: async (req: StreamRequest): Promise> => { - let startRequest = new Promise((resolve, reject) => { + /** + * next is what actually kicks off the request. The run call below will + * ultimately call this for us. + */ + next: async ( + streamReq: StreamRequest + ): Promise> => { + const startRequest = new Promise((resolve, reject) => { this.awaitingHeadersResult = { success: resolve, failure: reject, }; this.startRequest(); - this.sendMessages(req.message).catch((err) => { - console.error('error sending streaming message', err); + this.sendMessages(streamReq.message).catch((error) => { + console.error('error sending streaming message', error); this.closeWithRecvError(); }); }); @@ -68,7 +72,7 @@ export class StreamClientStream< const headers = await startRequest; return { - ...req, + ...streamReq, header: headers, trailer: this.trailers, message: this.respStream, @@ -100,16 +104,18 @@ export class StreamClientStream< protected onTrailers(respTrailers: ResponseTrailers): void { if (respTrailers.metadata?.md) { - for (let key in respTrailers.metadata.md) { - let value = respTrailers.metadata.md[key]; - for (let val in value?.values) { - this.trailers.append(key, val); + for (const key in respTrailers.metadata.md) { + if (Object.hasOwn(respTrailers.metadata.md, key)) { + const value = respTrailers.metadata.md[key]; + for (const val of value?.values ?? []) { + this.trailers.append(key, val); + } } } } this.respStream.close(); - if (!respTrailers.status || respTrailers.status.code == 0) { + if (!respTrailers.status || respTrailers.status.code === 0) { if (this.gotHeaders) { return; } @@ -124,17 +130,13 @@ export class StreamClientStream< } protected onMessage(msgBytes: Uint8Array) { - let msg = this.parseMessage(msgBytes); - if (this.respStreamQueue) { - this.respStreamQueue = this.respStreamQueue.then(async () => - this.respStream.write(msg) - ); - } else { - this.respStreamQueue = this.respStream.write(msg); - } - this.respStreamQueue.catch((err) => { + const msg = this.parseMessage(msgBytes); + this.respStreamQueue = this.respStreamQueue + ? this.respStreamQueue.then(async () => this.respStream.write(msg)) + : this.respStream.write(msg); + this.respStreamQueue.catch((error) => { console.error( - `error pushing received message into stream; failing: ${err}` + `error pushing received message into stream; failing: ${error}` ); this.resetStream(); }); diff --git a/rpc/js/src/UnaryClientStream.ts b/rpc/js/src/unary-client-stream.ts similarity index 79% rename from rpc/js/src/UnaryClientStream.ts rename to rpc/js/src/unary-client-stream.ts index 1511c7f5..3f429100 100644 --- a/rpc/js/src/UnaryClientStream.ts +++ b/rpc/js/src/unary-client-stream.ts @@ -1,12 +1,12 @@ -import { Message, PartialMessage } from '@bufbuild/protobuf'; -import { +import type { Message, PartialMessage } from '@bufbuild/protobuf'; +import type { ContextValues, UnaryRequest, UnaryResponse, - createContextValues, } from '@connectrpc/connect'; +import { createContextValues } from '@connectrpc/connect'; import { runUnaryCall } from '@connectrpc/connect/protocol'; -import { ClientStream, toGRPCMetadata } from './ClientStream'; +import { ClientStream, toGRPCMetadata } from './client-stream'; import { ResponseHeaders, ResponseTrailers, @@ -18,7 +18,7 @@ export class UnaryClientStream< > extends ClientStream { private result?: { success: (value: UnaryResponse) => void; - failure: (reason?: any) => void; + failure: (reason?: unknown) => void; }; private headers?: Headers; @@ -30,7 +30,7 @@ export class UnaryClientStream< message: PartialMessage, contextValues?: ContextValues ): Promise> { - let req = { + const req = { stream: false as const, url: '', init: {}, @@ -41,15 +41,19 @@ export class UnaryClientStream< message, }; type optParams = Parameters>[0]; - let opt: optParams = { + const opt: optParams = { req, - // next is what actually kicks off the request. The run call below will - // ultimately call this for us. - next: async (req: UnaryRequest): Promise> => { + /** + * next is what actually kicks off the request. The run call below will + * ultimately call this for us. + */ + next: async ( + unaryReq: UnaryRequest + ): Promise> => { return new Promise((resolve, reject) => { this.result = { success: resolve, failure: reject }; this.startRequest(); - this.sendMessage(req.message.toBinary()); + this.sendMessage(unaryReq.message.toBinary()); }); }, }; @@ -73,8 +77,8 @@ export class UnaryClientStream< } protected onTrailers(respTrailers: ResponseTrailers): void { - let trailers = toGRPCMetadata(respTrailers.metadata); - if (!respTrailers.status || respTrailers.status.code == 0) { + const trailers = toGRPCMetadata(respTrailers.metadata); + if (!respTrailers.status || respTrailers.status.code === 0) { if (!this.headers) { this.result?.failure( new Error( diff --git a/rpc/js/tsconfig.build.json b/rpc/js/tsconfig.build.json index 3dc922b7..60f0bc1d 100644 --- a/rpc/js/tsconfig.build.json +++ b/rpc/js/tsconfig.build.json @@ -4,7 +4,8 @@ "declaration": true, "emitDeclarationOnly": true, "rootDir": "src", - "outDir": "dist" + "outDir": "dist", + "noEmit": false }, "include": ["src"] } diff --git a/rpc/js/tsconfig.json b/rpc/js/tsconfig.json index e719e96e..ec499548 100644 --- a/rpc/js/tsconfig.json +++ b/rpc/js/tsconfig.json @@ -2,12 +2,10 @@ "extends": "@viamrobotics/typescript-config/base.json", "compilerOptions": { "target": "esnext", - "useDefineForClassFields": true, - "importHelpers": true, "module": "ESNext", "moduleResolution": "node", - "verbatimModuleSyntax": false, - "lib": ["ESNext", "DOM", "DOM.iterable"] + "lib": ["ESNext", "DOM", "DOM.iterable"], + "noEmit": true }, "include": ["src", "vite.config.ts", ".*.cjs"], "exclude": ["node_modules", "src/gen"] From 0b0cc6591e60fe130880d058cb47456d63d917d6 Mon Sep 17 00:00:00 2001 From: Eric Daniels Date: Mon, 23 Sep 2024 13:19:38 -0400 Subject: [PATCH 6/7] lint --- rpc/examples/echo/node-client/src/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rpc/examples/echo/node-client/src/index.ts b/rpc/examples/echo/node-client/src/index.ts index fe724442..b08ca606 100644 --- a/rpc/examples/echo/node-client/src/index.ts +++ b/rpc/examples/echo/node-client/src/index.ts @@ -1,5 +1,5 @@ import type { PartialMessage } from "@bufbuild/protobuf"; -import { ConnectError, createPromiseClient, PromiseClient, Transport } from "@connectrpc/connect"; +import { ConnectError, createPromiseClient, PromiseClient } from "@connectrpc/connect"; import { createWritableIterable } from "@connectrpc/connect/protocol"; import { dialDirect, dialWebRTC } from "@viamrobotics/rpc"; import { EchoService } from "./gen/proto/rpc/examples/echo/v1/echo_connect.js"; @@ -7,11 +7,6 @@ import { EchoBiDiRequest, EchoMultipleRequest, EchoRequest } from "./gen/proto/r import { createGrpcTransport } from '@connectrpc/connect-node'; import wrtc from "node-datachannel/polyfill"; -declare global { - var VIAM: { - GRPC_TRANSPORT_FACTORY: (opts: any) => Transport; - }; -} globalThis.VIAM = { GRPC_TRANSPORT_FACTORY: (opts: any) => createGrpcTransport({ httpVersion: "2", ...opts }), }; From 781ee3e2ff43922a88b94b9d4c72f7057ccbf71d Mon Sep 17 00:00:00 2001 From: Eric Daniels Date: Tue, 24 Sep 2024 12:24:40 -0400 Subject: [PATCH 7/7] simplify --- rpc/js/src/dial.ts | 118 +++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 68 deletions(-) diff --git a/rpc/js/src/dial.ts b/rpc/js/src/dial.ts index 5dbfd167..4be0b545 100644 --- a/rpc/js/src/dial.ts +++ b/rpc/js/src/dial.ts @@ -132,86 +132,68 @@ export const dialDirect = async ( return createTransport(transportOpts); } - const authFact = await makeAuthenticatedTransportFactory( + return makeAuthenticatedTransport( address, createTransport, - opts + opts, + transportOpts ); - return authFact(transportOpts); }; const addressCleanupRegex = /^.*:\/\//u; -const makeAuthenticatedTransportFactory = async ( +const makeAuthenticatedTransport = async ( address: string, defaultFactory: TransportFactory, - opts: DialOptions -): Promise => { - let accessToken = ''; - const getExtraHeaders = async (): Promise => { - const headers = new Headers(); - // TODO(GOUT-10): handle expiration - if (accessToken === '') { - let thisAccessToken = ''; + opts: DialOptions, + transportOpts: TransportInitOptions +): Promise => { + const authHeaders = new Headers(); - if (!opts.accessToken || opts.accessToken === '') { - const request = new AuthenticateRequest({ - entity: opts.authEntity ?? address.replace(addressCleanupRegex, ''), - }); - if (opts.credentials) { - request.credentials = new PBCredentials({ - type: opts.credentials.type, - payload: opts.credentials.payload, - }); - } + let accessToken; + if (!opts.accessToken || opts.accessToken === '') { + const request = new AuthenticateRequest({ + entity: opts.authEntity ?? address.replace(addressCleanupRegex, ''), + }); + if (opts.credentials) { + request.credentials = new PBCredentials({ + type: opts.credentials.type, + payload: opts.credentials.payload, + }); + } - const resolvedAddress = opts.externalAuthAddress ?? address; - const transport = defaultFactory({ baseUrl: resolvedAddress }); - const authClient = createPromiseClient(AuthService, transport); - const resp = await authClient.authenticate(request); - thisAccessToken = resp.accessToken; - } else { - thisAccessToken = opts.accessToken; - } + const resolvedAddress = opts.externalAuthAddress ?? address; + const transport = defaultFactory({ baseUrl: resolvedAddress }); + const authClient = createPromiseClient(AuthService, transport); + const resp = await authClient.authenticate(request); + accessToken = resp.accessToken; + } else { + accessToken = opts.accessToken; + } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- await race - if (accessToken === '') { - accessToken = thisAccessToken; - - if (opts.externalAuthAddress && opts.externalAuthToEntity) { - const authHeaders = new Headers(); - authHeaders.set('authorization', `Bearer ${accessToken}`); - - thisAccessToken = ''; - - const request = new AuthenticateRequest({ - entity: opts.externalAuthToEntity, - }); - const transport = defaultFactory({ - baseUrl: opts.externalAuthAddress, - }); - const externalAuthClient = createPromiseClient( - ExternalAuthService, - transport - ); - const resp = await externalAuthClient.authenticateTo(request, { - headers: authHeaders, - }); - thisAccessToken = resp.accessToken; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- await race - if (accessToken === '') { - accessToken = thisAccessToken; - } - } - } - } - headers.set('authorization', `Bearer ${accessToken}`); - return headers; - }; - const extraMd = await getExtraHeaders(); - return (transportOpts: TransportInitOptions): Transport => { - return new AuthenticatedTransport(transportOpts, defaultFactory, extraMd); - }; + if (opts.externalAuthAddress && opts.externalAuthToEntity) { + const extAuthHeaders = new Headers(); + extAuthHeaders.set('authorization', `Bearer ${accessToken}`); + + accessToken = ''; + + const request = new AuthenticateRequest({ + entity: opts.externalAuthToEntity, + }); + const transport = defaultFactory({ + baseUrl: opts.externalAuthAddress, + }); + const externalAuthClient = createPromiseClient( + ExternalAuthService, + transport + ); + const resp = await externalAuthClient.authenticateTo(request, { + headers: extAuthHeaders, + }); + accessToken = resp.accessToken; + } + authHeaders.set('authorization', `Bearer ${accessToken}`); + return new AuthenticatedTransport(transportOpts, defaultFactory, authHeaders); }; class AuthenticatedTransport implements Transport {