From d6ce2b17f118e4cf941d12336819f253616d9458 Mon Sep 17 00:00:00 2001 From: segfault16 Date: Thu, 14 Sep 2023 08:00:53 +0200 Subject: [PATCH 01/10] Initial version of devtools logging middleware --- .../.gitignore | 5 + .../CHANGELOG.md | 6 + .../LICENSE.md | 20 ++ .../README.md | 36 ++++ .../fixtures/.gitignore | 2 + .../fixtures/test.proto | 21 ++ .../jest.config.js | 12 ++ .../package.json | 36 ++++ .../src/index.test.ts | 14 ++ .../src/index.ts | 180 ++++++++++++++++++ .../tsconfig.build.json | 4 + .../tsconfig.json | 12 ++ 12 files changed, 348 insertions(+) create mode 100644 packages/nice-grpc-client-middleware-devtools/.gitignore create mode 100644 packages/nice-grpc-client-middleware-devtools/CHANGELOG.md create mode 100644 packages/nice-grpc-client-middleware-devtools/LICENSE.md create mode 100644 packages/nice-grpc-client-middleware-devtools/README.md create mode 100644 packages/nice-grpc-client-middleware-devtools/fixtures/.gitignore create mode 100644 packages/nice-grpc-client-middleware-devtools/fixtures/test.proto create mode 100644 packages/nice-grpc-client-middleware-devtools/jest.config.js create mode 100644 packages/nice-grpc-client-middleware-devtools/package.json create mode 100644 packages/nice-grpc-client-middleware-devtools/src/index.test.ts create mode 100644 packages/nice-grpc-client-middleware-devtools/src/index.ts create mode 100644 packages/nice-grpc-client-middleware-devtools/tsconfig.build.json create mode 100644 packages/nice-grpc-client-middleware-devtools/tsconfig.json diff --git a/packages/nice-grpc-client-middleware-devtools/.gitignore b/packages/nice-grpc-client-middleware-devtools/.gitignore new file mode 100644 index 00000000..7d949f8a --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/.gitignore @@ -0,0 +1,5 @@ +node_modules +lib +es +coverage +.DS_Store diff --git a/packages/nice-grpc-client-middleware-devtools/CHANGELOG.md b/packages/nice-grpc-client-middleware-devtools/CHANGELOG.md new file mode 100644 index 00000000..0b9e6356 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/CHANGELOG.md @@ -0,0 +1,6 @@ +# Change Log + +All notable changes to this project will be documented in this file. See +[Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## 1.0.0 (2023-09-14) diff --git a/packages/nice-grpc-client-middleware-devtools/LICENSE.md b/packages/nice-grpc-client-middleware-devtools/LICENSE.md new file mode 100644 index 00000000..c8cf2408 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2023 Sebastian Halder + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/nice-grpc-client-middleware-devtools/README.md b/packages/nice-grpc-client-middleware-devtools/README.md new file mode 100644 index 00000000..208550d1 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/README.md @@ -0,0 +1,36 @@ +# nice-grpc-client-middleware-devtools [![npm version][npm-image]][npm-url] + +Client middleware for [nice-grpc](https://github.com/deeplay-io/nice-grpc) that +enables seeing grpc-web requests in [grpc-web-tools](https://github.com/SafetyCulture/grpc-web-devtools). + +## Installation + +``` +npm install nice-grpc-client-middleware-devtools +``` + +## Usage + +```ts +import { + createClientFactory, + createChannel, + ClientError, + Status, +} from 'nice-grpc'; +import {devtoolsLoggingMiddleware} from 'nice-grpc-client-middleware-deadline'; + +const clientFactory = createClientFactory().use(devtoolsLoggingMiddlware); + +const channel = createChannel(address); +const client = clientFactory.create(ExampleService, channel); + +const response = await client.exampleMethod(request); +// The request and response will be visible in the Browser extension +``` + +Alternatively, only logging for unary requests can be achieved by using `devtoolsUnaryLoggingMiddleware` +or logging for streaming requests by using `devtoolsStreamLoggingMiddleware`. + +[npm-image]: https://badge.fury.io/js/nice-grpc-client-middleware-devtools.svg +[npm-url]: https://badge.fury.io/js/nice-grpc-client-middleware-devtools \ No newline at end of file diff --git a/packages/nice-grpc-client-middleware-devtools/fixtures/.gitignore b/packages/nice-grpc-client-middleware-devtools/fixtures/.gitignore new file mode 100644 index 00000000..4fa566d9 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/fixtures/.gitignore @@ -0,0 +1,2 @@ +*.ts +*.js diff --git a/packages/nice-grpc-client-middleware-devtools/fixtures/test.proto b/packages/nice-grpc-client-middleware-devtools/fixtures/test.proto new file mode 100644 index 00000000..1eb8c499 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/fixtures/test.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package nice_grpc.test; + +service Test { + rpc TestUnary(TestRequest) returns(TestResponse){}; + rpc TestServerStream(TestRequest) returns(stream TestResponse){}; + rpc TestClientStream(stream TestRequest) returns(TestResponse){}; + rpc TestBidiStream(stream TestRequest) returns(stream TestResponse){}; +} + +service Test2 { + rpc TestUnary(TestRequest) returns(TestResponse){}; +} + +message TestRequest { + string id = 1; +} +message TestResponse { + string id = 1; +} diff --git a/packages/nice-grpc-client-middleware-devtools/jest.config.js b/packages/nice-grpc-client-middleware-devtools/jest.config.js new file mode 100644 index 00000000..d4c74235 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/jest.config.js @@ -0,0 +1,12 @@ +/** @type {import('@jest/types').Config.GlobalConfig} */ +module.exports = { + testPathIgnorePatterns: ['/node_modules/', '/lib/'], + preset: 'ts-jest', + testEnvironment: 'node', + testTimeout: 15000, + reporters: ['default', 'github-actions'], + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: ['lcov', 'text'], + coveragePathIgnorePatterns: ['/node_modules/', '/lib/'], +}; diff --git a/packages/nice-grpc-client-middleware-devtools/package.json b/packages/nice-grpc-client-middleware-devtools/package.json new file mode 100644 index 00000000..952e3ff0 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/package.json @@ -0,0 +1,36 @@ +{ + "name": "nice-grpc-client-middleware-devtools", + "version": "1.0.0", + "description": "Client middleware for nice-grpc to work with grpc-web-devtools https://github.com/SafetyCulture/grpc-web-devtools", + "repository": "deeplay-io/nice-grpc", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "files": [ + "lib", + "src", + "!src/**/*.test.ts", + "!src/**/__tests__" + ], + "scripts": { + "clean": "rimraf lib", + "test": "jest", + "build": "tsc -P tsconfig.build.json", + "prepublishOnly": "npm run clean && npm run build && npm test", + "build:proto": "grpc_tools_node_protoc --plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:./fixtures --ts_out=grpc_js:./fixtures --grpc_out=grpc_js:./fixtures -I fixtures fixtures/*.proto", + "prepare": "npm run build:proto" + }, + "author": "Sebastian Halder", + "license": "MIT", + "devDependencies": { + "@tsconfig/recommended": "^1.0.1", + "@types/google-protobuf": "^3.7.4", + "abort-controller-x": "^0.4.0", + "google-protobuf": "^3.14.0", + "grpc-tools": "^1.10.0", + "grpc_tools_node_protoc_ts": "^5.0.1", + "nice-grpc": "^2.1.5" + }, + "dependencies": { + "nice-grpc-common": "^2.0.2" + } +} diff --git a/packages/nice-grpc-client-middleware-devtools/src/index.test.ts b/packages/nice-grpc-client-middleware-devtools/src/index.test.ts new file mode 100644 index 00000000..77bfaf89 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/src/index.test.ts @@ -0,0 +1,14 @@ +import defer = require('defer-promise'); +import {forever} from 'abort-controller-x'; +import { + createChannel, + createClientFactory, + createServer, + ServerError, + Status, +} from 'nice-grpc'; +import {deadlineMiddleware} from '.'; +import {TestService} from '../fixtures/test_grpc_pb'; +import {TestRequest, TestResponse} from '../fixtures/test_pb'; + +// TODO: Can this middleware be tested? \ No newline at end of file diff --git a/packages/nice-grpc-client-middleware-devtools/src/index.ts b/packages/nice-grpc-client-middleware-devtools/src/index.ts new file mode 100644 index 00000000..37f7a907 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/src/index.ts @@ -0,0 +1,180 @@ +import { isAbortError } from 'abort-controller-x'; +import { Message } from 'google-protobuf'; +import { + CallOptions, + ClientError, + ClientMiddleware, + ClientMiddlewareCall, + composeClientMiddleware, +} from 'nice-grpc-web'; + +export const devtoolsUnaryLoggingMiddleware: ClientMiddleware = async function* devtoolsLoggingMiddleware( + call: ClientMiddlewareCall, + options: CallOptions +): AsyncGenerator { + // skip streaming calls + if (call.requestStream || call.responseStream) { + return yield* call.next(call.request, options); + } + + const { path } = call.method; + + let reqObj: any = undefined; + + // TODO: When would it not be a jspb.Message? + if (call.request instanceof Message) { + const reqMsg = call.request as Message; + reqObj = reqMsg.toObject(); + } else { + //eslint-disable-next-line + reqObj = call.request; + // console.warn('TODO: Request isn\'t a jspb.Message'); + } + + try { + const result = yield* call.next(call.request, options); + let resObj: any = undefined; + // TODO: When would it not be a jspb.Message? + if (result instanceof Message) { + const resMsg = result as Message; + resObj = resMsg.toObject(); + } else { + //eslint-disable-next-line + resObj = result; + // console.warn("TODO: Response isn't a jspb.Message"); + } + window.postMessage( + { + method: path, + methodType: 'unary', + request: reqObj, + response: resObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + return result; + } catch (error) { + if (error instanceof ClientError) { + window.postMessage( + { + error: { + code: error?.code, + message: `${error?.message || error}`, + // meta: error?.meta, + // methodName: error?.methodName, + name: error?.name, + // serviceName: error?.serviceName, + stack: error?.stack, + }, + method: path, + methodType: 'unary', + request: reqObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + } else if (isAbortError(error)) { + //eslint-disable-next-line + console.debug('GRPC REQUEST CANCEL', path); + } else { + //eslint-disable-next-line + console.error('GRPC RESPONSE FAILURE', path, error); + } + + throw error; + } +}; + +export const devtoolsStreamLoggingMiddleware: ClientMiddleware = async function* devtoolsLoggingMiddleware( + call: ClientMiddlewareCall, + options: CallOptions +): AsyncGenerator { + const { path } = call.method; + + let reqObj: any = undefined; + + // TODO: When would it not be a jspb.Message? + if (call.request instanceof Message) { + const reqMsg = call.request as Message; + reqObj = reqMsg.toObject(); + } else { + //eslint-disable-next-line + reqObj = call.request; + } + + if (!call.responseStream && !call.requestStream) { + // UNARY + return yield* call.next(call.request, options); + } + // server streaming + let first = true; + try { + for await (const response of call.next(call.request, options)) { + // log response here + let resObj: any = undefined; + if (response instanceof Message) { + const resMsg = response as Message; + resObj = resMsg.toObject(); + } else { + resObj = response; + } + if (first) { + window.postMessage( + { + method: path, + methodType: 'server_streaming', + request: reqObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + first = false; + } + window.postMessage( + { + method: path, + methodType: 'server_streaming', + response: resObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + + yield response; + } + } catch (error) { + if (error instanceof ClientError) { + window.postMessage( + { + error: { + code: error?.code, + message: `${error?.message || error}`, + // meta: error?.meta, + // methodName: error?.methodName, + name: error?.name, + // serviceName: error?.serviceName, + stack: error?.stack, + }, + method: path, + methodType: 'server_streaming', + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + } else if (isAbortError(error)) { + //eslint-disable-next-line + console.debug('GRPC REQUEST CANCEL', path); + } else { + //eslint-disable-next-line + console.error('GRPC RESPONSE FAILURE', path, error); + } + + throw error; + } +}; + +export const devtoolsLoggingMiddleware: ClientMiddleware = composeClientMiddleware( + devtoolsUnaryLoggingMiddleware, + devtoolsStreamLoggingMiddleware +); diff --git a/packages/nice-grpc-client-middleware-devtools/tsconfig.build.json b/packages/nice-grpc-client-middleware-devtools/tsconfig.build.json new file mode 100644 index 00000000..cecb44a1 --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "exclude": ["src/**/__tests__/**/*.ts", "src/**/*.test.ts"] +} diff --git a/packages/nice-grpc-client-middleware-devtools/tsconfig.json b/packages/nice-grpc-client-middleware-devtools/tsconfig.json new file mode 100644 index 00000000..d83af9ca --- /dev/null +++ b/packages/nice-grpc-client-middleware-devtools/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/recommended/tsconfig.json", + "compilerOptions": { + "target": "ES2018", + "outDir": "lib", + "sourceMap": true, + "declaration": true, + "stripInternal": true + }, + "files": ["src/index.ts"], + "include": ["src/**/__tests__/**/*.ts", "src/**/*.test.ts"] +} From 9a26a879dcffc66610ca5403274d1de066f42f29 Mon Sep 17 00:00:00 2001 From: segfault16 Date: Wed, 4 Oct 2023 06:25:19 +0200 Subject: [PATCH 02/10] Update import --- packages/nice-grpc-client-middleware-devtools/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nice-grpc-client-middleware-devtools/src/index.ts b/packages/nice-grpc-client-middleware-devtools/src/index.ts index 37f7a907..bd3a1587 100644 --- a/packages/nice-grpc-client-middleware-devtools/src/index.ts +++ b/packages/nice-grpc-client-middleware-devtools/src/index.ts @@ -6,7 +6,7 @@ import { ClientMiddleware, ClientMiddlewareCall, composeClientMiddleware, -} from 'nice-grpc-web'; +} from 'nice-grpc-common'; export const devtoolsUnaryLoggingMiddleware: ClientMiddleware = async function* devtoolsLoggingMiddleware( call: ClientMiddlewareCall, From aae107184554676c212ac792084ab105bafdd0d2 Mon Sep 17 00:00:00 2001 From: segfault16 Date: Thu, 5 Oct 2023 06:35:11 +0200 Subject: [PATCH 03/10] Add optional logging of cancelled requests --- .../src/index.ts | 312 ++++++++++-------- 1 file changed, 174 insertions(+), 138 deletions(-) diff --git a/packages/nice-grpc-client-middleware-devtools/src/index.ts b/packages/nice-grpc-client-middleware-devtools/src/index.ts index bd3a1587..1814a8af 100644 --- a/packages/nice-grpc-client-middleware-devtools/src/index.ts +++ b/packages/nice-grpc-client-middleware-devtools/src/index.ts @@ -1,5 +1,4 @@ import { isAbortError } from 'abort-controller-x'; -import { Message } from 'google-protobuf'; import { CallOptions, ClientError, @@ -8,173 +7,210 @@ import { composeClientMiddleware, } from 'nice-grpc-common'; -export const devtoolsUnaryLoggingMiddleware: ClientMiddleware = async function* devtoolsLoggingMiddleware( - call: ClientMiddlewareCall, - options: CallOptions -): AsyncGenerator { - // skip streaming calls - if (call.requestStream || call.responseStream) { - return yield* call.next(call.request, options); - } - - const { path } = call.method; +export type DevtoolsLoggingOptions = { + /** + * Skip logging abort errors. + * + * By default, abort errors are logged. + */ + skipAbortErrorLogging?: boolean; +}; - let reqObj: any = undefined; +export const devtoolsUnaryLoggingMiddleware: ClientMiddleware = + async function* devtoolsLoggingMiddleware( + call: ClientMiddlewareCall, + options: CallOptions & Partial + ): AsyncGenerator { + // skip streaming calls + if (call.requestStream || call.responseStream) { + return yield* call.next(call.request, options); + } - // TODO: When would it not be a jspb.Message? - if (call.request instanceof Message) { - const reqMsg = call.request as Message; - reqObj = reqMsg.toObject(); - } else { - //eslint-disable-next-line - reqObj = call.request; - // console.warn('TODO: Request isn\'t a jspb.Message'); - } + // log unary calls + const { path } = call.method; + const reqObj = getAsObject(call.request); - try { - const result = yield* call.next(call.request, options); - let resObj: any = undefined; - // TODO: When would it not be a jspb.Message? - if (result instanceof Message) { - const resMsg = result as Message; - resObj = resMsg.toObject(); - } else { - //eslint-disable-next-line - resObj = result; - // console.warn("TODO: Response isn't a jspb.Message"); - } - window.postMessage( - { - method: path, - methodType: 'unary', - request: reqObj, - response: resObj, - type: '__GRPCWEB_DEVTOOLS__', - }, - '*' - ); - return result; - } catch (error) { - if (error instanceof ClientError) { + try { + const result = yield* call.next(call.request, options); + const resObj = getAsObject(result); window.postMessage( { - error: { - code: error?.code, - message: `${error?.message || error}`, - // meta: error?.meta, - // methodName: error?.methodName, - name: error?.name, - // serviceName: error?.serviceName, - stack: error?.stack, - }, method: path, methodType: 'unary', request: reqObj, + response: resObj, type: '__GRPCWEB_DEVTOOLS__', }, '*' ); - } else if (isAbortError(error)) { - //eslint-disable-next-line - console.debug('GRPC REQUEST CANCEL', path); - } else { - //eslint-disable-next-line - console.error('GRPC RESPONSE FAILURE', path, error); - } + return result; + } catch (error) { + if (error instanceof ClientError) { + window.postMessage( + { + error: { + code: error?.code, + message: `${error?.message || error}`, + name: error?.name, + stack: error?.stack, + }, + method: path, + methodType: 'unary', + request: reqObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + } else if (isAbortError(error) && error instanceof Error) { + if (!options.skipAbortErrorLogging) { + window.postMessage( + { + error: { + code: 1, + message: `${error?.message || error}`, + name: error?.name, + stack: error?.stack, + }, + method: path, + methodType: 'unary', + request: reqObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + } + } else if (error instanceof Error) { + window.postMessage( + { + error: { + code: 2, + message: `${error?.message || error}`, + name: error?.name, + stack: error?.stack, + }, + method: path, + methodType: 'unary', + request: reqObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + } - throw error; - } -}; + throw error; + } + }; -export const devtoolsStreamLoggingMiddleware: ClientMiddleware = async function* devtoolsLoggingMiddleware( - call: ClientMiddlewareCall, - options: CallOptions -): AsyncGenerator { - const { path } = call.method; +export const devtoolsStreamLoggingMiddleware: ClientMiddleware = + async function* devtoolsLoggingMiddleware( + call: ClientMiddlewareCall, + options: CallOptions & Partial + ): AsyncGenerator { + // skip unary calls + if (!call.responseStream && !call.requestStream) { + return yield* call.next(call.request, options); + } - let reqObj: any = undefined; + // log streaming calls + const { path } = call.method; + const reqObj = getAsObject(call.request); - // TODO: When would it not be a jspb.Message? - if (call.request instanceof Message) { - const reqMsg = call.request as Message; - reqObj = reqMsg.toObject(); - } else { - //eslint-disable-next-line - reqObj = call.request; - } + let first = true; + try { + for await (const response of call.next(call.request, options)) { + const resObj = getAsObject(response); + if (first) { + // log the request object only once + window.postMessage( + { + method: path, + methodType: 'server_streaming', + request: reqObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + first = false; + } + // log the response + window.postMessage( + { + method: path, + methodType: 'server_streaming', + response: resObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); - if (!call.responseStream && !call.requestStream) { - // UNARY - return yield* call.next(call.request, options); - } - // server streaming - let first = true; - try { - for await (const response of call.next(call.request, options)) { - // log response here - let resObj: any = undefined; - if (response instanceof Message) { - const resMsg = response as Message; - resObj = resMsg.toObject(); - } else { - resObj = response; + yield response; } - if (first) { + } catch (error) { + if (error instanceof ClientError) { window.postMessage( { + error: { + code: error?.code, + message: `${error?.message || error}`, + name: error?.name, + stack: error?.stack, + }, + method: path, + methodType: 'server_streaming', + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + } else if (isAbortError(error) && error instanceof Error) { + if (!options.skipAbortErrorLogging) { + window.postMessage( + { + error: { + code: 1, + message: `${error?.message || error}`, + name: error?.name, + stack: error?.stack, + }, + method: path, + methodType: 'server_streaming', + type: '__GRPCWEB_DEVTOOLS__', + }, + '*' + ); + } + } else if (error instanceof Error) { + window.postMessage( + { + error: { + code: 2, + message: `${error?.message || error}`, + name: error?.name, + stack: error?.stack, + }, method: path, methodType: 'server_streaming', - request: reqObj, type: '__GRPCWEB_DEVTOOLS__', }, '*' ); - first = false; } - window.postMessage( - { - method: path, - methodType: 'server_streaming', - response: resObj, - type: '__GRPCWEB_DEVTOOLS__', - }, - '*' - ); - yield response; + throw error; } - } catch (error) { - if (error instanceof ClientError) { - window.postMessage( - { - error: { - code: error?.code, - message: `${error?.message || error}`, - // meta: error?.meta, - // methodName: error?.methodName, - name: error?.name, - // serviceName: error?.serviceName, - stack: error?.stack, - }, - method: path, - methodType: 'server_streaming', - type: '__GRPCWEB_DEVTOOLS__', - }, - '*' - ); - } else if (isAbortError(error)) { - //eslint-disable-next-line - console.debug('GRPC REQUEST CANCEL', path); - } else { - //eslint-disable-next-line - console.error('GRPC RESPONSE FAILURE', path, error); - } - - throw error; - } -}; + }; -export const devtoolsLoggingMiddleware: ClientMiddleware = composeClientMiddleware( +export const devtoolsLoggingMiddleware: ClientMiddleware = composeClientMiddleware( devtoolsUnaryLoggingMiddleware, devtoolsStreamLoggingMiddleware ); + +// check whether the given object has toObject() method and return the object +// otherwise return the object itself +function getAsObject(obj: any) { + if ('toObject' in obj && typeof obj.toObject === 'function') { + // google-protobuf + return obj.toObject(); + } + // ts-proto + return obj; +} \ No newline at end of file From 072bee5935a0a0bfe5af609aeb79ae0bbaf2626b Mon Sep 17 00:00:00 2001 From: segfault16 Date: Thu, 5 Oct 2023 06:35:17 +0200 Subject: [PATCH 04/10] Fix README --- packages/nice-grpc-client-middleware-devtools/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nice-grpc-client-middleware-devtools/README.md b/packages/nice-grpc-client-middleware-devtools/README.md index 208550d1..5acd311f 100644 --- a/packages/nice-grpc-client-middleware-devtools/README.md +++ b/packages/nice-grpc-client-middleware-devtools/README.md @@ -18,7 +18,7 @@ import { ClientError, Status, } from 'nice-grpc'; -import {devtoolsLoggingMiddleware} from 'nice-grpc-client-middleware-deadline'; +import {devtoolsLoggingMiddleware} from 'nice-grpc-client-middleware-devtools'; const clientFactory = createClientFactory().use(devtoolsLoggingMiddlware); From 8d44c943d6234a36966899308135b82e47d7badb Mon Sep 17 00:00:00 2001 From: segfault16 Date: Thu, 5 Oct 2023 06:35:54 +0200 Subject: [PATCH 05/10] Update license --- packages/nice-grpc-client-middleware-devtools/LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nice-grpc-client-middleware-devtools/LICENSE.md b/packages/nice-grpc-client-middleware-devtools/LICENSE.md index c8cf2408..10667438 100644 --- a/packages/nice-grpc-client-middleware-devtools/LICENSE.md +++ b/packages/nice-grpc-client-middleware-devtools/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2023 Sebastian Halder +Copyright (c) 2023 Deeplay Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From b2e7e272e0ff43cf468a3fddfcaf10ce6d48ad81 Mon Sep 17 00:00:00 2001 From: segfault16 Date: Thu, 5 Oct 2023 06:57:56 +0200 Subject: [PATCH 06/10] Fix import in tests --- packages/nice-grpc-client-middleware-devtools/src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nice-grpc-client-middleware-devtools/src/index.test.ts b/packages/nice-grpc-client-middleware-devtools/src/index.test.ts index 77bfaf89..f87cee07 100644 --- a/packages/nice-grpc-client-middleware-devtools/src/index.test.ts +++ b/packages/nice-grpc-client-middleware-devtools/src/index.test.ts @@ -7,7 +7,7 @@ import { ServerError, Status, } from 'nice-grpc'; -import {deadlineMiddleware} from '.'; +import {devtoolsLoggingMiddleware} from '.'; import {TestService} from '../fixtures/test_grpc_pb'; import {TestRequest, TestResponse} from '../fixtures/test_pb'; From df938784c92b00ffee3684512fb7f0cb3bd1809f Mon Sep 17 00:00:00 2001 From: Sebastian Halder Date: Mon, 9 Oct 2023 05:34:21 +0000 Subject: [PATCH 07/10] Add tests for devtools middleware --- .../jest.config.js | 7 +- .../package.json | 6 +- .../src/index.test.ts | 119 +++++++++++++++++- 3 files changed, 126 insertions(+), 6 deletions(-) diff --git a/packages/nice-grpc-client-middleware-devtools/jest.config.js b/packages/nice-grpc-client-middleware-devtools/jest.config.js index d4c74235..5bb07079 100644 --- a/packages/nice-grpc-client-middleware-devtools/jest.config.js +++ b/packages/nice-grpc-client-middleware-devtools/jest.config.js @@ -1,6 +1,6 @@ /** @type {import('@jest/types').Config.GlobalConfig} */ module.exports = { - testPathIgnorePatterns: ['/node_modules/', '/lib/'], + testPathIgnorePatterns: ['/node_modules/', '/lib/', '/fixtures/'], preset: 'ts-jest', testEnvironment: 'node', testTimeout: 15000, @@ -9,4 +9,9 @@ module.exports = { coverageDirectory: 'coverage', coverageReporters: ['lcov', 'text'], coveragePathIgnorePatterns: ['/node_modules/', '/lib/'], + globals: { + window: { + postMessage: () => ({}), + }, + }, }; diff --git a/packages/nice-grpc-client-middleware-devtools/package.json b/packages/nice-grpc-client-middleware-devtools/package.json index 952e3ff0..afb4d27f 100644 --- a/packages/nice-grpc-client-middleware-devtools/package.json +++ b/packages/nice-grpc-client-middleware-devtools/package.json @@ -16,8 +16,10 @@ "test": "jest", "build": "tsc -P tsconfig.build.json", "prepublishOnly": "npm run clean && npm run build && npm test", - "build:proto": "grpc_tools_node_protoc --plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:./fixtures --ts_out=grpc_js:./fixtures --grpc_out=grpc_js:./fixtures -I fixtures fixtures/*.proto", - "prepare": "npm run build:proto" + "prepare:proto:grpc-js": "mkdirp ./fixtures/grpc-js && grpc_tools_node_protoc --plugin=protoc-gen-grpc=./node_modules/.bin/grpc_tools_node_protoc_plugin --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --js_out=import_style=commonjs,binary:./fixtures/grpc-js --ts_out=grpc_js:./fixtures/grpc-js --grpc_out=grpc_js:./fixtures/grpc-js -I fixtures fixtures/*.proto", + "prepare:proto:ts-proto": "mkdirp ./fixtures/ts-proto && grpc_tools_node_protoc --ts_proto_out=./fixtures/ts-proto --ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false,esModuleInterop=true -I fixtures fixtures/*.proto", + "prepare:proto": "npm run prepare:proto:grpc-js && npm run prepare:proto:ts-proto", + "prepare": "npm run prepare:proto" }, "author": "Sebastian Halder", "license": "MIT", diff --git a/packages/nice-grpc-client-middleware-devtools/src/index.test.ts b/packages/nice-grpc-client-middleware-devtools/src/index.test.ts index f87cee07..cd9182f2 100644 --- a/packages/nice-grpc-client-middleware-devtools/src/index.test.ts +++ b/packages/nice-grpc-client-middleware-devtools/src/index.test.ts @@ -8,7 +8,120 @@ import { Status, } from 'nice-grpc'; import {devtoolsLoggingMiddleware} from '.'; -import {TestService} from '../fixtures/test_grpc_pb'; -import {TestRequest, TestResponse} from '../fixtures/test_pb'; +import {TestService} from '../fixtures/grpc-js/test_grpc_pb'; +import { + TestDefinition, + TestRequest as TestRequestTS, +} from '../fixtures/ts-proto/test'; +import { + TestRequest as TestRequestJS, + TestResponse as TestResponseJS, +} from '../fixtures/grpc-js/test_pb'; + +function throwUnimplemented(): never { + throw new ServerError(Status.UNIMPLEMENTED, ''); +} + +let windowSpy: jest.SpyInstance; +let postMessageMock: jest.Mock; + +beforeEach(() => { + postMessageMock = jest.fn(); + windowSpy = jest.spyOn(window, 'postMessage'); + windowSpy.mockImplementation(postMessageMock); +}); + +afterEach(() => { + windowSpy.mockRestore(); +}); + +describe('devtools', () => { + test('grpc-js logs unary calls', async () => { + const server = createServer(); + + server.add(TestService, { + async testUnary(request: TestRequestJS, {signal}) { + return new TestResponseJS(); + }, + testServerStream: throwUnimplemented, + testClientStream: throwUnimplemented, + testBidiStream: throwUnimplemented, + }); + + const port = await server.listen('localhost:0'); + + const channel = createChannel(`localhost:${port}`); + const client = createClientFactory() + .use(devtoolsLoggingMiddleware) + .create(TestService, channel); + + const req = new TestRequestJS(); + req.setId('test-id'); + + const promise = client.testUnary(req); + + await expect(promise).resolves.toEqual(new TestResponseJS()); + await expect(postMessageMock).toHaveBeenCalledTimes(1); + await expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + request: { + id: 'test-id', + }, + response: { + id: '', + }, + methodType: 'unary', + method: '/nice_grpc.test.Test/TestUnary', + }), + '*', + ); + + channel.close(); + + await server.shutdown(); + }); + + test('ts-proto logs unary calls', async () => { + const server = createServer(); + + server.add(TestService, { + async testUnary(request: TestRequestJS, {signal}) { + return new TestResponseJS(); + }, + testServerStream: throwUnimplemented, + testClientStream: throwUnimplemented, + testBidiStream: throwUnimplemented, + }); + + const port = await server.listen('localhost:0'); + + const channel = createChannel(`localhost:${port}`); + const client = createClientFactory() + .use(devtoolsLoggingMiddleware) + .create(TestDefinition, channel); + + const req: TestRequestTS = {id: 'test-id'}; + + const promise = client.testUnary(req); + + await expect(promise).resolves.toEqual({id: ''}); + await expect(postMessageMock).toHaveBeenCalledTimes(1); + await expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + request: { + id: 'test-id', + }, + response: { + id: '', + }, + methodType: 'unary', + method: '/nice_grpc.test.Test/TestUnary', + }), + '*', + ); + + channel.close(); -// TODO: Can this middleware be tested? \ No newline at end of file + await server.shutdown(); + }); +}); From 39b44ffa851f893f8e8e8106c6c5e5541f536918 Mon Sep 17 00:00:00 2001 From: Sebastian Halder Date: Fri, 13 Oct 2023 03:51:22 +0000 Subject: [PATCH 08/10] Update abort-controller-x dependency --- packages/nice-grpc-client-middleware-devtools/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nice-grpc-client-middleware-devtools/package.json b/packages/nice-grpc-client-middleware-devtools/package.json index afb4d27f..accbc32b 100644 --- a/packages/nice-grpc-client-middleware-devtools/package.json +++ b/packages/nice-grpc-client-middleware-devtools/package.json @@ -26,13 +26,13 @@ "devDependencies": { "@tsconfig/recommended": "^1.0.1", "@types/google-protobuf": "^3.7.4", - "abort-controller-x": "^0.4.0", "google-protobuf": "^3.14.0", "grpc-tools": "^1.10.0", "grpc_tools_node_protoc_ts": "^5.0.1", "nice-grpc": "^2.1.5" }, "dependencies": { - "nice-grpc-common": "^2.0.2" + "nice-grpc-common": "^2.0.2", + "abort-controller-x": "^0.4.0" } } From 86f0caae389c3264b81dee4ea699b4c56853f883 Mon Sep 17 00:00:00 2001 From: Sebastian Halder Date: Fri, 13 Oct 2023 05:29:19 +0000 Subject: [PATCH 09/10] Support less prominent streaming methods: client-streaming and bidirectional streaming --- .../src/index.ts | 135 ++++++++++++------ 1 file changed, 89 insertions(+), 46 deletions(-) diff --git a/packages/nice-grpc-client-middleware-devtools/src/index.ts b/packages/nice-grpc-client-middleware-devtools/src/index.ts index 1814a8af..82f9bbff 100644 --- a/packages/nice-grpc-client-middleware-devtools/src/index.ts +++ b/packages/nice-grpc-client-middleware-devtools/src/index.ts @@ -1,4 +1,4 @@ -import { isAbortError } from 'abort-controller-x'; +import {isAbortError} from 'abort-controller-x'; import { CallOptions, ClientError, @@ -10,7 +10,7 @@ import { export type DevtoolsLoggingOptions = { /** * Skip logging abort errors. - * + * * By default, abort errors are logged. */ skipAbortErrorLogging?: boolean; @@ -19,7 +19,7 @@ export type DevtoolsLoggingOptions = { export const devtoolsUnaryLoggingMiddleware: ClientMiddleware = async function* devtoolsLoggingMiddleware( call: ClientMiddlewareCall, - options: CallOptions & Partial + options: CallOptions & Partial, ): AsyncGenerator { // skip streaming calls if (call.requestStream || call.responseStream) { @@ -27,7 +27,7 @@ export const devtoolsUnaryLoggingMiddleware: ClientMiddleware = async function* devtoolsLoggingMiddleware( call: ClientMiddlewareCall, - options: CallOptions & Partial + options: CallOptions & Partial, ): AsyncGenerator { // skip unary calls if (!call.responseStream && !call.requestStream) { @@ -112,38 +112,34 @@ export const devtoolsStreamLoggingMiddleware: ClientMiddleware = composeClientMiddleware( - devtoolsUnaryLoggingMiddleware, - devtoolsStreamLoggingMiddleware -); +export const devtoolsLoggingMiddleware: ClientMiddleware = + composeClientMiddleware( + devtoolsUnaryLoggingMiddleware, + devtoolsStreamLoggingMiddleware, + ); // check whether the given object has toObject() method and return the object // otherwise return the object itself @@ -213,4 +210,50 @@ function getAsObject(obj: any) { } // ts-proto return obj; -} \ No newline at end of file +} + +async function* emitRequestMessages( + iterable: AsyncIterable, + path: string, +): AsyncIterable { + for await (const request of iterable) { + logStreamingRequestMessage(request, path); + yield request; + } +} + +async function* emitResponseMessages( + iterable: AsyncIterable, + path: string, +): AsyncIterable { + for await (const reponse of iterable) { + logStreamingResponseMessage(reponse, path); + yield reponse; + } +} + +function logStreamingResponseMessage(response: T, path: string) { + const resObj = getAsObject(response); + window.postMessage( + { + method: path, + methodType: 'server_streaming', + response: resObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*', + ); +} + +function logStreamingRequestMessage(request: T, path: string) { + const reqObj = getAsObject(request); + window.postMessage( + { + method: path, + methodType: 'server_streaming', + request: reqObj, + type: '__GRPCWEB_DEVTOOLS__', + }, + '*', + ); +} From f99294108c398dedfa1ba1ca172826aaecaebd3c Mon Sep 17 00:00:00 2001 From: Sebastian Halder Date: Fri, 13 Oct 2023 05:39:33 +0000 Subject: [PATCH 10/10] Update main README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 39483fa7..2d683751 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ A gRPC library that is nice to you. — client middleware that adds support for setting call deadline. - [nice-grpc-client-middleware-retry](/packages/nice-grpc-client-middleware-retry) — client middleware that adds automatic retries to unary calls. +- [nice-grpc-client-middleware-devtools](/packages/nice-grpc-client-middleware-devtools) + — client middleware to log calls with + [grpc-web-tools](https://github.com/SafetyCulture/grpc-web-devtools) in the + browser. - [nice-grpc-server-middleware-terminator](/packages/nice-grpc-server-middleware-terminator) — server middleware that makes it possible to prevent long-running calls from blocking server graceful shutdown.