Files
NetMesh/domain/convergentSync/clock.ts

99 lines
3.0 KiB
TypeScript
Raw Normal View History

import type {
Dot,
HybridLogicalClock,
VersionVector,
} from './types';
import { ConvergentSyncInvariantError } from './types';
import { getOwnRecordValue, setOwnRecordValue } from './record';
export function compareStrings(left: string, right: string): number {
if (left < right) return -1;
if (left > right) return 1;
return 0;
}
export function compareDots(left: Dot, right: Dot): number {
const deviceOrder = compareStrings(left.deviceId, right.deviceId);
return deviceOrder !== 0 ? deviceOrder : left.counter - right.counter;
}
export function dotKey(dot: Dot): string {
return `${dot.deviceId}:${dot.counter}`;
}
export function observesDot(vector: VersionVector, dot: Dot): boolean {
return (getOwnRecordValue(vector, dot.deviceId) ?? 0) >= dot.counter;
}
export function mergeVersionVectors(
left: VersionVector,
right: VersionVector,
): VersionVector {
const merged: VersionVector = {};
const deviceIds = new Set([...Object.keys(left), ...Object.keys(right)]);
for (const deviceId of [...deviceIds].sort()) {
const counter = Math.max(
getOwnRecordValue(left, deviceId) ?? 0,
getOwnRecordValue(right, deviceId) ?? 0,
);
if (counter > 0) setOwnRecordValue(merged, deviceId, counter);
}
return merged;
}
/**
* Returns true when `candidate` has observed every write represented by
* `expected`. Extra counters in `candidate` are allowed: they represent a
* remote superset that must be joined and propagated, not a failed write.
*/
export function versionVectorDominates(
candidate: VersionVector,
expected: VersionVector,
): boolean {
return Object.keys(expected).every(
(deviceId) => (getOwnRecordValue(candidate, deviceId) ?? 0)
>= (getOwnRecordValue(expected, deviceId) ?? 0),
);
}
export function versionVectorsEqual(
left: VersionVector,
right: VersionVector,
): boolean {
return versionVectorDominates(left, right) && versionVectorDominates(right, left);
}
export function compareHybridLogicalClocks(
left: HybridLogicalClock,
right: HybridLogicalClock,
): number {
if (left.wallTime !== right.wallTime) return left.wallTime - right.wallTime;
return left.logical - right.logical;
}
export function maxHybridLogicalClock(
left: HybridLogicalClock,
right: HybridLogicalClock,
): HybridLogicalClock {
return compareHybridLogicalClocks(left, right) >= 0
? { ...left }
: { ...right };
}
export function tickHybridLogicalClock(
current: HybridLogicalClock,
now: number,
): HybridLogicalClock {
const safeNow = Number.isFinite(now) ? Math.max(0, Math.floor(now)) : 0;
if (!Number.isSafeInteger(safeNow)) {
throw new ConvergentSyncInvariantError('Hybrid logical clock wall time is out of range');
}
if (safeNow > current.wallTime) {
return { wallTime: safeNow, logical: 0 };
}
if (current.logical >= Number.MAX_SAFE_INTEGER) {
throw new ConvergentSyncInvariantError('Hybrid logical clock counter exhausted');
}
return { wallTime: current.wallTime, logical: current.logical + 1 };
}