@ledgerhq/live-countervalues-react 0.2.45-nightly.0 → 0.2.45-nightly.2

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/src/index.tsx CHANGED
@@ -1,11 +1,8 @@
1
1
  import { getAccountCurrency } from "@ledgerhq/coin-framework/account/helpers";
2
- import api from "@ledgerhq/live-countervalues/api/index";
3
2
  import {
4
3
  calculate,
5
- exportCountervalues,
6
4
  importCountervalues,
7
5
  inferTrackingPairForAccounts,
8
- initialState,
9
6
  loadCountervalues,
10
7
  trackingPairForTopCoins,
11
8
  } from "@ledgerhq/live-countervalues/logic";
@@ -16,7 +13,6 @@ import type {
16
13
  TrackingPair,
17
14
  } from "@ledgerhq/live-countervalues/types";
18
15
  import { useDebounce } from "@ledgerhq/live-hooks/useDebounce";
19
- import { log } from "@ledgerhq/logs";
20
16
  import type { Currency, Unit } from "@ledgerhq/types-cryptoassets";
21
17
  import type { Account, AccountLike } from "@ledgerhq/types-live";
22
18
  import { BigNumber } from "bignumber.js";
@@ -27,17 +23,33 @@ import React, {
27
23
  useContext,
28
24
  useEffect,
29
25
  useMemo,
30
- useReducer,
31
- useState,
32
26
  } from "react";
27
+ import { useMarketcapIds } from "./CountervaluesMarketcapProvider";
33
28
 
