@walkthru-earth/objex 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/components/browser/FileTreeSidebar.svelte +1 -1
  2. package/dist/components/layout/Sidebar.svelte +27 -0
  3. package/dist/components/viewers/ArchiveViewer.svelte +4 -4
  4. package/dist/components/viewers/CodeViewer.svelte +21 -5
  5. package/dist/components/viewers/CogViewer.svelte +21 -3
  6. package/dist/components/viewers/CopcViewer.svelte +20 -2
  7. package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
  8. package/dist/components/viewers/PmtilesViewer.svelte +2 -2
  9. package/dist/components/viewers/StacMapViewer.svelte +25 -9
  10. package/dist/components/viewers/TableViewer.svelte +50 -21
  11. package/dist/components/viewers/ZarrMapViewer.svelte +4 -4
  12. package/dist/components/viewers/ZarrViewer.svelte +2 -2
  13. package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
  14. package/dist/i18n/ar.js +1 -0
  15. package/dist/i18n/en.js +1 -0
  16. package/dist/query/index.d.ts +1 -1
  17. package/dist/query/index.js +1 -1
  18. package/dist/query/source.d.ts +12 -0
  19. package/dist/query/source.js +25 -8
  20. package/dist/query/wasm.js +130 -23
  21. package/dist/storage/adapter.d.ts +9 -0
  22. package/dist/storage/adapter.js +13 -1
  23. package/dist/storage/browser-azure.d.ts +1 -1
  24. package/dist/storage/browser-azure.js +4 -0
  25. package/dist/storage/browser-cloud.d.ts +1 -1
  26. package/dist/storage/browser-cloud.js +7 -0
  27. package/dist/storage/presign.d.ts +13 -0
  28. package/dist/storage/presign.js +55 -0
  29. package/dist/storage/providers.d.ts +6 -0
  30. package/dist/storage/providers.js +13 -2
  31. package/dist/stores/browser.svelte.d.ts +2 -0
  32. package/dist/stores/browser.svelte.js +17 -1
  33. package/dist/utils/url.d.ts +13 -0
  34. package/dist/utils/url.js +36 -0
  35. package/dist/utils/wkb.js +22 -8
  36. package/package.json +1 -1
