@zhin.js/adapter 1.1.11 → 1.2.1
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 +56 -4
- package/lib/adapter-index.d.ts +19 -6
- package/lib/adapter-index.js +74 -14
- package/lib/definition.d.ts +46 -27
- package/lib/definition.js +48 -5
- package/lib/endpoint-client.d.ts +62 -0
- package/lib/endpoint-client.js +163 -0
- package/lib/endpoint-control.d.ts +5 -1
- package/lib/endpoint-control.js +37 -0
- package/lib/endpoint-lifecycle.js +10 -16
- 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 +100 -31
- package/src/definition.ts +79 -29
- package/src/endpoint-client.ts +250 -0
- package/src/endpoint-control.ts +45 -1
- package/src/endpoint-lifecycle.ts +10 -16
- package/src/endpoint.ts +181 -0
- package/src/index.ts +6 -0
package/src/endpoint-control.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ConversationRef, MessageRef } from '@zhin.js/im-contract';
|
|
|
3
3
|
/**
|
|
4
4
|
* Transport-neutral control plane for a live endpoint.
|
|
5
5
|
*
|
|
6
|
-
* Sending belongs to
|
|
6
|
+
* Sending belongs to Endpoint.send(). This port intentionally owns
|
|
7
7
|
* operations that address an existing platform message, so Core never needs
|
|
8
8
|
* to know a protocol's method names or identifier layout.
|
|
9
9
|
*/
|
|
@@ -23,6 +23,15 @@ export interface EndpointWithControl {
|
|
|
23
23
|
readonly control?: EndpointControl;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Bridges the common platform `recall(messageId)` shape into canonical control. */
|
|
27
|
+
export function createRecallEndpointControl(
|
|
28
|
+
recallById: (messageId: string) => void | Promise<void>,
|
|
29
|
+
): Readonly<EndpointControl> {
|
|
30
|
+
return Object.freeze<EndpointControl>({
|
|
31
|
+
recall: (message) => Promise.resolve(recallById(message.id)),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
/** Reads the canonical control port without probing protocol-specific methods. */
|
|
27
36
|
export function endpointControlOf(endpoint: unknown): EndpointControl | undefined {
|
|
28
37
|
if (!endpoint || typeof endpoint !== 'object') return undefined;
|
|
@@ -46,6 +55,24 @@ export function hasExplicitEndpointOperation(
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
/** Lists the semantic operations implemented by an Endpoint's explicit control port. */
|
|
59
|
+
export function listExplicitEndpointOperations(
|
|
60
|
+
endpoint: unknown,
|
|
61
|
+
): readonly ('recall' | 'edit' | 'reaction' | 'typing')[] {
|
|
62
|
+
if (!endpoint || typeof endpoint !== 'object') return Object.freeze([]);
|
|
63
|
+
const control = (endpoint as EndpointWithControl).control;
|
|
64
|
+
if (!control || typeof control !== 'object') return Object.freeze([]);
|
|
65
|
+
const operations: Array<'recall' | 'edit' | 'reaction' | 'typing'> = [];
|
|
66
|
+
if (typeof control.recall === 'function') operations.push('recall');
|
|
67
|
+
if (typeof control.edit === 'function') operations.push('edit');
|
|
68
|
+
if (
|
|
69
|
+
typeof control.addReaction === 'function'
|
|
70
|
+
|| typeof control.removeReaction === 'function'
|
|
71
|
+
) operations.push('reaction');
|
|
72
|
+
if (typeof control.typing === 'function') operations.push('typing');
|
|
73
|
+
return Object.freeze(operations);
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
/** Rejects a declaration that cannot be fulfilled by the explicit control port. */
|
|
50
77
|
export function assertDeclaredEndpointOperations(
|
|
51
78
|
endpoint: unknown,
|
|
@@ -59,8 +86,25 @@ export function assertDeclaredEndpointOperations(
|
|
|
59
86
|
);
|
|
60
87
|
}
|
|
61
88
|
}
|
|
89
|
+
const declared = new Set(operations ?? []);
|
|
90
|
+
for (const operation of listExplicitEndpointOperations(endpoint)) {
|
|
91
|
+
if (!declared.has(operation)) {
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
`Adapter Endpoint ${id} exposes control.${explicitControlMethodName(endpoint, operation)} but does not declare ${operation}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
62
97
|
}
|
|
63
98
|
|
|
64
99
|
function controlMethodName(operation: 'recall' | 'edit' | 'reaction' | 'typing'): string {
|
|
65
100
|
return operation === 'reaction' ? 'addReaction' : operation;
|
|
66
101
|
}
|
|
102
|
+
|
|
103
|
+
function explicitControlMethodName(
|
|
104
|
+
endpoint: unknown,
|
|
105
|
+
operation: 'recall' | 'edit' | 'reaction' | 'typing',
|
|
106
|
+
): string {
|
|
107
|
+
if (operation !== 'reaction') return operation;
|
|
108
|
+
const control = (endpoint as EndpointWithControl).control;
|
|
109
|
+
return typeof control?.addReaction === 'function' ? 'addReaction' : 'removeReaction';
|
|
110
|
+
}
|
|
@@ -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,可按需覆盖
|
package/src/endpoint.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createToken,
|
|
3
|
+
type CapabilityId,
|
|
4
|
+
type GenerationAdmissionGate,
|
|
5
|
+
} from '@zhin.js/plugin-runtime';
|
|
6
|
+
import type { AdapterContext, EndpointSendRequest } from './definition.js';
|
|
7
|
+
import type { EndpointManagement } from './endpoint-management.js';
|
|
8
|
+
import type { EndpointControl } from './endpoint-control.js';
|
|
9
|
+
import type { EndpointContentPort } from './endpoint-content.js';
|
|
10
|
+
|
|
11
|
+
/** The one inbound event boundary shared by every platform Endpoint. */
|
|
12
|
+
export interface EndpointEventGateway {
|
|
13
|
+
receive(event: EndpointEvent): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Callback shape used by protocol normalizers which feed Endpoint.emit(). */
|
|
17
|
+
export type EndpointEventEmitter = <TPayload>(name: string, payload: TPayload) => Promise<unknown>;
|
|
18
|
+
|
|
19
|
+
/** Generation-bound gateway injected into Endpoint by AdapterIndex. */
|
|
20
|
+
export const endpointEventGatewayToken = createToken<EndpointEventGateway>(
|
|
21
|
+
'zhin.adapter.endpoint-events',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** Stable identity of the Endpoint which produced an event. */
|
|
25
|
+
export interface EndpointIdentity {
|
|
26
|
+
readonly id: CapabilityId;
|
|
27
|
+
readonly adapter: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The single event context delivered from an Endpoint into Core and plugins.
|
|
32
|
+
* `client` is the actual platform SDK/protocol client owned by the Endpoint.
|
|
33
|
+
*/
|
|
34
|
+
export interface EndpointEvent<
|
|
35
|
+
TPayload = unknown,
|
|
36
|
+
TClient = unknown,
|
|
37
|
+
TName extends string = string,
|
|
38
|
+
> {
|
|
39
|
+
readonly name: TName;
|
|
40
|
+
readonly payload: TPayload;
|
|
41
|
+
readonly endpoint: EndpointIdentity;
|
|
42
|
+
readonly client: TClient;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Lossless native event delivered before any optional canonical projection. */
|
|
46
|
+
export interface PlatformEvent<
|
|
47
|
+
TEvent = unknown,
|
|
48
|
+
TName extends string = string,
|
|
49
|
+
> {
|
|
50
|
+
/** Native SDK/protocol event name, for example `guild_member_add`. */
|
|
51
|
+
readonly name: TName;
|
|
52
|
+
/** Native SDK/protocol payload without canonicalization. */
|
|
53
|
+
readonly event: TEvent;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const endpointBrand = Symbol.for('zhin.adapter.endpoint/1');
|
|
57
|
+
const endpointBind = Symbol.for('zhin.adapter.endpoint-bind/1');
|
|
58
|
+
const PRE_ADMISSION_EVENT_LIMIT = 256;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Deep platform boundary.
|
|
62
|
+
*
|
|
63
|
+
* Platform implementations inherit this class, expose the platform-native
|
|
64
|
+
* `client`, own account/transport lifecycle, and normalize every inbound SDK
|
|
65
|
+
* callback through `emit()`. Core owns dispatch, admission and plugin context.
|
|
66
|
+
*/
|
|
67
|
+
export abstract class Endpoint<TClient = unknown> {
|
|
68
|
+
readonly [endpointBrand] = true;
|
|
69
|
+
/**
|
|
70
|
+
* Platform SDK or protocol client exposed to plugin code.
|
|
71
|
+
* It must be a distinct object: Endpoint owns framework lifecycle, Client
|
|
72
|
+
* owns platform operations.
|
|
73
|
+
*/
|
|
74
|
+
abstract readonly client: TClient;
|
|
75
|
+
|
|
76
|
+
readonly management?: EndpointManagement;
|
|
77
|
+
readonly control?: EndpointControl;
|
|
78
|
+
readonly content?: EndpointContentPort;
|
|
79
|
+
|
|
80
|
+
#identity?: EndpointIdentity;
|
|
81
|
+
#events?: EndpointEventGateway;
|
|
82
|
+
#admissionState: 'unbound' | 'pending' | 'active' | 'retired' = 'unbound';
|
|
83
|
+
#pendingEvents: EndpointEvent[] = [];
|
|
84
|
+
|
|
85
|
+
/** @internal Bound exactly once by the generation-owned AdapterIndex. */
|
|
86
|
+
[endpointBind](context: AdapterContext, admission?: GenerationAdmissionGate): void {
|
|
87
|
+
if (this.#events) throw new Error(`Endpoint ${context.id} is already bound`);
|
|
88
|
+
this.#identity = Object.freeze({ id: context.id, adapter: context.name });
|
|
89
|
+
this.#events = context.use(endpointEventGatewayToken);
|
|
90
|
+
if (!admission) {
|
|
91
|
+
this.#admissionState = 'active';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this.#admissionState = 'pending';
|
|
95
|
+
admission.onActivate(() => {
|
|
96
|
+
if (this.#admissionState !== 'pending') return;
|
|
97
|
+
this.#admissionState = 'active';
|
|
98
|
+
admission.onDeactivate(() => {
|
|
99
|
+
this.#admissionState = 'retired';
|
|
100
|
+
this.#pendingEvents.length = 0;
|
|
101
|
+
});
|
|
102
|
+
const pending = this.#pendingEvents.splice(0);
|
|
103
|
+
for (const event of pending) {
|
|
104
|
+
void this.#events?.receive(event).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The identity is available after AdapterDefinition.create returns. */
|
|
110
|
+
get identity(): EndpointIdentity {
|
|
111
|
+
if (!this.#identity) throw new Error('Endpoint is not bound to a runtime generation');
|
|
112
|
+
return this.#identity;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The only legal platform-to-framework event ingress. */
|
|
116
|
+
protected emit<TPayload, TName extends string>(
|
|
117
|
+
name: TName,
|
|
118
|
+
payload: TPayload,
|
|
119
|
+
): Promise<unknown> {
|
|
120
|
+
if (!this.#events || !this.#identity) {
|
|
121
|
+
throw new Error('Endpoint emitted before it was bound to a runtime generation');
|
|
122
|
+
}
|
|
123
|
+
const event = Object.freeze({
|
|
124
|
+
name,
|
|
125
|
+
payload,
|
|
126
|
+
endpoint: this.#identity,
|
|
127
|
+
client: this.client,
|
|
128
|
+
});
|
|
129
|
+
if (this.#admissionState === 'pending') {
|
|
130
|
+
if (this.#pendingEvents.length >= PRE_ADMISSION_EVENT_LIMIT) {
|
|
131
|
+
this.#pendingEvents.shift();
|
|
132
|
+
}
|
|
133
|
+
this.#pendingEvents.push(event);
|
|
134
|
+
return Promise.resolve(undefined);
|
|
135
|
+
}
|
|
136
|
+
if (this.#admissionState === 'retired') return Promise.resolve(undefined);
|
|
137
|
+
return this.#events.receive(event);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Lossless native-event projection. Adapters call this before deriving
|
|
142
|
+
* message/notice/request/system events, including for unknown event kinds.
|
|
143
|
+
*/
|
|
144
|
+
protected emitPlatform<TEvent, TName extends string>(
|
|
145
|
+
name: TName,
|
|
146
|
+
event: TEvent,
|
|
147
|
+
): Promise<unknown> {
|
|
148
|
+
return this.emit('platform.receive', Object.freeze({ name, event }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
abstract start(signal: AbortSignal): void | Promise<void>;
|
|
152
|
+
abstract open(): void;
|
|
153
|
+
abstract close(): void | Promise<void>;
|
|
154
|
+
abstract stop(): void | Promise<void>;
|
|
155
|
+
send?(_request: EndpointSendRequest): string | Promise<string>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @internal AdapterIndex binding hook; deliberately not exported by name. */
|
|
159
|
+
export function bindEndpoint(
|
|
160
|
+
endpoint: Endpoint,
|
|
161
|
+
context: AdapterContext,
|
|
162
|
+
admission?: GenerationAdmissionGate,
|
|
163
|
+
): void {
|
|
164
|
+
endpoint[endpointBind](context, admission);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @internal Cross-generation Endpoint check that survives ESM module re-evaluation. */
|
|
168
|
+
export function isEndpoint(value: unknown): value is Endpoint {
|
|
169
|
+
if (!value || typeof value !== 'object') return false;
|
|
170
|
+
const candidate = value as Partial<Endpoint> & {
|
|
171
|
+
readonly [endpointBrand]?: unknown;
|
|
172
|
+
readonly [endpointBind]?: unknown;
|
|
173
|
+
};
|
|
174
|
+
return candidate[endpointBrand] === true
|
|
175
|
+
&& typeof candidate[endpointBind] === 'function'
|
|
176
|
+
&& typeof candidate.start === 'function'
|
|
177
|
+
&& typeof candidate.open === 'function'
|
|
178
|
+
&& typeof candidate.close === 'function'
|
|
179
|
+
&& typeof candidate.stop === 'function'
|
|
180
|
+
&& 'client' in candidate;
|
|
181
|
+
}
|
package/src/index.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';
|