@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
@@ -6,3 +6,4 @@ export * from './players-and-variation-picker';
6
6
  export * from './use-debounced-value';
7
7
  export * from './use-localstore-state';
8
8
  export * from './use-players-and-variation-picker';
9
+ export * from './version-watcher';
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.167",
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.175",
18
- "@escapenavigator/types": "^1.10.161",
19
- "@escapenavigator/utils": "^1.10.168",
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": "9e4821b3ad8e4b74479ab23a01b4189ebf9fe5b8"
35
+ "gitHead": "ad28ed8f74f9a00d33dffd883d7f9ed26d596629"
36
36
  }