@tanstack/router-core 0.0.1-beta.45 → 0.0.1-beta.49

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 (44) hide show
  1. package/build/cjs/actions.js +94 -0
  2. package/build/cjs/actions.js.map +1 -0
  3. package/build/cjs/history.js +163 -0
  4. package/build/cjs/history.js.map +1 -0
  5. package/build/cjs/index.js +18 -20
  6. package/build/cjs/index.js.map +1 -1
  7. package/build/cjs/interop.js +175 -0
  8. package/build/cjs/interop.js.map +1 -0
  9. package/build/cjs/path.js +4 -5
  10. package/build/cjs/path.js.map +1 -1
  11. package/build/cjs/route.js +16 -138
  12. package/build/cjs/route.js.map +1 -1
  13. package/build/cjs/routeConfig.js +1 -7
  14. package/build/cjs/routeConfig.js.map +1 -1
  15. package/build/cjs/routeMatch.js +194 -199
  16. package/build/cjs/routeMatch.js.map +1 -1
  17. package/build/cjs/router.js +726 -703
  18. package/build/cjs/router.js.map +1 -1
  19. package/build/cjs/store.js +54 -0
  20. package/build/cjs/store.js.map +1 -0
  21. package/build/esm/index.js +1305 -1114
  22. package/build/esm/index.js.map +1 -1
  23. package/build/stats-html.html +1 -1
  24. package/build/stats-react.json +229 -192
  25. package/build/types/index.d.ts +172 -109
  26. package/build/umd/index.development.js +1381 -2331
  27. package/build/umd/index.development.js.map +1 -1
  28. package/build/umd/index.production.js +1 -1
  29. package/build/umd/index.production.js.map +1 -1
  30. package/package.json +3 -3
  31. package/src/actions.ts +157 -0
  32. package/src/history.ts +199 -0
  33. package/src/index.ts +4 -7
  34. package/src/interop.ts +169 -0
  35. package/src/link.ts +2 -2
  36. package/src/route.ts +34 -239
  37. package/src/routeConfig.ts +3 -34
  38. package/src/routeInfo.ts +6 -21
  39. package/src/routeMatch.ts +270 -285
  40. package/src/router.ts +967 -963
  41. package/src/store.ts +52 -0
  42. package/build/cjs/sharedClone.js +0 -122
  43. package/build/cjs/sharedClone.js.map +0 -1
  44. package/src/sharedClone.ts +0 -118
@@ -8,11 +8,154 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { createMemoryHistory, createBrowserHistory } from 'history';
12
- export { createBrowserHistory, createHashHistory, createMemoryHistory } from 'history';
13
11
  import invariant from 'tiny-invariant';
14
12
  export { default as invariant } from 'tiny-invariant';
15
- import { createStore, batch } from '@solidjs/reactivity';
13
+ import { setAutoFreeze, produce } from 'immer';
14
+
15
+ // While the public API was clearly inspired by the "history" npm package,
16
+ // This implementation attempts to be more lightweight by
17
+ // making assumptions about the way TanStack Router works
18
+
19
+ const popStateEvent = 'popstate';
20
+ function createHistory(opts) {
21
+ let currentLocation = opts.getLocation();
22
+ let unsub = () => {};
23
+ let listeners = new Set();
24
+ const onUpdate = () => {
25
+ currentLocation = opts.getLocation();
26
+ listeners.forEach(listener => listener());
27
+ };
28
+ return {
29
+ get location() {
30
+ return currentLocation;
31
+ },
32
+ listen: cb => {
33
+ if (listeners.size === 0) {
34
+ unsub = opts.listener(onUpdate);
35
+ }
36
+ listeners.add(cb);
37
+ return () => {
38
+ listeners.delete(cb);
39
+ if (listeners.size === 0) {
40
+ unsub();
41
+ }
42
+ };
43
+ },
44
+ push: (path, state) => {
45
+ opts.pushState(path, state);
46
+ onUpdate();
47
+ },
48
+ replace: (path, state) => {
49
+ opts.replaceState(path, state);
50
+ onUpdate();
51
+ },
52
+ go: index => {
53
+ opts.go(index);
54
+ onUpdate();
55
+ },
56
+ back: () => {
57
+ opts.back();
58
+ onUpdate();
59
+ },
60
+ forward: () => {
61
+ opts.forward();
62
+ onUpdate();
63
+ }
64
+ };
65
+ }
66
+ function createBrowserHistory(opts) {
67
+ const getHref = opts?.getHref ?? (() => `${window.location.pathname}${window.location.hash}${window.location.search}`);
68
+ const createHref = opts?.createHref ?? (path => path);
69
+ const getLocation = () => parseLocation(getHref(), history.state);
70
+ return createHistory({
71
+ getLocation,
72
+ listener: onUpdate => {
73
+ window.addEventListener(popStateEvent, onUpdate);
74
+ return () => {
75
+ window.removeEventListener(popStateEvent, onUpdate);
76
+ };
77
+ },
78
+ pushState: (path, state) => {
79
+ window.history.pushState({
80
+ ...state,
81
+ key: createRandomKey()
82
+ }, '', createHref(path));
83
+ },
84
+ replaceState: (path, state) => {
85
+ window.history.replaceState({
86
+ ...state,
87
+ key: createRandomKey()
88
+ }, '', createHref(path));
89
+ },
90
+ back: () => window.history.back(),
91
+ forward: () => window.history.forward(),
92
+ go: n => window.history.go(n)
93
+ });
94
+ }
95
+ function createHashHistory() {
96
+ return createBrowserHistory({
97
+ getHref: () => window.location.hash.substring(1),
98
+ createHref: path => `#${path}`
99
+ });
100
+ }
101
+ function createMemoryHistory(opts = {
102
+ initialEntries: ['/']
103
+ }) {
104
+ const entries = opts.initialEntries;
105
+ let index = opts.initialIndex ?? entries.length - 1;
106
+ let currentState = {};
107
+ const getLocation = () => parseLocation(entries[index], currentState);
108
+ return createHistory({
109
+ getLocation,
110
+ listener: onUpdate => {
111
+ window.addEventListener(popStateEvent, onUpdate);
112
+ // We might need to handle the hashchange event in the future
113
+ // window.addEventListener(hashChangeEvent, onUpdate)
114
+ return () => {
115
+ window.removeEventListener(popStateEvent, onUpdate);
116
+ };
117
+ },
118
+ pushState: (path, state) => {
119
+ currentState = {
120
+ ...state,
121
+ key: createRandomKey()
122
+ };
123
+ entries.push(path);
124
+ index++;
125
+ },
126
+ replaceState: (path, state) => {
127
+ currentState = {
128
+ ...state,
129
+ key: createRandomKey()
130
+ };
131
+ entries[index] = path;
132
+ },
133
+ back: () => {
134
+ index--;
135
+ },
136
+ forward: () => {
137
+ index = Math.min(index + 1, entries.length - 1);
138
+ },
139
+ go: n => window.history.go(n)
140
+ });
141
+ }
142
+ function parseLocation(href, state) {
143
+ let hashIndex = href.indexOf('#');
144
+ let searchIndex = href.indexOf('?');
145
+ const pathEnd = Math.min(hashIndex, searchIndex);
146
+ return {
147
+ href,
148
+ pathname: pathEnd > -1 ? href.substring(0, pathEnd) : href,
149
+ hash: hashIndex > -1 ? href.substring(hashIndex, searchIndex) : '',
150
+ search: searchIndex > -1 ? href.substring(searchIndex) : '',
151
+ state
152
+ };
153
+ }
154
+
155
+ // Thanks co-pilot!
156
+ function createRandomKey() {
157
+ return (Math.random() + 1).toString(36).substring(7);
158
+ }
16
159
 
