houdini-react 2.0.0-next.41 → 2.0.0-next.46

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "houdini-react",
3
- "version": "2.0.0-next.41",
3
+ "version": "2.0.0-next.46",
4
4
  "description": "The React plugin for houdini",
5
5
  "keywords": [
6
6
  "typescript",
@@ -81,13 +81,13 @@
81
81
  }
82
82
  },
83
83
  "optionalDependencies": {
84
- "houdini-react-darwin-x64": "2.0.0-next.41",
85
- "houdini-react-darwin-arm64": "2.0.0-next.41",
86
- "houdini-react-linux-x64": "2.0.0-next.41",
87
- "houdini-react-linux-arm64": "2.0.0-next.41",
88
- "houdini-react-win32-x64": "2.0.0-next.41",
89
- "houdini-react-win32-arm64": "2.0.0-next.41",
90
- "houdini-react-wasm": "2.0.0-next.41"
84
+ "houdini-react-darwin-x64": "2.0.0-next.46",
85
+ "houdini-react-darwin-arm64": "2.0.0-next.46",
86
+ "houdini-react-linux-x64": "2.0.0-next.46",
87
+ "houdini-react-linux-arm64": "2.0.0-next.46",
88
+ "houdini-react-win32-x64": "2.0.0-next.46",
89
+ "houdini-react-win32-arm64": "2.0.0-next.46",
90
+ "houdini-react-wasm": "2.0.0-next.46"
91
91
  },
