@verdant-web/common 2.8.0 → 2.9.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/src/undo.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import { AuthorizationKey } from './authz.js';
1
2
  import { ObjectIdentifier } from './oids.js';
2
3
  import { applyPatch, Operation } from './operation.js';
4
+ import { PatchCreator } from './patch.js';
5
+ import { isRef } from './refs.js';
3
6
  import { cloneDeep } from './utils.js';
4
7
 
5
8
  export function getUndoOperations(
@@ -7,7 +10,10 @@ export function getUndoOperations(
7
10
  initial: any,
8
11
  operations: Operation[],
9
12
  getNow: () => string,
13
+ getSubId?: () => string,
14
+ authz?: AuthorizationKey,
10
15
  ): Operation[] {
16
+ const patchCreator = new PatchCreator(getNow, getSubId);
11
17
  if (initial === undefined || initial === null) {
12
18
  // if the initial state is nothing, then the undo is to delete everything.
13
19
  // there's nothing else to worry about!
@@ -18,6 +24,7 @@ export function getUndoOperations(
18
24
  data: {
19
25
  op: 'delete',
20
26
  },
27
+ authz,
21
28
  },
22
29
  ];
23
30
  }
@@ -27,7 +34,7 @@ export function getUndoOperations(
27
34
  let state = cloneDeep(initial);
28
35
  const undoOperations: Operation[] = [];
29
36
  for (const operation of operations) {
30
- const undo = getUndoOperation(oid, state, operation, getNow);
37
+ const undo = getUndoOperation(oid, state, operation, patchCreator, authz);
31
38
  undoOperations.unshift(...undo);
32
39
  applyPatch(state, operation.data);
33
40
  }
@@ -38,195 +45,114 @@ function getUndoOperation(
38
45
  oid: ObjectIdentifier,
39
46
  initial: any,
40
47
  operation: Operation,
41
- getNow: () => string,
48
+ patchCreator: PatchCreator,
49
+ authz?: AuthorizationKey,
42
50
  ): Operation[] {
43
51
  const data = operation.data;
44
52
  switch (data.op) {
45
53
  case 'set':
54
+ if (initial[data.name] === undefined) {
55
+ return patchCreator.createRemove(oid, data.name, authz);
56
+ }
57
+ return patchCreator.createSet(oid, data.name, initial[data.name], authz);
46
58
  case 'remove':
47
- return [
48
- {
49
- oid,
50
- timestamp: getNow(),
51
- data: {
52
- op: 'set',
53
- name: data.name,
54
- value: initial[data.name],
55
- },
56
- },
57
- ];
59
+ if (initial[data.name] === undefined) {
60
+ return [];
61
+ }
62
+ return patchCreator.createSet(oid, data.name, initial[data.name], authz);
58
63
  case 'list-insert':
59
- return [
60
- {
61
- oid,
62
- timestamp: getNow(),
63
- data: {
64
- op: 'list-delete',
65
- index: data.index,
66
- count: 1,
67
- },
68
- },
69
- ];
64
+ if (data.value === undefined && data.values?.length === 0) return [];
65
+ return patchCreator.createListDelete(
66
+ oid,
67
+ data.index,
68
+ data.value ? 1 : data.values?.length || 0,
69
+ authz,
70
+ );
70
71
  case 'list-delete':
71
- return [
72
- {
73
- oid,
74
- timestamp: getNow(),
75
- data: {
76
- op: 'list-insert',
77
- index: data.index,
78
- values: initial.slice(data.index, data.count),
79
- },
80
- },
81
- ];
72
+ return patchCreator.createListInsertMany(
73
+ oid,
74
+ data.index,
75
+ initial.slice(data.index, data.index + data.count),
76
+ authz,
77
+ );
82
78
  case 'list-move-by-ref':
83
- return [
84
- {
85
- oid,
86
- timestamp: getNow(),
87
- data: {
88
- op: 'list-move-by-ref',
89
- value: data.value,
90
- index: initial.indexOf(data.value),
91
- },
92
- },
93
- ];
79
+ return patchCreator.createListMoveByRef(
80
+ oid,
81
+ data.value,
82
+ findItemRefIndex(initial, data.value),
83
+ authz,
84
+ );
94
85
  case 'list-move-by-index':
95
- return [
96
- {
97
- oid,
98
- timestamp: getNow(),
99
- data: {
100
- op: 'list-move-by-index',
101
- from: data.to,
102
- to: data.from,
103
- },
104
- },
105
- ];
86
+ return patchCreator.createListMoveByIndex(oid, data.to, data.from, authz);
106
87
  case 'delete':
107
- return [
108
- {
109
- oid,
110
- timestamp: getNow(),
111
- data: {
112
- op: 'initialize',
113
- value: initial,
114
- },
115
- },
116
- ];
88
+ return patchCreator.createInitialize(initial, oid, authz, true);
117
89
  case 'list-push':
118
- return [
119
- {
120
- oid,
121
- timestamp: getNow(),
122
- data: {
123
- op: 'list-remove',
124
- value: data.value,
125
- // best heuristic here - remove the last instance of the item.
126
- only: 'last',
127
- },
128
- },
129
- ];
90
+ return patchCreator.createListRemove(oid, data.value, 'last', authz);
130
91
  case 'list-remove':
131
92
  if (data.only === 'last') {
132
- const index = initial.lastIndexOf(data.value);
133
- return [
134
- {
135
- oid,
136
- timestamp: getNow(),
137
- data: {
138
- op: 'list-insert',
139
- index,
140
- values: [data.value],
141
- },
142
- },
143
- ];
93
+ const index = findItemRefIndex(initial, data.value, 0, 'backward');
94
+ if (index === -1) {
95
+ // if the value isn't in the list, then there's nothing to undo.
96
+ return [];
97
+ }
98
+ return patchCreator.createListInsert(oid, index, data.value, authz);
144
99
  } else if (data.only === 'first') {
145
- const index = initial.indexOf(data.value);
146
- return [
147
- {
148
- oid,
149
- timestamp: getNow(),
150
- data: {
151
- op: 'list-insert',
152
- index,
153
- values: [data.value],
154
- },
155
- },
156
- ];
100
+ const index = findItemRefIndex(initial, data.value);
101
+ if (index === -1) {
102
+ // if the value isn't in the list, then there's nothing to undo.
103
+ return [];
104
+ }
105
+ return patchCreator.createListInsert(oid, index, data.value, authz);
157
106
  } else {
158
107
  // find all instances of value and restore them at their
159
108
  // original index
160
109
  const indexesOfValue = [];
161
- let index = initial.indexOf(data.value);
110
+ let index = findItemRefIndex(initial, data.value);
162
111
  while (index !== -1) {
163
112
  indexesOfValue.push(index);
164
- index = initial.indexOf(data.value, index + 1);
113
+ index = findItemRefIndex(initial, data.value, index + 1);
165
114
  }
166
- return indexesOfValue.map((index) => ({
167
- oid,
168
- timestamp: getNow(),
169
- data: {
170
- op: 'list-insert',
171
- index,
172
- value: data.value,
173
- },
174
- }));
115
+ return indexesOfValue.flatMap((index) =>
116
+ patchCreator.createListInsert(oid, index, data.value, authz),
117
+ );
175
118
  }
176
119
  case 'list-add':
177
120
  // this one is kind of ambiguous. in theory the set may have
178
121
  // already included the value and so no change happened. but
179
122
  // basically we infer the intent is to remove what was meant
180
123
  // to be added by this change.
181
- return [
182
- {
183
- oid,
184
- timestamp: getNow(),
185
- data: {
186
- op: 'list-remove',
187
- value: data.value,
188
- only: 'last',
189
- },
190
- },
191
- ];
124
+ return patchCreator.createListRemove(oid, data.value, 'last', authz);
192
125
  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
- }
206
- return [
207
- {
126
+ if (initial[data.index] !== undefined) {
127
+ return patchCreator.createListSet(
208
128
  oid,
209
- timestamp: getNow(),
210
- data: {
211
- op: 'list-delete',
212
- index: data.index,
213
- count: 1,
214
- },
215
- },
216
- ];
129
+ data.index,
130
+ initial[data.index],
131
+ authz,
132
+ );
133
+ }
134
+ return patchCreator.createListDelete(oid, data.index, 1, authz);
217
135
  case 'initialize':
218
- return [
219
- {
220
- oid,
221
- timestamp: getNow(),
222
- data: {
223
- op: 'delete',
224
- },
225
- },
226
- ];
136
+ return patchCreator.createDelete(oid, authz);
227
137
  case 'touch':
228
138
  return [];
229
139
  default:
230
140
  throw new Error(`Cannot undo operation type: ${(data as any).op}`);
231
141
  }
232
142
  }
143
+
144
+ function findItemRefIndex(
145
+ list: any[],
146
+ item: any,
147
+ from: number = 0,
148
+ dir: 'forward' | 'backward' = 'forward',
149
+ ) {
150
+ const method = dir === 'forward' ? 'findIndex' : 'findLastIndex';
151
+ // for object items, attempt to find the matching item by OID.
152
+ if (isRef(item)) {
153
+ return list[method](
154
+ (i, idx) => isRef(i) && i.id === item.id && idx >= from,
155
+ );
156
+ }
157
+ return list[method]((i, idx) => i === item && idx >= from);
158
+ }