rxjs-grpc-minimal 0.2.5 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js CHANGED
@@ -1,24 +1,35 @@
1
1
  module.exports = {
2
2
  env: {
3
3
  node: true,
4
- commonjs: true,
5
- es6: true
4
+ es2022: true
5
+ },
6
+ extends: ['standard'],
7
+ parserOptions: {
8
+ ecmaVersion: 'latest'
6
9
  },
7
- extends: 'standard',
8
10
  rules: {
9
- 'no-var': 'error',
10
- 'space-before-function-paren': 0,
11
- 'prefer-const': 'error',
12
- semi: ['error', 'always']
11
+ semi: ['error', 'always'],
12
+ 'space-before-function-paren': ['error', {
13
+ anonymous: 'always',
14
+ named: 'never',
15
+ asyncArrow: 'always'
16
+ }],
17
+ 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
13
18
  },
14
- overrides: {
15
- files: ['tests/**/*spec.js'],
16
- env: {
17
- mocha: true,
18
- },
19
- rules: {
20
- 'handle-callback-err': 0,
21
- 'no-unused-expressions': 0
19
+ overrides: [
20
+ {
21
+ files: ['tests/**/*.js'],
22
+ globals: {
23
+ describe: 'readonly',
24
+ it: 'readonly',
25
+ expect: 'readonly',
26
+ beforeEach: 'readonly',
27
+ afterEach: 'readonly',
28
+ beforeAll: 'readonly',
29
+ afterAll: 'readonly',
30
+ vi: 'readonly'
31
+ }
22
32
  }
23
- }
33
+ ],
34
+ ignorePatterns: ['node_modules/', 'coverage/']
24
35
  };
package/README.md CHANGED
@@ -1,201 +1,247 @@
1
- [![Build Status](https://travis-ci.org/nmccready/rxjs-grpc-minimal.svg?branch=master)](https://travis-ci.org/nmccready/rxjs-grpc-minimal)
1
+ [![tests](https://github.com/brickhouse-tech/rxjs-grpc-minimal/actions/workflows/tests.yml/badge.svg)](https://github.com/brickhouse-tech/rxjs-grpc-minimal/actions/workflows/tests.yml)
2
2
  [![npm version](https://badge.fury.io/js/rxjs-grpc-minimal.svg)](https://badge.fury.io/js/rxjs-grpc-minimal)
3
3
 
4
4
  # rxjs-grpc-minimal
5
5
 
6
- Based off the great work of [rxjs-grpc](https://github.com/kondi/rxjs-grpc). However, this library intends to
7
- very little except to offer you to wrap your server or client GRPC implementation the way you want it.
6
+ Based off the great work of [rxjs-grpc](https://github.com/kondi/rxjs-grpc). This library wraps gRPC server and client implementations with RxJS Observables, giving you a reactive interface without imposing opinions on your setup.
8
7
 
9
- There is no cli as this library is trying to stay out of the way and allow grpc, or protobufjs do the amazing things they already do.
8
+ There is no CLI—this library stays out of the way and lets `@grpc/grpc-js` and `@grpc/proto-loader` do what they do best.
9
+
10
+ ## Requirements
11
+
12
+ - Node.js >= 18
13
+ - RxJS 7.x
14
+ - @grpc/grpc-js (replaces deprecated `grpc` package)
10
15
 
11
16
  ## Install
12
17
 
13
18
  ```bash
14
- > yarn add rxjs-grpc-minimal
19
+ npm install rxjs-grpc-minimal
20
+ # or
21
+ yarn add rxjs-grpc-minimal
22
+ ```
23
+
24
+ You'll also need gRPC dependencies:
25
+
26
+ ```bash
27
+ npm install @grpc/grpc-js @grpc/proto-loader
15
28
  ```
16
29
 
17
30
  ## Usage
18
31
 
19
- ### Client
32
+ ### Loading Proto Files
33
+
34
+ Use `@grpc/proto-loader` with `@grpc/grpc-js` (the modern approach):
20
35
 
21
36
  ```js
22
- const path = require('path');
23
- const { loadSync } = require('protobufjs');
24
- const { loadObject: toGrpc, credentials } = require('grpc');
25
- const { Subject, ReplaySubject } = require('rxjs');
26
-
27
- const {
28
- toRxClient, // used most often
29
- toRxServer, // used most often
30
- utils,
31
- errors
32
- } = require('rxjs-grpc-minimal');
33
-
34
- const pbAPI = loadSync(
35
- path.join(__dirname,'../examples/helloworld/helloworld.proto'))
36
- .lookup('helloworld');
37
-
38
- /*
39
- Wraps all service.prototype methods with RXJS implementations.
40
- Each method is appended to the prototype as `method${RX}` by default.
41
- Thus allowing you access to both RX and nonRx grpc implementations.
42
- */
43
- const grpcAPI = toRxClient(toGrpc(pbAPI));
44
- /*
45
- Wraps all service.prototype methods with RXJS implementations. However,
46
- this overrides / overwrites all original prototype methods with the RX impl.
47
- */
48
- const grpcApiOverride = toRxClient(toGrpc(pbAPI), '');
49
-
50
- const greeter = new grpcAPI.Greeter('localhost:56001', credentials.createInsecure());
51
-
52
- // non stream
53
- conn.sayHelloRx({ name: 'Bob' });
54
- .forEach(resp => {
55
- console.log(grpcAPI.cancelCache.size) // 0
56
- console.log(resp); // { message: 'Hello Bob' } // depends on server
57
- })
58
-
59
- let calls = 0;
60
-
61
- // STREAMING REPLY FROM SERVER
62
- conn.sayMultiHelloRx({
63
- name: 'Brody',
64
- numGreetings: 2,
65
- doComplete: true
66
- })
67
- .forEach(resp => {
68
- calls++;
69
- console.log({ size: grpcAPI.cancelCache.size})
70
- console.log(resp)
71
- })
72
- .then(() => {
73
- console.log({ size: grpcAPI.cancelCache.size})
74
- console.log({ calls })
37
+ import grpc from '@grpc/grpc-js';
38
+ import protoLoader from '@grpc/proto-loader';
39
+ import { toRxClient } from 'rxjs-grpc-minimal';
40
+
41
+ // Load proto file
42
+ const packageDefinition = protoLoader.loadSync('./helloworld.proto', {
43
+ keepCase: false,
44
+ longs: String,
45
+ enums: String,
46
+ defaults: true,
47
+ oneofs: true
75
48
  });
76
49
 
77
- calls = 0;
78
-
79
- /* console out
80
-
81
- { size: 1 }
82
- { message: 'Hello Brody' }
83
- { size: 1 }
84
- { message: 'Hello Brody' }
85
- { size: 0 }
86
- { calls: 2 }
87
- */
88
-
89
- // streaming reply from server
90
- const multiHelloStream = conn.sayMultiHelloRx({
91
- name: 'Brody',
92
- numGreetings: 2,
93
- doComplete: true
94
- })
95
- .forEach(resp => {
96
- calls++;
97
- console.log({ size: grpcAPI.cancelCache.size})
98
- console.log(resp)
99
- // imagine you need to cancel this stream in between and abort early
100
- multiHelloStream.grpcCancel();
101
- })
102
- .then(() => {
103
- console.log({ size: grpcAPI.cancelCache.size})
104
- console.log({ calls })
50
+ const grpcAPI = grpc.loadPackageDefinition(packageDefinition);
51
+ const helloworldAPI = toRxClient(grpcAPI.helloworld);
52
+ ```
53
+
54
+ ### Client
55
+
56
+ ```js
57
+ import grpc from '@grpc/grpc-js';
58
+ import protoLoader from '@grpc/proto-loader';
59
+ import { toRxClient } from 'rxjs-grpc-minimal';
60
+
61
+ // Load and wrap the API
62
+ const packageDefinition = protoLoader.loadSync('./helloworld.proto', {
63
+ keepCase: false,
64
+ longs: String,
65
+ enums: String,
66
+ defaults: true,
67
+ oneofs: true
68
+ });
69
+ const grpcAPI = toRxClient(
70
+ grpc.loadPackageDefinition(packageDefinition).helloworld
71
+ );
72
+
73
+ // Create client connection
74
+ const greeter = new grpcAPI.Greeter(
75
+ 'localhost:50051',
76
+ grpc.credentials.createInsecure()
77
+ );
78
+
79
+ // Unary call - returns Observable
80
+ await greeter.sayHelloRx({ name: 'Bob' }).forEach(resp => {
81
+ console.log(resp); // { message: 'Hello Bob!' }
105
82
  });
106
- calls = 0;
107
- /* console out
108
83
 
109
- { size: 1 }
110
- { message: 'Hello Brody' }
111
- { size: 0 }
112
- { calls: 1 }
113
- */
84
+ // Server streaming - Observable emits each response
85
+ await greeter
86
+ .sayMultiHelloRx({ name: 'World', numGreetings: 3 })
87
+ .forEach(resp => {
88
+ console.log(resp.message);
89
+ });
90
+
91
+ // Client streaming - pass a Subject/Observable as the request
92
+ import { Subject } from 'rxjs';
114
93
 
115
- // STREAMING REQUEST | client streaming to server
116
94
  const writer = new Subject();
117
- const observable = conn.streamSayHelloRx(writer);
118
-
119
- observable
120
- .forEach(resp => {
121
- calls++;
122
- console.log(resp);
123
- console.log({ calls });
124
- })
125
- .then(() => {
126
- writer.unsubscribe();
95
+ const response$ = greeter.streamSayHelloRx(writer);
96
+
97
+ response$.forEach(resp => {
98
+ console.log(resp.message);
127
99
  });
128
100
 
129
- console(grpcAPI.cancelCache.size) // 1
130
- // ok we're now subscribed
131
- writer.next({ name: 'Al' });
132
- writer.next({ name: 'Bundy' });
133
- writer.complete();
134
-
135
- console.log(grpcAPI.cancelCache.size) // 0
136
-
137
- /* console out
138
-
139
- 1
140
- { message: 'Hello Al' }
141
- { calls: 1 }
142
- { message: 'Hello Bundy' }
143
- { calls: 2 }
144
- 0
145
- */
146
-
147
- // CONNECTION CLEANUP
148
- /*
149
- Imagine we abort in between or crash but catch the problem.
150
- prior to conn.close we could clean up all.
151
-
152
- This guarantees that the observer on the sever side is cleaned up and released.
153
- This also allows you to truly close your connection without dangling a channel/subchannel.
154
- */
155
- grpcAPI.cancelCache.forEach((cancel) => cancel());
156
- conn.close();
101
+ // Send messages
102
+ writer.next({ name: 'Alice' });
103
+ writer.next({ name: 'Bob' });
104
+ writer.complete(); // Signal end of stream
105
+ ```
106
+
107
+ ### Cancellation
108
+
109
+ RxJS methods return Observables with a `grpcCancel()` function for early termination:
110
+
111
+ ```js
112
+ const stream$ = greeter.sayMultiHelloRx({ name: 'World', numGreetings: 100 });
113
+
114
+ stream$.forEach(resp => {
115
+ console.log(resp.message);
116
+ if (someCondition) {
117
+ stream$.grpcCancel(); // Cancel the underlying gRPC call
118
+ }
119
+ });
120
+
121
+ // Clean up all pending calls before closing connection
122
+ grpcAPI.cancelCache.forEach(cancel => cancel());
123
+ greeter.close();
157
124
  ```
158
125
 
159
126
  ### Server
160
127
 
161
- See [serverRx.js](./examples/helloworld/impls/serverRx.js)
128
+ ```js
129
+ import { of, Observable } from 'rxjs';
130
+ import grpc from '@grpc/grpc-js';
131
+ import protoLoader from '@grpc/proto-loader';
132
+ import { toRxServer } from 'rxjs-grpc-minimal';
133
+
134
+ // Load proto
135
+ const packageDefinition = protoLoader.loadSync('./helloworld.proto', {
136
+ keepCase: false,
137
+ longs: String,
138
+ enums: String,
139
+ defaults: true,
140
+ oneofs: true
141
+ });
142
+ const proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
143
+
144
+ // Define RxJS implementation
145
+ const rxImpl = {
146
+ // Unary: return an Observable
147
+ sayHello({ value: { name } }) {
148
+ return of({ message: `Hello ${name}!` });
149
+ },
150
+
151
+ // Server streaming: return Observable that emits multiple values
152
+ sayMultiHello({ value: { name, numGreetings } }) {
153
+ return new Observable(observer => {
154
+ for (let i = 0; i < numGreetings; i++) {
155
+ observer.next({ message: `Hello ${name}!` });
156
+ }
157
+ observer.complete();
158
+ });
159
+ },
160
+
161
+ // Client streaming: receive Observable, return Observable
162
+ streamSayHello(requestStream$) {
163
+ return new Observable(observer => {
164
+ requestStream$.forEach(val => {
165
+ observer.next({ message: `Hello ${val.name}!` });
166
+ }).then(
167
+ () => observer.complete(),
168
+ err => observer.error(err)
169
+ );
170
+ });
171
+ }
172
+ };
173
+
174
+ // Create and start server
175
+ const server = new grpc.Server();
176
+ server.addService(proto.Greeter.service, toRxServer(proto.Greeter, rxImpl, 'Greeter'));
177
+ server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
178
+ console.log('Server running on port 50051');
179
+ });
180
+ ```
181
+
182
+ See [examples/helloworld/impls/serverRx.js](./examples/helloworld/impls/serverRx.js) for a complete example.
162
183
 
163
184
  ## API
164
185
 
165
- ### toRxClient(grpcObject, methodExt)
186
+ ### `toRxClient(grpcObject, methodExt = 'Rx')`
187
+
188
+ Wraps all service prototype methods with RxJS implementations.
189
+
190
+ - **grpcObject** - Object created by `grpc.loadPackageDefinition()`
191
+ - **methodExt** - String appended to method names (default: `'Rx'`)
166
192
 
167
- - #### grpcObject
193
+ ```js
194
+ const api = toRxClient(grpcAPI);
195
+ greeter.sayHelloRx(); // RxJS Observable
196
+ greeter.sayHello(); // Original callback-based method
197
+
198
+ // Override original methods instead of extending:
199
+ const api = toRxClient(grpcAPI, '');
200
+ greeter.sayHello(); // Now returns Observable
201
+ ```
202
+
203
+ Returns the modified `grpcObject` with a `cancelCache` Set for tracking active calls.
204
+
205
+ ### `toRxServer(service, rxImpl, serviceName?)`
168
206
 
169
- Type: `Object` - initially created by `grpc.loadObject` w or w/o protobufjs `load`|`loadSync`
207
+ Wraps RxJS server handlers to work with gRPC.
170
208
 
171
- - #### methodExt
209
+ - **service** - gRPC service definition (e.g., `proto.Greeter`)
210
+ - **rxImpl** - Object with method handlers returning Observables
211
+ - **serviceName** - Optional string for debug logging
172
212
 
173
- Type: `String` - defaults to `'Rx'`
213
+ ## Development
174
214
 
175
- This is the method naming extension where the original method name is appended
176
- with `something{RX}`.
215
+ ```bash
216
+ # Install dependencies
217
+ npm install
177
218
 
178
- ```js
179
- greeter.sayMultiHelloRx // RX function
180
- greeter.sayMultiHello // node stream function, and other functions could be callback, callback and steams
219
+ # Run tests
220
+ npm test
181
221
 
182
- toRxClient(grpcAPI, '');
183
- // ...
184
- greeter.sayMultiHello // RX function all node stream, callback etc hidden / wrapped
185
- // NOTE: all RX functions will always return observables (consistent!) .
186
- ```
222
+ # Run tests in watch mode
223
+ npm run test:watch
187
224
 
188
- ### toRxServer(service, rxImpl, serviceName)
225
+ # Run tests with coverage
226
+ npm run test:coverage
189
227
 
190
- - #### service
228
+ # Lint code
229
+ npm run lint
191
230
 
192
- Type: `Object` - GrpcService definition `grpcAPI[serviceName]`
231
+ # Fix lint issues
232
+ npm run lint:fix
233
+ ```
193
234
 
194
- - #### rxImpl
235
+ ### Running Examples
195
236
 
196
- Type: `Object` - Your RxJS server implementation which matches the service method handles
197
- to be implemented.
237
+ ```bash
238
+ # Terminal 1: Start server
239
+ npm run server
240
+
241
+ # Terminal 2: Run client
242
+ npm run client
243
+ ```
198
244
 
199
- - #### serviceName (optional)
245
+ ## License
200
246
 
201
- Type: `String` - aids in [debug](./debug.js) via [debug-fabulous](https://github.com/nmccready/debug-fabulous) logging.
247
+ MIT
@@ -0,0 +1 @@
1
+ module.exports = { extends: ['@commitlint/config-conventional'] };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "main": "src/index",
3
2
  "name": "rxjs-grpc-minimal",
4
- "version": "0.2.5",
3
+ "version": "0.2.8",
5
4
  "description": "grpc node callback and streams wrapped in observables",
5
+ "main": "src/index.js",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "rxjs",
@@ -14,42 +14,43 @@
14
14
  ],
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://github.com/nmccready/rxjs-grpc-minimal.git"
17
+ "url": "https://github.com/brickhouse-tech/rxjs-grpc-minimal.git"
18
18
  },
19
19
  "files": [
20
20
  "src",
21
21
  "*.js"
22
22
  ],
23
23
  "scripts": {
24
- "lint": "eslint **/*.js",
25
- "test": "yarn lint && mocha --exclude node_modules ./**/*.js",
24
+ "lint": "eslint .",
25
+ "lint:fix": "eslint . --fix",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:coverage": "vitest run --coverage",
26
29
  "server": "node ./examples/helloworld/server.js",
27
30
  "client": "node ./examples/helloworld/client.js",
28
31
  "client2": "node ./examples/helloworld/client2.js"
29
32
  },
30
33
  "dependencies": {
31
- "debug-fabulous": "^1.1.0",
32
- "rxjs": "^5.2.0",
33
- "through2": "^2.0.3"
34
+ "debug-fabulous": "^2.0.1",
35
+ "rxjs": "^7.8.1",
36
+ "through2": "^4.0.2"
34
37
  },
35
38
  "devDependencies": {
36
- "JSONStream": "^1.3.2",
37
- "chai": "^4.1.2",
38
- "eslint": "^4",
39
- "eslint-config-standard": "^11.0.0",
40
- "eslint-plugin-import": "^2.11.0",
41
- "eslint-plugin-node": "^6.0.1",
42
- "eslint-plugin-promise": "^3.7.0",
43
- "eslint-plugin-standard": "^3.0.1",
44
- "grpc": "^1.13",
45
- "lodash": "^4.17.13",
46
- "mocha": "^5.1.1",
47
- "prettier": "^1",
48
- "prettier-eslint": "^8",
49
- "protobufjs": "^6",
50
- "sinon": "^7.2.0"
39
+ "@commitlint/cli": "^20.4.1",
40
+ "@commitlint/config-conventional": "^20.4.1",
41
+ "@grpc/grpc-js": "^1.12.5",
42
+ "@grpc/proto-loader": "^0.7.13",
43
+ "@vitest/coverage-v8": "^2.1.8",
44
+ "commit-and-tag-version": "^12.6.1",
45
+ "eslint": "^8.57.1",
46
+ "eslint-config-standard": "^17.1.0",
47
+ "eslint-plugin-import": "^2.31.0",
48
+ "eslint-plugin-n": "^16.6.2",
49
+ "eslint-plugin-promise": "^6.6.0",
50
+ "lodash": "^4.17.21",
51
+ "vitest": "^2.1.8"
51
52
  },
52
53
  "engines": {
53
- "node": ">=8"
54
+ "node": ">=18"
54
55
  }
55
56
  }
package/src/client.js CHANGED
@@ -1,12 +1,9 @@
1
1
  const { Observable } = require('rxjs');
2
2
  const though2 = require('through2');
3
- const EventEmitter = require('events');
4
- const { noop } = require('lodash');
5
3
 
6
4
  const { getServiceNames } = require('../src/utils');
7
5
 
8
6
  const debug = require('../debug').spawn('client');
9
-
10
7
  /**
11
8
  * @param {Object} grpcApi - pre-loaded grpcApi
12
9
  * @param {String} methExt - your choice to extend or override the methodNames
@@ -78,9 +75,6 @@ function createMethod(clientMethod, dbg, cancelCache) {
78
75
  }
79
76
 
80
77
  if (clientMethod.responseStream) {
81
- const domain = createDomain();
82
- call.domain = domain;
83
-
84
78
  const d = dbg.spawn('responseStream');
85
79
  const onData = (data, _, cb) => {
86
80
  d(() => ({ data }));
@@ -98,17 +92,23 @@ function createMethod(clientMethod, dbg, cancelCache) {
98
92
  call.removeListener('error', onError);
99
93
  };
100
94
 
101
- call.pipe(though2.obj(onData, onEnd));
102
- call.on('error', onError);
103
- domain.once('error', onError);
104
-
105
95
  const originalUnsub = observer.unsubscribe;
106
- observer.unsubscribe = function(...args) {
107
- grpcCancel();
96
+ observer.unsubscribe = function (...args) {
97
+ // Remove observable errror listener
108
98
  call.removeListener('error', onError);
109
- originalUnsub.apply(observer, args);
99
+ // Add silent error handler. Avoids errors if stream responses have already come in
100
+ // and we are delaying observable response, like when using delay()
101
+ call.on('error', () => {});
102
+ // Like end, but does not throw error
103
+ // @see https://nodejs.org/api/stream.html#stream_readable_streams
104
+ // note GRPC stream extends readable_stream
110
105
  call.destroy();
106
+ originalUnsub.call(observer, ...args);
107
+ grpcCancel(false);
111
108
  };
109
+
110
+ call.pipe(though2.obj(onData, onEnd));
111
+ call.on('error', onError);
112
112
  }
113
113
 
114
114
  let requestStreamCompleted = false;
@@ -148,10 +148,10 @@ function createMethod(clientMethod, dbg, cancelCache) {
148
148
  cb ? cb() : observer.complete();
149
149
  }
150
150
 
151
- function grpcCancel(doCancel = true) {
151
+ function grpcCancel() {
152
152
  dbg(() => 'canceled');
153
153
  cancelCache.delete(grpcCancel);
154
- if (doCancel) call.cancel();
154
+ call.cancel();
155
155
  }
156
156
  if (
157
157
  call.cancel &&
@@ -169,20 +169,6 @@ function createMethod(clientMethod, dbg, cancelCache) {
169
169
  return rxWrapper;
170
170
  }
171
171
 
172
- /*
173
- node domain require('domain') is deprecated
174
- mock one
175
-
176
- Domain is to trap unhandled connection resets etc.. and pass them on
177
- to some observer / observable.
178
- */
179
- const createDomain = () => {
180
- const domain = new EventEmitter();
181
- domain.enter = noop;
182
- domain.exit = noop;
183
- return domain;
184
- };
185
-
186
172
  module.exports = {
187
173
  create,
188
174
  createMethod
package/src/server.js CHANGED
@@ -1,4 +1,4 @@
1
- const { Observable } = require('rxjs');
1
+ const { of, Observable } = require('rxjs');
2
2
  const through2 = require('through2');
3
3
 
4
4
  const debug = require('../debug').spawn('server');
@@ -23,10 +23,12 @@ function create(Service, rxImpl, serviceName) {
23
23
 
24
24
  function createMethod(rxImpl, name, methods, dbg) {
25
25
  const serviceMethod = methods[name];
26
- return async function(call, callback) {
26
+ return async function (call, callback) {
27
27
  dbg(() => 'called');
28
28
  // SET SYNC REQUEST OBSERVER
29
- let observable = Observable.of(call.request);
29
+ // Create an observable that also has .value for backward compat
30
+ let observable = of(call.request);
31
+ observable.value = call.request;
30
32
 
31
33
  if (serviceMethod.requestStream) {
32
34
  observable = createRequestStream({ call, name, dbg });
@@ -59,7 +61,10 @@ function createRequestStream({ call, name, dbg }) {
59
61
  call.on('error', onError);
60
62
 
61
63
  function onError(err) {
62
- observer.error(err);
64
+ // Skip if observer is already closed (e.g., during server shutdown)
65
+ if (!observer.closed) {
66
+ observer.error(err);
67
+ }
63
68
  }
64
69
 
65
70
  function onData(data, _, cb) {
@@ -68,8 +73,17 @@ function createRequestStream({ call, name, dbg }) {
68
73
  }
69
74
 
70
75
  function onEnd(cb) {
76
+ // Capture cancelled state NOW, before setImmediate
77
+ // (forceShutdown() may set call.cancelled between now and the callback)
78
+ const wasCancelled = call.cancelled;
79
+
71
80
  setImmediate(() => {
72
- if (call.cancelled) {
81
+ // Skip if observer is already closed (e.g., during server shutdown)
82
+ // to avoid unhandled rejection errors
83
+ if (observer.closed) {
84
+ return;
85
+ }
86
+ if (wasCancelled) {
73
87
  /*
74
88
  TODO: DEBATING ON WHETHER THIS SHOULD BE AN ERROR OBJECT
75
89
  We're using error event here to signal cancellation.
@@ -2,8 +2,10 @@ function getServiceNames(grpcApi) {
2
2
  const keys = Object.keys(grpcApi);
3
3
  const serviceNames = keys.filter(name => {
4
4
  try {
5
- return grpcApi[name].service;
6
- } catch (e) {}
5
+ return Boolean(grpcApi[name].service);
6
+ } catch (_e) {
7
+ return false;
8
+ }
7
9
  });
8
10
 
9
11
  return serviceNames;
@@ -1,16 +1,28 @@
1
- const { Server, ServerCredentials } = require('grpc');
1
+ const grpc = require('@grpc/grpc-js');
2
+ const { Server, ServerCredentials } = grpc;
2
3
 
3
4
  const initServer = initService => ({ uri, grpcAPI, serviceName }) => {
4
5
  const server = new Server();
5
6
  const GrpcService = grpcAPI[serviceName];
6
7
 
7
- server.bind(uri, ServerCredentials.createInsecure());
8
8
  server.addService(GrpcService.service, initService());
9
- server.start();
9
+
10
+ // Create a promise that resolves when the server is ready
11
+ const ready = new Promise((resolve, reject) => {
12
+ server.bindAsync(uri, ServerCredentials.createInsecure(), (err, _port) => {
13
+ if (err) {
14
+ console.error('Failed to bind server:', err);
15
+ reject(err);
16
+ return;
17
+ }
18
+ resolve();
19
+ });
20
+ });
10
21
 
11
22
  return {
12
23
  server,
13
- GrpcService
24
+ GrpcService,
25
+ ready
14
26
  };
15
27
  };
16
28
 
@@ -1,4 +1,5 @@
1
- const { Server, ServerCredentials } = require('grpc');
1
+ const grpc = require('@grpc/grpc-js');
2
+ const { Server, ServerCredentials } = grpc;
2
3
  const { toRxServer } = require('../../..');
3
4
 
4
5
  const initServer = initService => ({ uri, grpcAPI, serviceName }) => {
@@ -6,17 +7,28 @@ const initServer = initService => ({ uri, grpcAPI, serviceName }) => {
6
7
  const GrpcService = grpcAPI[serviceName];
7
8
  const impl = initService();
8
9
 
9
- server.bind(uri, ServerCredentials.createInsecure());
10
10
  server.addService(
11
11
  GrpcService.service,
12
12
  toRxServer(GrpcService, impl, serviceName)
13
13
  );
14
- server.start();
14
+
15
+ // Create a promise that resolves when the server is ready
16
+ const ready = new Promise((resolve, reject) => {
17
+ server.bindAsync(uri, ServerCredentials.createInsecure(), (err, _port) => {
18
+ if (err) {
19
+ console.error('Failed to bind server:', err);
20
+ reject(err);
21
+ return;
22
+ }
23
+ resolve();
24
+ });
25
+ });
15
26
 
16
27
  return {
17
28
  server,
18
29
  GrpcService,
19
- impl
30
+ impl,
31
+ ready
20
32
  };
21
33
  };
22
34
 
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ testTimeout: 30000,
7
+ hookTimeout: 30000,
8
+ sequence: {
9
+ concurrent: false
10
+ },
11
+ fileParallelism: false,
12
+ include: ['tests/**/*.spec.js'],
13
+ coverage: {
14
+ provider: 'v8',
15
+ reporter: ['text', 'html', 'lcov'],
16
+ include: ['src/**/*.js'],
17
+ exclude: ['src/utils/testHelpers/**'],
18
+ thresholds: {
19
+ statements: 80,
20
+ branches: 70,
21
+ functions: 80,
22
+ lines: 80
23
+ }
24
+ }
25
+ }
26
+ });