1- // app.js — Arrow-first with Parquet fallback (pure frontend) — manifest-relative URL fix
1+ // app.js — Arrow-first with Parquet fallback — manifest-aware by subdir/overrides
22import * as duckdb from "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.28.0/+esm" ;
33
44const $id = ( id ) => document . getElementById ( id ) ;
@@ -12,30 +12,58 @@ const examples = $id("examples");
1212const setStatus = ( t , bg = "#eef5ff" ) => { if ( statusEl ) { statusEl . textContent = t ; statusEl . style . background = bg ; } } ;
1313const showError = ( m ) => { if ( errorEl ) errorEl . textContent = m || "" ; } ;
1414
15- function pickPreferBrowser ( bundles ) { return bundles . browser ?? bundles . mvp ?? Object . values ( bundles ) [ 0 ] ; }
16- async function sameOriginWorkerURL ( url ) { const r = await fetch ( url , { cache :"no-store" } ) ; if ( ! r . ok ) throw new Error ( "fetch worker " + r . status ) ; return URL . createObjectURL ( new Blob ( [ await r . text ( ) ] , { type :"text/javascript" } ) ) ; }
15+ // ---------- config helpers ----------
16+ function getMeta ( name ) { const el = document . querySelector ( `meta[name="${ name } "]` ) ; return el ?. content || "" ; }
17+ function getQ ( name ) { return new URLSearchParams ( location . search ) . get ( name ) || "" ; }
18+
19+ function resolveDataSubdir ( ) {
20+ // Priority: global var -> meta -> query -> heuristic(hostname)
21+ const ov = ( window . HX_DATA_SUBDIR || getMeta ( "hx-data-subdir" ) || getQ ( "data_subdir" ) || getQ ( "mode" ) || "" ) . toLowerCase ( ) ;
22+ if ( ov === "fixed" || ov === "local" ) return ov ;
23+ // Heuristic: 线上域名走 fixed,其余走 local(可按需调整)
24+ return location . hostname . endsWith ( "harborx.tech" ) ? "fixed" : "local" ;
25+ }
1726
27+ function resolveManifestURL ( ) {
28+ const override = window . HX_MANIFEST_URL || getMeta ( "hx-manifest-url" ) || getQ ( "manifest" ) ;
29+ if ( override ) return new URL ( override , document . baseURI ) ;
30+ const sub = resolveDataSubdir ( ) ;
31+ return new URL ( `data/${ sub } /manifest.json` , document . baseURI ) ;
32+ }
33+
34+ // ---------- data loading ----------
1835async function loadManifest ( ) {
19- // Resolve file paths relative to *manifest location* (not the page)
20- const manifestURL = new URL ( "data/manifest.json" , document . baseURI ) ;
21- const res = await fetch ( manifestURL . href , { cache : "no-store" } ) ;
22- if ( ! res . ok ) throw new Error ( `manifest ${ res . status } ` ) ;
36+ let manifestURL = resolveManifestURL ( ) ;
37+ // 拉取 manifest;失败时在 fixed/local 间做一次兜底切换
38+ let res = await fetch ( manifestURL . href , { cache : "no-store" } ) ;
39+ if ( ! res . ok ) {
40+ const sub = resolveDataSubdir ( ) ;
41+ const alt = sub === "fixed" ? "local" : "fixed" ;
42+ const fallback = new URL ( `data/${ alt } /manifest.json` , document . baseURI ) ;
43+ res = await fetch ( fallback . href , { cache : "no-store" } ) ;
44+ if ( ! res . ok ) throw new Error ( `manifest fetch failed (${ manifestURL } and ${ fallback } )` ) ;
45+ manifestURL = fallback ;
46+ }
2347 const m = await res . json ( ) ;
48+ // 相对路径以 manifest 所在目录为基准
2449 const toAbs = ( a ) => ( Array . isArray ( a ) ?a :[ ] ) . map ( p => new URL ( p , manifestURL ) . href ) ;
25- return { arrow :toAbs ( m . arrow ) , parquet :toAbs ( m . parquet ) } ;
50+ return { arrow :toAbs ( m . arrow ) , parquet :toAbs ( m . parquet ) , manifestURL } ;
2651}
2752
2853async function buildState ( conn ) {
29- const { arrow, parquet} = await loadManifest ( ) ;
54+ const { arrow, parquet, manifestURL } = await loadManifest ( ) ;
3055 let engine = arrow . length ? "arrow" : ( parquet . length ? "parquet" : null ) ;
3156 let files = engine === "arrow" ?arrow :engine === "parquet" ?parquet :[ ] ;
57+
3258 if ( ! engine ) throw new Error ( "manifest has no files" ) ;
3359
60+ // Enable httpfs/arrow extensions
3461 try { await conn . query ( "INSTALL httpfs;" ) ; } catch { }
3562 try { await conn . query ( "LOAD httpfs;" ) ; } catch { }
3663 try { await conn . query ( "INSTALL arrow;" ) ; } catch { }
3764 try { await conn . query ( "LOAD arrow;" ) ; } catch { }
3865
66+ // Probe first file; fallback to parquet if arrow fails
3967 try {
4068 const probe = engine === "arrow" ?`read_ipc('${ files [ 0 ] } ')` :`read_parquet('${ files [ 0 ] } ')` ;
4169 await conn . query ( `SELECT 1 FROM ${ probe } LIMIT 1` ) ;
@@ -44,6 +72,7 @@ async function buildState(conn){
4472 else throw e ;
4573 }
4674
75+ // Chunked views to limit query string size
4776 const CHUNK = 16 , views = [ ] ;
4877 for ( let i = 0 ; i < files . length ; i += CHUNK ) {
4978 const group = files . slice ( i , i + CHUNK ) . map ( f => engine === "arrow" ?
@@ -53,18 +82,33 @@ async function buildState(conn){
5382 views . push ( v ) ;
5483 }
5584 await conn . query ( `CREATE OR REPLACE VIEW state AS ${ views . map ( v => `SELECT * FROM ${ v } ` ) . join ( " UNION ALL " ) } ` ) ;
56- if ( metaEl ) metaEl . textContent = `Files: ${ files . length } · Engine: read_${ engine } ` ;
85+
86+ if ( metaEl ) {
87+ // 人类可读:显示使用的 manifest 与引擎
88+ const used = manifestURL . href . replace ( location . origin , "" ) ;
89+ metaEl . textContent = `Files: ${ files . length } · Engine: read_${ engine } · Manifest: ${ used } ` ;
90+ }
5791}
5892
93+ // ---------- render ----------
5994function renderTable ( table ) {
6095 const rows = table . toArray ( ) ; const cols = table . schema . fields . map ( f => f . name ) ;
6196 let html = "<table><thead><tr>" ; for ( const c of cols ) html += `<th>${ c } </th>` ; html += "</tr></thead><tbody>" ;
6297 for ( const r of rows ) { html += "<tr>" ; for ( const c of cols ) { let v = r [ c ] ;
63- if ( v && ( v . BYTES_PER_ELEMENT || v instanceof ArrayBuffer ) ) { const b = v instanceof ArrayBuffer ?new Uint8Array ( v ) :new Uint8Array ( v . buffer || v ) ; const hex = Array . from ( b . slice ( 0 , 16 ) ) . map ( x => x . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ; v = `0x${ hex } ${ b . length > 16 ?'…' :'' } ` ; }
64- html += `<td>${ ( v === null || v === undefined ) ?"" :String ( v ) } </td>` ; } html += "</tr>" ; }
98+ if ( v && ( v . BYTES_PER_ELEMENT || v instanceof ArrayBuffer ) ) {
99+ const b = v instanceof ArrayBuffer ?new Uint8Array ( v ) :new Uint8Array ( v . buffer || v ) ;
100+ const hex = Array . from ( b . slice ( 0 , 16 ) ) . map ( x => x . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
101+ v = `0x${ hex } ${ b . length > 16 ?'…' :'' } ` ;
102+ }
103+ html += `<td>${ ( v === null || v === undefined ) ?"" :String ( v ) } </td>` ;
104+ } html += "</tr>" ; }
65105 html += "</tbody></table>" ; resultEl . innerHTML = html ;
66106}
67107
108+ // ---------- boot ----------
109+ function pickPreferBrowser ( bundles ) { return bundles . browser ?? bundles . mvp ?? Object . values ( bundles ) [ 0 ] ; }
110+ async function sameOriginWorkerURL ( url ) { const r = await fetch ( url , { cache :"no-store" } ) ; if ( ! r . ok ) throw new Error ( "fetch worker " + r . status ) ; return URL . createObjectURL ( new Blob ( [ await r . text ( ) ] , { type :"text/javascript" } ) ) ; }
111+
68112async function boot ( ) {
69113 try {
70114 setStatus ( "Booting…" ) ;
@@ -80,14 +124,14 @@ async function boot(){
80124 await buildState ( conn ) ;
81125 setStatus ( "Ready" ) ;
82126
83- document . getElementById ( "run" ) . onclick = async ( ) => {
84- const sql = ( document . getElementById ( "sql" ) . value || "" ) . trim ( ) ; if ( ! sql ) return ;
127+ $id ( "run" ) . onclick = async ( ) => {
128+ const sql = ( $id ( "sql" ) . value || "" ) . trim ( ) ; if ( ! sql ) return ;
85129 const t0 = performance . now ( ) ;
86130 try { const tbl = await conn . query ( sql ) ; renderTable ( tbl ) ; setStatus ( `Done in ${ ( performance . now ( ) - t0 ) . toFixed ( 0 ) } ms` ) ; }
87- catch ( e ) { console . error ( e ) ; setStatus ( "Error" , "#ffecec" ) ; if ( errorEl ) errorEl . textContent = e ?. message || String ( e ) ; }
131+ catch ( e ) { console . error ( e ) ; setStatus ( "Error" , "#ffecec" ) ; showError ( e ?. message || String ( e ) ) ; }
88132 } ;
89- document . getElementById ( "fill" ) . onclick = ( ) => { const q = document . getElementById ( "examples" ) . value ; if ( q ) document . getElementById ( "sql" ) . value = q ; } ;
90- document . getElementById ( "init" ) . onclick = async ( ) => { if ( errorEl ) errorEl . textContent = "" ; if ( resultEl ) resultEl . innerHTML = "" ; setStatus ( "Rebuilding…" ) ; await buildState ( conn ) ; setStatus ( "Ready" ) ; } ;
91- } catch ( e ) { console . error ( e ) ; setStatus ( "Boot error" , "#ffecec" ) ; if ( errorEl ) errorEl . textContent = e ?. message || String ( e ) ; }
133+ $id ( "fill" ) . onclick = ( ) => { const q = $id ( "examples" ) . value ; if ( q ) $id ( "sql" ) . value = q ; } ;
134+ $id ( "init" ) . onclick = async ( ) => { showError ( "" ) ; resultEl . innerHTML = "" ; setStatus ( "Rebuilding…" ) ; await buildState ( conn ) ; setStatus ( "Ready" ) ; } ;
135+ } catch ( e ) { console . error ( e ) ; setStatus ( "Boot error" , "#ffecec" ) ; showError ( e ?. message || String ( e ) ) ; }
92136}
93137boot ( ) ;
0 commit comments