houdini-react 2.0.0-next.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/bin/houdini-react +173 -0
  2. package/package.json +63 -39
  3. package/postInstall.js +360 -0
  4. package/runtime/Link.tsx +50 -0
  5. package/runtime/client.ts +5 -0
  6. package/runtime/clientPlugin.ts +17 -0
  7. package/runtime/componentFields.ts +79 -0
  8. package/runtime/escape.ts +15 -0
  9. package/runtime/hooks/index.ts +17 -0
  10. package/runtime/hooks/recycleNodesInto.ts +60 -0
  11. package/runtime/hooks/useDeepCompareEffect.ts +93 -0
  12. package/runtime/hooks/useDocumentHandle.ts +243 -0
  13. package/runtime/hooks/useDocumentStore.ts +112 -0
  14. package/runtime/hooks/useDocumentSubscription.ts +65 -0
  15. package/runtime/hooks/useFragment.ts +269 -0
  16. package/runtime/hooks/useFragmentHandle.ts +274 -0
  17. package/runtime/hooks/useIsMounted.ts +14 -0
  18. package/runtime/hooks/useLogoutForm.tsx +60 -0
  19. package/runtime/hooks/useMutation.ts +80 -0
  20. package/runtime/hooks/useMutationForm.tsx +163 -0
  21. package/runtime/hooks/useQuery.ts +12 -0
  22. package/runtime/hooks/useQueryHandle.ts +194 -0
  23. package/runtime/hooks/useSubscription.ts +12 -0
  24. package/runtime/hooks/useSubscriptionHandle.ts +42 -0
  25. package/runtime/hydration.tsx +186 -0
  26. package/runtime/index.tsx +77 -0
  27. package/runtime/login.ts +22 -0
  28. package/runtime/manifest.ts +6 -0
  29. package/runtime/mock.ts +9 -0
  30. package/runtime/package.json +1 -0
  31. package/runtime/resolve-href.ts +151 -0
  32. package/runtime/routes.ts +93 -0
  33. package/runtime/routing/Router.tsx +1101 -0
  34. package/runtime/routing/cache.ts +57 -0
  35. package/runtime/routing/errors.tsx +145 -0
  36. package/runtime/routing/index.ts +17 -0
  37. package/runtime/testing.tsx +163 -0
  38. package/runtime/tsconfig.json +39 -0
  39. package/server/index.d.ts +1 -0
  40. package/server/index.js +4 -0
  41. package/server/react-streaming.d.js +0 -0
  42. package/vite/index.d.ts +3 -0
  43. package/vite/index.js +397 -0
  44. package/vite/strip-headers.d.ts +1 -0
  45. package/vite/strip-headers.js +35 -0
  46. package/vite/transform.d.ts +13 -0
  47. package/vite/transform.js +105 -0
  48. package/README.md +0 -36
  49. package/build/plugin/codegen/entries/documentWrappers.d.ts +0 -6
  50. package/build/plugin/codegen/entries/fallbacks.d.ts +0 -5
  51. package/build/plugin/codegen/entries/index.d.ts +0 -16
  52. package/build/plugin/codegen/entries/pages.d.ts +0 -2
  53. package/build/plugin/codegen/index.d.ts +0 -17
  54. package/build/plugin/codegen/manifest.d.ts +0 -5
  55. package/build/plugin/codegen/render.d.ts +0 -7
  56. package/build/plugin/codegen/router.d.ts +0 -7
  57. package/build/plugin/codegen/typeRoot.d.ts +0 -5
  58. package/build/plugin/config.d.ts +0 -4
  59. package/build/plugin/dedent.d.ts +0 -1
  60. package/build/plugin/extract.d.ts +0 -6
  61. package/build/plugin/index.d.ts +0 -5
  62. package/build/plugin/state.d.ts +0 -3
  63. package/build/plugin/transform.d.ts +0 -6
  64. package/build/plugin/vite.d.ts +0 -27
  65. package/build/plugin-cjs/index.js +0 -90125
  66. package/build/plugin-cjs/package.json +0 -1
  67. package/build/plugin-esm/index.js +0 -90121
  68. package/build/runtime/client.d.ts +0 -3
  69. package/build/runtime/clientPlugin.d.ts +0 -3
  70. package/build/runtime/componentFields.d.ts +0 -9
  71. package/build/runtime/hooks/index.d.ts +0 -8
  72. package/build/runtime/hooks/useDeepCompareEffect.d.ts +0 -35
  73. package/build/runtime/hooks/useDocumentHandle.d.ts +0 -36
  74. package/build/runtime/hooks/useDocumentStore.d.ts +0 -11
  75. package/build/runtime/hooks/useDocumentSubscription.d.ts +0 -11
  76. package/build/runtime/hooks/useFragment.d.ts +0 -16
  77. package/build/runtime/hooks/useFragmentHandle.d.ts +0 -8
  78. package/build/runtime/hooks/useIsMounted.d.ts +0 -3
  79. package/build/runtime/hooks/useMutation.d.ts +0 -14
  80. package/build/runtime/hooks/useQuery.d.ts +0 -5
  81. package/build/runtime/hooks/useQueryHandle.d.ts +0 -10
  82. package/build/runtime/hooks/useSubscription.d.ts +0 -4
  83. package/build/runtime/hooks/useSubscriptionHandle.d.ts +0 -25
  84. package/build/runtime/index.d.ts +0 -14
  85. package/build/runtime/manifest.d.ts +0 -3
  86. package/build/runtime/routing/Router.d.ts +0 -62
  87. package/build/runtime/routing/cache.d.ts +0 -7
  88. package/build/runtime/routing/hooks.d.ts +0 -40
  89. package/build/runtime/routing/index.d.ts +0 -3
  90. package/build/runtime-cjs/client.d.ts +0 -3
  91. package/build/runtime-cjs/client.js +0 -25
  92. package/build/runtime-cjs/clientPlugin.d.ts +0 -3
  93. package/build/runtime-cjs/clientPlugin.js +0 -37
  94. package/build/runtime-cjs/componentFields.d.ts +0 -9
  95. package/build/runtime-cjs/componentFields.js +0 -83
  96. package/build/runtime-cjs/hooks/index.d.ts +0 -8
  97. package/build/runtime-cjs/hooks/index.js +0 -45
  98. package/build/runtime-cjs/hooks/useDeepCompareEffect.d.ts +0 -35
  99. package/build/runtime-cjs/hooks/useDeepCompareEffect.js +0 -76
  100. package/build/runtime-cjs/hooks/useDocumentHandle.d.ts +0 -36
  101. package/build/runtime-cjs/hooks/useDocumentHandle.js +0 -177
  102. package/build/runtime-cjs/hooks/useDocumentStore.d.ts +0 -11
  103. package/build/runtime-cjs/hooks/useDocumentStore.js +0 -76
  104. package/build/runtime-cjs/hooks/useDocumentSubscription.d.ts +0 -11
  105. package/build/runtime-cjs/hooks/useDocumentSubscription.js +0 -76
  106. package/build/runtime-cjs/hooks/useFragment.d.ts +0 -16
  107. package/build/runtime-cjs/hooks/useFragment.js +0 -102
  108. package/build/runtime-cjs/hooks/useFragmentHandle.d.ts +0 -8
  109. package/build/runtime-cjs/hooks/useFragmentHandle.js +0 -47
  110. package/build/runtime-cjs/hooks/useIsMounted.d.ts +0 -3
  111. package/build/runtime-cjs/hooks/useIsMounted.js +0 -38
  112. package/build/runtime-cjs/hooks/useMutation.d.ts +0 -14
  113. package/build/runtime-cjs/hooks/useMutation.js +0 -67
  114. package/build/runtime-cjs/hooks/useQuery.d.ts +0 -5
  115. package/build/runtime-cjs/hooks/useQuery.js +0 -32
  116. package/build/runtime-cjs/hooks/useQueryHandle.d.ts +0 -10
  117. package/build/runtime-cjs/hooks/useQueryHandle.js +0 -131
  118. package/build/runtime-cjs/hooks/useSubscription.d.ts +0 -4
  119. package/build/runtime-cjs/hooks/useSubscription.js +0 -32
  120. package/build/runtime-cjs/hooks/useSubscriptionHandle.d.ts +0 -25
  121. package/build/runtime-cjs/hooks/useSubscriptionHandle.js +0 -42
  122. package/build/runtime-cjs/index.d.ts +0 -14
  123. package/build/runtime-cjs/index.js +0 -88
  124. package/build/runtime-cjs/manifest.d.ts +0 -3
  125. package/build/runtime-cjs/manifest.js +0 -25
  126. package/build/runtime-cjs/package.json +0 -1
  127. package/build/runtime-cjs/routing/Router.d.ts +0 -62
  128. package/build/runtime-cjs/routing/Router.js +0 -540
  129. package/build/runtime-cjs/routing/cache.d.ts +0 -7
  130. package/build/runtime-cjs/routing/cache.js +0 -61
  131. package/build/runtime-cjs/routing/hooks.d.ts +0 -40
  132. package/build/runtime-cjs/routing/hooks.js +0 -93
  133. package/build/runtime-cjs/routing/index.d.ts +0 -3
  134. package/build/runtime-cjs/routing/index.js +0 -33
  135. package/build/runtime-esm/client.d.ts +0 -3
  136. package/build/runtime-esm/client.js +0 -5
  137. package/build/runtime-esm/clientPlugin.d.ts +0 -3
  138. package/build/runtime-esm/clientPlugin.js +0 -17
  139. package/build/runtime-esm/componentFields.d.ts +0 -9
  140. package/build/runtime-esm/componentFields.js +0 -59
  141. package/build/runtime-esm/hooks/index.d.ts +0 -8
  142. package/build/runtime-esm/hooks/index.js +0 -15
  143. package/build/runtime-esm/hooks/useDeepCompareEffect.d.ts +0 -35
  144. package/build/runtime-esm/hooks/useDeepCompareEffect.js +0 -41
  145. package/build/runtime-esm/hooks/useDocumentHandle.d.ts +0 -36
  146. package/build/runtime-esm/hooks/useDocumentHandle.js +0 -143
  147. package/build/runtime-esm/hooks/useDocumentStore.d.ts +0 -11
  148. package/build/runtime-esm/hooks/useDocumentStore.js +0 -42
  149. package/build/runtime-esm/hooks/useDocumentSubscription.d.ts +0 -11
  150. package/build/runtime-esm/hooks/useDocumentSubscription.js +0 -42
  151. package/build/runtime-esm/hooks/useFragment.d.ts +0 -16
  152. package/build/runtime-esm/hooks/useFragment.js +0 -67
  153. package/build/runtime-esm/hooks/useFragmentHandle.d.ts +0 -8
  154. package/build/runtime-esm/hooks/useFragmentHandle.js +0 -23
  155. package/build/runtime-esm/hooks/useIsMounted.d.ts +0 -3
  156. package/build/runtime-esm/hooks/useIsMounted.js +0 -14
  157. package/build/runtime-esm/hooks/useMutation.d.ts +0 -14
  158. package/build/runtime-esm/hooks/useMutation.js +0 -42
  159. package/build/runtime-esm/hooks/useQuery.d.ts +0 -5
  160. package/build/runtime-esm/hooks/useQuery.js +0 -8
  161. package/build/runtime-esm/hooks/useQueryHandle.d.ts +0 -10
  162. package/build/runtime-esm/hooks/useQueryHandle.js +0 -97
  163. package/build/runtime-esm/hooks/useSubscription.d.ts +0 -4
  164. package/build/runtime-esm/hooks/useSubscription.js +0 -8
  165. package/build/runtime-esm/hooks/useSubscriptionHandle.d.ts +0 -25
  166. package/build/runtime-esm/hooks/useSubscriptionHandle.js +0 -18
  167. package/build/runtime-esm/index.d.ts +0 -14
  168. package/build/runtime-esm/index.js +0 -48
  169. package/build/runtime-esm/manifest.d.ts +0 -3
  170. package/build/runtime-esm/manifest.js +0 -5
  171. package/build/runtime-esm/routing/Router.d.ts +0 -62
  172. package/build/runtime-esm/routing/Router.js +0 -499
  173. package/build/runtime-esm/routing/cache.d.ts +0 -7
  174. package/build/runtime-esm/routing/cache.js +0 -36
  175. package/build/runtime-esm/routing/hooks.d.ts +0 -40
  176. package/build/runtime-esm/routing/hooks.js +0 -53
  177. package/build/runtime-esm/routing/index.d.ts +0 -3
  178. package/build/runtime-esm/routing/index.js +0 -6
  179. package/build/server/index.d.ts +0 -1
  180. package/build/server-cjs/index.js +0 -28
  181. package/build/server-cjs/package.json +0 -1
  182. package/build/server-esm/index.js +0 -4
  183. package/build/server-esm/package.json +0 -1
  184. /package/{build/plugin-esm → server}/package.json +0 -0
  185. /package/{build/runtime-esm → vite}/package.json +0 -0
