@hile/micro 1.0.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.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @hile/micro
2
+
3
+ 基于 `@hile/message-loader` 与 `@hile/message-ws` 的轻量级 **WebSocket 服务注册与发现**:用固定格式的连接 URL 标识对端,`Registry` 按逻辑 namespace 记录实例,`Application` 从注册中心拉取地址并缓存到远端服务的会话。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @hile/micro
9
+ ```
10
+
11
+ 依赖:`@hile/message-loader`、`@hile/message-ws`、`ws`(随 workspace 一并解析)。
12
+
13
+ ---
14
+
15
+ ## 能做什么?
16
+
17
+ | 组件 | 作用 |
18
+ |------|------|
19
+ | `Server` | 监听 WebSocket;解析路径中的调用方地址与可选分段;对内用 `register`/`dispatch` 处理 `{ url, data }` |
20
+ | `Client` | 连到远端 `Server`,`request`/`push` 走 `MessageModem`,`dispose()` 会关闭底层连接 |
21
+ | `Registry` | 固定 namespace `'registry'`;实例上线/下线更新 namespace→地址集合;`/-/find` **随机** 返回其中一个地址 |
22
+ | `Application` | 启动后连接 Registry;`get(ns)` 查询并 **缓存** 到目标服务的 `Client`,断连后清空缓存 |
23
+
24
+ 连接串格式(出站):
25
+
26
+ `ws://目标主机:端口/{本机广告IPv4}/{本机监听端口}/{本机namespace}`
27
+
28
+ 其中广告 IPv4 由 `getLocalIPv4()` 取第一个非回环网卡地址;多网卡或容器环境可能需要后续版本支持显式 `advertiseHost`(当前未暴露)。
29
+
30
+ 出站 `connect` 默认 **5 秒**握手超时,超时报错 `Connection timeout`。
31
+
32
+ ---
33
+
34
+ ## 快速示例
35
+
36
+ ### 1. Registry
37
+
38
+ ```typescript
39
+ import { Registry } from '@hile/micro';
40
+
41
+ const registry = new Registry();
42
+ await registry.listen(9000);
43
+ ```
44
+
45
+ ### 2. 服务提供方(被其它 Application 发现的进程)
46
+
47
+ ```typescript
48
+ import { Application } from '@hile/micro';
49
+
50
+ const provider = new Application({
51
+ namespace: 'payments',
52
+ registry: { host: '127.0.0.1', port: 9000 },
53
+ });
54
+
55
+ await provider.listen(9100);
56
+
57
+ provider.register('/charge', async ({ data }) => {
58
+ return { charged: true, amount: data.amount };
59
+ });
60
+ ```
61
+
62
+ ### 3. 调用方(通过 Registry 解析地址)
63
+
64
+ ```typescript
65
+ import { Application } from '@hile/micro';
66
+
67
+ const consumer = new Application({
68
+ namespace: 'checkout',
69
+ registry: { host: '127.0.0.1', port: 9000 },
70
+ });
71
+
72
+ await consumer.listen(9200);
73
+
74
+ const remote = await consumer.get('payments');
75
+ const { response } = remote.request('/charge', { amount: 100 });
76
+ const result = await response<{ charged: boolean; amount: number }>();
77
+ console.log(result);
78
+ ```
79
+
80
+ ### 关闭
81
+
82
+ `listen` 返回的 teardown 函数会关闭 WebSocketServer 并对已有 `Client` 调用 `dispose()`:
83
+
84
+ ```typescript
85
+ const stop = await provider.listen(9100);
86
+ // ...
87
+ await stop();
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 与 Hile core 的关系
93
+
94
+ `@hile/micro` **不依赖** `@hile/core`,可与任意 Node.js 进程或在未来由 `defineService` 包装后接入 Hile 容器。
95
+
96
+ ---
97
+
98
+ ## 文档策略
99
+
100
+ | 文档 | 读者 | 用途 |
101
+ |------|------|------|
102
+ | 本 README | 使用者 | 安装、概念、示例 |
103
+ | `SKILL.md` | AI / 贡献者 | 路由约定、类型、反模式、测试门禁 |
104
+
105
+ 仓库线上文档见 Mintlify:**API 参考 → @hile/micro**(若已启用)。
106
+
107
+ ---
108
+
109
+ ## License
110
+
111
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: micro
3
+ description: Code generation and contribution rules for @hile/micro. Use when editing this package or when the user asks about @hile/micro patterns or API.
4
+ ---
5
+
6
+ # @hile/micro
7
+
8
+ 本文档面向 **AI 编码模型** 与 **贡献者**:在修改或生成 `@hile/micro` 相关代码前必读,保证与现有 WebSocket 路由、注册中心与 `MessageLoader` 约定一致。
9
+
10
+ ---
11
+
12
+ ## 1. 架构总览
13
+
14
+ `@hile/micro` 在 `@hile/message-loader` + `@hile/message-ws` 之上提供 **轻量级进程间服务发现与会话**:
15
+
16
+ - **`Server`**:基于 `ws` 的 `WebSocketServer`,用 **URL 路径** 携带对端身份与命名空间;对内用 `MessageLoader.register` / `dispatch` 处理消息路由。
17
+ - **`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
+ 依赖链:
22
+
23
+ ```
24
+ MessageLoader (路由) + MessageWs (请求/响应传输)
25
+ └── Server / Client
26
+ ├── Registry
27
+ └── Application
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 2. 路由与 URL 约定(强约束)
33
+
34
+ ### 2.1 入站连接路径
35
+
36
+ 对端发起连接时的 URL 必须为:
37
+
38
+ ```text
39
+ ws://{listenHost}:{listenPort}/{callerHost}/{callerPort}/{...extras}
40
+ ```
41
+
42
+ 解析规则见 `Server.onConnected`:
43
+
44
+ - 至少 **三段** 路径:`callerHost`、`callerPort` 以及后续 `extras`(可为空)。
45
+ - `extras` 以 `/` 分段,在 `events.emit('connect', client, extras)` 中交给业务;**Registry** 用 `extras.join('/')` 作为 **服务逻辑 namespace**。
46
+
47
+ 生成侧:`Server.connect()` 使用:
48
+
49
+ ```text
50
+ ws://{host}:{port}/${this.ipv4}/${this.port}/${this.namespace}
51
+ ```
52
+
53
+ 其中 `this.ipv4` 来自 `getLocalIPv4()`(见 `utils.ts`),`this.port` 为当前 `listen` 端口,`this.namespace` 为构造传入的字符串(如 `provider`、`consumer`、`registry`)。
54
+
55
+ ### 2.2 MessageLoader 路由
56
+
57
+ - 对内消息体为 `{ url, data }`,与 `Client.exec` 一致。
58
+ - `Registry` 注册:`register<RegistryFindData>('/-/find', handler)`,请求体 `{ namespace: string }`。
59
+
60
+ ---
61
+
62
+ ## 3. 类型与关键 API(生成代码须一致)
63
+
64
+ ```typescript
65
+ // registry.ts
66
+ export interface RegistryFindData {
67
+ namespace: string;
68
+ }
69
+
70
+ export interface RegistryAddress {
71
+ host: string;
72
+ port: number;
73
+ }
74
+
75
+ export function selectRandomRegistryAddress(
76
+ keys: Iterable<string>,
77
+ ): RegistryAddress | undefined;
78
+
79
+ export class Registry extends Server { /* ... */ }
80
+
81
+ // application.ts
82
+ export type ApplicationProps = {
83
+ namespace: string;
84
+ registry: RegistryAddress;
85
+ } & MessageLoaderProps;
86
+
87
+ export class Application extends Server {
88
+ listen(port: number): Promise<() => Promise<void>>;
89
+ get(namespace: string): Promise<Client>;
90
+ }
91
+
92
+ // server.ts — connect 第三参仅用于测试或内部,默认超时 5s
93
+ protected async connect(host: string, port: number, timeout?: number): Promise<Client>;
94
+
95
+ // client.ts
96
+ export class Client extends MessageWs {
97
+ request(url: string, data: any, timeout?: number): ReturnType<MessageWs['_send']>;
98
+ push(url: string, data: any, timeout?: number): void;
99
+ dispose(): void;
100
+ }
101
+ ```
102
+
103
+ `Application` 内部缓存查找状态为 `IDLE` → `PENDING` → `READY`:首次 `get` 必须从 `IDLE` 触发 `findFromRegistry`,禁止再出现「初始状态与触发条件不匹配」导致永久挂起。
104
+
105
+ ---
106
+
107
+ ## 4. 代码生成模板与规则
108
+
109
+ ### 4.1 Registry 服务端
110
+
111
+ ```typescript
112
+ const registry = new Registry();
113
+ const dispose = await registry.listen(registryPort);
114
+ // shutdown: await dispose();
115
+ ```
116
+
117
+ ### 4.2 可被发现的服务(Application)
118
+
119
+ ```typescript
120
+ const app = new Application({
121
+ namespace: 'my-service',
122
+ registry: { host: '127.0.0.1', port: registryPort },
123
+ });
124
+ const dispose = await app.listen(appPort);
125
+
126
+ app.register('/hello', async ({ data }) => {
127
+ return { ok: true, data };
128
+ });
129
+
130
+ const peer = await otherApp.get('my-service');
131
+ const { response } = peer.request('/hello', { x: 1 });
132
+ const result = await response();
133
+ ```
134
+
135
+ ### 4.2 连接超时
136
+
137
+ `Server.connect` 默认 **5 秒** 内未完成握手则 `reject(new Error('Connection timeout'))`。自定义第三参仅限受保护方法与测试辅助类,不要在公开 API 上强制调用方传入。
138
+
139
+ ### 4.3 随机选择
140
+
141
+ `/-/find` 返回的地址必须来自 Registry 内存集合内的 `host:port` 键;新增选择策略时需保持 **Registry 端无额外状态机**(尽量无 cursor),除非你同时补充设计与测试。
142
+
143
+ ---
144
+
145
+ ## 5. 反模式(禁止)
146
+
147
+ - 修改 WebSocket URL 三段式约定却不同时更新 **`Server.onConnected`** 与 **`Server.connect`** 的拼接格式。
148
+ - 在 `Registry` 中按 `Set` **迭代顺序** 固定返回「第一个」实例(破坏负载分散);除非你明确改需求并改写测试。
149
+ - `Client`/`Server` **`dispose`** 后不关闭 **`ws`**(会导致 **`WebSocketServer.close`** 长时间等待);本包已在 `Client.dispose` 内关闭socket,不要随意删除。
150
+ - 假设 `host:port` 串可无损表达 **IPv6**(当前实现按 **首个 `:` 分割**,仅适合 IPv4 或不含冒号的 host)。
151
+ - `Application.props.registry` **不要传错端口**;丢失与注册中心的连接时依赖 `reconnectToRegistry`,不要在外部缓存 `registry` Client 绕过重连语义。
152
+
153
+ ---
154
+
155
+ ## 6. 测试与改动范围
156
+
157
+ - 包内测试:`packages/micro/src/index.test.ts`(Vitest)。
158
+ - 修改行为时至少覆盖:**随机选择 helper**、`Application` 端到端发现、**连接超时**。
159
+ - 发布前在项目根或通过 filter 运行:`pnpm --filter @hile/micro test` 与 `pnpm --filter @hile/micro build`。
@@ -0,0 +1,19 @@
1
+ import { Client } from './client.js';
2
+ import { Server } from './server.js';
3
+ import { MessageLoaderProps } from '@hile/message-loader';
4
+ import { RegistryAddress } from './registry.js';
5
+ export type ApplicationProps = {
6
+ namespace: string;
7
+ registry: RegistryAddress;
8
+ } & MessageLoaderProps;
9
+ export declare class Application extends Server {
10
+ private registry?;
11
+ private reconnectTimeout?;
12
+ private readonly _registry_address;
13
+ private readonly namespaces;
14
+ constructor(props: ApplicationProps);
15
+ listen(port: number): Promise<() => Promise<void>>;
16
+ private reconnectToRegistry;
17
+ private findFromRegistry;
18
+ get(namespace: string): Promise<Client>;
19
+ }
@@ -0,0 +1,91 @@
1
+ import { Server } from './server.js';
2
+ var RegistryLookupStatus;
3
+ (function (RegistryLookupStatus) {
4
+ RegistryLookupStatus[RegistryLookupStatus["IDLE"] = 0] = "IDLE";
5
+ RegistryLookupStatus[RegistryLookupStatus["PENDING"] = 1] = "PENDING";
6
+ RegistryLookupStatus[RegistryLookupStatus["READY"] = 2] = "READY";
7
+ })(RegistryLookupStatus || (RegistryLookupStatus = {}));
8
+ export class Application extends Server {
9
+ registry;
10
+ reconnectTimeout;
11
+ _registry_address;
12
+ namespaces = new Map();
13
+ constructor(props) {
14
+ const { namespace, registry, ...loaderProps } = props;
15
+ super(namespace, loaderProps);
16
+ this._registry_address = registry;
17
+ }
18
+ async listen(port) {
19
+ const callback = await super.listen(port);
20
+ await this.reconnectToRegistry();
21
+ return async () => {
22
+ if (this.reconnectTimeout)
23
+ clearTimeout(this.reconnectTimeout);
24
+ await callback();
25
+ };
26
+ }
27
+ 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);
34
+ });
35
+ };
36
+ reconnect();
37
+ });
38
+ this.registry = registry;
39
+ }
40
+ async findFromRegistry(namespace) {
41
+ if (!this.registry)
42
+ throw new Error('Registry not found');
43
+ const { response } = this.registry.request('/-/find', { namespace });
44
+ return await response();
45
+ }
46
+ get(namespace) {
47
+ if (!this.namespaces.has(namespace)) {
48
+ this.namespaces.set(namespace, {
49
+ host: '',
50
+ port: 0,
51
+ status: RegistryLookupStatus.IDLE,
52
+ handlers: new Set(),
53
+ });
54
+ }
55
+ const stack = this.namespaces.get(namespace);
56
+ const key = `${stack.host}:${stack.port}`;
57
+ if (stack.status === RegistryLookupStatus.READY && this.clients.has(key)) {
58
+ return Promise.resolve(this.clients.get(key));
59
+ }
60
+ return new Promise((resolve, reject) => {
61
+ stack.handlers.add([resolve, reject]);
62
+ if (stack.status === RegistryLookupStatus.IDLE) {
63
+ stack.status = RegistryLookupStatus.PENDING;
64
+ this.findFromRegistry(namespace).then(data => {
65
+ if (!data)
66
+ return Promise.reject(new Error('Namespace not found'));
67
+ return this.connect(data.host, data.port).then(client => {
68
+ stack.host = data.host;
69
+ stack.port = data.port;
70
+ stack.status = RegistryLookupStatus.READY;
71
+ for (const [resolve] of stack.handlers.values()) {
72
+ resolve(client);
73
+ }
74
+ return client;
75
+ });
76
+ }).then(client => {
77
+ client.events.on('disconnect', () => {
78
+ if (this.namespaces.has(namespace)) {
79
+ this.namespaces.delete(namespace);
80
+ }
81
+ });
82
+ }).catch(e => {
83
+ this.namespaces.delete(namespace);
84
+ for (const [_, reject] of stack.handlers.values()) {
85
+ reject(e);
86
+ }
87
+ }).finally(() => stack.handlers.clear());
88
+ }
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,29 @@
1
+ import { MessageWs } from "@hile/message-ws";
2
+ import { Server } from './server.js';
3
+ import { WebSocket } from 'ws';
4
+ import { EventEmitter } from 'node:events';
5
+ export interface ClientProps {
6
+ host: string;
7
+ port: number;
8
+ server: Server;
9
+ ws: WebSocket;
10
+ }
11
+ export declare class Client extends MessageWs {
12
+ private readonly server;
13
+ private readonly socket;
14
+ readonly host: string;
15
+ readonly port: number;
16
+ private _online;
17
+ readonly events: EventEmitter<any>;
18
+ constructor(props: ClientProps);
19
+ protected exec(data: {
20
+ url: string;
21
+ data: any;
22
+ }): Promise<any>;
23
+ request(url: string, data: any, timeout?: number): {
24
+ abort: () => void;
25
+ response: <U = any>() => Promise<U>;
26
+ };
27
+ push(url: string, data: any, timeout?: number): void;
28
+ dispose(): void;
29
+ }
package/dist/client.js ADDED
@@ -0,0 +1,44 @@
1
+ import { MessageWs } from "@hile/message-ws";
2
+ import { WebSocket } from 'ws';
3
+ import { EventEmitter } from 'node:events';
4
+ export class Client extends MessageWs {
5
+ server;
6
+ socket;
7
+ host;
8
+ port;
9
+ _online = true;
10
+ events = new EventEmitter();
11
+ constructor(props) {
12
+ const { server, ws, host, port } = props;
13
+ super(ws);
14
+ this.server = server;
15
+ this.socket = ws;
16
+ this.host = host;
17
+ this.port = port;
18
+ this.events.on('connect', () => this._online = true);
19
+ this.events.on('disconnect', () => this._online = false);
20
+ }
21
+ async exec(data) {
22
+ if (!this._online)
23
+ throw new Error('Client is not online');
24
+ return this.server.dispatch(data.url, data.data, {
25
+ client: this,
26
+ });
27
+ }
28
+ request(url, data, timeout) {
29
+ if (!this._online)
30
+ throw new Error('Client is not online');
31
+ return this._send({ url, data }, timeout);
32
+ }
33
+ push(url, data, timeout) {
34
+ if (!this._online)
35
+ throw new Error('Client is not online');
36
+ return this._push({ url, data }, timeout);
37
+ }
38
+ dispose() {
39
+ super.dispose();
40
+ if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
41
+ this.socket.close();
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,4 @@
1
+ export * from './server.js';
2
+ export * from './client.js';
3
+ export * from './application.js';
4
+ export * from './registry.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './server.js';
2
+ export * from './client.js';
3
+ export * from './application.js';
4
+ export * from './registry.js';
@@ -0,0 +1,15 @@
1
+ import { MessageLoaderProps } from '@hile/message-loader';
2
+ import { Server } from './server.js';
3
+ export interface RegistryFindData {
4
+ namespace: string;
5
+ }
6
+ export interface RegistryAddress {
7
+ host: string;
8
+ port: number;
9
+ }
10
+ export declare function selectRandomRegistryAddress(keys: Iterable<string>): RegistryAddress | undefined;
11
+ export declare class Registry extends Server {
12
+ private readonly namespaces;
13
+ constructor(props?: MessageLoaderProps);
14
+ onFind(): void;
15
+ }
@@ -0,0 +1,48 @@
1
+ import { Server } from './server.js';
2
+ 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
+ });
7
+ if (addresses.length === 0)
8
+ return;
9
+ const index = Math.floor(Math.random() * addresses.length);
10
+ return addresses[index];
11
+ }
12
+ export class Registry extends Server {
13
+ namespaces = new Map();
14
+ constructor(props) {
15
+ super('registry', props);
16
+ this.events.on('connect', (client, extras) => {
17
+ const key = client.host + ':' + client.port;
18
+ const namespace = extras.join('/');
19
+ if (!this.namespaces.has(namespace)) {
20
+ this.namespaces.set(namespace, new Set());
21
+ }
22
+ this.namespaces.get(namespace).add(key);
23
+ });
24
+ this.events.on('disconnect', (client, extras) => {
25
+ const key = client.host + ':' + client.port;
26
+ const namespace = extras.join('/');
27
+ if (this.namespaces.has(namespace)) {
28
+ const keys = this.namespaces.get(namespace);
29
+ if (keys.has(key)) {
30
+ keys.delete(key);
31
+ if (keys.size === 0) {
32
+ this.namespaces.delete(namespace);
33
+ }
34
+ }
35
+ }
36
+ });
37
+ this.onFind();
38
+ }
39
+ onFind() {
40
+ this.register('/-/find', async ({ data }) => {
41
+ const namespace = data.namespace;
42
+ const keys = this.namespaces.get(namespace);
43
+ if (!keys)
44
+ return;
45
+ return selectRandomRegistryAddress(keys);
46
+ });
47
+ }
48
+ }
@@ -0,0 +1,16 @@
1
+ import { MessageLoader, MessageLoaderProps } from "@hile/message-loader";
2
+ import { Client } from './client.js';
3
+ import { EventEmitter } from 'node:events';
4
+ export declare class Server extends MessageLoader {
5
+ private readonly namespace;
6
+ private wss?;
7
+ port?: number;
8
+ protected readonly clients: Map<string, Client>;
9
+ private readonly ipv4;
10
+ readonly events: EventEmitter<any>;
11
+ constructor(namespace: string, props?: MessageLoaderProps);
12
+ private onConnected;
13
+ private onRegister;
14
+ protected connect(host: string, port: number, timeout?: number): Promise<Client>;
15
+ listen(port?: number): Promise<() => Promise<void>>;
16
+ }
package/dist/server.js ADDED
@@ -0,0 +1,127 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { MessageLoader } from "@hile/message-loader";
3
+ import { WebSocket } from 'ws';
4
+ import { Client } from './client.js';
5
+ import { getLocalIPv4 } from './utils.js';
6
+ import { EventEmitter } from 'node:events';
7
+ const DEFAULT_CONNECT_TIMEOUT = 5000;
8
+ export class Server extends MessageLoader {
9
+ namespace;
10
+ wss;
11
+ port;
12
+ clients = new Map();
13
+ ipv4 = getLocalIPv4();
14
+ events = new EventEmitter();
15
+ constructor(namespace, props = {}) {
16
+ super(props);
17
+ this.namespace = namespace;
18
+ this.events.on('connect', (client, extras) => {
19
+ client.events.emit('connect', extras);
20
+ });
21
+ this.events.on('disconnect', (client, extras) => {
22
+ client.events.emit('disconnect', extras);
23
+ });
24
+ }
25
+ onConnected(ws, req) {
26
+ const path = req.url?.split('?')[0];
27
+ if (!path)
28
+ return ws.close();
29
+ let _path = path.startsWith('/') ? path.slice(1) : path;
30
+ _path = _path.endsWith('/') ? _path.slice(0, -1) : _path;
31
+ const sp = _path.split('/');
32
+ if (sp.length < 3)
33
+ return ws.close();
34
+ const [host, port, ...extras] = sp;
35
+ if (!host || !port)
36
+ return ws.close();
37
+ this.onRegister(ws, host, Number(port), extras);
38
+ }
39
+ onRegister(ws, host, port, extras = []) {
40
+ const key = `${host}:${port}`;
41
+ const client = new Client({ server: this, ws, host, port });
42
+ ws.on('close', () => {
43
+ if (this.clients.has(key)) {
44
+ this.clients.delete(key);
45
+ }
46
+ client.dispose();
47
+ this.events.emit('disconnect', client, extras);
48
+ });
49
+ this.clients.set(key, client);
50
+ this.events.emit('connect', client, extras);
51
+ return client;
52
+ }
53
+ async connect(host, port, timeout = DEFAULT_CONNECT_TIMEOUT) {
54
+ const key = `${host}:${port}`;
55
+ if (this.clients.has(key)) {
56
+ return this.clients.get(key);
57
+ }
58
+ const ws = await new Promise((resolve, reject) => {
59
+ const ws = new WebSocket(`ws://${host}:${port}/${this.ipv4}/${this.port}/${this.namespace}`);
60
+ const timer = setTimeout(() => {
61
+ clear();
62
+ ws.on('error', () => { });
63
+ try {
64
+ ws.terminate();
65
+ }
66
+ catch { }
67
+ reject(new Error('Connection timeout'));
68
+ }, timeout).unref();
69
+ const clear = () => {
70
+ clearTimeout(timer);
71
+ ws.off('open', onopen);
72
+ ws.off('error', onerror);
73
+ };
74
+ const onerror = (err) => {
75
+ clear();
76
+ reject(err);
77
+ };
78
+ const onopen = () => {
79
+ clear();
80
+ resolve(ws);
81
+ };
82
+ ws.on('open', onopen);
83
+ ws.on('error', onerror);
84
+ });
85
+ return this.onRegister(ws, host, port);
86
+ }
87
+ async listen(port) {
88
+ this.wss = await new Promise((resolve, reject) => {
89
+ 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;
108
+ return async () => {
109
+ for (const client of this.clients.values()) {
110
+ client.dispose();
111
+ }
112
+ this.clients.clear();
113
+ if (this.wss) {
114
+ this.wss.off('connection', onConnection);
115
+ await new Promise((resolve, reject) => {
116
+ this.wss.close((err) => {
117
+ if (err)
118
+ return reject(err);
119
+ resolve();
120
+ });
121
+ });
122
+ }
123
+ this.wss = undefined;
124
+ this.port = undefined;
125
+ };
126
+ }
127
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 获取本机第一个非回环、非内部的 IPv4 地址;若无则返回 `undefined`。
3
+ */
4
+ export declare function getLocalIPv4(): string | undefined;
package/dist/utils.js ADDED
@@ -0,0 +1,24 @@
1
+ import { networkInterfaces } from 'node:os';
2
+ function isIPv4(family) {
3
+ return family === 'IPv4' || family === 4;
4
+ }
5
+ /**
6
+ * 获取本机第一个非回环、非内部的 IPv4 地址;若无则返回 `undefined`。
7
+ */
8
+ 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;
24
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@hile/micro",
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc -b && fix-esm-import-path --preserve-import-type ./dist",
8
+ "dev": "tsc -b --watch",
9
+ "test": "vitest run"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "SKILL.md"
15
+ ],
16
+ "license": "MIT",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "devDependencies": {
21
+ "@types/ws": "^8.18.1",
22
+ "fix-esm-import-path": "^1.10.3",
23
+ "vitest": "^4.0.18"
24
+ },
25
+ "dependencies": {
26
+ "@hile/message-loader": "^1.0.8",
27
+ "@hile/message-ws": "^1.0.6",
28
+ "ws": "^8.19.0"
29
+ },
30
+ "gitHead": "75188f29c1a041ed596b9f8aa9ed5ab9fde1b615"
31
+ }