@zhin.js/middleware 1.1.3 → 1.1.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/README.md CHANGED
@@ -10,13 +10,24 @@ import { defineMiddleware } from 'zhin.js/middleware';
10
10
 
11
11
  export default defineMiddleware({
12
12
  phase: 'before-dispatch',
13
- async handle(_context, next) { await next(); },
13
+ handle(_context, next) { return next(); },
14
14
  });
15
15
  ```
16
16
 
17
17
  Middleware 不直接持有 Root 或运行时 registry;每次执行使用同一 generation snapshot,
18
18
  更新时由 Feature Slot 原子替换。
19
19
 
20
+ 入站中间件可返回文本、JSX 或其他发送内容。`return next()` 保留下游的返回值与作者;
21
+ `await next()` 后不返回也透传下游结果,返回新内容则替换待自动回复内容。不调用 `next()`
22
+ 会消费输入;不返回内容时不会自动回复。Runtime 在整条入站链回卷后只自动回复一次,
23
+ 显式调用 `input.$reply()` 仍可发送多条消息。已经发送的显式消息不会被返回值替换撤回。
24
+
25
+ 出站中间件使用 `target: 'outbound'`,通过 `input.replace(content)` 修改候选 payload,
26
+ 再调用 `next()` 放行;返回类型为 `void`,不参与入站的自动回复与 continuation 传递。
27
+
28
+ `next()` 的 continuation 只用于透传,不暴露内容,也不可跨调用保存复用。每层只能调用
29
+ 一次 `next()`;遗漏 await 的下游操作仍会在当前 scope 结束前结算,异常向调用方传播。
30
+
20
31
  单文件插件可用 `setup({ addMiddleware })` 注册 `defineMiddleware(...)`;它仍参与
21
32
  统一 phase/order/topology 排序和 generation 回滚。
22
33
 
@@ -0,0 +1,6 @@
1
+ /** Internal marker only; each frame additionally checks its exact carrier identity. */
2
+ export declare const continuationBrand: unique symbol;
3
+ /** Opaque result of `next()`. Return it to preserve the downstream author. */
4
+ export interface MiddlewareContinuation {
5
+ readonly [continuationBrand]: never;
6
+ }
@@ -0,0 +1,2 @@
1
+ /** Internal marker only; each frame additionally checks its exact carrier identity. */
2
+ export const continuationBrand = Symbol('zhin.middleware-continuation');
@@ -3,24 +3,33 @@
3
3
  * @module zhin.js/middleware
4
4
  */
5
5
  import type { AdapterClient, CapabilityContext, RegisteredAdapterName } from '@zhin.js/feature-kit';
6
+ import type { MiddlewareContinuation } from './continuation.js';
7
+ export type { MiddlewareContinuation } from './continuation.js';
6
8
  declare const middlewareBrand: "zhin.middleware/1";
7
9
  export type MiddlewarePhase = 'before-dispatch' | 'after-dispatch';
8
10
  export type MiddlewareTarget = 'inbound' | 'outbound';
9
- export type MiddlewareNext = () => Promise<void>;
11
+ export type MiddlewareNext = () => Promise<MiddlewareContinuation>;
12
+ export type OutboundMiddlewareNext = () => Promise<void>;
10
13
  export interface MiddlewareContext<TInput = unknown, TConfig = unknown, TAdapter extends string | undefined = undefined> extends CapabilityContext<TConfig> {
11
14
  readonly input: TInput;
12
15
  /** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
13
16
  readonly $client: AdapterClient<TAdapter>;
14
17
  }
15
- export interface MiddlewareDefinition<TInput = unknown, TConfig = unknown, TAdapter extends string | undefined = string | undefined> {
18
+ /** Shared metadata for the public inbound and outbound definition contracts. */
19
+ export interface MiddlewareDefinitionBase<TAdapter extends string | undefined = string | undefined> {
16
20
  /** @internal Runtime feature brand. */
17
21
  readonly $feature: typeof middlewareBrand;
18
22
  readonly phase: MiddlewarePhase;
19
- readonly target: MiddlewareTarget;
20
23
  readonly order: number;
21
24
  readonly adapter?: TAdapter;
22
- handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): void | Promise<void>;
23
25
  }
26
+ export type MiddlewareDefinition<TInput = unknown, TConfig = unknown, TAdapter extends string | undefined = string | undefined> = MiddlewareDefinitionBase<TAdapter> & ({
27
+ readonly target: 'inbound';
28
+ handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown;
29
+ } | {
30
+ readonly target: 'outbound';
31
+ handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void>;
32
+ });
24
33
  declare module '@zhin.js/plugin-runtime' {
25
34
  interface PluginSetupContext<TConfig = unknown> {
26
35
  addMiddleware<TInput = unknown>(localName: string, definition: MiddlewareDefinition<TInput, TConfig, string | undefined>): void;
@@ -32,19 +41,30 @@ declare module '@zhin.js/plugin-runtime' {
32
41
  *
33
42
  * @public
34
43
  */
35
- type MiddlewareAuthoringDefinition<TInput, TConfig> = (Omit<MiddlewareDefinition<TInput, TConfig, undefined>, '$feature' | 'phase' | 'target' | 'order'> & {
44
+ type AuthoringForAdapter<TInput, TConfig, TAdapter extends string | undefined> = Omit<MiddlewareDefinitionBase<TAdapter>, '$feature' | 'phase' | 'order'> & {
36
45
  readonly phase?: MiddlewarePhase;
37
- readonly target?: MiddlewareTarget;
38
46
  readonly order?: number;
39
- }) | {
40
- [TAdapter in RegisteredAdapterName]: Omit<MiddlewareDefinition<TInput, TConfig, TAdapter>, '$feature' | 'phase' | 'target' | 'order'> & {
47
+ } & ({
48
+ readonly target?: 'inbound';
49
+ handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown;
50
+ } | {
51
+ readonly target: 'outbound';
52
+ handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void>;
53
+ });
54
+ type MiddlewareAuthoringDefinition<TInput, TConfig> = AuthoringForAdapter<TInput, TConfig, undefined> | {
55
+ [TAdapter in RegisteredAdapterName]: AuthoringForAdapter<TInput, TConfig, TAdapter> & {
41
56
  readonly adapter: TAdapter;
42
- readonly phase?: MiddlewarePhase;
43
- readonly target?: MiddlewareTarget;
44
- readonly order?: number;
45
57
  };
46
58
  }[RegisteredAdapterName];
47
- export declare function defineMiddleware<TInput = unknown, TConfig = unknown>(definition: MiddlewareAuthoringDefinition<TInput, TConfig>): Readonly<MiddlewareDefinition<TInput, TConfig, string | undefined>>;
59
+ export declare function defineMiddleware<TInput = unknown, TConfig = unknown>(definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, {
60
+ readonly target: 'outbound';
61
+ }>): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, {
62
+ readonly target: 'outbound';
63
+ }>>;
64
+ export declare function defineMiddleware<TInput = unknown, TConfig = unknown>(definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, {
65
+ readonly target?: 'inbound';
66
+ }>): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, {
67
+ readonly target: 'inbound';
68
+ }>>;
48
69
  /** @internal Runtime validation for convention-discovered modules. */
49
70
  export declare function parseMiddlewareDefinition(value: unknown): MiddlewareDefinition;
50
- export {};
@@ -1,5 +1,14 @@
1
1
  import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
- import type { MiddlewareDefinition, MiddlewareNext, MiddlewarePhase, MiddlewareTarget } from './definition.js';
2
+ import type { MiddlewareDefinition, MiddlewarePhase, MiddlewareTarget, OutboundMiddlewareNext } from './definition.js';
3
+ /** Transmission-neutral result policy supplied by the composition layer. */
4
+ export interface MiddlewareResultPolicy<TResult> {
5
+ stop(owner: PluginId): TResult;
6
+ replace(owner: PluginId, value: unknown, downstream: TResult | undefined): TResult;
7
+ }
8
+ export interface MiddlewareRunResult {
9
+ readonly owner?: PluginId;
10
+ readonly value?: unknown;
11
+ }
3
12
  export interface MiddlewareDescriptor {
4
13
  readonly owner: PluginId;
5
14
  readonly name: string;
@@ -15,6 +24,10 @@ export declare class MiddlewareIndex {
15
24
  readonly $projection: "zhin.middleware-index/1";
16
25
  constructor(slots: readonly Readonly<CapabilitySlot<MiddlewareDefinition>>[], snapshot: RuntimeSnapshot);
17
26
  list(): readonly MiddlewareDescriptor[];
18
- run<TInput>(input: TInput, terminal?: MiddlewareNext, target?: MiddlewareTarget, operationSnapshot?: RuntimeSnapshot): Promise<void>;
27
+ run<TInput>(input: TInput, terminal?: OutboundMiddlewareNext, target?: 'inbound', operationSnapshot?: RuntimeSnapshot): Promise<MiddlewareRunResult>;
28
+ run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: 'outbound', operationSnapshot?: RuntimeSnapshot): Promise<void>;
29
+ run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: MiddlewareTarget, operationSnapshot?: RuntimeSnapshot): Promise<MiddlewareRunResult | void>;
30
+ /** Unwinds one inbound operation before its caller performs any automatic reply. */
31
+ runInbound<TInput, TResult>(input: TInput, terminal: () => Promise<TResult>, results: MiddlewareResultPolicy<TResult>, operationSnapshot?: RuntimeSnapshot): Promise<TResult>;
19
32
  }
20
33
  export declare function isMiddlewareIndex(value: unknown): value is MiddlewareIndex;
@@ -1,4 +1,5 @@
1
- import { createCapabilityContext, operationClientAdapter, readOperationClient, } from '@zhin.js/feature-kit';
1
+ import { createCapabilityContext, bindOperationInput, operationClientAdapter, readOperationClient, } from '@zhin.js/feature-kit';
2
+ import { continuationBrand } from './continuation.js';
2
3
  export class MiddlewareIndex {
3
4
  snapshot;
4
5
  $projection = 'zhin.middleware-index/1';
@@ -21,6 +22,12 @@ export class MiddlewareIndex {
21
22
  return this.#records.map(({ slot: _slot, ...descriptor }) => descriptor);
22
23
  }
23
24
  async run(input, terminal = async () => undefined, target = 'inbound', operationSnapshot = this.snapshot) {
25
+ if (target === 'inbound') {
26
+ return this.runInbound(input, async () => { await terminal(); return {}; }, {
27
+ stop: (owner) => ({ owner }),
28
+ replace: (owner, value) => ({ owner, value }),
29
+ }, operationSnapshot);
30
+ }
24
31
  const clientAdapter = operationClientAdapter(input);
25
32
  const records = this.#records.filter((record) => record.target === target && (!record.adapter || record.adapter === clientAdapter));
26
33
  let cursor = -1;
@@ -31,19 +38,91 @@ export class MiddlewareIndex {
31
38
  const record = records[index];
32
39
  if (!record)
33
40
  return terminal();
41
+ const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
34
42
  const context = {
35
43
  ...createCapabilityContext(operationSnapshot, record.owner),
36
- input,
44
+ input: authorInput,
37
45
  };
38
46
  Object.defineProperty(context, '$client', {
39
47
  enumerable: true,
40
- get: () => readOperationClient(input, record.adapter),
48
+ get: () => readOperationClient(authorInput, record.adapter),
41
49
  });
42
50
  Object.freeze(context);
43
- await record.slot.definition.handle(context, () => dispatch(index + 1));
51
+ const definition = record.slot.definition;
52
+ if (definition.target !== 'outbound')
53
+ throw new Error('Invalid outbound Middleware');
54
+ const result = await definition.handle(context, () => dispatch(index + 1));
55
+ if (result !== undefined)
56
+ throw new TypeError('Outbound Middleware must use input.replace(), not return content');
44
57
  };
45
58
  await dispatch(0);
46
59
  }
60
+ /** Unwinds one inbound operation before its caller performs any automatic reply. */
61
+ async runInbound(input, terminal, results, operationSnapshot = this.snapshot) {
62
+ const clientAdapter = operationClientAdapter(input);
63
+ const records = this.#records.filter((record) => record.target === 'inbound'
64
+ && (!record.adapter || record.adapter === clientAdapter));
65
+ const dispatch = async (index) => {
66
+ const record = records[index];
67
+ if (!record)
68
+ return terminal();
69
+ const definition = record.slot.definition;
70
+ if (definition.target !== 'inbound')
71
+ throw new Error('Invalid inbound Middleware');
72
+ const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
73
+ const context = {
74
+ ...createCapabilityContext(operationSnapshot, record.owner), input: authorInput,
75
+ };
76
+ Object.defineProperty(context, '$client', {
77
+ enumerable: true, get: () => readOperationClient(authorInput, record.adapter),
78
+ });
79
+ Object.freeze(context);
80
+ let active = true;
81
+ let nextCalled = false;
82
+ let nextTask;
83
+ let carrier;
84
+ const next = () => {
85
+ if (!active)
86
+ throw new Error('Middleware next() scope has ended');
87
+ if (nextCalled)
88
+ throw new Error('Middleware next() called more than once');
89
+ nextCalled = true;
90
+ nextTask = dispatch(index + 1);
91
+ // Attach a rejection observer immediately even when an author forgets await.
92
+ void nextTask.catch(() => undefined);
93
+ const continuation = nextTask.then(() => {
94
+ carrier = Object.freeze({ [continuationBrand]: undefined });
95
+ return carrier;
96
+ });
97
+ void continuation.catch(() => undefined);
98
+ return continuation;
99
+ };
100
+ let returned;
101
+ try {
102
+ returned = await definition.handle(context, next);
103
+ }
104
+ catch (error) {
105
+ // Drain an admitted child before the owning generation lease can close.
106
+ await nextTask?.catch(() => undefined);
107
+ throw error;
108
+ }
109
+ finally {
110
+ active = false;
111
+ }
112
+ const downstream = nextTask ? await nextTask : undefined;
113
+ if (returned === undefined)
114
+ return nextTask ?? results.stop(record.owner);
115
+ if (returned && typeof returned === 'object' && continuationBrand in returned) {
116
+ if (returned !== carrier)
117
+ throw new Error('Middleware continuation belongs to another frame or operation');
118
+ if (!nextTask)
119
+ throw new Error('Middleware continuation has no downstream operation');
120
+ return nextTask;
121
+ }
122
+ return results.replace(record.owner, returned, downstream);
123
+ };
124
+ return dispatch(0);
125
+ }
47
126
  }
48
127
  export function isMiddlewareIndex(value) {
49
128
  return !!value && typeof value === 'object'
package/lib/provider.js CHANGED
@@ -10,6 +10,7 @@ const middlewareFeature = defineFeatureProvider({
10
10
  setupMethod: 'addMiddleware',
11
11
  conventions: [directoryModules({
12
12
  id: 'middlewares-index',
13
+ extensions: ['ts', 'tsx', 'js', 'mjs', 'cjs'],
13
14
  layouts: [{ segments: ['middlewares', capture('name')], localName: (values) => captured(values, 'name') }],
14
15
  })],
15
16
  validate: parseMiddlewareDefinition,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/middleware",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Convention-based Middleware Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -17,8 +17,8 @@
17
17
  "src"
18
18
  ],
19
19
  "dependencies": {
20
- "@zhin.js/feature-kit": "1.1.2",
21
- "@zhin.js/plugin-runtime": "1.1.11"
20
+ "@zhin.js/feature-kit": "1.1.3",
21
+ "@zhin.js/plugin-runtime": "1.1.12"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.1.2",
@@ -0,0 +1,7 @@
1
+ /** Internal marker only; each frame additionally checks its exact carrier identity. */
2
+ export const continuationBrand: unique symbol = Symbol('zhin.middleware-continuation');
3
+
4
+ /** Opaque result of `next()`. Return it to preserve the downstream author. */
5
+ export interface MiddlewareContinuation {
6
+ readonly [continuationBrand]: never;
7
+ }
package/src/definition.ts CHANGED
@@ -7,12 +7,15 @@ import type {
7
7
  CapabilityContext,
8
8
  RegisteredAdapterName,
9
9
  } from '@zhin.js/feature-kit';
10
+ import type { MiddlewareContinuation } from './continuation.js';
11
+ export type { MiddlewareContinuation } from './continuation.js';
10
12
 
11
13
  const middlewareBrand = 'zhin.middleware/1' as const;
12
14
 
13
15
  export type MiddlewarePhase = 'before-dispatch' | 'after-dispatch';
14
16
  export type MiddlewareTarget = 'inbound' | 'outbound';
15
- export type MiddlewareNext = () => Promise<void>;
17
+ export type MiddlewareNext = () => Promise<MiddlewareContinuation>;
18
+ export type OutboundMiddlewareNext = () => Promise<void>;
16
19
 
17
20
  export interface MiddlewareContext<
18
21
  TInput = unknown,
@@ -25,23 +28,26 @@ export interface MiddlewareContext<
25
28
  readonly $client: AdapterClient<TAdapter>;
26
29
  }
27
30
 
28
- export interface MiddlewareDefinition<
29
- TInput = unknown,
30
- TConfig = unknown,
31
+ /** Shared metadata for the public inbound and outbound definition contracts. */
32
+ export interface MiddlewareDefinitionBase<
31
33
  TAdapter extends string | undefined = string | undefined,
32
34
  > {
33
35
  /** @internal Runtime feature brand. */
34
36
  readonly $feature: typeof middlewareBrand;
35
37
  readonly phase: MiddlewarePhase;
36
- readonly target: MiddlewareTarget;
37
38
  readonly order: number;
38
39
  readonly adapter?: TAdapter;
39
- handle(
40
- context: MiddlewareContext<TInput, TConfig, TAdapter>,
41
- next: MiddlewareNext,
42
- ): void | Promise<void>;
43
40
  }
44
41
 
42
+ export type MiddlewareDefinition<
43
+ TInput = unknown,
44
+ TConfig = unknown,
45
+ TAdapter extends string | undefined = string | undefined,
46
+ > = MiddlewareDefinitionBase<TAdapter> & (
47
+ | { readonly target: 'inbound'; handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown }
48
+ | { readonly target: 'outbound'; handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void> }
49
+ );
50
+
45
51
  declare module '@zhin.js/plugin-runtime' {
46
52
  interface PluginSetupContext<TConfig = unknown> {
47
53
  addMiddleware<TInput = unknown>(
@@ -57,27 +63,29 @@ declare module '@zhin.js/plugin-runtime' {
57
63
  *
58
64
  * @public
59
65
  */
66
+ type AuthoringForAdapter<TInput, TConfig, TAdapter extends string | undefined> =
67
+ Omit<MiddlewareDefinitionBase<TAdapter>, '$feature' | 'phase' | 'order'> & {
68
+ readonly phase?: MiddlewarePhase;
69
+ readonly order?: number;
70
+ } & (
71
+ | { readonly target?: 'inbound'; handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown }
72
+ | { readonly target: 'outbound'; handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void> }
73
+ );
74
+
60
75
  type MiddlewareAuthoringDefinition<TInput, TConfig> =
61
- | (Omit<
62
- MiddlewareDefinition<TInput, TConfig, undefined>,
63
- '$feature' | 'phase' | 'target' | 'order'
64
- > & {
65
- readonly phase?: MiddlewarePhase;
66
- readonly target?: MiddlewareTarget;
67
- readonly order?: number;
68
- })
76
+ | AuthoringForAdapter<TInput, TConfig, undefined>
69
77
  | {
70
- [TAdapter in RegisteredAdapterName]: Omit<
71
- MiddlewareDefinition<TInput, TConfig, TAdapter>,
72
- '$feature' | 'phase' | 'target' | 'order'
73
- > & {
78
+ [TAdapter in RegisteredAdapterName]: AuthoringForAdapter<TInput, TConfig, TAdapter> & {
74
79
  readonly adapter: TAdapter;
75
- readonly phase?: MiddlewarePhase;
76
- readonly target?: MiddlewareTarget;
77
- readonly order?: number;
78
80
  }
79
81
  }[RegisteredAdapterName];
80
82
 
83
+ export function defineMiddleware<TInput = unknown, TConfig = unknown>(
84
+ definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target: 'outbound' }>,
85
+ ): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'outbound' }>>;
86
+ export function defineMiddleware<TInput = unknown, TConfig = unknown>(
87
+ definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target?: 'inbound' }>,
88
+ ): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'inbound' }>>;
81
89
  export function defineMiddleware<TInput = unknown, TConfig = unknown>(
82
90
  definition: MiddlewareAuthoringDefinition<TInput, TConfig>,
83
91
  ): Readonly<MiddlewareDefinition<TInput, TConfig, string | undefined>> {
@@ -1,16 +1,29 @@
1
1
  import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
2
2
  import {
3
3
  createCapabilityContext,
4
+ bindOperationInput,
4
5
  operationClientAdapter,
5
6
  readOperationClient,
6
7
  } from '@zhin.js/feature-kit';
7
8
  import type {
8
9
  MiddlewareContext,
9
10
  MiddlewareDefinition,
10
- MiddlewareNext,
11
11
  MiddlewarePhase,
12
12
  MiddlewareTarget,
13
+ OutboundMiddlewareNext,
13
14
  } from './definition.js';
15
+ import { continuationBrand, type MiddlewareContinuation } from './continuation.js';
16
+
17
+ /** Transmission-neutral result policy supplied by the composition layer. */
18
+ export interface MiddlewareResultPolicy<TResult> {
19
+ stop(owner: PluginId): TResult;
20
+ replace(owner: PluginId, value: unknown, downstream: TResult | undefined): TResult;
21
+ }
22
+
23
+ export interface MiddlewareRunResult {
24
+ readonly owner?: PluginId;
25
+ readonly value?: unknown;
26
+ }
14
27
 
15
28
  export interface MiddlewareDescriptor {
16
29
  readonly owner: PluginId;
@@ -51,12 +64,21 @@ export class MiddlewareIndex {
51
64
  return this.#records.map(({ slot: _slot, ...descriptor }) => descriptor);
52
65
  }
53
66
 
67
+ run<TInput>(input: TInput, terminal?: OutboundMiddlewareNext, target?: 'inbound', operationSnapshot?: RuntimeSnapshot): Promise<MiddlewareRunResult>;
68
+ run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: 'outbound', operationSnapshot?: RuntimeSnapshot): Promise<void>;
69
+ run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: MiddlewareTarget, operationSnapshot?: RuntimeSnapshot): Promise<MiddlewareRunResult | void>;
54
70
  async run<TInput>(
55
71
  input: TInput,
56
- terminal: MiddlewareNext = async () => undefined,
72
+ terminal: OutboundMiddlewareNext = async () => undefined,
57
73
  target: MiddlewareTarget = 'inbound',
58
74
  operationSnapshot: RuntimeSnapshot = this.snapshot,
59
- ): Promise<void> {
75
+ ): Promise<MiddlewareRunResult | void> {
76
+ if (target === 'inbound') {
77
+ return this.runInbound(input, async () => { await terminal(); return {}; }, {
78
+ stop: (owner) => ({ owner }),
79
+ replace: (owner, value) => ({ owner, value }),
80
+ }, operationSnapshot);
81
+ }
60
82
  const clientAdapter = operationClientAdapter(input);
61
83
  const records = this.#records.filter((record) =>
62
84
  record.target === target && (!record.adapter || record.adapter === clientAdapter));
@@ -66,19 +88,86 @@ export class MiddlewareIndex {
66
88
  cursor = index;
67
89
  const record = records[index];
68
90
  if (!record) return terminal();
91
+ const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
69
92
  const context = {
70
93
  ...createCapabilityContext(operationSnapshot, record.owner),
71
- input,
94
+ input: authorInput,
72
95
  } as MiddlewareContext<TInput>;
73
96
  Object.defineProperty(context, '$client', {
74
97
  enumerable: true,
75
- get: () => readOperationClient(input, record.adapter),
98
+ get: () => readOperationClient(authorInput, record.adapter),
76
99
  });
77
100
  Object.freeze(context);
78
- await record.slot.definition.handle(context, () => dispatch(index + 1));
101
+ const definition = record.slot.definition;
102
+ if (definition.target !== 'outbound') throw new Error('Invalid outbound Middleware');
103
+ const result = await definition.handle(context, () => dispatch(index + 1));
104
+ if (result !== undefined) throw new TypeError('Outbound Middleware must use input.replace(), not return content');
79
105
  };
80
106
  await dispatch(0);
81
107
  }
108
+
109
+ /** Unwinds one inbound operation before its caller performs any automatic reply. */
110
+ async runInbound<TInput, TResult>(
111
+ input: TInput,
112
+ terminal: () => Promise<TResult>,
113
+ results: MiddlewareResultPolicy<TResult>,
114
+ operationSnapshot: RuntimeSnapshot = this.snapshot,
115
+ ): Promise<TResult> {
116
+ const clientAdapter = operationClientAdapter(input);
117
+ const records = this.#records.filter((record) => record.target === 'inbound'
118
+ && (!record.adapter || record.adapter === clientAdapter));
119
+ const dispatch = async (index: number): Promise<TResult> => {
120
+ const record = records[index];
121
+ if (!record) return terminal();
122
+ const definition = record.slot.definition;
123
+ if (definition.target !== 'inbound') throw new Error('Invalid inbound Middleware');
124
+ const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
125
+ const context = {
126
+ ...createCapabilityContext(operationSnapshot, record.owner), input: authorInput,
127
+ } as MiddlewareContext<TInput>;
128
+ Object.defineProperty(context, '$client', {
129
+ enumerable: true, get: () => readOperationClient(authorInput, record.adapter),
130
+ });
131
+ Object.freeze(context);
132
+ let active = true;
133
+ let nextCalled = false;
134
+ let nextTask: Promise<TResult> | undefined;
135
+ let carrier: MiddlewareContinuation | undefined;
136
+ const next = (): Promise<MiddlewareContinuation> => {
137
+ if (!active) throw new Error('Middleware next() scope has ended');
138
+ if (nextCalled) throw new Error('Middleware next() called more than once');
139
+ nextCalled = true;
140
+ nextTask = dispatch(index + 1);
141
+ // Attach a rejection observer immediately even when an author forgets await.
142
+ void nextTask.catch(() => undefined);
143
+ const continuation = nextTask.then(() => {
144
+ carrier = Object.freeze({ [continuationBrand]: undefined as never });
145
+ return carrier;
146
+ });
147
+ void continuation.catch(() => undefined);
148
+ return continuation;
149
+ };
150
+ let returned: unknown;
151
+ try {
152
+ returned = await definition.handle(context, next);
153
+ } catch (error) {
154
+ // Drain an admitted child before the owning generation lease can close.
155
+ await nextTask?.catch(() => undefined);
156
+ throw error;
157
+ } finally {
158
+ active = false;
159
+ }
160
+ const downstream = nextTask ? await nextTask : undefined;
161
+ if (returned === undefined) return nextTask ?? results.stop(record.owner);
162
+ if (returned && typeof returned === 'object' && continuationBrand in returned) {
163
+ if (returned !== carrier) throw new Error('Middleware continuation belongs to another frame or operation');
164
+ if (!nextTask) throw new Error('Middleware continuation has no downstream operation');
165
+ return nextTask;
166
+ }
167
+ return results.replace(record.owner, returned, downstream);
168
+ };
169
+ return dispatch(0);
170
+ }
82
171
  }
83
172
 
84
173
  export function isMiddlewareIndex(value: unknown): value is MiddlewareIndex {
package/src/provider.ts CHANGED
@@ -12,6 +12,7 @@ const middlewareFeature = defineFeatureProvider({
12
12
  setupMethod: 'addMiddleware',
13
13
  conventions: [directoryModules({
14
14
  id: 'middlewares-index',
15
+ extensions: ['ts', 'tsx', 'js', 'mjs', 'cjs'],
15
16
  layouts: [{ segments: ['middlewares', capture('name')], localName: (values) => captured(values, 'name') }],
16
17
  })],
17
18
  validate: parseMiddlewareDefinition,