@malloydata/malloyyo 0.2.20 → 0.2.21

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.
@@ -46,24 +46,30 @@ class StaticWasmConnection extends DuckDBWASMConnection {
46
46
  }
47
47
  }
48
48
 
49
- // Whole-file fetch, not ranged reads. For a columnar file whose dominant column
50
- // every query touches, ranged reads trade one bulk download for dozens of WAN
51
- // round trips and lose badly.
49
+ // Whole-file fetch, not ranged reads.
52
50
  //
53
- // This CANNOT go through registerRemoteTableCallback: db-duckdb's findTables
54
- // skips anything matching ^https?:// ("handled by duckdb-wasm") so the callback
55
- // never fires for a remote URL. Instead we pre-register the bytes under the
56
- // exact URL string before any query runs duckdb-wasm resolves its registered
57
- // files by name first, so the table reference finds the buffer and never issues
58
- // an HTTP request. The URL list is extracted from the model at build time.
59
- const REMOTE_TABLES: string[] = (window as any).__REMOTE_TABLES__ || [];
60
-
61
- async function preloadRemoteTables(db: any) {
51
+ // __TABLE_FILES__ maps each table reference the model makes to the URL this
52
+ // page fetches for it. The KEY is what the model wrote and what DuckDB will
53
+ // look up, so registering the bytes under that exact name means the model needs
54
+ // no rewriting between `dashboard dev` (reads the file off disk) and here.
55
+ //
56
+ // Project-relative refs like `data/x.parquet` resolve against the page, so they
57
+ // are SAME-ORIGIN no CORS involved at all. Absolute URLs are fetched
58
+ // cross-origin and do need CORS on that host.
59
+ //
60
+ // This cannot go through db-duckdb's registerRemoteTableCallback: findTables
61
+ // skips anything matching ^https?:// ("handled by duckdb-wasm"), so the callback
62
+ // never fires for a remote URL. Pre-registering is also strictly better — one
63
+ // bulk GET instead of dozens of ranged round trips.
64
+ const TABLE_FILES: Record<string, string> = (window as any).__TABLE_FILES__ || {};
65
+
66
+ async function preloadTables(db: any) {
62
67
  await Promise.all(
63
- REMOTE_TABLES.map(async (url) => {
68
+ Object.entries(TABLE_FILES).map(async ([name, href]) => {
69
+ const url = new URL(href, document.baseURI).href;
64
70
  const r = await fetch(url);
65
71
  if (!r.ok) throw new Error(`fetch ${url} failed: ${r.status} ${r.statusText}`);
66
- await db.registerFileBuffer(url, new Uint8Array(await r.arrayBuffer()));
72
+ await db.registerFileBuffer(name, new Uint8Array(await r.arrayBuffer()));
67
73
  }),
68
74
  );
69
75
  }
@@ -89,7 +95,7 @@ function getRuntime() {
89
95
  runtimeP = (async () => {
90
96
  const connection = new StaticWasmConnection({ name: "duckdb" });
91
97
  await connection.connecting;
92
- await preloadRemoteTables((connection as any).database);
98
+ await preloadTables((connection as any).database);
93
99
  return new SingleConnectionRuntime({ connection, urlReader });
94
100
  })();
95
101
  }
package/dist/index.js CHANGED
@@ -3202,13 +3202,54 @@ function inlineModelFiles(root) {
3202
3202
  walk(root);
3203
3203
  return files;
3204
3204
  }
3205
- function findRemoteTables(modelFiles) {
3206
- const urls = /* @__PURE__ */ new Set();
3207
- const re = /\btable\(\s*(['"])(https?:\/\/[^'"]+)\1/g;
3205
+ function reachableModelFiles(modelFiles, entries) {
3206
+ const IMPORT = /\bimport\s+(?:\{[^}]*\}\s+from\s+)?['"]([^'"]+)['"]/g;
3207
+ const out = {};
3208
+ const queue = entries.map((e) => `file:///${e.replace(/^\.?\//, "")}`);
3209
+ while (queue.length) {
3210
+ const key = queue.shift();
3211
+ if (key in out) continue;
3212
+ const src = modelFiles[key];
3213
+ if (src == null) continue;
3214
+ out[key] = src;
3215
+ const dir = key.slice(0, key.lastIndexOf("/"));
3216
+ for (const m of src.matchAll(IMPORT)) {
3217
+ queue.push(new URL(m[1], dir + "/").href);
3218
+ }
3219
+ }
3220
+ return out;
3221
+ }
3222
+ function findTableRefs(modelFiles) {
3223
+ const refs = /* @__PURE__ */ new Set();
3224
+ const re = /\btable\(\s*(['"])([^'"]+)\1/g;
3208
3225
  for (const src of Object.values(modelFiles)) {
3209
- for (const m of src.matchAll(re)) urls.add(m[2]);
3226
+ for (const m of src.matchAll(re)) refs.add(m[2]);
3227
+ }
3228
+ return [...refs].sort();
3229
+ }
3230
+ function isDataFile(ref) {
3231
+ if (/^https?:\/\//i.test(ref)) return true;
3232
+ return /\.(parquet|csv|tsv|json|ndjson)$/i.test(ref);
3233
+ }
3234
+ function tableFilePlan(modelFiles, outRel) {
3235
+ const map = {};
3236
+ const copies = [];
3237
+ const out = outRel.replace(/^\.?\//, "").replace(/\/$/, "");
3238
+ for (const ref of findTableRefs(modelFiles)) {
3239
+ if (!isDataFile(ref)) continue;
3240
+ if (/^https?:\/\//i.test(ref)) {
3241
+ map[ref] = ref;
3242
+ continue;
3243
+ }
3244
+ const rel = ref.replace(/^\.?\//, "");
3245
+ if (out && (rel === out || rel.startsWith(out + "/"))) {
3246
+ map[ref] = `./${rel.slice(out.length + 1)}`;
3247
+ } else {
3248
+ map[ref] = `./${rel}`;
3249
+ copies.push(rel);
3250
+ }
3210
3251
  }
3211
- return [...urls].sort();
3252
+ return { map, copies };
3212
3253
  }
3213
3254
  function copyDuckDBAssets(outDir) {
3214
3255
  const names = [
@@ -3321,6 +3362,12 @@ async function bundleDashboards(opts = {}) {
3321
3362
  const runner = await makeRunner(root);
3322
3363
  const dashboards = await discoverDashboards(root, runner);
3323
3364
  if (dashboards.length === 0) throw new Error(`no dashboards found in ${path7.join(root, "dashboards")}`);
3365
+ const manifestPath = path7.join(outDir, ".bundle-manifest.json");
3366
+ let priorData = [];
3367
+ try {
3368
+ priorData = JSON.parse(fs6.readFileSync(manifestPath, "utf8")).dataFiles ?? [];
3369
+ } catch {
3370
+ }
3324
3371
  for (const sub of ["assets", "duckdb"]) {
3325
3372
  fs6.rmSync(path7.join(outDir, sub), { recursive: true, force: true });
3326
3373
  }
@@ -3357,11 +3404,39 @@ async function bundleDashboards(opts = {}) {
3357
3404
  );
3358
3405
  }
3359
3406
  const modelFiles = inlineModelFiles(root);
3360
- const remoteTables = findRemoteTables(modelFiles);
3407
+ const outRel = path7.relative(root, outDir).split(path7.sep).join("/");
3408
+ const usedFiles = reachableModelFiles(
3409
+ modelFiles,
3410
+ dashboards.map((d) => d.entryFile).filter((f) => !!f)
3411
+ );
3412
+ const { map: tableFiles, copies } = tableFilePlan(usedFiles, outRel);
3413
+ for (const rel of copies) {
3414
+ const from = path7.join(root, rel);
3415
+ if (!fs6.existsSync(from)) {
3416
+ throw new Error(
3417
+ `model reads '${rel}' but ${from} does not exist.
3418
+ Data files are referenced by a path relative to the project root.`
3419
+ );
3420
+ }
3421
+ const to = path7.join(outDir, rel);
3422
+ fs6.mkdirSync(path7.dirname(to), { recursive: true });
3423
+ fs6.copyFileSync(from, to);
3424
+ }
3425
+ const copiedData = copies;
3426
+ for (const stale of priorData) {
3427
+ if (copies.includes(stale)) continue;
3428
+ fs6.rmSync(path7.join(outDir, stale), { force: true });
3429
+ try {
3430
+ fs6.rmdirSync(path7.dirname(path7.join(outDir, stale)));
3431
+ } catch {
3432
+ }
3433
+ console.log(` removed stale ${stale}`);
3434
+ }
3435
+ fs6.writeFileSync(manifestPath, JSON.stringify({ dataFiles: copies }, null, 2) + "\n");
3361
3436
  fs6.writeFileSync(
3362
3437
  path7.join(outDir, "assets", "model-files.js"),
3363
3438
  `window.__MODEL_FILES__ = ${JSON.stringify(modelFiles)};
3364
- window.__REMOTE_TABLES__ = ${JSON.stringify(remoteTables)};
3439
+ window.__TABLE_FILES__ = ${JSON.stringify(tableFiles)};
3365
3440
  ` + (selfHostDuckdb ? `window.__DUCKDB_BASE__ = "./duckdb/";
3366
3441
  ` : "")
3367
3442
  );
@@ -3458,6 +3533,10 @@ bundled ${dashboards.length} dashboard(s) \u2192 ${path7.relative(process.cwd(),
3458
3533
  selfHostDuckdb ? ` duckdb ${duck.length} assets (self-hosted)` : ` duckdb jsDelivr CDN (nothing copied)`
3459
3534
  );
3460
3535
  console.log(` model ${Object.keys(modelFiles).length} .malloy files inlined`);
3536
+ if (copiedData.length) {
3537
+ const mb = copiedData.reduce((a, r) => a + fs6.statSync(path7.join(root, r)).size, 0) / 1048576;
3538
+ console.log(` data ${copiedData.length} file(s) copied, ${mb.toFixed(1)} MB (same-origin)`);
3539
+ }
3461
3540
  console.log(
3462
3541
  target === "pages" ? `
3463
3542
  Publish: commit ${path7.basename(outDir)}/ and point GitHub Pages at it.` : `
@@ -3589,7 +3668,7 @@ async function launchCmd(mode, opts) {
3589
3668
  }
3590
3669
 
3591
3670
  // package.json
3592
- var version = "0.2.20";
3671
+ var version = "0.2.21";
3593
3672
 
3594
3673
  // src/index.ts
3595
3674
  function shortSha(sha) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloyyo",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Publish Malloy models to a Malloyyo instance",
5
5
  "license": "MIT",
6
6
  "repository": {