@onlineapps/conn-orch-registry 6.0.3 → 8.0.0

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/README.md CHANGED
@@ -25,11 +25,11 @@ Duty sections that apply:
25
25
 
26
26
  > A lightweight client for microservice registration and heartbeat over RabbitMQ.
27
27
  > Sends the full service specification (including `operations`) in the `register`
28
- > message — see [operations-registry-contract.md §2](../../../docs/biz/30-operations/registration-wire.md).
28
+ > message — see [registration-wire.md §2](../../../docs/biz/30-operations/registration-wire.md).
29
29
 
30
30
  ## 🚀 Features
31
31
 
32
- * Automatic queue management (`workflow`, `<serviceName>.registry`, `api_services_queuer`, `registry.register`)
32
+ * Automatic queue management (`workflow`, `<serviceName>.registry`, `<serviceName>.registry.<instanceId>`, `api_services_queuer`, `registry.register`)
33
33
  * Periodic heartbeat messages with metadata
34
34
  * Full specification (endpoints + operations) published inside `register`
35
35
  * Event-driven API using `EventEmitter`
@@ -56,22 +56,28 @@ yarn add @onlineapps/conn-orch-registry
56
56
  // NOTE: Do NOT call dotenv.config() in libraries. Export environment variables explicitly.
57
57
 
58
58
  const { ServiceRegistryClient, EVENTS } = require('@onlineapps/conn-orch-registry');
59
+ // requireEnv(name, description), requirePositiveIntegerEnv(name, description) — see examples/env.js
59
60
 
60
61
  const client = new ServiceRegistryClient({
61
- amqpUrl: process.env.AMQP_URL,
62
+ amqpUrl: requireEnv('RABBITMQ_URL', 'RabbitMQ URL, amqp://user:pass@host:port'),
62
63
  serviceName: 'invoicing',
63
64
  version: '1.0.0',
64
65
  specificationEndpoint: '/api/v1/specification', // required — see § Configuration
65
- heartbeatInterval: 30000, // required — see § Configuration
66
+ heartbeatInterval: requirePositiveIntegerEnv('BIZ_HEARTBEAT_INTERVAL_MS', 'milliseconds between heartbeats'), // required; see § Configuration
67
+ instanceId: requireEnv('SERVICE_INSTANCE_ID', 'id of THIS running copy, one value per process'), // required; see § Configuration
68
+ discoveryTimeoutMs: requirePositiveIntegerEnv('REGISTRY_DISCOVERY_TIMEOUT_MS', 'deadline in ms of one getService() read'), // required; see § Configuration
69
+ redis, // node-redis client getService() reads the projection with
66
70
  logger // required — see § Logger contract below
67
71
  });
68
72
 
69
73
  await client.init();
70
74
 
71
75
  // Publish full service spec in the register message (single round-trip).
76
+ // Only the keys of the registration contract are accepted — endpoints, operations,
77
+ // metadata, health, workspaceScoped, timeout; any other key is refused by name.
72
78
  await client.register({
73
79
  endpoints: [/* ... */],
74
- operations: await loadOperationsJson(), // see operations-registry-contract.md §3
80
+ operations: await loadOperationsJson(), // see registration-wire.md §3
75
81
  metadata: { /* ... */ },
76
82
  health: '/health'
77
83
  });
