@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 CHANGED
@@ -35,13 +35,14 @@ Message handler file:
35
35
 
36
36
  ```ts
37
37
  // src/messages/ping.msg.ts
38
- import { defineMessage } from '@hile/message-loader'
38
+ import { defineMicroMessage } from '@hile/micro'
39
39
 
40
- export default defineMessage(async ({ data, params }) => {
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
- const result = await app.call('example.service', '/ping', { hello: 'world' })
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 { defineMessage } from '@hile/message-loader'
91
+ import { defineMicroMessage } from '@hile/micro'
87
92
 
88
- export default defineMessage(async function* () {
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
- - Do not bypass `defineMessage()` for file-loaded handlers.
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 { Application, Registry, Server } from '@hile/micro'
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
- - `@hile/context` propagates context in micro message metadata.
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?)` returns a promise.
169
- - `Application.stream(namespace, url, data, options?)` returns a readable stream.
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
- - Message files default-export `defineMessage(...)`.
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 { defineMessage } from '@hile/message-loader'
229
+ import { defineMicroMessage } from '@hile/micro'
218
230
 
219
- export default defineMessage(async ({ data }) => {
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/message-loader`
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. Load `*.msg.ts` handlers through `app.load()`.
280
- 4. Call providers with `await app.call(namespace, url, data)`.
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 `defineMessage()`.
294
- - Consumer code awaits `app.call(...)` directly.
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 { defineMessage } from '@hile/message-loader'
25
+ import { defineMicroMessage } from '@hile/micro'
26
26
 
27
- export default defineMessage(async ({ data, params }) => {
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
- const result = await app.call('example.service', '/ping', { hello: 'world' })
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
- - Do not bypass `defineMessage()` for file-loaded handlers.
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
- - Message files default-export `defineMessage(...)`.
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
 
@@ -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
- timeout?: number;
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?: ClientStreamOptions): Promise<import('stream').Readable>;
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>;
@@ -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.request('/-/declare', request, this.registryRequestOptions());
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.request('/-/undeclare', { topic }, this.registryRequestOptions());
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.request('/-/unsubscribe', { topic }, this.registryRequestOptions());
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.request('/-/subscribe', { topic }, this.registryRequestOptions());
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.request('/-/find', { namespace, exclude });
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
- const { timeout = this._requestTimeoutMs, retries = 1, signal } = options || {};
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
- const { signal, retries = 1, timeout, idleTimeout, window } = options || {};
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.request('/-/topics', prefix === undefined ? {} : { prefix }, { ...this.registryRequestOptions(), signal: options?.signal });
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.request('/-/topic/snapshots', prefix === undefined ? {} : { prefix }, { ...this.registryRequestOptions(), signal: options?.signal });
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.request('/-/topic/get', { topic }, { ...this.registryRequestOptions(), signal: options?.signal });
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.request('/-/subscribe', { topic }, this.registryRequestOptions());
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 ContextData, type ContextInput } from '@hile/context';
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?: ContextInput<ContextData>;
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
- push(url: string, data: any, options?: {
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?: ClientStreamOptions): import("node:stream").Readable;
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 { isContextData, runWithContext, snapshotContext, } from '@hile/context';
2
+ import { createInvocationContext, MissingExecutionContextError, parseExecutionContext, } from '@hile/context';
3
3
  import { WebSocket } from 'ws';
4
4
  import { EventEmitter } from 'node:events';
5
- function createEnvelope(url, data) {
6
- const context = snapshotContext();
7
- if (Object.keys(context).length === 0)
8
- return { url, data };
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
- if (!isContextData(context))
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 dispatch = async () => {
99
- const result = await this.server.dispatch(data.url, data.data, {
100
- client: this,
101
- metadata: data.metadata,
102
- signal,
103
- });
104
- if (context && isAsyncIterable(result)) {
105
- return bindAsyncIterableToContext(result, context);
106
- }
107
- return result;
108
- };
109
- if (context)
110
- return runWithContext(context, dispatch);
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
- return this._send(createEnvelope(url, data), options);
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
- return this._push(createEnvelope(url, data), options);
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
- return this._stream(createEnvelope(url, data), options);
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
@@ -2,3 +2,4 @@ export * from './server.js';
2
2
  export * from './client.js';
3
3
  export * from './application.js';
4
4
  export * from './registry.js';
5
+ export * from './message.js';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from './server.js';
2
2
  export * from './client.js';
3
3
  export * from './application.js';
4
4
  export * from './registry.js';
5
+ export * from './message.js';
@@ -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>;
@@ -0,0 +1,5 @@
1
+ import { defineMessage, } from '@hile/message-loader';
2
+ /** Defines a file-loaded Micro business handler with an explicit invocation context. */
3
+ export function defineMicroMessage(handler) {
4
+ return defineMessage(handler);
5
+ }
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).push(`/-/topic/update`, { topic, payload });
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.1",
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.1",
28
- "@hile/logger": "^4.0.0",
29
- "@hile/message-loader": "^4.0.1",
30
- "@hile/message-ws": "^4.0.1",
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": "fe98c6f8cca2860ccbb213c575cfda44588cbd06"
35
+ "gitHead": "3ea69973f9373ddb1f7d5d37338966fd7d081d66"
36
36
  }