Skip to content

Commit 8f3adc3

Browse files
authored
Merge pull request #31 from xxxbrian/main
Support CDN loading for DuckDB WASM
2 parents 3839494 + 170f307 commit 8f3adc3

File tree

10 files changed

+140
-32
lines changed

10 files changed

+140
-32
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ docker run -p 5522:5522 \
4141
ghcr.io/ibero-data/duck-ui:latest
4242
```
4343

44-
| Variable | Description | Default |
44+
| Runtime Variable | Description | Default |
4545
|----------|-------------|---------|
4646
| `DUCK_UI_EXTERNAL_CONNECTION_NAME` | Name for the external connection | "" |
4747
| `DUCK_UI_EXTERNAL_HOST` | Host URL for external DuckDB | "" |
@@ -50,6 +50,14 @@ docker run -p 5522:5522 \
5050
| `DUCK_UI_EXTERNAL_PASS` | Password for external connection | "" |
5151
| `DUCK_UI_EXTERNAL_DATABASE_NAME` | Database name for external connection | "" |
5252
| `DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS` | Allow unsigned extensions in DuckDB | false |
53+
| `DUCK_UI_DUCKDB_WASM_USE_CDN` | Load DuckDB WASM from CDN (ignored when build-time `DUCK_UI_DUCKDB_WASM_CDN_ONLY=true`) | false |
54+
| `DUCK_UI_DUCKDB_WASM_BASE_URL` | Custom CDN base URL (used when `DUCK_UI_DUCKDB_WASM_USE_CDN=true`) | auto jsDelivr |
55+
56+
| Build-time Variable | Description | Default |
57+
|----------|-------------|---------|
58+
| `DUCK_UI_DUCKDB_WASM_CDN_ONLY` | Build a CDN-only artifact (local DuckDB WASM assets are not bundled). | false |
59+
60+
When `DUCK_UI_DUCKDB_WASM_CDN_ONLY=true`, runtime `DUCK_UI_DUCKDB_WASM_USE_CDN=false` cannot switch back to local WASM.
5361

5462

5563

@@ -197,4 +205,4 @@ This project is sponsored by:
197205

198206
<br/>
199207

200-
Want to be a sponsor? [Contact us](mailto:caio.ricciuti+sponsorship@outlook.com).
208+
Want to be a sponsor? [Contact us](mailto:caio.ricciuti+sponsorship@outlook.com).

docs/environment-variables.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,21 @@ These variables allow you to configure Duck-UI to connect to an external DuckDB
1717
| `DUCK_UI_EXTERNAL_PASS` | Password for authentication | No | `""` | `"your-password"` |
1818
| `DUCK_UI_EXTERNAL_DATABASE_NAME` | Database name to connect to | No | `""` | `"analytics"` |
1919

20-
### Extension Settings
20+
### Runtime Settings
2121

2222
| Variable | Description | Required | Default | Example |
2323
|----------|-------------|----------|---------|---------|
2424
| `DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS` | Allow loading unsigned DuckDB extensions | No | `false` | `"true"` |
25+
| `DUCK_UI_DUCKDB_WASM_USE_CDN` | Enable loading DuckDB WASM and worker files from CDN (ignored when `DUCK_UI_DUCKDB_WASM_CDN_ONLY=true` at build time) | No | `false` | `"true"` |
26+
| `DUCK_UI_DUCKDB_WASM_BASE_URL` | Custom CDN base URL (when `DUCK_UI_DUCKDB_WASM_USE_CDN=true`) | No | Auto (`duckdb.getJsDelivrBundles()`) | `"https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.33.1-dev19.0/dist"` |
27+
28+
### Build-time Settings
29+
30+
| Variable | Description | Required | Default | Example |
31+
|----------|-------------|----------|---------|---------|
32+
| `DUCK_UI_DUCKDB_WASM_CDN_ONLY` | Build a CDN-only artifact (local DuckDB WASM files are not bundled). Suitable for edge platforms with strict asset size limits. | No | `false` | `"true"` |
33+
34+
When `DUCK_UI_DUCKDB_WASM_CDN_ONLY=true`, runtime `DUCK_UI_DUCKDB_WASM_USE_CDN=false` cannot switch back to local WASM because local assets are not included in the build.
2535

2636
::: warning Security Note
2737
Enabling unsigned extensions may pose security risks. Only enable this in trusted environments.

inject-env.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ const envVars = {
1616
process.env.DUCK_UI_EXTERNAL_DATABASE_NAME || "",
1717
// Add new configuration for DuckDB settings
1818
DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS:
19-
process.env.DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS === "true" || false
19+
process.env.DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS === "true" || false,
20+
DUCK_UI_DUCKDB_WASM_USE_CDN:
21+
process.env.DUCK_UI_DUCKDB_WASM_USE_CDN === "true" || false,
22+
DUCK_UI_DUCKDB_WASM_BASE_URL:
23+
process.env.DUCK_UI_DUCKDB_WASM_BASE_URL || ""
2024
};
2125

2226
const scriptContent = `

