UNPKG

mute-structs

Version:

NodeJS module providing an implementation of the LogootSplit CRDT algorithm

255 lines (201 loc) 9.85 kB
/* This file is part of MUTE-structs. Copyright (C) 2017 Matthieu Nicolas, Victorien Elvinger This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { isObject } from "./data-validation" import { Epoch} from "./epoch/epoch" import { EpochId } from "./epoch/epochid" import { compareEpochFullIds, EpochStore } from "./epoch/epochstore" import { Identifier } from "./identifier" import { IdentifierInterval } from "./identifierinterval" import { createAtPosition } from "./idfactory" import { LogootSRopes } from "./logootsropes" import { LogootSDel } from "./operations/delete/logootsdel" import { RenamableLogootSDel } from "./operations/delete/renamablelogootsdel" import { TextDelete } from "./operations/delete/textdelete" import { LogootSAdd } from "./operations/insert/logootsadd" import { RenamableLogootSAdd } from "./operations/insert/renamablelogootsadd" import { TextInsert } from "./operations/insert/textinsert" import { LogootSRename } from "./operations/rename/logootsrename" import { Ordering } from "./ordering" import { RenamingMap } from "./renamingmap/renamingmap" import { RenamingMapStore } from "./renamingmap/renamingmapstore" import { mkNodeAt } from "./ropesnodes" function generateInsertOps (idIntervals: IdentifierInterval[], str: string): LogootSAdd[] { let currentOffset = 0 return idIntervals .map((idInterval: IdentifierInterval): LogootSAdd => { const nextOffset = currentOffset + idInterval.length const content = str.slice(currentOffset, nextOffset) currentOffset = nextOffset return new LogootSAdd(idInterval.idBegin, content) }) } export interface RenamableReplicableListJSON { readonly epochsStore: EpochStore readonly renamingMapStore: RenamingMapStore readonly list: LogootSRopes readonly currentEpoch: Epoch } export class RenamableReplicableList { static create (replicaNumber = 0, clock = 0): RenamableReplicableList { const list = new LogootSRopes(replicaNumber, clock) const currentEpoch = new Epoch(new EpochId(0, 0)) const epochsStore = new EpochStore(currentEpoch) const renamingMapStore = new RenamingMapStore() return new RenamableReplicableList(list, currentEpoch, epochsStore, renamingMapStore) } static fromPlain (o: unknown): RenamableReplicableList | null { if (isObject<RenamableReplicableListJSON>(o)) { const list = LogootSRopes.fromPlain(o.list) const epochsStore = EpochStore.fromPlain(o.epochsStore) const renamingMapStore = RenamingMapStore.fromPlain(o.renamingMapStore) const currentEpoch = Epoch.fromPlain(o.currentEpoch) if (list !== null && epochsStore !== null && renamingMapStore !== null && currentEpoch !== null) { return new RenamableReplicableList(list, currentEpoch, epochsStore, renamingMapStore) } } return null } static fromPlainLogootSRopes (o: unknown): RenamableReplicableList | null { const list = LogootSRopes.fromPlain(o) if (list !== null) { const currentEpoch = new Epoch(new EpochId(0, 0)) const epochsStore = new EpochStore(currentEpoch) const renamingMapStore = new RenamingMapStore() return new RenamableReplicableList(list, currentEpoch, epochsStore, renamingMapStore) } return null } readonly epochsStore: EpochStore readonly renamingMapStore: RenamingMapStore private list: LogootSRopes private currentEpoch: Epoch private constructor ( list: LogootSRopes, currentEpoch: Epoch, epochsStore: EpochStore, renamingMapStore: RenamingMapStore) { this.list = list this.currentEpoch = currentEpoch this.epochsStore = epochsStore this.renamingMapStore = renamingMapStore } get replicaNumber (): number { return this.list.replicaNumber } get clock (): number { return this.list.clock } get currentRenamingMap (): RenamingMap { return this.renamingMapStore.getRenamingMap(this.currentEpoch.id) as RenamingMap } getList (): LogootSRopes { return this.list } getCurrentEpoch (): Epoch { return this.currentEpoch } get str (): string { return this.list.str } insertLocal (pos: number, l: string): RenamableLogootSAdd { return new RenamableLogootSAdd(this.list.insertLocal(pos, l), this.currentEpoch) } insertRemote (epoch: Epoch, op: LogootSAdd): TextInsert[] { if (!epoch.equals(this.currentEpoch)) { const strat = (rmap: RenamingMap, ids: Identifier[]) => rmap.initRenameIds(ids) const newIds = this.renameFromEpochToCurrent(op.insertedIds, epoch, strat) const newIdIntervals = IdentifierInterval.mergeIdsIntoIntervals(newIds) const insertOps = generateInsertOps(newIdIntervals, op.content) return insertOps .flatMap((insertOp: LogootSAdd): TextInsert[] => insertOp.execute(this.list)) } return op.execute(this.list) } delLocal (begin: number, end: number): RenamableLogootSDel { return new RenamableLogootSDel(this.list.delLocal(begin, end), this.currentEpoch) } delRemote (epoch: Epoch, op: LogootSDel): TextDelete[] { if (!epoch.equals(this.currentEpoch)) { const idsToRename = op.lid .flatMap((idInterval: IdentifierInterval): Identifier[] => idInterval.toIds()) const strat = (rmap: RenamingMap, ids: Identifier[]) => rmap.initRenameIds(ids) const newIds = this.renameFromEpochToCurrent(idsToRename, epoch, strat) const newIdIntervals = IdentifierInterval.mergeIdsIntoIntervals(newIds) const newOp = new LogootSDel(newIdIntervals, op.author) return newOp.execute(this.list) } return op.execute(this.list) } renameLocal (): LogootSRename { const renamedIdIntervals = this.list.toList() const clock = this.clock const newEpochNumber = this.currentEpoch.id.epochNumber + 1 const newEpochId = new EpochId(this.replicaNumber, newEpochNumber) this.currentEpoch = new Epoch(newEpochId, this.currentEpoch.id) this.epochsStore.addEpoch(this.currentEpoch) const newRandom = renamedIdIntervals[0].idBegin.tuples[0].random const renamingMap = new RenamingMap(this.replicaNumber, clock, renamedIdIntervals) this.renamingMapStore.add(this.currentEpoch, renamingMap) const baseId = createAtPosition(this.replicaNumber, clock, newRandom, 0) const newRoot = mkNodeAt(baseId, this.str.length) this.list = new LogootSRopes(this.replicaNumber, clock + 1, newRoot, this.str) return new LogootSRename(this.replicaNumber, clock, this.currentEpoch, renamedIdIntervals) } renameRemote (replicaNumber: number, clock: number, newEpoch: Epoch, renamedIdIntervals: IdentifierInterval[]) { const renamingMap = new RenamingMap(replicaNumber, clock, renamedIdIntervals) this.epochsStore.addEpoch(newEpoch) this.renamingMapStore.add(newEpoch, renamingMap) const newEpochFullId = this.epochsStore.getEpochFullId(newEpoch) const currentEpochFullId = this.epochsStore.getEpochFullId(this.currentEpoch) if (compareEpochFullIds(currentEpochFullId, newEpochFullId) === Ordering.Less) { const previousEpoch = this.currentEpoch this.currentEpoch = newEpoch const idsToRename = this.list.toList().flatMap((idInterval) => idInterval.toIds()) const strat = (rmap: RenamingMap, ids: Identifier[]) => rmap.initRenameSeq(ids) const newIds = this.renameFromEpochToCurrent(idsToRename, previousEpoch, strat) const newIdIntervals = IdentifierInterval.mergeIdsIntoIntervals(newIds) const newList = new LogootSRopes(this.replicaNumber, this.clock) const insertOps = generateInsertOps(newIdIntervals, this.str) insertOps.forEach((insertOp: LogootSAdd) => { insertOp.execute(newList) }) this.list = newList } } renameFromEpochToCurrent ( idsToRename: Identifier[], fromEpoch: Epoch, strat: (rmap: RenamingMap, ids: Identifier[]) => Identifier[], ): Identifier[] { const [epochsToRevert, epochsToApply] = this.epochsStore.getPathBetweenEpochs(fromEpoch, this.currentEpoch) let ids = idsToRename epochsToRevert.forEach((epoch) => { const rmap = this.renamingMapStore.getRenamingMap(epoch.id) as RenamingMap ids = ids.map((id) => rmap.reverseRenameId(id)) }) epochsToApply.forEach((epoch) => { const rmap = this.renamingMapStore.getRenamingMap(epoch.id) as RenamingMap ids = strat(rmap, ids) }) return ids } getNbBlocks (): number { return this.list.toList().length } digest (): number { return this.list.digest() } }