From 4b3f4e986e088c4bdfdd75e62c2b8dbf2c4c39a0 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Fri, 6 Dec 2024 17:05:59 -0600 Subject: [PATCH 1/4] Node gRPC transport (163-2) --- package-lock.json | 42 ++++++++ package.json | 1 + src/controllers/ExtensionController.ts | 20 +++- src/dh/NodeHttp2gRPCTransport.ts | 141 +++++++++++++++++++++++++ src/extension.ts | 2 + 5 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 src/dh/NodeHttp2gRPCTransport.ts diff --git a/package-lock.json b/package-lock.json index 9a10dd3c..8ac1404b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@deephaven-enterprise/auth-nodejs": "^1.20240723.124-beta", "@deephaven-enterprise/query-utils": "^1.20240723.124-beta", "@deephaven/jsapi-nodejs": "^0.99.0", + "@improbable-eng/grpc-web": "^0.15.0", "esbuild": "^0.24.0", "nanoid": "^5.0.7" }, @@ -1190,6 +1191,17 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@improbable-eng/grpc-web": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz", + "integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==", + "dependencies": { + "browser-headers": "^0.4.1" + }, + "peerDependencies": { + "google-protobuf": "^3.14.0" + } + }, "node_modules/@internationalized/date": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.5.tgz", @@ -4290,6 +4302,11 @@ "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/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -7907,6 +7924,12 @@ "node": "*" } }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "peer": true + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -16336,6 +16359,14 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "@improbable-eng/grpc-web": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz", + "integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==", + "requires": { + "browser-headers": "^0.4.1" + } + }, "@internationalized/date": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.5.tgz", @@ -18620,6 +18651,11 @@ "fill-range": "^7.1.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==" + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -21273,6 +21309,12 @@ } } }, + "google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "peer": true + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", diff --git a/package.json b/package.json index 09ea4955..566c9fbd 100644 --- a/package.json +++ b/package.json @@ -849,6 +849,7 @@ "@deephaven-enterprise/auth-nodejs": "^1.20240723.124-beta", "@deephaven-enterprise/query-utils": "^1.20240723.124-beta", "@deephaven/jsapi-nodejs": "^0.99.0", + "@improbable-eng/grpc-web": "^0.15.0", "esbuild": "^0.24.0", "nanoid": "^5.0.7" }, diff --git a/src/controllers/ExtensionController.ts b/src/controllers/ExtensionController.ts index 4c2ff729..91ad8c7f 100644 --- a/src/controllers/ExtensionController.ts +++ b/src/controllers/ExtensionController.ts @@ -78,9 +78,20 @@ import { type AuthenticatedClient as DheAuthenticatedClient, type UnauthenticatedClient as DheUnauthenticatedClient, } from '@deephaven-enterprise/auth-nodejs'; +import type { grpc } from '@improbable-eng/grpc-web'; +import { NodeHttp2gRPCTransport } from '../dh/NodeHttp2gRPCTransport'; const logger = new Logger('ExtensionController'); +declare module '@deephaven/jsapi-types' { + export namespace dh { + export interface ConnectOptions { + debug?: boolean; + transportFactory?: grpc.TransportFactory; + } + } +} + export class ExtensionController implements Disposable { constructor(context: vscode.ExtensionContext, configService: IConfigService) { this._context = context; @@ -333,9 +344,12 @@ export class ExtensionController implements Disposable { assertDefined(this._coreJsApiCache, 'coreJsApiCache'); const dhc = await this._coreJsApiCache.get(url); - const client = new dhc.CoreClient( - url.toString() - ) as CoreUnauthenticatedClient; + const client = new dhc.CoreClient(url.toString(), { + debug: true, + // TODO: This should be optional, but types aren't happy yet + headers: {}, + transportFactory: NodeHttp2gRPCTransport.factory, + }) as CoreUnauthenticatedClient; // Attach a dispose method so that client caches can dispose of the client return Object.assign(client, { diff --git a/src/dh/NodeHttp2gRPCTransport.ts b/src/dh/NodeHttp2gRPCTransport.ts new file mode 100644 index 00000000..0cae3625 --- /dev/null +++ b/src/dh/NodeHttp2gRPCTransport.ts @@ -0,0 +1,141 @@ +import http2 from 'node:http2'; +import { grpc } from '@improbable-eng/grpc-web'; + +export class NodeHttp2gRPCTransport implements grpc.Transport { + static _sessionMap: Map = new Map(); + + /** + * TODO: Cleanup requests similar to https://github.com/deephaven/deephaven-core/blob/c05b35957e466fded4da61154ba106cfc3198bc5/web/client-api/src/main/java/io/deephaven/web/client/api/grpc/MultiplexedWebsocketTransport.java#L129 + * Create a Transport instance. + * @param options Transport options. + * @returns Transport instance. + */ + static factory: grpc.TransportFactory = options => { + const { origin } = new URL(options.url); + + if (!NodeHttp2gRPCTransport._sessionMap.has(origin)) { + const session = http2.connect(origin); + session.on('error', err => { + console.error('Session error', err); + }); + NodeHttp2gRPCTransport._sessionMap.set(origin, session); + } + + const session = NodeHttp2gRPCTransport._sessionMap.get(origin)!; + + return new NodeHttp2gRPCTransport(options, session); + }; + + /** + * Private constructor to restrict instantiation to static factory method. + * @param options Transport options. + * @param session node:http2 session. + */ + private constructor( + options: grpc.TransportOptions, + session: http2.ClientHttp2Session + ) { + this._options = options; + this._session = session; + } + + private readonly _options: grpc.TransportOptions; + private readonly _session: http2.ClientHttp2Session; + private _metadata: grpc.Metadata | null = null; + private _request: http2.ClientHttp2Stream | null = null; + + _createRequest = ( + headers: Record | null + ): http2.ClientHttp2Stream => { + const url = new URL(this._options.url); + + const req = this._session.request({ + ...headers, + // may need to set the :authority header at some point + ':method': 'POST', + ':path': url.pathname, + }); + + console.log('[NodeHttp2Transport] _createRequest', url.pathname); + + req.on('response', (headers, _flags) => { + const headersRecord: Record = {}; + + // strip any undefined headers or keys that start with `:` + for (const name in headers) { + if (headers[name] != null && !name.startsWith(':')) { + headersRecord[name] = headers[name]; + } + } + + this._options.onHeaders( + new grpc.Metadata(headersRecord), + Number(headers[':status']) + ); + }); + + req.on('data', (chunk: Buffer) => { + this._options.onChunk(chunk); + }); + req.on('end', () => { + this._options.onEnd(); + }); + req.on('error', err => { + this._options.onEnd(err); + }); + + return req; + }; + + start(metadata: grpc.Metadata): void { + console.log('[NodeHttp2Transport] start', metadata.headersMap); + + if (this._metadata != null) { + throw new Error('start called more than once'); + } + + this._metadata = metadata; + } + + sendMessage(msgBytes: Uint8Array): void { + console.log('[NodeHttp2Transport] sendMessage', msgBytes); + + const headers: Record = {}; + this._metadata?.forEach((key, values) => { + headers[key] = values.join(', '); + }); + + if ( + !this._options.methodDefinition.requestStream && + !this._options.methodDefinition.responseStream + ) { + // Disable chunked encoding for unary calls + headers['Content-Length'] = String(msgBytes.length); + } + + const request = this._createRequest(headers); + + request.write(msgBytes); + this._request = request; + } + + finishSend(): void { + console.log('[NodeHttp2Transport] finishSend'); + this._request!.end(); + } + + cancel(): void { + console.log('[NodeHttp2Transport] cancel'); + this._request!.close(); + } + + /** + * Cleanup. + */ + static dispose(): void { + for (const session of NodeHttp2gRPCTransport._sessionMap.values()) { + session.close(); + } + NodeHttp2gRPCTransport._sessionMap.clear(); + } +} diff --git a/src/extension.ts b/src/extension.ts index 14aef31b..3f40f132 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,8 @@ import * as vscode from 'vscode'; import { ExtensionController } from './controllers'; import { ConfigService } from './services'; +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + export function activate(context: vscode.ExtensionContext): void { const controller = new ExtensionController(context, ConfigService); From 266538d5daa44d0761c97a85cdf603bb0ccc6275 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Fri, 22 Nov 2024 13:22:04 -0600 Subject: [PATCH 2/4] Moved stream creation into start method (163-2) --- src/dh/NodeHttp2gRPCTransport.ts | 35 ++++++++++++-------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/dh/NodeHttp2gRPCTransport.ts b/src/dh/NodeHttp2gRPCTransport.ts index 0cae3625..89d72ec1 100644 --- a/src/dh/NodeHttp2gRPCTransport.ts +++ b/src/dh/NodeHttp2gRPCTransport.ts @@ -1,5 +1,6 @@ import http2 from 'node:http2'; import { grpc } from '@improbable-eng/grpc-web'; +import { assertDefined } from '../util'; export class NodeHttp2gRPCTransport implements grpc.Transport { static _sessionMap: Map = new Map(); @@ -41,7 +42,6 @@ export class NodeHttp2gRPCTransport implements grpc.Transport { private readonly _options: grpc.TransportOptions; private readonly _session: http2.ClientHttp2Session; - private _metadata: grpc.Metadata | null = null; private _request: http2.ClientHttp2Stream | null = null; _createRequest = ( @@ -90,43 +90,34 @@ export class NodeHttp2gRPCTransport implements grpc.Transport { start(metadata: grpc.Metadata): void { console.log('[NodeHttp2Transport] start', metadata.headersMap); - if (this._metadata != null) { + if (this._request != null) { throw new Error('start called more than once'); } - this._metadata = metadata; - } - - sendMessage(msgBytes: Uint8Array): void { - console.log('[NodeHttp2Transport] sendMessage', msgBytes); - const headers: Record = {}; - this._metadata?.forEach((key, values) => { + metadata?.forEach((key, values) => { headers[key] = values.join(', '); }); - if ( - !this._options.methodDefinition.requestStream && - !this._options.methodDefinition.responseStream - ) { - // Disable chunked encoding for unary calls - headers['Content-Length'] = String(msgBytes.length); - } - - const request = this._createRequest(headers); + this._request = this._createRequest(headers); + } - request.write(msgBytes); - this._request = request; + sendMessage(msgBytes: Uint8Array): void { + console.log('[NodeHttp2Transport] sendMessage', msgBytes); + assertDefined(this._request, '_request'); + this._request.write(msgBytes); } finishSend(): void { console.log('[NodeHttp2Transport] finishSend'); - this._request!.end(); + assertDefined(this._request, '_request'); + this._request.end(); } cancel(): void { console.log('[NodeHttp2Transport] cancel'); - this._request!.close(); + assertDefined(this._request, '_request'); + this._request.close(); } /** From 3520ac2a6b4155b29c7180523beca277ecfae4b5 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Tue, 10 Dec 2024 09:34:19 -0600 Subject: [PATCH 3/4] Using dh defined grpc interface (163-2) --- package-lock.json | 42 --------------- package.json | 1 - src/controllers/ExtensionController.ts | 4 +- src/dh/NodeHttp2gRPCTransport.ts | 53 +++++++++++-------- src/dh/grpc.ts | 73 ++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 src/dh/grpc.ts diff --git a/package-lock.json b/package-lock.json index 8ac1404b..9a10dd3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@deephaven-enterprise/auth-nodejs": "^1.20240723.124-beta", "@deephaven-enterprise/query-utils": "^1.20240723.124-beta", "@deephaven/jsapi-nodejs": "^0.99.0", - "@improbable-eng/grpc-web": "^0.15.0", "esbuild": "^0.24.0", "nanoid": "^5.0.7" }, @@ -1191,17 +1190,6 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "node_modules/@improbable-eng/grpc-web": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz", - "integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==", - "dependencies": { - "browser-headers": "^0.4.1" - }, - "peerDependencies": { - "google-protobuf": "^3.14.0" - } - }, "node_modules/@internationalized/date": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.5.tgz", @@ -4302,11 +4290,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/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -7924,12 +7907,6 @@ "node": "*" } }, - "node_modules/google-protobuf": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", - "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", - "peer": true - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -16359,14 +16336,6 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, - "@improbable-eng/grpc-web": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz", - "integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==", - "requires": { - "browser-headers": "^0.4.1" - } - }, "@internationalized/date": { "version": "3.5.5", "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.5.tgz", @@ -18651,11 +18620,6 @@ "fill-range": "^7.1.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==" - }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -21309,12 +21273,6 @@ } } }, - "google-protobuf": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", - "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", - "peer": true - }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", diff --git a/package.json b/package.json index 566c9fbd..09ea4955 100644 --- a/package.json +++ b/package.json @@ -849,7 +849,6 @@ "@deephaven-enterprise/auth-nodejs": "^1.20240723.124-beta", "@deephaven-enterprise/query-utils": "^1.20240723.124-beta", "@deephaven/jsapi-nodejs": "^0.99.0", - "@improbable-eng/grpc-web": "^0.15.0", "esbuild": "^0.24.0", "nanoid": "^5.0.7" }, diff --git a/src/controllers/ExtensionController.ts b/src/controllers/ExtensionController.ts index 91ad8c7f..5e94d21a 100644 --- a/src/controllers/ExtensionController.ts +++ b/src/controllers/ExtensionController.ts @@ -78,8 +78,8 @@ import { type AuthenticatedClient as DheAuthenticatedClient, type UnauthenticatedClient as DheUnauthenticatedClient, } from '@deephaven-enterprise/auth-nodejs'; -import type { grpc } from '@improbable-eng/grpc-web'; import { NodeHttp2gRPCTransport } from '../dh/NodeHttp2gRPCTransport'; +import type { GrpcTransportFactory } from '../dh/grpc'; const logger = new Logger('ExtensionController'); @@ -87,7 +87,7 @@ declare module '@deephaven/jsapi-types' { export namespace dh { export interface ConnectOptions { debug?: boolean; - transportFactory?: grpc.TransportFactory; + transportFactory?: GrpcTransportFactory; } } } diff --git a/src/dh/NodeHttp2gRPCTransport.ts b/src/dh/NodeHttp2gRPCTransport.ts index 89d72ec1..b117d615 100644 --- a/src/dh/NodeHttp2gRPCTransport.ts +++ b/src/dh/NodeHttp2gRPCTransport.ts @@ -1,8 +1,12 @@ import http2 from 'node:http2'; -import { grpc } from '@improbable-eng/grpc-web'; +import type { + GrpcTransport, + GrpcTransportFactory, + GrpcTransportOptions, +} from './grpc'; import { assertDefined } from '../util'; -export class NodeHttp2gRPCTransport implements grpc.Transport { +export class NodeHttp2gRPCTransport implements GrpcTransport { static _sessionMap: Map = new Map(); /** @@ -11,20 +15,26 @@ export class NodeHttp2gRPCTransport implements grpc.Transport { * @param options Transport options. * @returns Transport instance. */ - static factory: grpc.TransportFactory = options => { - const { origin } = new URL(options.url); - - if (!NodeHttp2gRPCTransport._sessionMap.has(origin)) { - const session = http2.connect(origin); - session.on('error', err => { - console.error('Session error', err); - }); - NodeHttp2gRPCTransport._sessionMap.set(origin, session); - } + static readonly factory: GrpcTransportFactory = { + create: options => { + const { origin } = new URL(options.url); + + if (!NodeHttp2gRPCTransport._sessionMap.has(origin)) { + const session = http2.connect(origin); + session.on('error', err => { + console.error('Session error', err); + }); + NodeHttp2gRPCTransport._sessionMap.set(origin, session); + } + + const session = NodeHttp2gRPCTransport._sessionMap.get(origin)!; - const session = NodeHttp2gRPCTransport._sessionMap.get(origin)!; + return new NodeHttp2gRPCTransport(options, session); + }, - return new NodeHttp2gRPCTransport(options, session); + get supportsClientStreaming(): boolean { + return false; + }, }; /** @@ -33,14 +43,14 @@ export class NodeHttp2gRPCTransport implements grpc.Transport { * @param session node:http2 session. */ private constructor( - options: grpc.TransportOptions, + options: GrpcTransportOptions, session: http2.ClientHttp2Session ) { this._options = options; this._session = session; } - private readonly _options: grpc.TransportOptions; + private readonly _options: GrpcTransportOptions; private readonly _session: http2.ClientHttp2Session; private _request: http2.ClientHttp2Stream | null = null; @@ -68,10 +78,7 @@ export class NodeHttp2gRPCTransport implements grpc.Transport { } } - this._options.onHeaders( - new grpc.Metadata(headersRecord), - Number(headers[':status']) - ); + this._options.onHeaders(headersRecord, Number(headers[':status'])); }); req.on('data', (chunk: Buffer) => { @@ -87,7 +94,7 @@ export class NodeHttp2gRPCTransport implements grpc.Transport { return req; }; - start(metadata: grpc.Metadata): void { + start(metadata: { [key: string]: string | Array }): void { console.log('[NodeHttp2Transport] start', metadata.headersMap); if (this._request != null) { @@ -95,8 +102,8 @@ export class NodeHttp2gRPCTransport implements grpc.Transport { } const headers: Record = {}; - metadata?.forEach((key, values) => { - headers[key] = values.join(', '); + Object.entries(metadata).forEach(([key, value]) => { + headers[key] = typeof value === 'string' ? value : value.join(', '); }); this._request = this._createRequest(headers); diff --git a/src/dh/grpc.ts b/src/dh/grpc.ts new file mode 100644 index 00000000..e8237265 --- /dev/null +++ b/src/dh/grpc.ts @@ -0,0 +1,73 @@ +/** + * Factory for creating gRPC transports. + */ +export interface GrpcTransportFactory { + /** + * Create a new transport instance. + * @param options - options for creating the transport + * @return a transport instance to use for gRPC communication + */ + create(options: GrpcTransportOptions): GrpcTransport; + /** + * Return true to signal that created transports may have {@link GrpcTransport.sendMessage} called on it + * more than once before {@link GrpcTransport.finishSend} should be called. + * @return true to signal that the implementation can stream multiple messages, false otherwise indicating that + * Open/Next gRPC calls should be used + */ + get supportsClientStreaming(): boolean; +} +/** + * Options for creating a gRPC stream transport instance. + */ +export interface GrpcTransportOptions { + /** + * The gRPC method URL. + */ + url: URL; + /** + * True to enable debug logging for this stream. + */ + debug: boolean; + /** + * Callback for when headers and status are received. The headers are a map of header names to values, and the + * status is the HTTP status code. If the connection could not be made, the status should be 0. + */ + onHeaders: ( + headers: { [key: string]: string | Array }, + status: number + ) => void; + /** + * Callback for when a chunk of data is received. + */ + onChunk: (chunk: Uint8Array) => void; + /** + * Callback for when the stream ends, with an error instance if it can be provided. Note that the present + * implementation does not consume errors, even if provided. + */ + onEnd: (error?: Error | undefined | null) => void; +} +/** + * gRPC transport implementation. + */ +export interface GrpcTransport { + /** + * Starts the stream, sending metadata to the server. + * @param metadata - the headers to send the server when opening the connection + */ + start(metadata: { [key: string]: string | Array }): void; + /** + * Sends a message to the server. + * @param msgBytes - bytes to send to the server + */ + sendMessage(msgBytes: Uint8Array): void; + /** + * "Half close" the stream, signaling to the server that no more messages will be sent, but that the client is still + * open to receiving messages. + */ + finishSend(): void; + /** + * End the stream, both notifying the server that no more messages will be sent nor received, and preventing the + * client from receiving any more events. + */ + cancel(): void; +} From 1d6fd216c23251023d3556bce2b62a3a58fd8b26 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Fri, 20 Dec 2024 11:12:34 -0600 Subject: [PATCH 4/4] Code changes for upcoming 0.38 jsapi-types (163-2) --- src/controllers/ExtensionController.ts | 12 ----- src/dh/NodeHttp2gRPCTransport.ts | 10 ++-- src/dh/grpc.ts | 73 -------------------------- 3 files changed, 5 insertions(+), 90 deletions(-) delete mode 100644 src/dh/grpc.ts diff --git a/src/controllers/ExtensionController.ts b/src/controllers/ExtensionController.ts index 5e94d21a..656bd813 100644 --- a/src/controllers/ExtensionController.ts +++ b/src/controllers/ExtensionController.ts @@ -79,19 +79,9 @@ import { type UnauthenticatedClient as DheUnauthenticatedClient, } from '@deephaven-enterprise/auth-nodejs'; import { NodeHttp2gRPCTransport } from '../dh/NodeHttp2gRPCTransport'; -import type { GrpcTransportFactory } from '../dh/grpc'; const logger = new Logger('ExtensionController'); -declare module '@deephaven/jsapi-types' { - export namespace dh { - export interface ConnectOptions { - debug?: boolean; - transportFactory?: GrpcTransportFactory; - } - } -} - export class ExtensionController implements Disposable { constructor(context: vscode.ExtensionContext, configService: IConfigService) { this._context = context; @@ -346,8 +336,6 @@ export class ExtensionController implements Disposable { const client = new dhc.CoreClient(url.toString(), { debug: true, - // TODO: This should be optional, but types aren't happy yet - headers: {}, transportFactory: NodeHttp2gRPCTransport.factory, }) as CoreUnauthenticatedClient; diff --git a/src/dh/NodeHttp2gRPCTransport.ts b/src/dh/NodeHttp2gRPCTransport.ts index b117d615..4d3fb691 100644 --- a/src/dh/NodeHttp2gRPCTransport.ts +++ b/src/dh/NodeHttp2gRPCTransport.ts @@ -1,11 +1,11 @@ import http2 from 'node:http2'; -import type { - GrpcTransport, - GrpcTransportFactory, - GrpcTransportOptions, -} from './grpc'; +import type { dh as DhcType } from '@deephaven/jsapi-types'; import { assertDefined } from '../util'; +type GrpcTransport = DhcType.grpc.GrpcTransport; +type GrpcTransportFactory = DhcType.grpc.GrpcTransportFactory; +type GrpcTransportOptions = DhcType.grpc.GrpcTransportOptions; + export class NodeHttp2gRPCTransport implements GrpcTransport { static _sessionMap: Map = new Map(); diff --git a/src/dh/grpc.ts b/src/dh/grpc.ts deleted file mode 100644 index e8237265..00000000 --- a/src/dh/grpc.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Factory for creating gRPC transports. - */ -export interface GrpcTransportFactory { - /** - * Create a new transport instance. - * @param options - options for creating the transport - * @return a transport instance to use for gRPC communication - */ - create(options: GrpcTransportOptions): GrpcTransport; - /** - * Return true to signal that created transports may have {@link GrpcTransport.sendMessage} called on it - * more than once before {@link GrpcTransport.finishSend} should be called. - * @return true to signal that the implementation can stream multiple messages, false otherwise indicating that - * Open/Next gRPC calls should be used - */ - get supportsClientStreaming(): boolean; -} -/** - * Options for creating a gRPC stream transport instance. - */ -export interface GrpcTransportOptions { - /** - * The gRPC method URL. - */ - url: URL; - /** - * True to enable debug logging for this stream. - */ - debug: boolean; - /** - * Callback for when headers and status are received. The headers are a map of header names to values, and the - * status is the HTTP status code. If the connection could not be made, the status should be 0. - */ - onHeaders: ( - headers: { [key: string]: string | Array }, - status: number - ) => void; - /** - * Callback for when a chunk of data is received. - */ - onChunk: (chunk: Uint8Array) => void; - /** - * Callback for when the stream ends, with an error instance if it can be provided. Note that the present - * implementation does not consume errors, even if provided. - */ - onEnd: (error?: Error | undefined | null) => void; -} -/** - * gRPC transport implementation. - */ -export interface GrpcTransport { - /** - * Starts the stream, sending metadata to the server. - * @param metadata - the headers to send the server when opening the connection - */ - start(metadata: { [key: string]: string | Array }): void; - /** - * Sends a message to the server. - * @param msgBytes - bytes to send to the server - */ - sendMessage(msgBytes: Uint8Array): void; - /** - * "Half close" the stream, signaling to the server that no more messages will be sent, but that the client is still - * open to receiving messages. - */ - finishSend(): void; - /** - * End the stream, both notifying the server that no more messages will be sent nor received, and preventing the - * client from receiving any more events. - */ - cancel(): void; -}