1
0

feat: add private profile media and realtime contracts

This commit is contained in:
2026-08-11 19:21:49 -07:00
parent a2a52bfdae
commit 1c37ef8f3e
22 changed files with 2321 additions and 8 deletions
+11
View File
@@ -0,0 +1,11 @@
export {
isEntityPoseSnapshot,
isInterestCell,
isRealtimeSequence,
isRealtimeTimestamp,
validateClientRealtimeMessage,
validateMotion,
validateServerRealtimeMessage,
} from "./protocol.ts";
export { PoseInterpolationBuffer } from "./interpolation.ts";
export type * from "./types.ts";
+181
View File
@@ -0,0 +1,181 @@
/** Deterministic snapshot interpolation with deliberately bounded prediction. */
import type {
BufferedPoseSample,
EntityPoseSnapshot,
InterpolationOptions,
PoseVelocity,
SpatialPose,
} from "./types.ts";
interface ResolvedOptions {
interpolationDelayMs: number;
maximumExtrapolationMs: number;
capacity: number;
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function options(value: InterpolationOptions): ResolvedOptions {
return {
interpolationDelayMs: clamp(value.interpolationDelayMs ?? 100, 0, 2_000),
maximumExtrapolationMs: clamp(value.maximumExtrapolationMs ?? 150, 0, 1_000),
capacity: Math.floor(clamp(value.capacity ?? 32, 2, 256)),
};
}
function angle(from: number, to: number, t: number): number {
const delta = ((to - from + 540) % 360) - 180;
return from + delta * t;
}
function compatible(a: SpatialPose, b: SpatialPose): boolean {
if (a.space !== b.space) return false;
if (a.space === "geographic") return true;
return b.space === "local" && JSON.stringify(a.cell) === JSON.stringify(b.cell);
}
function mixVelocity(a: PoseVelocity, b: PoseVelocity, t: number): PoseVelocity {
return {
xMps: a.xMps + (b.xMps - a.xMps) * t,
yMps: a.yMps + (b.yMps - a.yMps) * t,
zMps: a.zMps + (b.zMps - a.zMps) * t,
yawDegPerSec: a.yawDegPerSec + (b.yawDegPerSec - a.yawDegPerSec) * t,
};
}
function mixPose(a: SpatialPose, b: SpatialPose, t: number): SpatialPose {
if (!compatible(a, b)) return { ...b };
if (a.space === "geographic" && b.space === "geographic") {
return {
space: "geographic",
lat: a.lat + (b.lat - a.lat) * t,
lng: a.lng + (b.lng - a.lng) * t,
altitudeM: a.altitudeM + (b.altitudeM - a.altitudeM) * t,
headingDeg: angle(a.headingDeg, b.headingDeg, t),
pitchDeg: a.pitchDeg + (b.pitchDeg - a.pitchDeg) * t,
};
}
if (a.space === "local" && b.space === "local") {
return {
space: "local",
cell: { ...b.cell },
xM: a.xM + (b.xM - a.xM) * t,
yM: a.yM + (b.yM - a.yM) * t,
zM: a.zM + (b.zM - a.zM) * t,
headingDeg: angle(a.headingDeg, b.headingDeg, t),
pitchDeg: a.pitchDeg + (b.pitchDeg - a.pitchDeg) * t,
};
}
return { ...b };
}
function predictPose(pose: SpatialPose, velocity: PoseVelocity, seconds: number): SpatialPose {
if (pose.space === "local") {
return {
...pose,
cell: { ...pose.cell },
xM: pose.xM + velocity.xMps * seconds,
yM: pose.yM + velocity.yMps * seconds,
zM: pose.zM + velocity.zMps * seconds,
headingDeg: pose.headingDeg + velocity.yawDegPerSec * seconds,
};
}
// Geographic velocity uses east/up/north axes matching local x/y/z.
const earth = 6_371_000;
const latitudeRadians = pose.lat * Math.PI / 180;
return {
...pose,
lat: pose.lat + (velocity.zMps * seconds / earth) * 180 / Math.PI,
lng: pose.lng + (velocity.xMps * seconds / (earth * Math.max(0.01, Math.cos(latitudeRadians)))) * 180 / Math.PI,
altitudeM: pose.altitudeM + velocity.yMps * seconds,
headingDeg: pose.headingDeg + velocity.yawDegPerSec * seconds,
};
}
function id(snapshot: EntityPoseSnapshot): string {
return snapshot.entity === "actor" ? `actor:${snapshot.actorId}` : `vehicle:${snapshot.vehicleId}`;
}
export class PoseInterpolationBuffer {
private readonly settings: ResolvedOptions;
private readonly snapshots: EntityPoseSnapshot[] = [];
private entityId: string | null = null;
constructor(optionsValue: InterpolationOptions = {}) {
this.settings = options(optionsValue);
}
clear(): void {
this.snapshots.length = 0;
this.entityId = null;
}
size(): number {
return this.snapshots.length;
}
/** Reject duplicates, stale sequence numbers, and accidental entity mixing. */
push(snapshot: EntityPoseSnapshot): boolean {
const incomingId = id(snapshot);
if (this.entityId !== null && incomingId !== this.entityId) return false;
const latest = this.snapshots.at(-1);
if (latest && (snapshot.sequence <= latest.sequence || snapshot.timestampMs <= latest.timestampMs)) {
return false;
}
this.entityId = incomingId;
this.snapshots.push(snapshot);
if (this.snapshots.length > this.settings.capacity) this.snapshots.shift();
return true;
}
/** Sample server time minus interpolation delay; never predicts past the configured cap. */
sample(serverNowMs: number): BufferedPoseSample | null {
if (!Number.isFinite(serverNowMs) || this.snapshots.length === 0) return null;
const target = serverNowMs - this.settings.interpolationDelayMs;
const first = this.snapshots[0];
const latest = this.snapshots.at(-1);
if (!first || !latest) return null;
if (target <= first.timestampMs) return this.from(first, "held", first.timestampMs);
for (let index = 1; index < this.snapshots.length; index += 1) {
const after = this.snapshots[index];
const before = this.snapshots[index - 1];
if (!after || !before || target > after.timestampMs) continue;
if (target === after.timestampMs) return this.from(after, "exact", target);
if (!compatible(before.pose, after.pose)) return this.from(after, "held", target);
const t = (target - before.timestampMs) / (after.timestampMs - before.timestampMs);
return {
mode: "interpolated",
pose: mixPose(before.pose, after.pose, t),
velocity: mixVelocity(before.velocity, after.velocity, t),
timestampMs: target,
sourceSequence: after.sequence,
};
}
const extrapolationMs = clamp(target - latest.timestampMs, 0, this.settings.maximumExtrapolationMs);
if (extrapolationMs === 0) return this.from(latest, "exact", latest.timestampMs);
return {
mode: target - latest.timestampMs > this.settings.maximumExtrapolationMs ? "held" : "extrapolated",
pose: predictPose(latest.pose, latest.velocity, extrapolationMs / 1_000),
velocity: { ...latest.velocity },
timestampMs: latest.timestampMs + extrapolationMs,
sourceSequence: latest.sequence,
};
}
private from(snapshot: EntityPoseSnapshot, mode: "exact" | "held", timestampMs: number): BufferedPoseSample {
return {
mode,
pose: snapshot.pose.space === "local"
? { ...snapshot.pose, cell: { ...snapshot.pose.cell } }
: { ...snapshot.pose },
velocity: { ...snapshot.velocity },
timestampMs,
sourceSequence: snapshot.sequence,
};
}
}
+262
View File
@@ -0,0 +1,262 @@
/** Strict runtime validation for untrusted realtime protocol payloads. */
import type {
ClientRealtimeMessage,
EntityPoseSnapshot,
InterestCell,
MotionLimits,
MotionValidationResult,
RealtimeSequence,
RealtimeValidationResult,
ServerRealtimeMessage,
SpatialPose,
} from "./types.ts";
const UINT32_MAX = 4_294_967_295;
const MAX_TIMESTAMP_MS = 8_640_000_000_000_000;
const MAX_INTERESTS = 128;
const MAX_ENTITIES_PER_MESSAGE = 2_048;
const MAX_STRING = 256;
const EARTH_RADIUS_M = 6_371_000;
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function string(value: unknown, maximum = MAX_STRING): value is string {
return typeof value === "string" && value.length > 0 && value.length <= maximum;
}
function finite(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
export function isRealtimeSequence(value: unknown): value is RealtimeSequence {
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= UINT32_MAX;
}
export function isRealtimeTimestamp(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= MAX_TIMESTAMP_MS;
}
export function isInterestCell(value: unknown): value is InterestCell {
if (!record(value)) return false;
switch (value.kind) {
case "california-tile":
return (
Number.isInteger(value.x) &&
Number.isInteger(value.y) &&
Number.isInteger(value.level) &&
Number(value.x) >= -1_000_000 &&
Number(value.x) <= 1_000_000 &&
Number(value.y) >= -1_000_000 &&
Number(value.y) <= 1_000_000 &&
Number(value.level) >= 0 &&
Number(value.level) <= 24
);
case "city":
return value.cityId === "bay-area" || value.cityId === "socal";
case "office":
return string(value.officeId);
case "floor":
return string(value.officeId) && string(value.floorId);
case "room":
return string(value.officeId) && string(value.floorId) && string(value.roomId);
default:
return false;
}
}
function isSpatialPose(value: unknown): value is SpatialPose {
if (!record(value)) return false;
if (value.space === "geographic") {
return (
finite(value.lat) && value.lat >= -90 && value.lat <= 90 &&
finite(value.lng) && value.lng >= -180 && value.lng <= 180 &&
finite(value.altitudeM) && value.altitudeM >= -500 && value.altitudeM <= 100_000 &&
finite(value.headingDeg) && value.headingDeg >= -360 && value.headingDeg <= 360 &&
finite(value.pitchDeg) && value.pitchDeg >= -90 && value.pitchDeg <= 90
);
}
return (
value.space === "local" &&
isInterestCell(value.cell) &&
value.cell.kind !== "california-tile" &&
finite(value.xM) && Math.abs(value.xM) <= 1_000_000 &&
finite(value.yM) && Math.abs(value.yM) <= 100_000 &&
finite(value.zM) && Math.abs(value.zM) <= 1_000_000 &&
finite(value.headingDeg) && value.headingDeg >= -360 && value.headingDeg <= 360 &&
finite(value.pitchDeg) && value.pitchDeg >= -90 && value.pitchDeg <= 90
);
}
function isVelocity(value: unknown): boolean {
return (
record(value) &&
finite(value.xMps) && Math.abs(value.xMps) <= 1_000 &&
finite(value.yMps) && Math.abs(value.yMps) <= 1_000 &&
finite(value.zMps) && Math.abs(value.zMps) <= 1_000 &&
finite(value.yawDegPerSec) && Math.abs(value.yawDegPerSec) <= 10_000
);
}
export function isEntityPoseSnapshot(value: unknown): value is EntityPoseSnapshot {
if (
!record(value) ||
!isRealtimeSequence(value.sequence) ||
!isRealtimeTimestamp(value.timestampMs) ||
!isSpatialPose(value.pose) ||
!isVelocity(value.velocity)
) return false;
if (value.entity === "actor") {
return (
string(value.actorId) &&
(value.kind === "humanoid" || value.kind === "dog" || value.kind === "crow")
);
}
return (
value.entity === "vehicle" &&
string(value.vehicleId) &&
value.kind === "model-x" &&
(value.driverActorId === null || string(value.driverActorId)) &&
finite(value.steering) && value.steering >= -1 && value.steering <= 1 &&
finite(value.wheelRadians) && Math.abs(value.wheelRadians) <= 1_000_000
);
}
function interests(value: unknown): value is readonly InterestCell[] {
return Array.isArray(value) && value.length <= MAX_INTERESTS && value.every(isInterestCell);
}
function entities(value: unknown): value is readonly EntityPoseSnapshot[] {
return Array.isArray(value) && value.length <= MAX_ENTITIES_PER_MESSAGE && value.every(isEntityPoseSnapshot);
}
function common(value: Record<string, unknown>): boolean {
return value.protocolVersion === 1 && string(value.requestId);
}
export function validateClientRealtimeMessage(
value: unknown,
): RealtimeValidationResult<ClientRealtimeMessage> {
if (!record(value) || !common(value)) return { ok: false, error: "realtime: invalid envelope" };
if (value.type === "join-request") {
if (
string(value.actorId) &&
(value.resumeToken === null || string(value.resumeToken, 512)) &&
(value.lastReceivedSequence === null || isRealtimeSequence(value.lastReceivedSequence)) &&
interests(value.interests)
) return { ok: true, value: value as unknown as ClientRealtimeMessage };
return { ok: false, error: "realtime: invalid join request" };
}
if (value.type === "resume-request") {
if (
string(value.sessionId) && string(value.resumeToken, 512) && string(value.serverEpoch) &&
isRealtimeSequence(value.lastReceivedSequence) && interests(value.interests)
) return { ok: true, value: value as unknown as ClientRealtimeMessage };
return { ok: false, error: "realtime: invalid resume request" };
}
return { ok: false, error: "realtime: unknown client message" };
}
export function validateServerRealtimeMessage(
value: unknown,
): RealtimeValidationResult<ServerRealtimeMessage> {
if (!record(value) || value.protocolVersion !== 1) {
return { ok: false, error: "realtime: invalid envelope" };
}
if (value.type === "join-grant") {
if (
string(value.requestId) && string(value.sessionId) && string(value.actorId) &&
(value.role === "visitor" || value.role === "member" || value.role === "admin") &&
string(value.serverEpoch) && isRealtimeTimestamp(value.serverTimeMs) &&
isRealtimeSequence(value.nextSequence) && string(value.resumeToken, 512) &&
interests(value.interests) && entities(value.initial)
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid join grant" };
}
if (value.type === "resume-grant") {
if (
string(value.requestId) && string(value.sessionId) && string(value.serverEpoch) &&
isRealtimeTimestamp(value.serverTimeMs) && isRealtimeSequence(value.nextSequence) &&
string(value.resumeToken, 512) && typeof value.continuous === "boolean" && entities(value.snapshot)
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid resume grant" };
}
if (value.type === "pose-delta") {
if (
string(value.serverEpoch) && isRealtimeSequence(value.sequence) &&
isRealtimeTimestamp(value.timestampMs) && entities(value.updates) &&
Array.isArray(value.removedEntityIds) &&
value.removedEntityIds.length <= MAX_ENTITIES_PER_MESSAGE &&
value.removedEntityIds.every((id) => string(id))
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid pose delta" };
}
if (value.type === "membership-revoked") {
const reasons = new Set([
"signed-out", "membership-revoked", "session-replaced", "protocol-violation", "server-shutdown",
]);
if (
string(value.serverEpoch) && isRealtimeSequence(value.sequence) &&
isRealtimeTimestamp(value.timestampMs) && string(value.sessionId) && string(value.actorId) &&
reasons.has(String(value.reason)) && typeof value.reconnectAllowed === "boolean"
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid membership revocation" };
}
return { ok: false, error: "realtime: unknown server message" };
}
function entityId(snapshot: EntityPoseSnapshot): string {
return snapshot.entity === "actor" ? `actor:${snapshot.actorId}` : `vehicle:${snapshot.vehicleId}`;
}
function localCellKey(pose: SpatialPose): string | null {
if (pose.space !== "local") return null;
return JSON.stringify(pose.cell);
}
function angleDeltaDegrees(from: number, to: number): number {
return ((to - from + 540) % 360) - 180;
}
function displacementMetres(from: SpatialPose, to: SpatialPose): { horizontal: number; vertical: number } | null {
if (from.space !== to.space) return null;
if (from.space === "local" && to.space === "local") {
if (localCellKey(from) !== localCellKey(to)) return null;
return { horizontal: Math.hypot(to.xM - from.xM, to.zM - from.zM), vertical: to.yM - from.yM };
}
if (from.space === "geographic" && to.space === "geographic") {
const meanLat = ((from.lat + to.lat) / 2) * Math.PI / 180;
const north = (to.lat - from.lat) * Math.PI / 180 * EARTH_RADIUS_M;
const east = (to.lng - from.lng) * Math.PI / 180 * Math.cos(meanLat) * EARTH_RADIUS_M;
return { horizontal: Math.hypot(north, east), vertical: to.altitudeM - from.altitudeM };
}
return null;
}
/** Server-side helper for rejecting impossible client-proposed movement. */
export function validateMotion(
previous: EntityPoseSnapshot,
proposed: EntityPoseSnapshot,
limits: MotionLimits,
): MotionValidationResult {
if (entityId(previous) !== entityId(proposed)) return { ok: false, reason: "identity-mismatch" };
const displacement = displacementMetres(previous.pose, proposed.pose);
if (!displacement) return { ok: false, reason: "space-mismatch" };
const dt = (proposed.timestampMs - previous.timestampMs) / 1_000;
const minimum = Math.max(0.001, limits.minimumDeltaSeconds ?? 1 / 240);
const maximum = Math.max(minimum, limits.maximumDeltaSeconds ?? 2);
if (!Number.isFinite(dt) || dt < minimum || dt > maximum || proposed.sequence <= previous.sequence) {
return { ok: false, reason: "time-invalid" };
}
const horizontalSpeedMps = Math.max(0, displacement.horizontal - Math.max(0, limits.positionSlackM)) / dt;
const verticalSpeedMps = Math.abs(displacement.vertical) / dt;
const turnRateDegPerSec = Math.abs(angleDeltaDegrees(previous.pose.headingDeg, proposed.pose.headingDeg)) / dt;
if (
!finite(limits.maximumHorizontalSpeedMps) || horizontalSpeedMps > limits.maximumHorizontalSpeedMps ||
!finite(limits.maximumVerticalSpeedMps) || verticalSpeedMps > limits.maximumVerticalSpeedMps ||
!finite(limits.maximumTurnRateDegPerSec) || turnRateDegPerSec > limits.maximumTurnRateDegPerSec
) return { ok: false, reason: "speed-exceeded" };
return { ok: true, horizontalSpeedMps, verticalSpeedMps, turnRateDegPerSec };
}
+219
View File
@@ -0,0 +1,219 @@
/** JSON-safe contracts for an eventual server-authoritative Tera session. */
export type RealtimeActorKind = "humanoid" | "dog" | "crow";
export type RealtimeVehicleKind = "model-x";
export type RealtimeMemberRole = "visitor" | "member" | "admin";
export type RealtimeRevocationReason =
| "signed-out"
| "membership-revoked"
| "session-replaced"
| "protocol-violation"
| "server-shutdown";
export interface CaliforniaTileInterest {
kind: "california-tile";
/** Discrete authored world grid, not web-map tiles. */
x: number;
y: number;
level: number;
}
export interface CityInterest {
kind: "city";
cityId: "bay-area" | "socal";
}
export interface OfficeInterest {
kind: "office";
officeId: string;
}
export interface FloorInterest {
kind: "floor";
officeId: string;
floorId: string;
}
export interface RoomInterest {
kind: "room";
officeId: string;
floorId: string;
roomId: string;
}
export type InterestCell =
| CaliforniaTileInterest
| CityInterest
| OfficeInterest
| FloorInterest
| RoomInterest;
/** Monotonic within one server epoch and bounded to an unsigned 32-bit integer. */
export type RealtimeSequence = number;
/** Unix epoch milliseconds, serialized as a safe integer. */
export type RealtimeTimestamp = number;
export interface GeographicPose {
space: "geographic";
lat: number;
lng: number;
altitudeM: number;
headingDeg: number;
pitchDeg: number;
}
export interface LocalPose {
space: "local";
/** The smallest spatial cell whose coordinates define this pose. */
cell: CityInterest | OfficeInterest | FloorInterest | RoomInterest;
xM: number;
yM: number;
zM: number;
headingDeg: number;
pitchDeg: number;
}
export type SpatialPose = GeographicPose | LocalPose;
export interface PoseVelocity {
xMps: number;
yMps: number;
zMps: number;
yawDegPerSec: number;
}
interface EntityPoseSnapshotBase {
sequence: RealtimeSequence;
timestampMs: RealtimeTimestamp;
pose: SpatialPose;
velocity: PoseVelocity;
}
export interface ActorPoseSnapshot extends EntityPoseSnapshotBase {
entity: "actor";
actorId: string;
kind: RealtimeActorKind;
}
export interface VehiclePoseSnapshot extends EntityPoseSnapshotBase {
entity: "vehicle";
vehicleId: string;
kind: RealtimeVehicleKind;
driverActorId: string | null;
steering: number;
wheelRadians: number;
}
export type EntityPoseSnapshot = ActorPoseSnapshot | VehiclePoseSnapshot;
export interface JoinRequest {
type: "join-request";
protocolVersion: 1;
requestId: string;
actorId: string;
resumeToken: string | null;
lastReceivedSequence: RealtimeSequence | null;
interests: readonly InterestCell[];
}
export interface JoinGrant {
type: "join-grant";
protocolVersion: 1;
requestId: string;
sessionId: string;
actorId: string;
role: RealtimeMemberRole;
serverEpoch: string;
serverTimeMs: RealtimeTimestamp;
nextSequence: RealtimeSequence;
resumeToken: string;
interests: readonly InterestCell[];
initial: readonly EntityPoseSnapshot[];
}
export interface ResumeRequest {
type: "resume-request";
protocolVersion: 1;
requestId: string;
sessionId: string;
resumeToken: string;
serverEpoch: string;
lastReceivedSequence: RealtimeSequence;
interests: readonly InterestCell[];
}
export interface ResumeGrant {
type: "resume-grant";
protocolVersion: 1;
requestId: string;
sessionId: string;
serverEpoch: string;
serverTimeMs: RealtimeTimestamp;
nextSequence: RealtimeSequence;
resumeToken: string;
/** True means queued deltas bridge the requested sequence without a gap. */
continuous: boolean;
snapshot: readonly EntityPoseSnapshot[];
}
export interface PoseDelta {
type: "pose-delta";
protocolVersion: 1;
serverEpoch: string;
sequence: RealtimeSequence;
timestampMs: RealtimeTimestamp;
updates: readonly EntityPoseSnapshot[];
removedEntityIds: readonly string[];
}
export interface MembershipRevoked {
type: "membership-revoked";
protocolVersion: 1;
serverEpoch: string;
sequence: RealtimeSequence;
timestampMs: RealtimeTimestamp;
sessionId: string;
actorId: string;
reason: RealtimeRevocationReason;
reconnectAllowed: boolean;
}
export type ClientRealtimeMessage = JoinRequest | ResumeRequest;
export type ServerRealtimeMessage = JoinGrant | ResumeGrant | PoseDelta | MembershipRevoked;
export type RealtimeValidationResult<T> =
| { ok: true; value: T }
| { ok: false; error: string };
export interface MotionLimits {
maximumHorizontalSpeedMps: number;
maximumVerticalSpeedMps: number;
maximumTurnRateDegPerSec: number;
/** Allows quantization and packet jitter without permitting teleportation. */
positionSlackM: number;
minimumDeltaSeconds?: number;
maximumDeltaSeconds?: number;
}
export type MotionValidationResult =
| { ok: true; horizontalSpeedMps: number; verticalSpeedMps: number; turnRateDegPerSec: number }
| {
ok: false;
reason: "identity-mismatch" | "space-mismatch" | "time-invalid" | "speed-exceeded";
};
export interface InterpolationOptions {
interpolationDelayMs?: number;
maximumExtrapolationMs?: number;
capacity?: number;
}
export type BufferedSampleMode = "exact" | "interpolated" | "extrapolated" | "held";
export interface BufferedPoseSample {
mode: BufferedSampleMode;
pose: SpatialPose;
velocity: PoseVelocity;
timestampMs: RealtimeTimestamp;
sourceSequence: RealtimeSequence;
}