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 +27 -16
- package/README.md +203 -157
- package/commitlint.config.js +1 -0
- package/package.json +25 -24
- package/src/client.js +15 -29
- package/src/server.js +19 -5
- package/src/utils/index.js +4 -2
- package/src/utils/testHelpers/server.js +16 -4
- package/src/utils/testHelpers/serverRx.js +16 -4
- package/vitest.config.js +26 -0
package/.eslintrc.js
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
env: {
|
|
3
3
|
node: true,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
es2022: true
|
|
5
|
+
},
|
|
6
|
+
extends: ['standard'],
|
|
7
|
+
parserOptions: {
|
|
8
|
+
ecmaVersion: 'latest'
|
|
6
9
|
},
|
|
7
|
-
extends: 'standard',
|
|
8
10
|
rules: {
|
|
9
|
-
|
|
10
|
-
'space-before-function-paren':
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
[](https://github.com/brickhouse-tech/rxjs-grpc-minimal/actions/workflows/tests.yml)
|
|
2
2
|
[](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).
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
32
|
+
### Loading Proto Files
|
|
33
|
+
|
|
34
|
+
Use `@grpc/proto-loader` with `@grpc/grpc-js` (the modern approach):
|
|
20
35
|
|
|
21
36
|
```js
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
writer.next({ name:
|
|
132
|
-
writer.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
Wraps RxJS server handlers to work with gRPC.
|
|
170
208
|
|
|
171
|
-
-
|
|
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
|
-
|
|
213
|
+
## Development
|
|
174
214
|
|
|
175
|
-
|
|
176
|
-
|
|
215
|
+
```bash
|
|
216
|
+
# Install dependencies
|
|
217
|
+
npm install
|
|
177
218
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
greeter.sayMultiHello // node stream function, and other functions could be callback, callback and steams
|
|
219
|
+
# Run tests
|
|
220
|
+
npm test
|
|
181
221
|
|
|
182
|
-
|
|
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
|
-
|
|
225
|
+
# Run tests with coverage
|
|
226
|
+
npm run test:coverage
|
|
189
227
|
|
|
190
|
-
|
|
228
|
+
# Lint code
|
|
229
|
+
npm run lint
|
|
191
230
|
|
|
192
|
-
|
|
231
|
+
# Fix lint issues
|
|
232
|
+
npm run lint:fix
|
|
233
|
+
```
|
|
193
234
|
|
|
194
|
-
|
|
235
|
+
### Running Examples
|
|
195
236
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
245
|
+
## License
|
|
200
246
|
|
|
201
|
-
|
|
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.
|
|
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/
|
|
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
|
|
25
|
-
"
|
|
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": "^
|
|
32
|
-
"rxjs": "^
|
|
33
|
-
"through2": "^
|
|
34
|
+
"debug-fabulous": "^2.0.1",
|
|
35
|
+
"rxjs": "^7.8.1",
|
|
36
|
+
"through2": "^4.0.2"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"eslint
|
|
43
|
-
"eslint-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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": ">=
|
|
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
|
-
|
|
96
|
+
observer.unsubscribe = function (...args) {
|
|
97
|
+
// Remove observable errror listener
|
|
108
98
|
call.removeListener('error', onError);
|
|
109
|
-
|
|
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(
|
|
151
|
+
function grpcCancel() {
|
|
152
152
|
dbg(() => 'canceled');
|
|
153
153
|
cancelCache.delete(grpcCancel);
|
|
154
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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.
|
package/src/utils/index.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/vitest.config.js
ADDED
|
@@ -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
|
+
});
|