@zhin.js/adapter 1.2.0 → 1.2.2
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 +47 -1
- package/lib/adapter-index.d.ts +12 -5
- package/lib/adapter-index.js +46 -7
- package/lib/definition.d.ts +41 -26
- package/lib/definition.js +28 -2
- package/lib/endpoint-client.d.ts +62 -0
- package/lib/endpoint-client.js +163 -0
- package/lib/endpoint-control.d.ts +1 -1
- package/lib/endpoint-lifecycle.d.ts +1 -1
- package/lib/endpoint-lifecycle.js +35 -25
- package/lib/endpoint.d.ts +78 -0
- package/lib/endpoint.js +100 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/provider.d.ts +1 -1
- package/package.json +5 -5
- package/src/adapter-index.ts +52 -12
- package/src/definition.ts +49 -26
- package/src/endpoint-client.ts +250 -0
- package/src/endpoint-control.ts +1 -1
- package/src/endpoint-lifecycle.ts +31 -23
- package/src/endpoint.ts +181 -0
- package/src/index.ts +6 -0
|
@@ -22,25 +22,19 @@
|
|
|
22
22
|
* 构造器里 `this.#lifecycle = createEndpointLifecycle({ name: config.id, reconnect, heartbeat })`。
|
|
23
23
|
* 2. `start()` 改为:
|
|
24
24
|
* ```ts
|
|
25
|
-
* this.#
|
|
26
|
-
*
|
|
27
|
-
* await
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ws.on('close', (code, reason) => { handle.notifyClosed(...); rejectIfNotSettled(...); });
|
|
33
|
-
* ws.on('error', (err) => rejectIfNotSettled(err));
|
|
34
|
-
* });
|
|
25
|
+
* await this.#lifecycle.start(async (handle) => {
|
|
26
|
+
* this.#handle = handle; // 供 ws 'close' 回调引用
|
|
27
|
+
* await new Promise<void>((resolve, reject) => {
|
|
28
|
+
* const ws = createWebSocket(...); this.#ws = ws;
|
|
29
|
+
* ws.on('open', () => { this.#lifecycle.startHeartbeat(() => beat(), interval); resolve(); });
|
|
30
|
+
* ws.on('close', (code, reason) => { handle.notifyClosed(...); rejectIfNotSettled(...); });
|
|
31
|
+
* ws.on('error', (err) => rejectIfNotSettled(err));
|
|
35
32
|
* });
|
|
36
|
-
* }
|
|
37
|
-
* this.#unregisterAgent?.(); this.#unregisterAgent = undefined; // 反注册对称
|
|
38
|
-
* throw err;
|
|
39
|
-
* }
|
|
33
|
+
* });
|
|
40
34
|
* ```
|
|
41
|
-
* start 失败复位由基座保证;
|
|
35
|
+
* start 失败复位由基座保证;Client 由 Endpoint 直接持有,不建立旁路 registry。
|
|
42
36
|
* 3. `stop()` 改为:先 `await this.#lifecycle.stop()`(清定时器 + 强关 ws + 竞态 settle),
|
|
43
|
-
* 再做适配器专有清理(rejectAllPending、deduper.clear
|
|
37
|
+
* 再做适配器专有清理(rejectAllPending、deduper.clear)。
|
|
44
38
|
* 4. `handle.onForceClose(() => this.#ws?.close())` 在每次拿到新 socket 后注册,
|
|
45
39
|
* 供心跳看门狗主动断开;ws 'message'/'pong' 回调里调 `notifyHeartbeatAck()` 喂狗。
|
|
46
40
|
* 5. 退避参数由配置映射:reconnect_interval → initialIntervalMs,可按需覆盖
|
|
@@ -108,19 +102,20 @@ class EndpointLifecycleImpl {
|
|
|
108
102
|
this.#connect = connect;
|
|
109
103
|
this.#state = 'connecting';
|
|
110
104
|
this.#attempt = 0;
|
|
105
|
+
const generation = this.#generation + 1;
|
|
111
106
|
try {
|
|
112
107
|
await this.#runConnect(connect);
|
|
113
108
|
}
|
|
114
109
|
catch (err) {
|
|
115
110
|
// 注意:stop() 可能在 await 期间并发改写 #state,必须经 getter 读取避免 TS 窄化误判
|
|
116
|
-
if (this.#currentState() === 'stopped')
|
|
117
|
-
return;
|
|
111
|
+
if (generation !== this.#generation || this.#currentState() === 'stopped')
|
|
112
|
+
return;
|
|
118
113
|
// start 失败复位:回 idle、不武装重连,允许调用方重试
|
|
119
114
|
this.#state = 'idle';
|
|
120
115
|
throw err;
|
|
121
116
|
}
|
|
122
|
-
if (this.#currentState() === 'stopped')
|
|
123
|
-
return;
|
|
117
|
+
if (generation !== this.#generation || this.#currentState() === 'stopped')
|
|
118
|
+
return;
|
|
124
119
|
this.#state = 'open';
|
|
125
120
|
}
|
|
126
121
|
async stop() {
|
|
@@ -226,8 +221,18 @@ class EndpointLifecycleImpl {
|
|
|
226
221
|
this.#scheduleReconnect();
|
|
227
222
|
},
|
|
228
223
|
onForceClose: (close) => {
|
|
229
|
-
if (generation
|
|
230
|
-
|
|
224
|
+
if (generation !== this.#generation)
|
|
225
|
+
return;
|
|
226
|
+
if (this.#state === 'stopped') {
|
|
227
|
+
try {
|
|
228
|
+
close();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
/* same best-effort cleanup as stop() */
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.#forceClose = close;
|
|
231
236
|
},
|
|
232
237
|
};
|
|
233
238
|
}
|
|
@@ -237,7 +242,11 @@ class EndpointLifecycleImpl {
|
|
|
237
242
|
this.#forceClose = undefined;
|
|
238
243
|
const handle = this.#createHandle(generation);
|
|
239
244
|
// Promise.resolve().then 兜底同步抛错;额外 catch 防止 stop 竞态后迟到拒绝变 unhandled
|
|
240
|
-
const connecting = Promise.resolve().then(() =>
|
|
245
|
+
const connecting = Promise.resolve().then(() => {
|
|
246
|
+
if (generation !== this.#generation || this.#state === 'stopped')
|
|
247
|
+
return;
|
|
248
|
+
return connect(handle);
|
|
249
|
+
});
|
|
241
250
|
connecting.catch(() => { });
|
|
242
251
|
let wake;
|
|
243
252
|
const stopped = new Promise((resolve) => {
|
|
@@ -291,11 +300,12 @@ class EndpointLifecycleImpl {
|
|
|
291
300
|
const elapsed = await this.#sleep(delay);
|
|
292
301
|
if (!elapsed || this.#currentState() !== 'reconnecting')
|
|
293
302
|
return;
|
|
303
|
+
const generation = this.#generation + 1;
|
|
294
304
|
try {
|
|
295
305
|
await this.#runConnect(connect);
|
|
296
306
|
}
|
|
297
307
|
catch (err) {
|
|
298
|
-
if (this.#currentState() === 'stopped')
|
|
308
|
+
if (generation !== this.#generation || this.#currentState() === 'stopped')
|
|
299
309
|
return;
|
|
300
310
|
this.#attempt += 1;
|
|
301
311
|
logger.debug(formatCompact({
|
|
@@ -307,7 +317,7 @@ class EndpointLifecycleImpl {
|
|
|
307
317
|
}));
|
|
308
318
|
continue;
|
|
309
319
|
}
|
|
310
|
-
if (this.#currentState() === 'stopped')
|
|
320
|
+
if (generation !== this.#generation || this.#currentState() === 'stopped')
|
|
311
321
|
return;
|
|
312
322
|
this.#state = 'open';
|
|
313
323
|
this.#attempt = 0;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type CapabilityId, type GenerationAdmissionGate } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import type { AdapterContext, EndpointSendRequest } from './definition.js';
|
|
3
|
+
import type { EndpointManagement } from './endpoint-management.js';
|
|
4
|
+
import type { EndpointControl } from './endpoint-control.js';
|
|
5
|
+
import type { EndpointContentPort } from './endpoint-content.js';
|
|
6
|
+
/** The one inbound event boundary shared by every platform Endpoint. */
|
|
7
|
+
export interface EndpointEventGateway {
|
|
8
|
+
receive(event: EndpointEvent): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** Callback shape used by protocol normalizers which feed Endpoint.emit(). */
|
|
11
|
+
export type EndpointEventEmitter = <TPayload>(name: string, payload: TPayload) => Promise<unknown>;
|
|
12
|
+
/** Generation-bound gateway injected into Endpoint by AdapterIndex. */
|
|
13
|
+
export declare const endpointEventGatewayToken: import("@zhin.js/plugin-runtime").Token<EndpointEventGateway>;
|
|
14
|
+
/** Stable identity of the Endpoint which produced an event. */
|
|
15
|
+
export interface EndpointIdentity {
|
|
16
|
+
readonly id: CapabilityId;
|
|
17
|
+
readonly adapter: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The single event context delivered from an Endpoint into Core and plugins.
|
|
21
|
+
* `client` is the actual platform SDK/protocol client owned by the Endpoint.
|
|
22
|
+
*/
|
|
23
|
+
export interface EndpointEvent<TPayload = unknown, TClient = unknown, TName extends string = string> {
|
|
24
|
+
readonly name: TName;
|
|
25
|
+
readonly payload: TPayload;
|
|
26
|
+
readonly endpoint: EndpointIdentity;
|
|
27
|
+
readonly client: TClient;
|
|
28
|
+
}
|
|
29
|
+
/** Lossless native event delivered before any optional canonical projection. */
|
|
30
|
+
export interface PlatformEvent<TEvent = unknown, TName extends string = string> {
|
|
31
|
+
/** Native SDK/protocol event name, for example `guild_member_add`. */
|
|
32
|
+
readonly name: TName;
|
|
33
|
+
/** Native SDK/protocol payload without canonicalization. */
|
|
34
|
+
readonly event: TEvent;
|
|
35
|
+
}
|
|
36
|
+
declare const endpointBrand: unique symbol;
|
|
37
|
+
declare const endpointBind: unique symbol;
|
|
38
|
+
/**
|
|
39
|
+
* Deep platform boundary.
|
|
40
|
+
*
|
|
41
|
+
* Platform implementations inherit this class, expose the platform-native
|
|
42
|
+
* `client`, own account/transport lifecycle, and normalize every inbound SDK
|
|
43
|
+
* callback through `emit()`. Core owns dispatch, admission and plugin context.
|
|
44
|
+
*/
|
|
45
|
+
export declare abstract class Endpoint<TClient = unknown> {
|
|
46
|
+
#private;
|
|
47
|
+
readonly [endpointBrand] = true;
|
|
48
|
+
/**
|
|
49
|
+
* Platform SDK or protocol client exposed to plugin code.
|
|
50
|
+
* It must be a distinct object: Endpoint owns framework lifecycle, Client
|
|
51
|
+
* owns platform operations.
|
|
52
|
+
*/
|
|
53
|
+
abstract readonly client: TClient;
|
|
54
|
+
readonly management?: EndpointManagement;
|
|
55
|
+
readonly control?: EndpointControl;
|
|
56
|
+
readonly content?: EndpointContentPort;
|
|
57
|
+
/** @internal Bound exactly once by the generation-owned AdapterIndex. */
|
|
58
|
+
[endpointBind](context: AdapterContext, admission?: GenerationAdmissionGate): void;
|
|
59
|
+
/** The identity is available after AdapterDefinition.create returns. */
|
|
60
|
+
get identity(): EndpointIdentity;
|
|
61
|
+
/** The only legal platform-to-framework event ingress. */
|
|
62
|
+
protected emit<TPayload, TName extends string>(name: TName, payload: TPayload): Promise<unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* Lossless native-event projection. Adapters call this before deriving
|
|
65
|
+
* message/notice/request/system events, including for unknown event kinds.
|
|
66
|
+
*/
|
|
67
|
+
protected emitPlatform<TEvent, TName extends string>(name: TName, event: TEvent): Promise<unknown>;
|
|
68
|
+
abstract start(signal: AbortSignal): void | Promise<void>;
|
|
69
|
+
abstract open(): void;
|
|
70
|
+
abstract close(): void | Promise<void>;
|
|
71
|
+
abstract stop(): void | Promise<void>;
|
|
72
|
+
send?(_request: EndpointSendRequest): string | Promise<string>;
|
|
73
|
+
}
|
|
74
|
+
/** @internal AdapterIndex binding hook; deliberately not exported by name. */
|
|
75
|
+
export declare function bindEndpoint(endpoint: Endpoint, context: AdapterContext, admission?: GenerationAdmissionGate): void;
|
|
76
|
+
/** @internal Cross-generation Endpoint check that survives ESM module re-evaluation. */
|
|
77
|
+
export declare function isEndpoint(value: unknown): value is Endpoint;
|
|
78
|
+
export {};
|
package/lib/endpoint.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createToken, } from '@zhin.js/plugin-runtime';
|
|
2
|
+
/** Generation-bound gateway injected into Endpoint by AdapterIndex. */
|
|
3
|
+
export const endpointEventGatewayToken = createToken('zhin.adapter.endpoint-events');
|
|
4
|
+
const endpointBrand = Symbol.for('zhin.adapter.endpoint/1');
|
|
5
|
+
const endpointBind = Symbol.for('zhin.adapter.endpoint-bind/1');
|
|
6
|
+
const PRE_ADMISSION_EVENT_LIMIT = 256;
|
|
7
|
+
/**
|
|
8
|
+
* Deep platform boundary.
|
|
9
|
+
*
|
|
10
|
+
* Platform implementations inherit this class, expose the platform-native
|
|
11
|
+
* `client`, own account/transport lifecycle, and normalize every inbound SDK
|
|
12
|
+
* callback through `emit()`. Core owns dispatch, admission and plugin context.
|
|
13
|
+
*/
|
|
14
|
+
export class Endpoint {
|
|
15
|
+
[endpointBrand] = true;
|
|
16
|
+
management;
|
|
17
|
+
control;
|
|
18
|
+
content;
|
|
19
|
+
#identity;
|
|
20
|
+
#events;
|
|
21
|
+
#admissionState = 'unbound';
|
|
22
|
+
#pendingEvents = [];
|
|
23
|
+
/** @internal Bound exactly once by the generation-owned AdapterIndex. */
|
|
24
|
+
[endpointBind](context, admission) {
|
|
25
|
+
if (this.#events)
|
|
26
|
+
throw new Error(`Endpoint ${context.id} is already bound`);
|
|
27
|
+
this.#identity = Object.freeze({ id: context.id, adapter: context.name });
|
|
28
|
+
this.#events = context.use(endpointEventGatewayToken);
|
|
29
|
+
if (!admission) {
|
|
30
|
+
this.#admissionState = 'active';
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.#admissionState = 'pending';
|
|
34
|
+
admission.onActivate(() => {
|
|
35
|
+
if (this.#admissionState !== 'pending')
|
|
36
|
+
return;
|
|
37
|
+
this.#admissionState = 'active';
|
|
38
|
+
admission.onDeactivate(() => {
|
|
39
|
+
this.#admissionState = 'retired';
|
|
40
|
+
this.#pendingEvents.length = 0;
|
|
41
|
+
});
|
|
42
|
+
const pending = this.#pendingEvents.splice(0);
|
|
43
|
+
for (const event of pending) {
|
|
44
|
+
void this.#events?.receive(event).catch(() => undefined);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** The identity is available after AdapterDefinition.create returns. */
|
|
49
|
+
get identity() {
|
|
50
|
+
if (!this.#identity)
|
|
51
|
+
throw new Error('Endpoint is not bound to a runtime generation');
|
|
52
|
+
return this.#identity;
|
|
53
|
+
}
|
|
54
|
+
/** The only legal platform-to-framework event ingress. */
|
|
55
|
+
emit(name, payload) {
|
|
56
|
+
if (!this.#events || !this.#identity) {
|
|
57
|
+
throw new Error('Endpoint emitted before it was bound to a runtime generation');
|
|
58
|
+
}
|
|
59
|
+
const event = Object.freeze({
|
|
60
|
+
name,
|
|
61
|
+
payload,
|
|
62
|
+
endpoint: this.#identity,
|
|
63
|
+
client: this.client,
|
|
64
|
+
});
|
|
65
|
+
if (this.#admissionState === 'pending') {
|
|
66
|
+
if (this.#pendingEvents.length >= PRE_ADMISSION_EVENT_LIMIT) {
|
|
67
|
+
this.#pendingEvents.shift();
|
|
68
|
+
}
|
|
69
|
+
this.#pendingEvents.push(event);
|
|
70
|
+
return Promise.resolve(undefined);
|
|
71
|
+
}
|
|
72
|
+
if (this.#admissionState === 'retired')
|
|
73
|
+
return Promise.resolve(undefined);
|
|
74
|
+
return this.#events.receive(event);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Lossless native-event projection. Adapters call this before deriving
|
|
78
|
+
* message/notice/request/system events, including for unknown event kinds.
|
|
79
|
+
*/
|
|
80
|
+
emitPlatform(name, event) {
|
|
81
|
+
return this.emit('platform.receive', Object.freeze({ name, event }));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** @internal AdapterIndex binding hook; deliberately not exported by name. */
|
|
85
|
+
export function bindEndpoint(endpoint, context, admission) {
|
|
86
|
+
endpoint[endpointBind](context, admission);
|
|
87
|
+
}
|
|
88
|
+
/** @internal Cross-generation Endpoint check that survives ESM module re-evaluation. */
|
|
89
|
+
export function isEndpoint(value) {
|
|
90
|
+
if (!value || typeof value !== 'object')
|
|
91
|
+
return false;
|
|
92
|
+
const candidate = value;
|
|
93
|
+
return candidate[endpointBrand] === true
|
|
94
|
+
&& typeof candidate[endpointBind] === 'function'
|
|
95
|
+
&& typeof candidate.start === 'function'
|
|
96
|
+
&& typeof candidate.open === 'function'
|
|
97
|
+
&& typeof candidate.close === 'function'
|
|
98
|
+
&& typeof candidate.stop === 'function'
|
|
99
|
+
&& 'client' in candidate;
|
|
100
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter authoring contracts and platform-neutral Endpoint capabilities.
|
|
3
|
+
* @module @zhin.js/adapter
|
|
4
|
+
*/
|
|
1
5
|
/** @internal 适配器 projection(AdapterIndex),框架内部机制,不承诺不 break。 */
|
|
2
6
|
export * from './adapter-index.js';
|
|
3
7
|
export * from './credentials.js';
|
|
4
8
|
/** @public 用户侧创作面:`defineAdapter`(`adapters/` 约定目录默认导出,承诺 semver)。 */
|
|
5
9
|
export * from './definition.js';
|
|
10
|
+
export * from './endpoint.js';
|
|
6
11
|
export * from './endpoint-commands.js';
|
|
7
12
|
export * from './endpoint-lifecycle.js';
|
|
8
13
|
export * from './endpoint-management.js';
|
|
9
14
|
export * from './endpoint-control.js';
|
|
10
15
|
export * from './endpoint-content.js';
|
|
16
|
+
export * from './endpoint-client.js';
|
|
11
17
|
export * from './provider.js';
|
|
12
18
|
export { default } from './provider.js';
|
package/lib/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter authoring contracts and platform-neutral Endpoint capabilities.
|
|
3
|
+
* @module @zhin.js/adapter
|
|
4
|
+
*/
|
|
1
5
|
/** @internal 适配器 projection(AdapterIndex),框架内部机制,不承诺不 break。 */
|
|
2
6
|
export * from './adapter-index.js';
|
|
3
7
|
export * from './credentials.js';
|
|
4
8
|
/** @public 用户侧创作面:`defineAdapter`(`adapters/` 约定目录默认导出,承诺 semver)。 */
|
|
5
9
|
export * from './definition.js';
|
|
10
|
+
export * from './endpoint.js';
|
|
6
11
|
export * from './endpoint-commands.js';
|
|
7
12
|
export * from './endpoint-lifecycle.js';
|
|
8
13
|
export * from './endpoint-management.js';
|
|
9
14
|
export * from './endpoint-control.js';
|
|
10
15
|
export * from './endpoint-content.js';
|
|
16
|
+
export * from './endpoint-client.js';
|
|
11
17
|
export * from './provider.js';
|
|
12
18
|
export { default } from './provider.js';
|
package/lib/provider.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AdapterIndex } from './adapter-index.js';
|
|
2
2
|
export declare const adapterFeatureId: import("@zhin.js/plugin-runtime").FeatureId;
|
|
3
|
-
declare const adapterFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<import("./definition.js").AdapterDefinition<unknown>, AdapterIndex>>;
|
|
3
|
+
declare const adapterFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<import("./definition.js").AdapterDefinition<unknown, unknown>, AdapterIndex>>;
|
|
4
4
|
export { adapterFeature };
|
|
5
5
|
export default adapterFeature;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Convention-based Adapter and Endpoint Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"yaml": "^2.9.0",
|
|
21
|
-
"@zhin.js/feature-kit": "1.0.
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.13",
|
|
22
22
|
"@zhin.js/im-contract": "1.0.4",
|
|
23
|
-
"@zhin.js/logger": "1.0.
|
|
24
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
23
|
+
"@zhin.js/logger": "1.0.77",
|
|
24
|
+
"@zhin.js/plugin-runtime": "1.1.8"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
|
-
"@zhin.js/command": "1.0.
|
|
29
|
+
"@zhin.js/command": "1.0.16"
|
|
30
30
|
},
|
|
31
31
|
"zhin": {
|
|
32
32
|
"protocol": 1,
|
package/src/adapter-index.ts
CHANGED
|
@@ -17,9 +17,9 @@ import {
|
|
|
17
17
|
type AdapterDefinition,
|
|
18
18
|
type AdapterOperation,
|
|
19
19
|
type AdapterSegmentPolicy,
|
|
20
|
-
type EndpointInstance,
|
|
21
20
|
type EndpointSendRequest,
|
|
22
21
|
} from './definition.js';
|
|
22
|
+
import { bindEndpoint, isEndpoint, type Endpoint } from './endpoint.js';
|
|
23
23
|
import {
|
|
24
24
|
listEndpointManagementCapabilities,
|
|
25
25
|
type EndpointManagementCapability,
|
|
@@ -57,7 +57,7 @@ export type AdapterEndpointPhase =
|
|
|
57
57
|
'pending' | 'starting' | 'online';
|
|
58
58
|
|
|
59
59
|
interface AdapterRecord extends AdapterDescriptor {
|
|
60
|
-
readonly endpoint:
|
|
60
|
+
readonly endpoint: Endpoint;
|
|
61
61
|
readonly segments?: AdapterSegmentPolicy;
|
|
62
62
|
started: boolean;
|
|
63
63
|
open: boolean;
|
|
@@ -157,15 +157,49 @@ export class AdapterIndex {
|
|
|
157
157
|
return exact?.id ?? matches[0]?.id;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
*/
|
|
163
|
-
instance(adapter: string, endpointKey: string): EndpointInstance | undefined {
|
|
160
|
+
/** Resolve the framework-owned Endpoint for internal Host control ports. */
|
|
161
|
+
connection(adapter: string, endpointKey: string): Endpoint | undefined {
|
|
164
162
|
const id = this.resolve(adapter, endpointKey);
|
|
165
163
|
if (!id) return undefined;
|
|
166
164
|
return this.#records.get(id)?.endpoint;
|
|
167
165
|
}
|
|
168
166
|
|
|
167
|
+
/** Resolve the platform-native client owned by one active Endpoint. */
|
|
168
|
+
client<TClient>(adapter: string, endpointKey: string): TClient {
|
|
169
|
+
const id = this.resolve(adapter, endpointKey);
|
|
170
|
+
const record = id ? this.#records.get(id) : undefined;
|
|
171
|
+
if (!record) throw new Error(`Endpoint ${adapter}/${endpointKey} does not exist`);
|
|
172
|
+
if (!record.started || record.stopped) {
|
|
173
|
+
throw new Error(`Endpoint ${adapter}/${endpointKey} is not active`);
|
|
174
|
+
}
|
|
175
|
+
return record.endpoint.client as TClient;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Resolve the Client directly from a generation-stable CapabilityId. */
|
|
179
|
+
clientById<TClient>(id: CapabilityId): TClient {
|
|
180
|
+
const record = this.#records.get(id);
|
|
181
|
+
if (!record) throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
182
|
+
if (!record.started || record.stopped) {
|
|
183
|
+
throw new Error(`Adapter Endpoint ${id} is not active`);
|
|
184
|
+
}
|
|
185
|
+
return record.endpoint.client as TClient;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Literal adapter name used by authoring-context type discrimination. */
|
|
189
|
+
clientAdapter(id: CapabilityId): string {
|
|
190
|
+
const record = this.#records.get(id);
|
|
191
|
+
if (!record) throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
192
|
+
return record.endpoint.identity.adapter;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Optional Client lookup for cross-platform middleware and routing. */
|
|
196
|
+
findClient<TClient>(adapter: string, endpointKey: string): TClient | undefined {
|
|
197
|
+
const id = this.resolve(adapter, endpointKey);
|
|
198
|
+
const record = id ? this.#records.get(id) : undefined;
|
|
199
|
+
if (!record || !record.started || record.stopped) return undefined;
|
|
200
|
+
return record.endpoint.client as TClient;
|
|
201
|
+
}
|
|
202
|
+
|
|
169
203
|
owner(id: CapabilityId): PluginId {
|
|
170
204
|
const record = this.#records.get(id);
|
|
171
205
|
if (!record) throw new Error(`Unknown Adapter Endpoint: ${id}`);
|
|
@@ -228,6 +262,11 @@ export class AdapterIndex {
|
|
|
228
262
|
}
|
|
229
263
|
signal.throwIfAborted();
|
|
230
264
|
if (record.stopped) throw new Error(`Adapter Endpoint stopped during start: ${record.id}`);
|
|
265
|
+
if (record.endpoint.client === record.endpoint) {
|
|
266
|
+
throw new TypeError(
|
|
267
|
+
`Adapter Endpoint ${record.id} must expose a distinct platform client`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
231
270
|
record.started = true;
|
|
232
271
|
}
|
|
233
272
|
} catch (error) {
|
|
@@ -335,7 +374,7 @@ function matchesEndpoint(
|
|
|
335
374
|
|| record.id.endsWith(`/${adapter}`)
|
|
336
375
|
|| record.owner === adapter
|
|
337
376
|
|| record.owner.endsWith(`/${adapter}`);
|
|
338
|
-
//
|
|
377
|
+
// The live Endpoint identity is the bot runtime id (e.g. ICQQ uin). Host /
|
|
339
378
|
// activity-feedback resolve with that id; slot.localName alone is not enough
|
|
340
379
|
// when multiple plugin instances share localName "icqq".
|
|
341
380
|
const liveName = endpointLiveName(record.endpoint);
|
|
@@ -346,7 +385,7 @@ function matchesEndpoint(
|
|
|
346
385
|
return adapterOk && endpointOk;
|
|
347
386
|
}
|
|
348
387
|
|
|
349
|
-
function endpointLiveName(endpoint:
|
|
388
|
+
function endpointLiveName(endpoint: Endpoint): string | undefined {
|
|
350
389
|
const name = (endpoint as { readonly name?: unknown }).name;
|
|
351
390
|
return typeof name === 'string' && name.length > 0 ? name : undefined;
|
|
352
391
|
}
|
|
@@ -357,9 +396,9 @@ function endpointPhase(record: AdapterRecord): AdapterEndpointPhase {
|
|
|
357
396
|
return 'pending';
|
|
358
397
|
}
|
|
359
398
|
|
|
360
|
-
function assertEndpoint(value: unknown, id: CapabilityId): asserts value is
|
|
361
|
-
if (!value
|
|
362
|
-
throw new TypeError(`Adapter ${id} create() must return an Endpoint
|
|
399
|
+
function assertEndpoint(value: unknown, id: CapabilityId): asserts value is Endpoint {
|
|
400
|
+
if (!isEndpoint(value)) {
|
|
401
|
+
throw new TypeError(`Adapter ${id} create() must return an Endpoint subclass`);
|
|
363
402
|
}
|
|
364
403
|
}
|
|
365
404
|
|
|
@@ -425,7 +464,7 @@ async function createEndpoint(
|
|
|
425
464
|
admission: GenerationAdmissionGate,
|
|
426
465
|
signal: AbortSignal,
|
|
427
466
|
expansion?: EndpointExpansion,
|
|
428
|
-
): Promise<Readonly<{ endpoint:
|
|
467
|
+
): Promise<Readonly<{ endpoint: Endpoint; operations: readonly AdapterOperation[] }>> {
|
|
429
468
|
const context = Object.freeze({
|
|
430
469
|
...createCapabilityContext(snapshot, slot.owner, admission, signal),
|
|
431
470
|
...(expansion?.config ? { config: expansion.config } : {}),
|
|
@@ -435,6 +474,7 @@ async function createEndpoint(
|
|
|
435
474
|
const operations = resolveAdapterOperations(slot.definition, context);
|
|
436
475
|
const endpoint = await slot.definition.create(context);
|
|
437
476
|
assertEndpoint(endpoint, expansion?.id ?? slot.id);
|
|
477
|
+
bindEndpoint(endpoint, context, admission);
|
|
438
478
|
if (slot.definition.capabilities.includes('outbound') && typeof endpoint.send !== 'function') {
|
|
439
479
|
throw new TypeError(
|
|
440
480
|
`Adapter Endpoint ${String(expansion?.id ?? slot.id)} declares outbound but send() is missing`,
|
package/src/definition.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter authoring API consumed from `zhin.js/adapter`.
|
|
3
|
+
* @module zhin.js/adapter
|
|
4
|
+
*/
|
|
1
5
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
2
6
|
import type { CapabilityContext } from '@zhin.js/feature-kit';
|
|
3
7
|
import type {
|
|
@@ -5,9 +9,19 @@ import type {
|
|
|
5
9
|
EndpointCapabilities,
|
|
6
10
|
EndpointOperation,
|
|
7
11
|
} from '@zhin.js/im-contract';
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
import type { Endpoint } from './endpoint.js';
|
|
13
|
+
|
|
14
|
+
export { Endpoint } from './endpoint.js';
|
|
15
|
+
export type {
|
|
16
|
+
EndpointEvent,
|
|
17
|
+
EndpointIdentity,
|
|
18
|
+
PlatformEvent,
|
|
19
|
+
} from './endpoint.js';
|
|
20
|
+
export { defineEndpointClient } from './endpoint-client.js';
|
|
21
|
+
export type {
|
|
22
|
+
EndpointClientContext,
|
|
23
|
+
EndpointClientToken,
|
|
24
|
+
} from './endpoint-client.js';
|
|
11
25
|
|
|
12
26
|
const adapterBrand = 'zhin.adapter/1' as const;
|
|
13
27
|
|
|
@@ -35,25 +49,9 @@ export interface EndpointSendRequest {
|
|
|
35
49
|
readonly payload: unknown;
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Optional platform-neutral control surface for existing messages. */
|
|
42
|
-
readonly control?: EndpointControl;
|
|
43
|
-
/** Optional canonical resolver for message, merged-forward and media references. */
|
|
44
|
-
readonly content?: EndpointContentPort;
|
|
45
|
-
/** Required readiness; must observe abort and settle before rollback returns. */
|
|
46
|
-
start?(signal: AbortSignal): void | Promise<void>;
|
|
47
|
-
/** Opens Endpoint-local flow behind the candidate generation admission gate. */
|
|
48
|
-
open?(): void;
|
|
49
|
-
/** Stops new inbound events while preserving in-flight work. */
|
|
50
|
-
close?(): void | Promise<void>;
|
|
51
|
-
/** Releases transport resources. Calls must be idempotent. */
|
|
52
|
-
stop?(): void | Promise<void>;
|
|
53
|
-
/** Platform message id. Core wraps it in the canonical MessageRef/DeliveryReceipt. */
|
|
54
|
-
send?(request: EndpointSendRequest): string | Promise<string>;
|
|
55
|
-
}
|
|
56
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Generation-owned construction context for one runtime Endpoint.
|
|
54
|
+
*/
|
|
57
55
|
export interface AdapterContext<TConfig = unknown> extends CapabilityContext<TConfig> {
|
|
58
56
|
readonly id: CapabilityId;
|
|
59
57
|
readonly name: string;
|
|
@@ -100,7 +98,8 @@ const OUTBOUND_MEDIA_FORMS: readonly AdapterOutboundMedia[] = [
|
|
|
100
98
|
|
|
101
99
|
const ADAPTER_OPERATIONS: readonly AdapterOperation[] = ['recall', 'edit', 'reaction', 'typing'];
|
|
102
100
|
|
|
103
|
-
export interface AdapterDefinition<TConfig = unknown> {
|
|
101
|
+
export interface AdapterDefinition<TConfig = unknown, TClient = unknown> {
|
|
102
|
+
/** @internal Runtime feature brand. */
|
|
104
103
|
readonly $feature: typeof adapterBrand;
|
|
105
104
|
readonly capabilities: readonly AdapterCapability[];
|
|
106
105
|
/**
|
|
@@ -113,7 +112,7 @@ export interface AdapterDefinition<TConfig = unknown> {
|
|
|
113
112
|
readonly segments?: AdapterSegmentPolicy;
|
|
114
113
|
create(
|
|
115
114
|
context: AdapterContext<TConfig>,
|
|
116
|
-
):
|
|
115
|
+
): Endpoint<TClient> | Promise<Endpoint<TClient>>;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
declare module '@zhin.js/plugin-runtime' {
|
|
@@ -125,6 +124,23 @@ declare module '@zhin.js/plugin-runtime' {
|
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Define an Adapter module for the `adapters/` convention directory.
|
|
129
|
+
*
|
|
130
|
+
* The returned definition is immutable and declares capabilities before an
|
|
131
|
+
* Endpoint is created, so Runtime admission can fail closed.
|
|
132
|
+
*
|
|
133
|
+
* @public
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { defineAdapter } from 'zhin.js/adapter';
|
|
137
|
+
*
|
|
138
|
+
* export default defineAdapter({
|
|
139
|
+
* capabilities: ['inbound', 'outbound'],
|
|
140
|
+
* create: () => new MyPlatformEndpoint(),
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
128
144
|
export function defineAdapter<TConfig = unknown>(
|
|
129
145
|
definition: Omit<AdapterDefinition<TConfig>, '$feature'>,
|
|
130
146
|
): Readonly<AdapterDefinition<TConfig>> {
|
|
@@ -151,7 +167,10 @@ export function defineAdapter<TConfig = unknown>(
|
|
|
151
167
|
});
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Converts the definition's compact authoring form into the Runtime contract.
|
|
172
|
+
* @internal Adapter projection helper.
|
|
173
|
+
*/
|
|
155
174
|
export function endpointCapabilitiesOf(
|
|
156
175
|
definition: Pick<AdapterDefinition, 'capabilities' | 'operations'>,
|
|
157
176
|
resolvedOperations?: readonly AdapterOperation[],
|
|
@@ -172,7 +191,10 @@ export function endpointCapabilitiesOf(
|
|
|
172
191
|
});
|
|
173
192
|
}
|
|
174
193
|
|
|
175
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Resolve and validate the operation declaration for one concrete Endpoint.
|
|
196
|
+
* @internal Adapter projection helper.
|
|
197
|
+
*/
|
|
176
198
|
export function resolveAdapterOperations<TConfig>(
|
|
177
199
|
definition: Pick<AdapterDefinition<TConfig>, 'operations'>,
|
|
178
200
|
context: AdapterContext<TConfig>,
|
|
@@ -251,6 +273,7 @@ function normalizeSegmentPolicy(
|
|
|
251
273
|
});
|
|
252
274
|
}
|
|
253
275
|
|
|
276
|
+
/** @internal Runtime validation for convention-discovered modules. */
|
|
254
277
|
export function parseAdapterDefinition(value: unknown): AdapterDefinition {
|
|
255
278
|
if (!value || typeof value !== 'object') throw invalidAdapter();
|
|
256
279
|
const definition = value as Partial<AdapterDefinition>;
|