34
- /** Bridge enabling platform-specific persistence of market-cap ids. */
35
- export interface CountervaluesMarketcapBridge {
36
- useIds(): string[];
37
- useLastUpdated(): number | undefined;
38
- setLoading(loading: boolean): void;
39
- setIds(ids: string[]): void;
40
- setError(message: string): void;
29
+ export { CountervaluesMarketcapProvider, useMarketcapIds } from "./CountervaluesMarketcapProvider";
30
+
31
+ export interface PollingState {
32
+ isPolling: boolean;
33
+ triggerRef: number;
34
+ }
35
+
36
+ /**
37
+ * Bridge enabling platform-specific persistence of countervalues state.
38
+ * @note: make sure that the object is memoized to avoid re-renders.
39
+ */
40
+ export interface CountervaluesBridge {
41
+ setPollingIsPolling(polling: boolean): void;
42
+ setPollingTriggerLoad(triggerLoad: boolean): void;
43
+ setState(state: CounterValuesState): void;
44
+ setStateError(error: Error): void;
45
+ setStatePending(pending: boolean): void;
46
+ usePollingIsPolling(): boolean;
47
+ usePollingTriggerLoad(): boolean;
48
+ useStateError(): Error | null;
49
+ useStatePending(): boolean;
50
+ useState(): CounterValuesState;
51
+ useUserSettings(): CountervaluesSettings;
52
+ wipe(): void;
41
53
  }
42
54
 
43
55
  // Polling is the control object you get from the high level <PollingConsumer>{ polling => ...
@@ -59,36 +71,32 @@ export type Polling = {
59
71
  };
60
72
 
61
73
  export type Props = {
74
+ /** Bridge enabling platform-specific persistence of countervalues state. */
75
+ bridge: CountervaluesBridge;
62
76
  children: React.ReactNode;
63
- userSettings: CountervaluesSettings;
64
- // the time to wait before the first poll when app starts (allow things to render to not do all at boot time)
77
+ /** the time to wait before the first poll when app starts (allow things to render to not do all at boot time) */
65
78
  pollInitDelay?: number;
66
- // the minimum time to wait before two automatic polls (then one that happen whatever network/appstate events)
79
+ /** the minimum time to wait before two automatic polls (then one that happen whatever network/appstate events) */
67
80
  autopollInterval?: number;
68
- // debounce time before actually fetching
81
+ /** debounce time before actually fetching */
69
82
  debounceDelay?: number;
70
83
  savedState?: CounterValuesStateRaw;
71
84
  };
72
85
 
73
- const CountervaluesPollingContext = createContext<Polling>({
74
- wipe: () => {},
75
- poll: () => {},
76
- start: () => {},
77
- stop: () => {},
78
- pending: false,
79
- error: null,
80
- });
81
-
82
- const CountervaluesUserSettingsContext = createContext<CountervaluesSettings>({
83
- // dummy values that are overriden by the context provider
84
- trackingPairs: [],
85
- autofillGaps: true,
86
- refreshRate: 0,
87
- marketCapBatchingAfterRank: 0,
88
- });
86
+ /**
87
+ * Base Countervalues Context to use without polling logic.
88
+ */
89
+ export const CountervaluesContext = createContext<CountervaluesBridge | null>(null);
89
90
 
90
- const CountervaluesContext = createContext<CounterValuesState>(initialState);
91
- const CountervaluesMarketcapIdsContext = createContext<string[]>([]);
91
+ function useCountervaluesBridgeContext() {
92
+ const bridge = useContext(CountervaluesContext);
93
+ if (!bridge) {
94
+ throw new Error(
95
+ "'useCountervaluesBridgeContext' must be used within a 'CountervaluesProvider'",
96
+ );
97
+ }
98
+ return bridge;
99
+ }
92
100
 
93
101
  function trackingPairsHash(a: TrackingPair[]) {
94
102
  return a
@@ -97,71 +105,20 @@ function trackingPairsHash(a: TrackingPair[]) {
97
105
  .join("|");
98
106
  }
99
107
 
100
- const marketcapRefresh = 30 * 60000;
101
- const marketcapRefreshOnError = 60000;
102
-
103
- /** Provides market-cap ids via the supplied bridge. */
104
- export function CountervaluesMarketcapProvider({
105
- children,
106
- bridge,
107
- }: {
108
- children: React.ReactNode;
109
- bridge: CountervaluesMarketcapBridge;
110
- }): ReactElement {
111
- const ids = bridge.useIds();
112
- const lastUpdated = bridge.useLastUpdated();
113
- const [, forceUpdate] = useReducer(x => x + 1, 0);
114
-
115
- useEffect(() => {
116
- let timeout: ReturnType<typeof setTimeout> | null = null;
117
- const now = Date.now();
118
-
119
- if (!lastUpdated || now - lastUpdated > marketcapRefresh) {
120
- bridge.setLoading(true);
121
- api.fetchIdsSortedByMarketcap().then(
122
- fetchedIds => {
123
- bridge.setIds(fetchedIds);
124
- timeout = setTimeout(() => forceUpdate(), marketcapRefresh);
125
- },
126
- error => {
127
- log("countervalues", "error fetching marketcap ids " + error);
128
- bridge.setError(error.message);
129
- timeout = setTimeout(() => forceUpdate(), marketcapRefreshOnError);
130
- },
131
- );
132
- } else {
133
- timeout = setTimeout(() => forceUpdate(), marketcapRefresh - (now - lastUpdated));
134
- }
135
-
136
- return () => {
137
- if (timeout) clearTimeout(timeout);
138
- };
139
- }, [lastUpdated, bridge]);
140
-
141
- return (
142
- <CountervaluesMarketcapIdsContext.Provider value={ids}>
143
- {children}
144
- </CountervaluesMarketcapIdsContext.Provider>
145
- );
146
- }
147
-
148
108
  /**
149
- * Root countervalues provider (polling + calculation).
109
+ * Call side effects outside of the primary render tree, avoiding costly child re-renders
150
110
  */
151
- export function CountervaluesProvider({
152
- children,
153
- userSettings,
154
- pollInitDelay = 3 * 1000,
155
- debounceDelay = 1000,
111
+ function Effect({
112
+ bridge,
156
113
  savedState,
157
- }: Props): ReactElement {
158
- const autopollInterval = userSettings.refreshRate;
114
+ debounceDelay = 1000,
115
+ pollInitDelay = 3 * 1000,
116
+ }: Pick<Props, "autopollInterval" | "bridge" | "debounceDelay" | "pollInitDelay" | "savedState">) {
117
+ const userSettings = bridge.useUserSettings();
118
+ const { refreshRate, marketCapBatchingAfterRank } = userSettings;
159
119
  const debouncedUserSettings = useDebounce(userSettings, debounceDelay);
160
- const [{ state, pending, error }, dispatch] = useReducer(fetchReducer, initialFetchState);
161
120
 
162
- // TODO this is always using the initial value, doesn't react to changes.
163
- const marketcapIds = useContext(CountervaluesMarketcapIdsContext);
164
- const { marketCapBatchingAfterRank } = userSettings;
121
+ const marketcapIds = useMarketcapIds();
165
122
 
166
123
  const batchStrategySolver = useMemo(
167
124
  () => ({
@@ -175,125 +132,106 @@ export function CountervaluesProvider({
175
132
  );
176
133
 
177
134
  // flag used to trigger a loadCountervalues
178
- const [triggerLoad, setTriggerLoad] = useState(false);
179
- // trigger poll only when userSettings changes. in a debounced way.
135
+ const triggerLoad = bridge.usePollingTriggerLoad();
136
+
137
+ // trigger poll only when userSettings changes in a debounced way
180
138
  useEffect(() => {
181
- setTriggerLoad(true);
182
- }, [debouncedUserSettings]);
139
+ bridge.setPollingTriggerLoad(true);
140
+ }, [bridge, debouncedUserSettings]);
183
141
 
184
142
  // loadCountervalues logic
143
+ const currentState = bridge.useState();
144
+ const pending = bridge.useStatePending();
145
+
146
+ // loadCountervalues logic using bridge
185
147
  useEffect(() => {
186
148
  if (pending || !triggerLoad) return;
187
- setTriggerLoad(false);
188
- dispatch({ type: "pending" });
149
+ bridge.setPollingTriggerLoad(false);
150
+
151
+ bridge.setStatePending(true);
189
152
  loadCountervalues(
190
- state,
153
+ currentState,
191
154
  userSettings,
192
155
  batchStrategySolver,
193
156
  userSettings.granularitiesRates,
194
157
  ).then(
195
- newState => dispatch({ type: "success", payload: newState }),
196
- e => dispatch({ type: "error", payload: e }),
158
+ s => {
159
+ bridge.setState(s);
160
+ bridge.setStatePending(false);
161
+ },
162
+ e => {
163
+ bridge.setStateError(e);
164
+ bridge.setStatePending(false);
165
+ },
197
166
  );
198
- }, [pending, state, userSettings, triggerLoad, batchStrategySolver]);
167
+ }, [pending, currentState, userSettings, triggerLoad, batchStrategySolver, bridge]);
199
168
 
200
169
  // save the state when it changes
201
170
  useEffect(() => {
202
171
  if (!savedState?.status || !Object.keys(savedState.status).length) return;
203
- dispatch({
204
- type: "setCounterValueState",
205
- payload: importCountervalues(savedState, userSettings),
206
- });
207
- }, [savedState, userSettings]);
172
+ bridge.setState(importCountervalues(savedState, userSettings));
173
+ }, [bridge, savedState, userSettings]);
208
174
 
209
- // manage the auto polling loop and the interface for user land to trigger a reload
210
- const [isPolling, setIsPolling] = useState(true);
175
+ // manage the auto polling loop
176
+ const isPolling = bridge.usePollingIsPolling();
211
177
  useEffect(() => {
212
178
  if (!isPolling) return;
213
179
  let pollingTimeout: ReturnType<typeof setTimeout>;
214
180
  function pollingLoop() {
215
- setTriggerLoad(true);
216
- pollingTimeout = setTimeout(pollingLoop, autopollInterval);
181
+ bridge.setPollingTriggerLoad(true);
182
+ pollingTimeout = setTimeout(pollingLoop, refreshRate);
217
183
  }
218
184
  pollingTimeout = setTimeout(pollingLoop, pollInitDelay);
219
185
  return () => clearTimeout(pollingTimeout);
220
- }, [autopollInterval, pollInitDelay, isPolling]);
186
+ }, [refreshRate, pollInitDelay, isPolling, bridge]);
221
187
 
