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.
Files changed (176) 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 +81 -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/hooks/index.ts +9 -0
  9. package/runtime/hooks/recycleNodesInto.ts +60 -0
  10. package/runtime/hooks/useDeepCompareEffect.ts +93 -0
  11. package/runtime/hooks/useDocumentHandle.ts +238 -0
  12. package/runtime/hooks/useDocumentStore.ts +112 -0
  13. package/runtime/hooks/useDocumentSubscription.ts +65 -0
  14. package/runtime/hooks/useFragment.ts +101 -0
  15. package/runtime/hooks/useFragmentHandle.ts +178 -0
  16. package/runtime/hooks/useIsMounted.ts +14 -0
  17. package/runtime/hooks/useMutation.ts +70 -0
  18. package/runtime/hooks/useQuery.ts +12 -0
  19. package/runtime/hooks/useQueryHandle.ts +194 -0
  20. package/runtime/hooks/useSubscription.ts +12 -0
  21. package/runtime/hooks/useSubscriptionHandle.ts +38 -0
  22. package/runtime/hydration.tsx +155 -0
  23. package/runtime/index.tsx +66 -0
  24. package/runtime/manifest.ts +6 -0
  25. package/runtime/package.json +1 -0
  26. package/runtime/resolve-href.ts +14 -0
  27. package/runtime/routing/Router.tsx +959 -0
  28. package/runtime/routing/cache.ts +57 -0
  29. package/runtime/routing/errors.tsx +145 -0
  30. package/runtime/routing/index.ts +17 -0
  31. package/runtime/tsconfig.json +39 -0
  32. package/server/index.d.ts +1 -0
  33. package/server/index.js +4 -0
  34. package/server/react-streaming.d.js +0 -0
  35. package/vite/index.d.ts +3 -0
  36. package/vite/index.js +384 -0
  37. package/vite/transform.d.ts +11 -0
  38. package/vite/transform.js +90 -0
  39. package/README.md +0 -36
  40. package/build/plugin/codegen/entries/documentWrappers.d.ts +0 -6
  41. package/build/plugin/codegen/entries/fallbacks.d.ts +0 -5
  42. package/build/plugin/codegen/entries/index.d.ts +0 -16
  43. package/build/plugin/codegen/entries/pages.d.ts +0 -2
  44. package/build/plugin/codegen/index.d.ts +0 -17
  45. package/build/plugin/codegen/manifest.d.ts +0 -5
  46. package/build/plugin/codegen/render.d.ts +0 -7
  47. package/build/plugin/codegen/router.d.ts +0 -7
  48. package/build/plugin/codegen/typeRoot.d.ts +0 -5
  49. package/build/plugin/config.d.ts +0 -4
  50. package/build/plugin/dedent.d.ts +0 -1
  51. package/build/plugin/extract.d.ts +0 -6
  52. package/build/plugin/index.d.ts +0 -5
  53. package/build/plugin/state.d.ts +0 -3
  54. package/build/plugin/transform.d.ts +0 -6
  55. package/build/plugin/vite.d.ts +0 -27
  56. package/build/plugin-cjs/index.js +0 -90119
  57. package/build/plugin-cjs/package.json +0 -1
  58. package/build/plugin-esm/index.js +0 -90115
  59. package/build/runtime/client.d.ts +0 -3
  60. package/build/runtime/clientPlugin.d.ts +0 -3
  61. package/build/runtime/componentFields.d.ts +0 -9
  62. package/build/runtime/hooks/index.d.ts +0 -8
  63. package/build/runtime/hooks/useDeepCompareEffect.d.ts +0 -35
  64. package/build/runtime/hooks/useDocumentHandle.d.ts +0 -36
  65. package/build/runtime/hooks/useDocumentStore.d.ts +0 -11
  66. package/build/runtime/hooks/useDocumentSubscription.d.ts +0 -11
  67. package/build/runtime/hooks/useFragment.d.ts +0 -16
  68. package/build/runtime/hooks/useFragmentHandle.d.ts +0 -8
  69. package/build/runtime/hooks/useIsMounted.d.ts +0 -3
  70. package/build/runtime/hooks/useMutation.d.ts +0 -14
  71. package/build/runtime/hooks/useQuery.d.ts +0 -5
  72. package/build/runtime/hooks/useQueryHandle.d.ts +0 -10
  73. package/build/runtime/hooks/useSubscription.d.ts +0 -4
  74. package/build/runtime/hooks/useSubscriptionHandle.d.ts +0 -25
  75. package/build/runtime/index.d.ts +0 -14
  76. package/build/runtime/manifest.d.ts +0 -3
  77. package/build/runtime/routing/Router.d.ts +0 -62
  78. package/build/runtime/routing/cache.d.ts +0 -7
  79. package/build/runtime/routing/hooks.d.ts +0 -40
  80. package/build/runtime/routing/index.d.ts +0 -3
  81. package/build/runtime-cjs/client.d.ts +0 -3
  82. package/build/runtime-cjs/client.js +0 -25
  83. package/build/runtime-cjs/clientPlugin.d.ts +0 -3
  84. package/build/runtime-cjs/clientPlugin.js +0 -37
  85. package/build/runtime-cjs/componentFields.d.ts +0 -9
  86. package/build/runtime-cjs/componentFields.js +0 -83
  87. package/build/runtime-cjs/hooks/index.d.ts +0 -8
  88. package/build/runtime-cjs/hooks/index.js +0 -45
  89. package/build/runtime-cjs/hooks/useDeepCompareEffect.d.ts +0 -35
  90. package/build/runtime-cjs/hooks/useDeepCompareEffect.js +0 -76
  91. package/build/runtime-cjs/hooks/useDocumentHandle.d.ts +0 -36
  92. package/build/runtime-cjs/hooks/useDocumentHandle.js +0 -177
  93. package/build/runtime-cjs/hooks/useDocumentStore.d.ts +0 -11
  94. package/build/runtime-cjs/hooks/useDocumentStore.js +0 -76
  95. package/build/runtime-cjs/hooks/useDocumentSubscription.d.ts +0 -11
  96. package/build/runtime-cjs/hooks/useDocumentSubscription.js +0 -76
  97. package/build/runtime-cjs/hooks/useFragment.d.ts +0 -16
  98. package/build/runtime-cjs/hooks/useFragment.js +0 -102
  99. package/build/runtime-cjs/hooks/useFragmentHandle.d.ts +0 -8
  100. package/build/runtime-cjs/hooks/useFragmentHandle.js +0 -47
  101. package/build/runtime-cjs/hooks/useIsMounted.d.ts +0 -3
  102. package/build/runtime-cjs/hooks/useIsMounted.js +0 -38
  103. package/build/runtime-cjs/hooks/useMutation.d.ts +0 -14
  104. package/build/runtime-cjs/hooks/useMutation.js +0 -67
  105. package/build/runtime-cjs/hooks/useQuery.d.ts +0 -5
  106. package/build/runtime-cjs/hooks/useQuery.js +0 -32
  107. package/build/runtime-cjs/hooks/useQueryHandle.d.ts +0 -10
  108. package/build/runtime-cjs/hooks/useQueryHandle.js +0 -131
  109. package/build/runtime-cjs/hooks/useSubscription.d.ts +0 -4
  110. package/build/runtime-cjs/hooks/useSubscription.js +0 -32
  111. package/build/runtime-cjs/hooks/useSubscriptionHandle.d.ts +0 -25
  112. package/build/runtime-cjs/hooks/useSubscriptionHandle.js +0 -42
  113. package/build/runtime-cjs/index.d.ts +0 -14
  114. package/build/runtime-cjs/index.js +0 -88
  115. package/build/runtime-cjs/manifest.d.ts +0 -3
  116. package/build/runtime-cjs/manifest.js +0 -25
  117. package/build/runtime-cjs/package.json +0 -1
  118. package/build/runtime-cjs/routing/Router.d.ts +0 -62
  119. package/build/runtime-cjs/routing/Router.js +0 -540
  120. package/build/runtime-cjs/routing/cache.d.ts +0 -7
  121. package/build/runtime-cjs/routing/cache.js +0 -61
  122. package/build/runtime-cjs/routing/hooks.d.ts +0 -40
  123. package/build/runtime-cjs/routing/hooks.js +0 -93
  124. package/build/runtime-cjs/routing/index.d.ts +0 -3
  125. package/build/runtime-cjs/routing/index.js +0 -33
  126. package/build/runtime-esm/client.d.ts +0 -3
  127. package/build/runtime-esm/client.js +0 -5
  128. package/build/runtime-esm/clientPlugin.d.ts +0 -3
  129. package/build/runtime-esm/clientPlugin.js +0 -17
  130. package/build/runtime-esm/componentFields.d.ts +0 -9
  131. package/build/runtime-esm/componentFields.js +0 -59
  132. package/build/runtime-esm/hooks/index.d.ts +0 -8
  133. package/build/runtime-esm/hooks/index.js +0 -15
  134. package/build/runtime-esm/hooks/useDeepCompareEffect.d.ts +0 -35
  135. package/build/runtime-esm/hooks/useDeepCompareEffect.js +0 -41
  136. package/build/runtime-esm/hooks/useDocumentHandle.d.ts +0 -36
  137. package/build/runtime-esm/hooks/useDocumentHandle.js +0 -143
  138. package/build/runtime-esm/hooks/useDocumentStore.d.ts +0 -11
  139. package/build/runtime-esm/hooks/useDocumentStore.js +0 -42
  140. package/build/runtime-esm/hooks/useDocumentSubscription.d.ts +0 -11
  141. package/build/runtime-esm/hooks/useDocumentSubscription.js +0 -42
  142. package/build/runtime-esm/hooks/useFragment.d.ts +0 -16
  143. package/build/runtime-esm/hooks/useFragment.js +0 -67
  144. package/build/runtime-esm/hooks/useFragmentHandle.d.ts +0 -8
  145. package/build/runtime-esm/hooks/useFragmentHandle.js +0 -23
  146. package/build/runtime-esm/hooks/useIsMounted.d.ts +0 -3
  147. package/build/runtime-esm/hooks/useIsMounted.js +0 -14
  148. package/build/runtime-esm/hooks/useMutation.d.ts +0 -14
  149. package/build/runtime-esm/hooks/useMutation.js +0 -42
  150. package/build/runtime-esm/hooks/useQuery.d.ts +0 -5
  151. package/build/runtime-esm/hooks/useQuery.js +0 -8
  152. package/build/runtime-esm/hooks/useQueryHandle.d.ts +0 -10
  153. package/build/runtime-esm/hooks/useQueryHandle.js +0 -97
  154. package/build/runtime-esm/hooks/useSubscription.d.ts +0 -4
  155. package/build/runtime-esm/hooks/useSubscription.js +0 -8
  156. package/build/runtime-esm/hooks/useSubscriptionHandle.d.ts +0 -25
  157. package/build/runtime-esm/hooks/useSubscriptionHandle.js +0 -18
  158. package/build/runtime-esm/index.d.ts +0 -14
  159. package/build/runtime-esm/index.js +0 -48
  160. package/build/runtime-esm/manifest.d.ts +0 -3
  161. package/build/runtime-esm/manifest.js +0 -5
  162. package/build/runtime-esm/routing/Router.d.ts +0 -62
  163. package/build/runtime-esm/routing/Router.js +0 -499
  164. package/build/runtime-esm/routing/cache.d.ts +0 -7
  165. package/build/runtime-esm/routing/cache.js +0 -36
  166. package/build/runtime-esm/routing/hooks.d.ts +0 -40
  167. package/build/runtime-esm/routing/hooks.js +0 -53
  168. package/build/runtime-esm/routing/index.d.ts +0 -3
  169. package/build/runtime-esm/routing/index.js +0 -6
  170. package/build/server/index.d.ts +0 -1
  171. package/build/server-cjs/index.js +0 -28
  172. package/build/server-cjs/package.json +0 -1
  173. package/build/server-esm/index.js +0 -4
  174. package/build/server-esm/package.json +0 -1
  175. /package/{build/plugin-esm → server}/package.json +0 -0
  176. /package/{build/runtime-esm → vite}/package.json +0 -0