@@ -356,7 +356,7 @@ async function expandToPath(path: string) {
356
356
  ? await findNodeAtRoot(accumulatedPath)
357
357
  : await findNodeInParent(parentNode, accumulatedPath);
358
358
 
359
- if (!node || !node.entry.is_dir) break;
359
+ if (!node?.entry.is_dir) break;
360
360
 
361
361
  if (node.children.length === 0) {
362
362
  await loadChildren(node);
@@ -57,6 +57,33 @@ $effect(() => {
57
57
  }
58
58
  });
59
59
 
60
+ // Auto-detected ?url= buckets are saved anonymously (zero-click demo flow).
61
+ // If the first LIST returns 401/403, the bucket is actually private — flip
62
+ // the connection to non-anonymous and open the credential dialog so the
63
+ // user can paste keys instead of seeing a silent failure.
64
+ $effect(() => {
65
+ const conn = browser.authRequired;
66
+ if (!conn) return;
67
+ handleAuthRequired(conn);
68
+ });
69
+
70
+ async function handleAuthRequired(conn: Connection) {
71
+ browser.clearAuthRequired();
72
+ await connections.update(conn.id, {
73
+ name: conn.name,
74
+ provider: conn.provider,
75
+ endpoint: conn.endpoint,
76
+ bucket: conn.bucket,
77
+ region: conn.region,
78
+ anonymous: false,
79
+ authMethod: conn.authMethod,
80
+ rootPrefix: conn.rootPrefix
81
+ });
82
+ const updated = connections.getById(conn.id);
83
+ if (!updated) return;
84
+ await ensureCredentials(updated);
85
+ }
86
+
60
87
  async function handleAutoDetection() {
61
88
  const url = new URL(window.location.href);
62
89
  const rawUrl = url.searchParams.get('url');
@@ -29,7 +29,7 @@ import {
29
29
  streamZipEntriesFromUrl
30
30
  } from '../../utils/archive';
31
31
  import { formatFileSize } from '../../utils/format';
32
- import { buildHttpsUrl } from '../../utils/url.js';
32
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
33
33
 
34
34
  let { tab }: { tab: Tab } = $props();
35
35
 
@@ -177,7 +177,7 @@ async function loadZip() {
177
177
  const signal = abortController!.signal;
178
178
 
179
179
  if (tab.source === 'remote') {
180
- const url = buildHttpsUrl(tab);
180
+ const url = await buildHttpsUrlAsync(tab);
181
181
  try {
182
182
  scanning = true;
183
183
  for await (const batch of streamZipEntriesFromUrl(url, signal)) {
@@ -208,7 +208,7 @@ async function loadTar() {
208
208
  const signal = abortController!.signal;
209
209
 
210
210
  if (tab.source === 'remote') {
211
- const url = buildHttpsUrl(tab);
211
+ const url = await buildHttpsUrlAsync(tab);
212
212
  try {
213
213
  scanning = true;
214
214
  remoteUrl = url;
@@ -240,7 +240,7 @@ async function loadTarGz() {
240
240
 
241
241
  // For remote URLs: stream-fetch → decompress → parse progressively
242
242
  if (tab.source === 'remote' || tab.source === 'url') {
243
- const url = buildHttpsUrl(tab);
243
+ const url = await buildHttpsUrlAsync(tab);
244
244
  try {
245
245
  scanning = true;
246
246
  const decompressedChunks: Uint8Array[] = [];
@@ -11,7 +11,7 @@ import type { Tab } from '../../types';
11
11
  import { copyToClipboard } from '../../utils/clipboard.js';
12
12
  import { handleLoadError } from '../../utils/error.js';
13
13
  import { extensionToShikiLang, highlightCode } from '../../utils/shiki';
14
- import { buildHttpsUrl } from '../../utils/url.js';
14
+ import { buildHttpsUrl, buildHttpsUrlAsync, canStreamDirectly } from '../../utils/url.js';
15
15
  import { getUrlView, updateUrlView } from '../../utils/url-state.js';
16
16
  import { openZarrTab } from '../../utils/zarr-tab.js';
17
17
 
@@ -96,7 +96,23 @@ const stacBadgeKey = $derived<Record<string, string>>({
96
96
  'stac-collection': 'code.stacCollection',
97
97
  'stac-item': 'code.stacItem'
98
98
  });
99
- const styleUrl = $derived(buildHttpsUrl(tab));
99
+ // Third-party iframes can't route through the storage adapter, so the URL
100
+ // must carry auth. Public/SAS connections resolve synchronously; `signed-s3`
101
+ // must wait for the presign so the iframe never loads a bare `s3://` href.
102
+ let styleUrl = $state('');
103
+ $effect(() => {
104
+ const id = tab.id;
105
+ styleUrl = canStreamDirectly(tab) ? buildHttpsUrl(tab) : '';
106
+ let cancelled = false;
107
+ (async () => {
108
+ const url = await buildHttpsUrlAsync(tab);
109
+ if (cancelled || id !== tab.id) return;
110
+ styleUrl = url;
111
+ })();
112
+ return () => {
113
+ cancelled = true;
114
+ };
115
+ });
100
116
  const stacBrowserSrc = $derived(
101
117
  `https://radiantearth.github.io/stac-browser/#/external/${styleUrl}`
102
118
  );
@@ -481,7 +497,7 @@ async function copyCode() {
481
497
  </div>
482
498
  </div>
483
499
 
484
- {#if viewMode === 'stac-browser'}
500
+ {#if viewMode === 'stac-browser' && styleUrl}
485
501
  <div class="flex-1 overflow-hidden">
486
502
  <iframe
487
503
  src={stacBrowserSrc}
@@ -490,7 +506,7 @@ async function copyCode() {
490
506
  allow="fullscreen"
491
507
  ></iframe>
492
508
  </div>
493
- {:else if viewMode === 'kepler'}
509
+ {:else if viewMode === 'kepler' && styleUrl}
494
510
  <div class="flex-1 overflow-hidden">
495
511
  <iframe
496
512
  src={keplerSrc}
@@ -499,7 +515,7 @@ async function copyCode() {
499
515
  allow="fullscreen"
500
516
  ></iframe>
501
517
  </div>
502
- {:else if viewMode === 'maputnik'}
518
+ {:else if viewMode === 'maputnik' && styleUrl}
503
519
  <div class="flex-1 overflow-hidden">
504
520
  <iframe
505
521
  src={maputnikSrc}
@@ -27,7 +27,7 @@ import {
27
27
  resolveProj4Def,
28
28
  selectCogPipeline
29
29
  } from '../../utils/cog.js';
30
- import { buildHttpsUrl } from '../../utils/url.js';
30
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
31
31
  import CogControls from './CogControls.svelte';
32
32
  import MapContainer from './map/MapContainer.svelte';
33
33
 
@@ -57,6 +57,7 @@ let proj4DefRef: string | null = null;
57
57
  let sampleFormatRef = 1;
58
58
  let isTiledRef = true;
59
59
  let clickHandlerRef: ((e: maplibregl.MapMouseEvent) => void) | null = null;
60
+ let resolvedHttpsUrl: string | null = null;
60
61
  // True when the library-default uint pipeline will run. LinearRescale only
61
62
  // operates on already-normalized RGB 0..1, so the slider is meaningful only
62
63
  // here, and only for non-palette data (palette renders through Colormap).
@@ -104,6 +105,7 @@ $effect(() => {
104
105
  overlayRef = null;
105
106
  geotiffRef = null;
106
107
  proj4DefRef = null;
108
+ resolvedHttpsUrl = null;
107
109
  loading = true;
108
110
  error = null;
109
111
  cogInfo = null;
@@ -165,7 +167,9 @@ async function loadCog(map: maplibregl.Map) {
165
167
  const signal = abortController.signal;
166
168
 
167
169
  try {
168
- const url = buildHttpsUrl(tab);
170
+ const url = await buildHttpsUrlAsync(tab);
171
+ if (signal.aborted) return;
172
+ resolvedHttpsUrl = url;
169
173
 
170
174
  // Pre-flight: read first IFD to check if tiled (single range request).
171
175
  let isTiled = true;
@@ -187,6 +191,19 @@ async function loadCog(map: maplibregl.Map) {
187
191
  }
188
192
  } catch (preflightErr) {
189
193
  if (signal.aborted) return;
194
+ // `@developmentseed/geotiff` throws "Only tiff supported version:<n>"
195
+ // when the first 4 bytes don't match II*\0 / MM\0* / II+\0 / MM\0+.
196
+ // This happens on files that advertise image/tiff but are corrupt,
197
+ // encrypted, or a different format entirely (GDAL reports "not
198
+ // recognized as being in a supported file format" on the same file).
199
+ // Surface a clear message and bail — COGLayer would re-invoke the
200
+ // same loader and throw the identical error uncaught during update.
201
+ const msg = preflightErr instanceof Error ? preflightErr.message : String(preflightErr);
202
+ if (/Only tiff supported version|not a tiff|Invalid.*magic/i.test(msg)) {
203
+ error = t('map.cogInvalidTiff');
204
+ loading = false;
205
+ return;
206
+ }
190
207
  }
191
208
 
192
209
  // Store refs for pixel inspection and rebuild
@@ -255,7 +272,7 @@ function buildAndAddLayer(
255
272
  // Apply upstream-bug workarounds in place (overview filter, 4326 bbox clamp).
256
273
  if (preflightGeotiff) normalizeCogGeotiff(preflightGeotiff);
257
274
 
258
- const cogInput = preflightGeotiff ?? buildHttpsUrl(tab);
275
+ const cogInput = preflightGeotiff ?? resolvedHttpsUrl ?? '';
259
276
 
260
277
  const layer = new COGLayer({
261
278
  // Stable id per tab so rebuilds on band/style change don't force deck.gl
@@ -384,6 +401,7 @@ function cleanup() {
384
401
  geotiffRef = null;
385
402
  proj4DefRef = null;
386
403
  pixelValue = null;
404
+ resolvedHttpsUrl = null;
387
405
  }
388
406
 
389
407
  $effect(() => {
@@ -1,10 +1,28 @@
1
1
  <script lang="ts">
2
2
  import type { Tab } from '../../types';
3
- import { buildHttpsUrl } from '../../utils/url.js';
3
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
4
4
 
5
5
  let { tab }: { tab: Tab } = $props();
6
6
 
7
- const fileUrl = $derived(tab.source === 'url' ? tab.path : (buildHttpsUrl(tab) ?? ''));
7
+ let fileUrl = $state('');
8
+
9
+ $effect(() => {
10
+ const id = tab.id;
11
+ let cancelled = false;
12
+ (async () => {
13
+ if (tab.source === 'url') {
14
+ fileUrl = tab.path;
15
+ return;
16
+ }
17
+ const url = await buildHttpsUrlAsync(tab);
18
+ if (cancelled || id !== tab.id) return;
19
+ fileUrl = url;
20
+ })();
21
+ return () => {
22
+ cancelled = true;
23
+ };
24
+ });
25
+
8
26
  const viewerUrl = $derived(
9
27
  fileUrl ? `https://viewer.copc.io/?copc=${encodeURIComponent(fileUrl)}` : ''
10
28
  );
@@ -19,7 +19,7 @@ import {
19
19
  hoverCursor,
20
20
  loadDeckModules
21
21
  } from '../../utils/deck.js';
22
- import { buildHttpsUrl } from '../../utils/url.js';
22
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
23
23
  import AttributeTable from './map/AttributeTable.svelte';
24
24
  import MapContainer from './map/MapContainer.svelte';
25
25
 
@@ -55,6 +55,7 @@ let mapReadyPromise: Promise<void> | null = null;
55
55
  // Stored from preview for load-all (skip index)
56
56
  let storedHeader: HeaderMeta | null = null;
57
57
  let storedFeatureOffset = 0;
58
+ let signedUrl: string | null = null;
58
59
 
59
60
  // proj4 converter for reprojecting from source CRS → WGS84
60
61
  let proj4Forward: ((coord: [number, number]) => [number, number]) | null = null;
@@ -217,6 +218,7 @@ function cleanup() {
217
218
  dataVersion = 0;
218
219
  storedHeader = null;
219
220
  storedFeatureOffset = 0;
221
+ signedUrl = null;
220
222
  proj4Forward = null;
221
223
  }
222
224
 
@@ -261,9 +263,14 @@ async function loadFlatGeobuf() {
261
263
  await mapReadyPromise;
262
264
  if (!overlay) return;
263
265
 
266
+ // Sign once per load so header + feature stream share the same signature.
267
+ // Cached across loadAllFeatures() so the "Load all" button doesn't re-sign.
268
+ const url = await buildHttpsUrlAsync(tab);
269
+ signedUrl = url;
270
+
264
271
  // Read header via range requests (fast: 1-2 small requests)
265
272
  // Gets metadata + feature offset to skip the spatial index
266
- await readHeaderWithRangeRequests();
273
+ await readHeaderWithRangeRequests(url);
267
274
 
268
275
  // Set up on-the-fly reprojection if the file uses a non-WGS84 CRS
269
276
  proj4Forward = null;
@@ -298,7 +305,7 @@ async function loadFlatGeobuf() {
298
305
  }
299
306
 
300
307
  // Stream features (skips index if header was read, else sequential)
301
- await streamFeatures(settings.featureLimit);
308
+ await streamFeatures(url, settings.featureLimit);
302
309
  } catch (err) {
303
310
  console.error('[FGB]', 'loadFlatGeobuf error:', err);
304
311
  if (err instanceof DOMException && err.name === 'AbortError') return;
@@ -314,9 +321,7 @@ async function loadFlatGeobuf() {
314
321
  * Read header via range requests (fast: 1-2 small requests).
315
322
  * Stores header + feature offset for the composite stream approach.
316
323
  */
317
- async function readHeaderWithRangeRequests(): Promise<boolean> {
318
- const url = buildHttpsUrl(tab);
319
-
324
+ async function readHeaderWithRangeRequests(url: string): Promise<boolean> {
320
325
  let reader: HttpReader;
321
326
  try {
322
327
  reader = await HttpReader.open(url, false);
@@ -357,7 +362,9 @@ async function loadAllFeatures() {
357
362
  try {
358
363
  features = [];
359
364
  featureCount = 0;
360
- await streamFeatures();
365
+ const url = signedUrl ?? (await buildHttpsUrlAsync(tab));
366
+ signedUrl = url;
367
+ await streamFeatures(url);
361
368
  } catch (err) {
362
369
  console.error('[FGB]', 'loadAllFeatures error:', err);
363
370
  if (err instanceof DOMException && err.name === 'AbortError') return;
@@ -372,10 +379,9 @@ async function loadAllFeatures() {
372
379
  * Stream features sequentially.
373
380
  * If storedHeader is available, skips the index with a Range request + composite stream.
374
381
  */
375
- async function streamFeatures(limit?: number) {
382
+ async function streamFeatures(url: string, limit?: number) {
376
383
  const ac = new AbortController();
377
384
  abortController = ac;
378
- const url = buildHttpsUrl(tab);
379
385
  const t0 = performance.now();
380
386
 
381
387
  let iter: AsyncGenerator;
@@ -11,7 +11,7 @@ import { t } from '../../i18n/index.svelte.js';
11
11
  import { tabResources } from '../../stores/tab-resources.svelte.js';
12
12
  import type { Tab } from '../../types';
13
13
  import { loadPmtiles, type PmtilesMetadata } from '../../utils/pmtiles';
14
- import { buildHttpsUrl } from '../../utils/url.js';
14
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
15
15
  import { getUrlView, updateUrlView } from '../../utils/url-state.js';
16
16
 
17
17
  let { tab }: { tab: Tab } = $props();
@@ -73,7 +73,7 @@ async function load() {
73
73
  error = null;
74
74
 
75
75
  try {
76
- pmtilesUrl = buildHttpsUrl(tab);
76
+ pmtilesUrl = await buildHttpsUrlAsync(tab);
77
77
  const result = await loadPmtiles(pmtilesUrl);
78
78
  pmtilesInstance = result.pmtiles;
79
79
  metadata = result.metadata;
@@ -1,20 +1,36 @@
1
1
  <script lang="ts">
2
2
  import type { Tab } from '../../types';
3
- import { buildHttpsUrl } from '../../utils/url.js';
3
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
4
4
 
5
5
  let { tab }: { tab: Tab } = $props();
6
6
 
7
- const fileUrl = $derived(buildHttpsUrl(tab));
7
+ let fileUrl = $state('');
8
+
9
+ $effect(() => {
10
+ const id = tab.id;
11
+ let cancelled = false;
12
+ (async () => {
13
+ const url = await buildHttpsUrlAsync(tab);
14
+ if (cancelled || id !== tab.id) return;
15
+ fileUrl = url;
16
+ })();
17
+ return () => {
18
+ cancelled = true;
19
+ };
20
+ });
21
+
8
22
  const iframeSrc = $derived(
9
- `https://developmentseed.org/stac-map?href=${encodeURIComponent(fileUrl)}`
23
+ fileUrl ? `https://developmentseed.org/stac-map?href=${encodeURIComponent(fileUrl)}` : ''
10
24
  );
11
25
  </script>
12
26
 
13
27
  <div class="relative flex h-full overflow-hidden">
14
- <iframe
15
- src={iframeSrc}
16
- class="h-full w-full border-0"
17
- title="STAC Map"
18
- allow="fullscreen"
19
- ></iframe>
28
+ {#if iframeSrc}
29
+ <iframe
30
+ src={iframeSrc}
31
+ class="h-full w-full border-0"
32
+ title="STAC Map"
33
+ allow="fullscreen"
34
+ ></iframe>
35
+ {/if}
20
36
  </div>
@@ -11,7 +11,8 @@ import {
11
11
  QueryCancelledError,
12
12
  type QueryHandle,
13
13
  type ResolvedTableSource,
14
- resolveTableSource
14
+ resolveTableSource,
15
+ resolveTableSourceAsync
15
16
  } from '../../query/index.js';
16
17
  import { queryHistory } from '../../stores/query-history.svelte.js';
17
18
  import { settings } from '../../stores/settings.svelte.js';
@@ -61,6 +62,8 @@ let viewMode = $state<'table' | 'map' | 'stac' | 'info'>(
61
62
  );
62
63
  let sqlQuery = $state('');
63
64
  let customSql = $state('');
65
+ // Presigned URL for the source-cooperative parquet-table iframe (external fetcher).
66
+ let parquetIframeUrl = $state('');
64
67
  let queryRunning = $state(false);
65
68
  let executionTimeMs = $state(0);
66
69
 
@@ -98,8 +101,12 @@ const columnTypes = $derived(Object.fromEntries(schema.map((f) => [f.name, f.typ
98
101
  // Columns for display — exclude internal __wkb helper
99
102
  const displayColumns = $derived(columns.filter((c) => c !== '__wkb'));
100
103
 
101
- function buildDefaultSql(offset = 0): string {
102
- const resolved = resolveTableSource(tab);
104
+ let resolvedSource: ResolvedTableSource | null = null;
105
+
106
+ function buildDefaultSql(
107
+ offset = 0,
108
+ resolved: ResolvedTableSource = resolvedSource ?? resolveTableSource(tab)
109
+ ): string {
103
110
  const source = resolved.ref;
104
111
 
105
112
  let sql: string;
@@ -204,6 +211,8 @@ $effect(() => {
204
211
  geoCol = null;
205
212
  knownGeomType = undefined;
206
213
  metadataBounds = null;
214
+ resolvedSource = null;
215
+ parquetIframeUrl = '';
207
216
  error = null;
208
217
  });
209
218
  return unregister;
@@ -268,6 +277,10 @@ async function forceCancel() {
268
277
 
269
278
  async function loadTable() {
270
279
  const thisGen = ++loadGeneration;
280
+ // Snapshot reactive values once. Reading `$derived` across awaits after the
281
+ // component's effect is torn down returns Svelte's destroyed-signal sentinel,
282
+ // which throws "can't convert symbol to string" in downstream template literals.
283
+ const cid = connId;
271
284
 
272
285
  // Cancel in-flight query from a previous load to prevent duplicate concurrent queries
273
286
  if (activeHandle) {
@@ -287,13 +300,19 @@ async function loadTable() {
287
300
  loadStage = t('table.preparingQuery');
288
301
  loadProgress = [];
289
302
 
290
- // Set SQL eagerly so editor shows the query while loading
291
- const initialSql = buildDefaultSql(0);
292
- sqlQuery = initialSql;
293
- customSql = initialSql;
303
+ resolvedSource = null;
304
+ const eagerSql = buildDefaultSql(0);
305
+ sqlQuery = eagerSql;
306
+ customSql = eagerSql;
294
307
 
295
308
  try {
296
- const resolved: ResolvedTableSource = resolveTableSource(tab);
309
+ const resolved: ResolvedTableSource = await resolveTableSourceAsync(tab);
310
+ if (thisGen !== loadGeneration) return;
311
+ resolvedSource = resolved;
312
+ const resolvedSql = buildDefaultSql(0, resolved);
313
+ sqlQuery = resolvedSql;
314
+ // Only overwrite the editor if the user hasn't edited it during the presign await.
315
+ if (customSql === eagerSql) customSql = resolvedSql;
297
316
  const isFileSource = resolved.isFileSource;
298
317
  const fileUrl = resolved.fileUrl ?? '';
299
318
  const httpsUrl = isFileSource ? buildHttpsUrl(tab) : '';
@@ -301,6 +320,11 @@ async function loadTable() {
301
320
  const isParquet = isFileSource && /\.parquet$/i.test(tab.path);
302
321
  const streamable = isFileSource && canStreamDirectly(tab);
303
322
 
323
+ // Parquet-table iframe fetches from its own origin. `resolved.fileUrl`
324
+ // is already the presigned HTTPS URL for signed-s3 (or the public URL
325
+ // for anonymous / SAS), so reuse it instead of signing a second time.
326
+ if (isParquet) parquetIframeUrl = fileUrl;
327
+
304
328
  // Start DuckDB boot immediately (runs in parallel with hyparquet)
305
329
  loadStage = t('table.initEngine');
306
330
  const enginePromise = getQueryEngine();
@@ -448,7 +472,7 @@ async function loadTable() {
448
472
  // Disable conversion for this connection and fall back to BLOB handling.
449
473
  if (metaFromHyparquet && isLegacyGeoParquet && geoCol) {
450
474
  try {
451
- await engine.query(connId, 'SET enable_geoparquet_conversion = false');
475
+ await engine.query(cid, 'SET enable_geoparquet_conversion = false');
452
476
  geoColType = 'BLOB';
453
477
  } catch {
454
478
  // Setting failed — DuckDB may still handle it gracefully
@@ -460,7 +484,7 @@ async function loadTable() {
460
484
  // reads GeoParquet as GEOMETRY('EPSG:...') with CRS embedded in the type.
461
485
  if (metaFromHyparquet && geoCol && !isLegacyGeoParquet) {
462
486
  try {
463
- const duckSchema = await engine.getSchema(connId, resolved);
487
+ const duckSchema = await engine.getSchema(cid, resolved);
464
488
  if (thisGen !== loadGeneration) return;
465
489
  const duckGeoField = duckSchema.find((f: { name: string }) => f.name === geoCol);
466
490
  if (duckGeoField) {
@@ -490,7 +514,7 @@ async function loadTable() {
490
514
  // (native Parquet GEOMETRY without "geo" KV metadata), use DuckDB
491
515
  if (metaFromHyparquet && needsDuckDbCrs && geoCol) {
492
516
  try {
493
- sourceCrs = await engine.detectCrs(connId, resolved, geoCol);
517
+ sourceCrs = await engine.detectCrs(cid, resolved, geoCol);
494
518
  if (thisGen !== loadGeneration) return;
495
519
  if (sourceCrs) {
496
520
  loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
@@ -520,7 +544,7 @@ async function loadTable() {
520
544
  ];
521
545
 
522
546
  if (engine.getSchemaAndCrs) {
523
- const result = await engine.getSchemaAndCrs(connId, resolved, findGeoColumn);
547
+ const result = await engine.getSchemaAndCrs(cid, resolved, findGeoColumn);
524
548
  if (thisGen !== loadGeneration) return;
525
549
  schema = result.schema;
526
550
  columns = schema.map((f) => f.name);
@@ -547,7 +571,7 @@ async function loadTable() {
547
571
  }
548
572
  }
549
573
  } else {
550
- schema = await engine.getSchema(connId, resolved);
574
+ schema = await engine.getSchema(cid, resolved);
551
575
  if (thisGen !== loadGeneration) return;
552
576
  columns = schema.map((f) => f.name);
553
577
  const colPreview =
@@ -568,7 +592,7 @@ async function loadTable() {
568
592
  ...loadProgress,
569
593
  { label: t('progress.geometry'), value: `${detectedGeoCol} (${geoColType})` }
570
594
  ];
571
- sourceCrs = await engine.detectCrs(connId, resolved, detectedGeoCol);
595
+ sourceCrs = await engine.detectCrs(cid, resolved, detectedGeoCol);
572
596
  if (thisGen !== loadGeneration) return;
573
597
  if (sourceCrs) {
574
598
  loadProgress = [...loadProgress, { label: t('progress.crs'), value: sourceCrs }];
@@ -599,7 +623,7 @@ async function loadTable() {
599
623
  // Retry with enable_geoparquet_conversion=false and BLOB handling.
600
624
  if (!result && error && isParquet && geoCol && !isLegacyGeoParquet) {
601
625
  try {
602
- await engine.query(connId, 'SET enable_geoparquet_conversion = false');
626
+ await engine.query(cid, 'SET enable_geoparquet_conversion = false');
603
627
  geoColType = 'BLOB';
604
628
  sqlQuery = buildDefaultSql(0);
605
629
  customSql = sqlQuery;
@@ -673,7 +697,7 @@ async function loadTable() {
673
697
  } else {
674
698
  loadStage = t('table.countingRows');
675
699
  engine
676
- .getRowCount(connId, resolved)
700
+ .getRowCount(cid, resolved)
677
701
  .then((count) => {
678
702
  if (thisGen === loadGeneration) {
679
703
  totalRows = count;
@@ -695,11 +719,14 @@ async function loadTable() {
695
719
  }
696
720
 
697
721
  async function executeQuery(sql: string) {
722
+ // Snapshot `connId` — reading the $derived after the effect is destroyed
723
+ // returns a Symbol sentinel and crashes downstream template literals.
724
+ const cid = connId;
698
725
  try {
699
726
  const engine = await getQueryEngine();
700
727
 
701
728
  if (engine.queryCancellable) {
702
- const handle = engine.queryCancellable(connId, sql);
729
+ const handle = engine.queryCancellable(cid, sql);
703
730
  activeHandle = handle;
704
731
  try {
705
732
  const result = await handle.result;
@@ -716,7 +743,7 @@ async function executeQuery(sql: string) {
716
743
  }
717
744
  }
718
745
 
719
- const result = await engine.query(connId, sql);
746
+ const result = await engine.query(cid, sql);
720
747
  columns = result.columns;
721
748
  rows = result.rows;
722
749
  return result;
@@ -746,6 +773,8 @@ async function loadPage(page: number) {
746
773
  }
747
774
 
748
775
  async function runCustomSql() {
776
+ // Snapshot before any await — see note in executeQuery.
777
+ const cid = connId;
749
778
  queryRunning = true;
750
779
  error = null;
751
780
  isCustomQuery = true;
@@ -768,7 +797,7 @@ async function runCustomSql() {
768
797
  timestamp: Date.now(),
769
798
  durationMs: executionTimeMs,
770
799
  rowCount: rows.length,
771
- connectionId: connId || undefined
800
+ connectionId: cid || undefined
772
801
  });
773
802
  } catch (err) {
774
803
  executionTimeMs = Math.round(performance.now() - start);
@@ -780,7 +809,7 @@ async function runCustomSql() {
780
809
  durationMs: executionTimeMs,
781
810
  rowCount: 0,
782
811
  error: error ?? undefined,
783
- connectionId: connId || undefined
812
+ connectionId: cid || undefined
784
813
  });
785
814
  } finally {
786
815
  queryRunning = false;
@@ -984,7 +1013,7 @@ function setStacView() {
984
1013
  <FileInfo
985
1014
  entries={loadProgress}
986
1015
  {schema}
987
- parquetUrl={/\.parquet$/i.test(tab.path) ? buildHttpsUrl(tab) : ''}
1016
+ parquetUrl={/\.parquet$/i.test(tab.path) ? parquetIframeUrl : ''}
988
1017
  />
989
1018
  </div>
990
1019
  {:else if viewMode === 'stac'}
@@ -5,7 +5,7 @@ import { onDestroy, untrack } from 'svelte';
5
5
  import { t } from '../../i18n/index.svelte.js';
6
6
  import { tabResources } from '../../stores/tab-resources.svelte.js';
7
7
  import type { Tab } from '../../types';
8
- import { buildHttpsUrl } from '../../utils/url.js';
8
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
9
9
  import {
10
10
  ensureCodecsRegistered,
11
11
  extractZarrStoreUrl,
@@ -372,7 +372,7 @@ async function addZarrLayer(map: maplibregl.Map) {
372
372
  await ensureCodecsRegistered();
373
373
  const { ZarrLayer } = await import('@carbonplan/zarr-layer');
374
374
 
375
- const storeUrl = buildStoreUrl();
375
+ const storeUrl = await buildStoreUrl();
376
376
  const selector = buildSelector();
377
377
 
378
378
  const opts: any = {
@@ -452,8 +452,8 @@ async function addZarrLayer(map: maplibregl.Map) {
452
452
  }
453
453
  }
454
454
 
455
- function buildStoreUrl(): string {
456
- const rawUrl = buildHttpsUrl(tab).replace(/\/+$/, '');
455
+ async function buildStoreUrl(): Promise<string> {
456
+ const rawUrl = (await buildHttpsUrlAsync(tab)).replace(/\/+$/, '');
457
457
  return extractZarrStoreUrl(rawUrl) ?? rawUrl;
458
458
  }
459
459
 
@@ -9,7 +9,7 @@ import {
9
9
  } from '../ui/resizable/index.js';
10
10
  import { t } from '../../i18n/index.svelte.js';
11
11
  import type { Tab } from '../../types';
12
- import { buildHttpsUrl } from '../../utils/url.js';
12
+ import { buildHttpsUrlAsync } from '../../utils/url.js';
13
13
  import { getUrlView, updateUrlView } from '../../utils/url-state.js';
14
14
  import {
15
15
  computeChunkCount,
@@ -120,7 +120,7 @@ async function loadHierarchy() {
120
120
  error = null;
121
121
 
122
122
  try {
123
- const rawUrl = buildHttpsUrl(tab).replace(/\/+$/, '');
123
+ const rawUrl = (await buildHttpsUrlAsync(tab)).replace(/\/+$/, '');
124
124
  const url = extractZarrStoreUrl(rawUrl) ?? rawUrl;
125
125
  const storeName = tab.name.replace(/\.(zarr|zr3)$/, '');
126
126
 
@@ -12,7 +12,6 @@ import type { Tab } from '../../../types';
12
12
  import { setupSelectionLayer, updateSelection } from '../../../utils/map-selection.js';
13
13
  import { buildPmtilesLayers, getPmtilesProtocol, type PmtilesMetadata } from '../../../utils/pmtiles';
14
14
  import { layerHue } from '../../../utils/pmtiles-tile.js';
15
- import { buildHttpsUrl } from '../../../utils/url.js';
16
15
  import AttributeTable from '../map/AttributeTable.svelte';
17
16
  import MapContainer from '../map/MapContainer.svelte';
18
17
 
package/dist/i18n/ar.js CHANGED
@@ -342,6 +342,7 @@ export const ar = {
342
342
  'map.flatgeobufInfo': 'معلومات FlatGeobuf',
343
343
  'map.cogInfo': 'معلومات COG',
344
344
  'map.cogCorsError': 'تعذّر تحميل COG: الخادم لا يسمح بطلبات عبر النطاقات (CORS). يجب استضافة الملف مع تفعيل ترويسات CORS.',
345
+ 'map.cogInvalidTiff': 'هذا الملف ليس ملف TIFF صالح. نوع المحتوى image/tiff لكن الأجزاء الأولى من الملف لا تطابق توقيع TIFF، قد يكون الملف تالفاً أو مشفّراً أو مُعنوناً بشكل خاطئ.',
345
346
  'map.cogUnsupportedFormat': 'يستخدم هذا الملف صيغة {{type}} غير مدعومة لعرض الخريطة. يمكن عرض ملفات COG بصيغة RGB فقط.',
346
347
  'map.noGeoColumn': 'لم يتم اكتشاف عمود هندسي في المخطط',
347
348
  'map.noData': 'لا تتوفر بيانات لعرض الخريطة',