@hile/micro 1.0.2 → 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 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` | 监听 WebSocket;解析路径中的调用方地址与可选分段;对内用 `register`/`dispatch` 处理 `{ url, data }` |
27
+ | `Server` | **底层协议**:监听 WebSocket;解析路径中的调用方地址与可选分段;对内用 `register`/`dispatch` 处理 `{ url, data }` |
20
28
  | `Client` | 连到远端 `Server`,`request`/`push` 走 `MessageModem`,`dispose()` 会关闭底层连接 |
21
- | `Registry` | 固定 namespace `'registry'`;实例上线/下线更新 namespace→地址集合;`/-/find` **随机** 返回其中一个地址 |
22
- | `Application` | 启动后连接 Registry;`get(ns)` 查询并 **缓存** 到目标服务的 `Client`,断连后清空缓存 |
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://目标主机:端口/{本机广告IPv4}/{本机监听端口}/{本机namespace}`
34
+ `ws://目标主机:端口/{宣告地址}/{本机监听端口}/{本机namespace}`
35
+
36
+ 宣告地址:构造 `Server` / `Application` / `Registry` 时可通过 **`advertiseHost`** 显式传入;未传则使用 `getLocalIPv4()`。若二者皆无(例如无可用 IPv4),**构造阶段即抛错**。容器、多网卡或 CI 环境建议设置 `advertiseHost`(如 `127.0.0.1` 或 Pod IP)。
27
37
 
