@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 +63 -45
- package/dist/index.d.ts +52 -35
- package/dist/index.js +281 -226
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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`, `
|
|
7
|
-
`
|
|
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,
|
|
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 =
|
|
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`
|
|
47
|
-
|
|
48
|
-
|
|
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`
|
|
51
|
+
### `useQueryResult`
|
|
53
52
|
|
|
54
|
-
Use when
|
|
55
|
-
|
|
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
|
|
63
|
+
// status success | timeout, data may still be last-good during a retry
|
|
65
64
|
```
|
|
66
65
|
|
|
67
|
-
Statuses: `skip` | `loading` | `success` | `error` | `timeout
|
|
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 } =
|
|
72
|
+
const { isWebSocketConnected, hasEverConnected, connectionRetries } = useGonvexConnectionState();
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
This reflects the real WebSocket lifecycle (not a stub).
|
|
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
|
-
##
|
|
78
|
+
## Replica Collections
|
|
80
79
|
|
|
81
|
-
`
|
|
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 =
|
|
85
|
+
const tasks = useReplicaCollection<Task>(api.tasks.recent, { workspaceId });
|
|
87
86
|
```
|
|
88
87
|
|
|
89
|
-
|
|
88
|
+
Read completeness and truncation from the protocol instead of guessing from a
|
|
89
|
+
row count:
|
|
90
90
|
|
|
91
91
|
```tsx
|
|
92
|
-
const
|
|
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
|
|
118
|
+
## Native authentication
|
|
104
119
|
|
|
105
|
-
|
|
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 {
|
|
124
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
57
|
-
client:
|
|
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"
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
export declare function
|
|
111
|
-
|
|
112
|
-
export declare function
|
|
113
|
-
|
|
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
|
|
118
|
-
export declare function useAction(ref: FunctionReference, options?:
|
|
119
|
-
export declare function
|
|
120
|
-
export declare function
|
|
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";
|