Skip to content

Commit 1dc0886

Browse files
committed
feat(cli,web): add lake command for remote data sync
1 parent 6215994 commit 1dc0886

27 files changed

Lines changed: 374 additions & 123 deletions

.github/workflows/pages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: Verify static files exist
2626
run: |
2727
test -f apps/web/index.html
28-
test -f apps/web/data/manifest.json
28+
test -f apps/web/data/fixed/manifest.json
2929
3030
- name: Upload artifact (apps/web/)
3131
uses: actions/upload-pages-artifact@v3

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ build/
3232

3333
# Node
3434
node_modules/
35+
36+
apps/web/data/local/
37+
apps/web/data/_backups/
38+
apps/web/data/*/objects/

apps/web/app.js

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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
22
import * as duckdb from "https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.28.0/+esm";
33

44
const $id = (id) => document.getElementById(id);
@@ -12,30 +12,58 @@ const examples = $id("examples");
1212
const setStatus = (t, bg="#eef5ff") => { if (statusEl) { statusEl.textContent = t; statusEl.style.background = bg; }};
1313
const 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 ----------
1835
async 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

2853
async 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 ----------
5994
function 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+
68112
async 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
}
93137
boot();

apps/web/data/1456cfb63a334a39a06df3ee120daafb-0.arrow renamed to apps/web/data/fixed/1456cfb63a334a39a06df3ee120daafb-0.arrow

File renamed without changes.

apps/web/data/2bb64d90458245e990c29acd605dadce-0.arrow renamed to apps/web/data/fixed/2bb64d90458245e990c29acd605dadce-0.arrow

File renamed without changes.

apps/web/data/7721bc175b1a4194a04f2779536f0d15-0.arrow renamed to apps/web/data/fixed/7721bc175b1a4194a04f2779536f0d15-0.arrow

File renamed without changes.

apps/web/data/chain_id=1/date=2025-08-10/topic=raw/part-1754810196797.arrow renamed to apps/web/data/fixed/chain_id=1/date=2025-08-10/topic=raw/part-1754810196797.arrow

File renamed without changes.

apps/web/data/chain_id=1/date=2025-08-10/topic=raw/part-1754810196797.parquet renamed to apps/web/data/fixed/chain_id=1/date=2025-08-10/topic=raw/part-1754810196797.parquet

File renamed without changes.

apps/web/data/chain_id=1/date=2025-08-10/topic=raw/part-1754810224199.arrow renamed to apps/web/data/fixed/chain_id=1/date=2025-08-10/topic=raw/part-1754810224199.arrow

File renamed without changes.

apps/web/data/chain_id=1/date=2025-08-10/topic=raw/part-1754810224199.parquet renamed to apps/web/data/fixed/chain_id=1/date=2025-08-10/topic=raw/part-1754810224199.parquet

File renamed without changes.

0 commit comments

Comments
 (0)