@exodus/market-history 10.6.2 → 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 +12 -0
- package/lib/index.d.ts +4 -3
- package/lib/module/crop-history.d.ts +4 -2
- package/lib/module/crop-history.js +7 -3
- package/lib/module/index.js +86 -9
- package/lib/module/process-api-response.js +5 -2
- package/lib/plugin/index.d.ts +2 -1
- package/lib/plugin/index.js +5 -2
- package/lib/types.d.ts +2 -0
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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
|
+
|
|
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)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- fix(market-history): crop minute history to prevent unbounded growth (#15898)
|
|
17
|
+
|
|
6
18
|
## [10.6.2](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/market-history@10.6.1...@exodus/market-history@10.6.2) (2026-04-07)
|
|
7
19
|
|
|
8
20
|
### 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("
|
|
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("
|
|
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"];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PriceData } from '../types.js';
|
|
2
|
-
|
|
2
|
+
type CropStep = 'hour' | 'minute';
|
|
3
|
+
declare const cropHistory: ({ history, limit, step, }: {
|
|
3
4
|
history: Map<number, PriceData>;
|
|
4
|
-
|
|
5
|
+
limit: number;
|
|
6
|
+
step: CropStep;
|
|
5
7
|
}) => Map<number, PriceData>;
|
|
6
8
|
export default cropHistory;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import ms from 'ms';
|
|
2
2
|
import lastTimestampFromPricesMap from './last-timestamp-from-prices-map.js';
|
|
3
|
-
const
|
|
4
|
-
|
|
3
|
+
const STEP_MS = {
|
|
4
|
+
hour: ms('1h'),
|
|
5
|
+
minute: ms('1m'),
|
|
6
|
+
};
|
|
7
|
+
const cropHistory = ({ history, limit, step, }) => {
|
|
8
|
+
const stepMs = STEP_MS[step];
|
|
5
9
|
const lastCachedTime = lastTimestampFromPricesMap(history);
|
|
6
10
|
if (!lastCachedTime)
|
|
7
11
|
return history;
|
|
8
|
-
const limitedTime = lastCachedTime -
|
|
12
|
+
const limitedTime = lastCachedTime - stepMs * limit;
|
|
9
13
|
const result = new Map();
|
|
10
14
|
for (const [key, value] of history.entries()) {
|
|
11
15
|
if (key > limitedTime) {
|
package/lib/module/index.js
CHANGED
|
@@ -139,21 +139,38 @@ class MarketHistoryMonitorImpl {
|
|
|
139
139
|
}
|
|
140
140
|
#isActive = false;
|
|
141
141
|
#setCache = async ({ currency, granularity, pricesByAssetName, }) => {
|
|
142
|
-
|
|
143
|
-
const changes = Object.
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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) {
|
|
@@ -28,8 +28,11 @@ export default function processApiResponse({ assetTicker, fiatTicker, fetchedPri
|
|
|
28
28
|
added++;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
if (granularity === 'hour'
|
|
32
|
-
|
|
31
|
+
if ((granularity === 'hour' || granularity === 'minute') &&
|
|
32
|
+
!specificTimestamp &&
|
|
33
|
+
added > 0 &&
|
|
34
|
+
requestLimit) {
|
|
35
|
+
history = cropHistory({ history, limit: requestLimit, step: granularity });
|
|
33
36
|
}
|
|
34
37
|
return history;
|
|
35
38
|
}
|
package/lib/plugin/index.d.ts
CHANGED
|
@@ -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"];
|
package/lib/plugin/index.js
CHANGED
|
@@ -19,8 +19,11 @@ const createMarketHistoryLifecyclePlugin = ({ marketHistoryMonitor, marketHistor
|
|
|
19
19
|
previousMode = mode;
|
|
20
20
|
});
|
|
21
21
|
};
|
|
22
|
-
const
|
|
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 {
|
|
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.
|
|
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": "^
|
|
27
|
-
"@exodus/basic-utils": "^
|
|
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": "^
|
|
40
|
-
"@exodus/assets-feature": "^9.
|
|
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.
|
|
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.
|
|
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": "
|
|
64
|
+
"gitHead": "15750a8f551c1d4d869c6a133f1c5199c6dcfa93"
|
|
65
65
|
}
|