@0xmonaco/react 1.0.47 → 1.0.50

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.
@@ -1,5 +1,6 @@
1
1
  export * from "./useAuth";
2
2
  export * from "./useFees";
3
+ export * from "./useInstruments";
3
4
  export * from "./useMarket";
4
5
  export * from "./useMonaco";
5
6
  export * from "./useOHLCV";
@@ -1,5 +1,6 @@
1
1
  export * from "./useAuth";
2
2
  export * from "./useFees";
3
+ export * from "./useInstruments";
3
4
  export * from "./useMarket";
4
5
  export * from "./useMonaco";
5
6
  export * from "./useOHLCV";
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export { useInstruments } from "./useInstruments";
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export { useInstruments } from "./useInstruments";
@@ -0,0 +1,34 @@
1
+ import type { InstrumentEventData } from "@0xmonaco/types";
2
+ /**
3
+ * Return type for the useInstruments hook
4
+ */
5
+ export interface UseInstrumentsReturn {
6
+ /**
7
+ * Current configuration of every market seen on the channel, ordered by
8
+ * symbol and then trading pair id. Delisted markets stay in the list carrying
9
+ * `isActive: false` — filter on `isActive` for the tradable set.
10
+ */
11
+ instruments: InstrumentEventData[];
12
+ /** The same entries keyed by trading pair UUID, for direct lookup */
13
+ instrumentsByPair: Record<string, InstrumentEventData>;
14
+ /**
15
+ * Whether a subscription to the instrument channel is currently registered.
16
+ * This is not a connection indicator: the client accepts subscriptions while
17
+ * the socket is down and sends them on the next open, so `true` means
18
+ * "registered", not "receiving".
19
+ */
20
+ subscribed: boolean;
21
+ /**
22
+ * A rejected subscription request. The core helper validates its arguments
23
+ * synchronously — passing an empty `tradingPairId` is the reachable case — so
24
+ * treat this as a bad-usage signal.
25
+ *
26
+ * It is deliberately NOT a transport signal: the client registers handlers
27
+ * regardless of socket state, and logs malformed-frame failures internally,
28
+ * so connection loss and unparseable frames never appear here. Drive
29
+ * connection UI from the client's own status/resync callbacks instead.
30
+ */
31
+ error: Error | null;
32
+ /** Clear the current error state */
33
+ clearError: () => void;
34
+ }
File without changes
@@ -0,0 +1,27 @@
1
+ import type { UseInstrumentsReturn } from "./types";
2
+ /**
3
+ * Hook for subscribing to real-time instrument configuration via WebSocket (public)
4
+ *
5
+ * On subscribe the channel delivers a snapshot of the current active markets
6
+ * (one `listing` frame each), then live listing, delisting, halt/unhalt and
7
+ * config-change frames. Every frame carries a market's FULL current
8
+ * configuration rather than a delta, so state is kept as a terminal-state
9
+ * entry per trading pair: each event replaces that market's entry outright.
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.
15
+ *
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.
22
+ *
23
+ * @param tradingPairId - Optional trading pair UUID to filter on; omit to
24
+ * receive every market. An empty string is rejected by the core helper and is
25
+ * surfaced through `error`.
26
+ */
27
+ export declare function useInstruments(tradingPairId?: string): UseInstrumentsReturn;
@@ -0,0 +1,94 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useMonacoSDK } from "../useMonaco";
3
+ const IDLE = {
4
+ source: undefined,
5
+ tradingPairId: undefined,
6
+ byPair: {},
7
+ subscribed: false,
8
+ error: null,
9
+ };
10
+ /** Shared empty map, so an out-of-scope render keeps a stable identity. */
11
+ const NO_INSTRUMENTS = {};
12
+ /**
13
+ * Hook for subscribing to real-time instrument configuration via WebSocket (public)
14
+ *
15
+ * On subscribe the channel delivers a snapshot of the current active markets
16
+ * (one `listing` frame each), then live listing, delisting, halt/unhalt and
17
+ * config-change frames. Every frame carries a market's FULL current
18
+ * configuration rather than a delta, so state is kept as a terminal-state
19
+ * entry per trading pair: each event replaces that market's entry outright.
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.
25
+ *
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.
32
+ *
33
+ * @param tradingPairId - Optional trading pair UUID to filter on; omit to
34
+ * receive every market. An empty string is rejected by the core helper and is
35
+ * surfaced through `error`.
36
+ */
37
+ export function useInstruments(tradingPairId) {
38
+ const { sdk } = useMonacoSDK();
39
+ const ws = sdk?.ws;
40
+ const [subscription, setSubscription] = useState(IDLE);
41
+ // Entries only count while they belong to the subscription the caller is
42
+ // asking for right now; the effect below realigns them a tick later.
43
+ const inScope = subscription.source === ws && subscription.tradingPairId === tradingPairId;
44
+ const clearError = useCallback(() => {
45
+ setSubscription((prev) => (prev.error === null ? prev : { ...prev, error: null }));
46
+ }, []);
47
+ useEffect(() => {
48
+ if (!ws) {
49
+ setSubscription({ ...IDLE, tradingPairId });
50
+ return;
51
+ }
52
+ // Only a frame that belongs to *this* subscription may touch state — a
53
+ // late arrival from a torn-down one must not resurrect old markets.
54
+ const belongsToThisSubscription = (prev) => prev.source === ws && prev.tradingPairId === tradingPairId;
55
+ // Claim the scope before subscribing so the first frame, whenever it lands,
56
+ // already has somewhere in-scope to go.
57
+ setSubscription({ source: ws, tradingPairId, byPair: {}, subscribed: false, error: null });
58
+ // `instruments` throws synchronously on a missing handler or an empty pair
59
+ // id — surface that through `error` instead of letting it escape the effect.
60
+ let unsubscribe;
61
+ try {
62
+ unsubscribe = ws.instruments((event) => {
63
+ setSubscription((prev) => (belongsToThisSubscription(prev) ? { ...prev, byPair: { ...prev.byPair, [event.tradingPairId]: event.data } } : prev));
64
+ }, tradingPairId);
65
+ setSubscription((prev) => (belongsToThisSubscription(prev) ? { ...prev, subscribed: true } : prev));
66
+ }
67
+ catch (err) {
68
+ const error = err instanceof Error ? err : new Error(String(err));
69
+ setSubscription((prev) => (belongsToThisSubscription(prev) ? { ...prev, subscribed: false, error } : prev));
70
+ }
71
+ return () => {
72
+ unsubscribe?.();
73
+ setSubscription((prev) => (belongsToThisSubscription(prev) ? { ...prev, subscribed: false } : prev));
74
+ };
75
+ }, [ws, tradingPairId]);
76
+ const instrumentsByPair = inScope ? subscription.byPair : NO_INSTRUMENTS;
77
+ // Sorted with plain codepoint comparison rather than `localeCompare`, so the
78
+ // order does not shift with the runtime locale, and tie-broken on the pair id
79
+ // because a spot and a perp market can carry the same symbol.
80
+ const instruments = useMemo(() => Object.values(instrumentsByPair).sort((left, right) => {
81
+ if (left.symbol !== right.symbol)
82
+ return left.symbol < right.symbol ? -1 : 1;
83
+ if (left.tradingPairId === right.tradingPairId)
84
+ return 0;
85
+ return left.tradingPairId < right.tradingPairId ? -1 : 1;
86
+ }), [instrumentsByPair]);
87
+ return {
88
+ instruments,
89
+ instrumentsByPair,
90
+ subscribed: inScope && subscription.subscribed,
91
+ error: inScope ? subscription.error : null,
92
+ clearError,
93
+ };
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmonaco/react",
3
- "version": "1.0.47",
3
+ "version": "1.0.50",
4
4
  "type": "module",
5
5
  "homepage": "https://docs.0xmonaco.com/sdk/typescript",
6
6
  "main": "./dist/index.js",
@@ -16,8 +16,8 @@
16
16
  "lint": "biome lint ."
17
17
  },
18
18
  "dependencies": {
19
- "@0xmonaco/core": "1.0.47",
20
- "@0xmonaco/types": "1.0.47"
19
+ "@0xmonaco/core": "1.0.50",
20
+ "@0xmonaco/types": "1.0.50"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/react": "^19.1.12",