Skip to content

fix(vector): stream large local vector files through DuckDB - #1716

Merged
giswqs merged 9 commits into
mainfrom
fix/large-local-vector-preflight
Aug 5, 2026
Merged

fix(vector): stream large local vector files through DuckDB#1716
giswqs merged 9 commits into
mainfrom
fix/large-local-vector-preflight

Conversation

@giswqs

@giswqs giswqs commented Aug 5, 2026

Copy link
Copy Markdown
Member

Problem

A user reported that a zipped shapefile of 18,000 polygons (148 MB) and the same data as GeoJSON (539 MB) both freeze the app, while smaller files in the same formats load fine. There was no size guard of any kind on local vector loads.

  • GeoJSON: readLocalFileText materializes the whole file as one JS string, then JSON.parse runs synchronously. V8 caps a single string at 536,870,888 bytes, so a large file throws RangeError: Invalid string length before parsing starts.
  • Zipped shapefile: shpjs's parseShp is fully synchronous and applies the .prj proj4 transform per coordinate. Measured on a real 197 MB / 170k-polygon shapefile: an 8.9 second main-thread freeze with a projected .prj.
  • Neither hit the existing guard. DUCKDB_VECTOR_FEATURE_WARN_COUNT is a feature count checked after DuckDB has opened the source; the shpjs and JSON.parse paths bypassed it entirely.

What this does

