chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
REALTIME_EVENT_PROTOCOL,
|
||||
type EventTypeId,
|
||||
type StreamRegistrationId,
|
||||
} from "./realtime-streams.ts";
|
||||
|
||||
export const REALTIME_EVENT_FIELD_LIMITS = Object.freeze({
|
||||
maxOpaqueIdentifierLength: 128,
|
||||
maxScopeBindingLength: 256,
|
||||
maxResumeCursorLength: 1_024,
|
||||
maxSequenceDigits: 20,
|
||||
maxTimestampFractionDigits: 9,
|
||||
});
|
||||
|
||||
export const REALTIME_MAX_SEQUENCE = "18446744073709551615";
|
||||
|
||||
export type RealtimeEventBase<Payload> = Readonly<{
|
||||
protocol: typeof REALTIME_EVENT_PROTOCOL;
|
||||
streamId: StreamRegistrationId;
|
||||
streamEpoch: string;
|
||||
eventType: EventTypeId;
|
||||
eventId: string;
|
||||
sequence: string;
|
||||
occurredAt: string;
|
||||
scopeBinding: string;
|
||||
payload: Payload;
|
||||
}>;
|
||||
|
||||
export type RealtimeEventEnvelope<Payload = unknown> = Readonly<
|
||||
RealtimeEventBase<Payload> &
|
||||
(
|
||||
| Readonly<{
|
||||
recoveryMode: "CURSOR";
|
||||
resumeCursor: string;
|
||||
}>
|
||||
| Readonly<{
|
||||
recoveryMode: "SNAPSHOT_ONLY" | "SESSION_REBUILD";
|
||||
resumeCursor: null;
|
||||
}>
|
||||
)
|
||||
>;
|
||||
|
||||
export type SnapshotCheckpoint = Readonly<
|
||||
{
|
||||
streamEpoch: string;
|
||||
lastAppliedSequence: string;
|
||||
snapshotRevision: string;
|
||||
} & (
|
||||
| Readonly<{
|
||||
recoveryMode: "CURSOR";
|
||||
resumeCursor: string;
|
||||
}>
|
||||
| Readonly<{
|
||||
recoveryMode: "SNAPSHOT_ONLY";
|
||||
resumeCursor: null;
|
||||
}>
|
||||
)
|
||||
>;
|
||||
|
||||
export type RealtimeResumeState = Readonly<
|
||||
{
|
||||
streamEpoch: string;
|
||||
lastAppliedSequence: string;
|
||||
} & (
|
||||
| Readonly<{
|
||||
recoveryMode: "CURSOR";
|
||||
resumeCursor: string;
|
||||
}>
|
||||
| Readonly<{
|
||||
recoveryMode: "SNAPSHOT_ONLY" | "SESSION_REBUILD";
|
||||
resumeCursor: null;
|
||||
}>
|
||||
)
|
||||
>;
|
||||
|
||||
const OPAQUE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:~+/=-]*$/u;
|
||||
const HEADER_SAFE_CURSOR = /^[\x21-\x7e]+$/u;
|
||||
const CANONICAL_SEQUENCE = /^(?:0|[1-9][0-9]{0,19})$/u;
|
||||
const RFC_3339 =
|
||||
/^([0-9]{4})-(0[1-9]|1[0-2])-([0-2][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,9}))?(Z|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))$/u;
|
||||
|
||||
export function isRealtimeOpaqueIdentifier(
|
||||
value: unknown,
|
||||
): value is string {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
value.length >= 1 &&
|
||||
value.length <=
|
||||
REALTIME_EVENT_FIELD_LIMITS.maxOpaqueIdentifierLength &&
|
||||
OPAQUE_IDENTIFIER.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
export function isRealtimeScopeBinding(
|
||||
value: unknown,
|
||||
): value is string {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
value.length >= 1 &&
|
||||
value.length <= REALTIME_EVENT_FIELD_LIMITS.maxScopeBindingLength &&
|
||||
OPAQUE_IDENTIFIER.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
export function isRealtimeResumeCursor(
|
||||
value: unknown,
|
||||
): value is string {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
value.length >= 1 &&
|
||||
value.length <=
|
||||
REALTIME_EVENT_FIELD_LIMITS.maxResumeCursorLength &&
|
||||
HEADER_SAFE_CURSOR.test(value) &&
|
||||
!value.includes("\0") &&
|
||||
!value.includes("\r") &&
|
||||
!value.includes("\n")
|
||||
);
|
||||
}
|
||||
|
||||
export function isCanonicalRealtimeSequence(
|
||||
value: unknown,
|
||||
): value is string {
|
||||
if (
|
||||
typeof value !== "string" ||
|
||||
value.length > REALTIME_EVENT_FIELD_LIMITS.maxSequenceDigits ||
|
||||
!CANONICAL_SEQUENCE.test(value)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return BigInt(value) <= BigInt(REALTIME_MAX_SEQUENCE);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function compareRealtimeSequences(
|
||||
left: string,
|
||||
right: string,
|
||||
): -1 | 0 | 1 {
|
||||
if (
|
||||
!isCanonicalRealtimeSequence(left) ||
|
||||
!isCanonicalRealtimeSequence(right)
|
||||
) {
|
||||
throw new TypeError("Realtime sequence is invalid.");
|
||||
}
|
||||
const leftValue = BigInt(left);
|
||||
const rightValue = BigInt(right);
|
||||
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
||||
}
|
||||
|
||||
export function nextRealtimeSequence(value: string): string | null {
|
||||
if (!isCanonicalRealtimeSequence(value)) {
|
||||
throw new TypeError("Realtime sequence is invalid.");
|
||||
}
|
||||
const next = BigInt(value) + 1n;
|
||||
return next > BigInt(REALTIME_MAX_SEQUENCE) ? null : next.toString(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* A bounded RFC 3339 profile: uppercase T/Z, a required offset, real calendar
|
||||
* dates, seconds 00-59 and at most nanosecond fractional precision.
|
||||
*/
|
||||
export function isStrictRealtimeTimestamp(
|
||||
value: unknown,
|
||||
): value is string {
|
||||
if (typeof value !== "string") return false;
|
||||
const match = RFC_3339.exec(value);
|
||||
if (!match) return false;
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]);
|
||||
const day = Number(match[3]);
|
||||
if (year < 1 || day > daysInMonth(year, month)) return false;
|
||||
return Number.isFinite(Date.parse(value));
|
||||
}
|
||||
|
||||
function daysInMonth(year: number, month: number): number {
|
||||
if (month === 2) {
|
||||
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
||||
? 29
|
||||
: 28;
|
||||
}
|
||||
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user