92
92
  "scripts": {
93
93
  "compile": "scripts build-go",
package/postInstall.js CHANGED
@@ -5,7 +5,7 @@ const https = require('https')
5
5
  const child_process = require('child_process')
6
6
 
7
7
  // Adjust the version you want to install. You can also make this dynamic.
8
- const BINARY_DISTRIBUTION_VERSION = '2.0.0-next.41'
8
+ const BINARY_DISTRIBUTION_VERSION = '2.0.0-next.46'
9
9
 
10
10
  // Windows binaries end with .exe so we need to special case them.
11
11
  const binaryName = process.platform === 'win32' ? 'houdini-react.exe' : 'houdini-react'
package/runtime/Link.tsx CHANGED
@@ -1,81 +1,50 @@
1
1
  // this file is generated by houdini — do not edit
2
2
  // @refresh reset
3
3
  import type { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
4
+ import { getCurrentConfig } from '$houdini/runtime/config'
4
5
  import React from 'react'
5
6
 
6
7
  // @ts-ignore
7
- import type rawManifest from './manifest.js'
8
- // @ts-ignore
9
- import type { RouteScalars } from './manifest.js'
10
-
11
- import { resolveHref } from './resolve-href.js'
12
-
13
- type _Pages = (typeof rawManifest)['pages']
14
- type _TSType<T extends string> = T extends keyof RouteScalars
15
- ? RouteScalars[T]
16
- : T extends 'Int' | 'Float'
17
- ? number
18
- : T extends 'ID'
19
- ? string | number
20
- : T extends 'Boolean'
21
- ? boolean
22
- : string
23
- type _Param = { readonly name: string; readonly type: string; readonly optional: boolean }
24
- type _ParamObj<Ps extends readonly _Param[]> = {
25
- [P in Ps[number] as P['optional'] extends true ? P['name'] : never]?: _TSType<P['type']>
26
- } & {
27
- [P in Ps[number] as P['optional'] extends true ? never : P['name']]: _TSType<P['type']>
28
- }
29
-
30
- type _ExternalHref =
31
- | `http://${string}`
32
- | `https://${string}`
33
- | `mailto:${string}`
34
- | `tel:${string}`
35
- | `blob:${string}`
36
- | `data:${string}`
37
- | `//${string}`
38
- | `#${string}`
39
- | `./${string}`
40
- | `../${string}`
8
+ import manifest from './manifest.js'
9
+ import type { RouteHrefs, ExternalHref, ParamsForRoute, SearchForRoute } from './routes.js'
41
10
 
42
- // All known app route URL strings — useful as a constraint for custom link wrappers.
43
- export type RouteHrefs = _Pages[keyof _Pages] extends { readonly url: infer U extends string }
44
- ? U
45
- : never
11
+ import { buildHref, type RouteHrefInfo } from './resolve-href.js'
46
12
 
47
- // Separate 'to' from 'params' so TypeScript evaluates them independently:
48
- // - 'to' completions show all routes (including parameterized) because 'to' itself is always valid
49
- // - 'params' is a separate intersection that errors when required but absent
50
- type _PageForRoute<H extends string> = Extract<_Pages[keyof _Pages], { readonly url: H }>
51
- type _ParamsForRoute<H extends string> = [_PageForRoute<H>] extends [never]
52
- ? { params?: never }
53
- : _PageForRoute<H> extends { readonly params: readonly [] }
54
- ? { params?: never }
55
- : _PageForRoute<H> extends { readonly params: infer Ps extends readonly _Param[] }
56
- ? { params: _ParamObj<Ps> }
57
- : { params?: never }
13
+ // re-exported for custom link wrappers that constrain their own `to` prop
14
+ export type { RouteHrefs }
58
15
 
59
- export type LinkProps<H extends RouteHrefs | _ExternalHref = RouteHrefs | _ExternalHref> = Omit<
16
+ export type LinkProps<H extends RouteHrefs | ExternalHref = RouteHrefs | ExternalHref> = Omit<
60
17
  DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>,
61
18
  'href'
62
19
  > & {
63
20
  to: H
64
21
  disabled?: boolean
65
22
  preload?: boolean | 'data' | 'component' | 'page'
66
- } & _ParamsForRoute<H>
23
+ } & ParamsForRoute<H> &
24
+ SearchForRoute<H>
67
25
 
68
- export function Link<H extends RouteHrefs | _ExternalHref>({
26
+ export function Link<H extends RouteHrefs | ExternalHref>({
69
27
  to,
70
28
  params,
29
+ search,
71
30
  disabled,
72
31
  preload,
73
32
  ...rest
74
33
  }: LinkProps<H>): React.ReactElement {
34
+ // look up the destination route (O(1) via the codegen'd pagesByUrl map) so custom-scalar
35
+ // param/search values can be marshaled into their transport form (e.g. a Date →
36
+ // timestamp) before they hit the URL. External hrefs won't match a page, so marshalers
37
+ // stay empty.
38
+ const m = manifest as any
39
+ const page = m.pages[m.pagesByUrl[to as string]] as RouteHrefInfo | undefined
75
40
  const href = disabled
76
41
  ? undefined
77
- : params != null
78
- ? resolveHref(to as string, params as Record<string, string | number | boolean>)
79
- : (to as string)
42
+ : buildHref(
43
+ to as string,
44
+ page,
45
+ getCurrentConfig()?.scalars,
46
+ params as Record<string, unknown> | undefined,
47
+ search as Record<string, unknown> | undefined
48
+ )
80
49
  return React.createElement('a', { ...rest, href, 'data-houdini-preload': preload })
81
50
  }
@@ -16,7 +16,7 @@ import type {
16
16
  import type { DocumentStore } from 'houdini/runtime/client'
17
17
  import React from 'react'
18
18
 
19
- import { useClient, useLocation, useSession } from '../routing/Router.js'
19
+ import { useClient, useLocationContext, useSession } from '../routing/Router.js'
20
20
 
21
21
  export function useDocumentHandle<
22
22
  _Artifact extends QueryArtifact,
@@ -36,7 +36,7 @@ export function useDocumentHandle<
36
36
  // Stable cursor stacks for SinglePage pagination — must survive re-renders caused by store updates
37
37
  const previousCursorsRef = React.useRef<(string | null)[]>([])
38
38
  const nextCursorsRef = React.useRef<(string | null)[]>([])
39
- const location = useLocation()
39
+ const location = useLocationContext()
40
40
 
41
41
  // grab the current session value
42
42
  const [session] = useSession()
@@ -234,5 +234,10 @@ type RefetchHandlers<_Artifact extends QueryArtifact, _Data extends GraphQLObjec
234
234
  loadNext: OffsetHandlers<_Data, _Input>['loadNextPage']
235
235
  loadNextPending: boolean
236
236
  }
237
- : // the artifact does not support a known pagination method, don't add anything
238
- {}
237
+ : // a @refetchable fragment: embedded query keyed by id, re-run with new args
238
+ _Artifact extends { refetch: { paginated: false } }
239
+ ? {
240
+ refetch: (variables?: Partial<_Input>) => Promise<_Data>
241
+ }
242
+ : // the artifact does not support a known pagination method, don't add anything
243
+ {}
@@ -4,12 +4,36 @@ import type {
4
4
  GraphQLVariables,
5
5
  FragmentArtifact,
6
6
  QueryResult,
7
+ SubscriptionSpec,
7
8
  } from 'houdini/runtime'
8
9
  import * as React from 'react'
9
10
 
10
11
  import { useRouterContext } from '../routing/index.js'
11
12
  import { useDocumentSubscription } from './useDocumentSubscription.js'
12
13
 
14
+ // useFragment reads a fragment's data back out of the cache. When the fragment is marked
15
+ // @plural it is spread on a list field, so the reference is an array of fragment references
16
+ // and the hook returns an array of data (see usePluralFragment below).
17
+
18
+ // plural overloads: the reference is an array of fragment references
19
+ export function useFragment<
20
+ _Data extends GraphQLObject,
21
+ _ReferenceType extends {},
22
+ _Input extends GraphQLVariables = GraphQLVariables,
23
+ >(
24
+ reference: ReadonlyArray<_Data | { ' $fragments': _ReferenceType }>,
25
+ document: { artifact: FragmentArtifact }
26
+ ): _Data[]
27
+ export function useFragment<
28
+ _Data extends GraphQLObject,
29
+ _ReferenceType extends {},
30
+ _Input extends GraphQLVariables = GraphQLVariables,
31
+ >(
32
+ reference: ReadonlyArray<_Data | { ' $fragments': _ReferenceType }> | null,
33
+ document: { artifact: FragmentArtifact }
34
+ ): _Data[] | null
35
+
36
+ // singular overload: the reference is a single fragment reference
13
37
  export function useFragment<
14
38
  _Data extends GraphQLObject,
15
39
  _ReferenceType extends {},
@@ -17,6 +41,35 @@ export function useFragment<
17
41
  >(
18
42
  reference: _Data | { ' $fragments': _ReferenceType } | null,
19
43
  document: { artifact: FragmentArtifact }
44
+ ): _Data | null
45
+
46
+ export function useFragment(reference: any, document: { artifact: FragmentArtifact }): any {
47
+ const plural = Boolean(document.artifact.plural)
48
+
49
+ // a non-plural fragment given a list of references is a mistake (the fragment needs
50
+ // @plural to be read as an array)
51
+ if (!plural && Array.isArray(reference)) {
52
+ throw new Error(
53
+ `useFragment received a list of references for "${document.artifact.name}", but it is not marked @plural.`
54
+ )
55
+ }
56
+
57
+ // Both implementations run on every render so the rules of hooks are preserved; the one
58
+ // that isn't relevant is fed a null reference and becomes a no-op. Which result we return
59
+ // is keyed off the static @plural artifact flag, so a given call-site is always consistent.
60
+ const singularResult = useSingularFragment(plural ? null : reference, document)
61
+ const pluralResult = usePluralFragment(plural ? (reference ?? null) : null, document)
62
+
63
+ return plural ? pluralResult : singularResult
64
+ }
65
+
66
+ function useSingularFragment<
67
+ _Data extends GraphQLObject,
68
+ _ReferenceType extends {},
69
+ _Input extends GraphQLVariables = GraphQLVariables,
70
+ >(
71
+ reference: _Data | { ' $fragments': _ReferenceType } | null,
72
+ document: { artifact: FragmentArtifact }
20
73
  ): _Data | null {
21
74
  const { cache } = useRouterContext()
22
75
  const { parent, variables, loading } = fragmentReference<_Data, _Input, _ReferenceType>(
@@ -67,7 +120,9 @@ export function useFragment<
67
120
  artifact: document.artifact,
68
121
  variables,
69
122
  initialValue: cachedValue,
70
- disabled: loading,
123
+ // no parent means there is nothing to subscribe to (eg a null reference, or this
124
+ // singular hook running in no-op mode for a @plural fragment)
125
+ disabled: loading || !parent,
71
126
  send: {
72
127
  stuff: {
73
128
  parentID: parent,
@@ -80,6 +135,119 @@ export function useFragment<
80
135
  return storeValue.data
81
136
  }
82
137
 
138
+ // usePluralFragment consumes a @plural fragment: the reference is an array of fragment
139
+ // references (one per item in the list the fragment was spread on). Each item is bound to
140
+ // its own cache record, so we keep the list in state and register one cache subscription
141
+ // per item inside a single effect. A subscription message carries the new value for just
142
+ // that record, so we patch it into the list in place rather than re-reading everything.
143
+ // Doing the subscriptions in one effect keeps the hook count stable regardless of how many
144
+ // items the list contains (we can't call a hook per item).
145
+ function usePluralFragment<
146
+ _Data extends GraphQLObject,
147
+ _ReferenceType extends {},
148
+ _Input extends GraphQLVariables = GraphQLVariables,
149
+ >(
150
+ references: ReadonlyArray<_Data | { ' $fragments': _ReferenceType }> | null,
151
+ document: { artifact: FragmentArtifact }
152
+ ): _Data[] | null {
153
+ const { cache } = useRouterContext()
154
+ const artifact = document.artifact
155
+
156
+ // resolve the cache record + variables for each reference in the list
157
+ // biome-ignore lint/correctness/useExhaustiveDependencies: document is a stable import
158
+ const entries = React.useMemo(
159
+ () =>
160
+ references
161
+ ? references.map((reference) =>
162
+ fragmentReference<_Data, _Input, _ReferenceType>(reference, document)
163
+ )
164
+ : null,
165
+ [references]
166
+ )
167
+
168
+ // a stable key describing which records (and variables) we are bound to, so we only
169
+ // re-seed and re-subscribe when the set of records actually changes (eg an insert/remove)
170
+ const subscriptionKey = entries
171
+ ? entries
172
+ .map((entry) => `${entry.parent}:${JSON.stringify(entry.variables ?? {})}`)
173
+ .join('|')
174
+ : ''
175
+
176
+ // read every item from the cache. used to seed the list, and to re-seed it when the set
177
+ // of records changes (membership changes don't necessarily message the existing records).
178
+ const readAll = (): _Data[] | null => {
179
+ if (!references || !entries) {
180
+ return null
181
+ }
182
+ return entries.map(({ parent, variables, loading }, i) =>
183
+ parent
184
+ ? (cache.read({ selection: artifact.selection, parent, variables, loading })
185
+ .data as _Data)
186
+ : (references[i] as _Data)
187
+ )
188
+ }
189
+
190
+ const [data, setData] = React.useState<_Data[] | null>(readAll)
191
+
192
+ // the subscriptionKey the current `data` was seeded for; starts matching the initial
193
+ // useState seed so we don't redundantly re-seed on mount
194
+ const seededKey = React.useRef(subscriptionKey)
195
+
196
+ // re-seed when the set of records changes, then subscribe to each record individually.
197
+ // onMessage carries the new value for just that record, so we patch it in place.
198
+ // biome-ignore lint/correctness/useExhaustiveDependencies: keyed on subscriptionKey
199
+ React.useEffect(() => {
200
+ if (seededKey.current !== subscriptionKey) {
201
+ seededKey.current = subscriptionKey
202
+ setData(readAll())
203
+ }
204
+
205
+ if (!entries) {
206
+ return
207
+ }
208
+
209
+ const specs: SubscriptionSpec[] = []
210
+ entries.forEach(({ parent, variables, loading }, index) => {
211
+ if (!parent || loading) {
212
+ return
213
+ }
214
+ const spec: SubscriptionSpec = {
215
+ rootType: artifact.rootType,
216
+ kind: artifact.kind,
217
+ selection: artifact.selection,
218
+ parentID: parent,
219
+ variables: () => variables ?? {},
220
+ onMessage: (message) => {
221
+ // only a cache write ('update') carries a new value for this record. other
222
+ // messages (eg 'refetch' from cache.refresh()) don't apply to a fragment,
223
+ // which has no refetch of its own; the refreshed value arrives as an 'update'.
224
+ if (message.kind !== 'update') {
225
+ return
226
+ }
227
+ setData((current) => {
228
+ if (!current) {
229
+ return current
230
+ }
231
+ const next = current.slice()
232
+ next[index] = message.data as _Data
233
+ return next
234
+ })
235
+ },
236
+ }
237
+ cache.subscribe(spec)
238
+ specs.push(spec)
239
+ })
240
+
241
+ return () => {
242
+ for (const spec of specs) {
243
+ cache.unsubscribe(spec)
244
+ }
245
+ }
246
+ }, [subscriptionKey])
247
+
248
+ return data
249
+ }
250
+
83
251
  export function fragmentReference<_Data extends GraphQLObject, _Input, _ReferenceType extends {}>(
84
252
  reference: _Data | { ' $fragments': _ReferenceType } | null,
85
253
  document: { artifact: FragmentArtifact }
@@ -1,4 +1,10 @@
1
- import { extractPageInfo, cursorHandlers, offsetHandlers } from 'houdini/runtime'
1
+ import {
2
+ extractPageInfo,
3
+ cursorHandlers,
4
+ offsetHandlers,
5
+ getCurrentConfig,
6
+ entityRefetchVariables,
7
+ } from 'houdini/runtime'
2
8
  import type {
3
9
  GraphQLObject,
4
10
  FragmentArtifact,
@@ -25,7 +31,10 @@ export function useFragmentHandle<
25
31
  document: { artifact: FragmentArtifact; refetchArtifact?: QueryArtifact }
26
32
  ): any {
27
33
  const fragmentData = useFragment<_Data, _ReferenceType, _Input>(reference, document)
28
- const { variables } = fragmentReference<_Data, _Input, _ReferenceType>(reference, document)
34
+ const { variables, loading } = fragmentReference<_Data, _Input, _ReferenceType>(
35
+ reference,
36
+ document
37
+ )
29
38
 
30
39
  const client = useClient()
31
40
  const [session] = useSession()
@@ -71,8 +80,44 @@ export function useFragmentHandle<
71
80
  return (paginationData as any)[rootField] ?? null
72
81
  }, [paginationData, refetchArtifact])
73
82
 
83
+ // @refetchable fragments embed the fragment in a query keyed by id (paginated: false).
84
+ // We observe that query so refetch() can swap in fresh data computed with new argument
85
+ // values, just like SinglePage pagination swaps in the latest page.
86
+ const isRefetchable = !!refetchArtifact?.refetch && !refetchArtifact.refetch.paginated
87
+
88
+ const refetchObserver = React.useMemo(() => {
89
+ if (!isRefetchable || !refetchArtifact) return null
90
+ return client.observe<_Data, _Input>({ artifact: refetchArtifact })
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, [refetchArtifact?.name, isRefetchable])
93
+
94
+ const subscribeToRefetch = React.useCallback(
95
+ (fn: () => void) => refetchObserver?.subscribe(() => fn()) ?? (() => {}),
96
+ [refetchObserver]
97
+ )
98
+ const getRefetchSnapshot = React.useCallback(
99
+ () => refetchObserver?.state.data ?? null,
100
+ [refetchObserver]
101
+ )
102
+ const refetchData = React.useSyncExternalStore(
103
+ subscribeToRefetch,
104
+ getRefetchSnapshot,
105
+ getRefetchSnapshot
106
+ )
107
+
108
+ const refetchEntityData = React.useMemo<_Data | null>(() => {
109
+ if (!refetchData || !refetchArtifact?.selection?.fields) return null
110
+ const rootField = Object.keys(refetchArtifact.selection.fields)[0]
111
+ if (!rootField) return null
112
+ return (refetchData as any)[rootField] ?? null
113
+ }, [refetchData, refetchArtifact])
114
+
74
115
  const displayData =
75
- isSinglePage && paginationEntityData !== null ? paginationEntityData : fragmentData
116
+ isSinglePage && paginationEntityData !== null
117
+ ? paginationEntityData
118
+ : isRefetchable && refetchEntityData !== null
119
+ ? refetchEntityData
120
+ : fragmentData
76
121
 
77
122
  const wrapLoad = <_Result>(
78
123
  setLoading: (val: boolean) => void,
@@ -130,6 +175,8 @@ export function useFragmentHandle<
130
175
  getState: () => displayData as _Data | null,
131
176
  getVariables: () =>
132
177
  (paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
178
+ // no-op pagination while the parent is still in its @loading state (issue #1408)
179
+ getLoading: () => loading,
133
180
  fetch: fetchFn,
134
181
  fetchUpdate,
135
182
  getSession: async () => session,
@@ -154,6 +201,8 @@ export function useFragmentHandle<
154
201
  getState: () => displayData as _Data | null,
155
202
  getVariables: () =>
156
203
  (paginationObserver.state.variables ?? variables) as NonNullable<_Input>,
204
+ // no-op pagination while the parent is still in its @loading state (issue #1408)
205
+ getLoading: () => loading,
157
206
  storeName: refetchArtifact.name,
158
207
  fetch: fetchFn,
159
208
  fetchUpdate: async (args: any, updates = ['append']) =>
@@ -168,11 +217,58 @@ export function useFragmentHandle<
168
217
  }
169
218
 
170
219
  return null
171
- }, [refetchArtifact, paginationObserver, displayData, session, forwardPending, backwardPending])
220
+ }, [
221
+ refetchArtifact,
222
+ paginationObserver,
223
+ displayData,
224
+ session,
225
+ forwardPending,
226
+ backwardPending,
227
+ loading,
228
+ ])
229
+
230
+ // the fragment's current argument values: the initial args overlaid with everything that
231
+ // has been passed to refetch() so far. we track these explicitly rather than reading them
232
+ // back off the embedded query, whose variables also carry the synthetic id-lookup keys.
233
+ const [refetchArgs, setRefetchArgs] = React.useState<Partial<_Input>>({})
234
+
235
+ // re-run the embedded query with new argument values. the entity's id is derived from
236
+ // the fragment data (the parent reference), which always carries the visible id. we must
237
+ // NOT derive it from displayData: after a refetch that becomes the embedded query result,
238
+ // which masks the entity's id out of its selection, so reading it back would yield
239
+ // `id: undefined` and clobber the real id on a second refetch.
240
+ const refetch = React.useMemo(() => {
241
+ if (!isRefetchable || !refetchObserver || !refetchArtifact) return undefined
242
+ return (newVariables?: _Input) => {
243
+ setRefetchArgs((prev) => ({ ...prev, ...newVariables }))
244
+ const idVariables = entityRefetchVariables(
245
+ getCurrentConfig(),
246
+ refetchArtifact.refetch?.targetType,
247
+ fragmentData as Record<string, any> | null
248
+ )
249
+ return refetchObserver.send({
250
+ variables: {
251
+ ...(refetchObserver.state.variables ?? variables),
252
+ ...idVariables,
253
+ ...newVariables,
254
+ } as _Input,
255
+ // suppress loading-state placeholder data during the transition so the
256
+ // currently displayed value stays put until the fresh result arrives
257
+ stuff: { silenceLoading: true },
258
+ cacheParams: { disableSubscriptions: true, disablePartial: true },
259
+ session,
260
+ })
261
+ }
262
+ }, [isRefetchable, refetchObserver, refetchArtifact, fragmentData, variables, session])
263
+
264
+ const displayVariables = isRefetchable
265
+ ? ({ ...(variables as Record<string, any>), ...refetchArgs } as _Input)
266
+ : variables
172
267
 
173
268
  return {
174
269
  ...handle,
175
- variables,
270
+ variables: displayVariables,
176
271
  data: displayData,
272
+ refetch,
177
273
  }
178
274
  }
@@ -5,7 +5,7 @@ import { useSubscriptionHandle } from './useSubscriptionHandle.js'
5
5
  // a hook to subscribe to a subscription artifact
6
6
  export function useSubscription<_Result extends GraphQLObject, _Input extends GraphQLVariables>(
7
7
  document: { artifact: SubscriptionArtifact },
8
- variables: _Input
8
+ variables?: _Input
9
9
  ) {
10
10
  const { data } = useSubscriptionHandle(document, variables)
11
11
  return data
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  SubscriptionArtifact,
3
+ DocumentArtifact,
3
4
  GraphQLObject,
4
5
  GraphQLVariables,
5
6
  GraphQLError,
@@ -18,20 +19,23 @@ export type SubscriptionHandle<_Result extends GraphQLObject, _Input extends Gra
18
19
 
19
20
  // a hook to subscribe to a subscription artifact
20
21
  export function useSubscriptionHandle<
21
- _Result extends GraphQLObject,
22
- _Input extends GraphQLVariables,
23
- >({ artifact }: { artifact: SubscriptionArtifact }, variables: _Input) {
22
+ _Result extends GraphQLObject = GraphQLObject,
23
+ _Input extends GraphQLVariables = GraphQLVariables,
24
+ >(
25
+ { artifact }: { artifact: SubscriptionArtifact },
26
+ variables?: _Input
27
+ ): SubscriptionHandle<_Result, _Input> {
24
28
  // a subscription is basically just a live document
25
- const [storeValue, observer] = useDocumentSubscription({
29
+ const [storeValue, observer] = useDocumentSubscription<DocumentArtifact, _Result, _Input>({
26
30
  artifact,
27
- variables,
31
+ variables: variables as _Input,
28
32
  })
29
33
 
30
34
  return {
31
35
  data: storeValue.data,
32
36
  errors: storeValue.errors,
33
37
  fetching: storeValue.fetching,
34
- variables,
38
+ variables: variables as _Input,
35
39
  unlisten: observer.cleanup,
36
40
  listen: observer.send,
37
41
  }
@@ -20,6 +20,7 @@ declare global {
20
20
  __houdini__pending_artifacts__?: Record<string, QueryArtifact>
21
21
  __houdini__pending_data__?: Record<string, any>
22
22
  __houdini__pending_variables__?: Record<string, GraphQLVariables>
23
+ __houdini__pending_cache__?: any[]
23
24
  __houdini__nav_caches__?: RouterCache
24
25
  }
25
26
  }
@@ -57,6 +58,22 @@ export function hydrate_page(
57
58
  window.__houdini__hydration__layer__
58
59
  )
59
60
 
61
+ // apply any cache snapshots that streamed in before this module ran. an @loading query
62
+ // resolves while the document is still open, so its resolution script runs before this
63
+ // (deferred) module and couldn't hydrate the cache directly — it queued its snapshot here
64
+ // instead. drain the queue now that the cache exists so the observers below read the
65
+ // resolved data rather than the loading-state placeholder. each snapshot goes into its own
66
+ // layer (hydrate() replaces a layer's contents wholesale, so reusing one would keep only
67
+ // the last snapshot) and is then merged down so we end up with a single hydration layer.
68
+ const storage = window.__houdini__cache__?._internal_unstable.storage
69
+ for (const snapshot of window.__houdini__pending_cache__ ?? []) {
70
+ const layer = window.__houdini__cache__?.hydrate(snapshot)
71
+ if (layer && storage) {
72
+ storage.resolveLayer(layer.id)
73
+ }
74
+ }
75
+ window.__houdini__pending_cache__ = []
76
+
60
77
  // prime the data/artifact caches from anything the server streamed
61
78
  const initialData: Record<string, any> = {}
62
79
  const initialArtifacts: Record<string, QueryArtifact> = {}
@@ -112,7 +129,7 @@ export function hydrate_page(
112
129
  hydrateRoot(
113
130
  document,
114
131
  <App
115
- initialURL={window.location.pathname}
132
+ initialURL={window.location.pathname + window.location.search}
116
133
  cache={window.__houdini__cache__}
117
134
  session={window.__houdini__initial__session__}
118
135
  {...window.__houdini__nav_caches__}
@@ -145,7 +162,7 @@ export function mount_static_app(App: React.ComponentType<any>, manifest: any) {
145
162
 
146
163
  root.render(
147
164
  React.createElement(App, {
148
- initialURL: window.location.pathname,
165
+ initialURL: window.location.pathname + window.location.search,
149
166
  cache: cacheRef,
150
167
  session: null,
151
168
  manifest,
package/runtime/index.tsx CHANGED
@@ -10,9 +10,7 @@ export {
10
10
  router_cache,
11
11
  useCache,
12
12
  useSession,
13
- useLocation,
14
13
  useRoute,
15
- useCurrentVariables,
16
14
  notFound,
17
15
  unauthorized,
18
16
  forbidden,
@@ -23,7 +21,9 @@ export {
23
21
  RoutingError,
24
22
  RedirectError,
25
23
  } from './routing/index.js'
24
+ export type { GenericRoute } from './routing/index.js'
26
25
  export * from './Link.js'
26
+ export { createMock } from './mock.js'
27
27
 
28
28
  export function Router({
29
29
  cache,
@@ -1,4 +1,4 @@
1
- import type { RouterManifest } from 'houdini/runtime'
1
+ import type { RouterManifest } from 'houdini/router/types'
2
2
 
3
3
  // @ts-expect-error: this file will get replaced by the build system
4
4
  const manifest: RouterManifest = {}
@@ -0,0 +1,9 @@
1
+ import type React from 'react'
2
+
3
+ export function createMock(_args: {
4
+ url: string
5
+ params: any
6
+ data: any
7
+ }): React.ComponentType<{}> {
8
+ throw new Error('createMock: no routes have been generated yet. Run `houdini generate` first.')
9
+ }