28
- 其中广告 IPv4 `getLocalIPv4()` 取第一个非回环网卡地址;多网卡或容器环境可能需要后续版本支持显式 `advertiseHost`(当前未暴露)。
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`**:基于 `ws` `WebSocketServer`,用 **URL 路径** 携带对端身份与命名空间;对内用 `MessageLoader.register` / `dispatch` 处理消息路由。
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
- - **`Registry`**:固定 `namespace` 为 `'registry'`,维护「逻辑 namespace → 一组 `host:port`」;`/-/find` 在集合中 **随机** 返回一条地址(见 `selectRandomRegistryAddress`)。
19
- - **`Application`**:`Server` 的子类;`listen(port)` 后自动 `connect` 到注册中心;`get(targetNamespace)` 向注册中心查询并 **缓存** 到目标服务的 `Client`,目标断连后删除缓存以便下次重新发现。
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 / Client
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
- // registry.ts
66
- export interface RegistryFindData {
67
- namespace: string;
68
- }
67
+ // server.ts
68
+ export type MicroServerProps = MessageLoaderProps & {
69
+ /** 出站 URL 宣告段;缺省 getLocalIPv4(),皆无则构造抛错 */
70
+ advertiseHost?: string;
71
+ };
69
72
 
70
- export interface RegistryAddress {
71
- host: string;
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
- } & MessageLoaderProps;
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**(当前实现按 **首个 `:` 分割**,仅适合 IPv4 或不含冒号的 host)。
154
+ - 假设 `host:port` 串可无损表达 **IPv6**:注册表 Set 的键应使用 **`[IPv6]:port`**;`selectRandomRegistryAddress` / `parseAddressKey` 按 **最后一个 `:`** 切分 host 与 port。
151
155
  - `Application.props.registry` **不要传错端口**;丢失与注册中心的连接时依赖 `reconnectToRegistry`,不要在外部缓存 `registry` Client 绕过重连语义。
152
156
 
153
157
  ---
@@ -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
- } & MessageLoaderProps;
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
- listen(port: number): Promise<() => Promise<void>>;
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>;
@@ -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, ...loaderProps } = props;
15
- super(namespace, loaderProps);
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
- async listen(port) {
54
+ async listen(port = 0) {
55
+ this.stopped = false;
19
56
  const callback = await super.listen(port);
20
- await this.reconnectToRegistry();
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
- if (this.reconnectTimeout)
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
- const registry = await this.connect(this._registry_address.host, this._registry_address.port);
29
- registry.events.on('disconnect', () => {
30
- this.registry = undefined;
31
- const reconnect = () => {
32
- this.reconnectToRegistry().catch(e => {
33
- this.reconnectTimeout = setTimeout(reconnect, 3000);
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
- reconnect();
115
+ });
116
+ this.registry = registry;
117
+ })().finally(() => {
118
+ this.registryReconnectPromise = undefined;
37
119
  });
38
- this.registry = registry;
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
- return await response();
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;
@@ -1,5 +1,4 @@
1
- import { MessageLoaderProps } from '@hile/message-loader';
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
- constructor(props?: MessageLoaderProps);
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).map((key) => {
4
- const [host, port] = key.split(':');
5
- return { host, port: Number(port) };
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.onFind();
53
+ this.mountFindHandler();
38
54
  }
55
+ /** 幂等:重复调用会先注销上一条 `/-/find` 再注册,避免叠多条路由 */
39
56
  onFind() {
40
- this.register('/-/find', async ({ data }) => {
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
@@ -1,16 +1,28 @@
1
1
  import { MessageLoader, MessageLoaderProps } from "@hile/message-loader";
2
2
  import { Client } from './client.js';
3
+ import { IncomingMessage } from 'http';
3
4
  import { EventEmitter } from 'node:events';
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
+ };
4
14
  export declare class Server extends MessageLoader {
5
15
  private readonly namespace;
6
16
  private wss?;
7
17
  port?: number;
8
18
  protected readonly clients: Map<string, Client>;
9
- private readonly ipv4;
19
+ private readonly announceHost;
10
20
  readonly events: EventEmitter<any>;
11
- constructor(namespace: string, props?: MessageLoaderProps);
12
- private onConnected;
13
- private onRegister;
21
+ constructor(namespace: string, props?: MicroServerProps);
22
+ private upstream;
23
+ private createClient;
14
24
  protected connect(host: string, port: number, timeout?: number): Promise<Client>;
15
25
  listen(port?: number): Promise<() => Promise<void>>;
26
+ setPort(port: number): this;
27
+ handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): this;
16
28
  }
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
- ipv4 = getLocalIPv4();
13
+ announceHost;
14
14
  events = new EventEmitter();
15
15
  constructor(namespace, props = {}) {
16
- super(props);
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
  });
@@ -22,7 +28,7 @@ export class Server extends MessageLoader {
22
28
  client.events.emit('disconnect', extras);
23
29
  });
24
30
  }
25
- onConnected(ws, req) {
31
+ upstream(ws, req) {
26
32
  const path = req.url?.split('?')[0];
27
33
  if (!path)
28
34
  return ws.close();
@@ -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
- this.onRegister(ws, host, Number(port), extras);
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
- onRegister(ws, host, port, extras = []) {
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.has(key)) {
61
+ if (this.clients.get(key) === client) {
44
62
  this.clients.delete(key);
45
63
  }
46
64
  client.dispose();
@@ -55,8 +73,10 @@ export class Server extends MessageLoader {
55
73
  if (this.clients.has(key)) {
56
74
  return this.clients.get(key);
57
75
  }
76
+ if (!this.port)
77
+ throw new Error('You can not connect to a server without a local port, please use `.setPort(port)` for local port.');
58
78
  const ws = await new Promise((resolve, reject) => {
59
- const ws = new WebSocket(`ws://${host}:${port}/${this.ipv4}/${this.port}/${this.namespace}`);
79
+ const ws = new WebSocket(`ws://${host}:${port}/${this.announceHost}/${this.port}/${this.namespace}`);
60
80
  const timer = setTimeout(() => {
61
81
  clear();
62
82
  ws.on('error', () => { });
@@ -82,36 +102,40 @@ export class Server extends MessageLoader {
82
102
  ws.on('open', onopen);
83
103
  ws.on('error', onerror);
84
104
  });
85
- return this.onRegister(ws, host, port);
105
+ return this.createClient(ws, host, port);
86
106
  }
