@0xmonaco/react 1.0.50 → 1.0.53

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.
@@ -8,17 +8,19 @@ import type { UseInstrumentsReturn } from "./types";
8
8
  * configuration rather than a delta, so state is kept as a terminal-state
9
9
  * entry per trading pair: each event replaces that market's entry outright.
10
10
  *
11
- * A delisted market keeps its entry with `isActive: false` rather than
12
- * disappearing, so a consumer can distinguish "no longer tradable" from "never
13
- * seen"; read `instruments.filter((instrument) => instrument.isActive)` for the
14
- * tradable set.
11
+ * A market DELISTED while the socket was up keeps its entry with
12
+ * `isActive: false` rather than disappearing, so a consumer can distinguish
13
+ * "no longer tradable" from "never seen"; read
14
+ * `instruments.filter((instrument) => instrument.isActive)` for the tradable
15
+ * set.
15
16
  *
16
- * Caveat on reconnect: the server's post-reconnect snapshot covers the markets
17
- * active at that moment, and the core helper delivers it as ordinary per-market
18
- * events. A market delisted while the socket was down is simply absent from
19
- * that snapshot, so its entry here keeps its pre-outage state until the market
20
- * next emits. Closing that needs a snapshot-boundary signal the channel's
21
- * handler contract does not currently carry.
17
+ * Reconnect is the exception, and the snapshot boundary is what makes it
18
+ * expressible. Each snapshot is the authoritative active-market set at the
19
+ * moment it was taken, so it REPLACES the map wholesale rather than merging
20
+ * into it: a market delisted while the socket was down is absent from the
21
+ * post-reconnect snapshot and leaves this state with it. Merging would have
22
+ * stranded that market here forever carrying its pre-outage config, because a
23
+ * delisted market emits nothing further to correct it.
22
24
  *
23
25
  * @param tradingPairId - Optional trading pair UUID to filter on; omit to
24
26
  * receive every market. An empty string is rejected by the core helper and is
@@ -18,17 +18,19 @@ const NO_INSTRUMENTS = {};
18
18
  * configuration rather than a delta, so state is kept as a terminal-state
19
19
  * entry per trading pair: each event replaces that market's entry outright.
20
20
  *
21
- * A delisted market keeps its entry with `isActive: false` rather than
22
- * disappearing, so a consumer can distinguish "no longer tradable" from "never
23
- * seen"; read `instruments.filter((instrument) => instrument.isActive)` for the
24
- * tradable set.
21
+ * A market DELISTED while the socket was up keeps its entry with
22
+ * `isActive: false` rather than disappearing, so a consumer can distinguish
23
+ * "no longer tradable" from "never seen"; read
24
+ * `instruments.filter((instrument) => instrument.isActive)` for the tradable
25
+ * set.
25
26
  *
26
- * Caveat on reconnect: the server's post-reconnect snapshot covers the markets
27
- * active at that moment, and the core helper delivers it as ordinary per-market
28
- * events. A market delisted while the socket was down is simply absent from
29
- * that snapshot, so its entry here keeps its pre-outage state until the market
30
- * next emits. Closing that needs a snapshot-boundary signal the channel's
31
- * handler contract does not currently carry.
27
+ * Reconnect is the exception, and the snapshot boundary is what makes it
28
+ * expressible. Each snapshot is the authoritative active-market set at the
29
+ * moment it was taken, so it REPLACES the map wholesale rather than merging
30
+ * into it: a market delisted while the socket was down is absent from the
31
+ * post-reconnect snapshot and leaves this state with it. Merging would have
32
+ * stranded that market here forever carrying its pre-outage config, because a
33
+ * delisted market emits nothing further to correct it.
32
34
  *
33
35
  * @param tradingPairId - Optional trading pair UUID to filter on; omit to
34
36
  * receive every market. An empty string is rejected by the core helper and is
@@ -61,7 +63,10 @@ export function useInstruments(tradingPairId) {
61
63
  try {
62
64
  unsubscribe = ws.instruments((event) => {
63
65
  setSubscription((prev) => (belongsToThisSubscription(prev) ? { ...prev, byPair: { ...prev.byPair, [event.tradingPairId]: event.data } } : prev));
64
- }, tradingPairId);
66
+ }, tradingPairId, (items) => {
67
+ const byPair = Object.fromEntries(items.map((item) => [item.tradingPairId, item]));
68
+ setSubscription((prev) => (belongsToThisSubscription(prev) ? { ...prev, byPair } : prev));
69
+ });
65
70
  setSubscription((prev) => (belongsToThisSubscription(prev) ? { ...prev, subscribed: true } : prev));
66
71
  }
