@proj-airi/server-sdk 0.10.2 → 0.11.0
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/dist/index.d.mts +120 -67
- package/dist/index.mjs +387 -374
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,31 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClientConnector, ClientConnector as ClientConnector$1, ClientEvents } from "@proj-airi/better-ws";
|
|
2
|
+
import { ContextUpdateStrategy, ExtensionIdentity, ExtensionModuleIdentity, MessageHeartbeat, ModuleConfigSchema, ModuleDependency, ModulePermissionDeclaration, ProtocolEvents, WebSocketBaseEvent, WebSocketEvent, WebSocketEventOptionalSource, WebSocketEventSource, WebSocketEvents } from "@proj-airi/server-shared/types";
|
|
2
3
|
export * from "@proj-airi/server-shared/types";
|
|
3
4
|
|
|
4
|
-
//#region src/websocket-like.d.ts
|
|
5
|
-
interface WebSocketMessageEventLike<T = string> {
|
|
6
|
-
data: T;
|
|
7
|
-
}
|
|
8
|
-
interface WebSocketErrorEventLike {
|
|
9
|
-
error?: Error | unknown;
|
|
10
|
-
}
|
|
11
|
-
interface WebSocketLike {
|
|
12
|
-
readonly readyState: number;
|
|
13
|
-
onopen?: (event?: unknown) => void;
|
|
14
|
-
onmessage?: (event: WebSocketMessageEventLike) => void;
|
|
15
|
-
onerror?: (event: WebSocketErrorEventLike | unknown) => void;
|
|
16
|
-
onclose?: (event?: unknown) => void;
|
|
17
|
-
send: (data: string | ArrayBufferLike | ArrayBufferView) => void;
|
|
18
|
-
close: (code?: number, reason?: string) => void;
|
|
19
|
-
ping?: () => void;
|
|
20
|
-
pong?: () => void;
|
|
21
|
-
}
|
|
22
|
-
interface WebSocketLikeConstructor {
|
|
23
|
-
readonly OPEN: number;
|
|
24
|
-
readonly CLOSING: number;
|
|
25
|
-
readonly CLOSED: number;
|
|
26
|
-
new (url: string): WebSocketLike;
|
|
27
|
-
}
|
|
28
|
-
//#endregion
|
|
29
5
|
//#region src/client.d.ts
|
|
30
6
|
type ClientStatus = 'idle' | 'connecting' | 'authenticating' | 'announcing' | 'ready' | 'reconnecting' | 'closing' | 'closed' | 'failed';
|
|
31
7
|
interface ClientHeartbeatOptions {
|
|
@@ -45,13 +21,20 @@ interface ClientOptions<C = undefined> {
|
|
|
45
21
|
url?: string;
|
|
46
22
|
name: string;
|
|
47
23
|
token?: string;
|
|
48
|
-
|
|
24
|
+
connector?: ClientConnector$1<WebSocketEvent<C>>;
|
|
25
|
+
/**
|
|
26
|
+
* Selects the connection handshake owned by this client.
|
|
27
|
+
*
|
|
28
|
+
* @default 'module'
|
|
29
|
+
*/
|
|
30
|
+
handshake?: 'module' | 'manual';
|
|
49
31
|
connectTimeoutMs?: number;
|
|
50
32
|
possibleEvents?: Array<keyof WebSocketEvents<C>>;
|
|
51
|
-
|
|
33
|
+
extension?: ExtensionIdentity;
|
|
34
|
+
identity?: ExtensionModuleIdentity;
|
|
52
35
|
dependencies?: ModuleDependency[];
|
|
53
36
|
configSchema?: ModuleConfigSchema;
|
|
54
|
-
heartbeat?: ClientHeartbeatOptions;
|
|
37
|
+
heartbeat?: false | ClientHeartbeatOptions;
|
|
55
38
|
autoConnect?: boolean;
|
|
56
39
|
autoReconnect?: boolean;
|
|
57
40
|
maxReconnectAttempts?: number;
|
|
@@ -62,24 +45,16 @@ interface ClientOptions<C = undefined> {
|
|
|
62
45
|
onAnyMessage?: (data: WebSocketEvent<C>) => void;
|
|
63
46
|
onAnySend?: (data: WebSocketEvent<C>) => void;
|
|
64
47
|
}
|
|
48
|
+
/** Wraps a text websocket connector with AIRI protocol serialization. */
|
|
49
|
+
declare function createTextProtocolConnector<C = undefined>(textConnector: ClientConnector$1<string>): ClientConnector$1<WebSocketEvent<C>>;
|
|
65
50
|
declare class Client<C = undefined> {
|
|
66
|
-
private websocket?;
|
|
67
|
-
private shouldClose;
|
|
68
|
-
private connectTask?;
|
|
69
|
-
private heartbeatTimer?;
|
|
70
|
-
private lastPingAt;
|
|
71
|
-
private lastReadAt;
|
|
72
|
-
private reconnectAttempts;
|
|
73
|
-
private pendingReconnect;
|
|
74
|
-
private connectionAttempt?;
|
|
75
|
-
private failureReason?;
|
|
76
|
-
private status;
|
|
77
|
-
private readonly identity;
|
|
78
|
-
private readonly heartbeat;
|
|
79
|
-
private readonly websocketConstructor;
|
|
80
51
|
private readonly opts;
|
|
52
|
+
private readonly transport;
|
|
81
53
|
private readonly eventListeners;
|
|
82
54
|
private readonly stateListeners;
|
|
55
|
+
private status;
|
|
56
|
+
private connectTask?;
|
|
57
|
+
private failureReason?;
|
|
83
58
|
constructor(options: ClientOptions<C>);
|
|
84
59
|
get connectionStatus(): ClientStatus;
|
|
85
60
|
get isReady(): boolean;
|
|
@@ -93,33 +68,111 @@ declare class Client<C = undefined> {
|
|
|
93
68
|
offEvent<E extends keyof WebSocketEvents<C>>(event: E, callback?: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): void;
|
|
94
69
|
send(data: WebSocketEventOptionalSource<C>): boolean;
|
|
95
70
|
sendOrThrow(data: WebSocketEventOptionalSource<C>): void;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
private
|
|
99
|
-
private
|
|
100
|
-
private
|
|
101
|
-
private
|
|
102
|
-
private
|
|
103
|
-
private
|
|
104
|
-
private
|
|
105
|
-
private getReconnectDelay;
|
|
71
|
+
close(code?: number, reason?: string): void;
|
|
72
|
+
private createReconnectOptions;
|
|
73
|
+
private createHeartbeatOptions;
|
|
74
|
+
private prepareProtocolConnection;
|
|
75
|
+
private waitForManualReconnectHandshake;
|
|
76
|
+
private consumePrepareMessage;
|
|
77
|
+
private handleMessage;
|
|
78
|
+
private handleTransportStateChange;
|
|
79
|
+
private mapTransportStatus;
|
|
106
80
|
private transitionTo;
|
|
107
81
|
private waitForConnection;
|
|
108
|
-
private
|
|
109
|
-
private
|
|
110
|
-
private
|
|
111
|
-
private
|
|
112
|
-
private
|
|
113
|
-
private isSelfAnnouncement;
|
|
114
|
-
private dispatchMessage;
|
|
82
|
+
private waitForReady;
|
|
83
|
+
private errorFromServerEvent;
|
|
84
|
+
private normalizeError;
|
|
85
|
+
private isSelfModuleAnnouncement;
|
|
86
|
+
private hasSelfModuleInRegistrySync;
|
|
115
87
|
private createPayload;
|
|
116
|
-
private startHeartbeat;
|
|
117
|
-
private stopHeartbeat;
|
|
118
|
-
private sendNativeHeartbeat;
|
|
119
|
-
private sendHeartbeatPing;
|
|
120
|
-
private sendHeartbeatPong;
|
|
121
|
-
private reconnectAfterProtocolError;
|
|
122
88
|
}
|
|
89
|
+
declare function createClient<C = undefined>(options: ClientOptions<C>): Client<C>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/codec.d.ts
|
|
92
|
+
/** Options for websocket message validation failures. */
|
|
93
|
+
interface InvalidMessageErrorOptions {
|
|
94
|
+
/** Original parser or validator failure that made the websocket message unusable. */
|
|
95
|
+
cause?: unknown;
|
|
96
|
+
/** Parsed candidate event when available; otherwise the original websocket text. */
|
|
97
|
+
source?: unknown;
|
|
98
|
+
}
|
|
99
|
+
/** Error thrown when websocket text cannot be parsed as an AIRI event envelope. */
|
|
100
|
+
declare class InvalidMessageError extends Error {
|
|
101
|
+
readonly source?: unknown;
|
|
102
|
+
constructor(options?: InvalidMessageErrorOptions);
|
|
103
|
+
}
|
|
104
|
+
/** Parses one AIRI websocket protocol event from SuperJSON or plain JSON text. */
|
|
105
|
+
declare function parseEvent<C = undefined>(text: string): WebSocketEvent<C>;
|
|
106
|
+
/** Serializes one AIRI websocket protocol event with SuperJSON. */
|
|
107
|
+
declare function stringifyEvent<C = undefined>(event: WebSocketBaseEvent<string, unknown> | WebSocketEvent<C>): string;
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/extension-peer.d.ts
|
|
110
|
+
/**
|
|
111
|
+
* Describes the client operations required by {@link WebSocketExtensionPeer}.
|
|
112
|
+
*
|
|
113
|
+
* @param C - Optional custom protocol event map carried by the websocket client.
|
|
114
|
+
*/
|
|
115
|
+
interface ExtensionPeerClient<C = undefined> {
|
|
116
|
+
connect: (options?: ConnectOptions) => Promise<void>;
|
|
117
|
+
send: (data: WebSocketEventOptionalSource<C>) => boolean;
|
|
118
|
+
sendOrThrow: (data: WebSocketEventOptionalSource<C>) => void;
|
|
119
|
+
close: () => void;
|
|
120
|
+
onEvent?: <E extends keyof WebSocketEvents<C>>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>) => () => void;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Describes one module announcement emitted through a websocket extension peer.
|
|
124
|
+
*
|
|
125
|
+
* @param C - Optional custom protocol event map used for possible event declarations.
|
|
126
|
+
*/
|
|
127
|
+
interface AnnounceExtensionModuleInput<C = undefined> {
|
|
128
|
+
/** Stable module id within the owning extension session. */
|
|
129
|
+
id: string;
|
|
130
|
+
/** Human-readable module name used by registry and diagnostics. */
|
|
131
|
+
name: string;
|
|
132
|
+
/** Protocol events this module may emit or handle. */
|
|
133
|
+
possibleEvents?: Array<keyof ProtocolEvents<C>>;
|
|
134
|
+
/** Runtime permissions requested by this module. */
|
|
135
|
+
permissions?: ModulePermissionDeclaration;
|
|
136
|
+
/** Optional configuration schema understood by the module. */
|
|
137
|
+
configSchema?: ModuleConfigSchema;
|
|
138
|
+
/** Other modules or capabilities this module expects to exist. */
|
|
139
|
+
dependencies?: ModuleDependency[];
|
|
140
|
+
/** Optional labels for routing, diagnostics, or inspector views. */
|
|
141
|
+
labels?: Record<string, string>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Options for creating a websocket-backed extension peer. Supplying `client` lets
|
|
145
|
+
* tests and embedding runtimes provide their own protocol client implementation.
|
|
146
|
+
*
|
|
147
|
+
* @param C - Optional custom protocol event map carried by the websocket client.
|
|
148
|
+
*/
|
|
149
|
+
interface WebSocketExtensionPeerOptions<C = undefined> {
|
|
150
|
+
extension: ExtensionIdentity;
|
|
151
|
+
client?: ExtensionPeerClient<C>;
|
|
152
|
+
clientOptions?: Omit<ClientOptions<C>, 'name' | 'identity'>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Provides extension-level protocol helpers over a server-sdk protocol client.
|
|
156
|
+
*/
|
|
157
|
+
declare class WebSocketExtensionPeer<C = undefined> {
|
|
158
|
+
private readonly client;
|
|
159
|
+
private readonly extension;
|
|
160
|
+
constructor(options: WebSocketExtensionPeerOptions<C>);
|
|
161
|
+
connect(options?: ConnectOptions): Promise<void>;
|
|
162
|
+
authenticatePeer(input?: {
|
|
163
|
+
token?: string;
|
|
164
|
+
peerId?: string;
|
|
165
|
+
}): void;
|
|
166
|
+
announceExtension(input?: {
|
|
167
|
+
permissions?: ModulePermissionDeclaration;
|
|
168
|
+
}): void;
|
|
169
|
+
announceModule(input: AnnounceExtensionModuleInput<C>): void;
|
|
170
|
+
send(data: WebSocketEventOptionalSource<C>): boolean;
|
|
171
|
+
onEvent<E extends keyof WebSocketEvents<C>>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): () => void;
|
|
172
|
+
close(): void;
|
|
173
|
+
}
|
|
174
|
+
/** Creates a websocket extension peer over a server-sdk protocol client. */
|
|
175
|
+
declare function createWebSocketExtensionPeer<C = undefined>(options: WebSocketExtensionPeerOptions<C>): WebSocketExtensionPeer<C>;
|
|
123
176
|
//#endregion
|
|
124
|
-
export { Client, ClientHeartbeatOptions, ClientOptions, ClientStateChangeContext, ClientStatus, ConnectOptions, ContextUpdateStrategy,
|
|
177
|
+
export { AnnounceExtensionModuleInput, Client, type ClientConnector, type ClientEvents, ClientHeartbeatOptions, ClientOptions, ClientStateChangeContext, ClientStatus, ConnectOptions, ContextUpdateStrategy, ExtensionPeerClient, InvalidMessageError, InvalidMessageErrorOptions, WebSocketEventSource, WebSocketExtensionPeer, WebSocketExtensionPeerOptions, createClient, createTextProtocolConnector, createWebSocketExtensionPeer, parseEvent, stringifyEvent };
|
|
125
178
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,58 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { errorMessageFrom } from "@moeru/std";
|
|
2
|
+
import { createClient as createClient$1 } from "@proj-airi/better-ws";
|
|
3
|
+
import { createCrossWsConnector } from "@proj-airi/better-ws/client/crossws";
|
|
4
4
|
import { isTerminalAuthenticationServerErrorMessage, parseServerErrorMessage } from "@proj-airi/server-shared";
|
|
5
5
|
import { ContextUpdateStrategy, MessageHeartbeat, MessageHeartbeatKind, WebSocketEventSource } from "@proj-airi/server-shared/types";
|
|
6
|
+
import { parse, stringify } from "superjson";
|
|
7
|
+
import { check, objectWithRest, pipe, safeParse, string, unknown } from "valibot";
|
|
8
|
+
//#region src/codec.ts
|
|
9
|
+
const invalidMessage = "Invalid AIRI websocket message.";
|
|
10
|
+
const eventDataSchema = pipe(unknown(), check((value) => Boolean(value) && typeof value === "object" && !Array.isArray(value), "Expected event data to be a non-array object."));
|
|
11
|
+
const eventEnvelopeSchema = objectWithRest({
|
|
12
|
+
type: string(),
|
|
13
|
+
data: eventDataSchema
|
|
14
|
+
}, unknown());
|
|
15
|
+
/** Error thrown when websocket text cannot be parsed as an AIRI event envelope. */
|
|
16
|
+
var InvalidMessageError = class extends Error {
|
|
17
|
+
source;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
super(invalidMessage, { cause: options.cause });
|
|
20
|
+
this.name = "InvalidMessageError";
|
|
21
|
+
this.source = options.source;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** Parses one AIRI websocket protocol event from SuperJSON or plain JSON text. */
|
|
25
|
+
function parseEvent(text) {
|
|
26
|
+
let superJsonParsed;
|
|
27
|
+
let superJsonError;
|
|
28
|
+
try {
|
|
29
|
+
superJsonParsed = parse(text);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
superJsonError = error;
|
|
32
|
+
}
|
|
33
|
+
const potentialEvent = superJsonParsed && typeof superJsonParsed === "object" && "type" in superJsonParsed ? superJsonParsed : parsePlainJson(text, superJsonError);
|
|
34
|
+
const result = safeParse(eventEnvelopeSchema, potentialEvent);
|
|
35
|
+
if (!result.success) throw new InvalidMessageError({
|
|
36
|
+
cause: result.issues,
|
|
37
|
+
source: potentialEvent
|
|
38
|
+
});
|
|
39
|
+
return potentialEvent;
|
|
40
|
+
}
|
|
41
|
+
/** Serializes one AIRI websocket protocol event with SuperJSON. */
|
|
42
|
+
function stringifyEvent(event) {
|
|
43
|
+
return stringify(event);
|
|
44
|
+
}
|
|
45
|
+
function parsePlainJson(text, superJsonError) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
} catch (jsonError) {
|
|
49
|
+
throw new InvalidMessageError({
|
|
50
|
+
cause: superJsonError ?? jsonError,
|
|
51
|
+
source: text
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
6
56
|
//#region src/client.ts
|
|
7
57
|
function createInstanceId() {
|
|
8
58
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -10,19 +60,8 @@ function createInstanceId() {
|
|
|
10
60
|
function createEventId() {
|
|
11
61
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
12
62
|
}
|
|
13
|
-
function createDeferredPromise() {
|
|
14
|
-
let resolve;
|
|
15
|
-
let reject;
|
|
16
|
-
return {
|
|
17
|
-
promise: new Promise((innerResolve, innerReject) => {
|
|
18
|
-
resolve = innerResolve;
|
|
19
|
-
reject = innerReject;
|
|
20
|
-
}),
|
|
21
|
-
reject,
|
|
22
|
-
resolve
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
63
|
function normalizeHeartbeatOptions(heartbeat) {
|
|
64
|
+
if (heartbeat === false) return false;
|
|
26
65
|
const readTimeout = heartbeat?.readTimeout ?? 3e4;
|
|
27
66
|
const pingInterval = heartbeat?.pingInterval ?? Math.max(1e3, Math.floor(readTimeout / 2));
|
|
28
67
|
return {
|
|
@@ -31,55 +70,95 @@ function normalizeHeartbeatOptions(heartbeat) {
|
|
|
31
70
|
message: heartbeat?.message ?? MessageHeartbeat.Ping
|
|
32
71
|
};
|
|
33
72
|
}
|
|
73
|
+
/** Wraps a text websocket connector with AIRI protocol serialization. */
|
|
74
|
+
function createTextProtocolConnector(textConnector) {
|
|
75
|
+
return { async connect(events) {
|
|
76
|
+
const connection = await textConnector.connect({
|
|
77
|
+
message(text) {
|
|
78
|
+
try {
|
|
79
|
+
events.message(parseEvent(text));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
events.error(error);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
close: (details) => events.close(details),
|
|
85
|
+
error: (error) => events.error(error)
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
send: (message) => connection.send(stringifyEvent(message)),
|
|
89
|
+
close: (code, reason) => connection.close?.(code, reason),
|
|
90
|
+
ping: connection.ping,
|
|
91
|
+
pong: connection.pong
|
|
92
|
+
};
|
|
93
|
+
} };
|
|
94
|
+
}
|
|
95
|
+
function createDefaultProtocolConnector(url) {
|
|
96
|
+
return createTextProtocolConnector(createCrossWsConnector({ url }));
|
|
97
|
+
}
|
|
98
|
+
function normalizeOptions(options) {
|
|
99
|
+
const url = options.url ?? "ws://localhost:6121/ws";
|
|
100
|
+
const extension = options.extension ?? { id: options.name };
|
|
101
|
+
const identity = options.identity ?? {
|
|
102
|
+
id: createInstanceId(),
|
|
103
|
+
extension
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
url,
|
|
107
|
+
name: options.name,
|
|
108
|
+
token: options.token,
|
|
109
|
+
connector: options.connector ?? createDefaultProtocolConnector(url),
|
|
110
|
+
handshake: options.handshake ?? "module",
|
|
111
|
+
connectTimeoutMs: options.connectTimeoutMs ?? 15e3,
|
|
112
|
+
possibleEvents: options.possibleEvents ?? [],
|
|
113
|
+
extension,
|
|
114
|
+
identity,
|
|
115
|
+
dependencies: options.dependencies ?? [],
|
|
116
|
+
configSchema: options.configSchema,
|
|
117
|
+
heartbeat: normalizeHeartbeatOptions(options.heartbeat),
|
|
118
|
+
autoConnect: options.autoConnect ?? true,
|
|
119
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
120
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? -1,
|
|
121
|
+
onError: options.onError ?? (() => {}),
|
|
122
|
+
onClose: options.onClose ?? (() => {}),
|
|
123
|
+
onReady: options.onReady ?? (() => {}),
|
|
124
|
+
onStateChange: options.onStateChange ?? (() => {}),
|
|
125
|
+
onAnyMessage: options.onAnyMessage ?? (() => {}),
|
|
126
|
+
onAnySend: options.onAnySend ?? (() => {})
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function createConnectionTimeoutError(timeout) {
|
|
130
|
+
return /* @__PURE__ */ new Error(`Connection timed out after ${timeout}ms`);
|
|
131
|
+
}
|
|
132
|
+
function createAbortError() {
|
|
133
|
+
return /* @__PURE__ */ new Error("Connection aborted");
|
|
134
|
+
}
|
|
34
135
|
var Client = class {
|
|
35
|
-
websocket;
|
|
36
|
-
shouldClose = false;
|
|
37
|
-
connectTask;
|
|
38
|
-
heartbeatTimer;
|
|
39
|
-
lastPingAt = 0;
|
|
40
|
-
lastReadAt = 0;
|
|
41
|
-
reconnectAttempts = 0;
|
|
42
|
-
pendingReconnect = false;
|
|
43
|
-
connectionAttempt;
|
|
44
|
-
failureReason;
|
|
45
|
-
status = "idle";
|
|
46
|
-
identity;
|
|
47
|
-
heartbeat;
|
|
48
|
-
websocketConstructor;
|
|
49
136
|
opts;
|
|
137
|
+
transport;
|
|
50
138
|
eventListeners = /* @__PURE__ */ new Map();
|
|
51
139
|
stateListeners = /* @__PURE__ */ new Set();
|
|
140
|
+
status = "idle";
|
|
141
|
+
connectTask;
|
|
142
|
+
failureReason;
|
|
52
143
|
constructor(options) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
onStateChange: () => {},
|
|
72
|
-
autoConnect: true,
|
|
73
|
-
autoReconnect: true,
|
|
74
|
-
maxReconnectAttempts: -1,
|
|
75
|
-
...clientOptions,
|
|
76
|
-
heartbeat,
|
|
77
|
-
identity
|
|
78
|
-
};
|
|
79
|
-
this.identity = identity;
|
|
80
|
-
this.heartbeat = heartbeat;
|
|
81
|
-
this.websocketConstructor = websocketConstructor ?? NativeWebSocket;
|
|
82
|
-
if (this.opts.autoConnect) this.connect();
|
|
144
|
+
this.opts = normalizeOptions(options);
|
|
145
|
+
this.transport = createClient$1({
|
|
146
|
+
connector: this.opts.connector,
|
|
147
|
+
reconnect: this.createReconnectOptions(),
|
|
148
|
+
heartbeat: this.createHeartbeatOptions(),
|
|
149
|
+
prepare: (context) => this.prepareProtocolConnection(context)
|
|
150
|
+
});
|
|
151
|
+
this.transport.onMessage(({ message }) => {
|
|
152
|
+
this.handleMessage(message);
|
|
153
|
+
});
|
|
154
|
+
this.transport.onStateChange(({ previousState, state }) => {
|
|
155
|
+
this.handleTransportStateChange(previousState, state);
|
|
156
|
+
});
|
|
157
|
+
if (this.opts.autoConnect) this.connect().catch((error) => {
|
|
158
|
+
const normalized = this.normalizeError(error, "Failed to connect websocket client");
|
|
159
|
+
this.failureReason = normalized;
|
|
160
|
+
this.opts.onError(normalized);
|
|
161
|
+
});
|
|
83
162
|
}
|
|
84
163
|
get connectionStatus() {
|
|
85
164
|
return this.status;
|
|
@@ -88,16 +167,16 @@ var Client = class {
|
|
|
88
167
|
return this.status === "ready";
|
|
89
168
|
}
|
|
90
169
|
get isSocketOpen() {
|
|
91
|
-
return this.
|
|
170
|
+
return this.transport.state === "open" || this.transport.state === "preparing" || this.transport.state === "ready";
|
|
92
171
|
}
|
|
93
172
|
get lastError() {
|
|
94
173
|
return this.failureReason;
|
|
95
174
|
}
|
|
96
175
|
async connect(options) {
|
|
97
|
-
if (this.shouldClose) throw new Error("Client is closed");
|
|
98
176
|
if (this.status === "ready") return;
|
|
99
|
-
if (this.connectTask) return this.waitForConnection(this.
|
|
100
|
-
this.connectTask
|
|
177
|
+
if (!this.connectTask && this.transport.state === "reconnecting") return this.waitForConnection(this.waitForReady(), options);
|
|
178
|
+
if (!this.connectTask && (this.transport.state === "open" || this.transport.state === "preparing")) return this.waitForConnection(this.waitForReady(), options);
|
|
179
|
+
if (!this.connectTask) this.connectTask = this.transport.connect().finally(() => {
|
|
101
180
|
this.connectTask = void 0;
|
|
102
181
|
});
|
|
103
182
|
return this.waitForConnection(this.connectTask, options);
|
|
@@ -131,167 +210,162 @@ var Client = class {
|
|
|
131
210
|
if (callback) {
|
|
132
211
|
listeners.delete(callback);
|
|
133
212
|
if (!listeners.size) this.eventListeners.delete(event);
|
|
134
|
-
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.eventListeners.delete(event);
|
|
135
216
|
}
|
|
136
217
|
send(data) {
|
|
137
|
-
if (!this.isSocketOpen || !this.websocket) return false;
|
|
138
218
|
const payload = this.createPayload(data);
|
|
139
|
-
this.
|
|
140
|
-
this.
|
|
219
|
+
if (!this.transport.send(payload).ok) return false;
|
|
220
|
+
this.opts.onAnySend(payload);
|
|
141
221
|
return true;
|
|
142
222
|
}
|
|
143
223
|
sendOrThrow(data) {
|
|
144
224
|
if (!this.send(data)) throw new Error(`Client is not connected, current status: ${this.status}`);
|
|
145
225
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.websocket.send(data);
|
|
149
|
-
return true;
|
|
226
|
+
close(code, reason) {
|
|
227
|
+
this.transport.close(code, reason);
|
|
150
228
|
}
|
|
151
|
-
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.pendingReconnect = false;
|
|
164
|
-
while (!this.shouldClose) {
|
|
165
|
-
const reconnecting = this.reconnectAttempts > 0;
|
|
166
|
-
this.transitionTo(reconnecting ? "reconnecting" : "connecting");
|
|
167
|
-
try {
|
|
168
|
-
await this.connectOnce();
|
|
169
|
-
this.reconnectAttempts = 0;
|
|
170
|
-
return;
|
|
171
|
-
} catch (error) {
|
|
172
|
-
const normalizedError = error instanceof Error ? error : new Error(errorMessageFrom(error) ?? "Failed to connect websocket client");
|
|
173
|
-
this.failureReason = normalizedError;
|
|
174
|
-
this.opts.onError?.(normalizedError);
|
|
175
|
-
if (this.shouldClose) throw normalizedError;
|
|
176
|
-
if (isTerminalAuthenticationServerErrorMessage(normalizedError.message)) {
|
|
177
|
-
this.transitionTo("failed");
|
|
178
|
-
throw normalizedError;
|
|
179
|
-
}
|
|
180
|
-
if (!this.opts.autoReconnect && reconnecting) {
|
|
181
|
-
this.transitionTo("failed");
|
|
182
|
-
throw normalizedError;
|
|
183
|
-
}
|
|
184
|
-
if (!this.canRetry()) {
|
|
185
|
-
this.transitionTo("failed");
|
|
186
|
-
throw normalizedError;
|
|
187
|
-
}
|
|
188
|
-
const delay = this.getReconnectDelay(this.reconnectAttempts);
|
|
189
|
-
this.reconnectAttempts += 1;
|
|
190
|
-
await sleep(delay);
|
|
229
|
+
createReconnectOptions() {
|
|
230
|
+
if (!this.opts.autoReconnect) return false;
|
|
231
|
+
return {
|
|
232
|
+
retries: (attempt, error) => {
|
|
233
|
+
if (isTerminalAuthenticationServerErrorMessage(this.normalizeError(error, "Failed to connect websocket client").message)) return false;
|
|
234
|
+
return this.opts.maxReconnectAttempts === -1 || attempt <= this.opts.maxReconnectAttempts;
|
|
235
|
+
},
|
|
236
|
+
onFailed: (error) => {
|
|
237
|
+
const normalized = this.normalizeError(error, "Failed to connect websocket client");
|
|
238
|
+
if (this.failureReason === normalized) return;
|
|
239
|
+
this.failureReason = normalized;
|
|
240
|
+
this.opts.onError(normalized);
|
|
191
241
|
}
|
|
192
|
-
}
|
|
193
|
-
throw new Error("Client is closed");
|
|
194
|
-
}
|
|
195
|
-
connectOnce() {
|
|
196
|
-
const WebSocketConstructor = this.websocketConstructor;
|
|
197
|
-
const ws = new WebSocketConstructor(this.opts.url);
|
|
198
|
-
this.websocket = ws;
|
|
199
|
-
this.lastReadAt = Date.now();
|
|
200
|
-
this.lastPingAt = 0;
|
|
201
|
-
const deferred = createDeferredPromise();
|
|
202
|
-
const attempt = {
|
|
203
|
-
announced: false,
|
|
204
|
-
authenticated: !this.opts.token,
|
|
205
|
-
promise: deferred.promise,
|
|
206
|
-
reject: deferred.reject,
|
|
207
|
-
resolve: deferred.resolve,
|
|
208
|
-
socket: ws
|
|
209
|
-
};
|
|
210
|
-
this.connectionAttempt = attempt;
|
|
211
|
-
const isCurrentSocket = () => this.websocket === ws;
|
|
212
|
-
const connectTimeoutMs = this.opts.connectTimeoutMs;
|
|
213
|
-
const connectTimer = setTimeout(() => {
|
|
214
|
-
if (ws.readyState === WebSocket.OPEN) return;
|
|
215
|
-
ws.close();
|
|
216
|
-
deferred.reject(/* @__PURE__ */ new Error(`Connection timeout after ${connectTimeoutMs}ms`));
|
|
217
|
-
}, connectTimeoutMs);
|
|
218
|
-
const clearConnectTimer = () => {
|
|
219
|
-
clearTimeout(connectTimer);
|
|
220
242
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
243
|
+
}
|
|
244
|
+
createHeartbeatOptions() {
|
|
245
|
+
if (!this.opts.heartbeat) return false;
|
|
246
|
+
return {
|
|
247
|
+
mode: "message",
|
|
248
|
+
interval: this.opts.heartbeat.pingInterval,
|
|
249
|
+
timeout: this.opts.heartbeat.readTimeout,
|
|
250
|
+
message: () => this.createPayload({
|
|
251
|
+
type: "transport:connection:heartbeat",
|
|
252
|
+
data: {
|
|
253
|
+
kind: MessageHeartbeatKind.Ping,
|
|
254
|
+
message: this.opts.heartbeat ? this.opts.heartbeat.message : MessageHeartbeat.Ping,
|
|
255
|
+
at: Date.now()
|
|
256
|
+
}
|
|
257
|
+
})
|
|
234
258
|
};
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
this.
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
}
|
|
260
|
+
async prepareProtocolConnection(context) {
|
|
261
|
+
if (this.opts.handshake === "manual") {
|
|
262
|
+
if (!context.reconnecting) return;
|
|
263
|
+
this.transitionTo("authenticating");
|
|
264
|
+
await this.waitForManualReconnectHandshake(context);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (this.opts.token) {
|
|
268
|
+
this.transitionTo("authenticating");
|
|
269
|
+
context.send(this.createPayload({
|
|
270
|
+
type: "module:authenticate",
|
|
271
|
+
data: { token: this.opts.token }
|
|
272
|
+
}));
|
|
273
|
+
await context.waitFor((message) => {
|
|
274
|
+
const result = this.consumePrepareMessage(message);
|
|
275
|
+
if (result.error) throw result.error;
|
|
276
|
+
return message.type === "module:authenticated" && message.data.authenticated === true;
|
|
277
|
+
}, { timeout: this.opts.connectTimeoutMs });
|
|
278
|
+
}
|
|
279
|
+
this.transitionTo("announcing");
|
|
280
|
+
context.send(this.createPayload({
|
|
281
|
+
type: "extension:module:announce",
|
|
282
|
+
data: {
|
|
283
|
+
name: this.opts.name,
|
|
284
|
+
identity: this.opts.identity,
|
|
285
|
+
possibleEvents: this.opts.possibleEvents,
|
|
286
|
+
configSchema: this.opts.configSchema,
|
|
287
|
+
dependencies: this.opts.dependencies
|
|
247
288
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
289
|
+
}));
|
|
290
|
+
await context.waitFor((message) => {
|
|
291
|
+
const result = this.consumePrepareMessage(message);
|
|
292
|
+
if (result.error) throw result.error;
|
|
293
|
+
return result.ready;
|
|
294
|
+
}, { timeout: this.opts.connectTimeoutMs });
|
|
295
|
+
}
|
|
296
|
+
async waitForManualReconnectHandshake(context) {
|
|
297
|
+
await context.waitFor((message) => {
|
|
298
|
+
const result = this.consumePrepareMessage(message);
|
|
299
|
+
if (result.error) throw result.error;
|
|
300
|
+
return message.type === "peer:authenticated" && message.data.authenticated === true;
|
|
301
|
+
}, { timeout: this.opts.connectTimeoutMs });
|
|
302
|
+
this.transitionTo("announcing");
|
|
303
|
+
await context.waitFor((message) => {
|
|
304
|
+
const result = this.consumePrepareMessage(message);
|
|
305
|
+
if (result.error) throw result.error;
|
|
306
|
+
return message.type === "extension:announced" && message.data.identity.id === this.opts.extension.id;
|
|
307
|
+
}, { timeout: this.opts.connectTimeoutMs });
|
|
308
|
+
}
|
|
309
|
+
consumePrepareMessage(message) {
|
|
310
|
+
const error = this.errorFromServerEvent(message);
|
|
311
|
+
if (error) {
|
|
312
|
+
this.failureReason = error;
|
|
313
|
+
return {
|
|
314
|
+
ready: false,
|
|
315
|
+
error
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (message.type === "extension:module:announced") return { ready: this.isSelfModuleAnnouncement(message) };
|
|
319
|
+
if (message.type === "registry:modules:sync") return { ready: this.hasSelfModuleInRegistrySync(message) };
|
|
320
|
+
return { ready: false };
|
|
321
|
+
}
|
|
322
|
+
async handleMessage(message) {
|
|
323
|
+
this.opts.onAnyMessage(message);
|
|
324
|
+
const error = this.errorFromServerEvent(message);
|
|
325
|
+
if (error) {
|
|
326
|
+
this.failureReason = error;
|
|
327
|
+
this.opts.onError(error);
|
|
328
|
+
}
|
|
329
|
+
if (message.type === "transport:connection:heartbeat" && message.data.kind === MessageHeartbeatKind.Ping) this.send({
|
|
330
|
+
type: "transport:connection:heartbeat",
|
|
331
|
+
data: {
|
|
332
|
+
kind: MessageHeartbeatKind.Pong,
|
|
333
|
+
message: MessageHeartbeat.Pong,
|
|
334
|
+
at: Date.now()
|
|
262
335
|
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (!
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
336
|
+
});
|
|
337
|
+
const listeners = this.eventListeners.get(message.type);
|
|
338
|
+
if (!listeners?.size) return;
|
|
339
|
+
const results = await Promise.allSettled(Array.from(listeners).map((listener) => Promise.resolve(listener(message))));
|
|
340
|
+
for (const result of results) if (result.status === "rejected") {
|
|
341
|
+
this.failureReason = this.normalizeError(result.reason, "Client event listener failed");
|
|
342
|
+
this.opts.onError(result.reason);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
handleTransportStateChange(previousState, state) {
|
|
346
|
+
if (state === "ready") {
|
|
347
|
+
this.transitionTo("ready");
|
|
348
|
+
this.opts.onReady();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const nextStatus = this.mapTransportStatus(state);
|
|
352
|
+
if (!nextStatus) return;
|
|
353
|
+
this.transitionTo(nextStatus);
|
|
354
|
+
if (previousState === "ready" && state === "reconnecting") this.opts.onClose();
|
|
355
|
+
else if (state === "closed") this.opts.onClose();
|
|
356
|
+
}
|
|
357
|
+
mapTransportStatus(state) {
|
|
358
|
+
switch (state) {
|
|
359
|
+
case "idle":
|
|
360
|
+
case "connecting":
|
|
361
|
+
case "reconnecting":
|
|
362
|
+
case "closing":
|
|
363
|
+
case "closed":
|
|
364
|
+
case "failed": return state;
|
|
365
|
+
case "open":
|
|
366
|
+
case "preparing":
|
|
367
|
+
case "ready": return;
|
|
368
|
+
}
|
|
295
369
|
}
|
|
296
370
|
transitionTo(status) {
|
|
297
371
|
if (this.status === status) return;
|
|
@@ -301,26 +375,24 @@ var Client = class {
|
|
|
301
375
|
previousStatus,
|
|
302
376
|
status
|
|
303
377
|
};
|
|
304
|
-
this.opts.onStateChange
|
|
378
|
+
this.opts.onStateChange(context);
|
|
305
379
|
for (const listener of this.stateListeners) listener(context);
|
|
306
380
|
}
|
|
307
381
|
async waitForConnection(connectPromise, options) {
|
|
308
382
|
if (!options?.timeout && !options?.abortSignal) return connectPromise;
|
|
309
383
|
const timeout = options?.timeout;
|
|
310
|
-
if (typeof timeout !== "undefined" && timeout <= 0) throw
|
|
384
|
+
if (typeof timeout !== "undefined" && timeout <= 0) throw createConnectionTimeoutError(timeout);
|
|
311
385
|
const abortSignal = options?.abortSignal;
|
|
312
|
-
if (abortSignal?.aborted) throw
|
|
386
|
+
if (abortSignal?.aborted) throw createAbortError();
|
|
313
387
|
let timeoutHandle;
|
|
314
388
|
let removeAbortListener;
|
|
315
389
|
try {
|
|
316
390
|
await Promise.race([connectPromise, new Promise((_, reject) => {
|
|
317
391
|
if (typeof timeout !== "undefined") timeoutHandle = setTimeout(() => {
|
|
318
|
-
reject(
|
|
392
|
+
reject(createConnectionTimeoutError(timeout));
|
|
319
393
|
}, timeout);
|
|
320
394
|
if (abortSignal) {
|
|
321
|
-
const onAbort = () =>
|
|
322
|
-
reject(/* @__PURE__ */ new Error("Connection aborted"));
|
|
323
|
-
};
|
|
395
|
+
const onAbort = () => reject(createAbortError());
|
|
324
396
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
325
397
|
removeAbortListener = () => abortSignal.removeEventListener("abort", onAbort);
|
|
326
398
|
}
|
|
@@ -330,187 +402,128 @@ var Client = class {
|
|
|
330
402
|
removeAbortListener?.();
|
|
331
403
|
}
|
|
332
404
|
}
|
|
333
|
-
|
|
334
|
-
this.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
dependencies: this.opts.dependencies,
|
|
341
|
-
configSchema: this.opts.configSchema
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
tryAuthenticate() {
|
|
346
|
-
if (!this.opts.token) return;
|
|
347
|
-
this.sendOrThrow({
|
|
348
|
-
type: "module:authenticate",
|
|
349
|
-
data: { token: this.opts.token }
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
async handleMessage(event) {
|
|
353
|
-
this.lastReadAt = Date.now();
|
|
354
|
-
try {
|
|
355
|
-
const data = this.parseMessage(event.data);
|
|
356
|
-
this.opts.onAnyMessage?.(data);
|
|
357
|
-
await this.handleControlMessage(data);
|
|
358
|
-
await this.dispatchMessage(data);
|
|
359
|
-
} catch (error) {
|
|
360
|
-
const normalizedError = error instanceof Error ? error : new Error(errorMessageFrom(error) ?? "Failed to handle websocket message");
|
|
361
|
-
this.opts.onError?.(normalizedError);
|
|
362
|
-
if (this.connectionAttempt && this.status !== "ready") this.handleSocketFailure(normalizedError);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
parseMessage(raw) {
|
|
366
|
-
try {
|
|
367
|
-
const parsed = superjson.parse(raw);
|
|
368
|
-
if (parsed && typeof parsed === "object" && "type" in parsed) return parsed;
|
|
369
|
-
} catch {}
|
|
370
|
-
const parsed = JSON.parse(raw);
|
|
371
|
-
if (!parsed || typeof parsed !== "object" || !("type" in parsed)) throw new Error("Received invalid websocket message");
|
|
372
|
-
return parsed;
|
|
373
|
-
}
|
|
374
|
-
async handleControlMessage(data) {
|
|
375
|
-
switch (data.type) {
|
|
376
|
-
case "error": {
|
|
377
|
-
const message = data.data?.message;
|
|
378
|
-
if (!message || typeof message !== "string") return;
|
|
379
|
-
const parsedServerError = parseServerErrorMessage(message);
|
|
380
|
-
if (parsedServerError.authentication) {
|
|
381
|
-
const error = new Error(message);
|
|
382
|
-
if (parsedServerError.terminal) {
|
|
383
|
-
this.shouldClose = true;
|
|
384
|
-
this.handleSocketFailure(error);
|
|
385
|
-
this.transitionTo("failed");
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
await this.reconnectAfterProtocolError(error);
|
|
405
|
+
waitForReady() {
|
|
406
|
+
if (this.status === "ready") return Promise.resolve();
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
const dispose = this.onConnectionStateChange(({ status }) => {
|
|
409
|
+
if (status === "ready") {
|
|
410
|
+
dispose();
|
|
411
|
+
resolve();
|
|
389
412
|
return;
|
|
390
413
|
}
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
case "module:authenticated":
|
|
395
|
-
if (data.data.authenticated) {
|
|
396
|
-
if (!this.connectionAttempt || this.connectionAttempt.authenticated) return;
|
|
397
|
-
this.connectionAttempt.authenticated = true;
|
|
398
|
-
this.transitionTo("announcing");
|
|
399
|
-
this.tryAnnounce();
|
|
400
|
-
return;
|
|
414
|
+
if (status === "failed" || status === "closed") {
|
|
415
|
+
dispose();
|
|
416
|
+
reject(this.failureReason ?? /* @__PURE__ */ new Error(`Client connection ended with status: ${status}`));
|
|
401
417
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (!this.isSelfAnnouncement(data)) return;
|
|
405
|
-
if (this.status === "ready") return;
|
|
406
|
-
if (this.connectionAttempt) this.connectionAttempt.announced = true;
|
|
407
|
-
this.reconnectAttempts = 0;
|
|
408
|
-
this.transitionTo("ready");
|
|
409
|
-
this.resolveAttempt();
|
|
410
|
-
this.opts.onReady?.();
|
|
411
|
-
return;
|
|
412
|
-
case "registry:modules:sync":
|
|
413
|
-
if (this.status !== "announcing" || !this.connectionAttempt) return;
|
|
414
|
-
if (!(data.data?.modules ?? []).some((m) => m.name === this.opts.name && m.identity?.id === this.identity.id)) return;
|
|
415
|
-
if (this.connectionAttempt) this.connectionAttempt.announced = true;
|
|
416
|
-
this.reconnectAttempts = 0;
|
|
417
|
-
this.transitionTo("ready");
|
|
418
|
-
this.resolveAttempt();
|
|
419
|
-
this.opts.onReady?.();
|
|
420
|
-
return;
|
|
421
|
-
case "transport:connection:heartbeat": if (data.data.kind === MessageHeartbeatKind.Ping) this.sendHeartbeatPong();
|
|
422
|
-
}
|
|
418
|
+
});
|
|
419
|
+
});
|
|
423
420
|
}
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
errorFromServerEvent(message) {
|
|
422
|
+
if (message.type !== "error") return;
|
|
423
|
+
const errorMessage = typeof message.data.message === "string" ? message.data.message : "Unknown server error";
|
|
424
|
+
const parsed = parseServerErrorMessage(errorMessage);
|
|
425
|
+
if (parsed.code === "unknown") return new Error(errorMessage);
|
|
426
|
+
return new Error(parsed.message);
|
|
426
427
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
normalizeError(error, fallback) {
|
|
429
|
+
return error instanceof Error ? error : new Error(errorMessageFrom(error) ?? fallback);
|
|
430
|
+
}
|
|
431
|
+
isSelfModuleAnnouncement(event) {
|
|
432
|
+
return event.data.name === this.opts.name && event.data.identity?.id === this.opts.identity.id;
|
|
433
|
+
}
|
|
434
|
+
hasSelfModuleInRegistrySync(event) {
|
|
435
|
+
return event.data.modules.some((module) => module.name === this.opts.name && module.identity?.id === this.opts.identity.id);
|
|
432
436
|
}
|
|
433
437
|
createPayload(data) {
|
|
434
438
|
return {
|
|
435
439
|
...data,
|
|
436
440
|
metadata: {
|
|
437
|
-
...data
|
|
438
|
-
source: data
|
|
441
|
+
...data.metadata,
|
|
442
|
+
source: data.metadata?.source ?? {
|
|
443
|
+
kind: "plugin",
|
|
444
|
+
...this.opts.identity,
|
|
445
|
+
plugin: { id: this.opts.extension.id }
|
|
446
|
+
},
|
|
439
447
|
event: {
|
|
440
|
-
|
|
441
|
-
|
|
448
|
+
...data.metadata?.event,
|
|
449
|
+
id: data.metadata?.event?.id ?? createEventId()
|
|
442
450
|
}
|
|
443
451
|
}
|
|
444
452
|
};
|
|
445
453
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
454
|
+
};
|
|
455
|
+
function createClient(options) {
|
|
456
|
+
return new Client(options);
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/extension-peer.ts
|
|
460
|
+
/**
|
|
461
|
+
* Provides extension-level protocol helpers over a server-sdk protocol client.
|
|
462
|
+
*/
|
|
463
|
+
var WebSocketExtensionPeer = class {
|
|
464
|
+
client;
|
|
465
|
+
extension;
|
|
466
|
+
constructor(options) {
|
|
467
|
+
this.extension = options.extension;
|
|
468
|
+
this.client = options.client ?? createClient({
|
|
469
|
+
...options.clientOptions,
|
|
470
|
+
name: options.extension.id,
|
|
471
|
+
handshake: "manual",
|
|
472
|
+
autoConnect: options.clientOptions?.autoConnect ?? false,
|
|
473
|
+
autoReconnect: options.clientOptions?.autoReconnect ?? false
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
connect(options) {
|
|
477
|
+
return this.client.connect(options);
|
|
478
|
+
}
|
|
479
|
+
authenticatePeer(input = {}) {
|
|
480
|
+
this.client.sendOrThrow({
|
|
481
|
+
type: "peer:authenticate",
|
|
482
|
+
data: input
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
announceExtension(input = {}) {
|
|
486
|
+
this.client.sendOrThrow({
|
|
487
|
+
type: "extension:announce",
|
|
476
488
|
data: {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
at: Date.now()
|
|
489
|
+
identity: this.extension,
|
|
490
|
+
permissions: input.permissions
|
|
480
491
|
}
|
|
481
492
|
});
|
|
482
|
-
this.sendNativeHeartbeat("ping");
|
|
483
493
|
}
|
|
484
|
-
|
|
485
|
-
this.
|
|
486
|
-
type: "
|
|
494
|
+
announceModule(input) {
|
|
495
|
+
this.client.sendOrThrow({
|
|
496
|
+
type: "extension:module:announce",
|
|
487
497
|
data: {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
498
|
+
name: input.name,
|
|
499
|
+
identity: {
|
|
500
|
+
id: input.id,
|
|
501
|
+
extension: this.extension,
|
|
502
|
+
labels: input.labels
|
|
503
|
+
},
|
|
504
|
+
possibleEvents: input.possibleEvents ?? [],
|
|
505
|
+
permissions: input.permissions,
|
|
506
|
+
configSchema: input.configSchema,
|
|
507
|
+
dependencies: input.dependencies
|
|
491
508
|
}
|
|
492
509
|
});
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
this.
|
|
503
|
-
if (websocket && websocket.readyState !== this.websocketConstructor.CLOSED && websocket.readyState !== this.websocketConstructor.CLOSING) websocket.close();
|
|
504
|
-
if (hadSocket) this.opts.onClose?.();
|
|
505
|
-
if (!this.opts.autoReconnect) {
|
|
506
|
-
this.transitionTo("failed");
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
this.transitionTo("idle");
|
|
510
|
-
this.connect();
|
|
510
|
+
}
|
|
511
|
+
send(data) {
|
|
512
|
+
return this.client.send(data);
|
|
513
|
+
}
|
|
514
|
+
onEvent(event, callback) {
|
|
515
|
+
if (!this.client.onEvent) throw new Error("Wrapped extension peer client does not support event listeners.");
|
|
516
|
+
return this.client.onEvent(event, callback);
|
|
517
|
+
}
|
|
518
|
+
close() {
|
|
519
|
+
this.client.close();
|
|
511
520
|
}
|
|
512
521
|
};
|
|
522
|
+
/** Creates a websocket extension peer over a server-sdk protocol client. */
|
|
523
|
+
function createWebSocketExtensionPeer(options) {
|
|
524
|
+
return new WebSocketExtensionPeer(options);
|
|
525
|
+
}
|
|
513
526
|
//#endregion
|
|
514
|
-
export { Client, ContextUpdateStrategy, WebSocketEventSource };
|
|
527
|
+
export { Client, ContextUpdateStrategy, InvalidMessageError, WebSocketEventSource, WebSocketExtensionPeer, createClient, createTextProtocolConnector, createWebSocketExtensionPeer, parseEvent, stringifyEvent };
|
|
515
528
|
|
|
516
529
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n MetadataEventSource,\n ModuleConfigSchema,\n ModuleDependency,\n WebSocketBaseEvent,\n WebSocketEvent,\n WebSocketEventOptionalSource,\n WebSocketEvents,\n} from '@proj-airi/server-shared/types'\n\nimport type { WebSocketLike, WebSocketLikeConstructor, WebSocketMessageEventLike } from './websocket-like'\n\nimport NativeWebSocket from 'crossws/websocket'\nimport superjson from 'superjson'\n\nimport { errorMessageFrom, sleep } from '@moeru/std'\nimport { isTerminalAuthenticationServerErrorMessage, parseServerErrorMessage } from '@proj-airi/server-shared'\nimport { MessageHeartbeat, MessageHeartbeatKind } from '@proj-airi/server-shared/types'\n\nexport type ClientStatus\n = | 'idle'\n | 'connecting'\n | 'authenticating'\n | 'announcing'\n | 'ready'\n | 'reconnecting'\n | 'closing'\n | 'closed'\n | 'failed'\n\nexport interface ClientHeartbeatOptions {\n pingInterval?: number\n readTimeout?: number\n message?: MessageHeartbeat | string\n}\n\nexport interface ClientStateChangeContext {\n previousStatus: ClientStatus\n status: ClientStatus\n}\n\nexport interface ConnectOptions {\n abortSignal?: AbortSignal\n timeout?: number\n}\n\nexport interface ClientOptions<C = undefined> {\n url?: string\n name: string\n token?: string\n websocketConstructor?: WebSocketLikeConstructor\n\n connectTimeoutMs?: number\n possibleEvents?: Array<keyof WebSocketEvents<C>>\n identity?: MetadataEventSource\n dependencies?: ModuleDependency[]\n configSchema?: ModuleConfigSchema\n heartbeat?: ClientHeartbeatOptions\n\n autoConnect?: boolean\n autoReconnect?: boolean\n maxReconnectAttempts?: number\n\n onError?: (error: unknown) => void\n onClose?: () => void\n onReady?: () => void\n onStateChange?: (context: ClientStateChangeContext) => void\n\n onAnyMessage?: (data: WebSocketEvent<C>) => void\n onAnySend?: (data: WebSocketEvent<C>) => void\n}\n\ninterface ConnectionAttempt {\n announced: boolean\n authenticated: boolean\n promise: Promise<void>\n reject: (error: Error) => void\n resolve: () => void\n socket: WebSocketLike\n}\n\nfunction createInstanceId() {\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n}\n\nfunction createEventId() {\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n}\n\nfunction createDeferredPromise() {\n let resolve!: () => void\n let reject!: (error: Error) => void\n\n const promise = new Promise<void>((innerResolve, innerReject) => {\n resolve = innerResolve\n reject = innerReject\n })\n\n return { promise, reject, resolve }\n}\n\nfunction normalizeHeartbeatOptions(heartbeat?: ClientHeartbeatOptions): Required<ClientHeartbeatOptions> {\n const readTimeout = heartbeat?.readTimeout ?? 30_000\n const pingInterval = heartbeat?.pingInterval ?? Math.max(1_000, Math.floor(readTimeout / 2))\n\n return {\n readTimeout,\n pingInterval: Math.min(pingInterval, readTimeout),\n message: heartbeat?.message ?? MessageHeartbeat.Ping,\n }\n}\n\nexport class Client<C = undefined> {\n private websocket?: WebSocketLike\n private shouldClose = false\n private connectTask?: Promise<void>\n private heartbeatTimer?: ReturnType<typeof setInterval>\n private lastPingAt = 0\n private lastReadAt = 0\n private reconnectAttempts = 0\n private pendingReconnect = false\n private connectionAttempt?: ConnectionAttempt\n private failureReason?: Error\n private status: ClientStatus = 'idle'\n private readonly identity: MetadataEventSource\n private readonly heartbeat: Required<ClientHeartbeatOptions>\n private readonly websocketConstructor: WebSocketLikeConstructor\n\n private readonly opts:\n & Required<Omit<ClientOptions<C>, 'token' | 'heartbeat' | 'websocketConstructor' | 'configSchema'>>\n & Pick<ClientOptions<C>, 'token' | 'heartbeat' | 'configSchema'>\n\n private readonly eventListeners = new Map<\n keyof WebSocketEvents<C>,\n Set<(data: WebSocketBaseEvent<any, any>) => void | Promise<void>>\n >()\n\n private readonly stateListeners = new Set<(context: ClientStateChangeContext) => void>()\n\n constructor(options: ClientOptions<C>) {\n const { websocketConstructor, ...clientOptions } = options\n const identity = options.identity ?? {\n kind: 'plugin',\n plugin: { id: options.name },\n id: createInstanceId(),\n }\n\n const heartbeat = normalizeHeartbeatOptions(options.heartbeat)\n\n this.opts = {\n url: 'ws://localhost:6121/ws',\n connectTimeoutMs: 15_000,\n onAnyMessage: () => {},\n onAnySend: () => {},\n possibleEvents: [],\n dependencies: [],\n configSchema: undefined,\n onError: () => {},\n onClose: () => {},\n onReady: () => {},\n onStateChange: () => {},\n autoConnect: true,\n autoReconnect: true,\n maxReconnectAttempts: -1,\n ...clientOptions,\n heartbeat,\n identity,\n }\n\n this.identity = identity\n this.heartbeat = heartbeat\n this.websocketConstructor = websocketConstructor ?? (NativeWebSocket as unknown as WebSocketLikeConstructor)\n\n if (this.opts.autoConnect) {\n void this.connect()\n }\n }\n\n get connectionStatus() {\n return this.status\n }\n\n get isReady() {\n return this.status === 'ready'\n }\n\n get isSocketOpen() {\n return this.websocket?.readyState === this.websocketConstructor.OPEN\n }\n\n get lastError() {\n return this.failureReason\n }\n\n async connect(options?: ConnectOptions) {\n if (this.shouldClose) {\n throw new Error('Client is closed')\n }\n\n if (this.status === 'ready') {\n return\n }\n\n if (this.connectTask) {\n return this.waitForConnection(this.connectTask, options)\n }\n\n this.connectTask = this.runConnectLoop().finally(() => {\n this.connectTask = undefined\n })\n\n return this.waitForConnection(this.connectTask, options)\n }\n\n ready(options?: ConnectOptions) {\n return this.connect(options)\n }\n\n ensureConnected(options?: ConnectOptions) {\n return this.connect(options)\n }\n\n onConnectionStateChange(callback: (context: ClientStateChangeContext) => void): () => void {\n this.stateListeners.add(callback)\n\n return () => {\n this.stateListeners.delete(callback)\n }\n }\n\n onEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ): () => void {\n let listeners = this.eventListeners.get(event)\n if (!listeners) {\n listeners = new Set()\n this.eventListeners.set(event, listeners)\n }\n\n listeners.add(callback as any)\n\n return () => {\n this.offEvent(event, callback)\n }\n }\n\n offEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback?: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ): void {\n const listeners = this.eventListeners.get(event)\n if (!listeners) {\n return\n }\n\n if (callback) {\n listeners.delete(callback as any)\n if (!listeners.size) {\n this.eventListeners.delete(event)\n }\n }\n else {\n this.eventListeners.delete(event)\n }\n }\n\n send(data: WebSocketEventOptionalSource<C>): boolean {\n if (!this.isSocketOpen || !this.websocket) {\n return false\n }\n\n const payload = this.createPayload(data)\n this.opts.onAnySend?.(payload)\n this.websocket.send(superjson.stringify(payload))\n\n return true\n }\n\n sendOrThrow(data: WebSocketEventOptionalSource<C>): void {\n if (!this.send(data)) {\n throw new Error(`Client is not connected, current status: ${this.status}`)\n }\n }\n\n sendRaw(data: string | ArrayBufferLike | ArrayBufferView): boolean {\n if (!this.isSocketOpen || !this.websocket) {\n return false\n }\n\n this.websocket.send(data)\n return true\n }\n\n close(): void {\n this.shouldClose = true\n this.pendingReconnect = false\n this.transitionTo('closing')\n this.stopHeartbeat()\n this.rejectAttempt(new Error('Client closed'))\n\n const websocket = this.websocket\n this.websocket = undefined\n\n if (websocket && websocket.readyState !== this.websocketConstructor.CLOSED && websocket.readyState !== this.websocketConstructor.CLOSING) {\n websocket.close()\n }\n\n this.transitionTo('closed')\n }\n\n private async runConnectLoop() {\n this.pendingReconnect = false\n\n while (!this.shouldClose) {\n const reconnecting = this.reconnectAttempts > 0\n this.transitionTo(reconnecting ? 'reconnecting' : 'connecting')\n\n try {\n await this.connectOnce()\n this.reconnectAttempts = 0\n return\n }\n catch (error) {\n const normalizedError = error instanceof Error ? error : new Error(errorMessageFrom(error) ?? 'Failed to connect websocket client')\n this.failureReason = normalizedError\n this.opts.onError?.(normalizedError)\n\n if (this.shouldClose) {\n throw normalizedError\n }\n\n if (isTerminalAuthenticationServerErrorMessage(normalizedError.message)) {\n this.transitionTo('failed')\n throw normalizedError\n }\n\n if (!this.opts.autoReconnect && reconnecting) {\n this.transitionTo('failed')\n throw normalizedError\n }\n\n if (!this.canRetry()) {\n this.transitionTo('failed')\n throw normalizedError\n }\n\n const delay = this.getReconnectDelay(this.reconnectAttempts)\n this.reconnectAttempts += 1\n await sleep(delay)\n }\n }\n\n throw new Error('Client is closed')\n }\n\n private connectOnce(): Promise<void> {\n const WebSocketConstructor = this.websocketConstructor\n const ws = new WebSocketConstructor(this.opts.url)\n this.websocket = ws\n this.lastReadAt = Date.now()\n this.lastPingAt = 0\n\n const deferred = createDeferredPromise()\n const attempt: ConnectionAttempt = {\n announced: false,\n authenticated: !this.opts.token,\n promise: deferred.promise,\n reject: deferred.reject,\n resolve: deferred.resolve,\n socket: ws,\n }\n\n this.connectionAttempt = attempt\n\n const isCurrentSocket = () => this.websocket === ws\n const connectTimeoutMs = this.opts.connectTimeoutMs\n const connectTimer = setTimeout(() => {\n if (ws.readyState === WebSocket.OPEN) {\n return\n }\n\n ws.close()\n deferred.reject(new Error(`Connection timeout after ${connectTimeoutMs}ms`))\n }, connectTimeoutMs)\n\n const clearConnectTimer = () => {\n clearTimeout(connectTimer)\n }\n\n ws.onmessage = (event: WebSocketMessageEventLike) => {\n if (!isCurrentSocket()) {\n return\n }\n\n void this.handleMessage(event)\n }\n\n ws.onerror = (event: any) => {\n clearConnectTimer()\n\n if (!isCurrentSocket()) {\n return\n }\n\n const error = event?.error instanceof Error ? event.error : new Error('WebSocket error')\n if (this.connectionAttempt) {\n this.handleSocketFailure(error, ws)\n }\n else {\n this.opts.onError?.(error)\n void this.reconnectAfterProtocolError(error)\n }\n }\n\n ws.onclose = () => {\n clearConnectTimer()\n\n if (!isCurrentSocket()) {\n return\n }\n\n const wasReady = this.status === 'ready'\n this.cleanupSocket(ws)\n this.opts.onClose?.()\n\n if (this.shouldClose) {\n return\n }\n\n if (wasReady && this.opts.autoReconnect) {\n this.pendingReconnect = true\n this.transitionTo('idle')\n void this.connect()\n return\n }\n\n this.rejectAttempt(new Error('WebSocket closed'))\n }\n\n ws.onopen = () => {\n clearConnectTimer()\n\n if (!isCurrentSocket()) {\n return\n }\n\n this.startHeartbeat()\n\n if (this.opts.token) {\n attempt.authenticated = false\n this.transitionTo('authenticating')\n this.tryAuthenticate()\n }\n else {\n attempt.authenticated = true\n this.transitionTo('announcing')\n this.tryAnnounce()\n }\n }\n\n return attempt.promise\n }\n\n private handleSocketFailure(error: Error, socket?: WebSocketLike) {\n if (socket && this.websocket !== socket) {\n return\n }\n\n const currentSocket = socket ?? this.websocket\n this.cleanupSocket(socket)\n\n if (currentSocket && currentSocket.readyState !== this.websocketConstructor.CLOSED && currentSocket.readyState !== this.websocketConstructor.CLOSING) {\n currentSocket.close()\n }\n\n this.rejectAttempt(error)\n }\n\n private cleanupSocket(socket?: WebSocketLike) {\n if (socket && this.websocket !== socket) {\n return\n }\n\n this.stopHeartbeat()\n\n if (!socket || this.websocket === socket) {\n this.websocket = undefined\n }\n }\n\n private rejectAttempt(error: Error) {\n if (!this.connectionAttempt) {\n return\n }\n\n const attempt = this.connectionAttempt\n this.connectionAttempt = undefined\n attempt.reject(error)\n }\n\n private resolveAttempt() {\n if (!this.connectionAttempt) {\n return\n }\n\n const attempt = this.connectionAttempt\n this.connectionAttempt = undefined\n attempt.resolve()\n }\n\n private canRetry() {\n return this.opts.maxReconnectAttempts === -1 || this.reconnectAttempts < this.opts.maxReconnectAttempts\n }\n\n private getReconnectDelay(attempts: number) {\n return Math.min(2 ** attempts * 1_000, 30_000)\n }\n\n private transitionTo(status: ClientStatus) {\n if (this.status === status) {\n return\n }\n\n const previousStatus = this.status\n this.status = status\n const context = { previousStatus, status }\n\n this.opts.onStateChange?.(context)\n\n for (const listener of this.stateListeners) {\n listener(context)\n }\n }\n\n private async waitForConnection(connectPromise: Promise<void>, options?: ConnectOptions) {\n if (!options?.timeout && !options?.abortSignal) {\n return connectPromise\n }\n\n const timeout = options?.timeout\n if (typeof timeout !== 'undefined' && timeout <= 0) {\n throw new Error(`Connection timed out after ${timeout}ms`)\n }\n\n const abortSignal = options?.abortSignal\n if (abortSignal?.aborted) {\n throw new Error('Connection aborted')\n }\n\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n let removeAbortListener: (() => void) | undefined\n\n try {\n await Promise.race([\n connectPromise,\n new Promise<void>((_, reject) => {\n if (typeof timeout !== 'undefined') {\n timeoutHandle = setTimeout(() => {\n reject(new Error(`Connection timed out after ${timeout}ms`))\n }, timeout)\n }\n\n if (abortSignal) {\n const onAbort = () => {\n reject(new Error('Connection aborted'))\n }\n\n abortSignal.addEventListener('abort', onAbort, { once: true })\n removeAbortListener = () => abortSignal.removeEventListener('abort', onAbort)\n }\n }),\n ])\n }\n finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n\n removeAbortListener?.()\n }\n }\n\n private tryAnnounce() {\n this.sendOrThrow({\n type: 'module:announce',\n data: {\n name: this.opts.name,\n identity: this.identity,\n possibleEvents: this.opts.possibleEvents,\n dependencies: this.opts.dependencies,\n configSchema: this.opts.configSchema,\n },\n })\n }\n\n private tryAuthenticate() {\n if (!this.opts.token) {\n return\n }\n\n this.sendOrThrow({\n type: 'module:authenticate',\n data: { token: this.opts.token },\n })\n }\n\n private async handleMessage(event: WebSocketMessageEventLike) {\n this.lastReadAt = Date.now()\n\n try {\n const data = this.parseMessage(event.data as string)\n this.opts.onAnyMessage?.(data)\n\n await this.handleControlMessage(data)\n await this.dispatchMessage(data)\n }\n catch (error) {\n const normalizedError = error instanceof Error ? error : new Error(errorMessageFrom(error) ?? 'Failed to handle websocket message')\n this.opts.onError?.(normalizedError)\n\n if (this.connectionAttempt && this.status !== 'ready') {\n this.handleSocketFailure(normalizedError)\n }\n }\n }\n\n private parseMessage(raw: string): WebSocketEvent<C> {\n try {\n const parsed = superjson.parse<WebSocketEvent<C> | undefined>(raw)\n if (parsed && typeof parsed === 'object' && 'type' in parsed) {\n return parsed\n }\n }\n catch {\n // Try standard JSON next.\n }\n\n const parsed = JSON.parse(raw) as WebSocketEvent<C>\n if (!parsed || typeof parsed !== 'object' || !('type' in parsed)) {\n throw new Error('Received invalid websocket message')\n }\n\n return parsed\n }\n\n private async handleControlMessage(data: WebSocketEvent<C>) {\n switch (data.type) {\n case 'error': {\n const message = data.data?.message\n if (!message || typeof message !== 'string') {\n return\n }\n\n const parsedServerError = parseServerErrorMessage(message)\n if (parsedServerError.authentication) {\n const error = new Error(message)\n if (parsedServerError.terminal) {\n this.shouldClose = true\n this.handleSocketFailure(error)\n this.transitionTo('failed')\n return\n }\n\n await this.reconnectAfterProtocolError(error)\n return\n }\n\n if (parsedServerError.code !== 'unknown') {\n throw new Error(parsedServerError.message)\n }\n\n throw new Error(message)\n }\n\n case 'module:authenticated': {\n if (data.data.authenticated) {\n if (!this.connectionAttempt || this.connectionAttempt.authenticated) {\n return\n }\n\n this.connectionAttempt.authenticated = true\n this.transitionTo('announcing')\n this.tryAnnounce()\n return\n }\n\n throw new Error('Authentication failed')\n }\n\n case 'module:announced': {\n if (!this.isSelfAnnouncement(data)) {\n return\n }\n\n if (this.status === 'ready') {\n return\n }\n\n if (this.connectionAttempt) {\n this.connectionAttempt.announced = true\n }\n\n this.reconnectAttempts = 0\n this.transitionTo('ready')\n this.resolveAttempt()\n this.opts.onReady?.()\n return\n }\n\n case 'registry:modules:sync': {\n // Fallback: If the status is stuck at 'announcing' but the sync already contains this module,\n // it means the announce succeeded; the server simply didn't send back 'module:announced'\n if (this.status !== 'announcing' || !this.connectionAttempt) {\n return\n }\n\n const modules = (data.data as any)?.modules as Array<{\n name: string\n identity?: { id?: string }\n }> ?? []\n\n const selfRegistered = modules.some(\n m => m.name === this.opts.name\n && m.identity?.id === this.identity.id,\n )\n\n if (!selfRegistered) {\n return\n }\n\n if (this.connectionAttempt) {\n this.connectionAttempt.announced = true\n }\n\n this.reconnectAttempts = 0\n this.transitionTo('ready')\n this.resolveAttempt()\n this.opts.onReady?.()\n return\n }\n\n case 'transport:connection:heartbeat': {\n if (data.data.kind === MessageHeartbeatKind.Ping) {\n this.sendHeartbeatPong()\n }\n }\n }\n }\n\n private isSelfAnnouncement(event: WebSocketBaseEvent<'module:announced', WebSocketEvents<C>['module:announced']>) {\n return event.data.name === this.opts.name && event.data.identity?.id === this.identity.id\n }\n\n private async dispatchMessage(data: WebSocketEvent<C>) {\n const listeners = this.eventListeners.get(data.type)\n if (!listeners?.size) {\n return\n }\n\n const results = await Promise.allSettled(\n Array.from(listeners).map(listener => Promise.resolve(listener(data as any))),\n )\n\n for (const result of results) {\n if (result.status === 'rejected') {\n this.opts.onError?.(result.reason)\n }\n }\n }\n\n private createPayload(data: WebSocketEventOptionalSource<C>) {\n return {\n ...data,\n metadata: {\n ...data?.metadata,\n source: data?.metadata?.source ?? this.identity,\n event: {\n id: data?.metadata?.event?.id ?? createEventId(),\n ...data?.metadata?.event,\n },\n },\n } as WebSocketEvent<C>\n }\n\n private startHeartbeat() {\n if (!this.heartbeat.readTimeout || !this.heartbeat.pingInterval) {\n return\n }\n\n this.stopHeartbeat()\n this.lastReadAt = Date.now()\n this.lastPingAt = 0\n\n const interval = Math.max(1_000, Math.min(this.heartbeat.pingInterval, Math.floor(this.heartbeat.readTimeout / 2)))\n this.heartbeatTimer = setInterval(() => {\n if (!this.isSocketOpen) {\n return\n }\n\n const now = Date.now()\n if (now - this.lastReadAt > this.heartbeat.readTimeout) {\n void this.reconnectAfterProtocolError(new Error(`Read timeout after ${this.heartbeat.readTimeout}ms`))\n return\n }\n\n if (now - this.lastPingAt >= this.heartbeat.pingInterval) {\n this.sendHeartbeatPing()\n }\n }, interval)\n }\n\n private stopHeartbeat() {\n if (!this.heartbeatTimer) {\n return\n }\n\n clearInterval(this.heartbeatTimer)\n this.heartbeatTimer = undefined\n }\n\n private sendNativeHeartbeat(kind: 'ping' | 'pong') {\n const websocket = this.websocket as WebSocketLike & {\n ping?: () => void\n pong?: () => void\n }\n\n if (kind === 'ping') {\n websocket.ping?.()\n }\n else {\n websocket.pong?.()\n }\n }\n\n private sendHeartbeatPing() {\n this.lastPingAt = Date.now()\n this.send({\n type: 'transport:connection:heartbeat',\n data: {\n kind: MessageHeartbeatKind.Ping,\n message: this.heartbeat.message,\n at: Date.now(),\n },\n })\n this.sendNativeHeartbeat('ping')\n }\n\n private sendHeartbeatPong() {\n this.send({\n type: 'transport:connection:heartbeat',\n data: {\n kind: MessageHeartbeatKind.Pong,\n message: MessageHeartbeat.Pong,\n at: Date.now(),\n },\n })\n this.sendNativeHeartbeat('pong')\n }\n\n private async reconnectAfterProtocolError(error: Error) {\n if (this.shouldClose || this.pendingReconnect) {\n return\n }\n\n this.pendingReconnect = true\n const hadSocket = !!this.websocket\n\n if (!this.connectionAttempt || this.status === 'ready') {\n this.opts.onError?.(error)\n }\n\n const websocket = this.websocket\n this.cleanupSocket(websocket)\n this.rejectAttempt(error)\n\n if (websocket && websocket.readyState !== this.websocketConstructor.CLOSED && websocket.readyState !== this.websocketConstructor.CLOSING) {\n websocket.close()\n }\n\n if (hadSocket) {\n this.opts.onClose?.()\n }\n\n if (!this.opts.autoReconnect) {\n this.transitionTo('failed')\n return\n }\n\n this.transitionTo('idle')\n void this.connect()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["createBetterWsClient"],"sources":["../src/codec.ts","../src/client.ts","../src/extension-peer.ts"],"sourcesContent":["import type { WebSocketBaseEvent, WebSocketEvent } from '@proj-airi/server-shared/types'\n\nimport { parse, stringify } from 'superjson'\nimport { check, objectWithRest, pipe, safeParse, string, unknown } from 'valibot'\n\nconst invalidMessage = 'Invalid AIRI websocket message.'\n\nconst eventDataSchema = pipe(\n unknown(),\n check(\n value => Boolean(value) && typeof value === 'object' && !Array.isArray(value),\n 'Expected event data to be a non-array object.',\n ),\n)\n\nconst eventEnvelopeSchema = objectWithRest({\n type: string(),\n data: eventDataSchema,\n}, unknown())\n\n/** Options for websocket message validation failures. */\nexport interface InvalidMessageErrorOptions {\n /** Original parser or validator failure that made the websocket message unusable. */\n cause?: unknown\n /** Parsed candidate event when available; otherwise the original websocket text. */\n source?: unknown\n}\n\n/** Error thrown when websocket text cannot be parsed as an AIRI event envelope. */\nexport class InvalidMessageError extends Error {\n readonly source?: unknown\n\n constructor(options: InvalidMessageErrorOptions = {}) {\n super(invalidMessage, { cause: options.cause })\n this.name = 'InvalidMessageError'\n this.source = options.source\n }\n}\n\n/** Parses one AIRI websocket protocol event from SuperJSON or plain JSON text. */\nexport function parseEvent<C = undefined>(text: string): WebSocketEvent<C> {\n let superJsonParsed: WebSocketEvent<C> | undefined\n let superJsonError: unknown\n\n try {\n superJsonParsed = parse<WebSocketEvent<C>>(text)\n }\n catch (error) {\n superJsonError = error\n }\n\n const potentialEvent = superJsonParsed && typeof superJsonParsed === 'object' && 'type' in superJsonParsed\n ? superJsonParsed\n : parsePlainJson(text, superJsonError)\n\n const result = safeParse(eventEnvelopeSchema, potentialEvent)\n if (!result.success) {\n throw new InvalidMessageError({ cause: result.issues, source: potentialEvent })\n }\n\n return potentialEvent as WebSocketEvent<C>\n}\n\n/** Serializes one AIRI websocket protocol event with SuperJSON. */\nexport function stringifyEvent<C = undefined>(\n event: WebSocketBaseEvent<string, unknown> | WebSocketEvent<C>,\n) {\n return stringify(event)\n}\n\nfunction parsePlainJson(text: string, superJsonError: unknown): unknown {\n try {\n return JSON.parse(text)\n }\n catch (jsonError) {\n throw new InvalidMessageError({\n cause: superJsonError ?? jsonError,\n source: text,\n })\n }\n}\n","import type {\n Client as BetterWsClient,\n ClientConnector,\n PrepareContext,\n ReconnectOptions,\n} from '@proj-airi/better-ws'\nimport type {\n ExtensionIdentity,\n ExtensionModuleIdentity,\n ModuleConfigSchema,\n ModuleDependency,\n WebSocketBaseEvent,\n WebSocketEvent,\n WebSocketEventOptionalSource,\n WebSocketEvents,\n} from '@proj-airi/server-shared/types'\n\nimport { errorMessageFrom } from '@moeru/std'\nimport { createClient as createBetterWsClient } from '@proj-airi/better-ws'\nimport { createCrossWsConnector } from '@proj-airi/better-ws/client/crossws'\nimport { isTerminalAuthenticationServerErrorMessage, parseServerErrorMessage } from '@proj-airi/server-shared'\nimport { MessageHeartbeat, MessageHeartbeatKind } from '@proj-airi/server-shared/types'\n\nimport { parseEvent, stringifyEvent } from './codec'\n\nexport type { ClientConnector, ClientEvents } from '@proj-airi/better-ws'\n\nexport type ClientStatus\n = | 'idle'\n | 'connecting'\n | 'authenticating'\n | 'announcing'\n | 'ready'\n | 'reconnecting'\n | 'closing'\n | 'closed'\n | 'failed'\n\nexport interface ClientHeartbeatOptions {\n pingInterval?: number\n readTimeout?: number\n message?: MessageHeartbeat | string\n}\n\nexport interface ClientStateChangeContext {\n previousStatus: ClientStatus\n status: ClientStatus\n}\n\nexport interface ConnectOptions {\n abortSignal?: AbortSignal\n timeout?: number\n}\n\nexport interface ClientOptions<C = undefined> {\n url?: string\n name: string\n token?: string\n connector?: ClientConnector<WebSocketEvent<C>>\n /**\n * Selects the connection handshake owned by this client.\n *\n * @default 'module'\n */\n handshake?: 'module' | 'manual'\n\n connectTimeoutMs?: number\n possibleEvents?: Array<keyof WebSocketEvents<C>>\n extension?: ExtensionIdentity\n identity?: ExtensionModuleIdentity\n dependencies?: ModuleDependency[]\n configSchema?: ModuleConfigSchema\n heartbeat?: false | ClientHeartbeatOptions\n\n autoConnect?: boolean\n autoReconnect?: boolean\n maxReconnectAttempts?: number\n\n onError?: (error: unknown) => void\n onClose?: () => void\n onReady?: () => void\n onStateChange?: (context: ClientStateChangeContext) => void\n\n onAnyMessage?: (data: WebSocketEvent<C>) => void\n onAnySend?: (data: WebSocketEvent<C>) => void\n}\n\ninterface NormalizedClientOptions<C> {\n url: string\n name: string\n token?: string\n connector: ClientConnector<WebSocketEvent<C>>\n handshake: 'module' | 'manual'\n connectTimeoutMs: number\n possibleEvents: Array<keyof WebSocketEvents<C>>\n extension: ExtensionIdentity\n identity: ExtensionModuleIdentity\n dependencies: ModuleDependency[]\n configSchema?: ModuleConfigSchema\n heartbeat: false | Required<ClientHeartbeatOptions>\n autoConnect: boolean\n autoReconnect: boolean\n maxReconnectAttempts: number\n onError: (error: unknown) => void\n onClose: () => void\n onReady: () => void\n onStateChange: (context: ClientStateChangeContext) => void\n onAnyMessage: (data: WebSocketEvent<C>) => void\n onAnySend: (data: WebSocketEvent<C>) => void\n}\n\ninterface ProtocolWaitResult {\n ready: boolean\n error?: Error\n}\n\nfunction createInstanceId() {\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n}\n\nfunction createEventId() {\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`\n}\n\nfunction normalizeHeartbeatOptions(heartbeat?: false | ClientHeartbeatOptions): false | Required<ClientHeartbeatOptions> {\n if (heartbeat === false) {\n return false\n }\n\n const readTimeout = heartbeat?.readTimeout ?? 30_000\n const pingInterval = heartbeat?.pingInterval ?? Math.max(1_000, Math.floor(readTimeout / 2))\n\n return {\n readTimeout,\n pingInterval: Math.min(pingInterval, readTimeout),\n message: heartbeat?.message ?? MessageHeartbeat.Ping,\n }\n}\n\n/** Wraps a text websocket connector with AIRI protocol serialization. */\nexport function createTextProtocolConnector<C = undefined>(\n textConnector: ClientConnector<string>,\n): ClientConnector<WebSocketEvent<C>> {\n return {\n async connect(events) {\n const connection = await textConnector.connect({\n message(text) {\n try {\n events.message(parseEvent<C>(text))\n }\n catch (error) {\n events.error(error)\n }\n },\n close: details => events.close(details),\n error: error => events.error(error),\n })\n\n return {\n send: message => connection.send(stringifyEvent(message)),\n close: (code, reason) => connection.close?.(code, reason),\n ping: connection.ping,\n pong: connection.pong,\n }\n },\n }\n}\n\nfunction createDefaultProtocolConnector<C>(url: string): ClientConnector<WebSocketEvent<C>> {\n return createTextProtocolConnector(createCrossWsConnector({ url }))\n}\n\nfunction normalizeOptions<C>(options: ClientOptions<C>): NormalizedClientOptions<C> {\n const url = options.url ?? 'ws://localhost:6121/ws'\n const extension = options.extension ?? { id: options.name }\n const identity = options.identity ?? {\n id: createInstanceId(),\n extension,\n }\n\n return {\n url,\n name: options.name,\n token: options.token,\n connector: options.connector ?? createDefaultProtocolConnector<C>(url),\n handshake: options.handshake ?? 'module',\n connectTimeoutMs: options.connectTimeoutMs ?? 15_000,\n possibleEvents: options.possibleEvents ?? [],\n extension,\n identity,\n dependencies: options.dependencies ?? [],\n configSchema: options.configSchema,\n heartbeat: normalizeHeartbeatOptions(options.heartbeat),\n autoConnect: options.autoConnect ?? true,\n autoReconnect: options.autoReconnect ?? true,\n maxReconnectAttempts: options.maxReconnectAttempts ?? -1,\n onError: options.onError ?? (() => {}),\n onClose: options.onClose ?? (() => {}),\n onReady: options.onReady ?? (() => {}),\n onStateChange: options.onStateChange ?? (() => {}),\n onAnyMessage: options.onAnyMessage ?? (() => {}),\n onAnySend: options.onAnySend ?? (() => {}),\n }\n}\n\nfunction createConnectionTimeoutError(timeout: number) {\n return new Error(`Connection timed out after ${timeout}ms`)\n}\n\nfunction createAbortError() {\n return new Error('Connection aborted')\n}\n\nexport class Client<C = undefined> {\n private readonly opts: NormalizedClientOptions<C>\n private readonly transport: BetterWsClient<WebSocketEvent<C>>\n private readonly eventListeners = new Map<\n keyof WebSocketEvents<C>,\n Set<(data: WebSocketBaseEvent<string, unknown>) => void | Promise<void>>\n >()\n\n private readonly stateListeners = new Set<(context: ClientStateChangeContext) => void>()\n private status: ClientStatus = 'idle'\n private connectTask?: Promise<void>\n private failureReason?: Error\n\n constructor(options: ClientOptions<C>) {\n this.opts = normalizeOptions(options)\n this.transport = createBetterWsClient<WebSocketEvent<C>>({\n connector: this.opts.connector,\n reconnect: this.createReconnectOptions(),\n heartbeat: this.createHeartbeatOptions(),\n prepare: context => this.prepareProtocolConnection(context),\n })\n\n this.transport.onMessage(({ message }) => {\n void this.handleMessage(message)\n })\n this.transport.onStateChange(({ previousState, state }) => {\n this.handleTransportStateChange(previousState, state)\n })\n\n if (this.opts.autoConnect) {\n void this.connect().catch((error) => {\n const normalized = this.normalizeError(error, 'Failed to connect websocket client')\n this.failureReason = normalized\n this.opts.onError(normalized)\n })\n }\n }\n\n get connectionStatus() {\n return this.status\n }\n\n get isReady() {\n return this.status === 'ready'\n }\n\n get isSocketOpen() {\n return this.transport.state === 'open' || this.transport.state === 'preparing' || this.transport.state === 'ready'\n }\n\n get lastError() {\n return this.failureReason\n }\n\n async connect(options?: ConnectOptions) {\n if (this.status === 'ready') {\n return\n }\n\n if (!this.connectTask && this.transport.state === 'reconnecting') {\n return this.waitForConnection(this.waitForReady(), options)\n }\n\n if (!this.connectTask && (this.transport.state === 'open' || this.transport.state === 'preparing')) {\n return this.waitForConnection(this.waitForReady(), options)\n }\n\n if (!this.connectTask) {\n this.connectTask = this.transport.connect().finally(() => {\n this.connectTask = undefined\n })\n }\n\n return this.waitForConnection(this.connectTask, options)\n }\n\n ready(options?: ConnectOptions) {\n return this.connect(options)\n }\n\n ensureConnected(options?: ConnectOptions) {\n return this.connect(options)\n }\n\n onConnectionStateChange(callback: (context: ClientStateChangeContext) => void): () => void {\n this.stateListeners.add(callback)\n\n return () => {\n this.stateListeners.delete(callback)\n }\n }\n\n onEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ): () => void {\n let listeners = this.eventListeners.get(event)\n if (!listeners) {\n listeners = new Set()\n this.eventListeners.set(event, listeners)\n }\n\n listeners.add(callback as (data: WebSocketBaseEvent<string, unknown>) => void | Promise<void>)\n\n return () => {\n this.offEvent(event, callback)\n }\n }\n\n offEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback?: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ): void {\n const listeners = this.eventListeners.get(event)\n if (!listeners) {\n return\n }\n\n if (callback) {\n listeners.delete(callback as (data: WebSocketBaseEvent<string, unknown>) => void | Promise<void>)\n if (!listeners.size) {\n this.eventListeners.delete(event)\n }\n return\n }\n\n this.eventListeners.delete(event)\n }\n\n send(data: WebSocketEventOptionalSource<C>): boolean {\n const payload = this.createPayload(data)\n const result = this.transport.send(payload)\n if (!result.ok) {\n return false\n }\n\n this.opts.onAnySend(payload)\n return true\n }\n\n sendOrThrow(data: WebSocketEventOptionalSource<C>): void {\n if (!this.send(data)) {\n throw new Error(`Client is not connected, current status: ${this.status}`)\n }\n }\n\n close(code?: number, reason?: string): void {\n this.transport.close(code, reason)\n }\n\n private createReconnectOptions(): false | ReconnectOptions {\n if (!this.opts.autoReconnect) {\n return false\n }\n\n return {\n retries: (attempt, error) => {\n const normalized = this.normalizeError(error, 'Failed to connect websocket client')\n if (isTerminalAuthenticationServerErrorMessage(normalized.message)) {\n return false\n }\n\n return this.opts.maxReconnectAttempts === -1 || attempt <= this.opts.maxReconnectAttempts\n },\n onFailed: (error) => {\n const normalized = this.normalizeError(error, 'Failed to connect websocket client')\n if (this.failureReason === normalized) {\n return\n }\n\n this.failureReason = normalized\n this.opts.onError(normalized)\n },\n }\n }\n\n private createHeartbeatOptions() {\n if (!this.opts.heartbeat) {\n return false\n }\n\n return {\n mode: 'message' as const,\n interval: this.opts.heartbeat.pingInterval,\n timeout: this.opts.heartbeat.readTimeout,\n message: () => this.createPayload({\n type: 'transport:connection:heartbeat',\n data: {\n kind: MessageHeartbeatKind.Ping,\n message: this.opts.heartbeat ? this.opts.heartbeat.message : MessageHeartbeat.Ping,\n at: Date.now(),\n },\n } as WebSocketEventOptionalSource<C>),\n }\n }\n\n private async prepareProtocolConnection(context: PrepareContext<WebSocketEvent<C>>): Promise<void> {\n if (this.opts.handshake === 'manual') {\n if (!context.reconnecting) {\n return\n }\n\n this.transitionTo('authenticating')\n await this.waitForManualReconnectHandshake(context)\n return\n }\n\n if (this.opts.token) {\n this.transitionTo('authenticating')\n context.send(this.createPayload({\n type: 'module:authenticate',\n data: { token: this.opts.token },\n } as WebSocketEventOptionalSource<C>))\n\n await context.waitFor((message) => {\n const result = this.consumePrepareMessage(message)\n if (result.error) {\n throw result.error\n }\n\n return message.type === 'module:authenticated' && message.data.authenticated === true\n }, { timeout: this.opts.connectTimeoutMs })\n }\n\n this.transitionTo('announcing')\n context.send(this.createPayload({\n type: 'extension:module:announce',\n data: {\n name: this.opts.name,\n identity: this.opts.identity,\n possibleEvents: this.opts.possibleEvents,\n configSchema: this.opts.configSchema,\n dependencies: this.opts.dependencies,\n },\n } as WebSocketEventOptionalSource<C>))\n\n await context.waitFor((message) => {\n const result = this.consumePrepareMessage(message)\n if (result.error) {\n throw result.error\n }\n\n return result.ready\n }, { timeout: this.opts.connectTimeoutMs })\n }\n\n private async waitForManualReconnectHandshake(context: PrepareContext<WebSocketEvent<C>>): Promise<void> {\n await context.waitFor((message) => {\n const result = this.consumePrepareMessage(message)\n if (result.error) {\n throw result.error\n }\n\n return message.type === 'peer:authenticated' && message.data.authenticated === true\n }, { timeout: this.opts.connectTimeoutMs })\n\n this.transitionTo('announcing')\n\n await context.waitFor((message) => {\n const result = this.consumePrepareMessage(message)\n if (result.error) {\n throw result.error\n }\n\n return message.type === 'extension:announced'\n && message.data.identity.id === this.opts.extension.id\n }, { timeout: this.opts.connectTimeoutMs })\n }\n\n private consumePrepareMessage(message: WebSocketEvent<C>): ProtocolWaitResult {\n const error = this.errorFromServerEvent(message)\n if (error) {\n this.failureReason = error\n return { ready: false, error }\n }\n\n if (message.type === 'extension:module:announced') {\n return { ready: this.isSelfModuleAnnouncement(message) }\n }\n\n if (message.type === 'registry:modules:sync') {\n return { ready: this.hasSelfModuleInRegistrySync(message) }\n }\n\n return { ready: false }\n }\n\n private async handleMessage(message: WebSocketEvent<C>): Promise<void> {\n this.opts.onAnyMessage(message)\n\n const error = this.errorFromServerEvent(message)\n if (error) {\n this.failureReason = error\n this.opts.onError(error)\n }\n\n if (message.type === 'transport:connection:heartbeat' && message.data.kind === MessageHeartbeatKind.Ping) {\n this.send({\n type: 'transport:connection:heartbeat',\n data: {\n kind: MessageHeartbeatKind.Pong,\n message: MessageHeartbeat.Pong,\n at: Date.now(),\n },\n } as WebSocketEventOptionalSource<C>)\n }\n\n const listeners = this.eventListeners.get(message.type)\n if (!listeners?.size) {\n return\n }\n\n const results = await Promise.allSettled(\n Array.from(listeners).map(listener => Promise.resolve(listener(message as WebSocketBaseEvent<string, unknown>))),\n )\n\n for (const result of results) {\n if (result.status === 'rejected') {\n this.failureReason = this.normalizeError(result.reason, 'Client event listener failed')\n this.opts.onError(result.reason)\n }\n }\n }\n\n private handleTransportStateChange(previousState: BetterWsClient<WebSocketEvent<C>>['state'], state: BetterWsClient<WebSocketEvent<C>>['state']) {\n if (state === 'ready') {\n this.transitionTo('ready')\n this.opts.onReady()\n return\n }\n\n const nextStatus = this.mapTransportStatus(state)\n if (!nextStatus) {\n return\n }\n\n this.transitionTo(nextStatus)\n\n if (previousState === 'ready' && state === 'reconnecting') {\n this.opts.onClose()\n }\n else if (state === 'closed') {\n this.opts.onClose()\n }\n }\n\n private mapTransportStatus(state: BetterWsClient<WebSocketEvent<C>>['state']): ClientStatus | undefined {\n switch (state) {\n case 'idle':\n case 'connecting':\n case 'reconnecting':\n case 'closing':\n case 'closed':\n case 'failed':\n return state\n case 'open':\n case 'preparing':\n case 'ready':\n return undefined\n }\n }\n\n private transitionTo(status: ClientStatus) {\n if (this.status === status) {\n return\n }\n\n const previousStatus = this.status\n this.status = status\n const context = { previousStatus, status }\n\n this.opts.onStateChange(context)\n\n for (const listener of this.stateListeners) {\n listener(context)\n }\n }\n\n private async waitForConnection(connectPromise: Promise<void>, options?: ConnectOptions) {\n if (!options?.timeout && !options?.abortSignal) {\n return connectPromise\n }\n\n const timeout = options?.timeout\n if (typeof timeout !== 'undefined' && timeout <= 0) {\n throw createConnectionTimeoutError(timeout)\n }\n\n const abortSignal = options?.abortSignal\n if (abortSignal?.aborted) {\n throw createAbortError()\n }\n\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined\n let removeAbortListener: (() => void) | undefined\n\n try {\n await Promise.race([\n connectPromise,\n new Promise<void>((_, reject) => {\n if (typeof timeout !== 'undefined') {\n timeoutHandle = setTimeout(() => {\n reject(createConnectionTimeoutError(timeout))\n }, timeout)\n }\n\n if (abortSignal) {\n const onAbort = () => reject(createAbortError())\n abortSignal.addEventListener('abort', onAbort, { once: true })\n removeAbortListener = () => abortSignal.removeEventListener('abort', onAbort)\n }\n }),\n ])\n }\n finally {\n if (timeoutHandle) {\n clearTimeout(timeoutHandle)\n }\n\n removeAbortListener?.()\n }\n }\n\n private waitForReady(): Promise<void> {\n if (this.status === 'ready') {\n return Promise.resolve()\n }\n\n return new Promise<void>((resolve, reject) => {\n const dispose = this.onConnectionStateChange(({ status }) => {\n if (status === 'ready') {\n dispose()\n resolve()\n return\n }\n\n if (status === 'failed' || status === 'closed') {\n dispose()\n reject(this.failureReason ?? new Error(`Client connection ended with status: ${status}`))\n }\n })\n })\n }\n\n private errorFromServerEvent(message: WebSocketEvent<C>): Error | undefined {\n if (message.type !== 'error') {\n return undefined\n }\n\n const errorMessage = typeof message.data.message === 'string'\n ? message.data.message\n : 'Unknown server error'\n const parsed = parseServerErrorMessage(errorMessage)\n\n if (parsed.code === 'unknown') {\n return new Error(errorMessage)\n }\n\n return new Error(parsed.message)\n }\n\n private normalizeError(error: unknown, fallback: string): Error {\n return error instanceof Error\n ? error\n : new Error(errorMessageFrom(error) ?? fallback)\n }\n\n private isSelfModuleAnnouncement(event: WebSocketBaseEvent<'extension:module:announced', WebSocketEvents<C>['extension:module:announced']>) {\n return event.data.name === this.opts.name && event.data.identity?.id === this.opts.identity.id\n }\n\n private hasSelfModuleInRegistrySync(event: WebSocketBaseEvent<'registry:modules:sync', WebSocketEvents<C>['registry:modules:sync']>) {\n return event.data.modules.some(module =>\n module.name === this.opts.name\n && module.identity?.id === this.opts.identity.id,\n )\n }\n\n private createPayload(data: WebSocketEventOptionalSource<C>) {\n return {\n ...data,\n metadata: {\n ...data.metadata,\n source: data.metadata?.source ?? {\n kind: 'plugin',\n ...this.opts.identity,\n plugin: { id: this.opts.extension.id },\n },\n event: {\n ...data.metadata?.event,\n id: data.metadata?.event?.id ?? createEventId(),\n },\n },\n } as WebSocketEvent<C>\n }\n}\n\nexport function createClient<C = undefined>(options: ClientOptions<C>): Client<C> {\n return new Client(options)\n}\n","import type {\n ExtensionIdentity,\n ModuleConfigSchema,\n ModuleDependency,\n ModulePermissionDeclaration,\n ProtocolEvents,\n WebSocketBaseEvent,\n WebSocketEventOptionalSource,\n WebSocketEvents,\n} from '@proj-airi/server-shared/types'\n\nimport type { ClientOptions, ConnectOptions } from './client'\n\nimport { createClient } from './client'\n\n/**\n * Describes the client operations required by {@link WebSocketExtensionPeer}.\n *\n * @param C - Optional custom protocol event map carried by the websocket client.\n */\nexport interface ExtensionPeerClient<C = undefined> {\n connect: (options?: ConnectOptions) => Promise<void>\n send: (data: WebSocketEventOptionalSource<C>) => boolean\n sendOrThrow: (data: WebSocketEventOptionalSource<C>) => void\n close: () => void\n onEvent?: <E extends keyof WebSocketEvents<C>>(\n event: E,\n callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ) => () => void\n}\n\n/**\n * Describes one module announcement emitted through a websocket extension peer.\n *\n * @param C - Optional custom protocol event map used for possible event declarations.\n */\nexport interface AnnounceExtensionModuleInput<C = undefined> {\n /** Stable module id within the owning extension session. */\n id: string\n /** Human-readable module name used by registry and diagnostics. */\n name: string\n /** Protocol events this module may emit or handle. */\n possibleEvents?: Array<keyof ProtocolEvents<C>>\n /** Runtime permissions requested by this module. */\n permissions?: ModulePermissionDeclaration\n /** Optional configuration schema understood by the module. */\n configSchema?: ModuleConfigSchema\n /** Other modules or capabilities this module expects to exist. */\n dependencies?: ModuleDependency[]\n /** Optional labels for routing, diagnostics, or inspector views. */\n labels?: Record<string, string>\n}\n\n/**\n * Options for creating a websocket-backed extension peer. Supplying `client` lets\n * tests and embedding runtimes provide their own protocol client implementation.\n *\n * @param C - Optional custom protocol event map carried by the websocket client.\n */\nexport interface WebSocketExtensionPeerOptions<C = undefined> {\n extension: ExtensionIdentity\n client?: ExtensionPeerClient<C>\n clientOptions?: Omit<ClientOptions<C>, 'name' | 'identity'>\n}\n\n/**\n * Provides extension-level protocol helpers over a server-sdk protocol client.\n */\nexport class WebSocketExtensionPeer<C = undefined> {\n private readonly client: ExtensionPeerClient<C>\n private readonly extension: ExtensionIdentity\n\n constructor(options: WebSocketExtensionPeerOptions<C>) {\n this.extension = options.extension\n this.client = options.client ?? createClient<C>({\n ...options.clientOptions,\n name: options.extension.id,\n handshake: 'manual',\n autoConnect: options.clientOptions?.autoConnect ?? false,\n autoReconnect: options.clientOptions?.autoReconnect ?? false,\n })\n }\n\n connect(options?: ConnectOptions): Promise<void> {\n return this.client.connect(options)\n }\n\n authenticatePeer(input: { token?: string, peerId?: string } = {}): void {\n this.client.sendOrThrow({\n type: 'peer:authenticate',\n data: input,\n })\n }\n\n announceExtension(input: { permissions?: ModulePermissionDeclaration } = {}): void {\n this.client.sendOrThrow({\n type: 'extension:announce',\n data: {\n identity: this.extension,\n permissions: input.permissions,\n },\n })\n }\n\n announceModule(input: AnnounceExtensionModuleInput<C>): void {\n this.client.sendOrThrow({\n type: 'extension:module:announce',\n data: {\n name: input.name,\n identity: {\n id: input.id,\n extension: this.extension,\n labels: input.labels,\n },\n possibleEvents: input.possibleEvents ?? [],\n permissions: input.permissions,\n configSchema: input.configSchema,\n dependencies: input.dependencies,\n },\n })\n }\n\n send(data: WebSocketEventOptionalSource<C>): boolean {\n return this.client.send(data)\n }\n\n onEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ): () => void {\n if (!this.client.onEvent) {\n throw new Error('Wrapped extension peer client does not support event listeners.')\n }\n\n return this.client.onEvent(event, callback)\n }\n\n close(): void {\n this.client.close()\n }\n}\n\n/** Creates a websocket extension peer over a server-sdk protocol client. */\nexport function createWebSocketExtensionPeer<C = undefined>(\n options: WebSocketExtensionPeerOptions<C>,\n): WebSocketExtensionPeer<C> {\n return new WebSocketExtensionPeer(options)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proj-airi/server-sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "Client-side SDK implementation for connecting to AIRI server components and runtimes",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Moeru AI Project AIRI Team",
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@moeru/std": "0.1.0-beta.17",
|
|
36
|
-
"crossws": "^0.4.5",
|
|
37
36
|
"superjson": "^2.2.6",
|
|
38
|
-
"
|
|
37
|
+
"valibot": "^1.3.1",
|
|
38
|
+
"@proj-airi/better-ws": "^0.11.0",
|
|
39
|
+
"@proj-airi/server-shared": "^0.11.0"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"dev": "pnpm run build",
|