@@ -0,0 +1,959 @@
1
+ import type { GraphQLObject, GraphQLVariables } from 'houdini/runtime'
2
+ import type { QueryArtifact } from 'houdini/runtime'
3
+ import type { Cache } from 'houdini/runtime/cache'
4
+ import type { DocumentStore, HoudiniClient } from 'houdini/runtime/client'
5
+ import { getCurrentConfig } from '$houdini/runtime'
6
+ import configFile from '$houdini/runtime/imports/config'
7
+ import { deepEquals } from 'houdini/runtime'
8
+ import type { LRUCache } from 'houdini/runtime'
9
+ import { marshalSelection, marshalInputs } from 'houdini/runtime'
10
+ import { find_match, find_prefix_match } from 'houdini/router/match'
11
+ import type { RouterManifest, RouterPageManifest } from 'houdini/router/types'
12
+ import React from 'react'
13
+ import { useContext, useEffect } from 'react'
14
+
15
+ import { type DocumentHandle, useDocumentHandle } from '../hooks/useDocumentHandle.js'
16
+ import { useDocumentStore } from '../hooks/useDocumentStore.js'
17
+ import { type SuspenseCache, suspense_cache } from './cache.js'
18
+ import { GraphQLErrors, RoutingError, StatusContext } from './errors.js'
19
+
20
+ type PageComponent = React.ComponentType<{ url: string; children?: React.ReactNode }>
21
+
22
+ const PreloadWhich = {
23
+ component: 'component',
24
+ data: 'data',
25
+ page: 'page',
26
+ } as const
27
+
28
+ type PreloadWhichValue = (typeof PreloadWhich)[keyof typeof PreloadWhich]
29
+ type ComponentType = any
30
+ /**
31
+ * Router is the top level entry point for the filesystem-based router.
32
+ * It is responsible for loading various page sources (including API fetches) and
33
+ * then rendering when appropriate.
34
+ */
35
+ // In order to enable streaming SSR, individual page and layouts components
36
+ // must suspend. We can't just have one big suspense that we handle
37
+ // or else we can't isolate the first chunk. That being said, we
38
+ // don't want network waterfalls. So we need to send the request for everything all
39
+ // at once and then wrap the children in the necessary context so that when they render
40
+ // they can grab what they need if its ready and suspend if not.
41
+ export function Router({
42
+ manifest,
43
+ initialURL,
44
+ assetPrefix,
45
+ injectToStream,
46
+ }: {
47
+ manifest: RouterManifest<ComponentType>
48
+ initialURL?: string
49
+ assetPrefix: string
50
+ injectToStream?: undefined | ((chunk: string) => void)
51
+ }) {
52
+ // the current route is just a string in state.
53
+ const [currentURL, setCurrentURL] = React.useState(() => {
54
+ return initialURL || window.location.pathname
55
+ })
56
+
57
+ // find the matching page for the current route
58
+ const [page, variables] = find_match(configFile, manifest, currentURL)
59
+ const is404 = !page
60
+ // When no exact match, find the deepest prefix-matching page to render
61
+ // its layout chain with NotFoundGate throwing inside the appropriate boundary.
62
+ const targetPage = page ?? find_prefix_match(manifest, currentURL)
63
+ if (!targetPage) {
64
+ throw new RoutingError(404)
65
+ }
66
+
67
+ // the only time this component will directly suspend (instead of one of its children)
68
+ // is if we don't have the component source. Dependencies on query results or artifacts
69
+ // will be resolved by the component itself
70
+
71
+ // load the page assets (source, artifacts, data). this will suspend if the component is not available yet
72
+ // this hook embeds pending requests in context so that the component can suspend if necessary14
73
+ const { loadData, loadComponent } = usePageData({
74
+ page: targetPage,
75
+ variables,
76
+ assetPrefix,
77
+ injectToStream,
78
+ })
79
+ // if we get this far, it's safe to load the component
80
+ const { component_cache, data_cache, ssr_signals } = useRouterContext()
81
+ const PageComponent = component_cache.get(targetPage.id)!
82
+
83
+ // if we got this far then we're past suspense
84
+
85
+ //
86
+ // Now that we know we aren't going to throw, let's set up the event listeners
87
+ //
88
+
89
+ // whenever the route changes, we need to make sure the browser's stack is up to date
90
+ React.useEffect(() => {
91
+ if (globalThis.window && window.location.pathname !== currentURL) {
92
+ window.history.pushState({}, '', currentURL)
93
+ }
94
+ }, [currentURL])
95
+
96
+ // when we first mount we should start listening to the back button
97
+ React.useEffect(() => {
98
+ if (!globalThis.window) {
99
+ return
100
+ }
101
+ const onChange = (_evt: PopStateEvent) => {
102
+ setCurrentURL(window.location.pathname)
103
+ }
104
+ window.addEventListener('popstate', onChange)
105
+ return () => {
106
+ window.removeEventListener('popstate', onChange)
107
+ }
108
+ }, [])
109
+
110
+ // the function to call to navigate to a url
111
+ const goto = (url: string) => {
112
+ // clear the data cache so that we refetch queries with the new session (will force a cache-lookup)
113
+ data_cache.clear()
114
+ // clear pending signals so the next render starts fresh load_query calls
115
+ ssr_signals.clear()
116
+
117
+ // perform the navigation
118
+ setCurrentURL(url)
119
+ }
120
+
121
+ // links are powered using anchor tags that we intercept and handle ourselves
122
+ useLinkBehavior({
123
+ goto,
124
+ preload(url: string, which: PreloadWhichValue) {
125
+ // there are 2 things that we could preload: the page component and the data
126
+
127
+ // look for the matching route information
128
+ const [page, variables] = find_match(configFile, manifest, url)
129
+ if (!page) {
130
+ return
131
+ }
132
+
133
+ // load the page component if necessary
134
+ if (['page', 'component'].includes(which)) {
135
+ loadComponent(page)
136
+ }
137
+
138
+ // load the page component if necessary
139
+ if (['page', 'data'].includes(which)) {
140
+ loadData(page, variables)
141
+ }
142
+ },
143
+ })
144
+
145
+ // TODO: cleanup navigation caches
146
+ // render the component embedded in the necessary context so it can orchestrate
147
+ // its needs
148
+ return (
149
+ <VariableContext.Provider value={variables}>
150
+ <LocationContext.Provider
151
+ value={{
152
+ pathname: currentURL,
153
+ goto,
154
+ params: variables ?? {},
155
+ }}
156
+ >
157
+ <Is404Context.Provider value={is404}>
158
+ {is404 ? (
159
+ <NotFoundLayoutBoundary key={targetPage.id}>
160
+ <PageComponent url={currentURL} key={targetPage.id + '__404'} />
161
+ </NotFoundLayoutBoundary>
162
+ ) : (
163
+ <PageComponent url={currentURL} key={targetPage.id} />
164
+ )}
165
+ </Is404Context.Provider>
166
+ </LocationContext.Provider>
167
+ </VariableContext.Provider>
168
+ )
169
+ }
170
+
171
+ // export the location information in context
172
+ export const useLocation = () => useContext(LocationContext)
173
+
174
+ export const ClientRedirect = ({ to }: { to: string }) => {
175
+ const { goto } = useLocation()
176
+ useEffect(() => {
177
+ goto(to)
178
+ }, [to])
179
+ return null
180
+ }
181
+
182
+ /**
183
+ * usePageData is responsible for kicking off the network requests necessary to render the page.
184
+ * This includes loading the artifact, the component source, and any query results. This hook
185
+ * only suspends if the component source is not available. The other cases are handled by the specific
186
+ * page that is being rendered so that nested suspense boundaries are properly wired up.
187
+ */
188
+ function usePageData({
189
+ page,
190
+ variables,
191
+ assetPrefix: _assetPrefix,
192
+ injectToStream,
193
+ }: {
194
+ page: RouterPageManifest<ComponentType>
195
+ variables: GraphQLVariables
196
+ assetPrefix: string
197
+ injectToStream: undefined | ((chunk: string) => void)
198
+ }): {
199
+ loadData: (page: RouterPageManifest<ComponentType>, variables: {} | null) => void
200
+ loadComponent: (page: RouterPageManifest<ComponentType>) => void
201
+ } {
202
+ // grab context values
203
+ const {
204
+ client,
205
+ cache,
206
+ data_cache,
207
+ component_cache,
208
+ artifact_cache,
209
+ ssr_signals,
210
+ last_variables,
211
+ } = useRouterContext()
212
+
213
+ // grab the current session value
214
+ const [session] = useSession()
215
+
216
+ // the function to load a query using the cache references
217
+ function load_query({
218
+ id,
219
+ artifact,
220
+ variables,
221
+ }: {
222
+ id: string
223
+ artifact: QueryArtifact
224
+ variables: GraphQLVariables
225
+ }): Promise<void> {
226
+ // TODO: better tracking - only register the variables that were used
227
+ // track the new variables
228
+ for (const artifact of Object.keys(page.documents)) {
229
+ last_variables.set(artifact, variables)
230
+ }
231
+
232
+ // TODO: AbortController on send()
233
+ // TODO: we can read from cache here before making an asynchronous network call
234
+
235
+ // if there is a pending request and we were asked to load, don't do anything
236
+ if (ssr_signals.has(id)) {
237
+ return ssr_signals.get(id)!
238
+ }
239
+
240
+ // send the request
241
+ const observer: DocumentStore<GraphQLObject, GraphQLVariables> = data_cache.has(
242
+ artifact.name
243
+ )
244
+ ? data_cache.get(artifact.name)!
245
+ : client.observe({ artifact, cache })
246
+
247
+ // store the observer immediately so useQueryResult can access it
248
+ // during SSR rendering before the fetch resolves
249
+ let resolve: () => void = () => {}
250
+ let reject: (message: string) => void = () => {}
251
+ const promise = new Promise<void>((res, rej) => {
252
+ resolve = res
253
+ reject = rej
254
+
255
+ observer
256
+ .send({
257
+ variables: variables,
258
+ session,
259
+ })
260
+ .then(async () => {
261
+ data_cache.set(id, observer)
262
+
263
+ // if there is an error, signal completion (the error is visible via
264
+ // useQueryResult reading observer.state.errors) and clean up
265
+ if (observer.state.errors && observer.state.errors.length > 0) {
266
+ ssr_signals.delete(id)
267
+ resolve()
268
+ return
269
+ }
270
+
271
+ // if we are building up a stream (on the server), we want to add something
272
+ // to the client that resolves the pending request with the
273
+ // data that we just got
274
+ injectToStream?.(`
275
+ <script>
276
+ {
277
+ window.__houdini__cache__?.hydrate(${cache.serialize()}, window.__houdini__hydration__layer__)
278
+
279
+ const artifactName = "${artifact.name}"
280
+ const value = ${JSON.stringify(
281
+ marshalSelection({
282
+ selection: observer.artifact.selection,
283
+ data: observer.state.data,
284
+ config: getCurrentConfig(),
285
+ })
286
+ )}
287
+
288
+ // if the data is pending, we need to resolve it
289
+ if (window.__houdini__nav_caches__?.data_cache.has(artifactName)) {
290
+ // before we resolve the pending signals,
291
+ // fill the data cache with values we got on the server
292
+ const new_store = window.__houdini__client__.observe({
293
+ artifact: window.__houdini__nav_caches__.artifact_cache.get(artifactName),
294
+ cache: window.__houdini__cache__,
295
+ })
296
+
297
+ // we're pushing this store onto the client, it should be initialized
298
+ window.__houdini__nav_caches__.data_cache.get(artifactName).send({
299
+ setup: true,
300
+ variables: ${JSON.stringify(
301
+ marshalInputs({
302
+ artifact: observer.artifact,
303
+ input: variables,
304
+ config: configFile,
305
+ })
306
+ )}
307
+ }).then(() => {
308
+ window.__houdini__nav_caches__?.data_cache.set(artifactName, new_store)
309
+ })
310
+
311
+ }
312
+
313
+
314
+ // if there are no data caches available we need to populate the pending one instead
315
+ if (!window.__houdini__nav_caches__) {
316
+ if (!window.__houdini__pending_data__) {
317
+ window.__houdini__pending_data__ = {}
318
+ }
319
+
320
+ if (!window.__houdini__pending_variables__) {
321
+ window.__houdini__pending_variables__ = {}
322
+ }
323
+
324
+ if (!window.__houdini__pending_artifacts__) {
325
+ window.__houdini__pending_artifacts__ = {}
326
+ }
327
+ }
328
+
329
+ window.__houdini__pending_variables__[artifactName] = ${JSON.stringify(observer.state.variables)}
330
+ window.__houdini__pending_data__[artifactName] = value
331
+ window.__houdini__pending_artifacts__[artifactName] = ${JSON.stringify(artifact)}
332
+
333
+ // if this payload finishes off an ssr request, we need to resolve the signal
334
+ if (window.__houdini__nav_caches__?.ssr_signals.has(artifactName)) {
335
+
336
+ // if the data showed up on the client before
337
+ if (window.__houdini__nav_caches__.data_cache.has(artifactName)) {
338
+ // we're pushing this store onto the client, it should be initialized
339
+ window.__houdini__nav_caches__.data_cache.get(artifactName).send({
340
+ setup: true,
341
+ variables: ${JSON.stringify(
342
+ marshalInputs({
343
+ artifact: observer.artifact,
344
+ input: variables,
345
+ config: configFile,
346
+ })
347
+ )}
348
+ })
349
+ }
350
+
351
+
352
+ // trigger the signal
353
+ window.__houdini__nav_caches__.ssr_signals.get(artifactName).resolve()
354
+ window.__houdini__nav_caches__.ssr_signals.delete(artifactName)
355
+ }
356
+ }
357
+ </script>
358
+ `)
359
+
360
+ ssr_signals.delete(id)
361
+ resolve()
362
+ })
363
+ .catch((err) => {
364
+ ssr_signals.delete(id)
365
+ if (err?.name === 'AbortError') {
366
+ return
367
+ }
368
+ reject(err)
369
+ })
370
+ })
371
+
372
+ // register the pending signal on both client and server so that concurrent React renders
373
+ // (concurrent mode / strict mode) that call load_query before data_cache is populated
374
+ // find the existing signal and don't create a duplicate observer+send
375
+ const resolvable = { ...promise, resolve, reject }
376
+ ssr_signals.set(id, resolvable)
377
+
378
+ // we're done
379
+ return resolvable
380
+ }
381
+
382
+ // the function that loads all of the data for a page using the caches
383
+ function loadData(
384
+ targetPage: RouterPageManifest<ComponentType>,
385
+ variables: GraphQLVariables | null
386
+ ) {
387
+ if (!targetPage) {
388
+ return
389
+ }
390
+
391
+ // if any of the artifacts that this page on have new variables, we need to clear the data cache
392
+ for (const [artifact, { variables: pageVariables }] of Object.entries(
393
+ targetPage.documents
394
+ )) {
395
+ // if there are no last variables, there's nothing to do
396
+ if (!last_variables.has(artifact)) {
397
+ continue
398
+ }
399
+
400
+ // compare the last known variables with the current set
401
+ const last: GraphQLVariables = {}
402
+ const usedVariables: GraphQLVariables = {}
403
+ for (const variable of Object.keys(pageVariables)) {
404
+ last[variable] = last_variables.get(artifact)![variable]
405
+ usedVariables[variable] = variables?.[variable]
406
+ }
407
+
408
+ // before we can compare we need to only look at the variables that the artifact cares about
409
+ if (Object.keys(usedVariables ?? {}).length > 0 && !deepEquals(last, usedVariables)) {
410
+ data_cache.delete(artifact)
411
+ ssr_signals.delete(artifact)
412
+ }
413
+ }
414
+
415
+ // in order to avoid waterfalls, we need to kick off APIs requests in parallel
416
+ // to use loading any missing artifacts or the page component.
417
+
418
+ // group the necessary based on wether we have their artifact or not
419
+ const missing_artifacts: string[] = []
420
+ const found_artifacts: Record<string, QueryArtifact> = {}
421
+ for (const key of Object.keys(targetPage.documents)) {
422
+ if (artifact_cache.has(key)) {
423
+ found_artifacts[key] = artifact_cache.get(key)!
424
+ } else {
425
+ missing_artifacts.push(key)
426
+ }
427
+ }
428
+
429
+ // any missing artifacts need to be loaded and then have their queries loaded
430
+ for (const artifact_id of missing_artifacts) {
431
+ // load the artifact
432
+ targetPage.documents[artifact_id]
433
+ .artifact()
434
+ .then((mod) => {
435
+ // the artifact is the default export
436
+ const artifact = mod.default
437
+
438
+ // save the artifact in the cache
439
+ artifact_cache.set(artifact_id, artifact)
440
+
441
+ // now that we have the artifact, we can load the query too
442
+ load_query({ id: artifact.name, artifact, variables })
443
+ })
444
+ .catch((err) => {
445
+ // TODO: handle error
446
+ console.log(err)
447
+ })
448
+ }
449
+
450
+ // we need to make sure that every artifact we found is loaded
451
+ // or else we need to load the query
452
+ for (const artifact of Object.values(found_artifacts)) {
453
+ // if we don't have the query, load it
454
+ if (!data_cache.has(artifact.name)) {
455
+ load_query({ id: artifact.name, artifact, variables })
456
+ }
457
+ }
458
+ }
459
+
460
+ async function loadComponent(targetPage: RouterPageManifest<ComponentType>) {
461
+ if (component_cache.has(targetPage.id)) {
462
+ return
463
+ }
464
+ const mod = await targetPage.component()
465
+ component_cache.set(targetPage.id, mod.default)
466
+ }
467
+
468
+ // kick off requests for the current page
469
+ loadData(page, variables)
470
+
471
+ // if we haven't loaded the component yet, suspend and do so
472
+ if (!component_cache.has(page.id)) {
473
+ throw loadComponent(page)
474
+ }
475
+
476
+ return {
477
+ loadData,
478
+ loadComponent,
479
+ }
480
+ }
481
+
482
+ export function RouterContextProvider({
483
+ children,
484
+ client,
485
+ cache,
486
+ artifact_cache,
487
+ component_cache,
488
+ data_cache,
489
+ ssr_signals,
490
+ last_variables,
491
+ session: ssrSession = {},
492
+ }: {
493
+ children: React.ReactNode
494
+ client: HoudiniClient
495
+ cache: Cache
496
+ artifact_cache: SuspenseCache<QueryArtifact>
497
+ component_cache: SuspenseCache<PageComponent>
498
+ data_cache: SuspenseCache<DocumentStore<GraphQLObject, GraphQLVariables>>
499
+ ssr_signals: PendingCache
500
+ last_variables: LRUCache<GraphQLVariables>
501
+ session?: App.Session
502
+ }) {
503
+ // the session is top level state
504
+ // on the server, we can just use
505
+ const [session, setSession] = React.useState<App.Session>(ssrSession)
506
+
507
+ // if we detect an event that contains a new session value
508
+ const handleNewSession = React.useCallback((event: Event) => {
509
+ setSession((event as CustomEvent<App.Session>).detail)
510
+ }, [])
511
+
512
+ React.useEffect(() => {
513
+ window.addEventListener('_houdini_session_', handleNewSession)
514
+
515
+ // cleanup this component
516
+ return () => {
517
+ window.removeEventListener('_houdini_session_', handleNewSession)
518
+ }
519
+ }, [handleNewSession])
520
+
521
+ return (
522
+ <Context.Provider
523
+ value={{
524
+ client,
525
+ cache,
526
+ artifact_cache,
527
+ component_cache,
528
+ data_cache,
529
+ ssr_signals,
530
+ last_variables,
531
+ session,
532
+ setSession: (newSession) => setSession((old) => ({ ...old, ...newSession })),
533
+ }}
534
+ >
535
+ {children}
536
+ </Context.Provider>
537
+ )
538
+ }
539
+
540
+ type RouterContext = {
541
+ client: HoudiniClient
542
+ cache: Cache
543
+
544
+ // We also need a cache for artifacts so that we can avoid suspending to
545
+ // load them if possible.
546
+ artifact_cache: SuspenseCache<QueryArtifact>
547
+
548
+ // We also need a cache for component references so we can avoid suspending
549
+ // when we load the same page multiple times
550
+ component_cache: SuspenseCache<PageComponent>
551
+
552
+ // Pages need a way to wait for data
553
+ data_cache: SuspenseCache<DocumentStore<GraphQLObject, GraphQLVariables>>
554
+
555
+ // A way to dedupe requests for a query
556
+ ssr_signals: PendingCache
557
+
558
+ // A way to track the last known good variables
559
+ last_variables: LRUCache<GraphQLVariables>
560
+
561
+ // The current session
562
+ session: App.Session
563
+
564
+ // a function to call that sets the client-side session singletone
565
+ setSession: (newSession: Partial<App.Session>) => void
566
+ }
567
+
568
+ export type PendingCache = SuspenseCache<
569
+ Promise<void> & { resolve: () => void; reject: (message: string) => void }
570
+ >
571
+
572
+ const Context = React.createContext<RouterContext | null>(null)
573
+
574
+ export const useRouterContext = () => {
575
+ const ctx = React.useContext(Context)
576
+
577
+ if (!ctx) {
578
+ throw new Error('Could not find router context')
579
+ }
580
+
581
+ return ctx
582
+ }
583
+
584
+ export function useClient() {
585
+ return useRouterContext().client
586
+ }
587
+
588
+ export function useCache() {
589
+ return useRouterContext().cache
590
+ }
591
+
592
+ export function updateLocalSession(session: App.Session) {
593
+ window.dispatchEvent(
594
+ new CustomEvent<App.Session>('_houdini_session_', {
595
+ bubbles: true,
596
+ detail: session,
597
+ })
598
+ )
599
+ }
600
+
601
+ export function useSession(): [App.Session, (newSession: Partial<App.Session>) => void] {
602
+ const ctx = useRouterContext()
603
+
604
+ // when we update the session we have to do 2 things. (1) we have to update the local state
605
+ // that we will use on the client (2) we have to send a request to the server so that it
606
+ // can update the cookie that we use for the session
607
+ const updateSession = (newSession: Partial<App.Session>) => {
608
+ // clear the data cache so that we refetch queries with the new session (will force a cache-lookup)
609
+ ctx.data_cache.clear()
610
+ ctx.ssr_signals.clear()
611
+
612
+ // update the local state
613
+ ctx.setSession(newSession)
614
+
615
+ // figure out the url that we will use to send values to the server
616
+ const auth = configFile.router?.auth
617
+ if (!auth) {
618
+ return
619
+ }
620
+ const url = 'redirect' in auth ? auth.redirect : auth.url
621
+
622
+ fetch(url!, {
623
+ method: 'POST',
624
+ body: JSON.stringify(newSession),
625
+ headers: {
626
+ 'Content-Type': 'application/json',
627
+ Accept: 'application/json',
628
+ },
629
+ })
630
+ }
631
+
632
+ return [ctx.session, updateSession]
633
+ }
634
+
635
+ export function useCurrentVariables(): GraphQLVariables {
636
+ return React.useContext(VariableContext)
637
+ }
638
+
639
+ const VariableContext = React.createContext<GraphQLVariables>(null)
640
+
641
+ const LocationContext = React.createContext<{
642
+ pathname: string
643
+ params: Record<string, any>
644
+ // a function to imperatively navigate to a url
645
+ goto: (url: string) => void
646
+ }>({
647
+ pathname: '',
648
+ params: {},
649
+ goto: () => {},
650
+ })
651
+
652
+ export function useQueryResult<_Data extends GraphQLObject, _Input extends GraphQLVariables>(
653
+ name: string
654
+ ): [_Data | null, DocumentHandle<any, _Data, _Input>] {
655
+ // pull the global context values
656
+ const { data_cache, artifact_cache } = useRouterContext()
657
+
658
+ // load the store reference (this will suspend)
659
+ const store_ref = data_cache.get(name)! as unknown as DocumentStore<_Data, _Input>
660
+
661
+ // get the live data from the store
662
+ const [storeValue, observer] = useDocumentStore<_Data, _Input>({
663
+ artifact: store_ref.artifact,
664
+ observer: store_ref,
665
+ })
666
+
667
+ // pull out the store values we care about
668
+ const { data, errors } = storeValue
669
+
670
+ // if there is an error in the response we need to throw to the nearest boundary
671
+ if (errors && errors.length > 0) {
672
+ throw new GraphQLErrors(errors)
673
+ }
674
+ // create the handle that we will use to interact with the store
675
+ const handle = useDocumentHandle({
676
+ artifact: artifact_cache.get(name)!,
677
+ observer,
678
+ storeValue,
679
+ })
680
+
681
+ // we're done
682
+ return [data, handle]
683
+ }
684
+
685
+ function useLinkBehavior({
686
+ goto,
687
+ preload,
688
+ }: {
689
+ goto: (url: string) => void
690
+ preload: (url: string, which: PreloadWhichValue) => void
691
+ }) {
692
+ // always use the click handler
693
+ useLinkNavigation({ goto })
694
+
695
+ // only use the preload handler if the browser hasn't chosen to reduce data usage
696
+ // this doesn't break the rule of hooks because it will only ever have one value
697
+ // in the lifetime of the app
698
+ // @ts-expect-error
699
+ if (!globalThis.navigator?.connection?.saveData) {
700
+ // biome-ignore lint/correctness/useHookAtTopLevel: value is constant for the lifetime of the app
701
+ usePreload({ preload })
702
+ }
703
+ }
704
+
705
+ function useLinkNavigation({ goto }: { goto: (url: string) => void }) {
706
+ // navigations need to be registered as transitions
707
+ const [_pending, startTransition] = React.useTransition()
708
+
709
+ React.useEffect(() => {
710
+ const onClick: HTMLAnchorElement['onclick'] = (e) => {
711
+ if (!e.target) {
712
+ return
713
+ }
714
+
715
+ const link = (e.target as HTMLElement | null | undefined)?.closest('a')
716
+ // its a link we want to handle so don't navigate like normal
717
+
718
+ // we only want to capture a "normal click" ie something that indicates a route transition
719
+ // in the current tab
720
+ // courtesy of: https://gist.github.com/devongovett/919dc0f06585bd88af053562fd7c41b7
721
+ if (
722
+ !(
723
+ link &&
724
+ link instanceof HTMLAnchorElement &&
725
+ link.href &&
726
+ (!link.target || link.target === '_self') &&
727
+ link.origin === location.origin &&
728
+ !link.hasAttribute('download') &&
729
+ e.button === 0 && // left clicks only
730
+ !e.metaKey && // open in new tab (mac)
731
+ !e.ctrlKey && // open in new tab (windows)
732
+ !e.altKey && // download
733
+ !e.shiftKey &&
734
+ !e.defaultPrevented
735
+ )
736
+ ) {
737
+ return
738
+ }
739
+
740
+ // we need to figure out the target url by looking at the href attribute
741
+ const target = link.attributes.getNamedItem('href')?.value
742
+ // make sure its a link we recognize
743
+ if (!target?.startsWith('/')) {
744
+ return
745
+ }
746
+
747
+ // its a link we want to handle so don't navigate like normal
748
+ e.preventDefault()
749
+ e.stopPropagation()
750
+
751
+ // go to the next route as a low priority update
752
+ startTransition(() => {
753
+ goto(target)
754
+ })
755
+ }
756
+
757
+ window.addEventListener('click', onClick)
758
+ return () => {
759
+ window.removeEventListener('click', onClick!)
760
+ }
761
+ }, [goto])
762
+ }
763
+
764
+ function usePreload({ preload }: { preload: (url: string, which: PreloadWhichValue) => void }) {
765
+ const timeoutRef: React.MutableRefObject<NodeJS.Timeout | null> = React.useRef(null)
766
+
767
+ // if the mouse pauses on an element for 20ms then we register it as a hover
768
+ // this avoids that annoying double tap on mobile when the click captures the hover
769
+ React.useEffect(() => {
770
+ const mouseMove: HTMLAnchorElement['onmousemove'] = (e) => {
771
+ const target = e.target
772
+ if (!(target instanceof HTMLElement)) {
773
+ return
774
+ }
775
+
776
+ const anchor = target.closest('a')
777
+ if (!anchor) {
778
+ return
779
+ }
780
+
781
+ // if the anchor doesn't explicitly opt in to preloading, don't do anything
782
+ const preloadAttr = anchor.attributes.getNamedItem('data-houdini-preload')
783
+ if (!preloadAttr) {
784
+ return
785
+ }
786
+ const preloadWhichRaw = preloadAttr.value
787
+ const preloadWhich: PreloadWhichValue =
788
+ !preloadWhichRaw || preloadWhichRaw === 'true'
789
+ ? 'page'
790
+ : (preloadWhichRaw as PreloadWhichValue)
791
+
792
+ // validate the preload option
793
+ if (!PreloadWhich[preloadWhich]) {
794
+ console.log(
795
+ `invalid preload value "${preloadWhich}" must be "${PreloadWhich.component}", "${PreloadWhich.data}", or "${PreloadWhich.page}"`
796
+ )
797
+ return
798
+ }
799
+
800
+ // if we already have a timeout, remove it
801
+ if (timeoutRef.current) {
802
+ clearTimeout(timeoutRef.current)
803
+ }
804
+
805
+ // set the new timeout to track _this_ anchor
806
+ timeoutRef.current = setTimeout(() => {
807
+ const url = anchor.attributes.getNamedItem('href')?.value
808
+ if (!url) {
809
+ return
810
+ }
811
+ preload(url, preloadWhich)
812
+ }, 20)
813
+ }
814
+
815
+ // register/cleanup the event handler
816
+ document.addEventListener('mousemove', mouseMove)
817
+ return () => {
818
+ document.removeEventListener('mousemove', mouseMove)
819
+ }
820
+ }, [preload])
821
+ }
822
+
823
+ export type RouterCache = {
824
+ artifact_cache: SuspenseCache<QueryArtifact>
825
+ component_cache: SuspenseCache<PageComponent>
826
+ data_cache: SuspenseCache<DocumentStore<GraphQLObject, GraphQLVariables>>
827
+ last_variables: LRUCache<GraphQLVariables>
828
+ ssr_signals: PendingCache
829
+ }
830
+
831
+ export function router_cache({
832
+ pending_queries = [],
833
+ artifacts = {},
834
+ components = {},
835
+ initialData = {},
836
+ initialVariables = {},
837
+ initialArtifacts = {},
838
+ }: {
839
+ pending_queries?: string[]
840
+ artifacts?: Record<string, QueryArtifact>
841
+ components?: Record<string, PageComponent>
842
+ initialData?: Record<string, DocumentStore<GraphQLObject, GraphQLVariables>>
843
+ initialVariables?: Record<string, GraphQLVariables>
844
+ initialArtifacts?: Record<string, QueryArtifact>
845
+ } = {}): RouterCache {
846
+ const result: RouterCache = {
847
+ artifact_cache: suspense_cache(initialArtifacts),
848
+ component_cache: suspense_cache(),
849
+ data_cache: suspense_cache(initialData),
850
+ ssr_signals: suspense_cache(),
851
+ last_variables: suspense_cache(),
852
+ }
853
+
854
+ // we need to fill each query with an externally resolvable promise
855
+ for (const query of pending_queries) {
856
+ result.ssr_signals.set(query, signal_promise())
857
+ }
858
+
859
+ for (const [name, artifact] of Object.entries(artifacts)) {
860
+ result.artifact_cache.set(name, artifact)
861
+ }
862
+
863
+ for (const [name, component] of Object.entries(components)) {
864
+ result.component_cache.set(name, component)
865
+ }
866
+
867
+ for (const [name, variables] of Object.entries(initialVariables)) {
868
+ result.last_variables.set(name, variables)
869
+ }
870
+
871
+ return result
872
+ }
873
+
874
+ // Catches RoutingErrors that escape all HoudiniErrorBoundary instances during prefix-match
875
+ // (is404) rendering, preventing an infinite loop when a layout itself throws notFound().
876
+ class NotFoundLayoutBoundary extends React.Component<
877
+ { children: React.ReactNode },
878
+ { caught: boolean }
879
+ > {
880
+ static contextType = StatusContext
881
+ declare context: React.ContextType<typeof StatusContext>
882
+
883
+ constructor(props: { children: React.ReactNode }) {
884
+ super(props)
885
+ this.state = { caught: false }
886
+ }
887
+
888
+ static getDerivedStateFromError(error: unknown) {
889
+ if (error instanceof RoutingError) {
890
+ return { caught: true }
891
+ }
892
+ return null
893
+ }
894
+
895
+ componentDidCatch(error: Error): void {
896
+ if (error instanceof RoutingError && this.context) {
897
+ this.context.status = error.status
898
+ }
899
+ }
900
+
901
+ render() {
902
+ if (this.state.caught) {
903
+ return null
904
+ }
905
+ return this.props.children
906
+ }
907
+ }
908
+
909
+ export const Is404Context = React.createContext(false)
910
+
911
+ export function NotFoundGate({ children }: { children: React.ReactNode }) {
912
+ const is404 = React.useContext(Is404Context)
913
+ if (is404) {
914
+ throw new RoutingError(404)
915
+ }
916
+ return <>{children}</>
917
+ }
918
+
919
+ const PageContext = React.createContext<{ params: Record<string, any> }>({ params: {} })
920
+
921
+ export function PageContextProvider({
922
+ keys,
923
+ children,
924
+ }: {
925
+ keys: string[]
926
+ children: React.ReactNode
927
+ }) {
928
+ const location = useLocation()
929
+ const params = Object.fromEntries(
930
+ Object.entries(location.params).filter(([key]) => keys.includes(key))
931
+ )
932
+
933
+ return <PageContext.Provider value={{ params }}>{children}</PageContext.Provider>
934
+ }
935
+
936
+ export function useRoute<PageProps extends { Params: {} }>(): RouteProp<PageProps['Params']> {
937
+ return useContext(PageContext)
938
+ }
939
+
940
+ export type RouteProp<Params> = {
941
+ params: Params
942
+ }
943
+
944
+ // a signal promise is a promise is used to send signals by having listeners attach
945
+ // actions to the then()
946
+ function signal_promise(): Promise<void> & { resolve: () => void; reject: () => void } {
947
+ let resolve: () => void = () => {}
948
+ let reject: () => void = () => {}
949
+ const promise = new Promise<void>((res, rej) => {
950
+ resolve = res
951
+ reject = rej
952
+ })
953
+
954
+ return {
955
+ ...promise,
956
+ resolve,
957
+ reject,
958
+ }
959
+ }