@hile/micro 1.0.3 → 1.0.4
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 +18 -6
- package/SKILL.md +23 -19
- package/dist/application.d.ts +9 -3
- package/dist/application.js +104 -14
- package/dist/registry.d.ts +7 -3
- package/dist/registry.js +31 -7
- package/dist/server.d.ts +10 -2
- package/dist/server.js +25 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
基于 `@hile/message-loader` 与 `@hile/message-ws` 的轻量级 **WebSocket 服务注册与发现**:用固定格式的连接 URL 标识对端,`Registry` 按逻辑 namespace 记录实例,`Application` 从注册中心拉取地址并缓存到远端服务的会话。
|
|
4
4
|
|
|
5
|
+
## 概念分层
|
|
6
|
+
|
|
7
|
+
1. **`Server`**:实现「服务」的 **底层协议与运行时**——基于 `ws` 监听、按路径解析对端身份,对内用 `MessageLoader` 做 `{ url, data }` 路由;不关心注册中心,只负责连接语义与消息派发。
|
|
8
|
+
2. **`Registry`**:**注册中心**,维护「逻辑 namespace → 一组实例地址」,`/-/find` 随机返回其一。
|
|
9
|
+
3. **`Application`**:**基于 `Server` 的具体应用侧实现**——继承 `Server`,在 `listen` 后自动连上 `Registry`,用 `get(namespace)` 发现其它服务并缓存 `Client`。
|
|
10
|
+
|
|
11
|
+
同一进程里通常只建一个 `Application`,它 **同时** 扮演 **provider**(`register` 暴露接口)与 **consumer**(`get` + `request`/`push` 调用其它 namespace);文档里把「提供方 / 调用方」拆开示例,是为了说明两种用法,而不是两类不同的类。
|
|
12
|
+
|
|
5
13
|
## 安装
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -16,16 +24,18 @@ pnpm add @hile/micro
|
|
|
16
24
|
|
|
17
25
|
| 组件 | 作用 |
|
|
18
26
|
|------|------|
|
|
19
|
-
| `Server` |
|
|
27
|
+
| `Server` | **底层协议**:监听 WebSocket;解析路径中的调用方地址与可选分段;对内用 `register`/`dispatch` 处理 `{ url, data }` |
|
|
20
28
|
| `Client` | 连到远端 `Server`,`request`/`push` 走 `MessageModem`,`dispose()` 会关闭底层连接 |
|
|
21
|
-
| `Registry` |
|
|
22
|
-
| `Application` |
|
|
29
|
+
| `Registry` | **注册中心**:固定 namespace `'registry'`;实例上线/下线更新 namespace→地址集合;`/-/find` **随机** 返回其中一个地址 |
|
|
30
|
+
| `Application` | **基于 `Server` 的应用实现**:启动后连接 Registry;`register` 侧即 provider、`get` 侧即 consumer;`get(ns)` 查询并 **缓存** 到目标服务的 `Client`,断连后清空缓存 |
|
|
23
31
|
|
|
24
32
|
连接串格式(出站):
|
|
25
33
|
|
|
26
|
-
`ws://目标主机:端口/{
|
|
34
|
+
`ws://目标主机:端口/{宣告地址}/{本机监听端口}/{本机namespace}`
|
|
35
|
+
|
|
36
|
+
宣告地址:构造 `Server` / `Application` / `Registry` 时可通过 **`advertiseHost`** 显式传入;未传则使用 `getLocalIPv4()`。若二者皆无(例如无可用 IPv4),**构造阶段即抛错**。容器、多网卡或 CI 环境建议设置 `advertiseHost`(如 `127.0.0.1` 或 Pod IP)。
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
`Application` 还支持 **`registryLookupTimeoutMs`**(默认 `10000`),限制对 Registry `/-/find` 的响应等待时间。
|
|
29
39
|
|
|
30
40
|
出站 `connect` 默认 **5 秒**握手超时,超时报错 `Connection timeout`。
|
|
31
41
|
|
|
@@ -38,7 +48,7 @@ pnpm add @hile/micro
|
|
|
38
48
|
```typescript
|
|
39
49
|
import { Registry } from '@hile/micro';
|
|
40
50
|
|
|
41
|
-
const registry = new Registry();
|
|
51
|
+
const registry = new Registry({ advertiseHost: '127.0.0.1' });
|
|
42
52
|
await registry.listen(9000);
|
|
43
53
|
```
|
|
44
54
|
|
|
@@ -50,6 +60,7 @@ import { Application } from '@hile/micro';
|
|
|
50
60
|
const provider = new Application({
|
|
51
61
|
namespace: 'payments',
|
|
52
62
|
registry: { host: '127.0.0.1', port: 9000 },
|
|
63
|
+
advertiseHost: '127.0.0.1',
|
|
53
64
|
});
|
|
54
65
|
|
|
55
66
|
await provider.listen(9100);
|
|
@@ -67,6 +78,7 @@ import { Application } from '@hile/micro';
|
|
|
67
78
|
const consumer = new Application({
|
|
68
79
|
namespace: 'checkout',
|
|
69
80
|
registry: { host: '127.0.0.1', port: 9000 },
|
|
81
|
+
advertiseHost: '127.0.0.1',
|
|
70
82
|
});
|
|
71
83
|
|
|
72
84
|
await consumer.listen(9200);
|
package/SKILL.md
CHANGED
|
@@ -11,20 +11,22 @@ description: Code generation and contribution rules for @hile/micro. Use when ed
|
|
|
11
11
|
|
|
12
12
|
## 1. 架构总览
|
|
13
13
|
|
|
14
|
-
`@hile/micro` 在 `@hile/message-loader` + `@hile/message-ws` 之上提供
|
|
14
|
+
`@hile/micro` 在 `@hile/message-loader` + `@hile/message-ws` 之上提供 **轻量级进程间服务发现与会话**。分层理解:
|
|
15
15
|
|
|
16
|
-
- **`Server
|
|
16
|
+
- **`Server`**:实现服务的 **底层协议与运行时**——`WebSocketServer` + 路径约定;对内 `MessageLoader.register` / `dispatch` 完成 `{ url, data }` 路由。不包含注册中心逻辑。
|
|
17
|
+
- **`Registry`**:**注册中心**,固定 `namespace` 为 `'registry'`,维护「逻辑 namespace → 一组 `host:port`」;`/-/find` 在集合中 **随机** 返回一条地址(见 `selectRandomRegistryAddress`)。
|
|
18
|
+
- **`Application`**:**基于 `Server` 的应用侧实现**(`extends Server`);`listen(port)` 后自动 `connect` 到注册中心;`get(targetNamespace)` 向注册中心查询并 **缓存** 到目标服务的 `Client`,目标断连后删除缓存以便下次重新发现。
|
|
17
19
|
- **`Client`**:连接到远端 `Server`,`request(url, data)` / `push(url, data)` 将负载交给本端 `Server.dispatch`;`dispose()` 会移除监听并 **关闭底层 WebSocket**,避免 `listen` 关闭阶段挂住。
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
**应用模型**:`Application = provider + consumer`——同一实例既可 `register(...)`(对外提供能力),也可 `get(ns)` 再 `request`/`push`(消费其它 namespace)。文档示例常拆成两个进程分别演示 provider / consumer,API 层面仍是同一个类。
|
|
20
22
|
|
|
21
23
|
依赖链:
|
|
22
24
|
|
|
23
25
|
```
|
|
24
26
|
MessageLoader (路由) + MessageWs (请求/响应传输)
|
|
25
|
-
└── Server
|
|
26
|
-
├── Registry
|
|
27
|
-
└── Application
|
|
27
|
+
└── Server(底层协议)+ Client
|
|
28
|
+
├── Registry(注册中心,一种特殊的 Server 用法)
|
|
29
|
+
└── Application(基于 Server,叠加上注册发现)
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
---
|
|
@@ -62,27 +64,28 @@ ws://{host}:{port}/${this.ipv4}/${this.port}/${this.namespace}
|
|
|
62
64
|
## 3. 类型与关键 API(生成代码须一致)
|
|
63
65
|
|
|
64
66
|
```typescript
|
|
65
|
-
//
|
|
66
|
-
export
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
// server.ts
|
|
68
|
+
export type MicroServerProps = MessageLoaderProps & {
|
|
69
|
+
/** 出站 URL 宣告段;缺省 getLocalIPv4(),皆无则构造抛错 */
|
|
70
|
+
advertiseHost?: string;
|
|
71
|
+
};
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
port: number;
|
|
73
|
-
}
|
|
73
|
+
// registry.ts
|
|
74
|
+
export function parseAddressKey(key: string): RegistryAddress | undefined;
|
|
74
75
|
|
|
75
76
|
export function selectRandomRegistryAddress(
|
|
76
77
|
keys: Iterable<string>,
|
|
77
78
|
): RegistryAddress | undefined;
|
|
78
79
|
|
|
79
|
-
export class Registry extends Server { /*
|
|
80
|
+
export class Registry extends Server { /* constructor(props?: MicroServerProps); onFind() 幂等 */ }
|
|
80
81
|
|
|
81
82
|
// application.ts
|
|
82
83
|
export type ApplicationProps = {
|
|
83
84
|
namespace: string;
|
|
84
85
|
registry: RegistryAddress;
|
|
85
|
-
|
|
86
|
+
/** 默认 10000;对 `/-/find` 的 response 等待上限 */
|
|
87
|
+
registryLookupTimeoutMs?: number;
|
|
88
|
+
} & MicroServerProps;
|
|
86
89
|
|
|
87
90
|
export class Application extends Server {
|
|
88
91
|
listen(port: number): Promise<() => Promise<void>>;
|
|
@@ -109,7 +112,7 @@ export class Client extends MessageWs {
|
|
|
109
112
|
### 4.1 Registry 服务端
|
|
110
113
|
|
|
111
114
|
```typescript
|
|
112
|
-
const registry = new Registry();
|
|
115
|
+
const registry = new Registry({ advertiseHost: '127.0.0.1' });
|
|
113
116
|
const dispose = await registry.listen(registryPort);
|
|
114
117
|
// shutdown: await dispose();
|
|
115
118
|
```
|
|
@@ -120,6 +123,7 @@ const dispose = await registry.listen(registryPort);
|
|
|
120
123
|
const app = new Application({
|
|
121
124
|
namespace: 'my-service',
|
|
122
125
|
registry: { host: '127.0.0.1', port: registryPort },
|
|
126
|
+
advertiseHost: '127.0.0.1',
|
|
123
127
|
});
|
|
124
128
|
const dispose = await app.listen(appPort);
|
|
125
129
|
|
|
@@ -147,7 +151,7 @@ const result = await response();
|
|
|
147
151
|
- 修改 WebSocket URL 三段式约定却不同时更新 **`Server.onConnected`** 与 **`Server.connect`** 的拼接格式。
|
|
148
152
|
- 在 `Registry` 中按 `Set` **迭代顺序** 固定返回「第一个」实例(破坏负载分散);除非你明确改需求并改写测试。
|
|
149
153
|
- `Client`/`Server` **`dispose`** 后不关闭 **`ws`**(会导致 **`WebSocketServer.close`** 长时间等待);本包已在 `Client.dispose` 内关闭socket,不要随意删除。
|
|
150
|
-
- 假设 `host:port` 串可无损表达 **IPv6
|
|
154
|
+
- 假设 `host:port` 串可无损表达 **IPv6**:注册表 Set 的键应使用 **`[IPv6]:port`**;`selectRandomRegistryAddress` / `parseAddressKey` 按 **最后一个 `:`** 切分 host 与 port。
|
|
151
155
|
- `Application.props.registry` **不要传错端口**;丢失与注册中心的连接时依赖 `reconnectToRegistry`,不要在外部缓存 `registry` Client 绕过重连语义。
|
|
152
156
|
|
|
153
157
|
---
|
package/dist/application.d.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { Client } from './client.js';
|
|
2
|
-
import { Server } from './server.js';
|
|
3
|
-
import { MessageLoaderProps } from '@hile/message-loader';
|
|
2
|
+
import { Server, type MicroServerProps } from './server.js';
|
|
4
3
|
import { RegistryAddress } from './registry.js';
|
|
5
4
|
export type ApplicationProps = {
|
|
6
5
|
namespace: string;
|
|
7
6
|
registry: RegistryAddress;
|
|
8
|
-
|
|
7
|
+
/** `/-/find` 等待响应的上限(毫秒),默认 `10000` */
|
|
8
|
+
registryLookupTimeoutMs?: number;
|
|
9
|
+
} & MicroServerProps;
|
|
9
10
|
export declare class Application extends Server {
|
|
10
11
|
private registry?;
|
|
11
12
|
private reconnectTimeout?;
|
|
13
|
+
private registryReconnectPromise;
|
|
14
|
+
/** 为 true 时不再向 Registry 重连(listen 返回的 teardown 已触发) */
|
|
15
|
+
private stopped;
|
|
12
16
|
private readonly _registry_address;
|
|
17
|
+
private readonly _registryLookupTimeoutMs;
|
|
13
18
|
private readonly namespaces;
|
|
14
19
|
constructor(props: ApplicationProps);
|
|
15
20
|
listen(port?: number): Promise<() => Promise<void>>;
|
|
21
|
+
private scheduleRegistryRetry;
|
|
16
22
|
private reconnectToRegistry;
|
|
17
23
|
private findFromRegistry;
|
|
18
24
|
get(namespace: string): Promise<Client>;
|
package/dist/application.js
CHANGED
|
@@ -5,43 +5,126 @@ var RegistryLookupStatus;
|
|
|
5
5
|
RegistryLookupStatus[RegistryLookupStatus["PENDING"] = 1] = "PENDING";
|
|
6
6
|
RegistryLookupStatus[RegistryLookupStatus["READY"] = 2] = "READY";
|
|
7
7
|
})(RegistryLookupStatus || (RegistryLookupStatus = {}));
|
|
8
|
+
function assertValidRegistrySocket(meta, host, port) {
|
|
9
|
+
if (typeof host !== 'string' || !host || host.length > 253) {
|
|
10
|
+
throw new Error(`Invalid ${meta}: empty or oversized host`);
|
|
11
|
+
}
|
|
12
|
+
if (/[\s\r\n\0]/.test(host))
|
|
13
|
+
throw new Error(`Invalid ${meta}: host contains whitespace`);
|
|
14
|
+
if (!Number.isFinite(port) || port !== Math.trunc(port) || port < 1 || port > 65535) {
|
|
15
|
+
throw new Error(`Invalid ${meta}: port must be integer 1..65535`);
|
|
16
|
+
}
|
|
17
|
+
if (host.includes(':') && !host.startsWith('[')) {
|
|
18
|
+
throw new Error(`Invalid ${meta}: IPv6 host must be bracketed (e.g. [::1])`);
|
|
19
|
+
}
|
|
20
|
+
if (host.includes('/') || host.includes('?')) {
|
|
21
|
+
throw new Error(`Invalid ${meta}: illegal host characters`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function withTimeout(promise, ms, label) {
|
|
25
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
26
|
+
return promise;
|
|
27
|
+
let timer;
|
|
28
|
+
return Promise.race([
|
|
29
|
+
promise,
|
|
30
|
+
new Promise((_, reject) => {
|
|
31
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
32
|
+
}),
|
|
33
|
+
]).finally(() => {
|
|
34
|
+
if (timer)
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
8
38
|
export class Application extends Server {
|
|
9
39
|
registry;
|
|
10
40
|
reconnectTimeout;
|
|
41
|
+
registryReconnectPromise;
|
|
42
|
+
/** 为 true 时不再向 Registry 重连(listen 返回的 teardown 已触发) */
|
|
43
|
+
stopped = false;
|
|
11
44
|
_registry_address;
|
|
45
|
+
_registryLookupTimeoutMs;
|
|
12
46
|
namespaces = new Map();
|
|
13
47
|
constructor(props) {
|
|
14
|
-
const { namespace, registry, ...
|
|
15
|
-
super(namespace,
|
|
48
|
+
const { namespace, registry, registryLookupTimeoutMs = 10_000, ...microAndLoader } = props;
|
|
49
|
+
super(namespace, microAndLoader);
|
|
50
|
+
assertValidRegistrySocket('registry address', registry.host, registry.port);
|
|
16
51
|
this._registry_address = registry;
|
|
52
|
+
this._registryLookupTimeoutMs = registryLookupTimeoutMs;
|
|
17
53
|
}
|
|
18
54
|
async listen(port = 0) {
|
|
55
|
+
this.stopped = false;
|
|
19
56
|
const callback = await super.listen(port);
|
|
20
|
-
|
|
57
|
+
try {
|
|
58
|
+
await this.reconnectToRegistry();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
try {
|
|
62
|
+
await callback();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// ignore secondary errors from teardown
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
21
69
|
return async () => {
|
|
22
|
-
|
|
70
|
+
this.stopped = true;
|
|
71
|
+
if (this.reconnectTimeout) {
|
|
23
72
|
clearTimeout(this.reconnectTimeout);
|
|
73
|
+
this.reconnectTimeout = undefined;
|
|
74
|
+
}
|
|
75
|
+
this.registry = undefined;
|
|
24
76
|
await callback();
|
|
25
77
|
};
|
|
26
78
|
}
|
|
79
|
+
scheduleRegistryRetry() {
|
|
80
|
+
if (this.stopped)
|
|
81
|
+
return;
|
|
82
|
+
if (this.reconnectTimeout)
|
|
83
|
+
clearTimeout(this.reconnectTimeout);
|
|
84
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
85
|
+
this.reconnectTimeout = undefined;
|
|
86
|
+
void this.reconnectToRegistry().catch(() => {
|
|
87
|
+
if (this.stopped)
|
|
88
|
+
return;
|
|
89
|
+
this.scheduleRegistryRetry();
|
|
90
|
+
});
|
|
91
|
+
}, 3000);
|
|
92
|
+
}
|
|
27
93
|
async reconnectToRegistry() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
94
|
+
if (this.stopped)
|
|
95
|
+
return;
|
|
96
|
+
if (this.registryReconnectPromise)
|
|
97
|
+
return this.registryReconnectPromise;
|
|
98
|
+
this.registryReconnectPromise = (async () => {
|
|
99
|
+
const registry = await this.connect(this._registry_address.host, this._registry_address.port);
|
|
100
|
+
if (this.stopped) {
|
|
101
|
+
registry.dispose();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
registry.events.once('disconnect', () => {
|
|
105
|
+
if (this.registry !== registry)
|
|
106
|
+
return;
|
|
107
|
+
this.registry = undefined;
|
|
108
|
+
if (this.stopped)
|
|
109
|
+
return;
|
|
110
|
+
void this.reconnectToRegistry().catch(() => {
|
|
111
|
+
if (this.stopped)
|
|
112
|
+
return;
|
|
113
|
+
this.scheduleRegistryRetry();
|
|
34
114
|
});
|
|
35
|
-
};
|
|
36
|
-
|
|
115
|
+
});
|
|
116
|
+
this.registry = registry;
|
|
117
|
+
})().finally(() => {
|
|
118
|
+
this.registryReconnectPromise = undefined;
|
|
37
119
|
});
|
|
38
|
-
this.
|
|
120
|
+
return this.registryReconnectPromise;
|
|
39
121
|
}
|
|
40
122
|
async findFromRegistry(namespace) {
|
|
41
123
|
if (!this.registry)
|
|
42
124
|
throw new Error('Registry not found');
|
|
43
125
|
const { response } = this.registry.request('/-/find', { namespace });
|
|
44
|
-
|
|
126
|
+
const p = response();
|
|
127
|
+
return await withTimeout(p, this._registryLookupTimeoutMs, 'Registry /-/find');
|
|
45
128
|
}
|
|
46
129
|
get(namespace) {
|
|
47
130
|
if (!this.namespaces.has(namespace)) {
|
|
@@ -53,6 +136,12 @@ export class Application extends Server {
|
|
|
53
136
|
});
|
|
54
137
|
}
|
|
55
138
|
const stack = this.namespaces.get(namespace);
|
|
139
|
+
if (stack.status === RegistryLookupStatus.READY &&
|
|
140
|
+
!this.clients.has(`${stack.host}:${stack.port}`)) {
|
|
141
|
+
stack.status = RegistryLookupStatus.IDLE;
|
|
142
|
+
stack.host = '';
|
|
143
|
+
stack.port = 0;
|
|
144
|
+
}
|
|
56
145
|
const key = `${stack.host}:${stack.port}`;
|
|
57
146
|
if (stack.status === RegistryLookupStatus.READY && this.clients.has(key)) {
|
|
58
147
|
return Promise.resolve(this.clients.get(key));
|
|
@@ -64,6 +153,7 @@ export class Application extends Server {
|
|
|
64
153
|
this.findFromRegistry(namespace).then(data => {
|
|
65
154
|
if (!data)
|
|
66
155
|
return Promise.reject(new Error('Namespace not found'));
|
|
156
|
+
assertValidRegistrySocket('peer address from registry', data.host, data.port);
|
|
67
157
|
return this.connect(data.host, data.port).then(client => {
|
|
68
158
|
stack.host = data.host;
|
|
69
159
|
stack.port = data.port;
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Server } from './server.js';
|
|
1
|
+
import { Server, type MicroServerProps } from './server.js';
|
|
3
2
|
export interface RegistryFindData {
|
|
4
3
|
namespace: string;
|
|
5
4
|
}
|
|
@@ -7,9 +6,14 @@ export interface RegistryAddress {
|
|
|
7
6
|
host: string;
|
|
8
7
|
port: number;
|
|
9
8
|
}
|
|
9
|
+
/** 将 `host:port` 或 `[ipv6]:port` 形式的 key 解析为地址(端口取最后一个 `:` 之后) */
|
|
10
|
+
export declare function parseAddressKey(key: string): RegistryAddress | undefined;
|
|
10
11
|
export declare function selectRandomRegistryAddress(keys: Iterable<string>): RegistryAddress | undefined;
|
|
11
12
|
export declare class Registry extends Server {
|
|
12
13
|
private readonly namespaces;
|
|
13
|
-
|
|
14
|
+
private unregisterFind?;
|
|
15
|
+
constructor(props?: MicroServerProps);
|
|
16
|
+
/** 幂等:重复调用会先注销上一条 `/-/find` 再注册,避免叠多条路由 */
|
|
14
17
|
onFind(): void;
|
|
18
|
+
private mountFindHandler;
|
|
15
19
|
}
|
package/dist/registry.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { Server } from './server.js';
|
|
2
|
+
/** 将 `host:port` 或 `[ipv6]:port` 形式的 key 解析为地址(端口取最后一个 `:` 之后) */
|
|
3
|
+
export function parseAddressKey(key) {
|
|
4
|
+
const i = key.lastIndexOf(':');
|
|
5
|
+
if (i <= 0 || i >= key.length - 1)
|
|
6
|
+
return undefined;
|
|
7
|
+
const host = key.slice(0, i);
|
|
8
|
+
const port = Number(key.slice(i + 1));
|
|
9
|
+
if (!host ||
|
|
10
|
+
!Number.isFinite(port) ||
|
|
11
|
+
port !== Math.trunc(port) ||
|
|
12
|
+
port < 1 ||
|
|
13
|
+
port > 65535) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return { host, port };
|
|
17
|
+
}
|
|
2
18
|
export function selectRandomRegistryAddress(keys) {
|
|
3
|
-
const addresses = Array.from(keys)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
});
|
|
19
|
+
const addresses = Array.from(keys)
|
|
20
|
+
.map(parseAddressKey)
|
|
21
|
+
.filter((a) => a !== undefined);
|
|
7
22
|
if (addresses.length === 0)
|
|
8
23
|
return;
|
|
9
24
|
const index = Math.floor(Math.random() * addresses.length);
|
|
@@ -11,8 +26,9 @@ export function selectRandomRegistryAddress(keys) {
|
|
|
11
26
|
}
|
|
12
27
|
export class Registry extends Server {
|
|
13
28
|
namespaces = new Map();
|
|
29
|
+
unregisterFind;
|
|
14
30
|
constructor(props) {
|
|
15
|
-
super('registry', props);
|
|
31
|
+
super('registry', props ?? {});
|
|
16
32
|
this.events.on('connect', (client, extras) => {
|
|
17
33
|
const key = client.host + ':' + client.port;
|
|
18
34
|
const namespace = extras.join('/');
|
|
@@ -34,10 +50,18 @@ export class Registry extends Server {
|
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
});
|
|
37
|
-
this.
|
|
53
|
+
this.mountFindHandler();
|
|
38
54
|
}
|
|
55
|
+
/** 幂等:重复调用会先注销上一条 `/-/find` 再注册,避免叠多条路由 */
|
|
39
56
|
onFind() {
|
|
40
|
-
this.
|
|
57
|
+
this.mountFindHandler();
|
|
58
|
+
}
|
|
59
|
+
mountFindHandler() {
|
|
60
|
+
if (this.unregisterFind) {
|
|
61
|
+
this.unregisterFind();
|
|
62
|
+
this.unregisterFind = undefined;
|
|
63
|
+
}
|
|
64
|
+
this.unregisterFind = this.register('/-/find', async ({ data }) => {
|
|
41
65
|
const namespace = data.namespace;
|
|
42
66
|
const keys = this.namespaces.get(namespace);
|
|
43
67
|
if (!keys)
|
package/dist/server.d.ts
CHANGED
|
@@ -3,14 +3,22 @@ import { Client } from './client.js';
|
|
|
3
3
|
import { IncomingMessage } from 'http';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import type { Duplex } from "node:stream";
|
|
6
|
+
/** {@link MessageLoaderProps} 加上出站 WebSocket 宣告地址 */
|
|
7
|
+
export type MicroServerProps = MessageLoaderProps & {
|
|
8
|
+
/**
|
|
9
|
+
* 出站连接 URL 中 `ws://{host}:{port}/{本段}/...` 的「本段」宣告地址。
|
|
10
|
+
* 缺省使用 `getLocalIPv4()`;若仍为 `undefined`(无可用 IPv4)则构造 {@link Server} 时抛错。
|
|
11
|
+
*/
|
|
12
|
+
advertiseHost?: string;
|
|
13
|
+
};
|
|
6
14
|
export declare class Server extends MessageLoader {
|
|
7
15
|
private readonly namespace;
|
|
8
16
|
private wss?;
|
|
9
17
|
port?: number;
|
|
10
18
|
protected readonly clients: Map<string, Client>;
|
|
11
|
-
private readonly
|
|
19
|
+
private readonly announceHost;
|
|
12
20
|
readonly events: EventEmitter<any>;
|
|
13
|
-
constructor(namespace: string, props?:
|
|
21
|
+
constructor(namespace: string, props?: MicroServerProps);
|
|
14
22
|
private upstream;
|
|
15
23
|
private createClient;
|
|
16
24
|
protected connect(host: string, port: number, timeout?: number): Promise<Client>;
|
package/dist/server.js
CHANGED
|
@@ -10,11 +10,17 @@ export class Server extends MessageLoader {
|
|
|
10
10
|
wss;
|
|
11
11
|
port;
|
|
12
12
|
clients = new Map();
|
|
13
|
-
|
|
13
|
+
announceHost;
|
|
14
14
|
events = new EventEmitter();
|
|
15
15
|
constructor(namespace, props = {}) {
|
|
16
|
-
|
|
16
|
+
const { advertiseHost, ...loaderProps } = props;
|
|
17
|
+
super(loaderProps);
|
|
17
18
|
this.namespace = namespace;
|
|
19
|
+
const resolved = advertiseHost?.trim() || getLocalIPv4();
|
|
20
|
+
if (!resolved) {
|
|
21
|
+
throw new Error('Unable to resolve advertise host for @hile/micro Server: pass `advertiseHost` (e.g. "127.0.0.1") in constructor options, or ensure getLocalIPv4() returns an address.');
|
|
22
|
+
}
|
|
23
|
+
this.announceHost = resolved;
|
|
18
24
|
this.events.on('connect', (client, extras) => {
|
|
19
25
|
client.events.emit('connect', extras);
|
|
20
26
|
});
|
|
@@ -34,13 +40,25 @@ export class Server extends MessageLoader {
|
|
|
34
40
|
const [host, port, ...extras] = sp;
|
|
35
41
|
if (!host || !port)
|
|
36
42
|
return ws.close();
|
|
37
|
-
|
|
43
|
+
const portNum = Number(port);
|
|
44
|
+
if (!Number.isFinite(portNum) ||
|
|
45
|
+
portNum !== Math.trunc(portNum) ||
|
|
46
|
+
portNum < 1 ||
|
|
47
|
+
portNum > 65535) {
|
|
48
|
+
return ws.close();
|
|
49
|
+
}
|
|
50
|
+
this.createClient(ws, host, portNum, extras);
|
|
38
51
|
}
|
|
39
52
|
createClient(ws, host, port, extras = []) {
|
|
40
53
|
const key = `${host}:${port}`;
|
|
54
|
+
const previous = this.clients.get(key);
|
|
55
|
+
if (previous) {
|
|
56
|
+
this.clients.delete(key);
|
|
57
|
+
previous.dispose();
|
|
58
|
+
}
|
|
41
59
|
const client = new Client({ server: this, ws, host, port });
|
|
42
60
|
ws.on('close', () => {
|
|
43
|
-
if (this.clients.
|
|
61
|
+
if (this.clients.get(key) === client) {
|
|
44
62
|
this.clients.delete(key);
|
|
45
63
|
}
|
|
46
64
|
client.dispose();
|
|
@@ -58,7 +76,7 @@ export class Server extends MessageLoader {
|
|
|
58
76
|
if (!this.port)
|
|
59
77
|
throw new Error('You can not connect to a server without a local port, please use `.setPort(port)` for local port.');
|
|
60
78
|
const ws = await new Promise((resolve, reject) => {
|
|
61
|
-
const ws = new WebSocket(`ws://${host}:${port}/${this.
|
|
79
|
+
const ws = new WebSocket(`ws://${host}:${port}/${this.announceHost}/${this.port}/${this.namespace}`);
|
|
62
80
|
const timer = setTimeout(() => {
|
|
63
81
|
clear();
|
|
64
82
|
ws.on('error', () => { });
|
|
@@ -112,7 +130,8 @@ export class Server extends MessageLoader {
|
|
|
112
130
|
this.wss = new WebSocketServer({ noServer: true });
|
|
113
131
|
}
|
|
114
132
|
return async () => {
|
|
115
|
-
|
|
133
|
+
const toDispose = [...this.clients.values()];
|
|
134
|
+
for (const client of toDispose) {
|
|
116
135
|
client.dispose();
|
|
117
136
|
}
|
|
118
137
|
this.clients.clear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/micro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"internal-ip": "^9.0.0",
|
|
29
29
|
"ws": "^8.19.0"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "46d4b998c1fd2fd725bf484ca634f25b61f19cd5"
|
|
32
32
|
}
|