@hile/micro 3.0.2 → 3.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.
Files changed (3) hide show
  1. package/AI.md +116 -0
  2. package/dist/application.js +9 -6
  3. package/package.json +11 -12
package/AI.md CHANGED
@@ -363,6 +363,122 @@ Use this recipe when config changes should persist to Redis and be pushed throug
363
363
  - Change handlers use `change:key`.
364
364
  - Cleanup from `initialize()` is registered.
365
365
 
366
+ # Stable Runtime Reload
367
+
368
+ ## Complete Example
369
+
370
+ ```ts
371
+ import { defineService, loadService } from '@hile/core'
372
+ import { Application } from '@hile/micro'
373
+ import { Http } from '@hile/http'
374
+ import { createConfigAggregator, createRuntimeReloader } from '@hile/reloader'
375
+ import appService from './micro.app.boot'
376
+ import httpService from './http.boot'
377
+
378
+ type RuntimeConfig = {
379
+ mysql: { host: string; port: number }
380
+ redis: { host: string; port: number }
381
+ flags: Record<string, boolean>
382
+ }
383
+
384
+ export default defineService('runtime.reload', async (shutdown) => {
385
+ const app = await loadService<Application>(appService)
386
+ const http = await loadService<Http>(httpService)
387
+
388
+ const reloader = createRuntimeReloader<RuntimeConfig, AppRuntime>({
389
+ debounceMs: 100,
390
+ normalize: config => ({
391
+ mysql: config.mysql,
392
+ redis: config.redis,
393
+ flags: config.flags,
394
+ }),
395
+ create: config => createAppRuntime(config),
396
+ dispose: runtime => runtime.close(),
397
+ onError: (error, context) => {
398
+ logger.error({ error, stage: context.stage }, 'runtime reload failed')
399
+ },
400
+ })
401
+
402
+ const configs = createConfigAggregator<RuntimeConfig>({
403
+ required: ['mysql', 'redis'],
404
+ defaults: { flags: {} },
405
+ debounceMs: 100,
406
+ onError: (error) => {
407
+ logger.error({ error }, 'runtime config emit failed')
408
+ },
409
+ })
410
+
411
+ configs.onChange(config => reloader.update(config))
412
+
413
+ const cleanupMysql = await app.subscribe('config:mysql', value => configs.set('mysql', value))
414
+ const cleanupRedis = await app.subscribe('config:redis', value => configs.set('redis', value))
415
+ const cleanupFlags = await app.subscribe('config:flags', value => configs.set('flags', value))
416
+
417
+ http.use(async (ctx, next) => {
418
+ const runtime = reloader.current()
419
+ if (!runtime) {
420
+ ctx.status = 503
421
+ ctx.body = { error: 'runtime is not ready' }
422
+ return
423
+ }
424
+ await runtime.handle(ctx, next)
425
+ })
426
+
427
+ shutdown(async () => {
428
+ await cleanupMysql()
429
+ await cleanupRedis()
430
+ await cleanupFlags()
431
+ configs.dispose()
432
+ await reloader.stop()
433
+ })
434
+
435
+ return reloader
436
+ })
437
+ ```
438
+
439
+ ## File Layout
440
+
441
+ ```text
442
+ src/services/runtime.reload.boot.ts
443
+ src/services/micro.app.boot.ts
444
+ src/services/http.boot.ts
445
+ ```
446
+
447
+ ## User Intent
448
+
449
+ Use this recipe when `app.subscribe()` receives config updates that should rebuild business runtime state without thrashing services or rebinding ports.
450
+
451
+ ## Packages To Use
452
+
453
+ - `@hile/reloader`
454
+ - `@hile/micro`
455
+ - `@hile/http`
456
+ - `@hile/core`
457
+
458
+ ## Implementation Steps
459
+
460
+ 1. Keep the HTTP listener outside the reloader.
461
+ 2. Create `RuntimeReloader` for the runtime object that can be safely swapped.
462
+ 3. Create `ConfigAggregator` for all required subscribe topics.
463
+ 4. Feed each `app.subscribe()` callback into `configs.set(key, value)`.
464
+ 5. Use `configs.onChange(config => reloader.update(config))`.
465
+ 6. Register subscribe cleanup, `configs.dispose()`, and `reloader.stop()` with shutdown.
466
+
467
+ ## Failure And Cleanup Behavior
468
+
469
+ - Config bursts collapse through aggregator debounce and reloader debounce.
470
+ - Reloads never run concurrently.
471
+ - If `create()` fails, the previous runtime stays active.
472
+ - Old runtime disposal happens after the new runtime becomes current.
473
+ - Same-port HTTP services are not restarted.
474
+
475
+ ## Verification Checklist
476
+
477
+ - Required config keys are received before the first runtime is created.
478
+ - Repeated equivalent configs do not reload.
479
+ - A failed create logs an error and leaves old traffic handling intact.
480
+ - The HTTP server is started once and reads `reloader.current()` per request.
481
+
366
482
 
367
483
 
368
484
  # Global Guardrails
@@ -124,6 +124,15 @@ export class Application extends Server {
124
124
  this._registryLookupTimeoutMs = registryLookupTimeoutMs;
125
125
  this._requestTimeoutMs = requestTimeoutMs;
126
126
  this._circuitBreaker = resolveCircuitBreakerOptions(circuitBreaker);
127
+ this.events.on('disconnect', (client) => {
128
+ const disconnectedKey = `${client.host}:${client.port}`;
129
+ // 一个物理连接可承载多个 namespace;集中清理可避免每次服务发现都向 Client 重复注册监听器。
130
+ for (const [namespace, stack] of this.namespaces) {
131
+ if (`${stack.host}:${stack.port}` !== disconnectedKey)
132
+ continue;
133
+ this.namespaces.delete(namespace);
134
+ }
135
+ });
127
136
  this.register('/-/health', async () => ({
128
137
  status: 'ok',
129
138
  registry: !!this.registry,
@@ -711,12 +720,6 @@ export class Application extends Server {
711
720
  }
712
721
  return client;
713
722
  });
714
- }).then(client => {
715
- client.events.on('disconnect', () => {
716
- if (this.namespaces.has(namespace)) {
717
- this.namespaces.delete(namespace);
718
- }
719
- });
720
723
  }).catch(e => {
721
724
  // Registry unavailable but previously cached client still valid -> degrade
722
725
  const cachedKey = `${cachedHost}:${cachedPort}`;
package/package.json CHANGED
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "name": "@hile/micro",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "type": "module",
5
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
6
  "files": [
12
7
  "dist",
13
8
  "README.md",
@@ -23,13 +18,17 @@
23
18
  "vitest": "^4.0.18"
24
19
  },
25
20
  "dependencies": {
21
+ "internal-ip": "^9.0.0",
22
+ "ws": "^8.21.0",
23
+ "yaml": "^2.9.0",
26
24
  "@hile/context": "^3.0.2",
27
25
  "@hile/logger": "^3.0.0",
28
26
  "@hile/message-loader": "^3.0.0",
29
- "@hile/message-ws": "^3.0.0",
30
- "internal-ip": "^9.0.0",
31
- "ws": "^8.21.0",
32
- "yaml": "^2.9.0"
27
+ "@hile/message-ws": "^3.0.0"
33
28
  },
34
- "gitHead": "5d6a724ac4e99d493d42efe337e3ad17db40c3f7"
35
- }
29
+ "scripts": {
30
+ "build": "tsc -b && fix-esm-import-path --preserve-import-type ./dist",
31
+ "dev": "tsc -b --watch",
32
+ "test": "vitest run"
33
+ }
34
+ }