Files
gods-eye-view/src/data/layerState.js
T
f4fcf267c6 fix(osm): stop using public Overpass by default (#648) (#742)
Traffic roads come from OpenFreeMap vector tiles, or TomTom flow tiles with a
key (Hybrid by default). Military areas use OpenFreeMap polygons with a bundled
worldwide name index; wide views show named points. ALPR reads an hourly
OpenStreetMap extract. Cockpit place context and country, state and US county
outlines use bundled Natural Earth and US Census data. Overpass is used only
when an operator configures their own endpoint.

Also: camera arrivals no longer land below ground, keyless terrain retries on
429, bounded relation geometry for configured Overpass, one shared
OpenStreetMap credit, and separate tile and API transports for embedders.

Co-authored-by: theGaryLarson <[email protected]>
Co-authored-by: Milan Khanal <[email protected]>
2026-09-28 19:07:57 -05:00

1771 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import reservationRows from './layerStateTokenReservations.json' with { type: 'json' };
const VALID_DISPOSITIONS = new Set([
'enabled-only',
'enabled+options',
'enabled+mirrored-options',
]);
export const LAYER_STATE_VERSION = 2;
export const LAYER_STATE_TOKEN_ALPHABET =
'0123456789abcdefghijklmnopqrstuvwxyz';
const LAYER_STATE_TOKEN_PATTERN = /^[a-z0-9]{1,2}$/;
const NEW_SINGLE_CHARACTER_TOKEN_PATTERN = /^[0-9]$/;
/** Re-check cadence while a shared subject waits for its feed row to arrive. */
const PENDING_TRACKING_POLL_MS = 1_000;
/**
* A tracking ID is a transponder address, not free text: 6 hex digits for an
* ICAO24, with slack for TIS-B (`~abc123`) and similar prefixed forms. Bounding
* it at the codec keeps an arbitrarily long string out of durable state, the
* generated URL, and local storage. Identity is never TRUNCATED to fit — an
* out-of-grammar ID is rejected outright, because half an address is a
* DIFFERENT aircraft, not a shorter name for the same one.
*/
const TRACKING_ID_GRAMMAR = /^[0-9a-z~_-]{1,16}$/;
/**
* Ceilings for the untrusted v2 layer fields. The layer ceiling covers the
* complete reserved one-character space plus every two-character base-36
* allocation, while the option ceiling covers a dozen short assignments.
* Reject the WHOLE payload, matching the unknown-token rule — never salvage a
* prefix.
*/
const MAX_ENABLED_LAYERS_CHARS = 4_096;
const MAX_LAYER_OPTIONS_CHARS = 512;
export const LAYER_STATE_STORAGE_KEY = 'gev:layer-state:v2';
export const LAYER_RESTORE_ORIGINS = Object.freeze({
share: 'share-restore',
local: 'local-restore',
});
const RADIO_FILTER_CODES = Object.freeze({
all: 'a',
news: 'n',
talk: 't',
weather: 'w',
'public-safety': 'p',
'aviation-marine': 'v',
'traffic-transit': 'x',
music: 'm',
other: 'o',
});
const RADIO_CODE_FILTERS = Object.freeze(
Object.fromEntries(
Object.entries(RADIO_FILTER_CODES).map(([key, value]) => [value, key]),
),
);
function normalizeBoolean(value) {
return typeof value === 'boolean' ? value : null;
}
function normalizeEnum(values, value) {
return values.includes(value) ? value : null;
}
export function normalizeRadioFilter(value) {
const normalized = String(value || '')
.trim()
.toLowerCase();
if (Object.hasOwn(RADIO_FILTER_CODES, normalized)) return normalized;
if (/^genre:[a-z0-9][a-z0-9 &-]{0,31}$/.test(normalized)) return normalized;
return null;
}
function encodeRadioFilter(value) {
return RADIO_FILTER_CODES[value] || `g-${value.slice('genre:'.length)}`;
}
function decodeRadioFilter(value) {
if (Object.hasOwn(RADIO_CODE_FILTERS, value))
return RADIO_CODE_FILTERS[value];
if (/^g-[a-z0-9][a-z0-9 &-]{0,31}$/.test(value))
return `genre:${value.slice(2)}`;
return null;
}
function normalizeVolume(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return null;
return Math.round(Math.max(0, Math.min(1, numeric)) * 100) / 100;
}
function booleanOption(
key,
token,
defaultValue,
{ absentValue = defaultValue } = {},
) {
return Object.freeze({
key,
token,
defaultValue,
absentValue,
normalize: normalizeBoolean,
encode: (value) => (value ? '1' : '0'),
decode: (value) => (value === '1' ? true : value === '0' ? false : null),
});
}
/**
* What an ABSENT token means for this option inside the CURRENT schema version.
*
* Usually that is simply the default: the encoder omits default-valued fields to
* keep the URL short and the decoder fills the default back in. But a DEFAULT CAN
* MOVE while the schema version does not, and every link already in the wild was
* authored under the old one. `absentValue` is that frozen historical meaning —
* what an omitted token meant when links like it were being written — so a
* default flip cannot silently rewrite what an existing link SAYS. A share link
* is authored state; the only honest reading of `v=2&l=f` is the one its author
* saw.
*
* The consequence is not cosmetic: once `absentValue` and `defaultValue` differ,
* the NEW default has to be emitted EXPLICITLY, or one omission would mean two
* different things inside a single schema version. Both sides of the codec read
* this function so they cannot disagree about which it is.
*
* (This is the same rule `scf` follows in sharelink.js by hand. `models3d` is the
* first option in THIS codec to need it — flipped to default-ON on 2026-08-22.)
*/
function absentTokenValue(spec) {
return Object.hasOwn(spec, 'absentValue')
? spec.absentValue
: spec.defaultValue;
}
function trackingIdOption(key, token, defaultValue = null) {
const bounded = (candidate) => {
if (candidate === null || candidate === undefined) return null;
const raw =
typeof candidate === 'number' && Number.isFinite(candidate)
? String(candidate)
: typeof candidate === 'string'
? candidate
: null;
if (raw === null) return null;
const normalized = raw.trim().toLowerCase();
return TRACKING_ID_GRAMMAR.test(normalized) ? normalized : null;
};
return Object.freeze({
key,
token,
defaultValue,
normalize: bounded,
encode: (value) => String(value),
decode: bounded,
});
}
function stringOption(key, token, defaultValue) {
return Object.freeze({
key,
token,
defaultValue,
normalize: (value) => {
if (typeof value === 'number' && Number.isFinite(value))
return String(value).trim().toLowerCase();
if (typeof value !== 'string') return null;
const normalized = value.trim().toLowerCase();
return normalized ? normalized : null;
},
encode: (value) => String(value),
decode: (value) =>
typeof value === 'string' && value.trim()
? value.trim().toLowerCase()
: null,
});
}
function enumOption(
key,
token,
defaultValue,
values,
codes,
{ absentValue = defaultValue } = {},
) {
const reverse = Object.fromEntries(
Object.entries(codes).map(([name, code]) => [code, name]),
);
return Object.freeze({
key,
token,
defaultValue,
absentValue,
normalize: (value) => normalizeEnum(values, value),
encode: (value) => codes[value],
decode: (value) => reverse[value] || null,
});
}
function integerOption(key, token, defaultValue) {
return Object.freeze({
key,
token,
defaultValue,
normalize: (value) => {
if (typeof value === 'number' && Number.isInteger(value) && value > 0)
return value;
const candidate = typeof value === 'string' ? value.trim() : '';
const parsed = Number(candidate);
if (!candidate || !Number.isInteger(parsed) || parsed <= 0) return null;
return parsed;
},
encode: (value) => String(Math.trunc(value)),
decode: (value) => {
const candidate = Number(value);
if (!Number.isInteger(candidate) || candidate <= 0) return null;
return candidate;
},
});
}
/**
* A share-link-only option: the whole-state codec carries it, but the stored
* local blob always holds its default.
*/
function shareOnlyOption(spec) {
return Object.freeze({ ...spec, shareOnly: true });
}
/**
* A signed integer in `[min, max]`, rejected (never clamped) outside it: a
* clamped box edge would be a different box, not a shorter spelling.
*/
function boundedIntegerOption(key, token, defaultValue, { min, max }) {
const parse = (value) => {
const number =
typeof value === 'number'
? value
: /^-?\d{1,9}$/.test(String(value).trim())
? Number(String(value).trim())
: NaN;
return Number.isInteger(number) && number >= min && number <= max
? number
: null;
};
return Object.freeze({
key,
token,
defaultValue,
normalize: parse,
encode: (value) => String(value),
decode: parse,
});
}
/**
* Recent Imagery day: runtime key `S30:2026-09-18`, URL form `S20260918`.
* The calendar is checked both ways, and "today" is never consulted, so a
* link decodes the same whenever it is opened.
*/
const IMAGERY_LETTERS = Object.freeze({ S30: 'S', L30: 'L', VIIRS: 'V' });
const IMAGERY_PRODUCTS = Object.freeze({ S: 'S30', L: 'L30', V: 'VIIRS' });
function imageryKey(product, year, month, day) {
const time = Date.UTC(Number(year), Number(month) - 1, Number(day));
const iso = Number.isFinite(time)
? new Date(time).toISOString().slice(0, 10)
: '';
return product && iso === `${year}-${month}-${day}`
? `${product}:${iso}`
: null;
}
function imageryPinOption(key, token) {
return Object.freeze({
key,
token,
defaultValue: null,
normalize: (value) => {
const match = /^(S30|L30|VIIRS):(\d{4})-(\d{2})-(\d{2})$/.exec(
typeof value === 'string' ? value.trim() : '',
);
return match ? imageryKey(match[1], match[2], match[3], match[4]) : null;
},
encode: (value) => {
const [product, day] = value.split(':');
return `${IMAGERY_LETTERS[product]}${day.replaceAll('-', '')}`;
},
decode: (value) => {
const match = /^([SLV])(\d{4})(\d{2})(\d{2})$/.exec(
typeof value === 'string' ? value : '',
);
return match
? imageryKey(IMAGERY_PRODUCTS[match[1]], match[2], match[3], match[4])
: null;
},
});
}
const OPTION_GROUPS = Object.freeze({
traffic: Object.freeze([
enumOption('roadMode', 'r', null, ['tomtom', 'osm', 'hybrid'], {
tomtom: 't',
osm: 'o',
hybrid: 'h',
}),
]),
'weather-lightning': Object.freeze([
enumOption('opacity', 'o', 'strong', ['light', 'strong'], {
light: 'l',
strong: 's',
}),
]),
'weather-radar': Object.freeze([
enumOption('opacity', 'o', 'strong', ['light', 'strong'], {
light: 'l',
strong: 's',
}),
]),
'weather-satellite': Object.freeze([
enumOption('infrared', 'i', 'filtered', ['filtered', 'full'], {
filtered: 'f',
full: 'a',
}),
enumOption('opacity', 'o', 'strong', ['light', 'strong'], {
light: 'l',
strong: 's',
}),
enumOption(
'product',
'p',
'clouds-regional',
['clouds', 'clouds-regional'],
{ clouds: 'g', 'clouds-regional': 'r' },
),
]),
wind: Object.freeze([
enumOption('model', 'm', 'gfs', ['gfs', 'ifs'], { gfs: 'g', ifs: 'i' }),
enumOption(
'overlay',
'o',
'none',
['none', 'speed', 'temperature', 'pressure'],
{ none: 'n', speed: 's', temperature: 't', pressure: 'p' },
{ absentValue: 'speed' },
),
enumOption('units', 'u', 'km/h', ['km/h', 'm/s', 'mph'], {
'km/h': 'k',
'm/s': 'm',
mph: 'i',
}),
booleanOption('paused', 'p', false),
]),
flights: Object.freeze([
// Owner directive 2026-08-22: the fleet's 3D models are DEFAULT-ON in
// PROXIMITY mode. Proximity is itself the altitude/count gate — models only
// materialize once the camera is close enough and only for the nearest
// contacts in view — so "on" costs nothing at globe scale, and an operator
// who wants every in-view plane still opts into `all` deliberately.
// This default must stay in lockstep with `_models3dEnabled` in BOTH flight
// layers, `this._models3dEnabled` in ui.js, and the `active` / `visible`
// classes in index.html: the fresh-boot path skips restoration entirely (see
// `start()` below), so nothing ever pushes this value into the layers — those
// four initializers ARE the agreement, and they are pinned together in
// layerState.test.mjs.
//
// `absentValue: false` is what keeps the flip out of links already in the
// wild. Schema v2 shipped with OFF as the omitted default, so `v=2&l=f`
// MEANS off — and it has to keep meaning that. Moving the default without
// this would have silently turned 3D on for every existing v2 link, and
// `v=2&l=f&lo=f.m.a` (an OFF link that remembered mode All) would have come
// back as ON+All. The price is that ON is now written explicitly (`f.e.1`)
// instead of ridden in on the omission; see `absentTokenValue`.
booleanOption('models3d', 'e', true, { absentValue: false }),
enumOption('models3dMode', 'm', 'proximity', ['proximity', 'all'], {
proximity: 'p',
all: 'a',
}),
trackingIdOption('selectedFlightsTrackingId', 't', null),
trackingIdOption('selectedMilitaryTrackingId', 'u', null),
]),
satellites: Object.freeze([
enumOption('catalog', 'c', 'core', ['core', 'dense'], {
core: 'c',
dense: 'd',
}),
integerOption('selectedSatTrackingId', 't', null),
]),
cctv: Object.freeze([
enumOption('coverageMode', 'c', 'on', ['off', 'on', 'viewshed'], {
off: '0',
on: '1',
viewshed: 'v',
}),
booleanOption('showProjection', 'p', true),
booleanOption('autoHop', 'a', false),
]),
'recent-imagery': Object.freeze([
// Box edges in degrees × 100000; latitudes stop at the Web-Mercator limit.
boundedIntegerOption('west', 'w', null, { min: -18000000, max: 18000000 }),
boundedIntegerOption('south', 's', null, { min: -8505110, max: 8505110 }),
boundedIntegerOption('east', 'e', null, { min: -18000000, max: 18000000 }),
boundedIntegerOption('north', 'n', null, { min: -8505110, max: 8505110 }),
imageryPinOption('a', 'a'),
imageryPinOption('b', 'b'),
// 0 one image, 1 image against the basemap, 2 two images A / B.
boundedIntegerOption('mode', 'm', 0, { min: 0, max: 2 }),
// The swipe position is one comparison's framing, not a preference: a
// stored split resurfaced in the next session's first comparison.
shareOnlyOption(
boundedIntegerOption('split', 'p', 50, { min: 0, max: 100 }),
),
booleanOption('viirs', 'v', false),
]),
radio: Object.freeze([
Object.freeze({
key: 'filter',
token: 'f',
defaultValue: 'all',
normalize: normalizeRadioFilter,
encode: encodeRadioFilter,
decode: decodeRadioFilter,
}),
Object.freeze({
key: 'volume',
token: 'v',
defaultValue: 0.8,
normalize: normalizeVolume,
encode: (value) => String(Math.round(value * 100)),
decode: (value) =>
/^\d{1,3}$/.test(value) ? normalizeVolume(Number(value) / 100) : null,
}),
]),
});
const TRACKING_OPTION_KEY_BY_LAYER = Object.freeze({
flights: 'selectedFlightsTrackingId',
military: 'selectedMilitaryTrackingId',
satellites: 'selectedSatTrackingId',
});
export const SHARE_TRACKING_RESTORE_POLICIES = Object.freeze({
flights: Object.freeze({
optionOwner: 'flights',
optionKey: 'selectedFlightsTrackingId',
expiryWindowMs: 90_000,
label: 'flight',
}),
military: Object.freeze({
optionOwner: 'flights',
optionKey: 'selectedMilitaryTrackingId',
expiryWindowMs: 45_000,
label: 'military flight',
}),
satellites: Object.freeze({
optionOwner: 'satellites',
optionKey: 'selectedSatTrackingId',
expiryWindowMs: 300_000,
label: 'satellite',
}),
});
/**
* The original single-character assignments are a closed compatibility set.
* Keep their exact mapping pinned by the independent snapshot in the tests.
*/
export const LEGACY_LAYER_STATE_TOKENS = Object.freeze({
'ais-live-vessels': 'a',
'alpr-cameras': 'p',
'bhote-koshi-2026': 'h',
'bhote-koshi-locator': 'z',
bikeshare: 'b',
cctv: 'c',
directions: 'n',
earthquakes: 'e',
'fire-perimeters': '2',
flights: 'f',
'local-dams': 'q',
'local-datacenters': 'd',
'local-firms': 'w',
military: 'm',
'military-awareness': 'g',
'military-installations': 'i',
radio: 'r',
'recent-imagery': '1',
'rocket-launches': 'x',
satellites: 's',
'telegeography-submarine-cables': 'u',
traffic: 't',
transit: 'j',
'weather-cyclones': 'y',
'weather-lightning': 'l',
'weather-radar': 'v',
'weather-satellite': 'o',
wind: 'k',
});
/**
* Permanent token ownership. Existing share links are public authored state,
* so an allocation stays here even if its layer is later removed. The JSON
* ledger is also read directly from the published Git base by the checker.
*/
export function parseLayerStateTokenReservations(rows) {
if (!Array.isArray(rows)) {
throw new Error('Layer-state token ledger must be an array');
}
const reservations = Object.create(null);
const reservedIdsByToken = new Map();
for (const row of rows) {
if (
!Array.isArray(row) ||
row.length !== 2 ||
typeof row[0] !== 'string' ||
typeof row[1] !== 'string'
) {
throw new Error('Invalid layer-state token ledger row');
}
const [id, token] = row;
if (!/^[a-z0-9-]+$/.test(id) || !LAYER_STATE_TOKEN_PATTERN.test(token)) {
throw new Error(`Invalid layer-state token reservation: ${id}`);
}
if (Object.hasOwn(reservations, id)) {
throw new Error(`Duplicate layer-state token reservation id: ${id}`);
}
if (reservedIdsByToken.has(token)) {
throw new Error(`Duplicate layer-state token reservation: ${token}`);
}
if (
(Object.hasOwn(LEGACY_LAYER_STATE_TOKENS, id) &&
token !== LEGACY_LAYER_STATE_TOKENS[id]) ||
(token.length === 1 &&
!NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token) &&
LEGACY_LAYER_STATE_TOKENS[id] !== token)
) {
throw new Error(`Legacy layer-state token is immutable: ${id}`);
}
reservations[id] = token;
reservedIdsByToken.set(token, id);
}
for (const [id, token] of Object.entries(LEGACY_LAYER_STATE_TOKENS)) {
if (reservations[id] !== token) {
throw new Error(`Missing or changed legacy layer-state token: ${id}`);
}
}
return Object.freeze(reservations);
}
export const LAYER_STATE_TOKEN_RESERVATIONS =
parseLayerStateTokenReservations(reservationRows);
/**
* Canonical serialization registry. Its order, not runtime registration order,
* owns stable URL ordering.
*/
export const LAYER_STATE_REGISTRY = Object.freeze([
Object.freeze({
id: 'ais-live-vessels',
token: 'a',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'alpr-cameras',
token: 'p',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'bhote-koshi-2026',
token: 'h',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'bhote-koshi-locator',
token: 'z',
disposition: 'enabled-only',
}),
Object.freeze({ id: 'bikeshare', token: 'b', disposition: 'enabled-only' }),
Object.freeze({
id: 'cctv',
token: 'c',
disposition: 'enabled+options',
optionOwner: 'cctv',
}),
Object.freeze({ id: 'directions', token: 'n', disposition: 'enabled-only' }),
Object.freeze({ id: 'earthquakes', token: 'e', disposition: 'enabled-only' }),
Object.freeze({
id: 'fire-perimeters',
token: '2',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'flights',
token: 'f',
disposition: 'enabled+options',
optionOwner: 'flights',
}),
Object.freeze({ id: 'local-dams', token: 'q', disposition: 'enabled-only' }),
Object.freeze({
id: 'local-datacenters',
token: 'd',
disposition: 'enabled-only',
}),
Object.freeze({ id: 'local-firms', token: 'w', disposition: 'enabled-only' }),
Object.freeze({
id: 'military',
token: 'm',
disposition: 'enabled+mirrored-options',
optionOwner: 'flights',
}),
Object.freeze({
id: 'military-awareness',
token: 'g',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'military-installations',
token: 'i',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'radio',
token: 'r',
disposition: 'enabled+options',
optionOwner: 'radio',
}),
Object.freeze({
id: 'recent-imagery',
token: '1',
disposition: 'enabled+options',
optionOwner: 'recent-imagery',
}),
Object.freeze({
id: 'rocket-launches',
token: 'x',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'satellites',
token: 's',
disposition: 'enabled+options',
optionOwner: 'satellites',
}),
Object.freeze({
id: 'telegeography-submarine-cables',
token: 'u',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'traffic',
token: 't',
disposition: 'enabled+options',
optionOwner: 'traffic',
}),
Object.freeze({ id: 'transit', token: 'j', disposition: 'enabled-only' }),
Object.freeze({
id: 'weather-cyclones',
token: 'y',
disposition: 'enabled-only',
}),
Object.freeze({
id: 'weather-lightning',
token: 'l',
disposition: 'enabled+options',
optionOwner: 'weather-lightning',
}),
Object.freeze({
id: 'weather-radar',
token: 'v',
disposition: 'enabled+options',
optionOwner: 'weather-radar',
}),
Object.freeze({
id: 'weather-satellite',
token: 'o',
disposition: 'enabled+options',
optionOwner: 'weather-satellite',
}),
Object.freeze({
id: 'wind',
token: 'k',
disposition: 'enabled+options',
optionOwner: 'wind',
}),
]);
export const REGISTERED_LAYER_IDS = Object.freeze(
LAYER_STATE_REGISTRY.map((entry) => entry.id),
);
/** Consume unreserved digits before the two-character base-36 namespace. */
export function nextLayerStateToken(
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
const occupied = new Set(Object.values(reservations || {}));
for (const digit of '0123456789') {
if (!occupied.has(digit)) return digit;
}
for (const first of LAYER_STATE_TOKEN_ALPHABET) {
for (const second of LAYER_STATE_TOKEN_ALPHABET) {
const candidate = `${first}${second}`;
if (!occupied.has(candidate)) return candidate;
}
}
throw new Error('Layer-state token namespace exhausted');
}
function allocationRank(token) {
if (NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token)) return Number(token);
if (typeof token !== 'string' || token.length !== 2) return Infinity;
const first = LAYER_STATE_TOKEN_ALPHABET.indexOf(token[0]);
const second = LAYER_STATE_TOKEN_ALPHABET.indexOf(token[1]);
return first < 0 || second < 0 ? Infinity : 10 + first * 36 + second;
}
/** Check a proposed ledger against the published merge-time base. */
export function validateLayerStateAllocations(
baseReservations,
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
if (!baseReservations || typeof baseReservations !== 'object') {
throw new Error('Base layer-state token reservations are required');
}
for (const [id, token] of Object.entries(baseReservations)) {
if (reservations[id] !== token) {
throw new Error(`Published layer-state token changed or removed: ${id}`);
}
}
const newlyReserved = Object.entries(reservations)
.filter(([id]) => !Object.hasOwn(baseReservations, id))
.sort((left, right) => allocationRank(left[1]) - allocationRank(right[1]));
const occupied = { ...baseReservations };
for (const [id, token] of newlyReserved) {
const expected = nextLayerStateToken(occupied);
if (token !== expected) {
throw new Error(
`Layer-state token for ${id} must be the next free token ${expected}`,
);
}
occupied[id] = token;
}
return true;
}
const REGISTRY_BY_ID = new Map(
LAYER_STATE_REGISTRY.map((entry) => [entry.id, entry]),
);
const REGISTRY_BY_TOKEN = new Map(
LAYER_STATE_REGISTRY.map((entry) => [entry.token, entry]),
);
const OPTION_OWNER_IDS = Object.freeze([
...new Set(
LAYER_STATE_REGISTRY.map((entry) => entry.optionOwner).filter(Boolean),
),
]);
function optionSpecs(ownerId) {
return OPTION_GROUPS[ownerId] || [];
}
function defaultsForOwner(ownerId) {
return Object.fromEntries(
optionSpecs(ownerId).map((spec) => [spec.key, spec.defaultValue]),
);
}
function normalizeOwnerOptions(ownerId, candidate = {}) {
const input = candidate && typeof candidate === 'object' ? candidate : {};
const normalized = {};
for (const spec of optionSpecs(ownerId)) {
const value = Object.hasOwn(input, spec.key)
? spec.normalize(input[spec.key])
: null;
normalized[spec.key] = value === null ? spec.defaultValue : value;
}
return normalized;
}
/** Return whether an event origin represents durable direct intent. */
export function isExplicitLayerStateOrigin(origin) {
return origin === 'user' || origin === 'voice' || origin === 'tool';
}
/** Validate the static registry itself before it is used to seal a manager. */
export function validateLayerStateRegistry(
registry = LAYER_STATE_REGISTRY,
reservations = LAYER_STATE_TOKEN_RESERVATIONS,
) {
if (!Array.isArray(registry) || registry.length === 0) {
throw new Error('Layer-state registry must be a non-empty array');
}
if (
!reservations ||
typeof reservations !== 'object' ||
Array.isArray(reservations)
) {
throw new Error('Layer-state token reservations must be an object');
}
const reservedIdsByToken = new Map();
for (const [id, token] of Object.entries(reservations)) {
if (!/^[a-z0-9-]+$/.test(id) || !LAYER_STATE_TOKEN_PATTERN.test(token)) {
throw new Error(`Invalid layer-state token reservation: ${id}`);
}
if (
(Object.hasOwn(LEGACY_LAYER_STATE_TOKENS, id) &&
token !== LEGACY_LAYER_STATE_TOKENS[id]) ||
(token.length === 1 &&
!NEW_SINGLE_CHARACTER_TOKEN_PATTERN.test(token) &&
LEGACY_LAYER_STATE_TOKENS[id] !== token)
) {
throw new Error(`Legacy layer-state token is immutable: ${id}`);
}
if (reservedIdsByToken.has(token)) {
throw new Error(`Duplicate layer-state token reservation: ${token}`);
}
reservedIdsByToken.set(token, id);
}
const ids = new Set();
const tokens = new Set();
for (const entry of registry) {
if (!entry || typeof entry.id !== 'string' || !entry.id)
throw new Error('Layer-state entry missing id');
if (!/^[a-z0-9-]+$/.test(entry.id))
throw new Error(`Invalid layer-state id: ${entry.id}`);
if (ids.has(entry.id))
throw new Error(`Duplicate layer-state id: ${entry.id}`);
ids.add(entry.id);
if (!LAYER_STATE_TOKEN_PATTERN.test(entry.token || ''))
throw new Error(`Invalid layer-state token: ${entry.id}`);
if (reservations[entry.id] !== entry.token) {
throw new Error(`Unreserved layer-state token: ${entry.id}`);
}
if (reservedIdsByToken.get(entry.token) !== entry.id) {
throw new Error(`Layer-state token reservation mismatch: ${entry.token}`);
}
if (tokens.has(entry.token))
throw new Error(`Duplicate layer-state token: ${entry.token}`);
tokens.add(entry.token);
if (!VALID_DISPOSITIONS.has(entry.disposition)) {
throw new Error(`Invalid layer-state disposition: ${entry.id}`);
}
if (entry.disposition !== 'enabled-only') {
if (!entry.optionOwner || optionSpecs(entry.optionOwner).length === 0) {
throw new Error(`Layer-state option owner missing: ${entry.id}`);
}
} else if (entry.optionOwner) {
throw new Error(`Enabled-only layer cannot own options: ${entry.id}`);
}
}
return true;
}
validateLayerStateRegistry();
/** Produce the complete durable default state. */
export function createDefaultLayerState() {
return {
version: LAYER_STATE_VERSION,
enabledLayerIds: [],
options: Object.fromEntries(
OPTION_OWNER_IDS.map((ownerId) => [ownerId, defaultsForOwner(ownerId)]),
),
};
}
/** Sanitize and canonicalize an externally supplied layer-state object. */
export function normalizeLayerState(candidate) {
const input = candidate && typeof candidate === 'object' ? candidate : {};
const requestedEnabled = new Set(
Array.isArray(input.enabledLayerIds)
? input.enabledLayerIds.map(String)
: [],
);
const enabledLayerIds = REGISTERED_LAYER_IDS.filter((id) =>
requestedEnabled.has(id),
);
const enabled = new Set(enabledLayerIds);
const options = Object.fromEntries(
OPTION_OWNER_IDS.map((ownerId) => [
ownerId,
normalizeOwnerOptions(ownerId, input.options?.[ownerId]),
]),
);
// A selected entity cannot outlive an explicitly disabled owner layer.
// Keeping these IDs would resurrect tracking when that layer is enabled
// later, even though OFF was newer explicit intent.
if (!enabled.has('flights')) options.flights.selectedFlightsTrackingId = null;
if (!enabled.has('military'))
options.flights.selectedMilitaryTrackingId = null;
if (!enabled.has('satellites'))
options.satellites.selectedSatTrackingId = null;
// The codec has no cross-family recency field, so multiple tracking IDs are
// ambiguous rather than an ordered handoff. Fail closed instead of letting
// asynchronous feed arrival decide which tracker and camera owner wins.
const trackingSelectionCount = [
options.flights.selectedFlightsTrackingId,
options.flights.selectedMilitaryTrackingId,
options.satellites.selectedSatTrackingId,
].filter((value) => value !== null).length;
if (trackingSelectionCount > 1) {
options.flights.selectedFlightsTrackingId = null;
options.flights.selectedMilitaryTrackingId = null;
options.satellites.selectedSatTrackingId = null;
}
return {
version: LAYER_STATE_VERSION,
enabledLayerIds,
options,
};
}
export function cloneLayerState(state) {
const normalized = normalizeLayerState(state);
return {
...normalized,
enabledLayerIds: [...normalized.enabledLayerIds],
options: Object.fromEntries(
Object.entries(normalized.options).map(([id, options]) => [
id,
{ ...options },
]),
),
};
}
/** Append the compact v2 layer fields to an existing URLSearchParams object. */
export function encodeLayerStateParams(params, state) {
const normalized = normalizeLayerState(state);
const enabled = new Set(normalized.enabledLayerIds);
params.set(
'l',
LAYER_STATE_REGISTRY.filter((entry) => enabled.has(entry.id))
.map((entry) => entry.token)
.join('.'),
);
const encodedOptions = [];
for (const ownerId of OPTION_OWNER_IDS) {
const ownerEntry = REGISTRY_BY_ID.get(ownerId);
const ownerOptions = normalized.options[ownerId];
for (const spec of optionSpecs(ownerId)) {
// Omit against what an ABSENT token means to a DECODER, not against the
// current default — those are the same thing for every option whose
// default never moved, and deliberately different for one whose did.
if (ownerOptions[spec.key] === absentTokenValue(spec)) continue;
encodedOptions.push(
`${ownerEntry.token}.${spec.token}.${spec.encode(ownerOptions[spec.key])}`,
);
}
}
if (encodedOptions.length) params.set('lo', encodedOptions.join('_'));
else params.delete('lo');
return params;
}
/** Decode v2 fields. Null means that the layer payload is absent. */
export function decodeLayerStateParams(params) {
const layerFields = params.getAll('l');
if (
params.get('v') !== String(LAYER_STATE_VERSION) ||
layerFields.length !== 1
)
return null;
const rawLayers = layerFields[0];
const rawOptionsField = String(params.get('lo') || '');
// Fail closed on an oversized payload rather than decoding a truncated one.
if (rawLayers.length > MAX_ENABLED_LAYERS_CHARS) return null;
if (rawOptionsField.length > MAX_LAYER_OPTIONS_CHARS) return null;
const layerTokens = rawLayers ? rawLayers.split('.') : [];
// `l=` is the one valid explicit-empty representation. Reject repeated
// fields, empty members, duplicate members, or unknown members; silently
// removing one would turn a malformed share into a different state.
if (
layerTokens.some((token) => !token || !REGISTRY_BY_TOKEN.has(token)) ||
new Set(layerTokens).size !== layerTokens.length
)
return null;
const enabledLayerIds = layerTokens.map(
(token) => REGISTRY_BY_TOKEN.get(token).id,
);
const rawOptions = {};
for (const assignment of rawOptionsField.split('_')) {
if (!assignment) continue;
const [layerToken, optionToken, encodedValue, ...extra] =
assignment.split('.');
if (extra.length) continue;
const entry = REGISTRY_BY_TOKEN.get(layerToken);
const ownerId = entry?.optionOwner || null;
if (!ownerId) continue;
const spec = optionSpecs(ownerId).find(
(candidate) => candidate.token === optionToken,
);
if (!spec) continue;
const decoded = spec.decode(encodedValue);
if (decoded === null) continue;
if (!rawOptions[ownerId]) rawOptions[ownerId] = {};
rawOptions[ownerId][spec.key] = decoded;
}
// Fill every token the link did NOT carry with its absent-meaning before
// normalization, which would otherwise substitute the CURRENT default. For all
// but one option those are identical and this is a no-op; for `models3d` it is
// the whole point — an omitted `e` is a v2 author saying OFF, not a v2 author
// saying "whatever the default happens to be today". See `absentTokenValue`.
for (const ownerId of OPTION_OWNER_IDS) {
for (const spec of optionSpecs(ownerId)) {
if (rawOptions[ownerId] && Object.hasOwn(rawOptions[ownerId], spec.key))
continue;
if (!rawOptions[ownerId]) rawOptions[ownerId] = {};
rawOptions[ownerId][spec.key] = absentTokenValue(spec);
}
}
return normalizeLayerState({ enabledLayerIds, options: rawOptions });
}
/** Options with share-link-only values reset to their defaults. */
function withoutShareOnlyOptions(options) {
const out = {};
for (const ownerId of OPTION_OWNER_IDS) {
out[ownerId] = { ...(options?.[ownerId] || {}) };
for (const spec of optionSpecs(ownerId)) {
if (spec.shareOnly) out[ownerId][spec.key] = spec.defaultValue;
}
}
return out;
}
/**
* Stable local-storage representation (full IDs for debuggability), with
* share-link-only options at their defaults.
*/
export function serializeStoredLayerState(state) {
const normalized = normalizeLayerState(state);
return JSON.stringify({
v: LAYER_STATE_VERSION,
l: normalized.enabledLayerIds,
o: withoutShareOnlyOptions(normalized.options),
});
}
export function parseStoredLayerState(raw) {
if (typeof raw !== 'string' || !raw) return null;
try {
const parsed = JSON.parse(raw);
if (parsed?.v !== LAYER_STATE_VERSION || !Array.isArray(parsed.l))
return null;
return normalizeLayerState({
enabledLayerIds: parsed.l,
options: withoutShareOnlyOptions(parsed.o),
});
} catch {
return null;
}
}
/** Return sanitized options to apply to one registered module. */
export function layerOptionsForRestore(state, layerId) {
const entry = REGISTRY_BY_ID.get(layerId);
if (!entry?.optionOwner) return null;
return { ...normalizeLayerState(state).options[entry.optionOwner] };
}
function safeStorage() {
try {
return globalThis.localStorage || null;
} catch {
return null;
}
}
function currentLayerOutcome(dataManager, layerId) {
const state = dataManager.getLayerLifecycleState?.(layerId);
return {
settledEnabled: Boolean(state?.enabled),
lifecycleState: state?.lifecycleState || 'missing',
lifecycleUncertain: Boolean(state?.uncertain),
};
}
/**
* Owns durable user layer preferences independently from transient runtime
* choreography, and coordinates passive post-registration restoration.
*/
export class LayerStateCoordinator {
constructor(
dataManager,
shareLinkManager,
{
storage = safeStorage(),
restoreGate = null,
onDurableStateChange = null,
onTrackingRestoreStatus = null,
now = () => Date.now(),
// Injectable so the pending-window behavior is deterministically testable
// without sleeping out a 90 s expiry.
setTimer = (fn, ms) => setTimeout(fn, ms),
clearTimer = (handle) => clearTimeout(handle),
} = {},
) {
if (!dataManager?.registrationsFinalized) {
throw new Error(
'Layer state requires finalized data-layer registrations',
);
}
this.dataManager = dataManager;
this.shareLinkManager = shareLinkManager || null;
this.storage = storage;
this.restoreGate = restoreGate;
this.onDurableStateChange = onDurableStateChange;
this.onTrackingRestoreStatus = onTrackingRestoreStatus;
this.now = now;
this.setTimer = setTimer;
this.clearTimer = clearTimer;
this._durableState = createDefaultLayerState();
this._source = 'defaults';
this._destroyed = false;
this._restoreControllers = new Map();
this._shareCreatedAtMs = null;
this._trackingRestoreController = null;
this._trackingRestoreGeneration = 0;
this._pendingTrackingTimer = null;
this._pendingTrackingContext = null;
this._unsubscribe = this.dataManager.subscribe((change) =>
this._handleManagerChange(change),
);
this._unsubscribeVisibilityRequests =
this.dataManager.subscribeVisibilityRequests((change) =>
this._handleVisibilityRequest(change),
);
this.restorePromise = Promise.resolve([]);
this.lastRestoreResults = [];
}
start({
shareLayerState = null,
allowLocalState = true,
shareCreatedAtMs = null,
} = {}) {
if (this._destroyed)
throw new Error('Layer-state coordinator is destroyed');
let selected = shareLayerState
? normalizeLayerState(shareLayerState)
: null;
if (selected) {
this._source = 'share';
this._shareCreatedAtMs = Number.isFinite(shareCreatedAtMs)
? shareCreatedAtMs
: null;
} else if (allowLocalState) {
let stored = null;
try {
stored = parseStoredLayerState(
this.storage?.getItem?.(LAYER_STATE_STORAGE_KEY),
);
} catch {
/* best effort */
}
if (stored) {
selected = stored;
this._source = 'local';
}
} else {
// A valid historical camera/style share with no v2 layer payload keeps
// the exact legacy default-layer behavior. It must not inherit an
// unrelated recipient's saved local layer preferences.
this._source = 'legacy-share';
}
this._durableState = selected || createDefaultLayerState();
this.shareLinkManager?.setLayerStateProvider?.(() =>
this.getDurableState(),
);
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
if (!selected) return this.restorePromise;
this.restorePromise = this._restoreSelectedState(
this._source === 'share'
? LAYER_RESTORE_ORIGINS.share
: LAYER_RESTORE_ORIGINS.local,
);
return this.restorePromise;
}
get source() {
return this._source;
}
getDurableState() {
return cloneLayerState(this._durableState);
}
_notifyDurableState() {
try {
this.onDurableStateChange?.(this.getDurableState());
} catch {
/* UI sync is best effort */
}
}
_handleVisibilityRequest(change) {
if (!isExplicitLayerStateOrigin(change?.origin)) return;
this._restoreControllers
.get(change.layerId)
?.abort('superseded-by-explicit-visibility');
if (SHARE_TRACKING_RESTORE_POLICIES[change.layerId]) {
this._revokePendingTrackingWatch('superseded-by-explicit-visibility');
}
}
/** Revoke every passive restore before explicit navigation can be reclaimed. */
cancelPendingRestores(reason = 'superseded-by-explicit-navigation') {
for (const controller of this._restoreControllers.values())
controller.abort(reason);
this._revokePendingTrackingWatch(reason);
}
/**
* Revoke a pending shared Follow. Physical navigation may also clear only
* the exact passive selection, without writing recipient preferences.
*/
cancelPendingShareTracking(
reason = 'superseded-by-explicit-navigation',
{ clearSelection = false } = {},
) {
this._revokePendingTrackingWatch(reason);
if (!clearSelection) return false;
const selected = this._selectedShareTrackingTarget();
return selected ? this._passivelyClearTrackingSelection(selected) : false;
}
_handleManagerChange(change) {
if (!change || this._destroyed) return;
// Parameter and visibility ownership are independent. A newer explicit
// option request may replace passive share options, but it must not abort
// the same layer's visibility lifecycle.
if (change.type === 'params-requested') {
if (
isExplicitLayerStateOrigin(change.origin) &&
SHARE_TRACKING_RESTORE_POLICIES[change.layerId]
) {
this._revokePendingTrackingWatch('superseded-by-explicit-params');
}
return;
}
// A layer that goes away takes its latch with it, at ANY origin — a
// programmatic disable or teardown never reaches the explicit-intent path
// below, so revoke here before that early return.
if (
change.type === 'visibility' &&
change.enabled === false &&
SHARE_TRACKING_RESTORE_POLICIES[change.layerId]
) {
this._revokePendingTrackingWatch('owner-layer-disabled');
}
if (!isExplicitLayerStateOrigin(change.origin)) return;
if (change.type === 'visibility') {
this._restoreControllers
.get(change.layerId)
?.abort('superseded-by-explicit-visibility');
if (SHARE_TRACKING_RESTORE_POLICIES[change.layerId]) {
this._revokePendingTrackingWatch('superseded-by-explicit-visibility');
}
const enabled = new Set(this._durableState.enabledLayerIds);
if (change.enabled) enabled.add(change.layerId);
else enabled.delete(change.layerId);
this._commitExplicit({
...this._durableState,
enabledLayerIds: [...enabled],
});
return;
}
if (change.type !== 'params') return;
const entry = REGISTRY_BY_ID.get(change.layerId);
if (!entry?.optionOwner) return;
const ownerId = entry.optionOwner;
const nextOwnerOptions = { ...this._durableState.options[ownerId] };
const requestedParams = change.requestedParams || {};
const trackingOptionKey =
TRACKING_OPTION_KEY_BY_LAYER[change.layerId] || null;
let changed = false;
for (const spec of optionSpecs(ownerId)) {
// Only persist keys present in this explicit request. getLayerParams()
// can return a wider live snapshot containing transient or passively
// changed values that this user action did not own. Tracking is the one
// exception: an unrelated explicit option cancels a pending restoration
// in that family, so its wider live value (active ID or null) must replace
// the formerly durable pending ID instead of allowing reload resurrection.
const explicitlyRequested = Object.hasOwn(requestedParams, spec.key);
const implicitTrackingSync =
!explicitlyRequested && spec.key === trackingOptionKey;
if (!explicitlyRequested && !implicitTrackingSync) continue;
const value = spec.normalize(change.params[spec.key]);
if (value === null) {
if (implicitTrackingSync) {
nextOwnerOptions[spec.key] = null;
changed = true;
continue;
}
if (spec.defaultValue !== null || change.params[spec.key] !== null)
continue;
}
nextOwnerOptions[spec.key] = value;
changed = true;
}
if (!changed) return;
this._commitExplicit({
...this._durableState,
options: { ...this._durableState.options, [ownerId]: nextOwnerOptions },
});
}
_commitExplicit(candidate) {
this._durableState = normalizeLayerState(candidate);
const serialized = serializeStoredLayerState(this._durableState);
try {
if (this.storage?.getItem?.(LAYER_STATE_STORAGE_KEY) !== serialized) {
this.storage?.setItem?.(LAYER_STATE_STORAGE_KEY, serialized);
}
} catch {
/* storage can be unavailable or quota-limited */
}
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
}
async _waitForRestoreGate() {
if (!this.restoreGate) return;
await (typeof this.restoreGate === 'function'
? this.restoreGate()
: this.restoreGate);
}
async _restoreSelectedState(origin) {
for (const entry of LAYER_STATE_REGISTRY) {
this._restoreControllers.set(entry.id, new AbortController());
}
try {
await this._waitForRestoreGate();
const enabled = new Set(this._durableState.enabledLayerIds);
const settled = await Promise.allSettled(
LAYER_STATE_REGISTRY.map(async (entry) => {
const controller = this._restoreControllers.get(entry.id);
const targetEnabled = enabled.has(entry.id);
const options = layerOptionsForRestore(this._durableState, entry.id);
if (origin === LAYER_RESTORE_ORIGINS.share && options) {
for (const trackingKey of Object.values(
TRACKING_OPTION_KEY_BY_LAYER,
)) {
delete options[trackingKey];
}
}
if (this._destroyed || controller?.signal.aborted) {
return {
layerId: entry.id,
targetEnabled,
origin,
phase: 'reserved',
...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {},
cancellationReason: this._destroyed ? 'destroyed' : 'superseded',
errorClass: 'cancelled',
persistenceWrite: false,
succeeded: false,
};
}
// Reserve passive option state before any asynchronous lifecycle work.
// A later explicit params intent then wins on its own lane without
// cancelling or being overwritten by the visibility restore.
const paramsSucceeded =
!options ||
Object.keys(options).length === 0 ||
this.dataManager.setLayerParams(entry.id, options, { origin });
return this.dataManager
.restoreLayerState(
entry.id,
{
enabled: targetEnabled,
params: null,
},
{ origin, signal: controller.signal },
)
.then((result) => ({
...result,
appliedOptions: paramsSucceeded && options ? options : {},
errorClass: paramsSucceeded
? result.errorClass
: 'ParamsRejected',
succeeded: paramsSucceeded && result.succeeded,
}));
}),
);
this.lastRestoreResults = settled.map((result, index) => {
if (result.status === 'fulfilled') return result.value;
const entry = LAYER_STATE_REGISTRY[index];
return {
layerId: entry.id,
targetEnabled: enabled.has(entry.id),
origin,
phase: 'coordinator',
...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {},
cancellationReason: null,
errorClass: result.reason?.name || 'Error',
error: String(result.reason?.message || result.reason),
persistenceWrite: false,
succeeded: false,
};
});
return this.lastRestoreResults.map((result) => ({ ...result }));
} finally {
this._restoreControllers.clear();
this._notifyDurableState();
}
}
_selectedShareTrackingTarget() {
if (this._source !== 'share') return null;
for (const [layerId, policy] of Object.entries(
SHARE_TRACKING_RESTORE_POLICIES,
)) {
const targetId =
this._durableState.options?.[policy.optionOwner]?.[policy.optionKey];
if (targetId !== null && targetId !== undefined && targetId !== '') {
return { layerId, targetId, ...policy };
}
}
return null;
}
_passivelyClearTrackingSelection(selected) {
const current =
this._durableState.options?.[selected.optionOwner]?.[selected.optionKey];
if (String(current) !== String(selected.targetId)) return false;
const ownerOptions = {
...this._durableState.options[selected.optionOwner],
[selected.optionKey]: null,
};
this._durableState = normalizeLayerState({
...this._durableState,
options: {
...this._durableState.options,
[selected.optionOwner]: ownerOptions,
},
});
this.dataManager.setLayerParams?.(
selected.layerId,
{ [selected.optionKey]: null },
{ origin: LAYER_RESTORE_ORIGINS.share },
);
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
return true;
}
/**
* `atMs` is the moment the subject was first found ABSENT, not the moment the
* verdict is delivered. Waiting out the pending window must not by itself
* push a fresh link into the "expired" wording — that word describes the
* SHARE's age, not how long this client watched for the subject.
*/
_classifyMissingTrackingTarget(selected, atMs = this.now()) {
const copiedAt = this._shareCreatedAtMs;
if (!Number.isFinite(copiedAt)) return 'unavailable';
const ageMs = atMs - copiedAt;
return ageMs > selected.expiryWindowMs ? 'expired' : 'unavailable';
}
/** Whether the owning layer currently follows the shared subject. */
_trackingTargetLatched(selected) {
const params = this.dataManager.getLayerParams?.(selected.layerId);
const active = params?.[selected.optionKey];
return (
active !== null &&
active !== undefined &&
String(active) === String(selected.targetId)
);
}
/** Stop watching a pending shared subject without deciding its fate. */
_cancelPendingTrackingWatch() {
if (this._pendingTrackingTimer !== null)
this.clearTimer(this._pendingTrackingTimer);
this._pendingTrackingTimer = null;
const pending = this._pendingTrackingContext;
if (pending?.signal && pending.abortHandler) {
pending.signal.removeEventListener('abort', pending.abortHandler);
pending.abortHandler = null;
}
}
/** Publish a share-follow lifecycle update without allowing UI errors to own state. */
_publishTrackingRestoreStatus(status) {
try {
this.onTrackingRestoreStatus?.(status);
} catch {
/* status UI is best effort */
}
}
/**
* Revoke a pending shared Follow wholesale.
*
* The watch and the LAYER's own deferred-restore latch are two halves of one
* mechanism, so they must die together. Aborting only the restore controller
* left the timer alive: the controller has already settled by the time the
* watch exists, so the abort was a no-op and the orphaned timer went on to
* announce "Shared … unavailable" for a subject whose latch had been
* cancelled — a notice about work no longer being attempted.
*/
_revokePendingTrackingWatch(reason) {
this._trackingRestoreController?.abort(reason);
this._trackingRestoreGeneration += 1;
this._cancelPendingTrackingWatch();
const pending = this._pendingTrackingContext;
this._pendingTrackingContext = null;
if (pending) {
this.dataManager.cancelPendingLayerRestore?.(pending.selected.layerId, {
origin: LAYER_RESTORE_ORIGINS.share,
reason: String(reason || 'cancelled'),
});
this._publishTrackingRestoreStatus({
...pending.probe,
...pending.selected,
status: 'cancelled',
classification: 'cancelled',
reason: String(reason || 'cancelled'),
cleared: false,
});
}
}
/**
* Hold a not-yet-arrived shared subject PENDING instead of declaring it gone.
*
* A recipient's first authoritative refresh routinely lands without a given
* contact — the feed is polled, coverage is partial, and rendering trails the
* snapshot by a poll. Reload-from-local already survives this: its restore
* arms the layer's own deferred-restore latch, which re-attempts on every
* later poll. The shared path used to decide on that single refresh, clear
* the subject from durable state AND from the URL, then post a failure notice
* seconds into startup — so the same link healed on reload but never on the
* share.
*
* Arm the SAME latch, then watch it in the background: the caller is never
* blocked (startup must not wait out a 90 s window before it may write the
* URL again), and the terminal verdict is deferred until the source-specific
* window has genuinely expired. The existing wordings are unchanged.
*/
async _beginPendingTrackingRestore(selected, probe, signal = null) {
const generation = this._trackingRestoreGeneration;
const absentAtMs = this.now();
// Arm the layer's deferred-restore latch under the passive share origin, so
// it re-attempts each poll and never rewrites recipient preferences.
let armed = false;
let armError = null;
try {
armed =
(await this.dataManager.setLayerParams?.(
selected.layerId,
{ [selected.optionKey]: selected.targetId },
{ origin: LAYER_RESTORE_ORIGINS.share },
)) === true;
} catch (error) {
armError = error;
}
if (
this._destroyed ||
generation !== this._trackingRestoreGeneration ||
signal?.aborted
) {
if (armed) {
this.dataManager.cancelPendingLayerRestore?.(selected.layerId, {
origin: LAYER_RESTORE_ORIGINS.share,
reason: String(signal?.reason || 'superseded'),
});
}
return {
...probe,
...selected,
status: 'cancelled',
classification: 'cancelled',
reason: String(signal?.reason || 'superseded'),
cleared: false,
};
}
if (!armed) {
const terminal = {
...probe,
...selected,
status: 'source-unavailable',
classification: 'source-unavailable',
reason: String(
armError?.message || armError || 'tracking restore latch rejected',
),
cleared: this._passivelyClearTrackingSelection(selected),
};
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
const deadline = this.now() + selected.expiryWindowMs;
const settle = (terminal) => {
if (this._pendingTrackingContext?.generation !== generation) return;
this._cancelPendingTrackingWatch();
this._pendingTrackingContext = null;
this._publishTrackingRestoreStatus(terminal);
};
const poll = () => {
this._pendingTrackingTimer = null;
if (this._destroyed || generation !== this._trackingRestoreGeneration)
return;
// The layer that owns the latch may have gone away since the last tick
// (disable, teardown, replacement). There is nothing left attempting this
// restore, so abandon it silently rather than announcing a verdict.
if (this.dataManager.isEffectivelyEnabled?.(selected.layerId) === false) {
this._revokePendingTrackingWatch('owner-layer-disabled');
return;
}
if (this._trackingTargetLatched(selected)) {
settle({
...probe,
...selected,
status: 'found',
classification: 'followed',
cleared: false,
});
return;
}
if (this.now() >= deadline) {
// The window really has elapsed — only now does the verdict apply.
const classification =
probe.status === 'missing'
? this._classifyMissingTrackingTarget(selected, absentAtMs)
: 'source-unavailable';
const cleared = this._passivelyClearTrackingSelection(selected);
settle({ ...probe, ...selected, classification, cleared });
return;
}
this._pendingTrackingTimer = this.setTimer(
poll,
PENDING_TRACKING_POLL_MS,
);
this._pendingTrackingTimer?.unref?.();
};
this._cancelPendingTrackingWatch();
const pending = {
...probe,
...selected,
status: 'pending',
classification: 'pending',
cleared: false,
};
const pendingContext = {
generation,
selected,
probe,
signal,
abortHandler: null,
};
if (signal) {
pendingContext.abortHandler = () => {
if (this._pendingTrackingContext?.generation !== generation) return;
this._revokePendingTrackingWatch(signal.reason || 'aborted');
};
signal.addEventListener('abort', pendingContext.abortHandler, {
once: true,
});
}
this._pendingTrackingContext = pendingContext;
this._publishTrackingRestoreStatus(pending);
this._pendingTrackingTimer = this.setTimer(poll, PENDING_TRACKING_POLL_MS);
this._pendingTrackingTimer?.unref?.();
return pending;
}
/**
* Refresh and restore the one shareable tracked target after destination
* camera and ordinary layer restoration have settled.
*/
async restoreShareTrackingSelection({ signal = null } = {}) {
const selected = this._selectedShareTrackingTarget();
if (!selected || this._destroyed)
return { status: 'skipped', reason: 'no-shared-target' };
this._revokePendingTrackingWatch('superseded-by-newer-restore');
const controller = new AbortController();
const combinedSignal = signal
? AbortSignal.any([signal, controller.signal])
: controller.signal;
this._trackingRestoreController = controller;
const generation = ++this._trackingRestoreGeneration;
let result;
try {
result = await this.dataManager.resolveLayerTrackingTarget(
selected.layerId,
selected.targetId,
{ signal: combinedSignal, origin: LAYER_RESTORE_ORIGINS.share },
);
} catch (error) {
result = combinedSignal.aborted
? {
status: 'cancelled',
reason: String(combinedSignal.reason || 'aborted'),
}
: {
status: 'source-unavailable',
reason: String(error?.message || error),
};
}
if (
generation !== this._trackingRestoreGeneration ||
this._destroyed ||
combinedSignal.aborted
) {
if (this._trackingRestoreController === controller)
this._trackingRestoreController = null;
return {
...result,
status: 'cancelled',
reason: String(combinedSignal.reason || 'superseded'),
};
}
if (this._trackingRestoreController === controller)
this._trackingRestoreController = null;
if (result.status === 'found') {
const terminal = {
...result,
...selected,
classification: 'followed',
cleared: false,
};
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
if (['cancelled', 'superseded', 'destroyed'].includes(result.status))
return result;
// A subject that is simply not here YET is not a subject that is gone. Hold
// it on the layer's own deferred-restore latch for its source-specific
// window before any verdict is reached or shown. `unsupported` layers have
// no latch to arm, so they still decide immediately.
if (result.status === 'missing' || result.status === 'source-unavailable') {
return this._beginPendingTrackingRestore(
selected,
result,
combinedSignal,
);
}
const classification =
result.status === 'missing'
? this._classifyMissingTrackingTarget(selected)
: 'source-unavailable';
const cleared = this._passivelyClearTrackingSelection(selected);
const terminal = { ...result, ...selected, classification, cleared };
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
for (const controller of this._restoreControllers.values())
controller.abort('coordinator-destroyed');
this._restoreControllers.clear();
this._revokePendingTrackingWatch('coordinator-destroyed');
this._trackingRestoreController = null;
this._unsubscribe?.();
this._unsubscribe = null;
this._unsubscribeVisibilityRequests?.();
this._unsubscribeVisibilityRequests = null;
this.shareLinkManager?.setLayerStateProvider?.(null);
this.onDurableStateChange = null;
this.onTrackingRestoreStatus = null;
}
}