@vibedash/client 0.1.0 → 0.2.0
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.
- package/dist/duckdb-singleton.d.ts +13 -0
- package/dist/duckdb-singleton.js +44 -0
- package/dist/types.d.ts +5 -1
- package/dist/use-query.d.ts +15 -5
- package/dist/use-query.js +54 -10
- package/package.json +12 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy-initialized DuckDB-WASM singleton.
|
|
3
|
+
*
|
|
4
|
+
* Only loaded when the first cached query is detected — dashboards that don't
|
|
5
|
+
* use caching never pay the bundle cost (~200KB JS + 4MB WASM).
|
|
6
|
+
*/
|
|
7
|
+
interface DuckDBInstance {
|
|
8
|
+
query: (sql: string) => Promise<Record<string, unknown>[]>;
|
|
9
|
+
loadParquet: (name: string, url: string) => Promise<void>;
|
|
10
|
+
isTableLoaded: (name: string) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function getDuckDB(): Promise<DuckDBInstance>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy-initialized DuckDB-WASM singleton.
|
|
3
|
+
*
|
|
4
|
+
* Only loaded when the first cached query is detected — dashboards that don't
|
|
5
|
+
* use caching never pay the bundle cost (~200KB JS + 4MB WASM).
|
|
6
|
+
*/
|
|
7
|
+
let dbPromise = null;
|
|
8
|
+
export function getDuckDB() {
|
|
9
|
+
if (!dbPromise) {
|
|
10
|
+
dbPromise = initDuckDB();
|
|
11
|
+
}
|
|
12
|
+
return dbPromise;
|
|
13
|
+
}
|
|
14
|
+
async function initDuckDB() {
|
|
15
|
+
const duckdb = await import("@duckdb/duckdb-wasm");
|
|
16
|
+
// Use CDN-hosted bundles (browser caches these)
|
|
17
|
+
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
|
|
18
|
+
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
|
|
19
|
+
const worker = new Worker(bundle.mainWorker);
|
|
20
|
+
const logger = new duckdb.ConsoleLogger();
|
|
21
|
+
const db = new duckdb.AsyncDuckDB(logger, worker);
|
|
22
|
+
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
|
|
23
|
+
const conn = await db.connect();
|
|
24
|
+
const loadedTables = new Set();
|
|
25
|
+
return {
|
|
26
|
+
async query(sql) {
|
|
27
|
+
const result = await conn.query(sql);
|
|
28
|
+
return result.toArray().map((row) => row.toJSON());
|
|
29
|
+
},
|
|
30
|
+
async loadParquet(name, url) {
|
|
31
|
+
if (loadedTables.has(name))
|
|
32
|
+
return;
|
|
33
|
+
// Fetch the Parquet file and register it
|
|
34
|
+
const response = await fetch(url);
|
|
35
|
+
const buffer = await response.arrayBuffer();
|
|
36
|
+
await db.registerFileBuffer(`${name}.parquet`, new Uint8Array(buffer));
|
|
37
|
+
await conn.query(`CREATE TABLE IF NOT EXISTS "${name}" AS SELECT * FROM read_parquet('${name}.parquet')`);
|
|
38
|
+
loadedTables.add(name);
|
|
39
|
+
},
|
|
40
|
+
isTableLoaded(name) {
|
|
41
|
+
return loadedTables.has(name);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface UseQueryResult<T = Record<string, unknown>[]> {
|
|
|
17
17
|
loading: boolean;
|
|
18
18
|
/** Error message if the query failed */
|
|
19
19
|
error: string | null;
|
|
20
|
-
/** Re-run the query manually */
|
|
20
|
+
/** Re-run the query manually (forces cache refresh if cached) */
|
|
21
21
|
refetch: () => Promise<void>;
|
|
22
|
+
/** When the cached data was last refreshed (undefined if not cached) */
|
|
23
|
+
cachedAt?: Date;
|
|
24
|
+
/** Run SQL locally against cached data via DuckDB-WASM (undefined if not cached) */
|
|
25
|
+
query?: (sql: string) => Promise<Record<string, unknown>[]>;
|
|
22
26
|
}
|
package/dist/use-query.d.ts
CHANGED
|
@@ -2,13 +2,23 @@ import type { UseQueryOptions, UseQueryResult } from "./types";
|
|
|
2
2
|
/**
|
|
3
3
|
* Fetches data from a registered query by name.
|
|
4
4
|
*
|
|
5
|
+
* If the dashboard has a cache TTL set in the Hub, data is served from a cached
|
|
6
|
+
* Parquet file and loaded into DuckDB-WASM in the browser. This enables instant
|
|
7
|
+
* client-side SQL filtering via the returned `query()` function.
|
|
8
|
+
*
|
|
9
|
+
* If no cache TTL is set, data comes from BigQuery live (existing behavior).
|
|
10
|
+
*
|
|
5
11
|
* Usage:
|
|
6
12
|
* ```tsx
|
|
7
|
-
* const { data, loading,
|
|
8
|
-
* ```
|
|
13
|
+
* const { data, query, loading, cachedAt } = useQuery("monthly_revenue");
|
|
9
14
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
15
|
+
* // All rows (works with or without cache)
|
|
16
|
+
* const allRows = data;
|
|
17
|
+
*
|
|
18
|
+
* // Filter locally (only when cached)
|
|
19
|
+
* if (query) {
|
|
20
|
+
* const filtered = await query("SELECT * FROM monthly_revenue WHERE region = 'US'");
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
13
23
|
*/
|
|
14
24
|
export declare function useQuery<T = Record<string, unknown>[]>(queryName: string, options?: UseQueryOptions): UseQueryResult<T>;
|
package/dist/use-query.js
CHANGED
|
@@ -1,24 +1,38 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from "react";
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
2
|
import { useVibeDash } from "./provider";
|
|
3
3
|
/**
|
|
4
4
|
* Fetches data from a registered query by name.
|
|
5
5
|
*
|
|
6
|
+
* If the dashboard has a cache TTL set in the Hub, data is served from a cached
|
|
7
|
+
* Parquet file and loaded into DuckDB-WASM in the browser. This enables instant
|
|
8
|
+
* client-side SQL filtering via the returned `query()` function.
|
|
9
|
+
*
|
|
10
|
+
* If no cache TTL is set, data comes from BigQuery live (existing behavior).
|
|
11
|
+
*
|
|
6
12
|
* Usage:
|
|
7
13
|
* ```tsx
|
|
8
|
-
* const { data, loading,
|
|
9
|
-
*
|
|
14
|
+
* const { data, query, loading, cachedAt } = useQuery("monthly_revenue");
|
|
15
|
+
*
|
|
16
|
+
* // All rows (works with or without cache)
|
|
17
|
+
* const allRows = data;
|
|
10
18
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
19
|
+
* // Filter locally (only when cached)
|
|
20
|
+
* if (query) {
|
|
21
|
+
* const filtered = await query("SELECT * FROM monthly_revenue WHERE region = 'US'");
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
14
24
|
*/
|
|
15
25
|
export function useQuery(queryName, options) {
|
|
16
26
|
const { apiUrl, projectSlug, apiKey } = useVibeDash();
|
|
17
27
|
const [data, setData] = useState(null);
|
|
18
28
|
const [loading, setLoading] = useState(false);
|
|
19
29
|
const [error, setError] = useState(null);
|
|
30
|
+
const [cachedAt, setCachedAt] = useState(undefined);
|
|
31
|
+
// Store the query function in a ref so it's stable
|
|
32
|
+
const queryFnRef = useRef(undefined);
|
|
33
|
+
const [queryReady, setQueryReady] = useState(false);
|
|
20
34
|
const enabled = options?.enabled !== false;
|
|
21
|
-
const fetchData = useCallback(async () => {
|
|
35
|
+
const fetchData = useCallback(async (forceRefresh = false) => {
|
|
22
36
|
setLoading(true);
|
|
23
37
|
setError(null);
|
|
24
38
|
try {
|
|
@@ -35,6 +49,7 @@ export function useQuery(queryName, options) {
|
|
|
35
49
|
body: JSON.stringify({
|
|
36
50
|
project_slug: projectSlug,
|
|
37
51
|
query_name: queryName,
|
|
52
|
+
force_refresh: forceRefresh,
|
|
38
53
|
}),
|
|
39
54
|
});
|
|
40
55
|
if (!res.ok) {
|
|
@@ -42,7 +57,29 @@ export function useQuery(queryName, options) {
|
|
|
42
57
|
throw new Error(body.error || `Query failed with status ${res.status}`);
|
|
43
58
|
}
|
|
44
59
|
const body = await res.json();
|
|
45
|
-
|
|
60
|
+
if (body.cached && body.parquet_url) {
|
|
61
|
+
// Cached response — load Parquet into DuckDB-WASM
|
|
62
|
+
const { getDuckDB } = await import("./duckdb-singleton");
|
|
63
|
+
const db = await getDuckDB();
|
|
64
|
+
// Force reload on refresh, otherwise reuse if already loaded
|
|
65
|
+
if (forceRefresh || !db.isTableLoaded(queryName)) {
|
|
66
|
+
await db.loadParquet(queryName, body.parquet_url);
|
|
67
|
+
}
|
|
68
|
+
// Read all rows as default data
|
|
69
|
+
const allRows = await db.query(`SELECT * FROM "${queryName}"`);
|
|
70
|
+
setData(allRows);
|
|
71
|
+
setCachedAt(new Date(body.cached_at));
|
|
72
|
+
// Expose local SQL query function
|
|
73
|
+
queryFnRef.current = (sql) => db.query(sql);
|
|
74
|
+
setQueryReady(true);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Live response — standard JSON data
|
|
78
|
+
setData(body.data);
|
|
79
|
+
setCachedAt(undefined);
|
|
80
|
+
queryFnRef.current = undefined;
|
|
81
|
+
setQueryReady(false);
|
|
82
|
+
}
|
|
46
83
|
}
|
|
47
84
|
catch (err) {
|
|
48
85
|
const message = err instanceof Error ? err.message : "Failed to fetch data";
|
|
@@ -51,11 +88,18 @@ export function useQuery(queryName, options) {
|
|
|
51
88
|
finally {
|
|
52
89
|
setLoading(false);
|
|
53
90
|
}
|
|
54
|
-
}, [apiUrl, projectSlug, queryName]);
|
|
91
|
+
}, [apiUrl, projectSlug, queryName, apiKey]);
|
|
55
92
|
useEffect(() => {
|
|
56
93
|
if (enabled) {
|
|
57
94
|
fetchData();
|
|
58
95
|
}
|
|
59
96
|
}, [enabled, fetchData]);
|
|
60
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
data,
|
|
99
|
+
loading,
|
|
100
|
+
error,
|
|
101
|
+
cachedAt,
|
|
102
|
+
query: queryReady ? queryFnRef.current : undefined,
|
|
103
|
+
refetch: () => fetchData(true),
|
|
104
|
+
};
|
|
61
105
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibedash/client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React hooks for Vibe Dash dashboards — useQuery()
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React hooks for Vibe Dash dashboards — useQuery() with optional DuckDB-WASM caching",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"files": [
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
8
10
|
"scripts": {
|
|
9
11
|
"build": "tsc",
|
|
10
12
|
"dev": "tsc --watch"
|
|
11
13
|
},
|
|
12
14
|
"peerDependencies": {
|
|
15
|
+
"@duckdb/duckdb-wasm": ">=1.28.0",
|
|
13
16
|
"react": ">=18"
|
|
14
17
|
},
|
|
18
|
+
"peerDependenciesMeta": {
|
|
19
|
+
"@duckdb/duckdb-wasm": {
|
|
20
|
+
"optional": true
|
|
21
|
+
}
|
|
22
|
+
},
|
|
15
23
|
"devDependencies": {
|
|
24
|
+
"@duckdb/duckdb-wasm": "^1.33.1-dev20.0",
|
|
16
25
|
"@types/react": "^19",
|
|
17
26
|
"react": "^19",
|
|
18
27
|
"typescript": "^5"
|