1
0

The floor keeps up with the room

Occupancy was fetched once, on the way in. Somebody sat down and you did not
find out until you left the office and came back, which for a view whose entire
subject is who is in the building is the point missed by one request.

`watchPresence` is `watchWeather`'s shape without the two pieces of judgement
that one needs and this does not: weather has to decide whether an observation
is too old to be honest about and whether it describes the place you are looking
at, and a roster is neither — it is true when it is served, and it is addressed
by office id, so it cannot arrive about somewhere else.

Thirty seconds, and the number comes from what the data does rather than from
what the network will stand. Ten minutes is right for weather because nothing
upstream of it moves faster; occupancy moves when a person stands up. Backoff
tops out at five minutes rather than the weather watch's hour, because the
difference between the two is what the user is doing while the box is down:
nobody is staring at the sky waiting for it to be redescribed, and somebody *is*
standing in a room they expect to see people arrive in. An hour of silence there
reads as a broken feature rather than as a quiet API.

Two things it does that the weather watch does not, both because this one runs
while a person is looking at the thing it describes. It stops dead while the tab
is hidden and asks again the moment it comes back — a backgrounded tab polling a
roster nobody can see is waste on both ends, and it is the commonest state a
long-lived office tab is in; coming back has to be immediate rather than at the
next tick, or you return to a floor up to thirty seconds stale at exactly the
moment you are looking hardest. And it publishes only on change, by signature
rather than by identity: every answer is a fresh array, so without the
comparison a still floor would rebuild its presence meshes twice a minute
forever.

The watch belongs to the visit and not to the page, which is `weatherWatch`'s
rule for a sharper reason — a roster is requested with a credential and names
people, so one left running after somebody stepped out to the city is a page
quietly asking about a room nobody is looking at. Stopped before the scene swap
in `leaveOffice`, so the last thing it can do is abort a request rather than
publish into a room already left, and stopped again in `mountCity`, which
disposes the office under it.

Checked in the browser, both exits. Standing in the office: one ask on arrival,
the next 31 s later under failure backoff, not a flood. Stepping out by the `O`
key on the office host (a real navigation) and by the scene swap on
`?view=office` (which is what actually exercises `stop()`): zero requests in the
following 45 s, in both.
This commit is contained in:
2026-08-06 02:22:47 -07:00
parent d188db9299
commit dac12cecec
2 changed files with 232 additions and 16 deletions
+42 -16
View File
@@ -29,6 +29,7 @@ import SOCAL from "./cities/socal.ts";
import {
createTeraClient,
describeLiveness,
type PresenceWatch,
type TrafficSource,
type WeatherWatch,
} from "./adapters/http.ts";
@@ -204,6 +205,17 @@ let officePlan: OfficeMinimap | null = null;
* possible to take it without the caption.
*/
let presenceIsSample = false;
/**
* The running poll of who is in, or `null` when nobody is standing in the room.
*
* It belongs to the visit and not to the page, which is the same rule
* `weatherWatch` follows one line up and for a sharper reason: a roster is
* requested with a credential and describes people, so a watch left running
* after somebody stepped back out to the city is a page quietly asking about a
* room nobody is looking at. Started by `enterOffice`, stopped by `leaveOffice`
* and by `mountCity`, and there is never more than one.
*/
let presenceWatch: PresenceWatch | null = null;
/**
* `office.lumbridgecorp.com` and `tera.lumbridgecorp.com` are one bundle behind
@@ -351,6 +363,7 @@ async function mountCity(id: string) {
weatherWatch = null;
poseEditor?.destroy();
poseEditor = null;
stopWatchingOccupancy();
officePlan?.dispose();
officePlan = null;
office?.dispose();
@@ -623,11 +636,11 @@ async function enterOffice() {
showDetail(null);
refreshGodmodePlace();
renderLegend();
// After the room is on screen, not before. Occupancy is a second request and
// the building is worth looking at while it is in flight; awaiting it here
// would hold the door shut on a network round trip to draw people into a
// scene the user cannot see yet.
void refreshPresence();
// After the room is on screen, not before. The first ask is a second request
// and the building is worth looking at while it is in flight; awaiting it here
// would hold the door shut on a network round trip to draw people into a scene
// the user cannot see yet.
watchOccupancy();
}
/**
@@ -720,6 +733,9 @@ function showPlan() {
function leaveOffice() {
if (!city) return;
// Before the scene swap, so the last thing the watch can do is abort a request
// rather than publish into a room the user has already left.
stopWatchingOccupancy();
city.stage.setScene(city.stageScene);
inside = false;
showPlan();
@@ -807,19 +823,29 @@ function officeName(): string {
* is a real fact about an office, and overwriting it with invented people to
* make the demo livelier is the one thing this file must never do.
*/
async function refreshPresence() {
function watchOccupancy() {
stopWatchingOccupancy();
const scene = office;
if (!scene || scene.depth !== "full") return;
const body = await tera.presence(officePack?.id ?? "lumbridge-hq");
const people: Presence[] = body?.people ?? SAMPLE_PRESENCE;
presenceIsSample = body === null;
// The scene may have been torn down while the request was in flight — a city
// switch disposes the office — and writing people into a disposed layer is a
// use-after-free with a friendly name.
if (office !== scene) return;
scene.setPresence(people);
officePlan?.setPresence(people);
renderOfficeBadge();
presenceWatch = tera.watchPresence(officePack?.id ?? "lumbridge-hq", (body) => {
const people: Presence[] = body?.people ?? SAMPLE_PRESENCE;
presenceIsSample = body === null;
// The scene may have been torn down between a request going out and coming
// back — a city switch disposes the office — and writing people into a
// disposed layer is a use-after-free with a friendly name. The watch's own
// `stop()` already drops late answers; this is the belt to that brace,
// because the office can also be *replaced* (signing in rebuilds it at full
// depth) without the watch having been stopped in between.
if (office !== scene) return;
scene.setPresence(people);
officePlan?.setPresence(people);
renderOfficeBadge();
});
}
function stopWatchingOccupancy() {
presenceWatch?.stop();
presenceWatch = null;
}
// ---- Chrome ---------------------------------------------------------------