@0xmonaco/core 1.1.5-develop.129b2c6 → 1.1.5
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/dist/api/applications/api.d.ts +5 -5
- package/dist/api/applications/api.js +5 -5
- package/dist/api/buildercodes/api.d.ts +11 -18
- package/dist/api/buildercodes/api.js +15 -31
- package/dist/api/index.d.ts +0 -1
- package/dist/api/index.js +0 -1
- package/dist/api/orderbook/api.d.ts +1 -4
- package/dist/api/orderbook/api.js +0 -2
- package/dist/api/perp/routes.d.ts +0 -55
- package/dist/api/perp/routes.js +0 -18
- package/dist/api/trading/api.d.ts +2 -2
- package/dist/api/trading/api.js +6 -6
- package/dist/api/websocket/websocket.js +38 -601
- package/dist/coverage.d.ts +0 -16
- package/dist/coverage.js +0 -16
- package/dist/sdk.d.ts +1 -2
- package/dist/sdk.js +0 -4
- package/package.json +3 -3
- package/dist/api/analytics/api.d.ts +0 -40
- package/dist/api/analytics/api.js +0 -107
- package/dist/api/analytics/index.d.ts +0 -1
- package/dist/api/analytics/index.js +0 -1
- package/dist/api/websocket/local-book.d.ts +0 -112
- package/dist/api/websocket/local-book.js +0 -423
|
@@ -2,7 +2,6 @@ import { hexToBytes } from "@noble/hashes/utils.js";
|
|
|
2
2
|
import { composeWsSigningString, signMessage } from "../../crypto/session";
|
|
3
3
|
import { ALL_MAGNITUDES } from "../../utils";
|
|
4
4
|
import { ServerClock } from "../../utils/server-clock";
|
|
5
|
-
import { LocalBbo, LocalDepthBook, parseBboEvent, parseBboSnapshot, parseDepthDelta, parseDepthSnapshot, quoteSequenceNumbers, tryReadSequence, } from "./local-book";
|
|
6
5
|
import { keysToCamelCase } from "./utils";
|
|
7
6
|
// Connection constants
|
|
8
7
|
const CONNECTION_TIMEOUT = 10000;
|
|
@@ -22,8 +21,6 @@ const SKEW_REJECTION = /timestamp outside allowed skew/i;
|
|
|
22
21
|
// Unsubscribe for listing too many channels, after parsing it (ws-api
|
|
23
22
|
// `oversized_channel_list_message`) or before (`too_many_elements_message`).
|
|
24
23
|
// No ack follows either.
|
|
25
|
-
/** `Resynced` frames arriving this close together form one reported burst. */
|
|
26
|
-
const RESYNCED_BURST_MS = 50;
|
|
27
24
|
const OVERSIZED_REQUEST = /^(?:(?:Subscribe|Unsubscribe) lists \d+ channels; at most \d+ per request|Message lists too many elements; at most \d+ channels per request)$/;
|
|
28
25
|
function isRecord(value) {
|
|
29
26
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -434,41 +431,22 @@ function parseRfqSideViewEventData(raw) {
|
|
|
434
431
|
if (!isRecord(raw)) {
|
|
435
432
|
throw new Error("Invalid RFQ view: a side must be an object");
|
|
436
433
|
}
|
|
437
|
-
const grossVwap = readOptionalDecimalString(raw, "grossVwap");
|
|
438
|
-
const estimatedMonacoFee = readOptionalDecimalString(raw, "estimatedMonacoFee");
|
|
439
|
-
const estimatedBuilderFee = readOptionalDecimalString(raw, "estimatedBuilderFee");
|
|
440
|
-
const estimatedTotalFee = readOptionalDecimalString(raw, "estimatedTotalFee");
|
|
441
434
|
return {
|
|
442
435
|
vwap: readString(raw, "vwap"),
|
|
443
436
|
availableQuantity: readString(raw, "availableQuantity"),
|
|
444
|
-
...(grossVwap !== undefined && { grossVwap }),
|
|
445
|
-
...(estimatedMonacoFee !== undefined && { estimatedMonacoFee }),
|
|
446
|
-
...(estimatedBuilderFee !== undefined && { estimatedBuilderFee }),
|
|
447
|
-
...(estimatedTotalFee !== undefined && { estimatedTotalFee }),
|
|
448
437
|
};
|
|
449
438
|
}
|
|
450
|
-
/** An optional wire integer that may be negative; absent or `null` is `undefined`. */
|
|
451
|
-
function readOptionalInteger(source, field) {
|
|
452
|
-
const value = source[field];
|
|
453
|
-
if (value === undefined || value === null)
|
|
454
|
-
return undefined;
|
|
455
|
-
return readInteger(source, field);
|
|
456
|
-
}
|
|
457
439
|
/** Parse the taker's view. A side no maker quotes is absent. */
|
|
458
440
|
function parseRfqViewEventData(raw) {
|
|
459
441
|
if (!isRecord(raw)) {
|
|
460
442
|
throw new Error("Invalid RFQ view: view must be an object");
|
|
461
443
|
}
|
|
462
|
-
const monacoFeeCbps = readOptionalInteger(raw, "monacoFeeCbps");
|
|
463
|
-
const builderFeeCbps = readOptionalInteger(raw, "builderFeeCbps");
|
|
464
444
|
return {
|
|
465
445
|
viewVersion: readSequence(raw, "viewVersion"),
|
|
466
446
|
...(raw.bid != null && { bid: parseRfqSideViewEventData(raw.bid) }),
|
|
467
447
|
...(raw.ask != null && { ask: parseRfqSideViewEventData(raw.ask) }),
|
|
468
448
|
asOfMs: readSequence(raw, "asOfMs"),
|
|
469
449
|
sequence: readSequence(raw, "sequence"),
|
|
470
|
-
...(monacoFeeCbps !== undefined && { monacoFeeCbps }),
|
|
471
|
-
...(builderFeeCbps !== undefined && { builderFeeCbps }),
|
|
472
450
|
};
|
|
473
451
|
}
|
|
474
452
|
/** Parse a selected maker's own part of an acceptance. */
|
|
@@ -476,8 +454,6 @@ function parseRfqSelectionEventData(raw) {
|
|
|
476
454
|
if (!isRecord(raw)) {
|
|
477
455
|
throw new Error("Invalid RFQ selection: selection must be an object");
|
|
478
456
|
}
|
|
479
|
-
const maxBuilderFeeCbps = readOptionalInteger(raw, "maxBuilderFeeCbps");
|
|
480
|
-
const estimatedNet = readOptionalDecimalString(raw, "estimatedNet");
|
|
481
457
|
return {
|
|
482
458
|
acceptanceId: readString(raw, "acceptanceId"),
|
|
483
459
|
takerSide: readString(raw, "takerSide"),
|
|
@@ -489,23 +465,17 @@ function parseRfqSelectionEventData(raw) {
|
|
|
489
465
|
capacity: readString(raw, "capacity"),
|
|
490
466
|
makerFeeCbps: readInteger(raw, "makerFeeCbps"),
|
|
491
467
|
lastLookDeadlineAtMs: readSequence(raw, "lastLookDeadlineAtMs"),
|
|
492
|
-
...(maxBuilderFeeCbps !== undefined && { maxBuilderFeeCbps }),
|
|
493
|
-
...(estimatedNet !== undefined && { estimatedNet }),
|
|
494
468
|
};
|
|
495
469
|
}
|
|
496
470
|
function parseRfqFillEventData(raw) {
|
|
497
471
|
if (!isRecord(raw)) {
|
|
498
472
|
throw new Error("Invalid RFQ outcome: a fill must be an object");
|
|
499
473
|
}
|
|
500
|
-
const monacoFee = readOptionalDecimalString(raw, "monacoFee");
|
|
501
|
-
const builderFee = readOptionalDecimalString(raw, "builderFee");
|
|
502
474
|
return {
|
|
503
475
|
executionId: readString(raw, "executionId"),
|
|
504
476
|
price: readString(raw, "price"),
|
|
505
477
|
quantity: readString(raw, "quantity"),
|
|
506
478
|
fee: readString(raw, "fee"),
|
|
507
|
-
...(monacoFee !== undefined && { monacoFee }),
|
|
508
|
-
...(builderFee !== undefined && { builderFee }),
|
|
509
479
|
};
|
|
510
480
|
}
|
|
511
481
|
/** Parse the taker's outcome. Nothing filled leaves `vwap` absent and `fills` empty. */
|
|
@@ -807,18 +777,8 @@ function parseOrderbookEvent(rawData) {
|
|
|
807
777
|
quoteDecimals: data.quote_decimals || 0,
|
|
808
778
|
timestamp: data.timestamp || new Date().toISOString(),
|
|
809
779
|
sequence: data.sequence_number || 0,
|
|
810
|
-
// Sent only when the server renders `orderbook` from market-data's book
|
|
811
|
-
// (0XM-3552), as JSON numbers; surfaced as decimal strings like REST's
|
|
812
|
-
// `bookSeq` (a u64 may pass `Number.MAX_SAFE_INTEGER`).
|
|
813
|
-
...sequenceField("bookSeq", data, "book_seq"),
|
|
814
|
-
...sequenceField("prevBookSeq", data, "prev_book_seq"),
|
|
815
780
|
};
|
|
816
781
|
}
|
|
817
|
-
/** `{ [name]: sequence }` when `data[key]` is an exact u64 sequence, `{}` otherwise, so an absent or malformed key never surfaces. */
|
|
818
|
-
function sequenceField(name, data, key) {
|
|
819
|
-
const sequence = tryReadSequence(data, key);
|
|
820
|
-
return sequence === undefined ? {} : { [name]: sequence };
|
|
821
|
-
}
|
|
822
782
|
/**
|
|
823
783
|
* Parse the one row of an `orderbook` snapshot: the channel's current book in
|
|
824
784
|
* the live frame's shape, read the same way.
|
|
@@ -826,9 +786,7 @@ function sequenceField(name, data, key) {
|
|
|
826
786
|
* The live parser casts; here a row that is not an object, or carries no `data`
|
|
827
787
|
* object, throws, so {@link snapshotFrameHandler} withholds the frame rather
|
|
828
788
|
* than handing over an empty book. The engine's book read carries no sequence,
|
|
829
|
-
* so
|
|
830
|
-
* the channel from market-data's book (0XM-3552) the row carries `book_seq` as
|
|
831
|
-
* `sequence_number` and surfaces `bookSeq` / `prevBookSeq` like a live frame.
|
|
789
|
+
* so the row's `sequence` is 0.
|
|
832
790
|
*/
|
|
833
791
|
function parseOrderbookSnapshotRow(raw) {
|
|
834
792
|
if (!isRecord(raw) || !isRecord(raw.data)) {
|
|
@@ -1188,23 +1146,35 @@ function parseOrderSnapshotItem(raw) {
|
|
|
1188
1146
|
...(placementMakerFeeBps === undefined ? {} : { placementMakerFeeBps }),
|
|
1189
1147
|
};
|
|
1190
1148
|
}
|
|
1191
|
-
/**
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1149
|
+
/**
|
|
1150
|
+
* Wrap a caller's `onSnapshot` in the internal snapshot-frame handler.
|
|
1151
|
+
*
|
|
1152
|
+
* The frame's `data` is the channel's COMPLETE current state as a flat array,
|
|
1153
|
+
* and that completeness is the whole contract: a consumer is expected to
|
|
1154
|
+
* replace state from it, so absence from the array means the entity is gone.
|
|
1155
|
+
*
|
|
1156
|
+
* Delivery is therefore all-or-nothing. If the payload is not an array, or any
|
|
1157
|
+
* single row fails to parse, NOTHING is delivered.
|
|
1158
|
+
*
|
|
1159
|
+
* Skipping just the unreadable row and delivering the rest would be worse than
|
|
1160
|
+
* delivering nothing, because the array would still be presented as complete: a
|
|
1161
|
+
* consumer would drop the entity that row described — zero its balance, remove
|
|
1162
|
+
* its market, cancel its order in the UI — on the strength of an omission the
|
|
1163
|
+
* SDK itself created. Nor is that self-correcting. A resting order or an
|
|
1164
|
+
* unchanged market has no later diff to repair it, so the deletion would stand
|
|
1165
|
+
* for the life of the connection.
|
|
1166
|
+
*
|
|
1167
|
+
* Withholding the frame instead leaves the consumer on whatever baseline it
|
|
1168
|
+
* already had, and the channel keeps streaming live events over it. That is a
|
|
1169
|
+
* strictly smaller loss, and it is the same all-or-nothing rule this channel's
|
|
1170
|
+
* original `instrument` snapshot already applied.
|
|
1171
|
+
*
|
|
1172
|
+
* `[]` is a legitimate baseline, not a failure, and is always delivered:
|
|
1173
|
+
* "nothing is open" is exactly the information a consumer needs to clear stale
|
|
1174
|
+
* state after a reconnect.
|
|
1175
|
+
*/
|
|
1204
1176
|
function snapshotFrameHandler(channel, parseItem, onSnapshot) {
|
|
1205
|
-
|
|
1206
|
-
// only by a channel that reads something beside the rows (trades' cursor).
|
|
1207
|
-
return (rawData, _frame, meta) => {
|
|
1177
|
+
return (rawData) => {
|
|
1208
1178
|
try {
|
|
1209
1179
|
if (!Array.isArray(rawData)) {
|
|
1210
1180
|
throw new Error(`Invalid ${channel} snapshot: data must be an array`);
|
|
@@ -1212,86 +1182,13 @@ function snapshotFrameHandler(channel, parseItem, onSnapshot) {
|
|
|
1212
1182
|
// Parsed fully before anything is handed over, so a failure part-way
|
|
1213
1183
|
// through cannot deliver a truncated baseline.
|
|
1214
1184
|
const items = rawData.map(parseItem);
|
|
1215
|
-
|
|
1216
|
-
// sees exactly the call it always did.
|
|
1217
|
-
if (meta)
|
|
1218
|
-
onSnapshot(items, meta);
|
|
1219
|
-
else
|
|
1220
|
-
onSnapshot(items);
|
|
1185
|
+
onSnapshot(items);
|
|
1221
1186
|
}
|
|
1222
1187
|
catch (err) {
|
|
1223
1188
|
console.error(`WebSocket: Error processing ${channel} snapshot`, err);
|
|
1224
1189
|
}
|
|
1225
1190
|
};
|
|
1226
1191
|
}
|
|
1227
|
-
function tradeKeyAtOrBelow(key, bound) {
|
|
1228
|
-
return key.seq < bound.seq || (key.seq === bound.seq && key.matchIndex <= bound.matchIndex);
|
|
1229
|
-
}
|
|
1230
|
-
/** An optional chain field: a non-negative integer, else absent (a malformed value never drops the trade). */
|
|
1231
|
-
function readOptionalChainNumber(data, key) {
|
|
1232
|
-
const value = data[key];
|
|
1233
|
-
return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
1234
|
-
}
|
|
1235
|
-
/** The trade frame's chain fields (market-data source only), set only when present and well formed. */
|
|
1236
|
-
function readTradeChain(data) {
|
|
1237
|
-
const chain = {};
|
|
1238
|
-
const seq = readOptionalChainNumber(data, "seq");
|
|
1239
|
-
const matchIndex = readOptionalChainNumber(data, "match_index");
|
|
1240
|
-
const prevSeq = readOptionalChainNumber(data, "prev_seq");
|
|
1241
|
-
if (seq !== undefined)
|
|
1242
|
-
chain.seq = seq;
|
|
1243
|
-
if (matchIndex !== undefined)
|
|
1244
|
-
chain.matchIndex = matchIndex;
|
|
1245
|
-
if (prevSeq !== undefined)
|
|
1246
|
-
chain.prevSeq = prevSeq;
|
|
1247
|
-
if (data.prev_match_index === null)
|
|
1248
|
-
chain.prevMatchIndex = null;
|
|
1249
|
-
else {
|
|
1250
|
-
const prevMatchIndex = readOptionalChainNumber(data, "prev_match_index");
|
|
1251
|
-
if (prevMatchIndex !== undefined)
|
|
1252
|
-
chain.prevMatchIndex = prevMatchIndex;
|
|
1253
|
-
}
|
|
1254
|
-
return chain;
|
|
1255
|
-
}
|
|
1256
|
-
/** Backoff for repeated client-side rebuilds (a gap, an unreadable frame or snapshot, a thinned side). */
|
|
1257
|
-
const SNAPSHOT_RETRY_BASE_MS = 500;
|
|
1258
|
-
const SNAPSHOT_RETRY_MAX_MS = 15_000;
|
|
1259
|
-
/**
|
|
1260
|
-
* After SNAPSHOT_UNAVAILABLE the subscription stays routed: the server retries
|
|
1261
|
-
* a failed book (5 s doubling to 60 s) or a capped one once a slot frees, and
|
|
1262
|
-
* pushes the healing snapshot with no client action. A resubscribe lands on
|
|
1263
|
-
* the same per-pod book, so it is never required; this timer is only a
|
|
1264
|
-
* harmless safety net. The cases that never heal (an unknown pair id, a
|
|
1265
|
-
* durable-log reset until ws-api restarts) are not fixed by it either.
|
|
1266
|
-
*/
|
|
1267
|
-
const SNAPSHOT_FALLBACK_BASE_MS = 90_000;
|
|
1268
|
-
const SNAPSHOT_FALLBACK_MAX_MS = 300_000;
|
|
1269
|
-
/**
|
|
1270
|
-
* Whether a raw frame belongs to a channel carrying market-data sequences:
|
|
1271
|
-
* depth, bbo, or orderbook rendered from market-data's book (0XM-3552). A
|
|
1272
|
-
* cheap prefix test before the sequence quoting pass.
|
|
1273
|
-
*/
|
|
1274
|
-
const MARKET_DATA_CHANNEL_KEY = /"channel"\s*:\s*"(?:depth|bbo|orderbook):/;
|
|
1275
|
-
function isMarketDataFrame(raw) {
|
|
1276
|
-
// The substring test first: almost every frame fails it without a regex.
|
|
1277
|
-
return raw.includes("book_seq") && MARKET_DATA_CHANNEL_KEY.test(raw);
|
|
1278
|
-
}
|
|
1279
|
-
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1280
|
-
/**
|
|
1281
|
-
* The canonical market-data channel: lowercase pair UUID, uppercase mode. The
|
|
1282
|
-
* server sends live frames on this form and echoes the subscribed form on
|
|
1283
|
-
* the snapshot, so subscribing canonically keeps both on one key.
|
|
1284
|
-
*/
|
|
1285
|
-
function marketDataChannel(kind, tradingPairId, tradingMode) {
|
|
1286
|
-
if (!tradingPairId || !UUID_PATTERN.test(tradingPairId)) {
|
|
1287
|
-
throw new Error(`${kind} subscription requires a trading pair UUID, got "${tradingPairId}"`);
|
|
1288
|
-
}
|
|
1289
|
-
const mode = String(tradingMode ?? "").toUpperCase();
|
|
1290
|
-
if (mode !== "SPOT" && mode !== "MARGIN") {
|
|
1291
|
-
throw new Error(`${kind} subscription requires tradingMode "SPOT" or "MARGIN", got "${tradingMode}"`);
|
|
1292
|
-
}
|
|
1293
|
-
return `${kind}:${tradingPairId.toLowerCase()}:${mode}`;
|
|
1294
|
-
}
|
|
1295
1192
|
/**
|
|
1296
1193
|
* Create a Monaco WebSocket client
|
|
1297
1194
|
*
|
|
@@ -1334,22 +1231,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1334
1231
|
// Close code of the connection loss the next successful reopen recovers
|
|
1335
1232
|
// from; null when no resync is owed (first connect, clean disconnect).
|
|
1336
1233
|
let pendingResyncCode = null;
|
|
1337
|
-
// Scoped resync (0XM-3553): offer the capability once per socket, on its
|
|
1338
|
-
// first Authenticate or Subscribe; the server keeps it for the connection.
|
|
1339
|
-
// Only to a client with an onResync: without one a scoped resync would
|
|
1340
|
-
// leave the app silently stale, where the 1013 close at least resubscribes
|
|
1341
|
-
// and re-delivers subscribe snapshots.
|
|
1342
|
-
const offerResync = (options.scopedResync ?? true) && options.onResync !== undefined;
|
|
1343
|
-
let resyncOffered = false;
|
|
1344
|
-
let resyncedBursts = [];
|
|
1345
|
-
// A scoped refetch was still owed when a socket was lost: the next
|
|
1346
|
-
// connection's onResync reports it (as a 1013 gap if the close itself owed
|
|
1347
|
-
// none).
|
|
1348
|
-
let scopedResyncOwed = false;
|
|
1349
|
-
// Snapshot handlers that keep only internal state and hand the app
|
|
1350
|
-
// nothing (the trades chain cursor without an app `onSnapshot`): a re-sent
|
|
1351
|
-
// snapshot through one alone still reaches no app code.
|
|
1352
|
-
const internalSnapshotHandlers = new WeakSet();
|
|
1353
1234
|
// Authentication-ending Error frame received on the current socket, tagged
|
|
1354
1235
|
// with the session it rejected. The server answers handshakes in the order
|
|
1355
1236
|
// they were sent, so each Authenticated / AUTH_FAILED reply belongs to the
|
|
@@ -1371,14 +1252,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1371
1252
|
// reusing the Event handlers would feed every other channel's parser a frame
|
|
1372
1253
|
// it must reject.
|
|
1373
1254
|
const snapshotHandlers = new Map();
|
|
1374
|
-
// Per-channel listeners for Error frames that name a channel (the local
|
|
1375
|
-
// depth/bbo books retry a SNAPSHOT_UNAVAILABLE subscribe). They run before
|
|
1376
|
-
// the app's onError and never stop it.
|
|
1377
|
-
const channelErrorHandlers = new Map();
|
|
1378
|
-
// One shared local book per depth/bbo channel: the server sends a snapshot
|
|
1379
|
-
// only for a new wire subscription, so a second local subscriber must share
|
|
1380
|
-
// the first one's state rather than wait for a snapshot that never comes.
|
|
1381
|
-
const localStateChannels = new Map();
|
|
1382
1255
|
// Latest known full-config frame per market for each subscribed instrument
|
|
1383
1256
|
// channel, seeded by the subscribe-time Snapshot and superseded by every
|
|
1384
1257
|
// live frame (instrument frames are terminal-state, never deltas). The
|
|
@@ -1484,7 +1357,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1484
1357
|
public_key: session.publicKey,
|
|
1485
1358
|
timestamp,
|
|
1486
1359
|
signature,
|
|
1487
|
-
...capabilitiesOnce(),
|
|
1488
1360
|
}));
|
|
1489
1361
|
};
|
|
1490
1362
|
const stopHeartbeat = () => {
|
|
@@ -1572,67 +1444,7 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1572
1444
|
if (channels.length === 0 || ws?.readyState !== WebSocket.OPEN)
|
|
1573
1445
|
return;
|
|
1574
1446
|
pendingRequests.push({ kind: "subscribe", channels });
|
|
1575
|
-
send({ type: "Subscribe", channels
|
|
1576
|
-
};
|
|
1577
|
-
/** The capability offer, on the first request of each socket only. */
|
|
1578
|
-
const capabilitiesOnce = () => {
|
|
1579
|
-
if (!offerResync || resyncOffered)
|
|
1580
|
-
return {};
|
|
1581
|
-
resyncOffered = true;
|
|
1582
|
-
return { capabilities: ["resync"] };
|
|
1583
|
-
};
|
|
1584
|
-
/** Report `channel` in a scoped onResync, at least `delayMs` from now. */
|
|
1585
|
-
const scheduleResynced = (channel, delayMs) => {
|
|
1586
|
-
const now = performance.now();
|
|
1587
|
-
let burst = resyncedBursts.at(-1);
|
|
1588
|
-
if (!burst || now - burst.opened > RESYNCED_BURST_MS) {
|
|
1589
|
-
// Held open for the whole window, so frames still arriving join it.
|
|
1590
|
-
burst = { opened: now, due: now + RESYNCED_BURST_MS, channels: new Set(), timer: null };
|
|
1591
|
-
resyncedBursts.push(burst);
|
|
1592
|
-
}
|
|
1593
|
-
burst.channels.add(channel);
|
|
1594
|
-
burst.due = Math.max(burst.due, now + delayMs);
|
|
1595
|
-
armResynced(burst);
|
|
1596
|
-
};
|
|
1597
|
-
/** (Re)arm `burst`'s timer for its deadline, rounding up. */
|
|
1598
|
-
const armResynced = (burst) => {
|
|
1599
|
-
if (burst.timer)
|
|
1600
|
-
clearTimeout(burst.timer);
|
|
1601
|
-
burst.timer = setTimeout(() => flushResynced(burst), Math.max(0, Math.ceil(burst.due - performance.now())));
|
|
1602
|
-
};
|
|
1603
|
-
const flushResynced = (burst) => {
|
|
1604
|
-
// A timer can fire before the monotonic deadline: wait out the rest.
|
|
1605
|
-
if (performance.now() < burst.due) {
|
|
1606
|
-
armResynced(burst);
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
resyncedBursts = resyncedBursts.filter((other) => other !== burst);
|
|
1610
|
-
const channels = [...burst.channels].filter((channel) => handlers.has(channel));
|
|
1611
|
-
if (channels.length === 0)
|
|
1612
|
-
return;
|
|
1613
|
-
// Called from a timer: a throw or a rejection must not escape it.
|
|
1614
|
-
const logResyncError = (err) => console.error("WebSocket: onResync threw", err);
|
|
1615
|
-
try {
|
|
1616
|
-
const result = options.onResync?.({ code: 1013, slowClient: true, reason: "resynced", channels });
|
|
1617
|
-
if (result instanceof Promise)
|
|
1618
|
-
result.catch(logResyncError);
|
|
1619
|
-
}
|
|
1620
|
-
catch (err) {
|
|
1621
|
-
logResyncError(err);
|
|
1622
|
-
}
|
|
1623
|
-
};
|
|
1624
|
-
/**
|
|
1625
|
-
* Drop every pending scoped resync; whether one was owed. A reconnect's
|
|
1626
|
-
* onResync covers it.
|
|
1627
|
-
*/
|
|
1628
|
-
const cancelResynced = () => {
|
|
1629
|
-
// Owed only for a channel the app still holds.
|
|
1630
|
-
const owed = resyncedBursts.some((burst) => [...burst.channels].some((channel) => handlers.has(channel)));
|
|
1631
|
-
for (const burst of resyncedBursts)
|
|
1632
|
-
if (burst.timer)
|
|
1633
|
-
clearTimeout(burst.timer);
|
|
1634
|
-
resyncedBursts = [];
|
|
1635
|
-
return owed;
|
|
1447
|
+
send({ type: "Subscribe", channels });
|
|
1636
1448
|
};
|
|
1637
1449
|
const sendUnsubscribe = (channels) => {
|
|
1638
1450
|
if (channels.length === 0 || ws?.readyState !== WebSocket.OPEN)
|
|
@@ -1783,8 +1595,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1783
1595
|
// oldest outstanding request: retire it, or every later ack would
|
|
1784
1596
|
// pair with the wrong request.
|
|
1785
1597
|
if (msg.code === "INVALID_MESSAGE" && OVERSIZED_REQUEST.test(msg.message)) {
|
|
1786
|
-
// Refused unread: any capability it carried was never recorded.
|
|
1787
|
-
resyncOffered = false;
|
|
1788
1598
|
pendingRequests.shift();
|
|
1789
1599
|
}
|
|
1790
1600
|
if (msg.code === "AUTH_FAILED") {
|
|
@@ -1803,16 +1613,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1803
1613
|
// actionable without it. (The frame is untyped JSON, so the declared
|
|
1804
1614
|
// `string` type of `channel` is not trusted here.)
|
|
1805
1615
|
const channel = msg.channel;
|
|
1806
|
-
if (typeof channel === "string") {
|
|
1807
|
-
for (const handler of channelErrorHandlers.get(channel) ?? []) {
|
|
1808
|
-
try {
|
|
1809
|
-
handler(msg.code, msg.message);
|
|
1810
|
-
}
|
|
1811
|
-
catch (err) {
|
|
1812
|
-
console.error("WebSocket: Error in channel error handler", err);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
1616
|
options.onError?.(typeof channel === "string" ? { code: msg.code, message: msg.message, channel } : { code: msg.code, message: msg.message });
|
|
1817
1617
|
}
|
|
1818
1618
|
return;
|
|
@@ -1858,36 +1658,13 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1858
1658
|
}
|
|
1859
1659
|
return;
|
|
1860
1660
|
}
|
|
1861
|
-
if (msg.type === "Resynced") {
|
|
1862
|
-
// A channel the server could not repair in place (0XM-3553). A frame
|
|
1863
|
-
// missing either field is malformed and ignored.
|
|
1864
|
-
const retryAfterMs = msg.retry_after_ms;
|
|
1865
|
-
if (typeof msg.channel === "string" && typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
1866
|
-
scheduleResynced(msg.channel, retryAfterMs);
|
|
1867
|
-
}
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
1661
|
if (!msg.channel)
|
|
1871
1662
|
return;
|
|
1872
1663
|
if (msg.type === "Snapshot") {
|
|
1873
1664
|
// Exact match only: the Snapshot frame's channel echoes the raw
|
|
1874
1665
|
// subscribed channel, never a more specific one.
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
handler(msg.data, msg);
|
|
1878
|
-
}
|
|
1879
|
-
// A server-side repair after an upstream loss (0XM-3553) is marked; a
|
|
1880
|
-
// channel consumed without an app snapshot handler would never see it.
|
|
1881
|
-
if (offerResync && msg.resynced === true && handlers.has(msg.channel)) {
|
|
1882
|
-
// Every subscriber needs an app snapshot handler for the repair to
|
|
1883
|
-
// reach it; an event-only one is told to refetch instead.
|
|
1884
|
-
let appSnapshotHandlers = 0;
|
|
1885
|
-
for (const handler of channelSnapshotHandlers ?? []) {
|
|
1886
|
-
if (!internalSnapshotHandlers.has(handler))
|
|
1887
|
-
appSnapshotHandlers++;
|
|
1888
|
-
}
|
|
1889
|
-
if (appSnapshotHandlers < (handlers.get(msg.channel)?.size ?? 0))
|
|
1890
|
-
scheduleResynced(msg.channel, 0);
|
|
1666
|
+
for (const handler of snapshotHandlers.get(msg.channel) ?? []) {
|
|
1667
|
+
handler(msg.data);
|
|
1891
1668
|
}
|
|
1892
1669
|
return;
|
|
1893
1670
|
}
|
|
@@ -1910,12 +1687,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1910
1687
|
const handleSocketLoss = (code) => {
|
|
1911
1688
|
ws = null;
|
|
1912
1689
|
stopHeartbeat();
|
|
1913
|
-
// A scoped refetch still owed is owed by whichever connection comes next,
|
|
1914
|
-
// even one opened later by connect() rather than by the reconnect loop.
|
|
1915
|
-
if (cancelResynced())
|
|
1916
|
-
scopedResyncOwed = true;
|
|
1917
|
-
for (const entry of [...localStateChannels.values()])
|
|
1918
|
-
entry.onSocketLoss();
|
|
1919
1690
|
// The server dropped every subscription with the connection.
|
|
1920
1691
|
resetSubscriptionAcks();
|
|
1921
1692
|
// Terminal authentication loss: the server revoked the session
|
|
@@ -1995,7 +1766,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
1995
1766
|
try {
|
|
1996
1767
|
const socket = new WebSocket(getUrl());
|
|
1997
1768
|
ws = socket;
|
|
1998
|
-
resyncOffered = false;
|
|
1999
1769
|
lastAuthError = null;
|
|
2000
1770
|
pendingAuthSessions = [];
|
|
2001
1771
|
confirmedSession = undefined;
|
|
@@ -2037,10 +1807,9 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
2037
1807
|
//
|
|
2038
1808
|
// This callback waits for neither acknowledgements nor snapshots,
|
|
2039
1809
|
// and the SDK performs no REST recovery itself.
|
|
2040
|
-
if (pendingResyncCode !== null
|
|
2041
|
-
const code = pendingResyncCode
|
|
1810
|
+
if (pendingResyncCode !== null) {
|
|
1811
|
+
const code = pendingResyncCode;
|
|
2042
1812
|
pendingResyncCode = null;
|
|
2043
|
-
scopedResyncOwed = false;
|
|
2044
1813
|
options.onResync?.({ code, slowClient: code === 1013 });
|
|
2045
1814
|
}
|
|
2046
1815
|
resolve();
|
|
@@ -2050,10 +1819,7 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
2050
1819
|
return;
|
|
2051
1820
|
lastMessageAt = Date.now();
|
|
2052
1821
|
try {
|
|
2053
|
-
|
|
2054
|
-
// one above Number.MAX_SAFE_INTEGER stays exact.
|
|
2055
|
-
const raw = event.data;
|
|
2056
|
-
const msg = JSON.parse(typeof raw === "string" && isMarketDataFrame(raw) ? quoteSequenceNumbers(raw) : raw);
|
|
1822
|
+
const msg = JSON.parse(event.data);
|
|
2057
1823
|
// Handle pong silently
|
|
2058
1824
|
if (msg.type === "Pong")
|
|
2059
1825
|
return;
|
|
@@ -2103,14 +1869,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
2103
1869
|
stopReconnect();
|
|
2104
1870
|
stopHeartbeat();
|
|
2105
1871
|
pendingResyncCode = null;
|
|
2106
|
-
cancelResynced();
|
|
2107
|
-
scopedResyncOwed = false;
|
|
2108
|
-
// Local depth/bbo books go with their wire handlers: stop each (its retry
|
|
2109
|
-
// timer too) so a later depth()/bbo() call starts a fresh subscription.
|
|
2110
|
-
for (const entry of [...localStateChannels.values()])
|
|
2111
|
-
entry.stop();
|
|
2112
|
-
localStateChannels.clear();
|
|
2113
|
-
channelErrorHandlers.clear();
|
|
2114
1872
|
handlers.clear();
|
|
2115
1873
|
snapshotHandlers.clear();
|
|
2116
1874
|
instrumentBaseline.clear();
|
|
@@ -2195,266 +1953,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
2195
1953
|
}
|
|
2196
1954
|
}, onSnapshot && snapshotFrameHandler("orderbook", parseOrderbookSnapshotRow, onSnapshot));
|
|
2197
1955
|
};
|
|
2198
|
-
/**
|
|
2199
|
-
* Keep one shared, sequenced local state for a depth/bbo channel: the
|
|
2200
|
-
* subscribe-time snapshot is the base, linked frames update it, and a gap,
|
|
2201
|
-
* an unreadable frame or snapshot, an exhausted incomplete side or a
|
|
2202
|
-
* SNAPSHOT_UNAVAILABLE rebuilds it from a fresh wire subscription.
|
|
2203
|
-
*/
|
|
2204
|
-
const openLocalState = (channel, createState, onValue, localOptions) => {
|
|
2205
|
-
let entry = localStateChannels.get(channel);
|
|
2206
|
-
if (!entry) {
|
|
2207
|
-
const state = createState();
|
|
2208
|
-
const listeners = new Set();
|
|
2209
|
-
let unsubscribeWire = null;
|
|
2210
|
-
let stopped = false;
|
|
2211
|
-
// One pending resubscribe at a time (a rebuild, or the fallback after a
|
|
2212
|
-
// SNAPSHOT_UNAVAILABLE); scheduling another supersedes it, and a stale
|
|
2213
|
-
// timer or microtask finds its token outdated and does nothing.
|
|
2214
|
-
let scheduleToken = 0;
|
|
2215
|
-
let scheduledTimer = null;
|
|
2216
|
-
// Set while a rebuild is pending: frames still being routed for the old
|
|
2217
|
-
// subscription (the router may visit the re-added channel in the same
|
|
2218
|
-
// pass) are ignored rather than buffered against the next snapshot.
|
|
2219
|
-
let rebuildPending = false;
|
|
2220
|
-
// Rebuilds since the last frame that applied cleanly on a fresh
|
|
2221
|
-
// snapshot: a fault that recurs on every snapshot backs off instead of
|
|
2222
|
-
// resubscribing per frame.
|
|
2223
|
-
let consecutiveRebuilds = 0;
|
|
2224
|
-
let fallbackDelay = SNAPSHOT_FALLBACK_BASE_MS;
|
|
2225
|
-
// The channel's last rejection, replayed to a listener that joins later
|
|
2226
|
-
// (no new Subscribe goes out for it); a snapshot clears it.
|
|
2227
|
-
let lastRejection = null;
|
|
2228
|
-
const forEachListener = (what, call) => {
|
|
2229
|
-
for (const listener of [...listeners]) {
|
|
2230
|
-
try {
|
|
2231
|
-
call(listener);
|
|
2232
|
-
}
|
|
2233
|
-
catch (err) {
|
|
2234
|
-
console.error(`WebSocket: Error in ${channel} ${what}`, err);
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
};
|
|
2238
|
-
// The view is built once; each listener gets its own copy of it, so one
|
|
2239
|
-
// that mutates the book cannot change what the others see.
|
|
2240
|
-
const emit = () => {
|
|
2241
|
-
const value = state.view();
|
|
2242
|
-
if (value === null)
|
|
2243
|
-
return;
|
|
2244
|
-
forEachListener("listener", (listener) => listener.onValue(state.copy(value)));
|
|
2245
|
-
};
|
|
2246
|
-
const notifyResync = (reason) => forEachListener("onResync", (listener) => listener.options?.onResync?.(reason));
|
|
2247
|
-
const open = () => subscribe(channel, onFrame, onSnapshot);
|
|
2248
|
-
const cancelScheduled = () => {
|
|
2249
|
-
scheduleToken += 1;
|
|
2250
|
-
if (scheduledTimer)
|
|
2251
|
-
clearTimeout(scheduledTimer);
|
|
2252
|
-
scheduledTimer = null;
|
|
2253
|
-
rebuildPending = false;
|
|
2254
|
-
};
|
|
2255
|
-
const schedule = (delay, run) => {
|
|
2256
|
-
cancelScheduled();
|
|
2257
|
-
const token = scheduleToken;
|
|
2258
|
-
const fire = () => {
|
|
2259
|
-
if (token !== scheduleToken || stopped)
|
|
2260
|
-
return;
|
|
2261
|
-
scheduledTimer = null;
|
|
2262
|
-
rebuildPending = false;
|
|
2263
|
-
run();
|
|
2264
|
-
};
|
|
2265
|
-
if (delay === 0)
|
|
2266
|
-
queueMicrotask(fire);
|
|
2267
|
-
else
|
|
2268
|
-
scheduledTimer = setTimeout(fire, delay);
|
|
2269
|
-
};
|
|
2270
|
-
const resubscribe = () => {
|
|
2271
|
-
unsubscribeWire?.();
|
|
2272
|
-
unsubscribeWire = open();
|
|
2273
|
-
};
|
|
2274
|
-
/** Discard the state now; resubscribe once the current routing pass is done, backing off on repeats. */
|
|
2275
|
-
const rebuild = (reason) => {
|
|
2276
|
-
state.reset();
|
|
2277
|
-
notifyResync(reason);
|
|
2278
|
-
if (rebuildPending)
|
|
2279
|
-
return;
|
|
2280
|
-
const delay = consecutiveRebuilds === 0 ? 0 : Math.min(SNAPSHOT_RETRY_BASE_MS * 2 ** (consecutiveRebuilds - 1), SNAPSHOT_RETRY_MAX_MS);
|
|
2281
|
-
consecutiveRebuilds += 1;
|
|
2282
|
-
schedule(delay, resubscribe);
|
|
2283
|
-
rebuildPending = true;
|
|
2284
|
-
if (delay > 0) {
|
|
2285
|
-
// Waiting out the backoff unsubscribed, so no frame or snapshot
|
|
2286
|
-
// arrives for the abandoned subscription meanwhile.
|
|
2287
|
-
unsubscribeWire?.();
|
|
2288
|
-
unsubscribeWire = null;
|
|
2289
|
-
}
|
|
2290
|
-
};
|
|
2291
|
-
/**
|
|
2292
|
-
* The server could not serve a snapshot. The subscription stays routed
|
|
2293
|
-
* and ws-api pushes the healing snapshot with no client action, so stay
|
|
2294
|
-
* subscribed (frames are withheld until then). The resubscribe after the
|
|
2295
|
-
* long fallback is a safety net only: it lands on the same book.
|
|
2296
|
-
*/
|
|
2297
|
-
const awaitServerSnapshot = (reason) => {
|
|
2298
|
-
state.reset();
|
|
2299
|
-
notifyResync(reason);
|
|
2300
|
-
schedule(fallbackDelay, resubscribe);
|
|
2301
|
-
fallbackDelay = Math.min(fallbackDelay * 2, SNAPSHOT_FALLBACK_MAX_MS);
|
|
2302
|
-
if (unsubscribeWire === null)
|
|
2303
|
-
unsubscribeWire = open();
|
|
2304
|
-
};
|
|
2305
|
-
function onFrame(raw) {
|
|
2306
|
-
if (rebuildPending || stopped)
|
|
2307
|
-
return;
|
|
2308
|
-
let outcome;
|
|
2309
|
-
try {
|
|
2310
|
-
outcome = state.offerRaw(raw);
|
|
2311
|
-
}
|
|
2312
|
-
catch (err) {
|
|
2313
|
-
// An unreadable frame is a lost frame.
|
|
2314
|
-
console.error(`WebSocket: Error processing ${channel} frame`, err);
|
|
2315
|
-
rebuild("gap");
|
|
2316
|
-
return;
|
|
2317
|
-
}
|
|
2318
|
-
if (outcome === "applied") {
|
|
2319
|
-
consecutiveRebuilds = 0;
|
|
2320
|
-
emit();
|
|
2321
|
-
}
|
|
2322
|
-
else if (outcome === "gap")
|
|
2323
|
-
rebuild("gap");
|
|
2324
|
-
else if (outcome === "truncated")
|
|
2325
|
-
rebuild("truncated");
|
|
2326
|
-
}
|
|
2327
|
-
function onSnapshot(raw) {
|
|
2328
|
-
if (stopped)
|
|
2329
|
-
return;
|
|
2330
|
-
let outcome;
|
|
2331
|
-
try {
|
|
2332
|
-
outcome = state.applySnapshotRaw(raw);
|
|
2333
|
-
}
|
|
2334
|
-
catch (err) {
|
|
2335
|
-
// No further snapshot would come on this subscription unless the
|
|
2336
|
-
// server reloads the book: rebuild it (backing off on repeats).
|
|
2337
|
-
console.error(`WebSocket: Error processing ${channel} snapshot`, err);
|
|
2338
|
-
rebuild("snapshot_unavailable");
|
|
2339
|
-
return;
|
|
2340
|
-
}
|
|
2341
|
-
// A snapshot supersedes any pending fallback or rebuild, and any
|
|
2342
|
-
// earlier rejection.
|
|
2343
|
-
cancelScheduled();
|
|
2344
|
-
lastRejection = null;
|
|
2345
|
-
fallbackDelay = SNAPSHOT_FALLBACK_BASE_MS;
|
|
2346
|
-
if (outcome === "gap" || outcome === "truncated") {
|
|
2347
|
-
rebuild(outcome);
|
|
2348
|
-
return;
|
|
2349
|
-
}
|
|
2350
|
-
emit();
|
|
2351
|
-
}
|
|
2352
|
-
const onChannelError = (code, message) => {
|
|
2353
|
-
if (stopped)
|
|
2354
|
-
return;
|
|
2355
|
-
if (code === "SNAPSHOT_UNAVAILABLE") {
|
|
2356
|
-
awaitServerSnapshot("snapshot_unavailable");
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
lastRejection = { code, message };
|
|
2360
|
-
forEachListener("onError", (listener) => listener.options?.onError?.({ code, message }));
|
|
2361
|
-
};
|
|
2362
|
-
const onSocketLoss = () => {
|
|
2363
|
-
if (stopped)
|
|
2364
|
-
return;
|
|
2365
|
-
// The reconnect resubscribes and its snapshot rebuilds the book;
|
|
2366
|
-
// until then the old one is not live. Frames buffered from the dead
|
|
2367
|
-
// connection go too.
|
|
2368
|
-
const wasLive = state.ready;
|
|
2369
|
-
state.reset();
|
|
2370
|
-
if (wasLive)
|
|
2371
|
-
notifyResync("disconnected");
|
|
2372
|
-
};
|
|
2373
|
-
if (!channelErrorHandlers.has(channel))
|
|
2374
|
-
channelErrorHandlers.set(channel, new Set());
|
|
2375
|
-
channelErrorHandlers.get(channel)?.add(onChannelError);
|
|
2376
|
-
unsubscribeWire = open();
|
|
2377
|
-
const created = {
|
|
2378
|
-
listeners,
|
|
2379
|
-
current: () => {
|
|
2380
|
-
const value = state.view();
|
|
2381
|
-
return value === null ? null : state.copy(value);
|
|
2382
|
-
},
|
|
2383
|
-
rejection: () => lastRejection,
|
|
2384
|
-
onSocketLoss,
|
|
2385
|
-
stop: () => {
|
|
2386
|
-
if (stopped)
|
|
2387
|
-
return;
|
|
2388
|
-
stopped = true;
|
|
2389
|
-
cancelScheduled();
|
|
2390
|
-
unsubscribeWire?.();
|
|
2391
|
-
unsubscribeWire = null;
|
|
2392
|
-
channelErrorHandlers.get(channel)?.delete(onChannelError);
|
|
2393
|
-
if (channelErrorHandlers.get(channel)?.size === 0)
|
|
2394
|
-
channelErrorHandlers.delete(channel);
|
|
2395
|
-
if (localStateChannels.get(channel) === created)
|
|
2396
|
-
localStateChannels.delete(channel);
|
|
2397
|
-
},
|
|
2398
|
-
};
|
|
2399
|
-
localStateChannels.set(channel, created);
|
|
2400
|
-
entry = created;
|
|
2401
|
-
}
|
|
2402
|
-
const listener = { onValue: onValue, options: localOptions };
|
|
2403
|
-
const held = entry;
|
|
2404
|
-
held.listeners.add(listener);
|
|
2405
|
-
const current = held.current();
|
|
2406
|
-
const rejection = held.rejection();
|
|
2407
|
-
try {
|
|
2408
|
-
if (current !== null)
|
|
2409
|
-
listener.onValue(current);
|
|
2410
|
-
else if (rejection !== null)
|
|
2411
|
-
listener.options?.onError?.(rejection);
|
|
2412
|
-
}
|
|
2413
|
-
catch (err) {
|
|
2414
|
-
console.error(`WebSocket: Error in ${channel} listener`, err);
|
|
2415
|
-
}
|
|
2416
|
-
let unsubscribed = false;
|
|
2417
|
-
return () => {
|
|
2418
|
-
if (unsubscribed)
|
|
2419
|
-
return;
|
|
2420
|
-
unsubscribed = true;
|
|
2421
|
-
held.listeners.delete(listener);
|
|
2422
|
-
if (held.listeners.size === 0)
|
|
2423
|
-
held.stop();
|
|
2424
|
-
};
|
|
2425
|
-
};
|
|
2426
|
-
const subscribeDepth = (tradingPairId, tradingMode, onBook, localOptions) => {
|
|
2427
|
-
const channel = marketDataChannel("depth", tradingPairId, tradingMode);
|
|
2428
|
-
return openLocalState(channel, () => {
|
|
2429
|
-
const book = new LocalDepthBook();
|
|
2430
|
-
return {
|
|
2431
|
-
offerRaw: (raw) => book.offer(parseDepthDelta(raw)),
|
|
2432
|
-
applySnapshotRaw: (raw) => book.applySnapshot(parseDepthSnapshot(raw)),
|
|
2433
|
-
reset: () => book.reset(),
|
|
2434
|
-
view: () => book.view(),
|
|
2435
|
-
copy: (value) => ({ ...value, bids: value.bids.map((level) => ({ ...level })), asks: value.asks.map((level) => ({ ...level })) }),
|
|
2436
|
-
get ready() {
|
|
2437
|
-
return book.ready;
|
|
2438
|
-
},
|
|
2439
|
-
};
|
|
2440
|
-
}, onBook, localOptions);
|
|
2441
|
-
};
|
|
2442
|
-
const subscribeBbo = (tradingPairId, tradingMode, onBbo, localOptions) => {
|
|
2443
|
-
const channel = marketDataChannel("bbo", tradingPairId, tradingMode);
|
|
2444
|
-
return openLocalState(channel, () => {
|
|
2445
|
-
const bbo = new LocalBbo();
|
|
2446
|
-
return {
|
|
2447
|
-
offerRaw: (raw) => bbo.offer(parseBboEvent(raw)),
|
|
2448
|
-
applySnapshotRaw: (raw) => bbo.applySnapshot(parseBboSnapshot(raw)),
|
|
2449
|
-
reset: () => bbo.reset(),
|
|
2450
|
-
view: () => bbo.view(),
|
|
2451
|
-
copy: (value) => ({ ...value }),
|
|
2452
|
-
get ready() {
|
|
2453
|
-
return bbo.ready;
|
|
2454
|
-
},
|
|
2455
|
-
};
|
|
2456
|
-
}, onBbo, localOptions);
|
|
2457
|
-
};
|
|
2458
1956
|
const subscribeOHLCV = (tradingPairId, tradingMode, interval, handler, priceType, onSnapshot) => {
|
|
2459
1957
|
// Four-token channel selects the trade-derived candles (back-compat); the
|
|
2460
1958
|
// optional fifth token selects the price series (`mark` = close-only mark
|
|
@@ -2505,43 +2003,8 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
2505
2003
|
}
|
|
2506
2004
|
}, onSnapshot && snapshotFrameHandler("ohlcv", (raw) => parseOHLCVSnapshotCandle(raw, interval), onSnapshot));
|
|
2507
2005
|
};
|
|
2508
|
-
const subscribeTrades = (tradingPairId, handler, onSnapshot
|
|
2006
|
+
const subscribeTrades = (tradingPairId, handler, onSnapshot) => {
|
|
2509
2007
|
const channel = `trades:${tradingPairId}`;
|
|
2510
|
-
// The chain check runs once a snapshot carries a cursor key (the
|
|
2511
|
-
// market-data source); under the engine source every trade is delivered
|
|
2512
|
-
// as before. The server forwards only trades linking to this
|
|
2513
|
-
// connection's cursor and reseeds by itself on a break it sees, so the
|
|
2514
|
-
// check here is a safety net: it drops a trade at or below the cursor or
|
|
2515
|
-
// the last delivered one, and reports a trade that does not link (a
|
|
2516
|
-
// frame this client could not read, say) to onGap, then continues from
|
|
2517
|
-
// it. Withholding would freeze the feed, since the server believes the
|
|
2518
|
-
// connection is in sync and sends no fresh snapshot for it.
|
|
2519
|
-
let checking = false;
|
|
2520
|
-
let last = null;
|
|
2521
|
-
const deliverSnapshot = onSnapshot && snapshotFrameHandler("trades", parseTradeEventData, onSnapshot);
|
|
2522
|
-
const onSnapshotFrame = (rawData, frame) => {
|
|
2523
|
-
let meta;
|
|
2524
|
-
if (frame && "cursor" in frame) {
|
|
2525
|
-
try {
|
|
2526
|
-
const cursor = parseTradeSnapshotCursor(frame.cursor);
|
|
2527
|
-
// The internal bound is a copy: a consumer mutating meta.cursor
|
|
2528
|
-
// must not move it.
|
|
2529
|
-
last = cursor === null ? null : { ...cursor };
|
|
2530
|
-
checking = true;
|
|
2531
|
-
meta = { cursor };
|
|
2532
|
-
}
|
|
2533
|
-
catch (err) {
|
|
2534
|
-
// An unreadable cursor leaves nothing to check against: deliver as
|
|
2535
|
-
// the engine source does until the next snapshot (the rows still go).
|
|
2536
|
-
console.error("WebSocket: Error processing trades snapshot cursor", err);
|
|
2537
|
-
checking = false;
|
|
2538
|
-
last = null;
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
deliverSnapshot?.(rawData, frame, meta);
|
|
2542
|
-
};
|
|
2543
|
-
if (!deliverSnapshot)
|
|
2544
|
-
internalSnapshotHandlers.add(onSnapshotFrame);
|
|
2545
2008
|
return subscribe(channel, (rawData) => {
|
|
2546
2009
|
try {
|
|
2547
2010
|
const data = rawData;
|
|
@@ -2557,37 +2020,13 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
2557
2020
|
makerSide: tradeData.maker_side.toUpperCase(),
|
|
2558
2021
|
executedAt: tradeData.executed_at,
|
|
2559
2022
|
},
|
|
2560
|
-
...readTradeChain(data),
|
|
2561
2023
|
};
|
|
2562
|
-
if (checking && event.seq !== undefined && event.matchIndex !== undefined) {
|
|
2563
|
-
const key = { seq: event.seq, matchIndex: event.matchIndex };
|
|
2564
|
-
if (last !== null) {
|
|
2565
|
-
// At or below the cursor or the last delivered trade: already reflected.
|
|
2566
|
-
if (tradeKeyAtOrBelow(key, last))
|
|
2567
|
-
return;
|
|
2568
|
-
if (event.prevSeq !== last.seq || event.prevMatchIndex !== last.matchIndex) {
|
|
2569
|
-
// A throwing onGap must not stop the chain advancing or the
|
|
2570
|
-
// trade being delivered.
|
|
2571
|
-
try {
|
|
2572
|
-
tradesOptions?.onGap?.({
|
|
2573
|
-
tradingPairId: event.tradingPairId,
|
|
2574
|
-
expected: { seq: last.seq, matchIndex: last.matchIndex },
|
|
2575
|
-
received: { prevSeq: event.prevSeq ?? null, prevMatchIndex: event.prevMatchIndex ?? null },
|
|
2576
|
-
});
|
|
2577
|
-
}
|
|
2578
|
-
catch (err) {
|
|
2579
|
-
console.error("WebSocket: Error in trades onGap", err);
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
last = key;
|
|
2584
|
-
}
|
|
2585
2024
|
handler(event);
|
|
2586
2025
|
}
|
|
2587
2026
|
catch (err) {
|
|
2588
2027
|
console.error("WebSocket: Error processing trade event", err);
|
|
2589
2028
|
}
|
|
2590
|
-
},
|
|
2029
|
+
}, onSnapshot && snapshotFrameHandler("trades", parseTradeEventData, onSnapshot));
|
|
2591
2030
|
};
|
|
2592
2031
|
const subscribeMovements = (handler) => {
|
|
2593
2032
|
// User is identified from the authenticated session, so channel is just "movements"
|
|
@@ -3083,8 +2522,6 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
|
|
|
3083
2522
|
},
|
|
3084
2523
|
orders: subscribeOrders,
|
|
3085
2524
|
orderbook: subscribeOrderbook,
|
|
3086
|
-
depth: subscribeDepth,
|
|
3087
|
-
bbo: subscribeBbo,
|
|
3088
2525
|
ohlcv: subscribeOHLCV,
|
|
3089
2526
|
trades: subscribeTrades,
|
|
3090
2527
|
movements: subscribeMovements,
|