Skip to content

Commit 806ed7c

Browse files
committed
adding a CausalReference implementation - still untested
1 parent a4e6780 commit 806ed7c

File tree

2 files changed

+376
-0
lines changed

2 files changed

+376
-0
lines changed

src/data/collections.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './collections/mutable/MutableSet';
33
export * from './collections/mutable/MutableReference';
44
export * from './collections/mutable/MutableArray';
55
export * from './collections/causal/CausalSet';
6+
export * from './collections/causal/CausalReference';
67
export { SingleAuthorCausalSet } from './collections/causal/SingleAuthorCausalSet';
78
export { MultiAuthorCausalSet } from './collections/causal/MultiAuthorCausalSet';
89
export * from './collections/causal/CausalArray';
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
import { MultiMap } from 'util/multimap';
2+
import { Timestamps } from 'util/timestamps';
3+
4+
import { Identity } from '../../identity';
5+
import { Hash, HashedObject, MutableObject, MutationOp, MutableContentEvents, ClassRegistry } from '../../model';
6+
import { RefUpdateOp } from '../mutable/MutableReference';
7+
8+
import { Authorizer } from '../../model/causal/Authorization';
9+
import { Verification } from '../../model/causal/Authorization';
10+
11+
import { AuthError, BaseCausalCollection, CausalCollectionConfig } from './CausalCollection';
12+
13+
14+
type UpdateSig = {
15+
opHash: Hash,
16+
sequence: number,
17+
timestamp: string
18+
};
19+
20+
function sig(op: CausalRefUpdateOp<any>) {
21+
return {opHash: op.getLastHash(), sequence: op.sequence as number, timestamp: op.timestamp as string};
22+
}
23+
24+
// We want an array with the "latest" update (by causality, then timestmap, then hash) at the end
25+
26+
// the following should return -1 if u2 comes after u1;
27+
28+
function compareUpdateSigs(u1: UpdateSig, u2: UpdateSig) {
29+
if (u2.sequence > u1.sequence) {
30+
return -1;
31+
} else if (u1.sequence < u2.sequence) {
32+
return 1;
33+
} else { // u2.sequence === u1.sequence
34+
if (Timestamps.after(u2.timestamp, u1.timestamp)) {
35+
return -1;
36+
} else if (Timestamps.after(u1.timestamp, u2.timestamp)) {
37+
return 1;
38+
} else { // u2.timestamp === u1.timestamp
39+
return u1.opHash.localeCompare(u2.opHash);
40+
}
41+
}
42+
}
43+
44+
class CausalReference<T> extends BaseCausalCollection<T> {
45+
46+
static className = 'hhs/v0/CausalReference';
47+
48+
// all the applied update ops, the latest that is valid is the current value.
49+
_causallyOrderedUpdates: Array<UpdateSig>;
50+
51+
_latestValidIdx?: number; // <- we cache the idx of the latest valid op in the array,
52+
_value?: T; // <- and its value, if any.
53+
54+
_largestSequence?: number;
55+
56+
constructor(config?: CausalCollectionConfig) {
57+
super([CausalRefUpdateOp.className], config);
58+
59+
this.setRandomId();
60+
61+
this._causallyOrderedUpdates = [];
62+
}
63+
64+
getValue() : T|undefined {
65+
return this._value;
66+
}
67+
68+
async setValue(value: T, author?: Identity): Promise<void> {
69+
70+
if (!(value instanceof HashedObject) && !HashedObject.isLiteral(value)) {
71+
throw new Error('CausalReferences can contain either a class deriving from HashedObject or a pure literal (a constant, without any HashedObjects within).');
72+
}
73+
74+
if (!this.shouldAcceptElement(value)) {
75+
throw new Error('CausalReference has type/element contraints that reject the element that is being added:' + value)
76+
}
77+
78+
const nextSeq = this._largestSequence === undefined? 0 : this._largestSequence + 1;
79+
80+
let op = new CausalRefUpdateOp<T>(this, value, nextSeq, author);
81+
82+
const auth = this.createUpdateAuthorizer(author);
83+
84+
this.setCurrentPrevOpsTo(op);
85+
86+
if (!(await auth.attempt(op))) {
87+
throw new AuthError('Cannot authorize addition operation on CausalReference ' + this.hash() + ', author is: ' + author?.hash());
88+
}
89+
90+
return this.applyNewOp(op);
91+
}
92+
93+
protected createUpdateAuthorizer(author?: Identity): Authorizer {
94+
return this.createWriteAuthorizer(author);
95+
}
96+
97+
async canSetValue(_value?: T, author?: Identity): Promise<boolean> {
98+
return this.createUpdateAuthorizer(author).attempt();
99+
}
100+
101+
async mutate(op: MutationOp, valid: boolean): Promise<boolean> {
102+
let refUpdateOp = op as CausalRefUpdateOp<T>;
103+
104+
105+
let mutated = false;
106+
107+
if (op instanceof RefUpdateOp) {
108+
109+
const up = sig(op);
110+
111+
let idx: number; // the position of op in the array
112+
113+
if (!this._allAppliedOps.has(up.opHash)) {
114+
115+
// if the op has not been applied, we find the right place for it:
116+
const length = this._causallyOrderedUpdates.length
117+
idx = length;
118+
119+
while (idx>0 && compareUpdateSigs(this._causallyOrderedUpdates[idx-1], up) > 0) {
120+
idx=idx-1;
121+
}
122+
123+
// and then insert it there:
124+
this._causallyOrderedUpdates.splice(idx, 0, up);
125+
} else {
126+
127+
// else, we just go through the array until we find it
128+
idx = length-1;
129+
130+
while (idx>0 && this._causallyOrderedUpdates[idx-1].opHash !== up.opHash) {
131+
idx=idx-1;
132+
}
133+
}
134+
135+
let newValueIdx: number|undefined;
136+
let newValueOp: CausalRefUpdateOp<T>|undefined;
137+
138+
let unsetValue = false; // to indicate that there are no valid ops left,
139+
// and that the value should be set back to undefined
140+
141+
if (valid) {
142+
if (this._latestValidIdx === undefined || idx > this._latestValidIdx) {
143+
// we need to set a new value!
144+
newValueIdx = idx;
145+
newValueOp = op;
146+
}
147+
} else {
148+
if (this._latestValidIdx === idx) {
149+
// the current value has been invalidated, look for the next-best
150+
151+
let nextValueIdx = length;
152+
153+
while (nextValueIdx>=0 && !this.isValidOp(this._causallyOrderedUpdates[nextValueIdx].opHash)) {
154+
nextValueIdx=nextValueIdx-1;
155+
}
156+
157+
if (nextValueIdx >= 0) {
158+
newValueIdx = nextValueIdx;
159+
newValueOp = await this.loadOp(this._causallyOrderedUpdates[nextValueIdx].opHash) as CausalRefUpdateOp<T>;
160+
} else {
161+
if (this._value !== undefined) {
162+
unsetValue = true;
163+
}
164+
}
165+
}
166+
}
167+
168+
mutated = unsetValue || newValueIdx !== undefined;
169+
170+
const oldValue = this._value;
171+
172+
if (unsetValue) {
173+
this._latestValidIdx = undefined;
174+
this._value = undefined;
175+
this._largestSequence = undefined;
176+
} else if (newValueIdx !== undefined) {
177+
this._latestValidIdx = newValueIdx;
178+
this._value = newValueOp?.value;
179+
this._largestSequence = newValueOp?.sequence;
180+
}
181+
182+
if (mutated) {
183+
this._mutationEventSource?.emit({emitter: this, action: 'update', data: refUpdateOp.getValue()});
184+
185+
if (oldValue !== this._value) {
186+
if (oldValue instanceof HashedObject) {
187+
this._mutationEventSource?.emit({emitter: this, action: MutableContentEvents.RemoveObject, data: oldValue});
188+
}
189+
if (this._value instanceof HashedObject) {
190+
this._mutationEventSource?.emit({emitter: this, action: MutableContentEvents.AddObject, data: this._value});
191+
}
192+
}
193+
}
194+
195+
}
196+
197+
return Promise.resolve(mutated);
198+
}
199+
200+
getMutableContents(): MultiMap<Hash, HashedObject> {
201+
const contents = new MultiMap<Hash, HashedObject>();
202+
203+
if (this._value instanceof HashedObject) {
204+
contents.add(this._value.hash(), this._value);
205+
}
206+
207+
return contents;
208+
}
209+
210+
getMutableContentByHash(hash: Hash): Set<HashedObject> {
211+
212+
const found = new Set<HashedObject>();
213+
214+
if (this._value instanceof HashedObject && this._value.hash() === hash) {
215+
found.add(this._value);
216+
}
217+
218+
return found;
219+
}
220+
221+
getClassName(): string {
222+
return CausalReference.className;
223+
}
224+
225+
init(): void {
226+
227+
}
228+
229+
async validate(references: Map<Hash, HashedObject>) {
230+
231+
if (!(await super.validate(references))) {
232+
return false;
233+
}
234+
235+
return true;
236+
}
237+
238+
shouldAcceptMutationOp(op: MutationOp, opReferences: Map<Hash, HashedObject>): boolean {
239+
240+
if (!super.shouldAcceptMutationOp(op, opReferences)) {
241+
return false;
242+
}
243+
244+
if (op instanceof CausalRefUpdateOp) {
245+
246+
if (!this.shouldAcceptElement(op.value as T)) {
247+
return false;
248+
}
249+
250+
const author = op.getAuthor();
251+
252+
const auth = this.createUpdateAuthorizer(author);
253+
254+
const usedKeys = new Set<string>();
255+
256+
if (!auth.verify(op, usedKeys)) {
257+
return false;
258+
}
259+
260+
if (!Verification.checkKeys(usedKeys, op)) {
261+
return false;
262+
}
263+
264+
}
265+
266+
return true;
267+
}
268+
}
269+
270+
class CausalRefUpdateOp<T> extends MutationOp {
271+
272+
static className = 'hhs/v0/CausalRefUpdateOp';
273+
274+
sequence?: number;
275+
timestamp?: string;
276+
value?: T;
277+
278+
279+
constructor(targetObject?: CausalReference<T>, value?: T, sequence?: number, author?: Identity) {
280+
super(targetObject);
281+
282+
if (targetObject !== undefined) {
283+
this.value = value;
284+
this.sequence = sequence;
285+
this.timestamp = Timestamps.uniqueTimestamp();
286+
287+
if (author !== undefined) {
288+
this.setAuthor(author);
289+
}
290+
}
291+
292+
}
293+
294+
getClassName(): string {
295+
return CausalRefUpdateOp.className;
296+
}
297+
298+
init(): void {
299+
300+
}
301+
302+
async validate(references: Map<Hash, HashedObject>) {
303+
304+
if (!await super.validate(references)) {
305+
return false;
306+
}
307+
308+
const targetObject = this.getTargetObject();
309+
310+
if (!(targetObject instanceof CausalReference)) {
311+
return false;
312+
}
313+
314+
if (this.sequence === undefined) {
315+
MutableObject.validationLog.debug('The field sequence is mandatory in class RefUpdateOp');
316+
return false;
317+
}
318+
319+
if ((typeof this.sequence) !== 'number') {
320+
MutableObject.validationLog.debug('The field sequence should be of type number in class RefUpdateop');
321+
return false;
322+
}
323+
324+
if (this.timestamp === undefined) {
325+
MutableObject.validationLog.debug('The field timestamp is mandatory in class RefUpdateOp');
326+
return false;
327+
}
328+
329+
if ((typeof this.timestamp) !== 'string') {
330+
MutableObject.validationLog.debug('The field timestamp should be of type timestamp in class RefUpdateop');
331+
return false;
332+
}
333+
334+
if (this.value === undefined) {
335+
MutableObject.validationLog.debug('The field value is mandatory in class REfUpdateop');
336+
return false;
337+
}
338+
339+
if (targetObject.acceptedElementHashes !== undefined && !targetObject.acceptedElementHashes.has(HashedObject.hashElement(this.value))) {
340+
return false;
341+
}
342+
343+
if (targetObject.acceptedTypes !== undefined &&
344+
!(
345+
(this.value instanceof HashedObject && targetObject.acceptedTypes.has(this.value.getClassName()))
346+
||
347+
(!(this.value instanceof HashedObject) && targetObject.acceptedTypes.has(typeof(this.value)))
348+
)
349+
350+
) {
351+
352+
return false;
353+
354+
}
355+
356+
return true;
357+
}
358+
359+
getSequence() {
360+
return this.sequence as number;
361+
}
362+
363+
getTimestamp() {
364+
return this.timestamp as string;
365+
}
366+
367+
getValue() {
368+
return this.value as T;
369+
}
370+
}
371+
372+
ClassRegistry.register(CausalReference.className, CausalReference);
373+
ClassRegistry.register(CausalRefUpdateOp.className, CausalRefUpdateOp);
374+
375+
export { CausalReference };

0 commit comments

Comments
 (0)