@metamask-previews/perps-controller 3.0.0-preview-e61cfa5 → 3.1.0-preview-548bdd1d9
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 +19 -1
- package/dist/PerpsController-method-action-types.cjs.map +1 -1
- package/dist/PerpsController-method-action-types.d.cts +8 -0
- package/dist/PerpsController-method-action-types.d.cts.map +1 -1
- package/dist/PerpsController-method-action-types.d.mts +8 -0
- package/dist/PerpsController-method-action-types.d.mts.map +1 -1
- package/dist/PerpsController-method-action-types.mjs.map +1 -1
- package/dist/PerpsController.cjs +117 -29
- package/dist/PerpsController.cjs.map +1 -1
- package/dist/PerpsController.d.cts +14 -2
- package/dist/PerpsController.d.cts.map +1 -1
- package/dist/PerpsController.d.mts +14 -2
- package/dist/PerpsController.d.mts.map +1 -1
- package/dist/PerpsController.mjs +118 -30
- package/dist/PerpsController.mjs.map +1 -1
- package/dist/constants/eventNames.cjs +1 -0
- package/dist/constants/eventNames.cjs.map +1 -1
- package/dist/constants/eventNames.d.cts +1 -0
- package/dist/constants/eventNames.d.cts.map +1 -1
- package/dist/constants/eventNames.d.mts +1 -0
- package/dist/constants/eventNames.d.mts.map +1 -1
- package/dist/constants/eventNames.mjs +1 -0
- package/dist/constants/eventNames.mjs.map +1 -1
- package/dist/constants/perpsConfig.cjs +46 -1
- package/dist/constants/perpsConfig.cjs.map +1 -1
- package/dist/constants/perpsConfig.d.cts +35 -0
- package/dist/constants/perpsConfig.d.cts.map +1 -1
- package/dist/constants/perpsConfig.d.mts +35 -0
- package/dist/constants/perpsConfig.d.mts.map +1 -1
- package/dist/constants/perpsConfig.mjs +43 -0
- package/dist/constants/perpsConfig.mjs.map +1 -1
- package/dist/constants/transactionsHistoryConfig.cjs +23 -4
- package/dist/constants/transactionsHistoryConfig.cjs.map +1 -1
- package/dist/constants/transactionsHistoryConfig.d.cts +23 -4
- package/dist/constants/transactionsHistoryConfig.d.cts.map +1 -1
- package/dist/constants/transactionsHistoryConfig.d.mts +23 -4
- package/dist/constants/transactionsHistoryConfig.d.mts.map +1 -1
- package/dist/constants/transactionsHistoryConfig.mjs +23 -4
- package/dist/constants/transactionsHistoryConfig.mjs.map +1 -1
- package/dist/index.cjs +14 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/providers/HyperLiquidProvider.cjs +83 -27
- package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
- package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
- package/dist/providers/HyperLiquidProvider.mjs +83 -27
- package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.cjs +6 -0
- package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
- package/dist/services/HyperLiquidSubscriptionService.mjs +6 -0
- package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +6 -0
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.mts +6 -0
- package/dist/types/index.d.mts.map +1 -1
- package/dist/types/index.mjs.map +1 -1
- package/dist/utils/index.cjs +2 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +2 -0
- package/dist/utils/index.d.cts.map +1 -1
- package/dist/utils/index.d.mts +2 -0
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +2 -0
- package/dist/utils/index.mjs.map +1 -1
- package/dist/utils/perpsDiskPersistence.cjs +252 -0
- package/dist/utils/perpsDiskPersistence.cjs.map +1 -0
- package/dist/utils/perpsDiskPersistence.d.cts +108 -0
- package/dist/utils/perpsDiskPersistence.d.cts.map +1 -0
- package/dist/utils/perpsDiskPersistence.d.mts +108 -0
- package/dist/utils/perpsDiskPersistence.d.mts.map +1 -0
- package/dist/utils/perpsDiskPersistence.mjs +244 -0
- package/dist/utils/perpsDiskPersistence.mjs.map +1 -0
- package/dist/utils/perpsFormatters.cjs +498 -0
- package/dist/utils/perpsFormatters.cjs.map +1 -0
- package/dist/utils/perpsFormatters.d.cts +202 -0
- package/dist/utils/perpsFormatters.d.cts.map +1 -0
- package/dist/utils/perpsFormatters.d.mts +202 -0
- package/dist/utils/perpsFormatters.d.mts.map +1 -0
- package/dist/utils/perpsFormatters.mjs +489 -0
- package/dist/utils/perpsFormatters.mjs.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { buildProviderCacheKey, PERPS_CONSTANTS, PERPS_DISK_CACHE_MARKETS, PERPS_DISK_CACHE_USER_DATA, PROVIDER_CONFIG } from "../constants/perpsConfig.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Multiplier applied to staleGuardMs (preloadGuardMs, currently 30s) to compute
|
|
4
|
+
* the staleness cap for disk-hydrated timestamps. A factor of 10 ensures hydrated
|
|
5
|
+
* data is always TTL-expired so the stream manager overwrites it with live data,
|
|
6
|
+
* while still being recent enough for a useful first paint.
|
|
7
|
+
*/
|
|
8
|
+
const DISK_HYDRATION_STALENESS_FACTOR = 10;
|
|
9
|
+
/**
|
|
10
|
+
* Build the disk-cache payload for market data.
|
|
11
|
+
* In aggregated mode, groups markets by provider into separate entries.
|
|
12
|
+
*
|
|
13
|
+
* @param markets - Current market data snapshot.
|
|
14
|
+
* @param activeProvider - The active provider id (may be "aggregated").
|
|
15
|
+
* @param isTestnet - Global testnet flag.
|
|
16
|
+
* @param now - Timestamp to stamp entries with.
|
|
17
|
+
* @returns Payload ready for JSON serialization.
|
|
18
|
+
*/
|
|
19
|
+
export function buildMarketDataPayload(markets, activeProvider, isTestnet, now) {
|
|
20
|
+
if (activeProvider === 'aggregated') {
|
|
21
|
+
const entriesByKey = new Map();
|
|
22
|
+
for (const market of markets) {
|
|
23
|
+
const providerId = market.providerId ?? PROVIDER_CONFIG.DefaultProvider;
|
|
24
|
+
const key = buildProviderCacheKey(providerId, isTestnet);
|
|
25
|
+
const existing = entriesByKey.get(key);
|
|
26
|
+
if (existing) {
|
|
27
|
+
existing.data.push(market);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
entriesByKey.set(key, {
|
|
31
|
+
providerNetworkKey: key,
|
|
32
|
+
data: [market],
|
|
33
|
+
timestamp: now,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const entries = Array.from(entriesByKey.values());
|
|
38
|
+
return entries.length === 1 ? entries[0] : { entries };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
providerNetworkKey: buildProviderCacheKey(activeProvider ?? PROVIDER_CONFIG.DefaultProvider, isTestnet),
|
|
42
|
+
data: markets,
|
|
43
|
+
timestamp: now,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the disk-cache payload for user data (positions, orders, account).
|
|
48
|
+
* In aggregated mode, groups entries by provider.
|
|
49
|
+
*
|
|
50
|
+
* @param positions - Current positions snapshot.
|
|
51
|
+
* @param orders - Current orders snapshot.
|
|
52
|
+
* @param accountState - Current account state snapshot.
|
|
53
|
+
* @param address - EVM account address.
|
|
54
|
+
* @param activeProvider - The active provider id (may be "aggregated").
|
|
55
|
+
* @param isTestnet - Global testnet flag.
|
|
56
|
+
* @param now - Timestamp to stamp entries with.
|
|
57
|
+
* @returns Payload ready for JSON serialization.
|
|
58
|
+
*/
|
|
59
|
+
export function buildUserDataPayload(positions, orders, accountState, address, activeProvider, isTestnet, now) {
|
|
60
|
+
if (activeProvider === 'aggregated') {
|
|
61
|
+
const entriesByKey = new Map();
|
|
62
|
+
const ensureEntry = (providerId) => {
|
|
63
|
+
const key = buildProviderCacheKey(providerId, isTestnet);
|
|
64
|
+
let entry = entriesByKey.get(key);
|
|
65
|
+
if (!entry) {
|
|
66
|
+
entry = {
|
|
67
|
+
providerNetworkKey: key,
|
|
68
|
+
address,
|
|
69
|
+
positions: [],
|
|
70
|
+
orders: [],
|
|
71
|
+
accountState: null,
|
|
72
|
+
timestamp: now,
|
|
73
|
+
};
|
|
74
|
+
entriesByKey.set(key, entry);
|
|
75
|
+
}
|
|
76
|
+
return entry;
|
|
77
|
+
};
|
|
78
|
+
for (const position of positions) {
|
|
79
|
+
ensureEntry(position.providerId ?? PROVIDER_CONFIG.DefaultProvider).positions.push(position);
|
|
80
|
+
}
|
|
81
|
+
for (const order of orders) {
|
|
82
|
+
ensureEntry(order.providerId ?? PROVIDER_CONFIG.DefaultProvider).orders.push(order);
|
|
83
|
+
}
|
|
84
|
+
if (accountState) {
|
|
85
|
+
ensureEntry(accountState.providerId ?? PROVIDER_CONFIG.DefaultProvider).accountState = accountState;
|
|
86
|
+
}
|
|
87
|
+
const entries = Array.from(entriesByKey.values()).filter((entry) => entry.positions.length > 0 ||
|
|
88
|
+
entry.orders.length > 0 ||
|
|
89
|
+
entry.accountState !== null);
|
|
90
|
+
return entries.length === 1 ? entries[0] : { entries };
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
providerNetworkKey: buildProviderCacheKey(activeProvider ?? PROVIDER_CONFIG.DefaultProvider, isTestnet),
|
|
94
|
+
address,
|
|
95
|
+
positions,
|
|
96
|
+
orders,
|
|
97
|
+
accountState,
|
|
98
|
+
timestamp: now,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Write market entries to disk (best-effort, non-blocking).
|
|
103
|
+
*
|
|
104
|
+
* @param diskCache - Disk cache instance from controller infrastructure.
|
|
105
|
+
* @param entries - Pre-assembled market cache entries to persist.
|
|
106
|
+
*/
|
|
107
|
+
export function persistMarketEntriesToDisk(diskCache, entries) {
|
|
108
|
+
if (entries.length === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const payload = entries.length === 1 ? entries[0] : { entries };
|
|
112
|
+
diskCache
|
|
113
|
+
.setItem(PERPS_DISK_CACHE_MARKETS, JSON.stringify(payload))
|
|
114
|
+
.catch(() => {
|
|
115
|
+
// Disk persistence is best-effort and must never block preload.
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Write user data entries to disk (best-effort, non-blocking).
|
|
120
|
+
*
|
|
121
|
+
* @param diskCache - Disk cache instance from controller infrastructure.
|
|
122
|
+
* @param entries - Pre-assembled user cache entries to persist.
|
|
123
|
+
*/
|
|
124
|
+
export function persistUserEntriesToDisk(diskCache, entries) {
|
|
125
|
+
if (entries.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const payload = entries.length === 1 ? entries[0] : { entries };
|
|
129
|
+
diskCache
|
|
130
|
+
.setItem(PERPS_DISK_CACHE_USER_DATA, JSON.stringify(payload))
|
|
131
|
+
.catch(() => {
|
|
132
|
+
// Disk persistence is best-effort and must never block preload.
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Read disk-persisted cache snapshots and compute the state updates to apply.
|
|
137
|
+
* Returns plain objects rather than mutating state directly, so the caller
|
|
138
|
+
* can apply all changes in a single batched this.update() call.
|
|
139
|
+
*
|
|
140
|
+
* All returned timestamps are capped at DISK_HYDRATION_STALENESS_FACTOR * staleGuardMs
|
|
141
|
+
* in the past so the stream manager always overwrites disk data with fresh live data.
|
|
142
|
+
*
|
|
143
|
+
* @param diskCache - Disk cache instance from controller infrastructure.
|
|
144
|
+
* @param currentMarketCache - Current cachedMarketDataByProvider state.
|
|
145
|
+
* @param currentUserCache - Current cachedUserDataByProvider state.
|
|
146
|
+
* @param staleGuardMs - preloadGuardMs constant from the controller.
|
|
147
|
+
* @returns Updates to apply plus stats for debug logging.
|
|
148
|
+
*/
|
|
149
|
+
export function hydrateFromDiskSync(diskCache, currentMarketCache, currentUserCache, staleGuardMs) {
|
|
150
|
+
const hydrateT0 = Date.now();
|
|
151
|
+
const marketUpdates = {};
|
|
152
|
+
const userUpdates = {};
|
|
153
|
+
let marketCount = 0;
|
|
154
|
+
let userPositions = 0;
|
|
155
|
+
let userOrders = 0;
|
|
156
|
+
if (!diskCache.getItemSync) {
|
|
157
|
+
return {
|
|
158
|
+
marketUpdates,
|
|
159
|
+
userUpdates,
|
|
160
|
+
stats: { marketCount, userPositions, userOrders, durationMs: 0 },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const staleHydratedTimestamp = Date.now() - staleGuardMs * DISK_HYDRATION_STALENESS_FACTOR - 1;
|
|
164
|
+
try {
|
|
165
|
+
const marketsRaw = diskCache.getItemSync(PERPS_DISK_CACHE_MARKETS);
|
|
166
|
+
const userRaw = diskCache.getItemSync(PERPS_DISK_CACHE_USER_DATA);
|
|
167
|
+
if (marketsRaw) {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(marketsRaw);
|
|
170
|
+
const entries = Array.isArray(parsed.entries)
|
|
171
|
+
? parsed.entries
|
|
172
|
+
: [parsed];
|
|
173
|
+
for (const entry of entries) {
|
|
174
|
+
if (entry.providerNetworkKey && Array.isArray(entry.data)) {
|
|
175
|
+
const existing = currentMarketCache[entry.providerNetworkKey];
|
|
176
|
+
if (!existing || existing.timestamp < entry.timestamp) {
|
|
177
|
+
const strippedData = entry.data.map((market) => ({
|
|
178
|
+
...market,
|
|
179
|
+
price: PERPS_CONSTANTS.FallbackPriceDisplay,
|
|
180
|
+
change24h: PERPS_CONSTANTS.FallbackDataDisplay,
|
|
181
|
+
change24hPercent: PERPS_CONSTANTS.FallbackPercentageDisplay,
|
|
182
|
+
}));
|
|
183
|
+
marketUpdates[entry.providerNetworkKey] = {
|
|
184
|
+
data: strippedData,
|
|
185
|
+
// Disk-hydrated market snapshots are only for structural
|
|
186
|
+
// first paint. Keep them TTL-stale so the stream manager
|
|
187
|
+
// still fetches fresh prices on connect.
|
|
188
|
+
timestamp: Math.min(entry.timestamp, staleHydratedTimestamp),
|
|
189
|
+
};
|
|
190
|
+
marketCount += strippedData.length;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Corrupt JSON — silently ignore
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (userRaw) {
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(userRaw);
|
|
202
|
+
const entries = Array.isArray(parsed.entries)
|
|
203
|
+
? parsed.entries
|
|
204
|
+
: [parsed];
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
if (entry.providerNetworkKey && entry.address) {
|
|
207
|
+
// Skip address check here — accounts may not be loaded yet at
|
|
208
|
+
// constructor time. getCachedUserDataForActiveProvider validates
|
|
209
|
+
// the address at read time, so stale-account data is never served.
|
|
210
|
+
const existing = currentUserCache[entry.providerNetworkKey];
|
|
211
|
+
if (!existing || existing.timestamp < entry.timestamp) {
|
|
212
|
+
userUpdates[entry.providerNetworkKey] = {
|
|
213
|
+
positions: entry.positions,
|
|
214
|
+
orders: entry.orders,
|
|
215
|
+
accountState: entry.accountState,
|
|
216
|
+
timestamp: Math.min(entry.timestamp, staleHydratedTimestamp),
|
|
217
|
+
address: entry.address,
|
|
218
|
+
};
|
|
219
|
+
userPositions += entry.positions.length;
|
|
220
|
+
userOrders += entry.orders.length;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Corrupt JSON — silently ignore
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Disk read failure — non-critical
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
marketUpdates,
|
|
235
|
+
userUpdates,
|
|
236
|
+
stats: {
|
|
237
|
+
marketCount,
|
|
238
|
+
userPositions,
|
|
239
|
+
userOrders,
|
|
240
|
+
durationMs: Date.now() - hydrateT0,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=perpsDiskPersistence.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perpsDiskPersistence.mjs","sourceRoot":"","sources":["../../src/utils/perpsDiskPersistence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n buildProviderCacheKey,\n PERPS_CONSTANTS,\n PERPS_DISK_CACHE_MARKETS,\n PERPS_DISK_CACHE_USER_DATA,\n PROVIDER_CONFIG,\n} from '../constants/perpsConfig';\nimport type { AccountState, Order, PerpsMarketData, Position } from '../types';\n\n/**\n * Multiplier applied to staleGuardMs (preloadGuardMs, currently 30s) to compute\n * the staleness cap for disk-hydrated timestamps. A factor of 10 ensures hydrated\n * data is always TTL-expired so the stream manager overwrites it with live data,\n * while still being recent enough for a useful first paint.\n */\nconst DISK_HYDRATION_STALENESS_FACTOR = 10;\n\n/** Minimal disk cache interface required by persistence utilities. */\nexport type PerpsDiskCache = {\n getItem(key: string): Promise<string | null>;\n getItemSync?(key: string): string | null;\n setItem(key: string, value: string): Promise<void>;\n};\n\n/** Shape of a single market entry persisted to disk cache. */\nexport type DiskCacheMarketEntry = {\n providerNetworkKey: string;\n data: PerpsMarketData[];\n timestamp: number;\n};\n\n/** Shape of a single user-data entry persisted to disk cache. */\nexport type DiskCacheUserEntry = {\n providerNetworkKey: string;\n address: string;\n positions: Position[];\n orders: Order[];\n accountState: AccountState | null;\n timestamp: number;\n};\n\n/** Disk payload shape — either a single entry or a multi-provider wrapper. */\nexport type DiskCacheMarketPayload =\n | DiskCacheMarketEntry\n | { entries: DiskCacheMarketEntry[] };\n\nexport type DiskCacheUserPayload =\n | DiskCacheUserEntry\n | { entries: DiskCacheUserEntry[] };\n\n/**\n * Build the disk-cache payload for market data.\n * In aggregated mode, groups markets by provider into separate entries.\n *\n * @param markets - Current market data snapshot.\n * @param activeProvider - The active provider id (may be \"aggregated\").\n * @param isTestnet - Global testnet flag.\n * @param now - Timestamp to stamp entries with.\n * @returns Payload ready for JSON serialization.\n */\nexport function buildMarketDataPayload(\n markets: PerpsMarketData[],\n activeProvider: string,\n isTestnet: boolean,\n now: number,\n): DiskCacheMarketPayload {\n if (activeProvider === 'aggregated') {\n const entriesByKey = new Map<string, DiskCacheMarketEntry>();\n for (const market of markets) {\n const providerId = market.providerId ?? PROVIDER_CONFIG.DefaultProvider;\n const key = buildProviderCacheKey(providerId, isTestnet);\n const existing = entriesByKey.get(key);\n if (existing) {\n existing.data.push(market);\n } else {\n entriesByKey.set(key, {\n providerNetworkKey: key,\n data: [market],\n timestamp: now,\n });\n }\n }\n const entries = Array.from(entriesByKey.values());\n return entries.length === 1 ? entries[0] : { entries };\n }\n return {\n providerNetworkKey: buildProviderCacheKey(\n activeProvider ?? PROVIDER_CONFIG.DefaultProvider,\n isTestnet,\n ),\n data: markets,\n timestamp: now,\n };\n}\n\n/**\n * Build the disk-cache payload for user data (positions, orders, account).\n * In aggregated mode, groups entries by provider.\n *\n * @param positions - Current positions snapshot.\n * @param orders - Current orders snapshot.\n * @param accountState - Current account state snapshot.\n * @param address - EVM account address.\n * @param activeProvider - The active provider id (may be \"aggregated\").\n * @param isTestnet - Global testnet flag.\n * @param now - Timestamp to stamp entries with.\n * @returns Payload ready for JSON serialization.\n */\nexport function buildUserDataPayload(\n positions: Position[],\n orders: Order[],\n accountState: AccountState | null,\n address: string,\n activeProvider: string,\n isTestnet: boolean,\n now: number,\n): DiskCacheUserPayload {\n if (activeProvider === 'aggregated') {\n const entriesByKey = new Map<string, DiskCacheUserEntry>();\n const ensureEntry = (providerId: string): DiskCacheUserEntry => {\n const key = buildProviderCacheKey(providerId, isTestnet);\n let entry = entriesByKey.get(key);\n if (!entry) {\n entry = {\n providerNetworkKey: key,\n address,\n positions: [],\n orders: [],\n accountState: null,\n timestamp: now,\n };\n entriesByKey.set(key, entry);\n }\n return entry;\n };\n\n for (const position of positions) {\n ensureEntry(\n position.providerId ?? PROVIDER_CONFIG.DefaultProvider,\n ).positions.push(position);\n }\n for (const order of orders) {\n ensureEntry(\n order.providerId ?? PROVIDER_CONFIG.DefaultProvider,\n ).orders.push(order);\n }\n if (accountState) {\n ensureEntry(\n accountState.providerId ?? PROVIDER_CONFIG.DefaultProvider,\n ).accountState = accountState;\n }\n\n const entries = Array.from(entriesByKey.values()).filter(\n (entry) =>\n entry.positions.length > 0 ||\n entry.orders.length > 0 ||\n entry.accountState !== null,\n );\n return entries.length === 1 ? entries[0] : { entries };\n }\n return {\n providerNetworkKey: buildProviderCacheKey(\n activeProvider ?? PROVIDER_CONFIG.DefaultProvider,\n isTestnet,\n ),\n address,\n positions,\n orders,\n accountState,\n timestamp: now,\n };\n}\n\n/**\n * Write market entries to disk (best-effort, non-blocking).\n *\n * @param diskCache - Disk cache instance from controller infrastructure.\n * @param entries - Pre-assembled market cache entries to persist.\n */\nexport function persistMarketEntriesToDisk(\n diskCache: PerpsDiskCache,\n entries: DiskCacheMarketEntry[],\n): void {\n if (entries.length === 0) {\n return;\n }\n const payload = entries.length === 1 ? entries[0] : { entries };\n diskCache\n .setItem(PERPS_DISK_CACHE_MARKETS, JSON.stringify(payload))\n .catch(() => {\n // Disk persistence is best-effort and must never block preload.\n });\n}\n\n/**\n * Write user data entries to disk (best-effort, non-blocking).\n *\n * @param diskCache - Disk cache instance from controller infrastructure.\n * @param entries - Pre-assembled user cache entries to persist.\n */\nexport function persistUserEntriesToDisk(\n diskCache: PerpsDiskCache,\n entries: DiskCacheUserEntry[],\n): void {\n if (entries.length === 0) {\n return;\n }\n const payload = entries.length === 1 ? entries[0] : { entries };\n diskCache\n .setItem(PERPS_DISK_CACHE_USER_DATA, JSON.stringify(payload))\n .catch(() => {\n // Disk persistence is best-effort and must never block preload.\n });\n}\n\n/** Computed updates returned by hydrateFromDiskSync. */\nexport type HydrateFromDiskResult = {\n marketUpdates: Record<string, { data: PerpsMarketData[]; timestamp: number }>;\n userUpdates: Record<\n string,\n {\n positions: Position[];\n orders: Order[];\n accountState: AccountState | null;\n timestamp: number;\n address: string;\n }\n >;\n stats: {\n marketCount: number;\n userPositions: number;\n userOrders: number;\n durationMs: number;\n };\n};\n\n/**\n * Read disk-persisted cache snapshots and compute the state updates to apply.\n * Returns plain objects rather than mutating state directly, so the caller\n * can apply all changes in a single batched this.update() call.\n *\n * All returned timestamps are capped at DISK_HYDRATION_STALENESS_FACTOR * staleGuardMs\n * in the past so the stream manager always overwrites disk data with fresh live data.\n *\n * @param diskCache - Disk cache instance from controller infrastructure.\n * @param currentMarketCache - Current cachedMarketDataByProvider state.\n * @param currentUserCache - Current cachedUserDataByProvider state.\n * @param staleGuardMs - preloadGuardMs constant from the controller.\n * @returns Updates to apply plus stats for debug logging.\n */\nexport function hydrateFromDiskSync(\n diskCache: PerpsDiskCache,\n currentMarketCache: Record<string, { timestamp: number }>,\n currentUserCache: Record<string, { timestamp: number }>,\n staleGuardMs: number,\n): HydrateFromDiskResult {\n const hydrateT0 = Date.now();\n const marketUpdates: HydrateFromDiskResult['marketUpdates'] = {};\n const userUpdates: HydrateFromDiskResult['userUpdates'] = {};\n let marketCount = 0;\n let userPositions = 0;\n let userOrders = 0;\n\n if (!diskCache.getItemSync) {\n return {\n marketUpdates,\n userUpdates,\n stats: { marketCount, userPositions, userOrders, durationMs: 0 },\n };\n }\n\n const staleHydratedTimestamp =\n Date.now() - staleGuardMs * DISK_HYDRATION_STALENESS_FACTOR - 1;\n\n try {\n const marketsRaw = diskCache.getItemSync(PERPS_DISK_CACHE_MARKETS);\n const userRaw = diskCache.getItemSync(PERPS_DISK_CACHE_USER_DATA);\n\n if (marketsRaw) {\n try {\n const parsed = JSON.parse(marketsRaw) as\n | DiskCacheMarketEntry\n | { entries: DiskCacheMarketEntry[] };\n const entries = Array.isArray((parsed as { entries?: unknown }).entries)\n ? (parsed as { entries: DiskCacheMarketEntry[] }).entries\n : [parsed as DiskCacheMarketEntry];\n\n for (const entry of entries) {\n if (entry.providerNetworkKey && Array.isArray(entry.data)) {\n const existing = currentMarketCache[entry.providerNetworkKey];\n if (!existing || existing.timestamp < entry.timestamp) {\n const strippedData = entry.data.map((market) => ({\n ...market,\n price: PERPS_CONSTANTS.FallbackPriceDisplay,\n change24h: PERPS_CONSTANTS.FallbackDataDisplay,\n change24hPercent: PERPS_CONSTANTS.FallbackPercentageDisplay,\n }));\n marketUpdates[entry.providerNetworkKey] = {\n data: strippedData,\n // Disk-hydrated market snapshots are only for structural\n // first paint. Keep them TTL-stale so the stream manager\n // still fetches fresh prices on connect.\n timestamp: Math.min(entry.timestamp, staleHydratedTimestamp),\n };\n marketCount += strippedData.length;\n }\n }\n }\n } catch {\n // Corrupt JSON — silently ignore\n }\n }\n\n if (userRaw) {\n try {\n const parsed = JSON.parse(userRaw) as\n | DiskCacheUserEntry\n | { entries: DiskCacheUserEntry[] };\n const entries = Array.isArray((parsed as { entries?: unknown }).entries)\n ? (parsed as { entries: DiskCacheUserEntry[] }).entries\n : [parsed as DiskCacheUserEntry];\n\n for (const entry of entries) {\n if (entry.providerNetworkKey && entry.address) {\n // Skip address check here — accounts may not be loaded yet at\n // constructor time. getCachedUserDataForActiveProvider validates\n // the address at read time, so stale-account data is never served.\n const existing = currentUserCache[entry.providerNetworkKey];\n if (!existing || existing.timestamp < entry.timestamp) {\n userUpdates[entry.providerNetworkKey] = {\n positions: entry.positions,\n orders: entry.orders,\n accountState: entry.accountState,\n timestamp: Math.min(entry.timestamp, staleHydratedTimestamp),\n address: entry.address,\n };\n userPositions += entry.positions.length;\n userOrders += entry.orders.length;\n }\n }\n }\n } catch {\n // Corrupt JSON — silently ignore\n }\n }\n } catch {\n // Disk read failure — non-critical\n }\n\n return {\n marketUpdates,\n userUpdates,\n stats: {\n marketCount,\n userPositions,\n userOrders,\n durationMs: Date.now() - hydrateT0,\n },\n };\n}\n"]}
|