houdini-react 2.0.0-next.4 → 2.0.0-next.40
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/bin/houdini-react +173 -0
- package/package.json +63 -39
- package/postInstall.js +360 -0
- package/runtime/Link.tsx +81 -0
- package/runtime/client.ts +5 -0
- package/runtime/clientPlugin.ts +17 -0
- package/runtime/componentFields.ts +79 -0
- package/runtime/hooks/index.ts +9 -0
- package/runtime/hooks/recycleNodesInto.ts +60 -0
- package/runtime/hooks/useDeepCompareEffect.ts +93 -0
- package/runtime/hooks/useDocumentHandle.ts +238 -0
- package/runtime/hooks/useDocumentStore.ts +112 -0
- package/runtime/hooks/useDocumentSubscription.ts +65 -0
- package/runtime/hooks/useFragment.ts +101 -0
- package/runtime/hooks/useFragmentHandle.ts +191 -0
- package/runtime/hooks/useIsMounted.ts +14 -0
- package/runtime/hooks/useMutation.ts +70 -0
- package/runtime/hooks/useQuery.ts +12 -0
- package/runtime/hooks/useQueryHandle.ts +194 -0
- package/runtime/hooks/useSubscription.ts +12 -0
- package/runtime/hooks/useSubscriptionHandle.ts +38 -0
- package/runtime/hydration.tsx +155 -0
- package/runtime/index.tsx +66 -0
- package/runtime/manifest.ts +6 -0
- package/runtime/package.json +1 -0
- package/runtime/resolve-href.ts +14 -0
- package/runtime/routing/Router.tsx +959 -0
- package/runtime/routing/cache.ts +57 -0
- package/runtime/routing/errors.tsx +145 -0
- package/runtime/routing/index.ts +17 -0
- package/runtime/tsconfig.json +39 -0
- package/server/index.d.ts +1 -0
- package/server/index.js +4 -0
- package/server/react-streaming.d.js +0 -0
- package/vite/index.d.ts +3 -0
- package/vite/index.js +384 -0
- package/vite/transform.d.ts +11 -0
- package/vite/transform.js +90 -0
- package/README.md +0 -36
- package/build/plugin/codegen/entries/documentWrappers.d.ts +0 -6
- package/build/plugin/codegen/entries/fallbacks.d.ts +0 -5
- package/build/plugin/codegen/entries/index.d.ts +0 -16
- package/build/plugin/codegen/entries/pages.d.ts +0 -2
- package/build/plugin/codegen/index.d.ts +0 -17
- package/build/plugin/codegen/manifest.d.ts +0 -5
- package/build/plugin/codegen/render.d.ts +0 -7
- package/build/plugin/codegen/router.d.ts +0 -7
- package/build/plugin/codegen/typeRoot.d.ts +0 -5
- package/build/plugin/config.d.ts +0 -4
- package/build/plugin/dedent.d.ts +0 -1
- package/build/plugin/extract.d.ts +0 -6
- package/build/plugin/index.d.ts +0 -5
- package/build/plugin/state.d.ts +0 -3
- package/build/plugin/transform.d.ts +0 -6
- package/build/plugin/vite.d.ts +0 -27
- package/build/plugin-cjs/index.js +0 -90119
- package/build/plugin-cjs/package.json +0 -1
- package/build/plugin-esm/index.js +0 -90115
- package/build/runtime/client.d.ts +0 -3
- package/build/runtime/clientPlugin.d.ts +0 -3
- package/build/runtime/componentFields.d.ts +0 -9
- package/build/runtime/hooks/index.d.ts +0 -8
- package/build/runtime/hooks/useDeepCompareEffect.d.ts +0 -35
- package/build/runtime/hooks/useDocumentHandle.d.ts +0 -36
- package/build/runtime/hooks/useDocumentStore.d.ts +0 -11
- package/build/runtime/hooks/useDocumentSubscription.d.ts +0 -11
- package/build/runtime/hooks/useFragment.d.ts +0 -16
- package/build/runtime/hooks/useFragmentHandle.d.ts +0 -8
- package/build/runtime/hooks/useIsMounted.d.ts +0 -3
- package/build/runtime/hooks/useMutation.d.ts +0 -14
- package/build/runtime/hooks/useQuery.d.ts +0 -5
- package/build/runtime/hooks/useQueryHandle.d.ts +0 -10
- package/build/runtime/hooks/useSubscription.d.ts +0 -4
- package/build/runtime/hooks/useSubscriptionHandle.d.ts +0 -25
- package/build/runtime/index.d.ts +0 -14
- package/build/runtime/manifest.d.ts +0 -3
- package/build/runtime/routing/Router.d.ts +0 -62
- package/build/runtime/routing/cache.d.ts +0 -7
- package/build/runtime/routing/hooks.d.ts +0 -40
- package/build/runtime/routing/index.d.ts +0 -3
- package/build/runtime-cjs/client.d.ts +0 -3
- package/build/runtime-cjs/client.js +0 -25
- package/build/runtime-cjs/clientPlugin.d.ts +0 -3
- package/build/runtime-cjs/clientPlugin.js +0 -37
- package/build/runtime-cjs/componentFields.d.ts +0 -9
- package/build/runtime-cjs/componentFields.js +0 -83
- package/build/runtime-cjs/hooks/index.d.ts +0 -8
- package/build/runtime-cjs/hooks/index.js +0 -45
- package/build/runtime-cjs/hooks/useDeepCompareEffect.d.ts +0 -35
- package/build/runtime-cjs/hooks/useDeepCompareEffect.js +0 -76
- package/build/runtime-cjs/hooks/useDocumentHandle.d.ts +0 -36
- package/build/runtime-cjs/hooks/useDocumentHandle.js +0 -177
- package/build/runtime-cjs/hooks/useDocumentStore.d.ts +0 -11
- package/build/runtime-cjs/hooks/useDocumentStore.js +0 -76
- package/build/runtime-cjs/hooks/useDocumentSubscription.d.ts +0 -11
- package/build/runtime-cjs/hooks/useDocumentSubscription.js +0 -76
- package/build/runtime-cjs/hooks/useFragment.d.ts +0 -16
- package/build/runtime-cjs/hooks/useFragment.js +0 -102
- package/build/runtime-cjs/hooks/useFragmentHandle.d.ts +0 -8
- package/build/runtime-cjs/hooks/useFragmentHandle.js +0 -47
- package/build/runtime-cjs/hooks/useIsMounted.d.ts +0 -3
- package/build/runtime-cjs/hooks/useIsMounted.js +0 -38
- package/build/runtime-cjs/hooks/useMutation.d.ts +0 -14
- package/build/runtime-cjs/hooks/useMutation.js +0 -67
- package/build/runtime-cjs/hooks/useQuery.d.ts +0 -5
- package/build/runtime-cjs/hooks/useQuery.js +0 -32
- package/build/runtime-cjs/hooks/useQueryHandle.d.ts +0 -10
- package/build/runtime-cjs/hooks/useQueryHandle.js +0 -131
- package/build/runtime-cjs/hooks/useSubscription.d.ts +0 -4
- package/build/runtime-cjs/hooks/useSubscription.js +0 -32
- package/build/runtime-cjs/hooks/useSubscriptionHandle.d.ts +0 -25
- package/build/runtime-cjs/hooks/useSubscriptionHandle.js +0 -42
- package/build/runtime-cjs/index.d.ts +0 -14
- package/build/runtime-cjs/index.js +0 -88
- package/build/runtime-cjs/manifest.d.ts +0 -3
- package/build/runtime-cjs/manifest.js +0 -25
- package/build/runtime-cjs/package.json +0 -1
- package/build/runtime-cjs/routing/Router.d.ts +0 -62
- package/build/runtime-cjs/routing/Router.js +0 -540
- package/build/runtime-cjs/routing/cache.d.ts +0 -7
- package/build/runtime-cjs/routing/cache.js +0 -61
- package/build/runtime-cjs/routing/hooks.d.ts +0 -40
- package/build/runtime-cjs/routing/hooks.js +0 -93
- package/build/runtime-cjs/routing/index.d.ts +0 -3
- package/build/runtime-cjs/routing/index.js +0 -33
- package/build/runtime-esm/client.d.ts +0 -3
- package/build/runtime-esm/client.js +0 -5
- package/build/runtime-esm/clientPlugin.d.ts +0 -3
- package/build/runtime-esm/clientPlugin.js +0 -17
- package/build/runtime-esm/componentFields.d.ts +0 -9
- package/build/runtime-esm/componentFields.js +0 -59
- package/build/runtime-esm/hooks/index.d.ts +0 -8
- package/build/runtime-esm/hooks/index.js +0 -15
- package/build/runtime-esm/hooks/useDeepCompareEffect.d.ts +0 -35
- package/build/runtime-esm/hooks/useDeepCompareEffect.js +0 -41
- package/build/runtime-esm/hooks/useDocumentHandle.d.ts +0 -36
- package/build/runtime-esm/hooks/useDocumentHandle.js +0 -143
- package/build/runtime-esm/hooks/useDocumentStore.d.ts +0 -11
- package/build/runtime-esm/hooks/useDocumentStore.js +0 -42
- package/build/runtime-esm/hooks/useDocumentSubscription.d.ts +0 -11
- package/build/runtime-esm/hooks/useDocumentSubscription.js +0 -42
- package/build/runtime-esm/hooks/useFragment.d.ts +0 -16
- package/build/runtime-esm/hooks/useFragment.js +0 -67
- package/build/runtime-esm/hooks/useFragmentHandle.d.ts +0 -8
- package/build/runtime-esm/hooks/useFragmentHandle.js +0 -23
- package/build/runtime-esm/hooks/useIsMounted.d.ts +0 -3
- package/build/runtime-esm/hooks/useIsMounted.js +0 -14
- package/build/runtime-esm/hooks/useMutation.d.ts +0 -14
- package/build/runtime-esm/hooks/useMutation.js +0 -42
- package/build/runtime-esm/hooks/useQuery.d.ts +0 -5
- package/build/runtime-esm/hooks/useQuery.js +0 -8
- package/build/runtime-esm/hooks/useQueryHandle.d.ts +0 -10
- package/build/runtime-esm/hooks/useQueryHandle.js +0 -97
- package/build/runtime-esm/hooks/useSubscription.d.ts +0 -4
- package/build/runtime-esm/hooks/useSubscription.js +0 -8
- package/build/runtime-esm/hooks/useSubscriptionHandle.d.ts +0 -25
- package/build/runtime-esm/hooks/useSubscriptionHandle.js +0 -18
- package/build/runtime-esm/index.d.ts +0 -14
- package/build/runtime-esm/index.js +0 -48
- package/build/runtime-esm/manifest.d.ts +0 -3
- package/build/runtime-esm/manifest.js +0 -5
- package/build/runtime-esm/routing/Router.d.ts +0 -62
- package/build/runtime-esm/routing/Router.js +0 -499
- package/build/runtime-esm/routing/cache.d.ts +0 -7
- package/build/runtime-esm/routing/cache.js +0 -36
- package/build/runtime-esm/routing/hooks.d.ts +0 -40
- package/build/runtime-esm/routing/hooks.js +0 -53
- package/build/runtime-esm/routing/index.d.ts +0 -3
- package/build/runtime-esm/routing/index.js +0 -6
- package/build/server/index.d.ts +0 -1
- package/build/server-cjs/index.js +0 -28
- package/build/server-cjs/package.json +0 -1
- package/build/server-esm/index.js +0 -4
- package/build/server-esm/package.json +0 -1
- /package/{build/plugin-esm → server}/package.json +0 -0
- /package/{build/runtime-esm → vite}/package.json +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentArtifact,
|
|
3
|
+
GraphQLVariables,
|
|
4
|
+
QueryResult,
|
|
5
|
+
GraphQLObject,
|
|
6
|
+
} from 'houdini/runtime'
|
|
7
|
+
import type { DocumentStore, SendParams } from 'houdini/runtime/client'
|
|
8
|
+
|
|
9
|
+
import { useSession } from '../routing/Router.js'
|
|
10
|
+
import useDeepCompareEffect from './useDeepCompareEffect.js'
|
|
11
|
+
import { useDocumentStore, type UseDocumentStoreParams } from './useDocumentStore.js'
|
|
12
|
+
|
|
13
|
+
export function useDocumentSubscription<
|
|
14
|
+
_Artifact extends DocumentArtifact = DocumentArtifact,
|
|
15
|
+
_Data extends GraphQLObject = GraphQLObject,
|
|
16
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
17
|
+
>({
|
|
18
|
+
artifact,
|
|
19
|
+
variables,
|
|
20
|
+
send,
|
|
21
|
+
initialState,
|
|
22
|
+
disabled,
|
|
23
|
+
...observeParams
|
|
24
|
+
}: UseDocumentStoreParams<_Artifact, _Data, _Input> & {
|
|
25
|
+
variables: _Input
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
send?: Partial<Omit<SendParams, 'initialState'>>
|
|
28
|
+
}): [QueryResult<_Data, _Input> & { parent?: string | null }, DocumentStore<_Data, _Input>] {
|
|
29
|
+
const [storeValue, observer] = useDocumentStore<_Data, _Input>({
|
|
30
|
+
artifact,
|
|
31
|
+
initialState,
|
|
32
|
+
...observeParams,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// grab the current session value
|
|
36
|
+
const [session] = useSession()
|
|
37
|
+
|
|
38
|
+
// whenever the variables change, we need to retrigger the query
|
|
39
|
+
useDeepCompareEffect(() => {
|
|
40
|
+
if (!disabled) {
|
|
41
|
+
observer.send({
|
|
42
|
+
variables,
|
|
43
|
+
session,
|
|
44
|
+
// TODO: metadata
|
|
45
|
+
metadata: {},
|
|
46
|
+
...send,
|
|
47
|
+
initialState,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
if (!disabled) {
|
|
53
|
+
observer.cleanup()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, [disabled, session, observer, variables ?? {}, send ?? {}])
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
parent: send?.stuff?.parentID,
|
|
61
|
+
...storeValue,
|
|
62
|
+
},
|
|
63
|
+
observer,
|
|
64
|
+
]
|
|
65
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { fragmentKey } from 'houdini/runtime'
|
|
2
|
+
import type {
|
|
3
|
+
GraphQLObject,
|
|
4
|
+
GraphQLVariables,
|
|
5
|
+
FragmentArtifact,
|
|
6
|
+
QueryResult,
|
|
7
|
+
} from 'houdini/runtime'
|
|
8
|
+
import * as React from 'react'
|
|
9
|
+
|
|
10
|
+
import { useRouterContext } from '../routing/index.js'
|
|
11
|
+
import { useDocumentSubscription } from './useDocumentSubscription.js'
|
|
12
|
+
|
|
13
|
+
export function useFragment<
|
|
14
|
+
_Data extends GraphQLObject,
|
|
15
|
+
_ReferenceType extends {},
|
|
16
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
17
|
+
>(
|
|
18
|
+
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
19
|
+
document: { artifact: FragmentArtifact }
|
|
20
|
+
): _Data | null {
|
|
21
|
+
const { cache } = useRouterContext()
|
|
22
|
+
const { parent, variables, loading } = fragmentReference<_Data, _Input, _ReferenceType>(
|
|
23
|
+
reference,
|
|
24
|
+
document
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Read from cache whenever the parent or loading state changes. The parent
|
|
28
|
+
// path uniquely identifies which cache record this fragment is bound to, so
|
|
29
|
+
// variables are excluded from the dep array — they are forwarded to
|
|
30
|
+
// observer.send() separately and don't affect which record we read.
|
|
31
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: variables intentionally excluded
|
|
32
|
+
const cachedValue = React.useMemo(() => {
|
|
33
|
+
if (reference && parent) {
|
|
34
|
+
return cache.read({
|
|
35
|
+
selection: document.artifact.selection,
|
|
36
|
+
parent,
|
|
37
|
+
variables,
|
|
38
|
+
loading,
|
|
39
|
+
}).data as _Data
|
|
40
|
+
}
|
|
41
|
+
return reference as _Data | null
|
|
42
|
+
}, [parent, loading])
|
|
43
|
+
|
|
44
|
+
// Stable initialState derived from cachedValue. useDocumentStore uses this to
|
|
45
|
+
// seed box.current synchronously during render when the parent changes, so
|
|
46
|
+
// storeValue.data is immediately correct without waiting for the subscription
|
|
47
|
+
// effect to fire. Must be memoized (reference-stable) so the store doesn't
|
|
48
|
+
// re-seed on every render.
|
|
49
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: variables changes don't require re-seeding
|
|
50
|
+
const initialState = React.useMemo(
|
|
51
|
+
(): QueryResult<_Data, _Input> | undefined =>
|
|
52
|
+
cachedValue !== null
|
|
53
|
+
? {
|
|
54
|
+
data: cachedValue,
|
|
55
|
+
errors: null,
|
|
56
|
+
fetching: false,
|
|
57
|
+
partial: false,
|
|
58
|
+
stale: false,
|
|
59
|
+
source: null,
|
|
60
|
+
variables: variables ?? null,
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
63
|
+
[cachedValue]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const [storeValue] = useDocumentSubscription<FragmentArtifact, _Data, _Input>({
|
|
67
|
+
artifact: document.artifact,
|
|
68
|
+
variables,
|
|
69
|
+
initialValue: cachedValue,
|
|
70
|
+
disabled: loading,
|
|
71
|
+
send: {
|
|
72
|
+
stuff: {
|
|
73
|
+
parentID: parent,
|
|
74
|
+
},
|
|
75
|
+
setup: true,
|
|
76
|
+
},
|
|
77
|
+
initialState,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return storeValue.data
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function fragmentReference<_Data extends GraphQLObject, _Input, _ReferenceType extends {}>(
|
|
84
|
+
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
85
|
+
document: { artifact: FragmentArtifact }
|
|
86
|
+
): { variables: _Input; parent: string; loading: boolean } {
|
|
87
|
+
// @ts-expect-error: typescript can't guarantee that the fragment key is defined
|
|
88
|
+
// but if its not, then the fragment wasn't mixed into the right thing
|
|
89
|
+
// the variables for the fragment live on the initial value's $fragment key
|
|
90
|
+
const { variables, parent } = reference?.[fragmentKey]?.values?.[document.artifact.name] ?? {}
|
|
91
|
+
if (reference && fragmentKey in reference && (!variables || !parent)) {
|
|
92
|
+
console.warn(
|
|
93
|
+
`⚠️ Parent does not contain the information for this fragment. Something is wrong.
|
|
94
|
+
Please ensure that you have passed a record that has ${document.artifact.name} mixed into it.`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
// @ts-expect-error
|
|
98
|
+
const loading = Boolean(reference?.[fragmentKey]?.loading)
|
|
99
|
+
|
|
100
|
+
return { variables, parent, loading }
|
|
101
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { extractPageInfo, cursorHandlers, offsetHandlers } from 'houdini/runtime'
|
|
2
|
+
import type {
|
|
3
|
+
GraphQLObject,
|
|
4
|
+
FragmentArtifact,
|
|
5
|
+
QueryArtifact,
|
|
6
|
+
GraphQLVariables,
|
|
7
|
+
FetchFn,
|
|
8
|
+
} from 'houdini/runtime'
|
|
9
|
+
import * as React from 'react'
|
|
10
|
+
|
|
11
|
+
import { useClient, useSession } from '../routing/Router.js'
|
|
12
|
+
import type { DocumentHandle } from './useDocumentHandle.js'
|
|
13
|
+
import { fragmentReference, useFragment } from './useFragment.js'
|
|
14
|
+
|
|
15
|
+
// useFragmentHandle is just like useFragment except it also returns an imperative handle
|
|
16
|
+
// that users can use to interact with the fragment
|
|
17
|
+
export function useFragmentHandle<
|
|
18
|
+
_Artifact extends FragmentArtifact,
|
|
19
|
+
_Data extends GraphQLObject,
|
|
20
|
+
_ReferenceType extends {},
|
|
21
|
+
_PaginationArtifact extends QueryArtifact,
|
|
22
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
23
|
+
>(
|
|
24
|
+
reference: _Data | { ' $fragments': _ReferenceType } | null,
|
|
25
|
+
document: { artifact: FragmentArtifact; refetchArtifact?: QueryArtifact }
|
|
26
|
+
): any {
|
|
27
|
+
// get the fragment values
|
|
28
|
+
const fragmentData = useFragment<_Data, _ReferenceType, _Input>(reference, document)
|
|
29
|
+
|
|
30
|
+
// look at the fragment reference to get the variables
|
|
31
|
+
const { variables } = fragmentReference<_Data, _Input, _ReferenceType>(reference, document)
|
|
32
|
+
|
|
33
|
+
const client = useClient()
|
|
34
|
+
const [session] = useSession()
|
|
35
|
+
|
|
36
|
+
const [forwardPending, setForwardPending] = React.useState(false)
|
|
37
|
+
const [backwardPending, setBackwardPending] = React.useState(false)
|
|
38
|
+
|
|
39
|
+
// Stable cursor stacks for SinglePage pagination — must survive re-renders
|
|
40
|
+
const previousCursorsRef = React.useRef<(string | null)[]>([])
|
|
41
|
+
const nextCursorsRef = React.useRef<(string | null)[]>([])
|
|
42
|
+
|
|
43
|
+
const refetchArtifact = document.refetchArtifact as QueryArtifact | undefined
|
|
44
|
+
const refetchPath = refetchArtifact?.refetch?.path
|
|
45
|
+
|
|
46
|
+
// Dedicated observer for pagination queries — separate from the fragment observer.
|
|
47
|
+
// cursorHandlers derives entity variables (e.g. { id }) and artifact defaults
|
|
48
|
+
// automatically via the type config, so no manual variable extraction is needed here.
|
|
49
|
+
const paginationObserver = React.useMemo(() => {
|
|
50
|
+
if (!refetchArtifact?.refetch?.paginated) return null
|
|
51
|
+
return client.observe<_Data, _Input>({ artifact: refetchArtifact })
|
|
52
|
+
}, [refetchArtifact?.name])
|
|
53
|
+
|
|
54
|
+
// Subscribe to the pagination observer so React re-renders whenever a new page is fetched
|
|
55
|
+
// or served from cache (CacheOrNetwork). The fragment store subscription only watches the
|
|
56
|
+
// initial page's cache key; the observer is the live source of truth for SinglePage
|
|
57
|
+
// pagination where each page lives at its own per-cursor cache key.
|
|
58
|
+
const subscribeToObserver = React.useCallback(
|
|
59
|
+
(onChange: () => void) => {
|
|
60
|
+
if (!paginationObserver) return () => {}
|
|
61
|
+
return paginationObserver.subscribe(onChange)
|
|
62
|
+
},
|
|
63
|
+
[paginationObserver]
|
|
64
|
+
)
|
|
65
|
+
const getObserverSnapshot = React.useCallback(
|
|
66
|
+
() => paginationObserver?.state.data ?? null,
|
|
67
|
+
[paginationObserver]
|
|
68
|
+
)
|
|
69
|
+
const paginationData = React.useSyncExternalStore(
|
|
70
|
+
subscribeToObserver,
|
|
71
|
+
getObserverSnapshot,
|
|
72
|
+
getObserverSnapshot
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// Extract entity-level data from the pagination query response. For Node targetType
|
|
76
|
+
// the response is { node: EntityData }; we take the first root field to handle any type.
|
|
77
|
+
// Guard against partial cache hits (artifact has partial:true): only use the entity once the
|
|
78
|
+
// paginated connection field at refetch.path[0] is actually present in the response.
|
|
79
|
+
const paginationEntityData = React.useMemo<_Data | null>(() => {
|
|
80
|
+
if (!paginationData || !refetchArtifact?.selection?.fields) return null
|
|
81
|
+
const rootField = Object.keys(refetchArtifact.selection.fields)[0]
|
|
82
|
+
if (!rootField) return null
|
|
83
|
+
const entity = (paginationData as any)[rootField]
|
|
84
|
+
if (!entity) return null
|
|
85
|
+
const path = refetchArtifact.refetch?.path
|
|
86
|
+
if (path && path.length > 0 && (entity as any)[path[0]] == null) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
return entity as _Data
|
|
90
|
+
}, [paginationData, refetchArtifact])
|
|
91
|
+
|
|
92
|
+
const isSinglePage = refetchArtifact?.refetch?.mode === 'SinglePage'
|
|
93
|
+
|
|
94
|
+
// For SinglePage: use the pagination observer's entity data (each page has its own
|
|
95
|
+
// cache key) once a page fetch has landed. For Infinite: always use fragmentData,
|
|
96
|
+
// which reads accumulated pages from cache via the fragment's cache subscription.
|
|
97
|
+
const displayData =
|
|
98
|
+
isSinglePage && paginationEntityData !== null ? paginationEntityData : fragmentData
|
|
99
|
+
|
|
100
|
+
const wrapLoad = <_Result>(
|
|
101
|
+
setLoading: (val: boolean) => void,
|
|
102
|
+
fn: (value: any) => Promise<_Result>
|
|
103
|
+
) => {
|
|
104
|
+
return async (value: any) => {
|
|
105
|
+
setLoading(true)
|
|
106
|
+
let err: Error | null = null
|
|
107
|
+
let result: _Result | null = null
|
|
108
|
+
try {
|
|
109
|
+
result = await fn(value)
|
|
110
|
+
} catch (e) {
|
|
111
|
+
err = e as Error
|
|
112
|
+
}
|
|
113
|
+
setLoading(false)
|
|
114
|
+
if (err && err.name !== 'AbortError') throw err
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const handle = React.useMemo(() => {
|
|
120
|
+
if (!refetchArtifact?.refetch?.paginated || !paginationObserver) return null
|
|
121
|
+
|
|
122
|
+
const fetchFn: FetchFn<_Data, _Input> = (args) => {
|
|
123
|
+
return paginationObserver.send({ ...args, session })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fetchUpdate = (args: any, updates: string[]) => {
|
|
127
|
+
return paginationObserver.send({
|
|
128
|
+
...args,
|
|
129
|
+
cacheParams: {
|
|
130
|
+
...args?.cacheParams,
|
|
131
|
+
disableSubscriptions: true,
|
|
132
|
+
applyUpdates: updates,
|
|
133
|
+
},
|
|
134
|
+
session,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (refetchArtifact.refetch!.method === 'cursor') {
|
|
139
|
+
const handlers = cursorHandlers<_Data, _Input>({
|
|
140
|
+
artifact: refetchArtifact,
|
|
141
|
+
getState: () => displayData as _Data | null,
|
|
142
|
+
// Use the observer's own variable state so cursor history is preserved
|
|
143
|
+
// across page navigations without manual tracking in the hook.
|
|
144
|
+
getVariables: () =>
|
|
145
|
+
(paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
|
|
146
|
+
fetch: fetchFn,
|
|
147
|
+
fetchUpdate,
|
|
148
|
+
getSession: async () => session,
|
|
149
|
+
previousCursors: previousCursorsRef.current,
|
|
150
|
+
nextCursors: nextCursorsRef.current,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
|
|
155
|
+
loadNextPending: forwardPending,
|
|
156
|
+
loadPrevious: wrapLoad(setBackwardPending, handlers.loadPreviousPage),
|
|
157
|
+
loadPreviousPending: backwardPending,
|
|
158
|
+
pageInfo: refetchPath
|
|
159
|
+
? extractPageInfo(displayData as GraphQLObject, refetchPath)
|
|
160
|
+
: null,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (refetchArtifact.refetch!.method === 'offset') {
|
|
165
|
+
const handlers = offsetHandlers({
|
|
166
|
+
artifact: refetchArtifact,
|
|
167
|
+
getState: () => displayData as _Data | null,
|
|
168
|
+
getVariables: () =>
|
|
169
|
+
(paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
|
|
170
|
+
storeName: refetchArtifact.name,
|
|
171
|
+
fetch: fetchFn,
|
|
172
|
+
fetchUpdate: async (args: any, updates = ['append']) =>
|
|
173
|
+
fetchUpdate(args, updates) as any,
|
|
174
|
+
getSession: async () => session,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
|
|
179
|
+
loadNextPending: forwardPending,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null
|
|
184
|
+
}, [refetchArtifact, paginationObserver, displayData, session, forwardPending, backwardPending])
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
...handle,
|
|
188
|
+
variables,
|
|
189
|
+
data: displayData,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useIsMountedRef(): { current: boolean } {
|
|
4
|
+
const isMountedRef = useRef(true)
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
isMountedRef.current = true
|
|
8
|
+
return () => {
|
|
9
|
+
isMountedRef.current = false
|
|
10
|
+
}
|
|
11
|
+
}, [])
|
|
12
|
+
|
|
13
|
+
return isMountedRef
|
|
14
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MutationArtifact,
|
|
3
|
+
GraphQLObject,
|
|
4
|
+
QueryResult,
|
|
5
|
+
GraphQLVariables,
|
|
6
|
+
} from 'houdini/runtime'
|
|
7
|
+
|
|
8
|
+
import { useSession } from '../routing/Router.js'
|
|
9
|
+
import { useDocumentStore } from './useDocumentStore.js'
|
|
10
|
+
|
|
11
|
+
export type MutationHandler<_Result, _Input, _Optimistic extends GraphQLObject> = (args: {
|
|
12
|
+
variables: _Input
|
|
13
|
+
|
|
14
|
+
metadata?: App.Metadata
|
|
15
|
+
fetch?: typeof globalThis.fetch
|
|
16
|
+
optimisticResponse?: _Optimistic
|
|
17
|
+
abortController?: AbortController
|
|
18
|
+
}) => Promise<void>
|
|
19
|
+
|
|
20
|
+
export function useMutation<
|
|
21
|
+
_Result extends GraphQLObject,
|
|
22
|
+
_Input extends GraphQLVariables,
|
|
23
|
+
_Optimistic extends GraphQLObject,
|
|
24
|
+
>({
|
|
25
|
+
artifact,
|
|
26
|
+
}: {
|
|
27
|
+
artifact: MutationArtifact
|
|
28
|
+
}): [MutationHandler<_Result, _Input, _Optimistic>, boolean] {
|
|
29
|
+
// build the live document we'll use to send values
|
|
30
|
+
const [storeValue, observer] = useDocumentStore<_Result, _Input>({ artifact })
|
|
31
|
+
|
|
32
|
+
// grab the pending state from the document store
|
|
33
|
+
const pending = storeValue.fetching
|
|
34
|
+
|
|
35
|
+
// grab the current session value
|
|
36
|
+
const [session] = useSession()
|
|
37
|
+
|
|
38
|
+
// sending the mutation just means invoking the observer's send method
|
|
39
|
+
const mutate: MutationHandler<_Result, _Input, _Optimistic> = async ({
|
|
40
|
+
metadata,
|
|
41
|
+
fetch,
|
|
42
|
+
variables,
|
|
43
|
+
abortController,
|
|
44
|
+
...mutationConfig
|
|
45
|
+
}) => {
|
|
46
|
+
const result = await observer.send({
|
|
47
|
+
variables,
|
|
48
|
+
metadata,
|
|
49
|
+
session,
|
|
50
|
+
abortController,
|
|
51
|
+
stuff: {
|
|
52
|
+
...mutationConfig,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (result.errors && result.errors.length > 0) {
|
|
57
|
+
const err = new RuntimeGraphQLError(
|
|
58
|
+
result.errors.map((error) => error.message).join('. ')
|
|
59
|
+
)
|
|
60
|
+
err.raw = result.errors
|
|
61
|
+
throw err
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [mutate, pending]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class RuntimeGraphQLError extends Error {
|
|
69
|
+
raw: QueryResult['errors'] = []
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GraphQLObject, GraphQLVariables, QueryArtifact } from 'houdini/runtime'
|
|
2
|
+
|
|
3
|
+
import type { UseQueryConfig } from './useQueryHandle.js'
|
|
4
|
+
import { useQueryHandle } from './useQueryHandle.js'
|
|
5
|
+
|
|
6
|
+
export function useQuery<
|
|
7
|
+
_Artifact extends QueryArtifact,
|
|
8
|
+
_Data extends GraphQLObject = GraphQLObject,
|
|
9
|
+
>(document: { artifact: _Artifact }, variables: any = null, config: UseQueryConfig = {}): _Data {
|
|
10
|
+
const { data } = useQueryHandle<_Artifact, _Data>(document, variables, config)
|
|
11
|
+
return data as unknown as _Data
|
|
12
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createLRUCache } from 'houdini/runtime'
|
|
2
|
+
import type { GraphQLObject, CachePolicies, QueryArtifact, GraphQLVariables } from 'houdini/runtime'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
import { useClient } from '../routing/index.js'
|
|
6
|
+
import type { DocumentHandle } from './useDocumentHandle.js'
|
|
7
|
+
import { useDocumentHandle } from './useDocumentHandle.js'
|
|
8
|
+
import { useIsMountedRef } from './useIsMounted.js'
|
|
9
|
+
|
|
10
|
+
// Suspense requires a way to throw a promise that resolves to a place
|
|
11
|
+
// we can put when we go back on a susequent render. This means that we have to have
|
|
12
|
+
// a stable way to identify _this_ useQueryHandle called.
|
|
13
|
+
// For now, we're going to compute an identifier based on the name of the artifact
|
|
14
|
+
// and the variables that we were given.
|
|
15
|
+
//
|
|
16
|
+
// - If we have a cached promise that's pending, we should just throw that promise
|
|
17
|
+
// - If we have a cached promise that's been resolved, we should return that value
|
|
18
|
+
//
|
|
19
|
+
// When the Component unmounts, we need to remove the entry from the cache (so we can load again)
|
|
20
|
+
|
|
21
|
+
const promiseCache = createLRUCache<QuerySuspenseUnit>()
|
|
22
|
+
type QuerySuspenseUnit<
|
|
23
|
+
_Data extends GraphQLObject = GraphQLObject,
|
|
24
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
25
|
+
> = {
|
|
26
|
+
resolve: () => void
|
|
27
|
+
resolved?: DocumentHandle<QueryArtifact, _Data, _Input>
|
|
28
|
+
then: (val: any) => any
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useQueryHandle<
|
|
32
|
+
_Artifact extends QueryArtifact,
|
|
33
|
+
_Data extends GraphQLObject = GraphQLObject,
|
|
34
|
+
_Input extends GraphQLVariables = GraphQLVariables,
|
|
35
|
+
>(
|
|
36
|
+
{ artifact }: { artifact: QueryArtifact },
|
|
37
|
+
variables: any = null,
|
|
38
|
+
config: UseQueryConfig = {}
|
|
39
|
+
): any {
|
|
40
|
+
// figure out the identifier so we know what to look for
|
|
41
|
+
const identifier = queryIdentifier({ artifact, variables, config })
|
|
42
|
+
|
|
43
|
+
// see if we have an entry in the cache for the identifier
|
|
44
|
+
const suspenseValue = promiseCache.get(identifier)
|
|
45
|
+
|
|
46
|
+
const client = useClient()
|
|
47
|
+
|
|
48
|
+
const isMountedRef = useIsMountedRef()
|
|
49
|
+
|
|
50
|
+
// hold onto an observer we'll use
|
|
51
|
+
const [observer] = React.useState(
|
|
52
|
+
client.observe<_Data, _Input>({
|
|
53
|
+
artifact,
|
|
54
|
+
initialValue: (suspenseValue?.resolved?.data ?? {}) as _Data,
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// a ref flag we'll enable before throwing so that we don't update while suspend
|
|
59
|
+
const suspenseTracker = React.useRef(false)
|
|
60
|
+
|
|
61
|
+
// a stable box to put the store's value
|
|
62
|
+
const box = React.useRef(observer.state)
|
|
63
|
+
|
|
64
|
+
// a stable subscribe function for the document store
|
|
65
|
+
const subscribe: any = React.useCallback(
|
|
66
|
+
(fn: () => void) => {
|
|
67
|
+
return observer.subscribe((val) => {
|
|
68
|
+
box.current = val
|
|
69
|
+
if (isMountedRef.current && !suspenseTracker.current) {
|
|
70
|
+
fn()
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
[observer, isMountedRef.current]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// get a safe reference to the cache
|
|
78
|
+
const storeValue = React.useSyncExternalStore(subscribe, () => box.current)
|
|
79
|
+
|
|
80
|
+
// compute the imperative handle for this artifact
|
|
81
|
+
const handle = useDocumentHandle<_Artifact, _Data, _Input>({
|
|
82
|
+
artifact,
|
|
83
|
+
observer,
|
|
84
|
+
storeValue,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// if the identifier changes, we need to remove the old identifier from the
|
|
88
|
+
// suspense cache
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
return () => {
|
|
91
|
+
promiseCache.delete(identifier)
|
|
92
|
+
}
|
|
93
|
+
}, [identifier])
|
|
94
|
+
|
|
95
|
+
// when we unmount, we need to clean up
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
return () => {
|
|
98
|
+
observer.cleanup()
|
|
99
|
+
}
|
|
100
|
+
}, [observer])
|
|
101
|
+
|
|
102
|
+
// if the promise has resolved, let's use that for our first render
|
|
103
|
+
const result = storeValue.data
|
|
104
|
+
|
|
105
|
+
if (!suspenseValue) {
|
|
106
|
+
// we are going to cache the promise and then throw it
|
|
107
|
+
// when it resolves the cached value will be updated
|
|
108
|
+
// and it will be picked up in the next render
|
|
109
|
+
let resolve: () => void = () => {}
|
|
110
|
+
let reject: (reason?: any) => void = () => {}
|
|
111
|
+
const loadPromise = new Promise<void>((res, rej) => {
|
|
112
|
+
resolve = res
|
|
113
|
+
reject = rej
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const suspenseUnit: QuerySuspenseUnit<_Data, _Input> = {
|
|
117
|
+
// biome-ignore lint/suspicious/noThenProperty: suspense protocol requires a thenable
|
|
118
|
+
then: loadPromise.then.bind(loadPromise),
|
|
119
|
+
resolve,
|
|
120
|
+
// @ts-expect-error
|
|
121
|
+
variables,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
promiseCache.set(identifier, suspenseUnit as QuerySuspenseUnit)
|
|
125
|
+
|
|
126
|
+
// the suspense unit gives react something to hold onto
|
|
127
|
+
// and it acts as a place for us to register a callback on
|
|
128
|
+
// send to update the cache before resolving the suspense
|
|
129
|
+
handle
|
|
130
|
+
.fetch({
|
|
131
|
+
variables,
|
|
132
|
+
// @ts-expect-error: this is actually allowed... 🤫
|
|
133
|
+
stuff: {
|
|
134
|
+
silenceLoading: true,
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
.then((value) => {
|
|
138
|
+
// the final value
|
|
139
|
+
suspenseUnit.resolved = {
|
|
140
|
+
...handle,
|
|
141
|
+
data: value.data,
|
|
142
|
+
partial: value.partial,
|
|
143
|
+
artifact,
|
|
144
|
+
} as unknown as DocumentHandle<QueryArtifact, _Data, _Input>
|
|
145
|
+
|
|
146
|
+
suspenseUnit.resolve()
|
|
147
|
+
})
|
|
148
|
+
.catch((err) => {
|
|
149
|
+
promiseCache.delete(identifier)
|
|
150
|
+
reject(err)
|
|
151
|
+
})
|
|
152
|
+
suspenseTracker.current = true
|
|
153
|
+
throw suspenseUnit
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// if the promise is still pending, we're still waiting
|
|
157
|
+
if (!result && suspenseValue && !suspenseValue.resolved) {
|
|
158
|
+
suspenseTracker.current = true
|
|
159
|
+
throw suspenseValue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// make sure we prefer the latest store value instead of the initial version we loaded on mount
|
|
163
|
+
if (!result && suspenseValue?.resolved) {
|
|
164
|
+
return suspenseValue.resolved as unknown as DocumentHandle<_Artifact, _Data, _Input>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...handle,
|
|
169
|
+
variables: storeValue.variables,
|
|
170
|
+
data: result,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type UseQueryConfig = {
|
|
175
|
+
policy?: CachePolicies
|
|
176
|
+
metadata?: App.Metadata
|
|
177
|
+
fetchKey?: any
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function queryIdentifier(args: {
|
|
181
|
+
artifact: QueryArtifact
|
|
182
|
+
fetchKey?: number
|
|
183
|
+
variables: {}
|
|
184
|
+
config: UseQueryConfig
|
|
185
|
+
}): string {
|
|
186
|
+
// make sure there is always a fetchKey
|
|
187
|
+
args.fetchKey ??= 0
|
|
188
|
+
|
|
189
|
+
// pull the common stuff out
|
|
190
|
+
const { artifact, variables, fetchKey } = args
|
|
191
|
+
|
|
192
|
+
// a query identifier is a mix of its name, arguments, and the fetch key
|
|
193
|
+
return [artifact.name, JSON.stringify(variables), fetchKey].join('@@')
|
|
194
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SubscriptionArtifact, GraphQLObject, GraphQLVariables } from 'houdini/runtime'
|
|
2
|
+
|
|
3
|
+
import { useSubscriptionHandle } from './useSubscriptionHandle.js'
|
|
4
|
+
|
|
5
|
+
// a hook to subscribe to a subscription artifact
|
|
6
|
+
export function useSubscription<_Result extends GraphQLObject, _Input extends GraphQLVariables>(
|
|
7
|
+
document: { artifact: SubscriptionArtifact },
|
|
8
|
+
variables: _Input
|
|
9
|
+
) {
|
|
10
|
+
const { data } = useSubscriptionHandle(document, variables)
|
|
11
|
+
return data
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SubscriptionArtifact,
|
|
3
|
+
GraphQLObject,
|
|
4
|
+
GraphQLVariables,
|
|
5
|
+
GraphQLError,
|
|
6
|
+
} from 'houdini/runtime'
|
|
7
|
+
|
|
8
|
+
import { useDocumentSubscription } from './useDocumentSubscription.js'
|
|
9
|
+
|
|
10
|
+
export type SubscriptionHandle<_Result extends GraphQLObject, _Input extends GraphQLVariables> = {
|
|
11
|
+
data: _Result | null
|
|
12
|
+
errors: GraphQLError[] | null
|
|
13
|
+
variables: _Input
|
|
14
|
+
listen: (args: { variables?: _Input }) => void
|
|
15
|
+
unlisten: () => void
|
|
16
|
+
fetching: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// a hook to subscribe to a subscription artifact
|
|
20
|
+
export function useSubscriptionHandle<
|
|
21
|
+
_Result extends GraphQLObject,
|
|
22
|
+
_Input extends GraphQLVariables,
|
|
23
|
+
>({ artifact }: { artifact: SubscriptionArtifact }, variables: _Input) {
|
|
24
|
+
// a subscription is basically just a live document
|
|
25
|
+
const [storeValue, observer] = useDocumentSubscription({
|
|
26
|
+
artifact,
|
|
27
|
+
variables,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
data: storeValue.data,
|
|
32
|
+
errors: storeValue.errors,
|
|
33
|
+
fetching: storeValue.fetching,
|
|
34
|
+
variables,
|
|
35
|
+
unlisten: observer.cleanup,
|
|
36
|
+
listen: observer.send,
|
|
37
|
+
}
|
|
38
|
+
}
|