@astrale-os/shell-react 0.2.3 → 0.2.4
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.
|
@@ -12,8 +12,10 @@ export function useOut(node, edge, opts) {
|
|
|
12
12
|
export function useIn(node, edge, opts) {
|
|
13
13
|
return useEndpoint(node, edge, 'in', opts);
|
|
14
14
|
}
|
|
15
|
-
function useEndpoint(node, edge, side,
|
|
15
|
+
function useEndpoint(node, edge, side, opts) {
|
|
16
16
|
const schema = isIdleInput(node) ? undefined : schemaOf(node);
|
|
17
|
+
const single = schema !== undefined && isSingleEndpoint(schema, edge, side);
|
|
18
|
+
const key = `${node?.id ?? ''}\u0000${edge}\u0000${side}`;
|
|
17
19
|
const run = useCallback(async () => {
|
|
18
20
|
if (isIdleInput(node))
|
|
19
21
|
return null;
|
|
@@ -23,22 +25,26 @@ function useEndpoint(node, edge, side, _opts) {
|
|
|
23
25
|
return result;
|
|
24
26
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
25
27
|
}, [node?.id, edge, side]);
|
|
26
|
-
const { data, pending, error, refetch } = useAsyncRead(isIdleInput(node) ? null : run,
|
|
27
|
-
node?.id ?? null,
|
|
28
|
-
edge,
|
|
29
|
-
side,
|
|
30
|
-
]);
|
|
28
|
+
const { data, pending, error, refetch } = useAsyncRead(isIdleInput(node) ? null : run, key, opts?.keepPrevious === true);
|
|
31
29
|
return useMemo(() => {
|
|
32
30
|
const live = { pending, error, live: false, seq: 0, refetch };
|
|
33
31
|
if (isIdleInput(node))
|
|
34
32
|
return { ...live, node: null, nodes: EMPTY };
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
if (single) {
|
|
34
|
+
const value = Array.isArray(data) ? data[0] : data;
|
|
35
|
+
return { ...live, node: (value ?? null) };
|
|
36
|
+
}
|
|
37
|
+
const values = Array.isArray(data) ? data : data === null || data === undefined ? EMPTY : [data];
|
|
38
|
+
return { ...live, nodes: values };
|
|
39
|
+
}, [node, data, pending, error, refetch, single]);
|
|
39
40
|
}
|
|
40
41
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
41
42
|
const EMPTY = Object.freeze([]);
|
|
43
|
+
function isSingleEndpoint(schema, edge, side) {
|
|
44
|
+
const definition = schema.classes[edge];
|
|
45
|
+
const cardinality = (side === 'out' ? definition?.to : definition?.from)?.cardinality;
|
|
46
|
+
return cardinality === '1' || cardinality === '0..1';
|
|
47
|
+
}
|
|
42
48
|
function tagEndpoints(result, schema) {
|
|
43
49
|
if (Array.isArray(result))
|
|
44
50
|
for (const r of result)
|
|
@@ -47,30 +53,52 @@ function tagEndpoints(result, schema) {
|
|
|
47
53
|
tagSchema(result, schema);
|
|
48
54
|
}
|
|
49
55
|
/** One-shot async read with pending/error/refetch and stale-response guarding. */
|
|
50
|
-
function useAsyncRead(run,
|
|
51
|
-
const [state, setState] = useState({
|
|
56
|
+
function useAsyncRead(run, key, keepPrevious) {
|
|
57
|
+
const [state, setState] = useState({
|
|
58
|
+
pending: run !== null,
|
|
59
|
+
error: undefined,
|
|
60
|
+
key: run === null ? null : key,
|
|
61
|
+
});
|
|
52
62
|
const runRef = useRef(run);
|
|
53
63
|
runRef.current = run;
|
|
64
|
+
const keyRef = useRef(key);
|
|
65
|
+
keyRef.current = key;
|
|
66
|
+
const keepPreviousRef = useRef(keepPrevious);
|
|
67
|
+
keepPreviousRef.current = keepPrevious;
|
|
54
68
|
const seq = useRef(0);
|
|
55
69
|
const refetch = useCallback(() => {
|
|
56
70
|
const current = runRef.current;
|
|
71
|
+
const currentKey = keyRef.current;
|
|
57
72
|
if (current === null) {
|
|
58
|
-
setState({ pending: false, error: undefined, data: undefined });
|
|
73
|
+
setState({ pending: false, error: undefined, data: undefined, key: null });
|
|
59
74
|
return Promise.resolve();
|
|
60
75
|
}
|
|
61
76
|
const my = ++seq.current;
|
|
62
|
-
setState((s) => ({
|
|
77
|
+
setState((s) => ({
|
|
78
|
+
data: s.key === currentKey || keepPreviousRef.current === true ? s.data : undefined,
|
|
79
|
+
pending: true,
|
|
80
|
+
error: undefined,
|
|
81
|
+
key: currentKey,
|
|
82
|
+
}));
|
|
63
83
|
return current().then((data) => {
|
|
64
|
-
if (my === seq.current)
|
|
65
|
-
setState({ data, pending: false, error: undefined });
|
|
84
|
+
if (my === seq.current) {
|
|
85
|
+
setState({ data, pending: false, error: undefined, key: currentKey });
|
|
86
|
+
}
|
|
66
87
|
}, (error) => {
|
|
67
|
-
if (my === seq.current)
|
|
68
|
-
setState((s) => ({ data: s.data, pending: false, error }));
|
|
88
|
+
if (my === seq.current) {
|
|
89
|
+
setState((s) => ({ data: s.data, pending: false, error, key: currentKey }));
|
|
90
|
+
}
|
|
69
91
|
});
|
|
70
92
|
}, []);
|
|
71
93
|
useEffect(() => {
|
|
72
94
|
void refetch();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return {
|
|
95
|
+
}, [key, refetch]);
|
|
96
|
+
const current = state.key === key;
|
|
97
|
+
return {
|
|
98
|
+
data: current || keepPrevious ? state.data : undefined,
|
|
99
|
+
pending: run !== null && (!current || state.pending),
|
|
100
|
+
error: current ? state.error : undefined,
|
|
101
|
+
key: current ? state.key : null,
|
|
102
|
+
refetch,
|
|
103
|
+
};
|
|
76
104
|
}
|
package/package.json
CHANGED
|
@@ -39,9 +39,11 @@ function useEndpoint<S extends Schema>(
|
|
|
39
39
|
node: AnyBoundNode<S> | null | undefined,
|
|
40
40
|
edge: EdgeNames<S>,
|
|
41
41
|
side: 'out' | 'in',
|
|
42
|
-
|
|
42
|
+
opts?: ReadOptions,
|
|
43
43
|
): EndpointEnvelope<S> {
|
|
44
44
|
const schema = isIdleInput(node) ? undefined : schemaOf(node)
|
|
45
|
+
const single = schema !== undefined && isSingleEndpoint(schema, edge, side)
|
|
46
|
+
const key = `${node?.id ?? ''}\u0000${edge}\u0000${side}`
|
|
45
47
|
const run = useCallback(async (): Promise<unknown> => {
|
|
46
48
|
if (isIdleInput(node)) return null
|
|
47
49
|
const result = await (side === 'out' ? node.links.out(edge) : node.links.in(edge))
|
|
@@ -50,24 +52,39 @@ function useEndpoint<S extends Schema>(
|
|
|
50
52
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
53
|
}, [node?.id, edge, side])
|
|
52
54
|
|
|
53
|
-
const { data, pending, error, refetch } = useAsyncRead(
|
|
54
|
-
node
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const { data, pending, error, refetch } = useAsyncRead(
|
|
56
|
+
isIdleInput(node) ? null : run,
|
|
57
|
+
key,
|
|
58
|
+
opts?.keepPrevious === true,
|
|
59
|
+
)
|
|
58
60
|
|
|
59
61
|
return useMemo<EndpointEnvelope<S>>(() => {
|
|
60
62
|
const live: ReadState = { pending, error, live: false, seq: 0, refetch }
|
|
61
63
|
if (isIdleInput(node)) return { ...live, node: null, nodes: EMPTY }
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (single) {
|
|
65
|
+
const value = Array.isArray(data) ? data[0] : data
|
|
66
|
+
return { ...live, node: (value ?? null) as AnyBoundNode<S> | null }
|
|
67
|
+
}
|
|
68
|
+
const values = Array.isArray(data) ? data : data === null || data === undefined ? EMPTY : [data]
|
|
69
|
+
return { ...live, nodes: values as readonly AnyBoundNode<S>[] }
|
|
70
|
+
}, [node, data, pending, error, refetch, single])
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
68
74
|
|
|
69
75
|
const EMPTY: readonly never[] = Object.freeze([])
|
|
70
76
|
|
|
77
|
+
function isSingleEndpoint(schema: Schema, edge: string, side: 'out' | 'in'): boolean {
|
|
78
|
+
const definition = schema.classes[edge] as
|
|
79
|
+
| {
|
|
80
|
+
readonly from?: { readonly cardinality?: string }
|
|
81
|
+
readonly to?: { readonly cardinality?: string }
|
|
82
|
+
}
|
|
83
|
+
| undefined
|
|
84
|
+
const cardinality = (side === 'out' ? definition?.to : definition?.from)?.cardinality
|
|
85
|
+
return cardinality === '1' || cardinality === '0..1'
|
|
86
|
+
}
|
|
87
|
+
|
|
71
88
|
function tagEndpoints(result: unknown, schema: Schema): void {
|
|
72
89
|
if (Array.isArray(result)) for (const r of result) tagSchema(r, schema)
|
|
73
90
|
else if (result !== null && result !== undefined) tagSchema(result, schema)
|
|
@@ -77,40 +94,66 @@ interface AsyncState<T> {
|
|
|
77
94
|
data?: T
|
|
78
95
|
pending: boolean
|
|
79
96
|
error: unknown
|
|
97
|
+
key: string | null
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
/** One-shot async read with pending/error/refetch and stale-response guarding. */
|
|
83
101
|
function useAsyncRead<T>(
|
|
84
102
|
run: (() => Promise<T>) | null,
|
|
85
|
-
|
|
103
|
+
key: string,
|
|
104
|
+
keepPrevious: boolean,
|
|
86
105
|
): AsyncState<T> & { refetch(): Promise<void> } {
|
|
87
|
-
const [state, setState] = useState<AsyncState<T>>({
|
|
106
|
+
const [state, setState] = useState<AsyncState<T>>({
|
|
107
|
+
pending: run !== null,
|
|
108
|
+
error: undefined,
|
|
109
|
+
key: run === null ? null : key,
|
|
110
|
+
})
|
|
88
111
|
const runRef = useRef(run)
|
|
89
112
|
runRef.current = run
|
|
113
|
+
const keyRef = useRef(key)
|
|
114
|
+
keyRef.current = key
|
|
115
|
+
const keepPreviousRef = useRef(keepPrevious)
|
|
116
|
+
keepPreviousRef.current = keepPrevious
|
|
90
117
|
const seq = useRef(0)
|
|
91
118
|
|
|
92
119
|
const refetch = useCallback((): Promise<void> => {
|
|
93
120
|
const current = runRef.current
|
|
121
|
+
const currentKey = keyRef.current
|
|
94
122
|
if (current === null) {
|
|
95
|
-
setState({ pending: false, error: undefined, data: undefined })
|
|
123
|
+
setState({ pending: false, error: undefined, data: undefined, key: null })
|
|
96
124
|
return Promise.resolve()
|
|
97
125
|
}
|
|
98
126
|
const my = ++seq.current
|
|
99
|
-
setState((s) => ({
|
|
127
|
+
setState((s) => ({
|
|
128
|
+
data: s.key === currentKey || keepPreviousRef.current === true ? s.data : undefined,
|
|
129
|
+
pending: true,
|
|
130
|
+
error: undefined,
|
|
131
|
+
key: currentKey,
|
|
132
|
+
}))
|
|
100
133
|
return current().then(
|
|
101
134
|
(data) => {
|
|
102
|
-
if (my === seq.current)
|
|
135
|
+
if (my === seq.current) {
|
|
136
|
+
setState({ data, pending: false, error: undefined, key: currentKey })
|
|
137
|
+
}
|
|
103
138
|
},
|
|
104
139
|
(error: unknown) => {
|
|
105
|
-
if (my === seq.current)
|
|
140
|
+
if (my === seq.current) {
|
|
141
|
+
setState((s) => ({ data: s.data, pending: false, error, key: currentKey }))
|
|
142
|
+
}
|
|
106
143
|
},
|
|
107
144
|
)
|
|
108
145
|
}, [])
|
|
109
146
|
|
|
110
147
|
useEffect(() => {
|
|
111
148
|
void refetch()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return {
|
|
149
|
+
}, [key, refetch])
|
|
150
|
+
|
|
151
|
+
const current = state.key === key
|
|
152
|
+
return {
|
|
153
|
+
data: current || keepPrevious ? state.data : undefined,
|
|
154
|
+
pending: run !== null && (!current || state.pending),
|
|
155
|
+
error: current ? state.error : undefined,
|
|
156
|
+
key: current ? state.key : null,
|
|
157
|
+
refetch,
|
|
158
|
+
}
|
|
116
159
|
}
|