A world-class math input for the web
at main 618 lines 16 kB view raw
1export abstract class CRDT<V> { 2 abstract merge(other: CRDT<V>): CRDT<V>; 3 abstract getValue(): V; 4 abstract get DEBUG_data(): unknown; 5 // abstract setActorId(actorId: ActorID): CRDT<V>; 6} 7 8export type ActorID = string & { __brand: "ActorID" }; 9export function createActorID(id: string): ActorID { 10 return id as ActorID; 11} 12 13type DotHash = string & { __brand: "DotHash" }; 14 15export class Dot { 16 actorId: ActorID; 17 counter: number; 18 19 constructor(actorId: ActorID, counter: number) { 20 this.actorId = actorId; 21 this.counter = counter; 22 } 23 24 toString(): string { 25 return `${this.actorId}:${this.counter}`; 26 } 27 28 hash(): DotHash { 29 // TODO: Different/better hash function? 30 return btoa(`${this.actorId}:${this.counter}`) as DotHash; 31 } 32 33 equals(other: Dot): boolean { 34 return this.actorId === other.actorId && this.counter === other.counter; 35 } 36 37 compare(other: Dot): number { 38 if (this.counter !== other.counter) { 39 return this.counter - other.counter; 40 } 41 if (this.actorId < other.actorId) { 42 return -1; 43 } 44 if (this.actorId > other.actorId) { 45 return 1; 46 } 47 return 0; 48 } 49 50 static equals(a: Dot | null, b: Dot | null): boolean { 51 if (a === null || b === null) { 52 return a === b; 53 } 54 return a.equals(b); 55 } 56} 57 58export class GrowCounterCRDT extends CRDT<number> { 59 private data: Map<ActorID, number>; 60 61 constructor(data = new Map<ActorID, number>()) { 62 super(); 63 this.data = data; 64 } 65 66 get DEBUG_data(): Map<ActorID, number> { 67 return this.data; 68 } 69 70 increment(actorId: ActorID, amount: number = 1): GrowCounterCRDT { 71 if (amount <= 0) { 72 throw new Error("Increment amount must be positive"); 73 } 74 const current = this.data.get(actorId) || 0; 75 76 let newData = new Map(this.data); 77 newData.set(actorId, current + amount); 78 return new GrowCounterCRDT(newData); 79 } 80 81 getValue(): number { 82 let total = 0; 83 for (const value of this.data.values()) { 84 total += value; 85 } 86 return total; 87 } 88 89 merge(other: GrowCounterCRDT): GrowCounterCRDT { 90 const mergedData = new Map<ActorID, number>(this.data); 91 for (const [actorId, value] of other.data.entries()) { 92 const current = mergedData.get(actorId) ?? 0; 93 mergedData.set(actorId, Math.max(current, value)); 94 } 95 96 return new GrowCounterCRDT(mergedData); 97 } 98} 99 100export class PNCounterCRDT extends CRDT<number> { 101 private positive: GrowCounterCRDT; 102 private negative: GrowCounterCRDT; 103 104 constructor( 105 positive: GrowCounterCRDT = new GrowCounterCRDT(), 106 negative: GrowCounterCRDT = new GrowCounterCRDT(), 107 ) { 108 super(); 109 this.positive = positive; 110 this.negative = negative; 111 } 112 113 get DEBUG_data(): { 114 positive: Map<ActorID, number>; 115 negative: Map<ActorID, number>; 116 } { 117 return { 118 positive: this.positive.DEBUG_data, 119 negative: this.negative.DEBUG_data, 120 }; 121 } 122 123 change(actorId: ActorID, amount: number): PNCounterCRDT { 124 if (amount > 0) { 125 return new PNCounterCRDT( 126 this.positive.increment(actorId, amount), 127 this.negative, 128 ); 129 } else if (amount < 0) { 130 return new PNCounterCRDT( 131 this.positive, 132 this.negative.increment(actorId, -amount), 133 ); 134 } else { 135 return this; 136 } 137 } 138 139 set(actorId: ActorID, value: number): PNCounterCRDT { 140 const currentValue = this.getValue(); 141 const delta = value - currentValue; 142 return this.change(actorId, delta); 143 } 144 145 getValue(): number { 146 return this.positive.getValue() - this.negative.getValue(); 147 } 148 149 merge(other: PNCounterCRDT): PNCounterCRDT { 150 return new PNCounterCRDT( 151 this.positive.merge(other.positive), 152 this.negative.merge(other.negative), 153 ); 154 } 155} 156 157export class GrowSetCRDT<T> extends CRDT<Set<T>> { 158 private data: Set<T>; 159 160 constructor(data: Set<T> = new Set<T>()) { 161 super(); 162 this.data = data; 163 } 164 165 get DEBUG_data(): Set<T> { 166 return this.data; 167 } 168 169 add(element: T): GrowSetCRDT<T> { 170 const newData = new Set(this.data); 171 newData.add(element); 172 return new GrowSetCRDT(newData); 173 } 174 175 getValue(): Set<T> { 176 return new Set(this.data); 177 } 178 179 merge(other: GrowSetCRDT<T>): GrowSetCRDT<T> { 180 const mergedData = new Set<T>(this.data); 181 for (const element of other.data) { 182 mergedData.add(element); 183 } 184 return new GrowSetCRDT(mergedData); 185 } 186} 187 188export class SetCRDT<T> extends CRDT<Set<T>> { 189 private addSet: GrowSetCRDT<T>; 190 private removeSet: GrowSetCRDT<T>; 191 192 constructor( 193 addSet: GrowSetCRDT<T> = new GrowSetCRDT<T>(), 194 removeSet: GrowSetCRDT<T> = new GrowSetCRDT<T>(), 195 ) { 196 super(); 197 this.addSet = addSet; 198 this.removeSet = removeSet; 199 } 200 201 get DEBUG_data(): { addSet: Set<T>; removeSet: Set<T> } { 202 return { 203 addSet: this.addSet.DEBUG_data, 204 removeSet: this.removeSet.DEBUG_data, 205 }; 206 } 207 208 add(element: T): SetCRDT<T> { 209 return new SetCRDT(this.addSet.add(element), this.removeSet); 210 } 211 212 remove(element: T): SetCRDT<T> { 213 return new SetCRDT(this.addSet, this.removeSet.add(element)); 214 } 215 216 getValue(): Set<T> { 217 const result = new Set<T>(); 218 for (const element of this.addSet.getValue()) { 219 if (!this.removeSet.getValue().has(element)) { 220 result.add(element); 221 } 222 } 223 return result; 224 } 225 226 merge(other: SetCRDT<T>): SetCRDT<T> { 227 return new SetCRDT( 228 this.addSet.merge(other.addSet), 229 this.removeSet.merge(other.removeSet), 230 ); 231 } 232} 233 234export class LWWRegisterCRDT<T> extends CRDT<T> { 235 private data: { value: T; dot: Dot }; 236 237 constructor(value: T, dot: Dot) { 238 super(); 239 this.data = { value, dot }; 240 } 241 242 get DEBUG_data() { 243 return this.data; 244 } 245 246 set(actorId: ActorID, value: T): LWWRegisterCRDT<T> { 247 const newCounter = this.data.dot.counter + 1; 248 return new LWWRegisterCRDT<T>(value, new Dot(actorId, newCounter)); 249 } 250 251 getValue(): T { 252 return this.data.value; 253 } 254 255 merge(other: LWWRegisterCRDT<T>): LWWRegisterCRDT<T> { 256 const compare = other.data.dot.compare(this.data.dot); 257 258 if (compare === 0) { 259 if (this.data.value === other.data.value) { 260 return this; 261 } else { 262 throw new Error("Conflict detected in LWWRegisterCRDT merge"); 263 } 264 } else if (compare > 0) { 265 return other; 266 } else { 267 return this; 268 } 269 } 270} 271 272export class LWWMapCRDT<K, V> extends CRDT<Map<K, V>> { 273 private data: Map<K, LWWRegisterCRDT<V>>; 274 275 constructor(data: Map<K, LWWRegisterCRDT<V>> = new Map()) { 276 super(); 277 this.data = data; 278 } 279 280 get DEBUG_data(): Map<K, { value: V; dot: Dot }> { 281 const debugData = new Map<K, { value: V; dot: Dot }>(); 282 for (const [key, register] of this.data.entries()) { 283 debugData.set(key, register.DEBUG_data); 284 } 285 return debugData; 286 } 287 288 set(actorId: ActorID, key: K, value: V): LWWMapCRDT<K, V> { 289 const newData = new Map(this.data); 290 const existingRegister = newData.get(key); 291 if (existingRegister) { 292 newData.set(key, existingRegister.set(actorId, value)); 293 } else { 294 newData.set(key, new LWWRegisterCRDT<V>(value, new Dot(actorId, 1))); 295 } 296 return new LWWMapCRDT(newData); 297 } 298 299 getRegister(key: K): LWWRegisterCRDT<V> | undefined { 300 return this.data.get(key); 301 } 302 303 getValue(): Map<K, V> { 304 const result = new Map<K, V>(); 305 for (const [key, register] of this.data.entries()) { 306 result.set(key, register.getValue()); 307 } 308 return result; 309 } 310 311 merge(other: LWWMapCRDT<K, V>): LWWMapCRDT<K, V> { 312 const mergedData = new Map<K, LWWRegisterCRDT<V>>(this.data); 313 for (const [key, otherRegister] of other.data.entries()) { 314 const existingRegister = mergedData.get(key); 315 if (existingRegister) { 316 mergedData.set(key, existingRegister.merge(otherRegister)); 317 } else { 318 mergedData.set(key, otherRegister); 319 } 320 } 321 return new LWWMapCRDT(mergedData); 322 } 323} 324 325class DotIdSet<V extends { id: Dot }> { 326 private data: Map<DotHash, V>; 327 constructor(data: Map<DotHash, V> | DotIdSet<V> = new Map<DotHash, V>()) { 328 this.data = data instanceof DotIdSet ? new Map(data.data) : new Map(data); 329 } 330 331 add(element: V): void { 332 this.data.set(element.id.hash(), element); 333 } 334 335 get(id: Dot): V | undefined { 336 return this.data.get(id.hash()); 337 } 338 339 values(): IterableIterator<V> { 340 return this.data.values(); 341 } 342 343 delete(id: Dot): void { 344 this.data.delete(id.hash()); 345 } 346} 347 348export class SequenceCRDT<V, C extends CRDT<V>> extends CRDT< 349 { id: Dot; value: V }[] 350> { 351 private counter: number; 352 private data: DotIdSet<{ 353 id: Dot; 354 left: LWWRegisterCRDT<Dot | null>; 355 deleted: boolean; 356 value: C; 357 }>; 358 359 constructor( 360 counter: number = 0, 361 data: DotIdSet<{ 362 id: Dot; 363 left: LWWRegisterCRDT<Dot | null>; 364 deleted: boolean; 365 value: C; 366 }> = new DotIdSet(), 367 ) { 368 super(); 369 this.counter = counter; 370 this.data = data; 371 } 372 373 get DEBUG_data() { 374 return this.data; 375 } 376 377 insert( 378 actorId: ActorID, 379 leftId: Dot | null, 380 value: C, 381 ): [SequenceCRDT<V, C>, Dot | null] { 382 const elementId = new Dot(actorId, this.counter + 1); 383 384 const newData = new DotIdSet(this.data); 385 newData.add({ 386 id: elementId, 387 left: new LWWRegisterCRDT<Dot | null>(leftId, new Dot(actorId, 0)), 388 deleted: false, 389 value, 390 }); 391 392 // Any existing elements that had leftId as their left should now point to the new element 393 for (const element of this.data.values()) { 394 const elemLeft = element.left.getValue(); 395 if (Dot.equals(elemLeft, leftId)) { 396 newData.delete(element.id); 397 newData.add({ 398 id: element.id, 399 left: element.left.set(actorId, elementId), 400 deleted: element.deleted, 401 value: element.value, 402 }); 403 } 404 } 405 406 return [new SequenceCRDT(this.counter + 1, newData), elementId]; 407 } 408 409 setItemValue( 410 actorId: ActorID, 411 id: Dot, 412 value: C | ((oldValue: C) => C), 413 ): SequenceCRDT<V, C> { 414 const newData = new DotIdSet(this.data); 415 for (const element of this.data.values()) { 416 if (element.id.equals(id)) { 417 newData.delete(element.id); 418 newData.add({ 419 id: element.id, 420 left: element.left, 421 deleted: element.deleted, 422 value: typeof value === "function" ? value(element.value) : value, 423 }); 424 break; 425 } 426 } 427 return new SequenceCRDT(this.counter, newData); 428 } 429 430 delete(actorId: ActorID, id: Dot): SequenceCRDT<V, C> { 431 const newData = new DotIdSet(this.data); 432 let deletedElement: { 433 id: Dot; 434 left: LWWRegisterCRDT<Dot | null>; 435 deleted: boolean; 436 value: CRDT<unknown>; 437 } | null = null; 438 439 for (const element of this.data.values()) { 440 if (element.id.equals(id)) { 441 if (element.deleted) return this; // Already deleted 442 443 deletedElement = element; 444 newData.delete(element.id); 445 newData.add({ 446 id: element.id, 447 left: element.left, 448 deleted: true, 449 value: element.value, 450 }); 451 break; 452 } 453 } 454 455 if (deletedElement) { 456 const deletedLeftId = deletedElement.left.getValue(); 457 // Reassign right siblings to point to the deleted element's left 458 for (const element of this.data.values()) { 459 const elemLeft = element.left.getValue(); 460 if (Dot.equals(elemLeft, deletedElement.id)) { 461 newData.delete(element.id); 462 newData.add({ 463 id: element.id, 464 left: element.left.set(actorId, deletedLeftId), 465 deleted: element.deleted, 466 value: element.value, 467 }); 468 } 469 } 470 } 471 472 return new SequenceCRDT(this.counter, newData); 473 } 474 475 getValue(): { 476 id: Dot; 477 DEBUG_left: Dot | null; 478 value: V; 479 }[] { 480 const getElementsWithLeftId = (leftId: Dot | null) => { 481 return new Set( 482 [...this.data.values()].filter((element) => { 483 const elementLeft = element.left.getValue(); 484 if (leftId === null || elementLeft === null) { 485 return leftId === elementLeft; 486 } 487 return elementLeft.equals(leftId); 488 }), 489 ); 490 }; 491 492 const result: { 493 id: Dot; 494 DEBUG_left: Dot | null; 495 value: V; 496 }[] = []; 497 const visited = new Set<Dot>(); 498 499 const isVisited = (id: Dot) => { 500 for (const visitedId of visited) { 501 if (Dot.equals(visitedId, id)) return true; 502 } 503 return false; 504 }; 505 506 const appendRightSiblings = (leftId: Dot | null) => { 507 const siblings = getElementsWithLeftId(leftId); 508 const sortedSiblings = Array.from(siblings).sort((a, b) => 509 a.id.compare(b.id), 510 ); 511 for (const sibling of sortedSiblings) { 512 if (!isVisited(sibling.id)) { 513 if (!sibling.deleted) { 514 result.push({ 515 id: sibling.id, 516 DEBUG_left: sibling.left.getValue(), 517 value: sibling.value.getValue(), 518 }); 519 } 520 visited.add(sibling.id); 521 appendRightSiblings(sibling.id); 522 } 523 } 524 }; 525 526 appendRightSiblings(null); 527 528 return result; 529 } 530 531 getValueWithCursor( 532 cursor: Dot | null, 533 ): [{ id: Dot; DEBUG_left: Dot | null; value: V }[], number] { 534 const elements = this.getValue(); 535 if (cursor === null) { 536 return [elements, 0]; 537 } 538 539 let notDeletedLeftId: Dot = cursor; 540 const getElementById = (id: Dot) => { 541 for (const element of this.data.values()) { 542 if (element.id.equals(id)) return element; 543 } 544 return null; 545 }; 546 while (getElementById(notDeletedLeftId)?.deleted) { 547 const leftOfDeleted = getElementById(notDeletedLeftId)?.left.getValue(); 548 if (leftOfDeleted === undefined) { 549 throw new Error("leftId points to non-existent element"); 550 } 551 if (leftOfDeleted === null) { 552 return [elements, 0]; 553 } 554 notDeletedLeftId = leftOfDeleted; 555 } 556 557 let index = 0; 558 for (const element of elements) { 559 if (element.id.equals(notDeletedLeftId)) { 560 return [elements, index + 1]; 561 } 562 index++; 563 } 564 565 throw new Error("Cursor not found in sequence"); 566 } 567 568 merge(other: SequenceCRDT<V, C>): SequenceCRDT<V, C> { 569 const mergedData = new DotIdSet(this.data); 570 for (const otherElement of other.data.values()) { 571 // Find if this element already exists 572 let exists = false; 573 for (const existingElement of mergedData.values()) { 574 if (existingElement.id.equals(otherElement.id)) { 575 // Merge the LWWMapCRDT values 576 const mergedValue = existingElement.value.merge(otherElement.value); 577 const mergedLeft = existingElement.left.merge(otherElement.left); 578 mergedData.delete(existingElement.id); 579 mergedData.add({ 580 id: existingElement.id, 581 left: mergedLeft, 582 deleted: existingElement.deleted || otherElement.deleted, 583 value: mergedValue as C, 584 }); 585 exists = true; 586 break; 587 } 588 } 589 if (!exists) { 590 mergedData.add({ 591 id: otherElement.id, 592 left: otherElement.left, 593 deleted: otherElement.deleted, 594 value: otherElement.value, 595 }); 596 } 597 } 598 return new SequenceCRDT(Math.max(this.counter, other.counter), mergedData); 599 } 600 601 setActorId(actorId: ActorID): SequenceCRDT<V, C> { 602 const newData = new DotIdSet<{ 603 id: Dot; 604 left: LWWRegisterCRDT<Dot | null>; 605 deleted: boolean; 606 value: C; 607 }>(); 608 for (const element of this.data.values()) { 609 newData.add({ 610 id: new Dot(actorId, element.id.counter), 611 left: element.left, 612 deleted: element.deleted, 613 value: element.value, 614 }); 615 } 616 return new SequenceCRDT(this.counter, newData); 617 } 618}