Merge pull request #1634 from bilawalsidhu/fix/layer-state-registered-only

Layer state keeps only layers the application registered
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-08 14:28:48 -08:00
commit 922712121c
4 files changed
+101 -9

No files matched your search

@@ -0,0 +1,3 @@
- An application built with fewer layers no longer restores, saves or shares a
layer it does not have when a link or saved state from a full build names it
(samehkhamis, #1634).
+7
View File
@@ -5627,6 +5627,13 @@ and unreachable upstream (502/504) separately from road geometry.
explicit-empty form); unknown option tokens are ignored. Restoration
settles independently per layer so one failed or unavailable source cannot
block its siblings.
- An application that registers fewer layers (an embedder with a smaller
catalog) keeps only its own: `LayerStateCoordinator` drops enabled-layer ids
the application did not register before it normalizes a share, saved state or
explicit change, so such a layer is never restored, written to local storage
or carried into a link this application shares. Options that depend on a
dropped layer (a tracked selection) go with it. An application with every
layer registered is unchanged.
- Layer tokens are permanent public compatibility identifiers. The reservation
JSON ledger at `src/data/layerStateTokenReservations.json` pins all existing
one-character assignments (all letters plus `0`, `1` and
+67
View File
@@ -3121,3 +3121,70 @@ test('Active Fires: the codec accepts whole megawatts in bounds and the three co
confidence: 'nominal',
});
});
// An application built without a layer (a smaller catalog) receives a link or
// saved state from a build that has it: the layer is neither restored nor
// stored nor carried into the links this application shares.
function managerWithout(withheld) {
const manager = new DataLayerManager({});
for (const id of REGISTERED_LAYER_IDS)
if (!withheld.includes(id)) manager.register(fakeLayer(id));
manager.finalizeRegistrations(
LAYER_STATE_REGISTRY.filter(({ id }) => !withheld.includes(id)),
);
return manager;
}
test('layers the application did not register are never restored, stored or shared', async () => {
const manager = managerWithout(['earthquakes']);
const storage = memoryStorage();
const share = shareSink();
const coordinator = new LayerStateCoordinator(manager, share, { storage });
const incoming = createDefaultLayerState();
incoming.enabledLayerIds = ['earthquakes', 'traffic'];
await coordinator.start({ shareLayerState: incoming });
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, ['traffic']);
assert.deepEqual(share.provider().enabledLayerIds, ['traffic']);
assert.equal(manager.layers.has('earthquakes'), false);
await manager.setEnabled('cctv', true, { origin: 'user' });
const [[, written]] = storage.writes;
assert.deepEqual(parseStoredLayerState(written).enabledLayerIds, [
'cctv',
'traffic',
]);
assert.deepEqual(share.provider().enabledLayerIds, ['cctv', 'traffic']);
await coordinator.applyShareLayerState({ shareLayerState: incoming });
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, ['traffic']);
coordinator.destroy();
const saved = createDefaultLayerState();
saved.enabledLayerIds = ['earthquakes', 'traffic'];
const local = new LayerStateCoordinator(
managerWithout(['earthquakes']),
shareSink(),
{ storage: memoryStorage(serializeStoredLayerState(saved)) },
);
await local.start();
assert.equal(local.source, 'local');
assert.deepEqual(local.getDurableState().enabledLayerIds, ['traffic']);
local.destroy();
});
test('an application with every layer registered keeps every requested layer', async () => {
const coordinator = new LayerStateCoordinator(
productionManager(),
shareSink(),
{ storage: memoryStorage() },
);
const incoming = createDefaultLayerState();
incoming.enabledLayerIds = ['earthquakes', 'traffic'];
await coordinator.start({ shareLayerState: incoming });
assert.deepEqual(coordinator.getDurableState().enabledLayerIds, [
'earthquakes',
'traffic',
]);
coordinator.destroy();
});
+24 -9
View File
@@ -106,9 +106,7 @@ export class LayerStateCoordinator {
} = {}) {
if (this._destroyed)
throw new Error('Layer-state coordinator is destroyed');
let selected = shareLayerState
? normalizeLayerState(shareLayerState)
: null;
let selected = shareLayerState ? this._normalize(shareLayerState) : null;
if (selected) {
this._source = 'share';
this._shareCreatedAtMs = Number.isFinite(shareCreatedAtMs)
@@ -124,7 +122,7 @@ export class LayerStateCoordinator {
/* best effort */
}
if (stored) {
selected = stored;
selected = this._normalize(stored);
this._source = 'local';
}
} else {
@@ -163,9 +161,7 @@ export class LayerStateCoordinator {
for (const controller of this._restoreControllers.values())
controller.abort('superseded-by-share-navigation');
this._revokePendingTrackingWatch('superseded-by-share-navigation');
const selected = shareLayerState
? normalizeLayerState(shareLayerState)
: null;
const selected = shareLayerState ? this._normalize(shareLayerState) : null;
this._source = selected ? 'share' : 'legacy-share';
this._shareCreatedAtMs =
selected && Number.isFinite(shareCreatedAtMs) ? shareCreatedAtMs : null;
@@ -182,6 +178,25 @@ export class LayerStateCoordinator {
return this._source;
}
/**
* Canonical layer state holding only the layers this application
* registered. An application built without a layer never restores, stores
* or shares it, even when a link or saved state from a build that has it
* names it. Filtering before normalizing lets normalization drop options
* that depend on a layer that is not here (a tracked selection).
*/
_normalize(candidate) {
const input = candidate && typeof candidate === 'object' ? candidate : {};
return normalizeLayerState({
...input,
enabledLayerIds: Array.isArray(input.enabledLayerIds)
? input.enabledLayerIds.filter((id) =>
this.dataManager.layers.has(String(id)),
)
: [],
});
}
getDurableState() {
return cloneLayerState(this._durableState);
}
@@ -345,7 +360,7 @@ export class LayerStateCoordinator {
}
_commitExplicit(candidate) {
this._durableState = normalizeLayerState(candidate);
this._durableState = this._normalize(candidate);
const serialized = serializeStoredLayerState(this._durableState);
try {
if (this.storage?.getItem?.(LAYER_STATE_STORAGE_KEY) !== serialized) {
@@ -478,7 +493,7 @@ export class LayerStateCoordinator {
...this._durableState.options[selected.optionOwner],
[selected.optionKey]: null,
};
this._durableState = normalizeLayerState({
this._durableState = this._normalize({
...this._durableState,
options: {
...this._durableState.options,