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 +8 -8
- package/postInstall.js +1 -1
- package/runtime/Link.tsx +24 -55
- package/runtime/hooks/useDocumentHandle.ts +9 -4
- package/runtime/hooks/useFragment.ts +169 -1
- package/runtime/hooks/useFragmentHandle.ts +101 -5
- package/runtime/hooks/useSubscription.ts +1 -1
- package/runtime/hooks/useSubscriptionHandle.ts +10 -6
- package/runtime/hydration.tsx +19 -2
- package/runtime/index.tsx +2 -2
- package/runtime/manifest.ts +1 -1
- package/runtime/mock.ts +9 -0
- package/runtime/resolve-href.ts +155 -5
- package/runtime/routes.ts +93 -0
- package/runtime/routing/Router.tsx +130 -46
- package/runtime/testing.tsx +163 -0
- package/vite/index.js +11 -3
- package/vite/strip-headers.d.ts +1 -0
- package/vite/strip-headers.js +35 -0
- package/vite/transform.d.ts +3 -1
- package/vite/transform.js +18 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "houdini-react",
|
|
3
|
-
"version": "2.0.0-next.
|
|
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.
|
|
85
|
-
"houdini-react-darwin-arm64": "2.0.0-next.
|
|
86
|
-
"houdini-react-linux-x64": "2.0.0-next.
|
|
87
|
-
"houdini-react-linux-arm64": "2.0.0-next.
|
|
88
|
-
"houdini-react-win32-x64": "2.0.0-next.
|
|
89
|
-
"houdini-react-win32-arm64": "2.0.0-next.
|
|
90
|
-
"houdini-react-wasm": "2.0.0-next.
|
|
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.
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
48
|
-
|
|
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 |
|
|
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
|
-
} &
|
|
23
|
+
} & ParamsForRoute<H> &
|
|
24
|
+
SearchForRoute<H>
|
|
67
25
|
|
|
68
|
-
export function Link<H extends RouteHrefs |
|
|
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
|
-
:
|
|
78
|
-
|
|
79
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
: //
|
|
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
|
-
|
|
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 {
|
|
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>(
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
>(
|
|
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
|
}
|
package/runtime/hydration.tsx
CHANGED
|
@@ -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,
|
package/runtime/manifest.ts
CHANGED
package/runtime/mock.ts
ADDED