@0xmonaco/core 1.1.2 → 1.1.5-prerelease.de852e2

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.
@@ -2,6 +2,7 @@ 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";
5
6
  import { keysToCamelCase } from "./utils";
6
7
  // Connection constants
7
8
  const CONNECTION_TIMEOUT = 10000;
@@ -21,6 +22,8 @@ const SKEW_REJECTION = /timestamp outside allowed skew/i;
21
22
  // Unsubscribe for listing too many channels, after parsing it (ws-api
22
23
  // `oversized_channel_list_message`) or before (`too_many_elements_message`).
23
24
  // No ack follows either.
25
+ /** `Resynced` frames arriving this close together form one reported burst. */
26
+ const RESYNCED_BURST_MS = 50;
24
27
  const OVERSIZED_REQUEST = /^(?:(?:Subscribe|Unsubscribe) lists \d+ channels; at most \d+ per request|Message lists too many elements; at most \d+ channels per request)$/;
25
28
  function isRecord(value) {
26
29
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -116,6 +119,17 @@ function readOptionalNumber(source, field) {
116
119
  }
117
120
  return value;
118
121
  }
122
+ /**
123
+ * Read a wire integer that can be negative (a maker's fee rate is negative for
124
+ * a rebate): any safe integer, unlike {@link readSequence}.
125
+ */
126
+ function readInteger(source, field) {
127
+ const value = source[field];
128
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
129
+ throw new Error(`Invalid WebSocket event: ${field} must be a safe integer`);
130
+ }
131
+ return value;
132
+ }
119
133
  function readOptionalSequence(source, field) {
120
134
  const value = source[field];
121
135
  if (value === undefined || value === null)
@@ -395,6 +409,150 @@ function parseMarketLiquidationEvent(rawData) {
395
409
  data: parseMarketLiquidationEventData(rawData.data),
396
410
  };
397
411
  }
