blink-trade-sdk 0.1.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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +130 -0
- package/dist/managed-ws.d.ts +135 -0
- package/dist/managed-ws.js +475 -0
- package/dist/types.d.ts +288 -0
- package/dist/types.js +2 -0
- package/dist/ws-transport.d.ts +50 -0
- package/dist/ws-transport.js +118 -0
- package/dist/ws.d.ts +328 -0
- package/dist/ws.js +482 -0
- package/package.json +35 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { WsTransport } from './ws-transport.js';
|
|
2
|
+
import { WS_PROTOCOL_VERSION, WsSession } from './ws.js';
|
|
3
|
+
/**
|
|
4
|
+
* Gateway websocket protocol v1 (`/v1/ws`, docs/v1/websocket.md).
|
|
5
|
+
*
|
|
6
|
+
* One physical socket per tab. Channel strings carry their own parameters and
|
|
7
|
+
* are both the server subscription identity and the client routing key, so a
|
|
8
|
+
* channel is subscribed at most once no matter how many consumers share it.
|
|
9
|
+
*/
|
|
10
|
+
export const BLINK_WS_PROTOCOL_VERSION = WS_PROTOCOL_VERSION;
|
|
11
|
+
const MAX_RECONNECT_MS = 10_000;
|
|
12
|
+
/** A subscribed channel that has not produced its snapshot by then is replaced. */
|
|
13
|
+
const SNAPSHOT_TIMEOUT_MS = 15_000;
|
|
14
|
+
const WATCHDOG_MS = 5_000;
|
|
15
|
+
/** Delay before re-subscribing a channel the server closed with `503`. */
|
|
16
|
+
const STREAM_RETRY_MS = 1_000;
|
|
17
|
+
/** Used until the server hello advertises its own keepalive contract. */
|
|
18
|
+
const DEFAULT_KEEPALIVE_TIMEOUT_MS = 120_000;
|
|
19
|
+
const SOCKET_CONNECTING = 0;
|
|
20
|
+
const SOCKET_OPEN = 1;
|
|
21
|
+
export class BlinkSocketManager {
|
|
22
|
+
url;
|
|
23
|
+
options;
|
|
24
|
+
socket = null;
|
|
25
|
+
session = new WsSession();
|
|
26
|
+
channels = new Map();
|
|
27
|
+
statusHandlers = new Set();
|
|
28
|
+
diagnosticHandlers = new Set();
|
|
29
|
+
diagnostics = [];
|
|
30
|
+
status = 'idle';
|
|
31
|
+
connectScheduled = false;
|
|
32
|
+
reconnectTimer = null;
|
|
33
|
+
watchdogTimer = null;
|
|
34
|
+
reconnectAttempt = 0;
|
|
35
|
+
generation = 0;
|
|
36
|
+
helloAccepted = false;
|
|
37
|
+
protocolRejected = false;
|
|
38
|
+
keepaliveTimeoutMs = DEFAULT_KEEPALIVE_TIMEOUT_MS;
|
|
39
|
+
constructor(url, options = {}) {
|
|
40
|
+
this.url = url;
|
|
41
|
+
this.options = options;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Attach a consumer to a channel. The first consumer subscribes the channel
|
|
45
|
+
* on the server; further consumers share it. A consumer that attaches after
|
|
46
|
+
* the channel's snapshot was installed cannot rebuild that snapshot from
|
|
47
|
+
* deltas, so the server subscription is replaced and every consumer receives
|
|
48
|
+
* a fresh lifecycle (`reset`, then a snapshot `update`).
|
|
49
|
+
*/
|
|
50
|
+
subscribe(channel, handler) {
|
|
51
|
+
let state = this.channels.get(channel);
|
|
52
|
+
const isNew = !state;
|
|
53
|
+
if (!state) {
|
|
54
|
+
state = {
|
|
55
|
+
channel,
|
|
56
|
+
handlers: new Map(),
|
|
57
|
+
awaitingSnapshot: true,
|
|
58
|
+
subscribedAt: null,
|
|
59
|
+
retryTimer: null,
|
|
60
|
+
failed: false,
|
|
61
|
+
};
|
|
62
|
+
this.channels.set(channel, state);
|
|
63
|
+
}
|
|
64
|
+
const existingHandler = state.handlers.has(handler);
|
|
65
|
+
if (!isNew && !existingHandler && (!state.awaitingSnapshot || state.failed)) {
|
|
66
|
+
// Replace before attaching: existing consumers get the `reset`, the new
|
|
67
|
+
// one starts at the fresh snapshot.
|
|
68
|
+
this.replaceSubscription(state, state.failed ? 'retrying a rejected channel for a new consumer' : 'consumer attached after the snapshot');
|
|
69
|
+
}
|
|
70
|
+
state.handlers.set(handler, (state.handlers.get(handler) ?? 0) + 1);
|
|
71
|
+
this.ensureConnection();
|
|
72
|
+
if (isNew) {
|
|
73
|
+
// `ensureConnection` does nothing on an already connected socket, so a
|
|
74
|
+
// channel appearing later (wallet login, market switch) is sent now.
|
|
75
|
+
this.sendSubscribe(state);
|
|
76
|
+
}
|
|
77
|
+
let active = true;
|
|
78
|
+
return () => {
|
|
79
|
+
if (!active)
|
|
80
|
+
return;
|
|
81
|
+
active = false;
|
|
82
|
+
const current = this.channels.get(channel);
|
|
83
|
+
if (!current)
|
|
84
|
+
return;
|
|
85
|
+
const refs = current.handlers.get(handler) ?? 0;
|
|
86
|
+
if (refs <= 1)
|
|
87
|
+
current.handlers.delete(handler);
|
|
88
|
+
else
|
|
89
|
+
current.handlers.set(handler, refs - 1);
|
|
90
|
+
if (current.handlers.size === 0)
|
|
91
|
+
this.removeChannel(current);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Replace one channel's server subscription so it re-installs a snapshot. */
|
|
95
|
+
resubscribe(channel, reason) {
|
|
96
|
+
const state = this.channels.get(channel);
|
|
97
|
+
if (!state)
|
|
98
|
+
return;
|
|
99
|
+
this.replaceSubscription(state, reason);
|
|
100
|
+
}
|
|
101
|
+
onStatus(handler) {
|
|
102
|
+
this.statusHandlers.add(handler);
|
|
103
|
+
handler(this.status);
|
|
104
|
+
return () => this.statusHandlers.delete(handler);
|
|
105
|
+
}
|
|
106
|
+
onDiagnostic(handler) {
|
|
107
|
+
this.diagnosticHandlers.add(handler);
|
|
108
|
+
return () => this.diagnosticHandlers.delete(handler);
|
|
109
|
+
}
|
|
110
|
+
getDiagnostics() {
|
|
111
|
+
return this.diagnostics;
|
|
112
|
+
}
|
|
113
|
+
getStatus() {
|
|
114
|
+
return this.status;
|
|
115
|
+
}
|
|
116
|
+
/** Exposed for diagnostics and focused unit tests. */
|
|
117
|
+
debugState() {
|
|
118
|
+
let awaitingSnapshot = 0;
|
|
119
|
+
for (const state of this.channels.values()) {
|
|
120
|
+
if (state.awaitingSnapshot)
|
|
121
|
+
awaitingSnapshot += 1;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
channels: this.channels.size,
|
|
125
|
+
awaitingSnapshot,
|
|
126
|
+
keepaliveTimeoutMs: this.keepaliveTimeoutMs,
|
|
127
|
+
status: this.status,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
dispose() {
|
|
131
|
+
this.protocolRejected = true;
|
|
132
|
+
this.clearTimers();
|
|
133
|
+
for (const state of this.channels.values())
|
|
134
|
+
this.clearRetry(state);
|
|
135
|
+
this.channels.clear();
|
|
136
|
+
this.generation += 1;
|
|
137
|
+
this.socket?.close(1000, 'manager disposed');
|
|
138
|
+
this.socket = null;
|
|
139
|
+
this.helloAccepted = false;
|
|
140
|
+
this.setStatus('idle');
|
|
141
|
+
}
|
|
142
|
+
ensureConnection() {
|
|
143
|
+
if (this.protocolRejected ||
|
|
144
|
+
this.channels.size === 0 ||
|
|
145
|
+
this.socket ||
|
|
146
|
+
this.connectScheduled ||
|
|
147
|
+
this.reconnectTimer) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.connectScheduled = true;
|
|
151
|
+
queueMicrotask(() => {
|
|
152
|
+
this.connectScheduled = false;
|
|
153
|
+
if (this.channels.size > 0)
|
|
154
|
+
this.connect();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
connect() {
|
|
158
|
+
if (this.socket || this.protocolRejected || this.channels.size === 0)
|
|
159
|
+
return;
|
|
160
|
+
const generation = ++this.generation;
|
|
161
|
+
let socket;
|
|
162
|
+
try {
|
|
163
|
+
socket = new WsTransport(this.url, {
|
|
164
|
+
...this.options,
|
|
165
|
+
onMessage: (frame) => {
|
|
166
|
+
if (generation === this.generation)
|
|
167
|
+
this.handleMessage(frame);
|
|
168
|
+
},
|
|
169
|
+
onError: (error) => {
|
|
170
|
+
if (generation === this.generation)
|
|
171
|
+
this.addDiagnostic('warning', 'SOCKET_ERROR', 'WebSocket transport error', error);
|
|
172
|
+
},
|
|
173
|
+
onDiagnostic: (code, message, detail) => {
|
|
174
|
+
if (generation === this.generation)
|
|
175
|
+
this.addDiagnostic('warning', code, message, detail);
|
|
176
|
+
},
|
|
177
|
+
onProtocolError: (error) => {
|
|
178
|
+
if (generation === this.generation)
|
|
179
|
+
this.rejectProtocol(error.message, error);
|
|
180
|
+
},
|
|
181
|
+
onClose: (event) => this.handleClose(generation, event),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.addDiagnostic('error', 'CONNECT_FAILED', 'WebSocket creation failed', error);
|
|
186
|
+
this.scheduleReconnect();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.socket = socket;
|
|
190
|
+
this.session = new WsSession();
|
|
191
|
+
this.helloAccepted = false;
|
|
192
|
+
this.setStatus(this.reconnectAttempt > 0 ? 'reconnecting' : 'connecting');
|
|
193
|
+
}
|
|
194
|
+
handleClose(generation, event) {
|
|
195
|
+
if (generation !== this.generation)
|
|
196
|
+
return;
|
|
197
|
+
this.socket = null;
|
|
198
|
+
this.helloAccepted = false;
|
|
199
|
+
this.stopWatchdog();
|
|
200
|
+
for (const state of this.channels.values()) {
|
|
201
|
+
this.clearRetry(state);
|
|
202
|
+
state.awaitingSnapshot = true;
|
|
203
|
+
state.subscribedAt = null;
|
|
204
|
+
state.failed = false;
|
|
205
|
+
this.dispatch(state, {
|
|
206
|
+
type: 'reset',
|
|
207
|
+
channel: state.channel,
|
|
208
|
+
reason: 'socket closed',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (this.protocolRejected)
|
|
212
|
+
return;
|
|
213
|
+
this.addDiagnostic('warning', 'SOCKET_CLOSED', 'WebSocket disconnected', {
|
|
214
|
+
code: event.code,
|
|
215
|
+
reason: event.reason,
|
|
216
|
+
});
|
|
217
|
+
this.scheduleReconnect();
|
|
218
|
+
}
|
|
219
|
+
handleMessage(frame) {
|
|
220
|
+
if (frame.type === 'hello') {
|
|
221
|
+
this.keepaliveTimeoutMs = frame.keepalive_timeout_ms;
|
|
222
|
+
this.helloAccepted = true;
|
|
223
|
+
this.reconnectAttempt = 0;
|
|
224
|
+
this.setStatus('connected');
|
|
225
|
+
this.startWatchdog();
|
|
226
|
+
for (const state of this.channels.values())
|
|
227
|
+
this.sendSubscribe(state);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
switch (frame.type) {
|
|
231
|
+
case 'update': {
|
|
232
|
+
const state = this.channels.get(frame.channel);
|
|
233
|
+
if (state)
|
|
234
|
+
this.handleUpdate(state, frame);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
case 'error':
|
|
238
|
+
this.session.ingest(frame);
|
|
239
|
+
this.handleError(frame);
|
|
240
|
+
return;
|
|
241
|
+
case 'unsubscribed':
|
|
242
|
+
this.session.ingest(frame);
|
|
243
|
+
return;
|
|
244
|
+
case 'pong':
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
handleUpdate(state, frame) {
|
|
249
|
+
// Stateful channels carry `previous_sequence`/`sequence`; stateless ones
|
|
250
|
+
// (candles, bbo) carry neither and are continuous by construction.
|
|
251
|
+
const hasSequence = 'sequence' in frame || 'previous_sequence' in frame;
|
|
252
|
+
const sequence = typeof frame.sequence === 'string' ? frame.sequence : null;
|
|
253
|
+
const previous = typeof frame.previous_sequence === 'string' ? frame.previous_sequence : null;
|
|
254
|
+
// Old in-flight deltas must not trigger repeated resubscriptions while
|
|
255
|
+
// the replacement snapshot is already pending.
|
|
256
|
+
if (state.awaitingSnapshot && hasSequence && previous !== null) {
|
|
257
|
+
this.addDiagnostic('info', 'STALE_FRAME', 'Dropping a delta that arrived before the snapshot', {
|
|
258
|
+
channel: state.channel,
|
|
259
|
+
previous_sequence: previous,
|
|
260
|
+
sequence,
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const snapshot = state.awaitingSnapshot;
|
|
265
|
+
const event = this.session.ingest(frame);
|
|
266
|
+
if (event.type === 'resubscribe_required') {
|
|
267
|
+
this.addDiagnostic('warning', 'SEQUENCE_GAP', 'Channel sequence is discontinuous', event.reason);
|
|
268
|
+
this.replaceSubscription(state, 'sequence gap');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
state.awaitingSnapshot = false;
|
|
272
|
+
state.subscribedAt = null;
|
|
273
|
+
state.failed = false;
|
|
274
|
+
const { type: _type, channel: _channel, timestamp_ns, sequence: _sequence, previous_sequence: _previous, ...data } = frame;
|
|
275
|
+
this.dispatch(state, {
|
|
276
|
+
type: 'update',
|
|
277
|
+
channel: state.channel,
|
|
278
|
+
snapshot,
|
|
279
|
+
sequence,
|
|
280
|
+
previous_sequence: previous,
|
|
281
|
+
timestamp_ns: typeof timestamp_ns === 'string' ? timestamp_ns : null,
|
|
282
|
+
data,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
handleError(frame) {
|
|
286
|
+
const code = typeof frame.code === 'number' ? frame.code : null;
|
|
287
|
+
const message = typeof frame.message === 'string' ? frame.message : 'WebSocket server error';
|
|
288
|
+
if (typeof frame.channel !== 'string') {
|
|
289
|
+
this.addDiagnostic('error', `SERVER_${code ?? 'ERROR'}`, message, frame);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const state = this.channels.get(frame.channel);
|
|
293
|
+
if (!state)
|
|
294
|
+
return;
|
|
295
|
+
if (code === 503) {
|
|
296
|
+
// The server closed only this channel (stream unavailable / indexer
|
|
297
|
+
// rollback). Re-subscribing requests a fresh snapshot.
|
|
298
|
+
this.addDiagnostic('warning', 'STREAM_UNAVAILABLE', message, {
|
|
299
|
+
channel: state.channel,
|
|
300
|
+
});
|
|
301
|
+
this.clearRetry(state);
|
|
302
|
+
state.awaitingSnapshot = true;
|
|
303
|
+
state.subscribedAt = null;
|
|
304
|
+
this.dispatch(state, {
|
|
305
|
+
type: 'reset',
|
|
306
|
+
channel: state.channel,
|
|
307
|
+
reason: 'server closed the stream',
|
|
308
|
+
});
|
|
309
|
+
state.retryTimer = (this.options.schedule ?? setTimeout)(() => {
|
|
310
|
+
state.retryTimer = null;
|
|
311
|
+
if (this.channels.get(state.channel) === state)
|
|
312
|
+
this.sendSubscribe(state);
|
|
313
|
+
}, STREAM_RETRY_MS);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (code === 409) {
|
|
317
|
+
this.replaceSubscription(state, 'server reports the channel already subscribed');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
state.failed = true;
|
|
321
|
+
state.awaitingSnapshot = true;
|
|
322
|
+
state.subscribedAt = null;
|
|
323
|
+
this.addDiagnostic('error', `SERVER_${code ?? 'ERROR'}`, message, {
|
|
324
|
+
channel: state.channel,
|
|
325
|
+
});
|
|
326
|
+
this.dispatch(state, {
|
|
327
|
+
type: 'error',
|
|
328
|
+
channel: state.channel,
|
|
329
|
+
code,
|
|
330
|
+
message,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
rejectProtocol(message, detail) {
|
|
334
|
+
this.protocolRejected = true;
|
|
335
|
+
this.addDiagnostic('error', 'PROTOCOL_MISMATCH', message, detail);
|
|
336
|
+
this.setStatus('incompatible');
|
|
337
|
+
}
|
|
338
|
+
sendSubscribe(state) {
|
|
339
|
+
if (!this.canSend())
|
|
340
|
+
return;
|
|
341
|
+
state.awaitingSnapshot = true;
|
|
342
|
+
state.failed = false;
|
|
343
|
+
state.subscribedAt = this.now();
|
|
344
|
+
this.session.subscribe(state.channel);
|
|
345
|
+
this.send({ type: 'subscribe', channel: state.channel });
|
|
346
|
+
}
|
|
347
|
+
replaceSubscription(state, reason) {
|
|
348
|
+
this.clearRetry(state);
|
|
349
|
+
this.addDiagnostic('warning', 'CLIENT_RESYNC', reason, {
|
|
350
|
+
channel: state.channel,
|
|
351
|
+
});
|
|
352
|
+
state.awaitingSnapshot = true;
|
|
353
|
+
state.failed = false;
|
|
354
|
+
state.subscribedAt = null;
|
|
355
|
+
this.dispatch(state, { type: 'reset', channel: state.channel, reason });
|
|
356
|
+
if (!this.canSend())
|
|
357
|
+
return;
|
|
358
|
+
this.session.unsubscribe(state.channel);
|
|
359
|
+
this.send({ type: 'unsubscribe', channel: state.channel });
|
|
360
|
+
this.sendSubscribe(state);
|
|
361
|
+
}
|
|
362
|
+
removeChannel(state) {
|
|
363
|
+
this.clearRetry(state);
|
|
364
|
+
this.channels.delete(state.channel);
|
|
365
|
+
if (this.canSend()) {
|
|
366
|
+
this.session.unsubscribe(state.channel);
|
|
367
|
+
this.send({ type: 'unsubscribe', channel: state.channel });
|
|
368
|
+
}
|
|
369
|
+
if (this.channels.size === 0) {
|
|
370
|
+
this.clearTimers();
|
|
371
|
+
this.generation += 1;
|
|
372
|
+
this.socket?.close(1000, 'no active subscriptions');
|
|
373
|
+
this.socket = null;
|
|
374
|
+
this.helloAccepted = false;
|
|
375
|
+
this.setStatus('idle');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
clearRetry(state) {
|
|
379
|
+
if (state.retryTimer !== null) {
|
|
380
|
+
(this.options.cancelSchedule ?? clearTimeout)(state.retryTimer);
|
|
381
|
+
state.retryTimer = null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Replace subscriptions whose snapshot never arrived. */
|
|
385
|
+
startWatchdog() {
|
|
386
|
+
this.stopWatchdog();
|
|
387
|
+
this.watchdogTimer = (this.options.repeat ?? setInterval)(() => {
|
|
388
|
+
const now = this.now();
|
|
389
|
+
for (const state of [...this.channels.values()]) {
|
|
390
|
+
if (state.awaitingSnapshot && state.subscribedAt !== null && now - state.subscribedAt > SNAPSHOT_TIMEOUT_MS) {
|
|
391
|
+
this.replaceSubscription(state, 'snapshot timeout');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}, WATCHDOG_MS);
|
|
395
|
+
}
|
|
396
|
+
stopWatchdog() {
|
|
397
|
+
if (this.watchdogTimer !== null) {
|
|
398
|
+
(this.options.cancelRepeat ?? clearInterval)(this.watchdogTimer);
|
|
399
|
+
this.watchdogTimer = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
clearTimers() {
|
|
403
|
+
this.stopWatchdog();
|
|
404
|
+
if (this.reconnectTimer !== null) {
|
|
405
|
+
(this.options.cancelSchedule ?? clearTimeout)(this.reconnectTimer);
|
|
406
|
+
this.reconnectTimer = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
scheduleReconnect() {
|
|
410
|
+
if (this.protocolRejected || this.reconnectTimer || this.channels.size === 0) {
|
|
411
|
+
if (this.channels.size === 0)
|
|
412
|
+
this.setStatus('idle');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.reconnectAttempt += 1;
|
|
416
|
+
this.setStatus('reconnecting');
|
|
417
|
+
const exponential = 250 * 2 ** Math.min(this.reconnectAttempt - 1, 8);
|
|
418
|
+
const jitter = 0.5 + (this.options.random ?? Math.random)();
|
|
419
|
+
const delay = Math.min(MAX_RECONNECT_MS, exponential * jitter);
|
|
420
|
+
this.reconnectTimer = (this.options.schedule ?? setTimeout)(() => {
|
|
421
|
+
this.reconnectTimer = null;
|
|
422
|
+
this.connect();
|
|
423
|
+
}, delay);
|
|
424
|
+
}
|
|
425
|
+
canSend() {
|
|
426
|
+
return this.helloAccepted && this.socket != null && this.socket.canSend();
|
|
427
|
+
}
|
|
428
|
+
send(value) {
|
|
429
|
+
this.socket.send(value);
|
|
430
|
+
}
|
|
431
|
+
dispatch(state, frame) {
|
|
432
|
+
// Freeze the recipient set: a handler may synchronously attach another
|
|
433
|
+
// consumer, which must start at the replacement snapshot rather than
|
|
434
|
+
// receive the tail of the lifecycle that is ending.
|
|
435
|
+
for (const handler of [...state.handlers.keys()]) {
|
|
436
|
+
try {
|
|
437
|
+
handler(frame);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
this.addDiagnostic('error', 'HANDLER_FAILED', 'A subscription consumer rejected a frame', {
|
|
441
|
+
error,
|
|
442
|
+
channel: state.channel,
|
|
443
|
+
frameType: frame.type,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
now() {
|
|
449
|
+
return (this.options.now ?? Date.now)();
|
|
450
|
+
}
|
|
451
|
+
setStatus(status) {
|
|
452
|
+
if (this.status === status)
|
|
453
|
+
return;
|
|
454
|
+
this.status = status;
|
|
455
|
+
for (const handler of this.statusHandlers)
|
|
456
|
+
handler(status);
|
|
457
|
+
}
|
|
458
|
+
addDiagnostic(level, code, message, detail) {
|
|
459
|
+
const diagnostic = { at: this.now(), level, code, message, detail };
|
|
460
|
+
this.diagnostics.push(diagnostic);
|
|
461
|
+
if (this.diagnostics.length > 100)
|
|
462
|
+
this.diagnostics.shift();
|
|
463
|
+
for (const handler of this.diagnosticHandlers)
|
|
464
|
+
handler(diagnostic);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
export function gatewayWebSocketUrl(apiUrl) {
|
|
468
|
+
return `${apiUrl.replace(/^http/, 'ws').replace(/\/+$/, '')}/v1/ws`;
|
|
469
|
+
}
|
|
470
|
+
export const __testing = {
|
|
471
|
+
SOCKET_CONNECTING,
|
|
472
|
+
SOCKET_OPEN,
|
|
473
|
+
SNAPSHOT_TIMEOUT_MS,
|
|
474
|
+
STREAM_RETRY_MS,
|
|
475
|
+
};
|