@docstack/react 0.0.7 → 0.1.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.
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { DocStack } from '@docstack/client';
3
- import { ClientCredentials, StackConfig } from '@docstack/shared';
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
- * It handles the asynchronous initialization of the stack(s).
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
- * <StackProvider config={[{ name: 'my-db' }]}>
56
- * <MyApp />
57
- * </StackProvider>
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;
@@ -0,0 +1,155 @@
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
+ };
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { createContext, useContext, useRef, useCallback, useEffect, useMemo, useReducer, useState } from 'react';
12
+ import { DocStack } from '@docstack/client'; // Import your DocStack class
13
+ // You can give it a default value, e.g., null, which can be checked later.
14
+ /**
15
+ * Context object for the DocStack instance.
16
+ * It provides the current DocStack instance or null if not initialized.
17
+ */
18
+ export const DocStackContext = createContext(null);
19
+ /**
20
+ * Hook to access the DocStack instance.
21
+ *
22
+ * @returns The current {@link DocStack} instance or null if not yet initialized.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const MyComponent = () => {
27
+ * const docStack = useDocStack();
28
+ *
29
+ * if (!docStack) return <div>Loading...</div>;
30
+ *
31
+ * return <div>Connected to {docStack.getStacks().length} stacks</div>;
32
+ * };
33
+ * ```
34
+ */
35
+ export const useDocStack = () => {
36
+ return useContext(DocStackContext);
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
+ });
66
+ /**
67
+ * A provider component that initializes the DocStack client and makes it available
68
+ * to child components via the {@link useDocStack} hook.
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.
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * import { StackProvider } from '@docstack/react';
79
+ *
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
+ * };
92
+ * ```
93
+ */
94
+ const StackProvider = (props) => {
95
+ const { config, children, credentials, destroyRemovedStacks } = props;
96
+ // Use a ref to store the DocStack instance
97
+ const docStackRef = useRef(null);
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)]);
108
+ const setsDocStackWhenReady = useCallback(() => {
109
+ setDocStack(docStackRef.current);
110
+ }, []);
111
+ useEffect(() => {
112
+ if (!mergedConfig.length)
113
+ return;
114
+ if (docStackRef.current === null) {
115
+ console.log("DocStack provider - init instance", { config: mergedConfig });
116
+ const instance = new DocStack(...mergedConfig);
117
+ docStackRef.current = instance;
118
+ instance.addEventListener("ready", setsDocStackWhenReady);
119
+ instance.addEventListener("stack-added", signalStacksChanged);
120
+ instance.addEventListener("stack-removed", signalStacksChanged);
121
+ return;
122
+ }
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
+ }
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;
151
+ };
152
+ }, [mergedConfig, destroyRemovedStacks, setsDocStackWhenReady]);
153
+ return (_jsx(DocStackContext.Provider, { value: docStack, children: children }));
154
+ };
155
+ export default StackProvider;
@@ -0,0 +1 @@
1
+ "use strict";
@@ -1,5 +1,5 @@
1
1
  import { Class } from "@docstack/client";
