@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.
- package/lib/components/StackProvider/index.d.ts +24 -7
- package/lib/components/StackProvider/index.js +155 -0
- package/lib/components/index.js +1 -0
- package/lib/hooks/class.d.ts +1 -1
- package/lib/hooks/class.js +428 -0
- package/lib/hooks/domain.d.ts +1 -2
- package/lib/hooks/domain.js +423 -0
- package/lib/hooks/index.d.ts +23 -5
- package/lib/hooks/index.js +206 -0
- 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 +9 -3
- package/package.json +3 -4
- 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 +138 -59
- 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;
|
|
@@ -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";
|
package/lib/hooks/class.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/lib/hooks/domain.d.ts
CHANGED