@zero-server/grpc 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 +1 -1
- package/index.js +27 -27
- package/lib/debug.js +372 -0
- package/lib/grpc/balancer.js +378 -0
- package/lib/grpc/call.js +708 -0
- package/lib/grpc/client.js +764 -0
- package/lib/grpc/codec.js +1221 -0
- package/lib/grpc/credentials.js +398 -0
- package/lib/grpc/frame.js +262 -0
- package/lib/grpc/health.js +287 -0
- package/lib/grpc/index.js +121 -0
- package/lib/grpc/metadata.js +461 -0
- package/lib/grpc/proto.js +821 -0
- package/lib/grpc/reflection.js +590 -0
- package/lib/grpc/server.js +445 -0
- package/lib/grpc/status.js +118 -0
- package/lib/grpc/watch.js +173 -0
- package/package.json +10 -4
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module grpc/server
|
|
3
|
+
* @description gRPC server for zero-server.
|
|
4
|
+
* Intercepts HTTP/2 streams with `content-type: application/grpc`,
|
|
5
|
+
* routes by `:path` pseudo-header (`/package.Service/Method`),
|
|
6
|
+
* and dispatches to registered service handlers.
|
|
7
|
+
*
|
|
8
|
+
* Handles all four gRPC call types:
|
|
9
|
+
* - Unary (single request → single response)
|
|
10
|
+
* - Server streaming (single request → multiple responses)
|
|
11
|
+
* - Client streaming (multiple requests → single response)
|
|
12
|
+
* - Bidirectional streaming (multiple requests ↔ multiple responses)
|
|
13
|
+
*
|
|
14
|
+
* Supports interceptors (server-side middleware), deadline enforcement,
|
|
15
|
+
* message size limits, and graceful shutdown with call draining.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const { createApp, parseProto } = require('@zero-server/sdk');
|
|
19
|
+
* const app = createApp();
|
|
20
|
+
* const schema = parseProto(fs.readFileSync('hello.proto', 'utf8'));
|
|
21
|
+
*
|
|
22
|
+
* app.grpc(schema, 'Greeter', {
|
|
23
|
+
* SayHello(call) {
|
|
24
|
+
* return { message: 'Hello ' + call.request.name };
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* app.listen(50051, { http2: true });
|
|
29
|
+
*
|
|
30
|
+
* @example | Server streaming
|
|
31
|
+
* app.grpc(schema, 'DataService', {
|
|
32
|
+
* StreamData(call) {
|
|
33
|
+
* for (let i = 0; i < 100; i++) {
|
|
34
|
+
* call.write({ seq: i, payload: 'chunk-' + i });
|
|
35
|
+
* }
|
|
36
|
+
* call.end();
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* @example | Bidirectional streaming with interceptors
|
|
41
|
+
* app.grpc(schema, 'ChatService', {
|
|
42
|
+
* Chat(call) {
|
|
43
|
+
* for await (const msg of call) {
|
|
44
|
+
* call.write({ echo: msg.text, ts: Date.now() });
|
|
45
|
+
* }
|
|
46
|
+
* call.end();
|
|
47
|
+
* },
|
|
48
|
+
* }, {
|
|
49
|
+
* interceptors: [authInterceptor, loggingInterceptor],
|
|
50
|
+
* });
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const log = require('../debug')('zero:grpc');
|
|
54
|
+
const { GrpcStatus, statusName } = require('./status');
|
|
55
|
+
const { Metadata } = require('./metadata');
|
|
56
|
+
const { UnaryCall, ServerStreamCall, ClientStreamCall, BidiStreamCall } = require('./call');
|
|
57
|
+
|
|
58
|
+
// -- Service Registry --------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registry of gRPC services and their handlers.
|
|
62
|
+
* Manages routing of HTTP/2 streams to the correct service method.
|
|
63
|
+
*
|
|
64
|
+
* @class
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const registry = new GrpcServiceRegistry();
|
|
68
|
+
* registry.addService(schema, 'Greeter', handlers, opts);
|
|
69
|
+
*/
|
|
70
|
+
class GrpcServiceRegistry
|
|
71
|
+
{
|
|
72
|
+
constructor()
|
|
73
|
+
{
|
|
74
|
+
/**
|
|
75
|
+
* Map of path → handler descriptor.
|
|
76
|
+
* Keys are in the format `/package.ServiceName/MethodName`.
|
|
77
|
+
* @type {Map<string, { service: string, method: object, handler: Function, schema: object, opts: object }>}
|
|
78
|
+
*/
|
|
79
|
+
this._routes = new Map();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Global interceptors applied to all services.
|
|
83
|
+
* @type {Function[]}
|
|
84
|
+
*/
|
|
85
|
+
this._interceptors = [];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Active calls for graceful shutdown draining.
|
|
89
|
+
* @type {Set<import('./call').BaseCall>}
|
|
90
|
+
*/
|
|
91
|
+
this._activeCalls = new Set();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether the server is draining (rejecting new calls).
|
|
95
|
+
* @type {boolean}
|
|
96
|
+
*/
|
|
97
|
+
this._draining = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Register a service with its handlers.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} schema - Parsed proto schema from `parseProto()`.
|
|
104
|
+
* @param {string} serviceName - Name of the service as defined in the proto file.
|
|
105
|
+
* @param {Object<string, Function>} handlers - Map of method names to handler functions.
|
|
106
|
+
* @param {object} [opts] - Service options.
|
|
107
|
+
* @param {Function[]} [opts.interceptors] - Per-service interceptors.
|
|
108
|
+
* @param {number} [opts.maxMessageSize] - Max incoming message size in bytes.
|
|
109
|
+
* @param {boolean} [opts.compress=false] - Whether to compress outgoing messages.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* registry.addService(schema, 'Greeter', {
|
|
113
|
+
* SayHello(call) { return { message: 'Hello ' + call.request.name }; },
|
|
114
|
+
* });
|
|
115
|
+
*/
|
|
116
|
+
addService(schema, serviceName, handlers, opts = {})
|
|
117
|
+
{
|
|
118
|
+
const service = schema.services[serviceName];
|
|
119
|
+
if (!service)
|
|
120
|
+
{
|
|
121
|
+
throw new Error(`Service "${serviceName}" not found in proto schema. ` +
|
|
122
|
+
`Available: ${Object.keys(schema.services).join(', ') || 'none'}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Build the package prefix for routing
|
|
126
|
+
const packagePrefix = schema.package ? schema.package + '.' : '';
|
|
127
|
+
const pathPrefix = '/' + packagePrefix + serviceName;
|
|
128
|
+
|
|
129
|
+
for (const [methodName, methodDef] of Object.entries(service.methods))
|
|
130
|
+
{
|
|
131
|
+
if (!handlers[methodName])
|
|
132
|
+
{
|
|
133
|
+
log.warn('no handler for %s/%s — will return UNIMPLEMENTED', serviceName, methodName);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const routePath = pathPrefix + '/' + methodName;
|
|
137
|
+
this._routes.set(routePath, {
|
|
138
|
+
service: serviceName,
|
|
139
|
+
method: methodDef,
|
|
140
|
+
handler: handlers[methodName] || null,
|
|
141
|
+
schema,
|
|
142
|
+
opts,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
log.info('registered gRPC method %s [%s]', routePath,
|
|
146
|
+
_callType(methodDef));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Add a global interceptor that runs before every gRPC call.
|
|
152
|
+
* Interceptors receive `(call, next)` and must call `next()` to continue.
|
|
153
|
+
*
|
|
154
|
+
* @param {Function} fn - Interceptor function `(call, next) => void`.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* registry.addInterceptor(async (call, next) => {
|
|
158
|
+
* const token = call.metadata.get('authorization');
|
|
159
|
+
* if (!token) return call.sendError(GrpcStatus.UNAUTHENTICATED, 'Missing auth');
|
|
160
|
+
* await next();
|
|
161
|
+
* });
|
|
162
|
+
*/
|
|
163
|
+
addInterceptor(fn)
|
|
164
|
+
{
|
|
165
|
+
this._interceptors.push(fn);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Handle an incoming HTTP/2 stream. Determines if it's a gRPC call,
|
|
170
|
+
* routes to the correct handler, and manages the call lifecycle.
|
|
171
|
+
*
|
|
172
|
+
* @param {import('http2').Http2Stream} stream - The HTTP/2 stream.
|
|
173
|
+
* @param {object} headers - HTTP/2 headers from the stream event.
|
|
174
|
+
* @returns {boolean} `true` if this was handled as a gRPC call.
|
|
175
|
+
*/
|
|
176
|
+
handleStream(stream, headers)
|
|
177
|
+
{
|
|
178
|
+
const contentType = headers['content-type'] || '';
|
|
179
|
+
if (!contentType.startsWith('application/grpc'))
|
|
180
|
+
{
|
|
181
|
+
return false; // Not a gRPC request — let the normal HTTP pipeline handle it
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const grpcPath = headers[':path'];
|
|
185
|
+
const method = headers[':method'];
|
|
186
|
+
|
|
187
|
+
// gRPC always uses POST
|
|
188
|
+
if (method !== 'POST')
|
|
189
|
+
{
|
|
190
|
+
_sendError(stream, GrpcStatus.UNIMPLEMENTED,
|
|
191
|
+
'gRPC requires POST method');
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Reject new calls during shutdown drain
|
|
196
|
+
if (this._draining)
|
|
197
|
+
{
|
|
198
|
+
_sendError(stream, GrpcStatus.UNAVAILABLE,
|
|
199
|
+
'Server is shutting down');
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Look up the route
|
|
204
|
+
const route = this._routes.get(grpcPath);
|
|
205
|
+
if (!route)
|
|
206
|
+
{
|
|
207
|
+
log.warn('unregistered gRPC path: %s', grpcPath);
|
|
208
|
+
_sendError(stream, GrpcStatus.UNIMPLEMENTED,
|
|
209
|
+
`Method not found: ${grpcPath}`);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!route.handler)
|
|
214
|
+
{
|
|
215
|
+
_sendError(stream, GrpcStatus.UNIMPLEMENTED,
|
|
216
|
+
`Method not implemented: ${grpcPath}`);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Dispatch the call
|
|
221
|
+
this._dispatch(stream, headers, route)
|
|
222
|
+
.catch((err) =>
|
|
223
|
+
{
|
|
224
|
+
log.error('unhandled error in gRPC handler %s: %s', grpcPath, err.message);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Dispatch a gRPC call to the appropriate handler.
|
|
232
|
+
* @private
|
|
233
|
+
* @param {import('http2').Http2Stream} stream
|
|
234
|
+
* @param {object} headers
|
|
235
|
+
* @param {object} route
|
|
236
|
+
*/
|
|
237
|
+
async _dispatch(stream, headers, route)
|
|
238
|
+
{
|
|
239
|
+
const { method: methodDef, handler, schema, opts } = route;
|
|
240
|
+
|
|
241
|
+
// Parse metadata from headers
|
|
242
|
+
const metadata = Metadata.fromHeaders(headers);
|
|
243
|
+
|
|
244
|
+
// Create the appropriate call object
|
|
245
|
+
const CallClass = _pickCallClass(methodDef);
|
|
246
|
+
const call = new CallClass(stream, methodDef, schema.messages, metadata, {
|
|
247
|
+
maxMessageSize: opts.maxMessageSize,
|
|
248
|
+
compress: opts.compress,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Track for graceful shutdown
|
|
252
|
+
this._activeCalls.add(call);
|
|
253
|
+
stream.on('close', () => this._activeCalls.delete(call));
|
|
254
|
+
|
|
255
|
+
try
|
|
256
|
+
{
|
|
257
|
+
// Initialize the call (collect request body / set up streaming)
|
|
258
|
+
await call._init();
|
|
259
|
+
|
|
260
|
+
// Run interceptors + handler
|
|
261
|
+
const interceptors = [
|
|
262
|
+
...this._interceptors,
|
|
263
|
+
...(opts.interceptors || []),
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
await _runInterceptors(interceptors, call, async () =>
|
|
267
|
+
{
|
|
268
|
+
if (call.cancelled) return;
|
|
269
|
+
|
|
270
|
+
const result = await handler(call);
|
|
271
|
+
|
|
272
|
+
// For unary and client-streaming: if the handler returned
|
|
273
|
+
// a value, send it as the response automatically
|
|
274
|
+
if (result !== undefined && result !== null && !call._ended)
|
|
275
|
+
{
|
|
276
|
+
call.write(result);
|
|
277
|
+
call.sendStatus(GrpcStatus.OK);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (err)
|
|
282
|
+
{
|
|
283
|
+
log.error('gRPC handler error in %s: %s', methodDef.name, err.message);
|
|
284
|
+
if (!call._ended)
|
|
285
|
+
{
|
|
286
|
+
const code = err.grpcCode || GrpcStatus.INTERNAL;
|
|
287
|
+
call.sendError(code, err.message);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Begin draining — reject new calls and wait for active calls to finish.
|
|
294
|
+
*
|
|
295
|
+
* @param {number} [timeout=30000] - Maximum time to wait in ms.
|
|
296
|
+
* @returns {Promise<void>}
|
|
297
|
+
*/
|
|
298
|
+
async drain(timeout = 30000)
|
|
299
|
+
{
|
|
300
|
+
this._draining = true;
|
|
301
|
+
log.info('gRPC draining, %d active calls', this._activeCalls.size);
|
|
302
|
+
|
|
303
|
+
if (this._activeCalls.size === 0) return;
|
|
304
|
+
|
|
305
|
+
return new Promise((resolve) =>
|
|
306
|
+
{
|
|
307
|
+
const check = () =>
|
|
308
|
+
{
|
|
309
|
+
if (this._activeCalls.size === 0)
|
|
310
|
+
{
|
|
311
|
+
clearTimeout(timer);
|
|
312
|
+
resolve();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Check periodically
|
|
317
|
+
const interval = setInterval(check, 100);
|
|
318
|
+
if (interval.unref) interval.unref();
|
|
319
|
+
|
|
320
|
+
const timer = setTimeout(() =>
|
|
321
|
+
{
|
|
322
|
+
clearInterval(interval);
|
|
323
|
+
log.warn('gRPC drain timed out with %d active calls', this._activeCalls.size);
|
|
324
|
+
// Force-close remaining calls
|
|
325
|
+
for (const call of this._activeCalls)
|
|
326
|
+
{
|
|
327
|
+
call.sendError(GrpcStatus.UNAVAILABLE, 'Server shutting down');
|
|
328
|
+
}
|
|
329
|
+
resolve();
|
|
330
|
+
}, timeout);
|
|
331
|
+
if (timer.unref) timer.unref();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get all registered routes for introspection.
|
|
337
|
+
*
|
|
338
|
+
* @returns {{ method: string, path: string, type: string }[]}
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* registry.routes();
|
|
342
|
+
* // [{ method: 'GRPC', path: '/myapp.Greeter/SayHello', type: 'unary' }]
|
|
343
|
+
*/
|
|
344
|
+
routes()
|
|
345
|
+
{
|
|
346
|
+
const list = [];
|
|
347
|
+
for (const [path, route] of this._routes)
|
|
348
|
+
{
|
|
349
|
+
list.push({
|
|
350
|
+
method: 'GRPC',
|
|
351
|
+
path,
|
|
352
|
+
type: _callType(route.method),
|
|
353
|
+
implemented: !!route.handler,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return list;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// -- Helpers -----------------------------------------------
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Pick the Call class based on method streaming flags.
|
|
364
|
+
* @private
|
|
365
|
+
* @param {object} methodDef
|
|
366
|
+
* @returns {typeof import('./call').BaseCall}
|
|
367
|
+
*/
|
|
368
|
+
function _pickCallClass(methodDef)
|
|
369
|
+
{
|
|
370
|
+
if (methodDef.clientStreaming && methodDef.serverStreaming) return BidiStreamCall;
|
|
371
|
+
if (methodDef.clientStreaming) return ClientStreamCall;
|
|
372
|
+
if (methodDef.serverStreaming) return ServerStreamCall;
|
|
373
|
+
return UnaryCall;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Describe the call type for logging/introspection.
|
|
378
|
+
* @private
|
|
379
|
+
* @param {object} methodDef
|
|
380
|
+
* @returns {string}
|
|
381
|
+
*/
|
|
382
|
+
function _callType(methodDef)
|
|
383
|
+
{
|
|
384
|
+
if (methodDef.clientStreaming && methodDef.serverStreaming) return 'bidi';
|
|
385
|
+
if (methodDef.clientStreaming) return 'client-stream';
|
|
386
|
+
if (methodDef.serverStreaming) return 'server-stream';
|
|
387
|
+
return 'unary';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Run a chain of interceptors with final handler.
|
|
392
|
+
* @private
|
|
393
|
+
* @param {Function[]} interceptors
|
|
394
|
+
* @param {import('./call').BaseCall} call
|
|
395
|
+
* @param {Function} finalHandler
|
|
396
|
+
*/
|
|
397
|
+
async function _runInterceptors(interceptors, call, finalHandler)
|
|
398
|
+
{
|
|
399
|
+
let idx = 0;
|
|
400
|
+
|
|
401
|
+
async function next()
|
|
402
|
+
{
|
|
403
|
+
if (call._ended || call._cancelled) return;
|
|
404
|
+
if (idx < interceptors.length)
|
|
405
|
+
{
|
|
406
|
+
const fn = interceptors[idx++];
|
|
407
|
+
await fn(call, next);
|
|
408
|
+
}
|
|
409
|
+
else
|
|
410
|
+
{
|
|
411
|
+
await finalHandler();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
await next();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Send a gRPC error on a raw HTTP/2 stream (before a Call object is created).
|
|
420
|
+
* @private
|
|
421
|
+
* @param {import('http2').Http2Stream} stream
|
|
422
|
+
* @param {number} code
|
|
423
|
+
* @param {string} message
|
|
424
|
+
*/
|
|
425
|
+
function _sendError(stream, code, message)
|
|
426
|
+
{
|
|
427
|
+
try
|
|
428
|
+
{
|
|
429
|
+
stream.respond({
|
|
430
|
+
':status': 200,
|
|
431
|
+
'content-type': 'application/grpc+proto',
|
|
432
|
+
'grpc-status': String(code),
|
|
433
|
+
'grpc-message': encodeURIComponent(message),
|
|
434
|
+
}, { endStream: true });
|
|
435
|
+
}
|
|
436
|
+
catch (_)
|
|
437
|
+
{
|
|
438
|
+
try { stream.close(); }
|
|
439
|
+
catch (__) { /* stream already closed */ }
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
module.exports = {
|
|
444
|
+
GrpcServiceRegistry,
|
|
445
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module grpc/status
|
|
3
|
+
* @description Standard gRPC status codes (as defined by the gRPC specification).
|
|
4
|
+
* Each code has a numeric value, a name, and a human-readable description.
|
|
5
|
+
* Used by both server and client to communicate call outcomes via trailers.
|
|
6
|
+
*
|
|
7
|
+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const log = require('../debug')('zero:grpc');
|
|
11
|
+
|
|
12
|
+
// -- Status Codes ------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* gRPC status code enum. Mirrors the canonical codes from the gRPC spec.
|
|
16
|
+
*
|
|
17
|
+
* @enum {number}
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const { GrpcStatus } = require('@zero-server/sdk');
|
|
21
|
+
* call.sendError(GrpcStatus.NOT_FOUND, 'User not found');
|
|
22
|
+
*/
|
|
23
|
+
const GrpcStatus = {
|
|
24
|
+
/** The operation completed successfully. */
|
|
25
|
+
OK: 0,
|
|
26
|
+
/** The operation was cancelled (typically by the caller). */
|
|
27
|
+
CANCELLED: 1,
|
|
28
|
+
/** Unknown error — a catch-all for unexpected failures. */
|
|
29
|
+
UNKNOWN: 2,
|
|
30
|
+
/** The client specified an invalid argument. */
|
|
31
|
+
INVALID_ARGUMENT: 3,
|
|
32
|
+
/** The deadline expired before the operation could complete. */
|
|
33
|
+
DEADLINE_EXCEEDED: 4,
|
|
34
|
+
/** The requested entity was not found. */
|
|
35
|
+
NOT_FOUND: 5,
|
|
36
|
+
/** The entity that a client attempted to create already exists. */
|
|
37
|
+
ALREADY_EXISTS: 6,
|
|
38
|
+
/** The caller does not have permission to execute the operation. */
|
|
39
|
+
PERMISSION_DENIED: 7,
|
|
40
|
+
/** Some resource has been exhausted (e.g. quota, disk space). */
|
|
41
|
+
RESOURCE_EXHAUSTED: 8,
|
|
42
|
+
/** The operation was rejected because the system is not in a required state. */
|
|
43
|
+
FAILED_PRECONDITION: 9,
|
|
44
|
+
/** The operation was aborted, typically due to a concurrency conflict. */
|
|
45
|
+
ABORTED: 10,
|
|
46
|
+
/** The operation was attempted past the valid range. */
|
|
47
|
+
OUT_OF_RANGE: 11,
|
|
48
|
+
/** The operation is not implemented or not supported. */
|
|
49
|
+
UNIMPLEMENTED: 12,
|
|
50
|
+
/** Internal error — invariants expected by the server have been broken. */
|
|
51
|
+
INTERNAL: 13,
|
|
52
|
+
/** The service is currently unavailable, usually a transient condition. */
|
|
53
|
+
UNAVAILABLE: 14,
|
|
54
|
+
/** Unrecoverable data loss or corruption. */
|
|
55
|
+
DATA_LOSS: 15,
|
|
56
|
+
/** The request does not have valid authentication credentials. */
|
|
57
|
+
UNAUTHENTICATED: 16,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reverse lookup: number → string name.
|
|
62
|
+
* @type {Object<number, string>}
|
|
63
|
+
*/
|
|
64
|
+
const STATUS_NAMES = {};
|
|
65
|
+
for (const [name, code] of Object.entries(GrpcStatus))
|
|
66
|
+
{
|
|
67
|
+
STATUS_NAMES[code] = name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Map gRPC status code to the appropriate HTTP/2 status code for trailers-only responses.
|
|
72
|
+
*
|
|
73
|
+
* @param {number} grpcCode - gRPC status code.
|
|
74
|
+
* @returns {number} Corresponding HTTP status code.
|
|
75
|
+
*/
|
|
76
|
+
function grpcToHttp(grpcCode)
|
|
77
|
+
{
|
|
78
|
+
switch (grpcCode)
|
|
79
|
+
{
|
|
80
|
+
case GrpcStatus.OK: return 200;
|
|
81
|
+
case GrpcStatus.INVALID_ARGUMENT: return 400;
|
|
82
|
+
case GrpcStatus.FAILED_PRECONDITION: return 400;
|
|
83
|
+
case GrpcStatus.OUT_OF_RANGE: return 400;
|
|
84
|
+
case GrpcStatus.UNAUTHENTICATED: return 401;
|
|
85
|
+
case GrpcStatus.PERMISSION_DENIED: return 403;
|
|
86
|
+
case GrpcStatus.NOT_FOUND: return 404;
|
|
87
|
+
case GrpcStatus.ALREADY_EXISTS: return 409;
|
|
88
|
+
case GrpcStatus.ABORTED: return 409;
|
|
89
|
+
case GrpcStatus.RESOURCE_EXHAUSTED: return 429;
|
|
90
|
+
case GrpcStatus.CANCELLED: return 499;
|
|
91
|
+
case GrpcStatus.UNIMPLEMENTED: return 501;
|
|
92
|
+
case GrpcStatus.UNAVAILABLE: return 503;
|
|
93
|
+
case GrpcStatus.DEADLINE_EXCEEDED: return 504;
|
|
94
|
+
case GrpcStatus.UNKNOWN:
|
|
95
|
+
case GrpcStatus.INTERNAL:
|
|
96
|
+
case GrpcStatus.DATA_LOSS:
|
|
97
|
+
default:
|
|
98
|
+
return 500;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the human-readable name for a gRPC status code.
|
|
104
|
+
*
|
|
105
|
+
* @param {number} code - gRPC status code.
|
|
106
|
+
* @returns {string} Status name or 'UNKNOWN'.
|
|
107
|
+
*/
|
|
108
|
+
function statusName(code)
|
|
109
|
+
{
|
|
110
|
+
return STATUS_NAMES[code] || 'UNKNOWN';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
GrpcStatus,
|
|
115
|
+
STATUS_NAMES,
|
|
116
|
+
grpcToHttp,
|
|
117
|
+
statusName,
|
|
118
|
+
};
|