@hile/micro-dynamic-configs 3.0.0 → 3.0.2

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 +280 -0
  2. package/README.md +67 -62
  3. package/package.json +5 -5
package/AI.md ADDED
@@ -0,0 +1,280 @@
1
+ # AI Guide For @hile/micro-dynamic-configs
2
+
3
+
4
+
5
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
6
+
7
+
8
+
9
+ Purpose: Persist runtime config in Redis and publish typed changes through @hile/micro.
10
+
11
+
12
+
13
+ Use this file when an AI agent installs the npm package and needs package-local examples, package selection rules, boundaries, and verification steps.
14
+
15
+
16
+
17
+ ## Package Selection
18
+
19
+
20
+
21
+ | User asks for | Use | Also read |
22
+ |---|---|---|
23
+ | Push runtime config without restarts | `@hile/micro-dynamic-configs` | `packages/messaging-micro.md`, `recipes/runtime-config.md` |
24
+
25
+
26
+
27
+ # Messaging And Microservices
28
+
29
+ Packages: `@hile/message-modem`, `@hile/message-ws`, `@hile/message-ipc`, `@hile/message-worker-thread`, `@hile/message-loader`, `@hile/micro`, `@hile/micro-dynamic-configs`.
30
+
31
+ ## Copy-Paste Example
32
+
33
+ Message handler file:
34
+
35
+ ```ts
36
+ // src/messages/ping.msg.ts
37
+ import { defineMessage } from '@hile/message-loader'
38
+
39
+ export default defineMessage(async ({ data, params }) => {
40
+ return {
41
+ type: 'pong',
42
+ data,
43
+ params,
44
+ timestamp: Date.now(),
45
+ }
46
+ })
47
+ ```
48
+
49
+ Microservice boot file:
50
+
51
+ ```ts
52
+ // src/services/app.boot.ts
53
+ import { defineService } from '@hile/core'
54
+ import { Application } from '@hile/micro'
55
+
56
+ export default defineService('micro.app', async (shutdown) => {
57
+ const app = new Application({
58
+ namespace: process.env.MICRO_NAMESPACE ?? 'example.service',
59
+ registry: {
60
+ host: process.env.REGISTRY_HOST ?? '127.0.0.1',
61
+ port: Number(process.env.REGISTRY_PORT ?? 9876),
62
+ },
63
+ advertiseHost: process.env.HILE_ADVERTISE_HOST ?? '127.0.0.1',
64
+ })
65
+
66
+ await app.load(new URL('../messages', import.meta.url).pathname)
67
+ const stop = await app.listen(Number(process.env.MICRO_PORT ?? 0))
68
+ shutdown(stop)
69
+ return app
70
+ })
71
+ ```
72
+
73
+ Caller:
74
+
75
+ ```ts
76
+ const result = await app.call('example.service', '/ping', { hello: 'world' })
77
+ ```
78
+
79
+ ## More Examples
80
+
81
+ Streaming handler:
82
+
83
+ ```ts
84
+ // src/messages/events.msg.ts
85
+ import { defineMessage } from '@hile/message-loader'
86
+
87
+ export default defineMessage(async function* () {
88
+ for (let i = 0; i < 3; i++) {
89
+ yield { seq: i }
90
+ }
91
+ })
92
+ ```
93
+
94
+ Streaming caller:
95
+
96
+ ```ts
97
+ const stream = await app.stream('example.service', '/events', {})
98
+ for await (const chunk of stream) {
99
+ console.log(chunk)
100
+ }
101
+ ```
102
+
103
+ Custom WebSocket modem:
104
+
105
+ ```ts
106
+ import { MessageWs } from '@hile/message-ws'
107
+ import type WebSocket from 'ws'
108
+
109
+ class RpcWs extends MessageWs {
110
+ constructor(ws: WebSocket, private readonly dispatch: (url: string, data: unknown) => Promise<unknown>) {
111
+ super(ws)
112
+ }
113
+
114
+ protected exec(data: { url: string; data: unknown }) {
115
+ return this.dispatch(data.url, data.data)
116
+ }
117
+
118
+ request<T>(url: string, data: unknown, timeout = 30_000) {
119
+ return this._send<T>({ url, data }, { timeout })
120
+ }
121
+ }
122
+ ```
123
+
124
+ Notice that `request()` returns a `Promise<T>`. Await it directly.
125
+
126
+ ## Use When
127
+
128
+ Use the message packages for request/response messaging over WebSocket, process IPC, worker threads, file-system message handlers, service discovery, streaming RPC, and registry-backed pub/sub.
129
+
130
+ ## Do Not Use When
131
+
132
+ - Do not use `stream()` for normal single-result calls.
133
+ - Do not rely on message IDs for business idempotency. They are transport IDs.
134
+ - Do not bypass `defineMessage()` for file-loaded handlers.
135
+
136
+ ## Install
137
+
138
+ ```bash
139
+ pnpm add @hile/micro @hile/message-loader @hile/message-ws
140
+ ```
141
+
142
+ Use transport-specific packages only when you need to build custom IPC or worker-thread bridges.
143
+
144
+ ## Imports
145
+
146
+ ```ts
147
+ import { defineMessage, MessageLoader } from '@hile/message-loader'
148
+ import { Application, Registry, Server } from '@hile/micro'
149
+ import { MessageWs } from '@hile/message-ws'
150
+ import { MessageIpc } from '@hile/message-ipc'
151
+ import { MessageWorkerThread } from '@hile/message-worker-thread'
152
+ ```
153
+
154
+ ## Compose With
155
+
156
+ - `@hile/context` propagates context in micro message metadata.
157
+ - `@hile/redis-idempotency` protects retryable side effects in message handlers.
158
+ - `@hile/redis-stream-queue` is better for durable background jobs.
159
+
160
+ ## Runtime And Lifecycle Notes
161
+
162
+ - `MessageLoader` maps `*.msg.*` files to routes using `@hile/loader`.
163
+ - `MessageLoader.dispatch(path, data, extras?)` invokes the matched handler.
164
+ - `MessageModem._send()` returns a `Promise`.
165
+ - `MessageModem._stream()` returns a Node `Readable` in object mode.
166
+ - A stream request requires `exec()` to return an async iterable.
167
+ - `Application.call(namespace, url, data, options?)` returns a promise.
168
+ - `Application.stream(namespace, url, data, options?)` returns a readable stream.
169
+ - `Application.publish(topic, payload)` returns an object with `update()` and `unpublish()`.
170
+ - `Application.subscribe(topic, callback)` returns an unsubscribe function.
171
+ - `Registry` stores service addresses and retained config/topic state under `~/.registry`.
172
+
173
+ ## Anti-Patterns
174
+
175
+ - Appending a secondary response getter to `client.request('/x', data)`
176
+ - Returning a plain object from a handler called through `stream()`.
177
+ - Using pub/sub as a durable queue.
178
+ - Forgetting to register `shutdown(await app.listen(...))`.
179
+
180
+ ## Verification Checklist
181
+
182
+ - Message files default-export `defineMessage(...)`.
183
+ - RPC callers use `await app.call(...)`.
184
+ - Streaming handlers are async generators.
185
+ - Registry is started before application nodes need discovery.
186
+ - Micro apps use stable namespaces and advertise reachable hosts.
187
+
188
+
189
+
190
+ # Related Recipes
191
+
192
+
193
+
194
+ # Runtime Dynamic Config
195
+
196
+ ## Complete Example
197
+
198
+ ```ts
199
+ import { z } from 'zod'
200
+ import { MicroDynamicConfigsServer } from '@hile/micro-dynamic-configs'
201
+
202
+ const schema = z.object({
203
+ featureCheckout: z.boolean().default(false),
204
+ maxRetries: z.number().int().min(1).max(10).default(3),
205
+ })
206
+
207
+ const configs = new MicroDynamicConfigsServer({
208
+ app,
209
+ redis,
210
+ schema,
211
+ redis_key: 'configs:checkout',
212
+ })
213
+
214
+ const cleanup = await configs.initialize()
215
+ shutdown(cleanup)
216
+
217
+ configs.on('change:featureCheckout', (next, previous) => {
218
+ logger.info({ previous, next }, 'featureCheckout changed')
219
+ })
220
+
221
+ await configs.save({ featureCheckout: true })
222
+ ```
223
+
224
+ ## File Layout
225
+
226
+ ```text
227
+ src/services/configs.boot.ts
228
+ src/services/app.boot.ts
229
+ ```
230
+
231
+ ## User Intent
232
+
233
+ Use this recipe when config changes should persist to Redis and be pushed through the micro registry without restarting services.
234
+
235
+ ## Packages To Use
236
+
237
+ - `@hile/micro-dynamic-configs`
238
+ - `@hile/micro`
239
+ - `@hile/ioredis`
240
+ - `zod`
241
+
242
+ ## Implementation Steps
243
+
244
+ 1. Define a Zod object schema with defaults.
245
+ 2. Create `MicroDynamicConfigsServer`.
246
+ 3. Call `initialize()` after `app` and `redis` are ready.
247
+ 4. Register cleanup with `shutdown()`.
248
+ 5. Use `.save(partial)` for changes.
249
+
250
+ ## Failure And Cleanup Behavior
251
+
252
+ - `save()` validates fields before mutating memory.
253
+ - Redis is written before memory and event emission update.
254
+ - `initialize()` publishes every config field as a topic.
255
+ - Cleanup unpublishes topics and removes listeners.
256
+
257
+ ## Verification Checklist
258
+
259
+ - Schema defaults parse `{}`.
260
+ - `redis_key` is app-specific.
261
+ - Change handlers use `change:key`.
262
+ - Cleanup from `initialize()` is registered.
263
+
264
+
265
+
266
+ # Global Guardrails
267
+
268
+
269
+
270
+ ## Never Generate These Patterns
271
+
272
+ - Do not call `loadService()` at module top level; it starts resources during import.
273
+ - Do not default-export plain functions from `*.boot.*` files; `hile start` expects a Hile service.
274
+ - Do not set `ctx.body` and also return a controller value.
275
+ - Do not assume `@hile/http` Zod validation mutates or coerces `ctx.query`, `ctx.params`, or `ctx.request.body`.
276
+ - Do not put reusable business logic only in controllers, pages, queue workers, or message handlers.
277
+ - Do not use old message examples that append a secondary response getter; current request APIs return promises directly.
278
+ - Do not claim exactly-once delivery or execution from Redis locks, queues, idempotency, or rate limits.
279
+ - Do not use queue `jobId` as the only side-effect idempotency boundary.
280
+ - Do not log the entire async context by default.
package/README.md CHANGED
@@ -1,85 +1,90 @@
1
1
  # @hile/micro-dynamic-configs
