@vibedash/client 0.2.0 → 0.3.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.
@@ -6,7 +6,7 @@
6
6
  */
7
7
  interface DuckDBInstance {
8
8
  query: (sql: string) => Promise<Record<string, unknown>[]>;
9
- loadParquet: (name: string, url: string) => Promise<void>;
9
+ loadParquet: (name: string, url: string, forceReload?: boolean) => Promise<void>;
10
10
  isTableLoaded: (name: string) => boolean;
11
11
  }
12
12
  export declare function getDuckDB(): Promise<DuckDBInstance>;
@@ -27,13 +27,17 @@ async function initDuckDB() {
27
27
  const result = await conn.query(sql);
28
28
  return result.toArray().map((row) => row.toJSON());
29
29
  },
30
- async loadParquet(name, url) {
31
- if (loadedTables.has(name))
30
+ async loadParquet(name, url, forceReload = false) {
31
+ if (loadedTables.has(name) && !forceReload)
32
32
  return;
33
33
  // Fetch the Parquet file and register it
34
34
  const response = await fetch(url);
35
35
  const buffer = await response.arrayBuffer();
36
36
  await db.registerFileBuffer(`${name}.parquet`, new Uint8Array(buffer));
37
+ // Drop existing table on force reload so we pick up the new data
38
+ if (forceReload && loadedTables.has(name)) {
39
+ await conn.query(`DROP TABLE IF EXISTS "${name}"`);
40
+ }
37
41
  await conn.query(`CREATE TABLE IF NOT EXISTS "${name}" AS SELECT * FROM read_parquet('${name}.parquet')`);
38
42
  loadedTables.add(name);
39
43
  },
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { useQuery } from "./use-query";
2
- export { VibeDashProvider, useVibeDash } from "./provider";
2
+ export { VibeDashProvider, useVibeDash, useRefreshAll } from "./provider";
3
3
  export type { UseQueryOptions, UseQueryResult, VibeDashConfig } from "./types";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { useQuery } from "./use-query";
2
- export { VibeDashProvider, useVibeDash } from "./provider";
2
+ export { VibeDashProvider, useVibeDash, useRefreshAll } from "./provider";
@@ -1,5 +1,13 @@
1
1
  import type { VibeDashConfig } from "./types";
2
- export declare function VibeDashProvider({ apiUrl, projectSlug, apiKey, children, }: VibeDashConfig & {
2
+ type RefetchFn = () => Promise<void>;
3
+ interface VibeDashContextValue extends VibeDashConfig {
4
+ registerQuery: (name: string, fn: RefetchFn, cachedAt: Date | null) => void;
5
+ unregisterQuery: (name: string) => void;
6
+ refreshAll: () => Promise<void>;
7
+ }
8
+ export declare function VibeDashProvider({ apiUrl, projectSlug, dashboardSlug, apiKey, children, }: VibeDashConfig & {
3
9
  children: React.ReactNode;
4
10
  }): import("react/jsx-runtime").JSX.Element;
5
- export declare function useVibeDash(): VibeDashConfig;
11
+ export declare function useVibeDash(): VibeDashContextValue;
12
+ export declare function useRefreshAll(): () => Promise<void>;
13
+ export {};
package/dist/provider.js CHANGED
@@ -1,14 +1,103 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext } from "react";
2
+ import { createContext, useContext, useCallback, useRef, useEffect, useState, } from "react";
3
3
  const VibeDashContext = createContext(null);
4
- export function VibeDashProvider({ apiUrl, projectSlug, apiKey, children, }) {
5
- return (_jsx(VibeDashContext.Provider, { value: { apiUrl, projectSlug, apiKey }, children: children }));
4
+ const POLL_INTERVAL_ACTIVE = 30000; // 30s when caching is on
5
+ const POLL_INTERVAL_IDLE = 300000; // 5min when caching is off
6
+ export function VibeDashProvider({ apiUrl, projectSlug, dashboardSlug, apiKey, children, }) {
7
+ const queryMap = useRef(new Map());
8
+ const [pollInterval, setPollInterval] = useState(POLL_INTERVAL_IDLE);
9
+ const registerQuery = useCallback((name, fn, cachedAt) => {
10
+ queryMap.current.set(name, { refetch: fn, cachedAt });
11
+ }, []);
12
+ const unregisterQuery = useCallback((name) => {
13
+ queryMap.current.delete(name);
14
+ }, []);
15
+ const refreshAll = useCallback(async () => {
16
+ const fns = Array.from(queryMap.current.values()).map((e) => e.refetch);
17
+ await Promise.allSettled(fns.map((fn) => fn()));
18
+ }, []);
19
+ // Auto-polling for live cache updates
20
+ useEffect(() => {
21
+ if (!dashboardSlug)
22
+ return;
23
+ let timeoutId;
24
+ let cancelled = false;
25
+ async function poll() {
26
+ if (cancelled)
27
+ return;
28
+ try {
29
+ const headers = {
30
+ "Content-Type": "application/json",
31
+ };
32
+ if (apiKey) {
33
+ headers["Authorization"] = `Bearer ${apiKey}`;
34
+ }
35
+ const res = await fetch(`${apiUrl}/query/status`, {
36
+ method: "POST",
37
+ headers,
38
+ credentials: apiKey ? "omit" : "include",
39
+ body: JSON.stringify({
40
+ project_slug: projectSlug,
41
+ dashboard_slug: dashboardSlug,
42
+ }),
43
+ });
44
+ if (!res.ok)
45
+ return;
46
+ const status = await res.json();
47
+ // Adjust poll interval based on cache mode
48
+ const newInterval = status.cache_mode && status.cache_mode !== "none"
49
+ ? POLL_INTERVAL_ACTIVE
50
+ : POLL_INTERVAL_IDLE;
51
+ setPollInterval(newInterval);
52
+ // Check for stale queries and refetch
53
+ if (status.queries) {
54
+ for (const [queryName, info] of Object.entries(status.queries)) {
55
+ const entry = queryMap.current.get(queryName);
56
+ if (!entry || !info.cached_at)
57
+ continue;
58
+ const serverCachedAt = new Date(info.cached_at);
59
+ const clientCachedAt = entry.cachedAt;
60
+ // Server has newer data — trigger refetch
61
+ if (!clientCachedAt ||
62
+ serverCachedAt.getTime() > clientCachedAt.getTime()) {
63
+ entry.refetch();
64
+ }
65
+ }
66
+ }
67
+ }
68
+ catch {
69
+ // Silently fail — will retry next interval
70
+ }
71
+ if (!cancelled) {
72
+ timeoutId = setTimeout(poll, pollInterval);
73
+ }
74
+ }
75
+ // Start first poll after a short delay (let queries mount first)
76
+ timeoutId = setTimeout(poll, 5000);
77
+ return () => {
78
+ cancelled = true;
79
+ clearTimeout(timeoutId);
80
+ };
81
+ }, [apiUrl, projectSlug, dashboardSlug, apiKey, pollInterval]);
82
+ return (_jsx(VibeDashContext.Provider, { value: {
83
+ apiUrl,
84
+ projectSlug,
85
+ dashboardSlug,
86
+ apiKey,
87
+ registerQuery,
88
+ unregisterQuery,
89
+ refreshAll,
90
+ }, children: children }));
6
91
  }
7
92
  export function useVibeDash() {
8
93
  const ctx = useContext(VibeDashContext);
9
94
  if (!ctx) {
10
95
  throw new Error("useVibeDash must be used within a <VibeDashProvider>. " +
11
- "Wrap your app in <VibeDashProvider apiUrl=\"...\" projectSlug=\"...\">.");
96
+ 'Wrap your app in <VibeDashProvider apiUrl="..." projectSlug="...">.');
12
97
  }
13
98
  return ctx;
14
99
  }
100
+ export function useRefreshAll() {
101
+ const { refreshAll } = useVibeDash();
102
+ return refreshAll;
103
+ }
package/dist/types.d.ts CHANGED
@@ -3,6 +3,8 @@ export interface VibeDashConfig {
3
3
  apiUrl: string;
4
4
  /** The project slug (e.g. "data-man") */
5
5
  projectSlug: string;
6
+ /** The dashboard slug — enables auto-polling for live cache updates */
7
+ dashboardSlug?: string;
6
8
  /** API key for authentication (used in local dev, read from env) */
7
9
  apiKey?: string;
8
10
  }
@@ -8,6 +8,9 @@ import type { UseQueryOptions, UseQueryResult } from "./types";
8
8
  *
9
9
  * If no cache TTL is set, data comes from BigQuery live (existing behavior).
10
10
  *
11
+ * Data stays fresh automatically — when the Hub refreshes the cache in the
12
+ * background, the provider detects the change and triggers a reload.
13
+ *
11
14
  * Usage:
12
15
  * ```tsx
13
16
  * const { data, query, loading, cachedAt } = useQuery("monthly_revenue");
package/dist/use-query.js CHANGED
@@ -9,6 +9,9 @@ import { useVibeDash } from "./provider";
9
9
  *
10
10
  * If no cache TTL is set, data comes from BigQuery live (existing behavior).
11
11
  *
12
+ * Data stays fresh automatically — when the Hub refreshes the cache in the
13
+ * background, the provider detects the change and triggers a reload.
14
+ *
12
15
  * Usage:
13
16
  * ```tsx
14
17
  * const { data, query, loading, cachedAt } = useQuery("monthly_revenue");
@@ -23,7 +26,7 @@ import { useVibeDash } from "./provider";
23
26
  * ```
24
27
  */
25
28
  export function useQuery(queryName, options) {
26
- const { apiUrl, projectSlug, apiKey } = useVibeDash();
29
+ const { apiUrl, projectSlug, apiKey, registerQuery, unregisterQuery } = useVibeDash();
27
30
  const [data, setData] = useState(null);
28
31
  const [loading, setLoading] = useState(false);
29
32
  const [error, setError] = useState(null);
@@ -63,12 +66,13 @@ export function useQuery(queryName, options) {
63
66
  const db = await getDuckDB();
64
67
  // Force reload on refresh, otherwise reuse if already loaded
65
68
  if (forceRefresh || !db.isTableLoaded(queryName)) {
66
- await db.loadParquet(queryName, body.parquet_url);
69
+ await db.loadParquet(queryName, body.parquet_url, forceRefresh);
67
70
  }
68
71
  // Read all rows as default data
69
72
  const allRows = await db.query(`SELECT * FROM "${queryName}"`);
70
73
  setData(allRows);
71
- setCachedAt(new Date(body.cached_at));
74
+ const newCachedAt = new Date(body.cached_at);
75
+ setCachedAt(newCachedAt);
72
76
  // Expose local SQL query function
73
77
  queryFnRef.current = (sql) => db.query(sql);
74
78
  setQueryReady(true);
@@ -89,6 +93,16 @@ export function useQuery(queryName, options) {
89
93
  setLoading(false);
90
94
  }
91
95
  }, [apiUrl, projectSlug, queryName, apiKey]);
96
+ // Register this query by name so the provider can:
97
+ // 1. Call refetch via useRefreshAll()
98
+ // 2. Detect stale data via auto-polling and trigger refetch
99
+ const refetchFn = useCallback(() => fetchData(true), [fetchData]);
100
+ useEffect(() => {
101
+ if (enabled) {
102
+ registerQuery(queryName, refetchFn, cachedAt ?? null);
103
+ return () => unregisterQuery(queryName);
104
+ }
105
+ }, [enabled, queryName, refetchFn, cachedAt, registerQuery, unregisterQuery]);
92
106
  useEffect(() => {
93
107
  if (enabled) {
94
108
  fetchData();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedash/client",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
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",