Skip to content

Commit 7eabcfa

Browse files
committed
wip - op invalidation
1 parent d8324b1 commit 7eabcfa

File tree

5 files changed

+316
-373
lines changed

5 files changed

+316
-373
lines changed

src/data/model/CascadedInvalidateOp.ts

Lines changed: 262 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,281 @@
1+
import { HashedObject } from "data/model";
2+
import { Context } from "./Context";
3+
import { HashedSet } from "./HashedSet";
4+
import { Hash } from "./Hashing";
5+
import { HashReference } from "./HashReference";
16
import { InvalidateAfterOp } from "./InvalidateAfterOp";
27
import { MutationOp } from "./MutationOp";
38

9+
/*
10+
* Op0 <-
11+
* ^ \
12+
* target | c.\
13+
* | \ causal
14+
* InvAfterOp Op1 <------------------ Op2
15+
* ^ \ ^ ^
16+
* target | c.\ | target | target
17+
* | \ | causal |
18+
* UndoOp UndoOp(1) <-------------- UndoOp(2)
19+
* ^ \ ^ ^
20+
* target | c.\ | target | target
21+
* | \ | causal |
22+
* RedoOp RedoOp <---------------- RedoOp
23+
* ^ \ ^ ^
24+
* target | c.\ | target | target
25+
* | \ | causal |
26+
* UndoOp UndoOp(3) <-------------- UndoOp(4)
27+
*
28+
*/
429

