@hile/micro 3.0.1 → 3.0.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/AI.md CHANGED
@@ -186,6 +186,20 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
186
186
  - Registry is started before application nodes need discovery.
187
187
  - Micro apps use stable namespaces and advertise reachable hosts.
188
188
 
189
+ ## Registry Read APIs
190
+
191
+ `Registry` exposes read-only routes for diagnostics and admin tooling. These routes return
192
+ snapshots of current registry state; they do not change service discovery, topic declarations,
193
+ subscriptions, or retained config data.
194
+
195
+ - `/-/namespaces` returns all registered namespaces with peer counts and addresses.
196
+ - `/-/namespace/peers` accepts `{ namespace, exclude? }` and returns every peer for one namespace.
197
+ - `/-/registry/status` returns registry uptime and counts for clients, namespaces, topics, and config namespaces.
198
+ - `/-/topics` accepts `{ prefix? }` and returns topic summaries with publisher/subscriber counts and retained/data flags.
199
+ - `/-/topic/get` accepts `{ topic }` and returns the current topic payload snapshot when the topic exists.
200
+ - `/-/configs` returns loaded config namespaces and keys.
201
+ - `/-/config/get` accepts `{ namespace, key? }` and returns either a config snapshot or one config value.
202
+
189
203
 
190
204
 
191
205
  # Related Recipes
@@ -7,6 +7,75 @@ export interface RegistryAddress {
7
7
  host: string;
8
8
  port: number;
9
9
  }
10
+ export interface RegistryNamespaceSnapshot {
11
+ namespace: string;
12
+ peerCount: number;
13
+ peers: RegistryAddress[];
14
+ }
15
+ export interface RegistryNamespacesResult {
16
+ namespaces: RegistryNamespaceSnapshot[];
17
+ }
18
+ export type RegistryNamespacePeersData = RegistryFindData;
19
+ export interface RegistryNamespacePeersResult {
20
+ namespace: string;
21
+ peers: RegistryAddress[];
22
+ }
23
+ export interface RegistryStatusSnapshot {
24
+ status: 'ok';
25
+ startedAt: number;
26
+ uptime: number;
27
+ clientCount: number;
28
+ namespaceCount: number;
29
+ topicCount: number;
30
+ configNamespaceCount: number;
31
+ }
32
+ export interface RegistryTopicsData {
33
+ prefix?: string;
34
+ }
35
+ export interface RegistryTopicSummary {
36
+ topic: string;
37
+ publisherCount: number;
38
+ subscriberCount: number;
39
+ hasData: boolean;
40
+ retained: boolean;
41
+ }
42
+ export interface RegistryTopicsResult {
43
+ topics: RegistryTopicSummary[];
44
+ }
45
+ export interface RegistryTopicGetData {
46
+ topic: string;
47
+ }
48
+ export interface RegistryTopicSnapshot extends RegistryTopicSummary {
49
+ payload: any;
50
+ }
51
+ export interface RegistryConfigSummary {
52
+ namespace: string;
53
+ keys: string[];
54
+ }
55
+ export interface RegistryConfigsResult {
56
+ configs: RegistryConfigSummary[];
57
+ }
58
+ export interface RegistryConfigGetData {
59
+ namespace: string;
60
+ key?: string;
61
+ }
62
+ export type RegistryConfigGetResult = {
63
+ namespace: string;
64
+ hasConfig: true;
65
+ config: any;
66
+ } | {
67
+ namespace: string;
68
+ hasConfig: false;
69
+ } | {
70
+ namespace: string;
71
+ key: string;
72
+ hasValue: true;
73
+ value: any;
74
+ } | {
75
+ namespace: string;
76
+ key: string;
77
+ hasValue: false;
78
+ };
10
79
  /** 将 `host:port` 或 `[ipv6]:port` 形式的 key 解析为地址(端口取最后一个 `:` 之后) */
11
80
  export declare function parseAddressKey(key: string): RegistryAddress | undefined;
12
81
  export declare function selectRandomRegistryAddress(keys: Iterable<string>): RegistryAddress | undefined;
