Skip to content

feat: add restoreLayers API to rehydrate host-persisted layers#3

Merged
giswqs merged 1 commit into
mainfrom
feat/restore-layers-api
Jun 7, 2026
Merged

feat: add restoreLayers API to rehydrate host-persisted layers#3
giswqs merged 1 commit into
mainfrom
feat/restore-layers-api

Conversation

@giswqs
Copy link
Copy Markdown
Member

@giswqs giswqs commented Jun 7, 2026

Motivation

Host applications that save and restore map state (for example a project file) persist the control's added layers and recreate the native MapLibre source/layer themselves on reload, before the control is re-activated. Until now there was no way to hand those layers back to the control: MapLayerManager.addLayer always generates fresh random ids, which would duplicate the host-recreated natives.

API

MapLayerManager.restoreLayer(entry: AddedLayer): AddedLayer

  • Returns the already-tracked entry when the id is known.
  • Registers a copy of the entry and ensures the native source/layer exist, reusing whatever the host already created.
  • When the native source/layer already exist, reconciles opacity (via setPaintProperty) and visible (via setLayoutProperty) so manager state and the map agree.
  • Shared source/layer creation is refactored out of addLayer into private _createSource / _createLayer helpers used by both methods; addLayer behavior is unchanged.

EnviroAtlasControl.restoreLayers(entries: AddedLayer[]): void

  • Skips entries already tracked or matching an existing layer by service + sublayer.
  • Calls restoreLayer for the rest and emits layeradd per restored layer.
  • Updates state.addedLayers, refreshes the added-layers view, and emits a single statechange for the batch.
  • Never fits bounds and shows no notices.
  • When called before the control is added to a map, the entries are deferred and applied automatically in onAdd.

Tests

Added vitest coverage for: reusing existing natives (no duplication) with opacity/visibility reconciliation, creating natives when missing (tiles mode), duplicate-skipping with a single batched statechange, no-op emission, and deferred restore applied after onAdd.

Version bumped 0.1.0 -> 0.1.1 and README method table updated. Not published to npm.

Summary by CodeRabbit

  • New Features

    • Added layer restoration functionality to recover previously-persisted added layers, with support for deferred restoration before the control is attached to a map.
  • Documentation

    • Updated documentation to describe the new layer restoration method.
  • Tests

    • Added test coverage for layer restoration scenarios.
  • Chores

    • Version bumped to 0.1.1.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds layer persistence restoration to EnviroAtlasControl. MapLayerManager now eagerly creates native sources and layers and provides a restoreLayer method to re-register persisted layers with native reuse. EnviroAtlasControl queues restore requests before attachment and applies them on onAdd, exposing a public restoreLayers API for clients. Tests validate source/layer reuse, native creation with bounds and tile templating, duplicate skipping, and deferred execution. Documentation and version bumping complete the release.

Changes

Layer Restoration Feature