src/services/duckdb/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Extracted from the monolithic store for testability and modularity.
33

44
export { rawResultToJSON, resultToJSON } from "./resultParser";
5-
export { initializeWasmConnection, loadEmbeddedDatabases, MANUAL_BUNDLES } from "./wasmConnection";
5+
export { initializeWasmConnection, loadEmbeddedDatabases, resolveDuckdbBundles } from "./wasmConnection";
66
export { cleanupOPFSConnection, testOPFSConnection, opfsActivePaths } from "./opfsConnection";
77
export {
88
executeExternalQuery,

src/services/duckdb/opfsConnection.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as duckdb from "@duckdb/duckdb-wasm";
22
import { retryWithBackoff, validateConnection } from "./utils";
3-
import { MANUAL_BUNDLES } from "./wasmConnection";
3+
import { createDuckdbWorker, resolveDuckdbBundles } from "./wasmConnection";
44
import type { ConnectionProvider } from "@/store/types";
55

66
// OPFS connection tracking to prevent concurrent access
@@ -61,12 +61,17 @@ export const testOPFSConnection = async (
6161
);
6262
}
6363

64-
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
65-
const worker = new Worker(bundle.mainWorker!);
64+
const bundles = await resolveDuckdbBundles();
65+
const bundle = await duckdb.selectBundle(bundles);
66+
const { worker, revoke } = createDuckdbWorker(bundle.mainWorker!);
6667
const logger = new duckdb.VoidLogger();
6768
const db = new duckdb.AsyncDuckDB(logger, worker);
6869

69-
await db.instantiate(bundle.mainModule);
70+
try {
71+
await db.instantiate(bundle.mainModule);
72+
} finally {
73+
revoke();
74+
}
7075