@@ -21,7 +90,13 @@ export declare class Registry extends Server {
21
90
  private readonly fallbacks;
22
91
  private readonly topics;
23
92
  private topicRevision;
93
+ private readonly startedAt;
24
94
  constructor(props?: MicroServerProps);
95
+ private createNamespaceSnapshot;
96
+ private createTopicSummary;
97
+ private createTopicSnapshot;
98
+ private listTopicSummaries;
99
+ private registerReadApis;
25
100
  private cleanupTopicIfUnused;
26
101
  private clearTopicData;
27
102
  private rememberPublisherPayload;
package/dist/registry.js CHANGED
@@ -28,6 +28,28 @@ export function selectRandomRegistryAddress(keys) {
28
28
  const index = Math.floor(Math.random() * addresses.length);
29
29
  return addresses[index];
30
30
  }
31
+ function compareRegistryAddress(a, b) {
32
+ const byHost = a.host.localeCompare(b.host);
33
+ if (byHost !== 0)
34
+ return byHost;
35
+ return a.port - b.port;
36
+ }
37
+ function registryAddressesFromKeys(keys, exclude) {
38
+ const excludeSet = exclude?.length ? new Set(exclude) : undefined;
39
+ return Array.from(keys)
40
+ .filter(key => !excludeSet?.has(key))
41
+ .map(parseAddressKey)
42
+ .filter((address) => address !== undefined)
43
+ .sort(compareRegistryAddress);
44
+ }
45
+ function cloneRegistryValue(value) {
46
+ try {
47
+ return structuredClone(value);
48
+ }
49
+ catch {
50
+ return value;
51
+ }
52
+ }
31
53
  export function getRegistryConfigsDir() {
32
54
  return resolve(homedir(), '.registry', 'configs');
33
55
  }
@@ -81,6 +103,7 @@ export class Registry extends Server {
81
103
  fallbacks = new Set();
82
104
  topics = new Map();
83
105
  topicRevision = 0;
106
+ startedAt = Date.now();
84
107
  constructor(props = {}) {
85
108
  const workspace = resolve(homedir(), '.registry');
86
109
  if (!existsSync(workspace)) {
@@ -133,6 +156,112 @@ export class Registry extends Server {
133
156
  this.registerSubscribe();
134
157
  this.registerUnsubscribe();
135
158
  this.registerReceiveTopicUpdate();
159
+ this.registerReadApis();
160
+ }
161
+ createNamespaceSnapshot(namespace, keys) {
162
+ const peers = registryAddressesFromKeys(keys);
163
+ return {
164
+ namespace,
165
+ peerCount: peers.length,
166
+ peers,
167
+ };
168
+ }
169
+ createTopicSummary(topic, entry) {
170
+ return {
171
+ topic,
172
+ publisherCount: entry.publishers.size,
173
+ subscriberCount: entry.subscribers.size,
174
+ hasData: entry.hasData,
175
+ retained: entry.retained,
176
+ };
177
+ }
178
+ createTopicSnapshot(topic, entry) {
179
+ return {
180
+ ...this.createTopicSummary(topic, entry),
181
+ payload: cloneRegistryValue(entry.data),
182
+ };
183
+ }
184
+ listTopicSummaries(prefix) {
185
+ const hasPrefix = typeof prefix === 'string' && prefix.length > 0;
186
+ return [...this.topics.entries()]
187
+ .filter(([topic]) => !hasPrefix || topic.startsWith(prefix))
188
+ .sort(([a], [b]) => a.localeCompare(b))
189
+ .map(([topic, entry]) => this.createTopicSummary(topic, entry));
190
+ }
191
+ registerReadApis() {
192
+ this.fallbacks.add(this.register('/-/namespaces', async () => {
193
+ const namespaces = [...this.namespaces.entries()]
194
+ .sort(([a], [b]) => a.localeCompare(b))
195
+ .map(([namespace, keys]) => this.createNamespaceSnapshot(namespace, keys));
196
+ return { namespaces };
197
+ }));
198
+ this.fallbacks.add(this.register('/-/namespace/peers', async ({ data }) => {
199
+ const namespace = typeof data?.namespace === 'string' ? data.namespace : '';
200
+ const keys = this.namespaces.get(namespace);
201
+ return {
202
+ namespace,
203
+ peers: keys ? registryAddressesFromKeys(keys, data.exclude) : [],
204
+ };
205
+ }));
206
+ this.fallbacks.add(this.register('/-/registry/status', async () => ({
207
+ status: 'ok',
208
+ startedAt: this.startedAt,
209
+ uptime: Date.now() - this.startedAt,
210
+ clientCount: this.clients.size,
211
+ namespaceCount: this.namespaces.size,
212
+ topicCount: this.topics.size,
213
+ configNamespaceCount: this.configs.size,
214
+ })));
215
+ this.fallbacks.add(this.register('/-/topics', async ({ data }) => ({
216
+ topics: this.listTopicSummaries(data?.prefix),
217
+ })));
218
+ this.fallbacks.add(this.register('/-/topic/get', async ({ data }) => {
219
+ if (typeof data?.topic !== 'string')
220
+ return;
221
+ const entry = this.topics.get(data.topic);
222
+ if (!entry)
223
+ return;
224
+ return this.createTopicSnapshot(data.topic, entry);
225
+ }));
226
+ this.fallbacks.add(this.register('/-/configs', async () => {
227
+ const configs = [...this.configs.entries()]
228
+ .sort(([a], [b]) => a.localeCompare(b))
229
+ .map(([namespace, config]) => ({
230
+ namespace,
231
+ keys: Object.keys(config).sort(),
232
+ }));
233
+ return { configs };
234
+ }));
235
+ this.fallbacks.add(this.register('/-/config/get', async ({ data }) => {
236
+ const namespace = typeof data?.namespace === 'string' ? data.namespace : '';
237
+ const config = this.configs.get(namespace);
238
+ if (typeof data?.key === 'string') {
239
+ if (config && Object.prototype.hasOwnProperty.call(config, data.key)) {
240
+ return {
241
+ namespace,
242
+ key: data.key,
243
+ hasValue: true,
244
+ value: cloneRegistryValue(config[data.key]),
245
+ };
246
+ }
247
+ return {
248
+ namespace,
249
+ key: data.key,
250
+ hasValue: false,
251
+ };
252
+ }
253
+ if (!config) {
254
+ return {
255
+ namespace,
256
+ hasConfig: false,
257
+ };
258
+ }
259
+ return {
260
+ namespace,
261
+ hasConfig: true,
262
+ config: cloneRegistryValue(config),
263
+ };
264
+ }));
136
265
  }
137
266
  cleanupTopicIfUnused(topic, entry = this.topics.get(topic)) {
138
267
  if (!entry)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/micro",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -31,5 +31,5 @@
31
31
  "ws": "^8.21.0",
32
32
  "yaml": "^2.9.0"
33
33
  },
34
- "gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
34
+ "gitHead": "5d6a724ac4e99d493d42efe337e3ad17db40c3f7"
35
35
  }