@gonvex/react 0.1.31 → 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/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  React bindings for Gonvex.
4
4
 
5
5
  This package provides the provider and hooks used by generated Gonvex bindings:
6
- `useQuery`, `useQueryResult`, `useMutation`, `useAction`, `useSync`,
7
- `useSyncSelector`, auth-aware providers, and Convex-style compatibility exports.
6
+ `useQuery`, `useQueryResult`, `useLiveQuery`, `useLiveQueryState`, `useReducer`, `useAction`, `useEntity`,
7
+ `useReplicaCollection`, `useReplicaCollectionState`, `useReplicaEntities`,
8
+ `useRetainedLiveQuery`, `useControlQuery`, and auth-aware providers.
8
9
 
9
10
  ## Install
10
11
 
@@ -16,7 +17,7 @@ npm install @gonvex/react @gonvex/client
16
17
 
17
18
  ```tsx
18
19
  import { GonvexClient } from "@gonvex/client";
19
- import { GonvexProvider, useMutation, useQuery } from "@gonvex/react";
20
+ import { GonvexProvider, useReducer, useQuery } from "@gonvex/react";
20
21
  import { api } from "./gonvex/_generated/api";
21
22
 
22
23
  const client = new GonvexClient("ws://localhost:8080/ws", {
@@ -33,7 +34,7 @@ export function AppRoot() {
33
34
 
34
35
  function Tasks() {
35
36
  const tasks = useQuery(api.tasks.list, { status: "open" });
36
- const createTask = useMutation(api.tasks.create);
37
+ const createTask = useReducer(api.tasks.create);
37
38
 
38
39
  return (
39
40
  <button onClick={() => void createTask({ title: "New task" })}>
@@ -43,16 +44,14 @@ function Tasks() {
43
44
  }
44
45
  ```
45
46
 
46
- `useQuery` automatically benefits from the browser query-result cache. On a
47
- warm load it may receive the last scoped snapshot first, followed by the
48
- authoritative server result. Its signature remains `T | undefined`, but a
49
- server `query.error` now **throws** during render (Convex-compatible) so error
50
- boundaries can catch it instead of looking like an endless loading state.
47
+ `useQuery` executes a read-only Query once. Persistent, continuously verified
48
+ windows use `useLiveQueryState`, which returns normalized rows plus `source`,
49
+ `completeness`, and `freshness`.
51
50
 
52
- ### `useQueryResult` (preferred for new UI)
51
+ ### `useQueryResult`
53
52
 
54
- Use when you need loading vs error vs timeout vs disconnected, last-good data,
55
- or a retry button:
53
+ Use when a one-shot Query needs explicit loading, error, timeout, and retry
54
+ state. A retry can retain its last successful value while verifying again:
56
55
 
57
56
  ```tsx
58
57
  const { data, status, error, isStale, retry } = useQueryResult(api.tasks.list, { status: "open" });
@@ -61,35 +60,51 @@ if (status === "loading" && !data) return <Spinner />;
61
60
  if (status === "error") {
62
61
  return <button onClick={retry}>Retry: {error?.message}</button>;
63
62
  }
64
- // status success | timeout | disconnected — data may still be last-good (isStale)
63
+ // status success | timeout, data may still be last-good during a retry
65
64
  ```
66
65
 
67
- Statuses: `skip` | `loading` | `success` | `error` | `timeout` | `disconnected`.
66
+ Statuses: `skip` | `loading` | `success` | `error` | `timeout`.
68
67
  Soft timeout default is 15s (subscription stays alive; does not reject).
69
68
 
70
69
  ### Connection state
71
70
 
72
71
  ```tsx
73
- const { isWebSocketConnected, hasEverConnected, connectionRetries } = useConvexConnectionState();
72
+ const { isWebSocketConnected, hasEverConnected, connectionRetries } = useGonvexConnectionState();
74
73
  ```
75
74
 
76
- This reflects the real WebSocket lifecycle (not a stub). Mutations/actions
75
+ This reflects the real WebSocket lifecycle (not a stub). Reducers/Actions
77
76
  reject with `GonvexClientError` on timeout or disconnect and never hang forever.
78
77
 
79
- ## Durable sync collections
78
+ ## Replica Collections
80
79
 
81
- `useSync` reads a bounded entity collection from the client's normalized
80
+ `useReplicaCollection` reads a bounded entity collection from the client's normalized
82
81
  IndexedDB store, then updates it as the server resumes or snapshots the durable
83
82
  Postgres cursor:
84
83
 
85
84
  ```tsx
86
- const tasks = useSync<Task>(api.tasks.recent, { workspaceId });
85
+ const tasks = useReplicaCollection<Task>(api.tasks.recent, { workspaceId });
87
86
  ```
88
87
 
89
- Use `useSyncSelector` when a component needs only derived state:
88
+ Read completeness and truncation from the protocol instead of guessing from a
89
+ row count:
90
90
 
