@tanstack/router-core 0.0.1-beta.36 → 0.0.1-beta.39

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/src/routeMatch.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { boolean } from 'zod'
1
2
  import { GetFrameworkGeneric } from './frameworks'
2
3
  import { Route } from './route'
3
4
  import {
@@ -7,17 +8,15 @@ import {
7
8
  RouteInfo,
8
9
  } from './routeInfo'
9
10
  import { Router } from './router'
10
- import { Expand, replaceEqualDeep } from './utils'
11
+ import { batch, createStore } from '@solidjs/reactivity'
12
+ import { Expand } from './utils'
13
+ import { sharedClone } from './sharedClone'
11
14
 
12
- export interface RouteMatch<
15
+ export interface RouteMatchStore<
13
16
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
14
17
  TRouteInfo extends AnyRouteInfo = RouteInfo,
15
- > extends Route<TAllRouteInfo, TRouteInfo> {
16
- matchId: string
17
- pathname: string
18
- params: TRouteInfo['allParams']
18
+ > {
19
19
  parentMatch?: RouteMatch
20
- childMatches: RouteMatch[]
21
20
  routeSearch: TRouteInfo['searchSchema']
22
21
  search: Expand<
23
22
  TAllRouteInfo['fullSearchSchema'] & TRouteInfo['fullSearchSchema']
@@ -25,19 +24,39 @@ export interface RouteMatch<
25
24
  status: 'idle' | 'loading' | 'success' | 'error'
26
25
  updatedAt?: number
27
26
  error?: unknown
27
+ invalid: boolean
28
28
  isInvalid: boolean
29
- getIsInvalid: () => boolean
30
29
  loaderData: TRouteInfo['loaderData']
31
30
  routeLoaderData: TRouteInfo['routeLoaderData']
32
31
  isFetching: boolean
33
32
  invalidAt: number
33
+ }
34
+
35
+ export interface RouteMatch<
36
+ TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
37
+ TRouteInfo extends AnyRouteInfo = RouteInfo,
38
+ > extends Route<TAllRouteInfo, TRouteInfo> {
39
+ store: RouteMatchStore<TAllRouteInfo, TRouteInfo>
40
+ // setStore: WritableStore<RouteMatchStore<TAllRouteInfo, TRouteInfo>>
41
+ matchId: string
42
+ pathname: string
43
+ params: TRouteInfo['allParams']
44
+ childMatches: RouteMatch[]
45
+ cancel: () => void
46
+ load: (
47
+ loaderOpts?:
48
+ | { preload: true; maxAge: number; gcMaxAge: number }
49
+ | { preload?: false; maxAge?: never; gcMaxAge?: never },
50
+ ) => Promise<TRouteInfo['routeLoaderData']>
51
+ fetch: (opts?: { maxAge?: number }) => Promise<TRouteInfo['routeLoaderData']>
52
+ invalidate: () => void
53
+ hasLoaders: () => boolean
34
54
  __: {
55
+ setParentMatch: (parentMatch?: RouteMatch) => void
35
56
  component?: GetFrameworkGeneric<'Component'>
36
57
  errorComponent?: GetFrameworkGeneric<'ErrorComponent'>
37
58
  pendingComponent?: GetFrameworkGeneric<'Component'>
38
59
  loadPromise?: Promise<void>
39
- componentsPromise?: Promise<void>
40
- dataPromise?: Promise<TRouteInfo['routeLoaderData']>
41
60
  onExit?:
42
61
  | void
43
62
  | ((matchContext: {
@@ -45,22 +64,8 @@ export interface RouteMatch<
45
64
  search: TRouteInfo['fullSearchSchema']
46
65
  }) => void)
47
66
  abortController: AbortController
48
- latestId: string
49
- // setParentMatch: (parentMatch: RouteMatch) => void
50
- // addChildMatch: (childMatch: RouteMatch) => void
51
67
  validate: () => void
52
- notify: () => void
53
- resolve: () => void
54
68
  }
55
- cancel: () => void
56
- load: (
57
- loaderOpts?:
58
- | { preload: true; maxAge: number; gcMaxAge: number }
59
- | { preload?: false; maxAge?: never; gcMaxAge?: never },
60
- ) => Promise<TRouteInfo['routeLoaderData']>
61
- fetch: (opts?: { maxAge?: number }) => Promise<TRouteInfo['routeLoaderData']>
62
- invalidate: () => void
63
- hasLoaders: () => boolean
64
69
  }
65
70
 
66
71
  const componentTypes = [
@@ -82,60 +87,96 @@ export function createRouteMatch<
82
87
  pathname: string
83
88
  },
84
89
  ): RouteMatch<TAllRouteInfo, TRouteInfo> {
85
- const routeMatch: RouteMatch<TAllRouteInfo, TRouteInfo> = {
86
- ...route,
87
- ...opts,
88
- router,
90
+ let componentsPromise: Promise<void>
91
+ let dataPromise: Promise<TRouteInfo['routeLoaderData']>
92
+ let latestId = ''
93
+ let resolve = () => {}
94
+
95
+ function setLoaderData(loaderData: TRouteInfo['routeLoaderData']) {
96
+ batch(() => {
97
+ setStore((s) => {
98
+ s.routeLoaderData = sharedClone(s.routeLoaderData, loaderData)
99
+ })
100
+ updateLoaderData()
101
+ })
102
+ }
103
+
104
+ function updateLoaderData() {
105
+ setStore((s) => {
106
+ s.loaderData = sharedClone(s.loaderData, {
107
+ ...store.parentMatch?.store.loaderData,
108
+ ...s.routeLoaderData,
109
+ }) as TRouteInfo['loaderData']
110
+ })
111
+ }
112
+
113
+ const [store, setStore] = createStore<
114
+ RouteMatchStore<TAllRouteInfo, TRouteInfo>
115
+ >({
89
116
  routeSearch: {},
90
117
  search: {} as any,
91
- childMatches: [],
92
118
  status: 'idle',
93
119
  routeLoaderData: {} as TRouteInfo['routeLoaderData'],
94
120
  loaderData: {} as TRouteInfo['loaderData'],
95
121
  isFetching: false,
96
- isInvalid: false,
122
+ invalid: false,
97
123
  invalidAt: Infinity,
98
- // pendingActions: [],
99
- getIsInvalid: () => {
124
+ get isInvalid(): boolean {
100
125
  const now = Date.now()
101
- return routeMatch.isInvalid || routeMatch.invalidAt < now
126
+ return this.invalid || this.invalidAt < now
102
127
  },
128
+ })
129
+
130
+ const routeMatch: RouteMatch<TAllRouteInfo, TRouteInfo> = {
131
+ ...route,
132
+ ...opts,
133
+ store,
134
+ // setStore,
135
+ router,
136
+ childMatches: [],
103
137
  __: {
104
- abortController: new AbortController(),
105
- latestId: '',
106
- resolve: () => {},
107
- notify: () => {
108
- routeMatch.__.resolve()
109
- routeMatch.router.notify()
138
+ setParentMatch: (parentMatch?: RouteMatch) => {
139
+ batch(() => {
140
+ setStore((s) => {
141
+ s.parentMatch = parentMatch
142
+ })
143
+
144
+ updateLoaderData()
145
+ })
110
146
  },
147
+ abortController: new AbortController(),
111
148
  validate: () => {
112
149
  // Validate the search params and stabilize them
113
150
  const parentSearch =
114
- routeMatch.parentMatch?.search ?? router.state.currentLocation.search
151
+ store.parentMatch?.store.search ?? router.store.currentLocation.search
115
152
 
116
153
  try {
117
- const prevSearch = routeMatch.routeSearch
154
+ const prevSearch = store.routeSearch
118
155
 
119
156
  const validator =
120
157
  typeof routeMatch.options.validateSearch === 'object'
121
158
  ? routeMatch.options.validateSearch.parse
122
159
  : routeMatch.options.validateSearch
123
160
 
124
- let nextSearch = replaceEqualDeep(
161
+ let nextSearch = sharedClone(
125
162
  prevSearch,
126
163
  validator?.(parentSearch) ?? {},
127
164
  )
128
165
 
129
- // Invalidate route matches when search param stability changes
130
- if (prevSearch !== nextSearch) {
131
- routeMatch.isInvalid = true
132
- }
133
-
134
- routeMatch.routeSearch = nextSearch
166
+ batch(() => {
167
+ // Invalidate route matches when search param stability changes
168
+ if (prevSearch !== nextSearch) {
169
+ setStore((s) => (s.invalid = true))
170
+ }
135
171
 
136
- routeMatch.search = replaceEqualDeep(parentSearch, {
137
- ...parentSearch,
138
- ...nextSearch,
172
+ // TODO: Alright, do we need batch() here?
173
+ setStore((s) => {
174
+ s.routeSearch = nextSearch
175
+ s.search = sharedClone(parentSearch, {
176
+ ...parentSearch,
177
+ ...nextSearch,
178
+ })
179
+ })
139
180
  })
140
181
 
141
182
  componentTypes.map(async (type) => {
@@ -151,8 +192,12 @@ export function createRouteMatch<
151
192
  cause: err,
152
193
  })
153
194
  error.code = 'INVALID_SEARCH_PARAMS'
154
- routeMatch.status = 'error'
155
- routeMatch.error = error
195
+
196
+ setStore((s) => {
197
+ s.status = 'error'
198
+ s.error = error
199
+ })
200
+
156
201
  // Do not proceed with loading the route
157
202
  return
158
203
  }
@@ -162,7 +207,7 @@ export function createRouteMatch<
162
207
  routeMatch.__.abortController?.abort()
163
208
  },
164
209
  invalidate: () => {
165
- routeMatch.isInvalid = true
210
+ setStore((s) => (s.invalid = true))
166
211
  },
167
212
  hasLoaders: () => {
168
213
  return !!(
@@ -180,14 +225,14 @@ export function createRouteMatch<
180
225
  if (loaderOpts?.preload && minMaxAge > 0) {
181
226
  // If the match is currently active, don't preload it
182
227
  if (
183
- router.state.currentMatches.find(
228
+ router.store.currentMatches.find(
184
229
  (d) => d.matchId === routeMatch.matchId,
185
230
  )
186
231
  ) {
187
232
  return
188
233
  }
189
234
 
190
- router.matchCache[routeMatch.matchId] = {
235
+ router.store.matchCache[routeMatch.matchId] = {
191
236
  gc: now + loaderOpts.gcMaxAge,
192
237
  match: routeMatch as RouteMatch<any, any>,
193
238
  }
@@ -195,9 +240,9 @@ export function createRouteMatch<
195
240
 
196
241
  // If the match is invalid, errored or idle, trigger it to load
197
242
  if (
198
- (routeMatch.status === 'success' && routeMatch.getIsInvalid()) ||
199
- routeMatch.status === 'error' ||
200
- routeMatch.status === 'idle'
243
+ (store.status === 'success' && store.isInvalid) ||
244
+ store.status === 'error' ||
245
+ store.status === 'idle'
201
246
  ) {
202
247
  const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined
203
248
 
@@ -206,31 +251,33 @@ export function createRouteMatch<
206
251
  },
207
252
  fetch: async (opts) => {
208
253
  const loadId = '' + Date.now() + Math.random()
209
- routeMatch.__.latestId = loadId
254
+ latestId = loadId
210
255
  const checkLatest = async () => {
211
- if (loadId !== routeMatch.__.latestId) {
256
+ if (loadId !== latestId) {
212
257
  // warning(true, 'Data loader is out of date!')
213
258
  return new Promise(() => {})
214
259
  }
215
260
  }
216
261
 
217
- // If the match was in an error state, set it
218
- // to a loading state again. Otherwise, keep it
219
- // as loading or resolved
220
- if (routeMatch.status === 'idle') {
221
- routeMatch.status = 'loading'
222
- }
262
+ batch(() => {
263
+ // If the match was in an error state, set it
264
+ // to a loading state again. Otherwise, keep it
265
+ // as loading or resolved
266
+ if (store.status === 'idle') {
267
+ setStore((s) => (s.status = 'loading'))
268
+ }
223
269
 
224
- // We started loading the route, so it's no longer invalid
225
- routeMatch.isInvalid = false
270
+ // We started loading the route, so it's no longer invalid
271
+ setStore((s) => (s.invalid = false))
272
+ })
226
273
 
227
- routeMatch.__.loadPromise = new Promise(async (resolve) => {
274
+ routeMatch.__.loadPromise = new Promise(async (r) => {
228
275
  // We are now fetching, even if it's in the background of a
229
276
  // resolved state
230
- routeMatch.isFetching = true
231
- routeMatch.__.resolve = resolve as () => void
277
+ setStore((s) => (s.isFetching = true))
278
+ resolve = r as () => void
232
279
 
233
- routeMatch.__.componentsPromise = (async () => {
280
+ componentsPromise = (async () => {
234
281
  // then run all component and data loaders in parallel
235
282
  // For each component type, potentially load it asynchronously
236
283
 
@@ -247,29 +294,28 @@ export function createRouteMatch<
247
294
  )
248
295
  })()
249
296
 
250
- routeMatch.__.dataPromise = Promise.resolve().then(async () => {
297
+ dataPromise = Promise.resolve().then(async () => {
251
298
  try {
252
299
  if (routeMatch.options.loader) {
253
300
  const data = await router.loadMatchData(routeMatch)
254
301
  await checkLatest()
255
302
 
256
- routeMatch.routeLoaderData = replaceEqualDeep(
257
- routeMatch.routeLoaderData,
258
- data,
259
- )
303
+ setLoaderData(data)
260
304
  }
261
305
 
262
- routeMatch.error = undefined
263
- routeMatch.status = 'success'
264
- routeMatch.updatedAt = Date.now()
265
- routeMatch.invalidAt =
266
- routeMatch.updatedAt +
267
- (opts?.maxAge ??
268
- routeMatch.options.loaderMaxAge ??
269
- router.options.defaultLoaderMaxAge ??
270
- 0)
271
-
272
- return routeMatch.routeLoaderData
306
+ setStore((s) => {
307
+ s.error = undefined
308
+ s.status = 'success'
309
+ s.updatedAt = Date.now()
310
+ s.invalidAt =
311
+ s.updatedAt +
312
+ (opts?.maxAge ??
313
+ routeMatch.options.loaderMaxAge ??
314
+ router.options.defaultLoaderMaxAge ??
315
+ 0)
316
+ })
317
+
318
+ return store.routeLoaderData
273
319
  } catch (err) {
274
320
  await checkLatest()
275
321
 
@@ -277,9 +323,11 @@ export function createRouteMatch<
277
323
  console.error(err)
278
324
  }
279
325
 
280
- routeMatch.error = err
281
- routeMatch.status = 'error'
282
- routeMatch.updatedAt = Date.now()
326
+ setStore((s) => {
327
+ s.error = err
328
+ s.status = 'error'
329
+ s.updatedAt = Date.now()
330
+ })
283
331
 
284
332
  throw err
285
333
  }
@@ -287,16 +335,13 @@ export function createRouteMatch<
287
335
 
288
336
  const after = async () => {
289
337
  await checkLatest()
290
- routeMatch.isFetching = false
338
+ setStore((s) => (s.isFetching = false))
291
339
  delete routeMatch.__.loadPromise
292
- routeMatch.__.notify()
340
+ resolve()
293
341
  }
294
342
 
295
343
  try {
296
- await Promise.all([
297
- routeMatch.__.componentsPromise,
298
- routeMatch.__.dataPromise.catch(() => {}),
299
- ])
344
+ await Promise.all([componentsPromise, dataPromise.catch(() => {})])
300
345
  after()
301
346
  } catch {
302
347
  after()
@@ -309,7 +354,7 @@ export function createRouteMatch<
309
354
  }
310
355
 
311
356
  if (!routeMatch.hasLoaders()) {
312
- routeMatch.status = 'success'
357
+ setStore((s) => (s.status = 'success'))
313
358
  }
314
359
 
315
360
  return routeMatch