@sequent-org/moodboard 1.4.65 → 1.4.67
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
CHANGED
|
@@ -41,7 +41,9 @@ export class HistoryManager {
|
|
|
41
41
|
lastCommand.mergeWith(command);
|
|
42
42
|
this._executeCommandSafely(lastCommand);
|
|
43
43
|
this.eventBus.emit('history:changed', {
|
|
44
|
-
historySize: this.history.length
|
|
44
|
+
historySize: this.history.length,
|
|
45
|
+
canUndo: this.currentIndex >= 0,
|
|
46
|
+
canRedo: this.currentIndex < this.history.length - 1,
|
|
45
47
|
});
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
@@ -67,7 +69,9 @@ export class HistoryManager {
|
|
|
67
69
|
// Уведомляем об изменении истории
|
|
68
70
|
this.eventBus.emit(Events.History.Changed, {
|
|
69
71
|
historySize: this.history.length,
|
|
70
|
-
currentCommand: command.toString()
|
|
72
|
+
currentCommand: command.toString(),
|
|
73
|
+
canUndo: this.currentIndex >= 0,
|
|
74
|
+
canRedo: this.currentIndex < this.history.length - 1,
|
|
71
75
|
});
|
|
72
76
|
|
|
73
77
|
|
|
@@ -107,7 +111,9 @@ export class HistoryManager {
|
|
|
107
111
|
this.currentIndex = -1;
|
|
108
112
|
|
|
109
113
|
this.eventBus.emit(Events.History.Changed, {
|
|
110
|
-
historySize: 0
|
|
114
|
+
historySize: 0,
|
|
115
|
+
canUndo: false,
|
|
116
|
+
canRedo: false,
|
|
111
117
|
});
|
|
112
118
|
|
|
113
119
|
|
package/src/core/SaveManager.js
CHANGED
|
@@ -26,6 +26,10 @@ export class SaveManager {
|
|
|
26
26
|
// Если изменение пришло, пока предыдущий запрос ещё в полёте, помечаем
|
|
27
27
|
// необходимость дозаписи: иначе последняя правка перед выходом теряется.
|
|
28
28
|
this._pendingResave = false;
|
|
29
|
+
// Счётчик последовательных rebase после 409: ограничивает цикл
|
|
30
|
+
// «reload → reapply → resave → снова 409», если сервер уходит вперёд
|
|
31
|
+
// в каждом раунде. Сбрасывается при успешном сохранении.
|
|
32
|
+
this._staleRebaseCount = 0;
|
|
29
33
|
this._listenersAttached = false;
|
|
30
34
|
this._handlers = {};
|
|
31
35
|
|
|
@@ -134,9 +138,13 @@ export class SaveManager {
|
|
|
134
138
|
}
|
|
135
139
|
this.isRequestInProgress = true;
|
|
136
140
|
|
|
141
|
+
// Доступно и в catch: на 409 нужно переприменить именно эти локальные данные.
|
|
142
|
+
let pendingSaveData = null;
|
|
143
|
+
|
|
137
144
|
try {
|
|
138
145
|
// Получаем данные для сохранения
|
|
139
146
|
const saveData = data || await this.getBoardData();
|
|
147
|
+
pendingSaveData = saveData;
|
|
140
148
|
|
|
141
149
|
// Защита: если данные не получены (например, при уничтожении/отписке обработчиков) — выходим без ошибки
|
|
142
150
|
if (!saveData || typeof saveData !== 'object') {
|
|
@@ -194,6 +202,7 @@ export class SaveManager {
|
|
|
194
202
|
this.lastSavedData = JSON.parse(JSON.stringify(saveData));
|
|
195
203
|
this.hasUnsavedChanges = false;
|
|
196
204
|
this.retryCount = 0;
|
|
205
|
+
this._staleRebaseCount = 0;
|
|
197
206
|
this.updateSaveStatus('saved');
|
|
198
207
|
|
|
199
208
|
// Эмитируем событие успешного сохранения
|
|
@@ -214,26 +223,49 @@ export class SaveManager {
|
|
|
214
223
|
return;
|
|
215
224
|
}
|
|
216
225
|
|
|
217
|
-
// 409 stale_base_version:
|
|
218
|
-
//
|
|
226
|
+
// 409 stale_base_version: локальный baseVersion отстал от сервера
|
|
227
|
+
// (типично — гонка «финальное сохранение при закрытии ещё не доехало,
|
|
228
|
+
// а переоткрытие уже загрузило предыдущую версию»). Перезаписывать
|
|
229
|
+
// сервер целиком нельзя — затрём чужие правки. Поэтому:
|
|
230
|
+
// 1) перечитываем latest (reloadHandler выставляет актуальный baseVersion),
|
|
231
|
+
// 2) переприменяем поверх локальные НОВЫЕ объекты (например, только что
|
|
232
|
+
// нарисованный штрих) — reloadHandler возвращает их число,
|
|
233
|
+
// 3) пересохраняем с актуальной версией.
|
|
234
|
+
// Так рисунок не теряется, а аддитивный merge не удаляет серверные данные.
|
|
219
235
|
if (error?.code === 'stale_base_version') {
|
|
220
236
|
console.warn(
|
|
221
237
|
'SaveManager: 409 stale_base_version — версия доски устарела. Перечитываю latest с сервера...',
|
|
222
238
|
error.message
|
|
223
239
|
);
|
|
224
240
|
if (typeof this._reloadHandler === 'function') {
|
|
241
|
+
this._staleRebaseCount++;
|
|
225
242
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
243
|
+
const result = (this._staleRebaseCount <= this.options.maxRetries)
|
|
244
|
+
? await this._reloadHandler({
|
|
245
|
+
pendingData: pendingSaveData,
|
|
246
|
+
currentVersion: error.currentVersion ?? null,
|
|
247
|
+
})
|
|
248
|
+
: await this._reloadHandler();
|
|
249
|
+
// После перезагрузки сбрасываем кеш дедупа: пересохранение
|
|
250
|
+
// должно уйти на сервер с актуальной версией.
|
|
229
251
|
this.lastSavedData = null;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
|
|
253
|
+
const reapplied = Number(result?.reapplied) || 0;
|
|
254
|
+
if (reapplied > 0 && this._staleRebaseCount <= this.options.maxRetries) {
|
|
255
|
+
// Локальные новые объекты переприменены поверх свежего
|
|
256
|
+
// состояния — пересохраняем их с актуальным baseVersion.
|
|
257
|
+
this.hasUnsavedChanges = true;
|
|
258
|
+
this._pendingResave = true; // finally запустит saveImmediately()
|
|
259
|
+
this.updateSaveStatus('saving');
|
|
260
|
+
} else {
|
|
261
|
+
// Нечего переприменять (или превышен лимит rebase) —
|
|
262
|
+
// оставляем перечитанное серверное состояние как есть.
|
|
263
|
+
this.hasUnsavedChanges = false;
|
|
264
|
+
this.updateSaveStatus('saved');
|
|
265
|
+
if (this._staleRebaseCount > this.options.maxRetries) {
|
|
266
|
+
console.warn('SaveManager: превышен лимит rebase после 409, оставляю серверное состояние.');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
237
269
|
} catch (reloadErr) {
|
|
238
270
|
console.error('SaveManager: ошибка перечитывания после 409:', reloadErr);
|
|
239
271
|
this.updateSaveStatus('error', reloadErr.message);
|
package/src/core/index.js
CHANGED
|
@@ -230,6 +230,12 @@ export class CoreMoodBoard {
|
|
|
230
230
|
historySize: data.historySize,
|
|
231
231
|
});
|
|
232
232
|
}
|
|
233
|
+
if (data && ('canUndo' in data || 'canRedo' in data)) {
|
|
234
|
+
this.eventBus.emit(Events.UI.UpdateHistoryButtons, {
|
|
235
|
+
canUndo: !!data.canUndo,
|
|
236
|
+
canRedo: !!data.canRedo,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
233
239
|
});
|
|
234
240
|
}
|
|
235
241
|
|
|
@@ -111,6 +111,62 @@ export function bindTopbarEvents(board) {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Аддитивно переприменяет локальные объекты, которых нет в текущем (только что
|
|
116
|
+
* перечитанном с сервера) состоянии доски. Вызывается после 409
|
|
117
|
+
* stale_base_version, чтобы не потерять только что созданные локально объекты
|
|
118
|
+
* (например, нарисованный штрих). Серверные объекты не трогаем — это безопасный
|
|
119
|
+
* union по id, а не перезапись чужих данных.
|
|
120
|
+
* @returns {number} сколько объектов переприменено
|
|
121
|
+
*/
|
|
122
|
+
function reapplyLocalOnlyObjects(board, pendingData) {
|
|
123
|
+
const core = board?.coreMoodboard;
|
|
124
|
+
if (!core || typeof core.createObjectFromData !== 'function') {
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pendingObjects = Array.isArray(pendingData?.boardData?.objects)
|
|
129
|
+
? pendingData.boardData.objects
|
|
130
|
+
: Array.isArray(pendingData?.objects)
|
|
131
|
+
? pendingData.objects
|
|
132
|
+
: [];
|
|
133
|
+
if (pendingObjects.length === 0) {
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const currentObjects = Array.isArray(core.state?.state?.objects)
|
|
138
|
+
? core.state.state.objects
|
|
139
|
+
: [];
|
|
140
|
+
const currentIds = new Set(currentObjects.map((obj) => obj && obj.id).filter(Boolean));
|
|
141
|
+
|
|
142
|
+
let reapplied = 0;
|
|
143
|
+
for (const obj of pendingObjects) {
|
|
144
|
+
if (!obj || !obj.id || currentIds.has(obj.id)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
// Клонируем: createObjectFromData мутирует objectData (transform/properties).
|
|
149
|
+
const created = core.createObjectFromData(JSON.parse(JSON.stringify(obj)));
|
|
150
|
+
if (created) {
|
|
151
|
+
reapplied++;
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.warn('⚠️ rebase после 409: не удалось переприменить объект', obj.id, error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (reapplied > 0) {
|
|
159
|
+
// PIXI-объекты (рисунок) перерисовываются сразу; для текстовых/заметок
|
|
160
|
+
// синхронизируем HTML-оверлеи, как это делает DataManager.loadData.
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
try { window.moodboardHtmlTextLayer?.rebuildFromState?.(); } catch (_) {}
|
|
163
|
+
try { window.moodboardHtmlTextLayer?.updateAll?.(); } catch (_) {}
|
|
164
|
+
}, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return reapplied;
|
|
168
|
+
}
|
|
169
|
+
|
|
114
170
|
export function bindSaveCallbacks(board) {
|
|
115
171
|
if (!board.coreMoodboard || !board.coreMoodboard.eventBus) {
|
|
116
172
|
return;
|
|
@@ -125,9 +181,15 @@ export function bindSaveCallbacks(board) {
|
|
|
125
181
|
return (v !== null && Number.isFinite(Number(v)) && Number(v) > 0) ? Number(v) : null;
|
|
126
182
|
});
|
|
127
183
|
|
|
128
|
-
// На 409 stale_base_version — перечитываем latest с
|
|
129
|
-
|
|
184
|
+
// На 409 stale_base_version — перечитываем latest с сервера (не затираем
|
|
185
|
+
// чужие данные) и аддитивно переприменяем локальные НОВЫЕ объекты, которых
|
|
186
|
+
// ещё нет в свежем состоянии (например, только что нарисованный штрих).
|
|
187
|
+
// Возвращаем число переприменённых объектов, чтобы SaveManager понял,
|
|
188
|
+
// нужно ли пересохранять с актуальной версией.
|
|
189
|
+
saveManager.setReloadHandler(async (context = {}) => {
|
|
130
190
|
await loadExistingBoard(board, null, { fallbackToSeedOnError: false });
|
|
191
|
+
const reapplied = reapplyLocalOnlyObjects(board, context?.pendingData);
|
|
192
|
+
return { reapplied };
|
|
131
193
|
});
|
|
132
194
|
}
|
|
133
195
|
|