2
- import { Document } from "@docstack/shared";
2
+ import { Document } from "@docstack/client";
3
3
  /**
4
4
  * Hook to create a new Class in a specific stack.
5
5
  *
@@ -0,0 +1,428 @@
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
+ };
10
+ import { useContext, useCallback, useEffect, useRef, useState } from "react";
11
+ import { DocStackContext } from "../components/StackProvider/index.js";
12
+ import { Class } from "@docstack/client";
13
+ /**
14
+ * Hook to create a new Class in a specific stack.
15
+ *
16
+ * @param stack - The name of the stack to create the class in.
17
+ * @returns A callback function to create the class.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const MyComponent = () => {
22
+ * const createClass = useClassCreate('my-stack');
23
+ *
24
+ * const handleCreate = async () => {
25
+ * const newClass = await createClass('NewClass', 'Description of new class');
26
+ * if (newClass) {
27
+ * console.log('Class created:', newClass.name);
28
+ * }
29
+ * };
30
+ *
31
+ * return <button onClick={handleCreate}>Create Class</button>;
32
+ * };
33
+ * ```
34
+ */
35
+ export const useClassCreate = (stack) => {
36
+ const docStack = useContext(DocStackContext);
37
+ return useCallback((className, classDesc) => __awaiter(void 0, void 0, void 0, function* () {
38
+ try {
39
+ if (!docStack) {
40
+ // Handle the case where the provider is not yet initialized or missing
41
+ // You could throw an error or return an empty state.
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.');
45
+ // setLoading(false);
46
+ return Promise.resolve(null);
47
+ }
48
+ // Run the initial query
49
+ const stackInstance = docStack.getStack(stack);
50
+ if (stackInstance) {
51
+ const classObj_ = yield Class.create(stackInstance, className, "class", classDesc);
52
+ yield stackInstance.addClass(classObj_);
53
+ return classObj_;
54
+ }
55
+ return null;
56
+ }
57
+ catch (err) {
58
+ // setError(err);
59
+ console.error(err);
60
+ return null;
61
+ }
62
+ }), [docStack, stack]);
63
+ };
64
+ /**
65
+ * Hook to retrieve a list of classes from a stack based on a selector.
66
+ * Maintains a real-time list of classes matching the selector.
67
+ *
68
+ * @param stack - The name of the stack to query.
69
+ * @param selector - Mango selector to filter classes.
70
+ * @returns Object containing the list of classes, loading state, and error.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * const ClassList = () => {
75
+ * const { classList, loading } = useClassList('my-stack', {
76
+ * name: { $regex: '^User' }
77
+ * });
78
+ *
79
+ * if (loading) return <div>Loading...</div>;
80
+ *
81
+ * return (
82
+ * <ul>
83
+ * {classList.map(cls => <li key={cls.id}>{cls.name}</li>)}
84
+ * </ul>
85
+ * );
86
+ * };
87
+ * ```
88
+ */
89
+ export const useClassList = (stack, selector) => {
90
+ const docStack = useContext(DocStackContext);
91
+ const [originClass, setOriginClass] = useState();
92
+ const [classList, setClassList] = useState([]);
93
+ const classListRef = useRef([]);
94
+ const [loading, setLoading] = useState(true);
95
+ const [error, setError] = useState(null);
96
+ useEffect(() => {
97
+ // Only run if the docStack is available and a className is provided
98
+ if (!docStack) {
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);
103
+ return;
104
+ }
105
+ const fetchClass = () => __awaiter(void 0, void 0, void 0, function* () {
106
+ setLoading(true);
107
+ setError(null);
108
+ try {
109
+ const stackInstance = docStack.getStack(stack);
110
+ if (stackInstance) {
111
+ const retrievedClass = yield stackInstance.getClass('class');
112
+ if (retrievedClass) {
113
+ setOriginClass(retrievedClass);
114
+ }
115
+ }
116
+ }
117
+ catch (err) {
118
+ setError(err);
119
+ setLoading(false);
120
+ }
121
+ });
122
+ fetchClass();
123
+ return () => {
124
+ // clean what?
125
+ };
126
+ }, [docStack, stack]); // Dependency on docStack and stack
127
+ useEffect(() => {
128
+ if (!originClass) {
129
+ return;
130
+ }
131
+ let cancelled = false;
132
+ let attached = null;
133
+ const runQueryAndListen = () => __awaiter(void 0, void 0, void 0, function* () {
134
+ setLoading(true);
135
+ try {
136
+ const initialClassModelList = yield originClass.getCards(selector);
137
+ const initialClassList = [];
138
+ const stackInstance = docStack.getStack(stack);
139
+ for (const cls of initialClassModelList) {
140
+ const classInstance = yield Class.buildFromModel(stackInstance, cls);
141
+ initialClassList.push(classInstance);
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
+ }
150
+ classListRef.current = initialClassList;
151
+ setClassList(classListRef.current);
152
+ }
153
+ catch (err) {
154
+ if (!cancelled)
155
+ setError(err);
156
+ }
157
+ finally {
158
+ if (!cancelled)
159
+ setLoading(false);
160
+ }
161
+ if (cancelled)
162
+ return;
163
+ const changeListener = (change) => {
164
+ const doc = change.detail.doc;
165
+ console.log("useClassDocs - detail", { detail: change.detail });
166
+ if (!doc.active) {
167
+ // A doc was deleted
168
+ console.log("useClassDocs - a doc was deleted", { doc });
169
+ const docIndex = classListRef.current.findIndex((d) => d.id == doc._id);
170
+ if (docIndex != -1) {
171
+ classListRef.current = [
172
+ ...classListRef.current.slice(0, docIndex),
173
+ ...classListRef.current.slice(docIndex + 1, classListRef.current.length)
174
+ ];
175
+ }
176
+ }
177
+ else {
178
+ // A doc was changed or added
179
+ const docIndex = classListRef.current.findIndex((d) => d.id == doc._id);
180
+ if (docIndex != -1) {
181
+ // A doc was changed
182
+ classListRef.current = [
183
+ ...classListRef.current.slice(0, docIndex),
184
+ doc,
185
+ ...classListRef.current.slice(docIndex + 1, classListRef.current.length)
186
+ ];
187
+ }
188
+ else {
189
+ // A doc was added
190
+ classListRef.current.push(doc);
191
+ }
192
+ }
193
+ setClassList([...classListRef.current]);
194
+ };
195
+ attached = changeListener;
196
+ originClass.addEventListener('doc', attached);
197
+ });
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
+ };
213
+ }, [originClass, JSON.stringify(selector)]); // Dependency on classObj and query
214
+ return { classList, loading, error };
215
+ };
216
+ /**
217
+ * Hook to retrieve a single Class instance by name.
218
+ *
219
+ * @param stack - The name of the stack.
220
+ * @param className - The name of the class to retrieve.
221
+ * @returns Object containing the Class instance, loading state, and error.
222
+ *
223
+ * @example
224
+ * ```tsx
225
+ * const ClassDetails = () => {
226
+ * const { classObj, loading } = useClass('my-stack', 'User');
227
+ *
228
+ * if (loading) return <div>Loading...</div>;
229
+ * if (!classObj) return <div>Class not found</div>;
230
+ *
231
+ * return <div>Class Description: {classObj.description}</div>;
232
+ * };
233
+ * ```
234
+ */
235
+ export const useClass = (stack, className) => {
236
+ const docStack = useContext(DocStackContext);
237
+ const [loading, setLoading] = useState(false);
238
+ const [error, setError] = useState();
239
+ const [classObj, setClass] = useState();
240
+ const reqRef = useRef(false);
241
+ useEffect(() => {
242
+ if (!docStack) {
243
+ // Handle the case where the provider is not yet initialized or missing
244
+ // You could throw an error or return an empty state.
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);
253
+ return;
254
+ }
255
+ const fetchClass = () => __awaiter(void 0, void 0, void 0, function* () {
256
+ try {
257
+ const stackInstance = docStack.getStack(stack);
258
+ if (stackInstance) {
259
+ const res = yield stackInstance.getClass(className);
260
+ // TODO: manage class model (schema!) updates
261
+ if (res) {
262
+ setClass(res);
263
+ }
264
+ }
265
+ }
266
+ catch (e) {
267
+ setError(e);
268
+ }
269
+ finally {
270
+ setLoading(false);
271
+ }
272
+ });
273
+ if (!reqRef.current) {
274
+ reqRef.current = true;
275
+ setLoading(true);
276
+ fetchClass();
277
+ }
278
+ return () => {
279
+ // reqRef.current = false;
280
+ };
281
+ }, [docStack, stack, className]);
282
+ return { loading, error, classObj };
283
+ };
284
+ /**
285
+ * Hook to retrieve documents (cards) of a specific class.
286
+ * Maintains a real-time list of documents matching the query.
287
+ *
288
+ * @param stack - The name of the stack.
289
+ * @param className - The class name to fetch documents for.
290
+ * @param query - Optional Mango selector to filter documents.
291
+ * @returns Object containing the list of documents, loading state, and error.
292
+ *
293
+ * @example
294
+ * ```tsx
295
+ * const UserList = () => {
296
+ * const { docs, loading } = useClassDocs('my-stack', 'User', {
297
+ * age: { $gt: 18 }
298
+ * });
299
+ *
300
+ * if (loading) return <div>Loading...</div>;
301
+ *
302
+ * return (
303
+ * <ul>
304
+ * {docs.map(doc => <li key={doc._id}>{doc.name}</li>)}
305
+ * </ul>
306
+ * );
307
+ * };
308
+ * ```
309
+ */
310
+ export const useClassDocs = (stack, className, query = {}) => {
311
+ const docStack = useContext(DocStackContext);
312
+ const [classObj, setClass] = useState();
313
+ const [docs, setDocs] = useState([]);
314
+ const docsRef = useRef([]);
315
+ const [loading, setLoading] = useState(true);
316
+ const [error, setError] = useState(null);
317
+ useEffect(() => {
318
+ // Only run if the docStack is available and a className is provided
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.
329
+ setLoading(false);
330
+ return;
331
+ }
332
+ const fetchClass = () => __awaiter(void 0, void 0, void 0, function* () {
333
+ setLoading(true);
334
+ setError(null);
335
+ try {
336
+ const stackInstance = docStack.getStack(stack);
337
+ if (stackInstance) {
338
+ const retrievedClass = yield stackInstance.getClass(className);
339
+ if (retrievedClass) {
340
+ setClass(retrievedClass);
341
+ }
342
+ }
343
+ }
344
+ catch (err) {
345
+ setError(err);
346
+ setLoading(false);
347
+ }
348
+ });
349
+ fetchClass();
350
+ return () => {
351
+ // clean what?
352
+ };
353
+ }, [docStack, stack, className]); // Dependency on docStack and className
354
+ useEffect(() => {
355
+ if (!classObj) {
356
+ return;
357
+ }
358
+ let cancelled = false;
359
+ let attached = null;
360
+ const runQueryAndListen = () => __awaiter(void 0, void 0, void 0, function* () {
361
+ setLoading(true);
362
+ try {
363
+ // debugger;
364
+ const initialDocs = yield classObj.getCards(query);
365
+ if (cancelled)
366
+ return;
367
+ docsRef.current = initialDocs;
368
+ setDocs(docsRef.current);
369
+ }
370
+ catch (err) {
371
+ if (!cancelled)
372
+ setError(err);
373
+ }
374
+ finally {
375
+ if (!cancelled)
376
+ setLoading(false);
377
+ }
378
+ if (cancelled)
379
+ return;
380
+ const changeListener = (change) => {
381
+ const doc = change.detail.doc;
382
+ console.log("useClassDocs - detail", { detail: change.detail });
383
+ if (!doc.active) {
384
+ // A doc was deleted
385
+ console.log("useClassDocs - a doc was deleted", { doc });
386
+ const docIndex = docsRef.current.findIndex((d) => d._id == doc._id);
387
+ if (docIndex != -1) {
388
+ docsRef.current = [
389
+ ...docsRef.current.slice(0, docIndex),
390
+ ...docsRef.current.slice(docIndex + 1, docsRef.current.length)
391
+ ];
392
+ }
393
+ }
394
+ else {
395
+ // A doc was changed or added
396
+ console.log("useClassDocs - a doc was changed or added", { doc });
397
+ const docIndex = docsRef.current.findIndex((d) => d._id == doc._id);
398
+ if (docIndex != -1) {
399
+ // A doc was changed
400
+ console.log("useClassDocs - a doc was changed", { doc });
401
+ docsRef.current = [
402
+ ...docsRef.current.slice(0, docIndex),
403
+ doc,
404
+ ...docsRef.current.slice(docIndex + 1, docsRef.current.length)
405
+ ];
406
+ }
407
+ else {
408
+ // A doc was added
409
+ console.log("useClassDocs - a doc was added", { doc });
410
+ docsRef.current.push(doc);
411
+ }
412
+ }
413
+ setDocs([...docsRef.current]);
414
+ };
415
+ attached = changeListener;
416
+ classObj.addEventListener('doc', attached);
417
+ });
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
+ };
426
+ }, [classObj, JSON.stringify(query)]); // Dependency on classObj and query
427
+ return { docs, loading, error };
428
+ };
@@ -1,5 +1,4 @@
1
- import { Class } from "@docstack/client";
2
- import { Domain, RelationDocument } from "@docstack/shared";
1
+ import { Class, Domain, RelationDocument } from "@docstack/client";
3
2
  /**
4
3
  * Hook to create a new Domain in a specific stack.
5
4
  *