5-
class CascadedInvalidateOp extends MutationOp {
30+
/*
31+
*
32+
* causal causal
33+
* Op0 <--------------------- Op1 <------------------ Op2
34+
* ^ _________________/ ^ ^
35+
* target | / Op1 is too late | target | target
36+
* | / | causal |
37+
* InvAfterOp <--------------- UndoOp(1) <-------------- UndoOp(2)
38+
* ^ causal ^ ^
39+
* target | | target | target
40+
* | causal | causal |
41+
* UndoOp <----------------- RedoOp <---------------- RedoOp
42+
* ^ ^ ^
43+
* target | | target | target
44+
* | causal | causal |
45+
* RedoOp <----------------- UndoOp(3) <-------------- UndoOp(4)
46+
* ^ ^ ^
47+
* |... |... |...
48+
*
49+
*/
50+
51+
/*
52+
*
53+
* Always: this.causal.target \in this.target.causal
54+
*
55+
* if causal is InvAfterOp => target is NOT CascadeOp, this.undo = true
56+
* if causal is CascadeOp => target is CascadeOp, this.undo = !this.target.undo
57+
*
58+
*
59+
*/
60+
61+
62+
63+
/* The diagram above shows the situations where an UndoOp may be necessary. Here
64+
* InvAfterOp on the top left is invalidating Op1, and transitively Op2 that is
65+
* causally dependant on Op1. However, InvAfterOp is itself being undone, then
66+
* redone, then undone again, and those actions are also cascaded to Op1 and Op2.
67+
*
68+
* There are 4 possible cases, marked above:
69+
*
70+
* (1) is the direct case, where Op1 is being undone because it is outside of the
71+
* terminalOps defined in InvAfterOp.
72+
*
73+
* (2) is a cascade of (1) to Op2, that is dependent on Op1.
74+
*
75+
* (3) is a cascade of a RedoOp on the original InvAfterOp, that triggers a new
76+
* undo for Op1. Notice in this case that the RedoOp is cascaded as an undo.
77+
*
78+
* (4) is similar to (2), but is undoing a RedoOp for Op2 instead of Op2 itself.
79+
*
80+
* It is important to notice that in all cases but (1),
81+
*
82+
* undo.causal.target \in undo.target.causalOps
83+
*
84+
*/
85+
86+
abstract class CascadedInvalidateOp extends MutationOp {
687

7-
targetOp?: MutationOp;
888
undo?: boolean;
89+
targetOp?: MutationOp;
90+
991

10-
constructor(undo?: boolean, targetOp?: MutationOp, causalOp?: InvalidateAfterOp|CascadedInvalidateOp) {
92+
constructor(undo?: boolean, causalOp?: InvalidateAfterOp|CascadedInvalidateOp, targetOp?: MutationOp) {
1193
super(targetOp?.targetObject, causalOp === undefined? undefined : [causalOp].values());
1294

1395
if (undo !== undefined) {
1496
this.undo = undo;
97+
98+
const opType = undo? 'UndoOp':'RedoOp';
99+
100+
if (targetOp === undefined) {
101+
throw new Error('Cannot create ' + opType + ', targetOp not provided.');
102+
}
103+
104+
this.targetOp = targetOp;
105+
106+
if (causalOp === undefined) {
107+
throw new Error('Cannot create ' + opType + ', causalOp not provided.');
108+
}
109+
110+
// this.causalOps is initialized by call to super() above
111+
112+
// sanity checks:
113+
114+
// The cascade has merit: causalOp.targetOp \in targetOp.causalOps
115+
if (!targetOp.getCausalOps().has(causalOp.getTargetOp().createReference())) {
116+
throw new Error('Creating undo because of an InvalidateAfterOp, but the op being undone does not depend on the invalidated one.');
117+
}
118+
119+
// First CascadedInvOp in a chain is always an UndoOp, after that undos and redos alternate.
120+
if (targetOp instanceof CascadedInvalidateOp) {
121+
if (this.undo === targetOp.undo) {
122+
throw new Error('Creating ' + opType + ' that has another ' + opType + ' as target, only alternating undo <- redo <- undo ... chains are admissible.');
123+
}
124+
} else {
125+
if (!this.undo) {
126+
throw new Error('A RedoOp can only have an UndoOp as target (found a ' + targetOp.getClassName() + ')');
127+
}
128+
}
129+
130+
if (causalOp instanceof InvalidateAfterOp) {
131+
132+
const invAfterOp = causalOp;
133+
134+
// invAfterOps can only be used as cause for UndoOps
135+
if (!undo) {
136+
throw new Error('Creating a RedoOp using an InvalidateAfterOp as causalOp (this should be an UndoOp then).');
137+
}
138+
139+
// here we could also check that targetOp is really outside of invalidateAfterOp.terminalOps,
140+
// but that's costly, and constructor checks aim only to aid debugging, so we'll not.
141+
142+
// invAfterOps can only be used as cause for ops within the same MutableObject
143+
if (!this.getTargetObject().equals(invAfterOp.getTargetObject())) {
144+
throw new Error('Trying to undo an op in a different mutable object than the invalidation op.');
145+
}
146+
147+
// undo / redo ops cannot be invalidated by a InvAfterOp
148+
if (targetOp instanceof CascadedInvalidateOp) {
149+
throw new Error('Creating an ' + opType + ' with an UndoOp or RedoOp as target, an an InvalidateAfterOp as cause. InvalidateAfterOps only affect regular ops, not undos/redos.');
150+
}
151+
} else if (causalOp instanceof CascadedInvalidateOp) {
152+
// we're covered here
153+
} else {
154+
throw new Error('The cause of an undo/redo can only be another UndoOp/RedoOp, or an InvalidateAfterOp.');
155+
}
156+
157+
}
158+
159+
}
160+
// Obs: The validate() method in an UndoOp can only check if the UndoOp itself is well built. However,
161+
// it is important to verify that the undo is consistent with the history already in the store.
162+
// There is a special validateUndosInContext method for that (haven't decided where yet).
163+
164+
async validate(references: Map<Hash, HashedObject>): Promise<boolean> {
165+
166+
if (!(await super.validate(references))) {
167+
return false;
168+
}
169+
170+
if (this.getAuthor() !== undefined) {
171+
return false;
172+
}
173+
174+
if (this.undo === undefined) {
175+
return false;
176+
}
177+
178+
if (typeof(this.undo) !== 'boolean') {
179+
return false;
180+
}
181+
182+
if (this.targetOp === undefined) {
183+
return false;
184+
}
185+
186+
if (!(this.targetOp instanceof MutationOp)) {
187+
return false;
188+
}
189+
190+
if (this.causalOps === undefined) {
191+
return false;
192+
}
193+
194+
if (!(this.causalOps instanceof HashedSet)) {
195+
return false;
196+
}
197+
198+
if (this.causalOps?.size() !== 1) {
199+
return false;
200+
}
201+
202+
const causalOpRef = this.causalOps.values().next().value;
203+
204+
if (!(causalOpRef instanceof HashReference)) {
205+
return false;
206+
}
207+
208+
const causalOp = references.get(causalOpRef.hash);
209+
210+
if (causalOp instanceof InvalidateAfterOp) {
211+
212+
const invAfterOp = causalOp;
213+
214+
// invAfterOps can only be used as cause for UndoOps
215+
if (!this.undo) {
216+
return false;
217+
}
218+
219+
// here we could also check that targetOp is really outside of invalidateAfterOp.terminalOps,
220+
// but that's costly, and constructor checks aim only to aid debugging, so we'll not.
221+
222+
// invAfterOps can only be used as cause for ops within the same MutableObject
223+
if (!this.getTargetObject().equals(invAfterOp.getTargetObject())) {
224+
return false;
225+
}
226+
227+
// undo / redo ops cannot be invalidated by a InvAfterOp
228+
if (this.targetOp instanceof CascadedInvalidateOp) {
229+
return false;
230+
}
231+
} else if (causalOp instanceof CascadedInvalidateOp) {
232+
// we're covered here
233+
} else {
234+
return false;
235+
}
236+
237+
// The cascade has merit: causalOp.targetOp \in targetOp.causalOps
238+
if (!this.targetOp.getCausalOps().has(causalOp.getTargetOp().createReference())) {
239+
return false;
240+
}
241+
242+
// First CascadedInvOp in a chain is always an UndoOp, after that undos and redos alternate.
243+
if (this.targetOp instanceof CascadedInvalidateOp) {
244+
if (this.undo === this.targetOp.undo) {
245+
return false;
246+
}
247+
} else {
248+
if (!this.undo) {
249+
return false;
250+
}
15251
}
16252

253+
return true;
17254
}
18255

19-
getClassName(): string {
20-
throw new Error("Method not implemented.");
256+
getTargetOp(): MutationOp {
257+
if (this.targetOp === undefined) {
258+
throw new Error('Trying to get targetOp for InvalidateAfterOp ' + this.hash() + ', but it is not present.');
259+
}
260+
261+
return this.targetOp;
21262
}
22-
init(): void {
23-
throw new Error("Method not implemented.");
263+
264+
literalizeInContext(context: Context, path: string, flags?: Array<string>) : Hash {
265+
266+
if (flags === undefined) {
267+
flags = [];
268+
}
269+
270+
if (this.undo) {
271+
flags.push('undo');
272+
} else {
273+
flags.push('redo');
274+
}
275+
276+
277+
return super.literalizeInContext(context, path, flags);
278+
24279
}
25280

26281
}

src/data/model/InvalidateAfterOp.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CascadedInvalidateOp } from './CascadedInvalidateOp';
12
import { HashedObject } from './HashedObject';
23
import { HashedSet } from './HashedSet';
34
import { Hash } from './Hashing';
@@ -13,7 +14,7 @@ class InvalidateAfterOp extends MutationOp {
1314
terminalOps?: HashedSet<HashReference<MutationOp>>;
1415

1516
// Meaning: invalidate targetOp after terminalOps, i.e. undo any ops that
16-
// have targetOp as causalOp but are not contained in the set of ops that
17+
// have targetOp in causalOps but are not contained in the set of ops that
1718
// come up to {terminalOps}.
1819

1920
constructor(targetOp?: MutationOp, terminalOps?: IterableIterator<MutationOp>) {
@@ -23,10 +24,18 @@ class InvalidateAfterOp extends MutationOp {
2324
this.targetOp = targetOp;
2425

2526
if (terminalOps === undefined) {
26-
throw new Error('InvalidateAfterOp cannot be created: "after" parameter is missing.');
27+
throw new Error('InvalidateAfterOp cannot be created: terminalOps parameter is missing.');
2728
} else {
2829
this.terminalOps = new HashedSet(Array.from(terminalOps).map((op: MutationOp) => op.createReference()).values());
2930
}
31+
32+
if (targetOp instanceof CascadedInvalidateOp) {
33+
throw new Error('An InvalidateAfterOp cannot target an undo / redo op directly.');
34+
}
35+
36+
if (targetOp instanceof InvalidateAfterOp) {
37+
throw new Error('An InvalidateAfterOp cannot target another InvalidateAfterOp directly.');
38+
}
3039
}
3140

3241
}
@@ -41,6 +50,7 @@ class InvalidateAfterOp extends MutationOp {
4150
return false;
4251
}
4352

53+
// check that the terminalOps and the InvAfterOp itself all point to the same MutableObject.
4454
for (const terminalOpRef of (this.terminalOps as HashedSet<HashReference<MutationOp>>).values()) {
4555

4656
const terminalOp = references.get(terminalOpRef.hash) as MutationOp;
@@ -54,8 +64,16 @@ class InvalidateAfterOp extends MutationOp {
5464
}
5565

5666
}
67+
68+
if (this.targetOp instanceof CascadedInvalidateOp) {
69+
return false;
70+
}
71+
72+
if (this.targetOp instanceof InvalidateAfterOp) {
73+
return false;
74+
}
5775

58-
if (!(this.targetOp as MutationOp).shouldAcceptNoMoreConsequencesOp(this)) {
76+
if (!(this.targetOp as MutationOp).shouldAcceptInvalidateAfterOp(this)) {
5977
return false;
6078
}
6179

src/data/model/MutationOp.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Hash } from './Hashing';
66
import { HashReference } from './HashReference';
77
import { OpCausalHistory, OpCausalHistoryProps } from 'data/history/OpCausalHistory';
88
import { InvalidateAfterOp } from './InvalidateAfterOp';
9+
import { CascadedInvalidateOp } from './CascadedInvalidateOp';
910

1011
abstract class MutationOp extends HashedObject {
1112

@@ -68,6 +69,14 @@ abstract class MutationOp extends HashedObject {
6869
} else if (! (causalOp instanceof MutationOp)) {
6970
return false;
7071
}
72+
73+
const thisIsACascade = this instanceof CascadedInvalidateOp;
74+
75+
if (causalOp instanceof CascadedInvalidateOp) {
76+
if (!thisIsACascade) {
77+
return false;
78+
}
79+
}
7180
}
7281
}
7382

@@ -83,14 +92,22 @@ abstract class MutationOp extends HashedObject {
8392

8493
}
8594

95+
getCausalOps() {
96+
if (this.causalOps === undefined) {
97+
throw new Error('Called getCausalOps, but this.causalOps is undefined.');
98+
}
99+
100+
return this.causalOps as HashedSet<HashReference<MutationOp>>;
101+
}
102+
86103
// By default, reject any causal ops. Override if necessary.
87104
async validateCausalOps(references: Map<Hash, HashedObject>): Promise<boolean> {
88105
references;
89106

90107
return this.causalOps === undefined;
91108
}
92109

93-
shouldAcceptNoMoreConsequencesOp(op: InvalidateAfterOp): boolean {
110+
shouldAcceptInvalidateAfterOp(op: InvalidateAfterOp): boolean {
94111
op;
95112
return false;
96113
}

0 commit comments

Comments
 (0)