Skip to content

Commit 4f41b34

Browse files
committed
array diffs
1 parent eec9c94 commit 4f41b34

File tree

9 files changed

+164
-48
lines changed

9 files changed

+164
-48
lines changed

packages/apollo-forest-run/src/diff/diffObject.ts

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import type {
1919
ObjectDifference,
2020
ObjectDiffState,
2121
ValueDifference,
22+
CompositeListLayoutChangeItemRemoved,
23+
CompositeListLayoutChange,
2224
} from "./types";
2325
import * as Value from "../values";
2426
import * as Difference from "./difference";
2527
import { DiffErrorKind } from "./types";
2628
import { ValueKind } from "../values/types";
29+
import * as ChangeKind from "./itemChangeKind";
2730

2831
/**
2932
* Compares base object version with model version and returns a normalized Difference object,
@@ -420,8 +423,15 @@ function diffCompositeListValue(
420423
return undefined;
421424
}
422425

423-
const layoutDiffResult =
424-
diff?.layout ?? diffCompositeListLayout(context, base, model);
426+
let layoutDiffResult: any = diff?.layout;
427+
let itemsChanges: CompositeListLayoutChange[] = [];
428+
429+
if (!layoutDiffResult) {
430+
const { deletedItems: newDeletedItems, layout: newLayout } =
431+
diffCompositeListLayout(context, base, model);
432+
itemsChanges = newDeletedItems;
433+
layoutDiffResult = newLayout;
434+
}
425435

426436
if (layoutDiffResult === "BREAK") {
427437
// Fast-path, no further diffing necessary
@@ -432,6 +442,7 @@ function diffCompositeListValue(
432442
if (layoutDiffResult) {
433443
diff = diff ?? Difference.createCompositeListDifference();
434444
diff.layout = layoutDiffResult;
445+
diff.itemsChanges = itemsChanges;
435446
}
436447

437448
for (const index of itemQueue) {
@@ -478,7 +489,10 @@ function diffCompositeListLayout(
478489
context: DiffContext,
479490
base: CompositeListValue,
480491
model: CompositeListValue,
481-
): CompositeListLayoutDifference | undefined | "BREAK" {
492+
): {
493+
deletedItems: CompositeListLayoutChange[];
494+
layout: CompositeListLayoutDifference | undefined | "BREAK";
495+
} {
482496
// What constitutes layout change?
483497
// - Change of "keyed object" position in the list
484498
// - Change of list length
@@ -489,6 +503,11 @@ function diffCompositeListLayout(
489503

490504
const baseChunk = Value.isAggregate(base) ? base.chunks[0] : base;
491505
const modelChunk = Value.isAggregate(model) ? model.chunks[0] : model;
506+
const unusedBaseIndixes = new Set<number>();
507+
const itemChanges: CompositeListLayoutChange[] = [];
508+
for (let i = 0; i < baseLen; i++) {
509+
unusedBaseIndixes.add(i);
510+
}
492511

493512
let itemDiffRequired = false;
494513
let firstDirtyIndex = -1;
@@ -508,17 +527,32 @@ function diffCompositeListLayout(
508527
firstDirtyIndex = i;
509528
break;
510529
}
530+
unusedBaseIndixes.delete(i);
511531
}
512532
// Fast-path: no layout difference found
513533
if (firstDirtyIndex === -1) {
534+
const deletedItems: CompositeListLayoutChangeItemRemoved[] = [];
535+
for (const index of unusedBaseIndixes) {
536+
deletedItems.push({
537+
kind: ChangeKind.ItemRemove,
538+
oldIndex: index,
539+
data: baseChunk.data[index],
540+
});
541+
}
514542
if (baseLen > modelLen) {
515543
const layout: CompositeListLayoutDifference = [];
516544
for (let i = 0; i < modelLen; i++) {
517545
layout.push(i);
518546
}
519-
return layout;
547+
return {
548+
deletedItems,
549+
layout,
550+
};
520551
}
521-
return !itemDiffRequired ? "BREAK" : undefined;
552+
return {
553+
deletedItems,
554+
layout: !itemDiffRequired ? "BREAK" : undefined,
555+
};
522556
}
523557
// TODO: lastDirtyIndex to isolate changed segment (prepend case)
524558

@@ -527,12 +561,19 @@ function diffCompositeListLayout(
527561
layout.push(i);
528562
}
529563
let plainObjectLookupStartIndex = firstDirtyIndex;
530-
for (let i = firstDirtyIndex; i < modelLen; i++) {
531-
if (modelChunk.data[i] === null) {
564+
for (let index = firstDirtyIndex; index < modelLen; index++) {
565+
if (modelChunk.data[index] === null) {
532566
layout.push(null);
567+
if (baseChunk.data[index] !== null) {
568+
itemChanges.push({
569+
kind: ChangeKind.ItemAdd,
570+
index,
571+
data: null,
572+
});
573+
}
533574
continue;
534575
}
535-
const modelKey = resolveItemKey(env, modelChunk, i);
576+
const modelKey = resolveItemKey(env, modelChunk, index);
536577
const lookupStartIndex =
537578
modelKey === false ? plainObjectLookupStartIndex : 0; // TODO: should be firstDirtyIndex; (0 is necessary only for cases when array contains duplicates - we should detect such arrays when indexing and special-case it instead)
538579

@@ -545,8 +586,17 @@ function diffCompositeListLayout(
545586
);
546587
if (baseIndex !== -1) {
547588
layout.push(baseIndex);
589+
unusedBaseIndixes.delete(baseIndex);
590+
if (index !== baseIndex) {
591+
itemChanges.push({
592+
kind: ChangeKind.ItemIndexChange,
593+
index,
594+
oldIndex: baseIndex,
595+
data: baseChunk.data[baseIndex],
596+
});
597+
}
548598
} else {
549-
const value = Value.aggregateListItemValue(model, i);
599+
const value = Value.aggregateListItemValue(model, index);
550600
if (Value.isCompositeNullValue(value)) {
551601
layout.push(null);
552602
} else if (
@@ -556,13 +606,24 @@ function diffCompositeListLayout(
556606
layout.push(value);
557607
} else {
558608
throw new Error(
559-
`Unexpected list item value at index #${i}\n` +
609+
`Unexpected list item value at index #${index}\n` +
560610
` original list: ${JSON.stringify(model.data)}`,
561611
);
562612
}
563613
}
564614
}
565-
return layout;
615+
616+
for (const oldIndex of unusedBaseIndixes) {
617+
itemChanges.push({
618+
kind: ChangeKind.ItemRemove,
619+
oldIndex,
620+
data: baseChunk.data[oldIndex],
621+
});
622+
}
623+
return {
624+
deletedItems: itemChanges,
625+
layout,
626+
};
566627
}
567628

568629
function resolveItemKey(

packages/apollo-forest-run/src/diff/difference.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export function createCompositeListDifference(): CompositeListDifference {
7878
dirtyItems: undefined,
7979
layout: undefined,
8080
deletedKeys: undefined,
81+
itemsChanges: [],
8182
errors: undefined,
8283
};
8384
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const ItemAdd = 0,
2+
ItemRemove = 1,
3+
ItemIndexChange = 2;

packages/apollo-forest-run/src/diff/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import {
66
ObjectValue,
77
SourceObject,
88
SourceCompositeList,
9+
NestedList,
10+
MissingFieldsMap,
911
} from "../values/types";
1012
import { FieldInfo, NormalizedFieldEntry } from "../descriptor/types";
1113
import * as DifferenceKind from "./differenceKind";
1214
import * as DiffErrorKind from "./diffErrorKind";
15+
import * as ChangeKind from "./itemChangeKind";
1316

1417
export type DiffEnv = {
1518
allowMissingFields?: boolean;
@@ -90,13 +93,39 @@ export type FieldEntryDifference = {
9093
state: ValueDifference;
9194
};
9295

96+
export type CompositeListLayoutItemAdded = {
97+
kind: typeof ChangeKind.ItemAdd;
98+
index: number;
99+
missingFields?: MissingFieldsMap | undefined;
100+
data?: SourceObject | NestedList<SourceObject> | null;
101+
};
102+
103+
export type CompositeListLayoutChangeItemRemoved = {
104+
kind: typeof ChangeKind.ItemRemove;
105+
oldIndex: number;
106+
data?: SourceObject | NestedList<SourceObject>;
107+
};
108+
109+
export type CompositeListLayoutIndexChange = {
110+
kind: typeof ChangeKind.ItemIndexChange;
111+
index: number;
112+
oldIndex: number;
113+
data?: SourceObject | NestedList<SourceObject>;
114+
};
115+
116+
export type CompositeListLayoutChange =
117+
| CompositeListLayoutChangeItemRemoved
118+
| CompositeListLayoutIndexChange
119+
| CompositeListLayoutItemAdded;
120+
93121
export type CompositeListDifference = {
94122
readonly kind: typeof DifferenceKind.CompositeListDifference;
95123
itemQueue: Set<number>;
96124
itemState: Map<number, ValueDifference>;
97125
dirtyItems?: Set<number>;
98126
layout?: CompositeListLayoutDifference;
99127
deletedKeys?: string[];
128+
itemsChanges: CompositeListLayoutChange[];
100129
errors?: DiffError[];
101130
};
102131

packages/apollo-forest-run/src/forest/indexTree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function indexTree(
105105

106106
let history: HistoryArray | null = null;
107107
if (env.enableHistory) {
108-
const historySize = operation.historySize ?? env.defaultHistorySize;
108+
const historySize = operation.historySize ?? env.defaultHistorySize ?? 0;
109109
history =
110110
previousTreeState?.history ??
111111
new HistoryArray(historySize, env.enableHistory, env.enableDataHistory);

packages/apollo-forest-run/src/forest/types.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { UpdateTreeStats } from "../telemetry/updateStats/types";
3030
import { UpdateLogger } from "../telemetry/updateStats/updateLogger";
3131
import { HistoryArray } from "../jsutils/historyArray";
3232
import type * as DifferenceKind from "../diff/differenceKind";
33+
import { CompositeListLayoutChange } from "../diff/types";
3334

3435
export type IndexedTree = {
3536
operation: OperationDescriptor;
@@ -68,7 +69,7 @@ export type HistoryEntry = {
6869
operation?: OperationDescriptor | undefined;
6970
};
7071
updated: {
71-
changes: ChangedChunksMap;
72+
changes: any;
7273
result: OperationResult | undefined;
7374
};
7475
};
@@ -98,7 +99,7 @@ export type FieldChange = (
9899
}
99100
| {
100101
kind: typeof DifferenceKind.CompositeListDifference;
101-
newSize: number | undefined;
102+
itemChanges: CompositeListLayoutChange[] | undefined;
102103
}
103104
) & {
104105
fieldInfo: FieldInfo;
@@ -127,6 +128,7 @@ export type UpdateTreeContext = {
127128
completeObject: CompleteObjectFn;
128129
findParent: ParentLocator;
129130
env: ForestEnv;
131+
childChanges: any[];
130132
statsLogger?: UpdateLogger;
131133
};
132134

@@ -175,7 +177,8 @@ export type ForestEnv = {
175177
logStaleOperations?: boolean;
176178

177179
// History configuration
178-
enableHistory: boolean;
179-
enableDataHistory: boolean;
180-
defaultHistorySize: number;
180+
enableHistory?: boolean;
181+
enableDataHistory?: boolean;
182+
defaultHistorySize?: number;
183+
storeFieldPaths?: boolean; // Store computed field paths in history (more memory, faster summary generation)
181184
};

packages/apollo-forest-run/src/forest/updateObject.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
UpdateTreeContext,
3030
} from "./types";
3131
import * as Difference from "../diff/difference";
32+
import * as ChangeKind from "../diff/itemChangeKind";
3233
import * as Value from "../values";
3334
import { ValueKind } from "../values/types";
3435
import { DifferenceKind } from "../diff/types";
@@ -64,7 +65,7 @@ function updateObjectValue(
6465
let copy = context.drafts.get(base.data);
6566
assert(!Array.isArray(copy));
6667
context.statsLogger?.copyChunkStats(base, copy);
67-
let dirtyFields: FieldChange[] | undefined;
68+
const changes: FieldChange[] = [];
6869

6970
for (const fieldName of difference.dirtyFields) {
7071
const aliases = base.selection.fields.get(fieldName);
@@ -116,36 +117,35 @@ function updateObjectValue(
116117

117118
switch (fieldDiff.kind) {
118119
case DifferenceKind.Filler:
119-
dirtyFields ??= [];
120-
dirtyFields.push({
120+
changes.push({
121121
kind: fieldDiff.kind,
122122
fieldInfo,
123123
newValue: enableDataHistory ? updated : undefined,
124124
});
125125
break;
126126
case DifferenceKind.Replacement:
127-
dirtyFields ??= [];
128-
dirtyFields.push({
127+
changes.push({
129128
kind: fieldDiff.kind,
130129
fieldInfo,
131130
oldValue: enableDataHistory ? fieldDiff.oldValue : undefined,
132131
newValue: enableDataHistory ? updated : undefined,
133132
});
134133
break;
135134
case DifferenceKind.CompositeListDifference:
136-
dirtyFields ??= [];
137-
dirtyFields.push({
135+
changes.push({
138136
kind: fieldDiff.kind,
139137
fieldInfo,
140-
newSize: fieldDiff.layout?.length,
138+
itemChanges: enableDataHistory ? context.childChanges : undefined,
141139
});
140+
context.childChanges = [];
142141
break;
143142
}
144143
}
145144
}
146-
if (dirtyFields?.length) {
147-
context.changes.set(base, dirtyFields);
145+
if (changes.length) {
146+
context.changes.set(base, changes);
148147
}
148+
149149
return copy ?? base.data;
150150
}
151151

@@ -193,7 +193,7 @@ function updateCompositeListValue(
193193
const layoutDiff = difference.layout;
194194
let dirty = false; // Only dirty on self changes - item replacement/filler, layout changes (ignores child changes)
195195
let copy = drafts.get(base.data);
196-
const dirtyItemIndexes: any[] = [];
196+
const arrayChanges = difference.itemsChanges;
197197
assert(Array.isArray(copy) || copy === undefined);
198198
statsLogger?.copyChunkStats(base, copy);
199199

@@ -223,9 +223,6 @@ function updateCompositeListValue(
223223
dirty = true;
224224
}
225225
}
226-
if (dirty) {
227-
context.changes.set(base, null);
228-
}
229226
if (!layoutDiff) {
230227
return copy ?? base.data;
231228
}
@@ -264,7 +261,8 @@ function updateCompositeListValue(
264261
assert(newValue.data);
265262
accumulateMissingFields(context, newValue);
266263
result[i] = newValue.data;
267-
dirtyItemIndexes.push({
264+
arrayChanges.push({
265+
kind: ChangeKind.ItemAdd,
268266
index: i,
269267
data: newValue.data,
270268
missingFields: newValue.missingFields,
@@ -293,7 +291,7 @@ function updateCompositeListValue(
293291
dirty = true;
294292
}
295293
if (dirty) {
296-
context.changes.set(base, dirtyItemIndexes);
294+
context.childChanges = arrayChanges;
297295
}
298296

299297
return copy ?? base.data;

packages/apollo-forest-run/src/forest/updateTree.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export function updateTree(
6060
completeObject: completeObject.bind(null, env, base, getNodeChunks),
6161
findParent: createParentLocator(base.dataMap),
6262
statsLogger: createUpdateLogger(env.logUpdateStats),
63+
childChanges: [],
6364
env,
6465
};
6566
const {

0 commit comments

Comments
 (0)