geonix 1.23.6 → 1.30.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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Davor Tarandek <dtarandek@tria.hr>
3
+ Copyright (c) 2026 Davor Tarandek <dtarandek@tria.hr>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,11 +1,355 @@
1
1
  ## Geonix
2
2
 
3
- This is a compact yet robust library designed to facilitate Remote Procedure Call (RPC) communication patterns among various components in a system.
3
+ A Node.js microservices framework built on [NATS](https://nats.io/). Write self-announcing services as plain JavaScript classes with HTTP endpoints defined as specially-named methods. A gateway component automatically discovers services and routes HTTP/WebSocket traffic to them.
4
4
 
5
- At its core, it employs [NATS](https://nats.io/), a high-performance messaging system, as the primary medium for both communication and service discovery.
5
+ ## Install
6
6
 
7
- This library adeptly handles the intricacies of RPC interactions, ensuring seamless and efficient communication between distributed services. It is especially tailored for scenarios requiring reliable and fast message exchanges, making it ideal for microservices architectures where components need to communicate and discover each other's services effectively and efficiently.
7
+ ```sh
8
+ npm install geonix
9
+ ```
10
+
11
+ Requires a running NATS server. Default: `nats://localhost`.
12
+
13
+ ---
14
+
15
+ ## Quick Start
16
+
17
+ ### Service
18
+
19
+ ```js
20
+ import { Service } from "geonix";
21
+
22
+ class GreetingService extends Service {
23
+ "GET /hello"(req, res) {
24
+ res.json({ message: `Hello, ${req.query.name || "World"}` });
25
+ }
26
+
27
+ greet(name) {
28
+ return `Hello, ${name}`;
29
+ }
30
+ }
31
+
32
+ GreetingService.start();
33
+ ```
34
+
35
+ ### Gateway
36
+
37
+ ```js
38
+ import { Gateway } from "geonix";
39
+
40
+ Gateway.start();
41
+ ```
42
+
43
+ That's it. The gateway discovers all running services via NATS beacons and proxies HTTP traffic to them. No configuration required for basic use.
44
+
45
+ ---
46
+
47
+ ## Architecture
48
+
49
+ ```
50
+ Client (HTTP/WS)
51
+
52
+ [Gateway] ── discovers services via NATS beacons
53
+
54
+ ┌────┴────┐
55
+ │ Service │ ── embedded Express HTTP server
56
+ │ Service │ ── announces on NATS gx2.beacon every 3.5s
57
+ │ Service │
58
+ └─────────┘
59
+
60
+ [NATS] ── message bus for beacons, RPC, streaming
61
+ ```
62
+
63
+ ### Components
64
+
65
+ | File | Role |
66
+ |------|------|
67
+ | `Service` | Base class for all services. Manages HTTP endpoints, NATS RPC listeners, beacons. |
68
+ | `Gateway` | HTTP reverse proxy. Subscribes to beacons, builds a dynamic Express router. |
69
+ | `Connection` | Singleton NATS connection wrapper. Supports multiple connections with round-robin. |
70
+ | `Registry` | Maintains a live map of available services from beacon messages. Entries expire after 5s. |
71
+ | `Request` | Sends RPC calls to services over NATS. Waits up to 5 minutes for service to appear. |
72
+ | `Remote` | Proxy-based sugar: `Remote("ServiceName").methodName(args)` → RPC call. |
73
+ | `Stream` | Wraps large payloads as streamable objects. Can stream over HTTP or NATS. |
74
+
75
+ ---
76
+
77
+ ## HTTP Endpoints
78
+
79
+ Methods on a Service class are matched against:
80
+
81
+ ```
82
+ [options|]VERB /path
83
+ ```
84
+
85
+ | Example | Meaning |
86
+ |---------|---------|
87
+ | `"GET /api/users"` | HTTP GET endpoint |
88
+ | `"POST /upload"` | HTTP POST endpoint |
89
+ | `"WS /chat"` | WebSocket endpoint |
90
+ | `"SUB my.nats.subject"` | NATS subscription handler |
91
+ | `"order=10|GET /api"` | HTTP GET with routing priority 10 |
92
+
93
+ Multiple endpoints with the same verb+path and the same semver version are automatically load-balanced (round-robin for HTTP, IP-based affinity for WebSocket).
94
+
95
+ ---
96
+
97
+ ## RPC Calls
98
+
99
+ Call a method on any running service by name:
100
+
101
+ ```js
102
+ import { Request } from "geonix";
103
+
104
+ // Call a method
105
+ const result = await Request("GreetingService", "greet", ["Alice"]);
106
+
107
+ // Target a specific version
108
+ const result = await Request("GreetingService@1.2.x", "greet", ["Alice"]);
109
+ ```
110
+
111
+ Or use the proxy sugar:
112
+
113
+ ```js
114
+ import { Remote } from "geonix";
115
+
116
+ const greeting = Remote("GreetingService");
117
+ const result = await greeting.greet("Alice");
118
+ ```
119
+
120
+ RPC calls use NATS queue groups — multiple instances of the same service share load automatically.
121
+
122
+ ### RequestOptions
123
+
124
+ Pass `RequestOptions(...)` as the first argument to override per-call options:
125
+
126
+ ```js
127
+ import { Remote, RequestOptions } from "geonix";
128
+
129
+ const slow = Remote("HeavyService");
130
+ const result = await slow.compute(RequestOptions({ timeout: 60000 }), payload);
131
+ ```
132
+
133
+ | Option | Description | Default |
134
+ |--------|-------------|---------|
135
+ | `timeout` | Max wait for service to appear in registry (ms) | 300 000 |
136
+ | `httpTimeout` | Max wait for HTTP RPC response (ms) | 5 000 |
137
+
138
+ ---
139
+
140
+ ## Context Injection
141
+
142
+ Prefix the first parameter with `$` to receive a context object containing caller info:
143
+
144
+ ```js
145
+ class AuthService extends Service {
146
+ whoAmI($ctx) {
147
+ return { caller: $ctx.caller, me: $ctx.me };
148
+ }
149
+ }
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Method Visibility
155
+
156
+ | Prefix | Beacon | RPC callable | HTTP |
157
+ |--------|--------|--------------|------|
158
+ | (none) | yes | yes | yes (if endpoint pattern) |
159
+ | `$` | no | yes (internal use) | no |
160
+ | `$$` | no | no | no |
161
+
162
+ `$` methods are framework-reserved built-ins (e.g. `$getServiceInfo`). They are not advertised in beacons but can be called over NATS. `$$` methods are fully private — any remote call attempting to invoke a `$$` method is rejected with an error.
163
+
164
+ ---
165
+
166
+ ## Gateway Options
167
+
168
+ ```js
169
+ Gateway.start({
170
+ // CORS — four modes:
171
+ cors: undefined, // off (default) — no CORS headers
172
+ cors: "*", // wildcard — all origins, no credentials
173
+ cors: ["https://app.com"], // allowlist — credentials for listed origins only
174
+ cors: true, // open — reflect any origin with credentials (for auth-header APIs)
175
+
176
+ // Lifecycle hooks
177
+ beforeRequest: (req, res) => {},
178
+ afterRequest: (req, res) => {},
179
+
180
+ // Per-route middleware control
181
+ // handlers: { before: [...], after: [...] }
182
+ });
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Streaming
188
+
189
+ Large payloads are automatically streamed when they exceed the NATS max payload size (~512 KB):
190
+
191
+ ```js
192
+ import { Stream, getReadable, streamToBuffer, streamToString, streamToJSON } from "geonix";
193
+
194
+ // Sender — return a Stream from a service method
195
+ return Stream(largeBuffer);
196
+
197
+ // Receiver — materialise the stream
198
+ const readable = await getReadable(result); // Node.js Readable
199
+ const buf = await streamToBuffer(result);
200
+ const text = await streamToString(result);
201
+ const obj = await streamToJSON(result);
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Service Start Options
207
+
208
+ ```js
209
+ MyService.start({
210
+ name: "MyService", // override class name
211
+ version: "1.2.3", // override VERSION env var
212
+ id: "custom-id", // stable instance ID
213
+
214
+ middleware: {
215
+ json: true, // parse JSON bodies (default: true)
216
+ raw: true, // parse raw binary bodies (default: true)
217
+ cookies: true // parse cookies (default: true)
218
+ },
219
+
220
+ fullBeacon: true, // broadcast full service info (default: true)
221
+ // set false to broadcast instance ID only (heartbeat)
222
+
223
+ handlers: {
224
+ before: [myAuthMiddleware], // prepend to all HTTP handlers
225
+ after: [myLoggingMiddleware] // append to all HTTP handlers
226
+ // Note: for WS endpoints, `before` runs as route-scoped Express middleware
227
+ // before the WebSocket upgrade; `after` does not apply to WS or SUB endpoints.
228
+ }
229
+ });
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Built-in Service Methods
235
+
236
+ All services expose these internal methods (callable over NATS, not over HTTP):
237
+
238
+ | Method | Description |
239
+ |--------|-------------|
240
+ | `$createConnection(streamId)` | Opens a TCP tunnel for HTTP-over-NATS proxying |
241
+ | `$getEnv()` | Returns Node.js version, platform, arch, memory and CPU usage |
242
+ | `$getServiceInfo()` | Returns service metadata: name, version, methods, addresses |
243
+ | `$stop()` | Stops the service loop |
244
+
245
+ ---
246
+
247
+ ## Multipart form data
248
+
249
+ ```js
250
+ import { parseMultipart } from "geonix";
251
+
252
+ class UploadService extends Service {
253
+ "POST /upload"(req, res) {
254
+ const parts = await parseMultipart(req, {
255
+ useMemory: false, // stream parts to temp files (default)
256
+ // true = keep parts in memory
257
+ maxFiles: 10, // throw if more than N parts
258
+ maxFileSize: 10e6, // throw if any part exceeds N bytes
259
+ });
260
+
261
+ for (const part of parts) {
262
+ console.log(part.name, part.filename, part.headers);
263
+ // part.body is a Node.js Readable
264
+ }
265
+
266
+ res.send("ok");
267
+ }
268
+ }
269
+
270
+ UploadService.start({ middleware: { raw: false } });
271
+ // Disable the raw body middleware so the request stream reaches parseMultipart intact.
272
+ ```
273
+
274
+ Each returned part has:
275
+
276
+ | Property | Type | Description |
277
+ |----------|------|-------------|
278
+ | `name` | `string \| undefined` | Value of `name` in `Content-Disposition`, or `undefined` |
279
+ | `filename` | `string \| undefined` | Value of `filename` in `Content-Disposition`, or `undefined` |
280
+ | `headers` | `object` | Lowercased part headers |
281
+ | `body` | `Readable` | Part body as a readable stream |
282
+ | `size` | `number` | Bytes written so far (available during parsing) |
283
+
284
+ ---
285
+
286
+ ## Configuration
287
+
288
+ | Env Variable | Default | Description |
289
+ |---|---|---|
290
+ | `GX_TRANSPORT` | `nats://localhost` | NATS server URL(s) |
291
+ | `GX_PORT` | `8080` | Gateway listen port |
292
+ | `GX_LOCAL_PORT` | random | Force service HTTP server to a specific port |
293
+ | `GX_VERSION` | `999.999.<seconds>` | Service version |
294
+ | `GX_TRANSPORT_DEBUG` | — | Set to `true` to enable NATS protocol debug logging |
295
+ | `GX_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
296
+ | `GX_STREAM_TIMEOUT` | `90000` | Max wait for a stream consumer to connect (ms) |
297
+ | `GX_INACTIVITY_TIMEOUT` | `90000` | TCP proxy (HTTP-over-NATS) inactivity timeout (ms) |
298
+ | `GX_SECRET` | — | Encryption key: AES-256-GCM payloads + HMAC-SHA256 subjects. Services without the same key cannot communicate. |
299
+ | `GX_DEBUG_ENDPOINT` | — | Mount path for the debug router (e.g. `/_debug`). Disabled when unset. |
300
+
301
+ > **Deprecation notice:** The legacy unprefixed names `TRANSPORT`, `PORT`, `LOCAL_PORT`, `VERSION`, and `TRANSPORT_DEBUG` are still read as fallbacks but will be removed in the next major version. Migrate to the `GX_` prefixed names.
302
+
303
+ ### Bus Encryption (`GX_SECRET`)
304
+
305
+ When `GX_SECRET` is set, all NATS traffic is encrypted:
306
+
307
+ - **Payloads** — AES-256-GCM with a random 12-byte IV. Tampered messages are rejected.
308
+ - **Subjects** — each dot-separated segment is replaced with HMAC-SHA256(segment, key). `*` and `>` wildcards are preserved so NATS subscribe semantics are unchanged.
309
+
310
+ Services sharing the same `GX_SECRET` can communicate; services with a different or missing key cannot read subjects or payloads. This effectively creates a bus partition per environment.
311
+
312
+ ### Debug Router (`GX_DEBUG_ENDPOINT`)
313
+
314
+ When enabled, exposes diagnostic endpoints under the configured path:
315
+
316
+ | Path | Returns |
317
+ |------|---------|
318
+ | `/services` | List of all known services |
319
+ | `/endpoints` | All registered HTTP endpoints |
320
+ | `/registry` | Raw registry entries |
321
+ | `/router-registry` | Gateway's internal router state |
322
+ | `/stats` | Request/proxy counters |
323
+ | `/info` | Node.js version, platform, memory, CPU |
324
+
325
+ ---
326
+
327
+ ## Versioning
328
+
329
+ Services resolve their version in this priority order:
330
+
331
+ 1. `GX_VERSION` env var (or legacy `VERSION` / `version`)
332
+ 2. `options.version` passed to `Service.start()`
333
+ 3. `this.version` on the class instance
334
+ 4. Development default: `999.999.<seconds-since-midnight>`
335
+
336
+ The gateway routes requests to the highest semver version. Multiple versions of the same service can coexist. RPC calls can target a specific version range: `Request("MyService@1.2.x", "method", [])`.
337
+
338
+ ---
339
+
340
+ ## Dependencies
341
+
342
+ | Package | Purpose |
343
+ |---------|---------|
344
+ | `nats` | NATS client |
345
+ | `express` | HTTP server |
346
+ | `express-ws` | WebSocket support for Express |
347
+ | `cookie-parser` | Cookie parsing middleware |
348
+ | `semver` | Semantic versioning comparisons |
349
+ | `ws` | WebSocket client (for gateway WS proxy) |
350
+
351
+ ---
8
352
 
9
353
  ## License
10
354
 
11
- [MIT](LICENSE)
355
+ [MIT](LICENSE)
package/exports.js CHANGED
@@ -11,6 +11,4 @@ export { Request, Subscribe, Publish } from "./src/Request.js";
11
11
  export { RequestOptions } from "./src/RequestOptions.js";
12
12
  export { picoid as randomID } from "./src/Util.js";
13
13
 
14
- export { stats as StreamStats } from "./src/Stream.js";
15
-
16
14
  export { parseMultipart } from "./src/Util.js";