@proj-airi/server-sdk 0.9.0-alpha.2 → 0.9.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/dist/index.d.mts +60 -22
- package/dist/index.mjs +374 -163
- package/dist/index.mjs.map +1 -1
- package/dist/utils/node/index.mjs +1 -2
- package/dist/utils/node/index.mjs.map +1 -1
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -14,9 +14,32 @@ npm i @proj-airi/server-sdk -D
|
|
|
14
14
|
```typescript
|
|
15
15
|
import { Client } from '@proj-airi/server-sdk'
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const client = new Client({
|
|
18
|
+
name: 'your airi plugin',
|
|
19
|
+
autoConnect: false,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
await client.connect()
|
|
23
|
+
|
|
24
|
+
client.onEvent('input:text', async (event) => {
|
|
25
|
+
console.info(event.data.text)
|
|
26
|
+
})
|
|
18
27
|
```
|
|
19
28
|
|
|
29
|
+
`connect()` now resolves when the client is fully ready for use, not just when the websocket transport has opened. In practice that means:
|
|
30
|
+
|
|
31
|
+
- the socket is open
|
|
32
|
+
- authentication succeeded when a token is configured
|
|
33
|
+
- the module has announced itself successfully
|
|
34
|
+
|
|
35
|
+
Useful runtime helpers:
|
|
36
|
+
|
|
37
|
+
- `client.connectionStatus` exposes the current lifecycle state
|
|
38
|
+
- `client.isReady` tells you whether the client has completed authentication + announce
|
|
39
|
+
- `client.send()` returns `false` instead of silently dropping messages when the socket is unavailable
|
|
40
|
+
- `client.sendOrThrow()` is available when you want strict delivery semantics
|
|
41
|
+
- `client.onEvent()` returns an unsubscribe function
|
|
42
|
+
|
|
20
43
|
## License
|
|
21
44
|
|
|
22
45
|
[MIT](../../LICENSE)
|
package/dist/index.d.mts
CHANGED
|
@@ -2,58 +2,96 @@ import { ContextUpdateStrategy, MessageHeartbeat, MetadataEventSource, ModuleCon
|
|
|
2
2
|
export * from "@proj-airi/server-shared/types";
|
|
3
3
|
|
|
4
4
|
//#region src/client.d.ts
|
|
5
|
+
type ClientStatus = 'idle' | 'connecting' | 'authenticating' | 'announcing' | 'ready' | 'reconnecting' | 'closing' | 'closed' | 'failed';
|
|
6
|
+
interface ClientHeartbeatOptions {
|
|
7
|
+
pingInterval?: number;
|
|
8
|
+
readTimeout?: number;
|
|
9
|
+
message?: MessageHeartbeat | string;
|
|
10
|
+
}
|
|
11
|
+
interface ClientStateChangeContext {
|
|
12
|
+
previousStatus: ClientStatus;
|
|
13
|
+
status: ClientStatus;
|
|
14
|
+
}
|
|
15
|
+
interface ConnectOptions {
|
|
16
|
+
abortSignal?: AbortSignal;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}
|
|
5
19
|
interface ClientOptions<C = undefined> {
|
|
6
20
|
url?: string;
|
|
7
21
|
name: string;
|
|
8
|
-
possibleEvents?: Array<keyof WebSocketEvents<C>>;
|
|
9
22
|
token?: string;
|
|
23
|
+
possibleEvents?: Array<keyof WebSocketEvents<C>>;
|
|
10
24
|
identity?: MetadataEventSource;
|
|
11
25
|
dependencies?: ModuleDependency[];
|
|
12
26
|
configSchema?: ModuleConfigSchema;
|
|
13
|
-
heartbeat?:
|
|
14
|
-
readTimeout?: number;
|
|
15
|
-
message?: MessageHeartbeat | string;
|
|
16
|
-
};
|
|
17
|
-
onError?: (error: unknown) => void;
|
|
18
|
-
onClose?: () => void;
|
|
27
|
+
heartbeat?: ClientHeartbeatOptions;
|
|
19
28
|
autoConnect?: boolean;
|
|
20
29
|
autoReconnect?: boolean;
|
|
21
30
|
maxReconnectAttempts?: number;
|
|
31
|
+
onError?: (error: unknown) => void;
|
|
32
|
+
onClose?: () => void;
|
|
33
|
+
onReady?: () => void;
|
|
34
|
+
onStateChange?: (context: ClientStateChangeContext) => void;
|
|
22
35
|
onAnyMessage?: (data: WebSocketEvent<C>) => void;
|
|
23
36
|
onAnySend?: (data: WebSocketEvent<C>) => void;
|
|
24
37
|
}
|
|
25
38
|
declare class Client<C = undefined> {
|
|
26
|
-
private connected;
|
|
27
|
-
private connecting;
|
|
28
39
|
private websocket?;
|
|
29
40
|
private shouldClose;
|
|
30
|
-
private connectAttempt?;
|
|
31
41
|
private connectTask?;
|
|
32
42
|
private heartbeatTimer?;
|
|
43
|
+
private lastPingAt;
|
|
44
|
+
private lastReadAt;
|
|
45
|
+
private reconnectAttempts;
|
|
46
|
+
private pendingReconnect;
|
|
47
|
+
private connectionAttempt?;
|
|
48
|
+
private failureReason?;
|
|
49
|
+
private status;
|
|
33
50
|
private readonly identity;
|
|
51
|
+
private readonly heartbeat;
|
|
34
52
|
private readonly opts;
|
|
35
53
|
private readonly eventListeners;
|
|
54
|
+
private readonly stateListeners;
|
|
36
55
|
constructor(options: ClientOptions<C>);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
get connectionStatus(): ClientStatus;
|
|
57
|
+
get isReady(): boolean;
|
|
58
|
+
get isSocketOpen(): boolean;
|
|
59
|
+
get lastError(): Error;
|
|
60
|
+
connect(options?: ConnectOptions): Promise<void>;
|
|
61
|
+
ready(options?: ConnectOptions): Promise<void>;
|
|
62
|
+
ensureConnected(options?: ConnectOptions): Promise<void>;
|
|
63
|
+
onConnectionStateChange(callback: (context: ClientStateChangeContext) => void): () => void;
|
|
64
|
+
onEvent<E extends keyof WebSocketEvents<C>>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): () => void;
|
|
65
|
+
offEvent<E extends keyof WebSocketEvents<C>>(event: E, callback?: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): void;
|
|
66
|
+
send(data: WebSocketEventOptionalSource<C>): boolean;
|
|
67
|
+
sendOrThrow(data: WebSocketEventOptionalSource<C>): void;
|
|
68
|
+
sendRaw(data: string | ArrayBufferLike | ArrayBufferView): boolean;
|
|
69
|
+
close(): void;
|
|
70
|
+
private runConnectLoop;
|
|
71
|
+
private connectOnce;
|
|
72
|
+
private handleSocketFailure;
|
|
73
|
+
private cleanupSocket;
|
|
74
|
+
private rejectAttempt;
|
|
75
|
+
private resolveAttempt;
|
|
76
|
+
private canRetry;
|
|
77
|
+
private getReconnectDelay;
|
|
78
|
+
private transitionTo;
|
|
79
|
+
private waitForConnection;
|
|
41
80
|
private tryAnnounce;
|
|
42
81
|
private tryAuthenticate;
|
|
43
|
-
private readonly handleMessageBound;
|
|
44
82
|
private handleMessage;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
83
|
+
private parseMessage;
|
|
84
|
+
private handleControlMessage;
|
|
85
|
+
private isSelfAnnouncement;
|
|
86
|
+
private dispatchMessage;
|
|
87
|
+
private createPayload;
|
|
50
88
|
private startHeartbeat;
|
|
51
89
|
private stopHeartbeat;
|
|
52
90
|
private sendNativeHeartbeat;
|
|
53
91
|
private sendHeartbeatPing;
|
|
54
92
|
private sendHeartbeatPong;
|
|
55
|
-
private
|
|
93
|
+
private reconnectAfterProtocolError;
|
|
56
94
|
}
|
|
57
95
|
//#endregion
|
|
58
|
-
export { Client, ClientOptions, ContextUpdateStrategy, WebSocketEventSource };
|
|
96
|
+
export { Client, ClientHeartbeatOptions, ClientOptions, ClientStateChangeContext, ClientStatus, ConnectOptions, ContextUpdateStrategy, WebSocketEventSource };
|
|
59
97
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import WebSocket from "crossws/websocket";
|
|
2
2
|
import superjson from "superjson";
|
|
3
|
+
import { errorMessageFrom, sleep } from "@moeru/std";
|
|
4
|
+
import { isTerminalAuthenticationServerErrorMessage, parseServerErrorMessage } from "@proj-airi/server-shared";
|
|
3
5
|
import { ContextUpdateStrategy, MessageHeartbeat, MessageHeartbeatKind, WebSocketEventSource } from "@proj-airi/server-shared/types";
|
|
4
|
-
|
|
5
|
-
//#region ../../node_modules/.pnpm/@moeru+std@0.1.0-beta.14/node_modules/@moeru/std/dist/sleep/index.js
|
|
6
|
-
const sleep = async (delay) => new Promise((resolve) => setTimeout(resolve, delay));
|
|
7
|
-
|
|
8
|
-
//#endregion
|
|
9
6
|
//#region src/client.ts
|
|
10
7
|
function createInstanceId() {
|
|
11
8
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -13,23 +10,51 @@ function createInstanceId() {
|
|
|
13
10
|
function createEventId() {
|
|
14
11
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
15
12
|
}
|
|
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
|
+
function normalizeHeartbeatOptions(heartbeat) {
|
|
26
|
+
const readTimeout = heartbeat?.readTimeout ?? 3e4;
|
|
27
|
+
const pingInterval = heartbeat?.pingInterval ?? Math.max(1e3, Math.floor(readTimeout / 2));
|
|
28
|
+
return {
|
|
29
|
+
readTimeout,
|
|
30
|
+
pingInterval: Math.min(pingInterval, readTimeout),
|
|
31
|
+
message: heartbeat?.message ?? MessageHeartbeat.Ping
|
|
32
|
+
};
|
|
33
|
+
}
|
|
16
34
|
var Client = class {
|
|
17
|
-
connected = false;
|
|
18
|
-
connecting = false;
|
|
19
35
|
websocket;
|
|
20
36
|
shouldClose = false;
|
|
21
|
-
connectAttempt;
|
|
22
37
|
connectTask;
|
|
23
38
|
heartbeatTimer;
|
|
39
|
+
lastPingAt = 0;
|
|
40
|
+
lastReadAt = 0;
|
|
41
|
+
reconnectAttempts = 0;
|
|
42
|
+
pendingReconnect = false;
|
|
43
|
+
connectionAttempt;
|
|
44
|
+
failureReason;
|
|
45
|
+
status = "idle";
|
|
24
46
|
identity;
|
|
47
|
+
heartbeat;
|
|
25
48
|
opts;
|
|
26
49
|
eventListeners = /* @__PURE__ */ new Map();
|
|
50
|
+
stateListeners = /* @__PURE__ */ new Set();
|
|
27
51
|
constructor(options) {
|
|
28
52
|
const identity = options.identity ?? {
|
|
29
53
|
kind: "plugin",
|
|
30
54
|
plugin: { id: options.name },
|
|
31
55
|
id: createInstanceId()
|
|
32
56
|
};
|
|
57
|
+
const heartbeat = normalizeHeartbeatOptions(options.heartbeat);
|
|
33
58
|
this.opts = {
|
|
34
59
|
url: "ws://localhost:6121/ws",
|
|
35
60
|
onAnyMessage: () => {},
|
|
@@ -39,108 +64,256 @@ var Client = class {
|
|
|
39
64
|
configSchema: void 0,
|
|
40
65
|
onError: () => {},
|
|
41
66
|
onClose: () => {},
|
|
67
|
+
onReady: () => {},
|
|
68
|
+
onStateChange: () => {},
|
|
42
69
|
autoConnect: true,
|
|
43
70
|
autoReconnect: true,
|
|
44
71
|
maxReconnectAttempts: -1,
|
|
45
|
-
heartbeat: {
|
|
46
|
-
readTimeout: 3e4,
|
|
47
|
-
message: MessageHeartbeat.Ping
|
|
48
|
-
},
|
|
49
72
|
...options,
|
|
73
|
+
heartbeat,
|
|
50
74
|
identity
|
|
51
75
|
};
|
|
52
76
|
this.identity = identity;
|
|
53
|
-
this.
|
|
54
|
-
if (event.data.authenticated) this.tryAnnounce();
|
|
55
|
-
else await this.retryWithExponentialBackoff(() => this.tryAuthenticate());
|
|
56
|
-
});
|
|
57
|
-
this.onEvent("error", async (event) => {
|
|
58
|
-
if (event.data.message === "not authenticated") await this._reconnectDueToUnauthorized();
|
|
59
|
-
});
|
|
60
|
-
this.onEvent("transport:connection:heartbeat", (event) => {
|
|
61
|
-
if (event.data.kind === MessageHeartbeatKind.Ping) this.sendHeartbeatPong();
|
|
62
|
-
});
|
|
77
|
+
this.heartbeat = heartbeat;
|
|
63
78
|
if (this.opts.autoConnect) this.connect();
|
|
64
79
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
get connectionStatus() {
|
|
81
|
+
return this.status;
|
|
82
|
+
}
|
|
83
|
+
get isReady() {
|
|
84
|
+
return this.status === "ready";
|
|
85
|
+
}
|
|
86
|
+
get isSocketOpen() {
|
|
87
|
+
return this.websocket?.readyState === WebSocket.OPEN;
|
|
88
|
+
}
|
|
89
|
+
get lastError() {
|
|
90
|
+
return this.failureReason;
|
|
91
|
+
}
|
|
92
|
+
async connect(options) {
|
|
93
|
+
if (this.shouldClose) throw new Error("Client is closed");
|
|
94
|
+
if (this.status === "ready") return;
|
|
95
|
+
if (this.connectTask) return this.waitForConnection(this.connectTask, options);
|
|
96
|
+
this.connectTask = this.runConnectLoop().finally(() => {
|
|
97
|
+
this.connectTask = void 0;
|
|
98
|
+
});
|
|
99
|
+
return this.waitForConnection(this.connectTask, options);
|
|
100
|
+
}
|
|
101
|
+
ready(options) {
|
|
102
|
+
return this.connect(options);
|
|
103
|
+
}
|
|
104
|
+
ensureConnected(options) {
|
|
105
|
+
return this.connect(options);
|
|
106
|
+
}
|
|
107
|
+
onConnectionStateChange(callback) {
|
|
108
|
+
this.stateListeners.add(callback);
|
|
109
|
+
return () => {
|
|
110
|
+
this.stateListeners.delete(callback);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
onEvent(event, callback) {
|
|
114
|
+
let listeners = this.eventListeners.get(event);
|
|
115
|
+
if (!listeners) {
|
|
116
|
+
listeners = /* @__PURE__ */ new Set();
|
|
117
|
+
this.eventListeners.set(event, listeners);
|
|
118
|
+
}
|
|
119
|
+
listeners.add(callback);
|
|
120
|
+
return () => {
|
|
121
|
+
this.offEvent(event, callback);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
offEvent(event, callback) {
|
|
125
|
+
const listeners = this.eventListeners.get(event);
|
|
126
|
+
if (!listeners) return;
|
|
127
|
+
if (callback) {
|
|
128
|
+
listeners.delete(callback);
|
|
129
|
+
if (!listeners.size) this.eventListeners.delete(event);
|
|
130
|
+
} else this.eventListeners.delete(event);
|
|
131
|
+
}
|
|
132
|
+
send(data) {
|
|
133
|
+
if (!this.isSocketOpen || !this.websocket) return false;
|
|
134
|
+
const payload = this.createPayload(data);
|
|
135
|
+
this.opts.onAnySend?.(payload);
|
|
136
|
+
this.websocket.send(superjson.stringify(payload));
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
sendOrThrow(data) {
|
|
140
|
+
if (!this.send(data)) throw new Error(`Client is not connected, current status: ${this.status}`);
|
|
141
|
+
}
|
|
142
|
+
sendRaw(data) {
|
|
143
|
+
if (!this.isSocketOpen || !this.websocket) return false;
|
|
144
|
+
this.websocket.send(data);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
close() {
|
|
148
|
+
this.shouldClose = true;
|
|
149
|
+
this.pendingReconnect = false;
|
|
150
|
+
this.transitionTo("closing");
|
|
151
|
+
this.stopHeartbeat();
|
|
152
|
+
this.rejectAttempt(/* @__PURE__ */ new Error("Client closed"));
|
|
153
|
+
const websocket = this.websocket;
|
|
154
|
+
this.websocket = void 0;
|
|
155
|
+
if (websocket && websocket.readyState !== WebSocket.CLOSED && websocket.readyState !== WebSocket.CLOSING) websocket.close();
|
|
156
|
+
this.transitionTo("closed");
|
|
157
|
+
}
|
|
158
|
+
async runConnectLoop() {
|
|
159
|
+
this.pendingReconnect = false;
|
|
160
|
+
while (!this.shouldClose) {
|
|
161
|
+
const reconnecting = this.reconnectAttempts > 0;
|
|
162
|
+
this.transitionTo(reconnecting ? "reconnecting" : "connecting");
|
|
73
163
|
try {
|
|
74
|
-
await
|
|
164
|
+
await this.connectOnce();
|
|
165
|
+
this.reconnectAttempts = 0;
|
|
75
166
|
return;
|
|
76
|
-
} catch (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const normalizedError = error instanceof Error ? error : new Error(errorMessageFrom(error) ?? "Failed to connect websocket client");
|
|
169
|
+
this.failureReason = normalizedError;
|
|
170
|
+
this.opts.onError?.(normalizedError);
|
|
171
|
+
if (this.shouldClose) throw normalizedError;
|
|
172
|
+
if (isTerminalAuthenticationServerErrorMessage(normalizedError.message)) {
|
|
173
|
+
this.transitionTo("failed");
|
|
174
|
+
throw normalizedError;
|
|
175
|
+
}
|
|
176
|
+
if (!this.opts.autoReconnect && reconnecting) {
|
|
177
|
+
this.transitionTo("failed");
|
|
178
|
+
throw normalizedError;
|
|
179
|
+
}
|
|
180
|
+
if (!this.canRetry()) {
|
|
181
|
+
this.transitionTo("failed");
|
|
182
|
+
throw normalizedError;
|
|
183
|
+
}
|
|
184
|
+
const delay = this.getReconnectDelay(this.reconnectAttempts);
|
|
185
|
+
this.reconnectAttempts += 1;
|
|
186
|
+
await sleep(delay);
|
|
80
187
|
}
|
|
81
188
|
}
|
|
189
|
+
throw new Error("Client is closed");
|
|
82
190
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
connectOnce() {
|
|
192
|
+
const ws = new WebSocket(this.opts.url);
|
|
193
|
+
this.websocket = ws;
|
|
194
|
+
this.lastReadAt = Date.now();
|
|
195
|
+
this.lastPingAt = 0;
|
|
196
|
+
const deferred = createDeferredPromise();
|
|
197
|
+
const attempt = {
|
|
198
|
+
announced: false,
|
|
199
|
+
authenticated: !this.opts.token,
|
|
200
|
+
promise: deferred.promise,
|
|
201
|
+
reject: deferred.reject,
|
|
202
|
+
resolve: deferred.resolve,
|
|
203
|
+
socket: ws
|
|
204
|
+
};
|
|
205
|
+
this.connectionAttempt = attempt;
|
|
206
|
+
const isCurrentSocket = () => this.websocket === ws;
|
|
207
|
+
ws.onmessage = (event) => {
|
|
208
|
+
if (!isCurrentSocket()) return;
|
|
209
|
+
this.handleMessage(event);
|
|
210
|
+
};
|
|
211
|
+
ws.onerror = (event) => {
|
|
212
|
+
if (!isCurrentSocket()) return;
|
|
213
|
+
const error = event?.error instanceof Error ? event.error : /* @__PURE__ */ new Error("WebSocket error");
|
|
214
|
+
if (this.connectionAttempt) this.handleSocketFailure(error, ws);
|
|
215
|
+
else {
|
|
216
|
+
this.opts.onError?.(error);
|
|
217
|
+
this.reconnectAfterProtocolError(error);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
ws.onclose = () => {
|
|
221
|
+
if (!isCurrentSocket()) return;
|
|
222
|
+
const wasReady = this.status === "ready";
|
|
223
|
+
this.cleanupSocket(ws);
|
|
224
|
+
this.opts.onClose?.();
|
|
225
|
+
if (this.shouldClose) return;
|
|
226
|
+
if (wasReady && this.opts.autoReconnect) {
|
|
227
|
+
this.pendingReconnect = true;
|
|
228
|
+
this.connect();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.rejectAttempt(/* @__PURE__ */ new Error("WebSocket closed"));
|
|
232
|
+
};
|
|
233
|
+
ws.onopen = () => {
|
|
234
|
+
if (!isCurrentSocket()) return;
|
|
235
|
+
this.startHeartbeat();
|
|
236
|
+
if (this.opts.token) {
|
|
237
|
+
attempt.authenticated = false;
|
|
238
|
+
this.transitionTo("authenticating");
|
|
239
|
+
this.tryAuthenticate();
|
|
240
|
+
} else {
|
|
241
|
+
attempt.authenticated = true;
|
|
242
|
+
this.transitionTo("announcing");
|
|
243
|
+
this.tryAnnounce();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
return attempt.promise;
|
|
247
|
+
}
|
|
248
|
+
handleSocketFailure(error, socket) {
|
|
249
|
+
if (socket && this.websocket !== socket) return;
|
|
250
|
+
const currentSocket = socket ?? this.websocket;
|
|
251
|
+
this.cleanupSocket(socket);
|
|
252
|
+
if (currentSocket && currentSocket.readyState !== WebSocket.CLOSED && currentSocket.readyState !== WebSocket.CLOSING) currentSocket.close();
|
|
253
|
+
this.rejectAttempt(error);
|
|
254
|
+
}
|
|
255
|
+
cleanupSocket(socket) {
|
|
256
|
+
if (socket && this.websocket !== socket) return;
|
|
257
|
+
this.stopHeartbeat();
|
|
258
|
+
if (!socket || this.websocket === socket) this.websocket = void 0;
|
|
259
|
+
}
|
|
260
|
+
rejectAttempt(error) {
|
|
261
|
+
if (!this.connectionAttempt) return;
|
|
262
|
+
const attempt = this.connectionAttempt;
|
|
263
|
+
this.connectionAttempt = void 0;
|
|
264
|
+
attempt.reject(error);
|
|
265
|
+
}
|
|
266
|
+
resolveAttempt() {
|
|
267
|
+
if (!this.connectionAttempt) return;
|
|
268
|
+
const attempt = this.connectionAttempt;
|
|
269
|
+
this.connectionAttempt = void 0;
|
|
270
|
+
attempt.resolve();
|
|
271
|
+
}
|
|
272
|
+
canRetry() {
|
|
273
|
+
return this.opts.maxReconnectAttempts === -1 || this.reconnectAttempts < this.opts.maxReconnectAttempts;
|
|
274
|
+
}
|
|
275
|
+
getReconnectDelay(attempts) {
|
|
276
|
+
return Math.min(2 ** attempts * 1e3, 3e4);
|
|
277
|
+
}
|
|
278
|
+
transitionTo(status) {
|
|
279
|
+
if (this.status === status) return;
|
|
280
|
+
const previousStatus = this.status;
|
|
281
|
+
this.status = status;
|
|
282
|
+
const context = {
|
|
283
|
+
previousStatus,
|
|
284
|
+
status
|
|
285
|
+
};
|
|
286
|
+
this.opts.onStateChange?.(context);
|
|
287
|
+
for (const listener of this.stateListeners) listener(context);
|
|
135
288
|
}
|
|
136
|
-
async
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
289
|
+
async waitForConnection(connectPromise, options) {
|
|
290
|
+
if (!options?.timeout && !options?.abortSignal) return connectPromise;
|
|
291
|
+
const timeout = options?.timeout;
|
|
292
|
+
if (typeof timeout !== "undefined" && timeout <= 0) throw new Error(`Connection timed out after ${timeout}ms`);
|
|
293
|
+
const abortSignal = options?.abortSignal;
|
|
294
|
+
if (abortSignal?.aborted) throw new Error("Connection aborted");
|
|
295
|
+
let timeoutHandle;
|
|
296
|
+
let removeAbortListener;
|
|
297
|
+
try {
|
|
298
|
+
await Promise.race([connectPromise, new Promise((_, reject) => {
|
|
299
|
+
if (typeof timeout !== "undefined") timeoutHandle = setTimeout(() => {
|
|
300
|
+
reject(/* @__PURE__ */ new Error(`Connection timed out after ${timeout}ms`));
|
|
301
|
+
}, timeout);
|
|
302
|
+
if (abortSignal) {
|
|
303
|
+
const onAbort = () => {
|
|
304
|
+
reject(/* @__PURE__ */ new Error("Connection aborted"));
|
|
305
|
+
};
|
|
306
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
307
|
+
removeAbortListener = () => abortSignal.removeEventListener("abort", onAbort);
|
|
308
|
+
}
|
|
309
|
+
})]);
|
|
310
|
+
} finally {
|
|
311
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
312
|
+
removeAbortListener?.();
|
|
313
|
+
}
|
|
141
314
|
}
|
|
142
315
|
tryAnnounce() {
|
|
143
|
-
this.
|
|
316
|
+
this.sendOrThrow({
|
|
144
317
|
type: "module:announce",
|
|
145
318
|
data: {
|
|
146
319
|
name: this.opts.name,
|
|
@@ -152,88 +325,116 @@ var Client = class {
|
|
|
152
325
|
});
|
|
153
326
|
}
|
|
154
327
|
tryAuthenticate() {
|
|
155
|
-
if (this.opts.token)
|
|
328
|
+
if (!this.opts.token) return;
|
|
329
|
+
this.sendOrThrow({
|
|
156
330
|
type: "module:authenticate",
|
|
157
331
|
data: { token: this.opts.token }
|
|
158
332
|
});
|
|
159
333
|
}
|
|
160
|
-
handleMessageBound = (event) => {
|
|
161
|
-
this.handleMessage(event);
|
|
162
|
-
};
|
|
163
334
|
async handleMessage(event) {
|
|
335
|
+
this.lastReadAt = Date.now();
|
|
164
336
|
try {
|
|
165
|
-
const data =
|
|
166
|
-
if (!data) {
|
|
167
|
-
console.warn("Received empty message");
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
337
|
+
const data = this.parseMessage(event.data);
|
|
170
338
|
this.opts.onAnyMessage?.(data);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.error("Failed to parse message:", err);
|
|
178
|
-
this.opts.onError?.(err);
|
|
339
|
+
await this.handleControlMessage(data);
|
|
340
|
+
await this.dispatchMessage(data);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const normalizedError = error instanceof Error ? error : new Error(errorMessageFrom(error) ?? "Failed to handle websocket message");
|
|
343
|
+
this.opts.onError?.(normalizedError);
|
|
344
|
+
if (this.connectionAttempt && this.status !== "ready") this.handleSocketFailure(normalizedError);
|
|
179
345
|
}
|
|
180
346
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
offEvent(event, callback) {
|
|
190
|
-
const listeners = this.eventListeners.get(event);
|
|
191
|
-
if (!listeners) return;
|
|
192
|
-
if (callback) {
|
|
193
|
-
listeners.delete(callback);
|
|
194
|
-
if (!listeners.size) this.eventListeners.delete(event);
|
|
195
|
-
} else this.eventListeners.delete(event);
|
|
347
|
+
parseMessage(raw) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = superjson.parse(raw);
|
|
350
|
+
if (parsed && typeof parsed === "object" && "type" in parsed) return parsed;
|
|
351
|
+
} catch {}
|
|
352
|
+
const parsed = JSON.parse(raw);
|
|
353
|
+
if (!parsed || typeof parsed !== "object" || !("type" in parsed)) throw new Error("Received invalid websocket message");
|
|
354
|
+
return parsed;
|
|
196
355
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
356
|
+
async handleControlMessage(data) {
|
|
357
|
+
switch (data.type) {
|
|
358
|
+
case "error": {
|
|
359
|
+
const message = data.data?.message;
|
|
360
|
+
if (!message || typeof message !== "string") return;
|
|
361
|
+
const parsedServerError = parseServerErrorMessage(message);
|
|
362
|
+
if (parsedServerError.authentication) {
|
|
363
|
+
const error = new Error(message);
|
|
364
|
+
if (parsedServerError.terminal) {
|
|
365
|
+
this.shouldClose = true;
|
|
366
|
+
this.handleSocketFailure(error);
|
|
367
|
+
this.transitionTo("failed");
|
|
368
|
+
return;
|
|
207
369
|
}
|
|
370
|
+
await this.reconnectAfterProtocolError(error);
|
|
371
|
+
return;
|
|
208
372
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
373
|
+
if (parsedServerError.code !== "unknown") throw new Error(parsedServerError.message);
|
|
374
|
+
throw new Error(message);
|
|
375
|
+
}
|
|
376
|
+
case "module:authenticated":
|
|
377
|
+
if (data.data.authenticated) {
|
|
378
|
+
if (!this.connectionAttempt || this.connectionAttempt.authenticated) return;
|
|
379
|
+
this.connectionAttempt.authenticated = true;
|
|
380
|
+
this.transitionTo("announcing");
|
|
381
|
+
this.tryAnnounce();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
throw new Error("Authentication failed");
|
|
385
|
+
case "module:announced":
|
|
386
|
+
if (!this.isSelfAnnouncement(data)) return;
|
|
387
|
+
if (this.connectionAttempt) this.connectionAttempt.announced = true;
|
|
388
|
+
this.reconnectAttempts = 0;
|
|
389
|
+
this.transitionTo("ready");
|
|
390
|
+
this.resolveAttempt();
|
|
391
|
+
this.opts.onReady?.();
|
|
392
|
+
return;
|
|
393
|
+
case "transport:connection:heartbeat": if (data.data.kind === MessageHeartbeatKind.Ping) this.sendHeartbeatPong();
|
|
212
394
|
}
|
|
213
395
|
}
|
|
214
|
-
|
|
215
|
-
|
|
396
|
+
isSelfAnnouncement(event) {
|
|
397
|
+
return event.data.name === this.opts.name && event.data.identity?.id === this.identity.id;
|
|
216
398
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
399
|
+
async dispatchMessage(data) {
|
|
400
|
+
const listeners = this.eventListeners.get(data.type);
|
|
401
|
+
if (!listeners?.size) return;
|
|
402
|
+
const results = await Promise.allSettled(Array.from(listeners).map((listener) => Promise.resolve(listener(data))));
|
|
403
|
+
for (const result of results) if (result.status === "rejected") this.opts.onError?.(result.reason);
|
|
404
|
+
}
|
|
405
|
+
createPayload(data) {
|
|
406
|
+
return {
|
|
407
|
+
...data,
|
|
408
|
+
metadata: {
|
|
409
|
+
...data?.metadata,
|
|
410
|
+
source: data?.metadata?.source ?? this.identity,
|
|
411
|
+
event: {
|
|
412
|
+
id: data?.metadata?.event?.id ?? createEventId(),
|
|
413
|
+
...data?.metadata?.event
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
};
|
|
224
417
|
}
|
|
225
418
|
startHeartbeat() {
|
|
226
|
-
if (!this.
|
|
419
|
+
if (!this.heartbeat.readTimeout || !this.heartbeat.pingInterval) return;
|
|
227
420
|
this.stopHeartbeat();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
421
|
+
this.lastReadAt = Date.now();
|
|
422
|
+
this.lastPingAt = 0;
|
|
423
|
+
const interval = Math.max(1e3, Math.min(this.heartbeat.pingInterval, Math.floor(this.heartbeat.readTimeout / 2)));
|
|
424
|
+
this.heartbeatTimer = setInterval(() => {
|
|
425
|
+
if (!this.isSocketOpen) return;
|
|
426
|
+
const now = Date.now();
|
|
427
|
+
if (now - this.lastReadAt > this.heartbeat.readTimeout) {
|
|
428
|
+
this.reconnectAfterProtocolError(/* @__PURE__ */ new Error(`Read timeout after ${this.heartbeat.readTimeout}ms`));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (now - this.lastPingAt >= this.heartbeat.pingInterval) this.sendHeartbeatPing();
|
|
432
|
+
}, interval);
|
|
231
433
|
}
|
|
232
434
|
stopHeartbeat() {
|
|
233
|
-
if (this.heartbeatTimer)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
435
|
+
if (!this.heartbeatTimer) return;
|
|
436
|
+
clearInterval(this.heartbeatTimer);
|
|
437
|
+
this.heartbeatTimer = void 0;
|
|
237
438
|
}
|
|
238
439
|
sendNativeHeartbeat(kind) {
|
|
239
440
|
const websocket = this.websocket;
|
|
@@ -241,11 +442,12 @@ var Client = class {
|
|
|
241
442
|
else websocket.pong?.();
|
|
242
443
|
}
|
|
243
444
|
sendHeartbeatPing() {
|
|
445
|
+
this.lastPingAt = Date.now();
|
|
244
446
|
this.send({
|
|
245
447
|
type: "transport:connection:heartbeat",
|
|
246
448
|
data: {
|
|
247
449
|
kind: MessageHeartbeatKind.Ping,
|
|
248
|
-
message: this.
|
|
450
|
+
message: this.heartbeat.message,
|
|
249
451
|
at: Date.now()
|
|
250
452
|
}
|
|
251
453
|
});
|
|
@@ -262,15 +464,24 @@ var Client = class {
|
|
|
262
464
|
});
|
|
263
465
|
this.sendNativeHeartbeat("pong");
|
|
264
466
|
}
|
|
265
|
-
async
|
|
266
|
-
if (this.shouldClose) return;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
467
|
+
async reconnectAfterProtocolError(error) {
|
|
468
|
+
if (this.shouldClose || this.pendingReconnect) return;
|
|
469
|
+
this.pendingReconnect = true;
|
|
470
|
+
const hadSocket = !!this.websocket;
|
|
471
|
+
if (!this.connectionAttempt || this.status === "ready") this.opts.onError?.(error);
|
|
472
|
+
const websocket = this.websocket;
|
|
473
|
+
this.cleanupSocket(websocket);
|
|
474
|
+
this.rejectAttempt(error);
|
|
475
|
+
if (websocket && websocket.readyState !== WebSocket.CLOSED && websocket.readyState !== WebSocket.CLOSING) websocket.close();
|
|
476
|
+
if (hadSocket) this.opts.onClose?.();
|
|
477
|
+
if (!this.opts.autoReconnect) {
|
|
478
|
+
this.transitionTo("failed");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this.connect();
|
|
271
482
|
}
|
|
272
483
|
};
|
|
273
|
-
|
|
274
484
|
//#endregion
|
|
275
485
|
export { Client, ContextUpdateStrategy, WebSocketEventSource };
|
|
486
|
+
|
|
276
487
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../node_modules/.pnpm/@moeru+std@0.1.0-beta.14/node_modules/@moeru/std/dist/sleep/index.js","../src/client.ts"],"sourcesContent":["const sleep = async (delay) => new Promise((resolve) => setTimeout(resolve, delay));\n\nexport { sleep };\n","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 WebSocket from 'crossws/websocket'\nimport superjson from 'superjson'\n\nimport { sleep } from '@moeru/std'\nimport {\n MessageHeartbeat,\n MessageHeartbeatKind,\n} from '@proj-airi/server-shared/types'\n\nexport interface ClientOptions<C = undefined> {\n url?: string\n name: string\n possibleEvents?: Array<keyof WebSocketEvents<C>>\n token?: string\n identity?: MetadataEventSource\n dependencies?: ModuleDependency[]\n configSchema?: ModuleConfigSchema\n heartbeat?: {\n readTimeout?: number\n message?: MessageHeartbeat | string\n }\n onError?: (error: unknown) => void\n onClose?: () => void\n autoConnect?: boolean\n autoReconnect?: boolean\n maxReconnectAttempts?: number\n onAnyMessage?: (data: WebSocketEvent<C>) => void\n onAnySend?: (data: WebSocketEvent<C>) => void\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\nexport class Client<C = undefined> {\n private connected = false\n private connecting = false\n private websocket?: WebSocket\n private shouldClose = false\n private connectAttempt?: Promise<void>\n private connectTask?: Promise<void>\n private heartbeatTimer?: ReturnType<typeof setInterval>\n private readonly identity: MetadataEventSource\n\n private readonly opts: Required<Omit<ClientOptions<C>, 'token'>> & Pick<ClientOptions<C>, 'token'>\n private readonly eventListeners = new Map<\n keyof WebSocketEvents<C>,\n Set<(data: WebSocketBaseEvent<any, any>) => void | Promise<void>>\n >()\n\n constructor(options: ClientOptions<C>) {\n const identity = options.identity ?? {\n kind: 'plugin',\n plugin: { id: options.name },\n id: createInstanceId(),\n }\n\n this.opts = {\n url: 'ws://localhost:6121/ws',\n onAnyMessage: () => {},\n onAnySend: () => {},\n possibleEvents: [],\n dependencies: [],\n configSchema: undefined,\n onError: () => {},\n onClose: () => {},\n autoConnect: true,\n autoReconnect: true,\n maxReconnectAttempts: -1,\n heartbeat: {\n readTimeout: 30_000,\n message: MessageHeartbeat.Ping,\n },\n ...options,\n identity,\n }\n\n this.identity = identity\n\n // Authentication listener is registered once only\n this.onEvent('module:authenticated', async (event) => {\n if (event.data.authenticated) {\n this.tryAnnounce()\n }\n else {\n await this.retryWithExponentialBackoff(() => this.tryAuthenticate())\n }\n })\n\n this.onEvent('error', async (event) => {\n if (event.data.message === 'not authenticated') {\n await this._reconnectDueToUnauthorized()\n }\n })\n\n this.onEvent('transport:connection:heartbeat', (event) => {\n if (event.data.kind === MessageHeartbeatKind.Ping) {\n this.sendHeartbeatPong()\n }\n })\n\n if (this.opts.autoConnect) {\n void this.connect()\n }\n }\n\n private async retryWithExponentialBackoff(fn: () => void | Promise<void>) {\n const { maxReconnectAttempts } = this.opts\n let attempts = 0\n\n // Loop until attempts exceed maxReconnectAttempts, or unlimited if -1\n while (true) {\n if (maxReconnectAttempts !== -1 && attempts >= maxReconnectAttempts) {\n console.error(`Maximum retry attempts (${maxReconnectAttempts}) reached`)\n return\n }\n\n try {\n await fn()\n return\n }\n catch (err) {\n this.opts.onError?.(err)\n const delay = Math.min(2 ** attempts * 1000, 30_000) // capped exponential backoff\n await sleep(delay)\n attempts++\n }\n }\n }\n\n private async tryReconnectWithExponentialBackoff() {\n if (this.shouldClose) {\n throw new Error('Client is closed')\n }\n\n await this.retryWithExponentialBackoff(() => this._connect())\n }\n\n private _connect(): Promise<void> {\n if (this.shouldClose || this.connected) {\n return Promise.resolve()\n }\n if (this.connecting) {\n return this.connectAttempt ?? Promise.resolve()\n }\n\n this.connectAttempt = new Promise((resolve, reject) => {\n this.connecting = true\n let settled = false\n\n const settle = (fn: () => void) => {\n if (settled)\n return\n\n settled = true\n this.connecting = false\n this.connectAttempt = undefined\n fn()\n }\n\n const ws = new WebSocket(this.opts.url)\n this.websocket = ws\n\n ws.onmessage = this.handleMessageBound\n ws.onerror = (event: any) => {\n settle(() => {\n this.connected = false\n\n this.opts.onError?.(event)\n reject(event?.error ?? new Error('WebSocket error'))\n })\n }\n ws.onclose = () => {\n if (!settled && !this.connected) {\n settle(() => {\n reject(new Error('WebSocket closed before open'))\n })\n return\n }\n\n if (this.connected) {\n this.connected = false\n this.stopHeartbeat()\n this.opts.onClose?.()\n }\n if (this.opts.autoReconnect && !this.shouldClose) {\n void this.tryReconnectWithExponentialBackoff()\n }\n }\n ws.onopen = () => {\n settle(() => {\n this.connected = true\n\n this.startHeartbeat()\n\n if (this.opts.token)\n this.tryAuthenticate()\n else\n this.tryAnnounce()\n\n resolve()\n })\n }\n })\n\n return this.connectAttempt\n }\n\n async connect() {\n if (this.connected) {\n return\n }\n if (this.connectTask) {\n return this.connectTask\n }\n\n this.connectTask = this.tryReconnectWithExponentialBackoff().finally(() => (this.connectTask = undefined))\n\n return this.connectTask\n }\n\n private tryAnnounce() {\n this.send({\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 this.send({\n type: 'module:authenticate',\n data: { token: this.opts.token },\n })\n }\n }\n\n // bound reference avoids new closure allocation on every connect\n private readonly handleMessageBound = (event: MessageEvent) => {\n void this.handleMessage(event)\n }\n\n private async handleMessage(event: MessageEvent) {\n try {\n const data = superjson.parse<WebSocketEvent<C> | undefined>(event.data as string)\n if (!data) {\n console.warn('Received empty message')\n return\n }\n\n this.opts.onAnyMessage?.(data)\n const listeners = this.eventListeners.get(data.type)\n if (!listeners?.size) {\n return\n }\n\n // Execute all listeners concurrently\n const executions: Promise<void>[] = []\n for (const listener of listeners) {\n executions.push(Promise.resolve(listener(data as any)))\n }\n\n await Promise.allSettled(executions)\n }\n catch (err) {\n console.error('Failed to parse message:', err)\n this.opts.onError?.(err)\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 listeners.add(callback as any)\n }\n\n offEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback?: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => 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>): void {\n if (this.websocket && this.connected) {\n const payload = {\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 this.opts.onAnySend?.(payload)\n\n this.websocket.send(superjson.stringify(payload))\n }\n }\n\n sendRaw(data: string | ArrayBufferLike | ArrayBufferView): void {\n if (this.websocket && this.connected) {\n this.websocket.send(data)\n }\n }\n\n close(): void {\n this.shouldClose = true\n this.stopHeartbeat()\n if (this.websocket) {\n this.websocket.close()\n this.connected = false\n }\n }\n\n private startHeartbeat() {\n if (!this.opts.heartbeat?.readTimeout) {\n return\n }\n\n this.stopHeartbeat()\n\n const ping = () => this.sendHeartbeatPing()\n\n ping()\n this.heartbeatTimer = setInterval(ping, this.opts.heartbeat.readTimeout)\n }\n\n private stopHeartbeat() {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n this.heartbeatTimer = undefined\n }\n }\n\n private sendNativeHeartbeat(kind: 'ping' | 'pong') {\n const websocket = this.websocket as WebSocket & {\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.send({\n type: 'transport:connection:heartbeat',\n data: {\n kind: MessageHeartbeatKind.Ping,\n message: this.opts.heartbeat?.message ?? MessageHeartbeat.Ping,\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 _reconnectDueToUnauthorized() {\n if (this.shouldClose)\n return\n\n const ws = this.websocket\n this.connected = false\n if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {\n ws.close()\n }\n\n await this.connect()\n }\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}
|
|
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 WebSocket 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\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: WebSocket\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?: WebSocket\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\n private readonly opts: Required<Omit<ClientOptions<C>, 'token' | 'heartbeat'>> & Pick<ClientOptions<C>, 'token'> & {\n heartbeat: Required<ClientHeartbeatOptions>\n }\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 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 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 ...options,\n heartbeat,\n identity,\n }\n\n this.identity = identity\n this.heartbeat = heartbeat\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 === WebSocket.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 !== WebSocket.CLOSED && websocket.readyState !== WebSocket.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 ws = new WebSocket(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\n ws.onmessage = (event: MessageEvent) => {\n if (!isCurrentSocket()) {\n return\n }\n\n void this.handleMessage(event)\n }\n\n ws.onerror = (event: any) => {\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 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 void this.connect()\n return\n }\n\n this.rejectAttempt(new Error('WebSocket closed'))\n }\n\n ws.onopen = () => {\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?: WebSocket) {\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 !== WebSocket.CLOSED && currentSocket.readyState !== WebSocket.CLOSING) {\n currentSocket.close()\n }\n\n this.rejectAttempt(error)\n }\n\n private cleanupSocket(socket?: WebSocket) {\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: MessageEvent) {\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.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 WebSocket & {\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 !== WebSocket.CLOSED && websocket.readyState !== WebSocket.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 void this.connect()\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
|
|
3
2
|
//#region src/utils/node/process.ts
|
|
4
3
|
let running = true;
|
|
5
4
|
function killProcess() {
|
|
@@ -20,7 +19,7 @@ function runUntilSignal() {
|
|
|
20
19
|
if (running) runUntilSignal();
|
|
21
20
|
}, 10);
|
|
22
21
|
}
|
|
23
|
-
|
|
24
22
|
//#endregion
|
|
25
23
|
export { runUntilSignal };
|
|
24
|
+
|
|
26
25
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/utils/node/process.ts"],"sourcesContent":["import process from 'node:process'\n\nlet running = true\n\nfunction killProcess() {\n running = false\n}\n\nprocess.on('SIGTERM', () => {\n killProcess()\n})\nprocess.on('SIGINT', () => {\n killProcess()\n})\nprocess.on('uncaughtException', (e) => {\n console.error(e)\n killProcess()\n})\n\nexport function runUntilSignal() {\n setTimeout(() => {\n if (running)\n runUntilSignal()\n }, 10)\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/utils/node/process.ts"],"sourcesContent":["import process from 'node:process'\n\nlet running = true\n\nfunction killProcess() {\n running = false\n}\n\nprocess.on('SIGTERM', () => {\n killProcess()\n})\nprocess.on('SIGINT', () => {\n killProcess()\n})\nprocess.on('uncaughtException', (e) => {\n console.error(e)\n killProcess()\n})\n\nexport function runUntilSignal() {\n setTimeout(() => {\n if (running)\n runUntilSignal()\n }, 10)\n}\n"],"mappings":";;AAEA,IAAI,UAAU;AAEd,SAAS,cAAc;AACrB,WAAU;;AAGZ,QAAQ,GAAG,iBAAiB;AAC1B,cAAa;EACb;AACF,QAAQ,GAAG,gBAAgB;AACzB,cAAa;EACb;AACF,QAAQ,GAAG,sBAAsB,MAAM;AACrC,SAAQ,MAAM,EAAE;AAChB,cAAa;EACb;AAEF,SAAgB,iBAAiB;AAC/B,kBAAiB;AACf,MAAI,QACF,iBAAgB;IACjB,GAAG"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proj-airi/server-sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.0-alpha.
|
|
4
|
+
"version": "0.9.0-alpha.20",
|
|
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",
|
|
@@ -32,12 +32,10 @@
|
|
|
32
32
|
"package.json"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@moeru/std": "0.1.0-beta.17",
|
|
35
36
|
"crossws": "^0.4.4",
|
|
36
37
|
"superjson": "^2.2.6",
|
|
37
|
-
"@proj-airi/server-shared": "^0.9.0-alpha.
|
|
38
|
-
},
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@moeru/std": "0.1.0-beta.14"
|
|
38
|
+
"@proj-airi/server-shared": "^0.9.0-alpha.20"
|
|
41
39
|
},
|
|
42
40
|
"scripts": {
|
|
43
41
|
"dev": "pnpm run build",
|