houdini-react 2.0.0-next.11 → 2.0.0-next.22

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 (170) hide show
  1. package/bin/houdini-react +88 -0
  2. package/package.json +43 -22
  3. package/postInstall.js +353 -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 +185 -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/routing/Router.tsx +885 -0
  24. package/runtime/routing/cache.ts +54 -0
  25. package/runtime/routing/index.ts +2 -0
  26. package/server/index.d.ts +1 -0
  27. package/server/index.js +4 -0
  28. package/server/react-streaming.d.js +0 -0
  29. package/vite/index.d.ts +3 -0
  30. package/vite/index.js +284 -0
  31. package/vite/transform.d.ts +11 -0
  32. package/vite/transform.js +92 -0
  33. package/README.md +0 -36
  34. package/build/plugin/codegen/entries/documentWrappers.d.ts +0 -6
  35. package/build/plugin/codegen/entries/fallbacks.d.ts +0 -5
  36. package/build/plugin/codegen/entries/index.d.ts +0 -16
  37. package/build/plugin/codegen/entries/pages.d.ts +0 -2
  38. package/build/plugin/codegen/index.d.ts +0 -17
  39. package/build/plugin/codegen/manifest.d.ts +0 -5
  40. package/build/plugin/codegen/render.d.ts +0 -7
  41. package/build/plugin/codegen/router.d.ts +0 -7
  42. package/build/plugin/codegen/typeRoot.d.ts +0 -5
  43. package/build/plugin/config.d.ts +0 -4
  44. package/build/plugin/dedent.d.ts +0 -1
  45. package/build/plugin/extract.d.ts +0 -6
  46. package/build/plugin/index.d.ts +0 -5
  47. package/build/plugin/state.d.ts +0 -3
  48. package/build/plugin/transform.d.ts +0 -6
  49. package/build/plugin/vite.d.ts +0 -27
  50. package/build/plugin-cjs/index.js +0 -90122
  51. package/build/plugin-cjs/package.json +0 -1
  52. package/build/plugin-esm/index.js +0 -90118
  53. package/build/runtime/client.d.ts +0 -3
  54. package/build/runtime/clientPlugin.d.ts +0 -3
  55. package/build/runtime/componentFields.d.ts +0 -9
  56. package/build/runtime/hooks/index.d.ts +0 -8
  57. package/build/runtime/hooks/useDeepCompareEffect.d.ts +0 -35
  58. package/build/runtime/hooks/useDocumentHandle.d.ts +0 -36
  59. package/build/runtime/hooks/useDocumentStore.d.ts +0 -11
  60. package/build/runtime/hooks/useDocumentSubscription.d.ts +0 -11
  61. package/build/runtime/hooks/useFragment.d.ts +0 -16
  62. package/build/runtime/hooks/useFragmentHandle.d.ts +0 -8
  63. package/build/runtime/hooks/useIsMounted.d.ts +0 -3
  64. package/build/runtime/hooks/useMutation.d.ts +0 -14
  65. package/build/runtime/hooks/useQuery.d.ts +0 -5
  66. package/build/runtime/hooks/useQueryHandle.d.ts +0 -10
  67. package/build/runtime/hooks/useSubscription.d.ts +0 -4
  68. package/build/runtime/hooks/useSubscriptionHandle.d.ts +0 -25
  69. package/build/runtime/index.d.ts +0 -14
  70. package/build/runtime/manifest.d.ts +0 -3
  71. package/build/runtime/routing/Router.d.ts +0 -62
  72. package/build/runtime/routing/cache.d.ts +0 -7
  73. package/build/runtime/routing/hooks.d.ts +0 -40
  74. package/build/runtime/routing/index.d.ts +0 -3
  75. package/build/runtime-cjs/client.d.ts +0 -3
  76. package/build/runtime-cjs/client.js +0 -25
  77. package/build/runtime-cjs/clientPlugin.d.ts +0 -3
  78. package/build/runtime-cjs/clientPlugin.js +0 -37
  79. package/build/runtime-cjs/componentFields.d.ts +0 -9
  80. package/build/runtime-cjs/componentFields.js +0 -83
  81. package/build/runtime-cjs/hooks/index.d.ts +0 -8
  82. package/build/runtime-cjs/hooks/index.js +0 -45
  83. package/build/runtime-cjs/hooks/useDeepCompareEffect.d.ts +0 -35
  84. package/build/runtime-cjs/hooks/useDeepCompareEffect.js +0 -76
  85. package/build/runtime-cjs/hooks/useDocumentHandle.d.ts +0 -36
  86. package/build/runtime-cjs/hooks/useDocumentHandle.js +0 -177
  87. package/build/runtime-cjs/hooks/useDocumentStore.d.ts +0 -11
  88. package/build/runtime-cjs/hooks/useDocumentStore.js +0 -76
  89. package/build/runtime-cjs/hooks/useDocumentSubscription.d.ts +0 -11
  90. package/build/runtime-cjs/hooks/useDocumentSubscription.js +0 -76
  91. package/build/runtime-cjs/hooks/useFragment.d.ts +0 -16
  92. package/build/runtime-cjs/hooks/useFragment.js +0 -102
  93. package/build/runtime-cjs/hooks/useFragmentHandle.d.ts +0 -8
  94. package/build/runtime-cjs/hooks/useFragmentHandle.js +0 -47
  95. package/build/runtime-cjs/hooks/useIsMounted.d.ts +0 -3
  96. package/build/runtime-cjs/hooks/useIsMounted.js +0 -38
  97. package/build/runtime-cjs/hooks/useMutation.d.ts +0 -14
  98. package/build/runtime-cjs/hooks/useMutation.js +0 -67
  99. package/build/runtime-cjs/hooks/useQuery.d.ts +0 -5
  100. package/build/runtime-cjs/hooks/useQuery.js +0 -32
  101. package/build/runtime-cjs/hooks/useQueryHandle.d.ts +0 -10
  102. package/build/runtime-cjs/hooks/useQueryHandle.js +0 -131
  103. package/build/runtime-cjs/hooks/useSubscription.d.ts +0 -4
  104. package/build/runtime-cjs/hooks/useSubscription.js +0 -32
  105. package/build/runtime-cjs/hooks/useSubscriptionHandle.d.ts +0 -25
  106. package/build/runtime-cjs/hooks/useSubscriptionHandle.js +0 -42
  107. package/build/runtime-cjs/index.d.ts +0 -14
  108. package/build/runtime-cjs/index.js +0 -88
  109. package/build/runtime-cjs/manifest.d.ts +0 -3
  110. package/build/runtime-cjs/manifest.js +0 -25
  111. package/build/runtime-cjs/package.json +0 -1
  112. package/build/runtime-cjs/routing/Router.d.ts +0 -62
  113. package/build/runtime-cjs/routing/Router.js +0 -540
  114. package/build/runtime-cjs/routing/cache.d.ts +0 -7
  115. package/build/runtime-cjs/routing/cache.js +0 -61
  116. package/build/runtime-cjs/routing/hooks.d.ts +0 -40
  117. package/build/runtime-cjs/routing/hooks.js +0 -93
  118. package/build/runtime-cjs/routing/index.d.ts +0 -3
  119. package/build/runtime-cjs/routing/index.js +0 -33
  120. package/build/runtime-esm/client.d.ts +0 -3
  121. package/build/runtime-esm/client.js +0 -5
  122. package/build/runtime-esm/clientPlugin.d.ts +0 -3
  123. package/build/runtime-esm/clientPlugin.js +0 -17
  124. package/build/runtime-esm/componentFields.d.ts +0 -9
  125. package/build/runtime-esm/componentFields.js +0 -59
  126. package/build/runtime-esm/hooks/index.d.ts +0 -8
  127. package/build/runtime-esm/hooks/index.js +0 -15
  128. package/build/runtime-esm/hooks/useDeepCompareEffect.d.ts +0 -35
  129. package/build/runtime-esm/hooks/useDeepCompareEffect.js +0 -41
  130. package/build/runtime-esm/hooks/useDocumentHandle.d.ts +0 -36
  131. package/build/runtime-esm/hooks/useDocumentHandle.js +0 -143
  132. package/build/runtime-esm/hooks/useDocumentStore.d.ts +0 -11
  133. package/build/runtime-esm/hooks/useDocumentStore.js +0 -42
  134. package/build/runtime-esm/hooks/useDocumentSubscription.d.ts +0 -11
  135. package/build/runtime-esm/hooks/useDocumentSubscription.js +0 -42
  136. package/build/runtime-esm/hooks/useFragment.d.ts +0 -16
  137. package/build/runtime-esm/hooks/useFragment.js +0 -67
  138. package/build/runtime-esm/hooks/useFragmentHandle.d.ts +0 -8
  139. package/build/runtime-esm/hooks/useFragmentHandle.js +0 -23
  140. package/build/runtime-esm/hooks/useIsMounted.d.ts +0 -3
  141. package/build/runtime-esm/hooks/useIsMounted.js +0 -14
  142. package/build/runtime-esm/hooks/useMutation.d.ts +0 -14
  143. package/build/runtime-esm/hooks/useMutation.js +0 -42
  144. package/build/runtime-esm/hooks/useQuery.d.ts +0 -5
  145. package/build/runtime-esm/hooks/useQuery.js +0 -8
  146. package/build/runtime-esm/hooks/useQueryHandle.d.ts +0 -10
  147. package/build/runtime-esm/hooks/useQueryHandle.js +0 -97
  148. package/build/runtime-esm/hooks/useSubscription.d.ts +0 -4
  149. package/build/runtime-esm/hooks/useSubscription.js +0 -8
  150. package/build/runtime-esm/hooks/useSubscriptionHandle.d.ts +0 -25
  151. package/build/runtime-esm/hooks/useSubscriptionHandle.js +0 -18
  152. package/build/runtime-esm/index.d.ts +0 -14
  153. package/build/runtime-esm/index.js +0 -48
  154. package/build/runtime-esm/manifest.d.ts +0 -3
  155. package/build/runtime-esm/manifest.js +0 -5
  156. package/build/runtime-esm/routing/Router.d.ts +0 -62
  157. package/build/runtime-esm/routing/Router.js +0 -499
  158. package/build/runtime-esm/routing/cache.d.ts +0 -7
  159. package/build/runtime-esm/routing/cache.js +0 -36
  160. package/build/runtime-esm/routing/hooks.d.ts +0 -40
  161. package/build/runtime-esm/routing/hooks.js +0 -53
  162. package/build/runtime-esm/routing/index.d.ts +0 -3
  163. package/build/runtime-esm/routing/index.js +0 -6
  164. package/build/server/index.d.ts +0 -1
  165. package/build/server-cjs/index.js +0 -28
  166. package/build/server-cjs/package.json +0 -1
  167. package/build/server-esm/index.js +0 -4
  168. /package/{build/plugin-esm → runtime}/package.json +0 -0
  169. /package/{build/runtime-esm → server}/package.json +0 -0
  170. /package/{build/server-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'
16
+ import { useDocumentStore } from '../hooks/useDocumentStore'
17
+ import { type SuspenseCache, suspense_cache } from './cache'
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
+ let resolve: () => void = () => {}
226
+ let reject: (message: string) => void = () => {}
227
+ const promise = new Promise<void>((res, rej) => {
228
+ resolve = res
229
+ reject = rej
230
+
231
+ observer
232
+ .send({
233
+ variables: variables,
234
+ session,
235
+ })
236
+ .then(async () => {
237
+ data_cache.set(id, observer)
238
+
239
+ // if there is an error, we need to reject the promise
240
+ if (observer.state.errors && observer.state.errors.length > 0) {
241
+ reject(observer.state.errors.map((e) => e.message).join('\n'))
242
+ return
243
+ }
244
+
245
+ // if we are building up a stream (on the server), we want to add something
246
+ // to the client that resolves the pending request with the
247
+ // data that we just got
248
+ injectToStream?.(`
249
+ <script>
250
+ {
251
+ window.__houdini__cache__?.hydrate(${cache.serialize()}, window.__houdini__hydration__layer__)
252
+
253
+ const artifactName = "${artifact.name}"
254
+ const value = ${JSON.stringify(
255
+ marshalSelection({
256
+ selection: observer.artifact.selection,
257
+ data: observer.state.data,
258
+ config: getCurrentConfig(),
259
+ })
260
+ )}
261
+
262
+ // if the data is pending, we need to resolve it
263
+ if (window.__houdini__nav_caches__?.data_cache.has(artifactName)) {
264
+ // before we resolve the pending signals,
265
+ // fill the data cache with values we got on the server
266
+ const new_store = window.__houdini__client__.observe({
267
+ artifact: window.__houdini__nav_caches__.artifact_cache.get(artifactName),
268
+ cache: window.__houdini__cache__,
269
+ })
270
+
271
+ // we're pushing this store onto the client, it should be initialized
272
+ window.__houdini__nav_caches__.data_cache.get(artifactName).send({
273
+ setup: true,
274
+ variables: ${JSON.stringify(
275
+ marshalInputs({
276
+ artifact: observer.artifact,
277
+ input: variables,
278
+ config: configFile,
279
+ })
280
+ )}
281
+ }).then(() => {
282
+ window.__houdini__nav_caches__?.data_cache.set(artifactName, new_store)
283
+ })
284
+
285
+ }
286
+
287
+
288
+ // if there are no data caches available we need to populate the pending one instead
289
+ if (!window.__houdini__nav_caches__) {
290
+ if (!window.__houdini__pending_data__) {
291
+ window.__houdini__pending_data__ = {}
292
+ }
293
+
294
+ if (!window.__houdini__pending_variables__) {
295
+ window.__houdini__pending_variables__ = {}
296
+ }
297
+
298
+ if (!window.__houdini__pending_artifacts__) {
299
+ window.__houdini__pending_artifacts__ = {}
300
+ }
301
+ }
302
+
303
+ window.__houdini__pending_variables__[artifactName] = ${JSON.stringify(observer.state.variables)}
304
+ window.__houdini__pending_data__[artifactName] = value
305
+ window.__houdini__pending_artifacts__[artifactName] = ${JSON.stringify(artifact)}
306
+
307
+ // if this payload finishes off an ssr request, we need to resolve the signal
308
+ if (window.__houdini__nav_caches__?.ssr_signals.has(artifactName)) {
309
+
310
+ // if the data showed up on the client before
311
+ if (window.__houdini__nav_caches__.data_cache.has(artifactName)) {
312
+ // we're pushing this store onto the client, it should be initialized
313
+ window.__houdini__nav_caches__.data_cache.get(artifactName).send({
314
+ setup: true,
315
+ variables: ${JSON.stringify(
316
+ marshalInputs({
317
+ artifact: observer.artifact,
318
+ input: variables,
319
+ config: configFile,
320
+ })
321
+ )}
322
+ })
323
+ }
324
+
325
+
326
+ // trigger the signal
327
+ window.__houdini__nav_caches__.ssr_signals.get(artifactName).resolve()
328
+ window.__houdini__nav_caches__.ssr_signals.delete(artifactName)
329
+ }
330
+ }
331
+ </script>
332
+ `)
333
+
334
+ resolve()
335
+ })
336
+ .catch(reject)
337
+ })
338
+
339
+ // if we are on the server, we need to save a signal that we can use to
340
+ // communicate with the client when we're done
341
+ const resolvable = { ...promise, resolve, reject }
342
+ if (!globalThis.window) {
343
+ ssr_signals.set(id, resolvable)
344
+ }
345
+
346
+ // we're done
347
+ return resolvable
348
+ }
349
+
350
+ // the function that loads all of the data for a page using the caches
351
+ function loadData(
352
+ targetPage: RouterPageManifest<ComponentType>,
353
+ variables: GraphQLVariables | null
354
+ ) {
355
+ if (!targetPage) {
356
+ return
357
+ }
358
+
359
+ // if any of the artifacts that this page on have new variables, we need to clear the data cache
360
+ for (const [artifact, { variables: pageVariables }] of Object.entries(
361
+ targetPage.documents
362
+ )) {
363
+ // if there are no last variables, there's nothing to do
364
+ if (!last_variables.has(artifact)) {
365
+ continue
366
+ }
367
+
368
+ // compare the last known variables with the current set
369
+ const last: GraphQLVariables = {}
370
+ const usedVariables: GraphQLVariables = {}
371
+ for (const variable of Object.keys(pageVariables)) {
372
+ last[variable] = last_variables.get(artifact)![variable]
373
+ usedVariables[variable] = variables?.[variable]
374
+ }
375
+
376
+ // before we can compare we need to only look at the variables that the artifact cares about
377
+ if (Object.keys(usedVariables ?? {}).length > 0 && !deepEquals(last, usedVariables)) {
378
+ data_cache.delete(artifact)
379
+ }
380
+ }
381
+
382
+ // in order to avoid waterfalls, we need to kick off APIs requests in parallel
383
+ // to use loading any missing artifacts or the page component.
384
+
385
+ // group the necessary based on wether we have their artifact or not
386
+ const missing_artifacts: string[] = []
387
+ const found_artifacts: Record<string, QueryArtifact> = {}
388
+ for (const key of Object.keys(targetPage.documents)) {
389
+ if (artifact_cache.has(key)) {
390
+ found_artifacts[key] = artifact_cache.get(key)!
391
+ } else {
392
+ missing_artifacts.push(key)
393
+ }
394
+ }
395
+
396
+ // any missing artifacts need to be loaded and then have their queries loaded
397
+ for (const artifact_id of missing_artifacts) {
398
+ // load the artifact
399
+ targetPage.documents[artifact_id]
400
+ .artifact()
401
+ .then((mod) => {
402
+ // the artifact is the default export
403
+ const artifact = mod.default
404
+
405
+ // save the artifact in the cache
406
+ artifact_cache.set(artifact_id, artifact)
407
+
408
+ // now that we have the artifact, we can load the query too
409
+ load_query({ id: artifact.name, artifact, variables })
410
+ })
411
+ .catch((err) => {
412
+ // TODO: handle error
413
+ console.log(err)
414
+ })
415
+ }
416
+
417
+ // we need to make sure that every artifact we found is loaded
418
+ // or else we need to load the query
419
+ for (const artifact of Object.values(found_artifacts)) {
420
+ // if we don't have the query, load it
421
+ if (!data_cache.has(artifact.name)) {
422
+ load_query({ id: artifact.name, artifact, variables })
423
+ }
424
+ }
425
+ }
426
+
427
+ // if we don't have the component then we need to load it, save it in the cache, and
428
+ // then suspend with a promise that will resolve once its in cache
429
+ async function loadComponent(targetPage: RouterPageManifest<ComponentType>) {
430
+ // if we already have the component, don't do anything
431
+ if (component_cache.has(targetPage.id)) {
432
+ return
433
+ }
434
+
435
+ // load the component and then save it in the cache
436
+ const mod = await targetPage.component()
437
+
438
+ // save the component in the cache
439
+ component_cache.set(targetPage.id, mod.default)
440
+ }
441
+
442
+ // kick off requests for the current page
443
+ loadData(page, variables)
444
+
445
+ // if we haven't loaded the component yet, suspend and do so
446
+ if (!component_cache.has(page.id)) {
447
+ throw loadComponent(page)
448
+ }
449
+
450
+ return {
451
+ loadData,
452
+ loadComponent,
453
+ }
454
+ }
455
+
456
+ export function RouterContextProvider({
457
+ children,
458
+ client,
459
+ cache,
460
+ artifact_cache,
461
+ component_cache,
462
+ data_cache,
463
+ ssr_signals,
464
+ last_variables,
465
+ session: ssrSession = {},
466
+ }: {
467
+ children: React.ReactNode
468
+ client: HoudiniClient
469
+ cache: Cache
470
+ artifact_cache: SuspenseCache<QueryArtifact>
471
+ component_cache: SuspenseCache<PageComponent>
472
+ data_cache: SuspenseCache<DocumentStore<GraphQLObject, GraphQLVariables>>
473
+ ssr_signals: PendingCache
474
+ last_variables: LRUCache<GraphQLVariables>
475
+ session?: App.Session
476
+ }) {
477
+ // the session is top level state
478
+ // on the server, we can just use
479
+ const [session, setSession] = React.useState<App.Session>(ssrSession)
480
+
481
+ // if we detect an event that contains a new session value
482
+ const handleNewSession = React.useCallback((event: CustomEvent<App.Session>) => {
483
+ setSession(event.detail)
484
+ }, [])
485
+
486
+ React.useEffect(() => {
487
+ // @ts-ignore
488
+ window.addEventListener('_houdini_session_', handleNewSession)
489
+
490
+ // cleanup this component
491
+ return () => {
492
+ // @ts-ignore
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-ignore
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
+ }