412
+ /**
413
+ * Parse a new RFQ to quote. The `rfq` channel's wire keys are camelCase
414
+ * already (serde `rename_all = "camelCase"`), so they are read as sent, bar
415
+ * the `type` tag, surfaced as `eventType` (see {@link parseRfqEvent}). A
416
+ * two-way request carries neither side.
417
+ */
418
+ function parseRfqRequestedEvent(raw) {
419
+ const oneWaySide = readOptionalEnumValue(raw, "oneWaySide");
420
+ const quoteSide = readOptionalEnumValue(raw, "quoteSide");
421
+ return {
422
+ eventType: "RfqRequested",
423
+ rfqId: readString(raw, "rfqId"),
424
+ tradingPairId: readString(raw, "tradingPairId"),
425
+ // Value fields: a product or side this SDK predates is passed through.
426
+ product: readString(raw, "product"),
427
+ quantity: readString(raw, "quantity"),
428
+ ...(oneWaySide !== undefined && { oneWaySide }),
429
+ ...(quoteSide !== undefined && { quoteSide }),
430
+ newQuoteCutoffAtMs: readSequence(raw, "newQuoteCutoffAtMs"),
431
+ };
432
+ }
433
+ function parseRfqSideViewEventData(raw) {
434
+ if (!isRecord(raw)) {
435
+ throw new Error("Invalid RFQ view: a side must be an object");
436
+ }
437
+ return {
438
+ vwap: readString(raw, "vwap"),
439
+ availableQuantity: readString(raw, "availableQuantity"),
440
+ };
441
+ }
442
+ /** Parse the taker's view. A side no maker quotes is absent. */
443
+ function parseRfqViewEventData(raw) {
444
+ if (!isRecord(raw)) {
445
+ throw new Error("Invalid RFQ view: view must be an object");
446
+ }
447
+ return {
448
+ viewVersion: readSequence(raw, "viewVersion"),
449
+ ...(raw.bid != null && { bid: parseRfqSideViewEventData(raw.bid) }),
450
+ ...(raw.ask != null && { ask: parseRfqSideViewEventData(raw.ask) }),
451
+ asOfMs: readSequence(raw, "asOfMs"),
452
+ sequence: readSequence(raw, "sequence"),
453
+ };
454
+ }
455
+ /** Parse a selected maker's own part of an acceptance. */
456
+ function parseRfqSelectionEventData(raw) {
457
+ if (!isRecord(raw)) {
458
+ throw new Error("Invalid RFQ selection: selection must be an object");
459
+ }
460
+ return {
461
+ acceptanceId: readString(raw, "acceptanceId"),
462
+ takerSide: readString(raw, "takerSide"),
463
+ makerAccountId: readString(raw, "makerAccountId"),
464
+ quoteId: readString(raw, "quoteId"),
465
+ quoteVersion: readSequence(raw, "quoteVersion"),
466
+ quantity: readString(raw, "quantity"),
467
+ price: readString(raw, "price"),
468
+ capacity: readString(raw, "capacity"),
469
+ makerFeeCbps: readInteger(raw, "makerFeeCbps"),
470
+ lastLookDeadlineAtMs: readSequence(raw, "lastLookDeadlineAtMs"),
471
+ };
472
+ }
473
+ function parseRfqFillEventData(raw) {
474
+ if (!isRecord(raw)) {
475
+ throw new Error("Invalid RFQ outcome: a fill must be an object");
476
+ }
477
+ return {
478
+ executionId: readString(raw, "executionId"),
479
+ price: readString(raw, "price"),
480
+ quantity: readString(raw, "quantity"),
481
+ fee: readString(raw, "fee"),
482
+ };
483
+ }
484
+ /** Parse the taker's outcome. Nothing filled leaves `vwap` absent and `fills` empty. */
485
+ function parseRfqOutcomeEventData(raw) {
486
+ if (!isRecord(raw)) {
487
+ throw new Error("Invalid RFQ outcome: outcome must be an object");
488
+ }
489
+ if (!Array.isArray(raw.fills)) {
490
+ throw new Error("Invalid RFQ outcome: fills must be an array");
491
+ }
492
+ const vwap = readOptionalDecimalString(raw, "vwap");
493
+ return {
494
+ executedQuantity: readString(raw, "executedQuantity"),
495
+ unfilledQuantity: readString(raw, "unfilledQuantity"),
496
+ ...(vwap !== undefined && { vwap }),
497
+ fills: raw.fills.map(parseRfqFillEventData),
498
+ };
499
+ }
500
+ /** Parse an RFQ's end. Only the taker's push carries `outcome`. */
501
+ function parseRfqOutcomeEvent(raw) {
502
+ return {
503
+ eventType: "RfqOutcome",
504
+ rfqId: readString(raw, "rfqId"),
505
+ // A terminal state this SDK predates is passed through.
506
+ state: readString(raw, "state"),
507
+ ...(raw.outcome != null && { outcome: parseRfqOutcomeEventData(raw.outcome) }),
508
+ };
509
+ }
510
+ /**
511
+ * Parse an `rfq` channel push. The wire's `type` is the channel-class
512
+ * discriminator that picks the payload shape, so it is matched strictly: a push
513
+ * of a type this SDK predates is rejected rather than parsed as the wrong
514
+ * shape. It is surfaced as `eventType`, the discriminator every other channel's
515
+ * events carry; no `type` key is kept.
516
+ */
517
+ function parseRfqEvent(rawData) {
518
+ if (!isRecord(rawData)) {
519
+ throw new Error("Invalid RFQ event: payload must be an object");
520
+ }
521
+ const eventType = readEnum(rawData, "type", ["RfqRequested", "RfqView", "RfqSelected", "RfqOutcome"]);
522
+ switch (eventType) {
523
+ case "RfqRequested":
524
+ return parseRfqRequestedEvent(rawData);
525
+ case "RfqView":
526
+ return { eventType, rfqId: readString(rawData, "rfqId"), view: parseRfqViewEventData(rawData.view) };
527
+ case "RfqSelected":
528
+ return { eventType, rfqId: readString(rawData, "rfqId"), selection: parseRfqSelectionEventData(rawData.selection) };
529
+ case "RfqOutcome":
530
+ return parseRfqOutcomeEvent(rawData);
531
+ }
532
+ }
533
+ function parseRfqTradeEventData(raw) {
534
+ if (!isRecord(raw)) {
535
+ throw new Error("Invalid RFQ trade event: data must be an object");
536
+ }
537
+ return {
538
+ executionId: readString(raw, "execution_id"),
539
+ price: readString(raw, "price"),
540
+ quantity: readString(raw, "quantity"),
541
+ executedAt: readString(raw, "executed_at"),
542
+ publishedAt: readString(raw, "published_at"),
543
+ };
544
+ }
545
+ /** Parse a public RFQ block print from `rfq_trades:<pair>`. */
546
+ function parseRfqTradeEvent(rawData) {
547
+ if (!isRecord(rawData)) {
548
+ throw new Error("Invalid RFQ trade event: payload must be an object");
549
+ }
550
+ return {
551
+ tradingPairId: readString(rawData, "trading_pair_id"),
552
+ tradingMode: readString(rawData, "trading_mode"),
553
+ data: parseRfqTradeEventData(rawData.data),
554
+ };
555
+ }
398
556
  /**
399
557
  * Parse an account's margin-health payload. Shared by the live event and the
400
558
  * subscribe-time snapshot (see {@link parseConditionalOrderEventData}).
@@ -622,8 +780,18 @@ function parseOrderbookEvent(rawData) {
622
780
  quoteDecimals: data.quote_decimals || 0,
623
781
  timestamp: data.timestamp || new Date().toISOString(),
624
782
  sequence: data.sequence_number || 0,
783
+ // Sent only when the server renders `orderbook` from market-data's book
784
+ // (0XM-3552), as JSON numbers; surfaced as decimal strings like REST's
785
+ // `bookSeq` (a u64 may pass `Number.MAX_SAFE_INTEGER`).
786
+ ...sequenceField("bookSeq", data, "book_seq"),
787
+ ...sequenceField("prevBookSeq", data, "prev_book_seq"),
625
788
  };
626
789
  }
790
+ /** `{ [name]: sequence }` when `data[key]` is an exact u64 sequence, `{}` otherwise, so an absent or malformed key never surfaces. */
791
+ function sequenceField(name, data, key) {
792
+ const sequence = tryReadSequence(data, key);
793
+ return sequence === undefined ? {} : { [name]: sequence };
794
+ }
627
795
  /**
628
796
  * Parse the one row of an `orderbook` snapshot: the channel's current book in
629
797
  * the live frame's shape, read the same way.
@@ -631,7 +799,9 @@ function parseOrderbookEvent(rawData) {
631
799
  * The live parser casts; here a row that is not an object, or carries no `data`
632
800
  * object, throws, so {@link snapshotFrameHandler} withholds the frame rather
633
801
  * than handing over an empty book. The engine's book read carries no sequence,
634
- * so the row's `sequence` is 0.
802
+ * so under the engine source the row's `sequence` is 0; when the server renders
803
+ * the channel from market-data's book (0XM-3552) the row carries `book_seq` as
804
+ * `sequence_number` and surfaces `bookSeq` / `prevBookSeq` like a live frame.
635
805
  */
