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

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 +821 -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 +1384 -2059
  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 +1486 -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 +294 -322
  49. package/src/router.ts +1047 -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,346 @@
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
+ matchId: 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
+ matchId: opts.matchId,
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
186
200
 
187
- routeMatch.search = replaceEqualDeep(parentSearch, {
188
- ...parentSearch,
189
- ...nextSearch,
201
+ this.#setLoaderData(data)
202
+ }
203
+
204
+ this.store.setState((s) => {
205
+ s.error = undefined
206
+ s.status = 'success'
207
+ s.updatedAt = Date.now()
208
+ s.invalidAt =
209
+ s.updatedAt +
210
+ (opts?.maxAge ??
211
+ this.route.options.loaderMaxAge ??
212
+ this.router.options.defaultLoaderMaxAge ??
213
+ 0)
190
214
  })
191
- } catch (err: any) {
192
- console.error(err)
193
- const error = new (Error as any)('Invalid search params found', {
194
- cause: err,
215
+
216
+ return this.store.state.routeLoaderData
217
+ } catch (err) {
218
+ if ((latestPromise = checkLatest())) return latestPromise
219
+
220
+ if (process.env.NODE_ENV !== 'production') {
221
+ console.error(err)
222
+ }
223
+
224
+ this.store.setState((s) => {
225
+ s.error = err
226
+ s.status = 'error'
227
+ s.updatedAt = Date.now()
195
228
  })
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
229
 
232
- router.matchCache[routeMatch.matchId] = {
233
- gc: now + loaderOpts.gcMaxAge,
234
- match: routeMatch as RouteMatch<any, any>,
230
+ throw err
235
231
  }
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
232
+ })
245
233
 
246
- routeMatch.fetch({ maxAge })
234
+ const after = async () => {
235
+ if ((latestPromise = checkLatest())) return latestPromise
236
+ this.store.setState((s) => (s.isFetching = false))
237
+ this.#resolve()
238
+ delete this.__loadPromise
247
239
  }
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'
240
+
241
+ try {
242
+ await Promise.all([componentsPromise, dataPromise.catch(() => {})])
243
+ after()
244
+ } catch {
245
+ after()
258
246
  }
247
+ })
259
248
 
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
- })
249
+ return this.__loadPromise
250
+ }
251
+ invalidate = async () => {
252
+ this.store.setState((s) => (s.invalid = true))
253
+ if (this.router.store.state.currentMatches.find((d) => d.id === this.id)) {
254
+ await this.load()
255
+ }
256
+ }
257
+ __hasLoaders = () => {
258
+ return !!(
259
+ this.route.options.loader ||
260
+ componentTypes.some((d) => this.route.options[d]?.preload)
261
+ )
262
+ }
263
+ getIsInvalid = () => {
264
+ const now = Date.now()
265
+ return this.store.state.invalid || this.store.state.invalidAt < now
266
+ }
331
267
 
332
- try {
333
- await Promise.all([
334
- routeMatch.__.elementsPromise,
335
- routeMatch.__.dataPromise,
336
- ])
337
- if (id !== routeMatch.__.latestId) {
338
- return routeMatch.__.loaderPromise
339
- }
268
+ #updateLoaderData = () => {
269
+ this.store.setState((s) => {
270
+ s.loaderData = replaceEqualDeep(s.loaderData, {
271
+ ...this.parentMatch?.store.state.loaderData,
272
+ ...s.routeLoaderData,
273
+ }) as TRouteInfo['loaderData']
274
+ })
275
+ this.onLoaderDataListeners.forEach((listener) => listener())
276
+ }
340
277
 
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
- })()
278
+ __setParentMatch = (parentMatch?: RouteMatch) => {
279
+ if (!this.parentMatch && parentMatch) {
280
+ this.parentMatch = parentMatch
281
+ this.parentMatch.__onLoaderData(() => {
282
+ this.#updateLoaderData()
283
+ })
284
+ }
285
+ }
286
+
287
+ __onLoaderData = (listener: () => void) => {
288
+ this.onLoaderDataListeners.add(listener)
289
+ // return () => this.onLoaderDataListeners.delete(listener)
290
+ }
291
+
292
+ __validate = () => {
293
+ // Validate the search params and stabilize them
294
+ const parentSearch =
295
+ this.parentMatch?.store.state.search ??
296
+ this.router.store.state.latestLocation.search
297
+
298
+ try {
299
+ const prevSearch = this.store.state.routeSearch
300
+
301
+ const validator =
302
+ typeof this.route.options.validateSearch === 'object'
303
+ ? this.route.options.validateSearch.parse
304
+ : this.route.options.validateSearch
305
+
306
+ let nextSearch = validator?.(parentSearch) ?? {}
307
+
308
+ batch(() => {
309
+ // Invalidate route matches when search param stability changes
310
+ if (prevSearch !== nextSearch) {
311
+ this.store.setState((s) => (s.invalid = true))
312
+ }
313
+
314
+ this.store.setState((s) => {
315
+ s.routeSearch = nextSearch
316
+ s.search = {
317
+ ...parentSearch,
318
+ ...nextSearch,
319
+ } as any
320
+ })
321
+ })
355
322
 
356
- routeMatch.__.loaderPromise = loaderPromise
357
- await loaderPromise
323
+ componentTypes.map(async (type) => {
324
+ const component = this.route.options[type]
358
325
 
359
- if (id !== routeMatch.__.latestId) {
360
- return routeMatch.__.loaderPromise
326
+ if (typeof this[type] !== 'function') {
327
+ this[type] = component
361
328
  }
362
- delete routeMatch.__.loaderPromise
363
329
  })
330
+ } catch (err: any) {
331
+ console.error(err)
332
+ const error = new (Error as any)('Invalid search params found', {
333
+ cause: err,
334
+ })
335
+ error.code = 'INVALID_SEARCH_PARAMS'
364
336
 
365
- return await routeMatch.__.loadPromise
366
- },
367
- }
337
+ this.store.setState((s) => {
338
+ s.status = 'error'
339
+ s.error = error
340
+ })
368
341
 
369
- if (!routeMatch.hasLoaders()) {
370
- routeMatch.status = 'success'
342
+ // Do not proceed with loading the route
343
+ return
344
+ }
371
345
  }
372
-
373
- return routeMatch
374
346
  }