@designcrowd/library.brand-page 5.18.1-undo-33 → 5.18.1-undo-34
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/lib/src/{CreatedOnBrandCrowd-de-de-986OpXvU.js → CreatedOnBrandCrowd-de-de-D-2WhY-F.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-CTZuZ366.js → CreatedOnBrandCrowd-en-us-B7ldKpJK.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CHrTgX9-.js → CreatedOnBrandCrowd-es-es-BatQY8Tt.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-xJAvxQ-x.js → CreatedOnBrandCrowd-fr-ca-CFivOJkN.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-xJAvxQ-x.js → CreatedOnBrandCrowd-fr-fr-CFivOJkN.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-lpRIGIcs.js → CreatedOnBrandCrowd-pt-br-wHkXfYLP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-lpRIGIcs.js → CreatedOnBrandCrowd-pt-pt-wHkXfYLP.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CZuim3ee.js → CreatedOnDcom-de-de--HwniV-Y.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-DG4I5f4s.js → CreatedOnDcom-en-us-Bqm5NnRX.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-Hpieh1wX.js → CreatedOnDcom-es-es-COGQIuEn.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-DWr5P-df.js → CreatedOnDcom-fr-ca-eVDHO7MX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-CHfOyS6V.js → CreatedOnDcom-fr-fr-BhGKbdoG.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-bQwCCDDI.js → CreatedOnDcom-pt-br-5kDZKPZc.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-bo-i_h0N.js → CreatedOnDcom-pt-pt-CnTcVxct.js} +1 -1
- package/lib/src/brand-page-maker/stores/history/history-store.js +47 -18
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-DNHBgZyy.js → lib-B5jbN-ro.js} +3878 -3875
- package/package.json +1 -1
- package/src/brand-page-maker/stores/history/history-store.test.ts +32 -0
- package/src/brand-page-maker/stores/history/history-store.ts +48 -18
package/package.json
CHANGED
|
@@ -357,6 +357,38 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
|
|
|
357
357
|
expect(store.currentIndex).toBe(2);
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
+
it('drops a stale merge arm if no mutation follows in the same tick', async () => {
|
|
361
|
+
// Repro for the "first pre-purchase logo edit not captured" bug:
|
|
362
|
+
// a hydration site (e.g. ContentSettings.mounted on tab switch)
|
|
363
|
+
// armed the flag, but the user navigated away before the flagged
|
|
364
|
+
// write happened. A LATER unrelated mutation must NOT inherit
|
|
365
|
+
// the stale arm and merge — it must commit normally.
|
|
366
|
+
const store = useHistoryStore();
|
|
367
|
+
|
|
368
|
+
await store.captureBaseline();
|
|
369
|
+
await elapseSettleWindow();
|
|
370
|
+
|
|
371
|
+
// Land a real entry first.
|
|
372
|
+
liveData.v = 100;
|
|
373
|
+
await flushWatcher();
|
|
374
|
+
|
|
375
|
+
expect(store.entries.length).toBe(2);
|
|
376
|
+
|
|
377
|
+
// Hydration site arms the flag, but no mutation follows in the
|
|
378
|
+
// same tick — caller navigated away.
|
|
379
|
+
store.mergeNextMutation();
|
|
380
|
+
// Advance past the macrotask cleanup, no mutation in between.
|
|
381
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
382
|
+
|
|
383
|
+
// Some time later, a real edit happens.
|
|
384
|
+
liveData.unrelated = 'edit';
|
|
385
|
+
await flushWatcher();
|
|
386
|
+
|
|
387
|
+
// Must push, not merge.
|
|
388
|
+
expect(store.entries.length).toBe(3);
|
|
389
|
+
expect(store.currentIndex).toBe(2);
|
|
390
|
+
});
|
|
391
|
+
|
|
360
392
|
it('clears any armed merge flag during snapshot apply (undo/redo)', async () => {
|
|
361
393
|
const store = useHistoryStore();
|
|
362
394
|
|
|
@@ -153,13 +153,19 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
156
|
+
// Two-phase merge flag. Hydration write sites call `mergeNextMutation()`
|
|
157
|
+
// immediately before their imperative write, which sets `mergeArmed`.
|
|
158
|
+
// When the watcher's `scheduleAutoCapture` arms a new idle timer, it
|
|
159
|
+
// transfers `mergeArmed` into `pendingTimerIsMerge`. The flush then
|
|
160
|
+
// consumes `pendingTimerIsMerge` and merges into the current head
|
|
161
|
+
// instead of pushing.
|
|
162
|
+
//
|
|
163
|
+
// The microtask cleanup on `mergeArmed` prevents stale flags surviving
|
|
164
|
+
// when the user navigates away (e.g. sidebar tab switch) before the
|
|
165
|
+
// flagged write actually happens — a previously-set arm is dropped
|
|
166
|
+
// synchronously after the current tick if no watcher fired.
|
|
167
|
+
const mergeArmed = ref(false);
|
|
168
|
+
let pendingTimerIsMerge = false;
|
|
163
169
|
|
|
164
170
|
// Returns 'noop' when no-change vs current head, 'oversized' when over
|
|
165
171
|
// MAX_ENTRY_BYTES, else 'committed'. Truncates the redo tail, caps at
|
|
@@ -286,11 +292,12 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
286
292
|
currentIndex.value === 0 &&
|
|
287
293
|
Date.now() - baselineCapturedAt < BASELINE_SETTLE_MS;
|
|
288
294
|
|
|
289
|
-
// Merge:
|
|
290
|
-
// current head in place instead of
|
|
291
|
-
|
|
295
|
+
// Merge: the timer that fired this flush was armed during a
|
|
296
|
+
// hydration burst — replace the current head in place instead of
|
|
297
|
+
// pushing. Consumed once.
|
|
298
|
+
const shouldMerge = pendingTimerIsMerge;
|
|
292
299
|
|
|
293
|
-
|
|
300
|
+
pendingTimerIsMerge = false;
|
|
294
301
|
|
|
295
302
|
if (inSettleWindow) {
|
|
296
303
|
entries.value = [
|
|
@@ -334,12 +341,20 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
334
341
|
|
|
335
342
|
// Monotonic — first mutation arms; subsequent ones inside the window
|
|
336
343
|
// don't reset. Gated on isApplyingSnapshot so undo/redo don't leave
|
|
337
|
-
// a stale timer ticking.
|
|
344
|
+
// a stale timer ticking. If a hydration site armed `mergeArmed`
|
|
345
|
+
// immediately before the mutation that triggered this run, transfer
|
|
346
|
+
// the intent into `pendingTimerIsMerge` so the resulting flush
|
|
347
|
+
// merges instead of commits.
|
|
338
348
|
const scheduleAutoCapture = (): void => {
|
|
339
349
|
if (isApplyingSnapshot.value) return;
|
|
340
350
|
|
|
341
351
|
if (idleTimerId !== null) return;
|
|
342
352
|
|
|
353
|
+
if (mergeArmed.value) {
|
|
354
|
+
pendingTimerIsMerge = true;
|
|
355
|
+
mergeArmed.value = false;
|
|
356
|
+
}
|
|
357
|
+
|
|
343
358
|
idleTimerId = setTimeout(flush, CAPTURE_DEBOUNCE_MS);
|
|
344
359
|
maxWaitTimerId = setTimeout(flush, CAPTURE_MAX_WAIT_MS);
|
|
345
360
|
};
|
|
@@ -491,9 +506,10 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
491
506
|
const targetEntry = entries.value[targetIndex];
|
|
492
507
|
|
|
493
508
|
isApplyingSnapshot.value = true;
|
|
494
|
-
// Clear any unconsumed merge
|
|
509
|
+
// Clear any unconsumed merge intent — a stale flag would fold the
|
|
495
510
|
// post-apply tick into the restored entry and erase the redo.
|
|
496
|
-
|
|
511
|
+
mergeArmed.value = false;
|
|
512
|
+
pendingTimerIsMerge = false;
|
|
497
513
|
cancelPendingAutoCapture();
|
|
498
514
|
|
|
499
515
|
try {
|
|
@@ -568,7 +584,8 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
568
584
|
if (!baselineCaptured.value) return;
|
|
569
585
|
|
|
570
586
|
cancelPendingAutoCapture();
|
|
571
|
-
|
|
587
|
+
mergeArmed.value = false;
|
|
588
|
+
pendingTimerIsMerge = false;
|
|
572
589
|
|
|
573
590
|
entries.value = [];
|
|
574
591
|
currentIndex.value = -1;
|
|
@@ -579,10 +596,23 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
579
596
|
}
|
|
580
597
|
|
|
581
598
|
// Hydration write sites call this immediately before their imperative
|
|
582
|
-
// mutation
|
|
583
|
-
//
|
|
599
|
+
// mutation. The arm is auto-cleared on the next Vue tick — `nextTick`
|
|
600
|
+
// runs AFTER all queued reactive effects (including the watcher), so
|
|
601
|
+
// if the hydration write triggered `scheduleAutoCapture`, the arm has
|
|
602
|
+
// already been transferred into `pendingTimerIsMerge` before this
|
|
603
|
+
// cleanup fires. If no mutation followed (e.g. the user navigated
|
|
604
|
+
// away before the flagged write), the cleanup drops the stale arm so
|
|
605
|
+
// it can't be consumed by an unrelated future mutation.
|
|
584
606
|
function mergeNextMutation(): void {
|
|
585
|
-
|
|
607
|
+
mergeArmed.value = true;
|
|
608
|
+
// setTimeout(0) is a macrotask — runs AFTER the synchronous write
|
|
609
|
+
// and AFTER Vue's microtask flush of the watcher's reactive effect.
|
|
610
|
+
// If `scheduleAutoCapture` fired in between (the imperative write
|
|
611
|
+
// triggered the watcher), the arm has already been transferred into
|
|
612
|
+
// `pendingTimerIsMerge` before this cleanup runs.
|
|
613
|
+
setTimeout(() => {
|
|
614
|
+
mergeArmed.value = false;
|
|
615
|
+
}, 0);
|
|
586
616
|
}
|
|
587
617
|
|
|
588
618
|
// --- Instrumentation (NFR-04a) ---
|