@escapenavigator/hooks 1.10.167 → 1.10.169
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/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -22,3 +22,4 @@ __exportStar(require("./players-and-variation-picker"), exports);
|
|
|
22
22
|
__exportStar(require("./use-debounced-value"), exports);
|
|
23
23
|
__exportStar(require("./use-localstore-state"), exports);
|
|
24
24
|
__exportStar(require("./use-players-and-variation-picker"), exports);
|
|
25
|
+
__exportStar(require("./version-watcher"), exports);
|
|
@@ -126,8 +126,10 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
|
|
|
126
126
|
setChildren(value);
|
|
127
127
|
onChange === null || onChange === void 0 ? void 0 : onChange('children', value);
|
|
128
128
|
};
|
|
129
|
+
// Цена 0 за число игроков = «это кол-во недоступно для выбора». Такие
|
|
130
|
+
// ключи не рендерим кнопками и не учитываем в availablePlayerKeys/clamp.
|
|
129
131
|
const playersOptions = (0, react_1.useMemo)(() => Object.entries(tariff)
|
|
130
|
-
.filter(([key]) => key !== 'children' && !Number.isNaN(+key))
|
|
132
|
+
.filter(([key, price]) => key !== 'children' && !Number.isNaN(+key) && +price > 0)
|
|
131
133
|
.map(([key, price]) => ({
|
|
132
134
|
key: +key,
|
|
133
135
|
price,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Тихий вотчер версии фронта — проактивно переводит уже открытые вкладки на
|
|
3
|
+
* свежий релиз, чтобы они не «застревали» на старом index.html со ссылками на
|
|
4
|
+
* чанки, которые рано или поздно вычистят с сервера (источник битых чанков).
|
|
5
|
+
*
|
|
6
|
+
* Как работает:
|
|
7
|
+
* 1. На каждой prod-сборке Vite кладёт `version.json` ({version, builtAt}),
|
|
8
|
+
* version = REACT_APP_RELEASE (IMAGE_TAG из CI). См. vite.shared.ts.
|
|
9
|
+
* 2. Вотчер проверяет `version.json` (fetch с `cache: no-store`) НЕ по таймеру,
|
|
10
|
+
* а только в естественные моменты, без фонового полла:
|
|
11
|
+
* - при возврате фокуса/видимости вкладки (`visibilitychange`);
|
|
12
|
+
* - при навигации между страницами (route change);
|
|
13
|
+
* - один раз вскоре после старта.
|
|
14
|
+
* Сравнивает серверную версию с версией, с которой собран ТЕКУЩИЙ бандл.
|
|
15
|
+
* 3. Если на сервере версия отличается — НЕ дёргаем юзера сразу (он может
|
|
16
|
+
* быть в середине заполнения формы), а ставим флаг «pending» и ждём
|
|
17
|
+
* следующей навигации. На route change делаем `location.reload()` — в этот
|
|
18
|
+
* момент потеря состояния безопасна, а no-cache index.html отдаёт свежие
|
|
19
|
+
* ссылки на чанки.
|
|
20
|
+
*
|
|
21
|
+
* Почему без таймера и «на навигации»:
|
|
22
|
+
* - не дёргаем сервер фоновым поллом у простаивающих вкладок;
|
|
23
|
+
* - точка перехода между страницами — естественный разрыв, reload бесшовен;
|
|
24
|
+
* - старые чанки на сервере мы НЕ удаляем (retention в CI), поэтому до
|
|
25
|
+
* перезагрузки вкладка полностью рабочая.
|
|
26
|
+
*
|
|
27
|
+
* Анти-цикл: если после reload version.json всё ещё рапортует новую версию
|
|
28
|
+
* (рассинхрон CDN / index.html отстаёт), повторно не перезагружаемся для той
|
|
29
|
+
* же целевой версии в течение cooldown — иначе вкладка зациклится в reload.
|
|
30
|
+
*
|
|
31
|
+
* Framework-agnostic: навигацию ловим через патч history.pushState/replaceState
|
|
32
|
+
* + popstate, поэтому подключение — один вызов в index.tsx без привязки к
|
|
33
|
+
* конкретному роутеру (работает в app/auth/admin/orders).
|
|
34
|
+
*/
|
|
35
|
+
export type VersionWatcherOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* Версия, с которой собран текущий бандл (`process.env.REACT_APP_RELEASE`).
|
|
38
|
+
* Если пусто (локалка / релиз не проставлен) — вотчер не запускается.
|
|
39
|
+
*/
|
|
40
|
+
currentVersion: string | undefined;
|
|
41
|
+
/** URL манифеста версии. По умолчанию `/version.json`. */
|
|
42
|
+
versionUrl?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Колбэк в момент, когда обнаружена новая версия (до перезагрузки).
|
|
45
|
+
* Удобно для аналитики / тоста «доступно обновление».
|
|
46
|
+
*/
|
|
47
|
+
onUpdateDetected?: (remoteVersion: string) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Срабатывает, если reload не помог (после перезагрузки сервер всё ещё
|
|
50
|
+
* отдаёт другую версию) — маркер реально сломанного/рассинхронного деплоя.
|
|
51
|
+
* Прокидывайте сюда отправку в Sentry.
|
|
52
|
+
*/
|
|
53
|
+
onReloadLoopDetected?: (info: {
|
|
54
|
+
currentVersion: string;
|
|
55
|
+
remoteVersion: string;
|
|
56
|
+
}) => void;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Запускает фоновый вотчер версии. Идемпотентен (повторные вызовы — no-op).
|
|
60
|
+
* Возвращает функцию остановки (на случай unmount в тестах/SSR).
|
|
61
|
+
*/
|
|
62
|
+
export declare function registerVersionWatcher(opts: VersionWatcherOptions): () => void;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Тихий вотчер версии фронта — проактивно переводит уже открытые вкладки на
|
|
4
|
+
* свежий релиз, чтобы они не «застревали» на старом index.html со ссылками на
|
|
5
|
+
* чанки, которые рано или поздно вычистят с сервера (источник битых чанков).
|
|
6
|
+
*
|
|
7
|
+
* Как работает:
|
|
8
|
+
* 1. На каждой prod-сборке Vite кладёт `version.json` ({version, builtAt}),
|
|
9
|
+
* version = REACT_APP_RELEASE (IMAGE_TAG из CI). См. vite.shared.ts.
|
|
10
|
+
* 2. Вотчер проверяет `version.json` (fetch с `cache: no-store`) НЕ по таймеру,
|
|
11
|
+
* а только в естественные моменты, без фонового полла:
|
|
12
|
+
* - при возврате фокуса/видимости вкладки (`visibilitychange`);
|
|
13
|
+
* - при навигации между страницами (route change);
|
|
14
|
+
* - один раз вскоре после старта.
|
|
15
|
+
* Сравнивает серверную версию с версией, с которой собран ТЕКУЩИЙ бандл.
|
|
16
|
+
* 3. Если на сервере версия отличается — НЕ дёргаем юзера сразу (он может
|
|
17
|
+
* быть в середине заполнения формы), а ставим флаг «pending» и ждём
|
|
18
|
+
* следующей навигации. На route change делаем `location.reload()` — в этот
|
|
19
|
+
* момент потеря состояния безопасна, а no-cache index.html отдаёт свежие
|
|
20
|
+
* ссылки на чанки.
|
|
21
|
+
*
|
|
22
|
+
* Почему без таймера и «на навигации»:
|
|
23
|
+
* - не дёргаем сервер фоновым поллом у простаивающих вкладок;
|
|
24
|
+
* - точка перехода между страницами — естественный разрыв, reload бесшовен;
|
|
25
|
+
* - старые чанки на сервере мы НЕ удаляем (retention в CI), поэтому до
|
|
26
|
+
* перезагрузки вкладка полностью рабочая.
|
|
27
|
+
*
|
|
28
|
+
* Анти-цикл: если после reload version.json всё ещё рапортует новую версию
|
|
29
|
+
* (рассинхрон CDN / index.html отстаёт), повторно не перезагружаемся для той
|
|
30
|
+
* же целевой версии в течение cooldown — иначе вкладка зациклится в reload.
|
|
31
|
+
*
|
|
32
|
+
* Framework-agnostic: навигацию ловим через патч history.pushState/replaceState
|
|
33
|
+
* + popstate, поэтому подключение — один вызов в index.tsx без привязки к
|
|
34
|
+
* конкретному роутеру (работает в app/auth/admin/orders).
|
|
35
|
+
*/
|
|
36
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
37
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
38
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
39
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
40
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
41
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
42
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.registerVersionWatcher = registerVersionWatcher;
|
|
47
|
+
const DEFAULT_VERSION_URL = '/version.json';
|
|
48
|
+
/** Через сколько после старта сделать первую проверку (даём приложению смонтироваться). */
|
|
49
|
+
const INITIAL_CHECK_DELAY_MS = 10000;
|
|
50
|
+
/** Не перезагружаемся повторно для той же целевой версии чаще, чем раз в N. */
|
|
51
|
+
const RELOAD_COOLDOWN_MS = 60 * 1000;
|
|
52
|
+
const RELOAD_FLAG_KEY = 'en_version_reload';
|
|
53
|
+
/** Кастомное событие SPA-навигации (диспатчится из патча history). */
|
|
54
|
+
const LOCATION_CHANGE_EVENT = 'en:locationchange';
|
|
55
|
+
let historyPatched = false;
|
|
56
|
+
/**
|
|
57
|
+
* Патчим history.pushState/replaceState один раз на всё приложение, чтобы
|
|
58
|
+
* SPA-навигация (react-router и т.п.) порождала событие, которое мы можем
|
|
59
|
+
* слушать. popstate (back/forward) ловится отдельно стандартным слушателем.
|
|
60
|
+
*/
|
|
61
|
+
function ensureHistoryPatched() {
|
|
62
|
+
if (historyPatched || typeof window === 'undefined')
|
|
63
|
+
return;
|
|
64
|
+
historyPatched = true;
|
|
65
|
+
const dispatch = () => window.dispatchEvent(new Event(LOCATION_CHANGE_EVENT));
|
|
66
|
+
const wrap = (method) => {
|
|
67
|
+
const original = window.history[method];
|
|
68
|
+
window.history[method] = function patched(...args) {
|
|
69
|
+
const result = original.apply(this, args);
|
|
70
|
+
dispatch();
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
wrap('pushState');
|
|
75
|
+
wrap('replaceState');
|
|
76
|
+
}
|
|
77
|
+
function readReloadState() {
|
|
78
|
+
try {
|
|
79
|
+
const raw = window.sessionStorage.getItem(RELOAD_FLAG_KEY);
|
|
80
|
+
return raw ? JSON.parse(raw) : null;
|
|
81
|
+
}
|
|
82
|
+
catch (_a) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function writeReloadState(state) {
|
|
87
|
+
try {
|
|
88
|
+
window.sessionStorage.setItem(RELOAD_FLAG_KEY, JSON.stringify(state));
|
|
89
|
+
}
|
|
90
|
+
catch (_a) {
|
|
91
|
+
/* приватный режим / storage отключён — переживём без анти-цикла */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Уже зарегистрировали вотчер в этой жизни страницы — повторно не нужно. */
|
|
95
|
+
let registered = false;
|
|
96
|
+
/**
|
|
97
|
+
* Запускает фоновый вотчер версии. Идемпотентен (повторные вызовы — no-op).
|
|
98
|
+
* Возвращает функцию остановки (на случай unmount в тестах/SSR).
|
|
99
|
+
*/
|
|
100
|
+
function registerVersionWatcher(opts) {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
const noop = () => { };
|
|
103
|
+
if (typeof window === 'undefined')
|
|
104
|
+
return noop;
|
|
105
|
+
const currentVersion = ((_a = opts.currentVersion) !== null && _a !== void 0 ? _a : '').trim();
|
|
106
|
+
// Нет версии (локалка / релиз не проставлен) — нечего сравнивать.
|
|
107
|
+
if (!currentVersion)
|
|
108
|
+
return noop;
|
|
109
|
+
if (registered)
|
|
110
|
+
return noop;
|
|
111
|
+
registered = true;
|
|
112
|
+
const versionUrl = (_b = opts.versionUrl) !== null && _b !== void 0 ? _b : DEFAULT_VERSION_URL;
|
|
113
|
+
let updatePending = false;
|
|
114
|
+
let pendingVersion = '';
|
|
115
|
+
let stopped = false;
|
|
116
|
+
const reloadForUpdate = (remoteVersion) => {
|
|
117
|
+
var _a;
|
|
118
|
+
const last = readReloadState();
|
|
119
|
+
// Уже перезагружались под эту версию недавно, а сервер всё ещё отдаёт
|
|
120
|
+
// её же → reload не помогает (рассинхрон index.html/CDN). Не зацикливаемся.
|
|
121
|
+
if (last && last.to === remoteVersion && Date.now() - last.at < RELOAD_COOLDOWN_MS) {
|
|
122
|
+
(_a = opts.onReloadLoopDetected) === null || _a === void 0 ? void 0 : _a.call(opts, { currentVersion, remoteVersion });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
writeReloadState({ to: remoteVersion, at: Date.now() });
|
|
126
|
+
window.location.reload();
|
|
127
|
+
};
|
|
128
|
+
const markPending = (remoteVersion) => {
|
|
129
|
+
var _a;
|
|
130
|
+
if (updatePending)
|
|
131
|
+
return;
|
|
132
|
+
updatePending = true;
|
|
133
|
+
pendingVersion = remoteVersion;
|
|
134
|
+
(_a = opts.onUpdateDetected) === null || _a === void 0 ? void 0 : _a.call(opts, remoteVersion);
|
|
135
|
+
};
|
|
136
|
+
const check = () => __awaiter(this, void 0, void 0, function* () {
|
|
137
|
+
if (stopped || updatePending)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
const res = yield fetch(`${versionUrl}?_=${Date.now()}`, {
|
|
141
|
+
cache: 'no-store',
|
|
142
|
+
credentials: 'same-origin',
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok)
|
|
145
|
+
return;
|
|
146
|
+
const data = (yield res.json());
|
|
147
|
+
const remoteVersion = typeof data.version === 'string' ? data.version.trim() : '';
|
|
148
|
+
if (remoteVersion && remoteVersion !== currentVersion) {
|
|
149
|
+
markPending(remoteVersion);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (_a) {
|
|
153
|
+
// Сеть/парсинг упали — молча ждём следующего повода проверить.
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
// Навигация: если апдейт уже найден — перезагружаемся (безопасная точка);
|
|
157
|
+
// иначе пользуемся моментом, чтобы проверить версию без фонового полла.
|
|
158
|
+
const onNavigate = () => {
|
|
159
|
+
if (stopped)
|
|
160
|
+
return;
|
|
161
|
+
if (updatePending) {
|
|
162
|
+
reloadForUpdate(pendingVersion);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
void check();
|
|
166
|
+
};
|
|
167
|
+
const onVisible = () => {
|
|
168
|
+
if (document.visibilityState === 'visible')
|
|
169
|
+
void check();
|
|
170
|
+
};
|
|
171
|
+
ensureHistoryPatched();
|
|
172
|
+
window.addEventListener(LOCATION_CHANGE_EVENT, onNavigate);
|
|
173
|
+
window.addEventListener('popstate', onNavigate);
|
|
174
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
175
|
+
const initialTimer = setTimeout(() => void check(), INITIAL_CHECK_DELAY_MS);
|
|
176
|
+
return () => {
|
|
177
|
+
stopped = true;
|
|
178
|
+
registered = false;
|
|
179
|
+
clearTimeout(initialTimer);
|
|
180
|
+
window.removeEventListener(LOCATION_CHANGE_EVENT, onNavigate);
|
|
181
|
+
window.removeEventListener('popstate', onNavigate);
|
|
182
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
183
|
+
};
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/hooks",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.169",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/services": "^1.10.
|
|
18
|
-
"@escapenavigator/types": "^1.10.
|
|
19
|
-
"@escapenavigator/utils": "^1.10.
|
|
17
|
+
"@escapenavigator/services": "^1.10.177",
|
|
18
|
+
"@escapenavigator/types": "^1.10.163",
|
|
19
|
+
"@escapenavigator/utils": "^1.10.170",
|
|
20
20
|
"libphonenumber-js": "^1.12.24",
|
|
21
21
|
"react": "19.2.6",
|
|
22
22
|
"react-phone-input-2": "^2.15.1"
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"ts-jest": "^29.1.1",
|
|
33
33
|
"typescript": "^5.6"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "ad28ed8f74f9a00d33dffd883d7f9ed26d596629"
|
|
36
36
|
}
|