@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.
- package/dist/duckdb-singleton.d.ts +1 -1
- package/dist/duckdb-singleton.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/provider.d.ts +10 -2
- package/dist/provider.js +93 -4
- package/dist/types.d.ts +2 -0
- package/dist/use-query.d.ts +3 -0
- package/dist/use-query.js +17 -3
- package/package.json +1 -1
|
@@ -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>;
|
package/dist/duckdb-singleton.js
CHANGED
|
@@ -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
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";
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { VibeDashConfig } from "./types";
|
|
2
|
-
|
|
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():
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/use-query.d.ts
CHANGED
|
@@ -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
|
-
|
|
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();
|