1
0

feat: add authenticated realtime presence

This commit is contained in:
2026-08-11 20:22:12 -07:00
parent 16dc85a6f8
commit 92ebf8abb5
20 changed files with 3746 additions and 1 deletions
+281
View File
@@ -114,6 +114,13 @@ import type { MaterialRegistry } from "./assets/materials.ts";
import type { OfficeMinimap } from "./engine/officeMinimap.ts";
import type { Godmode, GodmodeHouseLights, GodmodePlace } from "./tools/index.ts";
import type { PoseEditor } from "./tools/poseEditor.ts";
import type {
EntityPoseSnapshot,
InterestCell,
RealtimeClient,
ServerRealtimeMessage,
} from "./realtime/index.ts";
import type { PresenceIndicator } from "./realtime/presenceIndicator.ts";
const CITIES: { id: string; label: string; city: City }[] = [
{ id: "california", label: "California", city: CALIFORNIA },
@@ -247,6 +254,15 @@ let wantedCity = "california";
let office: OfficeScene | null = null;
let inside = false;
let markers: Marker[] = SAMPLE_MARKERS;
let realtimeClient: RealtimeClient | null = null;
let presenceIndicator: PresenceIndicator | null = null;
let stopRealtimeSubscription: (() => void) | null = null;
let realtimeOperation = 0;
let lastRealtimePublishAt = 0;
let realtimePageActive = true;
/** Page-scoped wire identities; auth subject never enters the spatial protocol. */
const realtimeActorId = crypto.randomUUID();
const realtimeVehicleId = crypto.randomUUID();
/**
* The buildings you can walk into, as procedural glyphs on the city.
@@ -921,6 +937,9 @@ async function mountCity(id: string) {
onProgress: (p) => {
if (!mount.signal.aborted) bootProgress(entry.label, p.fraction, p.onMainThread);
},
...(access.subject !== null
? { realtimePeers: { localSceneUnitsPerMetre: entry.city.latScale / 111_320 } }
: {}),
});
if (!handle) {
/**
@@ -934,6 +953,7 @@ async function mountCity(id: string) {
return;
}
city = handle;
moveRealtimePresence();
// A new board builds a new layer, and a new layer starts visible. Reapply
// whatever the panel last said, or the setting silently undoes itself on the
// first city switch.
@@ -1086,6 +1106,7 @@ requestAnimationFrame(function pumpMinimap() {
// everyone who is not god, so this is one property read per frame on a
// public page.
poseEditor?.tick();
publishRealtimePresence(performance.now());
pollLiveness();
});
@@ -1231,12 +1252,14 @@ async function enterOffice() {
...(depth === "full"
? { onPresencePick: (p) => showDetail(p ? p.label : null) }
: { onPlacePick: (place) => showDetail(place ? place.label : null) }),
...(access.subject !== null ? { realtimePeers: {} } : {}),
});
office.onViewChange(() => renderLegend());
officePlan = buildOfficePlan(createOfficeMinimap, office);
}
city.stage.setScene(office);
inside = true;
moveRealtimePresence();
const desiredCity: JourneyCity = officeId === "mateo-court" ? "socal" : "bay-area";
journeyToCity(desiredCity);
dispatchJourney({ type: "enter-office", officeId: officeId as "lumbridge-hq" | "frontier-valley" | "mateo-court" });
@@ -1362,6 +1385,7 @@ function leaveOffice() {
dispatchJourney({ type: "leave-office" });
city.stage.setScene(city.stageScene);
inside = false;
moveRealtimePresence();
showPlan();
showDetail(null);
refreshGodmodePlace();
@@ -1512,6 +1536,7 @@ const source = document.querySelector<HTMLElement>("#source");
const minimapFrame = document.querySelector<HTMLElement>("#minimap .minimap-frame");
const minimapReadout = document.querySelector<HTMLElement>("#minimap-readout");
const tierBadge = document.querySelector<HTMLElement>("#tier");
const presenceHost = document.querySelector<HTMLElement>("#presence-host");
const officeBadge = document.querySelector<HTMLElement>("#office-badge");
const panelToggle = document.querySelector<HTMLButtonElement>("#panel-toggle");
const panelToggleLabel = document.querySelector<HTMLElement>("#panel-toggle-label");
@@ -1927,6 +1952,256 @@ function renderTierBadge() {
tierBadge.hidden = false;
}
// ---- Hosted realtime presence --------------------------------------------
function realtimeInterest(): InterestCell {
if (inside) return { kind: "office", officeId };
if (cityId === "california") return { kind: "california-tile", x: 0, y: 0, level: 0 };
return { kind: "city", cityId: cityId === "socal" ? "socal" : "bay-area" };
}
function degrees(radians: number): number {
return ((radians * 180 / Math.PI + 540) % 360) - 180;
}
function realtimePose(): EntityPoseSnapshot | null {
if (access.subject === null) return null;
const timestampMs = Date.now();
if (inside) {
const walker = office?.walker;
if (!walker) return null;
const state = walker.state();
const headingDeg = -degrees(Math.atan2(-state.facing.x, -state.facing.z));
return {
entity: "actor",
actorId: realtimeActorId,
kind: state.actor === "anonymous-dog" ? "dog" : "humanoid",
sequence: 0,
timestampMs,
pose: {
space: "local",
cell: { kind: "office", officeId },
xM: state.position.x,
yM: office?.plan.level(state.levelId)?.floorY ?? 0,
zM: state.position.z,
headingDeg,
pitchDeg: 0,
},
velocity: {
xMps: state.action.x * 1.6,
yMps: 0,
zMps: state.action.z * 1.6,
yawDegPerSec: 0,
},
};
}
const vehicle = journey.vehicle !== null ? city?.vehicleState() : null;
if (vehicle) {
const heading = vehicle.headingDeg * Math.PI / 180;
return {
entity: "vehicle",
vehicleId: realtimeVehicleId,
kind: "model-x",
driverActorId: realtimeActorId,
sequence: 0,
timestampMs,
pose: {
space: "geographic",
lat: vehicle.lat,
lng: vehicle.lng,
altitudeM: 0,
headingDeg: vehicle.headingDeg,
pitchDeg: 0,
},
velocity: {
xMps: Math.sin(heading) * vehicle.speedMps,
yMps: 0,
zMps: Math.cos(heading) * vehicle.speedMps,
yawDegPerSec: 0,
},
steering: vehicle.steering,
wheelRadians: vehicle.wheelRadians,
};
}
const actor = city?.actorState();
if (!actor || !city) return null;
const headingDeg = -degrees(actor.yaw);
const velocity = {
xMps: -Math.sin(actor.yaw) * actor.speedMps,
yMps: actor.verticalSpeedMps,
// Geographic velocity is east/up/north; scene +Z is south.
zMps: Math.cos(actor.yaw) * actor.speedMps,
yawDegPerSec: 0,
};
const origin = cityId === "california" ? { lat: 35.5, lng: -119.5 } : city.world.city.center;
const [originX, originZ] = city.world.project(origin.lat, origin.lng);
const [lat, lng] = city.world.unproject(
originX + actor.x / city.world.metresPerUnit,
originZ + actor.z / city.world.metresPerUnit,
);
return {
entity: "actor",
actorId: realtimeActorId,
kind: actor.kind,
sequence: 0,
timestampMs,
pose: {
space: "geographic",
lat,
lng,
altitudeM: actor.y,
headingDeg,
pitchDeg: degrees(actor.pitch),
},
velocity,
};
}
function ownRealtimeEntity(snapshot: EntityPoseSnapshot): boolean {
if (snapshot.entity === "actor") return snapshot.actorId === realtimeClient?.state().actorId;
return snapshot.driverActorId === realtimeClient?.state().actorId || snapshot.vehicleId === realtimeVehicleId;
}
function clearRealtimePeers(): void {
city?.clearRemoteEntities();
office?.clearRemoteEntities();
updatePresenceIndicator();
}
function applyRealtimeMessage(message: ServerRealtimeMessage): void {
if (message.type === "membership-revoked") {
clearRealtimePeers();
return;
}
const updates = message.type === "pose-delta"
? message.updates
: message.type === "join-grant"
? message.initial
: message.type === "resume-grant"
? message.snapshot
: [];
const target = inside ? office : city;
for (const snapshot of updates) {
if (!ownRealtimeEntity(snapshot)) target?.upsertRemoteSnapshot(snapshot);
}
if (message.type === "pose-delta") {
for (const id of message.removedEntityIds) {
if (id !== `actor:${realtimeClient?.state().actorId}` && id !== `vehicle:${realtimeVehicleId}`) {
target?.removeRemoteEntity(id);
}
}
}
updatePresenceIndicator();
}
function updatePresenceIndicator(state = realtimeClient?.state()): void {
if (!presenceIndicator || access.subject === null) return;
const connection: "connecting" | "live" | "reconnecting" | "offline" =
state?.status === "streaming" ? "live"
: state?.status === "reconnecting" ? "reconnecting"
: state?.status === "left" || state?.status === "disposed" ? "offline"
: "connecting";
const nearbyPeerCount = connection === "live"
? (inside ? office?.remoteEntityCount() : city?.remoteEntityCount()) ?? 0
: 0;
presenceIndicator.update({ signedIn: true, connection, nearbyPeerCount });
}
async function joinRealtimePresence(operation: number): Promise<void> {
const client = realtimeClient;
const pose = realtimePose();
if (!client || !pose) return;
try {
const grant = await client.join(realtimeInterest(), pose);
if (operation !== realtimeOperation) return;
applyRealtimeMessage(grant);
} catch {
if (operation === realtimeOperation) {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
}
}
function moveRealtimePresence(): void {
clearRealtimePeers();
const client = realtimeClient;
if (!client || access.subject === null) return;
const operation = ++realtimeOperation;
if (client.state().sessionId === null) {
void joinRealtimePresence(operation);
return;
}
updatePresenceIndicator({ ...client.state(), status: "reconnecting" });
void client.moveInterest(realtimeInterest())
.then((grant) => {
if (operation === realtimeOperation) applyRealtimeMessage(grant);
})
.catch(() => {
if (operation === realtimeOperation) {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
});
}
function publishRealtimePresence(now: number): void {
if (!realtimeClient || now - lastRealtimePublishAt < 100) return;
const pose = realtimePose();
if (!pose || realtimeClient.state().sessionId === null) return;
lastRealtimePublishAt = now;
try {
realtimeClient.publishPose(pose);
} catch {
presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 });
}
}
async function initializeRealtimePresence(): Promise<void> {
if (!realtimePageActive || access.subject === null || !presenceHost || realtimeClient) return;
const [{ createRealtimeClient }, { createPresenceIndicator }] = await Promise.all([
import("./realtime/client.ts"),
import("./realtime/presenceIndicator.ts"),
]);
// `pagehide` can overtake the lazy chunks. Never construct a fresh client
// after the page has already run its terminal cleanup.
if (!realtimePageActive || access.subject === null || realtimeClient) return;
presenceHost.hidden = false;
document.body.classList.add("presence-on");
presenceIndicator = createPresenceIndicator({
container: presenceHost,
onRetry: () => moveRealtimePresence(),
});
presenceIndicator.update({ signedIn: true, connection: "connecting", nearbyPeerCount: 0 });
realtimeClient = createRealtimeClient({
actorId: realtimeActorId,
authenticatedFetch: authFetch,
sendIntervalMs: 100,
});
stopRealtimeSubscription = realtimeClient.subscribe({
onMessage: applyRealtimeMessage,
onStateChange: updatePresenceIndicator,
onError: () => presenceIndicator?.update({ signedIn: true, connection: "offline", nearbyPeerCount: 0 }),
});
moveRealtimePresence();
}
window.addEventListener("pagehide", () => {
realtimePageActive = false;
realtimeOperation += 1;
stopRealtimeSubscription?.();
stopRealtimeSubscription = null;
clearRealtimePeers();
// Page teardown cannot await the leave round trip. Contain a rotated-token
// or offline failure so navigation never produces an unhandled rejection;
// the server's disconnect grace still removes the ephemeral session.
void realtimeClient?.dispose().catch(() => undefined);
realtimeClient = null;
presenceIndicator?.dispose();
presenceIndicator = null;
});
function applyProfilePreview(profile: LocalProfile): void {
if (access.subject === null) return;
if (inside) {
@@ -3165,6 +3440,12 @@ async function boot() {
mountCity(first?.id ?? "california"),
);
// There must be a mounted scene before remote snapshots have anywhere to go.
// Anonymous and zero-server builds retain their existing local-only path.
// Hosted presence is an enhancement: a failed lazy chunk or unavailable API
// must not stop profile/tools initialization after the world is already live.
await initializeRealtimePresence().catch(() => undefined);
// The instruments, after the first board, because the panel reads a live
// stage and there is not one before this line.
await mountGodmode();