@jusi/light-im-sdk 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/index.cjs +608 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +371 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +596 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types — mirror the Go server's internal/api/ws/frame.go and internal/api/rest/*.
|
|
3
|
+
*
|
|
4
|
+
* Field names use snake_case to match the JSON the server emits; callers may map to
|
|
5
|
+
* camelCase at the application boundary if they prefer.
|
|
6
|
+
*/
|
|
7
|
+
/** WebSocket frame envelope (every WS message follows this shape). */
|
|
8
|
+
interface Frame<P = unknown> {
|
|
9
|
+
type: string;
|
|
10
|
+
ts?: number;
|
|
11
|
+
/** Server-stamped unix-ms; populated on outbound frames. */
|
|
12
|
+
server_ts?: number;
|
|
13
|
+
payload?: P;
|
|
14
|
+
}
|
|
15
|
+
/** Frame type registry. Server's internal/api/ws/frame.go ⇄ this. */
|
|
16
|
+
declare const FrameType: {
|
|
17
|
+
readonly Echo: "echo";
|
|
18
|
+
readonly System: "system";
|
|
19
|
+
readonly Msg: "msg";
|
|
20
|
+
readonly Ack: "ack";
|
|
21
|
+
readonly Read: "read";
|
|
22
|
+
};
|
|
23
|
+
type FrameTypeValue = (typeof FrameType)[keyof typeof FrameType];
|
|
24
|
+
/** Client → server: send a chat message. */
|
|
25
|
+
interface MsgInPayload {
|
|
26
|
+
cid: string;
|
|
27
|
+
body: string;
|
|
28
|
+
content_type?: string;
|
|
29
|
+
/** Optional dedup token echoed back in the ack. */
|
|
30
|
+
client_msg_id?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Server → client: fan-out delivery of a persisted message. */
|
|
33
|
+
interface MsgOutPayload {
|
|
34
|
+
mid: number;
|
|
35
|
+
cid: string;
|
|
36
|
+
sender_uid: string;
|
|
37
|
+
seq: number;
|
|
38
|
+
content_type: string;
|
|
39
|
+
body: string;
|
|
40
|
+
/** unix ms */
|
|
41
|
+
created_at: number;
|
|
42
|
+
}
|
|
43
|
+
/** Server → client: ack for a previously-sent msg. */
|
|
44
|
+
interface AckPayload {
|
|
45
|
+
client_msg_id?: string;
|
|
46
|
+
mid: number;
|
|
47
|
+
seq: number;
|
|
48
|
+
cid: string;
|
|
49
|
+
}
|
|
50
|
+
/** Client → server: mark "I read up to seq N in cid". */
|
|
51
|
+
interface ReadInPayload {
|
|
52
|
+
cid: string;
|
|
53
|
+
seq: number;
|
|
54
|
+
}
|
|
55
|
+
/** Server → client: fan-out notice of another member's read marker move. */
|
|
56
|
+
interface ReadOutPayload {
|
|
57
|
+
cid: string;
|
|
58
|
+
uid: string;
|
|
59
|
+
seq: number;
|
|
60
|
+
}
|
|
61
|
+
/** System lifecycle event payload (e.g. {event: 'connected'} or {error: '...'}). */
|
|
62
|
+
type SystemPayload = {
|
|
63
|
+
event?: string;
|
|
64
|
+
error?: string;
|
|
65
|
+
[k: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
type ConversationType = 'direct' | 'group';
|
|
68
|
+
interface ConversationSummary {
|
|
69
|
+
cid: string;
|
|
70
|
+
type: ConversationType;
|
|
71
|
+
meta: unknown;
|
|
72
|
+
last_seq: number;
|
|
73
|
+
unread_count: number;
|
|
74
|
+
}
|
|
75
|
+
interface Message {
|
|
76
|
+
mid: number;
|
|
77
|
+
cid: string;
|
|
78
|
+
sender_uid: string;
|
|
79
|
+
seq: number;
|
|
80
|
+
content_type: string;
|
|
81
|
+
body: string;
|
|
82
|
+
/** unix ms */
|
|
83
|
+
ts: number;
|
|
84
|
+
}
|
|
85
|
+
interface MessagesResponse {
|
|
86
|
+
messages: Message[];
|
|
87
|
+
has_more: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* High-level connection lifecycle exposed via Client.onStateChange.
|
|
91
|
+
*
|
|
92
|
+
* - `disconnected` initial; after disconnect() or terminal auth_failed
|
|
93
|
+
* - `connecting` attempting first WS upgrade
|
|
94
|
+
* - `connected` WS connected and server sent "connected" system frame
|
|
95
|
+
* - `reconnecting` WS dropped; awaiting next backoff window
|
|
96
|
+
* - `auth_failed` terminal until next explicit connect(); tokenProvider returned a bad token twice
|
|
97
|
+
*/
|
|
98
|
+
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'auth_failed';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Typed error hierarchy. Callers can `instanceof` discriminate;
|
|
102
|
+
* the underlying failure is preserved on the standard ES2022 `cause` property.
|
|
103
|
+
*/
|
|
104
|
+
declare class IMError extends Error {
|
|
105
|
+
constructor(message: string, cause?: unknown);
|
|
106
|
+
}
|
|
107
|
+
/** HTTP 401 from /v1/* or WS upgrade refused by Bearer middleware. */
|
|
108
|
+
declare class AuthError extends IMError {
|
|
109
|
+
constructor(message: string, cause?: unknown);
|
|
110
|
+
}
|
|
111
|
+
/** Underlying fetch/WS transport failure (DNS, connect, timeout, 5xx). */
|
|
112
|
+
declare class NetworkError extends IMError {
|
|
113
|
+
constructor(message: string, cause?: unknown);
|
|
114
|
+
}
|
|
115
|
+
/** Frame parse failed, unexpected field, or schema mismatch. */
|
|
116
|
+
declare class ProtocolError extends IMError {
|
|
117
|
+
constructor(message: string, cause?: unknown);
|
|
118
|
+
}
|
|
119
|
+
/** A pending promise (e.g. ack on sendText) did not resolve before the timeout. */
|
|
120
|
+
declare class TimeoutError extends IMError {
|
|
121
|
+
constructor(message: string, cause?: unknown);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Minimal typed emitter. Each Emitter handles ONE event type — composition over hierarchy.
|
|
126
|
+
*
|
|
127
|
+
* Listener exceptions are caught and console.error'd so one bad handler can't break others
|
|
128
|
+
* or kill the WS pump that may be calling emit() inline.
|
|
129
|
+
*/
|
|
130
|
+
type Listener<T> = (event: T) => void;
|
|
131
|
+
type Unsubscribe = () => void;
|
|
132
|
+
declare class Emitter<T> {
|
|
133
|
+
private listeners;
|
|
134
|
+
on(cb: Listener<T>): Unsubscribe;
|
|
135
|
+
emit(event: T): void;
|
|
136
|
+
clear(): void;
|
|
137
|
+
get size(): number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Exponential backoff with optional uniform jitter — used by the Client's reconnect loop.
|
|
142
|
+
*
|
|
143
|
+
* Sequence (base=1000, factor=2, max=30000, jitter=0):
|
|
144
|
+
* 1000, 2000, 4000, 8000, 16000, 30000, 30000, ...
|
|
145
|
+
*
|
|
146
|
+
* With `jitter` in (0, 1], each value is multiplied by a factor sampled uniformly from
|
|
147
|
+
* [1 - jitter, 1 + jitter], so `jitter=0.3` keeps actual delay in 0.7× – 1.3× of base.
|
|
148
|
+
*/
|
|
149
|
+
interface BackoffOptions {
|
|
150
|
+
/** First delay in ms (default 1000). */
|
|
151
|
+
baseMs?: number;
|
|
152
|
+
/** Cap on a single delay (default 30000). */
|
|
153
|
+
maxMs?: number;
|
|
154
|
+
/** Multiplier between attempts (default 2). */
|
|
155
|
+
factor?: number;
|
|
156
|
+
/** 0–1, default 0.3. 0 = deterministic. */
|
|
157
|
+
jitter?: number;
|
|
158
|
+
/** Override Math.random for tests. */
|
|
159
|
+
random?: () => number;
|
|
160
|
+
}
|
|
161
|
+
declare class Backoff {
|
|
162
|
+
private attempt;
|
|
163
|
+
private readonly opts;
|
|
164
|
+
private constructor();
|
|
165
|
+
static create(opts?: BackoffOptions): Backoff;
|
|
166
|
+
/**
|
|
167
|
+
* Compute the next delay (in ms) and increment the internal attempt counter.
|
|
168
|
+
*
|
|
169
|
+
* Caller is responsible for sleeping; this method just returns the number.
|
|
170
|
+
*/
|
|
171
|
+
next(): number;
|
|
172
|
+
/** Reset attempts to 0; the next call to next() yields baseMs again. */
|
|
173
|
+
reset(): void;
|
|
174
|
+
/** Number of times next() has been called since the last reset. */
|
|
175
|
+
get attempts(): number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface RestClientOptions {
|
|
179
|
+
/**
|
|
180
|
+
* Origin (no trailing slash). Example: `https://im.we-meet.online`.
|
|
181
|
+
* REST paths are appended verbatim (e.g. `${baseURL}/v1/conversations`).
|
|
182
|
+
*/
|
|
183
|
+
baseURL: string;
|
|
184
|
+
/**
|
|
185
|
+
* Returns a fresh IM token. Called on first use and whenever the cached token
|
|
186
|
+
* yields a 401. The implementation MAY itself hit the host application's
|
|
187
|
+
* /api/v1.0/im/token bridge.
|
|
188
|
+
*/
|
|
189
|
+
tokenProvider: () => Promise<string>;
|
|
190
|
+
/**
|
|
191
|
+
* Optional override for the underlying fetch (test seam / Node polyfill).
|
|
192
|
+
* Defaults to global fetch.
|
|
193
|
+
*/
|
|
194
|
+
fetch?: typeof fetch;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Minimal typed REST client for jusi-light-im's /v1/* endpoints.
|
|
198
|
+
*
|
|
199
|
+
* Caches a single Bearer token between calls. On HTTP 401 the cached token is
|
|
200
|
+
* dropped, tokenProvider is invoked once for a fresh token, and the request is
|
|
201
|
+
* retried exactly once. A second 401 surfaces as AuthError.
|
|
202
|
+
*/
|
|
203
|
+
declare class RestClient {
|
|
204
|
+
private readonly baseURL;
|
|
205
|
+
private readonly tokenProvider;
|
|
206
|
+
private readonly fetchImpl;
|
|
207
|
+
private cachedToken;
|
|
208
|
+
constructor(opts: RestClientOptions);
|
|
209
|
+
/** Drop the cached token; next request will call tokenProvider again. */
|
|
210
|
+
invalidateToken(): void;
|
|
211
|
+
listConversations(): Promise<ConversationSummary[]>;
|
|
212
|
+
listMessages(cid: string, opts?: {
|
|
213
|
+
beforeSeq?: number;
|
|
214
|
+
limit?: number;
|
|
215
|
+
}): Promise<MessagesResponse>;
|
|
216
|
+
markRead(cid: string, seq: number): Promise<void>;
|
|
217
|
+
private getToken;
|
|
218
|
+
private request;
|
|
219
|
+
private doFetch;
|
|
220
|
+
private parseBody;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface WsClientOptions {
|
|
224
|
+
/** wss://im... — caller MUST omit the token query param; WsClient appends it on connect(). */
|
|
225
|
+
url: string;
|
|
226
|
+
/** Factory override (tests inject mock-socket). Defaults to `new WebSocket(url)`. */
|
|
227
|
+
wsFactory?: (url: string) => WebSocket;
|
|
228
|
+
/** Per-message ack deadline in ms (default 10_000). */
|
|
229
|
+
ackTimeoutMs?: number;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Low-level WebSocket wrapper.
|
|
233
|
+
*
|
|
234
|
+
* Responsibilities:
|
|
235
|
+
* - own one WebSocket instance through connect()/disconnect()
|
|
236
|
+
* - parse incoming frames as JSON and route to the right listener
|
|
237
|
+
* - track pending acks for client → server msg frames
|
|
238
|
+
*
|
|
239
|
+
* Does NOT:
|
|
240
|
+
* - reconnect on close (that's Client's job — close events bubble up)
|
|
241
|
+
* - know anything about backoff / token refresh
|
|
242
|
+
* - cache messages while disconnected (caller should queue at a higher layer)
|
|
243
|
+
*/
|
|
244
|
+
declare class WsClient {
|
|
245
|
+
private readonly baseURL;
|
|
246
|
+
private readonly wsFactory;
|
|
247
|
+
private readonly ackTimeoutMs;
|
|
248
|
+
private ws;
|
|
249
|
+
private pendingAcks;
|
|
250
|
+
private idCounter;
|
|
251
|
+
private readonly openEmit;
|
|
252
|
+
private readonly closeEmit;
|
|
253
|
+
private readonly frameEmit;
|
|
254
|
+
private readonly errorEmit;
|
|
255
|
+
constructor(opts: WsClientOptions);
|
|
256
|
+
/** True when the underlying socket is OPEN. */
|
|
257
|
+
get isOpen(): boolean;
|
|
258
|
+
/**
|
|
259
|
+
* Open a WebSocket with the given token appended as ?token=. Resolves when the
|
|
260
|
+
* native onopen fires; rejects on transport error (does NOT wait for the
|
|
261
|
+
* server-side "connected" system frame — that's Client's higher-level concern).
|
|
262
|
+
*/
|
|
263
|
+
connect(token: string): Promise<void>;
|
|
264
|
+
/** Send any frame. Throws NetworkError if the socket is not open. */
|
|
265
|
+
send(frame: Frame): void;
|
|
266
|
+
/**
|
|
267
|
+
* Send a TypeMsg frame and resolve when the matching ack arrives (matched on
|
|
268
|
+
* client_msg_id). Rejects with TimeoutError if no ack within ackTimeoutMs, or
|
|
269
|
+
* with NetworkError if the connection drops first.
|
|
270
|
+
*/
|
|
271
|
+
sendMsg(payload: MsgInPayload): Promise<AckPayload>;
|
|
272
|
+
/** Close the socket and reject any pending acks. Idempotent. */
|
|
273
|
+
disconnect(code?: number, reason?: string): void;
|
|
274
|
+
onOpen(cb: () => void): Unsubscribe;
|
|
275
|
+
onClose(cb: Listener<{
|
|
276
|
+
code: number;
|
|
277
|
+
reason: string;
|
|
278
|
+
}>): Unsubscribe;
|
|
279
|
+
onFrame(cb: Listener<Frame>): Unsubscribe;
|
|
280
|
+
onError(cb: Listener<unknown>): Unsubscribe;
|
|
281
|
+
private handleMessage;
|
|
282
|
+
private failPendingAcks;
|
|
283
|
+
private generateClientMsgId;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
interface ClientOptions {
|
|
287
|
+
/** Origin of jusi-light-im (no trailing slash). */
|
|
288
|
+
baseURL: string;
|
|
289
|
+
/**
|
|
290
|
+
* Returns a fresh IM token. Invoked on first connect and on 401 / auth_failed retry.
|
|
291
|
+
*/
|
|
292
|
+
tokenProvider: () => Promise<string>;
|
|
293
|
+
/**
|
|
294
|
+
* Optional explicit WS URL. If omitted, derived from baseURL by swapping the scheme
|
|
295
|
+
* (http → ws, https → wss) and appending /v1/ws.
|
|
296
|
+
*/
|
|
297
|
+
wsURL?: string;
|
|
298
|
+
/** Override the WS factory (test seam — pass mock-socket here). */
|
|
299
|
+
wsFactory?: WsClientOptions['wsFactory'];
|
|
300
|
+
/** Override fetch (test seam). */
|
|
301
|
+
fetch?: typeof fetch;
|
|
302
|
+
/** Reconnect backoff parameters (default: base 1s, max 30s, factor 2, jitter 0.3). */
|
|
303
|
+
backoff?: BackoffOptions;
|
|
304
|
+
/** Called on every state transition. */
|
|
305
|
+
onStateChange?: Listener<ConnectionState>;
|
|
306
|
+
/** Per-message ack deadline in ms. */
|
|
307
|
+
ackTimeoutMs?: number;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* High-level facade: combines REST + WS with a self-managing reconnect loop.
|
|
311
|
+
*
|
|
312
|
+
* Lifecycle:
|
|
313
|
+
* 1. connect() — transitions disconnected → connecting → connected (or auth_failed)
|
|
314
|
+
* 2. on close (network drop) — connected → reconnecting, schedule backoff retry
|
|
315
|
+
* 3. on 401 during reconnect — single tokenProvider refresh; second 401 → auth_failed
|
|
316
|
+
* 4. disconnect() — terminal, cancels pending reconnect, transitions → disconnected
|
|
317
|
+
*
|
|
318
|
+
* Send / fetch APIs always operate against the most recently established connection.
|
|
319
|
+
* Calls made while disconnected throw NetworkError; callers should await connect() first
|
|
320
|
+
* or listen to onStateChange.
|
|
321
|
+
*/
|
|
322
|
+
declare class Client {
|
|
323
|
+
private readonly opts;
|
|
324
|
+
private readonly rest;
|
|
325
|
+
private readonly wsURL;
|
|
326
|
+
private readonly backoff;
|
|
327
|
+
private readonly stateEmit;
|
|
328
|
+
private readonly msgEmit;
|
|
329
|
+
private readonly readEmit;
|
|
330
|
+
private readonly systemEmit;
|
|
331
|
+
private state_;
|
|
332
|
+
private ws;
|
|
333
|
+
private currentToken;
|
|
334
|
+
private reconnectTimer;
|
|
335
|
+
private explicitlyDisconnected;
|
|
336
|
+
constructor(opts: ClientOptions);
|
|
337
|
+
get state(): ConnectionState;
|
|
338
|
+
connect(): Promise<void>;
|
|
339
|
+
disconnect(): void;
|
|
340
|
+
sendText(cid: string, body: string, opts?: {
|
|
341
|
+
contentType?: string;
|
|
342
|
+
clientMsgId?: string;
|
|
343
|
+
}): Promise<AckPayload>;
|
|
344
|
+
loadHistory(cid: string, opts?: {
|
|
345
|
+
beforeSeq?: number;
|
|
346
|
+
limit?: number;
|
|
347
|
+
}): Promise<MessagesResponse>;
|
|
348
|
+
markRead(cid: string, seq: number): Promise<void>;
|
|
349
|
+
listConversations(): Promise<ConversationSummary[]>;
|
|
350
|
+
onMessage(cb: Listener<MsgOutPayload>): Unsubscribe;
|
|
351
|
+
onRead(cb: Listener<ReadOutPayload>): Unsubscribe;
|
|
352
|
+
onSystem(cb: Listener<SystemPayload>): Unsubscribe;
|
|
353
|
+
onStateChange(cb: Listener<ConnectionState>): Unsubscribe;
|
|
354
|
+
private deriveWsURL;
|
|
355
|
+
private transitionTo;
|
|
356
|
+
private fetchToken;
|
|
357
|
+
/**
|
|
358
|
+
* Try to open one WebSocket. On 401-style failure, retries once with a fresh token
|
|
359
|
+
* (when allowRefresh is true). Updates state.
|
|
360
|
+
*/
|
|
361
|
+
private openOnce;
|
|
362
|
+
private looksLikeAuthFailure;
|
|
363
|
+
private openWith;
|
|
364
|
+
private dispatchFrame;
|
|
365
|
+
private handleClose;
|
|
366
|
+
private scheduleReconnect;
|
|
367
|
+
private tryReconnect;
|
|
368
|
+
private cancelReconnect;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export { type AckPayload, AuthError, Backoff, type BackoffOptions, Client, type ClientOptions, type ConnectionState, type ConversationSummary, type ConversationType, Emitter, type Frame, FrameType, type FrameTypeValue, IMError, type Listener, type Message, type MessagesResponse, type MsgInPayload, type MsgOutPayload, NetworkError, ProtocolError, type ReadInPayload, type ReadOutPayload, RestClient, type RestClientOptions, type SystemPayload, TimeoutError, type Unsubscribe, WsClient, type WsClientOptions };
|