@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,173 @@
1
+ /**
2
+ * @module grpc/watch
3
+ * @description Proto file hot-reload for development.
4
+ * Watches `.proto` files for changes using `fs.watch()` and
5
+ * re-parses/re-registers gRPC services automatically.
6
+ *
7
+ * **Dev-only** — disabled by default when `NODE_ENV=production`.
8
+ *
9
+ * @example
10
+ * const { createApp, watchProto } = require('@zero-server/sdk');
11
+ * const app = createApp();
12
+ *
13
+ * watchProto(app, './protos/greeter.proto', 'Greeter', handlers, {
14
+ * onReload: (schema) => console.log('Reloaded!'),
15
+ * });
16
+ *
17
+ * app.listen(50051, { http2: true });
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const log = require('../debug')('zero:grpc:watch');
23
+
24
+ // Lazy-load proto parser
25
+ let _parseProtoFile = null;
26
+ function _getParser()
27
+ {
28
+ if (!_parseProtoFile) _parseProtoFile = require('./proto').parseProtoFile;
29
+ return _parseProtoFile;
30
+ }
31
+
32
+ // -- Constants ---------------------------------------------------
33
+
34
+ const DEFAULT_DEBOUNCE = 300; // ms
35
+
36
+ // -- watchProto --------------------------------------------------
37
+
38
+ /**
39
+ * Watch a `.proto` file and hot-reload the gRPC service on changes.
40
+ * Parses the file, registers the service, then watches for modifications.
41
+ *
42
+ * Disabled in production unless `opts.production` is `true`.
43
+ *
44
+ * @param {object} app - The zero-server App instance.
45
+ * @param {string} protoPath - Path to the `.proto` file.
46
+ * @param {string} serviceName - Service name to register.
47
+ * @param {Object<string, Function>} handlers - Method handlers map.
48
+ * @param {object} [opts] - Options.
49
+ * @param {Function[]} [opts.interceptors] - Per-service interceptors.
50
+ * @param {number} [opts.maxMessageSize] - Max incoming message size.
51
+ * @param {boolean} [opts.compress=false] - Compress outgoing messages.
52
+ * @param {number} [opts.debounce=300] - Debounce interval in ms.
53
+ * @param {boolean} [opts.production=false] - Allow in production.
54
+ * @param {Function} [opts.onReload] - `(schema) => void` callback after reload.
55
+ * @param {Function} [opts.onError] - `(err) => void` callback on parse/reload error.
56
+ * @returns {{ stop: () => void, schema: object }} Controller with `stop()` and current schema.
57
+ *
58
+ * @example
59
+ * const watcher = watchProto(app, './hello.proto', 'Greeter', {
60
+ * SayHello(call) { return { message: 'Hello ' + call.request.name }; },
61
+ * });
62
+ * // Later: watcher.stop();
63
+ */
64
+ function watchProto(app, protoPath, serviceName, handlers, opts = {})
65
+ {
66
+ if (!opts.production && process.env.NODE_ENV === 'production')
67
+ {
68
+ log.warn('watchProto disabled in production (set { production: true } to override)');
69
+ // Still do an initial load
70
+ const parseProtoFile = _getParser();
71
+ const schema = parseProtoFile(protoPath);
72
+ app.grpc(schema, serviceName, handlers, opts);
73
+ return { stop() {}, schema };
74
+ }
75
+
76
+ const debounceMs = opts.debounce || DEFAULT_DEBOUNCE;
77
+ const resolvedPath = path.resolve(protoPath);
78
+
79
+ const parseProtoFile = _getParser();
80
+ let currentSchema = null;
81
+
82
+ // Initial load
83
+ try
84
+ {
85
+ currentSchema = parseProtoFile(resolvedPath);
86
+ app.grpc(currentSchema, serviceName, handlers, opts);
87
+ log.info('proto loaded: %s → %s', resolvedPath, serviceName);
88
+ }
89
+ catch (err)
90
+ {
91
+ log.error('initial proto parse failed: %s', err.message);
92
+ if (typeof opts.onError === 'function') opts.onError(err);
93
+ else throw err;
94
+ }
95
+
96
+ // Debounced reload
97
+ let debounceTimer = null;
98
+
99
+ function _reload()
100
+ {
101
+ try
102
+ {
103
+ const schema = parseProtoFile(resolvedPath);
104
+
105
+ // Verify the service still exists in the schema
106
+ if (!schema.services[serviceName])
107
+ {
108
+ const err = new Error(`Service "${serviceName}" not found after reload`);
109
+ log.error(err.message);
110
+ if (typeof opts.onError === 'function') opts.onError(err);
111
+ return;
112
+ }
113
+
114
+ // Re-register: the grpc registry replaces the existing service entry
115
+ if (app._grpcRegistry)
116
+ {
117
+ app._grpcRegistry.addService(schema, serviceName, handlers, opts);
118
+ }
119
+
120
+ currentSchema = schema;
121
+ log.info('proto reloaded: %s', resolvedPath);
122
+
123
+ if (typeof opts.onReload === 'function') opts.onReload(schema);
124
+ }
125
+ catch (err)
126
+ {
127
+ log.error('proto reload failed: %s', err.message);
128
+ if (typeof opts.onError === 'function') opts.onError(err);
129
+ }
130
+ }
131
+
132
+ // Watch the file
133
+ let watcher;
134
+ try
135
+ {
136
+ watcher = fs.watch(resolvedPath, (eventType) =>
137
+ {
138
+ if (eventType !== 'change') return;
139
+
140
+ if (debounceTimer) clearTimeout(debounceTimer);
141
+ debounceTimer = setTimeout(_reload, debounceMs);
142
+ });
143
+
144
+ watcher.on('error', (err) =>
145
+ {
146
+ log.error('fs.watch error: %s', err.message);
147
+ if (typeof opts.onError === 'function') opts.onError(err);
148
+ });
149
+
150
+ log.info('watching proto file: %s', resolvedPath);
151
+ }
152
+ catch (err)
153
+ {
154
+ log.error('failed to watch proto file: %s', err.message);
155
+ if (typeof opts.onError === 'function') opts.onError(err);
156
+ }
157
+
158
+ return {
159
+ stop()
160
+ {
161
+ if (debounceTimer) clearTimeout(debounceTimer);
162
+ if (watcher) watcher.close();
163
+ log.info('stopped watching: %s', resolvedPath);
164
+ },
165
+ get schema() { return currentSchema; },
166
+ };
167
+ }
168
+
169
+ // -- Exports -------------------------------------------------
170
+
171
+ module.exports = {
172
+ watchProto,
173
+ };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@zero-server/grpc",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "gRPC server, client, codec, framing, status, metadata, health, reflection, balancer.",
5
5
  "keywords": [
6
6
  "zero-server",
7
- "zero-http",
7
+ "zero-server",
8
8
  "grpc"
9
9
  ],
10
10
  "author": "Anthony Wiedman",
@@ -20,6 +20,7 @@
20
20
  "./package.json": "./package.json"
21
21
  },
22
22
  "files": [
23
+ "lib",
23
24
  "index.js",
24
25
  "index.d.ts",
25
26
  "README.md",
@@ -42,7 +43,12 @@
42
43
  "access": "public"
43
44
  },
44
45
  "sideEffects": false,
45
- "dependencies": {
46
- "@zero-server/sdk": "0.9.0"
46
+ "peerDependencies": {
47
+ "@zero-server/sdk": ">=0.9.2"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@zero-server/sdk": {
51
+ "optional": true
52
+ }
47
53
  }
48
54
  }