@@ -95,7 +101,7 @@ process.on('SIGINT', async () => {
95
101
 
96
102
  ## 📄 Configuration
97
103
 
98
- Configuration can be provided via environment variables or constructor options:
104
+ Configuration is passed as constructor options; the client reads no environment variable:
99
105
 
100
106
  | Option | Description | Default |
101
107
  | ----------------------- | --------------------------------------------- | -------------------- |
@@ -104,6 +110,8 @@ Configuration can be provided via environment variables or constructor options:
104
110
  | `version` | Service version in SemVer format (required) | — |
105
111
  | `specificationEndpoint` | Path the `register`/`heartbeat` messages carry (**required**) | — |
106
112
  | `heartbeatInterval` | Milliseconds between heartbeats (**required**) | — |
113
+ | `instanceId` | Non-empty string naming THIS running copy, one value per process (**required**) | — |
114
+ | `discoveryTimeoutMs` | Deadline (ms) of one `getService()` read of the registry projection (**required**) | — |
107
115
  | `apiQueue` | Queue name for heartbeat/API messages | `api_services_queuer` |
108
116
  | `registryQueue` | Queue name for registry requests | `registry.register` |
109
117
 
@@ -115,13 +123,32 @@ whenever the caller forgets the first (architecture-principles §3):
115
123
  ([registration-wire.md](/api/docs/biz/30-operations/registration-wire.md) §2.1) and the
116
124
  registry stores what it is sent, so a substituted path lands in `registry:services` for
117
125
  a service that never declared it;
118
- * the heartbeat cadence has ONE owner — `wrapper.registry.heartbeatInterval`, fed from
119
- `BIZ_HEARTBEAT_INTERVAL_MS` (owner confirmation
126
+ * the heartbeat cadence has ONE owner — the environment key `BIZ_HEARTBEAT_INTERVAL_MS`,
127
+ which `ServiceWrapper` reads and passes to this client; the config key
128
+ `wrapper.registry.heartbeatInterval` is retired and refused (owner confirmation
120
129
  [`biz-health-freshness.md`](/api/docs/governance/confirmations/biz-health-freshness.md)
121
130
  001 point 3) — and the freshness window the monitoring side computes is a multiple of it.
122
131
 
123
132
  Both are refused at construction, by value, naming the fix.
124
133
 
134
+ **`discoveryTimeoutMs` carries no default** (d.1093). Without a deadline `getService()`
135
+ waited for as long as Redis was down or half-open, and the workflow step waiting on it hung
136
+ until the broker expired its message. The value belongs to the platform env, which
137
+ `ServiceWrapper` reads and passes here. A lookup that does not end in time is refused with
138
+ `code: 'REGISTRY_DISCOVERY_TIMEOUT'`, and a client that already knows Redis is gone
139
+ (`isReady === false`) with `code: 'REGISTRY_DISCOVERY_UNAVAILABLE'` — both `type: 'TRANSIENT'`,
140
+ so the step is retried. The deadline is a race beside the command, not node-redis'
141
+ `AbortSignal`: aborting a command node-redis 4.7.1 has already sent corrupts its command
142
+ queue (see the CHANGELOG entry of d.1093).
143
+
144
+ **`instanceId` carries no default either** (d.817, owner confirmation
145
+ `registry-instance-identity` 001). The service name says which service, the instance id
146
+ says which copy; `register`, `heartbeat` and `deregister` carry it as `instance_id`, and
147
+ the registry deletes a registration only when it matches. The client never makes one:
148
+ `@onlineapps/service-wrapper` makes it once at boot (`SERVICE_INSTANCE_ID` when the
149
+ deployment gives one, otherwise a UUID v4) and hands the same value to monitoring as
150
+ `serviceInstanceId`.
151
+
125
152
  `amqpUrl` carries the broker credential, so no error message and no log line of this
126
153
  package renders it as given: the URL passes through `redactUrl` from
127
154
  `@onlineapps/logger-contract`, which drops the userinfo (`user:password@`) and keeps
@@ -132,8 +159,11 @@ implementace přestěhovala do core vrstvy; one concern, one rail.)
132
159
 
133
160
  ## 📬 Delivery of registry messages
134
161
 
135
- Both consumers in this package — the service's own `<service>.registry` response queue
136
- and the `<service>.registry.events` fanout copy — run on the delivery contract of
162
+ The consumers in this package — the shared `<service>.registry` (the registry's own
163
+ prompts, such as `revalidate`, heard by every copy), the answer queue of THIS copy
164
+ `<service>.registry.<instanceId>` (named as `responseQueue` in `register`, so a copy
165
+ never reads the answer meant for another — owner confirmation `registry-instance-identity`
166
+ 001 (c), d.817c) and the `<service>.registry.events` fanout copy — run on the delivery contract of
137
167
  [`@onlineapps/mq-client-core`](https://www.npmjs.com/package/@onlineapps/mq-client-core)
138
168
  (§ `consume()` — the delivery contract), registered with `requeueOnError: false`.
139
169
 
@@ -191,15 +221,25 @@ demands is a logger that component writes to). Standard:
191
221
  ### Register Request
192
222
  ```javascript
193
223
  {
224
+ id: "request-uuid", // which request
225
+ instance_id: "copy-id", // which running copy (the constructor's instanceId)
194
226
  type: "register",
195
227
  serviceName: "hello-service",
196
228
  version: "1.0.0",
197
- token: "pre-validation-token",
229
+ specificationEndpoint: "/api/v1/specification",
198
230
  endpoints: [...],
231
+ operations: { ... },
232
+ metadata: { ... },
233
+ health: "/health",
234
+ responseQueue: "hello-service.registry.copy-id", // THIS copy's answer queue (d.817c)
199
235
  timestamp: "ISO-8601"
236
+ // + workspaceScoped when the service declares it,
237
+ // + validationProof / validationData when a proof was injected
200
238
  }
201
239
  ```
202
240
 
241
+ The wire format has one owner: `api/docs/biz/30-operations/registration-wire.md` §2.
242
+
203
243
  ### Register Confirmed
204
244
  ```javascript
205
245
  {
@@ -225,9 +265,12 @@ fixes it: `api/docs/biz/30-operations/registration-wire.md` §8.
225
265
  ### Heartbeat
226
266
  ```javascript
227
267
  {
268
+ id: "message-uuid",
228
269
  type: "heartbeat",
229
270
  serviceName: "hello-service",
230
- status: "UP",
271
+ instance_id: "copy-id",
272
+ version: "1.0.0",
273
+ specificationEndpoint: "/api/v1/specification",
231
274
  timestamp: "ISO-8601"
232
275
  }
233
276
  ```
@@ -31,7 +31,7 @@ Microservice Registry Backend
31
31
  > The legacy `apiDescriptionRequest` / `sendApiDescription` round-trip has
32
32
  > been removed — the full service specification (including the `operations`
33
33
  > map) travels as part of the `register` message. See
34
- > [operations-registry-contract.md §2](../../../../docs/biz/30-operations/registration-wire.md).
34
+ > [registration-wire.md §2.1](../../../../docs/biz/30-operations/registration-wire.md).
35
35
 
36
36
  ## Usage
37
37
 
@@ -131,7 +131,7 @@ against the Operations Registry contract and either confirms or rejects.
131
131
 
132
132
  **Parameters:**
133
133
  - `serviceInfo.endpoints` - Array of API endpoints
134
- - `serviceInfo.operations` - Operations map (see [operations-registry-contract.md §3](../../../../docs/biz/30-operations/registration-wire.md))
134
+ - `serviceInfo.operations` - Operations map (see [registration-wire.md §3](../../../../docs/biz/30-operations/registration-wire.md))
135
135
  - `serviceInfo.metadata` - Service metadata object
136
136
  - `serviceInfo.health` - Health check endpoint
137
137
 
@@ -1,58 +1,43 @@
1
1
  /**
2
2
  * basicUsage.js
3
3
  *
4
- * Example demonstrating full lifecycle of ServiceRegistryClient:
5
- * 1. Load environment variables
6
- * 2. Instantiate client
7
- * 3. Initialize (setup queues and start listening for responses)
8
- * 4. Register with the full service specification (endpoints + operations)
9
- * 5. Start heartbeat loop
10
- * 6. Graceful shutdown on process signals
4
+ * The lifecycle of ServiceRegistryClient:
5
+ * 1. read the settings from the environment (examples/env.js)
6
+ * 2. construct the client with every required option
7
+ * 3. init() — the response queues and their consumers
8
+ * 4. register() the service specification and branch on the registry's verdict
9
+ * 5. startHeartbeat() with its failure budget
10
+ * 6. on SIGINT/SIGTERM: deregister() this copy, then close()
11
11
  *
12
- * See: api/docs/biz/30-operations/registration-wire.md §2 MQ wire format
12
+ * Environment: RABBITMQ_URL, SERVICE_NAME, SERVICE_VERSION, SPECIFICATION_ENDPOINT,
13
+ * SERVICE_INSTANCE_ID, BIZ_HEARTBEAT_INTERVAL_MS, REGISTRY_DISCOVERY_TIMEOUT_MS.
14
+ *
15
+ * @see README.md § Configuration
16
+ * @see api/docs/biz/30-operations/registration-wire.md §2 MQ wire format
13
17
  */
14
18
 
15
- function requireEnv(name, description) {
16
- const value = process.env[name];
17
- if (!value || String(value).trim() === '') {
18
- throw new Error(`[conn-orch-registry][example] Missing environment variable - ${name} is required. ${description || ''}`);
19
- }
20
- return String(value).trim();
21
- }
22
-
23
19
  // Requiring the package by its own name does not resolve from inside the package
24
- // (no self-reference `exports` field), so the example loads the sources directly —
25
- // same as examples/event-consumer-example.js. Consumers require
26
- // '@onlineapps/conn-orch-registry'; see README § Quick Start.
20
+ // (no self-reference `exports` field), so the example loads the sources directly.
21
+ // Consumers require '@onlineapps/conn-orch-registry'; see README § Quick Start.
27
22
  const { ServiceRegistryClient, EVENTS } = require('../src/index');
23
+ const { requireEnv, requirePositiveIntegerEnv } = require('./env');
28
24
 
29
- // The logger is a hard requirement of every constructor in this package
30
- // (README § Logger contract). An example script has no service logger, so it passes
31
- // `console` explicitly — it implements info/warn/error/debug, and choosing it here is
32
- // the caller's decision, not a fallback inside the library.
25
+ // The logger is required by every constructor in this package (README § Logger
26
+ // contract). An example script has no service logger, so it passes `console`
27
+ // explicitly — it implements info/warn/error/debug.
33
28
  const registryClient = new ServiceRegistryClient({
34
29
  logger: console,
35
- amqpUrl: requireEnv('AMQP_URL', 'RabbitMQ URL (IPv4-only recommended: amqp://user:pass@127.0.0.1:PORT)'),
36
- serviceName: requireEnv('SERVICE_NAME', 'Service logical name'),
37
- version: requireEnv('SERVICE_VERSION', 'Service version (SemVer)'),
38
- // Both REQUIRED since d.278c — the client substitutes neither, so the example
39
- // demands them the same way it demands the URL and the service name.
40
- specificationEndpoint: requireEnv('SPECIFICATION_ENDPOINT', 'Path the register message carries (e.g. /api/v1/specification)'),
41
- heartbeatInterval: parseInt(requireEnv('HEARTBEAT_INTERVAL', 'Milliseconds between heartbeats'), 10),
42
- apiQueue: process.env.API_QUEUE,
43
- registryQueue: process.env.REGISTRY_QUEUE
30
+ amqpUrl: requireEnv('RABBITMQ_URL', 'RabbitMQ URL, amqp://user:pass@host:port'),
31
+ serviceName: requireEnv('SERVICE_NAME', 'service logical name'),
32
+ version: requireEnv('SERVICE_VERSION', 'service version, SemVer'),
33
+ specificationEndpoint: requireEnv('SPECIFICATION_ENDPOINT', 'path the register and heartbeat messages carry, e.g. /api/v1/specification'),
34
+ instanceId: requireEnv('SERVICE_INSTANCE_ID', 'id of THIS running copy, one value per process'),
35
+ heartbeatInterval: requirePositiveIntegerEnv('BIZ_HEARTBEAT_INTERVAL_MS', 'milliseconds between heartbeats'),
36
+ discoveryTimeoutMs: requirePositiveIntegerEnv('REGISTRY_DISCOVERY_TIMEOUT_MS', 'deadline in ms of one getService() read')
44
37
  });
45
38
 
46
39
  registryClient.on(EVENTS.HEARTBEAT_SENT, (msg) => {
47
- console.log(`[Heartbeat] Sent at ${msg.timestamp} for ${msg.serviceName}@${msg.version}`);
48
- });
49
-
50
- registryClient.on(EVENTS.REGISTER_CONFIRMED, (msg) => {
51
- console.log(`[Register] Confirmed for ${msg.serviceName || registryClient.serviceName}`);
52
- });
53
-
54
- registryClient.on(EVENTS.REGISTER_REJECTED, (msg) => {
55
- console.error('[Register] Rejected', msg);
40
+ console.log(`[Heartbeat] Sent at ${msg.timestamp} for ${msg.serviceName}@${msg.version} (${msg.instance_id})`);
56
41
  });
57
42
 
58
43
  registryClient.on(EVENTS.REVALIDATE, (payload) => {
@@ -68,8 +53,15 @@ registryClient.on(EVENTS.ERROR, (err) => {
68
53
  await registryClient.init();
69
54
  console.log('RegistryClient initialized: queues are ready.');
70
55
 
71
- await registryClient.register(await loadServiceSpec());
72
- console.log('Register message published.');
56
+ // register() resolves with the registry's verdict; `errorCode` is the machine
57
+ // reason of a refusal (registration-wire.md §8).
58
+ const verdict = await registryClient.register(serviceSpecification(registryClient.version));
59
+ if (verdict.success !== true) {
60
+ console.error(`[Register] Refused: ${verdict.errorCode} - ${verdict.message}`);
61
+ await registryClient.close();
62
+ process.exit(1);
63
+ }
64
+ console.log(`[Register] Confirmed for ${verdict.serviceName}@${verdict.version}`);
73
65
 
74
66
  registryClient.startHeartbeat({
75
67
  maxFailures: 5,
@@ -82,53 +74,55 @@ registryClient.on(EVENTS.ERROR, (err) => {
82
74
 
83
75
  const shutdown = async () => {
84
76
  console.log('Shutting down RegistryClient...');
77
+ // deregister() stops the heartbeat first; the registry deletes only the
78
+ // registration whose instance_id matches this copy.
79
+ await registryClient.deregister();
85
80
  await registryClient.close();
86
81
  process.exit(0);
87
82
  };
88
83
  process.on('SIGINT', shutdown);
89
84
  process.on('SIGTERM', shutdown);
90
85
  } catch (err) {
91
- console.error('Failed to initialize RegistryClient', err);
86
+ console.error('Failed to run RegistryClient', err);
92
87
  process.exit(1);
93
88
  }
94
89
  })();
95
90
 
96
91
  /**
97
- * Helper: assemble the service specification published in the register message.
98
- * In a real service this is typically loaded from config/service/operations.json
99
- * plus the endpoint manifest.
92
+ * The service specification the register message carries. A real service sends the
93
+ * `operations` map of its config/service/operations.json; every operation is a v3
94
+ * declaration.
100
95
  *
101
- * @see api/docs/biz/30-operations/registration-wire.md §3 Operation schema
96
+ * @param {string} version - the service version the metadata repeats
97
+ * @see api/docs/biz/30-operations/schema-v3.md § Per-operation keys
98
+ * @see api/docs/biz/30-operations/registration-wire.md §2.1
102
99
  */
103
- async function loadServiceSpec() {
100
+ function serviceSpecification(version) {
104
101
  return {
105
- endpoints: [
106
- { path: '/invoices', method: 'POST' },
107
- { path: '/invoices/:id', method: 'GET' }
108
- ],
102
+ endpoints: [],
109
103
  operations: {
110
104
  'create-invoice': {
111
105
  description: 'Create a new invoice',
112
- endpoint: '/invoices',
113
- method: 'POST',
106
+ handler: 'handlers/invoices#createInvoice',
107
+ bundle_scope: 'workspace',
114
108
  mutates: true,
115
109
  resource_type: 'invoice',
116
110
  minRole: 'EDITOR',
117
- input: {},
118
- output: {}
111
+ input: { type: 'object' },
112
+ output: { type: 'object' }
119
113
  },
120
114
  'get-invoice': {
121
- description: 'Retrieve invoice by ID',
122
- endpoint: '/invoices/:id',
123
- method: 'GET',
115
+ description: 'Retrieve an invoice by id',
116
+ handler: 'handlers/invoices#getInvoice',
117
+ bundle_scope: 'workspace',
124
118
  mutates: false,
125
- resource_type: null,
119
+ resource_type: 'invoice',
126
120
  minRole: 'VIEWER',
127
- input: {},
128
- output: {}
121
+ input: { type: 'object' },
122
+ output: { type: 'object' }
129
123
  }
130
124
  },
131
- metadata: {},
125
+ metadata: { version, description: 'Invoice example' },
132
126
  health: '/health'
133
127
  };
134
128
  }
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * env.js — how the examples read their settings: by key name, and a key that is
5
+ * missing or malformed stops the example naming the key and the fix. No default
6
+ * stands in for an unset key (architecture-principles.md §3, §5).
7
+ *
8
+ * The key names are the platform's own (`config/env-templates/shared.env`), so an
9
+ * example runs with the same environment a biz service gets.
10
+ */
11
+
12
+ /**
13
+ * @param {string} name - environment key
14
+ * @param {string} description - what the value is, for the error message
15
+ * @returns {string} the trimmed value
16
+ * @throws {Error} when the key is unset or blank
17
+ */
18
+ function requireEnv(name, description) {
19
+ const value = process.env[name];
20
+ if (value === undefined) {
21
+ throw new Error(
22
+ `[conn-orch-registry][example] Missing environment variable - ${name} is required (${description}). `
23
+ + `Fix: export ${name} before running the example.`
24
+ );
25
+ }
26
+ if (value.trim() === '') {
27
+ throw new Error(
28
+ `[conn-orch-registry][example] Empty environment variable - ${name} is set but blank (${description}). `
29
+ + `Fix: export ${name} with a value.`
30
+ );
31
+ }
32
+ return value.trim();
33
+ }
34
+
35
+ /**
36
+ * @param {string} name - environment key
37
+ * @param {string} description - what the value is, for the error message
38
+ * @returns {number} a positive whole number
39
+ * @throws {Error} when the key is unset, blank, not a whole number or not positive
40
+ */
41
+ function requirePositiveIntegerEnv(name, description) {
42
+ const raw = requireEnv(name, description);
43
+ const value = Number(raw);
44
+ if (!Number.isInteger(value)) {
45
+ throw new Error(
46
+ `[conn-orch-registry][example] Invalid environment variable - ${name} is ${JSON.stringify(raw)}. `
47
+ + `Expected: a positive whole number (${description}). Fix: export ${name} as a whole number.`
48
+ );
49
+ }
50
+ if (value <= 0) {
51
+ throw new Error(
52
+ `[conn-orch-registry][example] Invalid environment variable - ${name} is ${value}. `
53
+ + `Expected: a positive whole number (${description}). Fix: export ${name} above zero.`
54
+ );
55
+ }
56
+ return value;
57
+ }
58
+
59
+ module.exports = { requireEnv, requirePositiveIntegerEnv };
@@ -1,81 +1,85 @@
1
1
  /**
2
- * Example: Using ServiceRegistryClient with event consumption
2
+ * event-consumer-example.js
3
3
  *
4
- * Demonstrates opt-in subscription to registry change events. The events are an
5
- * invalidation hint ("something changed"), not a read path: to read another
6
- * service's spec, use createRegistryReader from @onlineapps/service-common.
4
+ * What this example shows: a client that registers, beats, and opts in to the
5
+ * registry change events through subscribeToChanges(), with listeners for them.
7
6
  *
8
- * @see api/docs/biz/30-operations/registration-wire.md §5, §7
7
+ * The consumer runs in index-only mode: no storageConfig, because
8
+ * subscribeToChanges() (src/registryClient.js) builds it without a StorageConnector
9
+ * and the RegistryEventConsumer constructor refuses a complete storageConfig
10
+ * without one. Redis persists the local service index.
11
+ *
12
+ * Environment: RABBITMQ_URL, REDIS_URL, SERVICE_NAME, SERVICE_VERSION,
13
+ * SPECIFICATION_ENDPOINT, SERVICE_INSTANCE_ID, BIZ_HEARTBEAT_INTERVAL_MS,
14
+ * REGISTRY_DISCOVERY_TIMEOUT_MS.
15
+ *
16
+ * @see README.md § Configuration
17
+ * @see api/docs/biz/30-operations/registration-wire.md §7
9
18
  */
10
19
 
20
+ const { createClient } = require('redis');
11
21
  const { ServiceRegistryClient } = require('../src/index');
12
- const Redis = require('ioredis');
22
+ const { requireEnv, requirePositiveIntegerEnv } = require('./env');
13
23
 
14
24
  async function main() {
15
- // Optional Redis for index persistence
16
- const redis = new Redis({
17
- host: process.env.REDIS_HOST || 'localhost',
18
- port: process.env.REDIS_PORT || 6379
25
+ // node-redis, the client getService() reads the registry projection with.
26
+ const redis = createClient({
27
+ url: requireEnv('REDIS_URL', 'Redis URL, redis://:password@host:port'),
28
+ disableOfflineQueue: true
19
29
  });
30
+ await redis.connect();
20
31
 
21
- // Initialize client with storage configuration
22
- // The logger is a hard requirement of every constructor in this package
23
- // (README § Logger contract). An example script has no service logger, so it passes
24
- // `console` explicitly — it implements info/warn/error/debug, and choosing it here is
25
- // the caller's decision, not a fallback inside the library.
32
+ // The logger is required by every constructor in this package (README § Logger
33
+ // contract). An example script has no service logger, so it passes `console`
34
+ // explicitly — it implements info/warn/error/debug.
26
35
  const client = new ServiceRegistryClient({
27
36
  logger: console,
28
- amqpUrl: process.env.AMQP_URL || 'amqp://localhost',
29
- serviceName: 'example-service',
30
- version: '1.0.0',
31
- redis: redis,
32
- storageConfig: {
33
- endPoint: process.env.MINIO_ENDPOINT || 'localhost',
34
- port: parseInt(process.env.MINIO_PORT || 9000),
35
- useSSL: process.env.MINIO_USE_SSL === 'true',
36
- accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
37
- secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin'
38
- }
37
+ amqpUrl: requireEnv('RABBITMQ_URL', 'RabbitMQ URL, amqp://user:pass@host:port'),
38
+ serviceName: requireEnv('SERVICE_NAME', 'service logical name'),
39
+ version: requireEnv('SERVICE_VERSION', 'service version, SemVer'),
40
+ specificationEndpoint: requireEnv('SPECIFICATION_ENDPOINT', 'path the register and heartbeat messages carry, e.g. /api/v1/specification'),
41
+ instanceId: requireEnv('SERVICE_INSTANCE_ID', 'id of THIS running copy, one value per process'),
42
+ heartbeatInterval: requirePositiveIntegerEnv('BIZ_HEARTBEAT_INTERVAL_MS', 'milliseconds between heartbeats'),
43
+ discoveryTimeoutMs: requirePositiveIntegerEnv('REGISTRY_DISCOVERY_TIMEOUT_MS', 'deadline in ms of one getService() read'),
44
+ redis
39
45
  });
40
46
 
41
- // Initialize connection
42
47
  await client.init();
43
48
 
44
- // Register ourselves with the registry. The full spec (endpoints +
45
- // operations) is published as part of the register message — see
46
- // api/docs/biz/30-operations/registration-wire.md §2.
47
- await client.register({
48
- endpoints: [{ path: '/hello', method: 'GET' }],
49
+ // register() resolves with the registry's verdict (registration-wire.md §2.4, §8).
50
+ const verdict = await client.register({
51
+ endpoints: [],
49
52
  operations: {
50
53
  'say-hello': {
51
54
  description: 'Say hello',
52
- endpoint: '/hello',
53
- method: 'GET',
55
+ handler: 'handlers/hello#sayHello',
56
+ bundle_scope: 'platform',
54
57
  mutates: false,
55
58
  resource_type: null,
56
59
  minRole: 'VIEWER',
57
- input: {},
58
- output: {}
60
+ input: { type: 'object' },
61
+ output: { type: 'object' }
59
62
  }
60
63
  },
61
- metadata: {},
64
+ metadata: { version: client.version, description: 'Event consumer example' },
62
65
  health: '/health'
63
66
  });
67
+ if (verdict.success !== true) {
68
+ throw new Error(`[conn-orch-registry][example] Registration refused - ${verdict.errorCode}: ${verdict.message}`);
69
+ }
64
70
 
65
- // Start heartbeat
66
71
  client.startHeartbeat({
67
72
  maxFailures: 5,
68
73
  onFatal: (err) => {
69
74
  console.error(err.message);
70
- process.exit(1);
75
+ process.exit(1); // the restart policy boots it again and it re-registers
71
76
  }
72
77
  });
73
78
 
74
- // OPT-IN: Subscribe to registry changes
79
+ // OPT-IN: subscribe to registry changes.
75
80
  console.log('Subscribing to registry changes...');
76
81
  await client.subscribeToChanges();
77
82
 
78
- // Listen for events
79
83
  client.on('serviceUpdated', ({ service, fingerprint, version }) => {
80
84
  console.log(`Service updated: ${service} v${version} (${fingerprint})`);
81
85
  });
@@ -88,14 +92,22 @@ async function main() {
88
92
  console.log(`Received snapshot with ${count} services`);
89
93
  });
90
94
 
91
- // Graceful shutdown
95
+ client.on('error', (err) => {
96
+ console.error('[RegistryClient Error]', err);
97
+ });
98
+
92
99
  process.on('SIGINT', async () => {
93
100
  console.log('\nShutting down...');
101
+ // deregister() stops the heartbeat first; the registry deletes only the
102
+ // registration whose instance_id matches this copy.
103
+ await client.deregister();
94
104
  await client.close();
95
105
  await redis.quit();
96
106
  process.exit(0);
97
107
  });
98
108
  }
99
109
 
100
- // Run example
101
- main().catch(console.error);
110
+ main().catch((err) => {
111
+ console.error(err);
112
+ process.exit(1);
113
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-registry",
3
- "version": "6.0.3",
3
+ "version": "8.0.0",
4
4
  "license": "MIT",
5
5
  "description": "conn-orch-registry provides the core communication mechanism for microservices in this environment. It enables them to interact with a services_registry to receive and fulfill tasks by submitting heartbeats or their API descriptions.",
6
6
  "keywords": [
@@ -18,7 +18,9 @@
18
18
  "docs/",
19
19
  "examples/",
20
20
  "README.md",
21
- "LICENSE"
21
+ "LICENSE",
22
+ "CHANGELOG.md",
23
+ "API.md"
22
24
  ],
23
25
  "oa": {
24
26
  "category": "orchestration"
@@ -37,14 +39,14 @@
37
39
  },
38
40
  "dependencies": {
39
41
  "@onlineapps/logger-contract": "2.0.0",
40
- "@onlineapps/mq-client-core": "4.2.0",
42
+ "@onlineapps/mq-client-core": "5.1.0",
41
43
  "uuid": "^9.0.1"
42
44
  },
43
45
  "devDependencies": {
44
46
  "amqplib": "0.10.9",
45
- "ioredis": "^5.8.0",
46
47
  "jest": "^29.6.1",
47
- "jsdoc-to-markdown": "^8.0.0"
48
+ "jsdoc-to-markdown": "^8.0.0",
49
+ "redis": "4.7.1"
48
50
  },
49
51
  "engines": {
50
52
  "node": ">=24.0.0 <25"