@escapenavigator/hooks 2.2.6 → 2.2.8

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.
@@ -57,6 +57,7 @@ const reduced_price_1 = require("@escapenavigator/utils/dist/reduced-price");
57
57
  const react_1 = __importStar(require("react"));
58
58
  const react_i18next_1 = require("react-i18next");
59
59
  const use_players_and_variation_picker_1 = require("../use-players-and-variation-picker");
60
+ const sync_composition_1 = require("./sync-composition");
60
61
  const ticket_row_1 = require("./ticket-row");
61
62
  /**
62
63
  * Единый пикер «вариация + состав», который используют админские модалки
@@ -163,20 +164,32 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
163
164
  * полях льготные места — одно число).
164
165
  */
165
166
  const emittedLegacy = (0, react_1.useRef)(null);
167
+ /** Состав последнего клика, пока хост не вернул его в пропсах. */
168
+ const pendingComposition = (0, react_1.useRef)(null);
166
169
  /**
167
170
  * Синхронизация сверху вниз. Состав по категориям приоритетнее старых
168
- * полей: их читаем, только пока хост на него не перешёл.
171
+ * полей: их читаем, только пока хост на него не перешёл. Свежий клик
172
+ * не затираем предыдущим пропом — иначе цена остаётся на прошлом числе.
169
173
  */
