@exodus/market-history 10.6.3 → 10.7.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.7.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/market-history@10.6.3...@exodus/market-history@10.7.0) (2026-05-01)
7
+
8
+ ### Features
9
+
10
+ - feat(market-history): hydrate prices from cache on start (#16273)
11
+
6
12
  ## [10.6.3](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/market-history@10.6.2...@exodus/market-history@10.6.3) (2026-04-08)
7
13
 
8
14
  ### Bug Fixes
package/lib/index.d.ts CHANGED
@@ -62,7 +62,7 @@ declare const marketHistory: ({ clearHistoryCacheDefaultValue, remoteConfigClear
62
62
  path: string;
63
63
  defaultValue: string | null;
64
64
  };
65
- remoteConfig: import("features/remote-config/lib/index.js").RemoteConfigType;
65
+ remoteConfig: import("@exodus/remote-config").RemoteConfigType;
66
66
  }) => import("@exodus/atoms").ReadonlyAtom<string | null>;
67
67
  readonly dependencies: readonly ["config", "remoteConfig"];
68
68
  readonly public: true;
@@ -83,7 +83,7 @@ declare const marketHistory: ({ clearHistoryCacheDefaultValue, remoteConfigClear
83
83
  path: string;
84
84
  defaultValue: number | null;
85
85
  };
86
- remoteConfig: import("features/remote-config/lib/index.js").RemoteConfigType;
86
+ remoteConfig: import("@exodus/remote-config").RemoteConfigType;
87
87
  }) => import("@exodus/atoms").ReadonlyAtom<number | null>;
88
88
  readonly dependencies: readonly ["config", "remoteConfig"];
89
89
  readonly public: true;
@@ -103,6 +103,7 @@ declare const marketHistory: ({ clearHistoryCacheDefaultValue, remoteConfigClear
103
103
  marketHistoryMonitor: {
104
104
  start: () => Promise<void>;
105
105
  stop: () => void;
106
+ hydrate: () => Promise<void>;
106
107
  };
107
108
  marketHistoryAtom: import("@exodus/atoms").Atom<import("./types.js").MarketHistoryState>;
108
109
  appProcessAtom?: import("@exodus/atoms").ReadonlyAtom<{
@@ -111,8 +112,8 @@ declare const marketHistory: ({ clearHistoryCacheDefaultValue, remoteConfigClear
111
112
  port: import("./types.js").MarketHistoryPort;
112
113
  errorTracking: import("./types.js").ErrorTracking;
113
114
  }) => {
115
+ onStart: () => void;
114
116
  onUnlock: () => void;
115
- onLoad: () => void;
116
117
  onStop: () => void;
117
118
  };
118
119
  readonly dependencies: readonly ["marketHistoryMonitor", "marketHistoryAtom", "appProcessAtom?", "port", "errorTracking"];
@@ -139,21 +139,38 @@ class MarketHistoryMonitorImpl {
139
139
  }
140
140
  #isActive = false;
141
141
  #setCache = async ({ currency, granularity, pricesByAssetName, }) => {
142
- let hasChanges = false;
143
- const changes = Object.keys(pricesByAssetName).reduce((acc, assetName) => {
144
- const key = this.#getCacheKey({ currency, assetName, granularity });
142
+ const assetNamesWithChanges = [];
143
+ const changes = Object.create(null);
144
+ for (const assetName of Object.keys(pricesByAssetName)) {
145
145
  const values = pricesByAssetName[assetName];
146
146
  if (values.length > 0) {
147
- hasChanges = true;
148
- acc[key] = values;
147
+ const key = this.#getCacheKey({ currency, assetName, granularity });
148
+ assetNamesWithChanges.push(assetName);
149
+ changes[key] = values;
149
150
  this.#runtimeCache.set(key, values);
150
151
  }
151
- return acc;
152
- }, Object.create(null));
153
- if (!hasChanges)
152
+ }
153
+ if (assetNamesWithChanges.length === 0)
154
154
  return;
155
155
  await this.storage.batchSet(changes);
156
+ await this.#extendCacheIndex(currency, assetNamesWithChanges);
156
157
  };
158
+ #cacheIndexKey = (currency) => `cached-assets-${currency}`;
159
+ #extendCacheIndex = makeConcurrent(async (currency, addedAssetNames) => {
160
+ const indexKey = this.#cacheIndexKey(currency);
161
+ const existing = ((await this.storage.get(indexKey)) || []);
162
+ const existingSet = new Set(existing);
163
+ let added = 0;
164
+ for (const name of addedAssetNames) {
165
+ if (!existingSet.has(name)) {
166
+ existingSet.add(name);
167
+ added += 1;
168
+ }
169
+ }
170
+ if (added === 0)
171
+ return;
172
+ await this.storage.set(indexKey, [...existingSet]);
173
+ }, { concurrency: 1 });
157
174
  #getCache = async ({ currency, granularity, assetName, }) => {