87
- async listen(port) {
88
- this.wss = await new Promise((resolve, reject) => {
107
+ async listen(port = 0) {
108
+ if (port > 0) {
89
109
  const wss = new WebSocketServer({ port });
90
- const clear = () => {
91
- wss.off('error', onerror);
92
- wss.off('listening', onlistening);
93
- };
94
- const onerror = (err) => {
95
- clear();
96
- reject(err);
97
- };
98
- const onlistening = () => {
99
- clear();
100
- resolve(wss);
101
- };
102
- wss.on('error', onerror);
103
- wss.on('listening', onlistening);
104
- });
105
- const onConnection = (ws, req) => this.onConnected(ws, req);
106
- this.wss.on('connection', onConnection);
107
- this.port = port;
110
+ this.wss = await new Promise((resolve, reject) => {
111
+ const clear = () => {
112
+ wss.off('error', onerror);
113
+ wss.off('listening', onlistening);
114
+ };
115
+ const onerror = (err) => {
116
+ clear();
117
+ reject(err);
118
+ };
119
+ const onlistening = () => {
120
+ clear();
121
+ resolve(wss);
122
+ };
123
+ wss.on('error', onerror);
124
+ wss.on('listening', onlistening);
125
+ });
126
+ this.wss.on('connection', (ws, req) => this.upstream(ws, req));
127
+ this.setPort(port);
128
+ }
129
+ else {
130
+ this.wss = new WebSocketServer({ noServer: true });
131
+ }
108
132
  return async () => {
109
- for (const client of this.clients.values()) {
133
+ const toDispose = [...this.clients.values()];
134
+ for (const client of toDispose) {
110
135
  client.dispose();
111
136
  }
112
137
  this.clients.clear();
113
138
  if (this.wss) {
114
- this.wss.off('connection', onConnection);
115
139
  await new Promise((resolve, reject) => {
116
140
  this.wss.close((err) => {
117
141
  if (err)
@@ -124,4 +148,16 @@ export class Server extends MessageLoader {
124
148
  this.port = undefined;
125
149
  };
126
150
  }
151
+ setPort(port) {
152
+ this.port = port;
153
+ return this;
154
+ }
155
+ handleUpgrade(req, socket, head) {
156
+ if (!this.wss)
157
+ throw new Error('WebSocket server not initialized');
158
+ this.wss.handleUpgrade(req, socket, head, ws => {
159
+ this.upstream(ws, req);
160
+ });
161
+ return this;
162
+ }
127
163
  }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
- * 获取本机第一个非回环、非内部的 IPv4 地址;若无则返回 `undefined`。
2
+ * 本机用于「宣告」给其他节点的 IPv4(与 {@link internalIpV4Sync} 行为一致:多网卡无法唯一确定时可能为 `undefined`)。
3
3
  */
4
4
  export declare function getLocalIPv4(): string | undefined;
package/dist/utils.js CHANGED
@@ -1,24 +1,7 @@
1
- import { networkInterfaces } from 'node:os';
2
- function isIPv4(family) {
3
- return family === 'IPv4' || family === 4;
4
- }
1
+ import { internalIpV4Sync } from 'internal-ip';
5
2
  /**
6
- * 获取本机第一个非回环、非内部的 IPv4 地址;若无则返回 `undefined`。
3
+ * 本机用于「宣告」给其他节点的 IPv4(与 {@link internalIpV4Sync} 行为一致:多网卡无法唯一确定时可能为 `undefined`)。
7
4
  */
8
5
  export function getLocalIPv4() {
9
- const ifaces = networkInterfaces();
10
- if (!ifaces) {
11
- return undefined;
12
- }
13
- for (const addrs of Object.values(ifaces)) {
14
- if (!addrs) {
15
- continue;
16
- }
17
- for (const addr of addrs) {
18
- if (isIPv4(addr.family) && !addr.internal) {
19
- return addr.address;
20
- }
21
- }
22
- }
23
- return undefined;
6
+ return internalIpV4Sync();
24
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/micro",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -25,7 +25,8 @@
25
25
  "dependencies": {
26
26
  "@hile/message-loader": "^1.0.8",
27
27
  "@hile/message-ws": "^1.0.6",
28
+ "internal-ip": "^9.0.0",
28
29
  "ws": "^8.19.0"
29
30
  },
30
- "gitHead": "4b006a4ea61b06b7746eeb66b9dad8b227cb6670"
31
+ "gitHead": "46d4b998c1fd2fd725bf484ca634f25b61f19cd5"
31
32
  }