-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlocation-loader.js
More file actions
180 lines (157 loc) · 6.33 KB
/
Copy pathlocation-loader.js
File metadata and controls
180 lines (157 loc) · 6.33 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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// ============================================================
// LocationLoader — lazy-load углублённых досье городов
// Role: Frontend Engineer + Data Scientist · v3.2
//
// Архитектура для масштаба 100k+ одновременных юзеров:
// • JSON-файлы вместо JS-глобалов (10× быстрее парсинг)
// • LRU-кеш на 5 локаций в памяти (~150 KB резидентно)
// • Дедупликация in-flight запросов (на повторный get() — один fetch)
// • prefetch() для warm-up без блокировки UI
// • CDN-кешируемые иммутабельные URL (cache: 'force-cache')
//
// Использование:
// const loc = await window.LocationLoader.get('kobuleti');
// // loc.overview, loc.rent, loc.legal, ...
//
// window.LocationLoader.prefetch('pomorie'); // фоновая загрузка
// window.LocationLoader.has('kobuleti'); // проверка наличия досье
// window.LocationLoader.list(); // список всех досье
// ============================================================
(function () {
'use strict';
const MAX_CACHED = 5;
const MANIFEST_PATH = 'src/data/locations/index.json';
// ---------- State ----------
let manifest = null;
let manifestPromise = null;
const cache = new Map(); // id → location data
const lru = []; // [most-recently-used, ..., least]
const inflight = new Map(); // id → Promise<location>
// ---------- Manifest loading ----------
function loadManifest() {
if (manifest) return Promise.resolve(manifest);
if (manifestPromise) return manifestPromise;
manifestPromise = fetch(MANIFEST_PATH, { cache: 'force-cache' })
.then(function (r) {
if (!r.ok) throw new Error('LocationLoader: manifest fetch failed ' + r.status);
return r.json();
})
.then(function (data) {
manifest = data;
return manifest;
})
.catch(function (err) {
// Не кешируем неудачный промис — даём шанс на ретрай
manifestPromise = null;
throw err;
});
return manifestPromise;
}
// ---------- LRU helpers ----------
function touch(id) {
const idx = lru.indexOf(id);
if (idx !== -1) lru.splice(idx, 1);
lru.unshift(id);
}
function evict() {
while (lru.length > MAX_CACHED) {
const dropId = lru.pop();
cache.delete(dropId);
}
}
// ---------- Public API ----------
const LocationLoader = {
// Список ID локаций, для которых есть углублённое досье
list: function () {
return loadManifest().then(function (m) {
return Object.keys(m.locations || {});
});
},
// Синхронная проверка из манифеста (после первого вызова list/get)
has: function (id) {
return !!(manifest && manifest.locations && manifest.locations[id]);
},
// Синхронный доступ к уже загруженной локации (или null)
peek: function (id) {
return cache.has(id) ? cache.get(id) : null;
},
// Метаданные из манифеста (size, version, updated)
meta: function (id) {
return loadManifest().then(function (m) {
return (m.locations && m.locations[id]) || null;
});
},
// Основной метод: получить локацию (lazy load, LRU cache)
get: function (id) {
if (!id) return Promise.reject(new Error('LocationLoader.get: id required'));
// Кеш-хит
if (cache.has(id)) {
touch(id);
return Promise.resolve(cache.get(id));
}
// Дедупликация: уже грузится — возвращаем тот же промис
if (inflight.has(id)) return inflight.get(id);
// Старт загрузки
const promise = loadManifest()
.then(function (m) {
const entry = m.locations && m.locations[id];
if (!entry) {
throw new Error('LocationLoader.get: unknown location "' + id + '"');
}
return fetch(entry.path, { cache: 'force-cache' });
})
.then(function (r) {
if (!r.ok) throw new Error('LocationLoader.get: fetch failed ' + r.status + ' for "' + id + '"');
return r.json();
})
.then(function (data) {
// Лёгкая валидация
if (!data || data.id !== id) {
throw new Error('LocationLoader.get: payload id mismatch for "' + id + '"');
}
if (typeof data.schemaVersion !== 'number') {
console.warn('LocationLoader: "' + id + '" missing schemaVersion');
}
cache.set(id, data);
touch(id);
evict();
inflight.delete(id);
return data;
})
.catch(function (err) {
inflight.delete(id);
throw err;
});
inflight.set(id, promise);
return promise;
},
// Фоновая загрузка (без ожидания, не блокирует)
prefetch: function (id) {
if (cache.has(id) || inflight.has(id)) return;
// Глотаем ошибки — это фоновая операция
this.get(id).catch(function () { /* silent */ });
},
// Освободить кеш (например, при смене профиля)
clear: function () {
cache.clear();
lru.length = 0;
inflight.clear();
},
// Диагностика — для DevTools
_debug: function () {
return {
manifestLoaded: !!manifest,
cachedIds: lru.slice(),
inflightIds: Array.from(inflight.keys()),
maxCached: MAX_CACHED
};
}
};
// Экспорт в window — единый паттерн проекта
window.LocationLoader = LocationLoader;
// Опциональный прелоад манифеста при загрузке скрипта (фоном, без блокировки)
// Это даёт LocationLoader.has() работать почти сразу после mount.
loadManifest().catch(function (err) {
console.warn('LocationLoader: initial manifest preload failed', err);
});
})();