houdini-react 2.0.0-next.3 → 2.0.0-next.31

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