@verdant-web/common 2.7.2 → 2.8.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.
- package/dist/esm/diffing.d.ts +39 -0
- package/dist/esm/diffing.js +257 -0
- package/dist/esm/diffing.js.map +1 -0
- package/dist/esm/diffing.test.d.ts +1 -0
- package/dist/esm/diffing.test.js +896 -0
- package/dist/esm/diffing.test.js.map +1 -0
- package/dist/esm/index.d.ts +20 -19
- package/dist/esm/index.js +17 -16
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/migration.js +1 -1
- package/dist/esm/migration.js.map +1 -1
- package/dist/esm/migration.test.d.ts +1 -0
- package/dist/esm/migration.test.js +98 -0
- package/dist/esm/migration.test.js.map +1 -0
- package/dist/esm/operation.d.ts +7 -21
- package/dist/esm/operation.js +28 -266
- package/dist/esm/operation.js.map +1 -1
- package/dist/esm/operation.test.js +122 -592
- package/dist/esm/operation.test.js.map +1 -1
- package/dist/esm/patch.d.ts +13 -13
- package/dist/esm/patch.js +41 -16
- package/dist/esm/patch.js.map +1 -1
- package/dist/esm/timestamp.d.ts +1 -0
- package/dist/esm/timestamp.js +3 -0
- package/dist/esm/timestamp.js.map +1 -1
- package/dist/esm/undo.js +15 -2
- package/dist/esm/undo.js.map +1 -1
- package/dist/esm/undo.test.d.ts +1 -0
- package/dist/esm/undo.test.js +20 -0
- package/dist/esm/undo.test.js.map +1 -0
- package/dist/esm/utils.js +13 -3
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/diffing.test.ts +939 -0
- package/src/diffing.ts +325 -0
- package/src/index.ts +21 -20
- package/src/migration.test.ts +112 -0
- package/src/migration.ts +2 -1
- package/src/operation.test.ts +148 -623
- package/src/operation.ts +37 -371
- package/src/patch.ts +68 -11
- package/src/timestamp.ts +4 -1
- package/src/undo.test.ts +21 -0
- package/src/undo.ts +15 -2
- package/src/utils.ts +13 -3
package/src/patch.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { AuthorizationKey } from './authz.js';
|
|
6
|
+
import { diffToPatches } from './diffing.js';
|
|
6
7
|
import { FileRef } from './files.js';
|
|
7
8
|
import { createRef, createSubOid, ObjectIdentifier } from './oids.js';
|
|
8
9
|
import {
|
|
9
|
-
diffToPatches,
|
|
10
10
|
initialToPatches,
|
|
11
11
|
ObjectRef,
|
|
12
12
|
Operation,
|
|
@@ -17,8 +17,8 @@ import { isObject } from './utils.js';
|
|
|
17
17
|
|
|
18
18
|
export class PatchCreator {
|
|
19
19
|
constructor(
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
readonly getNow: () => string,
|
|
21
|
+
readonly createSubId?: () => string,
|
|
22
22
|
) {}
|
|
23
23
|
|
|
24
24
|
isPrimitive = (value: any) => {
|
|
@@ -52,6 +52,7 @@ export class PatchCreator {
|
|
|
52
52
|
oid: ObjectIdentifier,
|
|
53
53
|
key: PropertyName,
|
|
54
54
|
value: any,
|
|
55
|
+
authz?: string,
|
|
55
56
|
): Operation[] => {
|
|
56
57
|
// incoming value must be normalized. if it's not a primitive, it and all sub-objects
|
|
57
58
|
// must be created
|
|
@@ -65,6 +66,7 @@ export class PatchCreator {
|
|
|
65
66
|
name: key,
|
|
66
67
|
value,
|
|
67
68
|
},
|
|
69
|
+
authz,
|
|
68
70
|
},
|
|
69
71
|
];
|
|
70
72
|
} else {
|
|
@@ -72,7 +74,16 @@ export class PatchCreator {
|
|
|
72
74
|
return [
|
|
73
75
|
// since we're setting a complex nested object, we can initialize it wholesale.
|
|
74
76
|
// no diffing to do.
|
|
75
|
-
...initialToPatches(
|
|
77
|
+
...initialToPatches(
|
|
78
|
+
value,
|
|
79
|
+
itemOid,
|
|
80
|
+
this.getNow,
|
|
81
|
+
this.createSubId,
|
|
82
|
+
undefined,
|
|
83
|
+
{
|
|
84
|
+
authz,
|
|
85
|
+
},
|
|
86
|
+
),
|
|
76
87
|
// then set the reference to the object
|
|
77
88
|
{
|
|
78
89
|
oid,
|
|
@@ -82,12 +93,17 @@ export class PatchCreator {
|
|
|
82
93
|
value: createRef(itemOid),
|
|
83
94
|
name: key,
|
|
84
95
|
},
|
|
96
|
+
authz,
|
|
85
97
|
},
|
|
86
98
|
];
|
|
87
99
|
}
|
|
88
100
|
};
|
|
89
101
|
|
|
90
|
-
createRemove = (
|
|
102
|
+
createRemove = (
|
|
103
|
+
oid: ObjectIdentifier,
|
|
104
|
+
key: PropertyName,
|
|
105
|
+
authz?: string,
|
|
106
|
+
): Operation[] => {
|
|
91
107
|
return [
|
|
92
108
|
{
|
|
93
109
|
oid,
|
|
@@ -96,6 +112,7 @@ export class PatchCreator {
|
|
|
96
112
|
op: 'remove',
|
|
97
113
|
name: key,
|
|
98
114
|
},
|
|
115
|
+
authz,
|
|
99
116
|
},
|
|
100
117
|
];
|
|
101
118
|
};
|
|
@@ -104,6 +121,7 @@ export class PatchCreator {
|
|
|
104
121
|
oid: ObjectIdentifier,
|
|
105
122
|
index: number,
|
|
106
123
|
value: any,
|
|
124
|
+
authz?: string,
|
|
107
125
|
): Operation[] => {
|
|
108
126
|
if (this.isPrimitive(value)) {
|
|
109
127
|
return [
|
|
@@ -115,12 +133,22 @@ export class PatchCreator {
|
|
|
115
133
|
index,
|
|
116
134
|
value,
|
|
117
135
|
},
|
|
136
|
+
authz,
|
|
118
137
|
},
|
|
119
138
|
];
|
|
120
139
|
} else {
|
|
121
140
|
const itemOid = createSubOid(oid, this.createSubId);
|
|
122
141
|
return [
|
|
123
|
-
...initialToPatches(
|
|
142
|
+
...initialToPatches(
|
|
143
|
+
value,
|
|
144
|
+
itemOid,
|
|
145
|
+
this.getNow,
|
|
146
|
+
this.createSubId,
|
|
147
|
+
undefined,
|
|
148
|
+
{
|
|
149
|
+
authz,
|
|
150
|
+
},
|
|
151
|
+
),
|
|
124
152
|
{
|
|
125
153
|
oid,
|
|
126
154
|
timestamp: this.getNow(),
|
|
@@ -129,12 +157,17 @@ export class PatchCreator {
|
|
|
129
157
|
index,
|
|
130
158
|
value: createRef(itemOid),
|
|
131
159
|
},
|
|
160
|
+
authz,
|
|
132
161
|
},
|
|
133
162
|
];
|
|
134
163
|
}
|
|
135
164
|
};
|
|
136
165
|
|
|
137
|
-
createListPush = (
|
|
166
|
+
createListPush = (
|
|
167
|
+
oid: ObjectIdentifier,
|
|
168
|
+
value: any,
|
|
169
|
+
authz?: string,
|
|
170
|
+
): Operation[] => {
|
|
138
171
|
if (this.isPrimitive(value)) {
|
|
139
172
|
return [
|
|
140
173
|
{
|
|
@@ -144,12 +177,15 @@ export class PatchCreator {
|
|
|
144
177
|
op: 'list-push',
|
|
145
178
|
value,
|
|
146
179
|
},
|
|
180
|
+
authz,
|
|
147
181
|
},
|
|
148
182
|
];
|
|
149
183
|
} else {
|
|
150
184
|
const itemOid = createSubOid(oid, this.createSubId);
|
|
151
185
|
return [
|
|
152
|
-
...initialToPatches(value, itemOid, this.getNow
|
|
186
|
+
...initialToPatches(value, itemOid, this.getNow, undefined, undefined, {
|
|
187
|
+
authz,
|
|
188
|
+
}),
|
|
153
189
|
{
|
|
154
190
|
oid,
|
|
155
191
|
timestamp: this.getNow(),
|
|
@@ -157,12 +193,17 @@ export class PatchCreator {
|
|
|
157
193
|
op: 'list-push',
|
|
158
194
|
value: createRef(itemOid),
|
|
159
195
|
},
|
|
196
|
+
authz,
|
|
160
197
|
},
|
|
161
198
|
];
|
|
162
199
|
}
|
|
163
200
|
};
|
|
164
201
|
|
|
165
|
-
createListAdd = (
|
|
202
|
+
createListAdd = (
|
|
203
|
+
oid: ObjectIdentifier,
|
|
204
|
+
value: any,
|
|
205
|
+
authz?: string,
|
|
206
|
+
): Operation[] => {
|
|
166
207
|
if (!this.isPrimitive(value)) {
|
|
167
208
|
return [
|
|
168
209
|
{
|
|
@@ -172,6 +213,7 @@ export class PatchCreator {
|
|
|
172
213
|
op: 'list-add',
|
|
173
214
|
value: createRef(value),
|
|
174
215
|
},
|
|
216
|
+
authz,
|
|
175
217
|
},
|
|
176
218
|
];
|
|
177
219
|
} else {
|
|
@@ -183,6 +225,7 @@ export class PatchCreator {
|
|
|
183
225
|
op: 'list-add',
|
|
184
226
|
value,
|
|
185
227
|
},
|
|
228
|
+
authz,
|
|
186
229
|
},
|
|
187
230
|
];
|
|
188
231
|
}
|
|
@@ -192,6 +235,7 @@ export class PatchCreator {
|
|
|
192
235
|
oid: ObjectIdentifier,
|
|
193
236
|
index: number,
|
|
194
237
|
value: any,
|
|
238
|
+
authz?: string,
|
|
195
239
|
): Operation[] => {
|
|
196
240
|
if (this.isPrimitive(value)) {
|
|
197
241
|
return [
|
|
@@ -203,12 +247,15 @@ export class PatchCreator {
|
|
|
203
247
|
value,
|
|
204
248
|
index,
|
|
205
249
|
},
|
|
250
|
+
authz,
|
|
206
251
|
},
|
|
207
252
|
];
|
|
208
253
|
} else {
|
|
209
254
|
const itemOid = createSubOid(oid, this.createSubId);
|
|
210
255
|
return [
|
|
211
|
-
...initialToPatches(value, itemOid, this.getNow
|
|
256
|
+
...initialToPatches(value, itemOid, this.getNow, undefined, undefined, {
|
|
257
|
+
authz,
|
|
258
|
+
}),
|
|
212
259
|
{
|
|
213
260
|
oid,
|
|
214
261
|
timestamp: this.getNow(),
|
|
@@ -217,6 +264,7 @@ export class PatchCreator {
|
|
|
217
264
|
value: createRef(itemOid),
|
|
218
265
|
index,
|
|
219
266
|
},
|
|
267
|
+
authz,
|
|
220
268
|
},
|
|
221
269
|
];
|
|
222
270
|
}
|
|
@@ -226,6 +274,7 @@ export class PatchCreator {
|
|
|
226
274
|
oid: ObjectIdentifier,
|
|
227
275
|
value: any,
|
|
228
276
|
only?: 'first' | 'last',
|
|
277
|
+
authz?: string,
|
|
229
278
|
): Operation[] => {
|
|
230
279
|
return [
|
|
231
280
|
{
|
|
@@ -236,6 +285,7 @@ export class PatchCreator {
|
|
|
236
285
|
value,
|
|
237
286
|
only,
|
|
238
287
|
},
|
|
288
|
+
authz,
|
|
239
289
|
},
|
|
240
290
|
];
|
|
241
291
|
};
|
|
@@ -244,6 +294,7 @@ export class PatchCreator {
|
|
|
244
294
|
oid: ObjectIdentifier,
|
|
245
295
|
index: number,
|
|
246
296
|
count: number = 1,
|
|
297
|
+
authz?: string,
|
|
247
298
|
): Operation[] => {
|
|
248
299
|
return [
|
|
249
300
|
{
|
|
@@ -254,6 +305,7 @@ export class PatchCreator {
|
|
|
254
305
|
index,
|
|
255
306
|
count,
|
|
256
307
|
},
|
|
308
|
+
authz,
|
|
257
309
|
},
|
|
258
310
|
];
|
|
259
311
|
};
|
|
@@ -262,6 +314,7 @@ export class PatchCreator {
|
|
|
262
314
|
oid: ObjectIdentifier,
|
|
263
315
|
value: ObjectRef | FileRef,
|
|
264
316
|
index: number,
|
|
317
|
+
authz?: string,
|
|
265
318
|
): Operation[] => {
|
|
266
319
|
return [
|
|
267
320
|
{
|
|
@@ -272,6 +325,7 @@ export class PatchCreator {
|
|
|
272
325
|
value,
|
|
273
326
|
index,
|
|
274
327
|
},
|
|
328
|
+
authz,
|
|
275
329
|
},
|
|
276
330
|
];
|
|
277
331
|
};
|
|
@@ -280,6 +334,7 @@ export class PatchCreator {
|
|
|
280
334
|
oid: ObjectIdentifier,
|
|
281
335
|
fromIndex: number,
|
|
282
336
|
toIndex: number,
|
|
337
|
+
authz?: string,
|
|
283
338
|
): Operation[] => {
|
|
284
339
|
return [
|
|
285
340
|
{
|
|
@@ -290,11 +345,12 @@ export class PatchCreator {
|
|
|
290
345
|
from: fromIndex,
|
|
291
346
|
to: toIndex,
|
|
292
347
|
},
|
|
348
|
+
authz,
|
|
293
349
|
},
|
|
294
350
|
];
|
|
295
351
|
};
|
|
296
352
|
|
|
297
|
-
createDelete = (oid: ObjectIdentifier): Operation[] => {
|
|
353
|
+
createDelete = (oid: ObjectIdentifier, authz?: string): Operation[] => {
|
|
298
354
|
return [
|
|
299
355
|
{
|
|
300
356
|
oid,
|
|
@@ -302,6 +358,7 @@ export class PatchCreator {
|
|
|
302
358
|
data: {
|
|
303
359
|
op: 'delete',
|
|
304
360
|
},
|
|
361
|
+
authz,
|
|
305
362
|
},
|
|
306
363
|
];
|
|
307
364
|
};
|
package/src/timestamp.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import cuid from 'cuid';
|
|
2
|
-
import { v4 } from 'uuid';
|
|
3
2
|
|
|
4
3
|
export interface TimestampProvider {
|
|
5
4
|
now(version: number): string;
|
|
@@ -37,6 +36,10 @@ export class NaiveTimestampProvider implements TimestampProvider {
|
|
|
37
36
|
getWallClockTime = (timestamp: string) => {
|
|
38
37
|
return parseInt(timestamp.slice(VERSION_BLOCK_LENGTH).split('-')[0], 10);
|
|
39
38
|
};
|
|
39
|
+
|
|
40
|
+
reset() {
|
|
41
|
+
this.counter = 0;
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export class HybridLogicalClockTimestampProvider implements TimestampProvider {
|
package/src/undo.test.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createOid } from './oids.js';
|
|
3
|
+
import { PatchCreator } from './patch.js';
|
|
4
|
+
import { getUndoOperations } from './undo.js';
|
|
5
|
+
|
|
6
|
+
describe('undo', () => {
|
|
7
|
+
it('correctly removes added list items', () => {
|
|
8
|
+
let time = 0;
|
|
9
|
+
const clock = () => `time-${time++}`;
|
|
10
|
+
let subId = 0;
|
|
11
|
+
const patchCreator = new PatchCreator(clock, () => `${subId++}`);
|
|
12
|
+
const initial = [1, 2];
|
|
13
|
+
const oid = createOid('test', 'foo');
|
|
14
|
+
const operations = patchCreator.createListSet(oid, 2, 3);
|
|
15
|
+
time = 0;
|
|
16
|
+
const undos = getUndoOperations(oid, initial, operations, clock);
|
|
17
|
+
|
|
18
|
+
time = 0;
|
|
19
|
+
expect(undos).toEqual(patchCreator.createListDelete(oid, 2, 1));
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/undo.ts
CHANGED
|
@@ -190,14 +190,27 @@ function getUndoOperation(
|
|
|
190
190
|
},
|
|
191
191
|
];
|
|
192
192
|
case 'list-set':
|
|
193
|
+
if (initial[data.index]) {
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
oid,
|
|
197
|
+
timestamp: getNow(),
|
|
198
|
+
data: {
|
|
199
|
+
op: 'list-set',
|
|
200
|
+
index: data.index,
|
|
201
|
+
value: initial[data.index],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
}
|
|
193
206
|
return [
|
|
194
207
|
{
|
|
195
208
|
oid,
|
|
196
209
|
timestamp: getNow(),
|
|
197
210
|
data: {
|
|
198
|
-
op: 'list-
|
|
211
|
+
op: 'list-delete',
|
|
199
212
|
index: data.index,
|
|
200
|
-
|
|
213
|
+
count: 1,
|
|
201
214
|
},
|
|
202
215
|
},
|
|
203
216
|
];
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import hash from 'object-hash';
|
|
1
2
|
import { v4 } from 'uuid';
|
|
2
3
|
import { assignOid, maybeGetOid } from './oids.js';
|
|
3
|
-
import hash from 'object-hash';
|
|
4
4
|
|
|
5
5
|
export function take<T extends object, Keys extends keyof T>(
|
|
6
6
|
obj: T,
|
|
@@ -45,7 +45,7 @@ export function getSortedIndex<T>(
|
|
|
45
45
|
return low;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function orderedReplacer(
|
|
48
|
+
function orderedReplacer(k: any, v: any) {
|
|
49
49
|
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
|
50
50
|
return v;
|
|
51
51
|
}
|
|
@@ -58,7 +58,17 @@ function orderedReplacer(_: any, v: any) {
|
|
|
58
58
|
* of key insertion order
|
|
59
59
|
*/
|
|
60
60
|
export function stableStringify(obj: any) {
|
|
61
|
-
|
|
61
|
+
const seen = new WeakMap();
|
|
62
|
+
let cyclicCount = 0;
|
|
63
|
+
return JSON.stringify(obj, (k, v) => {
|
|
64
|
+
if (typeof v === 'object' && v !== null) {
|
|
65
|
+
if (seen.has(v)) {
|
|
66
|
+
return { $ref: seen.get(v) };
|
|
67
|
+
}
|
|
68
|
+
seen.set(v, `cyclic-ref:${cyclicCount++}`);
|
|
69
|
+
}
|
|
70
|
+
return orderedReplacer(k, v);
|
|
71
|
+
});
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/**
|