@afterlink/browser 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +47 -0
- package/src/client.js +440 -0
- package/src/index.js +3 -0
- package/types/index.d.ts +93 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@afterlink/browser",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "AfterLink Browser Client - WebSocket transport for browser environments",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "types/index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/AJAYMYTH/AfterLink.git",
|
|
11
|
+
"directory": "packages/browser"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"afterlink",
|
|
15
|
+
"browser",
|
|
16
|
+
"websocket",
|
|
17
|
+
"client",
|
|
18
|
+
"protocol"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"src/**/*.js",
|
|
22
|
+
"types/**/*.d.ts",
|
|
23
|
+
"dist/**/*.js",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node scripts/build.js",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@afterlink/core": "workspace:*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@afterlink/server": "workspace:*",
|
|
38
|
+
"esbuild": "^0.24.0",
|
|
39
|
+
"vitest": "^2.1.0",
|
|
40
|
+
"ws": "^8.18.0"
|
|
41
|
+
},
|
|
42
|
+
"browser": {
|
|
43
|
+
"net": false,
|
|
44
|
+
"tls": false,
|
|
45
|
+
"zlib": false
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
const { Frame, FrameTypes, Serializer, compression } = require('@afterlink/core');
|
|
2
|
+
|
|
3
|
+
class BrowserClient {
|
|
4
|
+
constructor(url, options = {}) {
|
|
5
|
+
this.url = url;
|
|
6
|
+
this.options = {
|
|
7
|
+
autoReconnect: true,
|
|
8
|
+
maxReconnectAttempts: 10,
|
|
9
|
+
reconnectDelay: 1000,
|
|
10
|
+
timeout: 30000,
|
|
11
|
+
pingInterval: 30000,
|
|
12
|
+
protocols: ['afterlink'],
|
|
13
|
+
...options,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
this.ws = null;
|
|
17
|
+
this._connected = false;
|
|
18
|
+
this._connecting = false;
|
|
19
|
+
this._reconnectAttempts = 0;
|
|
20
|
+
this._reconnectTimer = null;
|
|
21
|
+
this._pingTimer = null;
|
|
22
|
+
this._pending = new Map();
|
|
23
|
+
this._handlers = new Map();
|
|
24
|
+
this._eventListeners = new Map();
|
|
25
|
+
this._msgId = 0;
|
|
26
|
+
this.sessionId = null;
|
|
27
|
+
this._serverClosing = false;
|
|
28
|
+
this._compression = { enabled: false, algorithm: 'none', level: 6, threshold: 1024 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_nextId() {
|
|
32
|
+
return (++this._msgId) >>> 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
connect() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
if (this._connected) {
|
|
38
|
+
return reject(new Error('Already connected'));
|
|
39
|
+
}
|
|
40
|
+
if (this._connecting) {
|
|
41
|
+
return reject(new Error('Connection in progress'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this._connecting = true;
|
|
45
|
+
this._serverClosing = false;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
this.ws = new WebSocket(this.url, this.options.protocols);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
this._connecting = false;
|
|
51
|
+
return reject(err);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.ws.binaryType = 'arraybuffer';
|
|
55
|
+
|
|
56
|
+
this.ws.onopen = () => {
|
|
57
|
+
this._connecting = false;
|
|
58
|
+
this._connected = true;
|
|
59
|
+
this._reconnectAttempts = 0;
|
|
60
|
+
this._doHandshake().then(resolve).catch((err) => {
|
|
61
|
+
this._connected = false;
|
|
62
|
+
reject(err);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.ws.onmessage = (event) => {
|
|
67
|
+
this._handleData(event.data);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this.ws.onclose = (event) => {
|
|
71
|
+
this._onDisconnect(event);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.ws.onerror = (err) => {
|
|
75
|
+
if (!this._connected) {
|
|
76
|
+
this._connecting = false;
|
|
77
|
+
reject(new Error(`WebSocket connection failed: ${err.message || 'unknown error'}`));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async _doHandshake() {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const id = this._nextId();
|
|
86
|
+
const capabilities = ['streaming', 'pubsub'];
|
|
87
|
+
|
|
88
|
+
const payload = Serializer.encode({
|
|
89
|
+
version: 'AL/1.1',
|
|
90
|
+
auth: this.options.auth || null,
|
|
91
|
+
capabilities,
|
|
92
|
+
compression: this.options.compression?.enabled
|
|
93
|
+
? (this.options.compression.algorithm || 'zlib')
|
|
94
|
+
: 'none',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const frame = Frame.encode(FrameTypes.HELLO, 0, id, payload);
|
|
98
|
+
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
this._pending.delete(id);
|
|
101
|
+
reject(new Error('Handshake timed out'));
|
|
102
|
+
}, 5000);
|
|
103
|
+
|
|
104
|
+
this._pending.set(id, {
|
|
105
|
+
resolve: (data) => {
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
this.sessionId = data.session_id;
|
|
108
|
+
const negotiated = data.compression || 'none';
|
|
109
|
+
this._compression.enabled = negotiated !== 'none';
|
|
110
|
+
this._compression.algorithm = negotiated;
|
|
111
|
+
resolve(data);
|
|
112
|
+
},
|
|
113
|
+
reject: (err) => {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
reject(err);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this.ws.send(frame, { binary: true });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_handleData(data) {
|
|
124
|
+
const buffer = data instanceof ArrayBuffer ? Buffer.from(data) : (Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
125
|
+
const frame = Frame.decode(buffer);
|
|
126
|
+
if (!frame) return;
|
|
127
|
+
|
|
128
|
+
this._handleFrame(frame);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_handleFrame(frame) {
|
|
132
|
+
const { type, messageId, payload, flags } = frame;
|
|
133
|
+
|
|
134
|
+
let decodedPayload = payload;
|
|
135
|
+
if (compression.isCompressed(flags)) {
|
|
136
|
+
try {
|
|
137
|
+
decodedPayload = compression.decompress(payload, true, this._compression.algorithm);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
this._reject(messageId, new Error(`Failed to decompress: ${err.message}`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
switch (type) {
|
|
145
|
+
case FrameTypes.RESPONSE: {
|
|
146
|
+
try {
|
|
147
|
+
const data = Serializer.decode(decodedPayload);
|
|
148
|
+
this._resolve(messageId, data.body || data);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
this._reject(messageId, new Error(`Failed to decode response: ${err.message}`));
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case FrameTypes.HELLO_ACK: {
|
|
155
|
+
try {
|
|
156
|
+
const data = Serializer.decode(decodedPayload);
|
|
157
|
+
this._resolve(messageId, data);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
this._reject(messageId, new Error(`Failed to decode handshake: ${err.message}`));
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case FrameTypes.ERROR: {
|
|
164
|
+
const { fromFramePayload } = require('@afterlink/core/errors');
|
|
165
|
+
const err = fromFramePayload(decodedPayload, messageId);
|
|
166
|
+
this._reject(messageId, err);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case FrameTypes.PUBLISH: {
|
|
170
|
+
try {
|
|
171
|
+
const { topic, data } = Serializer.decode(decodedPayload);
|
|
172
|
+
const handlers = this._handlers.get(topic);
|
|
173
|
+
if (handlers) {
|
|
174
|
+
for (const handler of handlers) {
|
|
175
|
+
try {
|
|
176
|
+
handler(data);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`[AfterLink] Handler error for topic '${topic}':`, err.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
this._emit('message', { topic, data });
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore malformed publish frames
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case FrameTypes.PING: {
|
|
189
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
190
|
+
const pongFrame = Frame.encode(FrameTypes.PONG, 0, 0, Buffer.alloc(0));
|
|
191
|
+
this.ws.send(pongFrame, { binary: true });
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case FrameTypes.PONG:
|
|
196
|
+
break;
|
|
197
|
+
case FrameTypes.SERVER_CLOSING: {
|
|
198
|
+
try {
|
|
199
|
+
const data = Serializer.decode(decodedPayload);
|
|
200
|
+
this._emit('server-closing', data);
|
|
201
|
+
this._serverClosing = true;
|
|
202
|
+
} catch {
|
|
203
|
+
// Ignore malformed frames
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
request(route, body = {}) {
|
|
211
|
+
if (!this._connected) throw new Error('Not connected');
|
|
212
|
+
|
|
213
|
+
const id = this._nextId();
|
|
214
|
+
const payload = Serializer.encode({ route, body });
|
|
215
|
+
const frame = Frame.encode(FrameTypes.REQUEST, 0, id, payload);
|
|
216
|
+
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
this._pending.set(id, { resolve, reject });
|
|
219
|
+
this.ws.send(frame, { binary: true });
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
subscribe(topic, handler) {
|
|
224
|
+
if (!this._connected) throw new Error('Not connected');
|
|
225
|
+
if (typeof handler !== 'function') throw new TypeError('Handler must be a function');
|
|
226
|
+
|
|
227
|
+
const id = this._nextId();
|
|
228
|
+
const payload = Serializer.encode({ topic });
|
|
229
|
+
const frame = Frame.encode(FrameTypes.SUBSCRIBE, 0, id, payload);
|
|
230
|
+
|
|
231
|
+
if (!this._handlers.has(topic)) {
|
|
232
|
+
this._handlers.set(topic, new Set());
|
|
233
|
+
}
|
|
234
|
+
this._handlers.get(topic).add(handler);
|
|
235
|
+
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
this._pending.set(id, { resolve, reject });
|
|
238
|
+
this.ws.send(frame, { binary: true });
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
unsubscribe(topic) {
|
|
243
|
+
if (!this._connected) throw new Error('Not connected');
|
|
244
|
+
|
|
245
|
+
this._handlers.delete(topic);
|
|
246
|
+
|
|
247
|
+
const id = this._nextId();
|
|
248
|
+
const payload = Serializer.encode({ topic });
|
|
249
|
+
const frame = Frame.encode(FrameTypes.UNSUBSCRIBE, 0, id, payload);
|
|
250
|
+
this.ws.send(frame, { binary: true });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
publish(topic, data) {
|
|
254
|
+
if (!this._connected) throw new Error('Not connected');
|
|
255
|
+
|
|
256
|
+
const payload = Serializer.encode({ topic, data });
|
|
257
|
+
const frame = Frame.encode(FrameTypes.PUBLISH, 0, 0, payload);
|
|
258
|
+
this.ws.send(frame, { binary: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
disconnect() {
|
|
262
|
+
this._clearReconnect();
|
|
263
|
+
this._stopPingInterval();
|
|
264
|
+
|
|
265
|
+
if (!this._connected && !this._connecting) return Promise.resolve();
|
|
266
|
+
|
|
267
|
+
this._pending.clear();
|
|
268
|
+
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
271
|
+
this._cleanup();
|
|
272
|
+
return resolve();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const id = this._nextId();
|
|
276
|
+
const frame = Frame.encode(FrameTypes.CLOSE, 0, id, Buffer.alloc(0));
|
|
277
|
+
|
|
278
|
+
let resolved = false;
|
|
279
|
+
const timeout = setTimeout(() => {
|
|
280
|
+
if (!resolved) {
|
|
281
|
+
resolved = true;
|
|
282
|
+
this.ws.close();
|
|
283
|
+
this._cleanup();
|
|
284
|
+
resolve();
|
|
285
|
+
}
|
|
286
|
+
}, 2000);
|
|
287
|
+
|
|
288
|
+
this.ws.onclose = () => {
|
|
289
|
+
if (!resolved) {
|
|
290
|
+
resolved = true;
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
this._cleanup();
|
|
293
|
+
resolve();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
this.ws.send(frame, { binary: true });
|
|
299
|
+
} catch {
|
|
300
|
+
this.ws.close();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_resolve(id, data) {
|
|
306
|
+
const pending = this._pending.get(id);
|
|
307
|
+
if (pending) {
|
|
308
|
+
this._pending.delete(id);
|
|
309
|
+
pending.resolve(data);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_reject(id, err) {
|
|
314
|
+
const pending = this._pending.get(id);
|
|
315
|
+
if (pending) {
|
|
316
|
+
this._pending.delete(id);
|
|
317
|
+
pending.reject(err);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_onDisconnect(event) {
|
|
322
|
+
this._connected = false;
|
|
323
|
+
this._connecting = false;
|
|
324
|
+
this._stopPingInterval();
|
|
325
|
+
this._pending.clear();
|
|
326
|
+
this._emit('disconnected', {
|
|
327
|
+
graceful: event.wasClean,
|
|
328
|
+
reason: event.reason,
|
|
329
|
+
code: event.code,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (this.options.autoReconnect && this._reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
333
|
+
this._scheduleReconnect();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
_scheduleReconnect() {
|
|
338
|
+
this._clearReconnect();
|
|
339
|
+
|
|
340
|
+
this._reconnectAttempts++;
|
|
341
|
+
const delay = Math.min(
|
|
342
|
+
this.options.reconnectDelay * Math.pow(2, this._reconnectAttempts - 1),
|
|
343
|
+
30000
|
|
344
|
+
);
|
|
345
|
+
const jitter = Math.random() * delay * 0.3;
|
|
346
|
+
|
|
347
|
+
this._emit('reconnecting', { attempt: this._reconnectAttempts, delay: delay + jitter });
|
|
348
|
+
|
|
349
|
+
this._reconnectTimer = setTimeout(async () => {
|
|
350
|
+
try {
|
|
351
|
+
await this.connect();
|
|
352
|
+
this._emit('reconnected');
|
|
353
|
+
} catch {
|
|
354
|
+
this._scheduleReconnect();
|
|
355
|
+
}
|
|
356
|
+
}, delay + jitter);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
_clearReconnect() {
|
|
360
|
+
if (this._reconnectTimer) {
|
|
361
|
+
clearTimeout(this._reconnectTimer);
|
|
362
|
+
this._reconnectTimer = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_cleanup() {
|
|
367
|
+
this._connected = false;
|
|
368
|
+
this._connecting = false;
|
|
369
|
+
this._stopPingInterval();
|
|
370
|
+
this._clearReconnect();
|
|
371
|
+
this._pending.clear();
|
|
372
|
+
this.ws = null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_startPingInterval() {
|
|
376
|
+
this._stopPingInterval();
|
|
377
|
+
this._pingTimer = setInterval(() => {
|
|
378
|
+
if (this._connected && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
379
|
+
try {
|
|
380
|
+
const frame = Frame.encode(FrameTypes.PING, 0, 0, Buffer.alloc(0));
|
|
381
|
+
this.ws.send(frame, { binary: true });
|
|
382
|
+
} catch {
|
|
383
|
+
// Socket may have closed between checks
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}, this.options.pingInterval);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_stopPingInterval() {
|
|
390
|
+
if (this._pingTimer) {
|
|
391
|
+
clearInterval(this._pingTimer);
|
|
392
|
+
this._pingTimer = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
on(event, listener) {
|
|
397
|
+
if (!this._eventListeners.has(event)) {
|
|
398
|
+
this._eventListeners.set(event, new Set());
|
|
399
|
+
}
|
|
400
|
+
this._eventListeners.get(event).add(listener);
|
|
401
|
+
|
|
402
|
+
// Start ping interval on connect
|
|
403
|
+
if (event === 'connected') {
|
|
404
|
+
this._startPingInterval();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
off(event, listener) {
|
|
411
|
+
const listeners = this._eventListeners.get(event);
|
|
412
|
+
if (listeners) {
|
|
413
|
+
listeners.delete(listener);
|
|
414
|
+
}
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
_emit(event, data) {
|
|
419
|
+
const listeners = this._eventListeners.get(event);
|
|
420
|
+
if (listeners) {
|
|
421
|
+
for (const listener of listeners) {
|
|
422
|
+
try {
|
|
423
|
+
listener(data);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error(`[AfterLink] Event listener error for '${event}':`, err.message);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
isConnected() {
|
|
432
|
+
return this._connected;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
getSessionId() {
|
|
436
|
+
return this.sessionId;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = { Client: BrowserClient };
|
package/src/index.js
ADDED
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { CompressionOptions, AfterLinkError } from '@afterlink/core/types/errors';
|
|
2
|
+
|
|
3
|
+
export interface BrowserClientOptions {
|
|
4
|
+
autoReconnect?: boolean;
|
|
5
|
+
maxReconnectAttempts?: number;
|
|
6
|
+
reconnectDelay?: number;
|
|
7
|
+
timeout?: number;
|
|
8
|
+
pingInterval?: number;
|
|
9
|
+
protocols?: string[];
|
|
10
|
+
auth?: string;
|
|
11
|
+
compression?: CompressionOptions;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RequestOptions {
|
|
15
|
+
timeout?: number;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ReconnectInfo {
|
|
20
|
+
attempt: number;
|
|
21
|
+
delay: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DisconnectInfo {
|
|
25
|
+
graceful: boolean;
|
|
26
|
+
reason?: string;
|
|
27
|
+
code?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ClosingInfo {
|
|
31
|
+
drainTimeout: number;
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class Client {
|
|
36
|
+
constructor(url: string, options?: BrowserClientOptions);
|
|
37
|
+
|
|
38
|
+
connect(): Promise<void>;
|
|
39
|
+
disconnect(): Promise<void>;
|
|
40
|
+
isConnected(): boolean;
|
|
41
|
+
|
|
42
|
+
request(route: string, payload?: unknown, options?: RequestOptions): Promise<unknown>;
|
|
43
|
+
|
|
44
|
+
subscribe(topic: string, handler: (data: unknown) => void): Promise<void>;
|
|
45
|
+
unsubscribe(topic: string): void;
|
|
46
|
+
publish(topic: string, data: unknown): void;
|
|
47
|
+
|
|
48
|
+
on(event: 'connected', handler: () => void): this;
|
|
49
|
+
on(event: 'disconnected', handler: (info: DisconnectInfo) => void): this;
|
|
50
|
+
on(event: 'reconnecting', handler: (info: ReconnectInfo) => void): this;
|
|
51
|
+
on(event: 'reconnected', handler: () => void): this;
|
|
52
|
+
on(event: 'server-closing', handler: (info: ClosingInfo) => void): this;
|
|
53
|
+
on(event: 'error', handler: (err: AfterLinkError) => void): this;
|
|
54
|
+
on(event: 'message', handler: (data: { topic: string; data: unknown }) => void): this;
|
|
55
|
+
|
|
56
|
+
off(event: string, listener: (...args: unknown[]) => void): this;
|
|
57
|
+
|
|
58
|
+
getSessionId(): string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Error Re-exports ──────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
AfterLinkError,
|
|
65
|
+
ProtocolError,
|
|
66
|
+
InvalidFrameError,
|
|
67
|
+
UnsupportedVersionError,
|
|
68
|
+
MalformedPayloadError,
|
|
69
|
+
UnknownFrameTypeError,
|
|
70
|
+
AuthError,
|
|
71
|
+
AuthRequiredError,
|
|
72
|
+
AuthFailedError,
|
|
73
|
+
AuthExpiredError,
|
|
74
|
+
AuthInsufficientPermissionsError,
|
|
75
|
+
RouteError,
|
|
76
|
+
RouteNotFoundError,
|
|
77
|
+
RouteHandlerError,
|
|
78
|
+
RouteTimeoutError,
|
|
79
|
+
ValidationError,
|
|
80
|
+
RateLimitError,
|
|
81
|
+
ConnectionError,
|
|
82
|
+
ConnectionRefusedError,
|
|
83
|
+
ConnectionTimeoutError,
|
|
84
|
+
ConnectionResetError,
|
|
85
|
+
TLSHandshakeFailedError,
|
|
86
|
+
TLSCertInvalidError,
|
|
87
|
+
ServerError,
|
|
88
|
+
InternalServerErrorError,
|
|
89
|
+
ServerShuttingDownError,
|
|
90
|
+
CompressionError,
|
|
91
|
+
DecompressionFailedError,
|
|
92
|
+
CompressionAlgorithmUnsupportedError,
|
|
93
|
+
} from '@afterlink/core/types/errors';
|