@hile/micro 2.0.8 → 2.1.1

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.
@@ -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
- watchEnvFile(): import("node:fs").FSWatcher | undefined;
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 中的关联(不删除 topic,保留 data 供后续 subscriber 使用)
68
- for (const [, { publishers, subscribers }] of this.topics) {
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
- return;
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, { publishers: new Set(), subscribers: new Set(), data: config[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, { publishers: new Set(), subscribers: new Set(), data: config[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, { publishers: new Set(), subscribers: new Set(), data: data.payload });
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 Date.now();
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
- if (publishers.size === 0 && subscribers.size === 0) {
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, { publishers: new Set(), subscribers: new Set(), data: undefined });
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
- this.publish(data.topic, data.payload);
249
- return Date.now();
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
- publish(topic, payload) {
253
- if (!this.topics.has(topic))
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.values()];
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.8",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -23,12 +23,12 @@
23
23
  "vitest": "^4.0.18"
24
24
  },
25
25
  "dependencies": {
26
- "@hile/logger": "^2.0.3",
27
- "@hile/message-loader": "^2.0.2",
28
- "@hile/message-ws": "^2.0.4",
26
+ "@hile/logger": "^2.1.1",
27
+ "@hile/message-loader": "^2.1.1",
28
+ "@hile/message-ws": "^2.1.1",
29
29
  "internal-ip": "^9.0.0",
30
30
  "ws": "^8.21.0",
31
31
  "yaml": "^2.9.0"
32
32
  },
33
- "gitHead": "a7615000fcb87e6bc0e573af760c814ec935bab2"
33
+ "gitHead": "7903ae989bd001d1ed1437cb90c9e828a1909061"
34
34
  }