2
2
 
3
- Dynamic configuration server for @hile/micro. Stores config in Redis, validates with Zod, and pushes changes to subscribers via micro's built-in pub/sub.
3
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
4
4
 
5
- ## Usage
5
+ Persist runtime config in Redis and publish typed changes through @hile/micro.
6
6
 
7
- ```ts
8
- import { Application } from '@hile/micro';
9
- import { MicroDynamicConfigsServer } from '@hile/micro-dynamic-configs';
10
- import Redis from 'ioredis';
11
- import { z } from 'zod';
12
-
13
- const schema = z.object({
14
- name: z.string().default(''),
15
- port: z.number().default(8080),
16
- debug: z.boolean().default(false),
17
- });
18
-
19
- const app = new Application({
20
- namespace: 'config-svc',
21
- registry: { host: '127.0.0.1', port: 6379 },
22
- });
23
-
24
- const redis = new Redis({ host: '127.0.0.1', port: 6379 });
25
-
26
- const configs = new MicroDynamicConfigsServer({
27
- app,
28
- redis,
29
- schema,
30
- redis_key: 'my-app:config',
31
- });
32
-
33
- await configs.initialize();
34
-
35
- // Read current value
36
- console.log(configs.value); // { name: '', port: 8080, debug: false }
37
-
38
- // Update and persist — subscribers receive push
39
- await configs.save({ name: 'production', port: 9090 });
40
- ```
7
+ This README is intentionally short and example-first. The complete AI-facing guide ships in `AI.md` in this package.
41
8
 
