@consento/hlc
Version:
A Hybrid Logical Clock implementation that comes with a codec to store it as Uint8Array (compatible to codecs)
190 lines (169 loc) • 5.42 kB
JavaScript
const bigIntTime = require('bigint-time')
const { toBytesBE, fromBigInt, fromInt, fromBytesBE, toBigInt, MAX_UNSIGNED_VALUE } = require('longfn')
function writeUint32 (target, offset, num) {
target[offset] = num >>> 24
target[offset + 1] = (num >>> 16) & 0xFF
target[offset + 2] = (num >>> 8) & 0xFF
target[offset + 3] = num & 0xFF
}
function readUint32 (target, offset) {
return (target[offset] * 0x1000000) +
(
(target[offset + 1] << 16) |
(target[offset + 2] << 8) |
target[offset + 3]
)
}
const TMP_INT = fromInt(0)
const n1e6 = BigInt(1e6)
const UINT64_MAX = toBigInt(MAX_UNSIGNED_VALUE)
const UINT32_MAX = 0xFFFFFFFF
function bigIntCoerce (input, fallback) {
if (typeof input === 'bigint') return input
if (typeof input === 'number' || typeof input === 'string') return BigInt(input)
return fallback
}
function bigIntJSON (bigInt) {
if (bigInt < Number.MAX_SAFE_INTEGER) {
return Number(bigInt)
}
return '0x' + bigInt.toString(16)
}
class ClockOffsetError extends Error {
constructor (offset, maxOffset) {
super(`The received time is ${offset / n1e6}ms ahead of the wall time, exceeding the 'maxOffset' limit of ${maxOffset / n1e6}ms.`)
this.offset = offset
this.maxOffset = maxOffset
}
}
ClockOffsetError.prototype.type = 'ClockOffsetError'
class WallTimeOverflowError extends Error {
constructor (time, maxTime) {
super(`The wall time ${time / n1e6}ms exceeds the max time of ${maxTime / n1e6}ms.`)
this.time = time
this.maxTime = maxTime
}
}
WallTimeOverflowError.prototype.type = 'WallTimeOverflowError'
class ForwardJumpError extends Error {
constructor (timejump, tolerance) {
super(`Detected a forward time jump of ${timejump / n1e6}ms, which exceed the allowed tolerance of ${tolerance / n1e6}ms.`)
this.timejump = timejump
this.tolerance = tolerance
}
}
ForwardJumpError.prototype.type = 'ForwardJumpError'
const codec = Object.freeze({
name: 'hlc',
encode (current, byob, offset = 0) {
let out
if (byob) {
if (byob.byteLength < 12) {
throw new Error(`The provided Uint8Array is too small. 12 byte required but only ${byob.byteLength} byte given.`)
}
out = byob
} else {
out = new Uint8Array(12)
}
offset = byob ? offset : 0
toBytesBE(fromBigInt(current.wallTime, true, TMP_INT), offset, out)
writeUint32(out, offset + 8, current.logical)
return out
},
decode (array, offset = 0) {
return new Timestamp(toBigInt(fromBytesBE(array, true, offset, TMP_INT)), readUint32(array, offset + 8))
}
})
class Timestamp {
constructor (wallTime, logical = 0) {
if (typeof wallTime === 'object') {
this.wallTime = bigIntCoerce(wallTime.wallTime, 0n)
this.logical = wallTime.logical
} else {
this.wallTime = bigIntCoerce(wallTime, 0n)
this.logical = logical
}
}
static compare (a, b) {
if (a.wallTime > b.wallTime) return 1
if (a.wallTime < b.wallTime) return -1
if (a.logical > b.logical) return 1
if (a.logical < b.logical) return -1
return 0
}
static bigger (a, b) {
return a.compare(b) === -1 ? b : a
}
encode (byob, offset = 0) {
return codec.encode(this, byob, offset)
}
toJSON () {
return {
wallTime: bigIntJSON(this.wallTime),
logical: this.logical
}
}
compare (other) {
return Timestamp.compare(this, other)
}
}
class HLC {
constructor ({ wallTime, maxOffset, wallTimeUpperBound, toleratedForwardClockJump, last } = {}) {
this.wallTime = wallTime || bigIntTime
this.maxOffset = bigIntCoerce(maxOffset, 0n)
this.wallTimeUpperBound = bigIntCoerce(wallTimeUpperBound, 0n)
this.toleratedForwardClockJump = bigIntCoerce(toleratedForwardClockJump, 0n)
this.last = new Timestamp(this.wallTime())
if (last) {
this.last = Timestamp.bigger(new Timestamp(last), this.last)
}
}
toJSON () {
return {
maxOffset: bigIntJSON(this.maxOffset),
wallTimeUpperBound: bigIntJSON(this.wallTimeUpperBound),
toleratedForwardClockJump: bigIntJSON(this.toleratedForwardClockJump),
last: this.last.toJSON()
}
}
now () {
return this.update(this.last)
}
validateOffset (offset) {
if (this.toleratedForwardClockJump > 0n && -offset > this.toleratedForwardClockJump) {
throw new ForwardJumpError(-offset, this.toleratedForwardClockJump)
}
if (this.maxOffset > 0n && offset > this.maxOffset) {
throw new ClockOffsetError(offset, this.maxOffset)
}
}
update (other) {
const last = Timestamp.bigger(other, this.last)
let wallTime = this.wallTime()
const offset = last.wallTime - wallTime
this.validateOffset(offset)
let logical
if (offset < 0n) {
logical = 0
} else {
wallTime = last.wallTime
logical = last.logical + 1
if (logical > UINT32_MAX) {
wallTime += 1n
logical = 0
}
}
const maxWallTime = this.wallTimeUpperBound > 0n ? this.wallTimeUpperBound : UINT64_MAX
if (wallTime > maxWallTime) {
throw new WallTimeOverflowError(wallTime, maxWallTime)
}
this.last = new Timestamp(wallTime, logical)
return this.last
}
}
HLC.Timestamp = Timestamp
HLC.WallTimeOverflowError = WallTimeOverflowError
HLC.ClockOffsetError = ClockOffsetError
HLC.ForwardJumpError = ForwardJumpError
HLC.codec = codec
module.exports = HLC