Layer / File(s) Summary
MapLayerManager layer restoration infrastructure
src/lib/core/mapLayers.ts
addLayer refactored to immediately create MapLibre natives via _createSource and _createLayer. New public restoreLayer method reuses or creates natives and reconciles raster opacity and visibility. Entry-driven bounds, opacity, and visible values applied consistently when building sources and layers.
Control-level deferred restore queue and public API
src/lib/core/EnviroAtlasControl.ts
New _pendingRestore queue stores entries when restoreLayers is called pre-attachment; onAdd applies and clears the queue. Public restoreLayers(entries) method queues or directly restores depending on attachment status, emits layeradd per restored layer, and emits single statechange for the batch.
Test coverage for restoreLayers
tests/control.test.ts
RESTORE_ENTRY fixture and five test cases covering native reuse with opacity/visibility updates, creation of missing natives with bounds and tiles URL templating, duplicate skipping with single statechange, empty restore handling, and deferred execution before onAdd.
Documentation and version release
README.md, package.json
restoreLayers(entries) method documented in control methods list with deferred-execution behavior. Package version incremented from 0.1.0 to 0.1.1.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through persisted lore,
Restoring layers once lost before,
Deferred with grace till the map takes hold,
Native reuse keeps the story told!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add restoreLayers API to rehydrate host-persisted layers' accurately and concisely describes the main change—adding a new restoreLayers API to restore previously persisted layers, which is the primary objective of the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/restore-layers-api

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/core/EnviroAtlasControl.ts`:
- Around line 413-418: Loop currently only checks service+sublayer and can still
emit layeradd when an entry with the same id is already tracked; before calling
this._layerManager.restoreLayer(entry) (or before emitting), call the layer-id
check using the entry.id (e.g., this._layerManager.findLayer(entry.id)) and skip
this entry if a layer with that id already exists so you don't emit a duplicate
this._emit('layeradd', { layer }) for already-tracked ids.
- Around line 413-423: The loop that calls
this._layerManager.restoreLayer(entry) should isolate per-entry failures so one
thrown exception doesn't abort the whole batch: wrap the call to
restoreLayer(entry) in a try/catch, on success set restored = true, call
this._emit('layeradd', { layer }) and continue; on catch log or handle the
single-entry error (do not rethrow) and continue with the next entry. After the
loop keep the existing final block that sets this._state.addedLayers =
this._layerManager.getLayers(), calls
this._addedView?.update(this._state.addedLayers) and this._emit('statechange')
only if restored is true so partial successes still reconcile UI/state. Ensure
references: restoreLayer, _layerManager, _emit, _state, and _addedView are used
as shown.

In `@src/lib/core/mapLayers.ts`:
- Around line 154-156: When restoring a layer (`restoreLayer`) we must not
delete a pre-existing/native source if `_createLayer` fails; update
`_createLayer` (and call sites from `restoreLayer`) so it knows whether it
actually created `copy.sourceId` (e.g., determine upfront if source existed or
return/throw with a flag). Only remove the source in `_createLayer`'s error
handler when that flag indicates this manager created it (and verify the source
still exists and matches the expected id before removal). Use the symbols
`_createLayer`, `restoreLayer`, and `copy.sourceId` to locate the logic and add
the created-by-manager check or pass a boolean from `restoreLayer` to avoid
deleting host-recreated/native sources.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3667ad70-a6a4-4a29-afc7-186025806de2

📥 Commits

Reviewing files that changed from the base of the PR and between 7acfba4 and ba81054.

📒 Files selected for processing (5)
  • README.md
  • package.json
  • src/lib/core/EnviroAtlasControl.ts
  • src/lib/core/mapLayers.ts
  • tests/control.test.ts

Comment on lines +413 to +418
for (const entry of entries) {
if (this._layerManager.findLayer(entry.service, entry.sublayerId)) continue;
const layer = this._layerManager.restoreLayer(entry);
restored = true;
this._emit('layeradd', { layer });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip already-tracked IDs before emitting layeradd.

This loop only de-dupes by service+sublayer. If an entry is already tracked by id but not by that key, restoreLayer can return the existing layer and this still emits a new layeradd, violating the “already tracked entries are skipped” contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/core/EnviroAtlasControl.ts` around lines 413 - 418, Loop currently
only checks service+sublayer and can still emit layeradd when an entry with the
same id is already tracked; before calling
this._layerManager.restoreLayer(entry) (or before emitting), call the layer-id
check using the entry.id (e.g., this._layerManager.findLayer(entry.id)) and skip
this entry if a layer with that id already exists so you don't emit a duplicate
this._emit('layeradd', { layer }) for already-tracked ids.

Comment on lines +413 to +423
for (const entry of entries) {
if (this._layerManager.findLayer(entry.service, entry.sublayerId)) continue;
const layer = this._layerManager.restoreLayer(entry);
restored = true;
this._emit('layeradd', { layer });
}

if (!restored) return;
this._state.addedLayers = this._layerManager.getLayers();
this._addedView?.update(this._state.addedLayers);
this._emit('statechange');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate per-entry restore failures so one bad entry doesn’t break batch/onAdd.

restoreLayer exceptions currently bubble out of restoreLayers. Because onAdd calls this method directly, one malformed/conflicting entry can abort control initialization and leave partial restores without final state/UI reconciliation.

