@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 +14 -0
- package/dist/registry.d.ts +75 -0
- package/dist/registry.js +129 -0
- package/package.json +2 -2
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
|
package/dist/registry.d.ts
CHANGED
|
@@ -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.
|
|
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": "
|
|
34
|
+
"gitHead": "5d6a724ac4e99d493d42efe337e3ad17db40c3f7"
|
|
35
35
|
}
|