7176
// Use retry with exponential backoff for OPFS access handle conflicts
7277
await retryWithBackoff(

src/services/duckdb/wasmConnection.ts

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,87 @@
11
import * as duckdb from "@duckdb/duckdb-wasm";
22
import { validateConnection } from "./utils";
33

4-
// Import WASM bundles
5-
import duckdb_wasm from "@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url";
6-
import mvp_worker from "@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url";
7-
import duckdb_wasm_eh from "@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url";
8-
import eh_worker from "@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url";
9-
10-
export const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
11-
mvp: {
12-
mainModule: duckdb_wasm,
13-
mainWorker: mvp_worker,
14-
},
15-
eh: {
16-
mainModule: duckdb_wasm_eh,
17-
mainWorker: eh_worker,
18-
},
4+
const getRuntimeEnv = (): Partial<NonNullable<Window["env"]>> =>
5+
(window.env ?? {}) as Partial<NonNullable<Window["env"]>>;
6+
7+
const getCdnBundles = (runtimeEnv: Partial<NonNullable<Window["env"]>>): duckdb.DuckDBBundles => {
8+
const configuredBaseUrl = runtimeEnv.DUCK_UI_DUCKDB_WASM_BASE_URL ?? "";
9+
if (!configuredBaseUrl) {
10+
return duckdb.getJsDelivrBundles();
11+
}
12+
13+
const baseUrl = configuredBaseUrl.replace(/\/+$/, "");
14+
15+
return {
16+
mvp: {
17+
mainModule: `${baseUrl}/duckdb-mvp.wasm`,
18+
mainWorker: `${baseUrl}/duckdb-browser-mvp.worker.js`,
19+
},
20+
eh: {
21+
mainModule: `${baseUrl}/duckdb-eh.wasm`,
22+
mainWorker: `${baseUrl}/duckdb-browser-eh.worker.js`,
23+
},
24+
};
25+
};
26+
27+
export const resolveDuckdbBundles: () => Promise<duckdb.DuckDBBundles> =
28+
__DUCK_UI_BUILD_DUCKDB_CDN_ONLY__
29+
? async () => getCdnBundles(getRuntimeEnv())
30+
: (() => {
31+
let localBundlesPromise: Promise<duckdb.DuckDBBundles> | null = null;
32+
33+
const getLocalBundles = async (): Promise<duckdb.DuckDBBundles> => {
34+
if (!localBundlesPromise) {
35+
localBundlesPromise = Promise.all([
36+
import("@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url"),
37+
import("@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url"),
38+
import("@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url"),
39+
import("@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url"),
40+
]).then(([duckdbWasm, mvpWorker, duckdbWasmEh, ehWorker]) => ({
41+
mvp: {
42+
mainModule: duckdbWasm.default,
43+
mainWorker: mvpWorker.default,
44+
},
45+
eh: {
46+
mainModule: duckdbWasmEh.default,
47+
mainWorker: ehWorker.default,
48+
},
49+
})).catch((error) => {
50+
// Allow retry after transient chunk/load failures.
51+
localBundlesPromise = null;
52+
throw error;
53+
});
54+
}
55+
56+
return localBundlesPromise;
57+
};
58+
59+
return async () => {
60+
const runtimeEnv = getRuntimeEnv();
61+
if (runtimeEnv.DUCK_UI_DUCKDB_WASM_USE_CDN === true) {
62+
return getCdnBundles(runtimeEnv);
63+
}
64+
return getLocalBundles();
65+
};
66+
})();
67+
68+
export const createDuckdbWorker = (
69+
mainWorkerUrl: string
70+
): { worker: Worker; revoke: () => void } => {
71+
if (/^https?:\/\//i.test(mainWorkerUrl)) {
72+
const workerUrl = URL.createObjectURL(
73+
new Blob([`importScripts(${JSON.stringify(mainWorkerUrl)});`], { type: "text/javascript" })
74+
);
75+
return {
76+
worker: new Worker(workerUrl),
77+
revoke: () => URL.revokeObjectURL(workerUrl),
78+
};
79+
}
80+
81+
return {
82+
worker: new Worker(mainWorkerUrl),
83+
revoke: () => {},
84+
};
1985
};
2086

2187
/**
@@ -25,8 +91,9 @@ export const initializeWasmConnection = async (): Promise<{
2591
db: duckdb.AsyncDuckDB;
2692
connection: duckdb.AsyncDuckDBConnection;
2793
}> => {
28-
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
29-
const worker = new Worker(bundle.mainWorker!);
94+
const bundles = await resolveDuckdbBundles();
95+
const bundle = await duckdb.selectBundle(bundles);
96+
const { worker, revoke } = createDuckdbWorker(bundle.mainWorker!);
3097
const logger = new duckdb.VoidLogger();
3198

3299
// Check if unsigned extensions are allowed from environment
@@ -35,7 +102,11 @@ export const initializeWasmConnection = async (): Promise<{
35102
// Create database with configuration
36103
const db = new duckdb.AsyncDuckDB(logger, worker);
37104

38-
await db.instantiate(bundle.mainModule);
105+
try {
106+
await db.instantiate(bundle.mainModule);
107+
} finally {
108+
revoke();
109+
}
39110

40111
const dbConfig: duckdb.DuckDBConfig = {
41112
allowUnsignedExtensions: allowUnsignedExtensions,

src/services/persistence/systemDb.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import * as duckdb from "@duckdb/duckdb-wasm";
10-
import { MANUAL_BUNDLES } from "@/services/duckdb/wasmConnection";
10+
import { createDuckdbWorker, resolveDuckdbBundles } from "@/services/duckdb/wasmConnection";
1111
import { runMigrations } from "./migrations";
1212

1313
// Singleton state
@@ -62,12 +62,17 @@ export async function initializeSystemDb(): Promise<void> {
6262
try {
6363
console.info("[SystemDB] Initializing system database (OPFS)...");
6464

65-
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
66-
const worker = new Worker(bundle.mainWorker!);
65+
const bundles = await resolveDuckdbBundles();
66+
const bundle = await duckdb.selectBundle(bundles);
67+
const { worker, revoke } = createDuckdbWorker(bundle.mainWorker!);
6768
const logger = new duckdb.VoidLogger();
6869

6970
systemDb = new duckdb.AsyncDuckDB(logger, worker);
70-
await systemDb.instantiate(bundle.mainModule);
71+
try {
72+
await systemDb.instantiate(bundle.mainModule);
73+
} finally {
74+
revoke();
75+
}
7176

7277
await systemDb.open({
7378
path: `opfs://${SYSTEM_DB_PATH}`,

src/store/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ declare global {
1515
DUCK_UI_EXTERNAL_PASS: string;
1616
DUCK_UI_EXTERNAL_DATABASE_NAME: string;
1717
DUCK_UI_ALLOW_UNSIGNED_EXTENSIONS: boolean;
18+
DUCK_UI_DUCKDB_WASM_USE_CDN?: boolean;
19+
DUCK_UI_DUCKDB_WASM_BASE_URL?: string;
1820
};
1921
}
2022
}

src/vite-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
declare const __DUCK_UI_VERSION__: string;
44
declare const __DUCK_UI_RELEASE_DATE__: string;
5+
declare const __DUCK_UI_BUILD_DUCKDB_CDN_ONLY__: boolean;

vite.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import tailwindcss from '@tailwindcss/vite';
66

77
export default defineConfig(({ mode }) => {
88
const env = loadEnv(mode, process.cwd(), '');
9+
const buildDuckdbCdnOnly = env.DUCK_UI_DUCKDB_WASM_CDN_ONLY === 'true';
910

1011
// Manually construct the object to be defined
1112
// Filter out keys with invalid JS identifier characters (fixes Windows builds where
@@ -28,7 +29,8 @@ export default defineConfig(({ mode }) => {
2829
define: {
2930
__DUCK_UI_VERSION__: JSON.stringify(pkg.version),
3031
__DUCK_UI_RELEASE_DATE__: JSON.stringify(pkg.release_date),
32+
__DUCK_UI_BUILD_DUCKDB_CDN_ONLY__: JSON.stringify(buildDuckdbCdnOnly),
3133
...processEnvValues // Spread the processed variables
3234
},
3335
};
34-
});
36+
});

0 commit comments

Comments
 (0)