// Record identity: the RECORD_ID and TIMESTAMP every Genesis table row carries. // // GSF sets TIMESTAMP from a high-resolution clock on every write and // RECORD_ID = TIMESTAMP on insert (genesis-db InsertOperation.kt); a modify // moves TIMESTAMP on and keeps RECORD_ID. The real values are around 7.5e18: // epoch millis shifted left 22 bits, plus a counter. That is far above 2^53, // so a JavaScript client's JSON.parse rounds neighbouring rows onto the same // number. The mock deliberately stays below that: its stamps are a counter // seeded from epoch millis that only goes up, so they stay exact, strictly // increasing safe integers, and a client that reads TIMESTAMP as a date (the // AI assistant shows a NANO_TIMESTAMP as epoch millis) still gets roughly the // write time. The same order, a different magnitude. import type { Row } from '../types.ts'; export const RECORD_ID = 'RECORD_ID'; export const TIMESTAMP = 'TIMESTAMP'; export const RECORD_FIELDS: readonly string[] = [RECORD_ID, TIMESTAMP]; const INTEGER_TEXT = /^-?\d+$/; // Hands out RECORD_ID/TIMESTAMP values: the current epoch millis, or one more // than the last value when that is not ahead of it (seeding a thousand rows // runs the counter a second or so ahead of the clock). Once a seed brings a // real server's id above 2^53 (as a string, which JSON can't carry exactly as // a number), later stamps continue from it as strings, so a new row is still // the newest one. export class RecordClock { private last = 0; private beyondSafe: bigint | undefined; next(): number | string { if (this.beyondSafe !== undefined) { this.beyondSafe += 1n; return String(this.beyondSafe); } this.last = Math.max(Date.now(), this.last + 1); return this.last; } // Keeps later stamps above a value a seed row brought with it. observe(value: unknown): void { if (typeof value === 'number' && Number.isSafeInteger(value)) { if (value > this.last) this.last = value; return; } if (typeof value !== 'string' || !INTEGER_TEXT.test(value)) return; const big = BigInt(value); if (big <= BigInt(Number.MAX_SAFE_INTEGER)) { if (Number(big) > this.last) this.last = Number(big); } else if (this.beyondSafe === undefined || big > this.beyondSafe) { this.beyondSafe = big; } } } // A row without its record identity — what a dataserver sends (the real // dataserver never puts RECORD_ID or TIMESTAMP in a row, only // DETAILS.ROW_REF) and what field metadata is derived from. export function withoutRecordFields(row: Row): Row { if (!(RECORD_ID in row) && !(TIMESTAMP in row)) return row; const { [RECORD_ID]: _recordId, [TIMESTAMP]: _timestamp, ...rest } = row; return rest; } // The Map key a record is tracked under. RECORD_IDs are numbers here, but a // seed may bring a recorded string, so they are compared as strings. export function recordKey(row: Row | undefined | null): string | undefined { const id = row?.[RECORD_ID]; return id === undefined || id === null ? undefined : String(id); }