fryorcraken c0ecb6abba
fix!: SDS lamport timestamp overflow and keep it to current time (#2664)
* fix!: avoid SDS lamport timestamp overflow

The SDS timestamp is initialized to the current time in milliseconds, which is a 13 digits value (e.g. 1,759,223,090,052).

The maximum value for int32 is 2,147,483,647 (10 digits), which is clearly less than the timestamp.
Maximum value for uint32 is 4,294,967,295 (10 digits), which does not help with ms timestamp.

uint64 is BigInt in JavaScript, so best to be avoided unless strictly necessary as it creates complexity.
max uint64 is 18,446,744,073,709,551,615 (20 digits).

Using seconds instead of milliseconds would enable usage of uint32 valid until the year 2106.

The lamport timestamp is only initialized to current time for a new channel. The only scenario is when a user comes in a channel, and thinks it's new (did not get previous messages), and then starts sending messages. Meaning that there may be an initial timestamp conflict until the logs are consolidated, which is already handled by the protocol.

* change lamportTimestamp to uint64 in protobuf

* lamport timestamp remains close to current time
2025-10-02 09:07:10 +10:00

213 lines
5.5 KiB
TypeScript

/* eslint-disable import/export */
/* eslint-disable complexity */
/* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
/* eslint-disable @typescript-eslint/no-empty-interface */
/* eslint-disable import/consistent-type-specifier-style */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { decodeMessage, encodeMessage, MaxLengthError, message } from 'protons-runtime'
import type { Codec, DecodeOptions } from 'protons-runtime'
import type { Uint8ArrayList } from 'uint8arraylist'
export interface HistoryEntry {
messageId: string
retrievalHint?: Uint8Array
}
export namespace HistoryEntry {
let _codec: Codec<HistoryEntry>
export const codec = (): Codec<HistoryEntry> => {
if (_codec == null) {
_codec = message<HistoryEntry>((obj, w, opts = {}) => {
if (opts.lengthDelimited !== false) {
w.fork()
}
if ((obj.messageId != null && obj.messageId !== '')) {
w.uint32(10)
w.string(obj.messageId)
}
if (obj.retrievalHint != null) {
w.uint32(18)
w.bytes(obj.retrievalHint)
}
if (opts.lengthDelimited !== false) {
w.ldelim()
}
}, (reader, length, opts = {}) => {
const obj: any = {
messageId: ''
}
const end = length == null ? reader.len : reader.pos + length
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
obj.messageId = reader.string()
break
}
case 2: {
obj.retrievalHint = reader.bytes()
break
}
default: {
reader.skipType(tag & 7)
break
}
}
}
return obj
})
}
return _codec
}
export const encode = (obj: Partial<HistoryEntry>): Uint8Array => {
return encodeMessage(obj, HistoryEntry.codec())
}
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<HistoryEntry>): HistoryEntry => {
return decodeMessage(buf, HistoryEntry.codec(), opts)
}
}
export interface SdsMessage {
senderId: string
messageId: string
channelId: string
lamportTimestamp?: bigint
causalHistory: HistoryEntry[]
bloomFilter?: Uint8Array
content?: Uint8Array
}
export namespace SdsMessage {
let _codec: Codec<SdsMessage>
export const codec = (): Codec<SdsMessage> => {
if (_codec == null) {
_codec = message<SdsMessage>((obj, w, opts = {}) => {
if (opts.lengthDelimited !== false) {
w.fork()
}
if ((obj.senderId != null && obj.senderId !== '')) {
w.uint32(10)
w.string(obj.senderId)
}
if ((obj.messageId != null && obj.messageId !== '')) {
w.uint32(18)
w.string(obj.messageId)
}
if ((obj.channelId != null && obj.channelId !== '')) {
w.uint32(26)
w.string(obj.channelId)
}
if (obj.lamportTimestamp != null) {
w.uint32(80)
w.uint64(obj.lamportTimestamp)
}
if (obj.causalHistory != null) {
for (const value of obj.causalHistory) {
w.uint32(90)
HistoryEntry.codec().encode(value, w)
}
}
if (obj.bloomFilter != null) {
w.uint32(98)
w.bytes(obj.bloomFilter)
}
if (obj.content != null) {
w.uint32(162)
w.bytes(obj.content)
}
if (opts.lengthDelimited !== false) {
w.ldelim()
}
}, (reader, length, opts = {}) => {
const obj: any = {
senderId: '',
messageId: '',
channelId: '',
causalHistory: []
}
const end = length == null ? reader.len : reader.pos + length
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
obj.senderId = reader.string()
break
}
case 2: {
obj.messageId = reader.string()
break
}
case 3: {
obj.channelId = reader.string()
break
}
case 10: {
obj.lamportTimestamp = reader.uint64()
break
}
case 11: {
if (opts.limits?.causalHistory != null && obj.causalHistory.length === opts.limits.causalHistory) {
throw new MaxLengthError('Decode error - map field "causalHistory" had too many elements')
}
obj.causalHistory.push(HistoryEntry.codec().decode(reader, reader.uint32(), {
limits: opts.limits?.causalHistory$
}))
break
}
case 12: {
obj.bloomFilter = reader.bytes()
break
}
case 20: {
obj.content = reader.bytes()
break
}
default: {
reader.skipType(tag & 7)
break
}
}
}
return obj
})
}
return _codec
}
export const encode = (obj: Partial<SdsMessage>): Uint8Array => {
return encodeMessage(obj, SdsMessage.codec())
}
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<SdsMessage>): SdsMessage => {
return decodeMessage(buf, SdsMessage.codec(), opts)
}
}