@docstack/react 0.1.0 → 0.1.2
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 +176 -133
- package/lib/components/StackProvider/index.d.ts +0 -0
- package/lib/components/StackProvider/index.js +0 -0
- package/lib/components/index.d.ts +0 -0
- package/lib/components/index.js +0 -0
- package/lib/hooks/class.d.ts +0 -0
- package/lib/hooks/class.js +0 -0
- package/lib/hooks/domain.d.ts +0 -0
- package/lib/hooks/domain.js +0 -0
- package/lib/hooks/index.d.ts +0 -0
- package/lib/hooks/index.js +16 -8
- package/lib/hooks/sync.d.ts +0 -0
- package/lib/hooks/sync.js +0 -0
- package/lib/index.d.ts +0 -0
- package/lib/index.js +0 -0
- package/package.json +4 -3
- package/src/components/StackProvider/index.tsx +0 -0
- package/src/components/index.ts +0 -0
- package/src/hooks/class.ts +0 -0
- package/src/hooks/domain.ts +0 -0
- package/src/hooks/index.ts +14 -9
- package/src/hooks/sync.ts +0 -0
- package/src/index.ts +0 -0
package/README.md
CHANGED
|
@@ -1,187 +1,230 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@docstack/react)
|
|
2
|
+
[](https://onyx-og.github.io/docstack/)
|
|
3
|
+
[](https://github.com/onyx-og/docstack/blob/main/LICENSE.md)
|
|
4
|
+
|
|
1
5
|
# @docstack/react
|
|
2
6
|
|
|
3
|
-
React bindings for
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
React 19 is a peer dependency. `pouchdb-browser` and `pouchdb-find` are peers of `@docstack/client`.
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
## 🚀 Why hooks instead of a data layer
|
|
16
22
|
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
32
|
+
## ⚡ Quick start
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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:
|
|
33
|
-
|
|
34
|
-
name: "Todo",
|
|
48
|
+
docs: [{
|
|
49
|
+
'~class': 'class',
|
|
50
|
+
_id: 'Todo',
|
|
51
|
+
name: 'Todo',
|
|
35
52
|
schema: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
66
|
+
Each entry accepts everything `ClientStack` accepts — `patches`, `documentKey`, `transactions`, `logLevel`, `plugins`, `credentials`.
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
### 2. Read — and stay live
|
|
56
69
|
|
|
57
70
|
```tsx
|
|
58
|
-
import { useClassDocs } from
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
88
|
+
Pass a Mango selector as the third argument to narrow it: `useClassDocs('my-app', 'Todo', { completed: { $eq: false } })`.
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
### 3. Write
|
|
81
91
|
|
|
82
92
|
```tsx
|
|
83
93
|
import { useState } from 'react';
|
|
84
|
-
import { useClass } from
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
115
|
+
### 4. SQL, live
|
|
109
116
|
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
+
### `<StackProvider />`
|
|
121
161
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
### Hooks
|
|
132
171
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
+
## 🧩 Patterns
|
|
143
190
|
|
|
144
|
-
|
|
191
|
+
**Gate on readiness, not on loading.** `useDocStack()` is `null` until the provider finishes opening its databases:
|
|
145
192
|
|
|
146
|
-
|
|
193
|
+
```tsx
|
|
194
|
+
const Root = () => {
|
|
195
|
+
const docstack = useDocStack();
|
|
196
|
+
if (!docstack) return <Splash />;
|
|
197
|
+
return <App />;
|
|
198
|
+
};
|
|
199
|
+
```
|
|
147
200
|
|
|
148
|
-
A
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
+
* [Full documentation](https://onyx-og.github.io/docstack/)
|
|
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
|
-
|
|
228
|
+
## License
|
|
183
229
|
|
|
184
|
-
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/components/index.js
CHANGED
|
File without changes
|
package/lib/hooks/class.d.ts
CHANGED
|
File without changes
|
package/lib/hooks/class.js
CHANGED
|
File without changes
|
package/lib/hooks/domain.d.ts
CHANGED
|
File without changes
|
package/lib/hooks/domain.js
CHANGED
|
File without changes
|
package/lib/hooks/index.d.ts
CHANGED
|
File without changes
|
package/lib/hooks/index.js
CHANGED
|
@@ -137,6 +137,9 @@ export const useFind = (stack, query, sort, limit = 50) => {
|
|
|
137
137
|
const [docs, setDocs] = useState([]);
|
|
138
138
|
const [loading, setLoading] = useState(true);
|
|
139
139
|
const [error, setError] = useState(null);
|
|
140
|
+
// Guards against a slow earlier run overwriting a fast later one - the same
|
|
141
|
+
// discipline as useQuerySQL's runId above.
|
|
142
|
+
const runId = useRef(0);
|
|
140
143
|
useEffect(() => {
|
|
141
144
|
var _a;
|
|
142
145
|
// Check if the docStack instance is available
|
|
@@ -155,22 +158,27 @@ export const useFind = (stack, query, sort, limit = 50) => {
|
|
|
155
158
|
}
|
|
156
159
|
setLoading(true);
|
|
157
160
|
const runQuery = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
|
+
const id = ++runId.current;
|
|
158
162
|
try {
|
|
159
163
|
const stackInstance = docStack.getStack(stack);
|
|
160
164
|
if (stackInstance) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
const found = yield stackInstance.findDocuments(query.selector, query.fields);
|
|
166
|
+
// An empty result is a result. This setter used to be guarded on
|
|
167
|
+
// `.docs.length`, so a live list could gain rows but never lose its
|
|
168
|
+
// last one - a deleted document stayed on screen until a remount.
|
|
169
|
+
// The hazard that guard was standing in for is *staleness*, and the
|
|
170
|
+
// counter above owns that. See ADR-0035.
|
|
171
|
+
if (id === runId.current)
|
|
172
|
+
setDocs(found.docs);
|
|
167
173
|
}
|
|
168
174
|
}
|
|
169
175
|
catch (err) {
|
|
170
|
-
|
|
176
|
+
if (id === runId.current)
|
|
177
|
+
setError(err);
|
|
171
178
|
}
|
|
172
179
|
finally {
|
|
173
|
-
|
|
180
|
+
if (id === runId.current)
|
|
181
|
+
setLoading(false);
|
|
174
182
|
}
|
|
175
183
|
});
|
|
176
184
|
runQuery();
|
package/lib/hooks/sync.d.ts
CHANGED
|
File without changes
|
package/lib/hooks/sync.js
CHANGED
|
File without changes
|
package/lib/index.d.ts
CHANGED
|
File without changes
|
package/lib/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docstack/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
26
|
+
"nosql",
|
|
27
|
+
"sql",
|
|
27
28
|
"storage",
|
|
28
29
|
"state-management"
|
|
29
30
|
],
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"react-dom": "^19.2.3"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"@docstack/client": "^0.
|
|
42
|
+
"@docstack/client": "^0.3.0",
|
|
42
43
|
"react": "^19.2.3",
|
|
43
44
|
"react-dom": "^19.2.3"
|
|
44
45
|
},
|
|
File without changes
|
package/src/components/index.ts
CHANGED
|
File without changes
|
package/src/hooks/class.ts
CHANGED
|
File without changes
|
package/src/hooks/domain.ts
CHANGED
|
File without changes
|
package/src/hooks/index.ts
CHANGED
|
@@ -184,6 +184,10 @@ export const useFind = (stack: string, query: {
|
|
|
184
184
|
const [loading, setLoading] = useState(true);
|
|
185
185
|
const [error, setError] = useState(null);
|
|
186
186
|
|
|
187
|
+
// Guards against a slow earlier run overwriting a fast later one - the same
|
|
188
|
+
// discipline as useQuerySQL's runId above.
|
|
189
|
+
const runId = useRef(0);
|
|
190
|
+
|
|
187
191
|
useEffect(() => {
|
|
188
192
|
// Check if the docStack instance is available
|
|
189
193
|
if (!docStack) {
|
|
@@ -203,21 +207,22 @@ export const useFind = (stack: string, query: {
|
|
|
203
207
|
setLoading(true);
|
|
204
208
|
|
|
205
209
|
const runQuery = async () => {
|
|
210
|
+
const id = ++runId.current;
|
|
206
211
|
try {
|
|
207
212
|
const stackInstance = docStack.getStack(stack);
|
|
208
213
|
if (stackInstance) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
const found = await stackInstance.findDocuments(query.selector, query.fields);
|
|
215
|
+
// An empty result is a result. This setter used to be guarded on
|
|
216
|
+
// `.docs.length`, so a live list could gain rows but never lose its
|
|
217
|
+
// last one - a deleted document stayed on screen until a remount.
|
|
218
|
+
// The hazard that guard was standing in for is *staleness*, and the
|
|
219
|
+
// counter above owns that. See ADR-0035.
|
|
220
|
+
if (id === runId.current) setDocs(found.docs as Document[]);
|
|
215
221
|
}
|
|
216
|
-
|
|
217
222
|
} catch (err: any) {
|
|
218
|
-
setError(err);
|
|
223
|
+
if (id === runId.current) setError(err);
|
|
219
224
|
} finally {
|
|
220
|
-
setLoading(false);
|
|
225
|
+
if (id === runId.current) setLoading(false);
|
|
221
226
|
}
|
|
222
227
|
};
|
|
223
228
|
|
package/src/hooks/sync.ts
CHANGED
|
File without changes
|
package/src/index.ts
CHANGED
|
File without changes
|