67
72
  catch (err) {
@@ -1,9 +1,86 @@
1
- import type { AccountBalance, UserBalanceEvent } from "@0xmonaco/types";
1
+ import type { AccountBalance, UserBalanceEvent, UserBalanceEventData } from "@0xmonaco/types";
2
2
  import type { UseUserBalancesReturn } from "./types";
3
3
  /**
4
4
  * Update an AccountBalance with data from a WebSocket balance event
5
5
  */
6
6
  export declare function updateBalanceFromEvent(balance: AccountBalance, event: UserBalanceEvent): AccountBalance;
7
+ /**
8
+ * Apply a subscribe-time balances snapshot to the current list.
9
+ *
10
+ * The snapshot is the user's COMPLETE balance set, which is what makes this a
11
+ * different operation from applying a live event rather than a loop over one:
12
+ *
13
+ * - A held asset the snapshot omits has no balance left, so it is zeroed rather
14
+ * than left showing a figure that is no longer true. The row itself stays —
15
+ * a portfolio needs to render "0 WSEI", not to lose the line.
16
+ * - The snapshot's totals are taken VERBATIM. A snapshot row is built from the
17
+ * same accounts-service read that produced the REST row, so its `total`
18
+ * already is the REST `totalBalance` with margin collateral included —
19
+ * unlike a live event, whose `total` can describe the spot balance alone and
20
+ * therefore needs {@link updateBalanceFromEvent}'s margin reconciliation.
21
+ * Running a snapshot row through that would add the derived margin component
22
+ * a second time.
23
+ * - A row for an asset the list does not hold cannot be completed here: the
24
+ * wire payload carries no `assetId`, `decimals` or wrapped-native flag. That
25
+ * case is reported by {@link snapshotHasUnknownAsset} rather than from here,
26
+ * so the caller can decide to re-read WITHOUT doing it inside a state
27
+ * updater — React invokes an updater during render, and may invoke it more
28
+ * than once, so a flag assigned inside one is not readable afterwards.
29
+ *
30
+ * Pure, so the caller can use it inside a state updater.
31
+ */
32
+ export declare function applyBalanceSnapshot(current: AccountBalance[], items: UserBalanceEventData[]): {
33
+ balances: AccountBalance[];
34
+ hasUnknownAsset: boolean;
35
+ };
36
+ /**
37
+ * Whether the snapshot names an asset the current list does not hold.
38
+ *
39
+ * Split out of {@link applyBalanceSnapshot} so a caller can ask the question
40
+ * before entering a state updater: the answer decides whether to fire a REST
41
+ * re-read, and a side effect must not depend on an updater having run.
42
+ */
43
+ export declare function snapshotHasUnknownAsset(current: AccountBalance[], items: UserBalanceEventData[]): boolean;
44
+ /**
45
+ * Merge a background REST read into the current list without disturbing it.
46
+ *
47
+ * The repair this serves has one job: learn about an asset the list does not
48
+ * hold yet, because the WebSocket payload carries no `assetId`, `decimals` or
49
+ * wrapped-native flag and so cannot introduce one on its own.
50
+ *
51
+ * It must not do more than that. The subscription stays live while the request
52
+ * is in flight, so an event or snapshot can land in between — and replacing the
53
+ * list with the response would roll those newer values back, including the
54
+ * authoritative snapshot figures that triggered the read in the first place.
55
+ * Rows already held therefore keep their live values untouched, and only
56
+ * genuinely new assets are appended.
57
+ *
58
+ * The initial load and the public `refresh()` still replace: the first runs
59
+ * before the subscription opens, so nothing newer can exist to lose, and the
60
+ * second is a deliberate "give me the server's answer" action.
61
+ */
62
+ export declare function reconcileBalances(current: AccountBalance[], fetched: AccountBalance[]): AccountBalance[];
63
+ /**
64
+ * Overlay buffered wire state onto rows a REST read has just supplied metadata
65
+ * for.
66
+ *
67
+ * A frame naming a token the list does not hold yet is dropped by the event
68
+ * handler — there is no row to apply it to — and the repair read that follows
69
+ * may have queried the server BEFORE that frame. Appending its result would
70
+ * then leave the token at a value the stream has already moved past, with no
71
+ * later frame to correct it because every subsequent frame finds a row and
72
+ * takes the update path. Buffering the newest wire state for an unheld token
73
+ * and laying it over the row once metadata arrives is what closes that.
74
+ *
75
+ * Only entries newer than the read are applied. One buffered BEFORE the read
76
+ * started is already reflected in its result, and re-applying it would roll the
77
+ * row back to that older value.
78
+ */
79
+ export declare function applyBufferedBalances(rows: AccountBalance[], buffered: Map<string, {
80
+ data: UserBalanceEventData;
81
+ seq: number;
82
+ source: "event" | "snapshot";
83
+ }>, readSeq: number): AccountBalance[];
7
84
  /**
8
85
  * Hook for subscribing to real-time user balance updates via WebSocket (authenticated)
9
86
  *