@cored-im/openapi-sdk 0.28.102

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.
@@ -0,0 +1,72 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /** 64-bit integer represented as string to avoid precision loss */
5
+ export type Int64 = string;
6
+
7
+ export interface ApiRequest {
8
+ method: string;
9
+ path: string;
10
+ pathParams?: Record<string, string>;
11
+ queryParams?: Record<string, string>;
12
+ headerParams?: Record<string, string>;
13
+ body?: unknown;
14
+ stream?: ReadableStream<Uint8Array> | null;
15
+ withAppAccessToken?: boolean;
16
+ withWebSocket?: boolean;
17
+ }
18
+
19
+ export interface ApiResponse {
20
+ json(): Promise<unknown>;
21
+ body(): Promise<Uint8Array>;
22
+ }
23
+
24
+ export interface ApiError {
25
+ code: number;
26
+ msg: string;
27
+ log_id: string;
28
+ data?: unknown;
29
+ }
30
+
31
+ export interface ApiClient {
32
+ preheat(): Promise<void>;
33
+ request(req: ApiRequest): Promise<ApiResponse>;
34
+ onEvent(eventType: string, handler: WrappedEventHandler): void;
35
+ offEvent(eventType: string, handler: WrappedEventHandler): void;
36
+ close(): Promise<void>;
37
+ }
38
+
39
+ export interface EventHeader {
40
+ event_id: string;
41
+ event_type: string;
42
+ event_created_at: Int64;
43
+ }
44
+
45
+ export type WrappedEventHandler = (header: EventHeader, body: Uint8Array | string) => void;
46
+
47
+ export type Marshaller = (v: unknown) => string;
48
+ export type Unmarshaller = (data: string) => unknown;
49
+
50
+ export interface HttpClient {
51
+ fetch(url: string, init: RequestInit): Promise<Response>;
52
+ }
53
+
54
+ export interface Logger {
55
+ debug(msg: string, ...args: unknown[]): void;
56
+ info(msg: string, ...args: unknown[]): void;
57
+ warn(msg: string, ...args: unknown[]): void;
58
+ error(msg: string, ...args: unknown[]): void;
59
+ }
60
+
61
+ export enum LoggerLevel {
62
+ Debug = 0,
63
+ Info = 1,
64
+ Warn = 2,
65
+ Error = 3,
66
+ }
67
+
68
+ export interface TimeManager {
69
+ getSystemTimestamp(): number;
70
+ getServerTimestamp(): number;
71
+ syncServerTimestamp(timestamp: number): void;
72
+ }
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export const VERSION = '0.28.102';
5
+ export const USER_AGENT = 'cored-openapi-sdk-js/0.28.102';
@@ -0,0 +1,423 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { Config } from './config';
5
+ import type { EventHeader, WrappedEventHandler } from './types';
6
+ import { USER_AGENT } from './version';
7
+ import { DEFAULT_WS_PATH } from './consts';
8
+ import type { CryptoManager } from './crypto';
9
+ import {
10
+ encodeSecureMessage,
11
+ decodeSecureMessage,
12
+ encodeWebSocketMessage,
13
+ decodeWebSocketMessage,
14
+ encodeHttpRequest,
15
+ decodeHttpResponse,
16
+ } from '@/internal/transport';
17
+ import type { HttpRequest, HttpResponse, WebSocketMessage } from '@/internal/transport';
18
+
19
+ // Resolve WebSocket implementation: native (browser / Node 22+) or 'ws' package (Node < 22)
20
+ let _WebSocketImpl: typeof WebSocket | undefined;
21
+
22
+ async function getWebSocket(): Promise<typeof WebSocket> {
23
+ if (_WebSocketImpl) return _WebSocketImpl;
24
+ if (typeof WebSocket !== 'undefined') {
25
+ _WebSocketImpl = WebSocket;
26
+ return _WebSocketImpl;
27
+ }
28
+ try {
29
+ // Dynamic import for Node.js < 22; 'ws' is an optional peer dependency
30
+ const mod = await (Function('return import("ws")')() as Promise<Record<string, unknown>>);
31
+ _WebSocketImpl = (mod.default || mod) as unknown as typeof WebSocket;
32
+ return _WebSocketImpl;
33
+ } catch {
34
+ throw new Error(
35
+ 'No WebSocket implementation found. ' +
36
+ 'Install the "ws" package: npm install ws',
37
+ );
38
+ }
39
+ }
40
+
41
+ const RECONNECT_CHECK_INTERVAL = 10_000;
42
+ const HEALTH_CHECK_INTERVAL = 20_000;
43
+ const ALIVE_TIMEOUT = 40_000;
44
+ const CONNECT_TIMEOUT = 5_000;
45
+ const WRITE_TIMEOUT = 60_000;
46
+
47
+ interface WsClientOptions {
48
+ config: Config;
49
+ getSecret: () => string;
50
+ getToken: () => Promise<string>;
51
+ ensurePing: () => Promise<void>;
52
+ cryptoManager: CryptoManager;
53
+ }
54
+
55
+ interface ReqCallback {
56
+ resolve: (resp: HttpResponse) => void;
57
+ reject: (err: Error) => void;
58
+ timer: ReturnType<typeof setTimeout>;
59
+ }
60
+
61
+ export class WsClient {
62
+ private config: Config;
63
+ private getSecret: () => string;
64
+ private getToken: () => Promise<string>;
65
+ private ensurePing: () => Promise<void>;
66
+ private cryptoManager: CryptoManager;
67
+
68
+ private eventHandlerMap = new Map<string, WrappedEventHandler[]>();
69
+ private socket: WebSocket | null = null;
70
+ private isConnecting = false;
71
+ private isReconnecting = false;
72
+ private shouldClose = false;
73
+ private reqCount = 0;
74
+ private reqCallbacks = new Map<string, ReqCallback>();
75
+ private reconnectAttempt = 0;
76
+ private lastMessageAt = 0;
77
+ private healthCheckTimer: ReturnType<typeof setInterval> | null = null;
78
+ private reconnectCheckTimer: ReturnType<typeof setInterval> | null = null;
79
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
80
+ private initDone = false;
81
+ private initPromise: Promise<void> | null = null;
82
+
83
+ constructor(options: WsClientOptions) {
84
+ this.config = options.config;
85
+ this.getSecret = options.getSecret;
86
+ this.getToken = options.getToken;
87
+ this.ensurePing = options.ensurePing;
88
+ this.cryptoManager = options.cryptoManager;
89
+ }
90
+
91
+ onEvent(eventType: string, handler: WrappedEventHandler): void {
92
+ this.ensureInit();
93
+ let handlers = this.eventHandlerMap.get(eventType);
94
+ if (!handlers) {
95
+ handlers = [];
96
+ this.eventHandlerMap.set(eventType, handlers);
97
+ }
98
+ handlers.push(handler);
99
+ }
100
+
101
+ offEvent(eventType: string, handler: WrappedEventHandler): void {
102
+ const handlers = this.eventHandlerMap.get(eventType);
103
+ if (!handlers) return;
104
+ const idx = handlers.indexOf(handler);
105
+ if (idx >= 0) {
106
+ handlers.splice(idx, 1);
107
+ }
108
+ }
109
+
110
+ async httpRequest(req: HttpRequest): Promise<HttpResponse> {
111
+ await this.ensureInit();
112
+
113
+ const reqId = String(++this.reqCount);
114
+ req.reqId = reqId;
115
+
116
+ return new Promise<HttpResponse>((resolve, reject) => {
117
+ const timer = setTimeout(() => {
118
+ this.reqCallbacks.delete(reqId);
119
+ reject(new Error(`websocket request timeout: ${WRITE_TIMEOUT}ms`));
120
+ }, WRITE_TIMEOUT);
121
+
122
+ this.reqCallbacks.set(reqId, { resolve, reject, timer });
123
+
124
+ const reqBytes = encodeHttpRequest(req);
125
+ this.sendMessage({
126
+ httpRequest: {
127
+ method: req.method,
128
+ path: req.path,
129
+ headers: req.headers,
130
+ body: req.body,
131
+ reqId,
132
+ },
133
+ }).catch((err) => {
134
+ clearTimeout(timer);
135
+ this.reqCallbacks.delete(reqId);
136
+ reject(err);
137
+ });
138
+ });
139
+ }
140
+
141
+ close(): void {
142
+ this.shouldClose = true;
143
+ this.clearTimers();
144
+ if (this.socket) {
145
+ try {
146
+ this.socket.close(1000, 'client close');
147
+ } catch {
148
+ // ignore
149
+ }
150
+ this.socket = null;
151
+ }
152
+ // Reject all pending callbacks
153
+ for (const [, cb] of this.reqCallbacks) {
154
+ clearTimeout(cb.timer);
155
+ cb.reject(new Error('websocket closed'));
156
+ }
157
+ this.reqCallbacks.clear();
158
+ }
159
+
160
+ // --- Internal ---
161
+
162
+ private ensureInit(): void {
163
+ if (this.initDone) return;
164
+ if (!this.initPromise) {
165
+ this.initPromise = this.doInit();
166
+ }
167
+ }
168
+
169
+ private async doInit(): Promise<void> {
170
+ if (this.initDone) return;
171
+ try {
172
+ await this.ensurePing();
173
+ await this.connect();
174
+ this.initDone = true;
175
+ } catch (err) {
176
+ this.config.logger.error('ws init failed', err);
177
+ this.reconnect();
178
+ }
179
+ }
180
+
181
+ private async connect(): Promise<void> {
182
+ if (this.isConnecting) return;
183
+ this.isConnecting = true;
184
+
185
+ try {
186
+ // Close existing socket
187
+ if (this.socket) {
188
+ try { this.socket.close(); } catch { /* ignore */ }
189
+ this.socket = null;
190
+ }
191
+
192
+ // Reject pending callbacks
193
+ for (const [, cb] of this.reqCallbacks) {
194
+ clearTimeout(cb.timer);
195
+ cb.reject(new Error('websocket reconnecting'));
196
+ }
197
+ this.reqCallbacks.clear();
198
+
199
+ await this.ensurePing();
200
+ const token = await this.getToken();
201
+
202
+ // Build WS URL
203
+ let wsUrl = this.config.backendUrl.replace(/^http/, 'ws') + DEFAULT_WS_PATH + `?token=${encodeURIComponent(token)}`;
204
+
205
+ const WS = await getWebSocket();
206
+ await new Promise<void>((resolve, reject) => {
207
+ const timer = setTimeout(() => {
208
+ reject(new Error('websocket connect timeout'));
209
+ }, CONNECT_TIMEOUT);
210
+
211
+ const ws = new WS(wsUrl);
212
+ ws.binaryType = 'arraybuffer';
213
+
214
+ ws.onopen = async () => {
215
+ clearTimeout(timer);
216
+ this.socket = ws;
217
+
218
+ try {
219
+ // Send InitRequest
220
+ await this.sendMessage({ initRequest: { userAgent: USER_AGENT } });
221
+
222
+ // Wait for InitResponse
223
+ await new Promise<void>((resolveInit, rejectInit) => {
224
+ const initTimer = setTimeout(() => {
225
+ rejectInit(new Error('init response timeout'));
226
+ }, CONNECT_TIMEOUT);
227
+
228
+ const origHandler = ws.onmessage;
229
+ ws.onmessage = async (event) => {
230
+ clearTimeout(initTimer);
231
+ try {
232
+ const data = new Uint8Array(event.data as ArrayBuffer);
233
+ const secureMsg = decodeSecureMessage(data);
234
+ const decrypted = await this.cryptoManager.decryptMessage(this.getSecret(), secureMsg);
235
+ const wsMsg = decodeWebSocketMessage(decrypted);
236
+ if (wsMsg.initResponse) {
237
+ resolveInit();
238
+ } else {
239
+ rejectInit(new Error('unexpected message during init'));
240
+ }
241
+ } catch (err) {
242
+ rejectInit(err as Error);
243
+ }
244
+ };
245
+ });
246
+
247
+ // Set up message handler
248
+ ws.onmessage = (event) => this.handleMessage(event);
249
+ ws.onclose = () => this.handleClose();
250
+ ws.onerror = (err) => {
251
+ this.config.logger.error('ws error', err);
252
+ };
253
+
254
+ this.lastMessageAt = Date.now();
255
+ this.startHealthCheck();
256
+ this.startReconnectCheck();
257
+ this.reconnectAttempt = 0;
258
+ this.isReconnecting = false;
259
+
260
+ resolve();
261
+ } catch (err) {
262
+ reject(err);
263
+ }
264
+ };
265
+
266
+ ws.onerror = () => {
267
+ clearTimeout(timer);
268
+ reject(new Error('websocket connect failed'));
269
+ };
270
+ });
271
+ } finally {
272
+ this.isConnecting = false;
273
+ }
274
+ }
275
+
276
+ private handleClose(): void {
277
+ this.socket = null;
278
+ this.clearTimers();
279
+ if (!this.shouldClose) {
280
+ this.reconnect();
281
+ }
282
+ }
283
+
284
+ private async handleMessage(event: MessageEvent): Promise<void> {
285
+ this.lastMessageAt = Date.now();
286
+
287
+ try {
288
+ const data = new Uint8Array(event.data as ArrayBuffer);
289
+ const secureMsg = decodeSecureMessage(data);
290
+ const decrypted = await this.cryptoManager.decryptMessage(this.getSecret(), secureMsg);
291
+ const wsMsg = decodeWebSocketMessage(decrypted);
292
+
293
+ if (wsMsg.pong) {
294
+ this.config.timeManager.syncServerTimestamp(Number(wsMsg.pong.timestamp));
295
+ } else if (wsMsg.event) {
296
+ const header: EventHeader = {
297
+ event_id: wsMsg.event.eventHeader?.eventId ?? '',
298
+ event_type: wsMsg.event.eventHeader?.eventType ?? '',
299
+ event_created_at: String(wsMsg.event.eventHeader?.eventCreatedAt ?? '0'),
300
+ };
301
+
302
+ const handlers = this.eventHandlerMap.get(header.event_type);
303
+ if (handlers) {
304
+ for (const handler of handlers) {
305
+ try {
306
+ handler(header, wsMsg.event.eventBody ?? new Uint8Array(0));
307
+ } catch (err) {
308
+ this.config.logger.error('event handler error', err);
309
+ }
310
+ }
311
+ }
312
+
313
+ // Send event ack
314
+ this.sendMessage({ eventAck: { eventId: header.event_id } }).catch(() => {});
315
+ } else if (wsMsg.httpResponse) {
316
+ const reqId = wsMsg.httpResponse.reqId;
317
+ const cb = this.reqCallbacks.get(reqId);
318
+ if (cb) {
319
+ clearTimeout(cb.timer);
320
+ this.reqCallbacks.delete(reqId);
321
+ cb.resolve(wsMsg.httpResponse);
322
+ }
323
+ }
324
+ } catch (err) {
325
+ this.config.logger.error('ws message parse error', err);
326
+ }
327
+ }
328
+
329
+ private async sendMessage(msg: WebSocketMessage): Promise<void> {
330
+ const WS = await getWebSocket();
331
+ if (!this.socket || this.socket.readyState !== WS.OPEN) {
332
+ throw new Error('websocket not connected');
333
+ }
334
+
335
+ const msgBytes = encodeWebSocketMessage(msg);
336
+ const secureMsg = await this.cryptoManager.encryptMessage(this.getSecret(), msgBytes);
337
+ const data = encodeSecureMessage(secureMsg);
338
+ this.socket.send(data);
339
+ }
340
+
341
+ private reconnect(): void {
342
+ if (this.shouldClose || this.isReconnecting) return;
343
+ this.isReconnecting = true;
344
+
345
+ const delay = this.getReconnectDelay();
346
+ this.config.logger.info(`ws reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
347
+
348
+ this.reconnectTimer = setTimeout(async () => {
349
+ this.reconnectAttempt++;
350
+ try {
351
+ await this.connect();
352
+ this.config.logger.info('ws reconnected');
353
+ } catch (err) {
354
+ this.config.logger.error('ws reconnect failed', err);
355
+ this.isReconnecting = false;
356
+ this.reconnect();
357
+ }
358
+ }, delay);
359
+ }
360
+
361
+ private getReconnectDelay(): number {
362
+ const attempt = this.reconnectAttempt;
363
+ if (attempt === 0) {
364
+ return 250 + Math.floor(Math.random() * 250);
365
+ }
366
+ if (attempt <= 4) {
367
+ return 750 + Math.floor(Math.random() * 500);
368
+ }
369
+ const base = Math.min(10000, Math.max(750, (attempt - 4) * 2000));
370
+ const jitter = Math.floor(Math.random() * 1000);
371
+ return Math.min(15000, base + jitter);
372
+ }
373
+
374
+ private startHealthCheck(): void {
375
+ this.stopHealthCheck();
376
+ this.healthCheckTimer = setInterval(async () => {
377
+ try {
378
+ const timestamp = this.config.timeManager.getServerTimestamp();
379
+ await this.sendMessage({ ping: { timestamp } });
380
+ } catch {
381
+ // ignore, reconnect check will handle it
382
+ }
383
+ }, HEALTH_CHECK_INTERVAL);
384
+ }
385
+
386
+ private stopHealthCheck(): void {
387
+ if (this.healthCheckTimer) {
388
+ clearInterval(this.healthCheckTimer);
389
+ this.healthCheckTimer = null;
390
+ }
391
+ }
392
+
393
+ private startReconnectCheck(): void {
394
+ this.stopReconnectCheck();
395
+ this.reconnectCheckTimer = setInterval(() => {
396
+ if (Date.now() - this.lastMessageAt > ALIVE_TIMEOUT) {
397
+ this.config.logger.warn('ws alive timeout, reconnecting');
398
+ this.clearTimers();
399
+ if (this.socket) {
400
+ try { this.socket.close(); } catch { /* ignore */ }
401
+ this.socket = null;
402
+ }
403
+ this.reconnect();
404
+ }
405
+ }, RECONNECT_CHECK_INTERVAL);
406
+ }
407
+
408
+ private stopReconnectCheck(): void {
409
+ if (this.reconnectCheckTimer) {
410
+ clearInterval(this.reconnectCheckTimer);
411
+ this.reconnectCheckTimer = null;
412
+ }
413
+ }
414
+
415
+ private clearTimers(): void {
416
+ this.stopHealthCheck();
417
+ this.stopReconnectCheck();
418
+ if (this.reconnectTimer) {
419
+ clearTimeout(this.reconnectTimer);
420
+ this.reconnectTimer = null;
421
+ }
422
+ }
423
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Copyright (c) 2026 Cored Limited
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ export { CoredClient } from '@/client';
5
+ export type { CoredClientOptions } from '@/client';
6
+ export { LoggerLevel } from '@/core/types';
7
+ export type {
8
+ Int64,
9
+ ApiClient,
10
+ ApiRequest,
11
+ ApiResponse,
12
+ ApiError,
13
+ EventHeader,
14
+ HttpClient,
15
+ Logger,
16
+ TimeManager,
17
+ } from '@/core/types';
18
+ export type { Config } from '@/core/config';
19
+ export { VERSION, USER_AGENT } from '@/core/version';