636
806
  function parseOrderbookSnapshotRow(raw) {
637
807
  if (!isRecord(raw) || !isRecord(raw.data)) {
@@ -991,35 +1161,23 @@ function parseOrderSnapshotItem(raw) {
991
1161
  ...(placementMakerFeeBps === undefined ? {} : { placementMakerFeeBps }),
992
1162
  };
993
1163
  }
994
- /**
995
- * Wrap a caller's `onSnapshot` in the internal snapshot-frame handler.
996
- *
997
- * The frame's `data` is the channel's COMPLETE current state as a flat array,
998
- * and that completeness is the whole contract: a consumer is expected to
999
- * replace state from it, so absence from the array means the entity is gone.
1000
- *
1001
- * Delivery is therefore all-or-nothing. If the payload is not an array, or any
1002
- * single row fails to parse, NOTHING is delivered.
1003
- *
1004
- * Skipping just the unreadable row and delivering the rest would be worse than
1005
- * delivering nothing, because the array would still be presented as complete: a
1006
- * consumer would drop the entity that row described — zero its balance, remove
1007
- * its market, cancel its order in the UI — on the strength of an omission the
1008
- * SDK itself created. Nor is that self-correcting. A resting order or an
1009
- * unchanged market has no later diff to repair it, so the deletion would stand
1010
- * for the life of the connection.
1011
- *
1012
- * Withholding the frame instead leaves the consumer on whatever baseline it
1013
- * already had, and the channel keeps streaming live events over it. That is a
1014
- * strictly smaller loss, and it is the same all-or-nothing rule this channel's
1015
- * original `instrument` snapshot already applied.
1016
- *
1017
- * `[]` is a legitimate baseline, not a failure, and is always delivered:
1018
- * "nothing is open" is exactly the information a consumer needs to clear stale
1019
- * state after a reconnect.
1020
- */
1164
+ /** A `trades:<pair>` snapshot cursor; `null` is a meaningful value (no trade applied since seeding). */
1165
+ function parseTradeSnapshotCursor(raw) {
1166
+ if (raw === null)
1167
+ return null;
1168
+ if (!isRecord(raw))
1169
+ throw new Error("Invalid trades snapshot cursor: must be an object or null");
1170
+ const seq = raw.seq;
1171
+ const matchIndex = raw.match_index;
1172
+ if (!Number.isSafeInteger(seq) || seq < 0 || !Number.isSafeInteger(matchIndex) || matchIndex < 0) {
1173
+ throw new Error("Invalid trades snapshot cursor: seq and match_index must be non-negative integers");
1174
+ }
1175
+ return { seq: seq, matchIndex: matchIndex };
1176
+ }
1021
1177
  function snapshotFrameHandler(channel, parseItem, onSnapshot) {
1022
- return (rawData) => {
1178
+ // `frame` is the router's second argument, unused here; `meta` is passed
1179
+ // only by a channel that reads something beside the rows (trades' cursor).
1180
+ return (rawData, _frame, meta) => {
1023
1181
  try {
1024
1182
  if (!Array.isArray(rawData)) {
1025
1183
  throw new Error(`Invalid ${channel} snapshot: data must be an array`);
@@ -1027,13 +1185,86 @@ function snapshotFrameHandler(channel, parseItem, onSnapshot) {
1027
1185
  // Parsed fully before anything is handed over, so a failure part-way
1028
1186
  // through cannot deliver a truncated baseline.
1029
1187
  const items = rawData.map(parseItem);
1030
- onSnapshot(items);
1188
+ // Only a snapshot that carries meta passes it, so every other channel
1189
+ // sees exactly the call it always did.
1190
+ if (meta)
1191
+ onSnapshot(items, meta);
1192
+ else
1193
+ onSnapshot(items);
1031
1194
  }
1032
1195
  catch (err) {
1033
1196
  console.error(`WebSocket: Error processing ${channel} snapshot`, err);
1034
1197
  }
1035
1198
  };
1036
1199
  }
1200
+ function tradeKeyAtOrBelow(key, bound) {
1201
+ return key.seq < bound.seq || (key.seq === bound.seq && key.matchIndex <= bound.matchIndex);
1202
+ }
1203
+ /** An optional chain field: a non-negative integer, else absent (a malformed value never drops the trade). */
1204
+ function readOptionalChainNumber(data, key) {
1205
+ const value = data[key];
1206
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
1207
+ }
1208
+ /** The trade frame's chain fields (market-data source only), set only when present and well formed. */
1209
+ function readTradeChain(data) {
1210
+ const chain = {};
1211
+ const seq = readOptionalChainNumber(data, "seq");
1212
+ const matchIndex = readOptionalChainNumber(data, "match_index");
1213
+ const prevSeq = readOptionalChainNumber(data, "prev_seq");
1214
+ if (seq !== undefined)
1215
+ chain.seq = seq;
1216
+ if (matchIndex !== undefined)
1217
+ chain.matchIndex = matchIndex;
1218
+ if (prevSeq !== undefined)
1219
+ chain.prevSeq = prevSeq;
1220
+ if (data.prev_match_index === null)
1221
+ chain.prevMatchIndex = null;
1222
+ else {
1223
+ const prevMatchIndex = readOptionalChainNumber(data, "prev_match_index");
1224
+ if (prevMatchIndex !== undefined)
1225
+ chain.prevMatchIndex = prevMatchIndex;
1226
+ }
1227
+ return chain;
1228
+ }
1229
+ /** Backoff for repeated client-side rebuilds (a gap, an unreadable frame or snapshot, a thinned side). */
1230
+ const SNAPSHOT_RETRY_BASE_MS = 500;
1231
+ const SNAPSHOT_RETRY_MAX_MS = 15_000;
1232
+ /**
1233
+ * After SNAPSHOT_UNAVAILABLE the subscription stays routed: the server retries
1234
+ * a failed book (5 s doubling to 60 s) or a capped one once a slot frees, and
1235
+ * pushes the healing snapshot with no client action. A resubscribe lands on
1236
+ * the same per-pod book, so it is never required; this timer is only a
1237
+ * harmless safety net. The cases that never heal (an unknown pair id, a
1238
+ * durable-log reset until ws-api restarts) are not fixed by it either.
1239
+ */
1240
+ const SNAPSHOT_FALLBACK_BASE_MS = 90_000;
1241
+ const SNAPSHOT_FALLBACK_MAX_MS = 300_000;
1242
+ /**
1243
+ * Whether a raw frame belongs to a channel carrying market-data sequences:
1244
+ * depth, bbo, or orderbook rendered from market-data's book (0XM-3552). A
1245
+ * cheap prefix test before the sequence quoting pass.
1246
+ */
1247
+ const MARKET_DATA_CHANNEL_KEY = /"channel"\s*:\s*"(?:depth|bbo|orderbook):/;
1248
+ function isMarketDataFrame(raw) {
1249
+ // The substring test first: almost every frame fails it without a regex.
1250
+ return raw.includes("book_seq") && MARKET_DATA_CHANNEL_KEY.test(raw);
1251
+ }
1252
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1253
+ /**
1254
+ * The canonical market-data channel: lowercase pair UUID, uppercase mode. The
1255
+ * server sends live frames on this form and echoes the subscribed form on
1256
+ * the snapshot, so subscribing canonically keeps both on one key.
1257
+ */
1258
+ function marketDataChannel(kind, tradingPairId, tradingMode) {
1259
+ if (!tradingPairId || !UUID_PATTERN.test(tradingPairId)) {
1260
+ throw new Error(`${kind} subscription requires a trading pair UUID, got "${tradingPairId}"`);
1261
+ }
1262
+ const mode = String(tradingMode ?? "").toUpperCase();
1263
+ if (mode !== "SPOT" && mode !== "MARGIN") {
1264
+ throw new Error(`${kind} subscription requires tradingMode "SPOT" or "MARGIN", got "${tradingMode}"`);
1265
+ }
1266
+ return `${kind}:${tradingPairId.toLowerCase()}:${mode}`;
1267
+ }
1037
1268
  /**
1038
1269
  * Create a Monaco WebSocket client
1039
1270
  *
@@ -1076,6 +1307,22 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1076
1307
  // Close code of the connection loss the next successful reopen recovers
1077
1308
  // from; null when no resync is owed (first connect, clean disconnect).
1078
1309
  let pendingResyncCode = null;
1310
+ // Scoped resync (0XM-3553): offer the capability once per socket, on its
1311
+ // first Authenticate or Subscribe; the server keeps it for the connection.
1312
+ // Only to a client with an onResync: without one a scoped resync would
1313
+ // leave the app silently stale, where the 1013 close at least resubscribes
1314
+ // and re-delivers subscribe snapshots.
1315
+ const offerResync = (options.scopedResync ?? true) && options.onResync !== undefined;
1316
+ let resyncOffered = false;
1317
+ let resyncedBursts = [];
1318
+ // A scoped refetch was still owed when a socket was lost: the next
1319
+ // connection's onResync reports it (as a 1013 gap if the close itself owed
1320
+ // none).
1321
+ let scopedResyncOwed = false;
1322
+ // Snapshot handlers that keep only internal state and hand the app
1323
+ // nothing (the trades chain cursor without an app `onSnapshot`): a re-sent
1324
+ // snapshot through one alone still reaches no app code.
1325
+ const internalSnapshotHandlers = new WeakSet();
1079
1326
  // Authentication-ending Error frame received on the current socket, tagged
1080
1327
  // with the session it rejected. The server answers handshakes in the order
1081
1328
  // they were sent, so each Authenticated / AUTH_FAILED reply belongs to the
@@ -1097,6 +1344,14 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1097
1344
  // reusing the Event handlers would feed every other channel's parser a frame
1098
1345
  // it must reject.
1099
1346
  const snapshotHandlers = new Map();
1347
+ // Per-channel listeners for Error frames that name a channel (the local
1348
+ // depth/bbo books retry a SNAPSHOT_UNAVAILABLE subscribe). They run before
1349
+ // the app's onError and never stop it.
1350
+ const channelErrorHandlers = new Map();
1351
+ // One shared local book per depth/bbo channel: the server sends a snapshot
1352
+ // only for a new wire subscription, so a second local subscriber must share
1353
+ // the first one's state rather than wait for a snapshot that never comes.
1354
+ const localStateChannels = new Map();
1100
1355
  // Latest known full-config frame per market for each subscribed instrument
1101
1356
  // channel, seeded by the subscribe-time Snapshot and superseded by every
1102
1357
  // live frame (instrument frames are terminal-state, never deltas). The
@@ -1202,6 +1457,7 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1202
1457
  public_key: session.publicKey,
1203
1458
  timestamp,
1204
1459
  signature,
1460
+ ...capabilitiesOnce(),
1205
1461
  }));
1206
1462
  };
1207
1463
  const stopHeartbeat = () => {
@@ -1289,7 +1545,67 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1289
1545
  if (channels.length === 0 || ws?.readyState !== WebSocket.OPEN)
1290
1546
  return;
1291
1547
  pendingRequests.push({ kind: "subscribe", channels });
1292
- send({ type: "Subscribe", channels });
1548
+ send({ type: "Subscribe", channels, ...capabilitiesOnce() });
1549
+ };
1550
+ /** The capability offer, on the first request of each socket only. */
1551
+ const capabilitiesOnce = () => {
1552
+ if (!offerResync || resyncOffered)
1553
+ return {};
1554
+ resyncOffered = true;
1555
+ return { capabilities: ["resync"] };
1556
+ };
1557
+ /** Report `channel` in a scoped onResync, at least `delayMs` from now. */
1558
+ const scheduleResynced = (channel, delayMs) => {
1559
+ const now = performance.now();
1560
+ let burst = resyncedBursts.at(-1);
1561
+ if (!burst || now - burst.opened > RESYNCED_BURST_MS) {
1562
+ // Held open for the whole window, so frames still arriving join it.
1563
+ burst = { opened: now, due: now + RESYNCED_BURST_MS, channels: new Set(), timer: null };
1564
+ resyncedBursts.push(burst);
1565
+ }
1566
+ burst.channels.add(channel);
1567
+ burst.due = Math.max(burst.due, now + delayMs);
1568
+ armResynced(burst);
1569
+ };
1570
+ /** (Re)arm `burst`'s timer for its deadline, rounding up. */
1571
+ const armResynced = (burst) => {
1572
+ if (burst.timer)
1573
+ clearTimeout(burst.timer);
1574
+ burst.timer = setTimeout(() => flushResynced(burst), Math.max(0, Math.ceil(burst.due - performance.now())));
1575
+ };
1576
+ const flushResynced = (burst) => {
1577
+ // A timer can fire before the monotonic deadline: wait out the rest.
1578
+ if (performance.now() < burst.due) {
1579
+ armResynced(burst);
1580
+ return;
1581
+ }
1582
+ resyncedBursts = resyncedBursts.filter((other) => other !== burst);
1583
+ const channels = [...burst.channels].filter((channel) => handlers.has(channel));
1584
+ if (channels.length === 0)
1585
+ return;
1586
+ // Called from a timer: a throw or a rejection must not escape it.
1587
+ const logResyncError = (err) => console.error("WebSocket: onResync threw", err);
1588
+ try {
1589
+ const result = options.onResync?.({ code: 1013, slowClient: true, reason: "resynced", channels });
1590
+ if (result instanceof Promise)
1591
+ result.catch(logResyncError);
1592
+ }
1593
+ catch (err) {
1594
+ logResyncError(err);
1595
+ }
1596
+ };
1597
+ /**
1598
+ * Drop every pending scoped resync; whether one was owed. A reconnect's
1599
+ * onResync covers it.
1600
+ */
1601
+ const cancelResynced = () => {
1602
+ // Owed only for a channel the app still holds.
1603
+ const owed = resyncedBursts.some((burst) => [...burst.channels].some((channel) => handlers.has(channel)));
1604
+ for (const burst of resyncedBursts)
1605
+ if (burst.timer)
1606
+ clearTimeout(burst.timer);
1607
+ resyncedBursts = [];
1608
+ return owed;
1293
1609
  };
1294
1610
  const sendUnsubscribe = (channels) => {
1295
1611
  if (channels.length === 0 || ws?.readyState !== WebSocket.OPEN)
@@ -1440,6 +1756,8 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1440
1756
  // oldest outstanding request: retire it, or every later ack would
1441
1757
  // pair with the wrong request.
1442
1758
  if (msg.code === "INVALID_MESSAGE" && OVERSIZED_REQUEST.test(msg.message)) {
1759
+ // Refused unread: any capability it carried was never recorded.
1760
+ resyncOffered = false;
1443
1761
  pendingRequests.shift();
1444
1762
  }
1445
1763
  if (msg.code === "AUTH_FAILED") {
@@ -1458,6 +1776,16 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1458
1776
  // actionable without it. (The frame is untyped JSON, so the declared
1459
1777
  // `string` type of `channel` is not trusted here.)
1460
1778
  const channel = msg.channel;
1779
+ if (typeof channel === "string") {
1780
+ for (const handler of channelErrorHandlers.get(channel) ?? []) {
1781
+ try {
1782
+ handler(msg.code, msg.message);
1783
+ }
1784
+ catch (err) {
1785
+ console.error("WebSocket: Error in channel error handler", err);
1786
+ }
1787
+ }
1788
+ }
1461
1789
  options.onError?.(typeof channel === "string" ? { code: msg.code, message: msg.message, channel } : { code: msg.code, message: msg.message });
1462
1790
  }
1463
1791
  return;
@@ -1503,13 +1831,36 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1503
1831
  }
1504
1832
  return;
1505
1833
  }
1834
+ if (msg.type === "Resynced") {
1835
+ // A channel the server could not repair in place (0XM-3553). A frame
1836
+ // missing either field is malformed and ignored.
1837
+ const retryAfterMs = msg.retry_after_ms;
1838
+ if (typeof msg.channel === "string" && typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
1839
+ scheduleResynced(msg.channel, retryAfterMs);
1840
+ }
1841
+ return;
1842
+ }
1506
1843
  if (!msg.channel)
1507
1844
  return;
1508
1845
  if (msg.type === "Snapshot") {
1509
1846
  // Exact match only: the Snapshot frame's channel echoes the raw
1510
1847
  // subscribed channel, never a more specific one.
1511
- for (const handler of snapshotHandlers.get(msg.channel) ?? []) {
1512
- handler(msg.data);
1848
+ const channelSnapshotHandlers = snapshotHandlers.get(msg.channel);
1849
+ for (const handler of channelSnapshotHandlers ?? []) {
1850
+ handler(msg.data, msg);
1851
+ }
1852
+ // A server-side repair after an upstream loss (0XM-3553) is marked; a
1853
+ // channel consumed without an app snapshot handler would never see it.
1854
+ if (offerResync && msg.resynced === true && handlers.has(msg.channel)) {
1855
+ // Every subscriber needs an app snapshot handler for the repair to
1856
+ // reach it; an event-only one is told to refetch instead.
1857
+ let appSnapshotHandlers = 0;
1858
+ for (const handler of channelSnapshotHandlers ?? []) {
1859
+ if (!internalSnapshotHandlers.has(handler))
1860
+ appSnapshotHandlers++;
1861
+ }
1862
+ if (appSnapshotHandlers < (handlers.get(msg.channel)?.size ?? 0))
1863
+ scheduleResynced(msg.channel, 0);
1513
1864
  }
1514
1865
  return;
1515
1866
  }
@@ -1532,6 +1883,12 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1532
1883
  const handleSocketLoss = (code) => {
1533
1884
  ws = null;
1534
1885
  stopHeartbeat();
1886
+ // A scoped refetch still owed is owed by whichever connection comes next,
1887
+ // even one opened later by connect() rather than by the reconnect loop.
1888
+ if (cancelResynced())
1889
+ scopedResyncOwed = true;
1890
+ for (const entry of [...localStateChannels.values()])
1891
+ entry.onSocketLoss();
1535
1892
  // The server dropped every subscription with the connection.
1536
1893
  resetSubscriptionAcks();
1537
1894
  // Terminal authentication loss: the server revoked the session
@@ -1611,6 +1968,7 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1611
1968
  try {
1612
1969
  const socket = new WebSocket(getUrl());
1613
1970
  ws = socket;
1971
+ resyncOffered = false;
1614
1972
  lastAuthError = null;
1615
1973
  pendingAuthSessions = [];
1616
1974
  confirmedSession = undefined;
@@ -1652,9 +2010,10 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1652
2010
  //
1653
2011
  // This callback waits for neither acknowledgements nor snapshots,
1654
2012
  // and the SDK performs no REST recovery itself.
1655
- if (pendingResyncCode !== null) {
1656
- const code = pendingResyncCode;
2013
+ if (pendingResyncCode !== null || scopedResyncOwed) {
2014
+ const code = pendingResyncCode ?? 1013;
1657
2015
  pendingResyncCode = null;
2016
+ scopedResyncOwed = false;
1658
2017
  options.onResync?.({ code, slowClient: code === 1013 });
1659
2018
  }
1660
2019
  resolve();
@@ -1664,7 +2023,10 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1664
2023
  return;
1665
2024
  lastMessageAt = Date.now();
1666
2025
  try {
1667
- const msg = JSON.parse(event.data);
2026
+ // Market-data sequences are u64: quote them before parsing so
2027
+ // one above Number.MAX_SAFE_INTEGER stays exact.
2028
+ const raw = event.data;
2029
+ const msg = JSON.parse(typeof raw === "string" && isMarketDataFrame(raw) ? quoteSequenceNumbers(raw) : raw);
1668
2030
  // Handle pong silently
1669
2031
  if (msg.type === "Pong")
1670
2032
  return;
@@ -1714,6 +2076,14 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1714
2076
  stopReconnect();
1715
2077
  stopHeartbeat();
1716
2078
  pendingResyncCode = null;
2079
+ cancelResynced();
2080
+ scopedResyncOwed = false;
2081
+ // Local depth/bbo books go with their wire handlers: stop each (its retry
2082
+ // timer too) so a later depth()/bbo() call starts a fresh subscription.
2083
+ for (const entry of [...localStateChannels.values()])
2084
+ entry.stop();
2085
+ localStateChannels.clear();
2086
+ channelErrorHandlers.clear();
1717
2087
  handlers.clear();
1718
2088
  snapshotHandlers.clear();
1719
2089
  instrumentBaseline.clear();
@@ -1798,6 +2168,266 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1798
2168
  }
1799
2169
  }, onSnapshot && snapshotFrameHandler("orderbook", parseOrderbookSnapshotRow, onSnapshot));
1800
2170
  };
2171
+ /**
2172
+ * Keep one shared, sequenced local state for a depth/bbo channel: the
2173
+ * subscribe-time snapshot is the base, linked frames update it, and a gap,
2174
+ * an unreadable frame or snapshot, an exhausted incomplete side or a
2175
+ * SNAPSHOT_UNAVAILABLE rebuilds it from a fresh wire subscription.
2176
+ */
2177
+ const openLocalState = (channel, createState, onValue, localOptions) => {
2178
+ let entry = localStateChannels.get(channel);
2179
+ if (!entry) {
2180
+ const state = createState();
2181
+ const listeners = new Set();
2182
+ let unsubscribeWire = null;
2183
+ let stopped = false;
2184
+ // One pending resubscribe at a time (a rebuild, or the fallback after a
2185
+ // SNAPSHOT_UNAVAILABLE); scheduling another supersedes it, and a stale
2186
+ // timer or microtask finds its token outdated and does nothing.
2187
+ let scheduleToken = 0;
2188
+ let scheduledTimer = null;
2189
+ // Set while a rebuild is pending: frames still being routed for the old
2190
+ // subscription (the router may visit the re-added channel in the same
2191
+ // pass) are ignored rather than buffered against the next snapshot.
2192
+ let rebuildPending = false;
2193
+ // Rebuilds since the last frame that applied cleanly on a fresh
2194
+ // snapshot: a fault that recurs on every snapshot backs off instead of
2195
+ // resubscribing per frame.
2196
+ let consecutiveRebuilds = 0;
2197
+ let fallbackDelay = SNAPSHOT_FALLBACK_BASE_MS;
2198
+ // The channel's last rejection, replayed to a listener that joins later
2199
+ // (no new Subscribe goes out for it); a snapshot clears it.
2200
+ let lastRejection = null;
2201
+ const forEachListener = (what, call) => {
2202
+ for (const listener of [...listeners]) {
2203
+ try {
2204
+ call(listener);
2205
+ }
2206
+ catch (err) {
2207
+ console.error(`WebSocket: Error in ${channel} ${what}`, err);
2208
+ }
2209
+ }
2210
+ };
2211
+ // The view is built once; each listener gets its own copy of it, so one
2212
+ // that mutates the book cannot change what the others see.
2213
+ const emit = () => {
2214
+ const value = state.view();
2215
+ if (value === null)
2216
+ return;
2217
+ forEachListener("listener", (listener) => listener.onValue(state.copy(value)));
2218
+ };
2219
+ const notifyResync = (reason) => forEachListener("onResync", (listener) => listener.options?.onResync?.(reason));
2220
+ const open = () => subscribe(channel, onFrame, onSnapshot);
2221
+ const cancelScheduled = () => {
2222
+ scheduleToken += 1;
2223
+ if (scheduledTimer)
2224
+ clearTimeout(scheduledTimer);
2225
+ scheduledTimer = null;
2226
+ rebuildPending = false;
2227
+ };
2228
+ const schedule = (delay, run) => {
2229
+ cancelScheduled();
2230
+ const token = scheduleToken;
2231
+ const fire = () => {
2232
+ if (token !== scheduleToken || stopped)
2233
+ return;
2234
+ scheduledTimer = null;
2235
+ rebuildPending = false;
2236
+ run();
2237
+ };
2238
+ if (delay === 0)
2239
+ queueMicrotask(fire);
2240
+ else
2241
+ scheduledTimer = setTimeout(fire, delay);
2242
+ };
2243
+ const resubscribe = () => {
2244
+ unsubscribeWire?.();
2245
+ unsubscribeWire = open();
2246
+ };
2247
+ /** Discard the state now; resubscribe once the current routing pass is done, backing off on repeats. */
2248
+ const rebuild = (reason) => {
2249
+ state.reset();
2250
+ notifyResync(reason);
2251
+ if (rebuildPending)
2252
+ return;
2253
+ const delay = consecutiveRebuilds === 0 ? 0 : Math.min(SNAPSHOT_RETRY_BASE_MS * 2 ** (consecutiveRebuilds - 1), SNAPSHOT_RETRY_MAX_MS);
2254
+ consecutiveRebuilds += 1;
2255
+ schedule(delay, resubscribe);
2256
+ rebuildPending = true;
2257
+ if (delay > 0) {
2258
+ // Waiting out the backoff unsubscribed, so no frame or snapshot
2259
+ // arrives for the abandoned subscription meanwhile.
2260
+ unsubscribeWire?.();
2261
+ unsubscribeWire = null;
2262
+ }
2263
+ };
2264
+ /**
2265
+ * The server could not serve a snapshot. The subscription stays routed
2266
+ * and ws-api pushes the healing snapshot with no client action, so stay
2267
+ * subscribed (frames are withheld until then). The resubscribe after the
2268
+ * long fallback is a safety net only: it lands on the same book.
2269
+ */
2270
+ const awaitServerSnapshot = (reason) => {
2271
+ state.reset();
2272
+ notifyResync(reason);
2273
+ schedule(fallbackDelay, resubscribe);
2274
+ fallbackDelay = Math.min(fallbackDelay * 2, SNAPSHOT_FALLBACK_MAX_MS);
2275
+ if (unsubscribeWire === null)
2276
+ unsubscribeWire = open();
2277
+ };
2278
+ function onFrame(raw) {
2279
+ if (rebuildPending || stopped)
2280
+ return;
2281
+ let outcome;
2282
+ try {
2283
+ outcome = state.offerRaw(raw);
2284
+ }
2285
+ catch (err) {
2286
+ // An unreadable frame is a lost frame.
2287
+ console.error(`WebSocket: Error processing ${channel} frame`, err);
2288
+ rebuild("gap");
2289
+ return;
2290
+ }
2291
+ if (outcome === "applied") {
2292
+ consecutiveRebuilds = 0;
2293
+ emit();
2294
+ }
2295
+ else if (outcome === "gap")
2296
+ rebuild("gap");
2297
+ else if (outcome === "truncated")
2298
+ rebuild("truncated");
2299
+ }
2300
+ function onSnapshot(raw) {
2301
+ if (stopped)
2302
+ return;
2303
+ let outcome;
2304
+ try {
2305
+ outcome = state.applySnapshotRaw(raw);
2306
+ }
2307
+ catch (err) {
2308
+ // No further snapshot would come on this subscription unless the
2309
+ // server reloads the book: rebuild it (backing off on repeats).
2310
+ console.error(`WebSocket: Error processing ${channel} snapshot`, err);
2311
+ rebuild("snapshot_unavailable");
2312
+ return;
2313
+ }
2314
+ // A snapshot supersedes any pending fallback or rebuild, and any
2315
+ // earlier rejection.
2316
+ cancelScheduled();
2317
+ lastRejection = null;
2318
+ fallbackDelay = SNAPSHOT_FALLBACK_BASE_MS;
2319
+ if (outcome === "gap" || outcome === "truncated") {
2320
+ rebuild(outcome);
2321
+ return;
2322
+ }
2323
+ emit();
2324
+ }
2325
+ const onChannelError = (code, message) => {
2326
+ if (stopped)
2327
+ return;
2328
+ if (code === "SNAPSHOT_UNAVAILABLE") {
2329
+ awaitServerSnapshot("snapshot_unavailable");
2330
+ return;
2331
+ }
2332
+ lastRejection = { code, message };
2333
+ forEachListener("onError", (listener) => listener.options?.onError?.({ code, message }));
2334
+ };
2335
+ const onSocketLoss = () => {
2336
+ if (stopped)
2337
+ return;
2338
+ // The reconnect resubscribes and its snapshot rebuilds the book;
2339
+ // until then the old one is not live. Frames buffered from the dead
2340
+ // connection go too.
2341
+ const wasLive = state.ready;
2342
+ state.reset();
2343
+ if (wasLive)
2344
+ notifyResync("disconnected");
2345
+ };
2346
+ if (!channelErrorHandlers.has(channel))
2347
+ channelErrorHandlers.set(channel, new Set());
2348
+ channelErrorHandlers.get(channel)?.add(onChannelError);
2349
+ unsubscribeWire = open();
2350
+ const created = {
2351
+ listeners,
2352
+ current: () => {
2353
+ const value = state.view();
2354
+ return value === null ? null : state.copy(value);
2355
+ },
2356
+ rejection: () => lastRejection,
2357
+ onSocketLoss,
2358
+ stop: () => {
2359
+ if (stopped)
2360
+ return;
2361
+ stopped = true;
2362
+ cancelScheduled();
2363
+ unsubscribeWire?.();
2364
+ unsubscribeWire = null;
2365
+ channelErrorHandlers.get(channel)?.delete(onChannelError);
2366
+ if (channelErrorHandlers.get(channel)?.size === 0)
2367
+ channelErrorHandlers.delete(channel);
2368
+ if (localStateChannels.get(channel) === created)
2369
+ localStateChannels.delete(channel);
2370
+ },
2371
+ };
2372
+ localStateChannels.set(channel, created);
2373
+ entry = created;
2374
+ }
2375
+ const listener = { onValue: onValue, options: localOptions };
2376
+ const held = entry;
2377
+ held.listeners.add(listener);
2378
+ const current = held.current();
2379
+ const rejection = held.rejection();
2380
+ try {
2381
+ if (current !== null)
2382
+ listener.onValue(current);
2383
+ else if (rejection !== null)
2384
+ listener.options?.onError?.(rejection);
2385
+ }
2386
+ catch (err) {
2387
+ console.error(`WebSocket: Error in ${channel} listener`, err);
2388
+ }
2389
+ let unsubscribed = false;
2390
+ return () => {
2391
+ if (unsubscribed)
2392
+ return;
2393
+ unsubscribed = true;
2394
+ held.listeners.delete(listener);
2395
+ if (held.listeners.size === 0)
2396
+ held.stop();
2397
+ };
2398
+ };
2399
+ const subscribeDepth = (tradingPairId, tradingMode, onBook, localOptions) => {
2400
+ const channel = marketDataChannel("depth", tradingPairId, tradingMode);
2401
+ return openLocalState(channel, () => {
2402
+ const book = new LocalDepthBook();
2403
+ return {
2404
+ offerRaw: (raw) => book.offer(parseDepthDelta(raw)),
2405
+ applySnapshotRaw: (raw) => book.applySnapshot(parseDepthSnapshot(raw)),
2406
+ reset: () => book.reset(),
2407
+ view: () => book.view(),
2408
+ copy: (value) => ({ ...value, bids: value.bids.map((level) => ({ ...level })), asks: value.asks.map((level) => ({ ...level })) }),
2409
+ get ready() {
2410
+ return book.ready;
2411
+ },
2412
+ };
2413
+ }, onBook, localOptions);
2414
+ };
2415
+ const subscribeBbo = (tradingPairId, tradingMode, onBbo, localOptions) => {
2416
+ const channel = marketDataChannel("bbo", tradingPairId, tradingMode);
2417
+ return openLocalState(channel, () => {
2418
+ const bbo = new LocalBbo();
2419
+ return {
2420
+ offerRaw: (raw) => bbo.offer(parseBboEvent(raw)),
2421
+ applySnapshotRaw: (raw) => bbo.applySnapshot(parseBboSnapshot(raw)),
2422
+ reset: () => bbo.reset(),
2423
+ view: () => bbo.view(),
2424
+ copy: (value) => ({ ...value }),
2425
+ get ready() {
2426
+ return bbo.ready;
2427
+ },
2428
+ };
2429
+ }, onBbo, localOptions);
2430
+ };
1801
2431
  const subscribeOHLCV = (tradingPairId, tradingMode, interval, handler, priceType, onSnapshot) => {
1802
2432
  // Four-token channel selects the trade-derived candles (back-compat); the
1803
2433
  // optional fifth token selects the price series (`mark` = close-only mark
@@ -1848,8 +2478,43 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1848
2478
  }
1849
2479
  }, onSnapshot && snapshotFrameHandler("ohlcv", (raw) => parseOHLCVSnapshotCandle(raw, interval), onSnapshot));
1850
2480
  };
1851
- const subscribeTrades = (tradingPairId, handler, onSnapshot) => {
2481
+ const subscribeTrades = (tradingPairId, handler, onSnapshot, tradesOptions) => {
1852
2482
  const channel = `trades:${tradingPairId}`;
2483
+ // The chain check runs once a snapshot carries a cursor key (the
2484
+ // market-data source); under the engine source every trade is delivered
2485
+ // as before. The server forwards only trades linking to this
2486
+ // connection's cursor and reseeds by itself on a break it sees, so the
2487
+ // check here is a safety net: it drops a trade at or below the cursor or
2488
+ // the last delivered one, and reports a trade that does not link (a
2489
+ // frame this client could not read, say) to onGap, then continues from
2490
+ // it. Withholding would freeze the feed, since the server believes the
2491
+ // connection is in sync and sends no fresh snapshot for it.
2492
+ let checking = false;
2493
+ let last = null;
2494
+ const deliverSnapshot = onSnapshot && snapshotFrameHandler("trades", parseTradeEventData, onSnapshot);
2495
+ const onSnapshotFrame = (rawData, frame) => {
2496
+ let meta;
2497
+ if (frame && "cursor" in frame) {
2498
+ try {
2499
+ const cursor = parseTradeSnapshotCursor(frame.cursor);
2500
+ // The internal bound is a copy: a consumer mutating meta.cursor
2501
+ // must not move it.
2502
+ last = cursor === null ? null : { ...cursor };
2503
+ checking = true;
2504
+ meta = { cursor };
2505
+ }
2506
+ catch (err) {
2507
+ // An unreadable cursor leaves nothing to check against: deliver as
2508
+ // the engine source does until the next snapshot (the rows still go).
2509
+ console.error("WebSocket: Error processing trades snapshot cursor", err);
2510
+ checking = false;
2511
+ last = null;
2512
+ }
2513
+ }
2514
+ deliverSnapshot?.(rawData, frame, meta);
2515
+ };
2516
+ if (!deliverSnapshot)
2517
+ internalSnapshotHandlers.add(onSnapshotFrame);
1853
2518
  return subscribe(channel, (rawData) => {
1854
2519
  try {
1855
2520
  const data = rawData;
@@ -1865,13 +2530,37 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
1865
2530
  makerSide: tradeData.maker_side.toUpperCase(),
1866
2531
  executedAt: tradeData.executed_at,
1867
2532
  },
2533
+ ...readTradeChain(data),
1868
2534
  };
2535
+ if (checking && event.seq !== undefined && event.matchIndex !== undefined) {
2536
+ const key = { seq: event.seq, matchIndex: event.matchIndex };
2537
+ if (last !== null) {
2538
+ // At or below the cursor or the last delivered trade: already reflected.
2539
+ if (tradeKeyAtOrBelow(key, last))
2540
+ return;
2541
+ if (event.prevSeq !== last.seq || event.prevMatchIndex !== last.matchIndex) {
2542
+ // A throwing onGap must not stop the chain advancing or the
2543
+ // trade being delivered.
2544
+ try {
2545
+ tradesOptions?.onGap?.({
2546
+ tradingPairId: event.tradingPairId,
2547
+ expected: { seq: last.seq, matchIndex: last.matchIndex },
2548
+ received: { prevSeq: event.prevSeq ?? null, prevMatchIndex: event.prevMatchIndex ?? null },
2549
+ });
2550
+ }
2551
+ catch (err) {
2552
+ console.error("WebSocket: Error in trades onGap", err);
2553
+ }
2554
+ }
2555
+ }
2556
+ last = key;
2557
+ }
1869
2558
  handler(event);
1870
2559
  }
1871
2560
  catch (err) {
1872
2561
  console.error("WebSocket: Error processing trade event", err);
1873
2562
  }
1874
- }, onSnapshot && snapshotFrameHandler("trades", parseTradeEventData, onSnapshot));
2563
+ }, onSnapshotFrame);
1875
2564
  };