@@ -0,0 +1,80 @@
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
+ type MutationConfig<_Optimistic extends GraphQLObject> = {
12
+ metadata?: App.Metadata
13
+ fetch?: typeof globalThis.fetch
14
+ optimisticResponse?: _Optimistic
15
+ abortController?: AbortController
16
+ }
17
+
18
+ // When the mutation has no variables its $input is `null | undefined`, so requiring a
19
+ // `variables` key would force callers to write `{ variables: undefined }`. In that case the
20
+ // whole argument is optional and the handler can be invoked as `mutate()`; otherwise
21
+ // `variables` is required.
22
+ export type MutationHandler<_Result, _Input, _Optimistic extends GraphQLObject> = [
23
+ undefined,
24
+ ] extends [_Input]
25
+ ? (args?: { variables?: _Input } & MutationConfig<_Optimistic>) => Promise<_Result>
26
+ : (args: { variables: _Input } & MutationConfig<_Optimistic>) => Promise<_Result>
27
+
28
+ export function useMutation<
29
+ _Result extends GraphQLObject,
30
+ _Input extends GraphQLVariables,
31
+ _Optimistic extends GraphQLObject,
32
+ >({
33
+ artifact,
34
+ }: {
35
+ artifact: MutationArtifact
36
+ }): [MutationHandler<_Result, _Input, _Optimistic>, boolean] {
37
+ // build the live document we'll use to send values
38
+ const [storeValue, observer] = useDocumentStore<_Result, _Input>({ artifact })
39
+
40
+ // grab the pending state from the document store
41
+ const pending = storeValue.fetching
42
+
43
+ // grab the current session value
44
+ const [session] = useSession()
45
+
46
+ // sending the mutation just means invoking the observer's send method
47
+ const mutate = async (
48
+ args: { variables?: _Input } & MutationConfig<_Optimistic> = {}
49
+ ): Promise<_Result> => {
50
+ const { metadata, fetch, variables, abortController, ...mutationConfig } = args
51
+ const result = await observer.send({
52
+ variables,
53
+ metadata,
54
+ session,
55
+ abortController,
56
+ stuff: {
57
+ ...mutationConfig,
58
+ },
59
+ })
60
+
61
+ if (result.errors && result.errors.length > 0) {
62
+ const err = new RuntimeGraphQLError(
63
+ result.errors.map((error) => error.message).join('. ')
64
+ )
65
+ err.raw = result.errors
66
+ throw err
67
+ }
68
+
69
+ // hand the data back so callers can use the mutation's result (e.g. `const data =
70
+ // await mutate(...)`), not just its side effects. Errors already threw above, so data is
71
+ // present on a successful mutation.
72
+ return result.data as _Result
73
+ }
74
+
75
+ return [mutate as MutationHandler<_Result, _Input, _Optimistic>, pending]
76
+ }
77
+
78
+ export class RuntimeGraphQLError extends Error {
79
+ raw: QueryResult['errors'] = []
80
+ }
@@ -0,0 +1,163 @@
1
+ import { getCurrentConfig } from '$houdini/runtime'
2
+ import type { MutationArtifact, GraphQLObject, GraphQLVariables } from 'houdini/runtime'
3
+ import { coerceFormData, interpolateRedirect } from 'houdini/runtime'
4
+ import React from 'react'
5
+
6
+ import { useSession, useRoute, useFormResult, useFormToken } from '../routing/Router.js'
7
+ import { useDocumentStore } from './useDocumentStore.js'
8
+
9
+ // a document with no variables still needs an InputObject shape for the coercer
10
+ const EMPTY_INPUT = { fields: {}, types: {}, defaults: {}, runtimeScalars: {} }
11
+
12
+ // data is null on the error path (and before any submission), populated on success
13
+ export type MutationFormState<_Result = any> = { data: _Result | null; errors: any } | null
14
+
15
+ export type UseMutationFormOptions<_Result = any> = {
16
+ // disambiguates multiple forms for the same mutation on one page, and is the key the
17
+ // server uses for the injected result. Defaults to the @endpoint(id:) or mutation name.
18
+ id?: string
19
+ // enhanced-path-only side effects (no-ops without JS, which is the correct degradation)
20
+ onSuccess?: (data: _Result) => void
21
+ onError?: (errors: any) => void
22
+ }
23
+
24
+ export type MutationForm<_Result = any> = {
25
+ // spread onto a <form>: a real string action (native POST before hydration) that the
26
+ // onSubmit intercepts after. Identical on server and client → clean hydration.
27
+ form: {
28
+ action: string
29
+ method: 'post'
30
+ onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
31
+ encType?: 'multipart/form-data'
32
+ }
33
+ // the hidden marker fields the no-JS POST needs; render inside the <form>
34
+ hidden: React.ReactNode
35
+ // an ergonomic alternative to `{...form}` + `hidden`: a <Form> that renders the form,
36
+ // injects the markers, and publishes `pending` to useMutationFormStatus() for any child.
37
+ Form: React.FC<React.PropsWithChildren<React.FormHTMLAttributes<HTMLFormElement>>>
38
+ // { data, errors } | null — seeded from the server's no-JS result, then the enhanced result
39
+ state: MutationFormState<_Result>
40
+ pending: boolean
41
+ }
42
+
43
+ // FormStatusContext carries the nearest <Form>'s pending state to useMutationFormStatus(),
44
+ // the no-prop-drilling ergonomic of React's useFormStatus (which only tracks function-action
45
+ // submissions, so it can't see our forms).
46
+ const FormStatusContext = React.createContext<{ pending: boolean }>({ pending: false })
47
+
48
+ /** useMutationFormStatus reads the pending state of the nearest <Form> from a child. */
49
+ export function useMutationFormStatus(): { pending: boolean } {
50
+ return React.useContext(FormStatusContext)
51
+ }
52
+
53
+ /**
54
+ * useMutationForm turns a mutation marked with @endpoint into a progressively-enhanced
55
+ * form. The returned `form`/`hidden` render a real string-action `<form>` that submits
56
+ * natively before/without JS; after hydration `onSubmit` runs the mutation client-side
57
+ * (optimistic + cache via the normal send) and navigates on success. `state` converges on
58
+ * the same `{ data, errors }` shape on both paths.
59
+ */
60
+ export function useMutationForm<
61
+ _Result extends GraphQLObject = GraphQLObject,
62
+ _Input extends GraphQLVariables = GraphQLVariables,
63
+ >(
64
+ document: { artifact: MutationArtifact },
65
+ opts: UseMutationFormOptions<_Result> = {}
66
+ ): MutationForm<_Result> {
67
+ const { artifact } = document
68
+ const [storeValue, observer] = useDocumentStore<_Result, _Input>({ artifact })
69
+ const [session] = useSession()
70
+ const { pathname, goto } = useRoute()
71
+
72
+ const formId = opts.id ?? artifact.endpoint?.id ?? artifact.name
73
+
74
+ // the session-bound CSRF token the server minted for this render
75
+ const csrfToken = useFormToken()
76
+
77
+ // seed from the server-injected result so the no-JS re-render and the enhanced path
78
+ // converge; null on a fresh form or once enhanced
79
+ const injected = useFormResult(formId)
80
+ const [state, setState] = React.useState<MutationFormState<_Result>>(injected)
81
+ const [submitting, setSubmitting] = React.useState(false)
82
+
83
+ const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
84
+ // only reached after hydration — before that the browser submits natively
85
+ event.preventDefault()
86
+ setSubmitting(true)
87
+ try {
88
+ const variables = coerceFormData(
89
+ new FormData(event.currentTarget),
90
+ artifact.input ?? EMPTY_INPUT,
91
+ getCurrentConfig(),
92
+ artifact.endpoint?.fields
93
+ ) as _Input
94
+ const result = await observer.send({ variables, session })
95
+ const errors = result.errors ?? null
96
+ setState({ data: result.data, errors })
97
+ if (errors && errors.length > 0) {
98
+ opts.onError?.(errors)
99
+ } else {
100
+ // @session writes are handled entirely by the sessionRelay client plugin during
101
+ // the send above: it relays the minted token to the auth endpoint (sets the
102
+ // cookie) and mirrors the result into local state, so useSession() updates without
103
+ // a reload. Nothing session-specific to do here.
104
+ opts.onSuccess?.(result.data as _Result)
105
+ // navigate to the same target the server would 303 to
106
+ const redirect = artifact.endpoint?.redirect
107
+ if (redirect) {
108
+ const target = interpolateRedirect(redirect, result.data)
109
+ if (target) {
110
+ goto(target)
111
+ }
112
+ }
113
+ }
114
+ } catch (error: any) {
115
+ const errors = error?.raw ?? [{ message: error?.message ?? String(error) }]
116
+ setState({ data: null, errors })
117
+ opts.onError?.(errors)
118
+ } finally {
119
+ setSubmitting(false)
120
+ }
121
+ }
122
+
123
+ const form: MutationForm<_Result>['form'] = {
124
+ action: pathname,
125
+ method: 'post',
126
+ onSubmit,
127
+ ...(artifact.endpoint?.multipart ? { encType: 'multipart/form-data' as const } : {}),
128
+ }
129
+
130
+ const hidden = (
131
+ <>
132
+ <input type="hidden" name="__houdini_form" value={artifact.name} />
133
+ <input type="hidden" name="__houdini_form_id" value={formId} />
134
+ {csrfToken && <input type="hidden" name="__houdini_csrf" value={csrfToken} />}
135
+ </>
136
+ )
137
+
138
+ const pending = submitting || storeValue.fetching
139
+
140
+ // the <Form> wrapper reads the latest props/pending from a ref so its component identity
141
+ // stays stable across renders — otherwise React would remount the form (and lose input
142
+ // focus) every time pending flips.
143
+ const live = React.useRef<{ form: typeof form; hidden: React.ReactNode; pending: boolean }>(
144
+ null as any
145
+ )
146
+ live.current = { form, hidden, pending }
147
+ const FormRef = React.useRef<MutationForm<_Result>['Form']>(null as any)
148
+ if (!FormRef.current) {
149
+ FormRef.current = ({ children, ...rest }) => {
150
+ const current = live.current
151
+ return (
152
+ <FormStatusContext.Provider value={{ pending: current.pending }}>
153
+ <form {...current.form} {...rest}>
154
+ {current.hidden}
155
+ {children}
156
+ </form>
157
+ </FormStatusContext.Provider>
158
+ )
159
+ }
160
+ }
161
+
162
+ return { form, hidden, Form: FormRef.current, state, pending }
163
+ }
@@ -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,42 @@
1
+ import type {
2
+ SubscriptionArtifact,
3
+ DocumentArtifact,
4
+ GraphQLObject,
5
+ GraphQLVariables,
6
+ GraphQLError,
7
+ } from 'houdini/runtime'
8
+
9
+ import { useDocumentSubscription } from './useDocumentSubscription.js'
10
+
11
+ export type SubscriptionHandle<_Result extends GraphQLObject, _Input extends GraphQLVariables> = {
12
+ data: _Result | null
13
+ errors: GraphQLError[] | null
14
+ variables: _Input
15
+ listen: (args: { variables?: _Input }) => void
16
+ unlisten: () => void
17
+ fetching: boolean
18
+ }
19
+
20
+ // a hook to subscribe to a subscription artifact
21
+ export function useSubscriptionHandle<
22
+ _Result extends GraphQLObject = GraphQLObject,
23
+ _Input extends GraphQLVariables = GraphQLVariables,
24
+ >(
25
+ { artifact }: { artifact: SubscriptionArtifact },
26
+ variables?: _Input
27
+ ): SubscriptionHandle<_Result, _Input> {
28
+ // a subscription is basically just a live document
29
+ const [storeValue, observer] = useDocumentSubscription<DocumentArtifact, _Result, _Input>({
30
+ artifact,
31
+ variables: variables as _Input,
32
+ })
33
+
34
+ return {
35
+ data: storeValue.data,
36
+ errors: storeValue.errors,
37
+ fetching: storeValue.fetching,
38
+ variables: variables as _Input,
39
+ unlisten: observer.cleanup,
40
+ listen: observer.send,
41
+ }
42
+ }
@@ -0,0 +1,186 @@
1
+ import React from 'react'
2
+ import { hydrateRoot, createRoot } from 'react-dom/client'
3
+ import type { QueryArtifact, GraphQLVariables } from '$houdini/runtime'
4
+ import type { Cache } from '$houdini/runtime/cache'
5
+ import type { HoudiniClient } from '$houdini/runtime/client'
6
+ import cacheRef from '$houdini/runtime/cache'
7
+ import { setAuthUrl } from 'houdini/runtime'
8
+
9
+ import { injectComponents } from './componentFields.js'
10
+ import { router_cache, type RouterCache, type FormResult } from './routing/index.js'
11
+ import clientFactory from './client.js'
12
+
13
+ declare global {
14
+ interface Window {
15
+ __houdini__client__?: HoudiniClient
16
+ __houdini__pending_components__?: Record<string, any>
17
+ __houdini__cache__?: Cache
18
+ __houdini__hydration__layer__?: any
19
+ __houdini__initial__cache__?: any
20
+ __houdini__initial__session__?: any
21
+ __houdini__form_result__?: FormResult | null
22
+ __houdini__form_token__?: string | null
23
+ // the session endpoint, injected at render (see generate.go) — the relay POSTs here and
24
+ // useSession reads it. The GraphQL endpoint + @session proxy path are derived client-side
25
+ // from the public config, so they aren't injected.
26
+ __houdini__auth_url__?: string | null
27
+ __houdini__pending_artifacts__?: Record<string, QueryArtifact>
28
+ __houdini__pending_data__?: Record<string, any>
29
+ __houdini__pending_variables__?: Record<string, GraphQLVariables>
30
+ __houdini__pending_cache__?: any[]
31
+ __houdini__nav_caches__?: RouterCache
32
+ }
33
+ }
34
+
35
+ export function hydrate_page(
36
+ App: React.ComponentType<any>,
37
+ Component: React.ComponentType<any>,
38
+ pageName: string,
39
+ pendingQueries: string[]
40
+ ) {
41
+ // publish the server-injected session endpoint BEFORE the client is constructed — the @session
42
+ // relay reads getAuthUrl(). It's server-only config so it's injected at render; the GraphQL
43
+ // endpoint and proxy path the client derives from the public config it already bundles.
44
+ setAuthUrl(window.__houdini__auth_url__)
45
+
46
+ // set up the client using its internally-managed singleton cache (cacheRef).
47
+ // the client's cachePolicy and queryPlugin are closure-bound to cacheRef at
48
+ // construction time, so we must use that same instance as window.__houdini__cache__
49
+ // rather than creating a second cache and trying to wire them together afterward.
50
+ window.__houdini__client__ ??= clientFactory()
51
+ window.__houdini__cache__ ??= cacheRef
52
+
53
+ // configure the singleton for React component field support
54
+ if (window.__houdini__pending_components__) {
55
+ window.__houdini__client__.componentCache = window.__houdini__pending_components__
56
+ }
57
+ window.__houdini__cache__._internal_unstable.componentCache =
58
+ window.__houdini__client__.componentCache
59
+ window.__houdini__cache__._internal_unstable.createComponent = (
60
+ fn: React.ComponentType<any>,
61
+ props: any
62
+ ) => React.createElement(fn, props)
63
+
64
+ window.__houdini__hydration__layer__ ??=
65
+ window.__houdini__cache__._internal_unstable.storage.createLayer()
66
+
67
+ // rehydrate the cache from the server-serialized snapshot
68
+ window.__houdini__cache__?.hydrate(
69
+ window.__houdini__initial__cache__,
70
+ window.__houdini__hydration__layer__
71
+ )
72
+
73
+ // apply any cache snapshots that streamed in before this module ran. an @loading query
74
+ // resolves while the document is still open, so its resolution script runs before this
75
+ // (deferred) module and couldn't hydrate the cache directly — it queued its snapshot here
76
+ // instead. drain the queue now that the cache exists so the observers below read the
77
+ // resolved data rather than the loading-state placeholder. each snapshot goes into its own
78
+ // layer (hydrate() replaces a layer's contents wholesale, so reusing one would keep only
79
+ // the last snapshot) and is then merged down so we end up with a single hydration layer.
80
+ const storage = window.__houdini__cache__?._internal_unstable.storage
81
+ for (const snapshot of window.__houdini__pending_cache__ ?? []) {
82
+ const layer = window.__houdini__cache__?.hydrate(snapshot)
83
+ if (layer && storage) {
84
+ storage.resolveLayer(layer.id)
85
+ }
86
+ }
87
+ window.__houdini__pending_cache__ = []
88
+
89
+ // prime the data/artifact caches from anything the server streamed
90
+ const initialData: Record<string, any> = {}
91
+ const initialArtifacts: Record<string, QueryArtifact> = {}
92
+
93
+ for (const [artifactName, artifact] of Object.entries(
94
+ window.__houdini__pending_artifacts__ ?? {}
95
+ )) {
96
+ initialArtifacts[artifactName] = artifact
97
+
98
+ if (window.__houdini__pending_data__?.[artifactName]) {
99
+ const variables = window.__houdini__pending_variables__![artifactName]
100
+
101
+ if ((artifact as any).hasComponents) {
102
+ injectComponents({
103
+ cache: window.__houdini__cache__!,
104
+ selection: (artifact as any).selection,
105
+ data: window.__houdini__pending_data__[artifactName],
106
+ variables,
107
+ })
108
+ }
109
+
110
+ const observer = window.__houdini__client__!.observe({
111
+ artifact,
112
+ cache: window.__houdini__cache__,
113
+ initialValue: window.__houdini__cache__!.read({
114
+ selection: (artifact as any).selection,
115
+ variables,
116
+ }).data,
117
+ initialVariables: variables,
118
+ })
119
+
120
+ observer.send({
121
+ setup: true,
122
+ variables,
123
+ session: window.__houdini__initial__session__,
124
+ })
125
+
126
+ initialData[artifactName] = observer
127
+ }
128
+ }
129
+
130
+ if (!window.__houdini__nav_caches__) {
131
+ window.__houdini__nav_caches__ = router_cache({
132
+ pending_queries: pendingQueries,
133
+ initialData,
134
+ initialVariables: window.__houdini__pending_variables__,
135
+ initialArtifacts,
136
+ components: { [pageName]: Component },
137
+ })
138
+ _flush_pending_artifacts()
139
+ }
140
+
141
+ hydrateRoot(
142
+ document,
143
+ <App
144
+ initialURL={window.location.pathname + window.location.search}
145
+ cache={window.__houdini__cache__}
146
+ session={window.__houdini__initial__session__}
147
+ formResult={window.__houdini__form_result__}
148
+ formToken={window.__houdini__form_token__}
149
+ {...window.__houdini__nav_caches__}
150
+ />
151
+ )
152
+ }
153
+
154
+ const _pendingArtifacts: Array<[string, QueryArtifact]> = []
155
+
156
+ export function register_artifact(name: string, artifact: QueryArtifact) {
157
+ const caches = window.__houdini__nav_caches__
158
+ if (!caches?.artifact_cache) {
159
+ _pendingArtifacts.push([name, artifact])
160
+ return
161
+ }
162
+ if (!caches.artifact_cache.has(name)) {
163
+ caches.artifact_cache.set(name, artifact)
164
+ }
165
+ }
166
+
167
+ function _flush_pending_artifacts() {
168
+ while (_pendingArtifacts.length > 0) {
169
+ const [name, artifact] = _pendingArtifacts.shift()!
170
+ register_artifact(name, artifact)
171
+ }
172
+ }
173
+
174
+ export function mount_static_app(App: React.ComponentType<any>, manifest: any) {
175
+ const root = createRoot(document.getElementById('app')!)
176
+
177
+ root.render(
178
+ React.createElement(App, {
179
+ initialURL: window.location.pathname + window.location.search,
180
+ cache: cacheRef,
181
+ session: null,
182
+ manifest,
183
+ ...router_cache(),
184
+ })
185
+ )
186
+ }