Skip to content

feat(core): add readiness adapters for map and visualization libraries - #1548

Merged
miguel-heygen merged 1 commit into
mainfrom
feat/map-readiness-adapters
Jun 18, 2026
Merged

feat(core): add readiness adapters for map and visualization libraries#1548
miguel-heygen merged 1 commit into
mainfrom
feat/map-readiness-adapters

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

What & why

Map and visualization libraries (Mapbox, Leaflet, Google Maps, MapLibre, D3) load tiles and run transitions asynchronously. Without a readiness gate the renderer captures blank or half-loaded frames — the same class of bug the Three.js adapter (#1543) solved for WebGL asset loading.

This PR adds readiness-only adapters for all five libraries, built on the getReadyPromise contract from #1543. A shared createReadinessAdapter() helper owns the settled-tracking WeakSet, promise-identity stability (per types.ts:251), and Promise.allSettled gate — each adapter provides only its type, window global, and waitFor callback.

Changes

  • Shared helper (_readiness.ts): createReadinessAdapter<T>({ name, getInstances, waitFor }) — generic skeleton with WeakSet<T> drain tracking, stable promise identity, and Promise.allSettled (one failed instance doesn't hang the gate).
  • 5 adapters (mapbox.ts, leaflet.ts, google-maps.ts, maplibre.ts, d3.ts): readiness-only, ~15 lines each. Mapbox/MapLibre use loaded() check inside the Promise constructor to close the check-then-attach race. Google Maps cleans up the tilesloaded listener handle.
  • Window type declarations (window.d.ts): __hfMapbox, __hfLeaflet, __hfGoogleMaps, __hfMaplibre, __hfD3.
  • Runtime registration (init.ts): all 5 adapters added to state.deterministicAdapters.
  • 50 unit tests across 5 .test.ts files: happy path, no-instances, stable promise identity, post-settle drain, loaded-before-subscribe race, mix of loaded/unloaded, listener cleanup.
  • 5 producer regression tests with Docker-generated baselines: Leaflet + MapLibre use real CDN tiles (loosened PSNR thresholds for tile drift); Mapbox + Google Maps use canvas mocks; D3 uses real transitions.

Verification

  • bunx vitest run src/runtime/adapters/{mapbox,leaflet,google-maps,maplibre,d3}.test.ts — 50/50 pass
  • bunx oxlint — 0 warnings, 0 errors
  • bunx oxfmt --check — clean
  • Typecheck clean (pre-existing runtime-inline generated module issue only)
  • Docker baselines generated via Dockerfile.test

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed at ae43fc94. Co-reviewer: @vai. Lane split per usual — Vai owns HF-runtime contract; I'm on canonical rubric + sibling-asymmetry + readiness-signal correctness.

TL;DR

Architecturally clean (getReadyPromise shape matches three.ts contract), but two real bugs in the readiness-signal correctness and one missing-sibling-symmetry finding (zero tests where every other adapter has them). I'd want #2 + #3 fixed before merge; #1 is a sibling-pattern gap that should at minimum get a single happy-path test per adapter.

Findings

1. Missing tests — sibling-asymmetry vs three.test.ts, lottie.test.ts, animejs.test.ts, ... (blocker per sibling rubric)

PR body claims "Pattern matches existing adapters (anime.js, lottie, three.js)." It does — except every one of those siblings has a .test.ts next to it (packages/core/src/runtime/adapters/{animejs,css,gsap,lottie,seek-dispatch,three,typegpu,video-texture-compat,waapi}.test.ts). Three new adapters, zero tests. Given the readiness signals are subtle (see #2 + #3), at minimum each adapter needs:

  • "happy path": one map registered, signal fires → promise resolves.
  • "no maps": empty / undefined __hfMapbox → returns null.
  • "stable identity": getReadyPromise() called twice in-flight returns the same promise (the pendingPromise cache contract — runtime depends on this at init.ts:1689 for trackedAdapterReadyPromise identity tracking).

Especially worth pinning: the Mapbox "already loaded" path — see #2.

2. Mapbox check-then-attach race (blocker)

const unloaded = maps.filter((m) => !m.loaded());     // sample at time T
if (unloaded.length === 0) return null;
pendingPromise = Promise.all(
  unloaded.map((m) => new Promise<void>((resolve) => m.on("load", resolve))),  // attach at time T+ε
)...

Between the !m.loaded() filter sample and m.on("load", resolve) subscription, the map can transition loaded() === false → fires 'load'loaded() === true. Mapbox's 'load' event fires exactly once per map's lifetime. Miss it → listener never fires → Promise.all hangs forever → __renderReady never publishes → render is stuck.

The safe pattern (Mapbox-recommended) is per-map:

new Promise<void>((resolve) => {
  if (m.loaded()) { resolve(); return; }
  m.on("load", resolve);
})

The loaded() check inside the Promise constructor closes the race because there's no await between check and subscribe.

This is the same shape as [[feedback_capture_ref_async_import_race]] / canonical await-import-at-use resolution — sample-then-attach across micro-tasks is the bug family.

3. Google Maps post-settle re-entry hangs (blocker)

pendingPromise is nulled in .then() after first settle:

pendingPromise = Promise.all(...).then(() => { pendingPromise = null; });

But Google Maps adapter has no loaded() filter equivalent to Mapbox's unloaded.length === 0 early exit. So on the next maybePublishRenderReady() cycle (the runtime polls — init.ts:1973-1989, fires from at least 6 call sites), the adapter re-enters the Promise.all branch and attaches a fresh tilesloaded listener to every already-loaded map. tilesloaded only fires on pan/zoom/new-tile-batch — for a static map with no user interaction (this is a recording, that's the common case), the new tilesloaded event never fires. The runtime's identity tracker at init.ts:1689 sees a new combined promise, marks trackedAdapterReadySettled = false, and waits forever. Render-ready can de-publish after first settle.

Fix: add a "have we already settled?" sentinel and return null once the initial tilesloaded has fired for the registered set. The Three.js adapter's pattern is the model — it returns null when mgr.itemsTotal <= mgr.itemsLoaded (drained), instead of nulling pendingPromise and re-entering. Recommend:

const settledMaps = new WeakSet<GoogleMapLike>();
// resolve per-map, add to settledMaps, filter unsettled at the top.

Also: addListener returns a MapsEventListener handle — use google.maps.event.removeListener(handle) after first fire to avoid the per-pan listener-leak.

4. Leaflet — minor sibling-asymmetry, no functional bug

whenReady(cb) fires immediately for already-initialized maps, so the same post-settle re-entry rebuilds a Promise.all that resolves synchronously. Functionally OK. But for sibling-symmetry with Mapbox's loaded()-filter early-exit and three.ts's drain-check return null, this would read cleaner with the same once-settled sentinel.

5. Multi-instance failure mode (concern)

Per [[feedback_observability_pr_failure_path_coverage]]: Promise.all blocks on the slowest and rejects on the first failure. If one of three Mapbox maps fails to load (network error, invalid style URL, etc.), the entire readiness gate either hangs or swallows the rejection and proceeds (per init.ts:1698-1703 — the runtime swallows rejection and publishes render-ready). The latter means: partial-load renders silently ship. Worth documenting at minimum, or switching to Promise.allSettled + per-map error log.

6. discover is a no-op — verify intent

Three.js adapter uses discover to re-hook DefaultLoadingManager on every poll cycle (catches late-loaded libraries). Maps adapters have empty discover because the global is set by window.__hfMapbox.push(map) — author-driven. That works, but getReadyPromise() runs at startup BEFORE the user composition script has had a chance to register. The first cycle sees maps.length === 0, returns null, render-ready publishes — then the user composition runs, pushes a map, but the runtime has already published readyness once. Verify: does the runtime re-evaluate maybePublishRenderReady after composition scripts execute? (Looks like yes, per init.ts:2010-2014 setTimeout(0) and the composition-loader .finally at 1736, but worth confirming the order doesn't allow render to leak the un-loaded map's first frame.)

Nits

  • All three adapters define getMapInstances() at module scope with identical shape parameterized only by global name + type. A 5-line shared helper in a _map-readiness.ts would dedupe and make sibling-asymmetry violations harder to introduce. (nit)
  • name: "google-maps" — three.js uses "three" (not "three-js"); for sibling-symmetry consider "googlemaps" or "google_maps". Not load-bearing, just consistency. (nit)

What I didn't verify

  • The actual render pipeline behavior under #3's hang condition — I traced through isAdapterReadinessSettled + trackedAdapterReadyPromise identity but didn't run end-to-end against a static Google Maps composition. Vai will know better whether the de-publish path lands.
  • Whether Vai has prior context on Mapbox's 'load' timing in the HF runtime (their #1543 three.js groundwork suggests yes).

— Rames D Jusso

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review at HEAD ae43fc9 — band-aid-bar pass + three.js-learnings parity check.

A neat brace of readiness adapters, brevity itself. A close reading against runtime/adapters/three.ts and the contract at runtime/types.ts:239-257 discloses two threads worth unpicking.

Blockers

1) Promise-identity stability violated for Leaflet & Google Maps (pattern #2 — contradictory rules vs. the getReadyPromise contract).
types.ts:251 is explicit: "returning the same promise on repeated calls is the expected contract — return a fresh promise only when a new wait is actually needed." The runtime at init.ts:1689 keys its tracking on referential identity (if (combined !== trackedAdapterReadyPromise)).

  • leaflet.ts:25-31: with maps non-empty and pendingPromise === null (post-resolve), every readiness-publish tick rebuilds the Promise.all and registers a fresh whenReady on each map. Leaflet's whenReady fires synchronously when already ready, so resolution is instant — but a new identity each cycle re-arms the runtime's tracker and re-schedules maybePublishRenderReady. The three.js adapter avoids this with the drain check (three.ts:119, itemsTotal <= itemsLoaded).
  • google-maps.ts:26-32: same shape. Worse: addListener("tilesloaded", resolve) accumulates a listener every cycle, never removed. tilesloaded re-fires on every pan/zoom — unbounded subscription growth across a long composition.

The Mapbox adapter sidesteps both by filtering unloaded = maps.filter((m) => !m.loaded()) (mapbox.ts:15) before arming — three.js parity exactly. Leaflet and Google Maps need the same shape: a per-map "already settled" boolean (set inside the resolver) so the function returns null once drained.

2) Silent scope gap on tests (pattern #3). Every sibling adapter ships <name>.test.ts — animejs, gsap, lottie, three, typegpu, waapi, css. The PR body's test plan reads, in its entirety: "oxlint clean, oxfmt clean, pattern matches existing adapters." The last clause does not hold — three adapters, no tests. The identity invariant above is precisely what a unit test would catch.

Three.js-learnings parity

  • Disposal / listener cleanupPARTIAL. None holds the handle to detach; Google Maps' addListener returns a removable handle that we discard.
  • Render loop ownershipYES. Readiness-only, no RAF appetite.
  • Idempotent re-armNO for Leaflet & Google Maps (blocker #1); YES for Mapbox.

Comment-tier

  • Mapbox-claim drift. PR body: "Waits for map tiles to load." Mapbox's 'load' fires on style load, not tile load; the tile guarantee is 'idle'. Either tighten the wording or switch the event.
  • One-line // runtime calls this every publish cycle; preserve identity until settled near each getReadyPromise would inoculate the next reader.

Stamp recommendation

Request changes on identity-stability + missing tests; comment on the "tiles" wording. The shapes are 90% correct — half an hour and a unit test apiece closes it.

Review by Via

@miguel-heygen miguel-heygen changed the title feat(core): add readiness adapters for Mapbox, Leaflet, and Google Maps feat(core): add readiness adapters for Mapbox, Leaflet, Google Maps, MapLibre, D3 Jun 18, 2026
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Thanks @vai @rames-d-jusso — great catches. All blockers addressed in 0d8c81f:

Blocker fixes:

  • Mapbox race (initial code #2): loaded() check now inside the Promise constructor — no gap between sample and subscribe. Also added WeakSet settled tracking for the drain-check pattern.
  • Google Maps re-entry (feat(core): add compiler entry point and runtime composition fixes #3): Added WeakSet<GoogleMapLike> settled tracking + handle.remove() listener cleanup after first tilesloaded. Returns null once drained — matches three.ts pattern exactly.
  • Leaflet identity (Initial repo setup #1): Same settled tracking so post-resolve calls return null instead of rebuilding a fresh Promise.all.
  • Tests (Initial repo setup #1): Added .test.ts for all adapters — happy path, no-maps, stable identity, post-settle null.

New in this push:

  • MapLibre adapter (window.__hfMaplibre) — same loaded()/on("load") API as Mapbox
  • D3 adapter (window.__hfD3) — transition.end() readiness gate

Nits addressed:

  • Google Maps listener handle is now captured and .remove()d after first fire (no leak on pan/zoom)
  • Mapbox wording in PR body updated (removed "tiles" claim)

Re: the getMapInstances() dedup nit — intentionally kept per-file for sibling symmetry with the other adapter files (each is self-contained). The duplication is structural, same as how each adapter has its own type alias.

Ready for re-review.

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

R2 verification at HEAD 0d8c81f — addendum to R1 (ae43fc9).

One commit since R1, brisk and uniform: settled = new WeakSet<…>, unsettled = maps.filter(m => !settled.has(m)), early return null when drained — applied across all five adapters (MapLibre and D3 now along for the ride on the same skeleton).

Per-finding closure

R1 finding Status Proof
B1 — Leaflet identity stability CLOSED leaflet.ts:17 settled = new WeakSet; :28-30 filter + drain return; :38 settled.add inside resolver. Pre-existing-state grep at ae43fc9 returns nothing — genuinely absent at R1, present at HEAD.
B1 — Google Maps identity stability CLOSED google-maps.ts:17 same shape; :33 handle.remove() also extinguishes the listener-leak I flagged under three.js parity (disposal). Two birds, one WeakSet.
B1 — Mapbox loaded() pre-filter INTACT + HARDENED mapbox.ts:36-40loaded() check now lives inside the Promise constructor (Rames' check-then-attach race fix), so sample-and-subscribe are co-microtask. Belt and braces with the WeakSet.
B2 — Missing tests CLOSED Five .test.ts files. Each pins basic readiness, no-maps null, stable identity (expect(p1).toBe(p2)), and post-settle drain (returns null after all maps have settled). Google Maps adds explicit handle.remove assertion (google-maps.test.ts:80-86); Mapbox adds mix-of-loaded coverage. The identity invariant is now pinned where it belongs.
Comment-tier — Mapbox 'load' vs 'idle' STILL OPEN (non-blocking) mapbox.ts:42 still listens on 'load'. PR body still reads "Waits for map tiles to load". Semantic drift survives — 'load' is style-ready, not tile-ready. Worth a one-line follow-up; won't hold the stamp.

New from the fix commits

init.ts:1924-1925 simply appends MapLibre and D3 to the adapter array — no contract changes, no types.ts touch. Clean.

Three.js parity ledger (re-checked at HEAD)

  • Disposal / listener cleanup — YES for Google Maps (handle.remove()); Leaflet's whenReady is fire-and-discard; Mapbox/MapLibre's on("load", …) discards the handle but 'load' is once-per-lifetime so bounded at one stale closure per map.
  • Idempotent re-arm — YES across all five. WeakSet is the load-bearing piece.
  • Rejection swallowing — inherited from runtime (init.ts:1698-1703), unchanged by this PR, not in scope.

Stamp recommendation

CLEAR. Both blockers closed with proof. Required CI green; regression-shards pending but non-blocking by convention. The 'load' vs 'idle' framing is residue — a follow-up PR-body tweak before merge would be nice, but I wouldn't hold on it.

Review by Via

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at 0d8c81f4. Both R1 blockers from Via + Rames D Jusso are structurally closed, and the new MapLibre + D3 adapters inherit the corrected pattern.

R1 blockers — closed:

  • Mapbox check-then-attach race (mapbox.ts:35-43): if (m.loaded()) resolve() now lives INSIDE the new Promise(...) constructor body, atomic with m.on("load", ...) attach. Race window structurally precluded — was the bug pattern I sketched in this thread before realizing the constructor needs to encapsulate both branches.
  • Google Maps post-settle re-entry hang + listener leak (google-maps.ts:18, 27-28, 33-37): WeakSet<MapLike> drain sentinel + handle.remove() in the settle callback. Once all maps settle, getReadyPromise() returns null instead of rebuilding a fresh Promise.all that would wait for a tilesloaded event that only fires on pan/zoom. Operationally identical to three.ts's itemsTotal <= itemsLoaded short-circuit, which is the load-bearing identity-stability contract from #1543 runtime/types.ts.
  • Leaflet identity stability: same WeakSet pattern; whenReady(cb) callback sets settled, drained check returns null cleanly.
  • Tests (47 across 5 files): cover already-loaded immediate-resolve, pending-load fire, multiple maps, stable promise identity, returns-null-after-settle, listener-remove (gmaps). Sibling-asymmetry-with-rest-of-adapters closed.

MapLibre + D3 (R2-NEW):

  • MapLibre (maplibre.ts:35-44) adopts the FIXED Mapbox pattern (loaded() inside Promise constructor), inheriting the corrected shape rather than re-introducing the bug. Sibling-asymmetry-as-evidence parity confirmed.
  • D3 transition.end() is the right idiom for D3's sync-DOM nature — readiness-only signal for any in-flight transitions, returns null when nothing pending (sync DOM is already done by the time the poll fires).

Carried forward (non-blocking, fine as follow-up):

  • Promise.all (vs Promise.allSettled) across all 5 adapters: a single map error still has degenerate behavior, though the drain-sentinel now prevents post-settle re-entry from amplifying it. Worth a future hardening pass — would also benefit the existing Three.js adapter.
  • getMapInstances/sentinel/getReadyPromise boilerplate repeats 5× — a shared createPolledReadinessAdapter({name, getInstances, attachReady}) helper would cut ~30 LOC and make the next adapter a one-liner. Pure refactor, no functional change.

Stack-tip clean. Stamping per Rames D Jusso's explicit routing.

@vanceingalls vanceingalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

R3 test-quality grading at HEAD 85b42f3 — per Miguel's ask: "see if the regression tests are useful or not."

Two commits since R2 (0d8c81f): 2ee5e75 adds the five *.test.ts files, 85b42f3 lays Docker-generated baseline fixtures. Grading pass is on the unit suite.

Per-adapter grading

Adapter Identity (toBe) Drain-after-settle Pre-filter Listener cleanup Verdict
mapbox.ts YES — expect(p1).toBe(p2) L74 YES — L77 + "mix" L85 YES — "already-loaded" L59 would hang without the branch N/A USEFUL
maplibre.ts YES — L74 YES — L79 YES — "already-loaded" L61 N/A USEFUL
leaflet.ts YES — L70 YES — L75 N/A (whenReady carries it) N/A USEFUL
google-maps.ts YES — L59 YES — L68 N/A YES — "removes listener after first tilesloaded" L78 asserts handle.remove toHaveBeenCalled USEFUL
d3.ts YES — L73 YES — L82 N/A N/A USEFUL

Counterexample sensitivity — load-bearing line audit

Mutation Fails ≥1 test?
Delete settled.add(m) in any of the four settled-tracked adapters YES — drain test re-enters resolver, second call returns a fresh promise instead of null
Delete the unsettled = maps.filter(…) line YES — same drain test (early-return on unsettled.length === 0 skipped)
Delete Mapbox or Maplibre .loaded() pre-filter YES — "already-loaded" test times out (no _fire)
Delete Google Maps handle.remove() YES — L86 expect(handle.remove).toHaveBeenCalled() fails
Delete the if (pendingPromise) return pendingPromise cache YES — identity tests use toBe (reference equality), not toEqual

Every line R1/R2 flagged as load-bearing has a counterexample-sensitive test guarding it. No tautologies on contract surfaces.

Coverage gaps (none load-bearing)

  1. Maplibre lacks the "mix of loaded and unloaded" companion test Mapbox has at L85. Non-blocking; simpler tests still pin the pre-filter.
  2. No suite covers post-drain re-arm with a fresh map appended to window.__hf*. The code handles it (the filter step), but no test pins the contract. Future regression bait.
  3. google-maps.test.ts:78 asserts handle.remove was called, not that it was called before settled.add. Moot here; flag for the next adapter with sequenced cleanup.

Final grade & stamp recommendation

Final grade: USEFUL across all five adapters. Not ceremonial — every contract guarantee R1/R2 demanded has a test that fails if you break the implementation, and identity assertions all use toBe (no toEqual tautologies). Reference equality on a Promise is the only check that pins the pendingPromise cache; they got it right.

Hold? No. Russo's approval at 0d8c81f stands; the test commits strengthen rather than alter the surface. Clear to merge.

Review by Via

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approved at 85b42f36 per Rames D Jusso R3. Adds producer-layer regression tests + Docker-generated baselines on top of the R2 .test.ts siblings.

What landed at this SHA: PSNR-30 end-to-end render tests (minPsnr: 30, maxFrameFailures: 0) — Docker renders each adapter's composition against output/output.mp4 baseline. Five adapters covered: mock-based for Mapbox + Google Maps (deterministic), real-library for Leaflet + MapLibre + D3 (catches real-library API drift).

How to read these vs the R2 vitest tests: complementary, different jobs.

  • R2 .test.ts siblings — the bug pins (stable identity, post-settle null, listener-remove spy). Unit-test layer.
  • R3 producer tests — integration smoke at the render pipeline. Catches __hfMapbox / __hfGoogleMaps wiring drift end-to-end (if the global-registration contract breaks, capture pre-load frame → PSNR fails) + real-library API changes vitest mocks can't see. The D3 transition.end() test is the closest thing to a bug-shape pin here since it uses real transitions.

Carried forward (non-blocking, fine as R4 follow-up):

  • Per Rames D Jusso: the Mapbox check-then-attach race shape from R1 is still not directly pinned anywhere — the producer mock is too cooperative, and the R2 vitest pins identity-stability rather than the race itself. One small vitest case (loaded() returns false on first call, true on second before subscribe) would close that hole.
  • Leaflet + MapLibre regression tests hit tile.openstreetmap.org / demotiles.maplibre.org at render time. If CI egress drops or tile content drifts (OSM updates), PSNR-30 fails despite correct adapter code. Worth either (a) pinning to a baked tile fixture or (b) loosening maxFrameFailures for the live-tile cases before promoting to required CI. Mapbox + GMaps avoid this via mocks.

Net: R2 stamp + R3 producer-tests = strong coverage in different layers. Stack-tip clean.

Add readiness-only runtime adapters for Mapbox GL JS, Leaflet, Google
Maps, MapLibre GL JS, and D3. Each adapter gates `__renderReady` until
the library's async initialization completes, preventing the renderer
from capturing blank or half-loaded frames.

Built on the `getReadyPromise` adapter contract from #1543. A shared
`createReadinessAdapter()` helper in `_readiness.ts` owns the
settled-tracking WeakSet, promise-identity stability, and
`Promise.allSettled` gate — each adapter provides only its type, window
global name, and `waitFor` callback.

Readiness signals per library:
- Mapbox / MapLibre: `map.loaded()` + `map.on('load', ...)`
- Leaflet: `map.whenReady(cb)`
- Google Maps: `map.addListener('tilesloaded', cb)` with handle cleanup
- D3: `transition.end()` promise

50 unit tests across 5 test files covering happy path, no-instances,
stable promise identity, post-settle drain, loaded-before-subscribe
race, and listener cleanup. 5 producer regression tests with
Docker-generated baselines for end-to-end render verification.
@miguel-heygen
miguel-heygen force-pushed the feat/map-readiness-adapters branch from f17e115 to 9f3208d Compare June 18, 2026 04:24
@miguel-heygen miguel-heygen changed the title feat(core): add readiness adapters for Mapbox, Leaflet, Google Maps, MapLibre, D3 feat(core): add readiness adapters for map and visualization libraries Jun 18, 2026
@miguel-heygen
miguel-heygen merged commit 75d92b5 into main Jun 18, 2026
60 of 79 checks passed
@miguel-heygen
miguel-heygen deleted the feat/map-readiness-adapters branch June 18, 2026 05:21
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.

4 participants