@hile/micro 2.0.7 → 2.0.15
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 +104 -19
- package/dist/application.d.ts +66 -3
- package/dist/application.js +676 -114
- package/dist/registry.d.ts +8 -1
- package/dist/registry.js +146 -36
- package/dist/server.d.ts +1 -0
- package/dist/server.js +13 -2
- package/package.json +5 -6
package/dist/registry.d.ts
CHANGED
|
@@ -20,8 +20,14 @@ export declare class Registry extends Server {
|
|
|
20
20
|
private readonly configs;
|
|
21
21
|
private readonly fallbacks;
|
|
22
22
|
private readonly topics;
|
|
23
|
+
private topicRevision;
|
|
23
24
|
constructor(props?: MicroServerProps);
|
|
24
|
-
|
|
25
|
+
private cleanupTopicIfUnused;
|
|
26
|
+
private clearTopicData;
|
|
27
|
+
private rememberPublisherPayload;
|
|
28
|
+
private setTopicData;
|
|
29
|
+
private restoreTopicDataFromRemainingPublishers;
|
|
30
|
+
watchEnvFile(): import("node:fs").FSWatcher;
|
|
25
31
|
listen(port?: number): Promise<() => Promise<void>>;
|
|
26
32
|
private registerFindApplication;
|
|
27
33
|
private registerDeclare;
|
|
@@ -29,5 +35,6 @@ export declare class Registry extends Server {
|
|
|
29
35
|
private registerSubscribe;
|
|
30
36
|
private registerUnsubscribe;
|
|
31
37
|
private registerReceiveTopicUpdate;
|
|
38
|
+
private notifySubscribers;
|
|
32
39
|
private publish;
|
|
33
40
|
}
|
package/dist/registry.js
CHANGED
|
@@ -39,6 +39,40 @@ export function parseConfigFilename(filename) {
|
|
|
39
39
|
return null;
|
|
40
40
|
return filename.slice(0, -'.config.yaml'.length);
|
|
41
41
|
}
|
|
42
|
+
function createTopicEntry(data, hasData = false, retained = false) {
|
|
43
|
+
return {
|
|
44
|
+
publishers: new Set(),
|
|
45
|
+
publisherPayloads: new Map(),
|
|
46
|
+
subscribers: new Set(),
|
|
47
|
+
data,
|
|
48
|
+
hasData,
|
|
49
|
+
signature: hasData ? stableSignature(data) : undefined,
|
|
50
|
+
retained,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function stableSignature(value) {
|
|
54
|
+
if (value === undefined)
|
|
55
|
+
return 'undefined';
|
|
56
|
+
if (value === null)
|
|
57
|
+
return 'null';
|
|
58
|
+
const type = typeof value;
|
|
59
|
+
if (type === 'string')
|
|
60
|
+
return `string:${JSON.stringify(value)}`;
|
|
61
|
+
if (type === 'number')
|
|
62
|
+
return `number:${String(value)}`;
|
|
63
|
+
if (type === 'boolean')
|
|
64
|
+
return `boolean:${String(value)}`;
|
|
65
|
+
if (type === 'bigint')
|
|
66
|
+
return `bigint:${String(value)}`;
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return `array:[${value.map(stableSignature).join(',')}]`;
|
|
69
|
+
}
|
|
70
|
+
if (type === 'object') {
|
|
71
|
+
const keys = Object.keys(value).sort();
|
|
72
|
+
return `object:{${keys.map(key => `${JSON.stringify(key)}:${stableSignature(value[key])}`).join(',')}}`;
|
|
73
|
+
}
|
|
74
|
+
return `${type}:${String(value)}`;
|
|
75
|
+
}
|
|
42
76
|
export class Registry extends Server {
|
|
43
77
|
namespaces = new Map();
|
|
44
78
|
workspace;
|
|
@@ -46,6 +80,7 @@ export class Registry extends Server {
|
|
|
46
80
|
configs = new Map();
|
|
47
81
|
fallbacks = new Set();
|
|
48
82
|
topics = new Map();
|
|
83
|
+
topicRevision = 0;
|
|
49
84
|
constructor(props = {}) {
|
|
50
85
|
const workspace = resolve(homedir(), '.registry');
|
|
51
86
|
if (!existsSync(workspace)) {
|
|
@@ -64,16 +99,19 @@ export class Registry extends Server {
|
|
|
64
99
|
});
|
|
65
100
|
this.events.on('disconnect', (client, extras) => {
|
|
66
101
|
const key = client.host + ':' + client.port;
|
|
67
|
-
// 清理 topic
|
|
68
|
-
for (const [,
|
|
69
|
-
if (publishers.has(key)) {
|
|
70
|
-
publishers.delete(key);
|
|
102
|
+
// 清理 topic 中的关联
|
|
103
|
+
for (const [topic, entry] of [...this.topics]) {
|
|
104
|
+
if (entry.publishers.has(key)) {
|
|
105
|
+
entry.publishers.delete(key);
|
|
106
|
+
entry.publisherPayloads.delete(key);
|
|
107
|
+
this.restoreTopicDataFromRemainingPublishers(topic, entry);
|
|
71
108
|
this.logger.debug('[delete publisher] %s', key);
|
|
72
109
|
}
|
|
73
|
-
if (subscribers.has(key)) {
|
|
74
|
-
subscribers.delete(key);
|
|
110
|
+
if (entry.subscribers.has(key)) {
|
|
111
|
+
entry.subscribers.delete(key);
|
|
75
112
|
this.logger.debug('[delete subscriber] %s', key);
|
|
76
113
|
}
|
|
114
|
+
this.cleanupTopicIfUnused(topic, entry);
|
|
77
115
|
}
|
|
78
116
|
// 清理 namespace 中的关联
|
|
79
117
|
const namespace = extras.join('/');
|
|
@@ -96,10 +134,67 @@ export class Registry extends Server {
|
|
|
96
134
|
this.registerUnsubscribe();
|
|
97
135
|
this.registerReceiveTopicUpdate();
|
|
98
136
|
}
|
|
137
|
+
cleanupTopicIfUnused(topic, entry = this.topics.get(topic)) {
|
|
138
|
+
if (!entry)
|
|
139
|
+
return;
|
|
140
|
+
if (entry.retained)
|
|
141
|
+
return;
|
|
142
|
+
if (entry.publishers.size === 0 && entry.subscribers.size === 0) {
|
|
143
|
+
this.topics.delete(topic);
|
|
144
|
+
this.logger.debug('[delete topic] %s', topic);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
clearTopicData(entry) {
|
|
148
|
+
if (entry.retained)
|
|
149
|
+
return;
|
|
150
|
+
entry.data = undefined;
|
|
151
|
+
entry.hasData = false;
|
|
152
|
+
entry.signature = undefined;
|
|
153
|
+
}
|
|
154
|
+
rememberPublisherPayload(entry, key, payload, revision) {
|
|
155
|
+
const nextRevision = Number.isFinite(revision) && revision > 0 ? revision : ++this.topicRevision;
|
|
156
|
+
this.topicRevision = Math.max(this.topicRevision, nextRevision);
|
|
157
|
+
entry.publisherPayloads.set(key, {
|
|
158
|
+
data: payload,
|
|
159
|
+
signature: stableSignature(payload),
|
|
160
|
+
revision: nextRevision,
|
|
161
|
+
});
|
|
162
|
+
return nextRevision;
|
|
163
|
+
}
|
|
164
|
+
setTopicData(entry, payload) {
|
|
165
|
+
const signature = stableSignature(payload);
|
|
166
|
+
const changed = !entry.hasData || entry.signature !== signature;
|
|
167
|
+
entry.data = payload;
|
|
168
|
+
entry.hasData = true;
|
|
169
|
+
entry.signature = signature;
|
|
170
|
+
return changed;
|
|
171
|
+
}
|
|
172
|
+
restoreTopicDataFromRemainingPublishers(topic, entry) {
|
|
173
|
+
if (entry.retained)
|
|
174
|
+
return;
|
|
175
|
+
let latest;
|
|
176
|
+
for (const payload of entry.publisherPayloads.values()) {
|
|
177
|
+
if (!latest || payload.revision > latest.revision) {
|
|
178
|
+
latest = payload;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!latest) {
|
|
182
|
+
this.clearTopicData(entry);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const changed = !entry.hasData || entry.signature !== latest.signature;
|
|
186
|
+
entry.data = latest.data;
|
|
187
|
+
entry.hasData = true;
|
|
188
|
+
entry.signature = latest.signature;
|
|
189
|
+
if (changed) {
|
|
190
|
+
this.notifySubscribers(topic, latest.data, entry);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
99
193
|
watchEnvFile() {
|
|
100
194
|
const configFile = resolve(this.workspace, 'configs');
|
|
101
|
-
if (!existsSync(configFile))
|
|
102
|
-
|
|
195
|
+
if (!existsSync(configFile)) {
|
|
196
|
+
mkdirSync(configFile, { recursive: true });
|
|
197
|
+
}
|
|
103
198
|
const configFiles = readdirSync(configFile).filter(filename => filename.endsWith(this.configFileSuffix));
|
|
104
199
|
for (const filename of configFiles) {
|
|
105
200
|
try {
|
|
@@ -112,8 +207,12 @@ export class Registry extends Server {
|
|
|
112
207
|
for (const key of keys) {
|
|
113
208
|
const _key = `registry:${namespace}/${key}`;
|
|
114
209
|
if (!this.topics.has(_key)) {
|
|
115
|
-
this.topics.set(_key,
|
|
210
|
+
this.topics.set(_key, createTopicEntry(config[key], true, true));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.topics.get(_key).retained = true;
|
|
116
214
|
}
|
|
215
|
+
this.publish(_key, config[key]);
|
|
117
216
|
}
|
|
118
217
|
}
|
|
119
218
|
catch { }
|
|
@@ -138,7 +237,10 @@ export class Registry extends Server {
|
|
|
138
237
|
for (const key of keys) {
|
|
139
238
|
const _key = `registry:${namespace}/${key}`;
|
|
140
239
|
if (!this.topics.has(_key)) {
|
|
141
|
-
this.topics.set(_key,
|
|
240
|
+
this.topics.set(_key, createTopicEntry(config[key], true, true));
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.topics.get(_key).retained = true;
|
|
142
244
|
}
|
|
143
245
|
this.publish(_key, config[key]);
|
|
144
246
|
}
|
|
@@ -156,10 +258,6 @@ export class Registry extends Server {
|
|
|
156
258
|
return async () => {
|
|
157
259
|
if (watcher)
|
|
158
260
|
watcher.close();
|
|
159
|
-
for (const fallback of this.fallbacks) {
|
|
160
|
-
fallback();
|
|
161
|
-
}
|
|
162
|
-
this.fallbacks.clear();
|
|
163
261
|
await teardown();
|
|
164
262
|
};
|
|
165
263
|
}
|
|
@@ -183,15 +281,14 @@ export class Registry extends Server {
|
|
|
183
281
|
this.fallbacks.add(this.register('/-/declare', async ({ data, client }) => {
|
|
184
282
|
const key = `${client.host}:${client.port}`;
|
|
185
283
|
if (!this.topics.has(data.topic)) {
|
|
186
|
-
this.topics.set(data.topic,
|
|
284
|
+
this.topics.set(data.topic, createTopicEntry());
|
|
187
285
|
}
|
|
188
286
|
const entry = this.topics.get(data.topic);
|
|
189
287
|
const publishers = entry.publishers;
|
|
190
|
-
entry.data = data.payload;
|
|
191
288
|
publishers.add(key);
|
|
192
|
-
this.publish(data.topic, data.payload);
|
|
289
|
+
const revision = this.publish(data.topic, data.payload, key, data.revision);
|
|
193
290
|
this.logger.debug('[declare] %s/%s', key, data.topic);
|
|
194
|
-
return
|
|
291
|
+
return revision;
|
|
195
292
|
}));
|
|
196
293
|
}
|
|
197
294
|
registerUndeclare() {
|
|
@@ -201,15 +298,13 @@ export class Registry extends Server {
|
|
|
201
298
|
return 0;
|
|
202
299
|
const entry = this.topics.get(data.topic);
|
|
203
300
|
const publishers = entry.publishers;
|
|
204
|
-
const subscribers = entry.subscribers;
|
|
205
301
|
const i = publishers.size;
|
|
206
302
|
if (publishers.has(key)) {
|
|
207
303
|
publishers.delete(key);
|
|
304
|
+
entry.publisherPayloads.delete(key);
|
|
305
|
+
this.restoreTopicDataFromRemainingPublishers(data.topic, entry);
|
|
208
306
|
this.logger.debug('[undeclare] %s/%s', key, data.topic);
|
|
209
|
-
|
|
210
|
-
this.topics.delete(data.topic);
|
|
211
|
-
this.logger.debug('[delete topic] %s', data.topic);
|
|
212
|
-
}
|
|
307
|
+
this.cleanupTopicIfUnused(data.topic, entry);
|
|
213
308
|
}
|
|
214
309
|
return i - publishers.size;
|
|
215
310
|
}));
|
|
@@ -218,13 +313,13 @@ export class Registry extends Server {
|
|
|
218
313
|
this.fallbacks.add(this.register('/-/subscribe', async ({ data, client }) => {
|
|
219
314
|
const key = `${client.host}:${client.port}`;
|
|
220
315
|
if (!this.topics.has(data.topic)) {
|
|
221
|
-
this.topics.set(data.topic,
|
|
316
|
+
this.topics.set(data.topic, createTopicEntry());
|
|
222
317
|
}
|
|
223
318
|
const entry = this.topics.get(data.topic);
|
|
224
319
|
const subscribers = entry.subscribers;
|
|
225
320
|
subscribers.add(key);
|
|
226
321
|
this.logger.debug('[subscribe] %s/%s', key, data.topic);
|
|
227
|
-
return entry.data;
|
|
322
|
+
return { hasData: entry.hasData, payload: entry.data };
|
|
228
323
|
}));
|
|
229
324
|
}
|
|
230
325
|
registerUnsubscribe() {
|
|
@@ -239,31 +334,46 @@ export class Registry extends Server {
|
|
|
239
334
|
subscribers.delete(key);
|
|
240
335
|
}
|
|
241
336
|
this.logger.debug('[unsubscribe] %s/%s', key, data.topic);
|
|
337
|
+
this.cleanupTopicIfUnused(data.topic, entry);
|
|
242
338
|
return i - subscribers.size;
|
|
243
339
|
}));
|
|
244
340
|
}
|
|
245
341
|
registerReceiveTopicUpdate() {
|
|
246
|
-
this.fallbacks.add(this.register('/-/topic/update', async ({ data }) => {
|
|
342
|
+
this.fallbacks.add(this.register('/-/topic/update', async ({ data, client }) => {
|
|
247
343
|
// 转发
|
|
248
|
-
|
|
249
|
-
|
|
344
|
+
const key = client ? `${client.host}:${client.port}` : undefined;
|
|
345
|
+
const entry = this.topics.get(data.topic);
|
|
346
|
+
if (key && entry?.publishers.has(key)) {
|
|
347
|
+
return this.publish(data.topic, data.payload, key) ?? Date.now();
|
|
348
|
+
}
|
|
349
|
+
return this.publish(data.topic, data.payload) ?? Date.now();
|
|
250
350
|
}));
|
|
251
351
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return;
|
|
255
|
-
const entry = this.topics.get(topic);
|
|
256
|
-
const subscribers = entry.subscribers;
|
|
257
|
-
entry.data = payload;
|
|
258
|
-
for (const key of subscribers.values()) {
|
|
352
|
+
notifySubscribers(topic, payload, entry) {
|
|
353
|
+
for (const key of entry.subscribers.values()) {
|
|
259
354
|
try {
|
|
260
355
|
if (this.clients.has(key)) {
|
|
261
356
|
this.clients.get(key).push(`/-/topic/update`, { topic, payload });
|
|
262
357
|
}
|
|
263
358
|
}
|
|
264
359
|
catch {
|
|
265
|
-
// 推送失败,disconnect 事件中会清理
|
|
360
|
+
// 推送失败,disconnect 事件中会清理
|
|
266
361
|
}
|
|
267
362
|
}
|
|
268
363
|
}
|
|
364
|
+
publish(topic, payload, publisherKey, revision) {
|
|
365
|
+
if (!this.topics.has(topic))
|
|
366
|
+
return;
|
|
367
|
+
const entry = this.topics.get(topic);
|
|
368
|
+
if (publisherKey) {
|
|
369
|
+
const nextRevision = this.rememberPublisherPayload(entry, publisherKey, payload, revision);
|
|
370
|
+
this.restoreTopicDataFromRemainingPublishers(topic, entry);
|
|
371
|
+
return nextRevision;
|
|
372
|
+
}
|
|
373
|
+
const changed = this.setTopicData(entry, payload);
|
|
374
|
+
if (!changed)
|
|
375
|
+
return;
|
|
376
|
+
this.notifySubscribers(topic, payload, entry);
|
|
377
|
+
return ++this.topicRevision;
|
|
378
|
+
}
|
|
269
379
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export declare class Server extends MessageLoader {
|
|
|
22
22
|
port?: number;
|
|
23
23
|
readonly logger: Logger | Console;
|
|
24
24
|
readonly clients: Map<string, Client>;
|
|
25
|
+
private readonly clientExtras;
|
|
25
26
|
private readonly announceHost;
|
|
26
27
|
readonly events: EventEmitter<any>;
|
|
27
28
|
get host(): string;
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@ export class Server extends MessageLoader {
|
|
|
11
11
|
port;
|
|
12
12
|
logger;
|
|
13
13
|
clients = new Map();
|
|
14
|
+
clientExtras = new Map();
|
|
14
15
|
announceHost;
|
|
15
16
|
events = new EventEmitter();
|
|
16
17
|
get host() {
|
|
@@ -58,18 +59,23 @@ export class Server extends MessageLoader {
|
|
|
58
59
|
const key = `${host}:${port}`;
|
|
59
60
|
const previous = this.clients.get(key);
|
|
60
61
|
if (previous) {
|
|
62
|
+
const previousExtras = this.clientExtras.get(key) ?? [];
|
|
61
63
|
this.clients.delete(key);
|
|
64
|
+
this.clientExtras.delete(key);
|
|
62
65
|
previous.dispose();
|
|
66
|
+
this.events.emit('disconnect', previous, previousExtras);
|
|
63
67
|
}
|
|
64
68
|
const client = new Client({ server: this, ws, host, port });
|
|
65
69
|
ws.on('close', () => {
|
|
66
70
|
if (this.clients.get(key) === client) {
|
|
67
71
|
this.clients.delete(key);
|
|
72
|
+
this.clientExtras.delete(key);
|
|
68
73
|
client.dispose();
|
|
69
74
|
this.events.emit('disconnect', client, extras);
|
|
70
75
|
}
|
|
71
76
|
});
|
|
72
77
|
this.clients.set(key, client);
|
|
78
|
+
this.clientExtras.set(key, extras);
|
|
73
79
|
this.events.emit('connect', client, extras);
|
|
74
80
|
return client;
|
|
75
81
|
}
|
|
@@ -149,11 +155,16 @@ export class Server extends MessageLoader {
|
|
|
149
155
|
});
|
|
150
156
|
});
|
|
151
157
|
}
|
|
152
|
-
const toDispose = [...this.clients.
|
|
153
|
-
for (const client of toDispose) {
|
|
158
|
+
const toDispose = [...this.clients.entries()];
|
|
159
|
+
for (const [key, client] of toDispose) {
|
|
160
|
+
const extras = this.clientExtras.get(key) ?? [];
|
|
161
|
+
this.clients.delete(key);
|
|
162
|
+
this.clientExtras.delete(key);
|
|
154
163
|
client.dispose();
|
|
164
|
+
this.events.emit('disconnect', client, extras);
|
|
155
165
|
}
|
|
156
166
|
this.clients.clear();
|
|
167
|
+
this.clientExtras.clear();
|
|
157
168
|
this.wss = undefined;
|
|
158
169
|
this.port = undefined;
|
|
159
170
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/micro",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -23,12 +23,11 @@
|
|
|
23
23
|
"vitest": "^4.0.18"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@hile/logger": "
|
|
27
|
-
"@hile/message-loader": "
|
|
28
|
-
"@hile/message-ws": "
|
|
26
|
+
"@hile/logger": "workspace:^",
|
|
27
|
+
"@hile/message-loader": "workspace:^",
|
|
28
|
+
"@hile/message-ws": "workspace:^",
|
|
29
29
|
"internal-ip": "^9.0.0",
|
|
30
30
|
"ws": "^8.21.0",
|
|
31
31
|
"yaml": "^2.9.0"
|
|
32
|
-
}
|
|
33
|
-
"gitHead": "cf057f1833ad144a6d7812d2d190d3a9d903429f"
|
|
32
|
+
}
|
|
34
33
|
}
|