@zero-server/sdk 0.9.0 → 0.9.2
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/LICENSE +21 -21
- package/README.md +460 -437
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +460 -460
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +136 -136
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +254 -254
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/grpc/call.js
CHANGED
|
@@ -1,708 +1,708 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module grpc/call
|
|
3
|
-
* @description gRPC call objects for the four RPC patterns.
|
|
4
|
-
* Wraps HTTP/2 streams with protobuf encode/decode, metadata,
|
|
5
|
-
* framing, deadline enforcement, and cancellation support.
|
|
6
|
-
*
|
|
7
|
-
* - `UnaryCall` — single request, single response
|
|
8
|
-
* - `ServerStreamCall` — single request, stream of responses
|
|
9
|
-
* - `ClientStreamCall` — stream of requests, single response
|
|
10
|
-
* - `BidiStreamCall` — bidirectional streaming
|
|
11
|
-
*
|
|
12
|
-
* @example | Unary handler
|
|
13
|
-
* async function GetUser(call) {
|
|
14
|
-
* const user = await db.findById(call.request.id);
|
|
15
|
-
* return user; // auto-encoded and sent
|
|
16
|
-
* }
|
|
17
|
-
*
|
|
18
|
-
* @example | Server streaming handler
|
|
19
|
-
* async function ListUsers(call) {
|
|
20
|
-
* for (const user of users) {
|
|
21
|
-
* call.write(user);
|
|
22
|
-
* }
|
|
23
|
-
* call.end();
|
|
24
|
-
* }
|
|
25
|
-
*
|
|
26
|
-
* @example | Bidirectional streaming handler
|
|
27
|
-
* async function Chat(call) {
|
|
28
|
-
* for await (const msg of call) {
|
|
29
|
-
* call.write({ echo: msg.text, ts: Date.now() });
|
|
30
|
-
* }
|
|
31
|
-
* }
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
const { EventEmitter } = require('events');
|
|
35
|
-
const log = require('../debug')('zero:grpc');
|
|
36
|
-
const { GrpcStatus, statusName } = require('./status');
|
|
37
|
-
const { Metadata } = require('./metadata');
|
|
38
|
-
const { frameEncode, FrameParser } = require('./frame');
|
|
39
|
-
const { encode, decode } = require('./codec');
|
|
40
|
-
|
|
41
|
-
// -- Base Call ---------------------------------------------
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Base class for all gRPC call types. Manages the HTTP/2 stream,
|
|
45
|
-
* metadata, deadlines, cancellation, and common lifecycle.
|
|
46
|
-
*
|
|
47
|
-
* @class
|
|
48
|
-
* @private
|
|
49
|
-
*/
|
|
50
|
-
class BaseCall extends EventEmitter
|
|
51
|
-
{
|
|
52
|
-
/**
|
|
53
|
-
* @constructor
|
|
54
|
-
* @param {import('http2').Http2Stream} stream - The HTTP/2 stream for this call.
|
|
55
|
-
* @param {object} methodDef - Method descriptor from the proto schema.
|
|
56
|
-
* @param {object} messageTypes - Map of all message descriptors.
|
|
57
|
-
* @param {Metadata} metadata - Initial metadata from the client.
|
|
58
|
-
* @param {object} [opts] - Call options.
|
|
59
|
-
* @param {number} [opts.maxMessageSize] - Max message size in bytes.
|
|
60
|
-
* @param {boolean} [opts.compress=false] - Whether to compress outgoing messages.
|
|
61
|
-
*/
|
|
62
|
-
constructor(stream, methodDef, messageTypes, metadata, opts = {})
|
|
63
|
-
{
|
|
64
|
-
super();
|
|
65
|
-
|
|
66
|
-
/** The underlying HTTP/2 stream. */
|
|
67
|
-
this.stream = stream;
|
|
68
|
-
|
|
69
|
-
/** Method descriptor from the parsed proto. */
|
|
70
|
-
this.method = methodDef;
|
|
71
|
-
|
|
72
|
-
/** Initial metadata from the client. */
|
|
73
|
-
this.metadata = metadata;
|
|
74
|
-
|
|
75
|
-
/** Response (trailing) metadata to be sent with the status. */
|
|
76
|
-
this.trailingMetadata = new Metadata();
|
|
77
|
-
|
|
78
|
-
/** @private */
|
|
79
|
-
this._messageTypes = messageTypes;
|
|
80
|
-
/** @private */
|
|
81
|
-
this._inputDesc = messageTypes[methodDef.inputType];
|
|
82
|
-
/** @private */
|
|
83
|
-
this._outputDesc = messageTypes[methodDef.outputType];
|
|
84
|
-
/** @private */
|
|
85
|
-
this._compress = !!opts.compress;
|
|
86
|
-
/** @private */
|
|
87
|
-
this._cancelled = false;
|
|
88
|
-
/** @private */
|
|
89
|
-
this._ended = false;
|
|
90
|
-
/** @private */
|
|
91
|
-
this._headersSent = false;
|
|
92
|
-
/** @private */
|
|
93
|
-
this._deadlineTimer = null;
|
|
94
|
-
|
|
95
|
-
if (!this._inputDesc)
|
|
96
|
-
throw new Error(`Unknown input message type: ${methodDef.inputType}`);
|
|
97
|
-
if (!this._outputDesc)
|
|
98
|
-
throw new Error(`Unknown output message type: ${methodDef.outputType}`);
|
|
99
|
-
|
|
100
|
-
/** @private */
|
|
101
|
-
this._parser = new FrameParser({ maxMessageSize: opts.maxMessageSize });
|
|
102
|
-
|
|
103
|
-
// Parse the deadline from grpc-timeout header
|
|
104
|
-
this._deadline = null;
|
|
105
|
-
const timeoutHeader = stream.sentHeaders && stream.sentHeaders['grpc-timeout'];
|
|
106
|
-
if (timeoutHeader) this._setupDeadline(timeoutHeader);
|
|
107
|
-
|
|
108
|
-
// Cancellation via stream reset
|
|
109
|
-
stream.on('close', () =>
|
|
110
|
-
{
|
|
111
|
-
if (!this._ended)
|
|
112
|
-
{
|
|
113
|
-
this._cancelled = true;
|
|
114
|
-
this.emit('cancelled');
|
|
115
|
-
}
|
|
116
|
-
this._cleanup();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
stream.on('error', (err) =>
|
|
120
|
-
{
|
|
121
|
-
log.error('stream error on %s: %s', methodDef.name, err.message);
|
|
122
|
-
this.emit('error', err);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
/** Peer IP address (from stream session). */
|
|
126
|
-
this.peer = stream.session && stream.session.socket
|
|
127
|
-
? stream.session.socket.remoteAddress || 'unknown'
|
|
128
|
-
: 'unknown';
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// -- Metadata Sending ----------------------------------
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Send initial response metadata (HTTP/2 headers).
|
|
135
|
-
* Must be called before writing any messages.
|
|
136
|
-
* If not called explicitly, headers are sent automatically on the first write.
|
|
137
|
-
*
|
|
138
|
-
* @param {Metadata|object} [md] - Additional metadata to merge into the response headers.
|
|
139
|
-
*/
|
|
140
|
-
sendMetadata(md)
|
|
141
|
-
{
|
|
142
|
-
if (this._headersSent || this._ended) return;
|
|
143
|
-
this._headersSent = true;
|
|
144
|
-
|
|
145
|
-
const headers = {
|
|
146
|
-
':status': 200,
|
|
147
|
-
'content-type': 'application/grpc+proto',
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (this._compress) headers['grpc-encoding'] = 'gzip';
|
|
151
|
-
|
|
152
|
-
if (md)
|
|
153
|
-
{
|
|
154
|
-
const extra = md instanceof Metadata ? md.toHeaders() : md;
|
|
155
|
-
Object.assign(headers, extra);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
try { this.stream.respond(headers, { waitForTrailers: true }); }
|
|
159
|
-
catch (e) { log.warn('failed to send metadata: %s', e.message); }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// -- Status / End --------------------------------------
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Send a gRPC status and close the call.
|
|
166
|
-
* Trailers carry `grpc-status` and optionally `grpc-message`.
|
|
167
|
-
*
|
|
168
|
-
* @param {number} code - gRPC status code.
|
|
169
|
-
* @param {string} [message] - Human-readable status message.
|
|
170
|
-
*/
|
|
171
|
-
sendStatus(code, message)
|
|
172
|
-
{
|
|
173
|
-
if (this._ended) return;
|
|
174
|
-
this._ended = true;
|
|
175
|
-
|
|
176
|
-
const trailHeaders = {
|
|
177
|
-
'grpc-status': String(code),
|
|
178
|
-
...this.trailingMetadata.toHeaders(),
|
|
179
|
-
};
|
|
180
|
-
if (message) trailHeaders['grpc-message'] = encodeURIComponent(message);
|
|
181
|
-
|
|
182
|
-
if (!this._headersSent)
|
|
183
|
-
{
|
|
184
|
-
// Trailers-Only response — include status in initial HEADERS frame
|
|
185
|
-
this._headersSent = true;
|
|
186
|
-
try
|
|
187
|
-
{
|
|
188
|
-
this.stream.respond({
|
|
189
|
-
':status': 200,
|
|
190
|
-
'content-type': 'application/grpc+proto',
|
|
191
|
-
...trailHeaders,
|
|
192
|
-
}, { endStream: true });
|
|
193
|
-
}
|
|
194
|
-
catch (_)
|
|
195
|
-
{
|
|
196
|
-
try { this.stream.close(); }
|
|
197
|
-
catch (__) { /* nothing to do */ }
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else
|
|
201
|
-
{
|
|
202
|
-
// Headers already sent — send trailing HEADERS after final DATA
|
|
203
|
-
this.stream.on('wantTrailers', () =>
|
|
204
|
-
{
|
|
205
|
-
try { this.stream.sendTrailers(trailHeaders); }
|
|
206
|
-
catch (_) { /* stream may already be closed */ }
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
try { this.stream.end(); }
|
|
210
|
-
catch (_)
|
|
211
|
-
{
|
|
212
|
-
try { this.stream.close(); }
|
|
213
|
-
catch (__) { /* nothing to do */ }
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
this._cleanup();
|
|
218
|
-
|
|
219
|
-
if (code !== GrpcStatus.OK)
|
|
220
|
-
log.warn('call %s ended with status %s: %s', this.method.name, statusName(code), message || '');
|
|
221
|
-
else
|
|
222
|
-
log.debug('call %s completed OK', this.method.name);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Send an error status and close the call.
|
|
227
|
-
* Convenience wrapper around `sendStatus`.
|
|
228
|
-
*
|
|
229
|
-
* @param {number} code - gRPC status code.
|
|
230
|
-
* @param {string} message - Error description.
|
|
231
|
-
*/
|
|
232
|
-
sendError(code, message)
|
|
233
|
-
{
|
|
234
|
-
this.sendStatus(code, message);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// -- Writing -------------------------------------------
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Write a response message. The object is encoded to protobuf, framed,
|
|
241
|
-
* and sent on the HTTP/2 stream.
|
|
242
|
-
*
|
|
243
|
-
* @param {object} message - JavaScript object matching the output message schema.
|
|
244
|
-
* @returns {boolean} `false` if the stream is not writable.
|
|
245
|
-
*/
|
|
246
|
-
write(message)
|
|
247
|
-
{
|
|
248
|
-
if (this._ended || this._cancelled) return false;
|
|
249
|
-
if (!this._headersSent) this.sendMetadata();
|
|
250
|
-
|
|
251
|
-
try
|
|
252
|
-
{
|
|
253
|
-
const buf = encode(message, this._outputDesc, this._messageTypes);
|
|
254
|
-
const frame = frameEncode(buf, { compress: this._compress });
|
|
255
|
-
|
|
256
|
-
if (frame instanceof Promise)
|
|
257
|
-
{
|
|
258
|
-
frame.then((f) =>
|
|
259
|
-
{
|
|
260
|
-
if (!this._ended && !this._cancelled)
|
|
261
|
-
this.stream.write(f);
|
|
262
|
-
}).catch((err) =>
|
|
263
|
-
{
|
|
264
|
-
log.error('compression error: %s', err.message);
|
|
265
|
-
this.sendError(GrpcStatus.INTERNAL, 'Compression failed');
|
|
266
|
-
});
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return this.stream.write(frame);
|
|
271
|
-
}
|
|
272
|
-
catch (err)
|
|
273
|
-
{
|
|
274
|
-
log.error('encode error in %s: %s', this.method.name, err.message);
|
|
275
|
-
this.sendError(GrpcStatus.INTERNAL, 'Failed to encode response');
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// -- Deadline ------------------------------------------
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Parse and set up a deadline from the `grpc-timeout` header.
|
|
284
|
-
* Format: `{value}{unit}` where unit is n(nano), u(micro), m(milli), S(seconds), M(minutes), H(hours).
|
|
285
|
-
* @private
|
|
286
|
-
* @param {string} timeoutStr
|
|
287
|
-
*/
|
|
288
|
-
_setupDeadline(timeoutStr)
|
|
289
|
-
{
|
|
290
|
-
const match = /^(\d+)([nmuSMH])$/.exec(timeoutStr);
|
|
291
|
-
if (!match)
|
|
292
|
-
{
|
|
293
|
-
log.warn('invalid grpc-timeout: %s', timeoutStr);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const val = parseInt(match[1], 10);
|
|
298
|
-
let ms;
|
|
299
|
-
|
|
300
|
-
switch (match[2])
|
|
301
|
-
{
|
|
302
|
-
case 'n': ms = val / 1e6; break; // nanoseconds
|
|
303
|
-
case 'u': ms = val / 1e3; break; // microseconds
|
|
304
|
-
case 'm': ms = val; break; // milliseconds
|
|
305
|
-
case 'S': ms = val * 1000; break; // seconds
|
|
306
|
-
case 'M': ms = val * 60 * 1000; break; // minutes
|
|
307
|
-
case 'H': ms = val * 3600 * 1000; break; // hours
|
|
308
|
-
default: return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
this._deadline = Date.now() + ms;
|
|
312
|
-
this._deadlineTimer = setTimeout(() =>
|
|
313
|
-
{
|
|
314
|
-
if (!this._ended)
|
|
315
|
-
{
|
|
316
|
-
log.warn('deadline exceeded for %s (%dms)', this.method.name, ms);
|
|
317
|
-
this.sendError(GrpcStatus.DEADLINE_EXCEEDED, 'Deadline exceeded');
|
|
318
|
-
}
|
|
319
|
-
}, Math.max(1, ms));
|
|
320
|
-
|
|
321
|
-
if (this._deadlineTimer.unref) this._deadlineTimer.unref();
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Check if the call has been cancelled.
|
|
326
|
-
*
|
|
327
|
-
* @returns {boolean}
|
|
328
|
-
*/
|
|
329
|
-
get cancelled()
|
|
330
|
-
{
|
|
331
|
-
return this._cancelled;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Cancel the call from the server side.
|
|
336
|
-
*/
|
|
337
|
-
cancel()
|
|
338
|
-
{
|
|
339
|
-
if (this._ended) return;
|
|
340
|
-
this._cancelled = true;
|
|
341
|
-
this.sendError(GrpcStatus.CANCELLED, 'Cancelled by server');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Clean up timers and references.
|
|
346
|
-
* @private
|
|
347
|
-
*/
|
|
348
|
-
_cleanup()
|
|
349
|
-
{
|
|
350
|
-
if (this._deadlineTimer)
|
|
351
|
-
{
|
|
352
|
-
clearTimeout(this._deadlineTimer);
|
|
353
|
-
this._deadlineTimer = null;
|
|
354
|
-
}
|
|
355
|
-
this._parser.destroy();
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// -- Unary Call --------------------------------------------
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* A unary gRPC call — single request message, single response message.
|
|
363
|
-
*
|
|
364
|
-
* @class
|
|
365
|
-
* @extends BaseCall
|
|
366
|
-
*
|
|
367
|
-
* @example
|
|
368
|
-
* // In a service handler:
|
|
369
|
-
* async function GetUser(call) {
|
|
370
|
-
* const user = await db.users.findById(call.request.id);
|
|
371
|
-
* if (!user) call.sendError(GrpcStatus.NOT_FOUND, 'User not found');
|
|
372
|
-
* return user; // returned value is sent as the response
|
|
373
|
-
* }
|
|
374
|
-
*/
|
|
375
|
-
class UnaryCall extends BaseCall
|
|
376
|
-
{
|
|
377
|
-
/**
|
|
378
|
-
* @constructor
|
|
379
|
-
* @param {import('http2').Http2Stream} stream
|
|
380
|
-
* @param {object} methodDef
|
|
381
|
-
* @param {object} messageTypes
|
|
382
|
-
* @param {Metadata} metadata
|
|
383
|
-
* @param {object} [opts]
|
|
384
|
-
*/
|
|
385
|
-
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
386
|
-
{
|
|
387
|
-
super(stream, methodDef, messageTypes, metadata, opts);
|
|
388
|
-
|
|
389
|
-
/** The decoded request message (populated after receiving the full request). */
|
|
390
|
-
this.request = null;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Initialize the call — collect the full request body and decode it.
|
|
395
|
-
* @private
|
|
396
|
-
* @returns {Promise<void>}
|
|
397
|
-
*/
|
|
398
|
-
_init()
|
|
399
|
-
{
|
|
400
|
-
return new Promise((resolve, reject) =>
|
|
401
|
-
{
|
|
402
|
-
this._parser.onMessage = (buf) =>
|
|
403
|
-
{
|
|
404
|
-
try
|
|
405
|
-
{
|
|
406
|
-
this.request = decode(buf, this._inputDesc, this._messageTypes);
|
|
407
|
-
resolve();
|
|
408
|
-
}
|
|
409
|
-
catch (err) { reject(err); }
|
|
410
|
-
};
|
|
411
|
-
this._parser.onError = reject;
|
|
412
|
-
|
|
413
|
-
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
414
|
-
this.stream.on('end', () =>
|
|
415
|
-
{
|
|
416
|
-
if (!this.request) resolve(); // empty body = default message
|
|
417
|
-
});
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// -- Server Streaming Call ---------------------------------
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* A server-streaming gRPC call — single request, multiple responses.
|
|
426
|
-
* The handler calls `call.write(msg)` for each response and `call.end()` to finish.
|
|
427
|
-
*
|
|
428
|
-
* @class
|
|
429
|
-
* @extends BaseCall
|
|
430
|
-
*
|
|
431
|
-
* @example
|
|
432
|
-
* async function ListUsers(call) {
|
|
433
|
-
* const cursor = db.users.cursor();
|
|
434
|
-
* for await (const user of cursor) {
|
|
435
|
-
* call.write(user);
|
|
436
|
-
* }
|
|
437
|
-
* call.end();
|
|
438
|
-
* }
|
|
439
|
-
*/
|
|
440
|
-
class ServerStreamCall extends BaseCall
|
|
441
|
-
{
|
|
442
|
-
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
443
|
-
{
|
|
444
|
-
super(stream, methodDef, messageTypes, metadata, opts);
|
|
445
|
-
this.request = null;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* End the server stream with OK status.
|
|
450
|
-
*/
|
|
451
|
-
end()
|
|
452
|
-
{
|
|
453
|
-
this.sendStatus(GrpcStatus.OK);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/** @private */
|
|
457
|
-
_init()
|
|
458
|
-
{
|
|
459
|
-
return new Promise((resolve, reject) =>
|
|
460
|
-
{
|
|
461
|
-
this._parser.onMessage = (buf) =>
|
|
462
|
-
{
|
|
463
|
-
try
|
|
464
|
-
{
|
|
465
|
-
this.request = decode(buf, this._inputDesc, this._messageTypes);
|
|
466
|
-
resolve();
|
|
467
|
-
}
|
|
468
|
-
catch (err) { reject(err); }
|
|
469
|
-
};
|
|
470
|
-
this._parser.onError = reject;
|
|
471
|
-
|
|
472
|
-
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
473
|
-
this.stream.on('end', () =>
|
|
474
|
-
{
|
|
475
|
-
if (!this.request) resolve();
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// -- Client Streaming Call ---------------------------------
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* A client-streaming gRPC call — multiple requests, single response.
|
|
485
|
-
* The handler iterates `for await (const msg of call)` to consume messages,
|
|
486
|
-
* then returns the response object.
|
|
487
|
-
*
|
|
488
|
-
* @class
|
|
489
|
-
* @extends BaseCall
|
|
490
|
-
*
|
|
491
|
-
* @example
|
|
492
|
-
* async function UploadChunks(call) {
|
|
493
|
-
* let total = 0;
|
|
494
|
-
* for await (const chunk of call) {
|
|
495
|
-
* total += chunk.data.length;
|
|
496
|
-
* }
|
|
497
|
-
* return { bytesReceived: total };
|
|
498
|
-
* }
|
|
499
|
-
*/
|
|
500
|
-
class ClientStreamCall extends BaseCall
|
|
501
|
-
{
|
|
502
|
-
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
503
|
-
{
|
|
504
|
-
super(stream, methodDef, messageTypes, metadata, opts);
|
|
505
|
-
|
|
506
|
-
/** @private */
|
|
507
|
-
this._messageQueue = [];
|
|
508
|
-
/** @private */
|
|
509
|
-
this._messageResolve = null;
|
|
510
|
-
/** @private */
|
|
511
|
-
this._streamEnded = false;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Initialize — set up the frame parser to enqueue decoded messages.
|
|
516
|
-
* @private
|
|
517
|
-
*/
|
|
518
|
-
_init()
|
|
519
|
-
{
|
|
520
|
-
this._parser.onMessage = (buf) =>
|
|
521
|
-
{
|
|
522
|
-
try
|
|
523
|
-
{
|
|
524
|
-
const msg = decode(buf, this._inputDesc, this._messageTypes);
|
|
525
|
-
if (this._messageResolve)
|
|
526
|
-
{
|
|
527
|
-
const resolve = this._messageResolve;
|
|
528
|
-
this._messageResolve = null;
|
|
529
|
-
resolve({ value: msg, done: false });
|
|
530
|
-
}
|
|
531
|
-
else
|
|
532
|
-
{
|
|
533
|
-
this._messageQueue.push(msg);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
catch (err)
|
|
537
|
-
{
|
|
538
|
-
log.error('decode error in client stream: %s', err.message);
|
|
539
|
-
this.sendError(GrpcStatus.INTERNAL, 'Failed to decode client message');
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
this._parser.onError = (err) =>
|
|
543
|
-
{
|
|
544
|
-
this.sendError(GrpcStatus.INTERNAL, err.message);
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
548
|
-
this.stream.on('end', () =>
|
|
549
|
-
{
|
|
550
|
-
this._streamEnded = true;
|
|
551
|
-
if (this._messageResolve)
|
|
552
|
-
{
|
|
553
|
-
const resolve = this._messageResolve;
|
|
554
|
-
this._messageResolve = null;
|
|
555
|
-
resolve({ value: undefined, done: true });
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
return Promise.resolve();
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Async iterator — enables `for await (const msg of call)`.
|
|
564
|
-
*
|
|
565
|
-
* @returns {AsyncIterator<object>}
|
|
566
|
-
*/
|
|
567
|
-
[Symbol.asyncIterator]()
|
|
568
|
-
{
|
|
569
|
-
return {
|
|
570
|
-
next: () =>
|
|
571
|
-
{
|
|
572
|
-
if (this._messageQueue.length > 0)
|
|
573
|
-
{
|
|
574
|
-
return Promise.resolve({ value: this._messageQueue.shift(), done: false });
|
|
575
|
-
}
|
|
576
|
-
if (this._streamEnded)
|
|
577
|
-
{
|
|
578
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
579
|
-
}
|
|
580
|
-
return new Promise((resolve) =>
|
|
581
|
-
{
|
|
582
|
-
this._messageResolve = resolve;
|
|
583
|
-
});
|
|
584
|
-
},
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// -- Bidirectional Streaming Call ---------------------------
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* A bidirectional streaming gRPC call — multiple requests AND multiple responses.
|
|
593
|
-
* The handler can `for await` incoming messages while simultaneously
|
|
594
|
-
* calling `call.write()` to send responses.
|
|
595
|
-
*
|
|
596
|
-
* @class
|
|
597
|
-
* @extends BaseCall
|
|
598
|
-
*
|
|
599
|
-
* @example
|
|
600
|
-
* async function Chat(call) {
|
|
601
|
-
* for await (const msg of call) {
|
|
602
|
-
* // Echo back with a timestamp
|
|
603
|
-
* call.write({ text: msg.text, ts: Date.now() });
|
|
604
|
-
* }
|
|
605
|
-
* call.end();
|
|
606
|
-
* }
|
|
607
|
-
*/
|
|
608
|
-
class BidiStreamCall extends BaseCall
|
|
609
|
-
{
|
|
610
|
-
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
611
|
-
{
|
|
612
|
-
super(stream, methodDef, messageTypes, metadata, opts);
|
|
613
|
-
|
|
614
|
-
/** @private */
|
|
615
|
-
this._messageQueue = [];
|
|
616
|
-
/** @private */
|
|
617
|
-
this._messageResolve = null;
|
|
618
|
-
/** @private */
|
|
619
|
-
this._streamEnded = false;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* End the bidirectional stream with OK status.
|
|
624
|
-
*/
|
|
625
|
-
end()
|
|
626
|
-
{
|
|
627
|
-
this.sendStatus(GrpcStatus.OK);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/** @private */
|
|
631
|
-
_init()
|
|
632
|
-
{
|
|
633
|
-
this._parser.onMessage = (buf) =>
|
|
634
|
-
{
|
|
635
|
-
try
|
|
636
|
-
{
|
|
637
|
-
const msg = decode(buf, this._inputDesc, this._messageTypes);
|
|
638
|
-
if (this._messageResolve)
|
|
639
|
-
{
|
|
640
|
-
const resolve = this._messageResolve;
|
|
641
|
-
this._messageResolve = null;
|
|
642
|
-
resolve({ value: msg, done: false });
|
|
643
|
-
}
|
|
644
|
-
else
|
|
645
|
-
{
|
|
646
|
-
this._messageQueue.push(msg);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
catch (err)
|
|
650
|
-
{
|
|
651
|
-
log.error('decode error in bidi stream: %s', err.message);
|
|
652
|
-
this.sendError(GrpcStatus.INTERNAL, 'Failed to decode message');
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
this._parser.onError = (err) =>
|
|
656
|
-
{
|
|
657
|
-
this.sendError(GrpcStatus.INTERNAL, err.message);
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
661
|
-
this.stream.on('end', () =>
|
|
662
|
-
{
|
|
663
|
-
this._streamEnded = true;
|
|
664
|
-
if (this._messageResolve)
|
|
665
|
-
{
|
|
666
|
-
const resolve = this._messageResolve;
|
|
667
|
-
this._messageResolve = null;
|
|
668
|
-
resolve({ value: undefined, done: true });
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
return Promise.resolve();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Async iterator — enables `for await (const msg of call)`.
|
|
677
|
-
*
|
|
678
|
-
* @returns {AsyncIterator<object>}
|
|
679
|
-
*/
|
|
680
|
-
[Symbol.asyncIterator]()
|
|
681
|
-
{
|
|
682
|
-
return {
|
|
683
|
-
next: () =>
|
|
684
|
-
{
|
|
685
|
-
if (this._messageQueue.length > 0)
|
|
686
|
-
{
|
|
687
|
-
return Promise.resolve({ value: this._messageQueue.shift(), done: false });
|
|
688
|
-
}
|
|
689
|
-
if (this._streamEnded)
|
|
690
|
-
{
|
|
691
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
692
|
-
}
|
|
693
|
-
return new Promise((resolve) =>
|
|
694
|
-
{
|
|
695
|
-
this._messageResolve = resolve;
|
|
696
|
-
});
|
|
697
|
-
},
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
module.exports = {
|
|
703
|
-
BaseCall,
|
|
704
|
-
UnaryCall,
|
|
705
|
-
ServerStreamCall,
|
|
706
|
-
ClientStreamCall,
|
|
707
|
-
BidiStreamCall,
|
|
708
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @module grpc/call
|
|
3
|
+
* @description gRPC call objects for the four RPC patterns.
|
|
4
|
+
* Wraps HTTP/2 streams with protobuf encode/decode, metadata,
|
|
5
|
+
* framing, deadline enforcement, and cancellation support.
|
|
6
|
+
*
|
|
7
|
+
* - `UnaryCall` — single request, single response
|
|
8
|
+
* - `ServerStreamCall` — single request, stream of responses
|
|
9
|
+
* - `ClientStreamCall` — stream of requests, single response
|
|
10
|
+
* - `BidiStreamCall` — bidirectional streaming
|
|
11
|
+
*
|
|
12
|
+
* @example | Unary handler
|
|
13
|
+
* async function GetUser(call) {
|
|
14
|
+
* const user = await db.findById(call.request.id);
|
|
15
|
+
* return user; // auto-encoded and sent
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* @example | Server streaming handler
|
|
19
|
+
* async function ListUsers(call) {
|
|
20
|
+
* for (const user of users) {
|
|
21
|
+
* call.write(user);
|
|
22
|
+
* }
|
|
23
|
+
* call.end();
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* @example | Bidirectional streaming handler
|
|
27
|
+
* async function Chat(call) {
|
|
28
|
+
* for await (const msg of call) {
|
|
29
|
+
* call.write({ echo: msg.text, ts: Date.now() });
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const { EventEmitter } = require('events');
|
|
35
|
+
const log = require('../debug')('zero:grpc');
|
|
36
|
+
const { GrpcStatus, statusName } = require('./status');
|
|
37
|
+
const { Metadata } = require('./metadata');
|
|
38
|
+
const { frameEncode, FrameParser } = require('./frame');
|
|
39
|
+
const { encode, decode } = require('./codec');
|
|
40
|
+
|
|
41
|
+
// -- Base Call ---------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Base class for all gRPC call types. Manages the HTTP/2 stream,
|
|
45
|
+
* metadata, deadlines, cancellation, and common lifecycle.
|
|
46
|
+
*
|
|
47
|
+
* @class
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
class BaseCall extends EventEmitter
|
|
51
|
+
{
|
|
52
|
+
/**
|
|
53
|
+
* @constructor
|
|
54
|
+
* @param {import('http2').Http2Stream} stream - The HTTP/2 stream for this call.
|
|
55
|
+
* @param {object} methodDef - Method descriptor from the proto schema.
|
|
56
|
+
* @param {object} messageTypes - Map of all message descriptors.
|
|
57
|
+
* @param {Metadata} metadata - Initial metadata from the client.
|
|
58
|
+
* @param {object} [opts] - Call options.
|
|
59
|
+
* @param {number} [opts.maxMessageSize] - Max message size in bytes.
|
|
60
|
+
* @param {boolean} [opts.compress=false] - Whether to compress outgoing messages.
|
|
61
|
+
*/
|
|
62
|
+
constructor(stream, methodDef, messageTypes, metadata, opts = {})
|
|
63
|
+
{
|
|
64
|
+
super();
|
|
65
|
+
|
|
66
|
+
/** The underlying HTTP/2 stream. */
|
|
67
|
+
this.stream = stream;
|
|
68
|
+
|
|
69
|
+
/** Method descriptor from the parsed proto. */
|
|
70
|
+
this.method = methodDef;
|
|
71
|
+
|
|
72
|
+
/** Initial metadata from the client. */
|
|
73
|
+
this.metadata = metadata;
|
|
74
|
+
|
|
75
|
+
/** Response (trailing) metadata to be sent with the status. */
|
|
76
|
+
this.trailingMetadata = new Metadata();
|
|
77
|
+
|
|
78
|
+
/** @private */
|
|
79
|
+
this._messageTypes = messageTypes;
|
|
80
|
+
/** @private */
|
|
81
|
+
this._inputDesc = messageTypes[methodDef.inputType];
|
|
82
|
+
/** @private */
|
|
83
|
+
this._outputDesc = messageTypes[methodDef.outputType];
|
|
84
|
+
/** @private */
|
|
85
|
+
this._compress = !!opts.compress;
|
|
86
|
+
/** @private */
|
|
87
|
+
this._cancelled = false;
|
|
88
|
+
/** @private */
|
|
89
|
+
this._ended = false;
|
|
90
|
+
/** @private */
|
|
91
|
+
this._headersSent = false;
|
|
92
|
+
/** @private */
|
|
93
|
+
this._deadlineTimer = null;
|
|
94
|
+
|
|
95
|
+
if (!this._inputDesc)
|
|
96
|
+
throw new Error(`Unknown input message type: ${methodDef.inputType}`);
|
|
97
|
+
if (!this._outputDesc)
|
|
98
|
+
throw new Error(`Unknown output message type: ${methodDef.outputType}`);
|
|
99
|
+
|
|
100
|
+
/** @private */
|
|
101
|
+
this._parser = new FrameParser({ maxMessageSize: opts.maxMessageSize });
|
|
102
|
+
|
|
103
|
+
// Parse the deadline from grpc-timeout header
|
|
104
|
+
this._deadline = null;
|
|
105
|
+
const timeoutHeader = stream.sentHeaders && stream.sentHeaders['grpc-timeout'];
|
|
106
|
+
if (timeoutHeader) this._setupDeadline(timeoutHeader);
|
|
107
|
+
|
|
108
|
+
// Cancellation via stream reset
|
|
109
|
+
stream.on('close', () =>
|
|
110
|
+
{
|
|
111
|
+
if (!this._ended)
|
|
112
|
+
{
|
|
113
|
+
this._cancelled = true;
|
|
114
|
+
this.emit('cancelled');
|
|
115
|
+
}
|
|
116
|
+
this._cleanup();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
stream.on('error', (err) =>
|
|
120
|
+
{
|
|
121
|
+
log.error('stream error on %s: %s', methodDef.name, err.message);
|
|
122
|
+
this.emit('error', err);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/** Peer IP address (from stream session). */
|
|
126
|
+
this.peer = stream.session && stream.session.socket
|
|
127
|
+
? stream.session.socket.remoteAddress || 'unknown'
|
|
128
|
+
: 'unknown';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// -- Metadata Sending ----------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Send initial response metadata (HTTP/2 headers).
|
|
135
|
+
* Must be called before writing any messages.
|
|
136
|
+
* If not called explicitly, headers are sent automatically on the first write.
|
|
137
|
+
*
|
|
138
|
+
* @param {Metadata|object} [md] - Additional metadata to merge into the response headers.
|
|
139
|
+
*/
|
|
140
|
+
sendMetadata(md)
|
|
141
|
+
{
|
|
142
|
+
if (this._headersSent || this._ended) return;
|
|
143
|
+
this._headersSent = true;
|
|
144
|
+
|
|
145
|
+
const headers = {
|
|
146
|
+
':status': 200,
|
|
147
|
+
'content-type': 'application/grpc+proto',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (this._compress) headers['grpc-encoding'] = 'gzip';
|
|
151
|
+
|
|
152
|
+
if (md)
|
|
153
|
+
{
|
|
154
|
+
const extra = md instanceof Metadata ? md.toHeaders() : md;
|
|
155
|
+
Object.assign(headers, extra);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try { this.stream.respond(headers, { waitForTrailers: true }); }
|
|
159
|
+
catch (e) { log.warn('failed to send metadata: %s', e.message); }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// -- Status / End --------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Send a gRPC status and close the call.
|
|
166
|
+
* Trailers carry `grpc-status` and optionally `grpc-message`.
|
|
167
|
+
*
|
|
168
|
+
* @param {number} code - gRPC status code.
|
|
169
|
+
* @param {string} [message] - Human-readable status message.
|
|
170
|
+
*/
|
|
171
|
+
sendStatus(code, message)
|
|
172
|
+
{
|
|
173
|
+
if (this._ended) return;
|
|
174
|
+
this._ended = true;
|
|
175
|
+
|
|
176
|
+
const trailHeaders = {
|
|
177
|
+
'grpc-status': String(code),
|
|
178
|
+
...this.trailingMetadata.toHeaders(),
|
|
179
|
+
};
|
|
180
|
+
if (message) trailHeaders['grpc-message'] = encodeURIComponent(message);
|
|
181
|
+
|
|
182
|
+
if (!this._headersSent)
|
|
183
|
+
{
|
|
184
|
+
// Trailers-Only response — include status in initial HEADERS frame
|
|
185
|
+
this._headersSent = true;
|
|
186
|
+
try
|
|
187
|
+
{
|
|
188
|
+
this.stream.respond({
|
|
189
|
+
':status': 200,
|
|
190
|
+
'content-type': 'application/grpc+proto',
|
|
191
|
+
...trailHeaders,
|
|
192
|
+
}, { endStream: true });
|
|
193
|
+
}
|
|
194
|
+
catch (_)
|
|
195
|
+
{
|
|
196
|
+
try { this.stream.close(); }
|
|
197
|
+
catch (__) { /* nothing to do */ }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
{
|
|
202
|
+
// Headers already sent — send trailing HEADERS after final DATA
|
|
203
|
+
this.stream.on('wantTrailers', () =>
|
|
204
|
+
{
|
|
205
|
+
try { this.stream.sendTrailers(trailHeaders); }
|
|
206
|
+
catch (_) { /* stream may already be closed */ }
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
try { this.stream.end(); }
|
|
210
|
+
catch (_)
|
|
211
|
+
{
|
|
212
|
+
try { this.stream.close(); }
|
|
213
|
+
catch (__) { /* nothing to do */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this._cleanup();
|
|
218
|
+
|
|
219
|
+
if (code !== GrpcStatus.OK)
|
|
220
|
+
log.warn('call %s ended with status %s: %s', this.method.name, statusName(code), message || '');
|
|
221
|
+
else
|
|
222
|
+
log.debug('call %s completed OK', this.method.name);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Send an error status and close the call.
|
|
227
|
+
* Convenience wrapper around `sendStatus`.
|
|
228
|
+
*
|
|
229
|
+
* @param {number} code - gRPC status code.
|
|
230
|
+
* @param {string} message - Error description.
|
|
231
|
+
*/
|
|
232
|
+
sendError(code, message)
|
|
233
|
+
{
|
|
234
|
+
this.sendStatus(code, message);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// -- Writing -------------------------------------------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Write a response message. The object is encoded to protobuf, framed,
|
|
241
|
+
* and sent on the HTTP/2 stream.
|
|
242
|
+
*
|
|
243
|
+
* @param {object} message - JavaScript object matching the output message schema.
|
|
244
|
+
* @returns {boolean} `false` if the stream is not writable.
|
|
245
|
+
*/
|
|
246
|
+
write(message)
|
|
247
|
+
{
|
|
248
|
+
if (this._ended || this._cancelled) return false;
|
|
249
|
+
if (!this._headersSent) this.sendMetadata();
|
|
250
|
+
|
|
251
|
+
try
|
|
252
|
+
{
|
|
253
|
+
const buf = encode(message, this._outputDesc, this._messageTypes);
|
|
254
|
+
const frame = frameEncode(buf, { compress: this._compress });
|
|
255
|
+
|
|
256
|
+
if (frame instanceof Promise)
|
|
257
|
+
{
|
|
258
|
+
frame.then((f) =>
|
|
259
|
+
{
|
|
260
|
+
if (!this._ended && !this._cancelled)
|
|
261
|
+
this.stream.write(f);
|
|
262
|
+
}).catch((err) =>
|
|
263
|
+
{
|
|
264
|
+
log.error('compression error: %s', err.message);
|
|
265
|
+
this.sendError(GrpcStatus.INTERNAL, 'Compression failed');
|
|
266
|
+
});
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this.stream.write(frame);
|
|
271
|
+
}
|
|
272
|
+
catch (err)
|
|
273
|
+
{
|
|
274
|
+
log.error('encode error in %s: %s', this.method.name, err.message);
|
|
275
|
+
this.sendError(GrpcStatus.INTERNAL, 'Failed to encode response');
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -- Deadline ------------------------------------------
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Parse and set up a deadline from the `grpc-timeout` header.
|
|
284
|
+
* Format: `{value}{unit}` where unit is n(nano), u(micro), m(milli), S(seconds), M(minutes), H(hours).
|
|
285
|
+
* @private
|
|
286
|
+
* @param {string} timeoutStr
|
|
287
|
+
*/
|
|
288
|
+
_setupDeadline(timeoutStr)
|
|
289
|
+
{
|
|
290
|
+
const match = /^(\d+)([nmuSMH])$/.exec(timeoutStr);
|
|
291
|
+
if (!match)
|
|
292
|
+
{
|
|
293
|
+
log.warn('invalid grpc-timeout: %s', timeoutStr);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const val = parseInt(match[1], 10);
|
|
298
|
+
let ms;
|
|
299
|
+
|
|
300
|
+
switch (match[2])
|
|
301
|
+
{
|
|
302
|
+
case 'n': ms = val / 1e6; break; // nanoseconds
|
|
303
|
+
case 'u': ms = val / 1e3; break; // microseconds
|
|
304
|
+
case 'm': ms = val; break; // milliseconds
|
|
305
|
+
case 'S': ms = val * 1000; break; // seconds
|
|
306
|
+
case 'M': ms = val * 60 * 1000; break; // minutes
|
|
307
|
+
case 'H': ms = val * 3600 * 1000; break; // hours
|
|
308
|
+
default: return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this._deadline = Date.now() + ms;
|
|
312
|
+
this._deadlineTimer = setTimeout(() =>
|
|
313
|
+
{
|
|
314
|
+
if (!this._ended)
|
|
315
|
+
{
|
|
316
|
+
log.warn('deadline exceeded for %s (%dms)', this.method.name, ms);
|
|
317
|
+
this.sendError(GrpcStatus.DEADLINE_EXCEEDED, 'Deadline exceeded');
|
|
318
|
+
}
|
|
319
|
+
}, Math.max(1, ms));
|
|
320
|
+
|
|
321
|
+
if (this._deadlineTimer.unref) this._deadlineTimer.unref();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if the call has been cancelled.
|
|
326
|
+
*
|
|
327
|
+
* @returns {boolean}
|
|
328
|
+
*/
|
|
329
|
+
get cancelled()
|
|
330
|
+
{
|
|
331
|
+
return this._cancelled;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Cancel the call from the server side.
|
|
336
|
+
*/
|
|
337
|
+
cancel()
|
|
338
|
+
{
|
|
339
|
+
if (this._ended) return;
|
|
340
|
+
this._cancelled = true;
|
|
341
|
+
this.sendError(GrpcStatus.CANCELLED, 'Cancelled by server');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Clean up timers and references.
|
|
346
|
+
* @private
|
|
347
|
+
*/
|
|
348
|
+
_cleanup()
|
|
349
|
+
{
|
|
350
|
+
if (this._deadlineTimer)
|
|
351
|
+
{
|
|
352
|
+
clearTimeout(this._deadlineTimer);
|
|
353
|
+
this._deadlineTimer = null;
|
|
354
|
+
}
|
|
355
|
+
this._parser.destroy();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// -- Unary Call --------------------------------------------
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A unary gRPC call — single request message, single response message.
|
|
363
|
+
*
|
|
364
|
+
* @class
|
|
365
|
+
* @extends BaseCall
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* // In a service handler:
|
|
369
|
+
* async function GetUser(call) {
|
|
370
|
+
* const user = await db.users.findById(call.request.id);
|
|
371
|
+
* if (!user) call.sendError(GrpcStatus.NOT_FOUND, 'User not found');
|
|
372
|
+
* return user; // returned value is sent as the response
|
|
373
|
+
* }
|
|
374
|
+
*/
|
|
375
|
+
class UnaryCall extends BaseCall
|
|
376
|
+
{
|
|
377
|
+
/**
|
|
378
|
+
* @constructor
|
|
379
|
+
* @param {import('http2').Http2Stream} stream
|
|
380
|
+
* @param {object} methodDef
|
|
381
|
+
* @param {object} messageTypes
|
|
382
|
+
* @param {Metadata} metadata
|
|
383
|
+
* @param {object} [opts]
|
|
384
|
+
*/
|
|
385
|
+
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
386
|
+
{
|
|
387
|
+
super(stream, methodDef, messageTypes, metadata, opts);
|
|
388
|
+
|
|
389
|
+
/** The decoded request message (populated after receiving the full request). */
|
|
390
|
+
this.request = null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Initialize the call — collect the full request body and decode it.
|
|
395
|
+
* @private
|
|
396
|
+
* @returns {Promise<void>}
|
|
397
|
+
*/
|
|
398
|
+
_init()
|
|
399
|
+
{
|
|
400
|
+
return new Promise((resolve, reject) =>
|
|
401
|
+
{
|
|
402
|
+
this._parser.onMessage = (buf) =>
|
|
403
|
+
{
|
|
404
|
+
try
|
|
405
|
+
{
|
|
406
|
+
this.request = decode(buf, this._inputDesc, this._messageTypes);
|
|
407
|
+
resolve();
|
|
408
|
+
}
|
|
409
|
+
catch (err) { reject(err); }
|
|
410
|
+
};
|
|
411
|
+
this._parser.onError = reject;
|
|
412
|
+
|
|
413
|
+
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
414
|
+
this.stream.on('end', () =>
|
|
415
|
+
{
|
|
416
|
+
if (!this.request) resolve(); // empty body = default message
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// -- Server Streaming Call ---------------------------------
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* A server-streaming gRPC call — single request, multiple responses.
|
|
426
|
+
* The handler calls `call.write(msg)` for each response and `call.end()` to finish.
|
|
427
|
+
*
|
|
428
|
+
* @class
|
|
429
|
+
* @extends BaseCall
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* async function ListUsers(call) {
|
|
433
|
+
* const cursor = db.users.cursor();
|
|
434
|
+
* for await (const user of cursor) {
|
|
435
|
+
* call.write(user);
|
|
436
|
+
* }
|
|
437
|
+
* call.end();
|
|
438
|
+
* }
|
|
439
|
+
*/
|
|
440
|
+
class ServerStreamCall extends BaseCall
|
|
441
|
+
{
|
|
442
|
+
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
443
|
+
{
|
|
444
|
+
super(stream, methodDef, messageTypes, metadata, opts);
|
|
445
|
+
this.request = null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* End the server stream with OK status.
|
|
450
|
+
*/
|
|
451
|
+
end()
|
|
452
|
+
{
|
|
453
|
+
this.sendStatus(GrpcStatus.OK);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** @private */
|
|
457
|
+
_init()
|
|
458
|
+
{
|
|
459
|
+
return new Promise((resolve, reject) =>
|
|
460
|
+
{
|
|
461
|
+
this._parser.onMessage = (buf) =>
|
|
462
|
+
{
|
|
463
|
+
try
|
|
464
|
+
{
|
|
465
|
+
this.request = decode(buf, this._inputDesc, this._messageTypes);
|
|
466
|
+
resolve();
|
|
467
|
+
}
|
|
468
|
+
catch (err) { reject(err); }
|
|
469
|
+
};
|
|
470
|
+
this._parser.onError = reject;
|
|
471
|
+
|
|
472
|
+
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
473
|
+
this.stream.on('end', () =>
|
|
474
|
+
{
|
|
475
|
+
if (!this.request) resolve();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// -- Client Streaming Call ---------------------------------
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* A client-streaming gRPC call — multiple requests, single response.
|
|
485
|
+
* The handler iterates `for await (const msg of call)` to consume messages,
|
|
486
|
+
* then returns the response object.
|
|
487
|
+
*
|
|
488
|
+
* @class
|
|
489
|
+
* @extends BaseCall
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* async function UploadChunks(call) {
|
|
493
|
+
* let total = 0;
|
|
494
|
+
* for await (const chunk of call) {
|
|
495
|
+
* total += chunk.data.length;
|
|
496
|
+
* }
|
|
497
|
+
* return { bytesReceived: total };
|
|
498
|
+
* }
|
|
499
|
+
*/
|
|
500
|
+
class ClientStreamCall extends BaseCall
|
|
501
|
+
{
|
|
502
|
+
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
503
|
+
{
|
|
504
|
+
super(stream, methodDef, messageTypes, metadata, opts);
|
|
505
|
+
|
|
506
|
+
/** @private */
|
|
507
|
+
this._messageQueue = [];
|
|
508
|
+
/** @private */
|
|
509
|
+
this._messageResolve = null;
|
|
510
|
+
/** @private */
|
|
511
|
+
this._streamEnded = false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Initialize — set up the frame parser to enqueue decoded messages.
|
|
516
|
+
* @private
|
|
517
|
+
*/
|
|
518
|
+
_init()
|
|
519
|
+
{
|
|
520
|
+
this._parser.onMessage = (buf) =>
|
|
521
|
+
{
|
|
522
|
+
try
|
|
523
|
+
{
|
|
524
|
+
const msg = decode(buf, this._inputDesc, this._messageTypes);
|
|
525
|
+
if (this._messageResolve)
|
|
526
|
+
{
|
|
527
|
+
const resolve = this._messageResolve;
|
|
528
|
+
this._messageResolve = null;
|
|
529
|
+
resolve({ value: msg, done: false });
|
|
530
|
+
}
|
|
531
|
+
else
|
|
532
|
+
{
|
|
533
|
+
this._messageQueue.push(msg);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch (err)
|
|
537
|
+
{
|
|
538
|
+
log.error('decode error in client stream: %s', err.message);
|
|
539
|
+
this.sendError(GrpcStatus.INTERNAL, 'Failed to decode client message');
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
this._parser.onError = (err) =>
|
|
543
|
+
{
|
|
544
|
+
this.sendError(GrpcStatus.INTERNAL, err.message);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
548
|
+
this.stream.on('end', () =>
|
|
549
|
+
{
|
|
550
|
+
this._streamEnded = true;
|
|
551
|
+
if (this._messageResolve)
|
|
552
|
+
{
|
|
553
|
+
const resolve = this._messageResolve;
|
|
554
|
+
this._messageResolve = null;
|
|
555
|
+
resolve({ value: undefined, done: true });
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return Promise.resolve();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Async iterator — enables `for await (const msg of call)`.
|
|
564
|
+
*
|
|
565
|
+
* @returns {AsyncIterator<object>}
|
|
566
|
+
*/
|
|
567
|
+
[Symbol.asyncIterator]()
|
|
568
|
+
{
|
|
569
|
+
return {
|
|
570
|
+
next: () =>
|
|
571
|
+
{
|
|
572
|
+
if (this._messageQueue.length > 0)
|
|
573
|
+
{
|
|
574
|
+
return Promise.resolve({ value: this._messageQueue.shift(), done: false });
|
|
575
|
+
}
|
|
576
|
+
if (this._streamEnded)
|
|
577
|
+
{
|
|
578
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
579
|
+
}
|
|
580
|
+
return new Promise((resolve) =>
|
|
581
|
+
{
|
|
582
|
+
this._messageResolve = resolve;
|
|
583
|
+
});
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// -- Bidirectional Streaming Call ---------------------------
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* A bidirectional streaming gRPC call — multiple requests AND multiple responses.
|
|
593
|
+
* The handler can `for await` incoming messages while simultaneously
|
|
594
|
+
* calling `call.write()` to send responses.
|
|
595
|
+
*
|
|
596
|
+
* @class
|
|
597
|
+
* @extends BaseCall
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* async function Chat(call) {
|
|
601
|
+
* for await (const msg of call) {
|
|
602
|
+
* // Echo back with a timestamp
|
|
603
|
+
* call.write({ text: msg.text, ts: Date.now() });
|
|
604
|
+
* }
|
|
605
|
+
* call.end();
|
|
606
|
+
* }
|
|
607
|
+
*/
|
|
608
|
+
class BidiStreamCall extends BaseCall
|
|
609
|
+
{
|
|
610
|
+
constructor(stream, methodDef, messageTypes, metadata, opts)
|
|
611
|
+
{
|
|
612
|
+
super(stream, methodDef, messageTypes, metadata, opts);
|
|
613
|
+
|
|
614
|
+
/** @private */
|
|
615
|
+
this._messageQueue = [];
|
|
616
|
+
/** @private */
|
|
617
|
+
this._messageResolve = null;
|
|
618
|
+
/** @private */
|
|
619
|
+
this._streamEnded = false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* End the bidirectional stream with OK status.
|
|
624
|
+
*/
|
|
625
|
+
end()
|
|
626
|
+
{
|
|
627
|
+
this.sendStatus(GrpcStatus.OK);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** @private */
|
|
631
|
+
_init()
|
|
632
|
+
{
|
|
633
|
+
this._parser.onMessage = (buf) =>
|
|
634
|
+
{
|
|
635
|
+
try
|
|
636
|
+
{
|
|
637
|
+
const msg = decode(buf, this._inputDesc, this._messageTypes);
|
|
638
|
+
if (this._messageResolve)
|
|
639
|
+
{
|
|
640
|
+
const resolve = this._messageResolve;
|
|
641
|
+
this._messageResolve = null;
|
|
642
|
+
resolve({ value: msg, done: false });
|
|
643
|
+
}
|
|
644
|
+
else
|
|
645
|
+
{
|
|
646
|
+
this._messageQueue.push(msg);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch (err)
|
|
650
|
+
{
|
|
651
|
+
log.error('decode error in bidi stream: %s', err.message);
|
|
652
|
+
this.sendError(GrpcStatus.INTERNAL, 'Failed to decode message');
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
this._parser.onError = (err) =>
|
|
656
|
+
{
|
|
657
|
+
this.sendError(GrpcStatus.INTERNAL, err.message);
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
this.stream.on('data', (chunk) => this._parser.push(chunk));
|
|
661
|
+
this.stream.on('end', () =>
|
|
662
|
+
{
|
|
663
|
+
this._streamEnded = true;
|
|
664
|
+
if (this._messageResolve)
|
|
665
|
+
{
|
|
666
|
+
const resolve = this._messageResolve;
|
|
667
|
+
this._messageResolve = null;
|
|
668
|
+
resolve({ value: undefined, done: true });
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
return Promise.resolve();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Async iterator — enables `for await (const msg of call)`.
|
|
677
|
+
*
|
|
678
|
+
* @returns {AsyncIterator<object>}
|
|
679
|
+
*/
|
|
680
|
+
[Symbol.asyncIterator]()
|
|
681
|
+
{
|
|
682
|
+
return {
|
|
683
|
+
next: () =>
|
|
684
|
+
{
|
|
685
|
+
if (this._messageQueue.length > 0)
|
|
686
|
+
{
|
|
687
|
+
return Promise.resolve({ value: this._messageQueue.shift(), done: false });
|
|
688
|
+
}
|
|
689
|
+
if (this._streamEnded)
|
|
690
|
+
{
|
|
691
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
692
|
+
}
|
|
693
|
+
return new Promise((resolve) =>
|
|
694
|
+
{
|
|
695
|
+
this._messageResolve = resolve;
|
|
696
|
+
});
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
module.exports = {
|
|
703
|
+
BaseCall,
|
|
704
|
+
UnaryCall,
|
|
705
|
+
ServerStreamCall,
|
|
706
|
+
ClientStreamCall,
|
|
707
|
+
BidiStreamCall,
|
|
708
|
+
};
|