@verdant-web/common 1.11.0

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 (241) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/EventSubscriber.d.ts +17 -0
  3. package/dist/cjs/EventSubscriber.js +67 -0
  4. package/dist/cjs/EventSubscriber.js.map +1 -0
  5. package/dist/cjs/baseline.d.ts +5 -0
  6. package/dist/cjs/baseline.js +3 -0
  7. package/dist/cjs/baseline.js.map +1 -0
  8. package/dist/cjs/batching.d.ts +41 -0
  9. package/dist/cjs/batching.js +89 -0
  10. package/dist/cjs/batching.js.map +1 -0
  11. package/dist/cjs/files.d.ts +19 -0
  12. package/dist/cjs/files.js +19 -0
  13. package/dist/cjs/files.js.map +1 -0
  14. package/dist/cjs/files.test.d.ts +1 -0
  15. package/dist/cjs/files.test.js +81 -0
  16. package/dist/cjs/files.test.js.map +1 -0
  17. package/dist/cjs/index.d.ts +18 -0
  18. package/dist/cjs/index.js +36 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/indexes.d.ts +14 -0
  21. package/dist/cjs/indexes.js +116 -0
  22. package/dist/cjs/indexes.js.map +1 -0
  23. package/dist/cjs/indexes.test.d.ts +1 -0
  24. package/dist/cjs/indexes.test.js +128 -0
  25. package/dist/cjs/indexes.test.js.map +1 -0
  26. package/dist/cjs/migration.d.ts +98 -0
  27. package/dist/cjs/migration.js +128 -0
  28. package/dist/cjs/migration.js.map +1 -0
  29. package/dist/cjs/oids.d.ts +87 -0
  30. package/dist/cjs/oids.js +375 -0
  31. package/dist/cjs/oids.js.map +1 -0
  32. package/dist/cjs/oids.test.d.ts +1 -0
  33. package/dist/cjs/oids.test.js +282 -0
  34. package/dist/cjs/oids.test.js.map +1 -0
  35. package/dist/cjs/operation.d.ts +139 -0
  36. package/dist/cjs/operation.js +533 -0
  37. package/dist/cjs/operation.js.map +1 -0
  38. package/dist/cjs/operation.test.d.ts +1 -0
  39. package/dist/cjs/operation.test.js +827 -0
  40. package/dist/cjs/operation.test.js.map +1 -0
  41. package/dist/cjs/patch.d.ts +26 -0
  42. package/dist/cjs/patch.js +227 -0
  43. package/dist/cjs/patch.js.map +1 -0
  44. package/dist/cjs/presence.d.ts +27 -0
  45. package/dist/cjs/presence.js +3 -0
  46. package/dist/cjs/presence.js.map +1 -0
  47. package/dist/cjs/protocol.d.ts +145 -0
  48. package/dist/cjs/protocol.js +4 -0
  49. package/dist/cjs/protocol.js.map +1 -0
  50. package/dist/cjs/refs.d.ts +5 -0
  51. package/dist/cjs/refs.js +22 -0
  52. package/dist/cjs/refs.js.map +1 -0
  53. package/dist/cjs/replica.d.ts +28 -0
  54. package/dist/cjs/replica.js +29 -0
  55. package/dist/cjs/replica.js.map +1 -0
  56. package/dist/cjs/schema/defaults.d.ts +4 -0
  57. package/dist/cjs/schema/defaults.js +72 -0
  58. package/dist/cjs/schema/defaults.js.map +1 -0
  59. package/dist/cjs/schema/defaults.test.d.ts +1 -0
  60. package/dist/cjs/schema/defaults.test.js +21 -0
  61. package/dist/cjs/schema/defaults.test.js.map +1 -0
  62. package/dist/cjs/schema/index.d.ts +8 -0
  63. package/dist/cjs/schema/index.js +29 -0
  64. package/dist/cjs/schema/index.js.map +1 -0
  65. package/dist/cjs/schema/indexFilters.d.ts +6 -0
  66. package/dist/cjs/schema/indexFilters.js +31 -0
  67. package/dist/cjs/schema/indexFilters.js.map +1 -0
  68. package/dist/cjs/schema/types/collection.d.ts +51 -0
  69. package/dist/cjs/schema/types/collection.js +3 -0
  70. package/dist/cjs/schema/types/collection.js.map +1 -0
  71. package/dist/cjs/schema/types/compounds.d.ts +12 -0
  72. package/dist/cjs/schema/types/compounds.js +3 -0
  73. package/dist/cjs/schema/types/compounds.js.map +1 -0
  74. package/dist/cjs/schema/types/fields.d.ts +59 -0
  75. package/dist/cjs/schema/types/fields.js +3 -0
  76. package/dist/cjs/schema/types/fields.js.map +1 -0
  77. package/dist/cjs/schema/types/filters.d.ts +29 -0
  78. package/dist/cjs/schema/types/filters.js +3 -0
  79. package/dist/cjs/schema/types/filters.js.map +1 -0
  80. package/dist/cjs/schema/types/shapes.d.ts +21 -0
  81. package/dist/cjs/schema/types/shapes.js +3 -0
  82. package/dist/cjs/schema/types/shapes.js.map +1 -0
  83. package/dist/cjs/schema/types/synthetics.d.ts +28 -0
  84. package/dist/cjs/schema/types/synthetics.js +3 -0
  85. package/dist/cjs/schema/types/synthetics.js.map +1 -0
  86. package/dist/cjs/schema/types.d.ts +18 -0
  87. package/dist/cjs/schema/types.js +23 -0
  88. package/dist/cjs/schema/types.js.map +1 -0
  89. package/dist/cjs/timestamp.d.ts +48 -0
  90. package/dist/cjs/timestamp.js +244 -0
  91. package/dist/cjs/timestamp.js.map +1 -0
  92. package/dist/cjs/timestamp.test.d.ts +1 -0
  93. package/dist/cjs/timestamp.test.js +94 -0
  94. package/dist/cjs/timestamp.test.js.map +1 -0
  95. package/dist/cjs/undo.d.ts +3 -0
  96. package/dist/cjs/undo.js +199 -0
  97. package/dist/cjs/undo.js.map +1 -0
  98. package/dist/cjs/utils.d.ts +15 -0
  99. package/dist/cjs/utils.js +128 -0
  100. package/dist/cjs/utils.js.map +1 -0
  101. package/dist/cjs/utils.test.d.ts +1 -0
  102. package/dist/cjs/utils.test.js +37 -0
  103. package/dist/cjs/utils.test.js.map +1 -0
  104. package/dist/esm/EventSubscriber.d.ts +17 -0
  105. package/dist/esm/EventSubscriber.js +63 -0
  106. package/dist/esm/EventSubscriber.js.map +1 -0
  107. package/dist/esm/baseline.d.ts +5 -0
  108. package/dist/esm/baseline.js +2 -0
  109. package/dist/esm/baseline.js.map +1 -0
  110. package/dist/esm/batching.d.ts +41 -0
  111. package/dist/esm/batching.js +84 -0
  112. package/dist/esm/batching.js.map +1 -0
  113. package/dist/esm/files.d.ts +19 -0
  114. package/dist/esm/files.js +13 -0
  115. package/dist/esm/files.js.map +1 -0
  116. package/dist/esm/files.test.d.ts +1 -0
  117. package/dist/esm/files.test.js +79 -0
  118. package/dist/esm/files.test.js.map +1 -0
  119. package/dist/esm/index.d.ts +18 -0
  120. package/dist/esm/index.js +16 -0
  121. package/dist/esm/index.js.map +1 -0
  122. package/dist/esm/indexes.d.ts +14 -0
  123. package/dist/esm/indexes.js +106 -0
  124. package/dist/esm/indexes.js.map +1 -0
  125. package/dist/esm/indexes.test.d.ts +1 -0
  126. package/dist/esm/indexes.test.js +126 -0
  127. package/dist/esm/indexes.test.js.map +1 -0
  128. package/dist/esm/migration.d.ts +98 -0
  129. package/dist/esm/migration.js +122 -0
  130. package/dist/esm/migration.js.map +1 -0
  131. package/dist/esm/oids.d.ts +87 -0
  132. package/dist/esm/oids.js +348 -0
  133. package/dist/esm/oids.js.map +1 -0
  134. package/dist/esm/oids.test.d.ts +1 -0
  135. package/dist/esm/oids.test.js +280 -0
  136. package/dist/esm/oids.test.js.map +1 -0
  137. package/dist/esm/operation.d.ts +139 -0
  138. package/dist/esm/operation.js +518 -0
  139. package/dist/esm/operation.js.map +1 -0
  140. package/dist/esm/operation.test.d.ts +1 -0
  141. package/dist/esm/operation.test.js +825 -0
  142. package/dist/esm/operation.test.js.map +1 -0
  143. package/dist/esm/patch.d.ts +26 -0
  144. package/dist/esm/patch.js +223 -0
  145. package/dist/esm/patch.js.map +1 -0
  146. package/dist/esm/presence.d.ts +27 -0
  147. package/dist/esm/presence.js +2 -0
  148. package/dist/esm/presence.js.map +1 -0
  149. package/dist/esm/protocol.d.ts +145 -0
  150. package/dist/esm/protocol.js +3 -0
  151. package/dist/esm/protocol.js.map +1 -0
  152. package/dist/esm/refs.d.ts +5 -0
  153. package/dist/esm/refs.js +17 -0
  154. package/dist/esm/refs.js.map +1 -0
  155. package/dist/esm/replica.d.ts +28 -0
  156. package/dist/esm/replica.js +26 -0
  157. package/dist/esm/replica.js.map +1 -0
  158. package/dist/esm/schema/defaults.d.ts +4 -0
  159. package/dist/esm/schema/defaults.js +66 -0
  160. package/dist/esm/schema/defaults.js.map +1 -0
  161. package/dist/esm/schema/defaults.test.d.ts +1 -0
  162. package/dist/esm/schema/defaults.test.js +19 -0
  163. package/dist/esm/schema/defaults.test.js.map +1 -0
  164. package/dist/esm/schema/index.d.ts +8 -0
  165. package/dist/esm/schema/index.js +10 -0
  166. package/dist/esm/schema/index.js.map +1 -0
  167. package/dist/esm/schema/indexFilters.d.ts +6 -0
  168. package/dist/esm/schema/indexFilters.js +23 -0
  169. package/dist/esm/schema/indexFilters.js.map +1 -0
  170. package/dist/esm/schema/types/collection.d.ts +51 -0
  171. package/dist/esm/schema/types/collection.js +2 -0
  172. package/dist/esm/schema/types/collection.js.map +1 -0
  173. package/dist/esm/schema/types/compounds.d.ts +12 -0
  174. package/dist/esm/schema/types/compounds.js +2 -0
  175. package/dist/esm/schema/types/compounds.js.map +1 -0
  176. package/dist/esm/schema/types/fields.d.ts +59 -0
  177. package/dist/esm/schema/types/fields.js +2 -0
  178. package/dist/esm/schema/types/fields.js.map +1 -0
  179. package/dist/esm/schema/types/filters.d.ts +29 -0
  180. package/dist/esm/schema/types/filters.js +2 -0
  181. package/dist/esm/schema/types/filters.js.map +1 -0
  182. package/dist/esm/schema/types/shapes.d.ts +21 -0
  183. package/dist/esm/schema/types/shapes.js +2 -0
  184. package/dist/esm/schema/types/shapes.js.map +1 -0
  185. package/dist/esm/schema/types/synthetics.d.ts +28 -0
  186. package/dist/esm/schema/types/synthetics.js +2 -0
  187. package/dist/esm/schema/types/synthetics.js.map +1 -0
  188. package/dist/esm/schema/types.d.ts +18 -0
  189. package/dist/esm/schema/types.js +7 -0
  190. package/dist/esm/schema/types.js.map +1 -0
  191. package/dist/esm/timestamp.d.ts +48 -0
  192. package/dist/esm/timestamp.js +229 -0
  193. package/dist/esm/timestamp.js.map +1 -0
  194. package/dist/esm/timestamp.test.d.ts +1 -0
  195. package/dist/esm/timestamp.test.js +92 -0
  196. package/dist/esm/timestamp.test.js.map +1 -0
  197. package/dist/esm/undo.d.ts +3 -0
  198. package/dist/esm/undo.js +195 -0
  199. package/dist/esm/undo.js.map +1 -0
  200. package/dist/esm/utils.d.ts +15 -0
  201. package/dist/esm/utils.js +114 -0
  202. package/dist/esm/utils.js.map +1 -0
  203. package/dist/esm/utils.test.d.ts +1 -0
  204. package/dist/esm/utils.test.js +35 -0
  205. package/dist/esm/utils.test.js.map +1 -0
  206. package/dist/tsconfig-cjs.tsbuildinfo +1 -0
  207. package/dist/tsconfig.tsbuildinfo +1 -0
  208. package/package.json +38 -0
  209. package/src/EventSubscriber.ts +81 -0
  210. package/src/baseline.ts +9 -0
  211. package/src/batching.ts +135 -0
  212. package/src/files.ts +34 -0
  213. package/src/index.ts +26 -0
  214. package/src/indexes.test.ts +158 -0
  215. package/src/indexes.ts +151 -0
  216. package/src/migration.ts +391 -0
  217. package/src/oids.test.ts +395 -0
  218. package/src/oids.ts +442 -0
  219. package/src/operation.test.ts +966 -0
  220. package/src/operation.ts +755 -0
  221. package/src/patch.ts +277 -0
  222. package/src/presence.ts +29 -0
  223. package/src/protocol.ts +184 -0
  224. package/src/refs.ts +16 -0
  225. package/src/replica.ts +29 -0
  226. package/src/schema/defaults.test.ts +19 -0
  227. package/src/schema/defaults.ts +72 -0
  228. package/src/schema/index.ts +28 -0
  229. package/src/schema/indexFilters.ts +49 -0
  230. package/src/schema/types/collection.ts +133 -0
  231. package/src/schema/types/compounds.ts +33 -0
  232. package/src/schema/types/fields.ts +85 -0
  233. package/src/schema/types/filters.ts +39 -0
  234. package/src/schema/types/shapes.ts +83 -0
  235. package/src/schema/types/synthetics.ts +45 -0
  236. package/src/schema/types.ts +28 -0
  237. package/src/timestamp.test.ts +113 -0
  238. package/src/timestamp.ts +284 -0
  239. package/src/undo.ts +205 -0
  240. package/src/utils.test.ts +38 -0
  241. package/src/utils.ts +137 -0