222
- const polling = useMemo<Polling>(
223
- () => ({
224
- wipe: () => dispatch({ type: "wipe" }),
225
- poll: () => setTriggerLoad(true),
226
- start: () => setIsPolling(true),
227
- stop: () => setIsPolling(false),
228
- pending,
229
- error,
230
- }),
231
- [pending, error],
232
- );
188
+ return null;
189
+ }
233
190
 
191
+ /**
192
+ * Root countervalues provider (polling + calculation).
193
+ */
194
+ export function CountervaluesProvider({ children, bridge, ...rest }: Props): ReactElement {
234
195
  return (
235
- <CountervaluesPollingContext.Provider value={polling}>
236
- <CountervaluesUserSettingsContext.Provider value={userSettings}>
237
- <CountervaluesContext.Provider value={state}>{children}</CountervaluesContext.Provider>
238
- </CountervaluesUserSettingsContext.Provider>
239
- </CountervaluesPollingContext.Provider>
196
+ <CountervaluesContext.Provider value={bridge}>
197
+ <Effect {...rest} bridge={bridge} />
198
+ {children}
199
+ </CountervaluesContext.Provider>
240
200
  );
241
201
  }
242
202
 
243
- type Action =
244
- | { type: "success"; payload: CounterValuesState }
245
- | { type: "error"; payload: Error }
246
- | { type: "pending" }
247
- | { type: "wipe" }
248
- | { type: "setCounterValueState"; payload: CounterValuesState };
249
-
250
- type FetchState = { state: CounterValuesState; pending: boolean; error?: Error };
251
- const initialFetchState: FetchState = { state: initialState, pending: false };
252
-
253
- function fetchReducer(state: FetchState, action: Action): FetchState {
254
- switch (action.type) {
255
- case "success":
256
- return { state: action.payload, pending: false, error: undefined };
257
- case "error":
258
- return { ...state, pending: false, error: action.payload };
259
- case "pending":
260
- return { ...state, pending: true, error: undefined };
261
- case "wipe":
262
- return { state: initialState, pending: false, error: undefined };
263
- case "setCounterValueState":
264
- return { ...state, state: action.payload };
265
- default:
266
- return state;
267
- }
268
- }
269
-
270
- /** Returns market-cap ids. */
271
- export function useMarketcapIds(): string[] {
272
- return useContext(CountervaluesMarketcapIdsContext);
273
- }
274
-
275
203
  /** Returns the full countervalues state. */
