@astrale-os/shell-react 0.3.2-beta → 0.3.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/graph/query-definition.hook.d.ts +2 -1
- package/dist/graph/query-definition.hook.js +18 -4
- package/dist/graph/resource.d.ts +3 -1
- package/dist/graph/resource.js +7 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/schema/action.hook.d.ts +4 -11
- package/dist/schema/action.hook.js +21 -5
- package/dist/schema/domain.context.d.ts +10 -0
- package/dist/schema/domain.context.js +17 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/mutation.hook.d.ts +2 -1
- package/dist/schema/mutation.hook.js +6 -3
- package/dist/schema/object.hook.js +3 -1
- package/dist/schema/relation.hook.js +3 -1
- package/package.json +5 -7
- package/src/graph/query-definition.hook.ts +29 -10
- package/src/graph/resource.ts +15 -3
- package/src/index.ts +2 -0
- package/src/schema/action.hook.ts +26 -15
- package/src/schema/domain.context.tsx +31 -0
- package/src/schema/index.ts +2 -0
- package/src/schema/mutation.hook.ts +9 -4
- package/src/schema/object.hook.ts +3 -1
- package/src/schema/relation.hook.ts +3 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QueryDefinition } from '@astrale-os/sdk/query';
|
|
2
|
+
import type { Domain } from '@astrale-os/sdk/schema';
|
|
2
3
|
import type { GraphResource } from './resource.js';
|
|
3
4
|
export interface QueryResourceOptions {
|
|
4
5
|
readonly keepPrevious?: boolean;
|
|
@@ -6,4 +7,4 @@ export interface QueryResourceOptions {
|
|
|
6
7
|
readonly refreshWhen?: 'always' | 'visible';
|
|
7
8
|
}
|
|
8
9
|
/** Execute one named SDK Query definition exclusively through the current GraphStore. */
|
|
9
|
-
export declare function useQuery<Input, Output>(definition: QueryDefinition<Input, Output> | null, input: Input, options?: QueryResourceOptions): GraphResource<Output>;
|
|
10
|
+
export declare function useQuery<DomainValue extends Domain, Input, Output>(definition: QueryDefinition<DomainValue, Input, Output> | null, input: Input, options?: QueryResourceOptions): GraphResource<Output>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { executeQuery } from '@astrale-os/sdk/query';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
3
|
+
import { useExecutionDomain } from '../schema/domain.context.js';
|
|
3
4
|
import { retainRefreshScheduler } from './resource.js';
|
|
4
5
|
import { useGraphStore } from './store.js';
|
|
5
6
|
class DefinitionObserver {
|
|
6
7
|
store;
|
|
8
|
+
domain;
|
|
7
9
|
definition;
|
|
8
10
|
input;
|
|
9
11
|
listeners = new Set();
|
|
@@ -14,8 +16,9 @@ class DefinitionObserver {
|
|
|
14
16
|
pending = null;
|
|
15
17
|
started = false;
|
|
16
18
|
rerunQueued = false;
|
|
17
|
-
constructor(store, definition, input) {
|
|
19
|
+
constructor(store, domain, definition, input) {
|
|
18
20
|
this.store = store;
|
|
21
|
+
this.domain = domain;
|
|
19
22
|
this.definition = definition;
|
|
20
23
|
this.input = input;
|
|
21
24
|
}
|
|
@@ -50,7 +53,7 @@ class DefinitionObserver {
|
|
|
50
53
|
touched.push(Object.freeze({ ast, options }));
|
|
51
54
|
return this.store.read(ast, { ...options, freshness });
|
|
52
55
|
};
|
|
53
|
-
const running = executeQuery({ query: execute }, this.definition, this.input)
|
|
56
|
+
const running = executeQuery({ query: execute }, this.domain, this.definition, this.input)
|
|
54
57
|
.then((data) => {
|
|
55
58
|
if (!this.started || generation !== this.generation)
|
|
56
59
|
return;
|
|
@@ -114,14 +117,15 @@ class DefinitionObserver {
|
|
|
114
117
|
/** Execute one named SDK Query definition exclusively through the current GraphStore. */
|
|
115
118
|
export function useQuery(definition, input, options = {}) {
|
|
116
119
|
const store = useGraphStore();
|
|
120
|
+
const domain = useExecutionDomain();
|
|
117
121
|
const inputIdentity = stableIdentity(input);
|
|
118
|
-
const observer = useMemo(() => (definition === null ? null : new DefinitionObserver(store, definition, input)), [store, definition, inputIdentity]);
|
|
122
|
+
const observer = useMemo(() => (definition === null ? null : new DefinitionObserver(store, domain, definition, input)), [store, domain, definition, inputIdentity]);
|
|
119
123
|
const idle = useMemo(() => Object.freeze({ state: 'idle' }), []);
|
|
120
124
|
const subscribe = useCallback((listener) => observer?.subscribe(listener) ?? (() => { }), [observer]);
|
|
121
125
|
const snapshot = useCallback(() => observer?.getSnapshot() ?? idle, [observer, idle]);
|
|
122
126
|
const current = useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
123
127
|
useEffect(() => observer?.start(), [observer]);
|
|
124
|
-
const identity = definition === null ? 'idle' : `${definition
|
|
128
|
+
const identity = definition === null ? 'idle' : `${definitionIdentity(definition)}\u0000${inputIdentity}`;
|
|
125
129
|
useEffect(() => {
|
|
126
130
|
if (observer === null || options.refreshInterval === undefined)
|
|
127
131
|
return;
|
|
@@ -148,6 +152,16 @@ export function useQuery(definition, input, options = {}) {
|
|
|
148
152
|
more: Object.freeze({ has: false, pending: false, load: () => Promise.resolve() }),
|
|
149
153
|
}), [current, previous, observer]);
|
|
150
154
|
}
|
|
155
|
+
const definitionIdentities = new WeakMap();
|
|
156
|
+
let nextDefinitionIdentity = 1;
|
|
157
|
+
function definitionIdentity(definition) {
|
|
158
|
+
const retained = definitionIdentities.get(definition);
|
|
159
|
+
if (retained !== undefined)
|
|
160
|
+
return retained;
|
|
161
|
+
const created = nextDefinitionIdentity++;
|
|
162
|
+
definitionIdentities.set(definition, created);
|
|
163
|
+
return created;
|
|
164
|
+
}
|
|
151
165
|
function dedupeShapes(input) {
|
|
152
166
|
const seen = new Set();
|
|
153
167
|
return Object.freeze(input.filter(({ ast, options }) => {
|
package/dist/graph/resource.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GraphStore, StoreQueryResponse } from '@astrale-os/sdk/client/store';
|
|
1
|
+
import type { GraphStore, StoreQueryResponse, StoreReadOptions } from '@astrale-os/sdk/client/store';
|
|
2
2
|
import type { QueryAST } from '@astrale-os/sdk/query';
|
|
3
3
|
export type GraphResourceState = 'idle' | 'loading' | 'ready' | 'stale' | 'refreshing' | 'optimistic' | 'failed';
|
|
4
4
|
export interface GraphResource<T> {
|
|
@@ -21,6 +21,8 @@ export interface GraphResource<T> {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
export interface StoreResourceOptions<Output> {
|
|
24
|
+
/** Exact closure evidence required by typed Domain projections. */
|
|
25
|
+
readonly expected?: StoreReadOptions['expected'];
|
|
24
26
|
readonly identity?: string;
|
|
25
27
|
readonly keepPrevious?: boolean;
|
|
26
28
|
readonly window?: 'page' | 'append';
|
package/dist/graph/resource.js
CHANGED
|
@@ -4,16 +4,18 @@ class StoreObserver {
|
|
|
4
4
|
store;
|
|
5
5
|
query;
|
|
6
6
|
size;
|
|
7
|
+
expected;
|
|
7
8
|
listeners = new Set();
|
|
8
9
|
pages = Object.freeze([Object.freeze({})]);
|
|
9
10
|
unsubs = Object.freeze([]);
|
|
10
11
|
current;
|
|
11
12
|
morePromise = null;
|
|
12
13
|
started = false;
|
|
13
|
-
constructor(store, query, size) {
|
|
14
|
+
constructor(store, query, size, expected) {
|
|
14
15
|
this.store = store;
|
|
15
16
|
this.query = query;
|
|
16
17
|
this.size = size;
|
|
18
|
+
this.expected = expected;
|
|
17
19
|
}
|
|
18
20
|
subscribe = (listener) => {
|
|
19
21
|
this.listeners.add(listener);
|
|
@@ -82,6 +84,7 @@ class StoreObserver {
|
|
|
82
84
|
size: this.size,
|
|
83
85
|
...(page.after === undefined ? {} : { after: page.after }),
|
|
84
86
|
}),
|
|
87
|
+
...(this.expected === undefined ? {} : { expected: this.expected }),
|
|
85
88
|
});
|
|
86
89
|
}
|
|
87
90
|
bindPages() {
|
|
@@ -98,7 +101,9 @@ const pollers = new WeakMap();
|
|
|
98
101
|
/** Adapt exact Store pages into a tear-free React resource without a parallel entity cache. */
|
|
99
102
|
export function useStoreResource(query, pageSize, project, options = {}) {
|
|
100
103
|
const store = useGraphStore();
|
|
101
|
-
const observer = useMemo(() =>
|
|
104
|
+
const observer = useMemo(() => query === null
|
|
105
|
+
? null
|
|
106
|
+
: new StoreObserver(store, query, positivePage(pageSize), options.expected), [store, query, pageSize, options.expected]);
|
|
102
107
|
const subscribe = useCallback((listener) => (observer === null ? () => { } : observer.subscribe(listener)), [observer]);
|
|
103
108
|
const idle = useMemo(() => Object.freeze({ snapshots: Object.freeze([]), pendingMore: false }), []);
|
|
104
109
|
const getSnapshot = useCallback(() => observer?.getSnapshot() ?? idle, [observer, idle]);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export { ActAs, useAuth, useCan, useSelf } from './auth/index.js';
|
|
|
5
5
|
export type { ActAsProps, PolicyObject } from './auth/index.js';
|
|
6
6
|
export { ClientError, NodeUnavailableError, ProtocolError, ResponseError, StaleRouteError, TransportError, isClientError, isNodeUnavailableError, useCall, useGraphStore, useQuery, useStream, } from './graph/index.js';
|
|
7
7
|
export type { CallOptions, GraphResource, GraphResourceState, GraphStore, QueryResourceOptions, StreamHandle, StreamOptions, } from './graph/index.js';
|
|
8
|
-
export { RelationUnavailableError, queryRefresh, useAction, useDomain, useDomains, useDynamicDomain, useDynamicObject, useKernel, useMutation, useObject, useRelation, } from './schema/index.js';
|
|
9
|
-
export type { ActionOptions, ActionRefresh, DomainAction, MutationOptions, QueryRefresh, RelationOptions, RelationResource, RelationUnavailable, RichReadOptions, } from './schema/index.js';
|
|
8
|
+
export { DomainProvider, RelationUnavailableError, queryRefresh, useAction, useDomain, useDomains, useDynamicDomain, useDynamicObject, useKernel, useMutation, useObject, useRelation, } from './schema/index.js';
|
|
9
|
+
export type { ActionOptions, ActionRefresh, DomainProviderProps, DomainAction, MutationOptions, QueryRefresh, RelationOptions, RelationResource, RelationUnavailable, RichReadOptions, } from './schema/index.js';
|
|
10
10
|
export { Link, NavigationScope, NavScope, NodeLink, pathParam, ViewLink, defineNavigation, enumParam, location, memoryAdapter, urlAdapter, useAncestors, useLocation, useNavigate, useNavigation, useNavScope, } from './navigation/index.js';
|
|
11
11
|
export type { LinkProps, LocationDefinition, MissingPolicy, NavAdapter, NavLocation, NavScopeHandle, NavScopeProps, NavSnapshot, NavTarget, NavigationDefinition, NavigationParam, NavigationParams, NavigationRoutes, NavigationTarget, NodeLinkProps, Navigate, SerializedLocation, ViewLinkProps, } from './navigation/index.js';
|
|
12
12
|
export { SelectionScope, useSelection } from './selection/index.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { ActAs, useAuth, useCan, useSelf } from './auth/index.js';
|
|
|
5
5
|
// One Store path and named SDK definitions. Raw Query/Mutation builders are intentionally absent.
|
|
6
6
|
export { ClientError, NodeUnavailableError, ProtocolError, ResponseError, StaleRouteError, TransportError, isClientError, isNodeUnavailableError, useCall, useGraphStore, useQuery, useStream, } from './graph/index.js';
|
|
7
7
|
// Rich exact Domain bindings.
|
|
8
|
-
export { RelationUnavailableError, queryRefresh, useAction, useDomain, useDomains, useDynamicDomain, useDynamicObject, useKernel, useMutation, useObject, useRelation, } from './schema/index.js';
|
|
8
|
+
export { DomainProvider, RelationUnavailableError, queryRefresh, useAction, useDomain, useDomains, useDynamicDomain, useDynamicObject, useKernel, useMutation, useObject, useRelation, } from './schema/index.js';
|
|
9
9
|
// Typed navigation and selection.
|
|
10
10
|
export { Link, NavigationScope, NavScope, NodeLink, pathParam, ViewLink, defineNavigation, enumParam, location, memoryAdapter, urlAdapter, useAncestors, useLocation, useNavigate, useNavigation, useNavScope, } from './navigation/index.js';
|
|
11
11
|
export { SelectionScope, useSelection } from './selection/index.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BoundNodeFor } from '@astrale-os/sdk/client';
|
|
2
2
|
import type { SessionRequestOptions } from '@astrale-os/sdk/client/session';
|
|
3
|
-
import type { QueryAST } from '@astrale-os/sdk/query';
|
|
3
|
+
import type { QueryAST, QueryDefinition } from '@astrale-os/sdk/query';
|
|
4
|
+
import type { Domain } from '@astrale-os/sdk/schema';
|
|
4
5
|
import type { CallableInputOf, CallableResultOf, MethodOwnerKeyOf, MethodInheritance, ResolvedFunction, ResolvedMethod } from '@astrale-os/sdk/schema';
|
|
5
6
|
type ExecutableInheritance = Exclude<MethodInheritance, 'abstract'>;
|
|
6
7
|
type InstanceMethod = ResolvedMethod<unknown, unknown, false, ExecutableInheritance>;
|
|
@@ -10,23 +11,15 @@ type MethodReceiver<C extends InstanceMethod> = BoundNodeFor<MethodOwnerKeyOf<C>
|
|
|
10
11
|
declare const QUERY_REFRESH: unique symbol;
|
|
11
12
|
export interface QueryRefresh {
|
|
12
13
|
readonly id: string;
|
|
13
|
-
readonly [QUERY_REFRESH]: () => {
|
|
14
|
+
readonly [QUERY_REFRESH]: (domain: Domain) => {
|
|
14
15
|
readonly ast: QueryAST;
|
|
15
16
|
readonly page: Readonly<{
|
|
16
17
|
readonly size: number;
|
|
17
18
|
}>;
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
|
-
type RefreshableQuery<Input, Query extends QueryAST> = {
|
|
21
|
-
readonly id: string;
|
|
22
|
-
readonly page: Readonly<{
|
|
23
|
-
readonly size: number;
|
|
24
|
-
}>;
|
|
25
|
-
prepare?(input: Input): Input;
|
|
26
|
-
build(input: Input): Query;
|
|
27
|
-
};
|
|
28
21
|
/** Pair one exact named Query definition with its admitted input for targeted invalidation. */
|
|
29
|
-
export declare function queryRefresh<Input,
|
|
22
|
+
export declare function queryRefresh<DomainValue extends Domain, Input, Output>(definition: QueryDefinition<DomainValue, Input, Output>, input: Input): QueryRefresh;
|
|
30
23
|
export type ActionRefresh = 'all' | 'none' | {
|
|
31
24
|
readonly queries: readonly QueryRefresh[];
|
|
32
25
|
};
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { reference } from '@astrale-os/sdk/client/session';
|
|
2
2
|
import { Path } from '@astrale-os/sdk/graph/path';
|
|
3
|
+
import { realizeQuery } from '@astrale-os/sdk/query';
|
|
3
4
|
import { useCallback, useMemo, useRef } from 'react';
|
|
4
5
|
import { useSessionKernel } from '../auth/session-kernel.js';
|
|
5
6
|
import { useAction as useAsyncAction } from '../graph/action.js';
|
|
6
7
|
import { useGraphStore } from '../graph/store.js';
|
|
7
8
|
import { bindingFor } from './domain.hook.js';
|
|
8
9
|
const QUERY_REFRESH = Symbol('shell-react.query-refresh');
|
|
10
|
+
const queryRefreshIdentities = new WeakMap();
|
|
11
|
+
let nextQueryRefreshIdentity = 0;
|
|
9
12
|
/** Pair one exact named Query definition with its admitted input for targeted invalidation. */
|
|
10
13
|
export function queryRefresh(definition, input) {
|
|
14
|
+
const id = queryRefreshIdentity(definition);
|
|
11
15
|
return Object.freeze({
|
|
12
|
-
id
|
|
13
|
-
[QUERY_REFRESH]: () => {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
+
id,
|
|
17
|
+
[QUERY_REFRESH]: (domain) => {
|
|
18
|
+
const realized = realizeQuery(definition, domain);
|
|
19
|
+
if (realized.kind !== 'single') {
|
|
20
|
+
throw new TypeError('Targeted refresh requires a single Query recipe.');
|
|
21
|
+
}
|
|
22
|
+
const prepared = realized.prepare?.(input) ?? input;
|
|
23
|
+
return Object.freeze({ ast: realized.build(prepared), page: realized.page });
|
|
16
24
|
},
|
|
17
25
|
});
|
|
18
26
|
}
|
|
@@ -40,7 +48,7 @@ export function useAction(callable, options = {}) {
|
|
|
40
48
|
store.invalidate();
|
|
41
49
|
else if (refresh !== 'none') {
|
|
42
50
|
refresh.queries.forEach((query) => {
|
|
43
|
-
const target = query[QUERY_REFRESH]();
|
|
51
|
+
const target = query[QUERY_REFRESH](bindingRef.current.domain);
|
|
44
52
|
store.invalidate(target.ast, { page: target.page });
|
|
45
53
|
});
|
|
46
54
|
}
|
|
@@ -75,3 +83,11 @@ function isInstanceMethod(callable) {
|
|
|
75
83
|
function receiverPath(receiver) {
|
|
76
84
|
return Path.id(receiver.id);
|
|
77
85
|
}
|
|
86
|
+
function queryRefreshIdentity(definition) {
|
|
87
|
+
const current = queryRefreshIdentities.get(definition);
|
|
88
|
+
if (current !== undefined)
|
|
89
|
+
return current;
|
|
90
|
+
const created = `query-${++nextQueryRefreshIdentity}`;
|
|
91
|
+
queryRefreshIdentities.set(definition, created);
|
|
92
|
+
return created;
|
|
93
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Domain } from '@astrale-os/sdk/schema';
|
|
2
|
+
import type { schema } from '@astrale-os/sdk/schema';
|
|
3
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
4
|
+
export interface DomainProviderProps<Schema extends schema.DomainSchema> {
|
|
5
|
+
readonly schema: Schema;
|
|
6
|
+
readonly children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** Resolve one exact installed Domain for every projected recipe in the React subtree. */
|
|
9
|
+
export declare function DomainProvider<const Schema extends schema.DomainSchema>(props: DomainProviderProps<Schema>): ReactElement;
|
|
10
|
+
export declare function useExecutionDomain<DomainValue extends Domain>(): DomainValue;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import { useDomain } from './domain.hook.js';
|
|
4
|
+
const DomainContext = createContext(null);
|
|
5
|
+
DomainContext.displayName = 'AstraleDomainContext';
|
|
6
|
+
/** Resolve one exact installed Domain for every projected recipe in the React subtree. */
|
|
7
|
+
export function DomainProvider(props) {
|
|
8
|
+
const binding = useDomain(props.schema);
|
|
9
|
+
return _jsx(DomainContext.Provider, { value: binding.domain, children: props.children });
|
|
10
|
+
}
|
|
11
|
+
export function useExecutionDomain() {
|
|
12
|
+
const domain = useContext(DomainContext);
|
|
13
|
+
if (domain === null) {
|
|
14
|
+
throw new Error('Projected Query and Mutation hooks require a surrounding DomainProvider.');
|
|
15
|
+
}
|
|
16
|
+
return domain;
|
|
17
|
+
}
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { useKernel } from './kernel.hook.js';
|
|
2
2
|
export { useDomain, useDomains, useDynamicDomain } from './domain.hook.js';
|
|
3
|
+
export { DomainProvider } from './domain.context.js';
|
|
4
|
+
export type { DomainProviderProps } from './domain.context.js';
|
|
3
5
|
export { queryRefresh, useAction } from './action.hook.js';
|
|
4
6
|
export type { ActionOptions, ActionRefresh, DomainAction, QueryRefresh } from './action.hook.js';
|
|
5
7
|
export { useMutation } from './mutation.hook.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useKernel } from './kernel.hook.js';
|
|
2
2
|
export { useDomain, useDomains, useDynamicDomain } from './domain.hook.js';
|
|
3
|
+
export { DomainProvider } from './domain.context.js';
|
|
3
4
|
export { queryRefresh, useAction } from './action.hook.js';
|
|
4
5
|
export { useMutation } from './mutation.hook.js';
|
|
5
6
|
export { useDynamicObject, useObject } from './object.hook.js';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Mutation } from '@astrale-os/sdk/mutation';
|
|
2
|
+
import type { Domain } from '@astrale-os/sdk/schema';
|
|
2
3
|
import type { DomainAction } from './action.hook.js';
|
|
3
4
|
export interface MutationOptions {
|
|
4
5
|
readonly optimistic?: boolean;
|
|
5
6
|
readonly onError?: (error: unknown) => void;
|
|
6
7
|
}
|
|
7
8
|
/** Execute one named SDK Mutation definition through GraphStore optimism/invalidation. */
|
|
8
|
-
export declare function useMutation<Input, Output>(definition: Mutation<Input, Output>, options?: MutationOptions): DomainAction<[Input], Output>;
|
|
9
|
+
export declare function useMutation<DomainValue extends Domain, Input, Output>(definition: Mutation<DomainValue, Input, Output>, options?: MutationOptions): DomainAction<[Input], Output>;
|
|
@@ -2,9 +2,11 @@ import { executeMutation } from '@astrale-os/sdk/mutation';
|
|
|
2
2
|
import { useCallback, useMemo, useRef } from 'react';
|
|
3
3
|
import { useAction as useAsyncAction } from '../graph/action.js';
|
|
4
4
|
import { useGraphStore } from '../graph/store.js';
|
|
5
|
+
import { useExecutionDomain } from './domain.context.js';
|
|
5
6
|
/** Execute one named SDK Mutation definition through GraphStore optimism/invalidation. */
|
|
6
7
|
export function useMutation(definition, options = {}) {
|
|
7
8
|
const store = useGraphStore();
|
|
9
|
+
const domain = useExecutionDomain();
|
|
8
10
|
const definitionRef = useRef(definition);
|
|
9
11
|
definitionRef.current = definition;
|
|
10
12
|
const optionsRef = useRef(options);
|
|
@@ -12,16 +14,17 @@ export function useMutation(definition, options = {}) {
|
|
|
12
14
|
const run = useCallback(async (input) => {
|
|
13
15
|
try {
|
|
14
16
|
return await executeMutation({
|
|
15
|
-
mutate: (ast) => store.mutate(ast, {
|
|
17
|
+
mutate: (ast, graphOptions) => store.mutate(ast, {
|
|
18
|
+
...graphOptions,
|
|
16
19
|
optimistic: optionsRef.current.optimistic ?? true,
|
|
17
20
|
}),
|
|
18
|
-
}, definitionRef.current, input);
|
|
21
|
+
}, domain, definitionRef.current, input);
|
|
19
22
|
}
|
|
20
23
|
catch (error) {
|
|
21
24
|
optionsRef.current.onError?.(error);
|
|
22
25
|
throw error;
|
|
23
26
|
}
|
|
24
|
-
}, [store]);
|
|
27
|
+
}, [store, domain]);
|
|
25
28
|
const action = useAsyncAction(run);
|
|
26
29
|
return useMemo(() => project(action), [action]);
|
|
27
30
|
}
|
|
@@ -22,7 +22,9 @@ export function useObject(definition, target, options = {}) {
|
|
|
22
22
|
: result.graph.nodes.find((candidate) => candidate.id === id);
|
|
23
23
|
return node === undefined ? null : binding.wrapNode(definition, node);
|
|
24
24
|
}, [binding, definition]);
|
|
25
|
-
return useStoreResource(query, options.pageSize ?? 1, project
|
|
25
|
+
return useStoreResource(query, options.pageSize ?? 1, project, {
|
|
26
|
+
expected: binding.domain.closure,
|
|
27
|
+
});
|
|
26
28
|
}
|
|
27
29
|
/** Explicitly untyped point read for inspectors; it never impersonates `useObject`. */
|
|
28
30
|
export function useDynamicObject(target, options = {}) {
|
|
@@ -52,7 +52,9 @@ export function useRelation(relation, source, options) {
|
|
|
52
52
|
return undefined;
|
|
53
53
|
return Object.freeze(values);
|
|
54
54
|
}, [binding, cardinality, options.to]);
|
|
55
|
-
const resource = useStoreResource(query, options.page?.size ?? 50, project
|
|
55
|
+
const resource = useStoreResource(query, options.page?.size ?? 50, project, {
|
|
56
|
+
expected: binding.domain.closure,
|
|
57
|
+
});
|
|
56
58
|
const unavailable = (cardinality === '1' || cardinality === '1..*') &&
|
|
57
59
|
resource.data === undefined &&
|
|
58
60
|
resource.state !== 'idle' &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/shell-react",
|
|
3
|
-
"version": "0.3.2-beta",
|
|
3
|
+
"version": "0.3.2-beta.2",
|
|
4
4
|
"description": "Astrale shell-react — the React face of the shell (provider, hooks, view components)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"registry": "https://registry.npmjs.org"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@astrale-os/shell": ">=0.4.2-beta <1.0.0",
|
|
29
30
|
"jose": "^6.2.8",
|
|
30
31
|
"zod": "^4.4.3"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@astrale-os/sdk": "0.5.0-beta.
|
|
34
|
-
"@astrale-os/shell": "0.4.2-beta",
|
|
34
|
+
"@astrale-os/sdk": "0.5.0-beta.34",
|
|
35
35
|
"@testing-library/dom": "^10.4.0",
|
|
36
36
|
"@testing-library/react": "^16.3.0",
|
|
37
37
|
"@types/node": "^26.2.0",
|
|
@@ -46,8 +46,7 @@
|
|
|
46
46
|
"vitest": "4.1.10"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@astrale-os/sdk": ">=0.5.0-beta.
|
|
50
|
-
"@astrale-os/shell": ">=0.4.2-beta <1.0.0",
|
|
49
|
+
"@astrale-os/sdk": ">=0.5.0-beta.32 <1.0.0",
|
|
51
50
|
"react": ">=18",
|
|
52
51
|
"react-dom": ">=18"
|
|
53
52
|
},
|
|
@@ -57,7 +56,6 @@
|
|
|
57
56
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
58
57
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
59
58
|
"test": "vitest run",
|
|
60
|
-
"test:watch": "vitest"
|
|
61
|
-
"preinstall": "node -e \"try{require('./.check-workspace.cjs')}catch{}\""
|
|
59
|
+
"test:watch": "vitest"
|
|
62
60
|
}
|
|
63
61
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { QueryPageRequest } from '@astrale-os/sdk/client'
|
|
2
2
|
import type { GraphStore, StoreQueryResponse, StoreSnapshot } from '@astrale-os/sdk/client/store'
|
|
3
3
|
import type { QueryAST, QueryDefinition } from '@astrale-os/sdk/query'
|
|
4
|
+
import type { Domain } from '@astrale-os/sdk/schema'
|
|
4
5
|
|
|
5
6
|
import { executeQuery } from '@astrale-os/sdk/query'
|
|
6
7
|
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
|
7
8
|
|
|
8
9
|
import type { GraphResource, GraphResourceState } from './resource.js'
|
|
9
10
|
|
|
11
|
+
import { useExecutionDomain } from '../schema/domain.context.js'
|
|
10
12
|
import { retainRefreshScheduler } from './resource.js'
|
|
11
13
|
import { useGraphStore } from './store.js'
|
|
12
14
|
|
|
@@ -18,7 +20,10 @@ export interface QueryResourceOptions {
|
|
|
18
20
|
|
|
19
21
|
interface TouchedShape {
|
|
20
22
|
readonly ast: QueryAST
|
|
21
|
-
readonly options: {
|
|
23
|
+
readonly options: {
|
|
24
|
+
readonly page: QueryPageRequest
|
|
25
|
+
readonly expected?: Domain['closure']
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
interface DefinitionSnapshot<Output> {
|
|
@@ -27,7 +32,7 @@ interface DefinitionSnapshot<Output> {
|
|
|
27
32
|
readonly error?: unknown
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
class DefinitionObserver<Input, Output> {
|
|
35
|
+
class DefinitionObserver<DomainValue extends Domain, Input, Output> {
|
|
31
36
|
private readonly listeners = new Set<() => void>()
|
|
32
37
|
private subscriptions: readonly (() => void)[] = Object.freeze([])
|
|
33
38
|
private touched: readonly TouchedShape[] = Object.freeze([])
|
|
@@ -39,7 +44,8 @@ class DefinitionObserver<Input, Output> {
|
|
|
39
44
|
|
|
40
45
|
constructor(
|
|
41
46
|
private readonly store: GraphStore,
|
|
42
|
-
private readonly
|
|
47
|
+
private readonly domain: DomainValue,
|
|
48
|
+
private readonly definition: QueryDefinition<DomainValue, Input, Output>,
|
|
43
49
|
private readonly input: Input,
|
|
44
50
|
) {}
|
|
45
51
|
|
|
@@ -75,12 +81,12 @@ class DefinitionObserver<Input, Output> {
|
|
|
75
81
|
})
|
|
76
82
|
const execute = async <Ast extends QueryAST>(
|
|
77
83
|
ast: Ast,
|
|
78
|
-
options: { readonly page: QueryPageRequest },
|
|
84
|
+
options: { readonly page: QueryPageRequest; readonly expected?: Domain['closure'] },
|
|
79
85
|
): Promise<StoreQueryResponse<Ast>> => {
|
|
80
86
|
touched.push(Object.freeze({ ast, options }))
|
|
81
87
|
return this.store.read(ast, { ...options, freshness })
|
|
82
88
|
}
|
|
83
|
-
const running = executeQuery({ query: execute }, this.definition, this.input)
|
|
89
|
+
const running = executeQuery({ query: execute }, this.domain, this.definition, this.input)
|
|
84
90
|
.then((data) => {
|
|
85
91
|
if (!this.started || generation !== this.generation) return
|
|
86
92
|
this.touched = dedupeShapes(touched)
|
|
@@ -148,16 +154,17 @@ class DefinitionObserver<Input, Output> {
|
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
/** Execute one named SDK Query definition exclusively through the current GraphStore. */
|
|
151
|
-
export function useQuery<Input, Output>(
|
|
152
|
-
definition: QueryDefinition<Input, Output> | null,
|
|
157
|
+
export function useQuery<DomainValue extends Domain, Input, Output>(
|
|
158
|
+
definition: QueryDefinition<DomainValue, Input, Output> | null,
|
|
153
159
|
input: Input,
|
|
154
160
|
options: QueryResourceOptions = {},
|
|
155
161
|
): GraphResource<Output> {
|
|
156
162
|
const store = useGraphStore()
|
|
163
|
+
const domain = useExecutionDomain<DomainValue>()
|
|
157
164
|
const inputIdentity = stableIdentity(input)
|
|
158
165
|
const observer = useMemo(
|
|
159
|
-
() => (definition === null ? null : new DefinitionObserver(store, definition, input)),
|
|
160
|
-
[store, definition, inputIdentity],
|
|
166
|
+
() => (definition === null ? null : new DefinitionObserver(store, domain, definition, input)),
|
|
167
|
+
[store, domain, definition, inputIdentity],
|
|
161
168
|
)
|
|
162
169
|
const idle = useMemo<DefinitionSnapshot<Output>>(() => Object.freeze({ state: 'idle' }), [])
|
|
163
170
|
const subscribe = useCallback(
|
|
@@ -168,7 +175,8 @@ export function useQuery<Input, Output>(
|
|
|
168
175
|
const current = useSyncExternalStore(subscribe, snapshot, snapshot)
|
|
169
176
|
useEffect(() => observer?.start(), [observer])
|
|
170
177
|
|
|
171
|
-
const identity =
|
|
178
|
+
const identity =
|
|
179
|
+
definition === null ? 'idle' : `${definitionIdentity(definition)}\u0000${inputIdentity}`
|
|
172
180
|
useEffect(() => {
|
|
173
181
|
if (observer === null || options.refreshInterval === undefined) return
|
|
174
182
|
return retainRefreshScheduler(
|
|
@@ -210,6 +218,17 @@ export function useQuery<Input, Output>(
|
|
|
210
218
|
)
|
|
211
219
|
}
|
|
212
220
|
|
|
221
|
+
const definitionIdentities = new WeakMap<object, number>()
|
|
222
|
+
let nextDefinitionIdentity = 1
|
|
223
|
+
|
|
224
|
+
function definitionIdentity(definition: object): number {
|
|
225
|
+
const retained = definitionIdentities.get(definition)
|
|
226
|
+
if (retained !== undefined) return retained
|
|
227
|
+
const created = nextDefinitionIdentity++
|
|
228
|
+
definitionIdentities.set(definition, created)
|
|
229
|
+
return created
|
|
230
|
+
}
|
|
231
|
+
|
|
213
232
|
function dedupeShapes(input: readonly TouchedShape[]): readonly TouchedShape[] {
|
|
214
233
|
const seen = new Set<string>()
|
|
215
234
|
return Object.freeze(
|
package/src/graph/resource.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
GraphStore,
|
|
3
|
+
StoreQueryResponse,
|
|
4
|
+
StoreReadOptions,
|
|
5
|
+
StoreSnapshot,
|
|
6
|
+
} from '@astrale-os/sdk/client/store'
|
|
2
7
|
import type { QueryAST } from '@astrale-os/sdk/query'
|
|
3
8
|
|
|
4
9
|
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
|
@@ -32,6 +37,8 @@ export interface GraphResource<T> {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export interface StoreResourceOptions<Output> {
|
|
40
|
+
/** Exact closure evidence required by typed Domain projections. */
|
|
41
|
+
readonly expected?: StoreReadOptions['expected']
|
|
35
42
|
readonly identity?: string
|
|
36
43
|
readonly keepPrevious?: boolean
|
|
37
44
|
readonly window?: 'page' | 'append'
|
|
@@ -61,6 +68,7 @@ class StoreObserver<Query extends QueryAST> {
|
|
|
61
68
|
private readonly store: GraphStore,
|
|
62
69
|
private readonly query: Query,
|
|
63
70
|
private readonly size: number,
|
|
71
|
+
private readonly expected: StoreReadOptions['expected'],
|
|
64
72
|
) {}
|
|
65
73
|
|
|
66
74
|
subscribe = (listener: () => void): (() => void) => {
|
|
@@ -139,6 +147,7 @@ class StoreObserver<Query extends QueryAST> {
|
|
|
139
147
|
size: this.size,
|
|
140
148
|
...(page.after === undefined ? {} : { after: page.after }),
|
|
141
149
|
}),
|
|
150
|
+
...(this.expected === undefined ? {} : { expected: this.expected }),
|
|
142
151
|
})
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -181,8 +190,11 @@ export function useStoreResource<Query extends QueryAST, Output>(
|
|
|
181
190
|
): GraphResource<Output> {
|
|
182
191
|
const store = useGraphStore()
|
|
183
192
|
const observer = useMemo(
|
|
184
|
-
() =>
|
|
185
|
-
|
|
193
|
+
() =>
|
|
194
|
+
query === null
|
|
195
|
+
? null
|
|
196
|
+
: new StoreObserver(store, query, positivePage(pageSize), options.expected),
|
|
197
|
+
[store, query, pageSize, options.expected],
|
|
186
198
|
)
|
|
187
199
|
const subscribe = useCallback(
|
|
188
200
|
(listener: () => void) => (observer === null ? () => {} : observer.subscribe(listener)),
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export type {
|
|
|
39
39
|
|
|
40
40
|
// Rich exact Domain bindings.
|
|
41
41
|
export {
|
|
42
|
+
DomainProvider,
|
|
42
43
|
RelationUnavailableError,
|
|
43
44
|
queryRefresh,
|
|
44
45
|
useAction,
|
|
@@ -54,6 +55,7 @@ export {
|
|
|
54
55
|
export type {
|
|
55
56
|
ActionOptions,
|
|
56
57
|
ActionRefresh,
|
|
58
|
+
DomainProviderProps,
|
|
57
59
|
DomainAction,
|
|
58
60
|
MutationOptions,
|
|
59
61
|
QueryRefresh,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { BoundNodeFor } from '@astrale-os/sdk/client'
|
|
2
2
|
import type { SessionRequestOptions } from '@astrale-os/sdk/client/session'
|
|
3
3
|
import type { NodeId } from '@astrale-os/sdk/graph/node'
|
|
4
|
-
import type { QueryAST } from '@astrale-os/sdk/query'
|
|
4
|
+
import type { QueryAST, QueryDefinition } from '@astrale-os/sdk/query'
|
|
5
|
+
import type { Domain } from '@astrale-os/sdk/schema'
|
|
5
6
|
import type {
|
|
6
7
|
CallableInputOf,
|
|
7
8
|
CallableResultOf,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
|
|
14
15
|
import { reference } from '@astrale-os/sdk/client/session'
|
|
15
16
|
import { Path } from '@astrale-os/sdk/graph/path'
|
|
17
|
+
import { realizeQuery } from '@astrale-os/sdk/query'
|
|
16
18
|
import { useCallback, useMemo, useRef } from 'react'
|
|
17
19
|
|
|
18
20
|
import type { Action as AsyncAction } from '../graph/action.js'
|
|
@@ -33,29 +35,30 @@ const QUERY_REFRESH = Symbol('shell-react.query-refresh')
|
|
|
33
35
|
|
|
34
36
|
export interface QueryRefresh {
|
|
35
37
|
readonly id: string
|
|
36
|
-
readonly [QUERY_REFRESH]: () => {
|
|
38
|
+
readonly [QUERY_REFRESH]: (domain: Domain) => {
|
|
37
39
|
readonly ast: QueryAST
|
|
38
40
|
readonly page: Readonly<{ readonly size: number }>
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
readonly page: Readonly<{ readonly size: number }>
|
|
45
|
-
prepare?(input: Input): Input
|
|
46
|
-
build(input: Input): Query
|
|
47
|
-
}
|
|
44
|
+
const queryRefreshIdentities = new WeakMap<object, string>()
|
|
45
|
+
let nextQueryRefreshIdentity = 0
|
|
48
46
|
|
|
49
47
|
/** Pair one exact named Query definition with its admitted input for targeted invalidation. */
|
|
50
|
-
export function queryRefresh<Input,
|
|
51
|
-
definition:
|
|
48
|
+
export function queryRefresh<DomainValue extends Domain, Input, Output>(
|
|
49
|
+
definition: QueryDefinition<DomainValue, Input, Output>,
|
|
52
50
|
input: Input,
|
|
53
51
|
): QueryRefresh {
|
|
52
|
+
const id = queryRefreshIdentity(definition)
|
|
54
53
|
return Object.freeze({
|
|
55
|
-
id
|
|
56
|
-
[QUERY_REFRESH]: () => {
|
|
57
|
-
const
|
|
58
|
-
|
|
54
|
+
id,
|
|
55
|
+
[QUERY_REFRESH]: (domain: Domain) => {
|
|
56
|
+
const realized = realizeQuery(definition, domain as DomainValue)
|
|
57
|
+
if (realized.kind !== 'single') {
|
|
58
|
+
throw new TypeError('Targeted refresh requires a single Query recipe.')
|
|
59
|
+
}
|
|
60
|
+
const prepared = realized.prepare?.(input) ?? input
|
|
61
|
+
return Object.freeze({ ast: realized.build(prepared), page: realized.page })
|
|
59
62
|
},
|
|
60
63
|
})
|
|
61
64
|
}
|
|
@@ -120,7 +123,7 @@ export function useAction<C extends Callable>(
|
|
|
120
123
|
if (refresh === 'all') store.invalidate()
|
|
121
124
|
else if (refresh !== 'none') {
|
|
122
125
|
refresh.queries.forEach((query) => {
|
|
123
|
-
const target = query[QUERY_REFRESH]()
|
|
126
|
+
const target = query[QUERY_REFRESH](bindingRef.current.domain)
|
|
124
127
|
store.invalidate(target.ast, { page: target.page })
|
|
125
128
|
})
|
|
126
129
|
}
|
|
@@ -161,3 +164,11 @@ function isInstanceMethod(callable: Callable): callable is InstanceMethod {
|
|
|
161
164
|
function receiverPath(receiver: Receiver): Path {
|
|
162
165
|
return Path.id(receiver.id)
|
|
163
166
|
}
|
|
167
|
+
|
|
168
|
+
function queryRefreshIdentity(definition: object): string {
|
|
169
|
+
const current = queryRefreshIdentities.get(definition)
|
|
170
|
+
if (current !== undefined) return current
|
|
171
|
+
const created = `query-${++nextQueryRefreshIdentity}`
|
|
172
|
+
queryRefreshIdentities.set(definition, created)
|
|
173
|
+
return created
|
|
174
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Domain } from '@astrale-os/sdk/schema'
|
|
2
|
+
import type { schema } from '@astrale-os/sdk/schema'
|
|
3
|
+
import type { ReactElement, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import { createContext, useContext } from 'react'
|
|
6
|
+
|
|
7
|
+
import { useDomain } from './domain.hook.js'
|
|
8
|
+
|
|
9
|
+
const DomainContext = createContext<Domain | null>(null)
|
|
10
|
+
DomainContext.displayName = 'AstraleDomainContext'
|
|
11
|
+
|
|
12
|
+
export interface DomainProviderProps<Schema extends schema.DomainSchema> {
|
|
13
|
+
readonly schema: Schema
|
|
14
|
+
readonly children: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Resolve one exact installed Domain for every projected recipe in the React subtree. */
|
|
18
|
+
export function DomainProvider<const Schema extends schema.DomainSchema>(
|
|
19
|
+
props: DomainProviderProps<Schema>,
|
|
20
|
+
): ReactElement {
|
|
21
|
+
const binding = useDomain(props.schema)
|
|
22
|
+
return <DomainContext.Provider value={binding.domain}>{props.children}</DomainContext.Provider>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useExecutionDomain<DomainValue extends Domain>(): DomainValue {
|
|
26
|
+
const domain = useContext(DomainContext)
|
|
27
|
+
if (domain === null) {
|
|
28
|
+
throw new Error('Projected Query and Mutation hooks require a surrounding DomainProvider.')
|
|
29
|
+
}
|
|
30
|
+
return domain as DomainValue
|
|
31
|
+
}
|
package/src/schema/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { useKernel } from './kernel.hook.js'
|
|
2
2
|
export { useDomain, useDomains, useDynamicDomain } from './domain.hook.js'
|
|
3
|
+
export { DomainProvider } from './domain.context.js'
|
|
4
|
+
export type { DomainProviderProps } from './domain.context.js'
|
|
3
5
|
export { queryRefresh, useAction } from './action.hook.js'
|
|
4
6
|
export type { ActionOptions, ActionRefresh, DomainAction, QueryRefresh } from './action.hook.js'
|
|
5
7
|
export { useMutation } from './mutation.hook.js'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Mutation } from '@astrale-os/sdk/mutation'
|
|
2
|
+
import type { Domain } from '@astrale-os/sdk/schema'
|
|
2
3
|
|
|
3
4
|
import { executeMutation } from '@astrale-os/sdk/mutation'
|
|
4
5
|
import { useCallback, useMemo, useRef } from 'react'
|
|
@@ -8,6 +9,7 @@ import type { DomainAction } from './action.hook.js'
|
|
|
8
9
|
|
|
9
10
|
import { useAction as useAsyncAction } from '../graph/action.js'
|
|
10
11
|
import { useGraphStore } from '../graph/store.js'
|
|
12
|
+
import { useExecutionDomain } from './domain.context.js'
|
|
11
13
|
|
|
12
14
|
export interface MutationOptions {
|
|
13
15
|
readonly optimistic?: boolean
|
|
@@ -15,11 +17,12 @@ export interface MutationOptions {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** Execute one named SDK Mutation definition through GraphStore optimism/invalidation. */
|
|
18
|
-
export function useMutation<Input, Output>(
|
|
19
|
-
definition: Mutation<Input, Output>,
|
|
20
|
+
export function useMutation<DomainValue extends Domain, Input, Output>(
|
|
21
|
+
definition: Mutation<DomainValue, Input, Output>,
|
|
20
22
|
options: MutationOptions = {},
|
|
21
23
|
): DomainAction<[Input], Output> {
|
|
22
24
|
const store = useGraphStore()
|
|
25
|
+
const domain = useExecutionDomain<DomainValue>()
|
|
23
26
|
const definitionRef = useRef(definition)
|
|
24
27
|
definitionRef.current = definition
|
|
25
28
|
const optionsRef = useRef(options)
|
|
@@ -29,11 +32,13 @@ export function useMutation<Input, Output>(
|
|
|
29
32
|
try {
|
|
30
33
|
return await executeMutation(
|
|
31
34
|
{
|
|
32
|
-
mutate: (ast) =>
|
|
35
|
+
mutate: (ast, graphOptions) =>
|
|
33
36
|
store.mutate(ast, {
|
|
37
|
+
...graphOptions,
|
|
34
38
|
optimistic: optionsRef.current.optimistic ?? true,
|
|
35
39
|
}),
|
|
36
40
|
},
|
|
41
|
+
domain,
|
|
37
42
|
definitionRef.current,
|
|
38
43
|
input,
|
|
39
44
|
)
|
|
@@ -42,7 +47,7 @@ export function useMutation<Input, Output>(
|
|
|
42
47
|
throw error
|
|
43
48
|
}
|
|
44
49
|
},
|
|
45
|
-
[store],
|
|
50
|
+
[store, domain],
|
|
46
51
|
)
|
|
47
52
|
const action = useAsyncAction(run)
|
|
48
53
|
return useMemo(() => project(action), [action])
|
|
@@ -47,7 +47,9 @@ export function useObject<Class extends ResolvedClass<'node'>>(
|
|
|
47
47
|
},
|
|
48
48
|
[binding, definition],
|
|
49
49
|
)
|
|
50
|
-
return useStoreResource(query, options.pageSize ?? 1, project
|
|
50
|
+
return useStoreResource(query, options.pageSize ?? 1, project, {
|
|
51
|
+
expected: binding.domain.closure,
|
|
52
|
+
})
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/** Explicitly untyped point read for inspectors; it never impersonates `useObject`. */
|
|
@@ -150,7 +150,9 @@ export function useRelation<
|
|
|
150
150
|
},
|
|
151
151
|
[binding, cardinality, options.to],
|
|
152
152
|
)
|
|
153
|
-
const resource = useStoreResource(query, options.page?.size ?? 50, project
|
|
153
|
+
const resource = useStoreResource(query, options.page?.size ?? 50, project, {
|
|
154
|
+
expected: binding.domain.closure,
|
|
155
|
+
})
|
|
154
156
|
const unavailable =
|
|
155
157
|
(cardinality === '1' || cardinality === '1..*') &&
|
|
156
158
|
resource.data === undefined &&
|