refactor(web): extract optimistic timeline reconciliation
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { reconcileOptimisticEntries } from "./timelineOptimisticAllocations.js";
|
||||||
|
|
||||||
|
describe("timelineOptimisticAllocations", () => {
|
||||||
|
it("keeps state unchanged when no optimistic allocations exist", () => {
|
||||||
|
expect(
|
||||||
|
reconcileOptimisticEntries(
|
||||||
|
new Map(),
|
||||||
|
[{ id: "alloc-1", startDate: "2025-01-01T00:00:00.000Z", endDate: "2025-01-02T00:00:00.000Z" }],
|
||||||
|
"alloc-1",
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
optimisticAllocations: new Map(),
|
||||||
|
pendingOptimisticAllocationId: "alloc-1",
|
||||||
|
changed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps unmatched optimistic entries and pending ids", () => {
|
||||||
|
const optimisticAllocations = new Map([
|
||||||
|
[
|
||||||
|
"alloc-1",
|
||||||
|
{
|
||||||
|
startDate: new Date("2025-01-03T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2025-01-04T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
reconcileOptimisticEntries(
|
||||||
|
optimisticAllocations,
|
||||||
|
[{ id: "alloc-1", startDate: "2025-01-05T00:00:00.000Z", endDate: "2025-01-06T00:00:00.000Z" }],
|
||||||
|
"alloc-1",
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
optimisticAllocations,
|
||||||
|
pendingOptimisticAllocationId: "alloc-1",
|
||||||
|
changed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops optimistic entries once server data matches the optimistic override", () => {
|
||||||
|
const optimisticAllocations = new Map([
|
||||||
|
[
|
||||||
|
"alloc-1",
|
||||||
|
{
|
||||||
|
startDate: new Date("2025-01-03T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2025-01-04T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"alloc-2",
|
||||||
|
{
|
||||||
|
startDate: new Date("2025-01-07T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2025-01-08T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
reconcileOptimisticEntries(
|
||||||
|
optimisticAllocations,
|
||||||
|
[
|
||||||
|
{ id: "alloc-1", startDate: "2025-01-03T00:00:00.000Z", endDate: "2025-01-04T00:00:00.000Z" },
|
||||||
|
{ id: "alloc-2", startDate: "2025-01-10T00:00:00.000Z", endDate: "2025-01-11T00:00:00.000Z" },
|
||||||
|
],
|
||||||
|
"alloc-1",
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
optimisticAllocations: new Map([
|
||||||
|
[
|
||||||
|
"alloc-2",
|
||||||
|
{
|
||||||
|
startDate: new Date("2025-01-07T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2025-01-08T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
pendingOptimisticAllocationId: null,
|
||||||
|
changed: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
export type OptimisticTimelineEntryLike = {
|
||||||
|
id: string;
|
||||||
|
startDate: Date | string;
|
||||||
|
endDate: Date | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OptimisticTimelineOverrideLike = {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function reconcileOptimisticEntries(
|
||||||
|
optimisticAllocations: ReadonlyMap<string, OptimisticTimelineOverrideLike>,
|
||||||
|
entries: readonly OptimisticTimelineEntryLike[],
|
||||||
|
pendingOptimisticAllocationId: string | null,
|
||||||
|
): {
|
||||||
|
optimisticAllocations: Map<string, OptimisticTimelineOverrideLike>;
|
||||||
|
pendingOptimisticAllocationId: string | null;
|
||||||
|
changed: boolean;
|
||||||
|
} {
|
||||||
|
if (optimisticAllocations.size === 0) {
|
||||||
|
return {
|
||||||
|
optimisticAllocations: new Map(optimisticAllocations),
|
||||||
|
pendingOptimisticAllocationId,
|
||||||
|
changed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = new Map(optimisticAllocations);
|
||||||
|
let nextPendingId = pendingOptimisticAllocationId;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const override = next.get(entry.id);
|
||||||
|
if (!override) continue;
|
||||||
|
|
||||||
|
const startTime = new Date(entry.startDate).getTime();
|
||||||
|
const endTime = new Date(entry.endDate).getTime();
|
||||||
|
if (startTime === override.startDate.getTime() && endTime === override.endDate.getTime()) {
|
||||||
|
next.delete(entry.id);
|
||||||
|
if (nextPendingId === entry.id) {
|
||||||
|
nextPendingId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
optimisticAllocations: next,
|
||||||
|
pendingOptimisticAllocationId: nextPendingId,
|
||||||
|
changed: next.size !== optimisticAllocations.size || nextPendingId !== pendingOptimisticAllocationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
finalizeMultiSelectDraft,
|
finalizeMultiSelectDraft,
|
||||||
updateMultiSelectDraft,
|
updateMultiSelectDraft,
|
||||||
} from "./timelineMultiSelect.js";
|
} from "./timelineMultiSelect.js";
|
||||||
|
import { reconcileOptimisticEntries } from "./timelineOptimisticAllocations.js";
|
||||||
import { finalizeRangeSelection, updateRangeSelectionDraft } from "./timelineRangeSelection.js";
|
import { finalizeRangeSelection, updateRangeSelectionDraft } from "./timelineRangeSelection.js";
|
||||||
import { getTouchPoint, resolveTouchDragDecision } from "./timelineTouch.js";
|
import { getTouchPoint, resolveTouchDragDecision } from "./timelineTouch.js";
|
||||||
|
|
||||||
@@ -510,27 +511,9 @@ export function useTimelineDrag({
|
|||||||
|
|
||||||
const reconcileOptimisticAllocations = useCallback((entries: readonly OptimisticTimelineEntry[]) => {
|
const reconcileOptimisticAllocations = useCallback((entries: readonly OptimisticTimelineEntry[]) => {
|
||||||
setOptimisticAllocations((prev) => {
|
setOptimisticAllocations((prev) => {
|
||||||
if (prev.size === 0) return prev;
|
const result = reconcileOptimisticEntries(prev, entries, pendingOptimisticAllocationIdRef.current);
|
||||||
|
pendingOptimisticAllocationIdRef.current = result.pendingOptimisticAllocationId;
|
||||||
const next = new Map(prev);
|
return result.changed ? result.optimisticAllocations : prev;
|
||||||
for (const entry of entries) {
|
|
||||||
const override = next.get(entry.id);
|
|
||||||
if (!override) continue;
|
|
||||||
|
|
||||||
const startTime = new Date(entry.startDate).getTime();
|
|
||||||
const endTime = new Date(entry.endDate).getTime();
|
|
||||||
if (
|
|
||||||
startTime === override.startDate.getTime() &&
|
|
||||||
endTime === override.endDate.getTime()
|
|
||||||
) {
|
|
||||||
next.delete(entry.id);
|
|
||||||
if (pendingOptimisticAllocationIdRef.current === entry.id) {
|
|
||||||
pendingOptimisticAllocationIdRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return next.size === prev.size ? prev : next;
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user