@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.
@@ -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
  }
@@ -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, error } = useQuery("monthly_revenue");
8
- * ```
13
+ * const { data, query, loading, cachedAt } = useQuery("monthly_revenue");
9
14
  *
10
- * The query must be registered at deploy time. The browser never sends raw SQL —
11
- * only the query name. Vibe Dash looks up the SQL server-side, runs it against
12
- * your BigQuery warehouse using stored credentials, and returns the results.
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, error } = useQuery("monthly_revenue");
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
- * The query must be registered at deploy time. The browser never sends raw SQL —
12
- * only the query name. Vibe Dash looks up the SQL server-side, runs it against
13
- * your BigQuery warehouse using stored credentials, and returns the results.
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
- setData(body.data);
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 { data, loading, error, refetch: fetchData };
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.1.0",
4
- "description": "React hooks for Vibe Dash dashboards — useQuery() for data fetching",
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": ["dist"],
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"