@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 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`. Sessions are stored in memory.
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
 
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAuB,WAAW,EAAE,MAAM,SAAS,CAAC;AAEpF,wBAAgB,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAsJrE"}
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.error?.message || "Sign up failed");
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
- state = {
129
+ setState({
54
130
  user: data.user,
55
131
  accessToken: data.accessToken,
56
- refreshToken: data.refreshToken
57
- };
58
- notify();
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.error?.message || "Sign in failed");
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
- state = {
148
+ setState({
73
149
  user: data.user,
74
150
  accessToken: data.accessToken,
75
- refreshToken: data.refreshToken
76
- };
77
- notify();
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
- state = { user: null, accessToken: null, refreshToken: null };
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
- state = { user: null, accessToken: null, refreshToken: null };
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
- state = {
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: false,
246
+ isLoading: state.isLoading,
153
247
  error: null
154
248
  };
155
249
  }
156
250
  // src/hooks/use-query.ts
157
- import { useState as useState2, useEffect as useEffect2, useCallback } from "react";
251
+ import { useState, useEffect, useCallback } from "react";
158
252
  function useQuery(table, options) {
159
253
  const client = useBasedClient();
160
- const [data, setData] = useState2(null);
161
- const [total, setTotal] = useState2(0);
162
- const [isLoading, setIsLoading] = useState2(true);
163
- const [error, setError] = useState2(null);
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
- useEffect2(() => {
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 useState3, useCallback as useCallback2 } from "react";
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] = useState3(false);
209
- const [error, setError] = useState3(null);
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>>;
@@ -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;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE;QACJ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QACzB,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;KACpC,CAAC;IACF,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,QAAQ,IAAI,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACvD,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IACzC,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACnE,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjD;AAID,MAAM,WAAW,aAAa;IAC5B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weirdscience/based-client",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "React SDK for Based — a minimal self-hosted BaaS",
5
5
  "license": "MIT",
6
6
  "repository": {