Cómo se despliega Meratyn y cómo está organizado el código. Dos vistas, un mismo sistema.
Entrega
Meratyn se distribuye de tres formas desde un único monorepo de workspaces Bun. El visor es una aplicación React servida como archivos estáticos por nginx (Docker) o instalada como app de escritorio Tauri v2 para Windows, macOS y Linux. La consola de gestión y la API de administración se despliegan como contenedores, y un anywidget de Jupyter incrusta el visor en cuadernos. La distribución es closed-source: imágenes Docker e instaladores de escritorio firmados, sin tiendas de aplicaciones ni servidores de actualización.
Topología en ejecución
Topología en ejecución. Los enlaces azul claro cruzan un límite de confianza o de red; la API de gestión es el hub del plano de control.
Flota de contenedores del perfil autoalojado: el contenedor del visor ejecuta nginx y el sidecar en una sola imagen; GeoNode se despliega como su propia pila de siete contenedores.
Límites de confianza
La app de escritorio habla con su sidecar local solo por 127.0.0.1; el procesamiento pesado en Python nunca sale de la máquina salvo que se configure un servicio remoto.
El contenedor del visor alcanza el sidecar en el mismo origen en /sidecar, sin superficie CORS. Con MAPPVIEW_DISABLE_SIDECAR=1 se sirve solo estático.
La API de gestión antepone el almacenamiento de objetos con URL de capa firmadas y temporales (LAYER_ACCESS_TTL_SECONDS) y cuotas por tenant.
El relé de IA autentica visores con token, restringe modelos y limita la tasa por cliente; las claves de proveedores nunca llegan al navegador.
Despliegue
La misma build React se distribuye de tres maneras: el shell de escritorio Tauri con un sidecar bajo demanda, un contenedor nginx que hace proxy inverso de su sidecar empaquetado same-origin, y la wheel Jupyter con la app incrustada en el kernel. El escritorio mantiene el procesamiento pesado local, el contenedor le hace proxy y el embed lo deja fuera.
El Dockerfile raíz empaqueta la versión de navegador de la app. Usa una etapa de compilación Node para correr la build del workspace de mappview-desktop, y luego copia apps/mappview/dist en una imagen runtime de nginx. La configuración de nginx sirve los assets estáticos y recurre a index.html para las URLs de entrada del navegador.
El workflow Container build de GitHub Actions verifica que la imagen compila en
cada push a main. No inicia sesión en ningún registry ni publica imágenes.
Compile y etiquete la imagen localmente para desarrollo o publíquela mediante
infraestructura propiedad de Meratyn.
La imagen también empaqueta el sidecar de Python opcional (uvicorn) y le hace proxy inverso en /sidecar, así que el navegador lo alcanza same-origin sin CORS. MAPPVIEW_CAPABILITIES se publica tanto en la configuración de runtime como en el sidecar y controla autoridad, no solo la visibilidad de los menús; consulte capacidades de despliegue para el contrato exacto. Defina MAPPVIEW_DISABLE_SIDECAR=1 para ejecutar solo nginx. El contenedor no ejecuta el shell de escritorio Tauri, así que los flujos de trabajo que dependen del acceso al filesystem de escritorio todavía requieren la app de escritorio instalada. Ver Ejecutar con Docker para lo que el sidecar empaquetado respalda y no respalda.
Seguridad
La CSP de Tauri mantiene una lista blanca de hosts de teselas y estilos (OpenFreeMap, CARTO).
El acceso a archivos usa solo rutas seleccionadas mediante diálogo.
Almacén de confianza HTTP nativo y TLS mutuo
La app de escritorio realiza algunas fetch remotas (resolución de teselas/URLs, GetCapabilities de OGC
y similares) desde el proceso Rust nativo en lugar de desde la
WebView. Esa ruta (guarded_http_client en src-tauri/src/lib.rs) confía en el
almacén de certificados del OS/sistema además de las raíces Mozilla incluidas, de modo que un
servidor firmado por una CA empresarial instalada en la máquina se acepta sin
configuración adicional.
Para endpoints detrás de TLS mutuo (mTLS), la ruta fetch de la WebView muestra un
aviso interactivo de certificados del OS; el cliente nativo Rust no puede hacerlo, así que apúntelo a
un certificado de cliente mediante variables de entorno:
Variable
Propósito
MAPPVIEW_HTTP_CA_CERT
Ruta a un bundle PEM de certificados CA adicionales en los que confiar, encima del almacén del OS (para una CA privada no instalada a nivel sistema).
MAPPVIEW_HTTP_CLIENT_CERT
Ruta al certificado de cliente a presentar. Un archivo .pem (cadena de certificados más una clave privada PKCS#8 sin cifrar) o un bundle PKCS#12 (.p12/.pfx).
MAPPVIEW_HTTP_CLIENT_CERT_PASSWORD
Frase de contraseña para un certificado de cliente PKCS#12. Su presencia también fuerza la ruta de código PKCS#12.
Una extensión .p12/.pfx (o una frase de contraseña suministrada) selecciona PKCS#12; cualquier otra
ruta se lee como PEM. Las identidades PEM usan el backend rustls predeterminado; las identidades PKCS#12
usan el backend native-tls nativo de la plataforma (SChannel en Windows, Secure
Transport en macOS, OpenSSL en Linux), que también lee el almacén de confianza del OS.
Convierta un export PKCS#12 a PEM con
openssl pkcs12 -in cert.p12 -out cert.pem -nodes si prefiere la ruta rustls.
Un valor definido pero vacío para cualquiera de estas variables se trata como no definido.
El certificado de cliente configurado se custodia en el cliente HTTP nativo compartido y
por tanto se presenta a cualquier servidor HTTPS que solicite uno durante una
fetch nativa (hosts de teselas, estilos y OGC a los que apunte un proyecto), no solo al endpoint para el
que fue emitido el certificado. TLS envía un certificado de cliente solo cuando el servidor
lo pide, así que esto no es una divulgación incondicional, pero configure un certificado de
cliente únicamente cuando los hosts con los que habla la app sean de confianza.
Rendimiento: renderizado del mapa en Linux (WebKitGTK)
La app de escritorio usa la WebView del sistema. En Linux es WebKitGTK, cuyo
pipeline WebGL/JavaScript es materialmente más lento que el motor Chromium en el que
corre la build de navegador. El efecto más visible es el desplazamiento del mapa a zoom bajo:
Un mapa en blanco (sin capa de teselas) se desplaza a un estable 60 FPS a cualquier zoom.
Con cualquier capa de teselas (vectorial o ráster XYZ), el FPS colapsa a dígitos
simples mientras se cargan teselas, y luego regresa a 60 una vez que la carga termina,
al mismo zoom. El zoom bajo solo lo vuelve constante porque desplazarse por todo el
mundo carga teselas continuamente y la caché nunca se asienta.
El costo es que WebKitGTK procesa cada tesela recién cargada en el hilo principal: la subida a la
GPU de su textura (ráster) o buffers de vértices (vectorial) mediante llamadas síncronas
WebGL, más los frames subsiguientes de repintado por fundido. El análisis de teselas vectoriales y
la construcción de buckets corren en los Web Workers de MapLibre, así que no son el cuello de botella
aquí. Cada ciclo de renderizado de integración de teselas medía ~125 ms de reloj en
WebKitGTK frente a unos pocos ms en Chromium. La brecha está en la implementación WebGL
de WebKitGTK y en su pipeline de composición (flush del command-stream del driver, composición
de superficie TextureMapper GL), no únicamente en su motor JavaScript JavaScriptCore. Es una
limitación del motor de WebView, no un bug de Meratyn, y no afecta a la
build de navegador ni (sin probar) a las WebView de macOS/Windows.
Descartado durante el diagnóstico (para que investigaciones futuras no lo repitan):
render por software (la GPU está en uso; Intel i915 confirmado), saturación de GPU (el
motor de render se mantiene ~20% ocioso), lectura de archivos por IPC de Tauri (~126 ms para un GeoJSON de 22 MB),
JSON.parse (~36 ms), latencia del compositor KWin, renderWorldCopies,
y la proyección globe vs. mercator. Preservar el drawing buffer de WebGL no era
la causa de esos picos de integración de teselas, pero imponía un costo global de
memoria/tasa GPU. Ya está deshabilitado; impresión y captura de video realizan
un repintado síncrono justo antes de leer el canvas.
Para reproducir la medición: registre temporalmente los eventos render de MapLibre,
los eventos dataloading (tesela) y un contador requestAnimationFrame una vez por
segundo; el FPS sigue inversamente la cantidad de teselas cargadas. La comprobación más
rápida es este medidor de frame-rate pegado en la consola devtools de la WebView, y luego desplazar mientras
observa el FPS registrado:
let f = 0, t = performance.now();(function loop(n) { f++; if (n - t >= 1000) { console.log("FPS", f); f = 0; t = n; } requestAnimationFrame(loop);})(t);
Mitigaciones (reducir cuántas teselas cargan durante un desplazamiento, dado que el costo por tesela está
fijado por el motor) todavía no están implementadas: un maxTileCacheSize mayor,
teselas ráster de 512px en lugar de 256px, y fadeDuration: 0, idealmente condicionadas a
WebKitGTK para que las builds basadas en Chromium conserven fidelidad total.
Estructura del workspace
Rangos del workspace. Todos los paquetes del visor descansan sobre @mappview/core; la consola reutiliza el kit de UI y el cliente del servicio de gestión; el escritorio arranca el sidecar bajo demanda. Edge y docs son independientes.
Visión general del sistema
Cómo se compone la app en ejecución: las sesiones poseen el estado, los renderizadores se suscriben. Versión comentada
Flujo de estado
El bucle de autoría: cada pasada escribe estado duradero en la sesión del proyecto, y el guardado del escritorio lo persiste como .mappview.json.
Motores
Conversión vectorial: DuckDB-WASM Spatial en el navegador; GeoPandas/Shapely en el sidecar o los mismos módulos vector_ops.py en Pyodide — una única fuente de verdad, tres tiempos de ejecución.
SQL espacial: DuckDB, PostGIS (PGlite en el navegador) y Apache Sedona (SedonaDB en el sidecar, CereusDB WASM en el navegador), con alternancia automática sidecar-first.
Renderizado: MapLibre GL JS principal; deck.gl para ráster, nubes de puntos y overlays 3D; globo CesiumJS como modo de vista por panel tras un chunk diferido de ~4,8 MB y un token Ion opcional.
Contratos: Effect v4 gestiona programas, esquemas y errores tipados; TanStack Query el ciclo de vida asíncrono; TanStack Store el estado de ejecución. El artefacto persistido es el proyecto .mappview.json agnóstico del motor (formato 0.2.0).
Selección de motores: los motores del navegador gestionan las operaciones compatibles; los motores con sidecar cubren las capacidades que lo necesitan, con fallback a WASM en el navegador cuando está disponible. Las pruebas de estado y las decisiones de fallback pasan por TanStack Query; cada motor es un programa de Effect. Las operaciones Whitebox y ráster compatibles se ejecutan en el navegador, con rutas opcionales al sidecar para archivos nativos y herramientas fuera de los motores del navegador.
Resumen
Meratyn es una plataforma GIS ligera y cloud-native que se ejecuta en el navegador web, en el escritorio, en dispositivos móviles y dentro de cuadernos Jupyter, todo desde un único monorepo de workspaces Bun. La interfaz es una aplicación React que se distribuye como aplicación de escritorio nativa alojada por Tauri v2 y como aplicación web basada en navegador, adaptándose de forma responsiva a móviles y pantallas pequeñas. El renderizado de mapas usa MapLibre GL JS en la vista web del navegador, con deck.gl para superposiciones avanzadas de ráster, nubes de puntos y 3D, y una vista opcional de globo 3D con CesiumJS (ver Vista de globo 3D (CesiumJS)) ofrecida como panel dividido. @mappview/core posee un runtime de la aplicación y una sesión de proyecto desechable. Cada sesión separa el documento persistido de los datos de runtime, la interacción con el mapa, la interfaz, la persistencia, el procesamiento y las capacidades de colaboración en stores nativos de TanStack. Las transiciones del documento creadas por edición se registran mediante Travels.
Tipos de dominio, esquema JSON del proyecto, runtime de la aplicación, sesiones de proyecto desechables y comandos de documento/capacidades
@mappview/map
Ciclo de vida de MapLibre, sincronización de capas, GeoJSON, ráster, teselas, MBTiles, estilizado de controles y de selección
@mappview/ui
Primitivas de interfaz compartidas (estilo shadcn)
@mappview/processing
Registro de algoritmos del lado del cliente
@mappview/plugins
Interfaz de plugins y plugins integrados
mappview-desktop
Layout del shell, E/S de Tauri, composición
Rangos de dependencias del workspace. @mappview/core concentra el mayor fan-in.
Vista de globo 3D (CesiumJS)
El mapa 2D de MapLibre puede complementarse con un globo 3D renderizado con CesiumJS, ofrecido como panel dividido en lugar de un reemplazo completo del motor de la app. Como el documento del proyecto es independiente del motor —contiene registros MappViewLayer sencillos y un MapViewState, no objetos de MapLibre— un segundo renderizador se conecta suscribiéndose a la misma sesión de proyecto, exactamente como lo hacen los paneles 2D de SecondaryMapCanvas. No existe una capa de abstracción de motores: MapLibre sigue siendo el mapa predeterminado y principal, y Cesium es un modo de vista de primera parte (no un plugin — la API de plugins está tipada para MapLibre).
Activación. Cada panel secundario de la cuadrícula del mapa lleva un conmutador 2D/3D. El globo solo se ofrece cuando hay configurado un token de Cesium Ion —Cesium World Imagery y Terrain requieren uno— así que sin token el conmutador está oculto y un proyecto guardado con panel de globo abre como el mapa 2D. El token se resuelve mediante getCesiumIonToken() (@mappview/core), que lee VITE_CESIUM_TOKEN/CESIUM_TOKEN desde la compilación o desde una sobrescritura en tiempo de ejecución, de modo que puede definirse al compilar (CESIUM_TOKEN; ver Credenciales opcionales del globo 3D) o en tiempo de ejecución sin recompilar. Ajustes → Variables de entorno tiene un campo dedicado enmascarado token de Cesium Ion respaldado por DesktopSettings locales del dispositivo (localStorage, nunca el archivo compartido del proyecto); useRuntimeEnvironmentVariables lo proyecta como VITE_CESIUM_TOKEN sobre el global window.__MAPPVIEW_RUNTIME_ENV__ (los valores vacíos no se proyectan, así que no pueden borrar un token definido al compilar). MapGrid vuelve a resolver el token ante el evento mappview:runtime-env-change, así que el conmutador aparece tan pronto se introduce uno.
Carga diferida. Todo el motor de Cesium (~4.8 MB) se importa con import() solo cuando un panel cambia al globo, se conserva en su propio chunk de compilación (manualChunks) y fuera de la ruta de arranque 2D. Un plugin de Vite (vite-plugins/copy-cesium-assets.ts) deja preparados los Workers/Assets/Widgets de runtime de Cesium en public/cesium/, y el canvas define window.CESIUM_BASE_URL para que el motor los encuentre.
Sincronización de cámara.packages/map/src/globe/cesium/camera.ts convierte entre el MapViewState Web-Mercator de MapLibre (zoom, pitch referido al nadir, bearing) y la cámara de Cesium (rango métrico, pitch referido al horizonte, rumbo), emparejados por resolución sobre el terreno (metros por píxel) para que la escala en pantalla se mantenga en paso incluso cuando los paneles difieren en altura. CesiumCanvas inicializa su cámara desde el mapView compartido, aplica los cambios del store al globo y escribe de vuelta los movimientos propios del globo —bidireccional, como los paneles 2D— con una comprobación de tolerancia que suprime el eco aplicar→moveEnd, de modo que no hay bucle de vibración.
Sincronización de capas.CesiumLayerSync (packages/map/src/globe/cesium/layer-sync.ts) reconcilia el arreglo MappViewLayer[] del store sobre el globo igual que lo hace MapController.syncLayers para MapLibre, reutilizando las mismas sobrescrituras de visibilidad por panel y efectos de grupo que SecondaryMapCanvas. Renderiza los tipos donde Cesium es encaje natural: GeoJSON (un GeoJsonDataSource draped estilizado a partir del fill/stroke/opacity de la capa), XYZ/raster/WMTS y WMS (como ImageryLayers) y 3D Tiles (una primitiva Cesium3DTileset que consume directamente la URL del tileset de la capa, sus cabeceras de petición y su desfase de altitud), todo con visibilidad/opacidad en vivo, reconstrucción al cambiar la fuente y eliminación. Los demás tipos de capa (PMTiles, MBTiles, Zarr, LiDAR, splats, visualizaciones de deck.gl, …) se omiten en el globo y siguen renderizándose en los paneles 2D; el predicado exportado isCesiumSupportedLayerType permite al menú de capas del panel marcarlos “solo 2D”. COG/imágenes derivadas de ráster es candidato para una pasada posterior.
Persistencia. El viewKind de un panel forma parte de SecondaryMapView y viaja de ida y vuelta a través del formato de proyecto .meratyn.json (normalizeSecondaryMapViews), de modo que un proyecto guardado con un panel de globo vuelve a abrirse como globo —siempre que haya un token disponible; si no, recurre al mapa 2D.
DuckDB-WASM
La importación de archivos vectoriales usa DuckDB-WASM para los formatos que requieren conversión antes de que MapLibre pueda renderizarlos:
INSTALL spatial;LOAD spatial;
GeoParquet se lee con el lector Parquet de DuckDB después de cargar Spatial. Los demás formatos vectoriales locales se pasan al ST_Read de Spatial cuando la extensión WebAssembly puede cargar el lector basado en GDAL. Los Shapefiles comprimidos se analizan primero con shpjs; si ese parser no puede leer el archivo, se intenta con DuckDB Spatial. Los archivos KML (y el KML dentro de archivos KMZ descomprimidos) los lee un parser propio que preserva la simbología incrustada, emitiendo propiedades de simplestyle-spec (fill, stroke, stroke-width, etc.) para que un KML estilizado se renderice tal como lo hace en Google Earth; el KML que el parser no puede manejar recurre al lector de DuckDB Spatial, que carga la geometría sin el estilizado. Una capa vectorial cuyas features llevan propiedades simplestyle se renderiza por feature: las expresiones de pintura leen el color/ancho por feature y recurren al estilo plano de la capa.
Enrutamiento de importación vectorial: cada camino termina en GeoJSON entregado a un comando createGeoJsonLayer.
Flujos de trabajo avanzados de Añadir datos
La superficie de Añadir datos de la v1.0 incluye diálogos nativos para XYZ, WMS, WFS, archivos vectoriales (vía el diálogo Añadir vector respaldado por maplibre-gl-vector), URLs de GeoJSON, fuentes de teselas vectoriales, texto delimitado, plantillas de teselas ráster, rásteres COG y GeoTIFF (vía el diálogo Añadir ráster respaldado por maplibre-gl-raster), MBTiles, capas de ArcGIS FeatureServer o VectorTileServer, y waypoints, tracks y rutas GPX. También admite capas 3D Tiles, refresco de WFS y URLs de GeoJSON, etiquetas de marcador de texto y múltiples capas de resultados de consultas SQL de DuckDB con soporte de identify, selección y tabla de atributos. El plugin Components envuelve paneles de maplibre-gl-components para FlatGeobuf, PMTiles, Zarr, LiDAR y splats gaussianos, y luego refleja las capas añadidas en el store de Meratyn para que el panel Capas, el formato de proyecto y el control de capas puedan razonar sobre ellas. Hay fuentes de datos adicionales disponibles en los paneles Planetary Computer y Earth Engine, los plugins Overture Maps y Planet Open Data, y los adaptadores Web Services (ArcGIS Hub, Socrata, CKAN y Natural Earth). El plugin Time Slider, respaldado por maplibre-gl-time-slider, anima series temporales de datos ráster y vectoriales (COG, XYZ/WMTS, WMS-Time y GeoJSON filtrado por tiempo) a través de una línea de tiempo acoplada, reflejando cada fuente que añade en el store de Meratyn como capa nativa externa.
Las teselas MBTiles locales se leen mediante un protocolo personalizado de MapLibre respaldado por comandos de Tauri. Los rásteres remotos se obtienen a través del backend de escritorio cuando es necesario, y el servidor local de desarrollo incluye un proxy de ráster para ciertos assets de release seleccionados que necesitan manejo de CORS.
Sidecar de Python
La app FastAPI en services/server proporciona rutas opcionales de sidecar para Whitebox, la conversión de formatos y las operaciones ráster que necesitan archivos nativos o superan los motores del navegador. La app de escritorio inicia el sidecar bajo demanda, se comunica por 127.0.0.1 y mantiene la pila pesada de procesamiento Python fuera del bundle del navegador.
Las herramientas Vector (Procesamiento → Vector) se ejecutan en el cliente con Turf.js y no necesitan sidecar. Todas las herramientas pueden ejecutarse opcionalmente sobre los endpoints /vector del sidecar, respaldados por GeoPandas y Shapely, para obtener resultados conscientes de la proyección; el sidecar informa su disponibilidad mediante /vector/status, y el diálogo recurre al motor cliente cuando el extra opcional vector no está instalado.
Un tercer motor Vector, Python (Pyodide), ejecuta el mismo código GeoPandas/Shapely en el navegador vía Pyodide (CPython compilado a WebAssembly), así que la ruta GeoPandas está disponible en la build web sin servidor. El propietario público libre de frameworks, services/server/src/sidecar/vector_ops.py, compone implementaciones cohesivas bajo _vector/; el sidecar y el navegador ejecutan ese mismo paquete. Un plugin de Vite (vite-plugins/copy-vector-engine.ts) copia solo esos módulos canónicos de Python al bundle de la app, y un Web Worker clásico (public/pyodide/pyodide-worker.js) instala el paquete en Pyodide antes de llamar a run_vector_tool a través de un límite de cadena JSON. Una única fuente de verdad significa que los motores Sidecar y Pyodide devuelven resultados idénticos. El runtime de Pyodide se descarga de forma diferida en el primer uso; la variable de entorno VITE_PYODIDE_INDEX_URL lo apunta a un espejo autoalojado para despliegues sin conexión/de producción.
El SQL Workspace ofrece un motor Apache Sedona junto a DuckDB y PostGIS. Ejecuta SQL espacial de Sedona sobre SedonaDB, el motor Rust de nodo único (DataFusion + Arrow), a través de los endpoints /sql del sidecar (/sql/status, /sql/run) respaldados por el extra opcional sedona (apache-sedona[db]). En el navegador —y en escritorio cuando el extra no está instalado— el mismo motor corre íntegramente en el cliente sobre CereusDB, una build WebAssembly de SedonaDB; el workspace prefiere el sidecar cuando está accesible y recurre automáticamente a CereusDB. El bundle de CereusDB es grande, así que se importa dinámicamente a su propio chunk y solo se descarga cuando corre la primera consulta Sedona.
El límite del sidecar ya está establecido para los flujos actuales de conversión, ráster, vector, SQL y SamGeo; el trabajo futuro de procesamiento puede ampliarlo a motores adicionales.
Soporte sin conexión (PWA)
La build web independiente es una Progressive Web App instalable. vite-plugin-pwa (configurado en apps/mappview/vite.config.ts) genera un manifiesto web además de un service worker de Workbox, y src/main.tsx lo registra junto a installStaleChunkReload para que ambos se coordinen. El service worker solo se compila para la build web; está deshabilitado para la build de escritorio Tauri (que ya está sin conexión gracias a los assets empaquetados) y para la wheel Jupyter incrustada (MAPPVIEW_EMBED=1), donde registerSW resuelve a una no-op.
El almacenamiento en caché se divide para mantener ligera la primera visita:
Precaché (shell de la app). El HTML y los chunks JS/CSS que arrancan el mapa se precachean, así que el shell carga sin red después de la primera visita. Los chunks pesados indicados abajo se excluyen de la precaché para evitar una gran descarga en la primera carga.
Caché en runtime, CacheFirst. Los assets de compilación con hash de contenido que la precaché omite (todo bajo /assets/) se cachean en el primer uso: el bundle de MapLibre, DuckDB-WASM y su extensión espacial, y los chunks de plugins-feature de MapLibre. Los nombres de archivo con hash hacen seguro CacheFirst: un redeploy acuña URLs nuevas, así que nunca se sirve como actual una entrada obsoleta. Esto es lo que hace que los flujos de trabajo con archivos locales (conversión con DuckDB Spatial) funcionen sin conexión después de haber corrido una vez en línea. Autoalojar la extensión espacial mediante VITE_DUCKDB_SPATIAL_EXTENSION_PATH la mantiene same-origin, de modo que también se cachea. PGlite/PostGISno está en esta lista: se obtiene del CDN jsDelivr (cross-origin) para sacarlo de la compilación —~25 MB crudos, y ~22 MB de peso casi incompresible en el binario Tauri— así que el motor SQL PostGIS necesita red en el primer uso (ver la nota sobre Pyodide más abajo).
Mapas base. Las teselas y estilos de los hosts predeterminados amigables con CORS (OpenFreeMap, CARTO) se cachean en runtime. Otras teselas remotas, servicios y fuentes ArcGIS/WMS/WFS permanecen solo-red por diseño y no están disponibles sin conexión.
El soporte sin conexión es estratificado: shell precacheado, chunks con hash en CacheFirst, teselas de mapas base cacheadas en runtime, servicios solo-red.
El motor vectorial Pyodide y el motor SQL PGlite/PostGISno admiten funcionamiento sin conexión en la configuración predeterminada: ambos se cargan desde el CDN jsDelivr (cross-origin), que el service worker no cachea. Apunte VITE_PYODIDE_INDEX_URL a un espejo same-origin para que Pyodide sea cacheable sin conexión; compile con MAPPVIEW_PGLITE_CDN=0 para reintegrar PGlite/PostGIS a la compilación (esto vuelve a añadir ~22 MB al binario Tauri). Esta dependencia del CDN aplica también a la build de escritorio —esta carga ambos desde jsDelivr.
Un nuevo despliegue se recoge mediante registerType: "prompt": el nuevo service worker se instala en segundo plano y queda en espera, así que una pestaña abierta nunca mezcla su grafo de módulos con un despliegue nuevo. La app busca un worker nuevo cada hora y cuando la pestaña vuelve a ser visible, porque el navegador solo lo hace al navegar. Un despliegue en espera añade la entrada “Actualización disponible” a la barra de estado (features/app-update/actions.tsx), para que el aviso nunca cubra el mapa; al elegirla se pregunta por los cambios sin guardar, se envía SKIP_WAITING, se espera a que el nuevo worker se active (clientsClaim) y se recarga. Toda recarga tras un fallo usa el mismo paso de activación (platform/runtime/app-update.ts): la pantalla de error general, el aviso de sección “actualización requerida”, la pantalla de fallo de arranque previa a React e installStaleChunkReload, que recarga un proyecto sin cambios cuando un chunk diferido huérfano da 404 (con guarda de cooldown; los proyectos con cambios conservan el aviso). Una recarga simple mantendría el worker en espera y volvería a servir el shell anterior. El viewer host conserva cada versión reemplazada en los últimos siete días (al menos tres y como máximo veinte), para que las pestañas que no se han actualizado sigan cargando sus secciones diferidas.