One threshold, no prompt. DUCKDB_VECTOR_ROUTE_BYTES (100 MB): at or above it a local vector file skips the in-memory JavaScript readers and streams through DuckDB instead — off the main thread, decided from filesystem metadata before a byte is read. For a zipped shapefile it is measured on the uncompressed .shp, since shapefiles compress heavily. DUCKDB_VECTOR_FEATURE_WARN_COUNT drops to 100,000 and remains a prompt, because it is about materializing features in memory, which file size does not predict for compressed formats. Both live in @geolibre/core so the Add Vector Layer panel (which configures maplibre-gl-vector's autoThreshold) switches at the same numbers as drag-and-drop.

Two formats are deliberately excluded from routing: delimited text, because loadDuckDbVectorFile cannot build points from lon/lat columns, and GPX, because ST_Read without a layer argument returns only the first OGR layer and would discard tracks and routes.

Warn on a mislabelled CRS. detectNonGeographicCoordinates samples a collection and reports coordinates outside ±180 / ±90 — a file declaring CRS84 while holding projected easting/northing. GeoLibre honours the declared CRS, so such a layer previously loaded cleanly, appeared in the Layers panel, and rendered nowhere with no error at all.

Stop autosave crashing. serializeProject runs synchronously outside the promise chain in useProjectHistory, so a project embedding a large vector layer threw an unhandled RangeError on every autosave tick.

Bump maplibre-gl-vector to 0.10.8, picking up opengeos/maplibre-gl-vector#59: large GeoPackage layers no longer fail with Invalid string length, and layers whose spatial-reference row is not an EPSG row now reproject from their stored WKT.

Verification

Measured in the built web app with a real 170k-polygon wetlands dataset, tracking main-thread stalls via a requestAnimationFrame ticker:

Case before after
Zip, 197 MB .shp, projected .prj 10.3s / 8886ms stall 13.3s / 1625ms stall
Zip, 197 MB .shp, WGS84 .prj 6.1s / 4673ms stall 10.3s / 1946ms stall
GeoJSON 835 MB 31.2s 29.4s, routed to DuckDB
GeoJSON 167 MB (under threshold) 1.8s unchanged

Routing trades total time for responsiveness rather than being a pure win: it costs 3-4 seconds to buy a 2.4-5.5x shorter freeze. A nine-second freeze reads as a crash; 13 seconds of a responsive UI reads as slow.

  • npm run test:frontend - 5236 pass
  • npm run typecheck, npm run test:e2e (29 pass), and pre-commit clean
  • CRS warning verified against genuinely mislabelled data, and silent on the reprojected copy
  • Custom-SRS GeoPackage verified against the published 0.10.8

Not addressed

The FeatureCollection still lands fully in memory once loaded, and MapLibre structured-clones it again to its own worker. The ~1.6-1.9s residual stall in every "after" row is that work. A 1.4 GB / 430k-feature GeoPackage now completes ingest and reprojection but still exhausts the renderer during tile generation.

A local vector file had no size guard of any kind. Two cases froze the app
with no actionable error:

- A GeoJSON at or above V8's maximum string length (~537 MB) cannot be read
  as text at all — `readTextFile`/`File.text()` throw `RangeError: Invalid
  string length` before `JSON.parse` runs. The loaders' `catch` swallowed it
  and silently re-read the whole file through DuckDB, so the user waited
  through the failed attempt first.
- A large zipped shapefile went to shpjs, whose `parseShp` is fully
  synchronous and applies the `.prj` proj4 transform per coordinate, wedging
  the main thread with no progress, no cancel, and no feature-count guard
  (that guard only runs once DuckDB has already opened the source).

Add a byte-size preflight that runs on filesystem metadata (`stat`) or
`File.size`, before any read:

- `LARGE_VECTOR_SIZE_WARN_BYTES` (200 MB) prompts via the new `onLargeFile`
  option, wired alongside `onLargeDataset` at every interactive entry point
  (drag-drop paths and files, the desktop picker, KML import).
- `MAX_TEXT_VECTOR_BYTES` diverts an oversized GeoJSON/KML/GPX/CSV straight
  to the DuckDB reader instead of attempting the doomed text parse.
- `MAX_SHPJS_SHP_BYTES` (64 MB, measured on the uncompressed `.shp`) sends a
  large shapefile to DuckDB, which reads off the main thread and reports a
  feature count first, so the existing large-dataset guard can fire.

Also document the practical ceilings and the convert-first workaround in the
Adding Data guide.
Copilot AI lite review requested due to automatic review settings August 5, 2026 17:05
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Large vector loading now routes files at least 100 MB to DuckDB across browser and Tauri readers. Imports detect sampled non-geographic coordinates and show localized warnings. Autosave handles serialization failures without unhandled errors. Shared thresholds, documentation, and tests were updated.

Changes

Vector loading diagnostics

Layer / File(s) Summary
Shared vector guard contracts
packages/core/src/types.ts, apps/geolibre-desktop/src/lib/duckdb-vector-guard.ts, packages/plugins/src/plugins/maplibre-vector.ts, tests/duckdb-vector-guard.test.ts
Defines shared DuckDB thresholds and coordinate diagnostics. Adds size routing and tests threshold, sampling, nesting, and invalid-coordinate behavior.
Browser and Tauri loading paths
apps/geolibre-desktop/src/lib/tauri-io.ts
Checks file sizes before parsing. Routes large files to DuckDB and uses uncompressed .shp size for zipped Shapefiles.
CRS diagnostics and import warnings
apps/geolibre-desktop/src/components/layout/DesktopShell.tsx, apps/geolibre-desktop/src/i18n/locales/*.json
Detects non-geographic coordinates during imports and displays a dismissible warning with affected layer names in supported locales.
Persistence resilience and loading guidance
apps/geolibre-desktop/src/hooks/useProjectHistory.ts, docs/user-guide/adding-data.md
Skips failed synchronous autosave serialization and documents large-vector routing and format guidance.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DesktopShell
  participant VectorLoader
  participant DuckDB
  participant CoreDiagnostics
  DesktopShell->>VectorLoader: import vector layer
  VectorLoader->>VectorLoader: check file size
  VectorLoader->>DuckDB: load oversized vector
  VectorLoader-->>DesktopShell: return imported data
  DesktopShell->>CoreDiagnostics: inspect sampled coordinates
  CoreDiagnostics-->>DesktopShell: return CRS diagnostic
  DesktopShell-->>DesktopShell: display localized warning
Loading

Possibly related PRs

  • opengeos/GeoLibre#1599: Both PRs modify KML import handling, but this PR adds CRS anomaly warnings while that PR adds Super-Overlay support.
  • opengeos/GeoLibre#1602: Both PRs modify useProjectHistory.ts; this PR hardens autosave serialization.

Suggested reviewers: craun718, harshshinde0

Poem

A rabbit guards each vector file,
Large ones stream in DuckDB style.
Strange coordinates raise a sign,
Autosave skips a broken line.
Local words make warnings clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: routing large local vector files through DuckDB.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/large-local-vector-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://6a670ee4.geolibre-preview.pages.dev
Demo app https://6a670ee4.geolibre-preview.pages.dev/demo/
Commit bd1a0b3

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@apps/geolibre-desktop/src/components/layout/DesktopShell.tsx`:
- Around line 203-222: Update the addFilePath flow’s loadDroppedVectorPaths
options to use the existing largeVectorGuards object, ensuring both
onLargeDataset and onLargeFile callbacks run for Browser Files tree additions.
Preserve the existing path-loading behavior and avoid duplicating the guard
configuration.

In `@docs/user-guide/adding-data.md`:
- Around line 28-32: Update the “Processing → Conversion” guidance in the
adding-data documentation to identify PMTiles as the format that streams map
tiles incrementally, while describing GeoParquet as a compact columnar vector
format rather than a tiled format. Preserve the existing source-format
recommendations and GeoJSON memory guidance.
🪄 Autofix

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: d0f801e3-e429-4fce-b672-cfa9fb4c5b99

📥 Commits

Reviewing files that changed from the base of the PR and between f35c7a2 and dac8c5d.

📒 Files selected for processing (22)
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/duckdb-vector-guard.ts
  • apps/geolibre-desktop/src/lib/tauri-io.ts
  • docs/user-guide/adding-data.md
  • tests/duckdb-vector-guard.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Outdated
Comment thread docs/user-guide/adding-data.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Some interactive drop/import paths still read GPX/KML/KMZ contents before the new preflight guard runs, which can still trigger the same large-text/memory failure modes this PR aims to prevent.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Adds a byte-size preflight guard to local vector imports so oversized files can be confirmed (or diverted away from text parsing / main-thread shpjs) before they’re read into memory, preventing silent freezes and double-work fallbacks in the vector loading pipeline.

Changes:

  • Introduces large-vector file size thresholds + guard helpers (confirmLargeVectorFile, exceedsTextVectorLimit, MAX_SHPJS_SHP_BYTES, etc.) alongside the existing feature-count guard.
  • Wires size/text-limit preflight into Tauri + browser vector loaders and routes large shapefile parses to DuckDB to avoid main-thread shpjs reprojection stalls.
  • Adds user-facing documentation + localized UI string describing large-vector behavior, plus unit tests covering the new thresholds/guards.
File summaries
File Description
tests/duckdb-vector-guard.test.ts Adds unit tests for the new size guard + text-limit diversion and shapefile threshold ordering.
docs/user-guide/adding-data.md Documents practical large-vector ceilings and the DuckDB/convert-first guidance.
apps/geolibre-desktop/src/lib/tauri-io.ts Integrates size/stat preflight, text-limit diversion, and large-shapefile-to-DuckDB routing into vector loaders.
apps/geolibre-desktop/src/lib/duckdb-vector-guard.ts Defines new thresholds/types and guard helpers for large local vector files and text-size limits.
apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Bundles and passes large-vector guards through interactive vector-load entry points.
apps/geolibre-desktop/src/i18n/locales/ar.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/de.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/en.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/es.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/fr.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/hi.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/id.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/it.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/ja.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/ka.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/ko.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/nl.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/pt.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/ru.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/th.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/tr.json Adds toolbar.item.largeVectorFileDesc translation.
apps/geolibre-desktop/src/i18n/locales/zh.json Adds toolbar.item.largeVectorFileDesc translation.
Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts
Comment thread apps/geolibre-desktop/src/lib/duckdb-vector-guard.ts Outdated
Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx (KML-import handler, ~line 1397-1403) and tauri-io.ts's loadDroppedVectorFiles/loadDroppedVectorPaths (extension === "kml" branches, ~line 2841 and ~3119): the new onLargeFile guard is wired into these call sites, but the KML branches read the entire file as text (file.text() / readLocalFileText(path)) to extract ground overlays/models before calling the guarded loadBrowserVectorFile/loadTauriVectorFile. A large dropped/imported .kml therefore skips the new size-confirmation prompt (200 MB–537 MB), and at ≥537 MB hits the very RangeError: Invalid string length this PR is designed to prevent — uncaught, since that read isn't wrapped in a try/catch. This directly undercuts the PR's claim of covering "every interactive entry point ... KML import." Confidence: medium-high (posted inline).
  • DesktopShell.tsx's addFilePath (Browser panel's Files-tree click-to-add, ~line 1449-1480, not touched by this diff): still constructs its own options object with only onLargeDataset, not the new onLargeFile. Clicking a 200–537 MB file there skips the new size-confirmation prompt entirely (the unconditional exceedsTextVectorLimit diversion still prevents an outright crash for ≥537 MB files, so this is a coverage gap rather than a crash). Not inline-commentable since the line isn't part of the diff. Confidence: medium.

Security

  • None found. Paths still route through existing traversal/scope checks; no new injection or secret-handling surface introduced.

Performance

  • No new problems; the change is squarely aimed at avoiding the main-thread freezes it describes, and the extra stat/size read per load is a single cheap IPC round-trip.

Quality

  • MAX_TEXT_VECTOR_BYTES (duckdb-vector-guard.ts:34) is derived from V8's MAX_STRING_LENGTH, which is accurate for the web build and Windows desktop (WebView2/Chromium) but Tauri v2 on macOS/Linux runs WebKit's JavaScriptCore, not V8 — its actual string-length ceiling may differ, and if it's lower than 536,870,888 on some platform/version, the same RangeError this PR fixes could still slip through there. Confidence: medium (posted inline).
  • New locale strings (toolbar.item.largeVectorFileDesc) are present in all 17 locale files, with Arabic correctly using the mirrored for RTL — good adherence to the i18n conventions in CLAUDE.md.

CLAUDE.md

  • No violations found — t() is used for new UI strings, RTL logical direction is respected in the translated copy, and docs were updated alongside the code change.

Verified against a real 197 MB / 170k-polygon wetlands shapefile and an
873 MB GeoJSON in the browser. Two claims in the original comments did not
survive measurement:

- The text-limit diversion saves ~2s, not a full wasted read: File.text()
  rejects on the known size rather than after reading the file. The value is
  the explicit route and the log line, not the time saved.
- The shpjs bypass trades total time for responsiveness rather than being a
  pure win: worst main-thread stall 8.9s -> 1.6s (projected .prj) and
  4.7s -> 1.9s (WGS84 .prj), while overall load time rises by 3-4s.

Record both numbers next to the constants so the trade-off is not
rediscovered later.
@giswqs

giswqs commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Verified against real data

Tested with the CT_Wetlands dataset (170,964 polygons; 197 MB .shp, 103 MB .dbf, 167 MB GeoJSON, 237 MB GeoPackage), driving the built web app in Chromium via Playwright — files injected through a real DataTransfer drop on the shell, so this exercises loadBrowserVectorFile / loadShapefileZip end to end, not mocks.

Alongside outcome and wall-clock, each run tracks a requestAnimationFrame ticker; the gap between frames is the main-thread stall — the number that corresponds to "it freezes".

Case main this PR
GeoJSON 167 MB (control, under all thresholds) 1.8s / 1645ms stall unchanged, no prompt
Zip → 197 MB .shp, WGS84 .prj 6.1s / 4673ms stall 10.3s / 1946ms stall
Zip → 197 MB .shp, projected .prj 10.3s / 8886ms stall 13.3s / 1625ms stall
GeoPackage 237 MB 3.2s / 3010ms 3.3s / 3058ms + size prompt
GeoJSON 873 MB 31.2s / 1188ms 29.4s / 1125ms + prompt, diverted
GeoPackage 237 MB, prompt declined n/a skipped cleanly, 38ms max stall

Both console breadcrumbs fire on the real files:

[GeoLibre] "CT_Wetlands.shp" is 197 MB uncompressed; reading it with DuckDB instead of shpjs to keep the parse off the main thread.
[GeoLibre] "CT_Wetlands_huge.geojson" exceeds the maximum JavaScript string length; reading it with DuckDB instead of the text parser.

Two corrections to the description above

Measurement contradicted two things I claimed when opening this, and the code comments have been updated to match (d539c84):

The shpjs bypass is a trade-off, not a pure win. It costs 3–4s of total load time and buys a 2.4–5.5× shorter freeze. The projected-CRS row is the case that justifies it: a 8.9-second hard freeze is indistinguishable from a crash, while 13.3s of a responsive UI is merely slow. But anyone tuning MAX_SHPJS_SHP_BYTES should know they are spending wall-clock to buy responsiveness.

The GeoJSON "double load" was overstated. I wrote that the user "waits through the failed attempt first" — measured, that wasted attempt costs ~2s, because File.text() rejects on the file's known size rather than after reading it. The diversion is still correct, but its value is the explicit route and the log line, not saved time. (The Tauri readTextFile-over-IPC path is not measured here and may differ.)

What this does not establish

The 873 MB GeoJSON loads in ~30s on main too, so I could not reproduce a hard freeze on the GeoJSON path on this machine (62 GB RAM). The reporter's 539 MB freeze may be memory pressure on smaller hardware rather than the string-length ceiling, or may be specific to the desktop build. The shapefile freeze reproduces clearly and is fixed; the GeoJSON side gets a guard and a prompt, but the root cause there is not confirmed.

Also unchanged: the FeatureCollection still lands fully in memory, and MapLibre structured-clones it again to its own worker. The stall floor of ~1.6–1.9s across every "after" row is that residual work, and it is what off-thread ingestion would need to address.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site Deploy failed. See the job log.
Demo app Unavailable
Commit ccf9ec3

Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • The new file-size preflight (onLargeFile / exceedsTextVectorLimit) is never invoked for GPX, KML, or KMZ files dropped onto the app or imported via the KML-import handler. largeVectorGuards is correctly threaded through to loadDroppedVectorPaths/loadDroppedVectorFiles at all three call sites in DesktopShell.tsx, but those functions' gpx/kml/kmz branches (tauri-io.ts ~2822-2867, ~3094-3144) call readLocalFileText/file.text()/loadKmzLayers directly, bypassing confirmLargeVectorFile entirely. A large GPX/KML dropped this way still hits the uncaught RangeError: Invalid string length (or gets no size prompt at all below that ceiling) that this PR set out to fix — this affects exactly the "KML import" and "file drops" entry points the PR description claims are covered. Only the single-file "Add Vector Layer" picker (which routes through loadBrowserVectorFile/loadTauriVectorFile) actually gets the new protection. Confidence: high — posted as an inline comment with the traced call sites.

Security

  • Nothing found. Path-traversal handling and Tauri filesystem-scope boundaries are unchanged by this PR.

Performance

  • No new inefficiencies; the byte-size preflight is cheap (stat/File.size) and runs before any expensive read, matching the PR's own goal.

Quality

  • No issues beyond the bug above. The new duckdb-vector-guard.ts functions are well-documented and the accompanying unit tests (tests/duckdb-vector-guard.test.ts) thoroughly cover the pure guard logic — they just don't cover the loadDroppedVectorFiles/loadDroppedVectorPaths integration, which is why the gap above wasn't caught.

CLAUDE.md

  • No violations noticed: i18n strings were added to all 17 locale files with matching placeholders, no node_modules edits, no relevant catalog/mirror-constant drift for this change.

Replace the size-confirmation prompt with automatic routing, and lower both
thresholds to the configured defaults:

- DUCKDB_VECTOR_ROUTE_BYTES (100 MB): at or above this, a local vector file
  skips the in-memory JavaScript readers (JSON.parse for GeoJSON, shpjs for
  shapefiles, the KML/GPX/CSV text readers) and streams through DuckDB
  instead — off the main thread, no prompt, just a console note. For a zipped
  shapefile the threshold is measured on the uncompressed .shp, since
  shapefiles compress heavily.
- DUCKDB_VECTOR_FEATURE_WARN_COUNT: 500,000 -> 100,000. This stays a prompt,
  because it is about materializing features in memory, which file size does
  not predict for compressed formats.

One threshold now covers every format, replacing the separate 64 MB shapefile
rule. MAX_TEXT_VECTOR_BYTES is gone: routing at 100 MB is far below V8's
536,870,888-byte string cap, so the RangeError it guarded is unreachable.

Removes onLargeFile, confirmLargeVectorFile, LargeVectorFile, and the
largeVectorFileDesc string from all 17 locales.
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts
Comment thread apps/geolibre-desktop/src/lib/duckdb-vector-guard.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Now finalizing the review summary.

Code review

Bugs

  • High confidence: loadTauriVectorFile and loadBrowserVectorFile (apps/geolibre-desktop/src/lib/tauri-io.ts:2140 and :1860) gate the JS delimited-text parser behind !streamViaDuckDb, so a CSV/TSV ≥100 MB with plain longitude/latitude columns (no WKT) now skips parseDelimitedTextFile and falls through to the generic loadDuckDbVector/ST_Read path, which has no lon/lat auto-detection (that logic only exists in the separate convertDuckDbVectorToGeoParquet conversion path). loadDuckDbVectorFile will throw "DuckDB did not find a geometry column in this file." for such files, a regression for large lon/lat CSVs that loaded fine before this change.

Performance

  • No new issues found; the byte-size preflight is a clear improvement (avoids the JSON.parse/shpjs main-thread freeze on oversized files) for the formats it actually covers.

Quality

  • Low confidence, informational: DUCKDB_VECTOR_FEATURE_WARN_COUNT was cut 5x (500,000 → 100,000, duckdb-vector-guard.ts:19), a broader behavior change than the stated bug (the guard never firing) strictly requires — worth confirming it's intentional rather than a drive-by change, since it affects every DuckDB vector load.
  • Low confidence, cosmetic: the console.info "streaming through DuckDB" log in both loader functions is computed from the zip/kmz container's own size, but the actual shapefile routing decision (in loadShapefileZip) uses the uncompressed .shp size — the log can be misleading right around the 100 MB boundary even though it doesn't affect actual behavior for those extensions (tauri-io.ts:2078, :1809).
  • Notable but not code-level: the PR description references symbols/behavior (onLargeFile, largeVectorGuards, MAX_TEXT_VECTOR_BYTES, LARGE_VECTOR_SIZE_WARN_BYTES, MAX_SHPJS_SHP_BYTES, new locale strings in 17 files, "17 new tests") that don't appear anywhere in this diff or the repo — the actual implementation uses a single DUCKDB_VECTOR_ROUTE_BYTES/shouldRouteToDuckDb mechanism with 5 new tests, and no locale files or DesktopShell.tsx wiring changes beyond a cosmetic dependency-array cleanup. This looks like the description is stale relative to what actually shipped; reviewers relying on the PR body to understand the change would be misled.

Security

  • None found. No new user input handling, injection surface, or secret exposure introduced.

CLAUDE.md adherence

  • No violations found: no locale/catalog changes needed here (none were added), no Whitebox/PMTiles/plugin constant mirrors touched, and the change doesn't touch dependency-bumped generated files.

I verified stat() usage is already proven safe in this codebase (pre-existing use in DesktopShell.tsx:1559), so no Tauri capability/permission gap — that was a dead end I ruled out during review.

…d CRS,

and stop autosave crashing on large projects

Three fixes to the large-vector handling, all found while testing a real
170k-polygon wetlands dataset.

1. Unify the Add Vector Layer threshold. The panel is backed by
   maplibre-gl-vector, whose autoThreshold defaulted to 25 MB / 50k features,
   while the drag-and-drop loaders switched to DuckDB at 100 MB / 100k. The
   same file therefore behaved differently depending on how it was added, and
   no single limit could be documented. Both thresholds now live in
   @geolibre/core and are passed to the control, so the two entry points agree.

2. Warn when a layer's coordinates cannot be WGS84.
   detectNonGeographicCoordinates samples a collection and reports coordinates
   outside +/-180 / +/-90 — the case where a file declares CRS84 or
   GCS_WGS_1984 but holds projected easting/northing. GeoLibre honours the
   declared CRS, so such a layer loads cleanly, lists in the Layers panel, and
   renders nowhere with no error at all. It now says so, in its own toast:
   the drop handler sets a success message after the layers are added, which
   would clobber a shared one, and both messages are true at once.

3. Stop autosave crashing. serializeProject runs synchronously outside the
   promise chain in useProjectHistory, so a project embedding a large vector
   layer threw an unhandled `RangeError: Invalid string length` (V8's
   536,870,888-byte string cap) on every autosave tick. Autosave is
   best-effort and must degrade to "no crash recovery", not to a crash.

Verified against the mislabelled CT_Wetlands data: the warning fires with the
real out-of-range values and stays quiet on the reprojected copy.
Comment thread packages/core/src/types.ts
Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found with high confidence. The size-preflight routing (shouldRouteToDuckDb), the shapefile-size routing in loadShapefileZip, and the useProjectHistory synchronous-RangeError fix in the autosave path are all logically sound and match their unit tests. Confidence: high.
  • detectNonGeographicCoordinates (packages/core/src/types.ts) silently skips GeometryCollection features — their coordinates live under geometry.geometries[].coordinates, not geometry.coordinates, so such a feature is never sampled and a mislabelled CRS in a GeometryCollection-only file won't be caught. Low severity since this is a best-effort warning. Confidence: medium. (inline comment posted)

Security

  • None found. No unsafe input handling, injection, or secret exposure in the changed code; the new crsWarning string is rendered as plain JSX text (React-escaped) and interpolated via i18next, not dangerouslySetInnerHTML.

Performance

  • None found. detectNonGeographicCoordinates caps sampling at 1000 coordinates by default, so the per-file cost during import is negligible. The byte-size preflight (stat/File.size) avoids reading oversized files into memory before deciding a strategy, which is the core goal of this change.

Quality

  • crsWarning (DesktopShell.tsx) is set only when addImportedVectorLayers runs and is otherwise only cleared by manual dismissal — unlike dropMessage/dropError, which reset at the start of every drop and auto-clear after 4s. A warning from one drop can linger on screen through a later, unrelated drop (raster/photo/project) that never touches this state. Confidence: low-medium. (inline comment posted)
  • The new CRS-warning UI wiring in DesktopShell.tsx (state, message construction, dismiss button) has no dedicated unit/e2e coverage — only the underlying detectNonGeographicCoordinates function is unit-tested in tests/duckdb-vector-guard.test.ts. Confidence: medium.
  • Minor: detectNonGeographicCoordinates is imported from @geolibre/core as a separate import statement in DesktopShell.tsx even though useAppStore/GeoLibreLayer are already imported from the same module at the top of the file — could be merged. Confidence: low, cosmetic only.

CLAUDE.md

  • i18n conventions are followed correctly: all 17 locale files received the new addData.nonGeographicCoordinates key, t() is used for the new user-facing string, and the dismiss button uses the logical ms-2 utility (not ml-2), consistent with the RTL-mirroring guidance. Confidence: high.
  • Note (not a code defect): the PR description/title describe a different implementation (LARGE_VECTOR_SIZE_WARN_BYTES, onLargeFile, MAX_TEXT_VECTOR_BYTES, MAX_SHPJS_SHP_BYTES, a largeVectorGuards bundle, a toolbar.item.largeVectorFileDesc string) — none of which appear anywhere in this diff. The actual diff instead unifies thresholds under DUCKDB_VECTOR_ROUTE_BYTES/DUCKDB_VECTOR_FEATURE_WARN_COUNT and adds the CRS-mismatch warning and autosave fix, matching the repo's latest commit message rather than the PR body. Worth updating the PR description before merge so the history stays accurate. Confidence: high (directly verifiable by diffing the description against the patch).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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 `@apps/geolibre-desktop/src/hooks/useProjectHistory.ts`:
- Around line 87-92: Separate buildProjectSnapshot(mapControllerRef) from
serializeProject in the autosave flow: catch snapshot-construction errors and
report them as autosave errors, while keeping serialization failures non-fatal
with the existing “project is too large to serialize” warning and early return.
Ensure both failure paths preserve the surrounding autosave behavior.

In `@apps/geolibre-desktop/src/i18n/locales/en.json`:
- Line 758: Make the aggregated layer warning count-neutral in
apps/geolibre-desktop/src/i18n/locales/en.json:758-758, ar.json:827-827,
de.json:747-747, es.json:747-747, fr.json:747-747, hi.json:747-747,
it.json:747-747, and ka.json:747-747 by replacing the identified singular
pronouns, agreement, and layer-visibility wording with language-appropriate
phrasing that works for one or multiple names; update only these translation
strings and preserve the warning’s meaning.

In `@apps/geolibre-desktop/src/i18n/locales/nl.json`:
- Line 747: Update the nonGeographicCoordinates translations to use
plural-neutral wording for the aggregated {{names}} value: in
apps/geolibre-desktop/src/i18n/locales/nl.json lines 747-747, use plural verbs
and “layers”; in apps/geolibre-desktop/src/i18n/locales/pt.json lines 747-747,
use plural verbs and a plural subject; and in
apps/geolibre-desktop/src/i18n/locales/ru.json lines 787-787, use plural verbs,
pronouns, and “layers.”

In `@packages/core/src/types.ts`:
- Around line 1902-1905: Update the GeoJSON feature traversal around the
geometry coordinate handling to detect GeometryCollection objects and
recursively visit each child in geometry.geometries, preserving the existing
sampled limit across all nested geometries. Continue visiting direct
geometry.coordinates as before, and add coverage for projected coordinates
nested within a GeometryCollection.

In `@tests/duckdb-vector-guard.test.ts`:
- Around line 148-157: Update the “stops at the sample limit instead of walking
the whole collection” test to place an out-of-range coordinate feature at index
25, then keep the assertion that detectNonGeographicCoordinates called with
sampleLimit 25 returns null, making the limit behavior observable.
🪄 Autofix

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: 93d52ca6-9578-4943-a53f-cace3441c2b6

📥 Commits

Reviewing files that changed from the base of the PR and between 14c99f5 and 2b50231.

📒 Files selected for processing (23)
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/hooks/useProjectHistory.ts
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/duckdb-vector-guard.ts
  • packages/core/src/types.ts
  • packages/plugins/src/plugins/maplibre-vector.ts
  • tests/duckdb-vector-guard.test.ts

Comment thread apps/geolibre-desktop/src/hooks/useProjectHistory.ts
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
Comment thread apps/geolibre-desktop/src/i18n/locales/nl.json Outdated
Comment thread packages/core/src/types.ts
Comment thread tests/duckdb-vector-guard.test.ts
- Keep delimited text on the JS parser at any size. loadDuckDbVectorFile has
  no lon/lat column detection (that lives only in the GeoParquet conversion
  path), so routing a plain lon/lat CSV over 100 MB to DuckDB failed with
  "DuckDB did not find a geometry column in this file" for files that loaded
  before. Reported on both loadTauriVectorFile and loadBrowserVectorFile.
- Do not abort a drop batch on an oversized GPX/KML. Those branches read the
  whole document as text to extract ground overlays and models before the
  guarded loader runs; that read now decodes from bytes past the string cap,
  and a failure yields no overlays instead of throwing out the whole batch.
- Only log the DuckDB route for extensions the flag actually gates. zip/kmz
  unpack first and decide from the uncompressed .shp, so the container's own
  size made the message misleading near the threshold.
- Visit GeometryCollection members in detectNonGeographicCoordinates. Their
  coordinates sit under geometries[], so such features were skipped entirely
  and an all-GeometryCollection file passed the check silently.
- Clear the CRS warning when a new drop starts, so it cannot linger over an
  unrelated drop the way dropMessage/dropError cannot.
- Correct the docs: PMTiles is the tiled format; GeoParquet is columnar and
  compact but not tiled.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/user-guide/adding-data.md (1)

22-23: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not describe DuckDB routing as streaming.

loadDuckDbVectorFile returns a complete FeatureCollection, so conversion still materializes all features before the caller receives them. “Streams the file” can imply incremental or bounded-memory loading. Describe this as routing through DuckDB in a worker.

Proposed correction
-    **100 MB or larger**, GeoLibre streams the file through DuckDB instead —
-    off the main thread, so the interface keeps responding. For a zipped
+    **100 MB or larger**, GeoLibre routes the file through DuckDB instead of
+    the in-memory reader. DuckDB runs off the main thread, so the interface
+    keeps responding. For a zipped
🤖 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 `@docs/user-guide/adding-data.md` around lines 22 - 23, Update the
documentation near the 100 MB threshold to describe routing the file through
DuckDB in a worker, not streaming it. Clarify that processing occurs off the
main thread while avoiding any implication of incremental or bounded-memory
feature loading.
♻️ Duplicate comments (1)
packages/core/src/types.ts (1)

1904-1915: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Traverse nested GeometryCollection members.

Line 1911 visits only direct members with coordinates. A valid nested GeometryCollection has geometries instead, so projected coordinates below that level do not produce a CRS warning.

Use a recursive geometry visitor. Add a nested-collection test.

#!/bin/bash
set -euo pipefail

tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT

npm pack --pack-destination "$tmp_dir" `@types/geojson`@7946.0.16 >/dev/null
archive="$(find "$tmp_dir" -name '*.tgz' -print -quit)"
types_file="$(tar -tzf "$archive" | rg '(^|/)index\.d\.ts$' | head -n1)"

tar -xOf "$archive" "$types_file" | rg -n -A4 -B2 'GeometryCollection|geometries'
rg -n -C3 'geometry\.geometries|visitGeometry|detectNonGeographicCoordinates' \
  packages/core/src/types.ts tests/duckdb-vector-guard.test.ts
🤖 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 `@packages/core/src/types.ts` around lines 1904 - 1915, Update the geometry
traversal in the CRS-detection visitor around the existing geometry handling to
recurse through each member’s own geometries collection, not just direct
coordinates. Preserve the sampled limit while descending nested
GeometryCollection structures, and add a test covering projected coordinates
inside a nested collection so the CRS warning is produced.
🤖 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 `@apps/geolibre-desktop/src/components/layout/DesktopShell.tsx`:
- Around line 1571-1573: Update the browser handleDrop flow to call
setCrsWarning(null) alongside the existing browser status reset, matching the
native drop handler behavior and clearing stale CRS warnings when a new browser
drop begins.

In `@apps/geolibre-desktop/src/lib/tauri-io.ts`:
- Around line 2842-2844: Update the dropped-file GPX branch to use
loadBrowserVectorFile for files at or above the existing 100 MB threshold,
retaining parseGpxTextLayers only for smaller files. Apply the same size-based
routing in the Tauri GPX batch path near its GPX handling logic, while
preserving existing layer-loading behavior.

---

Outside diff comments:
In `@docs/user-guide/adding-data.md`:
- Around line 22-23: Update the documentation near the 100 MB threshold to
describe routing the file through DuckDB in a worker, not streaming it. Clarify
that processing occurs off the main thread while avoiding any implication of
incremental or bounded-memory feature loading.

---

Duplicate comments:
In `@packages/core/src/types.ts`:
- Around line 1904-1915: Update the geometry traversal in the CRS-detection
visitor around the existing geometry handling to recurse through each member’s
own geometries collection, not just direct coordinates. Preserve the sampled
limit while descending nested GeometryCollection structures, and add a test
covering projected coordinates inside a nested collection so the CRS warning is
produced.
🪄 Autofix

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: a59a03ea-4614-4aea-a913-3dde5e69f6b9

📥 Commits

Reviewing files that changed from the base of the PR and between 2b50231 and c040765.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/lib/tauri-io.ts
  • docs/user-guide/adding-data.md
  • packages/core/src/types.ts
  • tests/duckdb-vector-guard.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
Comment thread packages/core/src/types.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • Medium confidenceapps/geolibre-desktop/src/lib/tauri-io.ts:2843 (and the Tauri counterpart at loadDroppedVectorPaths ~line 3119): the batch drag-and-drop paths never check shouldRouteToDuckDb for .gpx, even though gpx is included in ROUTABLE_TEXT_EXTENSIONS. A large dropped GPX file still builds the whole text and runs parseGpxTextLayers synchronously on the main thread — the exact freeze pattern this PR fixes for GeoJSON/KML/shapefile — and a GPX at/above V8's string cap will now throw an uncaught RangeError instead of being routed to DuckDB. The single-file open-dialog paths (loadBrowserVectorFile/loadTauriVectorFile) do gate this correctly, so the guard is inconsistent between entry points. (Inline comment posted.)
  • Low confidencepackages/core/src/types.ts:1887-1888: maxAbsX/maxAbsY are updated before the Number.isFinite check, so a non-finite coordinate (which is deliberately excluded from the "offending" determination) can still pollute the reported max magnitude shown in the CRS-mismatch warning. Hard to trigger via normal JSON.parsed GeoJSON, so low real-world impact. (Inline comment posted, with a suggested fix — note the suggestion block only replaces the x line, so applying it verbatim leaves a redundant duplicate y check; harmless but worth tidying.)

Security

  • None found. File-size preflight (stat/File.size) and text decoding changes don't introduce injection or unsafe-input issues; error paths fail closed (treat unknown size as "not large" rather than trusting an attacker-controlled value to skip guards).

Performance

  • The core changes (byte-size preflight before reading files, routing large text/shapefile vectors to DuckDB, sampling coordinates with an early-exit limit in detectNonGeographicCoordinates) are sound and match their stated intent. No new inefficiencies found in the changed code, aside from the GPX gap noted above, which is a missed optimization rather than a regression.

Quality

  • The PR body's described design (LARGE_VECTOR_SIZE_WARN_BYTES, MAX_TEXT_VECTOR_BYTES, MAX_SHPJS_SHP_BYTES, onLargeFile) doesn't match the actual diff, which instead uses a single unified DUCKDB_VECTOR_ROUTE_BYTES (100 MB) and DUCKDB_VECTOR_FEATURE_WARN_COUNT (100k) threshold shared between the desktop loaders and the Add Vector Layer panel. The code and docs (docs/user-guide/adding-data.md) are internally consistent with each other, so this looks like a simplification made during review (matches the "Address review feedback" commit) with a stale PR description — not a code defect, just worth a heads-up in case the description needs updating before merge.
  • Removing confirmLargeVectorDataset from the KML-import useEffect dependency array (DesktopShell.tsx) is correct, not a stale-closure bug: it's a module-level function, not component/hook state.
  • The autosave try/catch around serializeProject/buildProjectSnapshot correctly resets timerRef.current before the try, so subsequent debounced saves aren't blocked by a prior failure. The catch is broad (any synchronous error, not just the size-related RangeError, gets logged as "too large to serialize"), which is a defensible best-effort tradeoff but could mask an unrelated bug behind a misleading log message — very low-confidence nit, not flagged inline.

CLAUDE.md

  • New user-facing string (addData.nonGeographicCoordinates) is added to en.json and all other locale files with the {{names}} interpolation preserved, consistent with the i18n convention. RTL-specific styling wasn't needed here since the added UI (crsWarning banner) uses logical Tailwind utilities (ms-2) rather than physical ones, matching the RTL convention.
  • No other CLAUDE.md-governed mirrored constants (MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, MAP_PANEL_SELECTOR, propertySpecFor, DISTANCE_SEGMENTS) are touched by this PR.

- Separate snapshot construction from serialization in the autosave path. The
  single try block also covered buildProjectSnapshot, so a construction
  failure was misreported as "the project is too large".
- Make the mislabelled-CRS warning count-neutral in all 17 locales. The
  importer aggregates layer names into one message, so wording that agreed
  with a singular subject was wrong for a multi-file drop.
- Route an oversized GPX through the guarded loader instead of decoding the
  whole document into one string. The DuckDB reader returns a single merged
  layer rather than the waypoint/track/route split, which is the accepted
  trade at that size.
- Clear the CRS warning when a browser drop starts. It was previously cleared
  only on the 4s status timeout, which both missed the browser handler and
  wrongly auto-dismissed a warning that has its own Close button.
- Make the sampling-limit test observable by placing an out-of-range feature
  past the limit; the all-valid collection passed even if sampleLimit were
  ignored.
Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx:1414 — the KML import handler doesn't clear crsWarning the way the other two drop entry points do; a failed import can leave a stale CRS-mismatch banner from a previous drop on screen indefinitely. Medium confidence, cosmetic only.
  • apps/geolibre-desktop/src/lib/tauri-io.ts:1906 (and its Tauri mirror at ~2189) — the delimited-text (CSV/TSV) branch is intentionally left ungated on the new size threshold and still does an unconditional file.text() / readLocalFileText, so a delimited file near V8's ~537 MB string cap can still throw an uncaught RangeError, the same failure mode this PR fixes for GeoJSON. Low-medium confidence — likely a narrow, possibly-accepted gap, but not called out anywhere.

Security

  • Nothing found. The desktop stat/readFile paths use existing helpers and don't introduce new path handling; no new user input reaches a shell/SQL/HTML sink.

Performance

  • apps/geolibre-desktop/src/lib/tauri-io.ts:3129 — routing a large GPX calls localFileSizeBytes(path) and then loadTauriVectorFile, which stats the same path again internally, doubling the stat syscall for that path. Low confidence, negligible real-world cost.
  • The core size/feature-count guards themselves (shouldRouteToDuckDb, detectNonGeographicCoordinates) are cheap (metadata-only, capped coordinate sampling) and well-targeted at the reported freeze scenarios.

Quality

  • The new DUCKDB_VECTOR_ROUTE_BYTES / DUCKDB_VECTOR_FEATURE_WARN_COUNT constants are centralized in @geolibre/core and consistently re-exported/consumed by both the desktop loaders and the maplibre-gl-vector plugin's autoThreshold — good, avoids the drift the PR body describes fixing.
  • detectNonGeographicCoordinates is well-tested (Point/MultiPolygon nesting, GeometryCollection, NaN handling, sample-limit truncation) and its bounded-sampling tradeoff is clearly documented.
  • The PR description does not match the actual diff: it describes LARGE_VECTOR_SIZE_WARN_BYTES (200 MB warn prompt), a new onLargeFile option, a bundled largeVectorGuards object, separate MAX_TEXT_VECTOR_BYTES/MAX_SHPJS_SHP_BYTES constants, and a toolbar.item.largeVectorFileDesc locale string (with an Arabic-mirrored arrow) — none of which appear anywhere in this diff. The actual implementation uses one unified DUCKDB_VECTOR_ROUTE_BYTES (100 MB) threshold with silent auto-routing (no user prompt for size, only for feature count) and an unrelated addData.nonGeographicCoordinates string. The code and docs/user-guide/adding-data.md agree with each other and with the tests, so this looks like a stale PR description from an earlier iteration rather than a code defect — but it's worth updating before merge so reviewers/future readers aren't misled about the design that actually shipped.

CLAUDE.md

  • New user-facing strings go through t()/i18n catalogs as required, and the RTL guidance is followed (ms-2 logical utility used for the CRS-warning dismiss button, not ml-/mr-).
  • No changes touch the Whitebox menu catalog, MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, or MAP_PANEL_SELECTOR mirrors, so those drift-tracking rules aren't implicated here.

- Gate the maxAbsX/maxAbsY update on Number.isFinite, matching the finiteness
  check already used for the offending determination. A single Infinity would
  otherwise be reported as the offending magnitude and hide the real value.
- Clear the CRS warning in the KML import handler too. Its catch sets
  dropError without reaching addImportedVectorLayers, so a previous file's
  banner could sit beside the new error.
Comment thread packages/core/src/types.ts
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Both inline comments posted. Now the final summary.

Code review

Bugs

  • None found. The size-preflight routing (shouldRouteToDuckDb), the browser/desktop loader branches, the shapefile uncompressed-size check, and detectNonGeographicCoordinates's coordinate-tree walk (including the GeometryCollection and sample-limit handling) all checked out against their unit tests and call sites. The useEffect dependency-array trim in DesktopShell.tsx (removing confirmLargeVectorDataset) is safe since that function is a stable module-level reference, not a closure.

Security

  • None found. No new injection surfaces, secrets, or unsafe input handling; all new user-facing strings go through t()/JSX escaping and console logging only.

Performance

  • Desktop KML overlay extraction (readLocalFileTextOrEmptyreadLocalFileText) has no size gate before attempting a full-file text read, unlike its browser counterpart (readVectorFileText). For a genuinely oversized KML, this can attempt a doomed full read twice internally (readTextFile, then the read_local_file fallback) before the outer wrapper swallows the failure — a milder version of the "pay the cost twice" pattern the PR's own description calls out as a bug it's fixing elsewhere. Low confidence, since the failure is still caught and behavior stays correct. (Not posted inline — narrow enough to just flag here.)

Quality

  • (posted inline, packages/core/src/types.ts) The PR description documents a three-constant design (LARGE_VECTOR_SIZE_WARN_BYTES/onLargeFile/largeVectorGuards, MAX_TEXT_VECTOR_BYTES, MAX_SHPJS_SHP_BYTES) that doesn't exist anywhere in the diff — the shipped code unifies everything into one DUCKDB_VECTOR_ROUTE_BYTES (100 MB), with no separate large-file confirmation prompt. Likely just a stale write-up from an earlier iteration, but worth reconciling so the description matches what merges. Medium confidence.
  • (posted inline, apps/geolibre-desktop/src/lib/tauri-io.ts) readVectorFileText's docstring claims switching to TextDecoder().decode(arrayBuffer) avoids File.text()'s V8 string-length RangeError, but decoding to a string hits the same cap — the two are equivalent in the case that actually matters. Low-medium confidence; not a functional bug since the error is caught either way.

CLAUDE.md

  • No violations noted: new locale strings were added consistently across all 17 locale files with the {{names}} placeholder intact, i18n keys resolve correctly (addData.nonGeographicCoordinates), and the affected mirrored constants (DUCKDB_VECTOR_FEATURE_WARN_COUNT/DUCKDB_VECTOR_ROUTE_BYTES) were updated consistently across duckdb-vector-guard.ts, maplibre-vector.ts, and the docs, without leaving stale references elsewhere in the repo.

Picks up opengeos/maplibre-gl-vector#59, which fixes two bugs the Add Vector
Layer panel hit on a large, custom-CRS GeoPackage:

- A layer whose GeoJSON document exceeded the maximum JavaScript string length
  failed to ingest with "Invalid string length". It is now encoded feature by
  feature and ingested in batches.
- A layer whose gpkg_spatial_ref_sys row is not an EPSG row (organization
  "CUSTOM", carrying a WKT definition) was never reprojected, so it rendered
  off the map with "Invalid LngLat latitude value" and no explanation.

Per the bump checklist in CLAUDE.md, MAX_VECTOR_BYTES in
packages/plugins/src/plugins/remote-file-formats.ts was re-checked against
MAX_REMOTE_FILE_BYTES in the package's src/lib/utils/remote.ts: both are still
2 ** 31 - 1, so the mirror needs no change.

Verified in the built web app: a GeoPackage with a CUSTOM-organization Albers
SRS now reprojects and renders over Connecticut with zero diagnostics.
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useProjectHistory.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/lib/tauri-io.ts:2842-2851 and :3126-3132 — Routing a large GPX to the DuckDB path (loadBrowserVectorFile/loadTauriVectorFileloadDuckDbVector) passes no layer option, and ST_Read defaults to the first OGR layer only (per the code's own doc comment in duckdb-vector-guard.ts). GDAL's GPX driver exposes separate waypoints/routes/tracks layers, so a large track-heavy GPX silently loses its track/route geometry instead of getting "a single merged layer" as the surrounding comment claims. This is new behavior introduced by this PR (GPX never hit the DuckDB path before). Medium-high confidence.

Quality

  • apps/geolibre-desktop/src/lib/tauri-io.ts:573-582readVectorFileText's large-file branch (new TextDecoder().decode(await file.arrayBuffer())) still builds one JS string in a single call, so it hits the identical V8 max-string-length RangeError as File.text() for a sufficiently huge KML; it isn't actually the incremental/streaming decode the comment describes. Likely harmless in practice (the only caller catches and degrades gracefully, and the 100 MB route threshold is well under the ~512 MB cap), but the comment overstates the fix. Low-medium confidence.
  • apps/geolibre-desktop/src/hooks/useProjectHistory.ts:96-102 — The serializeProject catch treats any thrown error as "too large to serialize," which would mislabel a genuine serialization bug as a size-limit warning. Low priority. Low confidence.
  • The PR description's stated design (onLargeFile prompt, LARGE_VECTOR_SIZE_WARN_BYTES at 200 MB, MAX_TEXT_VECTOR_BYTES, MAX_SHPJS_SHP_BYTES at 64 MB) doesn't match what's actually in the diff, which instead unifies everything behind a single DUCKDB_VECTOR_ROUTE_BYTES (100 MB) constant with no user prompt on the size path (only the feature-count guard still prompts). The auto-generated CodeRabbit summary at the bottom of the PR body matches the real diff much better than the human-written description above it — worth a quick description update so reviewers aren't misled about what actually ships. Low confidence / informational.

Nothing found in Security or Performance beyond what's noted above — the size/feature-count guards, detectNonGeographicCoordinates sampling cap, and i18n additions all looked correct and consistent across the 17 locale files checked.

- Stop routing large GPX through DuckDB. `loadDuckDbVector` passes no `layer`
  argument, so `ST_Read` reads only a GPX's first OGR layer (usually
  `waypoints`) and silently discarded its tracks and routes — data loss, not
  the "single merged layer" the comment claimed. GPX now always uses
  `parseGpxTextLayers`, matching the decision already made for delimited text.
- Drop `readVectorFileText`. `TextDecoder.decode()` over the whole buffer
  builds one JS string exactly as `File.text()` does, so it hit the same
  RangeError one line later; the comment claimed an avoidance that never
  existed. `readVectorFileTextOrEmpty` now calls `file.text()` directly and
  keeps the catch, which is what actually made an oversized KML survivable.
- Report a non-RangeError serialization failure as an error rather than
  filing it under "the project is too large", which would have hidden that
  class of bug.
@giswqs giswqs changed the title fix(vector): guard large local vector loads before reading the file fix(vector): stream large local vector files through DuckDB Aug 5, 2026
Comment thread packages/core/src/types.ts
Comment thread apps/geolibre-desktop/src/lib/tauri-io.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • No correctness bugs found in the changed logic. The size-preflight routing (shouldRouteToDuckDb), the KML/GeoJSON/GPX/CSV gating in loadBrowserVectorFile/loadTauriVectorFile, the autosave RangeError-vs-other-error split in useProjectHistory.ts, and the detectNonGeographicCoordinates sampling/recursion in packages/core/src/types.ts all checked out against their edge cases (threshold boundaries, GeometryCollection nesting, sample-limit truncation, stat failure → "not large" fallback, crsWarning state being cleared/reset at every drop entry point).

Security

  • None found. The new CRS-warning text is rendered as plain React child content (not dangerouslySetInnerHTML), so layer-name interpolation can't inject markup even though it flows through i18next.

Performance

  • Low-medium confidence: loadShapefileZip now uses the same unified 100 MB DUCKDB_VECTOR_ROUTE_BYTES threshold for shapefiles as for GeoJSON/KML, rather than a lower shapefile-specific cutoff. Since the shpjs freeze this PR targets is driven by per-coordinate proj4 reprojection cost (vertex density), not raw byte size, a dense-geometry shapefile under 100 MB uncompressed could still freeze the main thread. Flagged inline on tauri-io.ts:799.

Quality

  • Medium confidence: The PR description references constants and behavior (LARGE_VECTOR_SIZE_WARN_BYTES, onLargeFile, MAX_TEXT_VECTOR_BYTES, MAX_SHPJS_SHP_BYTES) that don't exist in the actual diff — the implementation instead uses one unified DUCKDB_VECTOR_ROUTE_BYTES (100 MB) with only a console.info log, no interactive prompt, at the size threshold. Likely stale text from before the "Address Claude review feedback" commit; worth syncing the description to the code. Flagged inline on packages/core/src/types.ts:1838.
  • The actual docs/user-guide/adding-data.md addition, by contrast, correctly matches the implementation (100 MB / 100,000 features), so end-user documentation is fine — only the PR body itself is out of date.

CLAUDE.md

  • No violations found. i18n strings were added to all 17 locales including the Arabic RTL variant; the new packages/plugins/package.json / apps/geolibre-desktop/package.json version bump for maplibre-gl-vector is reflected in package-lock.json; no CSS uses physical ml-/left- utilities in the new banner (uses ms-2).

@giswqs
giswqs merged commit f879b67 into main Aug 5, 2026
48 checks passed
@giswqs
giswqs deleted the fix/large-local-vector-preflight branch August 5, 2026 20:50
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.

2 participants