@weirdscience/based-client 0.1.1 → 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.
- package/README.md +34 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/index.js +127 -33
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,40 @@ Methods on `client.auth`:
|
|
|
124
124
|
- `signOut()` → invalidates the session
|
|
125
125
|
- `refreshSession()` → manually refresh (happens automatically on 401)
|
|
126
126
|
|
|
127
|
-
Access tokens auto-refresh on `401`.
|
|
127
|
+
Access tokens auto-refresh on `401`.
|
|
128
|
+
|
|
129
|
+
## Session persistence
|
|
130
|
+
|
|
131
|
+
Sessions persist across page reloads via `localStorage` by default. On mount, the client restores the saved session and validates it by calling `/auth/me`.
|
|
132
|
+
|
|
133
|
+
Use `client.ready()` or `useUser().isLoading` to avoid flashing a logged-out UI during hydration:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
const { user, isLoading } = useUser();
|
|
137
|
+
if (isLoading) return <Spinner />;
|
|
138
|
+
if (!user) return <LoginForm />;
|
|
139
|
+
return <Dashboard user={user} />;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Opt out or use a custom storage adapter:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// Disable entirely (in-memory only)
|
|
146
|
+
createClient({ url, anonKey, storage: false });
|
|
147
|
+
|
|
148
|
+
// Custom storage (cookies, IndexedDB, React Native AsyncStorage, etc.)
|
|
149
|
+
createClient({
|
|
150
|
+
url,
|
|
151
|
+
anonKey,
|
|
152
|
+
storage: {
|
|
153
|
+
getItem: (k) => AsyncStorage.getItem(k),
|
|
154
|
+
setItem: (k, v) => AsyncStorage.setItem(k, v),
|
|
155
|
+
removeItem: (k) => AsyncStorage.removeItem(k),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Storage adapters can return promises — the client awaits them.
|
|
128
161
|
|
|
129
162
|
## Type safety
|
|
130
163
|
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAGlB,WAAW,EAEZ,MAAM,SAAS,CAAC;AAqBjB,wBAAgB,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAyPrE"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
|
+
var DEFAULT_STORAGE_KEY = "based.session";
|
|
3
|
+
function defaultStorage() {
|
|
4
|
+
if (typeof globalThis === "undefined")
|
|
5
|
+
return null;
|
|
6
|
+
const ls = globalThis.localStorage;
|
|
7
|
+
if (!ls || typeof ls.getItem !== "function")
|
|
8
|
+
return null;
|
|
9
|
+
return {
|
|
10
|
+
getItem: (k) => ls.getItem(k),
|
|
11
|
+
setItem: (k, v) => ls.setItem(k, v),
|
|
12
|
+
removeItem: (k) => ls.removeItem(k)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
2
15
|
function createClient(options) {
|
|
3
16
|
const { url, anonKey } = options;
|
|
4
17
|
const baseUrl = url.replace(/\/$/, "");
|
|
18
|
+
const storage = options.storage === false ? null : options.storage ?? defaultStorage();
|
|
19
|
+
const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
5
20
|
let state = {
|
|
6
21
|
user: null,
|
|
7
22
|
accessToken: null,
|
|
8
|
-
refreshToken: null
|
|
23
|
+
refreshToken: null,
|
|
24
|
+
isLoading: !!storage
|
|
9
25
|
};
|
|
10
26
|
const listeners = new Set;
|
|
11
27
|
function notify() {
|
|
@@ -19,6 +35,66 @@ function createClient(options) {
|
|
|
19
35
|
function getState() {
|
|
20
36
|
return state;
|
|
21
37
|
}
|
|
38
|
+
async function persist() {
|
|
39
|
+
if (!storage)
|
|
40
|
+
return;
|
|
41
|
+
const snapshot = {
|
|
42
|
+
user: state.user,
|
|
43
|
+
accessToken: state.accessToken,
|
|
44
|
+
refreshToken: state.refreshToken
|
|
45
|
+
};
|
|
46
|
+
if (!snapshot.refreshToken) {
|
|
47
|
+
await storage.removeItem(storageKey);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await storage.setItem(storageKey, JSON.stringify(snapshot));
|
|
51
|
+
}
|
|
52
|
+
function setState(next) {
|
|
53
|
+
state = { ...state, ...next };
|
|
54
|
+
notify();
|
|
55
|
+
persist().catch(() => {});
|
|
56
|
+
}
|
|
57
|
+
const readyPromise = (async () => {
|
|
58
|
+
if (!storage) {
|
|
59
|
+
setState({ isLoading: false });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const raw = await storage.getItem(storageKey);
|
|
64
|
+
if (!raw) {
|
|
65
|
+
setState({ isLoading: false });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const stored = JSON.parse(raw);
|
|
69
|
+
if (!stored.refreshToken) {
|
|
70
|
+
setState({ isLoading: false });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
state = {
|
|
74
|
+
user: stored.user,
|
|
75
|
+
accessToken: stored.accessToken,
|
|
76
|
+
refreshToken: stored.refreshToken,
|
|
77
|
+
isLoading: true
|
|
78
|
+
};
|
|
79
|
+
notify();
|
|
80
|
+
const user = await getUser();
|
|
81
|
+
if (!user) {
|
|
82
|
+
const refreshed = await refreshSession();
|
|
83
|
+
if (refreshed) {
|
|
84
|
+
await getUser();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
try {
|
|
89
|
+
await storage.removeItem(storageKey);
|
|
90
|
+
} catch {}
|
|
91
|
+
} finally {
|
|
92
|
+
setState({ isLoading: false });
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
95
|
+
function ready() {
|
|
96
|
+
return readyPromise;
|
|
97
|
+
}
|
|
22
98
|
async function fetchWithAuth(path, init = {}) {
|
|
23
99
|
const headers = new Headers(init.headers);
|
|
24
100
|
if (state.accessToken) {
|
|
@@ -46,16 +122,16 @@ function createClient(options) {
|
|
|
46
122
|
body: JSON.stringify({ email, password })
|
|
47
123
|
});
|
|
48
124
|
if (!res.ok) {
|
|
49
|
-
const json = await res.json();
|
|
50
|
-
throw new Error(json
|
|
125
|
+
const json = await res.json().catch(() => null);
|
|
126
|
+
throw new Error(json?.error?.message || "Sign up failed");
|
|
51
127
|
}
|
|
52
128
|
const { data } = await res.json();
|
|
53
|
-
|
|
129
|
+
setState({
|
|
54
130
|
user: data.user,
|
|
55
131
|
accessToken: data.accessToken,
|
|
56
|
-
refreshToken: data.refreshToken
|
|
57
|
-
|
|
58
|
-
|
|
132
|
+
refreshToken: data.refreshToken,
|
|
133
|
+
isLoading: false
|
|
134
|
+
});
|
|
59
135
|
return data.user;
|
|
60
136
|
}
|
|
61
137
|
async function signIn(email, password) {
|
|
@@ -65,16 +141,16 @@ function createClient(options) {
|
|
|
65
141
|
body: JSON.stringify({ email, password })
|
|
66
142
|
});
|
|
67
143
|
if (!res.ok) {
|
|
68
|
-
const json = await res.json();
|
|
69
|
-
throw new Error(json
|
|
144
|
+
const json = await res.json().catch(() => null);
|
|
145
|
+
throw new Error(json?.error?.message || "Sign in failed");
|
|
70
146
|
}
|
|
71
147
|
const { data } = await res.json();
|
|
72
|
-
|
|
148
|
+
setState({
|
|
73
149
|
user: data.user,
|
|
74
150
|
accessToken: data.accessToken,
|
|
75
|
-
refreshToken: data.refreshToken
|
|
76
|
-
|
|
77
|
-
|
|
151
|
+
refreshToken: data.refreshToken,
|
|
152
|
+
isLoading: false
|
|
153
|
+
});
|
|
78
154
|
return data.user;
|
|
79
155
|
}
|
|
80
156
|
async function signOut() {
|
|
@@ -86,8 +162,7 @@ function createClient(options) {
|
|
|
86
162
|
});
|
|
87
163
|
} catch {}
|
|
88
164
|
}
|
|
89
|
-
|
|
90
|
-
notify();
|
|
165
|
+
setState({ user: null, accessToken: null, refreshToken: null, isLoading: false });
|
|
91
166
|
}
|
|
92
167
|
async function refreshSession() {
|
|
93
168
|
if (!state.refreshToken)
|
|
@@ -99,27 +174,46 @@ function createClient(options) {
|
|
|
99
174
|
body: JSON.stringify({ refreshToken: state.refreshToken })
|
|
100
175
|
});
|
|
101
176
|
if (!res.ok) {
|
|
102
|
-
|
|
103
|
-
notify();
|
|
177
|
+
setState({ user: null, accessToken: null, refreshToken: null, isLoading: false });
|
|
104
178
|
return false;
|
|
105
179
|
}
|
|
106
180
|
const { data } = await res.json();
|
|
107
|
-
|
|
108
|
-
...state,
|
|
181
|
+
setState({
|
|
109
182
|
accessToken: data.accessToken,
|
|
110
183
|
refreshToken: data.refreshToken
|
|
111
|
-
};
|
|
112
|
-
notify();
|
|
184
|
+
});
|
|
113
185
|
return true;
|
|
114
186
|
} catch {
|
|
115
187
|
return false;
|
|
116
188
|
}
|
|
117
189
|
}
|
|
190
|
+
async function getUser() {
|
|
191
|
+
if (!state.accessToken)
|
|
192
|
+
return null;
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(`${baseUrl}/auth/me`, {
|
|
195
|
+
headers: { Authorization: `Bearer ${state.accessToken}` }
|
|
196
|
+
});
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
if (res.status === 401) {
|
|
199
|
+
setState({ user: null });
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const { data } = await res.json();
|
|
204
|
+
const user = { id: data.id, email: data.email };
|
|
205
|
+
setState({ user });
|
|
206
|
+
return user;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
118
211
|
return {
|
|
119
|
-
auth: { signUp, signIn, signOut, refreshSession },
|
|
212
|
+
auth: { signUp, signIn, signOut, refreshSession, getUser },
|
|
120
213
|
fetch: fetchWithAuth,
|
|
121
214
|
subscribe,
|
|
122
|
-
getState
|
|
215
|
+
getState,
|
|
216
|
+
ready
|
|
123
217
|
};
|
|
124
218
|
}
|
|
125
219
|
// src/provider.tsx
|
|
@@ -149,18 +243,18 @@ function useUser() {
|
|
|
149
243
|
const state = useSyncExternalStore(client.subscribe, () => client.getState(), () => client.getState());
|
|
150
244
|
return {
|
|
151
245
|
user: state.user,
|
|
152
|
-
isLoading:
|
|
246
|
+
isLoading: state.isLoading,
|
|
153
247
|
error: null
|
|
154
248
|
};
|
|
155
249
|
}
|
|
156
250
|
// src/hooks/use-query.ts
|
|
157
|
-
import { useState
|
|
251
|
+
import { useState, useEffect, useCallback } from "react";
|
|
158
252
|
function useQuery(table, options) {
|
|
159
253
|
const client = useBasedClient();
|
|
160
|
-
const [data, setData] =
|
|
161
|
-
const [total, setTotal] =
|
|
162
|
-
const [isLoading, setIsLoading] =
|
|
163
|
-
const [error, setError] =
|
|
254
|
+
const [data, setData] = useState(null);
|
|
255
|
+
const [total, setTotal] = useState(0);
|
|
256
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
257
|
+
const [error, setError] = useState(null);
|
|
164
258
|
const fetchData = useCallback(async () => {
|
|
165
259
|
setIsLoading(true);
|
|
166
260
|
setError(null);
|
|
@@ -196,17 +290,17 @@ function useQuery(table, options) {
|
|
|
196
290
|
setIsLoading(false);
|
|
197
291
|
}
|
|
198
292
|
}, [client, table, JSON.stringify(options)]);
|
|
199
|
-
|
|
293
|
+
useEffect(() => {
|
|
200
294
|
fetchData();
|
|
201
295
|
}, [fetchData]);
|
|
202
296
|
return { data, total, isLoading, error, refetch: fetchData };
|
|
203
297
|
}
|
|
204
298
|
// src/hooks/use-mutation.ts
|
|
205
|
-
import { useState as
|
|
299
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
206
300
|
function useMutation(table, operation) {
|
|
207
301
|
const client = useBasedClient();
|
|
208
|
-
const [isLoading, setIsLoading] =
|
|
209
|
-
const [error, setError] =
|
|
302
|
+
const [isLoading, setIsLoading] = useState2(false);
|
|
303
|
+
const [error, setError] = useState2(null);
|
|
210
304
|
const mutate = useCallback2(async (data) => {
|
|
211
305
|
setIsLoading(true);
|
|
212
306
|
setError(null);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
export interface StorageAdapter {
|
|
2
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
3
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
4
|
+
removeItem(key: string): void | Promise<void>;
|
|
5
|
+
}
|
|
1
6
|
export interface BasedClientOptions {
|
|
2
7
|
url: string;
|
|
3
8
|
anonKey: string;
|
|
9
|
+
/**
|
|
10
|
+
* Where to persist the session. Defaults to `localStorage` in browsers,
|
|
11
|
+
* in-memory elsewhere. Pass `false` to disable persistence entirely.
|
|
12
|
+
*/
|
|
13
|
+
storage?: StorageAdapter | false;
|
|
14
|
+
/**
|
|
15
|
+
* Key used for storage. Defaults to `based.session`.
|
|
16
|
+
*/
|
|
17
|
+
storageKey?: string;
|
|
4
18
|
}
|
|
5
19
|
export interface AuthUser {
|
|
6
20
|
id: string;
|
|
@@ -10,6 +24,11 @@ export interface AuthState {
|
|
|
10
24
|
user: AuthUser | null;
|
|
11
25
|
accessToken: string | null;
|
|
12
26
|
refreshToken: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* True during the initial session rehydration. Use this to avoid flashing
|
|
29
|
+
* a "logged out" UI before the stored session has been validated.
|
|
30
|
+
*/
|
|
31
|
+
isLoading: boolean;
|
|
13
32
|
}
|
|
14
33
|
export interface BasedClient {
|
|
15
34
|
auth: {
|
|
@@ -17,10 +36,19 @@ export interface BasedClient {
|
|
|
17
36
|
signIn(email: string, password: string): Promise<AuthUser>;
|
|
18
37
|
signOut(): Promise<void>;
|
|
19
38
|
refreshSession(): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Returns the current authenticated user by calling `/auth/me`.
|
|
41
|
+
*/
|
|
42
|
+
getUser(): Promise<AuthUser | null>;
|
|
20
43
|
};
|
|
21
44
|
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
22
45
|
subscribe(listener: () => void): () => void;
|
|
23
46
|
getState(): AuthState;
|
|
47
|
+
/**
|
|
48
|
+
* Resolves when the client finishes restoring a stored session (if any).
|
|
49
|
+
* Safe to await multiple times.
|
|
50
|
+
*/
|
|
51
|
+
ready(): Promise<void>;
|
|
24
52
|
}
|
|
25
53
|
export interface QueryOptions<T = Record<string, unknown>> {
|
|
26
54
|
filter?: Partial<Record<keyof T, string | number | boolean>>;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|