91
91
  ```tsx
92
- const openCount = useSyncSelector<Task, number>(
92
+ const state = useReplicaCollectionState<Task>(api.tasks.recent, { workspaceId });
93
+ // state: { rows, source, completeness, freshness, truncated, computedRevision }
94
+ ```
95
+
96
+ Virtualized grids retain query membership as ordered IDs and resolve all rows
97
+ with one Replica subscription:
98
+
99
+ ```tsx
100
+ const window = useRetainedLiveQuery<Task>(api.tasks.grid, args);
101
+ const rows = useReplicaEntities<Task>("tasks", window.ids);
102
+ ```
103
+
104
+ Use `useReplicaSelector` when a component needs only derived state:
105
+
106
+ ```tsx
107
+ const openCount = useReplicaSelector<Task, number>(
93
108
  api.tasks.recent,
94
109
  { workspaceId },
95
110
  (tasks) => tasks.filter((task) => task.status === "open").length,
@@ -100,9 +115,9 @@ Both hooks return `undefined` before any local/server snapshot is available and
100
115
  accept `"skip"` as the args value. Selectors use `Object.is` by default and
101
116
  accept a custom equality function as the fourth argument.
102
117
 
103
- ## Native Google auth
118
+ ## Native authentication
104
119
 
105
- Enable Google for the project and generate a configured auth module:
120
+ Configure the providers needed by the project and generate an auth module:
106
121
 
107
122
  ```bash
108
123
  npx gonvex auth add google --origin http://localhost:5173
@@ -120,31 +135,34 @@ function Root() {
120
135
  }
121
136
 
122
137
  function Account() {
123
- const { activeTenant, user } = useGonvexAuth();
124
- return <>{user?.email} · {activeTenant?.name}<GoogleSignInButton /></>;
138
+ const {
139
+ account,
140
+ activeTenant,
141
+ signInWithPassword,
142
+ signInWithProvider,
143
+ } = useGonvexAuth();
144
+
145
+ return (
146
+ <>
147
+ {account?.email} · {activeTenant?.name}
148
+ <button onClick={() => void signInWithProvider("microsoft")}>Microsoft</button>
149
+ <button onClick={() => void signInWithPassword(email, password)}>Password</button>
150
+ <GoogleSignInButton />
151
+ </>
152
+ );
125
153
  }
