@docstack/react 0.1.1 → 0.1.3

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.
Files changed (2) hide show
  1. package/README.md +176 -133
  2. package/package.json +5 -4
package/README.md CHANGED
@@ -1,187 +1,230 @@
1
+ [![npm](https://img.shields.io/npm/v/@docstack/react)](https://www.npmjs.com/package/@docstack/react)
2
+ [![Docs](https://img.shields.io/badge/docs-onyx.ac-blue)](https://onyx.ac/products/docstack/docs)
3
+ [![License](https://img.shields.io/badge/license-CC--BY--SA--4.0-lightgrey)](https://github.com/onyx-og/docstack/blob/main/LICENSE.md)
4
+
1
5
  # @docstack/react
2
6
 
3
- React bindings for **DocStack Client**, enabling seamless integration of the intelligent, offline-first database engine into React applications.
7
+ **React bindings for [DocStack](https://github.com/onyx-og/docstack) an offline-first embedded database with schemas, SQL, access scopes and encryption.**
8
+
9
+ A provider that owns the database lifecycle, and hooks that are **live by default**. Every hook subscribes to the local database, so when a document changes — because the user edited it, because a background job wrote it, or because it arrived over sync — the components reading it re-render. There is no fetching layer, no cache to invalidate, and no staleness to reason about.
4
10
 
5
- This package provides a context provider and hooks to initialize the database, subscribe to data changes, and perform write operations.
11
+ ---
6
12
 
7
13
  ## 📦 Installation
8
14
 
9
15
  ```bash
10
- npm install @docstack/react @docstack/client
16
+ npm install @docstack/react @docstack/client pouchdb-browser pouchdb-find
11
17
  ```
12
18
 
13
- ## 🚀 Usage
19
+ React 19 is a peer dependency. `pouchdb-browser` and `pouchdb-find` are peers of `@docstack/client`.
14
20
 
15
- ### 1. Setup the Provider
21
+ ## 🚀 Why hooks instead of a data layer
16
22
 
17
- Wrap your application root with `<StackProvider>`. This component initializes the DocStack engine and applies any schema patches provided in the configuration.
23
+ In a typical React app, "data" means a server, a client cache, and the machinery between them query keys, invalidation, refetch intervals, optimistic updates and rollback.
18
24
 
19
- ```tsx
20
- import { StackProvider } from "@docstack/react";
25
+ DocStack removes the server from that path. The database is in the browser, so:
26
+
27
+ * **A query is a subscription.** `useClassDocs`, `useQuerySQL`, `useFind` and the domain hooks all re-run when the data they read changes. Nothing to invalidate.
28
+ * **Writes are immediate and already true.** `classObj.add(...)` returns after the document has landed locally. There is no optimistic state to reconcile, because the write is not a request.
29
+ * **`useQuerySQL` watches the right things.** It derives which classes to subscribe to from the query's own AST, so a `JOIN` across three classes re-runs when any of the three changes — and bursts coalesce into one re-run (150 ms by default).
30
+ * **Offline is not a state you handle.** Components render from local storage. Sync status is something you *display* (`useSyncStatus`), not something a read has to survive.
21
31
 
22
- const DB_NAME = 'my-app-db';
32
+ ## Quick start
23
33
 
24
- // Define schema patches (versioned changes to your data model)
25
- const PATCHES = [
26
- {
27
- "~class": "patch",
28
- version: "1.0.0",
34
+ ### 1. Mount the provider
35
+
36
+ `StackProvider` builds the DocStack instance, opens each configured database, and applies the schema patches it is given. It reconciles when `config` changes: a stack added to the array is opened, one removed is closed.
37
+
38
+ ```tsx
39
+ import { StackProvider } from '@docstack/react';
40
+
41
+ const PATCHES = [{
42
+ '~class': 'patch',
43
+ _id: 'my-app-0.1.0',
44
+ version: '0.1.0',
45
+ target: 'my-app',
46
+ changelog: 'Add the Todo class.',
29
47
  active: true,
30
- docs: [
31
- {
32
- _id: "Todo",
33
- "~class": "class",
34
- name: "Todo",
48
+ docs: [{
49
+ '~class': 'class',
50
+ _id: 'Todo',
51
+ name: 'Todo',
35
52
  schema: {
36
- title: { name: "title", type: "string", config: { mandatory: true } },
37
- completed: { name: "completed", type: "boolean", config: { defaultValue: false } }
38
- }
39
- }
40
- ]
41
- }
42
- ];
43
-
44
- function App() {
45
- return (
46
- <StackProvider config={[{ name: DB_NAME, patches: PATCHES }]}>
47
- <MyComponent />
53
+ title: { name: 'title', type: 'string', config: { mandatory: true } },
54
+ completed: { name: 'completed', type: 'boolean', config: { defaultValue: false } },
55
+ },
56
+ }],
57
+ }];
58
+
59
+ const App = () => (
60
+ <StackProvider config={[{ name: 'my-app', patches: PATCHES }]}>
61
+ <TodoList />
48
62
  </StackProvider>
49
- );
50
- }
63
+ );
51
64
  ```
52
65
 
53
- ### 2. Read Data (Reactive)
66
+ Each entry accepts everything `ClientStack` accepts — `patches`, `documentKey`, `transactions`, `logLevel`, `plugins`, `credentials`.
54
67
 
55
- Use the `useClassDocs` hook to fetch documents for a specific class. This hook automatically subscribes to changes, so your component re-renders whenever the underlying data changes.
68
+ ### 2. Read and stay live
56
69
 
57
70
  ```tsx
58
- import { useClassDocs } from "@docstack/react";
59
-
60
- function TodoList() {
61
- // Fetch all documents of class 'Todo' from 'my-app-db'
62
- const { docs: todos, loading } = useClassDocs('my-app-db', 'Todo');
63
-
64
- if (loading) return <p>Loading...</p>;
65
-
66
- return (
67
- <ul>
68
- {todos.map(todo => (
69
- <li key={todo._id}>
70
- {todo.title} {todo.completed ? '✅' : '⭕'}
71
- </li>
72
- ))}
73
- </ul>
74
- );
75
- }
71
+ import { useClassDocs } from '@docstack/react';
72
+
73
+ const TodoList = () => {
74
+ const { docs, loading } = useClassDocs('my-app', 'Todo');
75
+
76
+ if (loading) return <p>Loading…</p>;
77
+
78
+ return (
79
+ <ul>
80
+ {docs.map(todo => (
81
+ <li key={todo._id}>{todo.title} {todo.completed ? '✅' : '⭕'}</li>
82
+ ))}
83
+ </ul>
84
+ );
85
+ };
76
86
  ```
77
87
 
78
- ### 3. Write Data
88
+ Pass a Mango selector as the third argument to narrow it: `useClassDocs('my-app', 'Todo', { completed: { $eq: false } })`.
79
89
 
80
- Use the `useDocStack` hook to access the underlying DocStack client instance for creating, updating, or deleting data.
90
+ ### 3. Write
81
91
 
82
92
  ```tsx
83
93
  import { useState } from 'react';
84
- import { useClass } from "@docstack/react";
85
-
86
- function AddTodo() {
87
- const { classObj: todoClass } = useClass('my-app-db', 'Todo');
88
- const [title, setTitle] = useState("");
89
-
90
- const handleAdd = async () => {
91
- if (!todoClass || !title) return;
92
-
93
- // Add the new document
94
- await todoClass.add({ title, completed: false });
95
-
96
- setTitle("");
97
- };
98
-
99
- return (
100
- <div>
101
- <input value={title} onChange={e => setTitle(e.target.value)} />
102
- <button onClick={handleAdd}>Add Task</button>
103
- </div>
104
- );
105
- }
94
+ import { useClass } from '@docstack/react';
95
+
96
+ const AddTodo = () => {
97
+ const { classObj: todoClass } = useClass('my-app', 'Todo');
98
+ const [title, setTitle] = useState('');
99
+
100
+ const handleAdd = async () => {
101
+ if (!todoClass || !title) return;
102
+ await todoClass.add({ title, completed: false });
103
+ setTitle(''); // the list above updates itself
104
+ };
105
+
106
+ return (
107
+ <>
108
+ <input value={title} onChange={e => setTitle(e.target.value)} />
109
+ <button onClick={handleAdd}>Add</button>
110
+ </>
111
+ );
112
+ };
106
113
  ```
107
114
 
108
- ## 📚 API Reference
115
+ ### 4. SQL, live
109
116
 
110
- ### `<StackProvider />`
117
+ ```tsx
118
+ import { useQuerySQL } from '@docstack/react';
119
+
120
+ const Overdue = ({ today }: { today: string }) => {
121
+ const { result, loading, error } = useQuerySQL(
122
+ 'my-app',
123
+ `SELECT t.title, p.name AS project
124
+ FROM Todo AS t
125
+ JOIN Project AS p ON p._id = t.projectId
126
+ WHERE t.completed = false AND t.dueDate < ?
127
+ ORDER BY t.dueDate`,
128
+ [today],
129
+ );
130
+
131
+ if (loading) return <p>Loading…</p>;
132
+ if (error) return <p>Query failed</p>;
133
+
134
+ return <ul>{result.rows.map(r => <li key={r.title}>{r.title} — {r.project}</li>)}</ul>;
135
+ };
136
+ ```
111
137
 
112
- The context provider that manages the lifecycle of the DocStack client.
138
+ **Params are an array**, and the query is live. For a deliberate one-shot read, say so at the call site: `useQuerySQL(stack, sql, [today], { live: false })`.
113
139
 
114
- | Prop | Type | Description |
115
- |------|------|-------------|
116
- | `config` | `Array<{ name: string, patches: any[] }>` | Configuration for initializing stacks, including database names and schema patches. |
140
+ ### 5. Show sync state honestly
117
141
 
118
- ### `useClassDocs(dbName: string, className: string)`
142
+ ```tsx
143
+ import { useSyncStatus } from '@docstack/react';
144
+
145
+ const SyncBadge = ({ stack }: { stack: string }) => {
146
+ const status = useSyncStatus(stack)[stack];
147
+
148
+ if (!status) return <span>Not syncing</span>;
149
+ if (status.state === 'error') return <span>Offline — retrying</span>;
150
+ return <span>Synced {status.lastConvergedAt ? timeAgo(status.lastConvergedAt) : 'never'}</span>;
151
+ };
152
+ ```
153
+
154
+ `lastConvergedAt` is the value to render as "last synced" — it marks a cycle that finished with nothing left to send. `lastActiveAt` only says documents moved, which is not the same promise.
155
+
156
+ The subscription is on the stacks rather than on the replication handles, so it survives a `handle.restart()` (a refreshed credential, say) and works whether it mounts before or after `sync()` was called.
157
+
158
+ ## 📚 API reference
119
159
 
120
- A hook that subscribes to a specific class in the database.
160
+ ### `<StackProvider />`
121
161
 
122
- - **Arguments**:
123
- - `dbName`: The name of the database stack.
124
- - `className`: The name of the class to query.
125
- - **Returns**:
126
- - `docs`: An array of documents belonging to the class.
127
- - `loading`: A boolean indicating if the initial data load is in progress.
162
+ | Prop | Type | Description |
163
+ |---|---|---|
164
+ | `config` | `StackConfig[]` | Stacks to open. A string is the database name; an object accepts every `ClientStack` option (`name`, `patches`, `documentKey`, `transactions`, `logLevel`, `plugins`, `credentials`). Reconciled on change. |
165
+ | `credentials` | `ClientCredentials \| ClientCredentials[]` | One credential for every stack, or one per config entry. Merged into the configs it applies to. |
166
+ | `destroyRemovedStacks` | `boolean` | Delete the underlying database when a stack drops out of `config`. Defaults to `false` — a workspace that disappears from the configuration is closed, not erased. |
128
167
 
129
- ### `useClass(dbName: string, className: string)`
168
+ The context publishes `null` until the instance is ready. That window is startup, not a missing provider — every hook handles it, and `useDocStack()` returning `null` is the signal to render a splash rather than an error.
130
169
 
131
- A hook that retrieves the class definition for writing data.
170
+ ### Hooks
132
171
 
133
- - **Arguments**:
134
- - `dbName`: The name of the database stack.
135
- - `className`: The name of the class.
136
- - **Returns**:
137
- - `classObj`: The class instance used for operations like `add` or `updateCard`.
138
- - `loading`: Boolean indicating if the class is loading.
172
+ | Hook | Signature | Returns |
173
+ |---|---|---|
174
+ | `useDocStack` | `()` | The `DocStack` instance, or `null` during startup |
175
+ | `useClassDocs` | `(stack, className, query?)` | `{ docs, loading, error }` — live documents of a class, optionally filtered by a Mango selector |
176
+ | `useClass` | `(stack, className)` | `{ classObj, loading, error }` the `Class` instance, for `add` / `updateCard` / `addTrigger` |
177
+ | `useClassList` | `(stack, selector)` | `{ classList, loading, error }` the classes defined in the database |
178
+ | `useClassCreate` | `(stack)` | `(className, description?) => Promise<Class>` |
179
+ | `useQuerySQL` | `(stack, sql, params?, options?)` | `{ result, loading, error, refetch }` — `result.rows` and `result.ast`. Live unless `{ live: false }`; `{ coalesceMs }` defaults to 150 |
180
+ | `useFind` | `(stack, { selector, fields? }, sort?, limit?)` | `{ docs, loading, error }` — Mango query; `limit` defaults to 50 |
181
+ | `useSyncStatus` | `(stackName?)` | `Record<string, SyncStatus>` — one stack, or every open stack |
182
+ | `useDomain` | `(stack, domainName)` | `{ domain, loading, error }` |
183
+ | `useDomainList` | `(stack, selector)` | `{ domainList, loading, error }` |
184
+ | `useDomainRelations` | `(stack, domainName, query?)` | `{ docs, loading, error }` — live relation documents |
185
+ | `useDomainCreate` | `(stack)` | `(name, cardinality, sourceClass, targetClass, description?) => Promise<Domain>` |
139
186
 
140
- ### `useDocStack()`
187
+ The package also re-exports DocStack's document-modelling types (`Patch`, `ClassModel`, `AttributeModel`, `Document`, `SyncStatus`, `StackConfig`, …) — sourced from `@docstack/client` so a consumer using both never ends up holding two structurally identical but distinct copies of `Patch`.
141
188
 
142
- A hook that returns the initialized DocStack client instance.
189
+ ## 🧩 Patterns
143
190
 
144
- - **Returns**: The `DocStack` instance (or `null` if not yet initialized). Use this to access `getStack()`, run queries, or execute jobs.
191
+ **Gate on readiness, not on loading.** `useDocStack()` is `null` until the provider finishes opening its databases:
145
192
 
146
- ### `useQuerySQL(dbName: string, sql: string, ...params: any[])`
193
+ ```tsx
194
+ const Root = () => {
195
+ const docstack = useDocStack();
196
+ if (!docstack) return <Splash />;
197
+ return <App />;
198
+ };
199
+ ```
147
200
 
148
- A hook to execute SQL queries against the local database.
201
+ **A database per workspace.** `config` is an array, so multiple databases are the normal case, not a workaround. Give each workspace its own stack and pass the active one's name down:
202
+
203
+ ```tsx
204
+ <StackProvider config={workspaces.map(w => ({ name: w.stackName, patches: WORKSPACE_PATCHES }))}>
205
+ <Workspace stackName={active.stackName} />
206
+ </StackProvider>
207
+ ```
149
208
 
150
- - **Arguments**:
151
- - `dbName`: The name of the database stack.
152
- - `sql`: The SQL query string.
153
- - `params`: Variable arguments for query parameters.
154
- - **Returns**:
155
- - `result`: An object containing `rows` (the query results).
156
- - `loading`: Boolean indicating execution status.
209
+ Every hook takes the stack name as its first argument, so switching workspaces is a prop change — not a remount, and not a second `DocStack` instance racing the first.
157
210
 
158
- ### `useFind(dbName: string, query: { selector: object, fields?: string[] })`
211
+ **Snapshots where you mean them.** A report that should not shift under the reader wants `{ live: false }`. Say it at the call site so the next reader knows it was a decision.
159
212
 
160
- A hook to find documents using a MongoDB-style selector.
213
+ **Reach for the client when hooks aren't the right shape.** `useDocStack()` gives you the full instance — `getStack(name)` for transactions, exports, job execution or `sync()`.
161
214
 
162
- - **Arguments**:
163
- - `dbName`: The name of the database stack.
164
- - `query`: An object with a `selector` property (Mango query syntax).
165
- - **Returns**:
166
- - `docs`: An array of matching documents.
167
- - `loading`: Boolean indicating if the search is in progress.
215
+ ## 🔍 How it compares
168
216
 
169
- ### `useClassList(dbName: string, selector: object)`
217
+ If you have used `useLiveQuery` in [Dexie](https://dexie.org/) or the reactive queries in [RxDB](https://rxdb.info/), the reactivity model here will feel familiar: a query that re-runs when its data changes, backed by local storage.
170
218
 
171
- A hook to retrieve a list of available classes (schemas) in the database.
219
+ What differs is what sits underneath the hook. A `useQuerySQL` call resolves against an engine that already applies schema validation, access scopes and field-level decryption, and the SQL it runs supports joins, aggregation and subqueries rather than a selector API. And because DocStack replicates to any PouchDB-compatible remote — including a folder in the end user's own Google Drive — `useSyncStatus` can describe a sync you never had to run a server for.
172
220
 
173
- - **Arguments**:
174
- - `dbName`: The name of the database stack.
175
- - `selector`: A filter object to select specific classes.
176
- - **Returns**:
177
- - `classList`: An array of `Class` instances.
178
- - `loading`: Boolean indicating if the list is loading.
221
+ ## 📖 Documentation
179
222
 
180
- ### `useClassCreate(dbName: string)`
223
+ * [Full documentation](https://onyx.ac/products/docstack/docs)
224
+ * [@docstack/client](https://github.com/onyx-og/docstack/blob/main/packages/client/README.md) — the engine these hooks wrap
225
+ * [Architecture decisions](https://github.com/onyx-og/docstack/tree/main/specs/adr) — including [ADR-0025](https://github.com/onyx-og/docstack/blob/main/specs/adr/0025-live-usequerysql.md) on live `useQuerySQL` and [ADR-0035](https://github.com/onyx-og/docstack/blob/main/specs/adr/0035-react-usefind-never-applies-an-empty-result.md) on `useFind` result ordering
226
+ * [Contributing](https://github.com/onyx-og/docstack/blob/main/CONTRIBUTING.md)
181
227
 
182
- A hook that returns a function to create new classes dynamically.
228
+ ## License
183
229
 
184
- - **Arguments**:
185
- - `dbName`: The name of the database stack.
186
- - **Returns**:
187
- - A function `(className: string, description?: string) => Promise<Class>` to create a new class.
230
+ [CC-BY-SA-4.0](https://github.com/onyx-og/docstack/blob/main/LICENSE.md) · © Onyx AC, LLC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docstack/react",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "One does not simply stack documents.",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.js",
@@ -23,7 +23,8 @@
23
23
  "components",
24
24
  "database",
25
25
  "documents",
26
- "nosql", "sql",
26
+ "nosql",
27
+ "sql",
27
28
  "storage",
28
29
  "state-management"
29
30
  ],
@@ -32,13 +33,13 @@
32
33
  "bugs": {
33
34
  "url": "https://github.com/onyx-og/docstack/issues"
34
35
  },
35
- "homepage": "https://onyx.ac/docstack",
36
+ "homepage": "https://onyx.ac/products/docstack",
36
37
  "peerDependencies": {
37
38
  "react": "^19.2.3",
38
39
  "react-dom": "^19.2.3"
39
40
  },
40
41
  "dependencies": {
41
- "@docstack/client": "^0.1.5",
42
+ "@docstack/client": "^0.3.2",
42
43
  "react": "^19.2.3",
43
44
  "react-dom": "^19.2.3"
44
45
  },