houdini-react 2.0.0-next.4 → 2.0.0-next.41
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 +178 -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,178 @@
|
|
|
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
|
+
const fragmentData = useFragment<_Data, _ReferenceType, _Input>(reference, document)
|
|
28
|
+
const { variables } = fragmentReference<_Data, _Input, _ReferenceType>(reference, document)
|
|
29
|
+
|
|
30
|
+
const client = useClient()
|
|
31
|
+
const [session] = useSession()
|
|
32
|
+
|
|
33
|
+
const [forwardPending, setForwardPending] = React.useState(false)
|
|
34
|
+
const [backwardPending, setBackwardPending] = React.useState(false)
|
|
35
|
+
|
|
36
|
+
const previousCursorsRef = React.useRef<(string | null)[]>([])
|
|
37
|
+
const nextCursorsRef = React.useRef<(string | null)[]>([])
|
|
38
|
+
|
|
39
|
+
const refetchArtifact = document.refetchArtifact as QueryArtifact | undefined
|
|
40
|
+
const refetchPath = refetchArtifact?.refetch?.path
|
|
41
|
+
|
|
42
|
+
const paginationObserver = React.useMemo(() => {
|
|
43
|
+
if (!refetchArtifact?.refetch?.paginated) return null
|
|
44
|
+
return client.observe<_Data, _Input>({ artifact: refetchArtifact })
|
|
45
|
+
}, [refetchArtifact?.name])
|
|
46
|
+
|
|
47
|
+
const isSinglePage = refetchArtifact?.refetch?.mode === 'SinglePage'
|
|
48
|
+
|
|
49
|
+
// Subscribe to the pagination observer so the component re-renders when a new page lands.
|
|
50
|
+
// For SinglePage we pass disablePartial so partial cache hits (entity found but connection
|
|
51
|
+
// not yet fetched) are never resolved back to the observer — we go straight to the network
|
|
52
|
+
// and update the observer only once we have a complete page.
|
|
53
|
+
const subscribeToObserver = React.useCallback(
|
|
54
|
+
(fn: () => void) => paginationObserver?.subscribe(() => fn()) ?? (() => {}),
|
|
55
|
+
[paginationObserver]
|
|
56
|
+
)
|
|
57
|
+
const getObserverSnapshot = React.useCallback(
|
|
58
|
+
() => paginationObserver?.state.data ?? null,
|
|
59
|
+
[paginationObserver]
|
|
60
|
+
)
|
|
61
|
+
const paginationData = React.useSyncExternalStore(
|
|
62
|
+
subscribeToObserver,
|
|
63
|
+
getObserverSnapshot,
|
|
64
|
+
getObserverSnapshot
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const paginationEntityData = React.useMemo<_Data | null>(() => {
|
|
68
|
+
if (!paginationData || !refetchArtifact?.selection?.fields) return null
|
|
69
|
+
const rootField = Object.keys(refetchArtifact.selection.fields)[0]
|
|
70
|
+
if (!rootField) return null
|
|
71
|
+
return (paginationData as any)[rootField] ?? null
|
|
72
|
+
}, [paginationData, refetchArtifact])
|
|
73
|
+
|
|
74
|
+
const displayData =
|
|
75
|
+
isSinglePage && paginationEntityData !== null ? paginationEntityData : fragmentData
|
|
76
|
+
|
|
77
|
+
const wrapLoad = <_Result>(
|
|
78
|
+
setLoading: (val: boolean) => void,
|
|
79
|
+
fn: (value: any) => Promise<_Result>
|
|
80
|
+
) => {
|
|
81
|
+
return async (value: any) => {
|
|
82
|
+
setLoading(true)
|
|
83
|
+
let err: Error | null = null
|
|
84
|
+
let result: _Result | null = null
|
|
85
|
+
try {
|
|
86
|
+
result = await fn(value)
|
|
87
|
+
} catch (e) {
|
|
88
|
+
err = e as Error
|
|
89
|
+
}
|
|
90
|
+
setLoading(false)
|
|
91
|
+
if (err && err.name !== 'AbortError') throw err
|
|
92
|
+
return result
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handle = React.useMemo(() => {
|
|
97
|
+
if (!refetchArtifact?.refetch?.paginated || !paginationObserver) return null
|
|
98
|
+
|
|
99
|
+
const fetchFn: FetchFn<_Data, _Input> = (args) => {
|
|
100
|
+
return paginationObserver.send({
|
|
101
|
+
...args,
|
|
102
|
+
session,
|
|
103
|
+
stuff: { silenceLoading: true },
|
|
104
|
+
cacheParams: {
|
|
105
|
+
disableSubscriptions: true,
|
|
106
|
+
// Suppress partial cache hits so an in-flight forward navigation never
|
|
107
|
+
// briefly resolves with an entity that is missing its connection field.
|
|
108
|
+
// Full cache hits (partial: false) still resolve, so backward navigation
|
|
109
|
+
// continues to be served instantly from cache.
|
|
110
|
+
disablePartial: true,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fetchUpdate = (args: any, updates: string[]) => {
|
|
116
|
+
return paginationObserver.send({
|
|
117
|
+
...args,
|
|
118
|
+
cacheParams: {
|
|
119
|
+
...args?.cacheParams,
|
|
120
|
+
disableSubscriptions: true,
|
|
121
|
+
applyUpdates: updates,
|
|
122
|
+
},
|
|
123
|
+
session,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (refetchArtifact.refetch!.method === 'cursor') {
|
|
128
|
+
const handlers = cursorHandlers<_Data, _Input>({
|
|
129
|
+
artifact: refetchArtifact,
|
|
130
|
+
getState: () => displayData as _Data | null,
|
|
131
|
+
getVariables: () =>
|
|
132
|
+
(paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
|
|
133
|
+
fetch: fetchFn,
|
|
134
|
+
fetchUpdate,
|
|
135
|
+
getSession: async () => session,
|
|
136
|
+
previousCursors: previousCursorsRef.current,
|
|
137
|
+
nextCursors: nextCursorsRef.current,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
|
|
142
|
+
loadNextPending: forwardPending,
|
|
143
|
+
loadPrevious: wrapLoad(setBackwardPending, handlers.loadPreviousPage),
|
|
144
|
+
loadPreviousPending: backwardPending,
|
|
145
|
+
pageInfo: refetchPath
|
|
146
|
+
? extractPageInfo(displayData as GraphQLObject, refetchPath)
|
|
147
|
+
: null,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (refetchArtifact.refetch!.method === 'offset') {
|
|
152
|
+
const handlers = offsetHandlers({
|
|
153
|
+
artifact: refetchArtifact,
|
|
154
|
+
getState: () => displayData as _Data | null,
|
|
155
|
+
getVariables: () =>
|
|
156
|
+
(paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
|
|
157
|
+
storeName: refetchArtifact.name,
|
|
158
|
+
fetch: fetchFn,
|
|
159
|
+
fetchUpdate: async (args: any, updates = ['append']) =>
|
|
160
|
+
fetchUpdate(args, updates) as any,
|
|
161
|
+
getSession: async () => session,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
loadNext: wrapLoad(setForwardPending, handlers.loadNextPage),
|
|
166
|
+
loadNextPending: forwardPending,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null
|
|
171
|
+
}, [refetchArtifact, paginationObserver, displayData, session, forwardPending, backwardPending])
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...handle,
|
|
175
|
+
variables,
|
|
176
|
+
data: displayData,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -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
|
+
}
|