@sequent-org/moodboard 1.4.61 → 1.4.62
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/package.json +1 -1
- package/src/core/SaveManager.js +106 -0
package/package.json
CHANGED
package/src/core/SaveManager.js
CHANGED
|
@@ -23,6 +23,9 @@ export class SaveManager {
|
|
|
23
23
|
this.retryCount = 0;
|
|
24
24
|
this.lastSavedData = null;
|
|
25
25
|
this.hasUnsavedChanges = false;
|
|
26
|
+
// Если изменение пришло, пока предыдущий запрос ещё в полёте, помечаем
|
|
27
|
+
// необходимость дозаписи: иначе последняя правка перед выходом теряется.
|
|
28
|
+
this._pendingResave = false;
|
|
26
29
|
this._listenersAttached = false;
|
|
27
30
|
this._handlers = {};
|
|
28
31
|
|
|
@@ -76,6 +79,25 @@ export class SaveManager {
|
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
this.eventBus.on(Events.History.Changed, this._handlers.onHistoryChanged);
|
|
82
|
+
|
|
83
|
+
// Flush при выходе: закрытие вкладки (pagehide) и сворачивание/скрытие
|
|
84
|
+
// (visibilitychange → hidden). Асинхронный fetch на unload не успевает,
|
|
85
|
+
// поэтому используем navigator.sendBeacon в _flushViaBeacon().
|
|
86
|
+
this._handlers.onPageHide = () => {
|
|
87
|
+
this._flushViaBeacon();
|
|
88
|
+
};
|
|
89
|
+
this._handlers.onVisibilityChange = () => {
|
|
90
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
|
|
91
|
+
this._flushViaBeacon();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
96
|
+
window.addEventListener('pagehide', this._handlers.onPageHide);
|
|
97
|
+
}
|
|
98
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
99
|
+
document.addEventListener('visibilitychange', this._handlers.onVisibilityChange);
|
|
100
|
+
}
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
/**
|
|
@@ -83,6 +105,12 @@ export class SaveManager {
|
|
|
83
105
|
*/
|
|
84
106
|
markAsChanged() {
|
|
85
107
|
this.hasUnsavedChanges = true;
|
|
108
|
+
// Если запрос уже выполняется, нельзя терять это изменение: помечаем
|
|
109
|
+
// дозапись, она запустится в finally текущего сохранения.
|
|
110
|
+
if (this.isRequestInProgress) {
|
|
111
|
+
this._pendingResave = true;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
86
114
|
this.saveImmediately();
|
|
87
115
|
}
|
|
88
116
|
|
|
@@ -215,6 +243,74 @@ export class SaveManager {
|
|
|
215
243
|
this.handleSaveError(error, data);
|
|
216
244
|
} finally {
|
|
217
245
|
this.isRequestInProgress = false;
|
|
246
|
+
// Изменение, пришедшее во время этого запроса, ещё не сохранено —
|
|
247
|
+
// запускаем повторное сохранение со свежими данными доски.
|
|
248
|
+
// Дедуп по lastSavedData отфильтрует случай "данные не менялись".
|
|
249
|
+
if (this._pendingResave) {
|
|
250
|
+
this._pendingResave = false;
|
|
251
|
+
this.saveImmediately();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Синхронно получает данные доски (для отправки через sendBeacon на выходе).
|
|
258
|
+
* Обработчик Save.GetBoardData заполняет requestData.data синхронно.
|
|
259
|
+
*/
|
|
260
|
+
_getBoardDataSync() {
|
|
261
|
+
const requestData = { data: null };
|
|
262
|
+
try {
|
|
263
|
+
this.eventBus.emit(Events.Save.GetBoardData, requestData);
|
|
264
|
+
} catch (_) {
|
|
265
|
+
// no-op
|
|
266
|
+
}
|
|
267
|
+
return requestData.data;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Финальное сохранение при выходе через navigator.sendBeacon.
|
|
272
|
+
* Гарантирует доставку последней правки при закрытии вкладки/модалки/панели,
|
|
273
|
+
* когда обычный async fetch не успевает завершиться. Эндпоинт публичный
|
|
274
|
+
* (middleware moodboard.cors, без CSRF), поэтому токен не требуется.
|
|
275
|
+
*/
|
|
276
|
+
_flushViaBeacon() {
|
|
277
|
+
try {
|
|
278
|
+
if (!this.hasUnsavedChanges) return;
|
|
279
|
+
if (typeof navigator === 'undefined' || typeof navigator.sendBeacon !== 'function') return;
|
|
280
|
+
|
|
281
|
+
const data = this._getBoardDataSync();
|
|
282
|
+
if (!data || typeof data !== 'object') return;
|
|
283
|
+
|
|
284
|
+
const boardId = data.id || 'default';
|
|
285
|
+
// Бэкенд и загрузка ожидают state в форме внутреннего boardData
|
|
286
|
+
// ({ name, objects, description }). core.getBoardData() оборачивает
|
|
287
|
+
// его в { id, settings, boardData }, а ApiClient.saveBoard разворачивает
|
|
288
|
+
// обратно перед отправкой — повторяем это здесь, иначе сохранится
|
|
289
|
+
// неверная форма и доска откроется пустой.
|
|
290
|
+
const stateForSave = (data.boardData && typeof data.boardData === 'object')
|
|
291
|
+
? data.boardData
|
|
292
|
+
: data;
|
|
293
|
+
const baseVersion = typeof this._versionGetter === 'function'
|
|
294
|
+
? this._versionGetter()
|
|
295
|
+
: null;
|
|
296
|
+
const validBaseVersion = (baseVersion !== null && Number.isFinite(Number(baseVersion)) && Number(baseVersion) > 0)
|
|
297
|
+
? Number(baseVersion)
|
|
298
|
+
: null;
|
|
299
|
+
|
|
300
|
+
const payload = {
|
|
301
|
+
moodboardId: boardId,
|
|
302
|
+
state: stateForSave,
|
|
303
|
+
actionType: 'command_execute',
|
|
304
|
+
...(validBaseVersion !== null ? { baseVersion: validBaseVersion } : {})
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
|
|
308
|
+
const queued = navigator.sendBeacon(this.options.saveEndpoint, blob);
|
|
309
|
+
if (queued) {
|
|
310
|
+
this.hasUnsavedChanges = false;
|
|
311
|
+
}
|
|
312
|
+
} catch (_) {
|
|
313
|
+
// Flush на выходе не должен бросать исключения.
|
|
218
314
|
}
|
|
219
315
|
}
|
|
220
316
|
|
|
@@ -499,6 +595,10 @@ export class SaveManager {
|
|
|
499
595
|
* Очистка ресурсов
|
|
500
596
|
*/
|
|
501
597
|
destroy() {
|
|
598
|
+
// Финальный flush перед снятием слушателей: покрывает закрытие модалки/панели,
|
|
599
|
+
// когда событие pagehide не срабатывает (SPA, без выгрузки документа).
|
|
600
|
+
this._flushViaBeacon();
|
|
601
|
+
|
|
502
602
|
if (this.saveTimer) {
|
|
503
603
|
clearTimeout(this.saveTimer);
|
|
504
604
|
this.saveTimer = null;
|
|
@@ -506,6 +606,12 @@ export class SaveManager {
|
|
|
506
606
|
|
|
507
607
|
// Удаляем обработчики событий, передавая исходные callback-ссылки.
|
|
508
608
|
if (this._handlers.onHistoryChanged) this.eventBus.off(Events.History.Changed, this._handlers.onHistoryChanged);
|
|
609
|
+
if (this._handlers.onPageHide && typeof window !== 'undefined' && typeof window.removeEventListener === 'function') {
|
|
610
|
+
window.removeEventListener('pagehide', this._handlers.onPageHide);
|
|
611
|
+
}
|
|
612
|
+
if (this._handlers.onVisibilityChange && typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
|
|
613
|
+
document.removeEventListener('visibilitychange', this._handlers.onVisibilityChange);
|
|
614
|
+
}
|
|
509
615
|
|
|
510
616
|
this._listenersAttached = false;
|
|
511
617
|
this._handlers = {};
|