@fluidframework/ai-collab 2.10.0-306579
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/.eslintrc.cjs +26 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +280 -0
- package/alpha.d.ts +11 -0
- package/api-extractor/api-extractor-lint-alpha.cjs.json +5 -0
- package/api-extractor/api-extractor-lint-alpha.esm.json +5 -0
- package/api-extractor/api-extractor-lint-bundle.json +5 -0
- package/api-extractor/api-extractor-lint-index.cjs.json +5 -0
- package/api-extractor/api-extractor-lint-index.esm.json +5 -0
- package/api-extractor/api-extractor-lint-public.cjs.json +5 -0
- package/api-extractor/api-extractor-lint-public.esm.json +5 -0
- package/api-extractor-lint.json +4 -0
- package/api-extractor.json +4 -0
- package/api-report/ai-collab.alpha.api.md +164 -0
- package/api-report/ai-collab.beta.api.md +7 -0
- package/api-report/ai-collab.public.api.md +7 -0
- package/biome.jsonc +4 -0
- package/dist/aiCollab.d.ts +65 -0
- package/dist/aiCollab.d.ts.map +1 -0
- package/dist/aiCollab.js +81 -0
- package/dist/aiCollab.js.map +1 -0
- package/dist/aiCollabApi.d.ts +173 -0
- package/dist/aiCollabApi.d.ts.map +1 -0
- package/dist/aiCollabApi.js +7 -0
- package/dist/aiCollabApi.js.map +1 -0
- package/dist/alpha.d.ts +41 -0
- package/dist/explicit-strategy/agentEditReducer.d.ts +12 -0
- package/dist/explicit-strategy/agentEditReducer.d.ts.map +1 -0
- package/dist/explicit-strategy/agentEditReducer.js +394 -0
- package/dist/explicit-strategy/agentEditReducer.js.map +1 -0
- package/dist/explicit-strategy/agentEditTypes.d.ts +158 -0
- package/dist/explicit-strategy/agentEditTypes.d.ts.map +1 -0
- package/dist/explicit-strategy/agentEditTypes.js +50 -0
- package/dist/explicit-strategy/agentEditTypes.js.map +1 -0
- package/dist/explicit-strategy/idGenerator.d.ts +22 -0
- package/dist/explicit-strategy/idGenerator.d.ts.map +1 -0
- package/dist/explicit-strategy/idGenerator.js +74 -0
- package/dist/explicit-strategy/idGenerator.js.map +1 -0
- package/dist/explicit-strategy/index.d.ts +51 -0
- package/dist/explicit-strategy/index.d.ts.map +1 -0
- package/dist/explicit-strategy/index.js +223 -0
- package/dist/explicit-strategy/index.js.map +1 -0
- package/dist/explicit-strategy/jsonTypes.d.ts +23 -0
- package/dist/explicit-strategy/jsonTypes.d.ts.map +1 -0
- package/dist/explicit-strategy/jsonTypes.js +7 -0
- package/dist/explicit-strategy/jsonTypes.js.map +1 -0
- package/dist/explicit-strategy/promptGeneration.d.ts +51 -0
- package/dist/explicit-strategy/promptGeneration.d.ts.map +1 -0
- package/dist/explicit-strategy/promptGeneration.js +218 -0
- package/dist/explicit-strategy/promptGeneration.js.map +1 -0
- package/dist/explicit-strategy/typeGeneration.d.ts +15 -0
- package/dist/explicit-strategy/typeGeneration.d.ts.map +1 -0
- package/dist/explicit-strategy/typeGeneration.js +264 -0
- package/dist/explicit-strategy/typeGeneration.js.map +1 -0
- package/dist/explicit-strategy/utils.d.ts +37 -0
- package/dist/explicit-strategy/utils.d.ts.map +1 -0
- package/dist/explicit-strategy/utils.js +47 -0
- package/dist/explicit-strategy/utils.js.map +1 -0
- package/dist/implicit-strategy/index.d.ts +8 -0
- package/dist/implicit-strategy/index.d.ts.map +1 -0
- package/dist/implicit-strategy/index.js +18 -0
- package/dist/implicit-strategy/index.js.map +1 -0
- package/dist/implicit-strategy/sharedTreeBranchManager.d.ts +63 -0
- package/dist/implicit-strategy/sharedTreeBranchManager.d.ts.map +1 -0
- package/dist/implicit-strategy/sharedTreeBranchManager.js +212 -0
- package/dist/implicit-strategy/sharedTreeBranchManager.js.map +1 -0
- package/dist/implicit-strategy/sharedTreeDiff.d.ts +102 -0
- package/dist/implicit-strategy/sharedTreeDiff.d.ts.map +1 -0
- package/dist/implicit-strategy/sharedTreeDiff.js +522 -0
- package/dist/implicit-strategy/sharedTreeDiff.js.map +1 -0
- package/dist/implicit-strategy/utils.d.ts +21 -0
- package/dist/implicit-strategy/utils.d.ts.map +1 -0
- package/dist/implicit-strategy/utils.js +49 -0
- package/dist/implicit-strategy/utils.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +3 -0
- package/dist/public.d.ts +19 -0
- package/eslintrc.cjs +11 -0
- package/internal.d.ts +11 -0
- package/lib/aiCollab.d.ts +65 -0
- package/lib/aiCollab.d.ts.map +1 -0
- package/lib/aiCollab.js +77 -0
- package/lib/aiCollab.js.map +1 -0
- package/lib/aiCollabApi.d.ts +173 -0
- package/lib/aiCollabApi.d.ts.map +1 -0
- package/lib/aiCollabApi.js +6 -0
- package/lib/aiCollabApi.js.map +1 -0
- package/lib/alpha.d.ts +41 -0
- package/lib/explicit-strategy/agentEditReducer.d.ts +12 -0
- package/lib/explicit-strategy/agentEditReducer.d.ts.map +1 -0
- package/lib/explicit-strategy/agentEditReducer.js +390 -0
- package/lib/explicit-strategy/agentEditReducer.js.map +1 -0
- package/lib/explicit-strategy/agentEditTypes.d.ts +158 -0
- package/lib/explicit-strategy/agentEditTypes.d.ts.map +1 -0
- package/lib/explicit-strategy/agentEditTypes.js +47 -0
- package/lib/explicit-strategy/agentEditTypes.js.map +1 -0
- package/lib/explicit-strategy/idGenerator.d.ts +22 -0
- package/lib/explicit-strategy/idGenerator.d.ts.map +1 -0
- package/lib/explicit-strategy/idGenerator.js +70 -0
- package/lib/explicit-strategy/idGenerator.js.map +1 -0
- package/lib/explicit-strategy/index.d.ts +51 -0
- package/lib/explicit-strategy/index.d.ts.map +1 -0
- package/lib/explicit-strategy/index.js +219 -0
- package/lib/explicit-strategy/index.js.map +1 -0
- package/lib/explicit-strategy/jsonTypes.d.ts +23 -0
- package/lib/explicit-strategy/jsonTypes.d.ts.map +1 -0
- package/lib/explicit-strategy/jsonTypes.js +6 -0
- package/lib/explicit-strategy/jsonTypes.js.map +1 -0
- package/lib/explicit-strategy/promptGeneration.d.ts +51 -0
- package/lib/explicit-strategy/promptGeneration.d.ts.map +1 -0
- package/lib/explicit-strategy/promptGeneration.js +208 -0
- package/lib/explicit-strategy/promptGeneration.js.map +1 -0
- package/lib/explicit-strategy/typeGeneration.d.ts +15 -0
- package/lib/explicit-strategy/typeGeneration.d.ts.map +1 -0
- package/lib/explicit-strategy/typeGeneration.js +260 -0
- package/lib/explicit-strategy/typeGeneration.js.map +1 -0
- package/lib/explicit-strategy/utils.d.ts +37 -0
- package/lib/explicit-strategy/utils.d.ts.map +1 -0
- package/lib/explicit-strategy/utils.js +41 -0
- package/lib/explicit-strategy/utils.js.map +1 -0
- package/lib/implicit-strategy/index.d.ts +8 -0
- package/lib/implicit-strategy/index.d.ts.map +1 -0
- package/lib/implicit-strategy/index.js +8 -0
- package/lib/implicit-strategy/index.js.map +1 -0
- package/lib/implicit-strategy/sharedTreeBranchManager.d.ts +63 -0
- package/lib/implicit-strategy/sharedTreeBranchManager.d.ts.map +1 -0
- package/lib/implicit-strategy/sharedTreeBranchManager.js +213 -0
- package/lib/implicit-strategy/sharedTreeBranchManager.js.map +1 -0
- package/lib/implicit-strategy/sharedTreeDiff.d.ts +102 -0
- package/lib/implicit-strategy/sharedTreeDiff.d.ts.map +1 -0
- package/lib/implicit-strategy/sharedTreeDiff.js +515 -0
- package/lib/implicit-strategy/sharedTreeDiff.js.map +1 -0
- package/lib/implicit-strategy/utils.d.ts +21 -0
- package/lib/implicit-strategy/utils.d.ts.map +1 -0
- package/lib/implicit-strategy/utils.js +43 -0
- package/lib/implicit-strategy/utils.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +15 -0
- package/lib/index.js.map +1 -0
- package/lib/public.d.ts +19 -0
- package/lib/tsdoc-metadata.json +11 -0
- package/mocharc.cjs +14 -0
- package/package.json +165 -0
- package/prettier.config.cjs +8 -0
- package/src/aiCollab.ts +86 -0
- package/src/aiCollabApi.ts +184 -0
- package/src/explicit-strategy/agentEditReducer.ts +498 -0
- package/src/explicit-strategy/agentEditTypes.ts +177 -0
- package/src/explicit-strategy/idGenerator.ts +90 -0
- package/src/explicit-strategy/index.ts +364 -0
- package/src/explicit-strategy/jsonTypes.ts +27 -0
- package/src/explicit-strategy/promptGeneration.ts +294 -0
- package/src/explicit-strategy/typeGeneration.ts +374 -0
- package/src/explicit-strategy/utils.ts +60 -0
- package/src/implicit-strategy/README.md +4 -0
- package/src/implicit-strategy/index.ts +21 -0
- package/src/implicit-strategy/sharedTreeBranchManager.ts +294 -0
- package/src/implicit-strategy/sharedTreeDiff.ts +735 -0
- package/src/implicit-strategy/utils.ts +54 -0
- package/src/index.ts +39 -0
- package/tsconfig.cjs.json +7 -0
- package/tsconfig.json +12 -0
- package/tsdoc.json +4 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type TreeArrayNode, NodeKind } from "@fluidframework/tree";
|
|
7
|
+
|
|
8
|
+
import { isTreeMapNode, sharedTreeTraverse } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents a path through a tree of objects.
|
|
12
|
+
* number values represent array indices whereas string values represent object keys.
|
|
13
|
+
*
|
|
14
|
+
* @alpha
|
|
15
|
+
*/
|
|
16
|
+
export type ObjectPath = (string | number)[];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a create operation between two branches of a tree.
|
|
20
|
+
* Meaning that an attribute (a shared tree node) was identified as being created.
|
|
21
|
+
*
|
|
22
|
+
* @alpha
|
|
23
|
+
*/
|
|
24
|
+
export interface DifferenceCreate {
|
|
25
|
+
type: "CREATE";
|
|
26
|
+
path: ObjectPath;
|
|
27
|
+
value: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Represents a remove operation between two branches of a tree.
|
|
32
|
+
* Meaning that an attribute (a shared tree node) was identified as being deleted.
|
|
33
|
+
* When using object ids, removes are idenitified by an object with a given id no longer existing.
|
|
34
|
+
*
|
|
35
|
+
* @alpha
|
|
36
|
+
*/
|
|
37
|
+
export interface DifferenceRemove {
|
|
38
|
+
type: "REMOVE";
|
|
39
|
+
path: ObjectPath;
|
|
40
|
+
oldValue: unknown;
|
|
41
|
+
objectId?: string | number | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Represents a change operation between two branches of a tree.
|
|
46
|
+
* Meaning that an attribute (a shared tree node) was identified as being changed from one value to another.
|
|
47
|
+
*
|
|
48
|
+
* @alpha
|
|
49
|
+
*/
|
|
50
|
+
export interface DifferenceChange {
|
|
51
|
+
type: "CHANGE";
|
|
52
|
+
path: ObjectPath;
|
|
53
|
+
value: unknown;
|
|
54
|
+
oldValue: unknown;
|
|
55
|
+
objectId?: string | number | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Represents a move operation between two branches of a tree.
|
|
60
|
+
* Meaning that an object (shared tree node) was identified as being moved from one index to another based on its unique id.
|
|
61
|
+
*
|
|
62
|
+
* @alpha
|
|
63
|
+
*/
|
|
64
|
+
export interface DifferenceMove {
|
|
65
|
+
type: "MOVE";
|
|
66
|
+
path: ObjectPath;
|
|
67
|
+
newIndex: number;
|
|
68
|
+
value: unknown;
|
|
69
|
+
objectId?: string | number | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Union for all possible difference types.
|
|
74
|
+
*
|
|
75
|
+
* @alpha
|
|
76
|
+
*/
|
|
77
|
+
export type Difference =
|
|
78
|
+
| DifferenceCreate
|
|
79
|
+
| DifferenceRemove
|
|
80
|
+
| DifferenceChange
|
|
81
|
+
| DifferenceMove;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Options for tree diffing.
|
|
85
|
+
* @alpha
|
|
86
|
+
*/
|
|
87
|
+
export interface Options {
|
|
88
|
+
cyclesFix: boolean;
|
|
89
|
+
useObjectIds?:
|
|
90
|
+
| {
|
|
91
|
+
idAttributeName: string;
|
|
92
|
+
}
|
|
93
|
+
| undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const richTypes = { Date: true, RegExp: true, String: true, Number: true };
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* By default, Object Diff supports cyclical references, but if you are sure that the object has no cycles like parsed JSON
|
|
100
|
+
* you can disable cycles by setting the cyclesFix option to false
|
|
101
|
+
*/
|
|
102
|
+
const DEFAULT_OPTIONS: Options = { cyclesFix: true };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Compares two objects and returns an array of differences between them.
|
|
106
|
+
*
|
|
107
|
+
* @alpha
|
|
108
|
+
*/
|
|
109
|
+
export function sharedTreeDiff(
|
|
110
|
+
obj: Record<string, unknown> | unknown[],
|
|
111
|
+
newObj: Record<string, unknown> | unknown[],
|
|
112
|
+
options: Options = DEFAULT_OPTIONS,
|
|
113
|
+
_stack: (Record<string, unknown> | unknown[])[] = [],
|
|
114
|
+
): Difference[] {
|
|
115
|
+
const diffs: Difference[] = [];
|
|
116
|
+
const isObjArray = isArrayOrTreeArrayNode(obj);
|
|
117
|
+
const isNewObjArray = isArrayOrTreeArrayNode(newObj);
|
|
118
|
+
|
|
119
|
+
// If useObjectIds is set, we'll create a map of object ids to their index in the array.
|
|
120
|
+
const oldObjArrayItemIdsToIndex =
|
|
121
|
+
isObjArray === false || options.useObjectIds === undefined
|
|
122
|
+
? new Map<string | number, number>()
|
|
123
|
+
: createObjectArrayItemIdsToIndexMap(obj, options.useObjectIds.idAttributeName);
|
|
124
|
+
|
|
125
|
+
const newObjArrayItemIdsToIndex =
|
|
126
|
+
isNewObjArray === false || options.useObjectIds === undefined
|
|
127
|
+
? new Map<string | number, number>()
|
|
128
|
+
: createObjectArrayItemIdsToIndexMap(newObj, options.useObjectIds.idAttributeName);
|
|
129
|
+
|
|
130
|
+
const objectKeys = isTreeMapNode(obj) ? obj.keys() : Object.keys(obj);
|
|
131
|
+
// We compare existence and values of all attributes within the old against new object, looking for removals or changes.
|
|
132
|
+
for (const key of objectKeys) {
|
|
133
|
+
const objValue: unknown = isTreeMapNode(obj) ? obj.get(key as string) : obj[key];
|
|
134
|
+
const path = isObjArray ? +key : key;
|
|
135
|
+
// 1. First, check if the key within the old object, exists within the new object. If it doesn't exist this would be an attribute removal.
|
|
136
|
+
if (!(key in newObj)) {
|
|
137
|
+
if (options.useObjectIds === undefined) {
|
|
138
|
+
diffs.push({
|
|
139
|
+
type: "REMOVE",
|
|
140
|
+
path: [path],
|
|
141
|
+
objectId: undefined,
|
|
142
|
+
oldValue: objValue,
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// If we're dealing with an object in an array, we can use the object's id to check if it was moved to a new index.
|
|
147
|
+
else if (
|
|
148
|
+
isNewObjArray === true &&
|
|
149
|
+
isObjArray &&
|
|
150
|
+
typeof objValue === "object" &&
|
|
151
|
+
objValue !== null
|
|
152
|
+
) {
|
|
153
|
+
const objectId = objValue[options.useObjectIds.idAttributeName] as
|
|
154
|
+
| string
|
|
155
|
+
| number
|
|
156
|
+
| undefined;
|
|
157
|
+
if (objectId !== undefined && newObjArrayItemIdsToIndex.has(objectId)) {
|
|
158
|
+
// The index no longer exists in the new root object array, however the object that lived at this index actually still exists at a new index.
|
|
159
|
+
// Therefore, this node was moved to a new index.
|
|
160
|
+
diffs.push({
|
|
161
|
+
type: "MOVE",
|
|
162
|
+
path: [path],
|
|
163
|
+
newIndex: newObjArrayItemIdsToIndex.get(objectId) as number,
|
|
164
|
+
value: objValue,
|
|
165
|
+
objectId,
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// The object with the given id cannot be found within the new array, therefore it was removed.
|
|
170
|
+
else {
|
|
171
|
+
diffs.push({
|
|
172
|
+
type: "REMOVE",
|
|
173
|
+
path: [path],
|
|
174
|
+
objectId,
|
|
175
|
+
oldValue: objValue,
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// If we're not dealing with an object in an array, we can't use id's to check for a move.
|
|
181
|
+
// We'll assume that a missing key in the new object means that the cooresponding value was removed.
|
|
182
|
+
else {
|
|
183
|
+
diffs.push({
|
|
184
|
+
type: "REMOVE",
|
|
185
|
+
path: [path],
|
|
186
|
+
objectId: undefined,
|
|
187
|
+
oldValue: objValue,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const newObjValue: unknown = newObj[key];
|
|
194
|
+
const areCompatibleObjects =
|
|
195
|
+
typeof objValue === "object" &&
|
|
196
|
+
typeof newObjValue === "object" &&
|
|
197
|
+
isArrayOrTreeArrayNode(objValue) === isArrayOrTreeArrayNode(newObjValue);
|
|
198
|
+
|
|
199
|
+
// 2a. If the given old object key exists in the new object, and the value of said key in both objects is ANOTHER nested object, we need to run a recursive diff check on them.
|
|
200
|
+
if (
|
|
201
|
+
objValue !== null &&
|
|
202
|
+
newObjValue !== null &&
|
|
203
|
+
areCompatibleObjects &&
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unsafe-member-access
|
|
205
|
+
!richTypes[Object.getPrototypeOf(objValue)?.constructor?.name] &&
|
|
206
|
+
(!options.cyclesFix || !_stack.includes(objValue as Record<string, unknown>))
|
|
207
|
+
) {
|
|
208
|
+
if (options.useObjectIds === undefined) {
|
|
209
|
+
const nestedDiffs = sharedTreeDiff(
|
|
210
|
+
objValue as Record<string, unknown> | unknown[],
|
|
211
|
+
newObjValue as Record<string, unknown> | unknown[],
|
|
212
|
+
options,
|
|
213
|
+
options.cyclesFix === true
|
|
214
|
+
? [..._stack, objValue as Record<string, unknown> | unknown[]]
|
|
215
|
+
: [],
|
|
216
|
+
);
|
|
217
|
+
// eslint-disable-next-line prefer-spread
|
|
218
|
+
diffs.push.apply(
|
|
219
|
+
diffs,
|
|
220
|
+
nestedDiffs.map((difference) => {
|
|
221
|
+
difference.path.unshift(path);
|
|
222
|
+
return difference;
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
// Use Object Id strategy to determine if the objects should be compared for changes
|
|
227
|
+
else {
|
|
228
|
+
const oldObjectId = (objValue as Record<string, unknown>)[
|
|
229
|
+
options.useObjectIds.idAttributeName
|
|
230
|
+
] as string | number | undefined;
|
|
231
|
+
const newObjectId = (newObjValue as Record<string, unknown>)[
|
|
232
|
+
options.useObjectIds.idAttributeName
|
|
233
|
+
] as string | number | undefined;
|
|
234
|
+
|
|
235
|
+
if (oldObjectId !== undefined && newObjectId !== undefined) {
|
|
236
|
+
// 2a.1 if the object id's are the same, we can continue a comparison between the two objects.
|
|
237
|
+
if (oldObjectId === newObjectId) {
|
|
238
|
+
const nestedDiffs = sharedTreeDiff(
|
|
239
|
+
objValue as Record<string, unknown> | unknown[],
|
|
240
|
+
newObjValue as Record<string, unknown> | unknown[],
|
|
241
|
+
options,
|
|
242
|
+
options.cyclesFix === true
|
|
243
|
+
? [..._stack, objValue as Record<string, unknown> | unknown[]]
|
|
244
|
+
: [],
|
|
245
|
+
);
|
|
246
|
+
diffs.push(
|
|
247
|
+
...nestedDiffs.map((difference) => {
|
|
248
|
+
difference.path.unshift(path);
|
|
249
|
+
return difference;
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
// 2a.2 The object id's are different, their attributes cannot be compared.
|
|
254
|
+
// We need to find the new index of the object, if it exists in the new array and do a diff comparison.
|
|
255
|
+
else {
|
|
256
|
+
const newIndexOfOldObject = newObjArrayItemIdsToIndex.get(oldObjectId);
|
|
257
|
+
// The object no longer exists in the new array, therefore it was removed.
|
|
258
|
+
if (newIndexOfOldObject === undefined) {
|
|
259
|
+
diffs.push({
|
|
260
|
+
type: "REMOVE",
|
|
261
|
+
path: [path],
|
|
262
|
+
oldValue: objValue,
|
|
263
|
+
objectId: oldObjectId,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// This object still exists but at a new index within the new array therefore it was moved.
|
|
267
|
+
// At this point we can determine whether a new move is necessary or there is one that will place it at the desired index.
|
|
268
|
+
else {
|
|
269
|
+
diffs.push({
|
|
270
|
+
type: "MOVE",
|
|
271
|
+
path: [path],
|
|
272
|
+
newIndex: newIndexOfOldObject,
|
|
273
|
+
value: objValue,
|
|
274
|
+
objectId: oldObjectId,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// An object could have been moved AND changed. We need to check for this.
|
|
278
|
+
const nestedDiffs = sharedTreeDiff(
|
|
279
|
+
obj[path] as Record<string, unknown> | unknown[],
|
|
280
|
+
newObj[newIndexOfOldObject] as Record<string, unknown> | unknown[],
|
|
281
|
+
options,
|
|
282
|
+
options.cyclesFix === true
|
|
283
|
+
? [..._stack, objValue as Record<string, unknown> | unknown[]]
|
|
284
|
+
: [],
|
|
285
|
+
);
|
|
286
|
+
diffs.push(
|
|
287
|
+
...nestedDiffs.map((difference) => {
|
|
288
|
+
difference.path.unshift(path);
|
|
289
|
+
return difference;
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
const nestedDiffs = sharedTreeDiff(
|
|
296
|
+
objValue as Record<string, unknown> | unknown[],
|
|
297
|
+
newObjValue as Record<string, unknown> | unknown[],
|
|
298
|
+
options,
|
|
299
|
+
options.cyclesFix === true
|
|
300
|
+
? [..._stack, objValue as Record<string, unknown> | unknown[]]
|
|
301
|
+
: [],
|
|
302
|
+
);
|
|
303
|
+
diffs.push(
|
|
304
|
+
...nestedDiffs.map((difference) => {
|
|
305
|
+
difference.path.unshift(path);
|
|
306
|
+
return difference;
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// 2b. If the given old object key exists in the new object, and the value of said key in both objects is NOT another nested object, we need to check if the values are the same.
|
|
313
|
+
else if (
|
|
314
|
+
objValue !== newObjValue &&
|
|
315
|
+
// treat NaN values as equivalent
|
|
316
|
+
!(Number.isNaN(objValue) && Number.isNaN(newObjValue)) &&
|
|
317
|
+
!(
|
|
318
|
+
areCompatibleObjects &&
|
|
319
|
+
(Number.isNaN(objValue)
|
|
320
|
+
? // eslint-disable-next-line prefer-template
|
|
321
|
+
objValue + "" === newObjValue + ""
|
|
322
|
+
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
+objValue === +newObjValue)
|
|
325
|
+
)
|
|
326
|
+
) {
|
|
327
|
+
diffs.push({
|
|
328
|
+
path: [path],
|
|
329
|
+
type: "CHANGE",
|
|
330
|
+
value: newObjValue,
|
|
331
|
+
oldValue: objValue,
|
|
332
|
+
objectId:
|
|
333
|
+
options.useObjectIds?.idAttributeName === undefined
|
|
334
|
+
? undefined
|
|
335
|
+
: (newObj[options.useObjectIds.idAttributeName] as string | number | undefined),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 3. Finally, we check for new keys in the new object that did not exist in the old object.
|
|
341
|
+
// The existence of new keys may signal new values or moved values.
|
|
342
|
+
const newObjKeys = isTreeMapNode(newObj) ? newObj.keys() : Object.keys(newObj);
|
|
343
|
+
for (const key of newObjKeys) {
|
|
344
|
+
const newObjValue: unknown = isTreeMapNode(newObj)
|
|
345
|
+
? newObj.get(key as string)
|
|
346
|
+
: newObj[key];
|
|
347
|
+
const path = isNewObjArray ? +key : key;
|
|
348
|
+
|
|
349
|
+
const isKeyInOldObject = isTreeMapNode(obj)
|
|
350
|
+
? obj.has(key as string)
|
|
351
|
+
: Object.keys(obj).includes(key as string);
|
|
352
|
+
if (!isKeyInOldObject) {
|
|
353
|
+
if (options.useObjectIds === undefined) {
|
|
354
|
+
diffs.push({
|
|
355
|
+
type: "CREATE",
|
|
356
|
+
path: [path],
|
|
357
|
+
value: newObjValue,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// If we're dealing with an object in an array, we can use the object's id to check if this new index actually
|
|
361
|
+
// contains a prexisting object that was moved from an old index.
|
|
362
|
+
else if (
|
|
363
|
+
isObjArray === true &&
|
|
364
|
+
isNewObjArray === true &&
|
|
365
|
+
typeof newObjValue === "object" &&
|
|
366
|
+
newObjValue !== null
|
|
367
|
+
) {
|
|
368
|
+
const objectId = newObjValue[options.useObjectIds.idAttributeName] as
|
|
369
|
+
| string
|
|
370
|
+
| number
|
|
371
|
+
| undefined;
|
|
372
|
+
if (objectId !== undefined && oldObjArrayItemIdsToIndex.has(objectId)) {
|
|
373
|
+
// The new root object array contains a new index, however the object that lives at this new index previously existed at an old index.
|
|
374
|
+
// Therefore, this object was moved to a new index.
|
|
375
|
+
diffs.push({
|
|
376
|
+
type: "MOVE",
|
|
377
|
+
path: [path],
|
|
378
|
+
newIndex: newObjArrayItemIdsToIndex.get(objectId) as number,
|
|
379
|
+
value: newObjValue,
|
|
380
|
+
objectId,
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
// If either the object's id attribute does not exist or the original array does not contain an object with the given id
|
|
385
|
+
// Then we assume this was a newly created object.
|
|
386
|
+
else {
|
|
387
|
+
diffs.push({
|
|
388
|
+
type: "CREATE",
|
|
389
|
+
path: [path],
|
|
390
|
+
value: newObjValue,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// If we're not dealing with an object in an array, we can't use id's to check for a move.
|
|
395
|
+
// We'll assume that a brand new key and value pair in the new object means that a new value was created.
|
|
396
|
+
else {
|
|
397
|
+
diffs.push({
|
|
398
|
+
type: "CREATE",
|
|
399
|
+
path: [path],
|
|
400
|
+
value: newObjValue,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} else if (options.useObjectIds !== undefined) {
|
|
404
|
+
// If we're dealing with an object in an array, we can use the object's id to check if this EXISTING index
|
|
405
|
+
// houses a new object based on a newly encountered id.
|
|
406
|
+
if (
|
|
407
|
+
isObjArray === true &&
|
|
408
|
+
isNewObjArray === true &&
|
|
409
|
+
typeof newObjValue === "object" &&
|
|
410
|
+
newObjValue !== null
|
|
411
|
+
) {
|
|
412
|
+
const objectId = newObjValue[options.useObjectIds.idAttributeName] as
|
|
413
|
+
| string
|
|
414
|
+
| number
|
|
415
|
+
| undefined;
|
|
416
|
+
// If this object has an id and it does not exist in the old array, then it was created.
|
|
417
|
+
if (objectId !== undefined && oldObjArrayItemIdsToIndex.has(objectId) === false) {
|
|
418
|
+
diffs.push({
|
|
419
|
+
type: "CREATE",
|
|
420
|
+
path: [path],
|
|
421
|
+
value: newObjValue,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return diffs;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Type Guard that determines if a given object is an array of type unknown[] or {@link TreeArrayNode}.
|
|
434
|
+
*/
|
|
435
|
+
function isArrayOrTreeArrayNode(obj: unknown): obj is unknown[] | TreeArrayNode {
|
|
436
|
+
if (typeof obj === "object" && obj !== null) {
|
|
437
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
438
|
+
const maybeNodeKind: unknown = Object.getPrototypeOf(obj)?.constructor?.kind;
|
|
439
|
+
const isTreeArrayNode = maybeNodeKind === NodeKind.Array;
|
|
440
|
+
return Array.isArray(obj) || isTreeArrayNode;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Helper that creates a map of object ids to their index in an array of objects.
|
|
447
|
+
*/
|
|
448
|
+
function createObjectArrayItemIdsToIndexMap(
|
|
449
|
+
obj: unknown[],
|
|
450
|
+
idAttributeName: string | number,
|
|
451
|
+
): Map<string | number, number> {
|
|
452
|
+
const objArrayItemIdsToIndex = new Map<string | number, number>();
|
|
453
|
+
for (let i = 0; i < obj.length; i++) {
|
|
454
|
+
const objArrayItem = obj[i];
|
|
455
|
+
if (typeof objArrayItem === "object" && objArrayItem !== null) {
|
|
456
|
+
const id = (objArrayItem as Record<string, unknown>)[idAttributeName] as string | number;
|
|
457
|
+
if (objArrayItemIdsToIndex.has(id)) {
|
|
458
|
+
throw new TypeError(`Duplicate object id found: ${id}`);
|
|
459
|
+
} else if (id !== undefined) {
|
|
460
|
+
objArrayItemIdsToIndex.set(id, i);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return objArrayItemIdsToIndex;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Creates a set of mergeable diffs from a series of diffs produced by {@link sharedTreeDiff}
|
|
470
|
+
* that are using the object ID strategy. These diffs don't need any modifications to be applied to the old object.
|
|
471
|
+
*
|
|
472
|
+
* @alpha
|
|
473
|
+
*/
|
|
474
|
+
export function createMergableIdDiffSeries(
|
|
475
|
+
oldObject: unknown,
|
|
476
|
+
diffs: Difference[],
|
|
477
|
+
idAttributeName: string | number,
|
|
478
|
+
): Difference[] {
|
|
479
|
+
// the final series of diffs that will be returned.
|
|
480
|
+
const finalDiffSeries: Difference[] = [];
|
|
481
|
+
// Diffs that aren't of type 'CHANGE'
|
|
482
|
+
const nonChangeDiffs: Difference[] = [];
|
|
483
|
+
|
|
484
|
+
for (const diff of diffs) {
|
|
485
|
+
if (diff.type === "CHANGE") {
|
|
486
|
+
// Changes must be applied before any other diff, ao so they are ordered first.
|
|
487
|
+
finalDiffSeries.push({ ...diff });
|
|
488
|
+
} else {
|
|
489
|
+
nonChangeDiffs.push({ ...diff });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Create sets of array diffs grouped by the array they are applying changes to.
|
|
494
|
+
const diffsByArrayUuid = new Map<string, Difference[]>();
|
|
495
|
+
for (const diff of nonChangeDiffs) {
|
|
496
|
+
if (!isDiffOnArray(diff)) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const arrayUuid = arrayUuidFromPath(diff.path);
|
|
501
|
+
|
|
502
|
+
if (diffsByArrayUuid.has(arrayUuid) === false) {
|
|
503
|
+
diffsByArrayUuid.set(arrayUuid, []);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
507
|
+
diffsByArrayUuid.get(arrayUuid)!.push(diff);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const shiftIndexesFromMove = (
|
|
511
|
+
diff: DifferenceMove,
|
|
512
|
+
targetArray: unknown[],
|
|
513
|
+
diffAdjustedObjectIndexes: Map<string | number, number>,
|
|
514
|
+
objectId: string | number,
|
|
515
|
+
): void => {
|
|
516
|
+
const sourceIndex = diff.path[diff.path.length - 1] as number;
|
|
517
|
+
|
|
518
|
+
if (diff.newIndex > sourceIndex) {
|
|
519
|
+
// This move diff shifts objects it moved over to the left.
|
|
520
|
+
// |----| |----|
|
|
521
|
+
// e.g. - shift with no length change: [{1}, {2}, {3}, {4}] -> [{2}, {3}, {1}, {4}]
|
|
522
|
+
const minIndex = sourceIndex;
|
|
523
|
+
const maxIndex = diff.newIndex;
|
|
524
|
+
for (const [id, index] of diffAdjustedObjectIndexes.entries()) {
|
|
525
|
+
const shouldIndexBeShifted =
|
|
526
|
+
id !== objectId && index <= maxIndex && index >= minIndex && index - 1 >= 0;
|
|
527
|
+
if (shouldIndexBeShifted) {
|
|
528
|
+
diffAdjustedObjectIndexes.set(id, index - 1);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} else if (diff.newIndex < sourceIndex) {
|
|
532
|
+
// This move diff shifts objects it moved over to the right.
|
|
533
|
+
// |----| |----|
|
|
534
|
+
// e.g. - shift with no length change: [{1}, {2}, {3}, {4}] -> [{3}, {1}, {2}, {4}]
|
|
535
|
+
const minIndex = diff.newIndex;
|
|
536
|
+
const maxIndex = sourceIndex;
|
|
537
|
+
for (const [id, index] of diffAdjustedObjectIndexes.entries()) {
|
|
538
|
+
const shouldIndexBeShifted =
|
|
539
|
+
id !== objectId &&
|
|
540
|
+
index <= maxIndex &&
|
|
541
|
+
index >= minIndex &&
|
|
542
|
+
index + 1 <= targetArray.length;
|
|
543
|
+
if (shouldIndexBeShifted) {
|
|
544
|
+
diffAdjustedObjectIndexes.set(id, index + 1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const shiftIndexesFromRemove = (
|
|
551
|
+
diff: DifferenceRemove,
|
|
552
|
+
diffAdjustedObjectIndexes: Map<string | number, number>,
|
|
553
|
+
objectId: string | number,
|
|
554
|
+
): void => {
|
|
555
|
+
const removalIndex = diff.path[diff.path.length - 1] as number;
|
|
556
|
+
for (const [id, index] of diffAdjustedObjectIndexes.entries()) {
|
|
557
|
+
const shouldIndexBeShifted = id !== objectId && index > removalIndex && index - 1 >= 0;
|
|
558
|
+
if (shouldIndexBeShifted) {
|
|
559
|
+
diffAdjustedObjectIndexes.set(id, index - 1);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const diffsMarkedForRemoval = new Set<Difference>();
|
|
565
|
+
const arrayDiffsMarkedForEndReorder = new Map<string, Difference[]>();
|
|
566
|
+
|
|
567
|
+
for (const [arrayUuid, arrayDiffs] of diffsByArrayUuid.entries()) {
|
|
568
|
+
// The prior grouping code ensures that each map value will have atleast 1 diff.
|
|
569
|
+
const targetArray = getTargetObjectFromPath(
|
|
570
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
571
|
+
arrayDiffs[0]!.path,
|
|
572
|
+
oldObject as TreeArrayNode,
|
|
573
|
+
) as unknown[];
|
|
574
|
+
const diffAdjustedObjectIndexes: Map<string | number, number> =
|
|
575
|
+
createObjectArrayItemIdsToIndexMap(targetArray, idAttributeName);
|
|
576
|
+
|
|
577
|
+
for (const diff of arrayDiffs) {
|
|
578
|
+
if (diff.type === "MOVE") {
|
|
579
|
+
const objectId = (diff.value as Record<string, unknown>)[idAttributeName] as
|
|
580
|
+
| string
|
|
581
|
+
| number;
|
|
582
|
+
const sourceIndex = diff.path[diff.path.length - 1] as number;
|
|
583
|
+
|
|
584
|
+
// 1. Prior moves may render the next move redundant.
|
|
585
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
586
|
+
const currentAdjustedIndex = diffAdjustedObjectIndexes.get(objectId)!;
|
|
587
|
+
if (currentAdjustedIndex === diff.newIndex) {
|
|
588
|
+
diffsMarkedForRemoval.add(diff);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (currentAdjustedIndex !== sourceIndex) {
|
|
592
|
+
// A Prior Remove or Move Diff moved the object to a new index, so update the diff source index to point to the new index.
|
|
593
|
+
diff.path[diff.path.length - 1] = currentAdjustedIndex;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Handle index shifts
|
|
597
|
+
diffAdjustedObjectIndexes.set(objectId, diff.newIndex);
|
|
598
|
+
|
|
599
|
+
// edge case: this MOVE should be applied after some series of creates that we haven't seen.
|
|
600
|
+
if (diff.newIndex > targetArray.length - 1) {
|
|
601
|
+
// It also wont shift any indexes since its moved to the total end of the array,
|
|
602
|
+
// after creations that produce the necessary indexes.
|
|
603
|
+
if (arrayDiffsMarkedForEndReorder.has(arrayUuid) === false) {
|
|
604
|
+
arrayDiffsMarkedForEndReorder.set(arrayUuid, []);
|
|
605
|
+
}
|
|
606
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
607
|
+
arrayDiffsMarkedForEndReorder.get(arrayUuid)!.push(diff);
|
|
608
|
+
} else {
|
|
609
|
+
shiftIndexesFromMove(diff, targetArray, diffAdjustedObjectIndexes, objectId);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (diff.type === "REMOVE") {
|
|
613
|
+
const objectId = (diff.oldValue as Record<string, unknown>)[idAttributeName] as
|
|
614
|
+
| string
|
|
615
|
+
| number;
|
|
616
|
+
const targetIndex = diff.path[diff.path.length - 1] as number;
|
|
617
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
618
|
+
const currentDiffAdjustedIndex = diffAdjustedObjectIndexes.get(objectId)!;
|
|
619
|
+
if (targetIndex !== diffAdjustedObjectIndexes.get(objectId)) {
|
|
620
|
+
// A Prior Remove or Move Diff moved the object to a new index, so update the diff source index to point to the new index.
|
|
621
|
+
diff.path[diff.path.length - 1] = currentDiffAdjustedIndex;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
shiftIndexesFromRemove(diff, diffAdjustedObjectIndexes, objectId);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Ignoring 'CREATE' for now.
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
for (let i = 0; i < nonChangeDiffs.length; i++) {
|
|
632
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
633
|
+
const diff = nonChangeDiffs[i]!;
|
|
634
|
+
|
|
635
|
+
if (diffsMarkedForRemoval.has(diff)) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const isLastDiffInArraySeries = (currentIndex: number): boolean => {
|
|
640
|
+
if (currentIndex === nonChangeDiffs.length - 1) {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
const nextIndex = currentIndex + 1;
|
|
644
|
+
if (nextIndex <= nonChangeDiffs.length - 1) {
|
|
645
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
646
|
+
const diffAfter = nonChangeDiffs[nextIndex]!;
|
|
647
|
+
|
|
648
|
+
if (diffsMarkedForRemoval.has(diffAfter)) {
|
|
649
|
+
return isLastDiffInArraySeries(nextIndex + 1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const arrayUuidAfter = arrayUuidFromPath(diffAfter.path);
|
|
653
|
+
const arrayUuid = arrayUuidFromPath(diff.path);
|
|
654
|
+
if (arrayUuidAfter === arrayUuid) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return true;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
if (isDiffOnArray(diff)) {
|
|
662
|
+
const arrayUuid = arrayUuidFromPath(diff.path);
|
|
663
|
+
const endReorderDiffs = arrayDiffsMarkedForEndReorder.get(arrayUuid);
|
|
664
|
+
const isDiffMarkedForReorder = endReorderDiffs?.includes(diff) ?? false;
|
|
665
|
+
|
|
666
|
+
if (isDiffMarkedForReorder === false) {
|
|
667
|
+
finalDiffSeries.push(diff);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (isLastDiffInArraySeries(i) && endReorderDiffs !== undefined) {
|
|
671
|
+
finalDiffSeries.push(...endReorderDiffs);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
finalDiffSeries.push(diff);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return finalDiffSeries;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Creates a set of mergeable diffs from a series of diffs produced by {@link sharedTreeDiff}
|
|
685
|
+
* that AREN'T using the object ID strategy. These diffs don't need any modifications to be applied to the old object.
|
|
686
|
+
*
|
|
687
|
+
* @alpha
|
|
688
|
+
*/
|
|
689
|
+
export function createMergableDiffSeries(diffs: Difference[]): Difference[] {
|
|
690
|
+
// the final series of diffs that will be returned.
|
|
691
|
+
const finalDiffSeries: Difference[] = [];
|
|
692
|
+
// Diffs that aren't of type 'CHANGE'
|
|
693
|
+
const nonChangeDiffs: Difference[] = [];
|
|
694
|
+
|
|
695
|
+
for (const diff of diffs) {
|
|
696
|
+
if (diff.type === "CHANGE") {
|
|
697
|
+
// Changes must be applied before any other diff, ao so they are ordered first.
|
|
698
|
+
finalDiffSeries.push({ ...diff });
|
|
699
|
+
} else {
|
|
700
|
+
nonChangeDiffs.push({ ...diff });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
finalDiffSeries.push(...nonChangeDiffs);
|
|
705
|
+
|
|
706
|
+
return finalDiffSeries;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Creates a UUID for the target array from a {@link Difference}'s ${@link ObjectPath}
|
|
711
|
+
*/
|
|
712
|
+
function arrayUuidFromPath(path: ObjectPath): string {
|
|
713
|
+
return path.length === 1 ? "" : path.slice(0, -1).join("");
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Determines if a given difference is on an array.
|
|
718
|
+
*/
|
|
719
|
+
export function isDiffOnArray(diff: Difference): boolean {
|
|
720
|
+
return typeof diff.path[diff.path.length - 1] === "number";
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Returns the target object that the given diff should be applied to.
|
|
725
|
+
*/
|
|
726
|
+
function getTargetObjectFromPath(
|
|
727
|
+
path: ObjectPath,
|
|
728
|
+
object: Record<string, unknown> | TreeArrayNode,
|
|
729
|
+
): unknown {
|
|
730
|
+
let targetObject: unknown = object;
|
|
731
|
+
if (path.length > 1) {
|
|
732
|
+
targetObject = sharedTreeTraverse(object, path.slice(0, -1));
|
|
733
|
+
}
|
|
734
|
+
return targetObject;
|
|
735
|
+
}
|