@tanstack/router-core 0.0.1-beta.5 → 0.0.1-beta.51

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/build/cjs/actions.js +94 -0
  3. package/build/cjs/actions.js.map +1 -0
  4. package/build/cjs/history.js +163 -0
  5. package/build/cjs/history.js.map +1 -0
  6. package/build/cjs/{packages/router-core/src/index.js → index.js} +26 -11
  7. package/build/cjs/{packages/router-core/src/index.js.map → index.js.map} +1 -1
  8. package/build/cjs/interop.js +175 -0
  9. package/build/cjs/interop.js.map +1 -0
  10. package/build/cjs/{packages/router-core/src/path.js → path.js} +23 -48
  11. package/build/cjs/path.js.map +1 -0
  12. package/build/cjs/{packages/router-core/src/qss.js → qss.js} +8 -13
  13. package/build/cjs/qss.js.map +1 -0
  14. package/build/cjs/route.js +33 -0
  15. package/build/cjs/route.js.map +1 -0
  16. package/build/cjs/{packages/router-core/src/routeConfig.js → routeConfig.js} +13 -18
  17. package/build/cjs/routeConfig.js.map +1 -0
  18. package/build/cjs/routeMatch.js +237 -0
  19. package/build/cjs/routeMatch.js.map +1 -0
  20. package/build/cjs/router.js +824 -0
  21. package/build/cjs/router.js.map +1 -0
  22. package/build/cjs/{packages/router-core/src/searchParams.js → searchParams.js} +10 -12
  23. package/build/cjs/searchParams.js.map +1 -0
  24. package/build/cjs/store.js +54 -0
  25. package/build/cjs/store.js.map +1 -0
  26. package/build/cjs/utils.js +47 -0
  27. package/build/cjs/utils.js.map +1 -0
  28. package/build/esm/index.js +1386 -2058
  29. package/build/esm/index.js.map +1 -1
  30. package/build/stats-html.html +59 -49
  31. package/build/stats-react.json +248 -193
  32. package/build/types/index.d.ts +385 -317
  33. package/build/umd/index.development.js +1489 -2142
  34. package/build/umd/index.development.js.map +1 -1
  35. package/build/umd/index.production.js +1 -1
  36. package/build/umd/index.production.js.map +1 -1
  37. package/package.json +6 -4
  38. package/src/actions.ts +157 -0
  39. package/src/frameworks.ts +2 -2
  40. package/src/history.ts +199 -0
  41. package/src/index.ts +4 -7
  42. package/src/interop.ts +169 -0
  43. package/src/link.ts +87 -44
  44. package/src/path.ts +12 -8
  45. package/src/route.ts +36 -229
  46. package/src/routeConfig.ts +99 -102
  47. package/src/routeInfo.ts +28 -25
  48. package/src/routeMatch.ts +293 -322
  49. package/src/router.ts +1060 -884
  50. package/src/store.ts +52 -0
  51. package/src/utils.ts +14 -72
  52. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -33
  53. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +0 -1
  54. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js +0 -33
  55. package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js.map +0 -1
  56. package/build/cjs/node_modules/history/index.js +0 -815
  57. package/build/cjs/node_modules/history/index.js.map +0 -1
  58. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js +0 -30
  59. package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js.map +0 -1
  60. package/build/cjs/packages/router-core/src/path.js.map +0 -1
  61. package/build/cjs/packages/router-core/src/qss.js.map +0 -1
  62. package/build/cjs/packages/router-core/src/route.js +0 -161
  63. package/build/cjs/packages/router-core/src/route.js.map +0 -1
  64. package/build/cjs/packages/router-core/src/routeConfig.js.map +0 -1
  65. package/build/cjs/packages/router-core/src/routeMatch.js +0 -266
  66. package/build/cjs/packages/router-core/src/routeMatch.js.map +0 -1
  67. package/build/cjs/packages/router-core/src/router.js +0 -797
  68. package/build/cjs/packages/router-core/src/router.js.map +0 -1
  69. package/build/cjs/packages/router-core/src/searchParams.js.map +0 -1
  70. package/build/cjs/packages/router-core/src/utils.js +0 -118
  71. package/build/cjs/packages/router-core/src/utils.js.map +0 -1