170
174
  (0, react_1.useEffect)(() => {
171
- if (compositionProp && Object.keys(compositionProp).length) {
172
- setComposition(compositionProp);
175
+ const decision = (0, sync_composition_1.decideCompositionSync)({
176
+ compositionProp,
177
+ pending: pendingComposition.current,
178
+ emittedLegacy: emittedLegacy.current,
179
+ players,
180
+ child,
181
+ reducedPlayers,
182
+ });
183
+ if (decision.action === 'keep') {
184
+ if (decision.clearPending)
185
+ pendingComposition.current = null;
173
186
  return;
174
187
  }
175
- const echo = emittedLegacy.current;
176
- if (echo &&
177
- echo.players === players &&
178
- echo.children === child &&
179
- echo.reducedPlayers === reducedPlayers) {
188
+ if (decision.action === 'adopt') {
189
+ if (decision.clearPending)
190
+ pendingComposition.current = null;
191
+ if (compositionProp)
192
+ setComposition(compositionProp);
180
193
  return;
181
194
  }
182
195
  setComposition((0, composition_price_1.compositionFromLegacy)({ players, children: child, reducedPlayers }, categories));
@@ -200,6 +213,7 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
200
213
  const baseCategory = categories.find((category) => category.mode === tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE);
201
214
  const baseCategoryKey = baseCategory === null || baseCategory === void 0 ? void 0 : baseCategory.key;
202
215
  const applyComposition = (next) => {
216
+ pendingComposition.current = next;
203
217
  setComposition(next);
204
218
  const legacy = (0, composition_price_1.legacyCountsFromComposition)(next, categories);
205
219
  emittedLegacy.current = legacy;
@@ -0,0 +1,35 @@
1
+ import { OrderComposition } from '@escapenavigator/types/dist/tariff/tariff-category';
2
+ type LegacyCounts = {
3
+ players: number;
4
+ children: number;
5
+ reducedPlayers: number;
6
+ };
7
+ /**
8
+ * Что делать с составом, который хост вернул пропсами, пока пикер уже
9
+ * показал свой клик.
10
+ *
11
+ * Цена рисуется из локального состава. Если эффект синхронизации
12
+ * подставляет предыдущий `composition` (хост уже записал `players`, а
13
+ * состав ещё старый — или записал оба, но эффект успел раньше), сумма
14
+ * остаётся на прошлом числе до размонтирования шага.
15
+ */
16
+ export type CompositionSyncDecision = {
17
+ action: 'adopt';
18
+ clearPending: boolean;
19
+ } | {
20
+ action: 'keep';
21
+ clearPending: boolean;
22
+ } | {
23
+ action: 'rebuild';
24
+ };
25
+ export declare const sameCompositionCounts: (left?: OrderComposition | null, right?: OrderComposition | null) => boolean;
26
+ export declare const decideCompositionSync: ({ compositionProp, pending, emittedLegacy, players, child, reducedPlayers, }: {
27
+ compositionProp?: OrderComposition | null;
28
+ /** Состав, который пикер только что поставил кликом и ещё не увидел в пропсах. */
29
+ pending: OrderComposition | null;
30
+ emittedLegacy: LegacyCounts | null;
31
+ players: number;
32
+ child: number;
33
+ reducedPlayers: number;
34
+ }) => CompositionSyncDecision;
35
+ export {};
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideCompositionSync = exports.sameCompositionCounts = void 0;
4
+ const hasComposition = (composition) => !!composition && Object.keys(composition).length > 0;
5
+ const sameCompositionCounts = (left, right) => {
6
+ const keys = new Set([...Object.keys(left || {}), ...Object.keys(right || {})]);
7
+ for (const key of keys) {
8
+ if (((left === null || left === void 0 ? void 0 : left[key]) || 0) !== ((right === null || right === void 0 ? void 0 : right[key]) || 0))
9
+ return false;
10
+ }
11
+ return true;
12
+ };
13
+ exports.sameCompositionCounts = sameCompositionCounts;
14
+ const legacyCaughtUp = (emitted, players, child, reducedPlayers) => !!emitted &&
15
+ emitted.players === players &&
16
+ emitted.children === child &&
17
+ emitted.reducedPlayers === reducedPlayers;
18
+ const decideCompositionSync = ({ compositionProp, pending, emittedLegacy, players, child, reducedPlayers, }) => {
19
+ const caughtUp = legacyCaughtUp(emittedLegacy, players, child, reducedPlayers);
20
+ if (pending) {
21
+ // Хост вернул ровно то, что ушло наверх — можно отпустить локальную копию.
22
+ if (hasComposition(compositionProp) && (0, exports.sameCompositionCounts)(compositionProp, pending)) {
23
+ return { action: 'adopt', clearPending: true };
24
+ }
25
+ // Легаси-хост состава не хранит, но число игроков уже наше.
26
+ // Локальный состав богаче денорма, пересобирать его нельзя.
27
+ if (!hasComposition(compositionProp) && caughtUp) {
28
+ return { action: 'keep', clearPending: true };
29
+ }
30
+ // Проп ещё предыдущий. Не откатывать цену и не отпускать клик,
31
+ // иначе следующий прогон подставит старый состав.
32
+ return { action: 'keep', clearPending: false };
33
+ }
34
+ if (hasComposition(compositionProp))
35
+ return { action: 'adopt', clearPending: false };
36
+ // Эхо собственного денорма: несколько льготных категорий схлопнулись бы в одну.
37
+ if (caughtUp)
38
+ return { action: 'keep', clearPending: false };
39
+ return { action: 'rebuild' };
40
+ };
41
+ exports.decideCompositionSync = decideCompositionSync;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const sync_composition_1 = require("./sync-composition");
4
+ const adult = (count) => ({ adult: count });
5
+ describe('decideCompositionSync', () => {
6
+ it('принимает состав хоста, пока своего клика нет', () => {
7
+ expect((0, sync_composition_1.decideCompositionSync)({
8
+ compositionProp: adult(5),
9
+ pending: null,
10
+ emittedLegacy: null,
11
+ players: 5,
12
+ child: 0,
13
+ reducedPlayers: 0,
14
+ })).toEqual({ action: 'adopt', clearPending: false });
15
+ });
16
+ it('не откатывает клик, если в пропсах ещё прошлый состав', () => {
17
+ expect((0, sync_composition_1.decideCompositionSync)({
18
+ compositionProp: adult(5),
19
+ pending: adult(4),
20
+ emittedLegacy: { players: 4, children: 0, reducedPlayers: 0 },
21
+ players: 5,
22
+ child: 0,
23
+ reducedPlayers: 0,
24
+ })).toEqual({ action: 'keep', clearPending: false });
25
+ });
26
+ it('не откатывает клик, даже если players уже новый, а composition старый', () => {
27
+ expect((0, sync_composition_1.decideCompositionSync)({
28
+ compositionProp: adult(5),
29
+ pending: adult(4),
30
+ emittedLegacy: { players: 4, children: 0, reducedPlayers: 0 },
31
+ players: 4,
32
+ child: 0,
33
+ reducedPlayers: 0,
34
+ })).toEqual({ action: 'keep', clearPending: false });
35
+ });
36
+ it('отпускает локальный состав, когда хост вернул тот же набор', () => {
37
+ expect((0, sync_composition_1.decideCompositionSync)({
38
+ compositionProp: { adult: 4, student: 0 },
39
+ pending: adult(4),
40
+ emittedLegacy: { players: 4, children: 0, reducedPlayers: 0 },
41
+ players: 4,
42
+ child: 0,
43
+ reducedPlayers: 0,
44
+ })).toEqual({ action: 'adopt', clearPending: true });
45
+ });
46
+ it('не пересобирает состав из денорма, пока хост не догнал клик', () => {
47
+ expect((0, sync_composition_1.decideCompositionSync)({
48
+ compositionProp: undefined,
49
+ pending: { adult: 2, student: 2 },
50
+ emittedLegacy: { players: 4, children: 0, reducedPlayers: 2 },
51
+ players: 2,
52
+ child: 0,
53
+ reducedPlayers: 0,
54
+ })).toEqual({ action: 'keep', clearPending: false });
55
+ });
56
+ it('отпускает клик у легаси-хоста, когда число игроков уже совпало', () => {
57
+ expect((0, sync_composition_1.decideCompositionSync)({
58
+ compositionProp: undefined,
59
+ pending: { adult: 2, student: 2 },
60
+ emittedLegacy: { players: 4, children: 0, reducedPlayers: 2 },
61
+ players: 4,
62
+ child: 0,
63
+ reducedPlayers: 2,
64
+ })).toEqual({ action: 'keep', clearPending: true });
65
+ });
66
+ it('не схлопывает льготные категории, когда денорм — эхо нашего ответа', () => {
67
+ expect((0, sync_composition_1.decideCompositionSync)({
68
+ compositionProp: undefined,
69
+ pending: null,
70
+ emittedLegacy: { players: 4, children: 0, reducedPlayers: 2 },
71
+ players: 4,
72
+ child: 0,
73
+ reducedPlayers: 2,
74
+ })).toEqual({ action: 'keep', clearPending: false });
75
+ });
76
+ it('пересобирает состав, если число игроков пришло снаружи', () => {
77
+ expect((0, sync_composition_1.decideCompositionSync)({
78
+ compositionProp: undefined,
79
+ pending: null,
80
+ emittedLegacy: { players: 4, children: 0, reducedPlayers: 0 },
81
+ players: 6,
82
+ child: 1,
83
+ reducedPlayers: 0,
84
+ })).toEqual({ action: 'rebuild' });
85
+ });
86
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/hooks",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
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": "^2.2.6",
18
- "@escapenavigator/types": "^2.2.5",
19
- "@escapenavigator/utils": "^2.2.5",
17
+ "@escapenavigator/services": "^2.2.8",
18
+ "@escapenavigator/types": "^2.2.7",
19
+ "@escapenavigator/utils": "^2.2.7",
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": "2a9e5dcd5dd44298d96d7dfa918d4bd86b611979"
35
+ "gitHead": "821f81560e48bb5554575ada7f5f8b0d7597fdcb"
36
36
  }