diff --git a/src/ControlledOpSet.ts b/src/ControlledOpSet.ts index ddd4740..dbb434d 100644 --- a/src/ControlledOpSet.ts +++ b/src/ControlledOpSet.ts @@ -1,14 +1,8 @@ import {Timestamp} from "./helper/Timestamp"; -import { - asType, - mapMapToMap, - mapWith, - mapWithout, - RoArray, - RoMap, -} from "./helper/Collection"; +import {asType, RoArray, RoMap} from "./helper/Collection"; import {TypedValue} from "./helper/TypedValue"; import {AssertFailed} from "./helper/Assert"; +import {HashMap} from "prelude-ts"; export class DeviceId extends TypedValue<"DeviceId", string> {} @@ -45,7 +39,7 @@ export type UndoOp = ( type DesiredHeads = ( value: Value, -) => RoMap>; +) => HashMap>; export class ControlledOpSet { static create( @@ -60,7 +54,7 @@ export class ControlledOpSet { desiredHeads, value, undefined, - RoMap(), + HashMap.empty(), ); } @@ -71,28 +65,31 @@ export class ControlledOpSet { readonly value: Value, readonly appliedHead: AppliedOpList | undefined, - readonly heads: RoMap>, + readonly heads: HashMap>, ) {} update( - remoteHeads: RoMap>, + remoteHeads: HashMap>, ): ControlledOpSet { const abstractDesiredHeads = this.desiredHeads(this.value); - const filteredAbstractDesiredHeads = RoMap( - Array.from(abstractDesiredHeads.entries()).filter( - ([deviceId]) => remoteHeads.get(deviceId) !== undefined, - ), - ); - const desiredHeads = mapMapToMap( - filteredAbstractDesiredHeads, - (deviceId, openOrOp) => [ - deviceId, - asType>( - openOrOp === "open" ? remoteHeads.get(deviceId)! : openOrOp, - ), - ], + const filteredAbstractDesiredHeads = abstractDesiredHeads.filterKeys( + (deviceId) => remoteHeads.get(deviceId).getOrUndefined() !== undefined, ); - if (ControlledOpSet.headsEqual(desiredHeads, this.heads)) return this; + const desiredHeads = filteredAbstractDesiredHeads.map< + DeviceId, + OpList + >((deviceId, openOrOp) => [ + deviceId, + asType>( + openOrOp === "open" + ? remoteHeads + .get(deviceId) + .getOrThrow("There should be a remote head for this deviceId")! + : openOrOp, + ), + ]); + + if (desiredHeads.equals(this.heads)) return this; const {value, appliedHead, ops} = ControlledOpSet.commonStateAndDesiredOps( this.undoOp, @@ -131,8 +128,8 @@ export class ControlledOpSet { AppliedOp extends AppliedOpBase, >( undoOp: UndoOp, - desiredHeads: RoMap>, - actualHeads: RoMap>, + desiredHeads: HashMap>, + actualHeads: HashMap>, value: Value, appliedHead: undefined | AppliedOpList, ): { @@ -142,7 +139,7 @@ export class ControlledOpSet { } { const ops = new Array(); - while (!ControlledOpSet.headsEqual(desiredHeads, actualHeads)) { + while (!desiredHeads.equals(actualHeads)) { const {heads: nextRemainingDesiredHeads, op: desiredOp} = ControlledOpSet.undoHeadsOnce(desiredHeads); const {heads: nextActualHeads, op: actualOp} = @@ -205,25 +202,25 @@ export class ControlledOpSet { } private static undoHeadsOnce( - heads: RoMap>, + heads: HashMap>, ): { op: undefined | AppliedOp["op"]; - heads: RoMap>; + heads: HashMap>; } { - if (heads.size === 0) return {op: undefined, heads}; + if (heads.length() === 0) return {op: undefined, heads}; - const [newestDeviceId, newestOpList] = Array.from(heads.entries()).reduce( - (winner, current) => { + const [newestDeviceId, newestOpList] = heads + .reduce((winner, current) => { return winner[1].op.timestamp > current[1].op.timestamp ? winner : current; - }, - ); + }) + .getOrThrow("Reducing over empty heads"); return { op: newestOpList.op, heads: newestOpList.prev - ? mapWith(heads, newestDeviceId, newestOpList.prev) - : mapWithout(heads, newestDeviceId), + ? heads.put(newestDeviceId, newestOpList.prev) + : heads.remove(newestDeviceId), }; } diff --git a/src/PermissionedTree.monkey.test.ts b/src/PermissionedTree.monkey.test.ts index 2b70409..ca24535 100644 --- a/src/PermissionedTree.monkey.test.ts +++ b/src/PermissionedTree.monkey.test.ts @@ -1,12 +1,5 @@ import seedRandom from "seed-random"; -import { - asType, - definedOrThrow, - mapMapToMap, - mapWith, - RoArray, - RoMap, -} from "./helper/Collection"; +import {asType, definedOrThrow, RoArray} from "./helper/Collection"; import { AppliedOp, createPermissionedTree, @@ -16,6 +9,7 @@ import { import {DeviceId} from "./ControlledOpSet"; import {Clock} from "./helper/Clock"; import {Timestamp} from "./helper/Timestamp"; +import {HashMap} from "prelude-ts"; import {expectPreludeEqual} from "./helper/Shared.testing"; type OpType = "add" | "move" | "update"; @@ -86,11 +80,11 @@ describe("PermissionedTree.monkey", function () { priority: -1, status: "open", }); - let devices = RoMap( - Array.from(Array(4).keys()).map((index) => [ - DeviceId.create(`device${index}`), - initialTree, - ]), + let devices = HashMap.of( + [DeviceId.create("device1"), initialTree], + [DeviceId.create("device2"), initialTree], + [DeviceId.create("device3"), initialTree], + [DeviceId.create("device4"), initialTree], ); for (let turn = 0; turn < 10000; turn++) { @@ -107,38 +101,42 @@ describe("PermissionedTree.monkey", function () { // Update a device. { - const device = randomInArray(rand, Array.from(devices.keys()))!; - const tree = devices.get(device)!; + const device = randomInArray(rand, Array.from(devices.keySet()))!; + const tree = devices.get(device).getOrThrow("Device should be listed"); const opType = randomOpType(rand); log(`turn ${turn}, device ${device}, op type ${opType}`); if (opType === "update") { - const remoteHeads = mapMapToMap(devices, (device, tree) => [ + const remoteHeads = devices.map((device, tree) => [ device, - tree.heads.get(device), + tree.heads + .get(device) + .getOrThrow("Couldn't get device in tree heads"), ]); const tree1 = tree.update(remoteHeads); - devices = mapWith(devices, device, tree1); + devices = devices.put(device, tree1); } else { const op = opForOpType(clock, log, rand, opType, tree); const tree1 = applyNewOp(tree, device, op); - devices = mapWith(devices, device, tree1); + devices = devices.put(device, tree1); } } // Check the devices. if (turn % 10 === 0) { - const remoteHeads = mapMapToMap(devices, (device, tree) => [ + const remoteHeads = devices.map((device, tree) => [ device, - tree.heads.get(device), + tree.heads + .get(device) + .getOrThrow("Couldn't retrieve device from tree heads"), ]); - const devices1 = mapMapToMap(devices, (device, tree) => [ + const devices1 = devices.map((device, tree) => [ device, tree.update(remoteHeads), ]); - // for (const [, tree] of devices1) { - // expectPreludeEqual(tree, Array.from(devices1.values())[0]); - // } + for (const [, tree] of devices1) { + expectPreludeEqual(tree, Array.from(devices1.values())[0]); + } } } }); @@ -190,7 +188,10 @@ function applyNewOp( device: DeviceId, op: AppliedOp["op"], ): PermissionedTree { - const opList1 = {prev: tree.heads.get(device), op}; - const heads1 = mapWith(tree.heads, device, opList1); + const opList1 = { + prev: tree.heads.get(device).getOrThrow("Couldn't construct op list 1"), + op, + }; + const heads1 = tree.heads.put(device, opList1); return tree.update(heads1); } diff --git a/src/PermissionedTree.ts b/src/PermissionedTree.ts index 29dd2bc..10efde7 100644 --- a/src/PermissionedTree.ts +++ b/src/PermissionedTree.ts @@ -1,10 +1,4 @@ -import { - asType, - definedOrThrow, - mapMapToMap, - mapWith, - RoMap, -} from "./helper/Collection"; +import {asType} from "./helper/Collection"; import {ControlledOpSet, DeviceId, OpList} from "./ControlledOpSet"; import {Timestamp} from "./helper/Timestamp"; import {TypedValue} from "./helper/TypedValue"; @@ -45,11 +39,27 @@ class ParentPos { } } +class PriorityStatus { + public priority: number; + public status: "open" | OpList; + constructor(priority: number, status: "open" | OpList) { + this.priority = priority; + this.status = status; + } + equals(other: PriorityStatus | undefined): boolean { + if (!other) return false; + return ( + areEqual(this.priority, other.priority) && + areEqual(this.status, other.status) + ); + } + hashCode(): number { + return fieldsHashCode(this.priority, this.status); + } +} + type PermissionedTreeValue = { - writers: RoMap< - DeviceId, - {priority: number; status: "open" | OpList} - >; + writers: HashMap; nodes: HashMap; }; export class NodeId extends TypedValue<"NodeId", string> {} @@ -78,21 +88,22 @@ export function createPermissionedTree(owner: DeviceId): PermissionedTree { return ControlledOpSet.create( persistentDoOpFactory((value, op) => { if (op.type === "set writer") { - const devicePriority = definedOrThrow( - value.writers.get(op.device), - "Cannot find writer entry for op author", - ).priority; - const writerPriority = value.writers.get(op.targetWriter)?.priority; + const devicePriority = value.writers + .get(op.device) + .getOrThrow("Cannot find writer entry for op author").priority; + const writerPriority = value.writers + .get(op.targetWriter) + .getOrUndefined()?.priority; if (writerPriority !== undefined && writerPriority >= devicePriority) return value; if (op.priority >= devicePriority) return value; return { ...value, - writers: mapWith(value.writers, op.targetWriter, { - priority: op.priority, - status: op.status, - }), + writers: value.writers.put( + op.targetWriter, + new PriorityStatus(op.priority, op.status), + ), }; } else { if (ancestor(value.nodes, op.node, op.parent)) return value; @@ -106,10 +117,9 @@ export function createPermissionedTree(owner: DeviceId): PermissionedTree { } }), persistentUndoOp, - (value) => - mapMapToMap(value.writers, (device, info) => [device, info.status]), + (value) => value.writers.mapValues((info) => info.status), asType({ - writers: RoMap([[owner, {priority: 0, status: "open"}]]), + writers: HashMap.of([owner, new PriorityStatus(0, "open")]), nodes: HashMap.empty(), }), ); diff --git a/src/helper/TypedValue.ts b/src/helper/TypedValue.ts index 6f09c40..3bf81fc 100644 --- a/src/helper/TypedValue.ts +++ b/src/helper/TypedValue.ts @@ -1,4 +1,6 @@ -export class TypedValue { +import {HasEquals} from "prelude-ts"; + +export class TypedValue implements HasEquals { static create( this: new (raw: T) => TypedValue, raw: T,