158
175
  const key = this.#getCacheKey({ currency, assetName, granularity });
159
176
  const cachedValue = this.#runtimeCache.get(key);
@@ -396,7 +413,7 @@ class MarketHistoryMonitorImpl {
396
413
  this.#logger.info('market history update because currency changes', currency);
397
414
  this.#currency = currency;
398
415
  void this.#marketHistoryAtom.set((current) => {
399
- const data = current?.data || {};
416
+ const data = current?.data || Object.create(null);
400
417
  return {
401
418
  data: {
402
419
  ...data,
@@ -442,12 +459,72 @@ class MarketHistoryMonitorImpl {
442
459
  };
443
460
  });
444
461
  };
462
+ #hydratePromise = null;
463
+ hydrate = () => {
464
+ this.#hydratePromise ??= this.#performHydrate();
465
+ return this.#hydratePromise;
466
+ };
467
+ #performHydrate = async () => {
468
+ const currency = await this.#currencyAtom.get();
469
+ const indexed = ((await this.storage.get(this.#cacheIndexKey(currency))) || []);
470
+ if (indexed.length === 0)
471
+ return;
472
+ const granularities = ['day', 'hour', 'minute'];
473
+ const keys = [];
474
+ const meta = [];
475
+ for (const assetName of indexed) {
476
+ for (const granularity of granularities) {
477
+ keys.push(this.#getCacheKey({ currency, assetName, granularity }));
478
+ meta.push({ assetName, granularity });
479
+ }
480
+ }
481
+ const results = (await this.storage.batchGet(keys));
482
+ const granularityPrices = {
483
+ daily: Object.create(null),
484
+ hourly: Object.create(null),
485
+ minutely: Object.create(null),
486
+ };
487
+ let hydratedCount = 0;
488
+ results.forEach((cacheEntries, index) => {
489
+ if (!cacheEntries || cacheEntries.length === 0)
490
+ return;
491
+ const { assetName, granularity } = meta[index];
492
+ const key = keys[index];
493
+ this.#runtimeCache.set(key, cacheEntries);
494
+ const priceRecord = new Map();
495
+ for (const [time, { close }] of cacheEntries) {
496
+ priceRecord.set(time, close);
497
+ }
498
+ const parsedGranularity = parseGranularity(granularity);
499
+ granularityPrices[parsedGranularity][assetName] = priceRecord;
500
+ hydratedCount += 1;
501
+ });
502
+ if (hydratedCount === 0)
503
+ return;
504
+ await this.#marketHistoryAtom.set((current) => {
505
+ if (current?.data?.[currency])
506
+ return current;
507
+ return {
508
+ data: {
509
+ ...current?.data,
510
+ [currency]: granularityPrices,
511
+ },
512
+ };
513
+ });
514
+ this.#logger.info(`market history hydrated from cache (currency=${currency})`);
515
+ };
445
516
  start = makeConcurrent(async () => {
446
517
  if (this.#isActive)
447
518
  return;
448
519
  this.#logger.info('market history start');
449
520
  this.#isActive = true;
450
521
  this.#abortController = new AbortController();
522
+ await this.hydrate().catch((error) => {
523
+ this.#logger.error('market history hydrate failed', error);
524
+ });
525
+ if (!this.#isActive) {
526
+ return;
527
+ }
451
528
  const remoteConfigClearCacheVersion = await this.#remoteConfigClearCacheAtom.get();
452
529
  await this.#invalidateStorage({ remoteConfigClearCacheVersion });
453
530
  if (!this.#isActive) {
@@ -3,6 +3,7 @@ import type { ErrorTracking, MarketHistoryPort, MarketHistoryState } from '../ty
3
3
  type MarketHistoryMonitor = {
4
4
  start: () => Promise<void>;
5
5
  stop: () => void;
6
+ hydrate: () => Promise<void>;
6
7
  };
7
8
  type Dependencies = {
8
9
  marketHistoryMonitor: MarketHistoryMonitor;
@@ -17,8 +18,8 @@ declare const marketHistoryLifecyclePluginDefinition: {
17
18
  readonly id: "marketHistoryLifecyclePlugin";
18
19
  readonly type: "plugin";
19
20
  readonly factory: ({ marketHistoryMonitor, marketHistoryAtom, appProcessAtom, port, errorTracking, }: Dependencies) => {
21
+ onStart: () => void;
20
22
  onUnlock: () => void;
21
- onLoad: () => void;
22
23
  onStop: () => void;
23
24
  };
24
25
  readonly dependencies: readonly ["marketHistoryMonitor", "marketHistoryAtom", "appProcessAtom?", "port", "errorTracking"];
@@ -19,8 +19,11 @@ const createMarketHistoryLifecyclePlugin = ({ marketHistoryMonitor, marketHistor
19
19
  previousMode = mode;
20
20
  });
21
21
  };
22
- const onLoad = () => {
22
+ const onStart = () => {
23
23
  void observer.start();
24
+ marketHistoryMonitor.hydrate().catch((e) => {
25
+ errorTracking.track({ error: e, context: 'start' });
26
+ });
24
27
  };
25
28
  const onUnlock = () => {
26
29
  const nonBlockingStart = async () => {
@@ -40,7 +43,7 @@ const createMarketHistoryLifecyclePlugin = ({ marketHistoryMonitor, marketHistor
40
43
  unobserve?.();
41
44
  marketHistoryMonitor.stop();
42
45
  };
43
- return { onUnlock, onLoad, onStop };
46
+ return { onStart, onUnlock, onStop };
44
47
  };
45
48
  const marketHistoryLifecyclePluginDefinition = {
46
49
  id: 'marketHistoryLifecyclePlugin',
package/lib/types.d.ts CHANGED
@@ -56,6 +56,7 @@ export type MarketHistoryStorage = {
56
56
  get: (key: string) => Promise<unknown>;
57
57
  set: (key: string, value: unknown) => Promise<void>;
58
58
  clear: () => Promise<void>;
59
+ batchGet: (keys: string[]) => Promise<unknown[]>;
59
60
  batchSet: (changes: Record<string, CacheEntry[]>) => Promise<void>;
60
61
  };
61
62
  export type AssetsModule = {
@@ -89,6 +90,7 @@ export type MarketHistoryMonitorDependencies = {
89
90
  export type MarketHistoryMonitor = {
90
91
  start: () => Promise<void>;
91
92
  stop: () => void;
93
+ hydrate: () => Promise<void>;
92
94
  update: (granularity: Granularity) => Promise<void>;
93
95
  updateAll: () => Promise<void>;
94
96
  fetchAssetPricesFromDate: (params: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/market-history",
3
- "version": "10.6.3",
3
+ "version": "10.7.0",
4
4
  "description": "Fetches historical prices for assets",
5
5
  "author": "Exodus Movement, Inc.",
6
6
  "license": "MIT",
@@ -23,8 +23,8 @@
23
23
  "README.md"
24
24
  ],
25
25
  "dependencies": {
26
- "@exodus/atoms": "^9.0.0",
27
- "@exodus/basic-utils": "^3.7.3",
26
+ "@exodus/atoms": "^10.3.3",
27
+ "@exodus/basic-utils": "^5.0.0",
28
28
  "@exodus/dayjs": "^1.0.2",
29
29
  "@exodus/remote-config": "^3.5.1",
30
30
  "@exodus/remote-config-atoms": "^1.1.0",
@@ -36,15 +36,15 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@exodus/assets": "^11.0.0",
39
- "@exodus/assets-base": "^10.0.0",
40
- "@exodus/assets-feature": "^9.0.1",
39
+ "@exodus/assets-base": "^12.0.0",
40
+ "@exodus/assets-feature": "^9.2.2",
41
41
  "@exodus/bitcoin-meta": "^2.0.0",
42
42
  "@exodus/dependency-types": "^2.1.1",
43
43
  "@exodus/ethereum-meta": "^2.4.1",
44
- "@exodus/locale": "^2.6.0",
44
+ "@exodus/locale": "^2.7.0",
45
45
  "@exodus/logger": "^1.2.3",
46
46
  "@exodus/rates-monitor": "^4.14.8",
47
- "@exodus/redux-dependency-injection": "^4.1.2",
47
+ "@exodus/redux-dependency-injection": "^4.4.0",
48
48
  "@exodus/storage-memory": "^2.4.0",
49
49
  "events": "^3.3.0",
50
50
  "redux": "^4.0.0"
@@ -61,5 +61,5 @@
61
61
  "access": "public",
62
62
  "provenance": false
63
63
  },
64
- "gitHead": "2c948759e38769a0d3a02916fd8b571e0c96e494"
64
+ "gitHead": "15750a8f551c1d4d869c6a133f1c5199c6dcfa93"
65
65
  }