@docstack/react 0.0.9 → 0.1.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/lib/components/StackProvider/index.d.ts +24 -7
- package/lib/components/StackProvider/index.js +102 -24
- package/lib/hooks/class.d.ts +1 -1
- package/lib/hooks/class.js +76 -18
- package/lib/hooks/domain.d.ts +1 -2
- package/lib/hooks/domain.js +75 -18
- package/lib/hooks/index.d.ts +23 -5
- package/lib/hooks/index.js +128 -85
- package/lib/hooks/sync.d.ts +27 -0
- package/lib/hooks/sync.js +76 -0
- package/lib/index.d.ts +11 -1
- package/lib/index.js +2 -1
- package/package.json +2 -3
- package/src/components/StackProvider/index.tsx +118 -34
- package/src/hooks/class.ts +69 -21
- package/src/hooks/domain.ts +68 -22
- package/src/hooks/index.ts +153 -69
- package/src/hooks/sync.ts +84 -0
- package/src/index.ts +36 -2
- package/lib/index.js.LICENSE.txt +0 -9
- package/lib/index.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { DocStack } from '@docstack/client';
|
|
3
|
-
import { ClientCredentials, StackConfig } from '@docstack/
|
|
3
|
+
import { ClientCredentials, StackConfig } from '@docstack/client';
|
|
4
4
|
/**
|
|
5
5
|
* Context object for the DocStack instance.
|
|
6
6
|
* It provides the current DocStack instance or null if not initialized.
|
|
@@ -39,23 +39,40 @@ export interface DocStackProviderProps {
|
|
|
39
39
|
config: StackConfig[];
|
|
40
40
|
/** Credentials for the stack(s). Can be a single object or an array matching the config. */
|
|
41
41
|
credentials?: ClientCredentials | ClientCredentials[];
|
|
42
|
+
/**
|
|
43
|
+
* Delete the underlying database when a stack drops out of `config`. Defaults to
|
|
44
|
+
* `false`: a workspace that disappears from the configuration is closed, not erased.
|
|
45
|
+
*/
|
|
46
|
+
destroyRemovedStacks?: boolean;
|
|
42
47
|
/** Child components. */
|
|
43
48
|
children?: ReactNode;
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
46
51
|
* A provider component that initializes the DocStack client and makes it available
|
|
47
52
|
* to child components via the {@link useDocStack} hook.
|
|
48
|
-
*
|
|
53
|
+
*
|
|
54
|
+
* The `config` prop is reconciled rather than read once: a stack that appears in it is
|
|
55
|
+
* opened, a stack that disappears is closed, and the stacks either side of the change
|
|
56
|
+
* are left running. An application whose set of databases grows at runtime - one per
|
|
57
|
+
* workspace, say - therefore does not have to reload to pick up a new one, which
|
|
58
|
+
* matters once each stack also carries a live replication that a reload would drop.
|
|
49
59
|
*
|
|
50
60
|
* @example
|
|
51
61
|
* ```tsx
|
|
52
62
|
* import { StackProvider } from '@docstack/react';
|
|
53
63
|
*
|
|
54
|
-
* const App = () =>
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
64
|
+
* const App = () => {
|
|
65
|
+
* const workspaces = useWorkspaces();
|
|
66
|
+
* const config = useMemo(
|
|
67
|
+
* () => [{ name: 'app' }, ...workspaces.map(w => ({ name: `ws-${w.slug}` }))],
|
|
68
|
+
* [workspaces]
|
|
69
|
+
* );
|
|
70
|
+
* return (
|
|
71
|
+
* <StackProvider config={config}>
|
|
72
|
+
* <MyApp />
|
|
73
|
+
* </StackProvider>
|
|
74
|
+
* );
|
|
75
|
+
* };
|
|
59
76
|
* ```
|
|
60
77
|
*/
|
|
61
78
|
declare const StackProvider: (props: DocStackProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
1
10
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useRef, useCallback, useEffect, useState } from 'react';
|
|
11
|
+
import { createContext, useContext, useRef, useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
|
3
12
|
import { DocStack } from '@docstack/client'; // Import your DocStack class
|
|
4
13
|
// You can give it a default value, e.g., null, which can be checked later.
|
|
5
14
|
/**
|
|
@@ -26,52 +35,121 @@ export const DocStackContext = createContext(null);
|
|
|
26
35
|
export const useDocStack = () => {
|
|
27
36
|
return useContext(DocStackContext);
|
|
28
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* The name a configuration entry will end up carrying as a stack.
|
|
40
|
+
*
|
|
41
|
+
* Mirrors `DocStack.resolveStackConfig`: a string configuration *is* the name, an
|
|
42
|
+
* object's `name` wins, and a connection-only entry is identified by its connection.
|
|
43
|
+
*
|
|
44
|
+
* @param config - One stack configuration.
|
|
45
|
+
* @returns The identifier to reconcile on.
|
|
46
|
+
*/
|
|
47
|
+
const stackKey = (config) => {
|
|
48
|
+
if (typeof config === 'string')
|
|
49
|
+
return config;
|
|
50
|
+
return config.name || config.connection || '';
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Merges the `credentials` prop into the configurations it applies to.
|
|
54
|
+
*
|
|
55
|
+
* @param config - The configurations as given.
|
|
56
|
+
* @param credentials - One credential for every stack, or one per configuration entry.
|
|
57
|
+
* @returns Configurations with credentials folded in.
|
|
58
|
+
*/
|
|
59
|
+
const mergeCredentials = (config, credentials) => config.map((cfg, idx) => {
|
|
60
|
+
const cred = Array.isArray(credentials) ? credentials[idx] : credentials;
|
|
61
|
+
if (typeof cfg === 'string') {
|
|
62
|
+
return cred ? { connection: `db-${cfg}`, name: cfg, credentials: cred } : cfg;
|
|
63
|
+
}
|
|
64
|
+
return cred ? Object.assign(Object.assign({}, cfg), { credentials: cred }) : cfg;
|
|
65
|
+
});
|
|
29
66
|
/**
|
|
30
67
|
* A provider component that initializes the DocStack client and makes it available
|
|
31
68
|
* to child components via the {@link useDocStack} hook.
|
|
32
|
-
*
|
|
69
|
+
*
|
|
70
|
+
* The `config` prop is reconciled rather than read once: a stack that appears in it is
|
|
71
|
+
* opened, a stack that disappears is closed, and the stacks either side of the change
|
|
72
|
+
* are left running. An application whose set of databases grows at runtime - one per
|
|
73
|
+
* workspace, say - therefore does not have to reload to pick up a new one, which
|
|
74
|
+
* matters once each stack also carries a live replication that a reload would drop.
|
|
33
75
|
*
|
|
34
76
|
* @example
|
|
35
77
|
* ```tsx
|
|
36
78
|
* import { StackProvider } from '@docstack/react';
|
|
37
79
|
*
|
|
38
|
-
* const App = () =>
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
80
|
+
* const App = () => {
|
|
81
|
+
* const workspaces = useWorkspaces();
|
|
82
|
+
* const config = useMemo(
|
|
83
|
+
* () => [{ name: 'app' }, ...workspaces.map(w => ({ name: `ws-${w.slug}` }))],
|
|
84
|
+
* [workspaces]
|
|
85
|
+
* );
|
|
86
|
+
* return (
|
|
87
|
+
* <StackProvider config={config}>
|
|
88
|
+
* <MyApp />
|
|
89
|
+
* </StackProvider>
|
|
90
|
+
* );
|
|
91
|
+
* };
|
|
43
92
|
* ```
|
|
44
93
|
*/
|
|
45
94
|
const StackProvider = (props) => {
|
|
46
|
-
const { config, children, credentials } = props;
|
|
95
|
+
const { config, children, credentials, destroyRemovedStacks } = props;
|
|
47
96
|
// Use a ref to store the DocStack instance
|
|
48
97
|
const docStackRef = useRef(null);
|
|
49
98
|
const [docStack, setDocStack] = useState(null);
|
|
99
|
+
// The DocStack instance is stable across reconciliations, so adding or removing a
|
|
100
|
+
// stack changes nothing React can see by itself.
|
|
101
|
+
const [, signalStacksChanged] = useReducer((count) => count + 1, 0);
|
|
102
|
+
// Reconciliations are serialized: opening a database is asynchronous and two
|
|
103
|
+
// overlapping passes would race to add the same stack twice.
|
|
104
|
+
const reconciling = useRef(Promise.resolve());
|
|
105
|
+
const mergedConfig = useMemo(() => mergeCredentials(config, credentials),
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
[JSON.stringify(config), JSON.stringify(credentials)]);
|
|
50
108
|
const setsDocStackWhenReady = useCallback(() => {
|
|
51
109
|
setDocStack(docStackRef.current);
|
|
52
110
|
}, []);
|
|
53
111
|
useEffect(() => {
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (typeof cfg === "string") {
|
|
59
|
-
return cred ? { connection: cfg, credentials: cred } : cfg;
|
|
60
|
-
}
|
|
61
|
-
return cred ? Object.assign(Object.assign({}, cfg), { credentials: cred }) : cfg;
|
|
62
|
-
});
|
|
112
|
+
if (!mergedConfig.length)
|
|
113
|
+
return;
|
|
114
|
+
if (docStackRef.current === null) {
|
|
115
|
+
console.log("DocStack provider - init instance", { config: mergedConfig });
|
|
63
116
|
const instance = new DocStack(...mergedConfig);
|
|
64
117
|
docStackRef.current = instance;
|
|
65
|
-
|
|
118
|
+
instance.addEventListener("ready", setsDocStackWhenReady);
|
|
119
|
+
instance.addEventListener("stack-added", signalStacksChanged);
|
|
120
|
+
instance.addEventListener("stack-removed", signalStacksChanged);
|
|
121
|
+
return;
|
|
66
122
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
123
|
+
const instance = docStackRef.current;
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
const reconcile = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
if (cancelled)
|
|
127
|
+
return;
|
|
128
|
+
const wanted = new Map(mergedConfig.map(cfg => [stackKey(cfg), cfg]));
|
|
129
|
+
for (const stack of [...instance.getStacks()]) {
|
|
130
|
+
if (cancelled)
|
|
131
|
+
return;
|
|
132
|
+
if (!wanted.has(stack.name)) {
|
|
133
|
+
console.log("DocStack provider - closing stack dropped from config", { name: stack.name });
|
|
134
|
+
yield instance.removeStack(stack.name, { destroy: destroyRemovedStacks });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const [name, cfg] of wanted) {
|
|
138
|
+
if (cancelled)
|
|
139
|
+
return;
|
|
140
|
+
if (!instance.getStack(name)) {
|
|
141
|
+
console.log("DocStack provider - opening stack added to config", { name });
|
|
142
|
+
yield instance.addStack(cfg);
|
|
143
|
+
}
|
|
72
144
|
}
|
|
145
|
+
});
|
|
146
|
+
reconciling.current = reconciling.current.then(reconcile).catch(error => {
|
|
147
|
+
console.error("DocStack provider - failed to reconcile stacks", error);
|
|
148
|
+
});
|
|
149
|
+
return () => {
|
|
150
|
+
cancelled = true;
|
|
73
151
|
};
|
|
74
|
-
}, [
|
|
152
|
+
}, [mergedConfig, destroyRemovedStacks, setsDocStackWhenReady]);
|
|
75
153
|
return (_jsx(DocStackContext.Provider, { value: docStack, children: children }));
|
|
76
154
|
};
|
|
77
155
|
export default StackProvider;
|
package/lib/hooks/class.d.ts
CHANGED
package/lib/hooks/class.js
CHANGED
|
@@ -39,7 +39,9 @@ export const useClassCreate = (stack) => {
|
|
|
39
39
|
if (!docStack) {
|
|
40
40
|
// Handle the case where the provider is not yet initialized or missing
|
|
41
41
|
// You could throw an error or return an empty state.
|
|
42
|
-
|
|
42
|
+
// Null until the provider's `ready` event; that is startup,
|
|
43
|
+
// not a missing provider. See ADR-0022.
|
|
44
|
+
console.warn('useClassCreate - stack not ready yet; the call was ignored.');
|
|
43
45
|
// setLoading(false);
|
|
44
46
|
return Promise.resolve(null);
|
|
45
47
|
}
|
|
@@ -94,7 +96,10 @@ export const useClassList = (stack, selector) => {
|
|
|
94
96
|
useEffect(() => {
|
|
95
97
|
// Only run if the docStack is available and a className is provided
|
|
96
98
|
if (!docStack) {
|
|
97
|
-
|
|
99
|
+
// Null until the provider's `ready` event: startup, not a missing
|
|
100
|
+
// provider. Reporting "loaded" here is indistinguishable from a genuinely
|
|
101
|
+
// empty result. See ADR-0022.
|
|
102
|
+
setLoading(true);
|
|
98
103
|
return;
|
|
99
104
|
}
|
|
100
105
|
const fetchClass = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -123,6 +128,8 @@ export const useClassList = (stack, selector) => {
|
|
|
123
128
|
if (!originClass) {
|
|
124
129
|
return;
|
|
125
130
|
}
|
|
131
|
+
let cancelled = false;
|
|
132
|
+
let attached = null;
|
|
126
133
|
const runQueryAndListen = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
127
134
|
setLoading(true);
|
|
128
135
|
try {
|
|
@@ -133,15 +140,26 @@ export const useClassList = (stack, selector) => {
|
|
|
133
140
|
const classInstance = yield Class.buildFromModel(stackInstance, cls);
|
|
134
141
|
initialClassList.push(classInstance);
|
|
135
142
|
}
|
|
143
|
+
if (cancelled) {
|
|
144
|
+
// The effect was torn down mid-query; these were built anyway, and
|
|
145
|
+
// each one holds a live subscription until it is closed.
|
|
146
|
+
for (const classInstance of initialClassList)
|
|
147
|
+
classInstance.close();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
136
150
|
classListRef.current = initialClassList;
|
|
137
151
|
setClassList(classListRef.current);
|
|
138
152
|
}
|
|
139
153
|
catch (err) {
|
|
140
|
-
|
|
154
|
+
if (!cancelled)
|
|
155
|
+
setError(err);
|
|
141
156
|
}
|
|
142
157
|
finally {
|
|
143
|
-
|
|
158
|
+
if (!cancelled)
|
|
159
|
+
setLoading(false);
|
|
144
160
|
}
|
|
161
|
+
if (cancelled)
|
|
162
|
+
return;
|
|
145
163
|
const changeListener = (change) => {
|
|
146
164
|
const doc = change.detail.doc;
|
|
147
165
|
console.log("useClassDocs - detail", { detail: change.detail });
|
|
@@ -174,12 +192,24 @@ export const useClassList = (stack, selector) => {
|
|
|
174
192
|
}
|
|
175
193
|
setClassList([...classListRef.current]);
|
|
176
194
|
};
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
originClass.removeEventListener('doc', changeListener);
|
|
180
|
-
};
|
|
195
|
+
attached = changeListener;
|
|
196
|
+
originClass.addEventListener('doc', attached);
|
|
181
197
|
});
|
|
182
198
|
runQueryAndListen();
|
|
199
|
+
// The cleanup used to be returned from `runQueryAndListen`, where React never saw
|
|
200
|
+
// it: the listener stayed attached and the built classes stayed subscribed for
|
|
201
|
+
// every render that changed the selector.
|
|
202
|
+
return () => {
|
|
203
|
+
var _a;
|
|
204
|
+
cancelled = true;
|
|
205
|
+
if (attached)
|
|
206
|
+
originClass.removeEventListener('doc', attached);
|
|
207
|
+
// Guarded: the change handler above pushes the raw document for a class it
|
|
208
|
+
// has not seen before, so the list is not uniformly Class instances.
|
|
209
|
+
for (const classInstance of classListRef.current)
|
|
210
|
+
(_a = classInstance === null || classInstance === void 0 ? void 0 : classInstance.close) === null || _a === void 0 ? void 0 : _a.call(classInstance);
|
|
211
|
+
classListRef.current = [];
|
|
212
|
+
};
|
|
183
213
|
}, [originClass, JSON.stringify(selector)]); // Dependency on classObj and query
|
|
184
214
|
return { classList, loading, error };
|
|
185
215
|
};
|
|
@@ -212,8 +242,14 @@ export const useClass = (stack, className) => {
|
|
|
212
242
|
if (!docStack) {
|
|
213
243
|
// Handle the case where the provider is not yet initialized or missing
|
|
214
244
|
// You could throw an error or return an empty state.
|
|
215
|
-
|
|
216
|
-
|
|
245
|
+
// The provider publishes `null` into the context until its `ready`
|
|
246
|
+
// event fires, so this is the normal startup window, not a missing
|
|
247
|
+
// provider. Reporting it as one sends the reader hunting for a bug
|
|
248
|
+
// that is not there - and `setLoading(false)` was worse than the
|
|
249
|
+
// message: it tells a consumer "loaded, and empty" during startup,
|
|
250
|
+
// which is indistinguishable from a genuinely empty result. See
|
|
251
|
+
// ADR-0022.
|
|
252
|
+
setLoading(true);
|
|
217
253
|
return;
|
|
218
254
|
}
|
|
219
255
|
const fetchClass = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -280,7 +316,16 @@ export const useClassDocs = (stack, className, query = {}) => {
|
|
|
280
316
|
const [error, setError] = useState(null);
|
|
281
317
|
useEffect(() => {
|
|
282
318
|
// Only run if the docStack is available and a className is provided
|
|
283
|
-
if (!docStack
|
|
319
|
+
if (!docStack) {
|
|
320
|
+
// Null until the provider's `ready` event: startup, not a missing
|
|
321
|
+
// provider. Reporting "loaded" here is indistinguishable from a genuinely
|
|
322
|
+
// empty result. See ADR-0022.
|
|
323
|
+
setLoading(true);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!className) {
|
|
327
|
+
// A genuinely absent className is "nothing to load", which is a settled state -
|
|
328
|
+
// unlike the pre-ready window above.
|
|
284
329
|
setLoading(false);
|
|
285
330
|
return;
|
|
286
331
|
}
|
|
@@ -310,20 +355,28 @@ export const useClassDocs = (stack, className, query = {}) => {
|
|
|
310
355
|
if (!classObj) {
|
|
311
356
|
return;
|
|
312
357
|
}
|
|
358
|
+
let cancelled = false;
|
|
359
|
+
let attached = null;
|
|
313
360
|
const runQueryAndListen = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
314
361
|
setLoading(true);
|
|
315
362
|
try {
|
|
316
|
-
debugger;
|
|
363
|
+
// debugger;
|
|
317
364
|
const initialDocs = yield classObj.getCards(query);
|
|
365
|
+
if (cancelled)
|
|
366
|
+
return;
|
|
318
367
|
docsRef.current = initialDocs;
|
|
319
368
|
setDocs(docsRef.current);
|
|
320
369
|
}
|
|
321
370
|
catch (err) {
|
|
322
|
-
|
|
371
|
+
if (!cancelled)
|
|
372
|
+
setError(err);
|
|
323
373
|
}
|
|
324
374
|
finally {
|
|
325
|
-
|
|
375
|
+
if (!cancelled)
|
|
376
|
+
setLoading(false);
|
|
326
377
|
}
|
|
378
|
+
if (cancelled)
|
|
379
|
+
return;
|
|
327
380
|
const changeListener = (change) => {
|
|
328
381
|
const doc = change.detail.doc;
|
|
329
382
|
console.log("useClassDocs - detail", { detail: change.detail });
|
|
@@ -359,12 +412,17 @@ export const useClassDocs = (stack, className, query = {}) => {
|
|
|
359
412
|
}
|
|
360
413
|
setDocs([...docsRef.current]);
|
|
361
414
|
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
classObj.removeEventListener('doc', changeListener);
|
|
365
|
-
};
|
|
415
|
+
attached = changeListener;
|
|
416
|
+
classObj.addEventListener('doc', attached);
|
|
366
417
|
});
|
|
367
418
|
runQueryAndListen();
|
|
419
|
+
// The cleanup used to be returned from `runQueryAndListen`, so React never
|
|
420
|
+
// received it and each query change left another listener on the class.
|
|
421
|
+
return () => {
|
|
422
|
+
cancelled = true;
|
|
423
|
+
if (attached)
|
|
424
|
+
classObj.removeEventListener('doc', attached);
|
|
425
|
+
};
|
|
368
426
|
}, [classObj, JSON.stringify(query)]); // Dependency on classObj and query
|
|
369
427
|
return { docs, loading, error };
|
|
370
428
|
};
|
package/lib/hooks/domain.d.ts
CHANGED
package/lib/hooks/domain.js
CHANGED
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { useContext, useCallback, useEffect, useRef, useState } from "react";
|
|
11
11
|
import { DocStackContext } from "../components/StackProvider/index.js";
|
|
12
|
-
import { Domain } from "@docstack/
|
|
12
|
+
import { Domain } from "@docstack/client";
|
|
13
13
|
/**
|
|
14
14
|
* Hook to create a new Domain in a specific stack.
|
|
15
15
|
*
|
|
@@ -43,7 +43,9 @@ export const useDomainCreate = (stack) => {
|
|
|
43
43
|
if (!docStack) {
|
|
44
44
|
// Handle the case where the provider is not yet initialized or missing
|
|
45
45
|
// You could throw an error or return an empty state.
|
|
46
|
-
|
|
46
|
+
// Null until the provider's `ready` event; that is startup,
|
|
47
|
+
// not a missing provider. See ADR-0022.
|
|
48
|
+
console.warn('useDomainCreate - stack not ready yet; the call was ignored.');
|
|
47
49
|
// setLoading(false);
|
|
48
50
|
return Promise.resolve(null);
|
|
49
51
|
}
|
|
@@ -97,7 +99,10 @@ export const useDomainList = (stack, selector) => {
|
|
|
97
99
|
useEffect(() => {
|
|
98
100
|
// Only run if the docStack is available and a className is provided
|
|
99
101
|
if (!docStack) {
|
|
100
|
-
|
|
102
|
+
// Null until the provider's `ready` event: startup, not a missing
|
|
103
|
+
// provider. Reporting "loaded" here is indistinguishable from a genuinely
|
|
104
|
+
// empty result. See ADR-0022.
|
|
105
|
+
setLoading(true);
|
|
101
106
|
return;
|
|
102
107
|
}
|
|
103
108
|
const fetchClass = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -126,21 +131,34 @@ export const useDomainList = (stack, selector) => {
|
|
|
126
131
|
if (!originClass) {
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
134
|
+
let cancelled = false;
|
|
135
|
+
let attached = null;
|
|
129
136
|
const runQueryAndListen = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
137
|
setLoading(true);
|
|
131
138
|
try {
|
|
132
139
|
const stackInstance = docStack.getStack(stack);
|
|
133
140
|
const initialDomainModelList = yield originClass.getCards(selector);
|
|
134
141
|
const initDomainList = yield Promise.all(initialDomainModelList.map((dm) => __awaiter(void 0, void 0, void 0, function* () { return yield Domain.buildFromModel(stackInstance, dm); })));
|
|
142
|
+
if (cancelled) {
|
|
143
|
+
// Torn down mid-query; these were built anyway and each holds a live
|
|
144
|
+
// subscription until it is closed.
|
|
145
|
+
for (const domain of initDomainList)
|
|
146
|
+
domain.close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
135
149
|
domainListRef.current = initDomainList;
|
|
136
150
|
setDomainList(domainListRef.current);
|
|
137
151
|
}
|
|
138
152
|
catch (err) {
|
|
139
|
-
|
|
153
|
+
if (!cancelled)
|
|
154
|
+
setError(err);
|
|
140
155
|
}
|
|
141
156
|
finally {
|
|
142
|
-
|
|
157
|
+
if (!cancelled)
|
|
158
|
+
setLoading(false);
|
|
143
159
|
}
|
|
160
|
+
if (cancelled)
|
|
161
|
+
return;
|
|
144
162
|
const changeListener = (change) => {
|
|
145
163
|
const doc = change.detail.doc;
|
|
146
164
|
if (!doc.active) {
|
|
@@ -171,12 +189,23 @@ export const useDomainList = (stack, selector) => {
|
|
|
171
189
|
}
|
|
172
190
|
setDomainList([...domainListRef.current]);
|
|
173
191
|
};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
originClass.removeEventListener('doc', changeListener);
|
|
177
|
-
};
|
|
192
|
+
attached = changeListener;
|
|
193
|
+
originClass.addEventListener('doc', attached);
|
|
178
194
|
});
|
|
179
195
|
runQueryAndListen();
|
|
196
|
+
// The cleanup used to be returned from `runQueryAndListen`, where React never saw
|
|
197
|
+
// it: the listener stayed attached and the built domains stayed subscribed.
|
|
198
|
+
return () => {
|
|
199
|
+
var _a;
|
|
200
|
+
cancelled = true;
|
|
201
|
+
if (attached)
|
|
202
|
+
originClass.removeEventListener('doc', attached);
|
|
203
|
+
// Guarded: the change handler above pushes the raw document for a domain it
|
|
204
|
+
// has not seen before, so the list is not uniformly Domain instances.
|
|
205
|
+
for (const domain of domainListRef.current)
|
|
206
|
+
(_a = domain === null || domain === void 0 ? void 0 : domain.close) === null || _a === void 0 ? void 0 : _a.call(domain);
|
|
207
|
+
domainListRef.current = [];
|
|
208
|
+
};
|
|
180
209
|
}, [originClass, JSON.stringify(selector)]); // Dependency on classObj and query
|
|
181
210
|
return { domainList, loading, error };
|
|
182
211
|
};
|
|
@@ -209,8 +238,14 @@ export const useDomain = (stack, domainName) => {
|
|
|
209
238
|
if (!docStack) {
|
|
210
239
|
// Handle the case where the provider is not yet initialized or missing
|
|
211
240
|
// You could throw an error or return an empty state.
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
// The provider publishes `null` into the context until its `ready`
|
|
242
|
+
// event fires, so this is the normal startup window, not a missing
|
|
243
|
+
// provider. Reporting it as one sends the reader hunting for a bug
|
|
244
|
+
// that is not there - and `setLoading(false)` was worse than the
|
|
245
|
+
// message: it tells a consumer "loaded, and empty" during startup,
|
|
246
|
+
// which is indistinguishable from a genuinely empty result. See
|
|
247
|
+
// ADR-0022.
|
|
248
|
+
setLoading(true);
|
|
214
249
|
return;
|
|
215
250
|
}
|
|
216
251
|
const fetchClass = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -279,7 +314,16 @@ export const useDomainRelations = (stack, domainName, query = {}) => {
|
|
|
279
314
|
const [error, setError] = useState(null);
|
|
280
315
|
useEffect(() => {
|
|
281
316
|
// Only run if the docStack is available and a className is provided
|
|
282
|
-
if (!docStack
|
|
317
|
+
if (!docStack) {
|
|
318
|
+
// Null until the provider's `ready` event: startup, not a missing
|
|
319
|
+
// provider. Reporting "loaded" here is indistinguishable from a genuinely
|
|
320
|
+
// empty result. See ADR-0022.
|
|
321
|
+
setLoading(true);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!domainName) {
|
|
325
|
+
// A genuinely absent domainName is "nothing to load", which is a settled state -
|
|
326
|
+
// unlike the pre-ready window above.
|
|
283
327
|
setLoading(false);
|
|
284
328
|
return;
|
|
285
329
|
}
|
|
@@ -309,19 +353,27 @@ export const useDomainRelations = (stack, domainName, query = {}) => {
|
|
|
309
353
|
if (!domain) {
|
|
310
354
|
return;
|
|
311
355
|
}
|
|
356
|
+
let cancelled = false;
|
|
357
|
+
let attached = null;
|
|
312
358
|
const runQueryAndListen = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
313
359
|
setLoading(true);
|
|
314
360
|
try {
|
|
315
361
|
const initialDocs = yield domain.getRelations(query);
|
|
362
|
+
if (cancelled)
|
|
363
|
+
return;
|
|
316
364
|
docsRef.current = initialDocs;
|
|
317
365
|
setDocs(docsRef.current);
|
|
318
366
|
}
|
|
319
367
|
catch (err) {
|
|
320
|
-
|
|
368
|
+
if (!cancelled)
|
|
369
|
+
setError(err);
|
|
321
370
|
}
|
|
322
371
|
finally {
|
|
323
|
-
|
|
372
|
+
if (!cancelled)
|
|
373
|
+
setLoading(false);
|
|
324
374
|
}
|
|
375
|
+
if (cancelled)
|
|
376
|
+
return;
|
|
325
377
|
const changeListener = (change) => {
|
|
326
378
|
const doc = change.detail.doc;
|
|
327
379
|
if (!doc.active) {
|
|
@@ -355,12 +407,17 @@ export const useDomainRelations = (stack, domainName, query = {}) => {
|
|
|
355
407
|
}
|
|
356
408
|
setDocs([...docsRef.current]);
|
|
357
409
|
};
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
domain.removeEventListener('doc', changeListener);
|
|
361
|
-
};
|
|
410
|
+
attached = changeListener;
|
|
411
|
+
domain.addEventListener('doc', attached);
|
|
362
412
|
});
|
|
363
413
|
runQueryAndListen();
|
|
414
|
+
// The cleanup used to be returned from `runQueryAndListen`, so React never
|
|
415
|
+
// received it and each query change left another listener on the domain.
|
|
416
|
+
return () => {
|
|
417
|
+
cancelled = true;
|
|
418
|
+
if (attached)
|
|
419
|
+
domain.removeEventListener('doc', attached);
|
|
420
|
+
};
|
|
364
421
|
}, [domain, JSON.stringify(query)]); // Dependency on classObj and query
|
|
365
422
|
return { docs, loading, error };
|
|
366
423
|
};
|
package/lib/hooks/index.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { Document, SelectAST, UnionAST } from '@docstack/
|
|
1
|
+
import { Document, SelectAST, UnionAST } from '@docstack/client';
|
|
2
2
|
/**
|
|
3
3
|
* Hook to execute a SQL query against a specific stack.
|
|
4
4
|
*
|
|
5
|
+
* Live by default: the query re-runs when a document changes in a class it actually
|
|
6
|
+
* reads, derived from the `ast` the query itself returns. It used to run once and never
|
|
7
|
+
* again, which was invisible beside `useClassDocs` in the same import - one list
|
|
8
|
+
* refreshing next to one that did not. See ADR-0025.
|
|
9
|
+
*
|
|
5
10
|
* @param stack - The name of the stack to query.
|
|
6
11
|
* @param sql - The SQL query string.
|
|
7
|
-
* @param params -
|
|
8
|
-
* @
|
|
12
|
+
* @param params - Values for the query's `?` placeholders.
|
|
13
|
+
* @param options - See {@link QuerySQLOptions}; `{ live: false }` for a snapshot.
|
|
14
|
+
* @returns The query result (rows and AST), loading state, error, and `refetch`.
|
|
9
15
|
*
|
|
10
16
|
* @example
|
|
11
17
|
* ```tsx
|
|
@@ -22,13 +28,25 @@ import { Document, SelectAST, UnionAST } from '@docstack/shared';
|
|
|
22
28
|
* };
|
|
23
29
|
* ```
|
|
24
30
|
*/
|
|
25
|
-
export
|
|
31
|
+
export type QuerySQLOptions = {
|
|
32
|
+
/**
|
|
33
|
+
* Re-run when a document changes in a class this query reads. Defaults to `true`.
|
|
34
|
+
*
|
|
35
|
+
* Pass `false` for a deliberate snapshot - and say so at the call site, which the
|
|
36
|
+
* previous behaviour never did.
|
|
37
|
+
*/
|
|
38
|
+
live?: boolean;
|
|
39
|
+
/** Coalesce a burst of changes into one re-run, in milliseconds. Defaults to 150. */
|
|
40
|
+
coalesceMs?: number;
|
|
41
|
+
};
|
|
42
|
+
export declare const useQuerySQL: (stack: string, sql: string, params?: any[], options?: QuerySQLOptions) => {
|
|
26
43
|
loading: boolean;
|
|
27
44
|
result: {
|
|
28
45
|
rows: any[];
|
|
29
46
|
ast: (SelectAST | UnionAST)[] | null;
|
|
30
47
|
};
|
|
31
|
-
error:
|
|
48
|
+
error: any;
|
|
49
|
+
refetch: () => Promise<void>;
|
|
32
50
|
};
|
|
33
51
|
/**
|
|
34
52
|
* Hook to find documents in a stack using a Mango selector.
|