42
- ## Topic Convention
9
+ ## When To Use
43
10
 
44
- Each schema field publishes to a separate topic:
11
+ Use the message packages for request/response messaging over WebSocket, process IPC, worker threads, file-system message handlers, service discovery, streaming RPC, and registry-backed pub/sub.
45
12
 
46
- ```
47
- {namespace}:{field}
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @hile/micro-dynamic-configs
48
17
  ```
49
18
 
50
- For example, with namespace `config-svc` and schema fields `name`, `port`, `debug`:
19
+ ## Copy-Paste Example
51
20
 
52
- - `config-svc:name`
53
- - `config-svc:port`
54
- - `config-svc:debug`
21
+ Message handler file:
55
22
 
56
- ## Subscribing
23
+ ```ts
24
+ // src/messages/ping.msg.ts
25
+ import { defineMessage } from '@hile/message-loader'
26
+
27
+ export default defineMessage(async ({ data, params }) => {
28
+ return {
29
+ type: 'pong',
30
+ data,
31
+ params,
32
+ timestamp: Date.now(),
33
+ }
34
+ })
35
+ ```
57
36
 
58
- Use `app.subscribe()` directly on any micro Application:
37
+ Microservice boot file:
59
38
 
60
39
  ```ts
61
- const values: Record<string, any> = {};
62
- const unsub = await app.subscribe('config-svc:name', (v) => values.name = v);
40
+ // src/services/app.boot.ts
41
+ import { defineService } from '@hile/core'
42
+ import { Application } from '@hile/micro'
43
+
44
+ export default defineService('micro.app', async (shutdown) => {
45
+ const app = new Application({
46
+ namespace: process.env.MICRO_NAMESPACE ?? 'example.service',
47
+ registry: {
48
+ host: process.env.REGISTRY_HOST ?? '127.0.0.1',
49
+ port: Number(process.env.REGISTRY_PORT ?? 9876),
50
+ },
51
+ advertiseHost: process.env.HILE_ADVERTISE_HOST ?? '127.0.0.1',
52
+ })
53
+
54
+ await app.load(new URL('../messages', import.meta.url).pathname)
55
+ const stop = await app.listen(Number(process.env.MICRO_PORT ?? 0))
56
+ shutdown(stop)
57
+ return app
58
+ })
63
59
  ```