package/src/routeMatch.ts CHANGED
@@ -1,374 +1,345 @@
1
1
  import { GetFrameworkGeneric } from './frameworks'
2
2
  import { Route } from './route'
3
- import { AnyPathParams } from './routeConfig'
4
3
  import {
5
4
  AnyAllRouteInfo,
6
5
  AnyRouteInfo,
7
6
  DefaultAllRouteInfo,
8
7
  RouteInfo,
9
8
  } from './routeInfo'
10
- import { Router } from './router'
11
- import { replaceEqualDeep, Timeout } from './utils'
9
+ import { AnyRouter, Router } from './router'
10
+ import { batch, createStore, Store } from './store'
11
+ import { Expand } from './utils'
12
+ import { replaceEqualDeep } from './interop'
12
13
 
13
- export interface RouteMatch<
14
+ export interface RouteMatchStore<
14
15
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
15
16
  TRouteInfo extends AnyRouteInfo = RouteInfo,
16
- > extends Route<TAllRouteInfo, TRouteInfo> {
17
- matchId: string
18
- pathname: string
19
- params: TRouteInfo['params']
20
- parentMatch?: RouteMatch
21
- childMatches: RouteMatch[]
17
+ > {
22
18
  routeSearch: TRouteInfo['searchSchema']
23
- search: TRouteInfo['fullSearchSchema']
19
+ search: Expand<
20
+ TAllRouteInfo['fullSearchSchema'] & TRouteInfo['fullSearchSchema']
21
+ >
24
22
  status: 'idle' | 'loading' | 'success' | 'error'
25
23
  updatedAt?: number
26
24
  error?: unknown
27
- isInvalid: boolean
28
- getIsInvalid: () => boolean
25
+ invalid: boolean
29
26
  loaderData: TRouteInfo['loaderData']
30
27
  routeLoaderData: TRouteInfo['routeLoaderData']
31
28
  isFetching: boolean
32
- isPending: boolean
33
29
  invalidAt: number
34
- __: {
35
- element?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
36
- errorElement?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
37
- catchElement?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
38
- pendingElement?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
39
- loadPromise?: Promise<void>
40
- loaderPromise?: Promise<void>
41
- elementsPromise?: Promise<void>
42
- dataPromise?: Promise<void>
43
- pendingTimeout?: Timeout
44
- pendingMinTimeout?: Timeout
45
- pendingMinPromise?: Promise<void>
46
- onExit?:
47
- | void
48
- | ((matchContext: {
49
- params: TRouteInfo['allParams']
50
- search: TRouteInfo['fullSearchSchema']
51
- }) => void)
52
- abortController: AbortController
53
- latestId: string
54
- // setParentMatch: (parentMatch: RouteMatch) => void
55
- // addChildMatch: (childMatch: RouteMatch) => void
56
- validate: () => void
57
- startPending: () => void
58
- cancelPending: () => void
59
- notify: () => void
60
- resolve: () => void
61
- }
62
- cancel: () => void
63
- load: (
64
- loaderOpts?: { withPending?: boolean } & (
65
- | { preload: true; maxAge: number; gcMaxAge: number }
66
- | { preload?: false; maxAge?: never; gcMaxAge?: never }
67
- ),
68
- ) => Promise<TRouteInfo['routeLoaderData']>
69
- fetch: (opts?: { maxAge?: number }) => Promise<TRouteInfo['routeLoaderData']>
70
- invalidate: () => void
71
- hasLoaders: () => boolean
72
30
  }
73
31
 
74
- const elementTypes = [
75
- 'element',
76
- 'errorElement',
77
- 'catchElement',
78
- 'pendingElement',
32
+ const componentTypes = [
33
+ 'component',
34
+ 'errorComponent',
35
+ 'pendingComponent',
79
36
  ] as const
80
37
 
81
- export function createRouteMatch<
38
+ export class RouteMatch<
82
39
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
83
40
  TRouteInfo extends AnyRouteInfo = RouteInfo,
84
- >(
85
- router: Router<any, any>,
86
- route: Route<TAllRouteInfo, TRouteInfo>,
87
- opts: {
88
- matchId: string
89
- params: TRouteInfo['allParams']
90
- pathname: string
91
- },
92
- ): RouteMatch<TAllRouteInfo, TRouteInfo> {
93
- const routeMatch: RouteMatch<TAllRouteInfo, TRouteInfo> = {
94
- ...route,
95
- ...opts,
96
- router,
97
- routeSearch: {},
98
- search: {},
99
- childMatches: [],
100
- status: 'idle',
101
- routeLoaderData: {} as TRouteInfo['routeLoaderData'],
102
- loaderData: {} as TRouteInfo['loaderData'],
103
- isPending: false,
104
- isFetching: false,
105
- isInvalid: false,
106
- invalidAt: Infinity,
107
- getIsInvalid: () => {
108
- const now = Date.now()
109
- return routeMatch.isInvalid || routeMatch.invalidAt < now
41
+ > {
42
+ route!: Route<TAllRouteInfo, TRouteInfo>
43
+ router!: Router<TAllRouteInfo['routeConfig'], TAllRouteInfo>
44
+ store!: Store<RouteMatchStore<TAllRouteInfo, TRouteInfo>>
45
+ id!: string
46
+ pathname!: string
47
+ params!: TRouteInfo['allParams']
48
+
49
+ component: GetFrameworkGeneric<'Component'>
50
+ errorComponent: GetFrameworkGeneric<'ErrorComponent'>
51
+ pendingComponent: GetFrameworkGeneric<'Component'>
52
+ abortController = new AbortController()
53
+ #latestId = ''
54
+ #resolve = () => {}
55
+ onLoaderDataListeners = new Set<() => void>()
56
+ parentMatch?: RouteMatch
57
+
58
+ __loadPromise?: Promise<void>
59
+ __onExit?:
60
+ | void
61
+ | ((matchContext: {
62
+ params: TRouteInfo['allParams']
63
+ search: TRouteInfo['fullSearchSchema']
64
+ }) => void)
65
+
66
+ constructor(
67
+ router: AnyRouter,
68
+ route: Route<TAllRouteInfo, TRouteInfo>,
69
+ opts: {
70
+ id: string
71
+ params: TRouteInfo['allParams']
72
+ pathname: string
110
73
  },
111
- __: {
112
- abortController: new AbortController(),
113
- latestId: '',
114
- resolve: () => {},
115
- notify: () => {
116
- routeMatch.__.resolve()
117
- routeMatch.router.notify()
118
- },
119
- startPending: () => {
120
- const pendingMs =
121
- routeMatch.options.pendingMs ?? router.options.defaultPendingMs
122
- const pendingMinMs =
123
- routeMatch.options.pendingMinMs ?? router.options.defaultPendingMinMs
124
-
125
- if (
126
- routeMatch.__.pendingTimeout ||
127
- routeMatch.status !== 'loading' ||
128
- typeof pendingMs === 'undefined'
129
- ) {
130
- return
74
+ ) {
75
+ Object.assign(this, {
76
+ route,
77
+ router,
78
+ id: opts.id,
79
+ pathname: opts.pathname,
80
+ params: opts.params,
81
+ store: createStore<RouteMatchStore<TAllRouteInfo, TRouteInfo>>({
82
+ routeSearch: {},
83
+ search: {} as any,
84
+ status: 'idle',
85
+ routeLoaderData: {} as TRouteInfo['routeLoaderData'],
86
+ loaderData: {} as TRouteInfo['loaderData'],
87
+ isFetching: false,
88
+ invalid: false,
89
+ invalidAt: Infinity,
90
+ }),
91
+ })
92
+
93
+ if (!this.__hasLoaders()) {
94
+ this.store.setState((s) => (s.status = 'success'))
95
+ }
96
+ }
97
+
98
+ #setLoaderData = (loaderData: TRouteInfo['routeLoaderData']) => {
99
+ batch(() => {
100
+ this.store.setState((s) => {
101
+ s.routeLoaderData = loaderData
102
+ })
103
+ this.#updateLoaderData()
104
+ })
105
+ }
106
+
107
+ cancel = () => {
108
+ this.abortController?.abort()
109
+ }
110
+
111
+ load = async (
112
+ loaderOpts?:
113
+ | { preload: true; maxAge: number; gcMaxAge: number }
114
+ | { preload?: false; maxAge?: never; gcMaxAge?: never },
115
+ ): Promise<void> => {
116
+ const now = Date.now()
117
+ const minMaxAge = loaderOpts?.preload
118
+ ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge)
119
+ : 0
120
+
121
+ // If this is a preload, add it to the preload cache
122
+ if (loaderOpts?.preload && minMaxAge > 0) {
123
+ // If the match is currently active, don't preload it
124
+ if (
125
+ this.router.store.state.currentMatches.find((d) => d.id === this.id)
126
+ ) {
127
+ return
128
+ }
129
+
130
+ this.router.store.setState((s) => {
131
+ s.matchCache[this.id] = {
132
+ gc: now + loaderOpts.gcMaxAge,
133
+ match: this as RouteMatch<any, any>,
134
+ }
135
+ })
136
+ }
137
+
138
+ // If the match is invalid, errored or idle, trigger it to load
139
+ if (
140
+ (this.store.state.status === 'success' && this.getIsInvalid()) ||
141
+ this.store.state.status === 'error' ||
142
+ this.store.state.status === 'idle'
143
+ ) {
144
+ const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
145
+ await this.fetch({ maxAge })
146
+ }
147
+ }
148
+
149
+ fetch = async (opts?: {
150
+ maxAge?: number
151
+ }): Promise<TRouteInfo['routeLoaderData']> => {
152
+ this.__loadPromise = new Promise(async (resolve) => {
153
+ const loadId = '' + Date.now() + Math.random()
154
+ this.#latestId = loadId
155
+
156
+ const checkLatest = () =>
157
+ loadId !== this.#latestId
158
+ ? this.__loadPromise?.then(() => resolve())
159
+ : undefined
160
+
161
+ let latestPromise
162
+
163
+ batch(() => {
164
+ // If the match was in an error state, set it
165
+ // to a loading state again. Otherwise, keep it
166
+ // as loading or resolved
167
+ if (this.store.state.status === 'idle') {
168
+ this.store.setState((s) => (s.status = 'loading'))
131
169
  }
132
170
 
133
- routeMatch.__.pendingTimeout = setTimeout(() => {
134
- routeMatch.isPending = true
135
- routeMatch.__.resolve()
136
- if (typeof pendingMinMs !== 'undefined') {
137
- routeMatch.__.pendingMinPromise = new Promise(
138
- (r) =>
139
- (routeMatch.__.pendingMinTimeout = setTimeout(r, pendingMinMs)),
140
- )
141
- }
142
- }, pendingMs)
143
- },
144
- cancelPending: () => {
145
- routeMatch.isPending = false
146
- clearTimeout(routeMatch.__.pendingTimeout)
147
- clearTimeout(routeMatch.__.pendingMinTimeout)
148
- delete routeMatch.__.pendingMinPromise
149
- },
150
- // setParentMatch: (parentMatch?: RouteMatch) => {
151
- // routeMatch.parentMatch = parentMatch
152
- // },
153
- // addChildMatch: (childMatch: RouteMatch) => {
154
- // if (
155
- // routeMatch.childMatches.find((d) => d.matchId === childMatch.matchId)
156
- // ) {
157
- // return
158
- // }
159
-
160
- // routeMatch.childMatches.push(childMatch)
161
- // },
162
- validate: () => {
163
- // Validate the search params and stabilize them
164
- const parentSearch =
165
- routeMatch.parentMatch?.search ?? router.location.search
171
+ // We started loading the route, so it's no longer invalid
172
+ this.store.setState((s) => (s.invalid = false))
173
+ })
166
174
 
167
- try {
168
- const prevSearch = routeMatch.routeSearch
175
+ // We are now fetching, even if it's in the background of a
176
+ // resolved state
177
+ this.store.setState((s) => (s.isFetching = true))
178
+ this.#resolve = resolve as () => void
169
179
 
170
- const validator =
171
- typeof routeMatch.options.validateSearch === 'object'
172
- ? routeMatch.options.validateSearch.parse
173
- : routeMatch.options.validateSearch
180
+ const componentsPromise = (async () => {
181
+ // then run all component and data loaders in parallel
182
+ // For each component type, potentially load it asynchronously
174
183
 
175
- let nextSearch = replaceEqualDeep(
176
- prevSearch,
177
- validator?.(parentSearch),
178
- )
184
+ await Promise.all(
185
+ componentTypes.map(async (type) => {
186
+ const component = this.route.options[type]
179
187
 
180
- // Invalidate route matches when search param stability changes
181
- if (prevSearch !== nextSearch) {
182
- routeMatch.isInvalid = true
183
- }
188
+ if (this[type]?.preload) {
189
+ this[type] = await this.router.options.loadComponent!(component)
190
+ }
191
+ }),
192
+ )
193
+ })()
184
194
 
185
- routeMatch.routeSearch = nextSearch
195
+ const dataPromise = Promise.resolve().then(async () => {
196
+ try {
197
+ if (this.route.options.loader) {
198
+ const data = await this.router.loadMatchData(this)
199
+ if ((latestPromise = checkLatest())) return latestPromise
200
+ this.#setLoaderData(data)
201
+ }
186
202
 
187
- routeMatch.search = replaceEqualDeep(parentSearch, {
188
- ...parentSearch,
189
- ...nextSearch,
203
+ this.store.setState((s) => {
204
+ s.error = undefined
205
+ s.status = 'success'
206
+ s.updatedAt = Date.now()
207
+ s.invalidAt =
208
+ s.updatedAt +
209
+ (opts?.maxAge ??
210
+ this.route.options.loaderMaxAge ??
211
+ this.router.options.defaultLoaderMaxAge ??
212
+ 0)
190
213
  })
191
- } catch (err: any) {
192
- console.error(err)
193
- const error = new (Error as any)('Invalid search params found', {
194
- cause: err,
214
+
215
+ return this.store.state.routeLoaderData
216
+ } catch (err) {
217
+ if ((latestPromise = checkLatest())) return latestPromise
218
+
219
+ if (process.env.NODE_ENV !== 'production') {
220
+ console.error(err)
221
+ }
222
+
223
+ this.store.setState((s) => {
224
+ s.error = err
225
+ s.status = 'error'
226
+ s.updatedAt = Date.now()
195
227
  })
196
- error.code = 'INVALID_SEARCH_PARAMS'
197
- routeMatch.status = 'error'
198
- routeMatch.error = error
199
- // Do not proceed with loading the route
200
- return
201
- }
202
- },
203
- },
204
- cancel: () => {
205
- routeMatch.__.abortController?.abort()
206
- routeMatch.__.cancelPending()
207
- },
208
- invalidate: () => {
209
- routeMatch.isInvalid = true
210
- },
211
- hasLoaders: () => {
212
- return !!(
213
- route.options.loader ||
214
- elementTypes.some((d) => typeof route.options[d] === 'function')
215
- )
216
- },
217
- load: async (loaderOpts) => {
218
- const now = Date.now()
219
- const minMaxAge = loaderOpts?.preload
220
- ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge)
221
- : 0
222
-
223
- // If this is a preload, add it to the preload cache
224
- if (loaderOpts?.preload && minMaxAge > 0) {
225
- // If the match is currently active, don't preload it
226
- if (
227
- router.state.matches.find((d) => d.matchId === routeMatch.matchId)
228
- ) {
229
- return
230
- }
231
228
 
232
- router.matchCache[routeMatch.matchId] = {
233
- gc: now + loaderOpts.gcMaxAge,
234
- match: routeMatch as RouteMatch<any, any>,
229
+ throw err
235
230
  }
236
- }
237
-
238
- // If the match is invalid, errored or idle, trigger it to load
239
- if (
240
- (routeMatch.status === 'success' && routeMatch.getIsInvalid()) ||
241
- routeMatch.status === 'error' ||
242
- routeMatch.status === 'idle'
243
- ) {
244
- const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
231
+ })
245
232
 
246
- routeMatch.fetch({ maxAge })
233
+ const after = async () => {
234
+ if ((latestPromise = checkLatest())) return latestPromise
235
+ this.store.setState((s) => (s.isFetching = false))
236
+ this.#resolve()
237
+ delete this.__loadPromise
247
238
  }
248
- },
249
- fetch: async (opts) => {
250
- const id = '' + Date.now() + Math.random()
251
- routeMatch.__.latestId = id
252
-
253
- // If the match was in an error state, set it
254
- // to a loading state again. Otherwise, keep it
255
- // as loading or resolved
256
- if (routeMatch.status === 'idle') {
257
- routeMatch.status = 'loading'
239
+
240
+ try {
241
+ await Promise.all([componentsPromise, dataPromise.catch(() => {})])
242
+ after()
243
+ } catch {
244
+ after()
258
245
  }
246
+ })
259
247
 
260
- // We started loading the route, so it's no longer invalid
261
- routeMatch.isInvalid = false
262
-
263
- routeMatch.__.loadPromise = new Promise(async (resolve) => {
264
- // We are now fetching, even if it's in the background of a
265
- // resolved state
266
- routeMatch.isFetching = true
267
- routeMatch.__.resolve = resolve as () => void
268
-
269
- const loaderPromise = (async () => {
270
- // Load the elements and data in parallel
271
-
272
- routeMatch.__.elementsPromise = (async () => {
273
- // then run all element and data loaders in parallel
274
- // For each element type, potentially load it asynchronously
275
-
276
- await Promise.all(
277
- elementTypes.map(async (type) => {
278
- const routeElement = routeMatch.options[type]
279
-
280
- if (routeMatch.__[type]) {
281
- return
282
- }
283
-
284
- routeMatch.__[type] = await router.options.createElement!(
285
- routeElement,
286
- )
287
- }),
288
- )
289
- })()
290
-
291
- routeMatch.__.dataPromise = Promise.resolve().then(async () => {
292
- try {
293
- if (routeMatch.options.loader) {
294
- const data = await routeMatch.options.loader({
295
- params: routeMatch.params,
296
- search: routeMatch.routeSearch,
297
- signal: routeMatch.__.abortController.signal,
298
- })
299
- if (id !== routeMatch.__.latestId) {
300
- return routeMatch.__.loaderPromise
301
- }
302
-
303
- routeMatch.routeLoaderData = replaceEqualDeep(
304
- routeMatch.routeLoaderData,
305
- data,
306
- )
307
- }
308
-
309
- routeMatch.error = undefined
310
- routeMatch.status = 'success'
311
- routeMatch.updatedAt = Date.now()
312
- routeMatch.invalidAt =
313
- routeMatch.updatedAt +
314
- (opts?.maxAge ??
315
- routeMatch.options.loaderMaxAge ??
316
- router.options.defaultLoaderMaxAge ??
317
- 0)
318
- } catch (err) {
319
- if (id !== routeMatch.__.latestId) {
320
- return routeMatch.__.loaderPromise
321
- }
322
-
323
- if (process.env.NODE_ENV !== 'production') {
324
- console.error(err)
325
- }
326
- routeMatch.error = err
327
- routeMatch.status = 'error'
328
- routeMatch.updatedAt = Date.now()
329
- }
330
- })
248
+ return this.__loadPromise
249
+ }
250
+ invalidate = async () => {
251
+ this.store.setState((s) => (s.invalid = true))
252
+ if (this.router.store.state.currentMatches.find((d) => d.id === this.id)) {
253
+ await this.load()
254
+ }
255
+ }
256
+ __hasLoaders = () => {
257
+ return !!(
258
+ this.route.options.loader ||
259
+ componentTypes.some((d) => this.route.options[d]?.preload)
260
+ )
261
+ }
262
+ getIsInvalid = () => {
263
+ const now = Date.now()
264
+ return this.store.state.invalid || this.store.state.invalidAt < now
265
+ }
331
266
 
332
- try {
333
- await Promise.all([
334
- routeMatch.__.elementsPromise,
335
- routeMatch.__.dataPromise,
336
- ])
337
- if (id !== routeMatch.__.latestId) {
338
- return routeMatch.__.loaderPromise
339
- }
267
+ #updateLoaderData = () => {
268
+ this.store.setState((s) => {
269
+ s.loaderData = replaceEqualDeep(s.loaderData, {
270
+ ...this.parentMatch?.store.state.loaderData,
271
+ ...s.routeLoaderData,
272
+ }) as TRouteInfo['loaderData']
273
+ })
274
+ this.onLoaderDataListeners.forEach((listener) => listener())
275
+ }
340
276
 
341
- if (routeMatch.__.pendingMinPromise) {
342
- await routeMatch.__.pendingMinPromise
343
- delete routeMatch.__.pendingMinPromise
344
- }
345
- } finally {
346
- if (id !== routeMatch.__.latestId) {
347
- return routeMatch.__.loaderPromise
348
- }
349
- routeMatch.__.cancelPending()
350
- routeMatch.isPending = false
351
- routeMatch.isFetching = false
352
- routeMatch.__.notify()
353
- }
354
- })()
277
+ __setParentMatch = (parentMatch?: RouteMatch) => {
278
+ if (!this.parentMatch && parentMatch) {
279
+ this.parentMatch = parentMatch
280
+ this.parentMatch.__onLoaderData(() => {
281
+ this.#updateLoaderData()
282
+ })
283
+ }
284
+ }
285
+
286
+ __onLoaderData = (listener: () => void) => {
287
+ this.onLoaderDataListeners.add(listener)
288
+ // return () => this.onLoaderDataListeners.delete(listener)
289
+ }
290
+
291
+ __validate = () => {
292
+ // Validate the search params and stabilize them
293
+ const parentSearch =
294
+ this.parentMatch?.store.state.search ??
295
+ this.router.store.state.latestLocation.search
355
296
 
356
- routeMatch.__.loaderPromise = loaderPromise
357
- await loaderPromise
297
+ try {
298
+ const prevSearch = this.store.state.routeSearch
358
299
 
359
- if (id !== routeMatch.__.latestId) {
360
- return routeMatch.__.loaderPromise
300
+ const validator =
301
+ typeof this.route.options.validateSearch === 'object'
302
+ ? this.route.options.validateSearch.parse
303
+ : this.route.options.validateSearch
304
+
305
+ let nextSearch = validator?.(parentSearch) ?? {}
306
+
307
+ batch(() => {
308
+ // Invalidate route matches when search param stability changes
309
+ if (prevSearch !== nextSearch) {
310
+ this.store.setState((s) => (s.invalid = true))
361
311
  }
362
- delete routeMatch.__.loaderPromise
312
+
313
+ this.store.setState((s) => {
314
+ s.routeSearch = nextSearch
315
+ s.search = {
316
+ ...parentSearch,
317
+ ...nextSearch,
318
+ } as any
319
+ })
363
320
  })
364
321
 
365
- return await routeMatch.__.loadPromise
366
- },
367
- }
322
+ componentTypes.map(async (type) => {
323
+ const component = this.route.options[type]
368
324
 
369
- if (!routeMatch.hasLoaders()) {
370
- routeMatch.status = 'success'
371
- }
325
+ if (typeof this[type] !== 'function') {
326
+ this[type] = component
327
+ }
328
+ })
329
+ } catch (err: any) {
330
+ console.error(err)
331
+ const error = new (Error as any)('Invalid search params found', {
332
+ cause: err,
333
+ })
334
+ error.code = 'INVALID_SEARCH_PARAMS'
335
+
336
+ this.store.setState((s) => {
337
+ s.status = 'error'
338
+ s.error = error
339
+ })
372
340
 
373
- return routeMatch
341
+ // Do not proceed with loading the route
342
+ return
343
+ }
344
+ }
374
345
  }