17
160
  function last(arr) {
18
161
  return arr[arr.length - 1];
@@ -73,9 +216,8 @@ function resolvePath(basepath, base, to) {
73
216
  baseSegments.push(toSegment);
74
217
  } else ;
75
218
  } else if (toSegment.value === '..') {
76
- var _last;
77
219
  // Extra trailing slash? pop it off
78
- if (baseSegments.length > 1 && ((_last = last(baseSegments)) == null ? void 0 : _last.value) === '/') {
220
+ if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
79
221
  baseSegments.pop();
80
222
  }
81
223
  baseSegments.pop();
@@ -172,14 +314,14 @@ function matchByPath(basepath, from, matchLocation) {
172
314
  const isLastBaseSegment = i === baseSegments.length - 1;
173
315
  if (routeSegment) {
174
316
  if (routeSegment.type === 'wildcard') {
175
- if (baseSegment != null && baseSegment.value) {
317
+ if (baseSegment?.value) {
176
318
  params['*'] = joinPaths(baseSegments.slice(i).map(d => d.value));
177
319
  return true;
178
320
  }
179
321
  return false;
180
322
  }
181
323
  if (routeSegment.type === 'pathname') {
182
- if (routeSegment.value === '/' && !(baseSegment != null && baseSegment.value)) {
324
+ if (routeSegment.value === '/' && !baseSegment?.value) {
183
325
  return true;
184
326
  }
185
327
  if (baseSegment) {
@@ -196,7 +338,7 @@ function matchByPath(basepath, from, matchLocation) {
196
338
  return false;
197
339
  }
198
340
  if (routeSegment.type === 'param') {
199
- if ((baseSegment == null ? void 0 : baseSegment.value) === '/') {
341
+ if (baseSegment?.value === '/') {
200
342
  return false;
201
343
  }
202
344
  if (baseSegment.value.charAt(0) !== '$') {
@@ -262,151 +404,25 @@ function decode(str) {
262
404
  return out;
263
405
  }
264
406
 
265
- function createRoute(routeConfig, options, originalIndex, parent, router) {
266
- const {
267
- id,
268
- routeId,
269
- path: routePath,
270
- fullPath
271
- } = routeConfig;
272
- let route = {
273
- routeInfo: undefined,
274
- routeId: id,
275
- routeRouteId: routeId,
276
- originalIndex,
277
- routePath,
278
- fullPath,
279
- options,
280
- router,
281
- childRoutes: undefined,
282
- parentRoute: parent,
283
- get action() {
284
- let action = router.store.actions[id] || (() => {
285
- router.setStore(s => {
286
- s.actions[id] = {
287
- submissions: [],
288
- submit: async (submission, actionOpts) => {
289
- if (!route) {
290
- return;
291
- }
292
- const invalidate = (actionOpts == null ? void 0 : actionOpts.invalidate) ?? true;
293
- const [actionStore, setActionStore] = createStore({
294
- submittedAt: Date.now(),
295
- status: 'pending',
296
- submission,
297
- isMulti: !!(actionOpts != null && actionOpts.multi)
298
- });
299
- router.setStore(s => {
300
- if (!(actionOpts != null && actionOpts.multi)) {
301
- s.actions[id].submissions = action.submissions.filter(d => d.isMulti);
302
- }
303
- s.actions[id].current = actionStore;
304
- s.actions[id].latest = actionStore;
305
- s.actions[id].submissions.push(actionStore);
306
- });
307
- try {
308
- const res = await (route.options.action == null ? void 0 : route.options.action(submission));
309
- setActionStore(s => {
310
- s.data = res;
311
- });
312
- if (invalidate) {
313
- router.invalidateRoute({
314
- to: '.',
315
- fromCurrent: true
316
- });
317
- await router.reload();
318
- }
319
- setActionStore(s => {
320
- s.status = 'success';
321
- });
322
- return res;
323
- } catch (err) {
324
- console.error(err);
325
- setActionStore(s => {
326
- s.error = err;
327
- s.status = 'error';
328
- });
329
- }
330
- }
331
- };
332
- });
333
- return router.store.actions[id];
334
- })();
335
- return action;
336
- },
337
- get loader() {
338
- let loader = router.store.loaders[id] || (() => {
339
- router.setStore(s => {
340
- s.loaders[id] = {
341
- pending: [],
342
- fetch: async loaderContext => {
343
- if (!route) {
344
- return;
345
- }
346
- const loaderState = {
347
- loadedAt: Date.now(),
348
- loaderContext
349
- };
350
- router.setStore(s => {
351
- s.loaders[id].current = loaderState;
352
- s.loaders[id].latest = loaderState;
353
- s.loaders[id].pending.push(loaderState);
354
- });
355
- try {
356
- return await (route.options.loader == null ? void 0 : route.options.loader(loaderContext));
357
- } finally {
358
- router.setStore(s => {
359
- s.loaders[id].pending = s.loaders[id].pending.filter(d => d !== loaderState);
360
- });
361
- }
362
- }
363
- };
364
- });
365
- return router.store.loaders[id];
366
- })();
367
- return loader;
368
- }
369
-
370
- // buildLink: (options) => {
371
- // return router.buildLink({
372
- // ...options,
373
- // from: fullPath,
374
- // } as any) as any
375
- // },
376
-
377
- // navigate: (options) => {
378
- // return router.navigate({
379
- // ...options,
380
- // from: fullPath,
381
- // } as any) as any
382
- // },
383
-
384
- // matchRoute: (matchLocation, opts) => {
385
- // return router.matchRoute(
386
- // {
387
- // ...matchLocation,
388
- // from: fullPath,
389
- // } as any,
390
- // opts,
391
- // ) as any
392
- // },
393
- };
394
-
395
- router.options.createRoute == null ? void 0 : router.options.createRoute({
396
- router,
397
- route
398
- });
399
- return route;
407
+ class Route {
408
+ constructor(routeConfig, options, originalIndex, parent, router) {
409
+ Object.assign(this, {
410
+ ...routeConfig,
411
+ originalIndex,
412
+ options,
413
+ getRouter: () => router,
414
+ childRoutes: undefined,
415
+ getParentRoute: () => parent
416
+ });
417
+ router.options.createRoute?.({
418
+ router,
419
+ route: this
420
+ });
421
+ }
400
422
  }
401
423
 
402
424
  const rootRouteId = '__root__';
403
- const createRouteConfig = function (options, children, isRoot, parentId, parentPath) {
404
- if (options === void 0) {
405
- options = {};
406
- }
407
- if (isRoot === void 0) {
408
- isRoot = true;
409
- }
425
+ const createRouteConfig = (options = {}, children = [], isRoot = true, parentId, parentPath) => {
410
426
  if (isRoot) {
411
427
  options.path = rootRouteId;
412
428
  }
@@ -445,80 +461,153 @@ const createRouteConfig = function (options, children, isRoot, parentId, parentP
445
461
  };
446
462
  };
447
463
 
464
+ setAutoFreeze(false);
465
+ let queue = [];
466
+ let batching = false;
467
+ function flush() {
468
+ if (batching) return;
469
+ queue.forEach(cb => cb());
470
+ queue = [];
471
+ }
472
+ function createStore(initialState, debug) {
473
+ const listeners = new Set();
474
+ const store = {
475
+ state: initialState,
476
+ subscribe: listener => {
477
+ listeners.add(listener);
478
+ return () => listeners.delete(listener);
479
+ },
480
+ setState: updater => {
481
+ const previous = store.state;
482
+ store.state = produce(d => {
483
+ updater(d);
484
+ })(previous);
485
+ if (debug) console.log(store.state);
486
+ queue.push(() => listeners.forEach(listener => listener(store.state, previous)));
487
+ flush();
488
+ }
489
+ };
490
+ return store;
491
+ }
492
+ function batch(cb) {
493
+ batching = true;
494
+ cb();
495
+ batching = false;
496
+ flush();
497
+ }
498
+
499
+ // /**
500
+ // * This function converts a store to an immutable value, which is
501
+ // * more complex than you think. On first read, (when prev is undefined)
502
+ // * every value must be recursively touched so tracking is "deep".
503
+ // * Every object/array structure must also be cloned to
504
+ // * have a new reference, otherwise it will get mutated by subsequent
505
+ // * store updates.
506
+ // *
507
+ // * In the case that prev is supplied, we have to do deep comparisons
508
+ // * between prev and next objects/array references and if they are deeply
509
+ // * equal, we can return the prev version for referential equality.
510
+ // */
511
+ // export function storeToImmutable<T>(prev: any, next: T): T {
512
+ // const cache = new Map()
513
+
514
+ // // Visit all nodes
515
+ // // clone all next structures
516
+ // // from bottom up, if prev === next, return prev
517
+
518
+ // function recurse(prev: any, next: any) {
519
+ // if (cache.has(next)) {
520
+ // return cache.get(next)
521
+ // }
522
+
523
+ // const prevIsArray = Array.isArray(prev)
524
+ // const nextIsArray = Array.isArray(next)
525
+ // const prevIsObj = isPlainObject(prev)
526
+ // const nextIsObj = isPlainObject(next)
527
+ // const nextIsComplex = nextIsArray || nextIsObj
528
+
529
+ // const isArray = prevIsArray && nextIsArray
530
+ // const isObj = prevIsObj && nextIsObj
531
+
532
+ // const isSameStructure = isArray || isObj
533
+
534
+ // if (nextIsComplex) {
535
+ // const prevSize = isArray
536
+ // ? prev.length
537
+ // : isObj
538
+ // ? Object.keys(prev).length
539
+ // : -1
540
+ // const nextKeys = isArray ? next : Object.keys(next)
541
+ // const nextSize = nextKeys.length
542
+
543
+ // let changed = false
544
+ // const copy: any = nextIsArray ? [] : {}
545
+
546
+ // for (let i = 0; i < nextSize; i++) {
547
+ // const key = isArray ? i : nextKeys[i]
548
+ // const prevValue = isSameStructure ? prev[key] : undefined
549
+ // const nextValue = next[key]
550
+
551
+ // // Recurse the new value
552
+ // try {
553
+ // console.count(key)
554
+ // copy[key] = recurse(prevValue, nextValue)
555
+ // } catch {}
556
+
557
+ // // If the new value has changed reference,
558
+ // // mark the obj/array as changed
559
+ // if (!changed && copy[key] !== prevValue) {
560
+ // changed = true
561
+ // }
562
+ // }
563
+
564
+ // // No items have changed!
565
+ // // If something has changed, return a clone of the next obj/array
566
+ // if (changed || prevSize !== nextSize) {
567
+ // cache.set(next, copy)
568
+ // return copy
569
+ // }
570
+
571
+ // // If they are exactly the same, return the prev obj/array
572
+ // cache.set(next, prev)
573
+ // return prev
574
+ // }
575
+
576
+ // cache.set(next, next)
577
+ // return next
578
+ // }
579
+
580
+ // return recurse(prev, next)
581
+ // }
582
+
448
583
  /**
449
584
  * This function returns `a` if `b` is deeply equal.
450
585
  * If not, it will replace any deeply equal children of `b` with those of `a`.
451
- * This can be used for structural sharing between JSON values for example.
586
+ * This can be used for structural sharing between immutable JSON values for example.
587
+ * Do not use this with signals
452
588
  */
453
- function sharedClone(prev, next, touchAll) {
454
- const things = new Map();
455
- function recurse(prev, next) {
456
- if (prev === next) {
457
- return prev;
458
- }
459
- if (things.has(next)) {
460
- return things.get(next);
461
- }
462
- const prevIsArray = Array.isArray(prev);
463
- const nextIsArray = Array.isArray(next);
464
- const prevIsObj = isPlainObject(prev);
465
- const nextIsObj = isPlainObject(next);
466
- const isArray = prevIsArray && nextIsArray;
467
- const isObj = prevIsObj && nextIsObj;
468
- const isSameStructure = isArray || isObj;
469
-
470
- // Both are arrays or objects
471
- if (isSameStructure) {
472
- const aSize = isArray ? prev.length : Object.keys(prev).length;
473
- const bItems = isArray ? next : Object.keys(next);
474
- const bSize = bItems.length;
475
- const copy = isArray ? [] : {};
476
- let equalItems = 0;
477
- for (let i = 0; i < bSize; i++) {
478
- const key = isArray ? i : bItems[i];
479
- if (copy[key] === prev[key]) {
480
- equalItems++;
481
- }
482
- }
483
- if (aSize === bSize && equalItems === aSize) {
484
- things.set(next, prev);
485
- return prev;
486
- }
487
- things.set(next, copy);
488
- for (let i = 0; i < bSize; i++) {
489
- const key = isArray ? i : bItems[i];
490
- if (typeof bItems[i] === 'function') {
491
- copy[key] = prev[key];
492
- } else {
493
- copy[key] = recurse(prev[key], next[key]);
494
- }
495
- if (copy[key] === prev[key]) {
496
- equalItems++;
497
- }
498
- }
499
- return copy;
500
- }
501
- if (nextIsArray) {
502
- const copy = [];
503
- things.set(next, copy);
504
- for (let i = 0; i < next.length; i++) {
505
- copy[i] = recurse(undefined, next[i]);
506
- }
507
- return copy;
508
- }
509
- if (nextIsObj) {
510
- const copy = {};
511
- things.set(next, copy);
512
- const nextKeys = Object.keys(next);
513
- for (let i = 0; i < nextKeys.length; i++) {
514
- const key = nextKeys[i];
515
- copy[key] = recurse(undefined, next[key]);
589
+ function replaceEqualDeep(prev, _next) {
590
+ if (prev === _next) {
591
+ return prev;
592
+ }
593
+ const next = _next;
594
+ const array = Array.isArray(prev) && Array.isArray(next);
595
+ if (array || isPlainObject(prev) && isPlainObject(next)) {
596
+ const prevSize = array ? prev.length : Object.keys(prev).length;
597
+ const nextItems = array ? next : Object.keys(next);
598
+ const nextSize = nextItems.length;
599
+ const copy = array ? [] : {};
600
+ let equalItems = 0;
601
+ for (let i = 0; i < nextSize; i++) {
602
+ const key = array ? i : nextItems[i];
603
+ copy[key] = replaceEqualDeep(prev[key], next[key]);
604
+ if (copy[key] === prev[key]) {
605
+ equalItems++;
516
606
  }
517
- return copy;
518
607
  }
519
- return next;
608
+ return prevSize === nextSize && equalItems === prevSize ? prev : copy;
520
609
  }
521
- return recurse(prev, next);
610
+ return next;
522
611
  }
523
612
 
524
613
  // Copied from: https://github.com/jonschlinkert/is-plain-object
@@ -550,228 +639,237 @@ function isPlainObject(o) {
550
639
  function hasObjectPrototype(o) {
551
640
  return Object.prototype.toString.call(o) === '[object Object]';
552
641
  }
642
+ function trackDeep(obj) {
643
+ const seen = new Set();
644
+ JSON.stringify(obj, (_, value) => {
645
+ if (typeof value === 'function') {
646
+ return undefined;
647
+ }
648
+ if (typeof value === 'object' && value !== null) {
649
+ if (seen.has(value)) return;
650
+ seen.add(value);
651
+ }
652
+ return value;
653
+ });
654
+ return obj;
655
+ }
553
656
 
554
657
  const componentTypes = ['component', 'errorComponent', 'pendingComponent'];
555
- function createRouteMatch(router, route, opts) {
556
- let componentsPromise;
557
- let dataPromise;
558
- let latestId = '';
559
- let resolve = () => {};
560
- function setLoaderData(loaderData) {
561
- batch(() => {
562
- setStore(s => {
563
- s.routeLoaderData = sharedClone(s.routeLoaderData, loaderData);
564
- });
565
- updateLoaderData();
658
+ class RouteMatch {
659
+ abortController = new AbortController();
660
+ #latestId = '';
661
+ #resolve = () => {};
662
+ onLoaderDataListeners = new Set();
663
+ constructor(router, route, opts) {
664
+ Object.assign(this, {
665
+ route,
666
+ router,
667
+ matchId: opts.matchId,
668
+ pathname: opts.pathname,
669
+ params: opts.params,
670
+ store: createStore({
671
+ routeSearch: {},
672
+ search: {},
673
+ status: 'idle',
674
+ routeLoaderData: {},
675
+ loaderData: {},
676
+ isFetching: false,
677
+ invalid: false,
678
+ invalidAt: Infinity
679
+ })
566
680
  });
681
+ if (!this.__hasLoaders()) {
682
+ this.store.setState(s => s.status = 'success');
683
+ }
567
684
  }
568
- function updateLoaderData() {
569
- setStore(s => {
570
- var _store$parentMatch;
571
- s.loaderData = sharedClone(s.loaderData, {
572
- ...((_store$parentMatch = store.parentMatch) == null ? void 0 : _store$parentMatch.store.loaderData),
573
- ...s.routeLoaderData
685
+ #setLoaderData = loaderData => {
686
+ batch(() => {
687
+ this.store.setState(s => {
688
+ s.routeLoaderData = loaderData;
574
689
  });
690
+ this.#updateLoaderData();
575
691
  });
576
- }
577
- const [store, setStore] = createStore({
578
- routeSearch: {},
579
- search: {},
580
- status: 'idle',
581
- routeLoaderData: {},
582
- loaderData: {},
583
- isFetching: false,
584
- invalid: false,
585
- invalidAt: Infinity,
586
- get isInvalid() {
587
- const now = Date.now();
588
- return this.invalid || this.invalidAt < now;
589
- }
590
- });
591
- const routeMatch = {
592
- ...route,
593
- ...opts,
594
- store,
595
- // setStore,
596
- router,
597
- childMatches: [],
598
- __: {
599
- setParentMatch: parentMatch => {
600
- batch(() => {
601
- setStore(s => {
602
- s.parentMatch = parentMatch;
603
- });
604
- updateLoaderData();
605
- });
606
- },
607
- abortController: new AbortController(),
608
- validate: () => {
609
- var _store$parentMatch2;
610
- // Validate the search params and stabilize them
611
- const parentSearch = ((_store$parentMatch2 = store.parentMatch) == null ? void 0 : _store$parentMatch2.store.search) ?? router.store.currentLocation.search;
612
- try {
613
- const prevSearch = store.routeSearch;
614
- const validator = typeof routeMatch.options.validateSearch === 'object' ? routeMatch.options.validateSearch.parse : routeMatch.options.validateSearch;
615
- let nextSearch = sharedClone(prevSearch, (validator == null ? void 0 : validator(parentSearch)) ?? {});
616
- batch(() => {
617
- // Invalidate route matches when search param stability changes
618
- if (prevSearch !== nextSearch) {
619
- setStore(s => s.invalid = true);
620
- }
621
-
622
- // TODO: Alright, do we need batch() here?
623
- setStore(s => {
624
- s.routeSearch = nextSearch;
625
- s.search = sharedClone(parentSearch, {
626
- ...parentSearch,
627
- ...nextSearch
628
- });
629
- });
630
- });
631
- componentTypes.map(async type => {
632
- const component = routeMatch.options[type];
633
- if (typeof routeMatch.__[type] !== 'function') {
634
- routeMatch.__[type] = component;
635
- }
636
- });
637
- } catch (err) {
638
- console.error(err);
639
- const error = new Error('Invalid search params found', {
640
- cause: err
641
- });
642
- error.code = 'INVALID_SEARCH_PARAMS';
643
- setStore(s => {
644
- s.status = 'error';
645
- s.error = error;
646
- });
692
+ };
693
+ cancel = () => {
694
+ this.abortController?.abort();
695
+ };
696
+ load = async loaderOpts => {
697
+ const now = Date.now();
698
+ const minMaxAge = loaderOpts?.preload ? Math.max(loaderOpts?.maxAge, loaderOpts?.gcMaxAge) : 0;
647
699
 
648
- // Do not proceed with loading the route
649
- return;
650
- }
700
+ // If this is a preload, add it to the preload cache
701
+ if (loaderOpts?.preload && minMaxAge > 0) {
702
+ // If the match is currently active, don't preload it
703
+ if (this.router.store.state.currentMatches.find(d => d.id === this.id)) {
704
+ return;
651
705
  }
652
- },
653
- cancel: () => {
654
- var _routeMatch$__$abortC;
655
- (_routeMatch$__$abortC = routeMatch.__.abortController) == null ? void 0 : _routeMatch$__$abortC.abort();
656
- },
657
- invalidate: () => {
658
- setStore(s => s.invalid = true);
659
- },
660
- hasLoaders: () => {
661
- return !!(route.options.loader || componentTypes.some(d => {
662
- var _route$options$d;
663
- return (_route$options$d = route.options[d]) == null ? void 0 : _route$options$d.preload;
664
- }));
665
- },
666
- load: async loaderOpts => {
667
- const now = Date.now();
668
- const minMaxAge = loaderOpts != null && loaderOpts.preload ? Math.max(loaderOpts == null ? void 0 : loaderOpts.maxAge, loaderOpts == null ? void 0 : loaderOpts.gcMaxAge) : 0;
669
-
670
- // If this is a preload, add it to the preload cache
671
- if (loaderOpts != null && loaderOpts.preload && minMaxAge > 0) {
672
- // If the match is currently active, don't preload it
673
- if (router.store.currentMatches.find(d => d.matchId === routeMatch.matchId)) {
674
- return;
675
- }
676
- router.store.matchCache[routeMatch.matchId] = {
706
+ this.router.store.setState(s => {
707
+ s.matchCache[this.id] = {
677
708
  gc: now + loaderOpts.gcMaxAge,
678
- match: routeMatch
709
+ match: this
679
710
  };
680
- }
711
+ });
712
+ }
681
713
 
682
- // If the match is invalid, errored or idle, trigger it to load
683
- if (store.status === 'success' && store.isInvalid || store.status === 'error' || store.status === 'idle') {
684
- const maxAge = loaderOpts != null && loaderOpts.preload ? loaderOpts == null ? void 0 : loaderOpts.maxAge : undefined;
685
- await routeMatch.fetch({
686
- maxAge
687
- });
688
- }
689
- },
690
- fetch: async opts => {
714
+ // If the match is invalid, errored or idle, trigger it to load
715
+ if (this.store.state.status === 'success' && this.getIsInvalid() || this.store.state.status === 'error' || this.store.state.status === 'idle') {
716
+ const maxAge = loaderOpts?.preload ? loaderOpts?.maxAge : undefined;
717
+ await this.fetch({
718
+ maxAge
719
+ });
720
+ }
721
+ };
722
+ fetch = async opts => {
723
+ this.__loadPromise = new Promise(async resolve => {
691
724
  const loadId = '' + Date.now() + Math.random();
692
- latestId = loadId;
693
- const checkLatest = async () => {
694
- if (loadId !== latestId) {
695
- // warning(true, 'Data loader is out of date!')
696
- return new Promise(() => {});
697
- }
698
- };
725
+ this.#latestId = loadId;
726
+ const checkLatest = () => loadId !== this.#latestId ? this.__loadPromise?.then(() => resolve()) : undefined;
727
+ let latestPromise;
699
728
  batch(() => {
700
729
  // If the match was in an error state, set it
701
730
  // to a loading state again. Otherwise, keep it
702
731
  // as loading or resolved
703
- if (store.status === 'idle') {
704
- setStore(s => s.status = 'loading');
732
+ if (this.store.state.status === 'idle') {
733
+ this.store.setState(s => s.status = 'loading');
705
734
  }
706
735
 
707
736
  // We started loading the route, so it's no longer invalid
708
- setStore(s => s.invalid = false);
737
+ this.store.setState(s => s.invalid = false);
709
738
  });
710
- routeMatch.__.loadPromise = new Promise(async r => {
711
- // We are now fetching, even if it's in the background of a
712
- // resolved state
713
- setStore(s => s.isFetching = true);
714
- resolve = r;
715
- componentsPromise = (async () => {
716
- // then run all component and data loaders in parallel
717
- // For each component type, potentially load it asynchronously
718
-
719
- await Promise.all(componentTypes.map(async type => {
720
- var _routeMatch$__$type;
721
- const component = routeMatch.options[type];
722
- if ((_routeMatch$__$type = routeMatch.__[type]) != null && _routeMatch$__$type.preload) {
723
- routeMatch.__[type] = await router.options.loadComponent(component);
724
- }
725
- }));
726
- })();
727
- dataPromise = Promise.resolve().then(async () => {
728
- try {
729
- if (routeMatch.options.loader) {
730
- const data = await router.loadMatchData(routeMatch);
731
- await checkLatest();
732
- setLoaderData(data);
733
- }
734
- setStore(s => {
735
- s.error = undefined;
736
- s.status = 'success';
737
- s.updatedAt = Date.now();
738
- s.invalidAt = s.updatedAt + ((opts == null ? void 0 : opts.maxAge) ?? routeMatch.options.loaderMaxAge ?? router.options.defaultLoaderMaxAge ?? 0);
739
- });
740
- return store.routeLoaderData;
741
- } catch (err) {
742
- await checkLatest();
743
- if (process.env.NODE_ENV !== 'production') {
744
- console.error(err);
745
- }
746
- setStore(s => {
747
- s.error = err;
748
- s.status = 'error';
749
- s.updatedAt = Date.now();
750
- });
751
- throw err;
739
+
740
+ // We are now fetching, even if it's in the background of a
741
+ // resolved state
742
+ this.store.setState(s => s.isFetching = true);
743
+ this.#resolve = resolve;
744
+ const componentsPromise = (async () => {
745
+ // then run all component and data loaders in parallel
746
+ // For each component type, potentially load it asynchronously
747
+
748
+ await Promise.all(componentTypes.map(async type => {
749
+ const component = this.route.options[type];
750
+ if (this[type]?.preload) {
751
+ this[type] = await this.router.options.loadComponent(component);
752
752
  }
753
- });
754
- const after = async () => {
755
- await checkLatest();
756
- setStore(s => s.isFetching = false);
757
- delete routeMatch.__.loadPromise;
758
- resolve();
759
- };
753
+ }));
754
+ })();
755
+ const dataPromise = Promise.resolve().then(async () => {
760
756
  try {
761
- await Promise.all([componentsPromise, dataPromise.catch(() => {})]);
762
- after();
763
- } catch {
764
- after();
757
+ if (this.route.options.loader) {
758
+ const data = await this.router.loadMatchData(this);
759
+ if (latestPromise = checkLatest()) return latestPromise;
760
+ this.#setLoaderData(data);
761
+ }
762
+ this.store.setState(s => {
763
+ s.error = undefined;
764
+ s.status = 'success';
765
+ s.updatedAt = Date.now();
766
+ s.invalidAt = s.updatedAt + (opts?.maxAge ?? this.route.options.loaderMaxAge ?? this.router.options.defaultLoaderMaxAge ?? 0);
767
+ });
768
+ return this.store.state.routeLoaderData;
769
+ } catch (err) {
770
+ if (latestPromise = checkLatest()) return latestPromise;
771
+ if (process.env.NODE_ENV !== 'production') {
772
+ console.error(err);
773
+ }
774
+ this.store.setState(s => {
775
+ s.error = err;
776
+ s.status = 'error';
777
+ s.updatedAt = Date.now();
778
+ });
779
+ throw err;
765
780
  }
766
781
  });
767
- await routeMatch.__.loadPromise;
768
- await checkLatest();
782
+ const after = async () => {
783
+ if (latestPromise = checkLatest()) return latestPromise;
784
+ this.store.setState(s => s.isFetching = false);
785
+ this.#resolve();
786
+ delete this.__loadPromise;
787
+ };
788
+ try {
789
+ await Promise.all([componentsPromise, dataPromise.catch(() => {})]);
790
+ after();
791
+ } catch {
792
+ after();
793
+ }
794
+ });
795
+ return this.__loadPromise;
796
+ };
797
+ invalidate = async () => {
798
+ this.store.setState(s => s.invalid = true);
799
+ if (this.router.store.state.currentMatches.find(d => d.id === this.id)) {
800
+ await this.load();
801
+ }
802
+ };
803
+ __hasLoaders = () => {
804
+ return !!(this.route.options.loader || componentTypes.some(d => this.route.options[d]?.preload));
805
+ };
806
+ getIsInvalid = () => {
807
+ const now = Date.now();
808
+ return this.store.state.invalid || this.store.state.invalidAt < now;
809
+ };
810
+ #updateLoaderData = () => {
811
+ this.store.setState(s => {
812
+ s.loaderData = replaceEqualDeep(s.loaderData, {
813
+ ...this.parentMatch?.store.state.loaderData,
814
+ ...s.routeLoaderData
815
+ });
816
+ });
817
+ this.onLoaderDataListeners.forEach(listener => listener());
818
+ };
819
+ __setParentMatch = parentMatch => {
820
+ if (!this.parentMatch && parentMatch) {
821
+ this.parentMatch = parentMatch;
822
+ this.parentMatch.__onLoaderData(() => {
823
+ this.#updateLoaderData();
824
+ });
825
+ }
826
+ };
827
+ __onLoaderData = listener => {
828
+ this.onLoaderDataListeners.add(listener);
829
+ // return () => this.onLoaderDataListeners.delete(listener)
830
+ };
831
+
832
+ __validate = () => {
833
+ // Validate the search params and stabilize them
834
+ const parentSearch = this.parentMatch?.store.state.search ?? this.router.store.state.latestLocation.search;
835
+ try {
836
+ const prevSearch = this.store.state.routeSearch;
837
+ const validator = typeof this.route.options.validateSearch === 'object' ? this.route.options.validateSearch.parse : this.route.options.validateSearch;
838
+ let nextSearch = validator?.(parentSearch) ?? {};
839
+ batch(() => {
840
+ // Invalidate route matches when search param stability changes
841
+ if (prevSearch !== nextSearch) {
842
+ this.store.setState(s => s.invalid = true);
843
+ }
844
+ this.store.setState(s => {
845
+ s.routeSearch = nextSearch;
846
+ s.search = {
847
+ ...parentSearch,
848
+ ...nextSearch
849
+ };
850
+ });
851
+ });
852
+ componentTypes.map(async type => {
853
+ const component = this.route.options[type];
854
+ if (typeof this[type] !== 'function') {
855
+ this[type] = component;
856
+ }
857
+ });
858
+ } catch (err) {
859
+ console.error(err);
860
+ const error = new Error('Invalid search params found', {
861
+ cause: err
862
+ });
863
+ error.code = 'INVALID_SEARCH_PARAMS';
864
+ this.store.setState(s => {
865
+ s.status = 'error';
866
+ s.error = error;
867
+ });
868
+
869
+ // Do not proceed with loading the route
870
+ return;
769
871
  }
770
872
  };
771
- if (!routeMatch.hasLoaders()) {
772
- setStore(s => s.status = 'success');
773
- }
774
- return routeMatch;
775
873
  }
776
874
 
777
875
  const defaultParseSearch = parseSearchWith(JSON.parse);
@@ -821,768 +919,792 @@ function stringifySearchWith(stringify) {
821
919
  };
822
920
  }
823
921
 
824
- var _window$document;
825
- // Detect if we're in the DOM
826
- const isServer = typeof window === 'undefined' || !((_window$document = window.document) != null && _window$document.createElement);
922
+ const defaultFetchServerDataFn = async ({
923
+ router,
924
+ routeMatch
925
+ }) => {
926
+ const next = router.buildNext({
927
+ to: '.',
928
+ search: d => ({
929
+ ...(d ?? {}),
930
+ __data: {
931
+ matchId: routeMatch.id
932
+ }
933
+ })
934
+ });
935
+ const res = await fetch(next.href, {
936
+ method: 'GET',
937
+ signal: routeMatch.abortController.signal
938
+ });
939
+ if (res.ok) {
940
+ return res.json();
941
+ }
942
+ throw new Error('Failed to fetch match data');
943
+ };
944
+ class Router {
945
+ // __location: Location<TAllRouteInfo['fullSearchSchema']>
827
946
 
828
- // This is the default history object if none is defined
829
- const createDefaultHistory = () => isServer ? createMemoryHistory() : createBrowserHistory();
830
- function getInitialRouterState() {
831
- return {
832
- status: 'idle',
833
- latestLocation: null,
834
- currentLocation: null,
835
- currentMatches: [],
836
- actions: {},
837
- loaders: {},
838
- lastUpdated: Date.now(),
839
- matchCache: {},
840
- get isFetching() {
841
- return this.status === 'loading' || this.currentMatches.some(d => d.store.isFetching);
842
- },
843
- get isPreloading() {
844
- return Object.values(this.matchCache).some(d => d.match.store.isFetching && !this.currentMatches.find(dd => dd.matchId === d.match.matchId));
845
- }
846
- };
847
- }
848
- function createRouter(userOptions) {
849
- const originalOptions = {
850
- defaultLoaderGcMaxAge: 5 * 60 * 1000,
851
- defaultLoaderMaxAge: 0,
852
- defaultPreloadMaxAge: 2000,
853
- defaultPreloadDelay: 50,
854
- context: undefined,
855
- ...userOptions,
856
- stringifySearch: (userOptions == null ? void 0 : userOptions.stringifySearch) ?? defaultStringifySearch,
857
- parseSearch: (userOptions == null ? void 0 : userOptions.parseSearch) ?? defaultParseSearch
858
- };
859
- const [store, setStore] = createStore(getInitialRouterState());
860
- let navigationPromise;
861
- let startedLoadingAt = Date.now();
862
- let resolveNavigation = () => {};
863
- function onFocus() {
864
- router.load();
947
+ startedLoadingAt = Date.now();
948
+ resolveNavigation = () => {};
949
+ constructor(options) {
950
+ this.options = {
951
+ defaultLoaderGcMaxAge: 5 * 60 * 1000,
952
+ defaultLoaderMaxAge: 0,
953
+ defaultPreloadMaxAge: 2000,
954
+ defaultPreloadDelay: 50,
955
+ context: undefined,
956
+ ...options,
957
+ stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
958
+ parseSearch: options?.parseSearch ?? defaultParseSearch,
959
+ fetchServerDataFn: options?.fetchServerDataFn ?? defaultFetchServerDataFn
960
+ };
961
+ this.history = this.options?.history ?? isServer ? createMemoryHistory() : createBrowserHistory();
962
+ this.store = createStore(getInitialRouterState());
963
+ this.basepath = '';
964
+ this.update(options);
965
+
966
+ // Allow frameworks to hook into the router creation
967
+ this.options.createRouter?.(this);
865
968
  }
866
- function buildRouteTree(rootRouteConfig) {
867
- const recurseRoutes = (routeConfigs, parent) => {
868
- return routeConfigs.map((routeConfig, i) => {
869
- const routeOptions = routeConfig.options;
870
- const route = createRoute(routeConfig, routeOptions, i, parent, router);
871
- const existingRoute = router.routesById[route.routeId];
872
- if (existingRoute) {
873
- if (process.env.NODE_ENV !== 'production') {
874
- console.warn(`Duplicate routes found with id: ${String(route.routeId)}`, router.routesById, route);
875
- }
876
- throw new Error();
969
+ reset = () => {
970
+ this.store.setState(s => Object.assign(s, getInitialRouterState()));
971
+ };
972
+ mount = () => {
973
+ // Mount only does anything on the client
974
+ if (!isServer) {
975
+ // If the router matches are empty, load the matches
976
+ if (!this.store.state.currentMatches.length) {
977
+ this.load();
978
+ }
979
+ const unsubHistory = this.history.listen(() => {
980
+ this.load(this.#parseLocation(this.store.state.latestLocation));
981
+ });
982
+ const visibilityChangeEvent = 'visibilitychange';
983
+ const focusEvent = 'focus';
984
+
985
+ // addEventListener does not exist in React Native, but window does
986
+ // In the future, we might need to invert control here for more adapters
987
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
988
+ if (window.addEventListener) {
989
+ // Listen to visibilitychange and focus
990
+ window.addEventListener(visibilityChangeEvent, this.#onFocus, false);
991
+ window.addEventListener(focusEvent, this.#onFocus, false);
992
+ }
993
+ return () => {
994
+ unsubHistory();
995
+ if (window.removeEventListener) {
996
+ // Be sure to unsubscribe if a new handler is set
997
+
998
+ window.removeEventListener(visibilityChangeEvent, this.#onFocus);
999
+ window.removeEventListener(focusEvent, this.#onFocus);
877
1000
  }
878
- router.routesById[route.routeId] = route;
879
- const children = routeConfig.children;
880
- route.childRoutes = children != null && children.length ? recurseRoutes(children, route) : undefined;
881
- return route;
1001
+ };
1002
+ }
1003
+ return () => {};
1004
+ };
1005
+ update = opts => {
1006
+ if (!this.store.state.latestLocation) {
1007
+ this.store.setState(s => {
1008
+ s.latestLocation = this.#parseLocation();
1009
+ s.currentLocation = s.latestLocation;
882
1010
  });
883
- };
884
- const routes = recurseRoutes([rootRouteConfig]);
885
- return routes[0];
886
- }
887
- function parseLocation(location, previousLocation) {
888
- const parsedSearch = router.options.parseSearch(location.search);
889
- return {
890
- pathname: location.pathname,
891
- searchStr: location.search,
892
- search: sharedClone(previousLocation == null ? void 0 : previousLocation.search, parsedSearch),
893
- hash: location.hash.split('#').reverse()[0] ?? '',
894
- href: `${location.pathname}${location.search}${location.hash}`,
895
- state: location.state,
896
- key: location.key
897
- };
898
- }
899
- function navigate(location) {
900
- const next = router.buildNext(location);
901
- return commitLocation(next, location.replace);
902
- }
903
- function buildLocation(dest) {
904
- var _last, _dest$__preSearchFilt, _dest$__preSearchFilt2, _dest$__postSearchFil;
905
- if (dest === void 0) {
906
- dest = {};
907
1011
  }
908
- const fromPathname = dest.fromCurrent ? store.latestLocation.pathname : dest.from ?? store.latestLocation.pathname;
909
- let pathname = resolvePath(router.basepath ?? '/', fromPathname, `${dest.to ?? '.'}`);
910
- const fromMatches = router.matchRoutes(store.latestLocation.pathname, {
911
- strictParseParams: true
1012
+ Object.assign(this.options, opts);
1013
+ const {
1014
+ basepath,
1015
+ routeConfig
1016
+ } = this.options;
1017
+ this.basepath = `/${trimPath(basepath ?? '') ?? ''}`;
1018
+ if (routeConfig) {
1019
+ this.routesById = {};
1020
+ this.routeTree = this.#buildRouteTree(routeConfig);
1021
+ }
1022
+ return this;
1023
+ };
1024
+ buildNext = opts => {
1025
+ const next = this.#buildLocation(opts);
1026
+ const matches = this.matchRoutes(next.pathname);
1027
+ const __preSearchFilters = matches.map(match => match.route.options.preSearchFilters ?? []).flat().filter(Boolean);
1028
+ const __postSearchFilters = matches.map(match => match.route.options.postSearchFilters ?? []).flat().filter(Boolean);
1029
+ return this.#buildLocation({
1030
+ ...opts,
1031
+ __preSearchFilters,
1032
+ __postSearchFilters
912
1033
  });
913
- const toMatches = router.matchRoutes(pathname);
914
- const prevParams = {
915
- ...((_last = last(fromMatches)) == null ? void 0 : _last.params)
916
- };
917
- let nextParams = (dest.params ?? true) === true ? prevParams : functionalUpdate(dest.params, prevParams);
918
- if (nextParams) {
919
- toMatches.map(d => d.options.stringifyParams).filter(Boolean).forEach(fn => {
920
- Object.assign({}, nextParams, fn(nextParams));
921
- });
922
- }
923
- pathname = interpolatePath(pathname, nextParams ?? {});
1034
+ };
1035
+ cancelMatches = () => {
1036
+ [...this.store.state.currentMatches, ...(this.store.state.pendingMatches || [])].forEach(match => {
1037
+ match.cancel();
1038
+ });
1039
+ };
1040
+ load = async next => {
1041
+ let now = Date.now();
1042
+ const startedAt = now;
1043
+ this.startedLoadingAt = startedAt;
924
1044
 
925
- // Pre filters first
926
- const preFilteredSearch = (_dest$__preSearchFilt = dest.__preSearchFilters) != null && _dest$__preSearchFilt.length ? dest.__preSearchFilters.reduce((prev, next) => next(prev), store.latestLocation.search) : store.latestLocation.search;
1045
+ // Cancel any pending matches
1046
+ this.cancelMatches();
1047
+ let matches;
1048
+ batch(() => {
1049
+ if (next) {
1050
+ // Ingest the new location
1051
+ this.store.setState(s => {
1052
+ s.latestLocation = next;
1053
+ });
1054
+ }
927
1055
 
928
- // Then the link/navigate function
929
- const destSearch = dest.search === true ? preFilteredSearch // Preserve resolvedFrom true
930
- : dest.search ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
931
- : (_dest$__preSearchFilt2 = dest.__preSearchFilters) != null && _dest$__preSearchFilt2.length ? preFilteredSearch // Preserve resolvedFrom filters
932
- : {};
1056
+ // Match the routes
1057
+ matches = this.matchRoutes(this.store.state.latestLocation.pathname, {
1058
+ strictParseParams: true
1059
+ });
1060
+ this.store.setState(s => {
1061
+ s.status = 'loading';
1062
+ s.pendingMatches = matches;
1063
+ s.pendingLocation = this.store.state.latestLocation;
1064
+ });
1065
+ });
933
1066
 
934
- // Then post filters
935
- const postFilteredSearch = (_dest$__postSearchFil = dest.__postSearchFilters) != null && _dest$__postSearchFil.length ? dest.__postSearchFilters.reduce((prev, next) => next(prev), destSearch) : destSearch;
936
- const search = sharedClone(store.latestLocation.search, postFilteredSearch);
937
- const searchStr = router.options.stringifySearch(search);
938
- let hash = dest.hash === true ? store.latestLocation.hash : functionalUpdate(dest.hash, store.latestLocation.hash);
939
- hash = hash ? `#${hash}` : '';
940
- return {
941
- pathname,
942
- search,
943
- searchStr,
944
- state: store.latestLocation.state,
945
- hash,
946
- href: `${pathname}${searchStr}${hash}`,
947
- key: dest.key
948
- };
949
- }
950
- function commitLocation(next, replace) {
951
- const id = '' + Date.now() + Math.random();
952
- let nextAction = 'replace';
953
- if (!replace) {
954
- nextAction = 'push';
1067
+ // Load the matches
1068
+ try {
1069
+ await this.loadMatches(matches);
1070
+ } catch (err) {
1071
+ console.warn(err);
1072
+ invariant(false, 'Matches failed to load due to error above ☝️. Navigation cancelled!');
955
1073
  }
956
- const isSameUrl = parseLocation(router.history.location).href === next.href;
957
- if (isSameUrl && !next.key) {
958
- nextAction = 'replace';
1074
+ if (this.startedLoadingAt !== startedAt) {
1075
+ // Ignore side-effects of outdated side-effects
1076
+ return this.navigationPromise;
959
1077
  }
960
- router.history[nextAction]({
961
- pathname: next.pathname,
962
- hash: next.hash,
963
- search: next.searchStr
964
- }, {
965
- id,
966
- ...next.state
1078
+ const previousMatches = this.store.state.currentMatches;
1079
+ const exiting = [],
1080
+ staying = [];
1081
+ previousMatches.forEach(d => {
1082
+ if (matches.find(dd => dd.id === d.id)) {
1083
+ staying.push(d);
1084
+ } else {
1085
+ exiting.push(d);
1086
+ }
967
1087
  });
968
- return navigationPromise = new Promise(resolve => {
969
- const previousNavigationResolve = resolveNavigation;
970
- resolveNavigation = () => {
971
- previousNavigationResolve();
972
- resolve();
973
- };
1088
+ const entering = matches.filter(d => {
1089
+ return !previousMatches.find(dd => dd.id === d.id);
974
1090
  });
975
- }
976
- const router = {
977
- types: undefined,
978
- // public api
979
- history: (userOptions == null ? void 0 : userOptions.history) || createDefaultHistory(),
980
- store,
981
- setStore,
982
- options: originalOptions,
983
- basepath: '',
984
- routeTree: undefined,
985
- routesById: {},
986
- reset: () => {
987
- setStore(s => Object.assign(s, getInitialRouterState()));
988
- },
989
- getRoute: id => {
990
- return router.routesById[id];
991
- },
992
- dehydrate: () => {
993
- return {
994
- store: {
995
- ...pick(store, ['latestLocation', 'currentLocation', 'status', 'lastUpdated']),
996
- currentMatches: store.currentMatches.map(match => ({
997
- matchId: match.matchId,
998
- store: pick(match.store, ['status', 'routeLoaderData', 'isInvalid', 'invalidAt'])
999
- }))
1000
- },
1001
- context: router.options.context
1002
- };
1003
- },
1004
- hydrate: dehydratedRouter => {
1005
- setStore(s => {
1006
- // Update the context TODO: make this part of state?
1007
- router.options.context = dehydratedRouter.context;
1008
-
1009
- // Match the routes
1010
- const currentMatches = router.matchRoutes(dehydratedRouter.store.latestLocation.pathname, {
1011
- strictParseParams: true
1012
- });
1013
- currentMatches.forEach((match, index) => {
1014
- const dehydratedMatch = dehydratedRouter.store.currentMatches[index];
1015
- invariant(dehydratedMatch && dehydratedMatch.matchId === match.matchId, 'Oh no! There was a hydration mismatch when attempting to restore the state of the router! 😬');
1016
- Object.assign(match, dehydratedMatch);
1017
- });
1018
- currentMatches.forEach(match => match.__.validate());
1019
- Object.assign(s, {
1020
- ...dehydratedRouter.store,
1021
- currentMatches
1022
- });
1091
+ now = Date.now();
1092
+ exiting.forEach(d => {
1093
+ d.__onExit?.({
1094
+ params: d.params,
1095
+ search: d.store.state.routeSearch
1023
1096
  });
1024
- },
1025
- mount: () => {
1026
- // Mount only does anything on the client
1027
- if (!isServer) {
1028
- // If the router matches are empty, load the matches
1029
- if (!store.currentMatches.length) {
1030
- router.load();
1031
- }
1032
- const unsub = router.history.listen(event => {
1033
- router.load(parseLocation(event.location, store.latestLocation));
1034
- });
1035
1097
 
1036
- // addEventListener does not exist in React Native, but window does
1037
- // In the future, we might need to invert control here for more adapters
1038
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1039
- if (window.addEventListener) {
1040
- // Listen to visibilitychange and focus
1041
- window.addEventListener('visibilitychange', onFocus, false);
1042
- window.addEventListener('focus', onFocus, false);
1043
- }
1044
- return () => {
1045
- unsub();
1046
- if (window.removeEventListener) {
1047
- // Be sure to unsubscribe if a new handler is set
1048
- window.removeEventListener('visibilitychange', onFocus);
1049
- window.removeEventListener('focus', onFocus);
1050
- }
1051
- };
1052
- }
1053
- return () => {};
1054
- },
1055
- update: opts => {
1056
- const newHistory = (opts == null ? void 0 : opts.history) !== router.history;
1057
- if (!store.latestLocation || newHistory) {
1058
- if (opts != null && opts.history) {
1059
- router.history = opts.history;
1060
- }
1061
- setStore(s => {
1062
- s.latestLocation = parseLocation(router.history.location);
1063
- s.currentLocation = s.latestLocation;
1098
+ // Clear non-loading error states when match leaves
1099
+ if (d.store.state.status === 'error' && !d.store.state.isFetching) {
1100
+ d.store.setState(s => {
1101
+ s.status = 'idle';
1102
+ s.error = undefined;
1064
1103
  });
1065
1104
  }
1066
- Object.assign(router.options, opts);
1067
- const {
1068
- basepath,
1069
- routeConfig
1070
- } = router.options;
1071
- router.basepath = `/${trimPath(basepath ?? '') ?? ''}`;
1072
- if (routeConfig) {
1073
- router.routesById = {};
1074
- router.routeTree = buildRouteTree(routeConfig);
1075
- }
1076
- return router;
1077
- },
1078
- cancelMatches: () => {
1079
- [...store.currentMatches, ...(store.pendingMatches || [])].forEach(match => {
1080
- match.cancel();
1081
- });
1082
- },
1083
- load: async next => {
1084
- let now = Date.now();
1085
- const startedAt = now;
1086
- startedLoadingAt = startedAt;
1087
-
1088
- // Cancel any pending matches
1089
- router.cancelMatches();
1090
- let matches;
1091
- batch(() => {
1092
- if (next) {
1093
- // Ingest the new location
1094
- setStore(s => {
1095
- s.latestLocation = next;
1096
- });
1097
- }
1098
-
1099
- // Match the routes
1100
- matches = router.matchRoutes(store.latestLocation.pathname, {
1101
- strictParseParams: true
1102
- });
1103
- console.log('set loading', matches);
1104
- setStore(s => {
1105
- s.status = 'loading';
1106
- s.pendingMatches = matches;
1107
- s.pendingLocation = store.latestLocation;
1108
- });
1109
- });
1110
-
1111
- // Load the matches
1112
- try {
1113
- await router.loadMatches(matches);
1114
- } catch (err) {
1115
- console.log(err);
1116
- invariant(false, 'Matches failed to load due to error above ☝️. Navigation cancelled!');
1117
- }
1118
- if (startedLoadingAt !== startedAt) {
1119
- // Ignore side-effects of outdated side-effects
1120
- return navigationPromise;
1121
- }
1122
- const previousMatches = store.currentMatches;
1123
- const exiting = [],
1124
- staying = [];
1125
- previousMatches.forEach(d => {
1126
- if (matches.find(dd => dd.matchId === d.matchId)) {
1127
- staying.push(d);
1128
- } else {
1129
- exiting.push(d);
1130
- }
1131
- });
1132
- const entering = matches.filter(d => {
1133
- return !previousMatches.find(dd => dd.matchId === d.matchId);
1134
- });
1135
- now = Date.now();
1136
- exiting.forEach(d => {
1137
- d.__.onExit == null ? void 0 : d.__.onExit({
1138
- params: d.params,
1139
- search: d.store.routeSearch
1140
- });
1141
-
1142
- // Clear idle error states when match leaves
1143
- if (d.store.status === 'error' && !d.store.isFetching) {
1144
- d.store.status = 'idle';
1145
- d.store.error = undefined;
1146
- }
1147
- const gc = Math.max(d.options.loaderGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0, d.options.loaderMaxAge ?? router.options.defaultLoaderMaxAge ?? 0);
1148
- if (gc > 0) {
1149
- store.matchCache[d.matchId] = {
1105
+ const gc = Math.max(d.route.options.loaderGcMaxAge ?? this.options.defaultLoaderGcMaxAge ?? 0, d.route.options.loaderMaxAge ?? this.options.defaultLoaderMaxAge ?? 0);
1106
+ if (gc > 0) {
1107
+ this.store.setState(s => {
1108
+ s.matchCache[d.id] = {
1150
1109
  gc: gc == Infinity ? Number.MAX_SAFE_INTEGER : now + gc,
1151
1110
  match: d
1152
1111
  };
1153
- }
1154
- });
1155
- staying.forEach(d => {
1156
- d.options.onTransition == null ? void 0 : d.options.onTransition({
1157
- params: d.params,
1158
- search: d.store.routeSearch
1159
- });
1160
- });
1161
- entering.forEach(d => {
1162
- d.__.onExit = d.options.onLoaded == null ? void 0 : d.options.onLoaded({
1163
- params: d.params,
1164
- search: d.store.search
1165
1112
  });
1166
- delete store.matchCache[d.matchId];
1167
- });
1168
- if (startedLoadingAt !== startedAt) {
1169
- // Ignore side-effects of match loading
1170
- return;
1171
1113
  }
1172
- matches.forEach(match => {
1173
- // Clear actions
1174
- if (match.action) {
1175
- // TODO: Check reactivity here
1176
- match.action.current = undefined;
1177
- match.action.submissions = [];
1178
- }
1114
+ });
1115
+ staying.forEach(d => {
1116
+ d.route.options.onTransition?.({
1117
+ params: d.params,
1118
+ search: d.store.state.routeSearch
1179
1119
  });
1180
- setStore(s => {
1181
- console.log('set', matches);
1182
- Object.assign(s, {
1183
- status: 'idle',
1184
- currentLocation: store.latestLocation,
1185
- currentMatches: matches,
1186
- pendingLocation: undefined,
1187
- pendingMatches: undefined
1188
- });
1120
+ });
1121
+ entering.forEach(d => {
1122
+ d.__onExit = d.route.options.onLoaded?.({
1123
+ params: d.params,
1124
+ search: d.store.state.search
1189
1125
  });
1190
- resolveNavigation();
1191
- },
1192
- cleanMatchCache: () => {
1193
- const now = Date.now();
1194
- setStore(s => {
1195
- Object.keys(s.matchCache).forEach(matchId => {
1196
- const entry = s.matchCache[matchId];
1197
-
1198
- // Don't remove loading matches
1199
- if (entry.match.store.status === 'loading') {
1200
- return;
1201
- }
1126
+ delete this.store.state.matchCache[d.id];
1127
+ });
1128
+ this.store.setState(s => {
1129
+ Object.assign(s, {
1130
+ status: 'idle',
1131
+ currentLocation: this.store.state.latestLocation,
1132
+ currentMatches: matches,
1133
+ pendingLocation: undefined,
1134
+ pendingMatches: undefined
1135
+ });
1136
+ });
1137
+ this.options.onRouteChange?.();
1138
+ this.resolveNavigation();
1139
+ };
1140
+ cleanMatchCache = () => {
1141
+ const now = Date.now();
1142
+ this.store.setState(s => {
1143
+ Object.keys(s.matchCache).forEach(matchId => {
1144
+ const entry = s.matchCache[matchId];
1202
1145
 
1203
- // Do not remove successful matches that are still valid
1204
- if (entry.gc > 0 && entry.gc > now) {
1205
- return;
1206
- }
1146
+ // Don't remove loading matches
1147
+ if (entry.match.store.state.status === 'loading') {
1148
+ return;
1149
+ }
1207
1150
 
1208
- // Everything else gets removed
1209
- delete s.matchCache[matchId];
1210
- });
1211
- });
1212
- },
1213
- loadRoute: async function (navigateOpts) {
1214
- if (navigateOpts === void 0) {
1215
- navigateOpts = store.latestLocation;
1216
- }
1217
- const next = router.buildNext(navigateOpts);
1218
- const matches = router.matchRoutes(next.pathname, {
1219
- strictParseParams: true
1220
- });
1221
- await router.loadMatches(matches);
1222
- return matches;
1223
- },
1224
- preloadRoute: async function (navigateOpts, loaderOpts) {
1225
- if (navigateOpts === void 0) {
1226
- navigateOpts = store.latestLocation;
1227
- }
1228
- const next = router.buildNext(navigateOpts);
1229
- const matches = router.matchRoutes(next.pathname, {
1230
- strictParseParams: true
1231
- });
1232
- await router.loadMatches(matches, {
1233
- preload: true,
1234
- maxAge: loaderOpts.maxAge ?? router.options.defaultPreloadMaxAge ?? router.options.defaultLoaderMaxAge ?? 0,
1235
- gcMaxAge: loaderOpts.gcMaxAge ?? router.options.defaultPreloadGcMaxAge ?? router.options.defaultLoaderGcMaxAge ?? 0
1151
+ // Do not remove successful matches that are still valid
1152
+ if (entry.gc > 0 && entry.gc > now) {
1153
+ return;
1154
+ }
1155
+
1156
+ // Everything else gets removed
1157
+ delete s.matchCache[matchId];
1236
1158
  });
1159
+ });
1160
+ };
1161
+ getRoute = id => {
1162
+ const route = this.routesById[id];
1163
+ invariant(route, `Route with id "${id}" not found`);
1164
+ return route;
1165
+ };
1166
+ loadRoute = async (navigateOpts = this.store.state.latestLocation) => {
1167
+ const next = this.buildNext(navigateOpts);
1168
+ const matches = this.matchRoutes(next.pathname, {
1169
+ strictParseParams: true
1170
+ });
1171
+ await this.loadMatches(matches);
1172
+ return matches;
1173
+ };
1174
+ preloadRoute = async (navigateOpts = this.store.state.latestLocation, loaderOpts) => {
1175
+ const next = this.buildNext(navigateOpts);
1176
+ const matches = this.matchRoutes(next.pathname, {
1177
+ strictParseParams: true
1178
+ });
1179
+ await this.loadMatches(matches, {
1180
+ preload: true,
1181
+ maxAge: loaderOpts.maxAge ?? this.options.defaultPreloadMaxAge ?? this.options.defaultLoaderMaxAge ?? 0,
1182
+ gcMaxAge: loaderOpts.gcMaxAge ?? this.options.defaultPreloadGcMaxAge ?? this.options.defaultLoaderGcMaxAge ?? 0
1183
+ });
1184
+ return matches;
1185
+ };
1186
+ matchRoutes = (pathname, opts) => {
1187
+ const matches = [];
1188
+ if (!this.routeTree) {
1237
1189
  return matches;
1238
- },
1239
- matchRoutes: (pathname, opts) => {
1240
- router.cleanMatchCache();
1241
- const matches = [];
1242
- if (!router.routeTree) {
1243
- return matches;
1244
- }
1245
- const existingMatches = [...store.currentMatches, ...(store.pendingMatches ?? [])];
1246
- const recurse = async routes => {
1247
- var _foundRoute$childRout;
1248
- const parentMatch = last(matches);
1249
- let params = (parentMatch == null ? void 0 : parentMatch.params) ?? {};
1250
- const filteredRoutes = (router.options.filterRoutes == null ? void 0 : router.options.filterRoutes(routes)) ?? routes;
1251
- let foundRoutes = [];
1252
- const findMatchInRoutes = (parentRoutes, routes) => {
1253
- routes.some(route => {
1254
- var _route$childRoutes, _route$childRoutes2;
1255
- if (!route.routePath && (_route$childRoutes = route.childRoutes) != null && _route$childRoutes.length) {
1256
- return findMatchInRoutes([...foundRoutes, route], route.childRoutes);
1257
- }
1258
- const fuzzy = !!(route.routePath !== '/' || (_route$childRoutes2 = route.childRoutes) != null && _route$childRoutes2.length);
1259
- const matchParams = matchPathname(router.basepath, pathname, {
1260
- to: route.fullPath,
1261
- fuzzy,
1262
- caseSensitive: route.options.caseSensitive ?? router.options.caseSensitive
1263
- });
1264
- if (matchParams) {
1265
- let parsedParams;
1266
- try {
1267
- parsedParams = (route.options.parseParams == null ? void 0 : route.options.parseParams(matchParams)) ?? matchParams;
1268
- } catch (err) {
1269
- if (opts != null && opts.strictParseParams) {
1270
- throw err;
1271
- }
1190
+ }
1191
+ const existingMatches = [...this.store.state.currentMatches, ...(this.store.state.pendingMatches ?? [])];
1192
+ const recurse = async routes => {
1193
+ const parentMatch = last(matches);
1194
+ let params = parentMatch?.params ?? {};
1195
+ const filteredRoutes = this.options.filterRoutes?.(routes) ?? routes;
1196
+ let foundRoutes = [];
1197
+ const findMatchInRoutes = (parentRoutes, routes) => {
1198
+ routes.some(route => {
1199
+ if (!route.path && route.childRoutes?.length) {
1200
+ return findMatchInRoutes([...foundRoutes, route], route.childRoutes);
1201
+ }
1202
+ const fuzzy = !!(route.path !== '/' || route.childRoutes?.length);
1203
+ const matchParams = matchPathname(this.basepath, pathname, {
1204
+ to: route.fullPath,
1205
+ fuzzy,
1206
+ caseSensitive: route.options.caseSensitive ?? this.options.caseSensitive
1207
+ });
1208
+ if (matchParams) {
1209
+ let parsedParams;
1210
+ try {
1211
+ parsedParams = route.options.parseParams?.(matchParams) ?? matchParams;
1212
+ } catch (err) {
1213
+ if (opts?.strictParseParams) {
1214
+ throw err;
1272
1215
  }
1273
- params = {
1274
- ...params,
1275
- ...parsedParams
1276
- };
1277
- }
1278
- if (!!matchParams) {
1279
- foundRoutes = [...parentRoutes, route];
1280
1216
  }
1281
- return !!foundRoutes.length;
1282
- });
1217
+ params = {
1218
+ ...params,
1219
+ ...parsedParams
1220
+ };
1221
+ }
1222
+ if (!!matchParams) {
1223
+ foundRoutes = [...parentRoutes, route];
1224
+ }
1283
1225
  return !!foundRoutes.length;
1284
- };
1285
- findMatchInRoutes([], filteredRoutes);
1286
- if (!foundRoutes.length) {
1287
- return;
1288
- }
1289
- foundRoutes.forEach(foundRoute => {
1290
- var _store$matchCache$mat;
1291
- const interpolatedPath = interpolatePath(foundRoute.routePath, params);
1292
- const matchId = interpolatePath(foundRoute.routeId, params, true);
1293
- const match = existingMatches.find(d => d.matchId === matchId) || ((_store$matchCache$mat = store.matchCache[matchId]) == null ? void 0 : _store$matchCache$mat.match) || createRouteMatch(router, foundRoute, {
1294
- parentMatch,
1295
- matchId,
1296
- params,
1297
- pathname: joinPaths([router.basepath, interpolatedPath])
1298
- });
1299
- matches.push(match);
1300
1226
  });
1301
- const foundRoute = last(foundRoutes);
1302
- if ((_foundRoute$childRout = foundRoute.childRoutes) != null && _foundRoute$childRout.length) {
1303
- recurse(foundRoute.childRoutes);
1304
- }
1227
+ return !!foundRoutes.length;
1305
1228
  };
1306
- recurse([router.routeTree]);
1307
- linkMatches(matches);
1308
- return matches;
1309
- },
1310
- loadMatches: async (resolvedMatches, loaderOpts) => {
1311
- resolvedMatches.forEach(async match => {
1312
- // Validate the match (loads search params etc)
1313
- match.__.validate();
1229
+ findMatchInRoutes([], filteredRoutes);
1230
+ if (!foundRoutes.length) {
1231
+ return;
1232
+ }
1233
+ foundRoutes.forEach(foundRoute => {
1234
+ const interpolatedPath = interpolatePath(foundRoute.path, params);
1235
+ const matchId = interpolatePath(foundRoute.id, params, true);
1236
+ const match = existingMatches.find(d => d.id === matchId) || this.store.state.matchCache[matchId]?.match || new RouteMatch(this, foundRoute, {
1237
+ matchId,
1238
+ params,
1239
+ pathname: joinPaths([this.basepath, interpolatedPath])
1240
+ });
1241
+ matches.push(match);
1314
1242
  });
1243
+ const foundRoute = last(foundRoutes);
1244
+ if (foundRoute.childRoutes?.length) {
1245
+ recurse(foundRoute.childRoutes);
1246
+ }
1247
+ };
1248
+ recurse([this.routeTree]);
1249
+ linkMatches(matches);
1250
+ return matches;
1251
+ };
1252
+ loadMatches = async (resolvedMatches, loaderOpts) => {
1253
+ this.cleanMatchCache();
1254
+ resolvedMatches.forEach(async match => {
1255
+ // Validate the match (loads search params etc)
1256
+ match.__validate();
1257
+ });
1315
1258
 
1316
- // Check each match middleware to see if the route can be accessed
1317
- await Promise.all(resolvedMatches.map(async match => {
1318
- try {
1319
- await (match.options.beforeLoad == null ? void 0 : match.options.beforeLoad({
1320
- router: router,
1321
- match
1322
- }));
1323
- } catch (err) {
1324
- if (!(loaderOpts != null && loaderOpts.preload)) {
1325
- match.options.onLoadError == null ? void 0 : match.options.onLoadError(err);
1326
- }
1327
- throw err;
1328
- }
1329
- }));
1330
- const matchPromises = resolvedMatches.map(async match => {
1331
- var _search$__data;
1332
- const search = match.store.search;
1333
- if ((_search$__data = search.__data) != null && _search$__data.matchId && search.__data.matchId !== match.matchId) {
1334
- return;
1335
- }
1336
- match.load(loaderOpts);
1337
- if (match.store.status !== 'success' && match.__.loadPromise) {
1338
- // Wait for the first sign of activity from the match
1339
- await match.__.loadPromise;
1340
- }
1341
- });
1342
- await Promise.all(matchPromises);
1343
- },
1344
- loadMatchData: async routeMatch => {
1345
- if (isServer || !router.options.useServerData) {
1346
- return (await (routeMatch.options.loader == null ? void 0 : routeMatch.options.loader({
1347
- // parentLoaderPromise: routeMatch.parentMatch?.__.dataPromise,
1348
- params: routeMatch.params,
1349
- search: routeMatch.store.routeSearch,
1350
- signal: routeMatch.__.abortController.signal
1351
- }))) || {};
1352
- } else {
1353
- const next = router.buildNext({
1354
- to: '.',
1355
- search: d => ({
1356
- ...(d ?? {}),
1357
- __data: {
1358
- matchId: routeMatch.matchId
1359
- }
1360
- })
1259
+ // Check each match middleware to see if the route can be accessed
1260
+ await Promise.all(resolvedMatches.map(async match => {
1261
+ try {
1262
+ await match.route.options.beforeLoad?.({
1263
+ router: this,
1264
+ match
1361
1265
  });
1266
+ } catch (err) {
1267
+ if (!loaderOpts?.preload) {
1268
+ match.route.options.onLoadError?.(err);
1269
+ }
1270
+ throw err;
1271
+ }
1272
+ }));
1273
+ const matchPromises = resolvedMatches.map(async (match, index) => {
1274
+ const prevMatch = resolvedMatches[1];
1275
+ const search = match.store.state.search;
1276
+ if (search.__data?.matchId && search.__data.matchId !== match.id) {
1277
+ return;
1278
+ }
1279
+ match.load(loaderOpts);
1280
+ if (match.store.state.status !== 'success' && match.__loadPromise) {
1281
+ // Wait for the first sign of activity from the match
1282
+ await match.__loadPromise;
1283
+ }
1284
+ if (prevMatch) {
1285
+ await prevMatch.__loadPromise;
1286
+ }
1287
+ });
1288
+ await Promise.all(matchPromises);
1289
+ };
1290
+ loadMatchData = async routeMatch => {
1291
+ if (isServer || !this.options.useServerData) {
1292
+ return (await routeMatch.route.options.loader?.({
1293
+ // parentLoaderPromise: routeMatch.parentMatch.dataPromise,
1294
+ params: routeMatch.params,
1295
+ search: routeMatch.store.state.routeSearch,
1296
+ signal: routeMatch.abortController.signal
1297
+ })) || {};
1298
+ } else {
1299
+ // Refresh:
1300
+ // '/dashboard'
1301
+ // '/dashboard/invoices/'
1302
+ // '/dashboard/invoices/123'
1362
1303
 
1363
- // Refresh:
1364
- // '/dashboard'
1365
- // '/dashboard/invoices/'
1366
- // '/dashboard/invoices/123'
1367
-
1368
- // New:
1369
- // '/dashboard/invoices/456'
1370
-
1371
- // TODO: batch requests when possible
1304
+ // New:
1305
+ // '/dashboard/invoices/456'
1372
1306
 
1373
- const res = await fetch(next.href, {
1374
- method: 'GET'
1375
- // signal: routeMatch.__.abortController.signal,
1376
- });
1307
+ // TODO: batch requests when possible
1377
1308
 
1378
- if (res.ok) {
1379
- return res.json();
1380
- }
1381
- throw new Error('Failed to fetch match data');
1382
- }
1383
- },
1384
- invalidateRoute: opts => {
1385
- const next = router.buildNext(opts);
1386
- const unloadedMatchIds = router.matchRoutes(next.pathname).map(d => d.matchId);
1387
- [...store.currentMatches, ...(store.pendingMatches ?? [])].forEach(match => {
1388
- if (unloadedMatchIds.includes(match.matchId)) {
1389
- match.invalidate();
1390
- }
1309
+ return this.options.fetchServerDataFn({
1310
+ router: this,
1311
+ routeMatch
1391
1312
  });
1392
- },
1393
- reload: () => navigate({
1313
+ }
1314
+ };
1315
+ invalidateRoute = async opts => {
1316
+ const next = this.buildNext(opts);
1317
+ const unloadedMatchIds = this.matchRoutes(next.pathname).map(d => d.id);
1318
+ await Promise.allSettled([...this.store.state.currentMatches, ...(this.store.state.pendingMatches ?? [])].map(async match => {
1319
+ if (unloadedMatchIds.includes(match.id)) {
1320
+ return match.invalidate();
1321
+ }
1322
+ }));
1323
+ };
1324
+ reload = () => {
1325
+ this.navigate({
1394
1326
  fromCurrent: true,
1395
1327
  replace: true,
1396
1328
  search: true
1397
- }),
1398
- resolvePath: (from, path) => {
1399
- return resolvePath(router.basepath, from, cleanPath(path));
1400
- },
1401
- matchRoute: (location, opts) => {
1402
- // const location = router.buildNext(opts)
1329
+ });
1330
+ };
1331
+ resolvePath = (from, path) => {
1332
+ return resolvePath(this.basepath, from, cleanPath(path));
1333
+ };
1334
+ navigate = async ({
1335
+ from,
1336
+ to = '.',
1337
+ search,
1338
+ hash,
1339
+ replace,
1340
+ params
1341
+ }) => {
1342
+ // If this link simply reloads the current route,
1343
+ // make sure it has a new key so it will trigger a data refresh
1403
1344
 
1404
- location = {
1405
- ...location,
1406
- to: location.to ? router.resolvePath(location.from ?? '', location.to) : undefined
1407
- };
1408
- const next = router.buildNext(location);
1409
- if (opts != null && opts.pending) {
1410
- if (!store.pendingLocation) {
1411
- return false;
1412
- }
1413
- return !!matchPathname(router.basepath, store.pendingLocation.pathname, {
1414
- ...opts,
1415
- to: next.pathname
1416
- });
1345
+ // If this `to` is a valid external URL, return
1346
+ // null for LinkUtils
1347
+ const toString = String(to);
1348
+ const fromString = String(from);
1349
+ let isExternal;
1350
+ try {
1351
+ new URL(`${toString}`);
1352
+ isExternal = true;
1353
+ } catch (e) {}
1354
+ invariant(!isExternal, 'Attempting to navigate to external url with this.navigate!');
1355
+ return this.#commitLocation({
1356
+ from: fromString,
1357
+ to: toString,
1358
+ search,
1359
+ hash,
1360
+ replace,
1361
+ params
1362
+ });
1363
+ };
1364
+ matchRoute = (location, opts) => {
1365
+ location = {
1366
+ ...location,
1367
+ to: location.to ? this.resolvePath(location.from ?? '', location.to) : undefined
1368
+ };
1369
+ const next = this.buildNext(location);
1370
+ if (opts?.pending) {
1371
+ if (!this.store.state.pendingLocation) {
1372
+ return false;
1417
1373
  }
1418
- return matchPathname(router.basepath, store.currentLocation.pathname, {
1374
+ return matchPathname(this.basepath, this.store.state.pendingLocation.pathname, {
1419
1375
  ...opts,
1420
1376
  to: next.pathname
1421
1377
  });
1422
- },
1423
- navigate: async _ref => {
1424
- let {
1425
- from,
1426
- to = '.',
1427
- search,
1428
- hash,
1429
- replace,
1430
- params
1431
- } = _ref;
1432
- // If this link simply reloads the current route,
1433
- // make sure it has a new key so it will trigger a data refresh
1434
-
1435
- // If this `to` is a valid external URL, return
1436
- // null for LinkUtils
1437
- const toString = String(to);
1438
- const fromString = String(from);
1439
- let isExternal;
1440
- try {
1441
- new URL(`${toString}`);
1442
- isExternal = true;
1443
- } catch (e) {}
1444
- invariant(!isExternal, 'Attempting to navigate to external url with router.navigate!');
1445
- return navigate({
1446
- from: fromString,
1447
- to: toString,
1448
- search,
1449
- hash,
1450
- replace,
1451
- params
1452
- });
1453
- },
1454
- buildLink: _ref2 => {
1455
- let {
1456
- from,
1457
- to = '.',
1458
- search,
1459
- params,
1460
- hash,
1461
- target,
1462
- replace,
1463
- activeOptions,
1464
- preload,
1465
- preloadMaxAge: userPreloadMaxAge,
1466
- preloadGcMaxAge: userPreloadGcMaxAge,
1467
- preloadDelay: userPreloadDelay,
1468
- disabled
1469
- } = _ref2;
1470
- // If this link simply reloads the current route,
1471
- // make sure it has a new key so it will trigger a data refresh
1472
-
1473
- // If this `to` is a valid external URL, return
1474
- // null for LinkUtils
1378
+ }
1379
+ return matchPathname(this.basepath, this.store.state.currentLocation.pathname, {
1380
+ ...opts,
1381
+ to: next.pathname
1382
+ });
1383
+ };
1384
+ buildLink = ({
1385
+ from,
1386
+ to = '.',
1387
+ search,
1388
+ params,
1389
+ hash,
1390
+ target,
1391
+ replace,
1392
+ activeOptions,
1393
+ preload,
1394
+ preloadMaxAge: userPreloadMaxAge,
1395
+ preloadGcMaxAge: userPreloadGcMaxAge,
1396
+ preloadDelay: userPreloadDelay,
1397
+ disabled
1398
+ }) => {
1399
+ // If this link simply reloads the current route,
1400
+ // make sure it has a new key so it will trigger a data refresh
1475
1401
 
1476
- try {
1477
- new URL(`${to}`);
1478
- return {
1479
- type: 'external',
1480
- href: to
1481
- };
1482
- } catch (e) {}
1483
- const nextOpts = {
1484
- from,
1485
- to,
1486
- search,
1487
- params,
1488
- hash,
1489
- replace
1402
+ // If this `to` is a valid external URL, return
1403
+ // null for LinkUtils
1404
+
1405
+ try {
1406
+ new URL(`${to}`);
1407
+ return {
1408
+ type: 'external',
1409
+ href: to
1490
1410
  };
1491
- const next = router.buildNext(nextOpts);
1492
- preload = preload ?? router.options.defaultPreload;
1493
- const preloadDelay = userPreloadDelay ?? router.options.defaultPreloadDelay ?? 0;
1494
-
1495
- // Compare path/hash for matches
1496
- const pathIsEqual = store.currentLocation.pathname === next.pathname;
1497
- const currentPathSplit = store.currentLocation.pathname.split('/');
1498
- const nextPathSplit = next.pathname.split('/');
1499
- const pathIsFuzzyEqual = nextPathSplit.every((d, i) => d === currentPathSplit[i]);
1500
- const hashIsEqual = store.currentLocation.hash === next.hash;
1501
- // Combine the matches based on user options
1502
- const pathTest = activeOptions != null && activeOptions.exact ? pathIsEqual : pathIsFuzzyEqual;
1503
- const hashTest = activeOptions != null && activeOptions.includeHash ? hashIsEqual : true;
1504
-
1505
- // The final "active" test
1506
- const isActive = pathTest && hashTest;
1507
-
1508
- // The click handler
1509
- const handleClick = e => {
1510
- if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!target || target === '_self') && e.button === 0) {
1511
- e.preventDefault();
1512
- if (pathIsEqual && !search && !hash) {
1513
- router.invalidateRoute(nextOpts);
1514
- }
1411
+ } catch (e) {}
1412
+ const nextOpts = {
1413
+ from,
1414
+ to,
1415
+ search,
1416
+ params,
1417
+ hash,
1418
+ replace
1419
+ };
1420
+ const next = this.buildNext(nextOpts);
1421
+ preload = preload ?? this.options.defaultPreload;
1422
+ const preloadDelay = userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0;
1423
+
1424
+ // Compare path/hash for matches
1425
+ const pathIsEqual = this.store.state.currentLocation.pathname === next.pathname;
1426
+ const currentPathSplit = this.store.state.currentLocation.pathname.split('/');
1427
+ const nextPathSplit = next.pathname.split('/');
1428
+ const pathIsFuzzyEqual = nextPathSplit.every((d, i) => d === currentPathSplit[i]);
1429
+ const hashIsEqual = this.store.state.currentLocation.hash === next.hash;
1430
+ // Combine the matches based on user options
1431
+ const pathTest = activeOptions?.exact ? pathIsEqual : pathIsFuzzyEqual;
1432
+ const hashTest = activeOptions?.includeHash ? hashIsEqual : true;
1515
1433
 
1516
- // All is well? Navigate!
1517
- navigate(nextOpts);
1434
+ // The final "active" test
1435
+ const isActive = pathTest && hashTest;
1436
+
1437
+ // The click handler
1438
+ const handleClick = e => {
1439
+ if (!disabled && !isCtrlEvent(e) && !e.defaultPrevented && (!target || target === '_self') && e.button === 0) {
1440
+ e.preventDefault();
1441
+ if (pathIsEqual && !search && !hash) {
1442
+ this.invalidateRoute(nextOpts);
1518
1443
  }
1519
- };
1520
1444
 
1521
- // The click handler
1522
- const handleFocus = e => {
1523
- if (preload) {
1524
- router.preloadRoute(nextOpts, {
1445
+ // All is well? Navigate!
1446
+ this.#commitLocation(nextOpts);
1447
+ }
1448
+ };
1449
+
1450
+ // The click handler
1451
+ const handleFocus = e => {
1452
+ if (preload) {
1453
+ this.preloadRoute(nextOpts, {
1454
+ maxAge: userPreloadMaxAge,
1455
+ gcMaxAge: userPreloadGcMaxAge
1456
+ }).catch(err => {
1457
+ console.warn(err);
1458
+ console.warn('Error preloading route! ☝️');
1459
+ });
1460
+ }
1461
+ };
1462
+ const handleEnter = e => {
1463
+ const target = e.target || {};
1464
+ if (preload) {
1465
+ if (target.preloadTimeout) {
1466
+ return;
1467
+ }
1468
+ target.preloadTimeout = setTimeout(() => {
1469
+ target.preloadTimeout = null;
1470
+ this.preloadRoute(nextOpts, {
1525
1471
  maxAge: userPreloadMaxAge,
1526
1472
  gcMaxAge: userPreloadGcMaxAge
1527
1473
  }).catch(err => {
1528
- console.log(err);
1474
+ console.warn(err);
1529
1475
  console.warn('Error preloading route! ☝️');
1530
1476
  });
1531
- }
1532
- };
1533
- const handleEnter = e => {
1534
- const target = e.target || {};
1535
- if (preload) {
1536
- if (target.preloadTimeout) {
1537
- return;
1477
+ }, preloadDelay);
1478
+ }
1479
+ };
1480
+ const handleLeave = e => {
1481
+ const target = e.target || {};
1482
+ if (target.preloadTimeout) {
1483
+ clearTimeout(target.preloadTimeout);
1484
+ target.preloadTimeout = null;
1485
+ }
1486
+ };
1487
+ return {
1488
+ type: 'internal',
1489
+ next,
1490
+ handleFocus,
1491
+ handleClick,
1492
+ handleEnter,
1493
+ handleLeave,
1494
+ isActive,
1495
+ disabled
1496
+ };
1497
+ };
1498
+ dehydrate = () => {
1499
+ return {
1500
+ state: {
1501
+ ...pick(this.store.state, ['latestLocation', 'currentLocation', 'status', 'lastUpdated']),
1502
+ currentMatches: this.store.state.currentMatches.map(match => ({
1503
+ matchId: match.id,
1504
+ state: {
1505
+ ...pick(match.store.state, ['status', 'routeLoaderData', 'invalidAt', 'invalid'])
1538
1506
  }
1539
- target.preloadTimeout = setTimeout(() => {
1540
- target.preloadTimeout = null;
1541
- router.preloadRoute(nextOpts, {
1542
- maxAge: userPreloadMaxAge,
1543
- gcMaxAge: userPreloadGcMaxAge
1544
- }).catch(err => {
1545
- console.log(err);
1546
- console.warn('Error preloading route! ☝️');
1507
+ }))
1508
+ },
1509
+ context: this.options.context
1510
+ };
1511
+ };
1512
+ hydrate = dehydratedRouter => {
1513
+ this.store.setState(s => {
1514
+ // Update the context TODO: make this part of state?
1515
+ this.options.context = dehydratedRouter.context;
1516
+
1517
+ // Match the routes
1518
+ const currentMatches = this.matchRoutes(dehydratedRouter.state.latestLocation.pathname, {
1519
+ strictParseParams: true
1520
+ });
1521
+ currentMatches.forEach((match, index) => {
1522
+ const dehydratedMatch = dehydratedRouter.state.currentMatches[index];
1523
+ invariant(dehydratedMatch && dehydratedMatch.matchId === match.id, 'Oh no! There was a hydration mismatch when attempting to rethis.store the state of the router! 😬');
1524
+ Object.assign(match, dehydratedMatch);
1525
+ });
1526
+ currentMatches.forEach(match => match.__validate());
1527
+ Object.assign(s, {
1528
+ ...dehydratedRouter.state,
1529
+ currentMatches
1530
+ });
1531
+ });
1532
+ };
1533
+ getLoader = opts => {
1534
+ const id = opts.from || '/';
1535
+ const route = this.getRoute(id);
1536
+ if (!route) return undefined;
1537
+ let loader = this.store.state.loaders[id] || (() => {
1538
+ this.store.setState(s => {
1539
+ s.loaders[id] = {
1540
+ pending: [],
1541
+ fetch: async loaderContext => {
1542
+ if (!route) {
1543
+ return;
1544
+ }
1545
+ const loaderState = {
1546
+ loadedAt: Date.now(),
1547
+ loaderContext
1548
+ };
1549
+ this.store.setState(s => {
1550
+ s.loaders[id].current = loaderState;
1551
+ s.loaders[id].latest = loaderState;
1552
+ s.loaders[id].pending.push(loaderState);
1547
1553
  });
1548
- }, preloadDelay);
1549
- }
1550
- };
1551
- const handleLeave = e => {
1552
- const target = e.target || {};
1553
- if (target.preloadTimeout) {
1554
- clearTimeout(target.preloadTimeout);
1555
- target.preloadTimeout = null;
1554
+ try {
1555
+ return await route.options.loader?.(loaderContext);
1556
+ } finally {
1557
+ this.store.setState(s => {
1558
+ s.loaders[id].pending = s.loaders[id].pending.filter(d => d !== loaderState);
1559
+ });
1560
+ }
1561
+ }
1562
+ };
1563
+ });
1564
+ return this.store.state.loaders[id];
1565
+ })();
1566
+ return loader;
1567
+ };
1568
+ #buildRouteTree = rootRouteConfig => {
1569
+ const recurseRoutes = (routeConfigs, parent) => {
1570
+ return routeConfigs.map((routeConfig, i) => {
1571
+ const routeOptions = routeConfig.options;
1572
+ const route = new Route(routeConfig, routeOptions, i, parent, this);
1573
+ const existingRoute = this.routesById[route.id];
1574
+ if (existingRoute) {
1575
+ if (process.env.NODE_ENV !== 'production') {
1576
+ console.warn(`Duplicate routes found with id: ${String(route.id)}`, this.routesById, route);
1577
+ }
1578
+ throw new Error();
1556
1579
  }
1557
- };
1558
- return {
1559
- type: 'internal',
1560
- next,
1561
- handleFocus,
1562
- handleClick,
1563
- handleEnter,
1564
- handleLeave,
1565
- isActive,
1566
- disabled
1567
- };
1568
- },
1569
- buildNext: opts => {
1570
- const next = buildLocation(opts);
1571
- const matches = router.matchRoutes(next.pathname);
1572
- const __preSearchFilters = matches.map(match => match.options.preSearchFilters ?? []).flat().filter(Boolean);
1573
- const __postSearchFilters = matches.map(match => match.options.postSearchFilters ?? []).flat().filter(Boolean);
1574
- return buildLocation({
1575
- ...opts,
1576
- __preSearchFilters,
1577
- __postSearchFilters
1580
+ this.routesById[route.id] = route;
1581
+ const children = routeConfig.children;
1582
+ route.childRoutes = children.length ? recurseRoutes(children, route) : undefined;
1583
+ return route;
1584
+ });
1585
+ };
1586
+ const routes = recurseRoutes([rootRouteConfig]);
1587
+ return routes[0];
1588
+ };
1589
+ #parseLocation = previousLocation => {
1590
+ let {
1591
+ pathname,
1592
+ search,
1593
+ hash,
1594
+ state
1595
+ } = this.history.location;
1596
+ const parsedSearch = this.options.parseSearch(search);
1597
+ console.log({
1598
+ pathname: pathname,
1599
+ searchStr: search,
1600
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1601
+ hash: hash.split('#').reverse()[0] ?? '',
1602
+ href: `${pathname}${search}${hash}`,
1603
+ state: state,
1604
+ key: state?.key || '__init__'
1605
+ });
1606
+ return {
1607
+ pathname: pathname,
1608
+ searchStr: search,
1609
+ search: replaceEqualDeep(previousLocation?.search, parsedSearch),
1610
+ hash: hash.split('#').reverse()[0] ?? '',
1611
+ href: `${pathname}${search}${hash}`,
1612
+ state: state,
1613
+ key: state?.key || '__init__'
1614
+ };
1615
+ };
1616
+ #onFocus = () => {
1617
+ this.load();
1618
+ };
1619
+ #buildLocation = (dest = {}) => {
1620
+ const fromPathname = dest.fromCurrent ? this.store.state.latestLocation.pathname : dest.from ?? this.store.state.latestLocation.pathname;
1621
+ let pathname = resolvePath(this.basepath ?? '/', fromPathname, `${dest.to ?? '.'}`);
1622
+ const fromMatches = this.matchRoutes(this.store.state.latestLocation.pathname, {
1623
+ strictParseParams: true
1624
+ });
1625
+ const toMatches = this.matchRoutes(pathname);
1626
+ const prevParams = {
1627
+ ...last(fromMatches)?.params
1628
+ };
1629
+ let nextParams = (dest.params ?? true) === true ? prevParams : functionalUpdate(dest.params, prevParams);
1630
+ if (nextParams) {
1631
+ toMatches.map(d => d.route.options.stringifyParams).filter(Boolean).forEach(fn => {
1632
+ Object.assign({}, nextParams, fn(nextParams));
1578
1633
  });
1579
1634
  }
1635
+ pathname = interpolatePath(pathname, nextParams ?? {});
1636
+
1637
+ // Pre filters first
1638
+ const preFilteredSearch = dest.__preSearchFilters?.length ? dest.__preSearchFilters?.reduce((prev, next) => next(prev), this.store.state.latestLocation.search) : this.store.state.latestLocation.search;
1639
+
1640
+ // Then the link/navigate function
1641
+ const destSearch = dest.search === true ? preFilteredSearch // Preserve resolvedFrom true
1642
+ : dest.search ? functionalUpdate(dest.search, preFilteredSearch) ?? {} // Updater
1643
+ : dest.__preSearchFilters?.length ? preFilteredSearch // Preserve resolvedFrom filters
1644
+ : {};
1645
+
1646
+ // Then post filters
1647
+ const postFilteredSearch = dest.__postSearchFilters?.length ? dest.__postSearchFilters.reduce((prev, next) => next(prev), destSearch) : destSearch;
1648
+ const search = replaceEqualDeep(this.store.state.latestLocation.search, postFilteredSearch);
1649
+ const searchStr = this.options.stringifySearch(search);
1650
+ let hash = dest.hash === true ? this.store.state.latestLocation.hash : functionalUpdate(dest.hash, this.store.state.latestLocation.hash);
1651
+ hash = hash ? `#${hash}` : '';
1652
+ return {
1653
+ pathname,
1654
+ search,
1655
+ searchStr,
1656
+ state: this.store.state.latestLocation.state,
1657
+ hash,
1658
+ href: `${pathname}${searchStr}${hash}`,
1659
+ key: dest.key
1660
+ };
1661
+ };
1662
+ #commitLocation = location => {
1663
+ const next = this.buildNext(location);
1664
+ const id = '' + Date.now() + Math.random();
1665
+ if (this.navigateTimeout) clearTimeout(this.navigateTimeout);
1666
+ let nextAction = 'replace';
1667
+ if (!location.replace) {
1668
+ nextAction = 'push';
1669
+ }
1670
+ const isSameUrl = this.store.state.latestLocation.href === next.href;
1671
+ if (isSameUrl && !next.key) {
1672
+ nextAction = 'replace';
1673
+ }
1674
+ const href = `${next.pathname}${next.searchStr}${next.hash ? `#${next.hash}` : ''}`;
1675
+ this.history[nextAction === 'push' ? 'push' : 'replace'](href, {
1676
+ id,
1677
+ ...next.state
1678
+ });
1679
+ this.load(this.#parseLocation(this.store.state.latestLocation));
1680
+ return this.navigationPromise = new Promise(resolve => {
1681
+ const previousNavigationResolve = this.resolveNavigation;
1682
+ this.resolveNavigation = () => {
1683
+ previousNavigationResolve();
1684
+ resolve();
1685
+ };
1686
+ });
1580
1687
  };
1581
- router.update(userOptions);
1688
+ }
1582
1689
 
1583
- // Allow frameworks to hook into the router creation
1584
- router.options.createRouter == null ? void 0 : router.options.createRouter(router);
1585
- return router;
1690
+ // Detect if we're in the DOM
1691
+ const isServer = typeof window === 'undefined' || !window.document.createElement;
1692
+ function getInitialRouterState() {
1693
+ return {
1694
+ status: 'idle',
1695
+ latestLocation: null,
1696
+ currentLocation: null,
1697
+ currentMatches: [],
1698
+ loaders: {},
1699
+ lastUpdated: Date.now(),
1700
+ matchCache: {},
1701
+ get isFetching() {
1702
+ return this.status === 'loading' || this.currentMatches.some(d => d.store.state.isFetching);
1703
+ },
1704
+ get isPreloading() {
1705
+ return Object.values(this.matchCache).some(d => d.match.store.state.isFetching && !this.currentMatches.find(dd => dd.id === d.match.id));
1706
+ }
1707
+ };
1586
1708
  }
1587
1709
  function isCtrlEvent(e) {
1588
1710
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey);
@@ -1591,12 +1713,81 @@ function linkMatches(matches) {
1591
1713
  matches.forEach((match, index) => {
1592
1714
  const parent = matches[index - 1];
1593
1715
  if (parent) {
1594
- match.__.setParentMatch(parent);
1595
- } else {
1596
- match.__.setParentMatch(undefined);
1716
+ match.__setParentMatch(parent);
1597
1717
  }
1598
1718
  });
1599
1719
  }
1600
1720
 
1601
- export { cleanPath, createRoute, createRouteConfig, createRouteMatch, createRouter, decode, defaultParseSearch, defaultStringifySearch, encode, functionalUpdate, interpolatePath, joinPaths, last, matchByPath, matchPathname, parsePathname, parseSearchWith, pick, resolvePath, rootRouteId, sharedClone, stringifySearchWith, trimPath, trimPathLeft, trimPathRight, warning };
1721
+ // createRouterAction is a constrained identify function that takes options: key, action, onSuccess, onError, onSettled, etc
1722
+ function createAction(options) {
1723
+ const store = createStore({
1724
+ submissions: []
1725
+ }, options.debug);
1726
+ return {
1727
+ options,
1728
+ store,
1729
+ reset: () => {
1730
+ store.setState(s => {
1731
+ s.submissions = [];
1732
+ });
1733
+ },
1734
+ submit: async payload => {
1735
+ const submission = {
1736
+ submittedAt: Date.now(),
1737
+ status: 'pending',
1738
+ payload: payload,
1739
+ invalidate: () => {
1740
+ setSubmission(s => {
1741
+ s.isInvalid = true;
1742
+ });
1743
+ },
1744
+ getIsLatest: () => store.state.submissions[store.state.submissions.length - 1]?.submittedAt === submission.submittedAt
1745
+ };
1746
+ const setSubmission = updater => {
1747
+ store.setState(s => {
1748
+ const a = s.submissions.find(d => d.submittedAt === submission.submittedAt);
1749
+ invariant(a, 'Could not find submission in store');
1750
+ updater(a);
1751
+ });
1752
+ };
1753
+ store.setState(s => {
1754
+ s.submissions.push(submission);
1755
+ s.submissions.reverse();
1756
+ s.submissions = s.submissions.slice(0, options.maxSubmissions ?? 10);
1757
+ s.submissions.reverse();
1758
+ });
1759
+ const after = async () => {
1760
+ options.onEachSettled?.(submission);
1761
+ if (submission.getIsLatest()) await options.onLatestSettled?.(submission);
1762
+ };
1763
+ try {
1764
+ const res = await options.action?.(submission.payload);
1765
+ setSubmission(s => {
1766
+ s.response = res;
1767
+ });
1768
+ await options.onEachSuccess?.(submission);
1769
+ if (submission.getIsLatest()) await options.onLatestSuccess?.(submission);
1770
+ await after();
1771
+ setSubmission(s => {
1772
+ s.status = 'success';
1773
+ });
1774
+ return res;
1775
+ } catch (err) {
1776
+ console.error(err);
1777
+ setSubmission(s => {
1778
+ s.error = err;
1779
+ });
1780
+ await options.onEachError?.(submission);
1781
+ if (submission.getIsLatest()) await options.onLatestError?.(submission);
1782
+ await after();
1783
+ setSubmission(s => {
1784
+ s.status = 'error';
1785
+ });
1786
+ throw err;
1787
+ }
1788
+ }
1789
+ };
1790
+ }
1791
+
1792
+ export { Route, RouteMatch, Router, batch, cleanPath, createAction, createBrowserHistory, createHashHistory, createMemoryHistory, createRouteConfig, createStore, decode, defaultFetchServerDataFn, defaultParseSearch, defaultStringifySearch, encode, functionalUpdate, interpolatePath, joinPaths, last, matchByPath, matchPathname, parsePathname, parseSearchWith, pick, replaceEqualDeep, resolvePath, rootRouteId, stringifySearchWith, trackDeep, trimPath, trimPathLeft, trimPathRight, warning };
1602
1793
  //# sourceMappingURL=index.js.map