mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
Users with several past installs/reinstalls accumulate >20 client IDs in the vector clock, so pruning fires on nearly every download-merge. The old notice was a sticky WARNING recommending a destructive "sync reset" for a benign, self-healing cleanup, and it recurred constantly. Split the signal by purpose without touching pruning correctness: - Log: OpLog.info -> OpLog.warn, plus prunedIds/survivingIds so the churn is diagnosable from the exported log history users share in reports. - Snack: throttle to once per app session (take(1)); WARNING -> CUSTOM with a neutral icon and auto-dismiss instead of a sticky alert. - Reword the message: drop "exceeding the limit"/"sync reset", keep the numbers as a breadcrumb.
This commit is contained in:
parent
4e0e2d9ffd
commit
6b375db007
4 changed files with 77 additions and 6 deletions
|
|
@ -3,8 +3,10 @@ import {
|
|||
VectorClockComparison,
|
||||
compareVectorClocks,
|
||||
hasVectorClockChanges,
|
||||
vectorClockPruned$,
|
||||
} from './vector-clock';
|
||||
import { MAX_VECTOR_CLOCK_SIZE } from '../../op-log/core/operation-log.const';
|
||||
import { OpLog } from '../log';
|
||||
|
||||
describe('vector-clock', () => {
|
||||
describe('limitVectorClockSize', () => {
|
||||
|
|
@ -66,6 +68,56 @@ describe('vector-clock', () => {
|
|||
|
||||
expect(Object.keys(result).length).toBeLessThanOrEqual(MAX_VECTOR_CLOCK_SIZE);
|
||||
});
|
||||
|
||||
it('should emit on vectorClockPruned$ when pruning occurs (drives the user notice)', () => {
|
||||
const events: { originalSize: number; maxSize: number }[] = [];
|
||||
const sub = vectorClockPruned$.subscribe((e) => events.push(e));
|
||||
|
||||
const clock: Record<string, number> = { current: 500 };
|
||||
const overflow = MAX_VECTOR_CLOCK_SIZE + 3;
|
||||
for (let i = 0; i < overflow; i++) {
|
||||
clock[`client_${i}`] = 100 + i;
|
||||
}
|
||||
limitVectorClockSize(clock, 'current');
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(events.length).toBe(1);
|
||||
expect(events[0].originalSize).toBe(overflow + 1); // + current
|
||||
expect(events[0].maxSize).toBe(MAX_VECTOR_CLOCK_SIZE);
|
||||
});
|
||||
|
||||
it('should NOT emit on vectorClockPruned$ when within the limit', () => {
|
||||
const events: unknown[] = [];
|
||||
const sub = vectorClockPruned$.subscribe((e) => events.push(e));
|
||||
limitVectorClockSize({ a: 1, b: 2 }, 'a');
|
||||
sub.unsubscribe();
|
||||
expect(events.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should log pruned + surviving client IDs at WARN level for bug-report diagnostics', () => {
|
||||
const warnSpy = spyOn(OpLog, 'warn');
|
||||
|
||||
const clock: Record<string, number> = {
|
||||
current: 500,
|
||||
staleLow: 1, // lowest counter → gets pruned
|
||||
};
|
||||
for (let i = 0; i < MAX_VECTOR_CLOCK_SIZE; i++) {
|
||||
clock[`client_${i}`] = 100 + i;
|
||||
}
|
||||
|
||||
limitVectorClockSize(clock, 'current');
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
const payload = warnSpy.calls.mostRecent().args[1] as {
|
||||
prunedIds: string[];
|
||||
survivingIds: string[];
|
||||
prunedCount: number;
|
||||
};
|
||||
expect(payload.prunedIds).toContain('staleLow');
|
||||
expect(payload.survivingIds).toContain('current');
|
||||
expect(payload.prunedCount).toBe(payload.prunedIds.length);
|
||||
expect(payload.survivingIds.length).toBe(MAX_VECTOR_CLOCK_SIZE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareVectorClocks', () => {
|
||||
|
|
|
|||
|
|
@ -307,11 +307,21 @@ export const limitVectorClockSize = (
|
|||
return clock;
|
||||
}
|
||||
|
||||
OpLog.info('Vector clock pruning triggered', {
|
||||
const limited = sharedLimitVectorClockSize(clock, [currentClientId]);
|
||||
const prunedIds = Object.keys(clock).filter((id) => !(id in limited));
|
||||
|
||||
// WARN (not info): pruning is meant to be rare, so when it fires it is worth
|
||||
// surfacing in the exported log history users share in bug reports (see issue
|
||||
// #8696). clientIds are safe to log — they are not user content — and knowing
|
||||
// which identities keep churning is the key diagnostic for stale-device
|
||||
// accumulation.
|
||||
OpLog.warn('Vector clock pruning triggered', {
|
||||
originalSize: entries.length,
|
||||
maxSize: MAX_VECTOR_CLOCK_SIZE,
|
||||
currentClientId,
|
||||
pruned: entries.length - MAX_VECTOR_CLOCK_SIZE,
|
||||
prunedCount: prunedIds.length,
|
||||
prunedIds,
|
||||
survivingIds: Object.keys(limited),
|
||||
});
|
||||
|
||||
vectorClockPruned$.next({
|
||||
|
|
@ -319,5 +329,5 @@ export const limitVectorClockSize = (
|
|||
maxSize: MAX_VECTOR_CLOCK_SIZE,
|
||||
});
|
||||
|
||||
return sharedLimitVectorClockSize(clock, [currentClientId]);
|
||||
return limited;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -100,15 +100,24 @@ export class SyncEffects {
|
|||
vectorClockPruningNotification$ = createEffect(
|
||||
() =>
|
||||
vectorClockPruned$.pipe(
|
||||
// Pruning fires on essentially every download-merge once a user has
|
||||
// accumulated >20 client IDs (see #8696). Show a calm breadcrumb at
|
||||
// most once per app session — enough to recognise it is happening,
|
||||
// without nagging. The durable record lives in the (WARN-level) log.
|
||||
take(1),
|
||||
tap(({ originalSize, maxSize }) => {
|
||||
this._snackService.open({
|
||||
msg: T.F.SYNC.S.VECTOR_CLOCK_LIMIT_REACHED,
|
||||
type: 'WARNING',
|
||||
// CUSTOM (not WARNING): this is a benign, self-healing cleanup, not
|
||||
// an alert the user must act on. Neutral icon, auto-dismiss — but a
|
||||
// touch longer than the 3s default so the sentence is readable.
|
||||
type: 'CUSTOM',
|
||||
ico: 'sync',
|
||||
translateParams: {
|
||||
originalSize,
|
||||
maxSize,
|
||||
},
|
||||
config: { duration: 0 },
|
||||
config: { duration: 5000 },
|
||||
});
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1579,7 +1579,7 @@
|
|||
"UNKNOWN_ERROR": "Unknown Sync Error: {{err}}",
|
||||
"UPLOAD_ERROR": "Unknown Upload Error (Settings correct?): {{err}}",
|
||||
"UPLOAD_OPS_REJECTED": "{{count}} operation(s) were permanently rejected by the server and won't be synced.",
|
||||
"VECTOR_CLOCK_LIMIT_REACHED": "Sync has tracked {{originalSize}} devices/browsers, exceeding the limit of {{maxSize}}. Inactive device entries were pruned. Consider a sync reset if you experience issues.",
|
||||
"VECTOR_CLOCK_LIMIT_REACHED": "Sync tidied up old device entries ({{originalSize}} → {{maxSize}}). This is normal — no action needed.",
|
||||
"VERSION_TOO_OLD": "Your app version is too old for the synced data. Please update!",
|
||||
"VERSION_UNSUPPORTED": "Cannot sync: data requires a newer app version. Please update.",
|
||||
"WEB_CRYPTO_NOT_AVAILABLE": "Encryption is not available on this device. On Android, encryption requires a secure context (HTTPS) which is not available. Please disable encryption in sync settings or sync from a desktop web browser."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue