@basictech/react 0.9.0-beta.0 → 0.9.0-beta.1
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/changelog.md +34 -0
- package/dist/index.d.mts +269 -27
- package/dist/index.d.ts +269 -27
- package/dist/index.js +790 -129
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +786 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/readme.md +66 -12
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -13,6 +13,8 @@ Basic gives every user their own personal datastore (a PDS). Your app authentica
|
|
|
13
13
|
- [Installation](#installation)
|
|
14
14
|
- [Quick start](#quick-start)
|
|
15
15
|
- [How it works](#how-it-works)
|
|
16
|
+
- [Anonymous & local-first](#anonymous--local-first)
|
|
17
|
+
- [Multiple users](#multiple-users)
|
|
16
18
|
- [Authentication](#authentication)
|
|
17
19
|
- [Database](#database)
|
|
18
20
|
- [Live queries](#live-queries)
|
|
@@ -110,17 +112,63 @@ function Todos() {
|
|
|
110
112
|
}
|
|
111
113
|
```
|
|
112
114
|
|
|
113
|
-
That's the whole loop
|
|
115
|
+
That's the whole loop — and note the app **works before sign-in too**: `db.table(...)` writes go to a local anonymous workspace and merge into the account automatically when the user signs in (see [Anonymous & local-first](#anonymous--local-first)). `useQuery` re-renders whenever local data changes — whether the change came from this tab, another device, or another user via a share.
|
|
114
116
|
|
|
115
117
|
## How it works
|
|
116
118
|
|
|
117
119
|
In the default `sync` mode, the SDK maintains a **local replica** of your app's slice of the user's datastore, in IndexedDB, and syncs it over one WebSocket:
|
|
118
120
|
|
|
119
|
-
1. **
|
|
121
|
+
1. **Local first** — the local database opens immediately, with no session and no network. Reads and writes work from the first render, including offline cold starts.
|
|
120
122
|
2. **Writes are ops** — every write is a `put` (create/replace), `patch` (shallow field merge), or `delete` operation with a client-minted unique id. Ops apply to the local view immediately (optimistic), queue in IndexedDB, and push to the server when online. Retries are always safe: the server deduplicates by op id.
|
|
121
123
|
3. **Reads are local** — `get`/`getAll`/`find` read the local view: confirmed server state with your pending writes layered on top. No network round-trip.
|
|
122
|
-
4. **
|
|
123
|
-
5. **
|
|
124
|
+
4. **Bootstrap on connect** — when a session exists, the SDK fetches a snapshot of the account's current state, layers your pending writes on top, then subscribes for live changes.
|
|
125
|
+
5. **The server is the authority** — it orders all ops (last-writer-wins per record; `patch`es merge per field), validates them against your published schema, and echoes them to every connected device. If it rejects a write, the SDK rolls it back locally and surfaces it (see [rejected ops](#rejected-ops)).
|
|
126
|
+
6. **Sign-out is clean** — the user's local data is wiped, the session is revoked server-side, and the app continues in a fresh anonymous workspace. No page reloads.
|
|
127
|
+
|
|
128
|
+
## Anonymous & local-first
|
|
129
|
+
|
|
130
|
+
Anonymous mode is **on by default**: users can use your app without signing in. Their data lives in a local-only workspace (sync status `local`), fully readable and writable, surviving reloads.
|
|
131
|
+
|
|
132
|
+
When they sign in, the anonymous workspace **becomes** their account: every anonymous write is already a queued op, so the normal bootstrap-and-push flow merges them into the account — no copy step, no data loss, and live queries keep working through the transition.
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const { isAnonymous, isSignedIn, signIn, db } = useBasic();
|
|
136
|
+
|
|
137
|
+
// works signed in or not:
|
|
138
|
+
await db.table("todos").create({ title: "works offline & anonymous" });
|
|
139
|
+
|
|
140
|
+
// prompt when you want the data to start syncing:
|
|
141
|
+
{isAnonymous && <button onClick={() => signIn()}>Sign in to sync</button>}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Safety guarantees:
|
|
145
|
+
|
|
146
|
+
- A keyspace is **stamped with the account DID** at first bootstrap. Local data from one account can never merge into another — if a different account signs in over it, the local cache is wiped first (it's server-backed; nothing is lost).
|
|
147
|
+
- Signing out wipes that user's local data and drops into a fresh anonymous workspace (nothing readable is left behind on shared machines).
|
|
148
|
+
|
|
149
|
+
Set `anonymous={false}` on the provider to require sign-in before any local data exists (classic behavior).
|
|
150
|
+
|
|
151
|
+
## Multiple users
|
|
152
|
+
|
|
153
|
+
Several local users — anonymous or signed-in — can exist side by side, one active at a time. The active user is **per tab** (two tabs can be on different users); the profiles themselves are shared across tabs. Each user has an isolated keyspace and session.
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
const { users, activeUser, switchUser, addUser, removeUser } = useUsers();
|
|
157
|
+
|
|
158
|
+
<select value={activeUser?.id} onChange={(e) => switchUser(e.target.value)}>
|
|
159
|
+
{users.map((u) => (
|
|
160
|
+
<option key={u.id} value={u.id}>
|
|
161
|
+
{u.kind === "anon" ? "Anonymous" : u.email || u.name || u.did}
|
|
162
|
+
</option>
|
|
163
|
+
))}
|
|
164
|
+
</select>
|
|
165
|
+
<button onClick={() => addUser()}>Add user</button>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- `switchUser(id)` swaps the active session, database, and sync connection in place. Live queries via `useQuery` re-attach automatically.
|
|
169
|
+
- `addUser()` creates a fresh anonymous user and switches to it (sign in from there to add an account).
|
|
170
|
+
- `removeUser(id)` wipes that user's local data and revokes its session; removing the active user is the same as `signOut()`.
|
|
171
|
+
- `signOut()` removes the active user and falls through to the next one (or a fresh anonymous workspace).
|
|
124
172
|
|
|
125
173
|
## Authentication
|
|
126
174
|
|
|
@@ -148,8 +196,8 @@ await auth.signOut(); // revokes server-side + clears local
|
|
|
148
196
|
| `bootstrapping` | SDK is initializing | splash / nothing (provider hides children by default) |
|
|
149
197
|
| `authenticated` | Healthy session | your app |
|
|
150
198
|
| `recovering` | Session likely exists but unconfirmed (offline, mid-refresh) | your app (data still works locally) |
|
|
151
|
-
| `reauth_required` | Session definitively invalid — user must sign in again |
|
|
152
|
-
| `signed_out` | No session | sign
|
|
199
|
+
| `reauth_required` | Session definitively invalid — user must sign in again | your app (local reads/writes still work) + a re-sign-in prompt |
|
|
200
|
+
| `signed_out` | No session — anonymous local workspace (default mode) | your app + a "sign in to sync" prompt (`isAnonymous` is true) |
|
|
153
201
|
|
|
154
202
|
### Calling your own API
|
|
155
203
|
|
|
@@ -226,12 +274,14 @@ const recent = useQuery(() =>
|
|
|
226
274
|
```tsx
|
|
227
275
|
const sync = useSyncStatus();
|
|
228
276
|
|
|
229
|
-
sync.status; // 'idle' | 'connecting' | 'online' | 'offline' | 'auth_required' | 'revoked' | 'stopped'
|
|
277
|
+
sync.status; // 'idle' | 'local' | 'connecting' | 'online' | 'offline' | 'auth_required' | 'revoked' | 'stopped'
|
|
230
278
|
sync.enabled; // false when the schema is invalid/unpublished
|
|
231
279
|
sync.pendingCount; // local writes not yet confirmed by the server
|
|
232
280
|
```
|
|
233
281
|
|
|
234
|
-
|
|
282
|
+
`local` means the database is working without a connection — an anonymous workspace, an unpublished (version 0) schema during development, or a paused session.
|
|
283
|
+
|
|
284
|
+
**Offline is not an error.** Writes keep working and queue in IndexedDB; reads serve the local view; everything pushes on reconnect (safe to retry — ops are idempotent). The queue survives page reloads — including offline cold starts: reloading with no network serves all data from the local replica.
|
|
235
285
|
|
|
236
286
|
`revoked` means the user disconnected your app from their account — data access is gone until they sign in and consent again.
|
|
237
287
|
|
|
@@ -327,6 +377,7 @@ The table API is identical, with REST semantics: `create` gets its id from the s
|
|
|
327
377
|
| --- | --- | --- |
|
|
328
378
|
| `schema` | — | Your Basic schema document (`project_id` is read from it) |
|
|
329
379
|
| `mode` | `"sync"` | `"sync"` (local replica) or `"rest"` (direct API) |
|
|
380
|
+
| `anonymous` | `true` | Local workspace without sign-in; merges into the account on sign-in |
|
|
330
381
|
| `auth.scopes` | `"profile,email,app:admin"` | OAuth scopes to request |
|
|
331
382
|
| `auth.pds_url` | `https://pds.basic.id` | The Basic server (auth + data + sync) |
|
|
332
383
|
| `auth.sync_url` | `wss://<pds>/sync/` | Sync WebSocket URL (rarely needed) |
|
|
@@ -339,14 +390,17 @@ The table API is identical, with REST semantics: `create` gets its id from the s
|
|
|
339
390
|
|
|
340
391
|
| Hook | Returns |
|
|
341
392
|
| --- | --- |
|
|
342
|
-
| `useBasic()` | Everything: auth fields + actions, `db`, `sync`, `devInfo`, `client` |
|
|
343
|
-
| `useAuth()` | `{ isReady, isSignedIn, status, user, did, scope, hasScope, missingScopes, signIn, signInWithHandle, signInWithCode, signOut, getToken, getSignInUrl }` |
|
|
344
|
-
| `useDb()` | The table API for the user's own data |
|
|
345
|
-
| `useQuery(fn, deps?)` | Live query result (`undefined` while loading) |
|
|
393
|
+
| `useBasic()` | Everything: auth fields + actions, `db`, `sync`, `users`, `activeUser`, `devInfo`, `client` |
|
|
394
|
+
| `useAuth()` | `{ isReady, isSignedIn, isAnonymous, status, user, did, scope, hasScope, missingScopes, signIn, signInWithHandle, signInWithCode, signOut, getToken, getSignInUrl }` |
|
|
395
|
+
| `useDb()` | The table API for the active user's own data |
|
|
396
|
+
| `useQuery(fn, deps?)` | Live query result (`undefined` while loading); re-attaches automatically on user switch |
|
|
346
397
|
| `useSyncStatus()` | `{ status, enabled, pendingCount, listRejected, clearRejected }` |
|
|
398
|
+
| `useUsers()` | `{ users, activeUser, isAnonymous, switchUser, addUser, removeUser }` |
|
|
347
399
|
| `useShares()` | `{ granted, received, isLoading, error, refresh }` |
|
|
348
400
|
| `useShare(shareId)` | `{ db, status, error }` — a mounted share |
|
|
349
401
|
|
|
402
|
+
Note on `useQuery` and user switching: the active user's identity is appended to your deps automatically, so queries that read `db` from the current render (via `useBasic()`/`useDb()`) re-attach to the new user's database on switch. If your query closure captures `db` from outside render, pass `[db]` in deps explicitly — or remount the subtree with `key={activeUser?.id}`.
|
|
403
|
+
|
|
350
404
|
### Table API
|
|
351
405
|
|
|
352
406
|
| Method | Sync mode | REST mode |
|