126
154
  ```
127
155
 
128
- The provider performs Authorization Code + PKCE against the Gonvex runtime and
129
- attaches the resulting project-scoped session to the realtime client. Access tokens
130
- are short-lived and refresh tokens rotate across tabs. Multi-tenant memberships are
131
- verified by the runtime and switched with `setActiveTenant`. The provider does not
132
- load a Google browser SDK.
133
-
134
- ## Convex Compatibility
135
-
136
- The package also exports Convex-style names for incremental migration:
137
-
138
- - `ConvexProvider`
139
- - `ConvexProviderWithAuth`
140
- - `ConvexReactClient`
141
- - `useConvex`
142
- - `useConvexAuth`
143
- - `useConvexConnectionState`
144
- - `usePaginatedQuery`
145
- - `useQueryResult`
146
- - `useSync`
147
- - `useSyncSelector`
156
+ `signInWithProvider` accepts `google`, `microsoft`, or `apple` and performs
157
+ Authorization Code + PKCE through Gonvex. `signInWithPassword` installs the
158
+ native password session through the same path. Access tokens are short-lived,
159
+ refresh tokens rotate across tabs, and the provider persists the active tenant.
160
+ The host verifies tenant membership before switching with `setActiveTenant`.
161
+
162
+ Use `useCurrentTenantProfile()` for subscribed domain, timezone, description,
163
+ and public profile fields. Use `useControlQuery(reference, args)` for an
164
+ authorized Control Plane live Query. Reducers refresh those subscriptions, so
165
+ the application must not refetch them manually.
148
166
 
149
167
  ## Related Packages
150
168
 
package/dist/index.d.ts CHANGED
@@ -1,40 +1,47 @@
1
1
  import { type ButtonHTMLAttributes, type ReactNode } from "react";
2
- import { ConvexReactClient, type ConnectionState, type FunctionReference, type GonvexClient } from "@gonvex/client";
2
+ import { GonvexClient, type ConnectionState, type ControlInvitationAcceptance, type ControlInvitationListItem, type ControlToken, type FunctionReference, type LiveQueryResult, type ReplicaCollectionState, type ReplicaRow } from "@gonvex/client";
3
3
  import type { JsonValue } from "@gonvex/protocol";
4
4
  export { GonvexClientError, type ConnectionState } from "@gonvex/client";
5
- export declare const ConvexProvider: typeof GonvexProvider;
6
5
  export declare function GonvexProvider(props: {
7
6
  client: GonvexClient;
8
7
  children: ReactNode;
9
8
  }): import("react/jsx-runtime").JSX.Element;
10
- export { ConvexReactClient };
11
9
  export type AuthState = {
12
10
  isLoading: boolean;
13
11
  isAuthenticated: boolean;
12
+ /** Terminal runtime rejection after one forced token refresh attempt. */
13
+ authError?: Error | null;
14
14
  fetchAccessToken?: (args: {
15
15
  forceRefreshToken: boolean;
16
16
  }) => Promise<string | null>;
17
17
  };
18
- export type GonvexAuthUser = {
18
+ export type GonvexAuthAccount = {
19
19
  id: string;
20
20
  email?: string;
21
21
  emailVerified: boolean;
22
22
  name?: string;
23
23
  picture?: string;
24
- provider: "google" | string;
24
+ provider: "password" | "google" | "microsoft" | "apple" | string;
25
25
  };
26
26
  export type GonvexAuthTenant = {
27
27
  id: string;
28
28
  name: string;
29
29
  role: "owner" | "admin" | "member" | "viewer" | string;
30
30
  permissions?: Record<string, unknown>;
31
+ domain: string;
32
+ timezone: string;
33
+ description: string;
34
+ profile: JsonValue;
31
35
  };
36
+ export type GonvexAuthProviderName = "google" | "microsoft" | "apple";
32
37
  export type GonvexAuthValue = AuthState & {
33
- user: GonvexAuthUser | null;
38
+ account: GonvexAuthAccount | null;
34
39
  tenants: GonvexAuthTenant[];
35
40
  activeTenant: GonvexAuthTenant | null;
36
41
  error: string | null;
37
- signIn: () => Promise<void>;
42
+ signIn: (provider?: GonvexAuthProviderName) => Promise<void>;
43
+ signInWithProvider: (provider: GonvexAuthProviderName) => Promise<void>;
44
+ signInWithPassword: (email: string, password: string) => Promise<void>;
38
45
  signOut: (options?: {
39
46
  allDevices?: boolean;
40
47
  }) => Promise<void>;
@@ -44,17 +51,20 @@ export type GonvexAuthValue = AuthState & {
44
51
  inviteMember: (tenantId: string, email: string, options?: {
45
52
  role?: GonvexAuthTenant["role"];
46
53
  permissions?: Record<string, unknown>;
47
- }) => Promise<void>;
54
+ teamIds?: string[];
55
+ allowedAuthProviders?: string[];
56
+ payload?: JsonValue;
57
+ }) => Promise<ControlToken>;
58
+ acceptInvitation: (token: string) => Promise<ControlInvitationAcceptance>;
48
59
  revokeInvitation: (tenantId: string, email: string) => Promise<void>;
49
- removeMember: (tenantId: string, userId: string) => Promise<void>;
50
60
  };
51
61
  export type GonvexAuthConfig = {
52
62
  runtimeUrl: string;
53
63
  projectId: string;
54
64
  callbackPath?: string;
55
65
  };
56
- export declare function ConvexProviderWithAuth(props: {
57
- client: ConvexReactClient;
66
+ export declare function GonvexProviderWithAuth(props: {
67
+ client: GonvexClient;
58
68
  children: ReactNode;
59
69
  useAuth: () => AuthState;
60
70
  }): import("react/jsx-runtime").JSX.Element;
@@ -63,6 +73,12 @@ export declare function GonvexAuthProvider(props: GonvexAuthConfig & {
63
73
  children: ReactNode;
64
74
  }): import("react/jsx-runtime").JSX.Element;
65
75
  export declare function useGonvexAuth(): GonvexAuthValue;
76
+ /** Subscribed profile for the active tenant, reconciled by GonvexAuthProvider. */
77
+ export declare function useCurrentTenantProfile(): GonvexAuthTenant | null;
78
+ /** Live tenant-admin invitation list; reducer changes reconcile automatically. */
79
+ export declare function useInvitationList(): ControlInvitationListItem[] | undefined;
80
+ /** Read the auth state installed by either auth provider. */
81
+ export declare function useGonvexAuthState(): AuthState;
66
82
  export declare function GonvexGoogleAuthButton(props: ButtonHTMLAttributes<HTMLButtonElement> & {
67
83
  signOutLabel?: string;
68
84
  }): import("react/jsx-runtime").JSX.Element;
@@ -74,7 +90,7 @@ export declare function createGonvexAuth(config: GonvexAuthConfig): {
74
90
  GoogleSignInButton: typeof GonvexGoogleAuthButton;
75
91
  useGonvexAuth: typeof useGonvexAuth;
76
92
  };
77
- export type QueryStatus = "skip" | "loading" | "success" | "error" | "timeout" | "disconnected";
93
+ export type QueryStatus = "skip" | "loading" | "success" | "error" | "timeout";
78
94
  export type UseQueryResultOptions = {
79
95
  /**
80
96
  * Soft "pending too long" signal for the live subscription. When no result
@@ -101,30 +117,31 @@ export type UseQueryResult<T> = {
101
117
  /** Re-request the query from the server (drops error/timeout state). */
102
118
  retry: () => void;
103
119
  };
104
- /**
105
- * Live query hook with an explicit status surface. Unlike `useQuery`, this
106
- * distinguishes loading / success / error / timeout / disconnected, keeps the
107
- * last good result while reconnecting, and exposes `retry()`.
108
- */
109
- export declare function useQueryResult<T = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip", options?: UseQueryResultOptions): UseQueryResult<T>;
110
- export declare function useQuery<T = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip"): T | undefined;
111
- export declare function useSync<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip"): T[] | undefined;
112
- export declare function useSyncSelector<T extends JsonValue = JsonValue, Selected = unknown>(ref: FunctionReference, args: JsonValue | "skip", selector: (rows: T[]) => Selected, isEqual?: (left: Selected, right: Selected) => boolean): Selected | undefined;
113
- export type UseMutationOptions = {
120
+ /** One-shot Query hook with explicit loading/error/timeout status and retry. */
121
+ export declare function useQueryResult<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip", options?: UseQueryResultOptions): UseQueryResult<T>;
122
+ export declare function useLiveQuery<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip"): T | undefined;
123
+ /** Subscribe to a host-owned Control Plane Query on the existing Gonvex connection. */
124
+ export declare function useControlQuery<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip"): T | undefined;
125
+ /** Read one normalized entity from the single Gonvex Local Replica. */
126
+ export declare function useEntity<T extends ReplicaRow = ReplicaRow>(entity: string, id: string): T | undefined;
127
+ /** Resolve an ordered entity batch with one Local Replica subscription. */
128
+ export declare function useReplicaEntities<T extends ReplicaRow = ReplicaRow>(entity: string, ids: readonly string[]): Array<T | undefined>;
129
+ /** Read a persisted Live Query window without opening another server subscription. */
130
+ export declare function useRetainedLiveQuery<T extends ReplicaRow = ReplicaRow>(signatureOrReference: string | FunctionReference, args?: JsonValue): LiveQueryResult<T>;
131
+ /** Structured Live Query state backed by normalized Local Replica entities. */
132
+ export declare function useLiveQueryState<T extends ReplicaRow = ReplicaRow>(ref: FunctionReference, args?: JsonValue | "skip"): LiveQueryResult<T>;
133
+ /** Execute a read-only Query once. Queries never subscribe or rerun. */
134
+ export declare function useQuery<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip"): T | undefined;
135
+ export declare function useReplicaCollection<T extends JsonValue = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip"): T[] | undefined;
136
+ /** Replica rows plus authoritative completeness, truncation, and freshness metadata. */
137
+ export declare function useReplicaCollectionState<T extends ReplicaRow = ReplicaRow>(ref: FunctionReference, args?: JsonValue | "skip"): ReplicaCollectionState<T> | undefined;
138
+ export declare function useReplicaSelector<T extends JsonValue = JsonValue, Selected = unknown>(ref: FunctionReference, args: JsonValue | "skip", selector: (rows: T[]) => Selected, isEqual?: (left: Selected, right: Selected) => boolean): Selected | undefined;
139
+ export type UseReducerOptions = {
114
140
  /** Per-call timeout override forwarded to the client. `0` disables. */
115
141
  timeoutMs?: number;
116
142
  };
117
- export declare function useMutation(ref: FunctionReference, options?: UseMutationOptions): (args?: JsonValue) => Promise<JsonValue>;
118
- export declare function useAction(ref: FunctionReference, options?: UseMutationOptions): (args?: JsonValue) => Promise<JsonValue>;
119
- export declare function useConvex(): GonvexClient;
120
- export declare function useConvexAuth(): AuthState;
121
- export declare function useConvexConnectionState(): ConnectionState;
122
- export declare function usePaginatedQuery<T = JsonValue>(ref: FunctionReference, args?: JsonValue | "skip", options?: {
123
- initialNumItems?: number;
124
- }): {
125
- results: T[];
126
- status: string;
127
- isLoading: boolean;
128
- loadMore: (_numItems: number) => undefined;
129
- };
143
+ export declare function useReducer(ref: FunctionReference, options?: UseReducerOptions): (args?: JsonValue) => Promise<JsonValue>;
144
+ export declare function useAction(ref: FunctionReference, options?: UseReducerOptions): (args?: JsonValue) => Promise<JsonValue>;
145
+ export declare function useGonvexConnectionState(): ConnectionState;
146
+ export declare function useGonvexClient(): GonvexClient;
130
147
  export { useSignalValue } from "./useSignal.js";