@zhin.js/adapter 1.2.0 → 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 +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.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 +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 +10 -16
- package/src/endpoint.ts +181 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { CapabilityContext } from '@zhin.js/feature-kit';
|
|
2
|
+
import { isAdapterIndex } from './adapter-index.js';
|
|
3
|
+
import { Endpoint } from './endpoint.js';
|
|
4
|
+
import { adapterFeatureId } from './provider.js';
|
|
5
|
+
|
|
6
|
+
const endpointClientBrand = 'zhin.endpoint-client/1' as const;
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
AdapterClient,
|
|
10
|
+
AdapterClientRegistry,
|
|
11
|
+
AdapterClientTypes,
|
|
12
|
+
AdapterEvents,
|
|
13
|
+
RegisteredAdapterName,
|
|
14
|
+
} from '@zhin.js/feature-kit';
|
|
15
|
+
|
|
16
|
+
/** Convert an EventEmitter-style tuple map into the payload map used by handlers. */
|
|
17
|
+
export type ClientEventPayloads<TEvents extends object> = {
|
|
18
|
+
readonly [K in keyof TEvents]: TEvents[K] extends readonly [infer TPayload, ...unknown[]]
|
|
19
|
+
? TPayload
|
|
20
|
+
: never;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface ClientEventSource {
|
|
24
|
+
on(name: string, listener: (payload: unknown) => void): unknown;
|
|
25
|
+
off(name: string, listener: (payload: unknown) => void): unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Forward one Client's public event surface through the Endpoint boundary.
|
|
30
|
+
* The returned disposer is useful when a Client can outlive its Endpoint.
|
|
31
|
+
*/
|
|
32
|
+
export function forwardEndpointClientEvents(
|
|
33
|
+
client: ClientEventSource,
|
|
34
|
+
names: readonly string[],
|
|
35
|
+
receive: (name: string, payload: unknown) => void,
|
|
36
|
+
): () => void {
|
|
37
|
+
const subscriptions = names.map((name) => {
|
|
38
|
+
const listener = (payload: unknown) => receive(name, payload);
|
|
39
|
+
client.on(name, listener);
|
|
40
|
+
return { name, listener };
|
|
41
|
+
});
|
|
42
|
+
return () => {
|
|
43
|
+
for (const { name, listener } of subscriptions) client.off(name, listener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ClientEventSubscription = (
|
|
48
|
+
receive: (name: string, payload: unknown) => void,
|
|
49
|
+
) => () => void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Deep Endpoint base for SDK/protocol Clients.
|
|
53
|
+
*
|
|
54
|
+
* It owns the open admission gate and the single Client-event → Endpoint-event
|
|
55
|
+
* bridge. Concrete Endpoints only own account transport and raw-event
|
|
56
|
+
* normalization; they do not repeat dispatch plumbing.
|
|
57
|
+
*/
|
|
58
|
+
export abstract class ClientEndpoint<TClient = unknown> extends Endpoint<TClient> {
|
|
59
|
+
#clientEventsOpen = false;
|
|
60
|
+
#clientEventsRelease?: () => void;
|
|
61
|
+
|
|
62
|
+
open(): void {
|
|
63
|
+
this.#clientEventsOpen = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
close(): void {
|
|
67
|
+
this.#clientEventsOpen = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected get clientEventsOpen(): boolean {
|
|
71
|
+
return this.#clientEventsOpen;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected bindClientEvents(
|
|
75
|
+
subscribe: ClientEventSubscription,
|
|
76
|
+
receive?: (name: string, payload: unknown) => void,
|
|
77
|
+
onError?: (name: string, error: unknown) => void,
|
|
78
|
+
): void {
|
|
79
|
+
this.#clientEventsRelease?.();
|
|
80
|
+
this.#clientEventsRelease = subscribe((name, payload) => {
|
|
81
|
+
if (!this.#clientEventsOpen) return;
|
|
82
|
+
void this.emitPlatform(name, payload).catch((error) => onError?.(name, error));
|
|
83
|
+
try {
|
|
84
|
+
receive?.(name, payload);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
onError?.(name, error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
protected releaseClientEvents(): void {
|
|
92
|
+
this.#clientEventsRelease?.();
|
|
93
|
+
this.#clientEventsRelease = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Typed identity for one platform's native Client surface. */
|
|
98
|
+
export interface EndpointClientToken<TClient, TEvents extends object = Record<string, unknown>> {
|
|
99
|
+
readonly $client: typeof endpointClientBrand;
|
|
100
|
+
readonly adapter: string;
|
|
101
|
+
/** @internal Type-only covariance anchor. */
|
|
102
|
+
readonly _client?: TClient;
|
|
103
|
+
/** @internal Type-only covariance anchor for native platform events. */
|
|
104
|
+
readonly _events?: TEvents;
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the Client from an operation context. Current inbound operations
|
|
107
|
+
* infer their Endpoint; detached operations must provide `endpointKey`.
|
|
108
|
+
*/
|
|
109
|
+
get(context: EndpointClientContext, endpointKey?: string): TClient;
|
|
110
|
+
/** Resolve when this operation belongs to the platform; otherwise return undefined. */
|
|
111
|
+
find(context: EndpointClientContext, endpointKey?: string): TClient | undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Operation-scoped sources that can resolve an Endpoint Client. */
|
|
115
|
+
export interface EndpointClientContext {
|
|
116
|
+
readonly project?: CapabilityContext['project'];
|
|
117
|
+
readonly message?: unknown;
|
|
118
|
+
readonly input?: unknown;
|
|
119
|
+
readonly endpoint?: string;
|
|
120
|
+
readonly origin?: unknown;
|
|
121
|
+
readonly conversation?: unknown;
|
|
122
|
+
readonly $client?: unknown;
|
|
123
|
+
readonly clientAdapter?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Declare the native Client type exported by a platform adapter. */
|
|
127
|
+
export function defineEndpointClient<
|
|
128
|
+
TClient,
|
|
129
|
+
TEvents extends object = Record<string, unknown>,
|
|
130
|
+
>(adapter: string): EndpointClientToken<TClient, TEvents> {
|
|
131
|
+
const normalized = adapter.trim();
|
|
132
|
+
if (!normalized) throw new TypeError('Endpoint Client adapter cannot be empty');
|
|
133
|
+
const token: EndpointClientToken<TClient, TEvents> = {
|
|
134
|
+
$client: endpointClientBrand,
|
|
135
|
+
adapter: normalized,
|
|
136
|
+
get(context, endpointKey) {
|
|
137
|
+
return resolveEndpointClient(context, token, endpointKey);
|
|
138
|
+
},
|
|
139
|
+
find(context, endpointKey) {
|
|
140
|
+
return findEndpointClient(context, token, endpointKey);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
return Object.freeze(token);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve a platform Client from the current generation.
|
|
148
|
+
*
|
|
149
|
+
* The returned object is valid only for the lifetime of `context`; callers
|
|
150
|
+
* must not retain it beyond the current command, handler, tool, task, or
|
|
151
|
+
* schedule operation.
|
|
152
|
+
*/
|
|
153
|
+
function resolveEndpointClient<TClient, TEvents extends object>(
|
|
154
|
+
context: EndpointClientContext,
|
|
155
|
+
token: EndpointClientToken<TClient, TEvents>,
|
|
156
|
+
endpointKey?: string,
|
|
157
|
+
): TClient {
|
|
158
|
+
if (token.$client !== endpointClientBrand) {
|
|
159
|
+
throw new TypeError('Invalid Endpoint Client token');
|
|
160
|
+
}
|
|
161
|
+
const current = currentClientSource(context);
|
|
162
|
+
if (current && '$client' in current) {
|
|
163
|
+
if (endpointKey && !matchesCurrentEndpoint(current, endpointKey)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Endpoint Client ${endpointKey} does not match the current operation Endpoint`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (current.clientAdapter && current.clientAdapter !== token.adapter) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Endpoint Client ${token.adapter} cannot access ${current.clientAdapter}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return current.$client as TClient;
|
|
174
|
+
}
|
|
175
|
+
const resolvedEndpointKey = endpointKey ?? endpointKeyFromContext(context);
|
|
176
|
+
if (!resolvedEndpointKey) {
|
|
177
|
+
throw new Error('Detached Endpoint Client access requires an explicit endpoint key');
|
|
178
|
+
}
|
|
179
|
+
if (!context.project) {
|
|
180
|
+
throw new Error('Endpoint Client access requires a generation operation context');
|
|
181
|
+
}
|
|
182
|
+
const projection = context.project<unknown>(adapterFeatureId);
|
|
183
|
+
if (!isAdapterIndex(projection)) {
|
|
184
|
+
throw new Error('Adapter Feature projection is not installed');
|
|
185
|
+
}
|
|
186
|
+
return projection.client<TClient>(token.adapter, resolvedEndpointKey);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface CurrentClientSource {
|
|
190
|
+
readonly endpointId?: string;
|
|
191
|
+
readonly clientAdapter?: string;
|
|
192
|
+
readonly conversation?: unknown;
|
|
193
|
+
readonly $client?: unknown;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function findEndpointClient<TClient, TEvents extends object>(
|
|
197
|
+
context: EndpointClientContext,
|
|
198
|
+
token: EndpointClientToken<TClient, TEvents>,
|
|
199
|
+
endpointKey?: string,
|
|
200
|
+
): TClient | undefined {
|
|
201
|
+
const current = currentClientSource(context);
|
|
202
|
+
if (current && '$client' in current) {
|
|
203
|
+
if (current.clientAdapter && current.clientAdapter !== token.adapter) return undefined;
|
|
204
|
+
if (endpointKey && !matchesCurrentEndpoint(current, endpointKey)) return undefined;
|
|
205
|
+
try {
|
|
206
|
+
return current.$client as TClient;
|
|
207
|
+
} catch {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const resolvedEndpointKey = endpointKey ?? endpointKeyFromContext(context);
|
|
212
|
+
if (!resolvedEndpointKey || !context.project) return undefined;
|
|
213
|
+
const projection = context.project<unknown>(adapterFeatureId);
|
|
214
|
+
if (!isAdapterIndex(projection)) return undefined;
|
|
215
|
+
return projection.findClient<TClient>(token.adapter, resolvedEndpointKey);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function currentClientSource(context: EndpointClientContext): CurrentClientSource | undefined {
|
|
219
|
+
for (const candidate of [context, context.message, context.input]) {
|
|
220
|
+
if (candidate && typeof candidate === 'object'
|
|
221
|
+
&& ('$client' in candidate || 'clientAdapter' in candidate)) {
|
|
222
|
+
return candidate as CurrentClientSource;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function matchesCurrentEndpoint(source: CurrentClientSource, endpointKey: string): boolean {
|
|
229
|
+
const candidates = [source.endpointId, conversationEndpointId(source.conversation)]
|
|
230
|
+
.filter((value): value is string => typeof value === 'string' && value.length > 0);
|
|
231
|
+
return candidates.length === 0 || candidates.includes(endpointKey);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function endpointKeyFromContext(context: EndpointClientContext): string | undefined {
|
|
235
|
+
if (typeof context.endpoint === 'string' && context.endpoint.length > 0) return context.endpoint;
|
|
236
|
+
const origin = context.origin as { readonly kind?: unknown; readonly endpoint?: unknown } | undefined;
|
|
237
|
+
if (origin?.kind === 'im' && typeof origin.endpoint === 'string' && origin.endpoint.length > 0) {
|
|
238
|
+
return origin.endpoint;
|
|
239
|
+
}
|
|
240
|
+
return conversationEndpointId(context.conversation)
|
|
241
|
+
?? conversationEndpointId((context.input as { readonly conversation?: unknown } | undefined)?.conversation);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function conversationEndpointId(value: unknown): string | undefined {
|
|
245
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
246
|
+
const endpoint = (value as { readonly endpoint?: unknown }).endpoint;
|
|
247
|
+
if (!endpoint || typeof endpoint !== 'object') return undefined;
|
|
248
|
+
const id = (endpoint as { readonly id?: unknown }).id;
|
|
249
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
250
|
+
}
|
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
|
*/
|
|
@@ -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';
|