houdini-react 2.0.0-go.0 → 2.0.0-go.2

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