@hile/micro 3.0.2 → 3.0.3
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/AI.md +116 -0
- package/package.json +2 -2
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/micro",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,5 +31,5 @@
|
|
|
31
31
|
"ws": "^8.21.0",
|
|
32
32
|
"yaml": "^2.9.0"
|
|
33
33
|
},
|
|
34
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "eac3a74c71ec2c514b920015f1c0d3415d3b8057"
|
|
35
35
|
}
|