-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcache.js
More file actions
153 lines (141 loc) · 6.22 KB
/
Copy pathcache.js
File metadata and controls
153 lines (141 loc) · 6.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* cache.js — client-side cache helpers (schema-versioned).
*
* Two responsibilities:
*
* 1. Schema version sentinel for `localStorage` so a deploy that changes the
* persisted form/config shape automatically purges any incompatible
* entries on the next page load. Bump `CACHE_SCHEMA_VERSION` whenever
* the localStorage layout changes; old keys are wiped silently.
*
* 2. A typed `sessionCache` helper backed by `sessionStorage` for caching
* expensive analysis responses by `(ticker, date_range)` so switching
* between recently-viewed tickers in the same tab is instantaneous.
*
* Loaded via <script> tag before main.js — exposes `window.appCache`.
*/
(function (root) {
'use strict';
// Bump this whenever the shape of any persisted cache changes.
const CACHE_SCHEMA_VERSION = 2;
const SCHEMA_KEY = '__cache_schema_version__';
// Keys belonging to OptionView that participate in versioning.
const MANAGED_LOCAL_KEYS = ['marketAnalysisForm', 'marketAnalysisConfig'];
// Prefix for sessionStorage analysis cache entries.
const SESSION_PREFIX = 'mv:analysis:';
const SESSION_DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
const SESSION_MAX_ENTRIES = 8;
function _safeGet(storage, key) {
try { return storage.getItem(key); } catch (_) { return null; }
}
function _safeSet(storage, key, value) {
try { storage.setItem(key, value); return true; } catch (_) { return false; }
}
function _safeRemove(storage, key) {
try { storage.removeItem(key); } catch (_) { /* ignore */ }
}
/* Validate (and migrate) the schema version. Runs once at script load.
On mismatch we wipe ONLY the keys we manage so unrelated apps in the
same origin (e.g. browser extensions) keep their data. */
function _ensureSchema() {
const stored = _safeGet(localStorage, SCHEMA_KEY);
if (stored === String(CACHE_SCHEMA_VERSION)) return;
for (const k of MANAGED_LOCAL_KEYS) _safeRemove(localStorage, k);
// Clear all sessionStorage entries we own (analysis cache).
try {
const toDelete = [];
for (let i = 0; i < sessionStorage.length; i++) {
const k = sessionStorage.key(i);
if (k && k.startsWith(SESSION_PREFIX)) toDelete.push(k);
}
toDelete.forEach((k) => _safeRemove(sessionStorage, k));
} catch (_) { /* ignore */ }
_safeSet(localStorage, SCHEMA_KEY, String(CACHE_SCHEMA_VERSION));
if (stored !== null) {
try { console.info('[cache] schema bumped', stored, '→', CACHE_SCHEMA_VERSION); } catch (_) { /* ignore */ }
}
}
/* Tiny FNV-1a 32-bit hash → hex string. Stable across page loads.
Used to compress (ticker + date_range) into a fixed-length cache key. */
function _hash(s) {
let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
}
return ('00000000' + h.toString(16)).slice(-8);
}
function buildKey(ticker, startDate, endDate, extra) {
const t = (ticker || '').toUpperCase();
const range = `${startDate || ''}|${endDate || ''}`;
const ex = extra ? JSON.stringify(extra) : '';
return SESSION_PREFIX + t + ':' + _hash(range + '|' + ex);
}
function _evictOldest() {
try {
const entries = [];
for (let i = 0; i < sessionStorage.length; i++) {
const k = sessionStorage.key(i);
if (k && k.startsWith(SESSION_PREFIX)) {
const raw = _safeGet(sessionStorage, k);
if (!raw) continue;
try {
const parsed = JSON.parse(raw);
entries.push({ k, ts: parsed && parsed._ts ? parsed._ts : 0 });
} catch (_) { _safeRemove(sessionStorage, k); }
}
}
if (entries.length <= SESSION_MAX_ENTRIES) return;
entries.sort((a, b) => a.ts - b.ts);
const toRemove = entries.slice(0, entries.length - SESSION_MAX_ENTRIES);
toRemove.forEach((e) => _safeRemove(sessionStorage, e.k));
} catch (_) { /* ignore */ }
}
const sessionCache = {
/* Read a cached payload. Returns `null` on miss / expiry / parse error. */
get(ticker, startDate, endDate, extra) {
const key = buildKey(ticker, startDate, endDate, extra);
const raw = _safeGet(sessionStorage, key);
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
if (parsed._ts && (Date.now() - parsed._ts) > SESSION_DEFAULT_TTL_MS) {
_safeRemove(sessionStorage, key);
return null;
}
return parsed.data;
} catch (_) {
_safeRemove(sessionStorage, key);
return null;
}
},
/* Persist a payload. Silent on quota errors; oldest entries are
evicted first so the cache never grows beyond `SESSION_MAX_ENTRIES`. */
set(ticker, startDate, endDate, extra, data) {
const key = buildKey(ticker, startDate, endDate, extra);
const payload = JSON.stringify({ _ts: Date.now(), data });
if (!_safeSet(sessionStorage, key, payload)) {
_evictOldest();
_safeSet(sessionStorage, key, payload);
}
_evictOldest();
},
clear() {
try {
const toDelete = [];
for (let i = 0; i < sessionStorage.length; i++) {
const k = sessionStorage.key(i);
if (k && k.startsWith(SESSION_PREFIX)) toDelete.push(k);
}
toDelete.forEach((k) => _safeRemove(sessionStorage, k));
} catch (_) { /* ignore */ }
},
// Exposed for tests / debugging.
_buildKey: buildKey,
};
_ensureSchema();
root.appCache = {
SCHEMA_VERSION: CACHE_SCHEMA_VERSION,
session: sessionCache,
};
})(window);