@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,966 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createFileRef, FileRef } from './files.js';
3
+ import {
4
+ assignOid,
5
+ assignOidsToAllSubObjects,
6
+ createRef,
7
+ getOid,
8
+ OID_KEY,
9
+ } from './oids.js';
10
+ import {
11
+ applyPatch,
12
+ applyOperations,
13
+ diffToPatches,
14
+ initialToPatches,
15
+ substituteRefsWithObjects,
16
+ ObjectRef,
17
+ } from './operation.js';
18
+
19
+ function createClock(init = 0) {
20
+ let i = init;
21
+ return () => (i++).toString();
22
+ }
23
+
24
+ describe('creating diff patch operations', () => {
25
+ describe('on flat objects', () => {
26
+ it('generates and applies set and remove operations', () => {
27
+ const from = { foo: 'bar', baz: 'qux', zing: 1 };
28
+ assignOid(from, 'test/a');
29
+ const to = { foo: 'bar', baz: 'pop' };
30
+ assignOid(to, 'test/a');
31
+ const ops = diffToPatches(from, to, createClock(), []);
32
+ expect(ops).toMatchInlineSnapshot(`
33
+ [
34
+ {
35
+ "data": {
36
+ "name": "baz",
37
+ "op": "set",
38
+ "value": "pop",
39
+ },
40
+ "oid": "test/a",
41
+ "timestamp": "0",
42
+ },
43
+ {
44
+ "data": {
45
+ "name": "zing",
46
+ "op": "remove",
47
+ },
48
+ "oid": "test/a",
49
+ "timestamp": "1",
50
+ },
51
+ ]
52
+ `);
53
+ const result = applyOperations(from, ops);
54
+ expect(result).toEqual(to);
55
+ });
56
+ });
57
+ describe('on nested objects', () => {
58
+ it('replaces whole nested objects', () => {
59
+ const from = assignOid(
60
+ {
61
+ foo: 'bar',
62
+ baz: assignOid({ qux: 'corge' }, 'test/a:0'),
63
+ },
64
+ 'test/a',
65
+ );
66
+ const to = assignOid(
67
+ {
68
+ foo: 'bar',
69
+ baz: assignOid({ qux: 'grault' }, 'test/a:1'),
70
+ },
71
+ 'test/a',
72
+ );
73
+ const patches = diffToPatches(from, to, createClock(), []);
74
+ expect(patches).toMatchInlineSnapshot(`
75
+ [
76
+ {
77
+ "data": {
78
+ "op": "initialize",
79
+ "value": {
80
+ "qux": "grault",
81
+ },
82
+ },
83
+ "oid": "test/a:1",
84
+ "timestamp": "0",
85
+ },
86
+ {
87
+ "data": {
88
+ "name": "baz",
89
+ "op": "set",
90
+ "value": {
91
+ "@@type": "ref",
92
+ "id": "test/a:1",
93
+ },
94
+ },
95
+ "oid": "test/a",
96
+ "timestamp": "1",
97
+ },
98
+ {
99
+ "data": {
100
+ "op": "delete",
101
+ },
102
+ "oid": "test/a:0",
103
+ "timestamp": "2",
104
+ },
105
+ ]
106
+ `);
107
+ });
108
+ it('replaces whole nested objects with different ids even if fields are the same', () => {
109
+ const from = {
110
+ foo: 'bar',
111
+ baz: { qux: 'corge' },
112
+ };
113
+ assignOid(from, 'test/a');
114
+ assignOid(from.baz, 'test/a:0');
115
+ const to = {
116
+ foo: 'bar',
117
+ baz: { qux: 'corge' },
118
+ };
119
+ assignOid(to, 'test/a');
120
+ assignOid(to.baz, 'test/a:1');
121
+ const patches = diffToPatches(from, to, createClock(), []);
122
+ expect(patches).toMatchInlineSnapshot(`
123
+ [
124
+ {
125
+ "data": {
126
+ "op": "initialize",
127
+ "value": {
128
+ "qux": "corge",
129
+ },
130
+ },
131
+ "oid": "test/a:1",
132
+ "timestamp": "0",
133
+ },
134
+ {
135
+ "data": {
136
+ "name": "baz",
137
+ "op": "set",
138
+ "value": {
139
+ "@@type": "ref",
140
+ "id": "test/a:1",
141
+ },
142
+ },
143
+ "oid": "test/a",
144
+ "timestamp": "1",
145
+ },
146
+ {
147
+ "data": {
148
+ "op": "delete",
149
+ },
150
+ "oid": "test/a:0",
151
+ "timestamp": "2",
152
+ },
153
+ ]
154
+ `);
155
+ });
156
+ it('does not replace objects with the same identity and same fields', () => {
157
+ const from = {
158
+ foo: 'bar',
159
+ baz: { qux: 'corge' },
160
+ };
161
+ assignOid(from, 'test/a');
162
+ assignOid(from.baz, 'test/a:0');
163
+ const to = {
164
+ foo: 'bar',
165
+ baz: { qux: 'corge' },
166
+ };
167
+ assignOid(to, 'test/a');
168
+ assignOid(to.baz, 'test/a:0');
169
+ const patches = diffToPatches(from, to, createClock(), []);
170
+ expect(patches).toEqual([]);
171
+ });
172
+ it('patches sub-objects with same identity', () => {
173
+ const from = {
174
+ foo: 'bar',
175
+ baz: { qux: 'corge' },
176
+ };
177
+ assignOid(from, 'test/a');
178
+ assignOid(from.baz, 'test/a:0');
179
+ const to = {
180
+ foo: 'bar',
181
+ baz: { qux: 'fig' },
182
+ };
183
+ assignOid(to, 'test/a');
184
+ assignOid(to.baz, 'test/a:0');
185
+ const patches = diffToPatches(from, to, createClock(), []);
186
+ expect(patches).toMatchInlineSnapshot(`
187
+ [
188
+ {
189
+ "data": {
190
+ "name": "qux",
191
+ "op": "set",
192
+ "value": "fig",
193
+ },
194
+ "oid": "test/a:0",
195
+ "timestamp": "0",
196
+ },
197
+ ]
198
+ `);
199
+ });
200
+ it('retains keys which are undefined in new object if defaultUndefined is set', () => {
201
+ const from = {
202
+ foo: 'bar',
203
+ baz: { qux: 'corge' },
204
+ };
205
+ assignOid(from, 'test/a');
206
+ assignOid(from.baz, 'test/a:0');
207
+ const to = {
208
+ foo: 'bip',
209
+ };
210
+ assignOid(to, 'test/a');
211
+ const patches = diffToPatches(
212
+ from,
213
+ to,
214
+ createClock(),
215
+ [],
216
+ undefined,
217
+ [],
218
+ {
219
+ defaultUndefined: true,
220
+ mergeUnknownObjects: true,
221
+ },
222
+ );
223
+ expect(patches).toMatchInlineSnapshot(`
224
+ [
225
+ {
226
+ "data": {
227
+ "name": "foo",
228
+ "op": "set",
229
+ "value": "bip",
230
+ },
231
+ "oid": "test/a",
232
+ "timestamp": "0",
233
+ },
234
+ ]
235
+ `);
236
+ });
237
+ });
238
+ describe('on lists of primitives', () => {
239
+ it('pushes items', async () => {
240
+ expect(
241
+ applyOperations(assignOid(['foo', 'bar'], 'test/a'), [
242
+ {
243
+ oid: 'test/a',
244
+ timestamp: 'meh',
245
+ data: {
246
+ op: 'list-push',
247
+ value: 'baz',
248
+ },
249
+ },
250
+ ]),
251
+ ).toEqual(assignOid(['foo', 'bar', 'baz'], 'test/a'));
252
+ });
253
+ it.todo('sets items');
254
+ it.todo('inserts items');
255
+ it.todo('removes items by index');
256
+ it.todo('removes items by value');
257
+ it.todo('moves items by index');
258
+ it.todo('moves items by value');
259
+ });
260
+ describe.todo('on lists of objects', () => {
261
+ it.todo('pushes items');
262
+ it.todo('moves objects by identity');
263
+ it.todo('removes objects by identity');
264
+ });
265
+ describe.todo('on lists of lists', () => {
266
+ it.todo('rejects operations by value or identity');
267
+ });
268
+ it('should work for complex nested arrays and objects', () => {
269
+ const from = {
270
+ foo: {
271
+ bar: [1, 2, 3],
272
+ },
273
+ baz: [
274
+ {
275
+ corge: true,
276
+ },
277
+ ],
278
+ };
279
+ assignOid(from, 'test/a');
280
+ assignOid(from.foo, 'test/a:1');
281
+ assignOid(from.foo.bar, 'test/a:2');
282
+ assignOid(from.baz, 'test/a:3');
283
+ assignOid(from.baz[0], 'test/a:4');
284
+
285
+ const to = {
286
+ foo: {
287
+ bar: [1, 2],
288
+ bop: [0],
289
+ },
290
+ baz: [
291
+ {
292
+ corge: false,
293
+ },
294
+ {
295
+ corge: false,
296
+ },
297
+ ],
298
+ };
299
+ assignOid(to, 'test/a');
300
+ assignOid(to.foo, 'test/a:1');
301
+ assignOid(to.foo.bar, 'test/a:2');
302
+ assignOid(to.foo.bop, 'test/a:6');
303
+ assignOid(to.baz, 'test/a:3');
304
+ assignOid(to.baz[0], 'test/a:4');
305
+ assignOid(to.baz[1], 'test/a:5');
306
+ expect(diffToPatches(from, to, createClock(), [])).toMatchInlineSnapshot(`
307
+ [
308
+ {
309
+ "data": {
310
+ "count": 1,
311
+ "index": 2,
312
+ "op": "list-delete",
313
+ },
314
+ "oid": "test/a:2",
315
+ "timestamp": "0",
316
+ },
317
+ {
318
+ "data": {
319
+ "op": "initialize",
320
+ "value": [
321
+ 0,
322
+ ],
323
+ },
324
+ "oid": "test/a:6",
325
+ "timestamp": "1",
326
+ },
327
+ {
328
+ "data": {
329
+ "name": "bop",
330
+ "op": "set",
331
+ "value": {
332
+ "@@type": "ref",
333
+ "id": "test/a:6",
334
+ },
335
+ },
336
+ "oid": "test/a:1",
337
+ "timestamp": "2",
338
+ },
339
+ {
340
+ "data": {
341
+ "name": "corge",
342
+ "op": "set",
343
+ "value": false,
344
+ },
345
+ "oid": "test/a:4",
346
+ "timestamp": "3",
347
+ },
348
+ {
349
+ "data": {
350
+ "op": "initialize",
351
+ "value": {
352
+ "corge": false,
353
+ },
354
+ },
355
+ "oid": "test/a:5",
356
+ "timestamp": "4",
357
+ },
358
+ {
359
+ "data": {
360
+ "name": 1,
361
+ "op": "set",
362
+ "value": {
363
+ "@@type": "ref",
364
+ "id": "test/a:5",
365
+ },
366
+ },
367
+ "oid": "test/a:3",
368
+ "timestamp": "5",
369
+ },
370
+ ]
371
+ `);
372
+ });
373
+
374
+ it('should try to merge unknown objects if specified to do so', () => {
375
+ const from = {
376
+ foo: {
377
+ bar: [1, 2, 3],
378
+ },
379
+ baz: [
380
+ {
381
+ corge: true,
382
+ },
383
+ ],
384
+ };
385
+ assignOid(from, 'test/a');
386
+ assignOidsToAllSubObjects(from, createClock());
387
+
388
+ const to = {
389
+ foo: {
390
+ bar: [1, 2],
391
+ bop: [0],
392
+ },
393
+ baz: [
394
+ {
395
+ corge: false,
396
+ },
397
+ {
398
+ corge: false,
399
+ },
400
+ ],
401
+ };
402
+ expect(
403
+ diffToPatches(from, to, createClock(), [], createClock(5), [], {
404
+ mergeUnknownObjects: true,
405
+ }),
406
+ ).toMatchInlineSnapshot(`
407
+ [
408
+ {
409
+ "data": {
410
+ "count": 1,
411
+ "index": 2,
412
+ "op": "list-delete",
413
+ },
414
+ "oid": "test/a.foo.bar:1",
415
+ "timestamp": "0",
416
+ },
417
+ {
418
+ "data": {
419
+ "op": "initialize",
420
+ "value": [
421
+ 0,
422
+ ],
423
+ },
424
+ "oid": "test/a.foo.bop:5",
425
+ "timestamp": "1",
426
+ },
427
+ {
428
+ "data": {
429
+ "name": "bop",
430
+ "op": "set",
431
+ "value": {
432
+ "@@type": "ref",
433
+ "id": "test/a.foo.bop:5",
434
+ },
435
+ },
436
+ "oid": "test/a.foo:0",
437
+ "timestamp": "2",
438
+ },
439
+ {
440
+ "data": {
441
+ "name": "corge",
442
+ "op": "set",
443
+ "value": false,
444
+ },
445
+ "oid": "test/a.baz.#:3",
446
+ "timestamp": "3",
447
+ },
448
+ {
449
+ "data": {
450
+ "op": "initialize",
451
+ "value": {
452
+ "corge": false,
453
+ },
454
+ },
455
+ "oid": "test/a.baz.#:6",
456
+ "timestamp": "4",
457
+ },
458
+ {
459
+ "data": {
460
+ "name": 1,
461
+ "op": "set",
462
+ "value": {
463
+ "@@type": "ref",
464
+ "id": "test/a.baz.#:6",
465
+ },
466
+ },
467
+ "oid": "test/a.baz:2",
468
+ "timestamp": "5",
469
+ },
470
+ ]
471
+ `);
472
+ });
473
+
474
+ it('should not diff file refs', () => {
475
+ const from = {
476
+ foo: {
477
+ file: createFileRef('abc123'),
478
+ },
479
+ bar: [createFileRef('def456'), createFileRef('ghi789')],
480
+ };
481
+ assignOid(from, 'test/a');
482
+ assignOidsToAllSubObjects(from, createClock());
483
+
484
+ const to = {
485
+ foo: {
486
+ file: createFileRef('abc456'),
487
+ },
488
+ bar: [createFileRef('def456')],
489
+ };
490
+ expect(
491
+ diffToPatches(from, to, createClock(), [], createClock(5), [], {
492
+ mergeUnknownObjects: true,
493
+ }),
494
+ ).toMatchInlineSnapshot(`
495
+ [
496
+ {
497
+ "data": {
498
+ "name": "file",
499
+ "op": "set",
500
+ "value": {
501
+ "@@type": "file",
502
+ "id": "abc456",
503
+ },
504
+ },
505
+ "oid": "test/a.foo:0",
506
+ "timestamp": "0",
507
+ },
508
+ {
509
+ "data": {
510
+ "count": 1,
511
+ "index": 1,
512
+ "op": "list-delete",
513
+ },
514
+ "oid": "test/a.bar:1",
515
+ "timestamp": "1",
516
+ },
517
+ ]
518
+ `);
519
+ });
520
+ });
521
+
522
+ describe('substituting refs with objects', () => {
523
+ it('does nothing when no refs exist', () => {
524
+ const root = {
525
+ foo: 'bar',
526
+ };
527
+ assignOid(root, 'test/a');
528
+ const substituted = substituteRefsWithObjects(root, new Map());
529
+ expect(root).toEqual(
530
+ assignOid(
531
+ {
532
+ foo: 'bar',
533
+ },
534
+ 'test/a',
535
+ ),
536
+ );
537
+ expect(substituted).toEqual([]);
538
+ });
539
+
540
+ it('inserts top level objects with oids', () => {
541
+ const root = {
542
+ foo: {
543
+ '@@type': 'ref',
544
+ id: 'test/1:a',
545
+ },
546
+ qux: 1,
547
+ };
548
+ assignOid(root, 'test/1');
549
+
550
+ const substituted = substituteRefsWithObjects(
551
+ root,
552
+ new Map([['test/1:a', { foo: 'bar' }]]),
553
+ );
554
+ expect(root).toEqual(
555
+ assignOid(
556
+ {
557
+ foo: assignOid(
558
+ {
559
+ foo: 'bar',
560
+ },
561
+ 'test/1:a',
562
+ ),
563
+ qux: 1,
564
+ },
565
+ 'test/1',
566
+ ),
567
+ );
568
+ expect(substituted).toEqual(['test/1:a']);
569
+ expect(getOid(root)).toEqual('test/1');
570
+ expect(getOid(root.foo)).toEqual('test/1:a');
571
+ });
572
+
573
+ it('inserts nested objects with oids', () => {
574
+ const root: any = {
575
+ foo: {
576
+ '@@type': 'ref',
577
+ id: 'test/1:a',
578
+ },
579
+ };
580
+ assignOid(root, 'test/1');
581
+ const substituted = substituteRefsWithObjects(
582
+ root,
583
+ new Map([
584
+ [
585
+ 'test/1:a',
586
+ {
587
+ foo: 'bar',
588
+ baz: {
589
+ '@@type': 'ref',
590
+ id: 'test/1:b',
591
+ },
592
+ },
593
+ ],
594
+ [
595
+ 'test/1:b',
596
+ {
597
+ qux: 'corge',
598
+ },
599
+ ],
600
+ ]),
601
+ );
602
+ expect(root).toEqual(
603
+ assignOid(
604
+ {
605
+ foo: assignOid(
606
+ {
607
+ foo: 'bar',
608
+ baz: assignOid(
609
+ {
610
+ qux: 'corge',
611
+ },
612
+ 'test/1:b',
613
+ ),
614
+ },
615
+ 'test/1:a',
616
+ ),
617
+ },
618
+ 'test/1',
619
+ ),
620
+ );
621
+ expect(substituted).toEqual(['test/1:a', 'test/1:b']);
622
+ expect(getOid(root)).toEqual('test/1');
623
+ expect(getOid(root.foo)).toEqual('test/1:a');
624
+ expect(getOid(root.foo.baz)).toEqual('test/1:b');
625
+ });
626
+
627
+ it('substitutes arrays of objects', () => {
628
+ const root: any = {
629
+ foo: {
630
+ '@@type': 'ref',
631
+ id: 'test/1:c',
632
+ },
633
+ };
634
+ assignOid(root, 'test/1');
635
+
636
+ const substituted = substituteRefsWithObjects(
637
+ root,
638
+ new Map([
639
+ [
640
+ 'test/1:c',
641
+ [
642
+ {
643
+ '@@type': 'ref',
644
+ id: 'test/1:a',
645
+ },
646
+ {
647
+ '@@type': 'ref',
648
+ id: 'test/1:b',
649
+ },
650
+ ],
651
+ ],
652
+ [
653
+ 'test/1:a',
654
+ {
655
+ foo: 'bar',
656
+ },
657
+ ],
658
+ [
659
+ 'test/1:b',
660
+ {
661
+ qux: 'corge',
662
+ },
663
+ ],
664
+ ]),
665
+ );
666
+ expect(root).toEqual({
667
+ foo: assignOid(
668
+ [
669
+ assignOid(
670
+ {
671
+ foo: 'bar',
672
+ },
673
+ 'test/1:a',
674
+ ),
675
+ assignOid(
676
+ {
677
+ qux: 'corge',
678
+ },
679
+ 'test/1:b',
680
+ ),
681
+ ],
682
+ 'test/1:c',
683
+ ),
684
+ });
685
+ expect(substituted).toEqual(['test/1:c', 'test/1:a', 'test/1:b']);
686
+ expect(getOid(root)).toEqual('test/1');
687
+ expect(getOid(root.foo)).toEqual('test/1:c');
688
+ expect(getOid(root.foo[0])).toEqual('test/1:a');
689
+ expect(getOid(root.foo[1])).toEqual('test/1:b');
690
+ });
691
+
692
+ it.todo('substitutes arrays of arrays of objects');
693
+ });
694
+
695
+ describe('creating patches from initial state', () => {
696
+ it('adds oids to all sub-objects', async () => {
697
+ let i = 0;
698
+ function createSubId() {
699
+ return (i++).toString();
700
+ }
701
+ const result = initialToPatches(
702
+ {
703
+ foo: {
704
+ bar: 'baz',
705
+ },
706
+ qux: [
707
+ {
708
+ corge: 'grault',
709
+ },
710
+ {
711
+ bin: {
712
+ oof: 1,
713
+ },
714
+ },
715
+ ],
716
+ },
717
+ 'test/a',
718
+ createClock(),
719
+ createSubId,
720
+ );
721
+ /**
722
+ * Patches should not include assigned OID property
723
+ */
724
+ expect(result).toMatchInlineSnapshot(`
725
+ [
726
+ {
727
+ "data": {
728
+ "op": "initialize",
729
+ "value": {
730
+ "bar": "baz",
731
+ },
732
+ },
733
+ "oid": "test/a.foo:0",
734
+ "timestamp": "0",
735
+ },
736
+ {
737
+ "data": {
738
+ "op": "initialize",
739
+ "value": {
740
+ "corge": "grault",
741
+ },
742
+ },
743
+ "oid": "test/a.qux.#:2",
744
+ "timestamp": "1",
745
+ },
746
+ {
747
+ "data": {
748
+ "op": "initialize",
749
+ "value": {
750
+ "oof": 1,
751
+ },
752
+ },
753
+ "oid": "test/a.qux.#.bin:4",
754
+ "timestamp": "2",
755
+ },
756
+ {
757
+ "data": {
758
+ "op": "initialize",
759
+ "value": {
760
+ "bin": {
761
+ "@@type": "ref",
762
+ "id": "test/a.qux.#.bin:4",
763
+ },
764
+ },
765
+ },
766
+ "oid": "test/a.qux.#:3",
767
+ "timestamp": "3",
768
+ },
769
+ {
770
+ "data": {
771
+ "op": "initialize",
772
+ "value": [
773
+ {
774
+ "@@type": "ref",
775
+ "id": "test/a.qux.#:2",
776
+ },
777
+ {
778
+ "@@type": "ref",
779
+ "id": "test/a.qux.#:3",
780
+ },
781
+ ],
782
+ },
783
+ "oid": "test/a.qux:1",
784
+ "timestamp": "4",
785
+ },
786
+ {
787
+ "data": {
788
+ "op": "initialize",
789
+ "value": {
790
+ "foo": {
791
+ "@@type": "ref",
792
+ "id": "test/a.foo:0",
793
+ },
794
+ "qux": {
795
+ "@@type": "ref",
796
+ "id": "test/a.qux:1",
797
+ },
798
+ },
799
+ },
800
+ "oid": "test/a",
801
+ "timestamp": "5",
802
+ },
803
+ ]
804
+ `);
805
+ });
806
+ });
807
+
808
+ describe('applying individual operations', () => {
809
+ it('applies a list-remove of only the last instance', () => {
810
+ expect(
811
+ applyPatch(['a', 'b', 'c', 'a', 'b'], {
812
+ op: 'list-remove',
813
+ value: 'a',
814
+ only: 'last',
815
+ }),
816
+ ).toEqual(['a', 'b', 'c', 'b']);
817
+ });
818
+ it('applies a list-remove of only the first instance', () => {
819
+ expect(
820
+ applyPatch(['a', 'b', 'c', 'a', 'b'], {
821
+ op: 'list-remove',
822
+ value: 'a',
823
+ only: 'first',
824
+ }),
825
+ ).toEqual(['b', 'c', 'a', 'b']);
826
+ });
827
+ it('applies a list-remove of all instances', () => {
828
+ expect(
829
+ applyPatch(['a', 'b', 'c', 'a', 'b'], {
830
+ op: 'list-remove',
831
+ value: 'a',
832
+ }),
833
+ ).toEqual(['b', 'c', 'b']);
834
+ });
835
+ it('applies a list-remove of an object ref', () => {
836
+ expect(
837
+ applyPatch(
838
+ [
839
+ createRef('a'),
840
+ createRef('b'),
841
+ createRef('c'),
842
+ createRef('a'),
843
+ createRef('b'),
844
+ ],
845
+ {
846
+ op: 'list-remove',
847
+ value: createRef('a'),
848
+ only: 'last',
849
+ },
850
+ ),
851
+ ).toEqual([createRef('a'), createRef('b'), createRef('c'), createRef('b')]);
852
+ });
853
+ it('applies a list-add of a nonexistent item', () => {
854
+ expect(
855
+ applyPatch(['a', 'b', 'c'], {
856
+ op: 'list-add',
857
+ value: 'd',
858
+ }),
859
+ ).toEqual(['a', 'b', 'c', 'd']);
860
+ });
861
+ it('does not apply a list-add if the item is already present', () => {
862
+ expect(
863
+ applyPatch(['a', 'b', 'c'], {
864
+ op: 'list-add',
865
+ value: 'b',
866
+ }),
867
+ ).toEqual(['a', 'b', 'c']);
868
+ });
869
+ });
870
+
871
+ describe('collecting deleted refs during patch application', () => {
872
+ it('collects refs that are removed from a list', () => {
873
+ const refs: (ObjectRef | FileRef)[] = [];
874
+ expect(
875
+ applyPatch(
876
+ [
877
+ createRef('a'),
878
+ createRef('b'),
879
+ createRef('c'),
880
+ createRef('d'),
881
+ createRef('e'),
882
+ ],
883
+ {
884
+ op: 'list-remove',
885
+ value: createRef('c'),
886
+ },
887
+ refs,
888
+ ),
889
+ ).toEqual([createRef('a'), createRef('b'), createRef('d'), createRef('e')]);
890
+ expect(refs).toEqual([createRef('c')]);
891
+ });
892
+ it('collects a deleted ref field from an object', () => {
893
+ const refs: (ObjectRef | FileRef)[] = [];
894
+ expect(
895
+ applyPatch(
896
+ {
897
+ a: createRef('a'),
898
+ b: createRef('b'),
899
+ c: createRef('c'),
900
+ },
901
+ {
902
+ op: 'remove',
903
+ name: 'b',
904
+ },
905
+ refs,
906
+ ),
907
+ ).toEqual({
908
+ a: createRef('a'),
909
+ c: createRef('c'),
910
+ });
911
+ expect(refs).toEqual([createRef('b')]);
912
+ });
913
+ it('collects all refs in a deleted object', () => {
914
+ const refs: (ObjectRef | FileRef)[] = [];
915
+ expect(
916
+ applyPatch(
917
+ {
918
+ a: createRef('a'),
919
+ b: createRef('b'),
920
+ c: createRef('c'),
921
+ },
922
+ {
923
+ op: 'delete',
924
+ },
925
+ refs,
926
+ ),
927
+ ).toEqual(undefined);
928
+ expect(refs).toEqual([createRef('a'), createRef('b'), createRef('c')]);
929
+ });
930
+ it('collects all refs in a deleted list', () => {
931
+ const refs: (ObjectRef | FileRef)[] = [];
932
+ expect(
933
+ applyPatch(
934
+ [createRef('a'), createRef('b'), createRef('c')],
935
+ {
936
+ op: 'delete',
937
+ },
938
+ refs,
939
+ ),
940
+ ).toEqual(undefined);
941
+ expect(refs).toEqual([createRef('a'), createRef('b'), createRef('c')]);
942
+ });
943
+ it('collects a ref in a replaced field', () => {
944
+ const refs: (ObjectRef | FileRef)[] = [];
945
+ expect(
946
+ applyPatch(
947
+ {
948
+ a: createRef('a'),
949
+ b: createRef('b'),
950
+ c: createRef('c'),
951
+ },
952
+ {
953
+ op: 'set',
954
+ name: 'b',
955
+ value: createRef('d'),
956
+ },
957
+ refs,
958
+ ),
959
+ ).toEqual({
960
+ a: createRef('a'),
961
+ b: createRef('d'),
962
+ c: createRef('c'),
963
+ });
964
+ expect(refs).toEqual([createRef('b')]);
965
+ });
966
+ });