@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.
Files changed (168) hide show
  1. package/.eslintrc.cjs +26 -0
  2. package/CHANGELOG.md +9 -0
  3. package/LICENSE +21 -0
  4. package/README.md +280 -0
  5. package/alpha.d.ts +11 -0
  6. package/api-extractor/api-extractor-lint-alpha.cjs.json +5 -0
  7. package/api-extractor/api-extractor-lint-alpha.esm.json +5 -0
  8. package/api-extractor/api-extractor-lint-bundle.json +5 -0
  9. package/api-extractor/api-extractor-lint-index.cjs.json +5 -0
  10. package/api-extractor/api-extractor-lint-index.esm.json +5 -0
  11. package/api-extractor/api-extractor-lint-public.cjs.json +5 -0
  12. package/api-extractor/api-extractor-lint-public.esm.json +5 -0
  13. package/api-extractor-lint.json +4 -0
  14. package/api-extractor.json +4 -0
  15. package/api-report/ai-collab.alpha.api.md +164 -0
  16. package/api-report/ai-collab.beta.api.md +7 -0
  17. package/api-report/ai-collab.public.api.md +7 -0
  18. package/biome.jsonc +4 -0
  19. package/dist/aiCollab.d.ts +65 -0
  20. package/dist/aiCollab.d.ts.map +1 -0
  21. package/dist/aiCollab.js +81 -0
  22. package/dist/aiCollab.js.map +1 -0
  23. package/dist/aiCollabApi.d.ts +173 -0
  24. package/dist/aiCollabApi.d.ts.map +1 -0
  25. package/dist/aiCollabApi.js +7 -0
  26. package/dist/aiCollabApi.js.map +1 -0
  27. package/dist/alpha.d.ts +41 -0
  28. package/dist/explicit-strategy/agentEditReducer.d.ts +12 -0
  29. package/dist/explicit-strategy/agentEditReducer.d.ts.map +1 -0
  30. package/dist/explicit-strategy/agentEditReducer.js +394 -0
  31. package/dist/explicit-strategy/agentEditReducer.js.map +1 -0
  32. package/dist/explicit-strategy/agentEditTypes.d.ts +158 -0
  33. package/dist/explicit-strategy/agentEditTypes.d.ts.map +1 -0
  34. package/dist/explicit-strategy/agentEditTypes.js +50 -0
  35. package/dist/explicit-strategy/agentEditTypes.js.map +1 -0
  36. package/dist/explicit-strategy/idGenerator.d.ts +22 -0
  37. package/dist/explicit-strategy/idGenerator.d.ts.map +1 -0
  38. package/dist/explicit-strategy/idGenerator.js +74 -0
  39. package/dist/explicit-strategy/idGenerator.js.map +1 -0
  40. package/dist/explicit-strategy/index.d.ts +51 -0
  41. package/dist/explicit-strategy/index.d.ts.map +1 -0
  42. package/dist/explicit-strategy/index.js +223 -0
  43. package/dist/explicit-strategy/index.js.map +1 -0
  44. package/dist/explicit-strategy/jsonTypes.d.ts +23 -0
  45. package/dist/explicit-strategy/jsonTypes.d.ts.map +1 -0
  46. package/dist/explicit-strategy/jsonTypes.js +7 -0
  47. package/dist/explicit-strategy/jsonTypes.js.map +1 -0
  48. package/dist/explicit-strategy/promptGeneration.d.ts +51 -0
  49. package/dist/explicit-strategy/promptGeneration.d.ts.map +1 -0
  50. package/dist/explicit-strategy/promptGeneration.js +218 -0
  51. package/dist/explicit-strategy/promptGeneration.js.map +1 -0
  52. package/dist/explicit-strategy/typeGeneration.d.ts +15 -0
  53. package/dist/explicit-strategy/typeGeneration.d.ts.map +1 -0
  54. package/dist/explicit-strategy/typeGeneration.js +264 -0
  55. package/dist/explicit-strategy/typeGeneration.js.map +1 -0
  56. package/dist/explicit-strategy/utils.d.ts +37 -0
  57. package/dist/explicit-strategy/utils.d.ts.map +1 -0
  58. package/dist/explicit-strategy/utils.js +47 -0
  59. package/dist/explicit-strategy/utils.js.map +1 -0
  60. package/dist/implicit-strategy/index.d.ts +8 -0
  61. package/dist/implicit-strategy/index.d.ts.map +1 -0
  62. package/dist/implicit-strategy/index.js +18 -0
  63. package/dist/implicit-strategy/index.js.map +1 -0
  64. package/dist/implicit-strategy/sharedTreeBranchManager.d.ts +63 -0
  65. package/dist/implicit-strategy/sharedTreeBranchManager.d.ts.map +1 -0
  66. package/dist/implicit-strategy/sharedTreeBranchManager.js +212 -0
  67. package/dist/implicit-strategy/sharedTreeBranchManager.js.map +1 -0
  68. package/dist/implicit-strategy/sharedTreeDiff.d.ts +102 -0
  69. package/dist/implicit-strategy/sharedTreeDiff.d.ts.map +1 -0
  70. package/dist/implicit-strategy/sharedTreeDiff.js +522 -0
  71. package/dist/implicit-strategy/sharedTreeDiff.js.map +1 -0
  72. package/dist/implicit-strategy/utils.d.ts +21 -0
  73. package/dist/implicit-strategy/utils.d.ts.map +1 -0
  74. package/dist/implicit-strategy/utils.js +49 -0
  75. package/dist/implicit-strategy/utils.js.map +1 -0
  76. package/dist/index.d.ts +16 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +24 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/package.json +3 -0
  81. package/dist/public.d.ts +19 -0
  82. package/eslintrc.cjs +11 -0
  83. package/internal.d.ts +11 -0
  84. package/lib/aiCollab.d.ts +65 -0
  85. package/lib/aiCollab.d.ts.map +1 -0
  86. package/lib/aiCollab.js +77 -0
  87. package/lib/aiCollab.js.map +1 -0
  88. package/lib/aiCollabApi.d.ts +173 -0
  89. package/lib/aiCollabApi.d.ts.map +1 -0
  90. package/lib/aiCollabApi.js +6 -0
  91. package/lib/aiCollabApi.js.map +1 -0
  92. package/lib/alpha.d.ts +41 -0
  93. package/lib/explicit-strategy/agentEditReducer.d.ts +12 -0
  94. package/lib/explicit-strategy/agentEditReducer.d.ts.map +1 -0
  95. package/lib/explicit-strategy/agentEditReducer.js +390 -0
  96. package/lib/explicit-strategy/agentEditReducer.js.map +1 -0
  97. package/lib/explicit-strategy/agentEditTypes.d.ts +158 -0
  98. package/lib/explicit-strategy/agentEditTypes.d.ts.map +1 -0
  99. package/lib/explicit-strategy/agentEditTypes.js +47 -0
  100. package/lib/explicit-strategy/agentEditTypes.js.map +1 -0
  101. package/lib/explicit-strategy/idGenerator.d.ts +22 -0
  102. package/lib/explicit-strategy/idGenerator.d.ts.map +1 -0
  103. package/lib/explicit-strategy/idGenerator.js +70 -0
  104. package/lib/explicit-strategy/idGenerator.js.map +1 -0
  105. package/lib/explicit-strategy/index.d.ts +51 -0
  106. package/lib/explicit-strategy/index.d.ts.map +1 -0
  107. package/lib/explicit-strategy/index.js +219 -0
  108. package/lib/explicit-strategy/index.js.map +1 -0
  109. package/lib/explicit-strategy/jsonTypes.d.ts +23 -0
  110. package/lib/explicit-strategy/jsonTypes.d.ts.map +1 -0
  111. package/lib/explicit-strategy/jsonTypes.js +6 -0
  112. package/lib/explicit-strategy/jsonTypes.js.map +1 -0
  113. package/lib/explicit-strategy/promptGeneration.d.ts +51 -0
  114. package/lib/explicit-strategy/promptGeneration.d.ts.map +1 -0
  115. package/lib/explicit-strategy/promptGeneration.js +208 -0
  116. package/lib/explicit-strategy/promptGeneration.js.map +1 -0
  117. package/lib/explicit-strategy/typeGeneration.d.ts +15 -0
  118. package/lib/explicit-strategy/typeGeneration.d.ts.map +1 -0
  119. package/lib/explicit-strategy/typeGeneration.js +260 -0
  120. package/lib/explicit-strategy/typeGeneration.js.map +1 -0
  121. package/lib/explicit-strategy/utils.d.ts +37 -0
  122. package/lib/explicit-strategy/utils.d.ts.map +1 -0
  123. package/lib/explicit-strategy/utils.js +41 -0
  124. package/lib/explicit-strategy/utils.js.map +1 -0
  125. package/lib/implicit-strategy/index.d.ts +8 -0
  126. package/lib/implicit-strategy/index.d.ts.map +1 -0
  127. package/lib/implicit-strategy/index.js +8 -0
  128. package/lib/implicit-strategy/index.js.map +1 -0
  129. package/lib/implicit-strategy/sharedTreeBranchManager.d.ts +63 -0
  130. package/lib/implicit-strategy/sharedTreeBranchManager.d.ts.map +1 -0
  131. package/lib/implicit-strategy/sharedTreeBranchManager.js +213 -0
  132. package/lib/implicit-strategy/sharedTreeBranchManager.js.map +1 -0
  133. package/lib/implicit-strategy/sharedTreeDiff.d.ts +102 -0
  134. package/lib/implicit-strategy/sharedTreeDiff.d.ts.map +1 -0
  135. package/lib/implicit-strategy/sharedTreeDiff.js +515 -0
  136. package/lib/implicit-strategy/sharedTreeDiff.js.map +1 -0
  137. package/lib/implicit-strategy/utils.d.ts +21 -0
  138. package/lib/implicit-strategy/utils.d.ts.map +1 -0
  139. package/lib/implicit-strategy/utils.js +43 -0
  140. package/lib/implicit-strategy/utils.js.map +1 -0
  141. package/lib/index.d.ts +16 -0
  142. package/lib/index.d.ts.map +1 -0
  143. package/lib/index.js +15 -0
  144. package/lib/index.js.map +1 -0
  145. package/lib/public.d.ts +19 -0
  146. package/lib/tsdoc-metadata.json +11 -0
  147. package/mocharc.cjs +14 -0
  148. package/package.json +165 -0
  149. package/prettier.config.cjs +8 -0
  150. package/src/aiCollab.ts +86 -0
  151. package/src/aiCollabApi.ts +184 -0
  152. package/src/explicit-strategy/agentEditReducer.ts +498 -0
  153. package/src/explicit-strategy/agentEditTypes.ts +177 -0
  154. package/src/explicit-strategy/idGenerator.ts +90 -0
  155. package/src/explicit-strategy/index.ts +364 -0
  156. package/src/explicit-strategy/jsonTypes.ts +27 -0
  157. package/src/explicit-strategy/promptGeneration.ts +294 -0
  158. package/src/explicit-strategy/typeGeneration.ts +374 -0
  159. package/src/explicit-strategy/utils.ts +60 -0
  160. package/src/implicit-strategy/README.md +4 -0
  161. package/src/implicit-strategy/index.ts +21 -0
  162. package/src/implicit-strategy/sharedTreeBranchManager.ts +294 -0
  163. package/src/implicit-strategy/sharedTreeDiff.ts +735 -0
  164. package/src/implicit-strategy/utils.ts +54 -0
  165. package/src/index.ts +39 -0
  166. package/tsconfig.cjs.json +7 -0
  167. package/tsconfig.json +12 -0
  168. 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
+ }