@zhin.js/middleware 1.1.13 → 1.1.15
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/lib/definition.d.ts +32 -2
- package/lib/definition.js +7 -1
- package/lib/middleware-index.d.ts +7 -2
- package/lib/middleware-index.js +32 -4
- package/package.json +3 -3
- package/src/definition.ts +36 -3
- package/src/middleware-index.ts +39 -4
package/lib/definition.d.ts
CHANGED
|
@@ -10,11 +10,19 @@ export type MiddlewarePhase = 'before-dispatch' | 'after-dispatch';
|
|
|
10
10
|
export type MiddlewareTarget = 'inbound' | 'outbound';
|
|
11
11
|
export type MiddlewareNext = () => Promise<MiddlewareContinuation>;
|
|
12
12
|
export type OutboundMiddlewareNext = () => Promise<void>;
|
|
13
|
+
export interface InboundDispatchSummary {
|
|
14
|
+
readonly matched: boolean;
|
|
15
|
+
readonly path: 'button' | 'command' | 'handler' | 'none';
|
|
16
|
+
readonly handlerId?: string;
|
|
17
|
+
readonly replied: boolean;
|
|
18
|
+
}
|
|
13
19
|
export interface MiddlewareContext<TInput = unknown, TConfig = unknown, TAdapter extends string | undefined = undefined> extends CapabilityContext<TConfig> {
|
|
14
20
|
readonly input: TInput;
|
|
15
21
|
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
16
22
|
readonly $client: AdapterClient<TAdapter>;
|
|
17
23
|
readonly session: OperationSession<TAdapter> | undefined;
|
|
24
|
+
readonly dispatch?: InboundDispatchSummary;
|
|
25
|
+
onAfterDispatch?(listener: (summary: InboundDispatchSummary) => void | Promise<void>): void;
|
|
18
26
|
}
|
|
19
27
|
/** Shared metadata for the public inbound and outbound definition contracts. */
|
|
20
28
|
export interface MiddlewareDefinitionBase<TAdapter extends string | undefined = string | undefined> {
|
|
@@ -46,16 +54,17 @@ declare module '@zhin.js/plugin-runtime' {
|
|
|
46
54
|
*/
|
|
47
55
|
type AuthoringForAdapter<TInput, TConfig, TAdapter extends string | undefined> = Omit<MiddlewareDefinitionBase<TAdapter>, '$feature' | 'phase' | 'order'> & {
|
|
48
56
|
readonly phase?: MiddlewarePhase;
|
|
57
|
+
readonly stage?: MiddlewarePhase;
|
|
49
58
|
readonly order?: number;
|
|
50
59
|
} & ({
|
|
51
60
|
readonly target?: 'inbound';
|
|
52
|
-
handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown;
|
|
61
|
+
handle(this: MiddlewareContext<TInput, TConfig, TAdapter>, context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown;
|
|
53
62
|
} | {
|
|
54
63
|
readonly target: 'outbound';
|
|
55
64
|
/** Opt in to cumulative prefixes. Previously published text, format and addressing must stay stable;
|
|
56
65
|
* do not repeat approvals, external sends or other side effects. */
|
|
57
66
|
readonly incremental?: true;
|
|
58
|
-
handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void>;
|
|
67
|
+
handle(this: MiddlewareContext<TInput, TConfig, TAdapter>, context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void>;
|
|
59
68
|
});
|
|
60
69
|
/** @internal Shape shared with the typed IM facade; not a separate authoring API. */
|
|
61
70
|
export type MiddlewareAuthoringDefinition<TInput, TConfig> = AuthoringForAdapter<TInput, TConfig, undefined> | {
|
|
@@ -63,6 +72,24 @@ export type MiddlewareAuthoringDefinition<TInput, TConfig> = AuthoringForAdapter
|
|
|
63
72
|
readonly adapter: TAdapter;
|
|
64
73
|
};
|
|
65
74
|
}[RegisteredAdapterName];
|
|
75
|
+
/**
|
|
76
|
+
* Returned by `defineMiddleware.withConfig<Config>()`: fixes the plugin config type while
|
|
77
|
+
* keeping inbound/outbound input inference intact.
|
|
78
|
+
*
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
export interface MiddlewareDefiner<TConfig> {
|
|
82
|
+
<TInput = unknown>(definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, {
|
|
83
|
+
readonly target: 'outbound';
|
|
84
|
+
}>): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, {
|
|
85
|
+
readonly target: 'outbound';
|
|
86
|
+
}>>;
|
|
87
|
+
<TInput = unknown>(definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, {
|
|
88
|
+
readonly target?: 'inbound';
|
|
89
|
+
}>): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, {
|
|
90
|
+
readonly target: 'inbound';
|
|
91
|
+
}>>;
|
|
92
|
+
}
|
|
66
93
|
export declare function defineMiddleware<TInput = unknown, TConfig = unknown>(definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, {
|
|
67
94
|
readonly target: 'outbound';
|
|
68
95
|
}>): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, {
|
|
@@ -73,5 +100,8 @@ export declare function defineMiddleware<TInput = unknown, TConfig = unknown>(de
|
|
|
73
100
|
}>): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, {
|
|
74
101
|
readonly target: 'inbound';
|
|
75
102
|
}>>;
|
|
103
|
+
export declare namespace defineMiddleware {
|
|
104
|
+
var withConfig: <TConfig = unknown>() => MiddlewareDefiner<TConfig>;
|
|
105
|
+
}
|
|
76
106
|
/** @internal Runtime validation for convention-discovered modules. */
|
|
77
107
|
export declare function parseMiddlewareDefinition(value: unknown): MiddlewareDefinition;
|
package/lib/definition.js
CHANGED
|
@@ -8,7 +8,7 @@ export function defineMiddleware(definition) {
|
|
|
8
8
|
&& (typeof adapter !== 'string' || adapter.trim() === '')) {
|
|
9
9
|
throw new TypeError('Middleware adapter must be a non-empty string');
|
|
10
10
|
}
|
|
11
|
-
const phase = definition.phase ?? 'before-dispatch';
|
|
11
|
+
const phase = definition.phase ?? definition.stage ?? 'before-dispatch';
|
|
12
12
|
if (phase !== 'before-dispatch' && phase !== 'after-dispatch') {
|
|
13
13
|
throw new TypeError(`Invalid Middleware phase: ${String(phase)}`);
|
|
14
14
|
}
|
|
@@ -21,6 +21,12 @@ export function defineMiddleware(definition) {
|
|
|
21
21
|
throw new TypeError('Middleware order must be a safe integer');
|
|
22
22
|
return Object.freeze({ ...definition, $feature: middlewareBrand, phase, target, order });
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Fix the plugin config type first, then define the middleware.
|
|
26
|
+
*/
|
|
27
|
+
defineMiddleware.withConfig = function withConfig() {
|
|
28
|
+
return ((definition) => defineMiddleware(definition));
|
|
29
|
+
};
|
|
24
30
|
/** @internal Runtime validation for convention-discovered modules. */
|
|
25
31
|
export function parseMiddlewareDefinition(value) {
|
|
26
32
|
if (!value || typeof value !== 'object')
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { CapabilitySlot, PluginId, RuntimeSnapshot } from '@zhin.js/plugin-runtime';
|
|
2
|
-
import type { MiddlewareDefinition, MiddlewarePhase, MiddlewareTarget, OutboundMiddlewareNext } from './definition.js';
|
|
2
|
+
import type { InboundDispatchSummary, MiddlewareDefinition, MiddlewarePhase, MiddlewareTarget, OutboundMiddlewareNext } from './definition.js';
|
|
3
|
+
/** @internal Terminal control passed to inbound terminal callbacks to publish dispatch summary before after-dispatch unwinds. */
|
|
4
|
+
export interface InboundTerminalControl {
|
|
5
|
+
reportDispatch(summary: InboundDispatchSummary): Promise<void>;
|
|
6
|
+
}
|
|
3
7
|
/** @internal Transmission-neutral result policy supplied by the composition layer. */
|
|
4
8
|
export interface MiddlewareResultPolicy<TResult> {
|
|
5
9
|
stop(owner: PluginId): TResult;
|
|
6
10
|
replace(owner: PluginId, value: unknown, downstream: TResult | undefined): TResult;
|
|
11
|
+
summarize?(result: TResult): InboundDispatchSummary | undefined;
|
|
7
12
|
}
|
|
8
13
|
/** @internal Runtime continuation result. */
|
|
9
14
|
export interface MiddlewareRunResult {
|
|
@@ -32,7 +37,7 @@ export declare class MiddlewareIndex {
|
|
|
32
37
|
run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: 'outbound', operationSnapshot?: RuntimeSnapshot): Promise<void>;
|
|
33
38
|
run<TInput>(input: TInput, terminal: OutboundMiddlewareNext | undefined, target: MiddlewareTarget, operationSnapshot?: RuntimeSnapshot): Promise<MiddlewareRunResult | void>;
|
|
34
39
|
/** Unwinds one inbound operation before its caller performs any automatic reply. */
|
|
35
|
-
runInbound<TInput, TResult>(input: TInput, terminal: () => Promise<TResult>, results: MiddlewareResultPolicy<TResult>, operationSnapshot?: RuntimeSnapshot): Promise<TResult>;
|
|
40
|
+
runInbound<TInput, TResult>(input: TInput, terminal: (control: InboundTerminalControl) => Promise<TResult>, results: MiddlewareResultPolicy<TResult>, operationSnapshot?: RuntimeSnapshot): Promise<TResult>;
|
|
36
41
|
}
|
|
37
42
|
/** @internal Runtime projection guard. */
|
|
38
43
|
export declare function isMiddlewareIndex(value: unknown): value is MiddlewareIndex;
|
package/lib/middleware-index.js
CHANGED
|
@@ -56,7 +56,7 @@ export class MiddlewareIndex {
|
|
|
56
56
|
const definition = record.slot.definition;
|
|
57
57
|
if (definition.target !== 'outbound')
|
|
58
58
|
throw new Error('Invalid outbound Middleware');
|
|
59
|
-
const result = await definition.handle(context, () => dispatch(index + 1));
|
|
59
|
+
const result = await definition.handle.call(context, context, () => dispatch(index + 1));
|
|
60
60
|
if (result !== undefined)
|
|
61
61
|
throw new TypeError('Outbound Middleware must use input.replace(), not return content');
|
|
62
62
|
};
|
|
@@ -67,16 +67,41 @@ export class MiddlewareIndex {
|
|
|
67
67
|
const clientAdapter = operationClientAdapter(input);
|
|
68
68
|
const records = this.#records.filter((record) => record.target === 'inbound'
|
|
69
69
|
&& (!record.adapter || record.adapter === clientAdapter));
|
|
70
|
+
let dispatchSummary;
|
|
71
|
+
let reported = false;
|
|
72
|
+
const afterDispatchListeners = [];
|
|
73
|
+
const reportDispatch = async (summary) => {
|
|
74
|
+
dispatchSummary = Object.freeze({ ...summary });
|
|
75
|
+
if (reported)
|
|
76
|
+
return;
|
|
77
|
+
reported = true;
|
|
78
|
+
for (const listener of afterDispatchListeners.splice(0)) {
|
|
79
|
+
await listener(dispatchSummary);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
70
82
|
const dispatch = async (index) => {
|
|
71
83
|
const record = records[index];
|
|
72
|
-
if (!record)
|
|
73
|
-
|
|
84
|
+
if (!record) {
|
|
85
|
+
const terminalResult = await terminal({ reportDispatch });
|
|
86
|
+
if (!reported) {
|
|
87
|
+
const inferred = results.summarize?.(terminalResult)
|
|
88
|
+
?? { matched: false, path: 'none', replied: false };
|
|
89
|
+
await reportDispatch(inferred);
|
|
90
|
+
}
|
|
91
|
+
return terminalResult;
|
|
92
|
+
}
|
|
74
93
|
const definition = record.slot.definition;
|
|
75
94
|
if (definition.target !== 'inbound')
|
|
76
95
|
throw new Error('Invalid inbound Middleware');
|
|
77
96
|
const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
|
|
78
97
|
const context = {
|
|
79
98
|
...createCapabilityContext({ snapshot: operationSnapshot, ownerId: record.owner, admission: undefined, signal: readOperationSession(authorInput, record.adapter)?.signal }), input: authorInput,
|
|
99
|
+
onAfterDispatch(listener) {
|
|
100
|
+
if (typeof listener !== 'function') {
|
|
101
|
+
throw new TypeError('Middleware onAfterDispatch listener must be a function');
|
|
102
|
+
}
|
|
103
|
+
afterDispatchListeners.push(listener);
|
|
104
|
+
},
|
|
80
105
|
};
|
|
81
106
|
Object.defineProperty(context, 'session', {
|
|
82
107
|
enumerable: true, get: () => readOperationSession(authorInput, record.adapter),
|
|
@@ -84,6 +109,9 @@ export class MiddlewareIndex {
|
|
|
84
109
|
Object.defineProperty(context, '$client', {
|
|
85
110
|
enumerable: true, get: () => readOperationClient(authorInput, record.adapter),
|
|
86
111
|
});
|
|
112
|
+
Object.defineProperty(context, 'dispatch', {
|
|
113
|
+
enumerable: true, get: () => dispatchSummary,
|
|
114
|
+
});
|
|
87
115
|
Object.freeze(context);
|
|
88
116
|
let active = true;
|
|
89
117
|
let nextCalled = false;
|
|
@@ -107,7 +135,7 @@ export class MiddlewareIndex {
|
|
|
107
135
|
};
|
|
108
136
|
let returned;
|
|
109
137
|
try {
|
|
110
|
-
returned = await definition.handle(context, next);
|
|
138
|
+
returned = await definition.handle.call(context, context, next);
|
|
111
139
|
}
|
|
112
140
|
catch (error) {
|
|
113
141
|
// Drain an admitted child before the owning generation lease can close.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/middleware",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "Convention-based Middleware Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"src"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@zhin.js/feature-kit": "1.1.
|
|
20
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
19
|
+
"@zhin.js/feature-kit": "1.1.14",
|
|
20
|
+
"@zhin.js/plugin-runtime": "1.1.22"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^26.1.2",
|
package/src/definition.ts
CHANGED
|
@@ -18,6 +18,13 @@ export type MiddlewareTarget = 'inbound' | 'outbound';
|
|
|
18
18
|
export type MiddlewareNext = () => Promise<MiddlewareContinuation>;
|
|
19
19
|
export type OutboundMiddlewareNext = () => Promise<void>;
|
|
20
20
|
|
|
21
|
+
export interface InboundDispatchSummary {
|
|
22
|
+
readonly matched: boolean;
|
|
23
|
+
readonly path: 'button' | 'command' | 'handler' | 'none';
|
|
24
|
+
readonly handlerId?: string;
|
|
25
|
+
readonly replied: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export interface MiddlewareContext<
|
|
22
29
|
TInput = unknown,
|
|
23
30
|
TConfig = unknown,
|
|
@@ -28,6 +35,8 @@ export interface MiddlewareContext<
|
|
|
28
35
|
/** Lazily resolved native Client. Without `adapter`, its static type is `unknown`. */
|
|
29
36
|
readonly $client: AdapterClient<TAdapter>;
|
|
30
37
|
readonly session: OperationSession<TAdapter> | undefined;
|
|
38
|
+
readonly dispatch?: InboundDispatchSummary;
|
|
39
|
+
onAfterDispatch?(listener: (summary: InboundDispatchSummary) => void | Promise<void>): void;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/** Shared metadata for the public inbound and outbound definition contracts. */
|
|
@@ -71,15 +80,16 @@ declare module '@zhin.js/plugin-runtime' {
|
|
|
71
80
|
type AuthoringForAdapter<TInput, TConfig, TAdapter extends string | undefined> =
|
|
72
81
|
Omit<MiddlewareDefinitionBase<TAdapter>, '$feature' | 'phase' | 'order'> & {
|
|
73
82
|
readonly phase?: MiddlewarePhase;
|
|
83
|
+
readonly stage?: MiddlewarePhase;
|
|
74
84
|
readonly order?: number;
|
|
75
85
|
} & (
|
|
76
|
-
| { readonly target?: 'inbound'; handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown }
|
|
86
|
+
| { readonly target?: 'inbound'; handle(this: MiddlewareContext<TInput, TConfig, TAdapter>, context: MiddlewareContext<TInput, TConfig, TAdapter>, next: MiddlewareNext): unknown }
|
|
77
87
|
| {
|
|
78
88
|
readonly target: 'outbound';
|
|
79
89
|
/** Opt in to cumulative prefixes. Previously published text, format and addressing must stay stable;
|
|
80
90
|
* do not repeat approvals, external sends or other side effects. */
|
|
81
91
|
readonly incremental?: true;
|
|
82
|
-
handle(context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void>;
|
|
92
|
+
handle(this: MiddlewareContext<TInput, TConfig, TAdapter>, context: MiddlewareContext<TInput, TConfig, TAdapter>, next: OutboundMiddlewareNext): void | Promise<void>;
|
|
83
93
|
}
|
|
84
94
|
);
|
|
85
95
|
|
|
@@ -92,6 +102,21 @@ export type MiddlewareAuthoringDefinition<TInput, TConfig> =
|
|
|
92
102
|
}
|
|
93
103
|
}[RegisteredAdapterName];
|
|
94
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Returned by `defineMiddleware.withConfig<Config>()`: fixes the plugin config type while
|
|
107
|
+
* keeping inbound/outbound input inference intact.
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export interface MiddlewareDefiner<TConfig> {
|
|
112
|
+
<TInput = unknown>(
|
|
113
|
+
definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target: 'outbound' }>,
|
|
114
|
+
): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'outbound' }>>;
|
|
115
|
+
<TInput = unknown>(
|
|
116
|
+
definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target?: 'inbound' }>,
|
|
117
|
+
): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'inbound' }>>;
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
export function defineMiddleware<TInput = unknown, TConfig = unknown>(
|
|
96
121
|
definition: Extract<MiddlewareAuthoringDefinition<TInput, TConfig>, { readonly target: 'outbound' }>,
|
|
97
122
|
): Readonly<Extract<MiddlewareDefinition<TInput, TConfig>, { readonly target: 'outbound' }>>;
|
|
@@ -109,7 +134,7 @@ export function defineMiddleware<TInput = unknown, TConfig = unknown>(
|
|
|
109
134
|
&& (typeof adapter !== 'string' || adapter.trim() === '')) {
|
|
110
135
|
throw new TypeError('Middleware adapter must be a non-empty string');
|
|
111
136
|
}
|
|
112
|
-
const phase = definition.phase ?? 'before-dispatch';
|
|
137
|
+
const phase = definition.phase ?? (definition as { readonly stage?: MiddlewarePhase }).stage ?? 'before-dispatch';
|
|
113
138
|
if (phase !== 'before-dispatch' && phase !== 'after-dispatch') {
|
|
114
139
|
throw new TypeError(`Invalid Middleware phase: ${String(phase)}`);
|
|
115
140
|
}
|
|
@@ -124,6 +149,14 @@ export function defineMiddleware<TInput = unknown, TConfig = unknown>(
|
|
|
124
149
|
>;
|
|
125
150
|
}
|
|
126
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Fix the plugin config type first, then define the middleware.
|
|
154
|
+
*/
|
|
155
|
+
defineMiddleware.withConfig = function withConfig<TConfig = unknown>(): MiddlewareDefiner<TConfig> {
|
|
156
|
+
return ((definition: MiddlewareAuthoringDefinition<unknown, TConfig>) =>
|
|
157
|
+
defineMiddleware(definition as never)) as MiddlewareDefiner<TConfig>;
|
|
158
|
+
};
|
|
159
|
+
|
|
127
160
|
/** @internal Runtime validation for convention-discovered modules. */
|
|
128
161
|
export function parseMiddlewareDefinition(value: unknown): MiddlewareDefinition {
|
|
129
162
|
if (!value || typeof value !== 'object') throw invalidMiddleware();
|
package/src/middleware-index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readOperationSession,
|
|
8
8
|
} from '@zhin.js/feature-kit';
|
|
9
9
|
import type {
|
|
10
|
+
InboundDispatchSummary,
|
|
10
11
|
MiddlewareContext,
|
|
11
12
|
MiddlewareDefinition,
|
|
12
13
|
MiddlewarePhase,
|
|
@@ -15,10 +16,16 @@ import type {
|
|
|
15
16
|
} from './definition.js';
|
|
16
17
|
import { continuationBrand, type MiddlewareContinuation } from './continuation.js';
|
|
17
18
|
|
|
19
|
+
/** @internal Terminal control passed to inbound terminal callbacks to publish dispatch summary before after-dispatch unwinds. */
|
|
20
|
+
export interface InboundTerminalControl {
|
|
21
|
+
reportDispatch(summary: InboundDispatchSummary): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
/** @internal Transmission-neutral result policy supplied by the composition layer. */
|
|
19
25
|
export interface MiddlewareResultPolicy<TResult> {
|
|
20
26
|
stop(owner: PluginId): TResult;
|
|
21
27
|
replace(owner: PluginId, value: unknown, downstream: TResult | undefined): TResult;
|
|
28
|
+
summarize?(result: TResult): InboundDispatchSummary | undefined;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
/** @internal Runtime continuation result. */
|
|
@@ -109,7 +116,7 @@ export class MiddlewareIndex {
|
|
|
109
116
|
Object.freeze(context);
|
|
110
117
|
const definition = record.slot.definition;
|
|
111
118
|
if (definition.target !== 'outbound') throw new Error('Invalid outbound Middleware');
|
|
112
|
-
const result = await definition.handle(context, () => dispatch(index + 1));
|
|
119
|
+
const result = await definition.handle.call(context, context, () => dispatch(index + 1));
|
|
113
120
|
if (result !== undefined) throw new TypeError('Outbound Middleware must use input.replace(), not return content');
|
|
114
121
|
};
|
|
115
122
|
await dispatch(0);
|
|
@@ -118,21 +125,46 @@ export class MiddlewareIndex {
|
|
|
118
125
|
/** Unwinds one inbound operation before its caller performs any automatic reply. */
|
|
119
126
|
async runInbound<TInput, TResult>(
|
|
120
127
|
input: TInput,
|
|
121
|
-
terminal: () => Promise<TResult>,
|
|
128
|
+
terminal: (control: InboundTerminalControl) => Promise<TResult>,
|
|
122
129
|
results: MiddlewareResultPolicy<TResult>,
|
|
123
130
|
operationSnapshot: RuntimeSnapshot = this.snapshot,
|
|
124
131
|
): Promise<TResult> {
|
|
125
132
|
const clientAdapter = operationClientAdapter(input);
|
|
126
133
|
const records = this.#records.filter((record) => record.target === 'inbound'
|
|
127
134
|
&& (!record.adapter || record.adapter === clientAdapter));
|
|
135
|
+
let dispatchSummary: InboundDispatchSummary | undefined;
|
|
136
|
+
let reported = false;
|
|
137
|
+
const afterDispatchListeners: Array<(summary: InboundDispatchSummary) => void | Promise<void>> = [];
|
|
138
|
+
const reportDispatch = async (summary: InboundDispatchSummary): Promise<void> => {
|
|
139
|
+
dispatchSummary = Object.freeze({ ...summary });
|
|
140
|
+
if (reported) return;
|
|
141
|
+
reported = true;
|
|
142
|
+
for (const listener of afterDispatchListeners.splice(0)) {
|
|
143
|
+
await listener(dispatchSummary);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
128
146
|
const dispatch = async (index: number): Promise<TResult> => {
|
|
129
147
|
const record = records[index];
|
|
130
|
-
if (!record)
|
|
148
|
+
if (!record) {
|
|
149
|
+
const terminalResult = await terminal({ reportDispatch });
|
|
150
|
+
if (!reported) {
|
|
151
|
+
const inferred = results.summarize?.(terminalResult)
|
|
152
|
+
?? { matched: false, path: 'none' as const, replied: false };
|
|
153
|
+
await reportDispatch(inferred);
|
|
154
|
+
}
|
|
155
|
+
return terminalResult;
|
|
156
|
+
}
|
|
131
157
|
const definition = record.slot.definition;
|
|
132
158
|
if (definition.target !== 'inbound') throw new Error('Invalid inbound Middleware');
|
|
133
159
|
const authorInput = bindOperationInput(input, record.owner, operationSnapshot);
|
|
134
160
|
const context = {
|
|
135
161
|
...createCapabilityContext({ snapshot: operationSnapshot, ownerId: record.owner, admission: undefined, signal: readOperationSession(authorInput, record.adapter)?.signal }), input: authorInput,
|
|
162
|
+
onAfterDispatch(listener: (summary: InboundDispatchSummary) => void | Promise<void>) {
|
|
163
|
+
if (typeof listener !== 'function') {
|
|
164
|
+
throw new TypeError('Middleware onAfterDispatch listener must be a function');
|
|
165
|
+
}
|
|
166
|
+
afterDispatchListeners.push(listener);
|
|
167
|
+
},
|
|
136
168
|
} as MiddlewareContext<TInput>;
|
|
137
169
|
Object.defineProperty(context, 'session', {
|
|
138
170
|
enumerable: true, get: () => readOperationSession(authorInput, record.adapter),
|
|
@@ -140,6 +172,9 @@ export class MiddlewareIndex {
|
|
|
140
172
|
Object.defineProperty(context, '$client', {
|
|
141
173
|
enumerable: true, get: () => readOperationClient(authorInput, record.adapter),
|
|
142
174
|
});
|
|
175
|
+
Object.defineProperty(context, 'dispatch', {
|
|
176
|
+
enumerable: true, get: () => dispatchSummary,
|
|
177
|
+
});
|
|
143
178
|
Object.freeze(context);
|
|
144
179
|
let active = true;
|
|
145
180
|
let nextCalled = false;
|
|
@@ -161,7 +196,7 @@ export class MiddlewareIndex {
|
|
|
161
196
|
};
|
|
162
197
|
let returned: unknown;
|
|
163
198
|
try {
|
|
164
|
-
returned = await definition.handle(context, next);
|
|
199
|
+
returned = await definition.handle.call(context, context, next);
|
|
165
200
|
} catch (error) {
|
|
166
201
|
// Drain an admitted child before the owning generation lease can close.
|
|
167
202
|
await nextTask?.catch(() => undefined);
|