@hile/micro 4.0.2 → 4.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.
- package/AI.md +34 -25
- package/README.md +11 -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,13 +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.
|
|
136
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.
|
|
137
142
|
|
|
138
143
|
## Install
|
|
139
144
|
|
|
140
145
|
```bash
|
|
141
|
-
pnpm add @hile/micro @hile/message-loader @hile/message-ws
|
|
146
|
+
pnpm add @hile/context @hile/micro @hile/message-loader @hile/message-ws
|
|
142
147
|
```
|
|
143
148
|
|
|
144
149
|
Use transport-specific packages only when you need to build custom IPC or worker-thread bridges.
|
|
@@ -147,7 +152,8 @@ Use transport-specific packages only when you need to build custom IPC or worker
|
|
|
147
152
|
|
|
148
153
|
```ts
|
|
149
154
|
import { defineMessage, MessageLoader } from '@hile/message-loader'
|
|
150
|
-
import {
|
|
155
|
+
import { createExecutionContext } from '@hile/context'
|
|
156
|
+
import { Application, defineMicroMessage, Registry, Server } from '@hile/micro'
|
|
151
157
|
import { MessageWs } from '@hile/message-ws'
|
|
152
158
|
import { MessageIpc } from '@hile/message-ipc'
|
|
153
159
|
import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
@@ -155,7 +161,7 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
155
161
|
|
|
156
162
|
## Compose With
|
|
157
163
|
|
|
158
|
-
-
|
|
164
|
+
- Pass `ExecutionContext` explicitly in every business call or stream option; the receiver gets it in `invocation.context`.
|
|
159
165
|
- `@hile/redis-idempotency` protects retryable side effects in message handlers.
|
|
160
166
|
- `@hile/redis-stream-queue` is better for durable background jobs.
|
|
161
167
|
|
|
@@ -170,8 +176,8 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
170
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.
|
|
171
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.
|
|
172
178
|
- A stream request requires `exec()` to return an async iterable.
|
|
173
|
-
- `Application.call(namespace, url, data, options
|
|
174
|
-
- `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.
|
|
175
181
|
- `Application.publish(topic, payload)` returns an object with `update()` and `unpublish()`.
|
|
176
182
|
- `Application.subscribe(topic, callback)` returns an unsubscribe function.
|
|
177
183
|
- `Registry` stores service addresses and retained config/topic state under `~/.registry`.
|
|
@@ -185,8 +191,8 @@ import { MessageWorkerThread } from '@hile/message-worker-thread'
|
|
|
185
191
|
|
|
186
192
|
## Verification Checklist
|
|
187
193
|
|
|
188
|
-
-
|
|
189
|
-
- 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 })`.
|
|
190
196
|
- Streaming handlers are async generators.
|
|
191
197
|
- Custom modem timeout values use the documented safe-integer range.
|
|
192
198
|
- Registry is started before application nodes need discovery.
|
|
@@ -220,10 +226,10 @@ Provider handler:
|
|
|
220
226
|
|
|
221
227
|
```ts
|
|
222
228
|
// src/messages/charge.msg.ts
|
|
223
|
-
import {
|
|
229
|
+
import { defineMicroMessage } from '@hile/micro'
|
|
224
230
|
|
|
225
|
-
export default
|
|
226
|
-
return { charged: true, input: data }
|
|
231
|
+
export default defineMicroMessage(async ({ data, invocation }) => {
|
|
232
|
+
return { charged: true, input: data, requestId: invocation.context.values.requestId }
|
|
227
233
|
})
|
|
228
234
|
```
|
|
229
235
|
|
|
@@ -251,10 +257,14 @@ export default defineService('billing.micro', async (shutdown) => {
|
|
|
251
257
|
Consumer:
|
|
252
258
|
|
|
253
259
|
```ts
|
|
260
|
+
import { randomUUID } from 'node:crypto'
|
|
261
|
+
import { createExecutionContext } from '@hile/context'
|
|
262
|
+
|
|
263
|
+
const context = createExecutionContext({ requestId: randomUUID(), tenantId: 't1' })
|
|
254
264
|
const result = await app.call('billing', '/charge', {
|
|
255
265
|
tenantId: 't1',
|
|
256
266
|
amount: 100,
|
|
257
|
-
})
|
|
267
|
+
}, { context })
|
|
258
268
|
```
|
|
259
269
|
|
|
260
270
|
## File Layout
|
|
@@ -274,16 +284,15 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
274
284
|
## Packages To Use
|
|
275
285
|
|
|
276
286
|
- `@hile/micro`
|
|
277
|
-
- `@hile/
|
|
278
|
-
- `@hile/context` when context must cross service boundaries
|
|
287
|
+
- `@hile/context` for the required explicit execution context carrier
|
|
279
288
|
- `@hile/redis-idempotency` for retryable side effects
|
|
280
289
|
|
|
281
290
|
## Implementation Steps
|
|
282
291
|
|
|
283
292
|
1. Start a Registry with `hile registry`.
|
|
284
293
|
2. Start providers with stable namespaces.
|
|
285
|
-
3.
|
|
286
|
-
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 })`.
|
|
287
296
|
5. Use `app.stream()` only for async-generator handlers.
|
|
288
297
|
|
|
289
298
|
## Failure And Cleanup Behavior
|
|
@@ -296,8 +305,8 @@ Use this recipe when services communicate over Hile registry-backed RPC.
|
|
|
296
305
|
|
|
297
306
|
- Registry is reachable.
|
|
298
307
|
- Provider namespace matches consumer call.
|
|
299
|
-
- Handlers default-export `
|
|
300
|
-
- 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.
|
|
301
310
|
|
|
302
311
|
# Runtime Dynamic Config
|
|
303
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,18 @@ 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.
|
|
72
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.
|
|
73
78
|
|
|
74
79
|
- Appending a secondary response getter to `client.request('/x', data)`
|
|
@@ -78,8 +83,8 @@ const result = await app.call('example.service', '/ping', { hello: 'world' })
|
|
|
78
83
|
|
|
79
84
|
## Verify
|
|
80
85
|
|
|
81
|
-
-
|
|
82
|
-
- 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 })`.
|
|
83
88
|
- Streaming handlers are async generators.
|
|
84
89
|
- Custom modem timeout values use the documented safe-integer range.
|
|
85
90
|
- Registry is started before application nodes need discovery.
|
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.4",
|
|
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.4",
|
|
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": "c89cb395e014c1973dcc0053a533cfea039a91fe"
|
|
36
36
|
}
|