@@ -0,0 +1,755 @@
1
+ import { DocumentBaseline } from './baseline.js';
2
+ import { FileRef } from './files.js';
3
+ import {
4
+ assignOid,
5
+ assignOidsToAllSubObjects,
6
+ createRef,
7
+ decomposeOid,
8
+ ensureOid,
9
+ getOid,
10
+ getOidRoot,
11
+ isOidKey,
12
+ KeyPath,
13
+ maybeGetOid,
14
+ normalize,
15
+ ObjectIdentifier,
16
+ OID_KEY,
17
+ removeOid,
18
+ } from './oids.js';
19
+ import { compareRefs, isRef } from './refs.js';
20
+ import { isObject, assert, cloneDeep, findLastIndex } from './utils.js';
21
+
22
+ // export type ObjectIdentifier<
23
+ // CollectionName extends string = string,
24
+ // DocumentId extends string = string,
25
+ // DocumentOid extends string = string,
26
+ // > =
27
+ // | `${CollectionName}/${DocumentId}:${DocumentOid}`
28
+ // | `${CollectionName}/${DocumentId}:${DocumentOid}#${string}`;
29
+
30
+ export type ObjectRef = {
31
+ '@@type': 'ref';
32
+ id: ObjectIdentifier;
33
+ };
34
+
35
+ export function isObjectRef(obj: any): obj is ObjectRef {
36
+ return obj && typeof obj === 'object' && obj['@@type'] === 'ref';
37
+ }
38
+
39
+ export type Normalized<T> = {
40
+ [key in keyof T]: T[key] extends Object ? ObjectRef : T[key];
41
+ };
42
+
43
+ // patches v2
44
+
45
+ export type PropertyName = string | number;
46
+ /**
47
+ * List patches can target a particular child list or
48
+ * nested lists. The first path item is the property path
49
+ * of the first child list, any subsequent values are nested
50
+ * list indices.
51
+ */
52
+ export type PropertyValue =
53
+ | string
54
+ | number
55
+ | boolean
56
+ | null
57
+ | undefined
58
+ | ObjectRef;
59
+
60
+ // all patches refer to a specific sub-object.
61
+ interface BaseOperationPatch {}
62
+
63
+ export interface OperationPatchInitialize extends BaseOperationPatch {
64
+ op: 'initialize';
65
+ value: any;
66
+ }
67
+ export interface OperationPatchSet extends BaseOperationPatch {
68
+ op: 'set';
69
+ name: PropertyName;
70
+ value: PropertyValue;
71
+ }
72
+ export interface OperationPatchRemove extends BaseOperationPatch {
73
+ op: 'remove';
74
+ name: PropertyName;
75
+ }
76
+ export interface OperationPatchListPush extends BaseOperationPatch {
77
+ op: 'list-push';
78
+ value: PropertyValue;
79
+ }
80
+ export interface OperationPatchListInsert extends BaseOperationPatch {
81
+ op: 'list-insert';
82
+ index: number;
83
+ value?: PropertyValue;
84
+ values?: PropertyValue[];
85
+ }
86
+ export interface OperationPatchListDelete extends BaseOperationPatch {
87
+ op: 'list-delete';
88
+ index: number;
89
+ count: number;
90
+ }
91
+ /**
92
+ * Optimal for lists of object references. Moves
93
+ * the selected item to the target index even if it
94
+ * is not at the original index anymore.
95
+ */
96
+ export interface OperationPatchListMoveByRef extends BaseOperationPatch {
97
+ op: 'list-move-by-ref';
98
+ value: ObjectRef;
99
+ index: number;
100
+ }
101
+ /**
102
+ * Suitable for any list move, whether object lists
103
+ * or primitive lists.
104
+ */
105
+ export interface OperationPatchListMoveByIndex extends BaseOperationPatch {
106
+ op: 'list-move-by-index';
107
+ from: number;
108
+ to: number;
109
+ }
110
+ /**
111
+ * Removes all instances of the value from
112
+ * the list. Good for set behavior or removing
113
+ * a specific item even if it changes index
114
+ * from conflicts.
115
+ */
116
+ export interface OperationPatchListRemove extends BaseOperationPatch {
117
+ op: 'list-remove';
118
+ value: PropertyValue;
119
+ only?: 'first' | 'last';
120
+ }
121
+
122
+ export interface OperationPatchListAdd extends BaseOperationPatch {
123
+ op: 'list-add';
124
+ value: PropertyValue;
125
+ }
126
+
127
+ export interface OperationPatchDelete extends BaseOperationPatch {
128
+ op: 'delete';
129
+ }
130
+
131
+ export type OperationPatch =
132
+ | OperationPatchInitialize
133
+ | OperationPatchSet
134
+ | OperationPatchRemove
135
+ | OperationPatchListPush
136
+ | OperationPatchListInsert
137
+ | OperationPatchListDelete
138
+ | OperationPatchListMoveByRef
139
+ | OperationPatchListMoveByIndex
140
+ | OperationPatchListRemove
141
+ | OperationPatchDelete
142
+ | OperationPatchListAdd;
143
+
144
+ export type Operation = {
145
+ oid: ObjectIdentifier;
146
+ timestamp: string;
147
+ data: OperationPatch;
148
+ };
149
+
150
+ function isDiffableObject(val: any) {
151
+ return isObject(val) && !isRef(val);
152
+ }
153
+
154
+ function compareNonDiffable(a: any, b: any) {
155
+ if (a === b) return true;
156
+ if (isRef(a) && isRef(b)) return compareRefs(a, b);
157
+ return false;
158
+ }
159
+
160
+ export function diffToPatches<T extends { [key: string]: any } | any[]>(
161
+ from: T,
162
+ to: T,
163
+ getNow: () => string,
164
+ keyPath: KeyPath,
165
+ createSubId?: () => string,
166
+ patches: Operation[] = [],
167
+ options: {
168
+ /**
169
+ * If an object is merged with another and the new one does not
170
+ * have an OID assigned, assume it is the same identity as previous
171
+ */
172
+ mergeUnknownObjects?: boolean;
173
+ /**
174
+ * If an incoming value is not assigned on the new object, use the previous value.
175
+ * If false, undefined properties will erase the previous value.
176
+ */
177
+ defaultUndefined?: boolean;
178
+ } = {},
179
+ ): Operation[] {
180
+ const oid = getOid(from);
181
+
182
+ function diffItems(key: string | number, value: any, oldValue: any) {
183
+ if (!isDiffableObject(value)) {
184
+ // for primitive fields, we can use plain sets and
185
+ // do not need to recurse, of course
186
+ if (!compareNonDiffable(value, oldValue)) {
187
+ patches.push({
188
+ oid,
189
+ timestamp: getNow(),
190
+ data: {
191
+ op: 'set',
192
+ name: key,
193
+ value,
194
+ },
195
+ });
196
+ }
197
+ } else {
198
+ const oldValueOid = maybeGetOid(oldValue);
199
+ let valueOid;
200
+ if (!options.mergeUnknownObjects) {
201
+ valueOid = ensureOid(value, oid, key, createSubId);
202
+ } else {
203
+ // if merge unknown objects is requested, we copy the previous value's oid
204
+ // to any mirrored new value if it doesn't have one assigned already.
205
+ valueOid = maybeGetOid(value);
206
+ if (!valueOid && oldValueOid) {
207
+ assignOid(value, oldValueOid);
208
+ valueOid = oldValueOid;
209
+ } else {
210
+ valueOid = ensureOid(value, oid, key, createSubId);
211
+ }
212
+ }
213
+
214
+ if (oldValue === undefined || valueOid !== oldValueOid) {
215
+ // first case: previous value exists but the OIDs are different,
216
+ // meaning the object identity has changed
217
+ // we add the whole new object and also update the reference on this
218
+ // key of the parent to point to the new object
219
+ initialToPatches(value, valueOid, getNow, createSubId, patches);
220
+ patches.push({
221
+ oid,
222
+ timestamp: getNow(),
223
+ data: {
224
+ op: 'set',
225
+ name: key,
226
+ value: createRef(valueOid),
227
+ },
228
+ });
229
+ // if there was an old value, we need to delete it altogether.
230
+ if (oldValueOid !== undefined) {
231
+ patches.push({
232
+ oid: oldValueOid,
233
+ timestamp: getNow(),
234
+ data: {
235
+ op: 'delete',
236
+ },
237
+ });
238
+ }
239
+ } else {
240
+ // third case: OIDs are the same, meaning the identity is the same,
241
+ // and we must diff the objects
242
+ diffToPatches(
243
+ oldValue,
244
+ value,
245
+ getNow,
246
+ [...keyPath, key],
247
+ createSubId,
248
+ patches,
249
+ options,
250
+ );
251
+ }
252
+ }
253
+ }
254
+
255
+ if (Array.isArray(from) && Array.isArray(to)) {
256
+ // diffing is more naive than native array operations.
257
+ // we can only look at each element and decide if it should
258
+ // be replaced or removed - no moves or pushes, etc.
259
+ for (let i = 0; i < to.length; i++) {
260
+ const value = to[i];
261
+ const oldValue = from[i];
262
+ diffItems(i, value, oldValue);
263
+ }
264
+ // remove any remaining items at the end of the array
265
+ const deletedItemsAtEnd = from.length - to.length;
266
+ if (deletedItemsAtEnd > 0) {
267
+ // if sub-items were objects, we need to delete them all
268
+ for (let i = to.length; i < from.length; i++) {
269
+ const value = from[i];
270
+ const valueOid = maybeGetOid(value);
271
+ if (valueOid) {
272
+ patches.push({
273
+ oid: valueOid,
274
+ timestamp: getNow(),
275
+ data: {
276
+ op: 'delete',
277
+ },
278
+ });
279
+ }
280
+ }
281
+ // push the list-delete for the deleted items
282
+ patches.push({
283
+ oid,
284
+ timestamp: getNow(),
285
+ data: {
286
+ op: 'list-delete',
287
+ index: to.length,
288
+ count: deletedItemsAtEnd,
289
+ },
290
+ });
291
+ }
292
+ } else if (Array.isArray(from) || Array.isArray(to)) {
293
+ throw new Error('Cannot diff an array with an object');
294
+ } else if (isDiffableObject(from) && isDiffableObject(to)) {
295
+ const oldKeys = new Set(Object.keys(from));
296
+ for (const [key, value] of Object.entries(to)) {
297
+ if (value === undefined && options.defaultUndefined) continue;
298
+
299
+ oldKeys.delete(key);
300
+
301
+ if (isOidKey(key)) continue;
302
+
303
+ const oldValue = from[key];
304
+
305
+ diffItems(key, value, oldValue);
306
+ }
307
+
308
+ // this set now only contains keys which were not in the new object
309
+ if (!options.defaultUndefined) {
310
+ for (const key of oldKeys) {
311
+ if (isOidKey(key)) continue;
312
+ // push the delete for the property
313
+ patches.push({
314
+ oid,
315
+ timestamp: getNow(),
316
+ data: {
317
+ op: 'remove',
318
+ name: key,
319
+ },
320
+ });
321
+ }
322
+ }
323
+ }
324
+
325
+ return patches;
326
+ }
327
+
328
+ export function shallowDiffToPatches(
329
+ from: any,
330
+ to: any,
331
+ getNow: () => string,
332
+ patches: Operation[] = [],
333
+ ) {
334
+ const oid = getOid(from);
335
+
336
+ function diffItems(key: string | number, value: any, oldValue: any) {
337
+ if (!isDiffableObject(value)) {
338
+ // for primitive fields, we can use plain sets and
339
+ // do not need to recurse, of course
340
+ if (!compareNonDiffable(value, oldValue)) {
341
+ patches.push({
342
+ oid,
343
+ timestamp: getNow(),
344
+ data: {
345
+ op: 'set',
346
+ name: key,
347
+ value,
348
+ },
349
+ });
350
+ }
351
+ } else {
352
+ throw new Error(
353
+ 'Shallow diff was given a nested object. This is an error in verdant!',
354
+ );
355
+ }
356
+ }
357
+
358
+ if (Array.isArray(from) && Array.isArray(to)) {
359
+ // diffing is more naive than native array operations.
360
+ // we can only look at each element and decide if it should
361
+ // be replaced or removed - no moves or pushes, etc.
362
+ for (let i = 0; i < to.length; i++) {
363
+ const value = to[i];
364
+ const oldValue = from[i];
365
+ diffItems(i, value, oldValue);
366
+ }
367
+ // remove any remaining items at the end of the array
368
+ const deletedItemsAtEnd = from.length - to.length;
369
+ if (deletedItemsAtEnd > 0) {
370
+ // push the list-delete for the deleted items
371
+ patches.push({
372
+ oid,
373
+ timestamp: getNow(),
374
+ data: {
375
+ op: 'list-delete',
376
+ index: to.length,
377
+ count: deletedItemsAtEnd,
378
+ },
379
+ });
380
+ }
381
+ } else if (Array.isArray(from) || Array.isArray(to)) {
382
+ throw new Error('Cannot diff an array with an object');
383
+ } else if (isDiffableObject(from) && isDiffableObject(to)) {
384
+ const oldKeys = new Set(Object.keys(from));
385
+ for (const [key, value] of Object.entries(to)) {
386
+ oldKeys.delete(key);
387
+
388
+ if (isOidKey(key)) continue;
389
+
390
+ const oldValue = from[key];
391
+
392
+ diffItems(key, value, oldValue);
393
+ }
394
+
395
+ // this set now only contains keys which were not in the new object
396
+ for (const key of oldKeys) {
397
+ if (isOidKey(key)) continue;
398
+ // push the delete for the property
399
+ patches.push({
400
+ oid,
401
+ timestamp: getNow(),
402
+ data: {
403
+ op: 'remove',
404
+ name: key,
405
+ },
406
+ });
407
+ }
408
+ }
409
+
410
+ return patches;
411
+ }
412
+
413
+ /**
414
+ * Takes a basic object and constructs a patch list to create it and
415
+ * all of its nested objects.
416
+ */
417
+ export function initialToPatches(
418
+ initial: any,
419
+ rootOid: ObjectIdentifier,
420
+ getNow: () => string,
421
+ createSubId?: () => string,
422
+ patches: Operation[] = [],
423
+ ) {
424
+ assignOid(initial, rootOid);
425
+ assignOidsToAllSubObjects(initial, createSubId);
426
+ const normalized = normalize(initial);
427
+ for (const key of normalized.keys()) {
428
+ const value = normalized.get(key);
429
+ patches.push({
430
+ oid: key,
431
+ timestamp: getNow(),
432
+ data: {
433
+ op: 'initialize',
434
+ value: removeOid(value),
435
+ },
436
+ });
437
+ }
438
+ return patches;
439
+ }
440
+
441
+ export function shallowInitialToPatches(
442
+ initial: any,
443
+ rootOid: ObjectIdentifier,
444
+ getNow: () => string,
445
+ patches: Operation[] = [],
446
+ ) {
447
+ patches.push({
448
+ oid: rootOid,
449
+ timestamp: getNow(),
450
+ data: {
451
+ op: 'initialize',
452
+ value: initial,
453
+ },
454
+ });
455
+ return patches;
456
+ }
457
+
458
+ export function groupPatchesByIdentifier(patches: Operation[]) {
459
+ const grouped: Record<ObjectIdentifier, Operation[]> = {};
460
+ for (const patch of patches) {
461
+ if (patch.oid in grouped) {
462
+ grouped[patch.oid].push(patch);
463
+ } else {
464
+ grouped[patch.oid] = [patch];
465
+ }
466
+ }
467
+ return grouped;
468
+ }
469
+
470
+ export function groupPatchesByRootOid(patches: Operation[]) {
471
+ const grouped: Record<ObjectIdentifier, Operation[]> = {};
472
+ for (const patch of patches) {
473
+ const root = getOidRoot(patch.oid);
474
+ if (root in grouped) {
475
+ grouped[root].push(patch);
476
+ } else {
477
+ grouped[root] = [patch];
478
+ }
479
+ }
480
+ return grouped;
481
+ }
482
+
483
+ export function groupBaselinesByRootOid(baselines: DocumentBaseline[]) {
484
+ const grouped: Record<ObjectIdentifier, DocumentBaseline[]> = {};
485
+ for (const patch of baselines) {
486
+ const root = getOidRoot(patch.oid);
487
+ if (root in grouped) {
488
+ grouped[root].push(patch);
489
+ } else {
490
+ grouped[root] = [patch];
491
+ }
492
+ }
493
+ return grouped;
494
+ }
495
+
496
+ export type NormalizedObject =
497
+ | {
498
+ [key: PropertyName]: PropertyValue;
499
+ }
500
+ | Array<PropertyValue>;
501
+
502
+ function listCheck(obj: any): obj is Array<unknown> {
503
+ if (!Array.isArray(obj)) {
504
+ console.error(
505
+ `Cannot apply list patch; expected array, received ${JSON.stringify(
506
+ obj,
507
+ )}. This suggests your data is changing from a list to an object over time. (OID: ${maybeGetOid(
508
+ obj,
509
+ )})`,
510
+ );
511
+ return false;
512
+ } else {
513
+ return true;
514
+ }
515
+ }
516
+
517
+ /**
518
+ * The incoming object should already be normalized!
519
+ * This function will mutate the base object.
520
+ */
521
+ export function applyPatch<T extends NormalizedObject>(
522
+ base: T | undefined,
523
+ patch: OperationPatch,
524
+ /**
525
+ * Optionally supply a list to which any refs which
526
+ * are removed during the patch will be appended.
527
+ */
528
+ deletedRefs?: (FileRef | ObjectRef)[],
529
+ ): T | undefined {
530
+ // deleted objects are represented by undefined
531
+ // and remain deleted unless re-initialized
532
+ if ((base === undefined || base === null) && patch.op !== 'initialize') {
533
+ return base;
534
+ }
535
+ // ditch typing internally.
536
+ const baseAsAny = base as any;
537
+ let index;
538
+ let spliceResult: any[];
539
+
540
+ // a helper, pass it a value which is about
541
+ // to be removed and it will append it to the list
542
+ // if it was a ref
543
+ function checkRef(field: any) {
544
+ // don't bother if not supplied
545
+ if (!deletedRefs) return;
546
+ if (isRef(field)) {
547
+ deletedRefs.push(field);
548
+ }
549
+ }
550
+
551
+ switch (patch.op) {
552
+ case 'set':
553
+ checkRef(baseAsAny[patch.name]);
554
+ baseAsAny[patch.name] = patch.value;
555
+ break;
556
+ case 'remove':
557
+ checkRef(baseAsAny[patch.name]);
558
+ delete baseAsAny[patch.name];
559
+ break;
560
+ case 'list-push':
561
+ if (listCheck(base)) {
562
+ base.push(patch.value);
563
+ }
564
+ break;
565
+ case 'list-delete':
566
+ if (listCheck(base)) {
567
+ checkRef(base[patch.index]);
568
+ base.splice(patch.index, patch.count);
569
+ }
570
+ break;
571
+ case 'list-move-by-index':
572
+ if (listCheck(base)) {
573
+ spliceResult = base.splice(patch.from, 1);
574
+ base.splice(patch.to, 0, spliceResult[0]);
575
+ }
576
+ break;
577
+ case 'list-remove':
578
+ if (listCheck(base)) {
579
+ do {
580
+ const valueToRemove = patch.value;
581
+ if (patch.only === 'last') {
582
+ if (isObjectRef(valueToRemove)) {
583
+ index = findLastIndex(
584
+ base,
585
+ (item: any) => item.id === valueToRemove.id,
586
+ );
587
+ } else {
588
+ index = base.lastIndexOf(valueToRemove);
589
+ }
590
+ } else {
591
+ if (isObjectRef(valueToRemove)) {
592
+ index = base.findIndex(
593
+ (item: any) => item.id === valueToRemove.id,
594
+ );
595
+ } else {
596
+ index = base.indexOf(valueToRemove);
597
+ }
598
+ }
599
+ if (index !== -1) {
600
+ checkRef(base[index]);
601
+ base.splice(index, 1);
602
+ }
603
+ } while (!patch.only && index !== -1);
604
+ }
605
+ break;
606
+ case 'list-add':
607
+ if (listCheck(base)) {
608
+ const alreadyHas = base.some((item: any) => {
609
+ if (isObjectRef(item) && isObjectRef(patch.value)) {
610
+ return item.id === patch.value.id;
611
+ } else {
612
+ return item === patch.value;
613
+ }
614
+ });
615
+ if (!alreadyHas) {
616
+ base.push(patch.value);
617
+ }
618
+ }
619
+ break;
620
+ case 'list-move-by-ref':
621
+ if (listCheck(base)) {
622
+ index = base.indexOf(patch.value);
623
+ spliceResult = base.splice(index, 1);
624
+ base.splice(patch.index, 0, spliceResult[0]);
625
+ }
626
+ break;
627
+ case 'list-insert':
628
+ if (listCheck(base)) {
629
+ if (!patch.value && !patch.values) {
630
+ throw new Error(
631
+ `Cannot apply list insert patch; expected value or values, received ${JSON.stringify(
632
+ patch,
633
+ )}`,
634
+ );
635
+ }
636
+ if (patch.value) {
637
+ base.splice(patch.index, 0, patch.value);
638
+ } else {
639
+ base.splice(patch.index, 0, ...patch.values!);
640
+ }
641
+ }
642
+ break;
643
+ case 'delete':
644
+ // collect all refs that are deleted
645
+ if (Array.isArray(base)) {
646
+ base.forEach(checkRef);
647
+ } else if (isObject(base)) {
648
+ Object.values(base || {}).forEach(checkRef);
649
+ }
650
+ return undefined;
651
+ case 'initialize':
652
+ return cloneDeep(patch.value);
653
+ default:
654
+ throw new Error(`Unsupported patch operation: ${(patch as any).op}`);
655
+ }
656
+ return base;
657
+ }
658
+
659
+ export function applyOperations<T extends NormalizedObject>(
660
+ base: T | undefined,
661
+ operations: Operation[],
662
+ deletedRefs?: (ObjectRef | FileRef)[],
663
+ ): T | undefined {
664
+ let cur = base as T | undefined;
665
+ for (const op of operations) {
666
+ cur = applyPatch(cur, op.data, deletedRefs);
667
+ }
668
+ return cur;
669
+ }
670
+
671
+ /**
672
+ * Mutates the original object in place. Returns all referenced
673
+ * objects oids.
674
+ */
675
+ export function substituteRefsWithObjects(
676
+ base: any,
677
+ refs: Map<ObjectIdentifier, any>,
678
+ used: ObjectIdentifier[] = [],
679
+ ): ObjectIdentifier[] {
680
+ if (Array.isArray(base)) {
681
+ for (let i = 0; i < base.length; i++) {
682
+ const item = base[i];
683
+ base[i] = dereference(item, refs, used);
684
+ if (isObject(base[i])) {
685
+ substituteRefsWithObjects(base[i], refs, used);
686
+ }
687
+ }
688
+ } else if (isObject(base)) {
689
+ // not sure where to put this assertion but it's important to make
690
+ // sure all nested objects include an OID
691
+ assert(
692
+ maybeGetOid(base),
693
+ `Object ${JSON.stringify(base)} must have an oid`,
694
+ );
695
+ for (const key of Object.keys(base)) {
696
+ base[key] = dereference(base[key], refs, used);
697
+
698
+ // now that objects are in place, recursively substitute
699
+ if (isObject(base[key])) {
700
+ substituteRefsWithObjects(base[key], refs, used);
701
+ }
702
+ }
703
+ }
704
+
705
+ return used;
706
+ }
707
+
708
+ function dereference(
709
+ input: any,
710
+ refs: Map<ObjectIdentifier, any>,
711
+ used: ObjectIdentifier[],
712
+ ): any {
713
+ if (isObjectRef(input)) {
714
+ used.push(input.id);
715
+ const resolved = refs.get(input.id);
716
+ assert(!!resolved, `No value was found in object map for ${input.id}`);
717
+ return assignOid(resolved, input.id);
718
+ } else {
719
+ return input;
720
+ }
721
+ }
722
+
723
+ export function substituteFirstLevelObjectsWithRefs<
724
+ Base extends { [key: string]: any } | any[],
725
+ >(
726
+ base: Base,
727
+ refObjects: Map<ObjectIdentifier, any> = new Map(),
728
+ ): Map<ObjectIdentifier, any> {
729
+ if (Array.isArray(base)) {
730
+ for (let i = 0; i < base.length; i++) {
731
+ const item = base[i];
732
+ if (isObject(item)) {
733
+ const oid = getOid(item);
734
+ base[i] = {
735
+ '@@type': 'ref',
736
+ id: oid,
737
+ };
738
+ refObjects.set(oid, item);
739
+ }
740
+ }
741
+ } else {
742
+ for (const [key, value] of Object.entries(base)) {
743
+ if (isObject(value)) {
744
+ base[key] = {
745
+ '@@type': 'ref',
746
+ id: getOid(value),
747
+ };
748
+
749
+ refObjects.set(getOid(value), value);
750
+ }
751
+ }
752
+ }
753
+
754
+ return refObjects;
755
+ }