@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.
@@ -0,0 +1,287 @@
1
+ /**
2
+ * @module grpc/health
3
+ * @description gRPC Health Checking Protocol implementation (grpc.health.v1.Health).
4
+ * Supports `Check` (unary) and `Watch` (server-stream) RPCs.
5
+ *
6
+ * Required for production deployments behind Kubernetes, Envoy,
7
+ * AWS ALB, and any load balancer that uses standard gRPC health probes.
8
+ *
9
+ * @see https://github.com/grpc/grpc/blob/master/doc/health-checking.md
10
+ *
11
+ * @example
12
+ * const { createApp } = require('@zero-server/sdk');
13
+ * const app = createApp();
14
+ * app.grpcHealth();
15
+ * app.listen(50051, { http2: true });
16
+ *
17
+ * @example | Per-service health status
18
+ * app.grpcHealth();
19
+ * app.setServiceStatus('myapp.UserService', 'SERVING');
20
+ * app.setServiceStatus('myapp.OrderService', 'NOT_SERVING');
21
+ */
22
+
23
+ const log = require('../debug')('zero:grpc:health');
24
+ const { GrpcStatus } = require('./status');
25
+ const { encode, decode } = require('./codec');
26
+ const { frameEncode, FrameParser } = require('./frame');
27
+ const { Metadata } = require('./metadata');
28
+
29
+ // -- Health Status Enum -----------------------------------
30
+
31
+ /**
32
+ * Health check status values.
33
+ * Mirrors `grpc.health.v1.HealthCheckResponse.ServingStatus`.
34
+ * @enum {number}
35
+ */
36
+ const ServingStatus = {
37
+ UNKNOWN: 0,
38
+ SERVING: 1,
39
+ NOT_SERVING: 2,
40
+ SERVICE_UNKNOWN: 3,
41
+ };
42
+
43
+ /** Reverse mapping for logging. */
44
+ const STATUS_NAME = {
45
+ 0: 'UNKNOWN',
46
+ 1: 'SERVING',
47
+ 2: 'NOT_SERVING',
48
+ 3: 'SERVICE_UNKNOWN',
49
+ };
50
+
51
+ // -- Health proto descriptors (hand-coded to avoid parsing) --
52
+
53
+ /** @private HealthCheckRequest message descriptor */
54
+ const _healthRequestDesc = {
55
+ name: 'HealthCheckRequest',
56
+ fields: [
57
+ { name: 'service', type: 'string', number: 1, repeated: false, optional: false, map: false },
58
+ ],
59
+ };
60
+
61
+ /** @private HealthCheckResponse message descriptor */
62
+ const _healthResponseDesc = {
63
+ name: 'HealthCheckResponse',
64
+ fields: [
65
+ { name: 'status', type: 'int32', number: 1, repeated: false, optional: false, map: false, enumType: 'ServingStatus' },
66
+ ],
67
+ };
68
+
69
+ /** @private Message type map for encode/decode */
70
+ const _healthMessages = {
71
+ HealthCheckRequest: _healthRequestDesc,
72
+ HealthCheckResponse: _healthResponseDesc,
73
+ };
74
+
75
+ // -- Health Service Manager --------------------------------
76
+
77
+ /**
78
+ * Manages per-service health status and Watch subscriptions.
79
+ * Cached serialized response bytes are invalidated only on status change.
80
+ *
81
+ * @class
82
+ */
83
+ class HealthService
84
+ {
85
+ constructor()
86
+ {
87
+ /**
88
+ * Current status per service name. Empty string = overall server health.
89
+ * @type {Map<string, number>}
90
+ */
91
+ this._statuses = new Map();
92
+
93
+ /**
94
+ * Watch subscribers per service name.
95
+ * Each subscriber is a function `(status) => void` that pushes to the client stream.
96
+ * @type {Map<string, Set<Function>>}
97
+ */
98
+ this._watchers = new Map();
99
+
100
+ /**
101
+ * Cached serialized response bytes per service name.
102
+ * Invalidated on status change for zero-allocation hot path.
103
+ * @type {Map<string, Buffer>}
104
+ */
105
+ this._cache = new Map();
106
+
107
+ // Overall server health defaults to SERVING
108
+ this._statuses.set('', ServingStatus.SERVING);
109
+ }
110
+
111
+ /**
112
+ * Set the health status for a service.
113
+ *
114
+ * @param {string} serviceName - Service name (empty string for overall).
115
+ * @param {number|string} status - Status value or name.
116
+ */
117
+ setStatus(serviceName, status)
118
+ {
119
+ const code = typeof status === 'string' ? ServingStatus[status] : status;
120
+ if (code === undefined || STATUS_NAME[code] === undefined)
121
+ throw new Error(`Invalid health status: ${status}`);
122
+
123
+ const prev = this._statuses.get(serviceName);
124
+ this._statuses.set(serviceName, code);
125
+
126
+ // Invalidate cached response
127
+ this._cache.delete(serviceName);
128
+
129
+ // Notify watch subscribers if status changed
130
+ if (prev !== code)
131
+ {
132
+ log.info('health status changed: "%s" → %s', serviceName || '<overall>', STATUS_NAME[code]);
133
+ const watchers = this._watchers.get(serviceName);
134
+ if (watchers)
135
+ {
136
+ for (const notify of watchers) notify(code);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Get the current status for a service.
143
+ *
144
+ * @param {string} serviceName - Service name.
145
+ * @returns {number} Status code, or SERVICE_UNKNOWN if not registered.
146
+ */
147
+ getStatus(serviceName)
148
+ {
149
+ if (this._statuses.has(serviceName))
150
+ return this._statuses.get(serviceName);
151
+ return ServingStatus.SERVICE_UNKNOWN;
152
+ }
153
+
154
+ /**
155
+ * Set all registered services to NOT_SERVING for graceful shutdown.
156
+ */
157
+ setAllNotServing()
158
+ {
159
+ for (const [name] of this._statuses)
160
+ {
161
+ this.setStatus(name, ServingStatus.NOT_SERVING);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Build and cache the serialized response for a given status.
167
+ * @private
168
+ * @param {number} status - Status code.
169
+ * @returns {Buffer}
170
+ */
171
+ _getResponseBytes(status)
172
+ {
173
+ return encode({ status }, _healthResponseDesc, _healthMessages);
174
+ }
175
+
176
+ /**
177
+ * Subscribe a watcher for status changes on a service.
178
+ * @param {string} serviceName
179
+ * @param {Function} callback - `(status: number) => void`
180
+ * @returns {Function} Unsubscribe function.
181
+ */
182
+ _watch(serviceName, callback)
183
+ {
184
+ if (!this._watchers.has(serviceName))
185
+ this._watchers.set(serviceName, new Set());
186
+ this._watchers.get(serviceName).add(callback);
187
+
188
+ return () =>
189
+ {
190
+ const set = this._watchers.get(serviceName);
191
+ if (set) { set.delete(callback); if (set.size === 0) this._watchers.delete(serviceName); }
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Handle a Check RPC — unary request for current health of a service.
197
+ * @param {import('./call').UnaryCall} call
198
+ */
199
+ Check(call)
200
+ {
201
+ const serviceName = call.request.service || '';
202
+ const status = this.getStatus(serviceName);
203
+ log.debug('health Check for "%s" → %s', serviceName || '<overall>', STATUS_NAME[status]);
204
+ return { status };
205
+ }
206
+
207
+ /**
208
+ * Handle a Watch RPC — server-stream that pushes status changes.
209
+ * Sends the current status immediately, then pushes on every change.
210
+ * @param {import('./call').ServerStreamCall} call
211
+ */
212
+ Watch(call)
213
+ {
214
+ const serviceName = call.request.service || '';
215
+ const currentStatus = this.getStatus(serviceName);
216
+
217
+ // Send current status immediately
218
+ call.write({ status: currentStatus });
219
+
220
+ // Subscribe to changes
221
+ const unsubscribe = this._watch(serviceName, (newStatus) =>
222
+ {
223
+ if (!call._ended && !call._cancelled)
224
+ {
225
+ call.write({ status: newStatus });
226
+ }
227
+ });
228
+
229
+ // Cleanup on stream close
230
+ call.stream.on('close', unsubscribe);
231
+ }
232
+
233
+ /**
234
+ * Get the schema object needed for server registration.
235
+ * Avoids requiring proto parsing — returns descriptors directly.
236
+ * @returns {object} Schema compatible with GrpcServiceRegistry.addService
237
+ */
238
+ getSchema()
239
+ {
240
+ return {
241
+ package: 'grpc.health.v1',
242
+ services: {
243
+ Health: {
244
+ methods: {
245
+ Check: {
246
+ name: 'Check',
247
+ inputType: 'HealthCheckRequest',
248
+ outputType: 'HealthCheckResponse',
249
+ clientStreaming: false,
250
+ serverStreaming: false,
251
+ },
252
+ Watch: {
253
+ name: 'Watch',
254
+ inputType: 'HealthCheckRequest',
255
+ outputType: 'HealthCheckResponse',
256
+ clientStreaming: false,
257
+ serverStreaming: true,
258
+ },
259
+ },
260
+ },
261
+ },
262
+ messages: _healthMessages,
263
+ enums: {
264
+ ServingStatus: { values: ServingStatus },
265
+ },
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Get the handler map for server registration.
271
+ * Binds methods to this instance.
272
+ * @returns {Object<string, Function>}
273
+ */
274
+ getHandlers()
275
+ {
276
+ return {
277
+ Check: (call) => this.Check(call),
278
+ Watch: (call) => this.Watch(call),
279
+ };
280
+ }
281
+ }
282
+
283
+ module.exports = {
284
+ HealthService,
285
+ ServingStatus,
286
+ STATUS_NAME,
287
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @module grpc
3
+ * @description Full gRPC support for zero-server — zero external dependencies.
4
+ * Provides a proto3 parser, protobuf codec, gRPC framing, call objects,
5
+ * a service server, and a client for all four RPC patterns.
6
+ *
7
+ * Features:
8
+ * - Proto3 schema parsing (messages, enums, services, imports)
9
+ * - Full protobuf binary encoding/decoding (all scalar types, nested, repeated, map, oneof, packed)
10
+ * - gRPC over HTTP/2 with length-prefixed framing and optional gzip compression
11
+ * - All four call types: unary, server-streaming, client-streaming, bidirectional
12
+ * - Server interceptors (middleware for gRPC calls)
13
+ * - Client with lazy connect, keep-alive, deadlines, and metadata
14
+ * - Graceful shutdown with call draining
15
+ * - Message size limits and deadline enforcement
16
+ *
17
+ * @example | Quick Start — Server
18
+ * const { createApp, parseProto } = require('@zero-server/sdk');
19
+ * const app = createApp();
20
+ * const schema = parseProto(`
21
+ * syntax = "proto3";
22
+ * package myapp;
23
+ * service Greeter {
24
+ * rpc SayHello (HelloRequest) returns (HelloReply);
25
+ * }
26
+ * message HelloRequest { string name = 1; }
27
+ * message HelloReply { string message = 1; }
28
+ * `);
29
+ *
30
+ * app.grpc(schema, 'Greeter', {
31
+ * SayHello(call) {
32
+ * return { message: 'Hello ' + call.request.name };
33
+ * },
34
+ * });
35
+ *
36
+ * app.listen(50051, { http2: true });
37
+ *
38
+ * @example | Quick Start — Client
39
+ * const { GrpcClient, parseProto } = require('@zero-server/sdk');
40
+ * const schema = parseProto(fs.readFileSync('hello.proto', 'utf8'));
41
+ * const client = new GrpcClient('http://localhost:50051', schema, 'Greeter');
42
+ * const reply = await client.call('SayHello', { name: 'World' });
43
+ * console.log(reply.message);
44
+ * client.close();
45
+ */
46
+
47
+ const { GrpcStatus, grpcToHttp, statusName, STATUS_NAMES } = require('./status');
48
+ const { Metadata } = require('./metadata');
49
+ const { Writer, Reader, encode, decode, WIRE_TYPE, TYPE_INFO } = require('./codec');
50
+ const { parseProto, parseProtoFile, tokenize } = require('./proto');
51
+ const { frameEncode, FrameParser, FRAME_HEADER_SIZE, MAX_FRAME_SIZE } = require('./frame');
52
+ const { BaseCall, UnaryCall, ServerStreamCall, ClientStreamCall, BidiStreamCall } = require('./call');
53
+ const { GrpcServiceRegistry } = require('./server');
54
+ const { GrpcClient } = require('./client');
55
+ const { HealthService, ServingStatus } = require('./health');
56
+ const { ReflectionService } = require('./reflection');
57
+ const { LoadBalancer, Subchannel, SubchannelState } = require('./balancer');
58
+ const { ChannelCredentials, createRotatingCredentials } = require('./credentials');
59
+ const { watchProto } = require('./watch');
60
+
61
+ module.exports = {
62
+ // Status codes
63
+ GrpcStatus,
64
+ grpcToHttp,
65
+ statusName,
66
+ STATUS_NAMES,
67
+
68
+ // Metadata
69
+ Metadata,
70
+
71
+ // Protobuf codec
72
+ Writer,
73
+ Reader,
74
+ encode,
75
+ decode,
76
+ WIRE_TYPE,
77
+ TYPE_INFO,
78
+
79
+ // Proto3 parser
80
+ parseProto,
81
+ parseProtoFile,
82
+ tokenize,
83
+
84
+ // gRPC framing
85
+ frameEncode,
86
+ FrameParser,
87
+ FRAME_HEADER_SIZE,
88
+ MAX_FRAME_SIZE,
89
+
90
+ // Call objects
91
+ BaseCall,
92
+ UnaryCall,
93
+ ServerStreamCall,
94
+ ClientStreamCall,
95
+ BidiStreamCall,
96
+
97
+ // Server
98
+ GrpcServiceRegistry,
99
+
100
+ // Client
101
+ GrpcClient,
102
+
103
+ // Health check
104
+ HealthService,
105
+ ServingStatus,
106
+
107
+ // Server reflection
108
+ ReflectionService,
109
+
110
+ // Load balancing
111
+ LoadBalancer,
112
+ Subchannel,
113
+ SubchannelState,
114
+
115
+ // Channel credentials
116
+ ChannelCredentials,
117
+ createRotatingCredentials,
118
+
119
+ // Proto hot-reload
120
+ watchProto,
121
+ };