@hile/micro 1.0.5 → 1.0.6
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 +93 -0
- package/SKILL.md +115 -12
- package/dist/application.d.ts +17 -0
- package/dist/application.js +11 -0
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +77 -3
- package/dist/server.d.ts +1 -0
- package/dist/server.js +3 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -243,6 +243,92 @@ const health = await app.dispatch('/-/health', {});
|
|
|
243
243
|
|
|
244
244
|
---
|
|
245
245
|
|
|
246
|
+
## 配置管理
|
|
247
|
+
|
|
248
|
+
### Registry 工作目录
|
|
249
|
+
|
|
250
|
+
Registry 启动时自动创建 `~/.registry/` 工作目录。YAML 配置文件存放在 `~/.registry/configs/` 下,按 namespace 分文件管理:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
~/.registry/
|
|
254
|
+
└── configs/
|
|
255
|
+
├── service-a.config.yaml
|
|
256
|
+
├── service-b.config.yaml
|
|
257
|
+
└── global.config.yaml
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 配置文件热加载
|
|
261
|
+
|
|
262
|
+
Regsitry 监听 `configs/` 目录的文件变化,新增或修改 `*.config.yaml` 文件时自动加载(兼容 vim 原子写入),无需重启:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# 创建或修改 ~/.registry/configs/my-service.config.yaml
|
|
266
|
+
# Registry 自动检测变化并更新内存中的配置
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### 远程读取配置
|
|
270
|
+
|
|
271
|
+
通过 `/-/env/variables` 端点,已连服务可按 namespace 和字段从 Registry 远程读取配置:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// Application 侧
|
|
275
|
+
const result = await app.getEnvVariables(
|
|
276
|
+
{ namespace: 'service-a', fields: ['db.host', 'db.port'] },
|
|
277
|
+
{ namespace: 'global' },
|
|
278
|
+
);
|
|
279
|
+
// result = {
|
|
280
|
+
// 'service-a': { 'db.host': '10.0.0.2', 'db.port': 3306 },
|
|
281
|
+
// 'global': { featureFlag: true },
|
|
282
|
+
// }
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## CLI
|
|
288
|
+
|
|
289
|
+
### `hile registry`
|
|
290
|
+
|
|
291
|
+
启动注册中心:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
# 使用默认配置
|
|
295
|
+
hile registry
|
|
296
|
+
|
|
297
|
+
# 指定端口
|
|
298
|
+
hile registry --port 8888
|
|
299
|
+
|
|
300
|
+
# 指定宣告地址
|
|
301
|
+
hile registry --host 10.0.0.1
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### `hile registry configs`
|
|
305
|
+
|
|
306
|
+
管理 `~/.registry/configs/` 下的 YAML 配置文件:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
# 列出所有 namespace
|
|
310
|
+
hile registry configs
|
|
311
|
+
|
|
312
|
+
# 查看配置(YAML 输出)
|
|
313
|
+
hile registry configs get my-service
|
|
314
|
+
|
|
315
|
+
# 查看配置(JSON 输出)
|
|
316
|
+
hile registry configs get my-service --json
|
|
317
|
+
|
|
318
|
+
# 设置配置项
|
|
319
|
+
hile registry configs set my-service port=8080
|
|
320
|
+
hile registry configs set my-service debug=true
|
|
321
|
+
|
|
322
|
+
# 删除整个 namespace
|
|
323
|
+
hile registry configs del my-service
|
|
324
|
+
|
|
325
|
+
# 删除某个字段(需确认)
|
|
326
|
+
hile registry configs del my-service port
|
|
327
|
+
|
|
328
|
+
# 跳过确认删除
|
|
329
|
+
hile registry configs del my-service -y
|
|
330
|
+
```
|
|
331
|
+
|
|
246
332
|
## 连接协议
|
|
247
333
|
|
|
248
334
|
### 连接 URL 格式
|
|
@@ -308,6 +394,12 @@ class Application extends Server {
|
|
|
308
394
|
|
|
309
395
|
// 同进程调用路由
|
|
310
396
|
dispatch(url: string, data: any): Promise<any>;
|
|
397
|
+
|
|
398
|
+
// 远程读取 Registry 的配置(强类型)
|
|
399
|
+
getEnvVariables<
|
|
400
|
+
T extends Record<string, Record<string, any>>,
|
|
401
|
+
const Requests extends readonly EnvRequest<T>[],
|
|
402
|
+
>(...data: Requests): Promise<GetEnvVariablesResult<T, Requests>>;
|
|
311
403
|
}
|
|
312
404
|
```
|
|
313
405
|
|
|
@@ -318,6 +410,7 @@ class Registry extends Server {
|
|
|
318
410
|
constructor(props?: MicroServerProps);
|
|
319
411
|
listen(port: number): Promise<() => Promise<void>>;
|
|
320
412
|
onFind(): void; // 幂等地挂载 /-/find 路由
|
|
413
|
+
watchEnvFile(): fs.FSWatcher | undefined; // 监听 ~/.registry/configs/ 目录内的 *.config.yaml 文件变化
|
|
321
414
|
}
|
|
322
415
|
```
|
|
323
416
|
|
package/SKILL.md
CHANGED
|
@@ -29,8 +29,8 @@ description: Code generation and contribution rules for @hile/micro. Use when ed
|
|
|
29
29
|
|----|------|------|---------|
|
|
30
30
|
| `Server` | `server.ts` | WebSocketServer 生命周期, 出入站连接, Client Map | 不感知 Registry |
|
|
31
31
|
| `Client` | `client.ts` | 远端 Server 的 WebSocket 会话代理 | `dispose()` 必须关闭底层 socket |
|
|
32
|
-
| `Registry` | `registry.ts` | namespace → Set\<host:port\>, 心跳检测, /-/find
|
|
33
|
-
| `Application` | `application.ts` | 注册发现 + 熔断 + 重试 + 追踪 + 心跳 | `listen()` 后自动连 Registry |
|
|
32
|
+
| `Registry` | `registry.ts` | namespace → Set\<host:port\>, 心跳检测, /-/find 随机返回, 环境变量管理 | 自动创建 `~/.registry/` 工作目录 |
|
|
33
|
+
| `Application` | `application.ts` | 注册发现 + 熔断 + 重试 + 追踪 + 心跳 + 远程环境变量读取 | `listen()` 后自动连 Registry |
|
|
34
34
|
|
|
35
35
|
### 应用模型
|
|
36
36
|
|
|
@@ -95,17 +95,28 @@ export interface RegistryFindData {
|
|
|
95
95
|
|
|
96
96
|
export function parseAddressKey(key: string): RegistryAddress | undefined;
|
|
97
97
|
export function selectRandomRegistryAddress(keys: Iterable<string>): RegistryAddress | undefined;
|
|
98
|
+
// 配置文件路径工具:
|
|
99
|
+
export function getRegistryConfigsDir(): string; // 返回 ~/.registry/configs/
|
|
100
|
+
export function namespaceToConfigFile(ns: string): string; // 返回 ~/.registry/configs/{ns}.config.yaml
|
|
101
|
+
export function parseConfigFilename(filename: string): string | null; // 解析 ".config.yaml" 后缀,提取 namespace
|
|
98
102
|
|
|
99
103
|
export class Registry extends Server {
|
|
100
104
|
// heartbeat 常量:
|
|
101
105
|
// HEARTBEAT_INTERVAL = 1000 (1s 轮询)
|
|
102
106
|
// HEARTBEAT_TIMEOUT = 20000 (20s 未收到心跳则剔除)
|
|
107
|
+
// 工作目录: ~/.registry/ (自动创建)
|
|
108
|
+
// - configs/ 目录存放 *.config.yaml 配置文件
|
|
109
|
+
// - watchEnvFile() 监听 configs/ 目录,兼容 vim 原子写入
|
|
110
|
+
// - configs Map<string, any> 按 namespace 存储解析后的 YAML 内容
|
|
111
|
+
// 内部路由:
|
|
112
|
+
// /-/find — 按 namespace 随机返回地址 (支持 exclude)
|
|
113
|
+
// /-/heartbeat — 更新实例心跳时间戳
|
|
114
|
+
// /-/env/variables — 按 namespace + fields 返回配置 (通过 getEnvVariables 调用)
|
|
115
|
+
|
|
103
116
|
constructor(props?: MicroServerProps);
|
|
104
117
|
listen(port: number): Promise<() => Promise<void>>;
|
|
105
118
|
onFind(): void; // 幂等,可重复调用
|
|
106
|
-
//
|
|
107
|
-
// /-/find — 按 namespace 随机返回地址 (支持 exclude)
|
|
108
|
-
// /-/heartbeat — 更新实例心跳时间戳
|
|
119
|
+
watchEnvFile(): fs.FSWatcher | undefined; // 监听 ~/.registry/configs/ 目录
|
|
109
120
|
}
|
|
110
121
|
```
|
|
111
122
|
|
|
@@ -140,6 +151,12 @@ export class Application extends Server {
|
|
|
140
151
|
retries?: number, // 重试次数, 默认 1
|
|
141
152
|
): Promise<T>;
|
|
142
153
|
|
|
154
|
+
// 远程读取 Registry 的配置(强类型,按 namespace + fields)
|
|
155
|
+
getEnvVariables<
|
|
156
|
+
T extends Record<string, Record<string, any>>,
|
|
157
|
+
const Requests extends readonly EnvRequest<T>[],
|
|
158
|
+
>(...data: Requests): Promise<GetEnvVariablesResult<T, Requests>>;
|
|
159
|
+
|
|
143
160
|
// 继承自 Server/MessageLoader:
|
|
144
161
|
// register<T, E>(url, handler): () => void;
|
|
145
162
|
// dispatch(url, data, extras): Promise<any>;
|
|
@@ -359,6 +376,72 @@ listen() teardown 触发:
|
|
|
359
376
|
|
|
360
377
|
**不处理的情况:** 全新 namespace(无缓存)、缓存 Client 已断连。
|
|
361
378
|
|
|
379
|
+
### 3.10 配置管理
|
|
380
|
+
|
|
381
|
+
**存储结构:**
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
~/.registry/
|
|
385
|
+
└── configs/
|
|
386
|
+
├── service-a.config.yaml
|
|
387
|
+
├── service-b.config.yaml
|
|
388
|
+
└── global.config.yaml
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Registry 侧:**
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
Registry 构造:
|
|
395
|
+
1. 创建 ~/.registry/ 目录 (自动)
|
|
396
|
+
|
|
397
|
+
Registry listen():
|
|
398
|
+
1. 调用 watchEnvFile()
|
|
399
|
+
└─ 读取 ~/.registry/configs/ 下所有 *.config.yaml → YAML.parse
|
|
400
|
+
└─ 按 namespace 存入 this.configs Map
|
|
401
|
+
└─ 监听 configs/ 目录 (兼容 vim 原子写入)
|
|
402
|
+
└─ 文件变化 → 重新 YAML.parse 并更新 this.configs
|
|
403
|
+
|
|
404
|
+
/-/env/variables 端点:
|
|
405
|
+
register('/-/env/variables', async ({ data }) => {
|
|
406
|
+
data.map(({ namespace, fields }) => {
|
|
407
|
+
if (!this.configs.has(namespace))
|
|
408
|
+
return { namespace, value: null }
|
|
409
|
+
if (!fields?.length)
|
|
410
|
+
return { namespace, value: this.configs.get(namespace) }
|
|
411
|
+
// 按 fields 过滤
|
|
412
|
+
return { namespace, value: filteredConfig }
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Application 侧:**
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// 强类型查询
|
|
421
|
+
type EnvRequest<T> = {
|
|
422
|
+
[N in keyof T]: { namespace: N; fields?: readonly (keyof T[N])[] };
|
|
423
|
+
}[keyof T];
|
|
424
|
+
|
|
425
|
+
type GetEnvVariablesResult<T, Requests> = UnionToIntersection<...>;
|
|
426
|
+
|
|
427
|
+
getEnvVariables(...data: EnvRequest<T>[]): Promise<GetEnvVariablesResult<T, Requests>>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**CLI 管理配置:**
|
|
431
|
+
|
|
432
|
+
通过 `hile registry configs` 子命令直接管理 `~/.registry/configs/` 下的 YAML 文件:
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
hile registry configs # 列出所有 namespace
|
|
436
|
+
hile registry configs get <namespace> # 查看配置(YAML/--json)
|
|
437
|
+
hile registry configs set <namespace> <key>=<value> # 设置配置项
|
|
438
|
+
hile registry configs del <namespace> [key] # 删除(带确认,-y 跳过)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
- CLI 直接读写 YAML 文件,运行中的 Registry 通过 `fs.watch` 自动感知
|
|
442
|
+
- 值类型自动推断:`true/false` → boolean, `null` → null, 纯数字 → number, 其余 → string
|
|
443
|
+
- 实现代码在 `packages/cli/src/configs.ts`,工具函数在 `packages/micro/src/registry.ts`
|
|
444
|
+
|
|
362
445
|
---
|
|
363
446
|
|
|
364
447
|
## 4. 代码生成模板
|
|
@@ -470,6 +553,19 @@ pnpm --filter @hile/micro test # 必须全部通过
|
|
|
470
553
|
| 超时 reject | `request timeout > rejects when request exceeds the timeout` |
|
|
471
554
|
| 超时充足则成功 | `request timeout > succeeds when timeout is long enough` |
|
|
472
555
|
| 缓存降级 | `cache degradation > uses cached client when registry lookup fails due to exclusion` |
|
|
556
|
+
| YAML 配置加载 | `config file loading > loads yaml config files on watchEnvFile` |
|
|
557
|
+
| YAML 配置热加载 | `config file loading > reloads config when yaml file changes` |
|
|
558
|
+
| configs 目录不存在 | `config file loading > does not crash when configs directory does not exist` |
|
|
559
|
+
| /-/env/variables 按字段过滤 | `/-/env/variables endpoint > returns requested config by namespace and fields` |
|
|
560
|
+
| /-/env/variables 全字段 | `/-/env/variables endpoint > returns all config when fields not specified` |
|
|
561
|
+
| 不存在的 namespace | `/-/env/variables endpoint > returns null value for non-existent namespace` |
|
|
562
|
+
| /-/env/variables 空列表 | `/-/env/variables endpoint > handles empty data list` |
|
|
563
|
+
| getEnvVariables 集成 | `Application.getEnvVariables > fetches config from Registry` |
|
|
564
|
+
| getEnvVariables 不存在 namespace | `Application.getEnvVariables > returns null when namespace config does not exist` |
|
|
565
|
+
| getRegistryConfigsDir | `config file utilities > getRegistryConfigsDir returns path ending with .registry/configs` |
|
|
566
|
+
| namespaceToConfigFile | `config file utilities > namespaceToConfigFile returns path with .config.yaml suffix` |
|
|
567
|
+
| parseConfigFilename 正例 | `config file utilities > parseConfigFilename extracts namespace from valid filename` |
|
|
568
|
+
| parseConfigFilename 反例 | `config file utilities > parseConfigFilename returns null for non-config file` |
|
|
473
569
|
|
|
474
570
|
### 5.3 测试规范(必须遵守)
|
|
475
571
|
|
|
@@ -490,7 +586,7 @@ pnpm --filter @hile/micro test # 必须全部通过
|
|
|
490
586
|
4. **不要假设 `host:port` 可无损表达 IPv6** — 使用 `[IPv6]:port` 格式,`parseAddressKey` 按最后一个 `:` 切分
|
|
491
587
|
5. **不要传错 Registry 端口** — 丢失 Registry 连接时依赖 `reconnectToRegistry`,不要在外部缓存 registry Client
|
|
492
588
|
6. **不要在 call() 中修改原始 data 对象** — 必须使用浅拷贝 `{ ...data, _correlationId }`
|
|
493
|
-
7. **不要在其他文件中重复
|
|
589
|
+
7. **不要在其他文件中重复 Registry 的 helper 函数** — `selectRandomRegistryAddress`、`parseAddressKey`、`getRegistryConfigsDir`、`namespaceToConfigFile`、`parseConfigFilename` 都在 `registry.ts` 中导出复用
|
|
494
590
|
8. **不要给 call() 增加非可选参数** — `timeout` 和 `retries` 都在尾部且保持可选,不影响现有调用
|
|
495
591
|
|
|
496
592
|
---
|
|
@@ -500,13 +596,18 @@ pnpm --filter @hile/micro test # 必须全部通过
|
|
|
500
596
|
| 文件 | 可修改 | 说明 |
|
|
501
597
|
|------|--------|------|
|
|
502
598
|
| `packages/micro/src/application.ts` | ✅ | 核心业务逻辑 |
|
|
503
|
-
| `packages/micro/src/index.test.ts` | ✅ |
|
|
599
|
+
| `packages/micro/src/index.test.ts` | ✅ | 测试(主测试文件) |
|
|
600
|
+
| `packages/micro/src/env-config.test.ts` | ✅ | 测试(环境变量配置测试) |
|
|
504
601
|
| `packages/micro/src/server.ts` | ❌ | 底层协议,不动 |
|
|
505
602
|
| `packages/micro/src/client.ts` | ❌ | 底层协议,不动 |
|
|
506
|
-
| `packages/micro/src/registry.ts` |
|
|
603
|
+
| `packages/micro/src/registry.ts` | ✅ | 注册中心(配置管理、路径工具函数) |
|
|
507
604
|
| `packages/micro/src/utils.ts` | ❌ | 工具函数,不动 |
|
|
508
605
|
| `packages/micro/README.md` | ✅ | 用户文档 |
|
|
509
606
|
| `packages/micro/SKILL.md` | ✅ | AI 参考文档 |
|
|
607
|
+
| `packages/cli/src/index.ts` | ✅ | CLI 入口(registry configs 子命令组) |
|
|
608
|
+
| `packages/cli/src/configs.ts` | ✅ | CLI 配置管理 handler(list/get/set/del) |
|
|
609
|
+
| `packages/cli/src/start.ts` | ❌ | 启动逻辑,不动 |
|
|
610
|
+
| `packages/cli/src/exitHook.ts` | ❌ | 退出钩子,不动 |
|
|
510
611
|
|
|
511
612
|
---
|
|
512
613
|
|
|
@@ -514,7 +615,9 @@ pnpm --filter @hile/micro test # 必须全部通过
|
|
|
514
615
|
|
|
515
616
|
| 文件 | 用途 |
|
|
516
617
|
|------|------|
|
|
517
|
-
| `packages/micro/src/
|
|
518
|
-
| `packages/micro/src/
|
|
519
|
-
| `
|
|
520
|
-
| `
|
|
618
|
+
| `packages/micro/src/registry.ts` | 注册中心(含配置管理、路径工具函数) |
|
|
619
|
+
| `packages/micro/src/application.ts` | 应用服务(含 getEnvVariables) |
|
|
620
|
+
| `packages/micro/src/index.test.ts` | 主测试文件(28 个用例) |
|
|
621
|
+
| `packages/micro/src/env-config.test.ts` | 配置管理测试文件(13 个用例) |
|
|
622
|
+
| `packages/cli/src/index.ts` | CLI 入口(含 registry configs 子命令组) |
|
|
623
|
+
| `packages/cli/src/configs.ts` | CLI 配置管理 handler(list/get/set/del) |
|
package/dist/application.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { Client } from './client.js';
|
|
2
2
|
import { Server, type MicroServerProps } from './server.js';
|
|
3
3
|
import { RegistryAddress } from './registry.js';
|
|
4
|
+
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
5
|
+
type EnvRequest<T extends Record<string, Record<string, any>>> = {
|
|
6
|
+
[N in keyof T]: {
|
|
7
|
+
namespace: N;
|
|
8
|
+
fields?: readonly (keyof T[N])[];
|
|
9
|
+
};
|
|
10
|
+
}[keyof T];
|
|
11
|
+
type EnvFieldsForRequest<T extends Record<string, Record<string, any>>, N extends keyof T, F> = F extends readonly (infer K extends keyof T[N])[] ? Pick<T[N], K> : T[N];
|
|
12
|
+
type EnvRequestResult<T extends Record<string, Record<string, any>>, R> = R extends {
|
|
13
|
+
namespace: infer N extends keyof T;
|
|
14
|
+
fields?: infer F;
|
|
15
|
+
} ? {
|
|
16
|
+
[K in N]: EnvFieldsForRequest<T, N, F>;
|
|
17
|
+
} : never;
|
|
18
|
+
export type GetEnvVariablesResult<T extends Record<string, Record<string, any>>, Requests extends readonly EnvRequest<T>[]> = UnionToIntersection<EnvRequestResult<T, Requests[number]>>;
|
|
4
19
|
export type ApplicationProps = {
|
|
5
20
|
namespace: string;
|
|
6
21
|
registry: RegistryAddress;
|
|
@@ -35,4 +50,6 @@ export declare class Application extends Server {
|
|
|
35
50
|
private findFromRegistry;
|
|
36
51
|
get(namespace: string, exclude?: string[]): Promise<Client>;
|
|
37
52
|
call<T = any>(namespace: string, url: string, data: any, timeout?: number, retries?: number): Promise<T>;
|
|
53
|
+
getEnvVariables<T extends Record<string, Record<string, any>>, const Requests extends readonly EnvRequest<T>[] = readonly EnvRequest<T>[]>(...data: Requests): Promise<GetEnvVariablesResult<T, Requests>>;
|
|
38
54
|
}
|
|
55
|
+
export {};
|
package/dist/application.js
CHANGED
|
@@ -293,4 +293,15 @@ export class Application extends Server {
|
|
|
293
293
|
throw err;
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
+
async getEnvVariables(...data) {
|
|
297
|
+
if (!this.registry)
|
|
298
|
+
throw new Error('Registry not found');
|
|
299
|
+
const { response } = this.registry.request('/-/env/variables', data);
|
|
300
|
+
const configs = await response();
|
|
301
|
+
const out = {};
|
|
302
|
+
for (const { namespace, value } of configs) {
|
|
303
|
+
out[namespace] = value;
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
296
307
|
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -10,15 +10,23 @@ export interface RegistryAddress {
|
|
|
10
10
|
/** 将 `host:port` 或 `[ipv6]:port` 形式的 key 解析为地址(端口取最后一个 `:` 之后) */
|
|
11
11
|
export declare function parseAddressKey(key: string): RegistryAddress | undefined;
|
|
12
12
|
export declare function selectRandomRegistryAddress(keys: Iterable<string>): RegistryAddress | undefined;
|
|
13
|
+
export declare function getRegistryConfigsDir(): string;
|
|
14
|
+
export declare function namespaceToConfigFile(ns: string): string;
|
|
15
|
+
export declare function parseConfigFilename(filename: string): string | null;
|
|
13
16
|
export declare class Registry extends Server {
|
|
14
17
|
private readonly namespaces;
|
|
15
18
|
private unregisterFind?;
|
|
16
19
|
private static readonly HEARTBEAT_INTERVAL;
|
|
17
20
|
private static readonly HEARTBEAT_TIMEOUT;
|
|
18
21
|
private readonly heartbeats;
|
|
22
|
+
private readonly workspace;
|
|
23
|
+
private readonly configFileSuffix;
|
|
24
|
+
private readonly configs;
|
|
19
25
|
constructor(props?: MicroServerProps);
|
|
26
|
+
watchEnvFile(): import("node:fs").FSWatcher | undefined;
|
|
20
27
|
listen(port?: number): Promise<() => Promise<void>>;
|
|
21
28
|
/** 幂等:重复调用会先注销上一条 `/-/find` 再注册,避免叠多条路由 */
|
|
22
29
|
onFind(): void;
|
|
23
30
|
private mountFindHandler;
|
|
31
|
+
private registerEnvVariables;
|
|
24
32
|
}
|
package/dist/registry.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Server } from './server.js';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { resolve, join } from 'node:path';
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, watch } from 'node:fs';
|
|
5
|
+
import YAML from 'yaml';
|
|
2
6
|
/** 将 `host:port` 或 `[ipv6]:port` 形式的 key 解析为地址(端口取最后一个 `:` 之后) */
|
|
3
7
|
export function parseAddressKey(key) {
|
|
4
8
|
const i = key.lastIndexOf(':');
|
|
@@ -24,14 +28,33 @@ export function selectRandomRegistryAddress(keys) {
|
|
|
24
28
|
const index = Math.floor(Math.random() * addresses.length);
|
|
25
29
|
return addresses[index];
|
|
26
30
|
}
|
|
31
|
+
export function getRegistryConfigsDir() {
|
|
32
|
+
return resolve(homedir(), '.registry', 'configs');
|
|
33
|
+
}
|
|
34
|
+
export function namespaceToConfigFile(ns) {
|
|
35
|
+
return join(getRegistryConfigsDir(), `${ns}.config.yaml`);
|
|
36
|
+
}
|
|
37
|
+
export function parseConfigFilename(filename) {
|
|
38
|
+
if (!filename.endsWith('.config.yaml'))
|
|
39
|
+
return null;
|
|
40
|
+
return filename.slice(0, -'.config.yaml'.length);
|
|
41
|
+
}
|
|
27
42
|
export class Registry extends Server {
|
|
28
43
|
namespaces = new Map();
|
|
29
44
|
unregisterFind;
|
|
30
45
|
static HEARTBEAT_INTERVAL = 1000;
|
|
31
46
|
static HEARTBEAT_TIMEOUT = 20000;
|
|
32
47
|
heartbeats = new Map();
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
workspace;
|
|
49
|
+
configFileSuffix = '.config.yaml';
|
|
50
|
+
configs = new Map();
|
|
51
|
+
constructor(props = {}) {
|
|
52
|
+
const workspace = resolve(homedir(), '.registry');
|
|
53
|
+
if (!existsSync(workspace)) {
|
|
54
|
+
mkdirSync(workspace, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
super('registry', props);
|
|
57
|
+
this.workspace = workspace;
|
|
35
58
|
this.events.on('connect', (client, extras) => {
|
|
36
59
|
const key = client.host + ':' + client.port;
|
|
37
60
|
this.heartbeats.set(key, Date.now());
|
|
@@ -56,6 +79,7 @@ export class Registry extends Server {
|
|
|
56
79
|
}
|
|
57
80
|
});
|
|
58
81
|
this.mountFindHandler();
|
|
82
|
+
this.registerEnvVariables();
|
|
59
83
|
this.register('/-/heartbeat', async ({ client }) => {
|
|
60
84
|
if (!client)
|
|
61
85
|
return;
|
|
@@ -63,8 +87,38 @@ export class Registry extends Server {
|
|
|
63
87
|
this.heartbeats.set(key, Date.now());
|
|
64
88
|
});
|
|
65
89
|
}
|
|
90
|
+
watchEnvFile() {
|
|
91
|
+
const configFile = resolve(this.workspace, 'configs');
|
|
92
|
+
if (!existsSync(configFile))
|
|
93
|
+
return;
|
|
94
|
+
const configFiles = readdirSync(configFile).filter(filename => filename.endsWith(this.configFileSuffix));
|
|
95
|
+
for (const filename of configFiles) {
|
|
96
|
+
try {
|
|
97
|
+
const config = YAML.parse(readFileSync(resolve(configFile, filename), 'utf8'));
|
|
98
|
+
if (typeof config !== 'object' || config === null)
|
|
99
|
+
continue;
|
|
100
|
+
this.configs.set(parseConfigFilename(filename), config);
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
}
|
|
104
|
+
return watch(configFile, (_, filename) => {
|
|
105
|
+
if (!filename?.endsWith(this.configFileSuffix))
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
const config = YAML.parse(readFileSync(resolve(configFile, filename), 'utf8'));
|
|
109
|
+
if (typeof config !== 'object' || config === null)
|
|
110
|
+
return;
|
|
111
|
+
this.configs.set(parseConfigFilename(filename), config);
|
|
112
|
+
}
|
|
113
|
+
catch { /* vim 替换文件时的中间态读错误,忽略 */ }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
66
116
|
async listen(port = 0) {
|
|
67
|
-
const
|
|
117
|
+
const registry_port = process.env.REGISTRY_PORT ? Number(process.env.REGISTRY_PORT) : 0;
|
|
118
|
+
const _port = port || registry_port;
|
|
119
|
+
if (!_port || _port <= 0)
|
|
120
|
+
throw new Error('Unable to resolve registry port: pass `port` in constructor options, or ensure process.env.REGISTRY_PORT is set.');
|
|
121
|
+
const teardown = await super.listen(_port);
|
|
68
122
|
const timer = setInterval(() => {
|
|
69
123
|
const now = Date.now();
|
|
70
124
|
for (const [key, lastTime] of this.heartbeats) {
|
|
@@ -77,7 +131,10 @@ export class Registry extends Server {
|
|
|
77
131
|
}
|
|
78
132
|
}
|
|
79
133
|
}, Registry.HEARTBEAT_INTERVAL);
|
|
134
|
+
const watcher = this.watchEnvFile();
|
|
80
135
|
return async () => {
|
|
136
|
+
if (watcher)
|
|
137
|
+
watcher.close();
|
|
81
138
|
clearInterval(timer);
|
|
82
139
|
await teardown();
|
|
83
140
|
};
|
|
@@ -106,4 +163,21 @@ export class Registry extends Server {
|
|
|
106
163
|
return selectRandomRegistryAddress(keys);
|
|
107
164
|
});
|
|
108
165
|
}
|
|
166
|
+
registerEnvVariables() {
|
|
167
|
+
this.register('/-/env/variables', async ({ data }) => {
|
|
168
|
+
return data.map(({ namespace, fields }) => {
|
|
169
|
+
if (!this.configs.has(namespace)) {
|
|
170
|
+
return { namespace, value: null };
|
|
171
|
+
}
|
|
172
|
+
if (!fields?.length)
|
|
173
|
+
return { namespace, value: this.configs.get(namespace) };
|
|
174
|
+
const config = this.configs.get(namespace);
|
|
175
|
+
const value = {};
|
|
176
|
+
for (const field of fields) {
|
|
177
|
+
value[field] = config[field];
|
|
178
|
+
}
|
|
179
|
+
return { namespace, value };
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
109
183
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare class Server extends MessageLoader {
|
|
|
18
18
|
protected readonly clients: Map<string, Client>;
|
|
19
19
|
private readonly announceHost;
|
|
20
20
|
readonly events: EventEmitter<any>;
|
|
21
|
+
get host(): string;
|
|
21
22
|
constructor(namespace: string, props?: MicroServerProps);
|
|
22
23
|
private upstream;
|
|
23
24
|
private createClient;
|
package/dist/server.js
CHANGED
|
@@ -12,6 +12,9 @@ export class Server extends MessageLoader {
|
|
|
12
12
|
clients = new Map();
|
|
13
13
|
announceHost;
|
|
14
14
|
events = new EventEmitter();
|
|
15
|
+
get host() {
|
|
16
|
+
return this.announceHost;
|
|
17
|
+
}
|
|
15
18
|
constructor(namespace, props = {}) {
|
|
16
19
|
const { advertiseHost, ...loaderProps } = props;
|
|
17
20
|
super(loaderProps);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/micro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"@hile/message-loader": "^1.0.8",
|
|
27
27
|
"@hile/message-ws": "^1.0.6",
|
|
28
28
|
"internal-ip": "^9.0.0",
|
|
29
|
-
"ws": "^8.19.0"
|
|
29
|
+
"ws": "^8.19.0",
|
|
30
|
+
"yaml": "^2.9.0"
|
|
30
31
|
},
|
|
31
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "b2272b434848ff9d93a88796e25a66665afcaa0a"
|
|
32
33
|
}
|