276
204
  export function useCountervaluesState(): CounterValuesState {
277
- return useContext(CountervaluesContext);
205
+ return useCountervaluesBridgeContext().useState();
278
206
  }
279
207
 
280
- // allows consumer to access the countervalues polling control object
208
+ /** Allows consumer to access the countervalues polling control object */
281
209
  export function useCountervaluesPolling(): Polling {
282
- return useContext(CountervaluesPollingContext);
283
- }
284
-
285
- // allows consumer to access the user settings that was used to fetch the countervalues
286
- export function useCountervaluesUserSettingsContext(): CountervaluesSettings {
287
- return useContext(CountervaluesUserSettingsContext);
210
+ const bridge = useCountervaluesBridgeContext();
211
+ const pending = bridge.useStatePending();
212
+ const error = bridge.useStateError();
213
+ return useMemo(
214
+ () => ({
215
+ poll: () => bridge.setPollingTriggerLoad(true),
216
+ start: () => bridge.setPollingIsPolling(true),
217
+ stop: () => bridge.setPollingIsPolling(false),
218
+ wipe: () => bridge.wipe(),
219
+ pending,
220
+ error,
221
+ }),
222
+ [bridge, error, pending],
223
+ );
288
224
  }
289
225
 
290
- // provides an export of the countervalues state
291
- export function useCountervaluesExport(): CounterValuesStateRaw {
292
- const state = useCountervaluesState();
293
- return useMemo(() => exportCountervalues(state), [state]);
226
+ /** Allows consumer to access the user settings that was used to fetch the countervalues */
227
+ export function useCountervaluesUserSettings(): CountervaluesSettings {
228
+ return useCountervaluesBridgeContext().useUserSettings();
294
229
  }
295
230
 
296
- // provides a way to calculate a countervalue from a value
231
+ /**
232
+ * Provides a way to calculate a countervalue from a value
233
+ * Seems like a major bottleneck, see if it actually needs the full state or we can select only the needed data
234
+ */
297
235
  export function useCalculate(query: {
298
236
  value: number;
299
237
  from: Currency;
@@ -303,10 +241,10 @@ export function useCalculate(query: {
303
241
  reverse?: boolean;
304
242
  }): number | null | undefined {
305
243
  const state = useCountervaluesState();
306
- return calculate(state, query);
244
+ return useMemo(() => calculate(state, query), [state, query]);
307
245
  }
308
246
 
309
- // provides a way to calculate a countervalue from a value using a callback
247
+ /** Provides a way to calculate a countervalue from a value using a callback */
310
248
  export function useCalculateCountervalueCallback({
311
249
  to,
312
250
  }: {
@@ -366,8 +304,10 @@ export function useSendAmount({
366
304
  return { fiatAmount, fiatUnit, calculateCryptoAmount };
367
305
  }
368
306
 
369
- // infer the tracking pairs for the top coins that the portfolio needs to display itself
370
- // if startDate is undefined, the feature is disabled
307
+ /**
308
+ * Infer the tracking pairs for the top coins that the portfolio needs to display itself
309
+ * if startDate is undefined, the feature is disabled
310
+ */
371
311
  export function useTrackingPairsForTopCoins(
372
312
  marketcapIds: string[],
373
313
  countervalue: Currency,