64
60
 
65
- ## Local Events
66
-
67
- The server emits `change:{field}` events locally:
61
+ Caller:
68
62
 
69
63
  ```ts
70
- configs.on('change:name', (newValue, oldValue) => {
71
- console.log(`name changed from ${oldValue} to ${newValue}`);
72
- });
64
+ const result = await app.call('example.service', '/ping', { hello: 'world' })
73
65
  ```
74
66
 
75
- ## Persistence
67
+ ## Boundaries
76
68
 
77
- Config is persisted to Redis on every `save()`. On `initialize()`, the server loads the last saved state from Redis. Schema defaults apply when no value exists.
69
+ - Do not use `stream()` for normal single-result calls.
70
+ - Do not rely on message IDs for business idempotency. They are transport IDs.
71
+ - Do not bypass `defineMessage()` for file-loaded handlers.
78
72
 
79
- ## Teardown
73
+ - Appending a secondary response getter to `client.request('/x', data)`
74
+ - Returning a plain object from a handler called through `stream()`.
75
+ - Using pub/sub as a durable queue.
76
+ - Forgetting to register `shutdown(await app.listen(...))`.
80
77
 
81
- ```ts
82
- const teardown = await configs.initialize();
83
- // Later:
84
- await teardown(); // unpublishes all topics and cleans up listeners
85
- ```
78
+ ## Verify
79
+
80
+ - Message files default-export `defineMessage(...)`.
81
+ - RPC callers use `await app.call(...)`.
82
+ - Streaming handlers are async generators.
83
+ - Registry is started before application nodes need discovery.
84
+ - Micro apps use stable namespaces and advertise reachable hosts.
85
+
86
+ ## More Context
87
+
88
+ - `AI.md` in this package: full package-local AI guide.
89
+ - Root `llms-full.txt`: full monorepo AI context.
90
+ - Root `references/`: source files copied from `docs/ai`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/micro-dynamic-configs",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "README.md",
14
- "SKILL.md"
14
+ "AI.md"
15
15
  ],
16
16
  "license": "MIT",
17
17
  "publishConfig": {
@@ -22,10 +22,10 @@
22
22
  "vitest": "^4.0.18"
23
23
  },
24
24
  "dependencies": {
25
- "@hile/ioredis": "^2.1.1",
26
- "@hile/micro": "^3.0.0",
25
+ "@hile/ioredis": "^3.0.0",
26
+ "@hile/micro": "^3.0.2",
27
27
  "ioredis": "^5.11.0",
28
28
  "zod": "^4.4.3"
29
29
  },
30
- "gitHead": "14b55afba0e9af80a782eaa84f6f48c1e66861a1"
30
+ "gitHead": "5d6a724ac4e99d493d42efe337e3ad17db40c3f7"
31
31
  }