1876
2565
  const subscribeMovements = (handler) => {
1877
2566
  // User is identified from the authenticated session, so channel is just "movements"
@@ -2310,6 +2999,38 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
2310
2999
  }
2311
3000
  });
2312
3001
  };
3002
+ /**
3003
+ * Subscribe to the authenticated account's RFQ pushes. The account comes from
3004
+ * the signed session, so the channel is just `rfq`; a subscribe the server
3005
+ * refuses for want of authentication is retried once the connection
3006
+ * authenticates, as on every private channel.
3007
+ */
3008
+ const subscribeRfq = (handler) => {
3009
+ if (!handler) {
3010
+ throw new Error("rfq subscription requires a handler");
3011
+ }
3012
+ return subscribe("rfq", (rawData) => {
3013
+ try {
3014
+ handler(parseRfqEvent(rawData));
3015
+ }
3016
+ catch (err) {
3017
+ console.error("WebSocket: Error processing RFQ event", err);
3018
+ }
3019
+ });
3020
+ };
3021
+ const subscribeRfqTrades = (tradingPairId, handler) => {
3022
+ if (!handler) {
3023
+ throw new Error("rfqTrades subscription requires a handler");
3024
+ }
3025
+ return subscribe(`rfq_trades:${tradingPairId}`, (rawData) => {
3026
+ try {
3027
+ handler(parseRfqTradeEvent(rawData));
3028
+ }
3029
+ catch (err) {
3030
+ console.error("WebSocket: Error processing RFQ trade event", err);
3031
+ }
3032
+ });
3033
+ };
2313
3034
  return {
2314
3035
  connect,
2315
3036
  disconnect,
@@ -2335,6 +3056,8 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
2335
3056
  },
2336
3057
  orders: subscribeOrders,
2337
3058
  orderbook: subscribeOrderbook,
3059
+ depth: subscribeDepth,
3060
+ bbo: subscribeBbo,
2338
3061
  ohlcv: subscribeOHLCV,
2339
3062
  trades: subscribeTrades,
2340
3063
  movements: subscribeMovements,
@@ -2350,5 +3073,7 @@ export function createMonacoWebSocket(baseUrl, options = {}, serverClock = new S
2350
3073
  marketStats: subscribeMarketStats,
2351
3074
  marketStatsAll: subscribeMarketStatsAll,
2352
3075
  marketLiquidations: subscribeMarketLiquidations,
3076
+ rfq: subscribeRfq,
3077
+ rfqTrades: subscribeRfqTrades,
2353
3078
  };
2354
3079
  }