@electric-sql/client
Version:
Postgres everywhere - your data, in sync, wherever you need it.
113 lines (103 loc) • 3.8 kB
text/typescript
import {
ChangeMessage,
ControlMessage,
Message,
NormalizedPgSnapshot,
Offset,
PostgresSnapshot,
Row,
} from './types'
/**
* Type guard for checking {@link Message} is {@link ChangeMessage}.
*
* See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
* for information on how to use type guards.
*
* @param message - the message to check
* @returns true if the message is a {@link ChangeMessage}
*
* @example
* ```ts
* if (isChangeMessage(message)) {
* const msgChng: ChangeMessage = message // Ok
* const msgCtrl: ControlMessage = message // Err, type mismatch
* }
* ```
*/
export function isChangeMessage<T extends Row<unknown> = Row>(
message: Message<T>
): message is ChangeMessage<T> {
return message != null && `key` in message
}
/**
* Type guard for checking {@link Message} is {@link ControlMessage}.
*
* See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
* for information on how to use type guards.
*
* @param message - the message to check
* @returns true if the message is a {@link ControlMessage}
*
* * @example
* ```ts
* if (isControlMessage(message)) {
* const msgChng: ChangeMessage = message // Err, type mismatch
* const msgCtrl: ControlMessage = message // Ok
* }
* ```
*/
export function isControlMessage<T extends Row<unknown> = Row>(
message: Message<T>
): message is ControlMessage {
return message != null && `headers` in message && `control` in message.headers
}
export function isUpToDateMessage<T extends Row<unknown> = Row>(
message: Message<T>
): message is ControlMessage & { up_to_date: true } {
return isControlMessage(message) && message.headers.control === `up-to-date`
}
/**
* Parses the LSN from the up-to-date message and turns it into an offset.
* The LSN is only present in the up-to-date control message when in SSE mode.
* If we are not in SSE mode this function will return undefined.
*/
export function getOffset(message: ControlMessage): Offset | undefined {
if (message.headers.control != `up-to-date`) return
const lsn = message.headers.global_last_seen_lsn
return lsn ? (`${lsn}_0` as Offset) : undefined
}
function bigintReplacer(_key: string, value: unknown): unknown {
return typeof value === `bigint` ? value.toString() : value
}
/**
* BigInt-safe version of JSON.stringify.
* Converts BigInt values to their string representation (as JSON strings,
* e.g. `{ id: 42n }` becomes `{"id":"42"}`) instead of throwing.
* Assumes input is a JSON-serializable value — passing `undefined` at the
* top level will return `undefined` (matching `JSON.stringify` behavior).
*/
export function bigintSafeStringify(value: unknown): string {
return JSON.stringify(value, bigintReplacer)
}
/**
* Checks if a transaction is visible in a snapshot.
*
* @param txid - the transaction id to check
* @param snapshot - the information about the snapshot
* @returns true if the transaction is visible in the snapshot
*/
export function isVisibleInSnapshot(
txid: number | bigint | `${bigint}`,
snapshot: PostgresSnapshot | NormalizedPgSnapshot
): boolean {
const xid = BigInt(txid)
const xmin = BigInt(snapshot.xmin)
const xmax = BigInt(snapshot.xmax)
const xip = snapshot.xip_list.map(BigInt)
// If the transaction id is less than the minimum transaction id, it is visible in the snapshot.
// If the transaction id is less than the maximum transaction id and not in the list of active
// transactions at the time of the snapshot, it has been committed before the snapshot was taken
// and is therefore visible in the snapshot.
// Otherwise, it is not visible in the snapshot.
return xid < xmin || (xid < xmax && !xip.includes(xid))
}