Suggested fix
   restoreLayers(entries: AddedLayer[]): void {
     if (!this._layerManager) {
       this._pendingRestore.push(...entries);
       return;
     }

+    const tracked = this._layerManager.getLayers();
+    const seenIds = new Set(tracked.map((l) => l.id));
+    const seenServiceKeys = new Set(
+      tracked.map((l) => `${l.service.fullName}::${l.sublayerId ?? ''}`)
+    );
+
     let restored = false;
     for (const entry of entries) {
-      if (this._layerManager.findLayer(entry.service, entry.sublayerId)) continue;
-      const layer = this._layerManager.restoreLayer(entry);
-      restored = true;
-      this._emit('layeradd', { layer });
+      const serviceKey = `${entry.service.fullName}::${entry.sublayerId ?? ''}`;
+      if (seenIds.has(entry.id) || seenServiceKeys.has(serviceKey)) continue;
+      try {
+        const layer = this._layerManager.restoreLayer(entry);
+        restored = true;
+        seenIds.add(layer.id);
+        seenServiceKeys.add(`${layer.service.fullName}::${layer.sublayerId ?? ''}`);
+        this._emit('layeradd', { layer });
+      } catch (error) {
+        this._handleError(error instanceof Error ? error : new Error(String(error)));
+      }
     }

     if (!restored) return;
     this._state.addedLayers = this._layerManager.getLayers();
     this._addedView?.update(this._state.addedLayers);
     this._emit('statechange');
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/core/EnviroAtlasControl.ts` around lines 413 - 423, The loop that
calls this._layerManager.restoreLayer(entry) should isolate per-entry failures
so one thrown exception doesn't abort the whole batch: wrap the call to
restoreLayer(entry) in a try/catch, on success set restored = true, call
this._emit('layeradd', { layer }) and continue; on catch log or handle the
single-entry error (do not rethrow) and continue with the next entry. After the
loop keep the existing final block that sets this._state.addedLayers =
this._layerManager.getLayers(), calls
this._addedView?.update(this._state.addedLayers) and this._emit('statechange')
only if restored is true so partial successes still reconcile UI/state. Ensure
references: restoreLayer, _layerManager, _emit, _state, and _addedView are used
as shown.

Comment thread src/lib/core/mapLayers.ts
Comment on lines +154 to +156
if (!this._map.getLayer(copy.layerId)) {
this._createLayer(copy);
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not delete pre-existing sources on restore-layer failure.

When restoreLayer hits this branch, copy.sourceId may belong to a host-recreated source. If _createLayer throws, _createLayer’s catch currently removes that source unconditionally, which can delete a native source the manager did not create.

Suggested fix
-  private _createLayer(entry: AddedLayer): void {
+  private _createLayer(entry: AddedLayer, removeSourceOnFailure = true): void {
@@
     } catch (error) {
       // Avoid orphaning the source when layer creation fails
-      if (this._map.getSource(entry.sourceId)) {
+      if (removeSourceOnFailure && this._map.getSource(entry.sourceId)) {
         this._map.removeSource(entry.sourceId);
       }
       throw error;
     }
   }
-    if (!this._map.getLayer(copy.layerId)) {
-      this._createLayer(copy);
+    const sourceExisted = Boolean(this._map.getSource(copy.sourceId));
+    if (!this._map.getLayer(copy.layerId)) {
+      this._createLayer(copy, !sourceExisted);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/core/mapLayers.ts` around lines 154 - 156, When restoring a layer
(`restoreLayer`) we must not delete a pre-existing/native source if
`_createLayer` fails; update `_createLayer` (and call sites from `restoreLayer`)
so it knows whether it actually created `copy.sourceId` (e.g., determine upfront
if source existed or return/throw with a flag). Only remove the source in
`_createLayer`'s error handler when that flag indicates this manager created it
(and verify the source still exists and matches the expected id before removal).
Use the symbols `_createLayer`, `restoreLayer`, and `copy.sourceId` to locate
the logic and add the created-by-manager check or pass a boolean from
`restoreLayer` to avoid deleting host-recreated/native sources.

@giswqs giswqs merged commit eb95fb3 into main Jun 7, 2026
2 checks passed
@giswqs giswqs deleted the feat/restore-layers-api branch June 7, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant