refactor(team): extract snapshot structural sharing

This commit is contained in:
777genius 2026-05-22 10:48:08 +03:00
parent c091bd8d96
commit e2031bf928
3 changed files with 179 additions and 60 deletions

View file

@ -91,6 +91,7 @@ import {
collectTeamScopedStateRemovals,
collectTeamScopedVisibleLoadingResets,
} from '../team/teamScopedStateCleanup';
import { structurallyShareTeamSnapshot } from '../team/teamSnapshotStructuralSharing';
import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics';
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
@ -532,66 +533,6 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
if (value == null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
function structurallySharePlainValue<T>(previous: T, next: T): T {
if (Object.is(previous, next)) {
return previous;
}
if (Array.isArray(previous) && Array.isArray(next)) {
let changed = previous.length !== next.length;
const result = next.map((nextItem, index) => {
const sharedItem = structurallySharePlainValue(previous[index], nextItem);
if (!Object.is(sharedItem, previous[index])) {
changed = true;
}
return sharedItem;
});
return changed ? (result as T) : previous;
}
if (isPlainObject(previous) && isPlainObject(next)) {
const previousRecord = previous as Record<string, unknown>;
const nextRecord = next as Record<string, unknown>;
const previousKeys = Object.keys(previousRecord);
const nextKeys = Object.keys(nextRecord);
let changed = previousKeys.length !== nextKeys.length;
const result: Record<string, unknown> = {};
for (const key of nextKeys) {
if (!Object.prototype.hasOwnProperty.call(previousRecord, key)) {
changed = true;
}
const sharedValue = structurallySharePlainValue(previousRecord[key], nextRecord[key]);
if (!Object.is(sharedValue, previousRecord[key])) {
changed = true;
}
result[key] = sharedValue;
}
return changed ? (result as T) : previous;
}
return next;
}
function structurallyShareTeamSnapshot(
previous: TeamViewSnapshot | null | undefined,
next: TeamViewSnapshot
): TeamViewSnapshot {
if (!previous) {
return next;
}
return structurallySharePlainValue(previous, next);
}
const ACTIVE_PROVISIONING_STATES = new Set([
'validating',
'spawning',

View file

@ -0,0 +1,61 @@
import type { TeamViewSnapshot } from '@shared/types';
function isPlainObject(value: unknown): value is Record<string, unknown> {
if (value == null || typeof value !== 'object') {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
export function structurallySharePlainValue<T>(previous: T, next: T): T {
if (Object.is(previous, next)) {
return previous;
}
if (Array.isArray(previous) && Array.isArray(next)) {
let changed = previous.length !== next.length;
const result = next.map((nextItem, index) => {
const sharedItem = structurallySharePlainValue(previous[index], nextItem);
if (!Object.is(sharedItem, previous[index])) {
changed = true;
}
return sharedItem;
});
return changed ? (result as T) : previous;
}
if (isPlainObject(previous) && isPlainObject(next)) {
const previousRecord = previous as Record<string, unknown>;
const nextRecord = next as Record<string, unknown>;
const previousKeys = Object.keys(previousRecord);
const nextKeys = Object.keys(nextRecord);
let changed = previousKeys.length !== nextKeys.length;
const result: Record<string, unknown> = {};
for (const key of nextKeys) {
if (!Object.prototype.hasOwnProperty.call(previousRecord, key)) {
changed = true;
}
const sharedValue = structurallySharePlainValue(previousRecord[key], nextRecord[key]);
if (!Object.is(sharedValue, previousRecord[key])) {
changed = true;
}
result[key] = sharedValue;
}
return changed ? (result as T) : previous;
}
return next;
}
export function structurallyShareTeamSnapshot(
previous: TeamViewSnapshot | null | undefined,
next: TeamViewSnapshot
): TeamViewSnapshot {
if (!previous) {
return next;
}
return structurallySharePlainValue(previous, next);
}

View file

@ -0,0 +1,117 @@
import { describe, expect, it } from 'vitest';
import {
structurallySharePlainValue,
structurallyShareTeamSnapshot,
} from '../../../src/renderer/store/team/teamSnapshotStructuralSharing';
import type { TeamViewSnapshot } from '../../../src/shared/types';
function createSnapshot(overrides: Partial<TeamViewSnapshot> = {}): TeamViewSnapshot {
return {
teamName: 'my-team',
config: { name: 'My Team' },
members: [],
tasks: [],
kanbanState: { teamName: 'my-team', reviewers: [], tasks: {} },
processes: [],
...overrides,
};
}
describe('teamSnapshotStructuralSharing', () => {
it('returns the next snapshot when there is no previous snapshot', () => {
const next = createSnapshot();
expect(structurallyShareTeamSnapshot(null, next)).toBe(next);
expect(structurallyShareTeamSnapshot(undefined, next)).toBe(next);
});
it('preserves the previous snapshot reference when values are deeply equal', () => {
const previous = createSnapshot({
config: { name: 'My Team', description: 'Same description' },
warnings: ['same warning'],
isAlive: true,
});
const next = createSnapshot({
config: { name: 'My Team', description: 'Same description' },
warnings: ['same warning'],
isAlive: true,
});
expect(structurallyShareTeamSnapshot(previous, next)).toBe(previous);
});
it('replaces only changed snapshot branches while sharing unchanged branches', () => {
const previousWarnings = ['same warning'];
const previous = createSnapshot({
config: { name: 'My Team', description: 'Old description' },
warnings: previousWarnings,
isAlive: true,
});
const next = createSnapshot({
config: { name: 'My Team', description: 'New description' },
warnings: ['same warning'],
isAlive: true,
});
const shared = structurallyShareTeamSnapshot(previous, next);
expect(shared).not.toBe(previous);
expect(shared).toEqual(next);
expect(shared.config).not.toBe(previous.config);
expect(shared.warnings).toBe(previousWarnings);
expect(shared.members).toBe(previous.members);
expect(shared.tasks).toBe(previous.tasks);
expect(shared.kanbanState).toBe(previous.kanbanState);
expect(shared.processes).toBe(previous.processes);
});
it('shares unchanged array entries and replaces changed entries', () => {
const previous = [
{ id: 'task-1', title: 'Keep' },
{ id: 'task-2', title: 'Old' },
];
const next = [
{ id: 'task-1', title: 'Keep' },
{ id: 'task-2', title: 'New' },
];
const shared = structurallySharePlainValue(previous, next);
expect(shared).not.toBe(previous);
expect(shared).toEqual(next);
expect(shared[0]).toBe(previous[0]);
expect(shared[1]).not.toBe(previous[1]);
});
it('replaces objects when keys are added or removed', () => {
const previous = { id: 'task-1', title: 'Same', extra: true };
const next = { id: 'task-1', title: 'Same' };
const shared = structurallySharePlainValue(previous, next);
expect(shared).not.toBe(previous);
expect(shared).toEqual(next);
});
it('treats null-prototype objects as plain values', () => {
const previous = Object.assign(Object.create(null) as Record<string, unknown>, {
id: 'task-1',
title: 'Same',
});
const next = Object.assign(Object.create(null) as Record<string, unknown>, {
id: 'task-1',
title: 'Same',
});
expect(structurallySharePlainValue(previous, next)).toBe(previous);
});
it('replaces non-plain objects instead of traversing them', () => {
const previous = new Date('2026-05-22T10:00:00.000Z');
const next = new Date('2026-05-22T10:00:00.000Z');
expect(structurallySharePlainValue(previous, next)).toBe(next);
});
});