@hile/micro 4.0.1 → 4.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 +40 -25
- package/README.md +13 -6
- package/dist/application.d.ts +13 -9
- package/dist/application.js +24 -13
- package/dist/client.d.ts +19 -5
- package/dist/client.js +66 -53
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/message.d.ts +12 -0
- package/dist/message.js +5 -0
- package/dist/registry.js +1 -1
- package/package.json +6 -6
package/AI.md
CHANGED
|
@@ -35,13 +35,14 @@ Message handler file:
|
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
37
|
// src/messages/ping.msg.ts
|
|
38
|
-
import {
|
|
38
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
39
39
|
|
|
40
|
-
export default
|
|
40
|
+
export default defineMicroMessage(async ({ data, params, invocation }) => {
|
|
41
41
|
return {
|
|
42
42
|
type: 'pong',
|
|
43
43
|
data,
|
|
44
44
|
params,
|
|
45
|
+
requestId: invocation.context.values.requestId,
|
|
45
46
|
timestamp: Date.now(),
|
|
46
47
|
}
|
|
47
48
|
})
|
|
@@ -74,7 +75,11 @@ export default defineService('micro.app', async (shutdown) => {
|
|
|
74
75
|
Caller:
|
|
75
76
|
|
|
76
77
|
```ts
|
|
77
|
-
|
|
78
|
+
import { randomUUID } from 'node:crypto'
|
|
79
|
+
import { createExecutionContext } from '@hile/context'
|
|
80
|
+
|
|
81
|
+
const context = createExecutionContext({ requestId: randomUUID() })
|
|
82
|
+
const result = await app.call('example.service', '/ping', { hello: 'world' }, { context })
|
|
78
83
|
```
|
|
79
84
|
|
|
80
85
|
## More Examples
|
|
@@ -83,11 +88,11 @@ Streaming handler:
|
|
|
83
88
|
|
|
84
89
|
```ts
|
|
85
90
|
// src/messages/events.msg.ts
|
|
86
|
-
import {
|
|
91
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
87
92
|
|
|
88
|
-
export default
|
|
93
|
+
export default defineMicroMessage(async function* ({ invocation }) {
|
|
89
94
|
for (let i = 0; i < 3; i++) {
|
|
90
|
-
yield { seq: i }
|
|
95
|
+
yield { seq: i, requestId: invocation.context.values.requestId }
|
|
91
96
|
}
|
|
92
97
|
})
|
|
93
98
|
```
|
|
@@ -95,7 +100,7 @@ export default defineMessage(async function* () {
|
|
|
95
100
|
Streaming caller:
|
|
96
101
|
|
|
97
102
|
```ts
|
|
98
|
-
const stream = await app.stream('example.service', '/events', {})
|
|
103
|
+
const stream = await app.stream('example.service', '/events', {}, { context })
|
|
99
104
|
for await (const chunk of stream) {
|
|
100
105
|
console.log(chunk)
|
|
101
106
|
}
|
|
@@ -132,12 +137,13 @@ Use the message packages for request/response messaging over WebSocket, process
|
|
|
132
137
|
|
|
133
138
|
- Do not use `stream()` for normal single-result calls.
|
|
134
139
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
135
|
-
-
|
|
140
|
+
- Use `defineMicroMessage()` for Micro business handlers; reserve generic `defineMessage()` for transport-neutral loaders.
|
|
141
|
+
- Do not pass zero, fractional, non-finite, or oversized message timeouts. Explicit timeout values must be safe integers from `1` through `2_147_483_647` milliseconds.
|
|
136
142
|
|
|
137
143
|
## Install
|
|
138
144
|
|
|
139
145
|
```bash
|
|
140
|
-
pnpm add @hile/micro @hile/message-loader @hile/message-ws
|
|
146
|
+
pnpm add @hile/context @hile/micro @hile/message-loader @hile/message-ws
|
|
141
147
|
```
|
|
142
148
|
|
|
143
149
|
Use transport-specific packages only when you need to build custom IPC or worker-thread bridges.
|
|
@@ -146,7 +152,8 @@ Use transport-specific packages only when you need to build custom IPC or worker
|
|
|
146
152
|
|
|
147
153
|
```ts
|
|
148
154
|
import { defineMessage, MessageLoader } from '@hile/message-loader'
|
|
149
|
-
import {
|
|
155
|
+
import { createExecutionContext } from '@hile/context'
|
|
156
|
+
import { Application, defineMicroMessage, Registry, Server } from '@hile/micro'
|
|
150
157
|
import { MessageWs } from '@hile/message-ws'
|
|
151
158
|
import { MessageIpc } from '@hile/message-ipc'
|
|
152
159
|
import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
@@ -154,7 +161,7 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
154
161
|
|
|
155
162
|
## Compose With
|
|
156
163
|
|
|
157
|
-
-
|
|
164
|
+
- Pass `ExecutionContext` explicitly in every business call or stream option; the receiver gets it in `invocation.context`.
|
|
158
165
|
- `@hile/redis-idempotency` protects retryable side effects in message handlers.
|
|
159
166
|
- `@hile/redis-stream-queue` is better for durable background jobs.
|
|
160
167
|
|
|
@@ -163,10 +170,14 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
163
170
|
- `MessageLoader` maps `*.msg.*` files to routes using `@hile/loader`.
|
|
164
171
|
- `MessageLoader.dispatch(path, data, extras?)` invokes the matched handler.
|
|
165
172
|
- `MessageModem._send()` returns a `Promise`.
|
|
173
|
+
- `MessageModem._send()` and `_push()` use a `30_000` ms timeout when none is provided. An explicit timeout must be a safe integer from `1` through `2_147_483_647`; invalid values throw `TypeError` before a message is sent.
|
|
166
174
|
- `MessageModem._stream()` returns a Node `Readable` in object mode.
|
|
175
|
+
- Stream `timeout` and `idleTimeout` values use the same `1` through `2_147_483_647` ms range. The stream `window` must be a safe integer from `1` through `64` and defaults to `1`.
|
|
176
|
+
- Each modem schedules request, total-stream, and idle-stream deadlines through one internal deadline scheduler. This reduces active Node.js timers without changing timeout, cancellation, ordering, or error semantics.
|
|
177
|
+
- `@hile/message-ws` keeps public `decodeMessageFrame()` payloads isolated from caller-owned input by default. Its owned WebSocket `RawData` path uses a zero-copy binary Flight payload view internally.
|
|
167
178
|
- A stream request requires `exec()` to return an async iterable.
|
|
168
|
-
- `Application.call(namespace, url, data, options
|
|
169
|
-
- `Application.stream(namespace, url, data, options
|
|
179
|
+
- `Application.call(namespace, url, data, options)` requires `options.context` and returns a promise.
|
|
180
|
+
- `Application.stream(namespace, url, data, options)` requires `options.context` and returns a readable stream.
|
|
170
181
|
- `Application.publish(topic, payload)` returns an object with `update()` and `unpublish()`.
|
|
171
182
|
- `Application.subscribe(topic, callback)` returns an unsubscribe function.
|
|
172
183
|
- `Registry` stores service addresses and retained config/topic state under `~/.registry`.
|
|
@@ -180,9 +191,10 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
180
191
|
|
|
181
192
|
## Verification Checklist
|
|
182
193
|
|
|
183
|
-
-
|
|
184
|
-
- RPC callers use `await app.call(
|
|
194
|
+
- Micro message files default-export `defineMicroMessage(...)` and receive `invocation.context`.
|
|
195
|
+
- RPC callers use `await app.call(..., { context })`.
|
|
185
196
|
- Streaming handlers are async generators.
|
|
197
|
+
- Custom modem timeout values use the documented safe-integer range.
|
|
186
198
|
- Registry is started before application nodes need discovery.
|
|
187
199
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
|
188
200
|
|
|
@@ -214,10 +226,10 @@ Provider handler:
|
|
|
214
226
|
|
|
215
227
|
```ts
|
|
216
228
|
// src/messages/charge.msg.ts
|
|
217
|
-
import {
|
|
229
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
218
230
|
|
|
219
|
-
export default
|
|
220
|
-
return { charged: true, input: data }
|
|
231
|
+
export default defineMicroMessage(async ({ data, invocation }) => {
|
|
232
|
+
return { charged: true, input: data, requestId: invocation.context.values.requestId }
|
|
221
233
|
})
|
|
222
234
|
```
|
|
223
235
|
|
|
@@ -245,10 +257,14 @@ export default defineService('billing.micro', async (shutdown) => {
|
|
|
245
257
|
Consumer:
|
|
246
258
|
|
|
247
259
|
```ts
|
|
260
|
+
import { randomUUID } from 'node:crypto'
|
|
261
|
+
import { createExecutionContext } from '@hile/context'
|
|
262
|
+
|
|
263
|
+
const context = createExecutionContext({ requestId: randomUUID(), tenantId: 't1' })
|
|
248
264
|
const result = await app.call('billing', '/charge', {
|
|
249
265
|
tenantId: 't1',
|
|
250
266
|
amount: 100,
|
|
251
|
-
})
|
|
267
|
+
}, { context })
|
|
252
268
|
```
|
|
253
269
|
|
|
254
270
|
## File Layout
|
|
@@ -268,16 +284,15 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
268
284
|
## Packages To Use
|
|
269
285
|
|
|
270
286
|
- `@hile/micro`
|
|
271
|
-
- `@hile/
|
|
272
|
-
- `@hile/context` when context must cross service boundaries
|
|
287
|
+
- `@hile/context` for the required explicit execution context carrier
|
|
273
288
|
- `@hile/redis-idempotency` for retryable side effects
|
|
274
289
|
|
|
275
290
|
## Implementation Steps
|
|
276
291
|
|
|
277
292
|
1. Start a Registry with `hile registry`.
|
|
278
293
|
2. Start providers with stable namespaces.
|
|
279
|
-
3.
|
|
280
|
-
4.
|
|
294
|
+
3. Default-export `defineMicroMessage()` handlers and load them through `app.load()`.
|
|
295
|
+
4. Create context at ingress and call providers with `await app.call(namespace, url, data, { context })`.
|
|
281
296
|
5. Use `app.stream()` only for async-generator handlers.
|
|
282
297
|
|
|
283
298
|
## Failure And Cleanup Behavior
|
|
@@ -290,8 +305,8 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
290
305
|
|
|
291
306
|
- Registry is reachable.
|
|
292
307
|
- Provider namespace matches consumer call.
|
|
293
|
-
- Handlers default-export `
|
|
294
|
-
- Consumer code awaits `app.call(
|
|
308
|
+
- Handlers default-export `defineMicroMessage()` and consume explicit invocation context when needed.
|
|
309
|
+
- Consumer code awaits `app.call(..., { context })` directly.
|
|
295
310
|
|
|
296
311
|
# Runtime Dynamic Config
|
|
297
312
|
|
package/README.md
CHANGED
|
@@ -22,13 +22,14 @@ Message handler file:
|
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
// src/messages/ping.msg.ts
|
|
25
|
-
import {
|
|
25
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
26
26
|
|
|
27
|
-
export default
|
|
27
|
+
export default defineMicroMessage(async ({ data, params, invocation }) => {
|
|
28
28
|
return {
|
|
29
29
|
type: 'pong',
|
|
30
30
|
data,
|
|
31
31
|
params,
|
|
32
|
+
requestId: invocation.context.values.requestId,
|
|
32
33
|
timestamp: Date.now(),
|
|
33
34
|
}
|
|
34
35
|
})
|
|
@@ -61,14 +62,19 @@ export default defineService('micro.app', async (shutdown) => {
|
|
|
61
62
|
Caller:
|
|
62
63
|
|
|
63
64
|
```ts
|
|
64
|
-
|
|
65
|
+
import { randomUUID } from 'node:crypto'
|
|
66
|
+
import { createExecutionContext } from '@hile/context'
|
|
67
|
+
|
|
68
|
+
const context = createExecutionContext({ requestId: randomUUID() })
|
|
69
|
+
const result = await app.call('example.service', '/ping', { hello: 'world' }, { context })
|
|
65
70
|
```
|
|
66
71
|
|
|
67
72
|
## Boundaries
|
|
68
73
|
|
|
69
74
|
- Do not use `stream()` for normal single-result calls.
|
|
70
75
|
- Do not rely on message IDs for business idempotency. They are transport IDs.
|
|
71
|
-
-
|
|
76
|
+
- Use `defineMicroMessage()` for Micro business handlers; reserve generic `defineMessage()` for transport-neutral loaders.
|
|
77
|
+
- Do not pass zero, fractional, non-finite, or oversized message timeouts. Explicit timeout values must be safe integers from `1` through `2_147_483_647` milliseconds.
|
|
72
78
|
|
|
73
79
|
- Appending a secondary response getter to `client.request('/x', data)`
|
|
74
80
|
- Returning a plain object from a handler called through `stream()`.
|
|
@@ -77,9 +83,10 @@ const result = await app.call('example.service', '/ping', { hello: 'world' })
|
|
|
77
83
|
|
|
78
84
|
## Verify
|
|
79
85
|
|
|
80
|
-
-
|
|
81
|
-
- RPC callers use `await app.call(
|
|
86
|
+
- Micro message files default-export `defineMicroMessage(...)` and receive `invocation.context`.
|
|
87
|
+
- RPC callers use `await app.call(..., { context })`.
|
|
82
88
|
- Streaming handlers are async generators.
|
|
89
|
+
- Custom modem timeout values use the documented safe-integer range.
|
|
83
90
|
- Registry is started before application nodes need discovery.
|
|
84
91
|
- Micro apps use stable namespaces and advertise reachable hosts.
|
|
85
92
|
|
package/dist/application.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ExecutionContext } from '@hile/context';
|
|
1
2
|
import { Client, type ClientStreamOptions } from './client.js';
|
|
2
3
|
import { Server, type MicroServerProps } from './server.js';
|
|
3
4
|
import type { RegistryAddress, RegistryTopicSnapshot, RegistryTopicSnapshotsResult, RegistryTopicSummary } from './registry';
|
|
@@ -48,6 +49,15 @@ export type ApplicationProps = {
|
|
|
48
49
|
/** 本地内存熔断策略配置 */
|
|
49
50
|
circuitBreaker?: CircuitBreakerOptions;
|
|
50
51
|
} & MicroServerProps;
|
|
52
|
+
export type ApplicationCallOptions = {
|
|
53
|
+
context: ExecutionContext;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
retries?: number;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
};
|
|
58
|
+
export type ApplicationStreamOptions = ClientStreamOptions & {
|
|
59
|
+
retries?: number;
|
|
60
|
+
};
|
|
51
61
|
export declare class Application extends Server {
|
|
52
62
|
private registry?;
|
|
53
63
|
private reconnectTimeout?;
|
|
@@ -108,16 +118,10 @@ export declare class Application extends Server {
|
|
|
108
118
|
private findFromRegistry;
|
|
109
119
|
get(namespace: string, exclude?: string[]): Promise<Client>;
|
|
110
120
|
protected resolveClient(namespace: string, exclude?: string[], options?: RegistryLookupOptions): Promise<Client>;
|
|
111
|
-
call<T = any>(namespace: string, url: string, data: any, options
|
|
112
|
-
|
|
113
|
-
retries?: number;
|
|
114
|
-
signal?: AbortSignal;
|
|
115
|
-
}): Promise<T>;
|
|
116
|
-
stream(namespace: string, url: string, data: any, options?: ClientStreamOptions & {
|
|
117
|
-
retries?: number;
|
|
118
|
-
}): Promise<import('stream').Readable>;
|
|
121
|
+
call<T = any>(namespace: string, url: string, data: any, options: ApplicationCallOptions): Promise<T>;
|
|
122
|
+
stream(namespace: string, url: string, data: any, options: ApplicationStreamOptions): Promise<import('stream').Readable>;
|
|
119
123
|
/** Opens a stream against one exact service instance without registry selection. */
|
|
120
|
-
streamPeer(address: RegistryAddress, url: string, data: any, options
|
|
124
|
+
streamPeer(address: RegistryAddress, url: string, data: any, options: ClientStreamOptions): Promise<import('stream').Readable>;
|
|
121
125
|
publish<T = any>(topic: string, data: T): Promise<{
|
|
122
126
|
update: (payload: T) => Promise</*elided*/ any>;
|
|
123
127
|
unpublish: () => Promise</*elided*/ any>;
|
package/dist/application.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MissingExecutionContextError, parseExecutionContext, } from '@hile/context';
|
|
1
2
|
import { Server } from './server.js';
|
|
2
3
|
var RegistryLookupStatus;
|
|
3
4
|
(function (RegistryLookupStatus) {
|
|
@@ -302,7 +303,7 @@ export class Application extends Server {
|
|
|
302
303
|
if (preserveRevision && this.publishedTopicRevisions.has(topic)) {
|
|
303
304
|
request.revision = this.publishedTopicRevisions.get(topic);
|
|
304
305
|
}
|
|
305
|
-
const revision = await registry.
|
|
306
|
+
const revision = await registry.requestControl('/-/declare', request, this.registryRequestOptions());
|
|
306
307
|
if (this.publishedTopics.has(topic) &&
|
|
307
308
|
this.publishedTopicVersions.get(topic) === version &&
|
|
308
309
|
Number.isFinite(revision)) {
|
|
@@ -313,12 +314,12 @@ export class Application extends Server {
|
|
|
313
314
|
}
|
|
314
315
|
syncUnpublishedTopic(topic, options = {}) {
|
|
315
316
|
return this.enqueueTopicSync(topic, async (registry) => {
|
|
316
|
-
await registry.
|
|
317
|
+
await registry.requestControl('/-/undeclare', { topic }, this.registryRequestOptions());
|
|
317
318
|
}, options);
|
|
318
319
|
}
|
|
319
320
|
syncUnsubscribedTopic(topic, options = {}) {
|
|
320
321
|
return this.enqueueTopicSync(topic, async (registry) => {
|
|
321
|
-
await registry.
|
|
322
|
+
await registry.requestControl('/-/unsubscribe', { topic }, this.registryRequestOptions());
|
|
322
323
|
}, options);
|
|
323
324
|
}
|
|
324
325
|
syncRestoredSubscription(topic, callback, options = {}) {
|
|
@@ -330,7 +331,7 @@ export class Application extends Server {
|
|
|
330
331
|
}
|
|
331
332
|
async restoreSubscription(topic, callback, registry, isReconnect, requireLocal = true) {
|
|
332
333
|
const replayBaseVersion = this.topicUpdateVersions.get(topic) ?? 0;
|
|
333
|
-
const snapshot = await registry.
|
|
334
|
+
const snapshot = await registry.requestControl('/-/subscribe', { topic }, this.registryRequestOptions());
|
|
334
335
|
if (requireLocal && !this.topics.get(topic)?.has(callback))
|
|
335
336
|
return;
|
|
336
337
|
if (!snapshot.hasData)
|
|
@@ -680,7 +681,7 @@ export class Application extends Server {
|
|
|
680
681
|
async findFromRegistry(namespace, exclude) {
|
|
681
682
|
if (!this.registry)
|
|
682
683
|
throw new Error('Registry not found');
|
|
683
|
-
const promise = this.registry.
|
|
684
|
+
const promise = this.registry.requestControl('/-/find', { namespace, exclude });
|
|
684
685
|
return await withTimeout(promise, this._registryLookupTimeoutMs, 'Registry /-/find');
|
|
685
686
|
}
|
|
686
687
|
get(namespace, exclude) {
|
|
@@ -752,7 +753,10 @@ export class Application extends Server {
|
|
|
752
753
|
});
|
|
753
754
|
}
|
|
754
755
|
async call(namespace, url, data, options) {
|
|
755
|
-
|
|
756
|
+
if (!options?.context)
|
|
757
|
+
throw new MissingExecutionContextError(`micro call ${namespace}${url}`);
|
|
758
|
+
const context = parseExecutionContext(options.context);
|
|
759
|
+
const { timeout = this._requestTimeoutMs, retries = 1, signal } = options;
|
|
756
760
|
let remainingRetries = retries;
|
|
757
761
|
let retrySourceError;
|
|
758
762
|
let hasRetrySourceError = false;
|
|
@@ -769,6 +773,7 @@ export class Application extends Server {
|
|
|
769
773
|
const { client, probe } = selected;
|
|
770
774
|
try {
|
|
771
775
|
const result = await client.request(url, data, {
|
|
776
|
+
context,
|
|
772
777
|
timeout: timeout ?? this._requestTimeoutMs,
|
|
773
778
|
signal,
|
|
774
779
|
});
|
|
@@ -791,7 +796,10 @@ export class Application extends Server {
|
|
|
791
796
|
}
|
|
792
797
|
}
|
|
793
798
|
async stream(namespace, url, data, options) {
|
|
794
|
-
|
|
799
|
+
if (!options?.context)
|
|
800
|
+
throw new MissingExecutionContextError(`micro stream ${namespace}${url}`);
|
|
801
|
+
const context = parseExecutionContext(options.context);
|
|
802
|
+
const { signal, retries = 1, timeout, idleTimeout, window } = options;
|
|
795
803
|
let remainingRetries = retries;
|
|
796
804
|
let retrySourceError;
|
|
797
805
|
let hasRetrySourceError = false;
|
|
@@ -807,7 +815,7 @@ export class Application extends Server {
|
|
|
807
815
|
}
|
|
808
816
|
const { client, probe } = selected;
|
|
809
817
|
try {
|
|
810
|
-
const readable = client.stream(url, data, { signal, timeout, idleTimeout, window });
|
|
818
|
+
const readable = client.stream(url, data, { context, signal, timeout, idleTimeout, window });
|
|
811
819
|
return this.trackCircuitStream(namespace, client.host, client.port, probe, readable);
|
|
812
820
|
}
|
|
813
821
|
catch (err) {
|
|
@@ -825,6 +833,9 @@ export class Application extends Server {
|
|
|
825
833
|
}
|
|
826
834
|
/** Opens a stream against one exact service instance without registry selection. */
|
|
827
835
|
async streamPeer(address, url, data, options) {
|
|
836
|
+
if (!options?.context)
|
|
837
|
+
throw new MissingExecutionContextError(`micro peer stream ${address.host}:${address.port}${url}`);
|
|
838
|
+
const context = parseExecutionContext(options.context);
|
|
828
839
|
assertValidRegistrySocket('peer address', address.host, address.port);
|
|
829
840
|
if (options?.timeout !== undefined && (!Number.isSafeInteger(options.timeout) || options.timeout < 1 || options.timeout > 2_147_483_647)) {
|
|
830
841
|
throw new TypeError('Stream timeout must be a positive safe integer not exceeding 2147483647');
|
|
@@ -834,7 +845,7 @@ export class Application extends Server {
|
|
|
834
845
|
const timeout = options?.timeout === undefined
|
|
835
846
|
? undefined
|
|
836
847
|
: Math.max(1, options.timeout - (Date.now() - startedAt));
|
|
837
|
-
return client.stream(url, data, { ...options, timeout });
|
|
848
|
+
return client.stream(url, data, { ...options, context, timeout });
|
|
838
849
|
}
|
|
839
850
|
async publish(topic, data) {
|
|
840
851
|
this.assertCanUsePubSub();
|
|
@@ -894,7 +905,7 @@ export class Application extends Server {
|
|
|
894
905
|
this.ensureRegistryReconnectScheduled();
|
|
895
906
|
throw new Error('Registry is not connected');
|
|
896
907
|
}
|
|
897
|
-
const result = await registry.
|
|
908
|
+
const result = await registry.requestControl('/-/topics', prefix === undefined ? {} : { prefix }, { ...this.registryRequestOptions(), signal: options?.signal });
|
|
898
909
|
return structuredClone(result.topics);
|
|
899
910
|
}
|
|
900
911
|
/** Reads current topic payloads in one Registry round trip. */
|
|
@@ -904,7 +915,7 @@ export class Application extends Server {
|
|
|
904
915
|
this.ensureRegistryReconnectScheduled();
|
|
905
916
|
throw new Error('Registry is not connected');
|
|
906
917
|
}
|
|
907
|
-
const result = await registry.
|
|
918
|
+
const result = await registry.requestControl('/-/topic/snapshots', prefix === undefined ? {} : { prefix }, { ...this.registryRequestOptions(), signal: options?.signal });
|
|
908
919
|
return structuredClone(result.topics);
|
|
909
920
|
}
|
|
910
921
|
/** Reads one retained/current Registry topic payload without subscribing to it. */
|
|
@@ -917,7 +928,7 @@ export class Application extends Server {
|
|
|
917
928
|
this.ensureRegistryReconnectScheduled();
|
|
918
929
|
throw new Error('Registry is not connected');
|
|
919
930
|
}
|
|
920
|
-
const snapshot = await registry.
|
|
931
|
+
const snapshot = await registry.requestControl('/-/topic/get', { topic }, { ...this.registryRequestOptions(), signal: options?.signal });
|
|
921
932
|
return snapshot === undefined ? undefined : structuredClone(snapshot);
|
|
922
933
|
}
|
|
923
934
|
/**
|
|
@@ -969,7 +980,7 @@ export class Application extends Server {
|
|
|
969
980
|
await this.enqueueTopicSync(topic, async (registry) => {
|
|
970
981
|
if (!this.topics.get(topic)?.has(callback))
|
|
971
982
|
return;
|
|
972
|
-
snapshot = await registry.
|
|
983
|
+
snapshot = await registry.requestControl('/-/subscribe', { topic }, this.registryRequestOptions());
|
|
973
984
|
synced = true;
|
|
974
985
|
});
|
|
975
986
|
if (!synced || !snapshot) {
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MessageWs } from "@hile/message-ws";
|
|
2
|
-
import { type
|
|
2
|
+
import { type ExecutionContext } from '@hile/context';
|
|
3
3
|
import { Server } from './server.js';
|
|
4
4
|
import { WebSocket } from 'ws';
|
|
5
5
|
import { EventEmitter } from 'node:events';
|
|
@@ -10,13 +10,15 @@ export interface ClientProps {
|
|
|
10
10
|
ws: WebSocket;
|
|
11
11
|
}
|
|
12
12
|
export interface ClientStreamOptions {
|
|
13
|
+
context: ExecutionContext;
|
|
13
14
|
signal?: AbortSignal;
|
|
14
15
|
timeout?: number;
|
|
15
16
|
idleTimeout?: number;
|
|
16
17
|
window?: number;
|
|
17
18
|
}
|
|
18
19
|
export type MicroMessageMetadata = {
|
|
19
|
-
context?:
|
|
20
|
+
context?: ExecutionContext;
|
|
21
|
+
control?: true;
|
|
20
22
|
[key: string]: unknown;
|
|
21
23
|
};
|
|
22
24
|
export type MicroMessage<T = any> = {
|
|
@@ -37,14 +39,26 @@ export declare class Client extends MessageWs {
|
|
|
37
39
|
constructor(props: ClientProps);
|
|
38
40
|
private startHeartbeat;
|
|
39
41
|
protected exec(data: MicroMessage, signal?: AbortSignal): Promise<any>;
|
|
40
|
-
request<T = any>(url: string, data: any, options
|
|
42
|
+
request<T = any>(url: string, data: any, options: {
|
|
43
|
+
context: ExecutionContext;
|
|
41
44
|
timeout?: number;
|
|
42
45
|
signal?: AbortSignal;
|
|
43
46
|
}): Promise<T>;
|
|
44
|
-
|
|
47
|
+
/** Framework-internal transport path. Business requests must use request() with context. */
|
|
48
|
+
requestControl<T = any>(url: string, data: any, options?: {
|
|
49
|
+
timeout?: number;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}): Promise<T>;
|
|
52
|
+
push(url: string, data: any, options: {
|
|
53
|
+
context: ExecutionContext;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
}): void;
|
|
57
|
+
/** Framework-internal transport path. Business pushes must use push() with context. */
|
|
58
|
+
pushControl(url: string, data: any, options?: {
|
|
45
59
|
timeout?: number;
|
|
46
60
|
signal?: AbortSignal;
|
|
47
61
|
}): void;
|
|
48
|
-
stream(url: string, data: any, options
|
|
62
|
+
stream(url: string, data: any, options: ClientStreamOptions): import("node:stream").Readable;
|
|
49
63
|
dispose(): void;
|
|
50
64
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,51 +1,44 @@
|
|
|
1
1
|
import { MessageWs } from "@hile/message-ws";
|
|
2
|
-
import {
|
|
2
|
+
import { createInvocationContext, MissingExecutionContextError, parseExecutionContext, } from '@hile/context';
|
|
3
3
|
import { WebSocket } from 'ws';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const FRAMEWORK_CONTROL_ROUTES = new Set([
|
|
6
|
+
'/-/config/get',
|
|
7
|
+
'/-/configs',
|
|
8
|
+
'/-/declare',
|
|
9
|
+
'/-/find',
|
|
10
|
+
'/-/namespace/peers',
|
|
11
|
+
'/-/namespaces',
|
|
12
|
+
'/-/registry/status',
|
|
13
|
+
'/-/subscribe',
|
|
14
|
+
'/-/topic/get',
|
|
15
|
+
'/-/topic/snapshots',
|
|
16
|
+
'/-/topic/update',
|
|
17
|
+
'/-/topics',
|
|
18
|
+
'/-/undeclare',
|
|
19
|
+
'/-/unsubscribe',
|
|
20
|
+
]);
|
|
21
|
+
function assertFrameworkControlRoute(url) {
|
|
22
|
+
if (!FRAMEWORK_CONTROL_ROUTES.has(url)) {
|
|
23
|
+
throw new TypeError(`Unknown framework control route: ${url}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function createEnvelope(url, data, context) {
|
|
9
27
|
return {
|
|
10
28
|
url,
|
|
11
29
|
data,
|
|
12
30
|
metadata: {
|
|
13
|
-
context,
|
|
31
|
+
context: parseExecutionContext(context),
|
|
14
32
|
},
|
|
15
33
|
};
|
|
16
34
|
}
|
|
35
|
+
function createControlEnvelope(url, data) {
|
|
36
|
+
assertFrameworkControlRoute(url);
|
|
37
|
+
return { url, data, metadata: { control: true } };
|
|
38
|
+
}
|
|
17
39
|
function getEnvelopeContext(data) {
|
|
18
40
|
const context = data.metadata?.context;
|
|
19
|
-
|
|
20
|
-
return undefined;
|
|
21
|
-
return context;
|
|
22
|
-
}
|
|
23
|
-
function isAsyncIterable(value) {
|
|
24
|
-
return value != null && typeof value[Symbol.asyncIterator] === 'function';
|
|
25
|
-
}
|
|
26
|
-
function bindAsyncIterableToContext(iterable, context) {
|
|
27
|
-
return {
|
|
28
|
-
[Symbol.asyncIterator]() {
|
|
29
|
-
const iterator = iterable[Symbol.asyncIterator]();
|
|
30
|
-
return {
|
|
31
|
-
next() {
|
|
32
|
-
return Promise.resolve(runWithContext(context, () => iterator.next()));
|
|
33
|
-
},
|
|
34
|
-
return(value) {
|
|
35
|
-
if (!iterator.return) {
|
|
36
|
-
return Promise.resolve({ done: true, value });
|
|
37
|
-
}
|
|
38
|
-
return Promise.resolve(runWithContext(context, () => iterator.return(value)));
|
|
39
|
-
},
|
|
40
|
-
throw(error) {
|
|
41
|
-
if (!iterator.throw) {
|
|
42
|
-
return Promise.reject(error);
|
|
43
|
-
}
|
|
44
|
-
return Promise.resolve(runWithContext(context, () => iterator.throw(error)));
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
};
|
|
41
|
+
return context === undefined ? undefined : parseExecutionContext(context);
|
|
49
42
|
}
|
|
50
43
|
export class Client extends MessageWs {
|
|
51
44
|
server;
|
|
@@ -95,35 +88,55 @@ export class Client extends MessageWs {
|
|
|
95
88
|
if (!this._online)
|
|
96
89
|
throw new Error('Client is not online');
|
|
97
90
|
const context = getEnvelopeContext(data);
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return dispatch();
|
|
91
|
+
const isControl = data.metadata?.control === true && FRAMEWORK_CONTROL_ROUTES.has(data.url);
|
|
92
|
+
if (!context && !isControl) {
|
|
93
|
+
throw new MissingExecutionContextError(`inbound micro message ${data.url}`);
|
|
94
|
+
}
|
|
95
|
+
const invocation = context
|
|
96
|
+
? createInvocationContext(context, signal ?? new AbortController().signal, `inbound micro message ${data.url}`)
|
|
97
|
+
: undefined;
|
|
98
|
+
return this.server.dispatch(data.url, data.data, {
|
|
99
|
+
client: this,
|
|
100
|
+
metadata: data.metadata,
|
|
101
|
+
signal,
|
|
102
|
+
invocation,
|
|
103
|
+
});
|
|
112
104
|
}
|
|
113
105
|
request(url, data, options) {
|
|
114
106
|
if (!this._online)
|
|
115
107
|
throw new Error('Client is not online');
|
|
116
|
-
|
|
108
|
+
if (!options?.context)
|
|
109
|
+
throw new MissingExecutionContextError(`micro client request ${url}`);
|
|
110
|
+
const { context, ...transport } = options;
|
|
111
|
+
return this._send(createEnvelope(url, data, context), transport);
|
|
112
|
+
}
|
|
113
|
+
/** Framework-internal transport path. Business requests must use request() with context. */
|
|
114
|
+
requestControl(url, data, options) {
|
|
115
|
+
if (!this._online)
|
|
116
|
+
throw new Error('Client is not online');
|
|
117
|
+
return this._send(createControlEnvelope(url, data), options);
|
|
117
118
|
}
|
|
118
119
|
push(url, data, options) {
|
|
119
120
|
if (!this._online)
|
|
120
121
|
throw new Error('Client is not online');
|
|
121
|
-
|
|
122
|
+
if (!options?.context)
|
|
123
|
+
throw new MissingExecutionContextError(`micro client push ${url}`);
|
|
124
|
+
const { context, ...transport } = options;
|
|
125
|
+
return this._push(createEnvelope(url, data, context), transport);
|
|
126
|
+
}
|
|
127
|
+
/** Framework-internal transport path. Business pushes must use push() with context. */
|
|
128
|
+
pushControl(url, data, options) {
|
|
129
|
+
if (!this._online)
|
|
130
|
+
throw new Error('Client is not online');
|
|
131
|
+
return this._push(createControlEnvelope(url, data), options);
|
|
122
132
|
}
|
|
123
133
|
stream(url, data, options) {
|
|
124
134
|
if (!this._online)
|
|
125
135
|
throw new Error('Client is not online');
|
|
126
|
-
|
|
136
|
+
if (!options?.context)
|
|
137
|
+
throw new MissingExecutionContextError(`micro client stream ${url}`);
|
|
138
|
+
const { context, ...transport } = options;
|
|
139
|
+
return this._stream(createEnvelope(url, data, context), transport);
|
|
127
140
|
}
|
|
128
141
|
dispose() {
|
|
129
142
|
if (this.heartbeatTimer)
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { InvocationContext } from '@hile/context';
|
|
2
|
+
import { type MessageFunction, type MessageRegisterProps } from '@hile/message-loader';
|
|
3
|
+
import type { Client, MicroMessageMetadata } from './client';
|
|
4
|
+
export type MicroMessageHandlerExtras = {
|
|
5
|
+
client: Client;
|
|
6
|
+
metadata?: MicroMessageMetadata;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
invocation: InvocationContext;
|
|
9
|
+
};
|
|
10
|
+
export type MicroMessageFunction<T = any> = MessageFunction<T, MicroMessageHandlerExtras>;
|
|
11
|
+
/** Defines a file-loaded Micro business handler with an explicit invocation context. */
|
|
12
|
+
export declare function defineMicroMessage<T = any>(handler: MicroMessageFunction<T>): MessageRegisterProps<T, MicroMessageHandlerExtras>;
|
package/dist/message.js
ADDED
package/dist/registry.js
CHANGED
|
@@ -490,7 +490,7 @@ export class Registry extends Server {
|
|
|
490
490
|
for (const key of entry.subscribers.values()) {
|
|
491
491
|
try {
|
|
492
492
|
if (this.clients.has(key)) {
|
|
493
|
-
this.clients.get(key).
|
|
493
|
+
this.clients.get(key).pushControl(`/-/topic/update`, { topic, payload });
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/micro",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"vitest": "^4.0.18"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@hile/context": "^4.0.
|
|
28
|
-
"@hile/logger": "^4.0.
|
|
29
|
-
"@hile/message-loader": "^4.0.
|
|
30
|
-
"@hile/message-ws": "^4.0.
|
|
27
|
+
"@hile/context": "^4.0.2",
|
|
28
|
+
"@hile/logger": "^4.0.1",
|
|
29
|
+
"@hile/message-loader": "^4.0.3",
|
|
30
|
+
"@hile/message-ws": "^4.0.3",
|
|
31
31
|
"internal-ip": "^9.0.0",
|
|
32
32
|
"ws": "^8.21.0",
|
|
33
33
|
"yaml": "^2.9.0"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "3ea69973f9373ddb1f7d5d37338966fd7d081d66"
|
|
36
36
|
}
|