@solidjs/router 0.10.0-beta.4 → 0.10.0-beta.6

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/dist/index.js CHANGED
@@ -1,174 +1,7 @@
1
- import { isServer, delegateEvents, getRequestEvent, createComponent as createComponent$1, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
2
- import { createSignal, onCleanup, getOwner, runWithOwner, createMemo, createContext, useContext, untrack, createRenderEffect, on, startTransition, resetErrorBoundaries, createComponent, children, createRoot, Show, mergeProps, splitProps, createResource, sharedConfig, $TRACK } from 'solid-js';
1
+ import { isServer, getRequestEvent, createComponent as createComponent$1, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
2
+ import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, createComponent, children, mergeProps, createRoot, Show, sharedConfig, $TRACK, splitProps, createResource } from 'solid-js';
3
3
  import { createStore, reconcile } from 'solid-js/store';
4
4
 
5
- function bindEvent(target, type, handler) {
6
- target.addEventListener(type, handler);
7
- return () => target.removeEventListener(type, handler);
8
- }
9
- function intercept([value, setValue], get, set) {
10
- return [get ? () => get(value()) : value, set ? v => setValue(set(v)) : setValue];
11
- }
12
- function querySelector(selector) {
13
- if (selector === "#") {
14
- return null;
15
- }
16
- // Guard against selector being an invalid CSS selector
17
- try {
18
- return document.querySelector(selector);
19
- } catch (e) {
20
- return null;
21
- }
22
- }
23
- function scrollToHash(hash, fallbackTop) {
24
- const el = querySelector(`#${hash}`);
25
- if (el) {
26
- el.scrollIntoView();
27
- } else if (fallbackTop) {
28
- window.scrollTo(0, 0);
29
- }
30
- }
31
- function createMemoryHistory() {
32
- const entries = ["/"];
33
- let index = 0;
34
- const listeners = [];
35
- const go = n => {
36
- // https://github.com/remix-run/react-router/blob/682810ca929d0e3c64a76f8d6e465196b7a2ac58/packages/router/history.ts#L245
37
- index = Math.max(0, Math.min(index + n, entries.length - 1));
38
- const value = entries[index];
39
- listeners.forEach(listener => listener(value));
40
- };
41
- return {
42
- get: () => entries[index],
43
- set: ({
44
- value,
45
- scroll,
46
- replace
47
- }) => {
48
- if (replace) {
49
- entries[index] = value;
50
- } else {
51
- entries.splice(index + 1, entries.length - index, value);
52
- index++;
53
- }
54
- if (scroll) {
55
- scrollToHash(value.split("#")[1] || "", true);
56
- }
57
- },
58
- back: () => {
59
- go(-1);
60
- },
61
- forward: () => {
62
- go(1);
63
- },
64
- go,
65
- listen: listener => {
66
- listeners.push(listener);
67
- return () => {
68
- const index = listeners.indexOf(listener);
69
- listeners.splice(index, 1);
70
- };
71
- }
72
- };
73
- }
74
- function createIntegration(get, set, init, utils) {
75
- let ignore = false;
76
- const wrap = value => typeof value === "string" ? {
77
- value
78
- } : value;
79
- const signal = intercept(createSignal(wrap(get()), {
80
- equals: (a, b) => a.value === b.value
81
- }), undefined, next => {
82
- !ignore && set(next);
83
- return next;
84
- });
85
- init && onCleanup(init((value = get()) => {
86
- ignore = true;
87
- signal[1](wrap(value));
88
- ignore = false;
89
- }));
90
- return {
91
- signal,
92
- utils
93
- };
94
- }
95
- function normalizeIntegration(integration) {
96
- if (!integration) {
97
- return {
98
- signal: createSignal({
99
- value: ""
100
- })
101
- };
102
- } else if (Array.isArray(integration)) {
103
- return {
104
- signal: integration
105
- };
106
- }
107
- return integration;
108
- }
109
- function staticIntegration(obj) {
110
- return {
111
- signal: [() => obj, next => Object.assign(obj, next)]
112
- };
113
- }
114
- function pathIntegration() {
115
- return createIntegration(() => ({
116
- value: window.location.pathname + window.location.search + window.location.hash,
117
- state: history.state
118
- }), ({
119
- value,
120
- replace,
121
- scroll,
122
- state
123
- }) => {
124
- if (replace) {
125
- window.history.replaceState(state, "", value);
126
- } else {
127
- window.history.pushState(state, "", value);
128
- }
129
- scrollToHash(window.location.hash.slice(1), scroll);
130
- }, notify => bindEvent(window, "popstate", () => notify()), {
131
- go: delta => window.history.go(delta)
132
- });
133
- }
134
- function hashIntegration() {
135
- return createIntegration(() => window.location.hash.slice(1), ({
136
- value,
137
- replace,
138
- scroll,
139
- state
140
- }) => {
141
- if (replace) {
142
- window.history.replaceState(state, "", "#" + value);
143
- } else {
144
- window.location.hash = value;
145
- }
146
- const hashIndex = value.indexOf("#");
147
- const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
148
- scrollToHash(hash, scroll);
149
- }, notify => bindEvent(window, "hashchange", () => notify()), {
150
- go: delta => window.history.go(delta),
151
- renderPath: path => `#${path}`,
152
- parsePath: str => {
153
- const to = str.replace(/^.*?#/, "");
154
- // Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
155
- // `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash
156
- // to the current path so we can handle these in-page anchors correctly.
157
- if (!to.startsWith("/")) {
158
- const [, path = "/"] = window.location.hash.split("#", 2);
159
- return `${path}#${to}`;
160
- }
161
- return to;
162
- }
163
- });
164
- }
165
- function memoryIntegration() {
166
- const memoryHistory = createMemoryHistory();
167
- return createIntegration(memoryHistory.get, memoryHistory.set, memoryHistory.listen, {
168
- go: memoryHistory.go
169
- });
170
- }
171
-
172
5
  function createBeforeLeave() {
173
6
  let listeners = new Set();
174
7
  function subscribe(listener) {
@@ -521,10 +354,6 @@ function createLocation(path, state) {
521
354
  query: createMemoObject(on(search, () => extractSearchParams(url())))
522
355
  };
523
356
  }
524
- const actions = new Map();
525
- function registerAction(url, fn) {
526
- actions.set(url, fn);
527
- }
528
357
  let intent;
529
358
  function getIntent() {
530
359
  return intent;
@@ -533,12 +362,11 @@ function createRouterContext(integration, getBranches, options = {}) {
533
362
  const {
534
363
  signal: [source, setSource],
535
364
  utils = {}
536
- } = normalizeIntegration(integration);
365
+ } = integration;
537
366
  const parsePath = utils.parsePath || (p => p);
538
367
  const renderPath = utils.renderPath || (p => p);
539
368
  const beforeLeave = utils.beforeLeave || createBeforeLeave();
540
369
  const basePath = resolvePath("", options.base || "");
541
- const actionBase = options.actionBase || "/_server";
542
370
  if (basePath === undefined) {
543
371
  throw new Error(`${basePath} is not a valid base path`);
544
372
  } else if (basePath && !source().value) {
@@ -561,6 +389,7 @@ function createRouterContext(integration, getBranches, options = {}) {
561
389
  const [state, setState] = createSignal(source().state);
562
390
  const location = createLocation(reference, state);
563
391
  const referrers = [];
392
+ const submissions = createSignal(initFromFlash(location.query));
564
393
  const baseRoute = {
565
394
  pattern: basePath,
566
395
  params: {},
@@ -570,15 +399,37 @@ function createRouterContext(integration, getBranches, options = {}) {
570
399
  return resolvePath(basePath, to);
571
400
  }
572
401
  };
573
- const router = {
402
+ createRenderEffect(() => {
403
+ const {
404
+ value,
405
+ state
406
+ } = source();
407
+ // Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
408
+ untrack(() => {
409
+ if (value !== reference()) {
410
+ start(() => {
411
+ intent = "native";
412
+ setReference(value);
413
+ setState(state);
414
+ resetErrorBoundaries();
415
+ submissions[1]([]);
416
+ }).then(() => {
417
+ intent = undefined;
418
+ });
419
+ }
420
+ });
421
+ });
422
+ return {
574
423
  base: baseRoute,
424
+ actionBase: options.actionBase || "/_server",
575
425
  location,
576
426
  isRouting,
577
427
  renderPath,
578
428
  parsePath,
579
429
  navigatorFactory,
580
430
  beforeLeave,
581
- submissions: createSignal(initFromFlash(location.query))
431
+ preloadRoute,
432
+ submissions
582
433
  };
583
434
  function navigateFromRoute(route, to, options) {
584
435
  // Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
@@ -636,6 +487,7 @@ function createRouterContext(integration, getBranches, options = {}) {
636
487
  setReference(resolvedTo);
637
488
  setState(nextState);
638
489
  resetErrorBoundaries();
490
+ submissions[1]([]);
639
491
  }).then(() => {
640
492
  if (referrers.length === len) {
641
493
  intent = undefined;
@@ -667,152 +519,41 @@ function createRouterContext(integration, getBranches, options = {}) {
667
519
  referrers.length = 0;
668
520
  }
669
521
  }
522
+ function preloadRoute(url, preloadData) {
523
+ const matches = getRouteMatches(getBranches(), url.pathname);
524
+ const prevIntent = intent;
525
+ intent = "preload";
526
+ for (let match in matches) {
527
+ const {
528
+ route,
529
+ params
530
+ } = matches[match];
531
+ route.component && route.component.preload && route.component.preload();
532
+ preloadData && route.load && route.load({
533
+ params,
534
+ location: {
535
+ pathname: url.pathname,
536
+ search: url.search,
537
+ hash: url.hash,
538
+ query: extractSearchParams(url),
539
+ state: null,
540
+ key: ""
541
+ },
542
+ intent: "preload"
543
+ });
544
+ }
545
+ intent = prevIntent;
546
+ }
670
547
  function initFromFlash(params) {
671
548
  let param = params.form ? JSON.parse(params.form) : null;
672
549
  if (!param || !param.result) return [];
673
550
  const input = new Map(param.entries);
674
551
  return [{
675
552
  url: param.url,
676
- result: param.error ? new Error(param.result.message) : param.result,
553
+ result: param.error ? new Error(param.result) : param.result,
677
554
  input: input
678
555
  }];
679
556
  }
680
- createRenderEffect(() => {
681
- const {
682
- value,
683
- state
684
- } = source();
685
- // Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
686
- untrack(() => {
687
- if (value !== reference()) {
688
- start(() => {
689
- intent = "native";
690
- setReference(value);
691
- setState(state);
692
- }).then(() => {
693
- intent = undefined;
694
- });
695
- }
696
- });
697
- });
698
- if (!isServer) {
699
- let preloadTimeout = {};
700
- function isSvg(el) {
701
- return el.namespaceURI === "http://www.w3.org/2000/svg";
702
- }
703
- function handleAnchor(evt) {
704
- if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
705
- const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
706
- if (!a) return;
707
- const svg = isSvg(a);
708
- const href = svg ? a.href.baseVal : a.href;
709
- const target = svg ? a.target.baseVal : a.target;
710
- if (target || !href && !a.hasAttribute("state")) return;
711
- const rel = (a.getAttribute("rel") || "").split(/\s+/);
712
- if (a.hasAttribute("download") || rel && rel.includes("external")) return;
713
- const url = svg ? new URL(href, document.baseURI) : new URL(href);
714
- if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
715
- return [a, url];
716
- }
717
- function handleAnchorClick(evt) {
718
- const res = handleAnchor(evt);
719
- if (!res) return;
720
- const [a, url] = res;
721
- const to = parsePath(url.pathname + url.search + url.hash);
722
- const state = a.getAttribute("state");
723
- evt.preventDefault();
724
- navigateFromRoute(baseRoute, to, {
725
- resolve: false,
726
- replace: a.hasAttribute("replace"),
727
- scroll: !a.hasAttribute("noscroll"),
728
- state: state && JSON.parse(state)
729
- });
730
- }
731
- function doPreload(a, url) {
732
- const preload = a.getAttribute("preload") !== "false";
733
- const matches = getRouteMatches(getBranches(), url.pathname);
734
- const prevIntent = intent;
735
- intent = "preload";
736
- for (let match in matches) {
737
- const {
738
- route,
739
- params
740
- } = matches[match];
741
- route.component && route.component.preload && route.component.preload();
742
- preload && route.load && route.load({
743
- params,
744
- location: {
745
- pathname: url.pathname,
746
- search: url.search,
747
- hash: url.hash,
748
- query: extractSearchParams(url),
749
- state: null,
750
- key: ""
751
- },
752
- intent
753
- });
754
- }
755
- intent = prevIntent;
756
- }
757
- function handleAnchorPreload(evt) {
758
- const res = handleAnchor(evt);
759
- if (!res) return;
760
- const [a, url] = res;
761
- if (!preloadTimeout[url.pathname]) doPreload(a, url);
762
- }
763
- function handleAnchorIn(evt) {
764
- const res = handleAnchor(evt);
765
- if (!res) return;
766
- const [a, url] = res;
767
- if (preloadTimeout[url.pathname]) return;
768
- preloadTimeout[url.pathname] = setTimeout(() => {
769
- doPreload(a, url);
770
- delete preloadTimeout[url.pathname];
771
- }, 200);
772
- }
773
- function handleAnchorOut(evt) {
774
- const res = handleAnchor(evt);
775
- if (!res) return;
776
- const [, url] = res;
777
- if (preloadTimeout[url.pathname]) {
778
- clearTimeout(preloadTimeout[url.pathname]);
779
- delete preloadTimeout[url.pathname];
780
- }
781
- }
782
- function handleFormSubmit(evt) {
783
- let actionRef = evt.submitter && evt.submitter.getAttribute("formaction") || evt.target.action;
784
- if (!actionRef) return;
785
- if (!actionRef.startsWith("action:")) {
786
- const url = new URL(actionRef);
787
- actionRef = parsePath(url.pathname + url.search);
788
- if (!actionRef.startsWith(actionBase)) return;
789
- }
790
- const handler = actions.get(actionRef);
791
- if (handler) {
792
- evt.preventDefault();
793
- const data = new FormData(evt.target);
794
- handler.call(router, data);
795
- }
796
- }
797
-
798
- // ensure delegated event run first
799
- delegateEvents(["click", "submit"]);
800
- document.addEventListener("click", handleAnchorClick);
801
- document.addEventListener("mouseover", handleAnchorIn);
802
- document.addEventListener("mouseout", handleAnchorOut);
803
- document.addEventListener("focusin", handleAnchorPreload);
804
- document.addEventListener("touchstart", handleAnchorPreload);
805
- document.addEventListener("submit", handleFormSubmit);
806
- onCleanup(() => {
807
- document.removeEventListener("click", handleAnchorClick);
808
- document.removeEventListener("mouseover", handleAnchorIn);
809
- document.removeEventListener("mouseout", handleAnchorOut);
810
- document.removeEventListener("focusin", handleAnchorPreload);
811
- document.removeEventListener("touchstart", handleAnchorPreload);
812
- document.removeEventListener("submit", handleFormSubmit);
813
- });
814
- }
815
- return router;
816
557
  }
817
558
  function createRouteContext(router, parent, outlet, match, params) {
818
559
  const {
@@ -845,32 +586,26 @@ function createRouteContext(router, parent, outlet, match, params) {
845
586
  load && load({
846
587
  params,
847
588
  location,
848
- intent: intent || "navigate"
589
+ intent: intent || "initial"
849
590
  });
850
591
  return route;
851
592
  }
852
593
 
853
- const _tmpl$ = /*#__PURE__*/template(`<a>`);
854
- const Router = props => {
855
- let e;
594
+ const createRouterComponent = router => props => {
856
595
  const {
857
- source,
858
- url,
859
596
  base,
860
597
  actionBase
861
598
  } = props;
862
- const integration = source || (isServer ? staticIntegration({
863
- value: url || (e = getRequestEvent()) && getPath(e.request.url) || ""
864
- }) : pathIntegration());
865
599
  const routeDefs = children(() => props.children);
866
600
  const branches = createMemo(() => createBranches(props.root ? {
867
601
  component: props.root,
868
602
  children: routeDefs()
869
603
  } : routeDefs(), props.base || ""));
870
- const routerState = createRouterContext(integration, branches, {
604
+ const routerState = createRouterContext(router, branches, {
871
605
  base,
872
606
  actionBase
873
607
  });
608
+ router.create && router.create(routerState);
874
609
  return createComponent$1(RouterContextObj.Provider, {
875
610
  value: routerState,
876
611
  get children() {
@@ -883,10 +618,6 @@ const Router = props => {
883
618
  }
884
619
  });
885
620
  };
886
- function getPath(url) {
887
- const u = new URL(url);
888
- return u.pathname + u.search;
889
- }
890
621
  function Routes(props) {
891
622
  const matches = createMemo(() => getRouteMatches(props.branches, props.routerState.location.pathname));
892
623
  const params = createMemoObject(() => {
@@ -960,150 +691,117 @@ const Route = props => {
960
691
  }
961
692
  });
962
693
  };
963
- function A(props) {
964
- props = mergeProps({
965
- inactiveClass: "inactive",
966
- activeClass: "active"
967
- }, props);
968
- const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
969
- const to = useResolvedPath(() => props.href);
970
- const href = useHref(to);
971
- const location = useLocation();
972
- const isActive = createMemo(() => {
973
- const to_ = to();
974
- if (to_ === undefined) return false;
975
- const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
976
- const loc = normalizePath(location.pathname).toLowerCase();
977
- return props.end ? path === loc : loc.startsWith(path);
978
- });
979
- return (() => {
980
- const _el$ = _tmpl$();
981
- spread(_el$, mergeProps$1(rest, {
982
- get href() {
983
- return href() || props.href;
984
- },
985
- get state() {
986
- return JSON.stringify(props.state);
987
- },
988
- get classList() {
989
- return {
990
- ...(props.class && {
991
- [props.class]: true
992
- }),
993
- [props.inactiveClass]: !isActive(),
994
- [props.activeClass]: isActive(),
995
- ...rest.classList
996
- };
997
- },
998
- get ["aria-current"]() {
999
- return isActive() ? "page" : undefined;
1000
- }
1001
- }), false, false);
1002
- return _el$;
1003
- })();
1004
- }
1005
- function Navigate(props) {
1006
- const navigate = useNavigate();
1007
- const location = useLocation();
1008
- const {
1009
- href,
1010
- state
1011
- } = props;
1012
- const path = typeof href === "function" ? href({
1013
- navigate,
1014
- location
1015
- }) : href;
1016
- navigate(path, {
1017
- replace: true,
1018
- state
1019
- });
1020
- return null;
1021
- }
1022
694
 
1023
- /**
1024
- * This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
1025
- */
1026
- function createAsync(fn, options) {
1027
- const [resource] = createResource(() => subFetch(fn), v => v, options);
1028
- return () => resource();
695
+ function intercept([value, setValue], get, set) {
696
+ return [get ? () => get(value()) : value, set ? v => setValue(set(v)) : setValue];
1029
697
  }
1030
-
1031
- // mock promise while hydrating to prevent fetching
1032
- class MockPromise {
1033
- static all() {
1034
- return new MockPromise();
698
+ function querySelector(selector) {
699
+ if (selector === "#") {
700
+ return null;
1035
701
  }
1036
- static allSettled() {
1037
- return new MockPromise();
1038
- }
1039
- static any() {
1040
- return new MockPromise();
1041
- }
1042
- static race() {
1043
- return new MockPromise();
1044
- }
1045
- static reject() {
1046
- return new MockPromise();
1047
- }
1048
- static resolve() {
1049
- return new MockPromise();
1050
- }
1051
- catch() {
1052
- return new MockPromise();
1053
- }
1054
- then() {
1055
- return new MockPromise();
1056
- }
1057
- finally() {
1058
- return new MockPromise();
702
+ // Guard against selector being an invalid CSS selector
703
+ try {
704
+ return document.querySelector(selector);
705
+ } catch (e) {
706
+ return null;
1059
707
  }
1060
708
  }
1061
- function subFetch(fn) {
1062
- if (isServer || !sharedConfig.context) return fn();
1063
- const ogFetch = fetch;
1064
- const ogPromise = Promise;
1065
- try {
1066
- window.fetch = () => new MockPromise();
1067
- Promise = MockPromise;
1068
- return fn();
1069
- } finally {
1070
- window.fetch = ogFetch;
1071
- Promise = ogPromise;
709
+ function createRouter(config) {
710
+ let ignore = false;
711
+ const wrap = value => typeof value === "string" ? {
712
+ value
713
+ } : value;
714
+ const signal = intercept(createSignal(wrap(config.get()), {
715
+ equals: (a, b) => a.value === b.value
716
+ }), undefined, next => {
717
+ !ignore && config.set(next);
718
+ return next;
719
+ });
720
+ config.init && onCleanup(config.init((value = config.get()) => {
721
+ ignore = true;
722
+ signal[1](wrap(value));
723
+ ignore = false;
724
+ }));
725
+ return createRouterComponent({
726
+ signal,
727
+ create: config.create,
728
+ utils: config.utils
729
+ });
730
+ }
731
+ function bindEvent(target, type, handler) {
732
+ target.addEventListener(type, handler);
733
+ return () => target.removeEventListener(type, handler);
734
+ }
735
+ function scrollToHash(hash, fallbackTop) {
736
+ const el = querySelector(`#${hash}`);
737
+ if (el) {
738
+ el.scrollIntoView();
739
+ } else if (fallbackTop) {
740
+ window.scrollTo(0, 0);
1072
741
  }
1073
742
  }
1074
743
 
744
+ function getPath(url) {
745
+ const u = new URL(url);
746
+ return u.pathname + u.search;
747
+ }
748
+ function StaticRouter(props) {
749
+ let e;
750
+ const obj = {
751
+ value: props.url || (e = getRequestEvent()) && getPath(e.request.url) || ""
752
+ };
753
+ return createRouterComponent({
754
+ signal: [() => obj, next => Object.assign(obj, next)]
755
+ })(props);
756
+ }
757
+
1075
758
  const LocationHeader = "Location";
1076
759
  const PRELOAD_TIMEOUT = 5000;
760
+ const CACHE_TIMEOUT = 180000;
1077
761
  let cacheMap = new Map();
762
+
763
+ // cleanup forward/back cache
764
+ if (!isServer) {
765
+ setInterval(() => {
766
+ const now = Date.now();
767
+ for (let [k, v] of cacheMap.entries()) {
768
+ if (!v[3].size && now - v[0] > CACHE_TIMEOUT) {
769
+ cacheMap.delete(k);
770
+ }
771
+ }
772
+ }, 300000);
773
+ }
1078
774
  function getCache() {
1079
775
  if (!isServer) return cacheMap;
1080
776
  const req = getRequestEvent() || sharedConfig.context;
1081
777
  return req.routerCache || (req.routerCache = new Map());
1082
778
  }
1083
779
  function revalidate(key) {
780
+ key && !Array.isArray(key) && (key = [key]);
1084
781
  return startTransition(() => {
1085
782
  const now = Date.now();
1086
783
  for (let k of cacheMap.keys()) {
1087
- if (key === undefined || k === key) {
1088
- const set = cacheMap.get(k)[3];
1089
- revalidateSignals(set, now);
1090
- cacheMap.delete(k);
784
+ if (key === undefined || matchKey(k, key)) {
785
+ const entry = cacheMap.get(k);
786
+ entry[0] = 0; //force cache miss
787
+ revalidateSignals(entry[3], now); // retrigger live signals
1091
788
  }
1092
789
  }
1093
790
  });
1094
791
  }
792
+
1095
793
  function revalidateSignals(set, time) {
1096
794
  for (let s of set) s[1](time);
1097
795
  }
1098
796
  function cache(fn, name, options) {
1099
797
  const [store, setStore] = createStore({});
1100
- return (...args) => {
798
+ const cachedFn = (...args) => {
1101
799
  const cache = getCache();
1102
800
  const intent = getIntent();
1103
801
  const owner = getOwner();
1104
802
  const navigate = owner ? useNavigate() : undefined;
1105
803
  const now = Date.now();
1106
- const key = name + (args.length ? ":" + args.join(":") : "");
804
+ const key = name + hashKey(args);
1107
805
  let cached = cache.get(key);
1108
806
  let version;
1109
807
  if (owner) {
@@ -1172,8 +870,31 @@ function cache(fn, name, options) {
1172
870
  };
1173
871
  }
1174
872
  };
873
+ cachedFn.keyFor = (...args) => name + hashKey(args);
874
+ cachedFn.key = name;
875
+ return cachedFn;
876
+ }
877
+ function matchKey(key, keys) {
878
+ for (let k of keys) {
879
+ if (key.startsWith(k)) return true;
880
+ }
881
+ return false;
882
+ }
883
+
884
+ // Modified from the amazing Tanstack Query library (MIT)
885
+ // https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts#L168
886
+ function hashKey(args) {
887
+ return JSON.stringify(args, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => {
888
+ result[key] = val[key];
889
+ return result;
890
+ }, {}) : val);
891
+ }
892
+ function isPlainObject(obj) {
893
+ let proto;
894
+ return obj != null && typeof obj === "object" && (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype);
1175
895
  }
1176
896
 
897
+ const actions = /* #__PURE__ */new Map();
1177
898
  function useSubmissions(fn, filter) {
1178
899
  const router = useRouter();
1179
900
  const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.toString() && (!filter || filter(s.input))));
@@ -1187,26 +908,11 @@ function useSubmissions(fn, filter) {
1187
908
  }
1188
909
  function useSubmission(fn, filter) {
1189
910
  const submissions = useSubmissions(fn, filter);
1190
- return {
1191
- get clear() {
1192
- return submissions[submissions.length - 1]?.clear;
1193
- },
1194
- get retry() {
1195
- return submissions[submissions.length - 1]?.retry;
1196
- },
1197
- get url() {
1198
- return submissions[submissions.length - 1]?.url;
1199
- },
1200
- get input() {
1201
- return submissions[submissions.length - 1]?.input;
1202
- },
1203
- get result() {
1204
- return submissions[submissions.length - 1]?.result;
1205
- },
1206
- get pending() {
1207
- return submissions[submissions.length - 1]?.pending;
911
+ return new Proxy({}, {
912
+ get(_, property) {
913
+ return submissions[submissions.length - 1]?.[property];
1208
914
  }
1209
- };
915
+ });
1210
916
  }
1211
917
  function useAction(action) {
1212
918
  const router = useRouter();
@@ -1252,35 +958,378 @@ function action(fn, name) {
1252
958
  if (!url) throw new Error("Client Actions need explicit names if server rendered");
1253
959
  return url;
1254
960
  };
1255
- if (!isServer) registerAction(url, mutate);
961
+ if (!isServer) actions.set(url, mutate);
1256
962
  return mutate;
1257
963
  }
1258
964
  async function handleResponse(response, navigate) {
1259
965
  let data;
1260
- if (response instanceof Response && redirectStatusCodes.has(response.status)) {
1261
- const locationUrl = response.headers.get("Location") || "/";
1262
- if (locationUrl.startsWith("http")) {
1263
- window.location.href = locationUrl;
1264
- } else {
1265
- navigate(locationUrl);
966
+ let keys;
967
+ if (response instanceof Response) {
968
+ if (response.headers.has("X-Revalidate")) {
969
+ keys = response.headers.get("X-Revalidate").split(",");
970
+ }
971
+ if (response.customBody) data = await response.customBody();
972
+ if (redirectStatusCodes.has(response.status)) {
973
+ const locationUrl = response.headers.get("Location") || "/";
974
+ if (locationUrl.startsWith("http")) {
975
+ window.location.href = locationUrl;
976
+ } else {
977
+ navigate(locationUrl);
978
+ }
1266
979
  }
1267
980
  } else data = response;
1268
- // TODO: handle keys
1269
- await revalidate();
981
+ await revalidate(keys);
1270
982
  return data;
1271
983
  }
1272
984
 
985
+ function setupNativeEvents(router) {
986
+ const basePath = router.base.path();
987
+ const navigateFromRoute = router.navigatorFactory(router.base);
988
+ let preloadTimeout = {};
989
+ function isSvg(el) {
990
+ return el.namespaceURI === "http://www.w3.org/2000/svg";
991
+ }
992
+ function handleAnchor(evt) {
993
+ if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
994
+ const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
995
+ if (!a) return;
996
+ const svg = isSvg(a);
997
+ const href = svg ? a.href.baseVal : a.href;
998
+ const target = svg ? a.target.baseVal : a.target;
999
+ if (target || !href && !a.hasAttribute("state")) return;
1000
+ const rel = (a.getAttribute("rel") || "").split(/\s+/);
1001
+ if (a.hasAttribute("download") || rel && rel.includes("external")) return;
1002
+ const url = svg ? new URL(href, document.baseURI) : new URL(href);
1003
+ if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
1004
+ return [a, url];
1005
+ }
1006
+ function handleAnchorClick(evt) {
1007
+ const res = handleAnchor(evt);
1008
+ if (!res) return;
1009
+ const [a, url] = res;
1010
+ const to = router.parsePath(url.pathname + url.search + url.hash);
1011
+ const state = a.getAttribute("state");
1012
+ evt.preventDefault();
1013
+ navigateFromRoute(to, {
1014
+ resolve: false,
1015
+ replace: a.hasAttribute("replace"),
1016
+ scroll: !a.hasAttribute("noscroll"),
1017
+ state: state && JSON.parse(state)
1018
+ });
1019
+ }
1020
+ function handleAnchorPreload(evt) {
1021
+ const res = handleAnchor(evt);
1022
+ if (!res) return;
1023
+ const [a, url] = res;
1024
+ if (!preloadTimeout[url.pathname]) router.preloadRoute(url, a.getAttribute("preload") !== "false");
1025
+ }
1026
+ function handleAnchorIn(evt) {
1027
+ const res = handleAnchor(evt);
1028
+ if (!res) return;
1029
+ const [a, url] = res;
1030
+ if (preloadTimeout[url.pathname]) return;
1031
+ preloadTimeout[url.pathname] = setTimeout(() => {
1032
+ router.preloadRoute(url, a.getAttribute("preload") !== "false");
1033
+ delete preloadTimeout[url.pathname];
1034
+ }, 200);
1035
+ }
1036
+ function handleAnchorOut(evt) {
1037
+ const res = handleAnchor(evt);
1038
+ if (!res) return;
1039
+ const [, url] = res;
1040
+ if (preloadTimeout[url.pathname]) {
1041
+ clearTimeout(preloadTimeout[url.pathname]);
1042
+ delete preloadTimeout[url.pathname];
1043
+ }
1044
+ }
1045
+ function handleFormSubmit(evt) {
1046
+ let actionRef = evt.submitter && evt.submitter.getAttribute("formaction") || evt.target.action;
1047
+ if (!actionRef) return;
1048
+ if (!actionRef.startsWith("action:")) {
1049
+ const url = new URL(actionRef);
1050
+ actionRef = router.parsePath(url.pathname + url.search);
1051
+ if (!actionRef.startsWith(router.actionBase)) return;
1052
+ }
1053
+ const handler = actions.get(actionRef);
1054
+ if (handler) {
1055
+ evt.preventDefault();
1056
+ const data = new FormData(evt.target);
1057
+ handler.call(router, data);
1058
+ }
1059
+ }
1060
+
1061
+ // ensure delegated event run first
1062
+ delegateEvents(["click", "submit"]);
1063
+ document.addEventListener("click", handleAnchorClick);
1064
+ document.addEventListener("mouseover", handleAnchorIn);
1065
+ document.addEventListener("mouseout", handleAnchorOut);
1066
+ document.addEventListener("focusin", handleAnchorPreload);
1067
+ document.addEventListener("touchstart", handleAnchorPreload);
1068
+ document.addEventListener("submit", handleFormSubmit);
1069
+ onCleanup(() => {
1070
+ document.removeEventListener("click", handleAnchorClick);
1071
+ document.removeEventListener("mouseover", handleAnchorIn);
1072
+ document.removeEventListener("mouseout", handleAnchorOut);
1073
+ document.removeEventListener("focusin", handleAnchorPreload);
1074
+ document.removeEventListener("touchstart", handleAnchorPreload);
1075
+ document.removeEventListener("submit", handleFormSubmit);
1076
+ });
1077
+ }
1078
+
1079
+ function Router(props) {
1080
+ if (isServer) return StaticRouter(props);
1081
+ return createRouter({
1082
+ get: () => ({
1083
+ value: window.location.pathname + window.location.search + window.location.hash,
1084
+ state: history.state
1085
+ }),
1086
+ set({
1087
+ value,
1088
+ replace,
1089
+ scroll,
1090
+ state
1091
+ }) {
1092
+ if (replace) {
1093
+ window.history.replaceState(state, "", value);
1094
+ } else {
1095
+ window.history.pushState(state, "", value);
1096
+ }
1097
+ scrollToHash(window.location.hash.slice(1), scroll);
1098
+ },
1099
+ init: notify => bindEvent(window, "popstate", () => notify()),
1100
+ create: setupNativeEvents,
1101
+ utils: {
1102
+ go: delta => window.history.go(delta)
1103
+ }
1104
+ })(props);
1105
+ }
1106
+
1107
+ function hashParser(str) {
1108
+ const to = str.replace(/^.*?#/, "");
1109
+ // Hash-only hrefs like `#foo` from plain anchors will come in as `/#foo` whereas a link to
1110
+ // `/foo` will be `/#/foo`. Check if the to starts with a `/` and if not append it as a hash
1111
+ // to the current path so we can handle these in-page anchors correctly.
1112
+ if (!to.startsWith("/")) {
1113
+ const [, path = "/"] = window.location.hash.split("#", 2);
1114
+ return `${path}#${to}`;
1115
+ }
1116
+ return to;
1117
+ }
1118
+ function HashRouter(props) {
1119
+ return createRouter({
1120
+ get: () => window.location.hash.slice(1),
1121
+ set({
1122
+ value,
1123
+ replace,
1124
+ scroll,
1125
+ state
1126
+ }) {
1127
+ if (replace) {
1128
+ window.history.replaceState(state, "", "#" + value);
1129
+ } else {
1130
+ window.location.hash = value;
1131
+ }
1132
+ const hashIndex = value.indexOf("#");
1133
+ const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : "";
1134
+ scrollToHash(hash, scroll);
1135
+ },
1136
+ init: notify => bindEvent(window, "hashchange", () => notify()),
1137
+ create: setupNativeEvents,
1138
+ utils: {
1139
+ go: delta => window.history.go(delta),
1140
+ renderPath: path => `#${path}`,
1141
+ parsePath: hashParser
1142
+ }
1143
+ })(props);
1144
+ }
1145
+
1146
+ function createMemoryHistory() {
1147
+ const entries = ["/"];
1148
+ let index = 0;
1149
+ const listeners = [];
1150
+ const go = n => {
1151
+ // https://github.com/remix-run/react-router/blob/682810ca929d0e3c64a76f8d6e465196b7a2ac58/packages/router/history.ts#L245
1152
+ index = Math.max(0, Math.min(index + n, entries.length - 1));
1153
+ const value = entries[index];
1154
+ listeners.forEach(listener => listener(value));
1155
+ };
1156
+ return {
1157
+ get: () => entries[index],
1158
+ set: ({
1159
+ value,
1160
+ scroll,
1161
+ replace
1162
+ }) => {
1163
+ if (replace) {
1164
+ entries[index] = value;
1165
+ } else {
1166
+ entries.splice(index + 1, entries.length - index, value);
1167
+ index++;
1168
+ }
1169
+ if (scroll) {
1170
+ scrollToHash(value.split("#")[1] || "", true);
1171
+ }
1172
+ },
1173
+ back: () => {
1174
+ go(-1);
1175
+ },
1176
+ forward: () => {
1177
+ go(1);
1178
+ },
1179
+ go,
1180
+ listen: listener => {
1181
+ listeners.push(listener);
1182
+ return () => {
1183
+ const index = listeners.indexOf(listener);
1184
+ listeners.splice(index, 1);
1185
+ };
1186
+ }
1187
+ };
1188
+ }
1189
+ function MemoryRouter(props) {
1190
+ const memoryHistory = props.history || createMemoryHistory();
1191
+ return createRouter({
1192
+ get: memoryHistory.get,
1193
+ set: memoryHistory.set,
1194
+ init: memoryHistory.listen,
1195
+ utils: {
1196
+ go: memoryHistory.go
1197
+ }
1198
+ })(props);
1199
+ }
1200
+
1201
+ const _tmpl$ = /*#__PURE__*/template(`<a>`);
1202
+ function A(props) {
1203
+ props = mergeProps({
1204
+ inactiveClass: "inactive",
1205
+ activeClass: "active"
1206
+ }, props);
1207
+ const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
1208
+ const to = useResolvedPath(() => props.href);
1209
+ const href = useHref(to);
1210
+ const location = useLocation();
1211
+ const isActive = createMemo(() => {
1212
+ const to_ = to();
1213
+ if (to_ === undefined) return false;
1214
+ const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
1215
+ const loc = normalizePath(location.pathname).toLowerCase();
1216
+ return props.end ? path === loc : loc.startsWith(path);
1217
+ });
1218
+ return (() => {
1219
+ const _el$ = _tmpl$();
1220
+ spread(_el$, mergeProps$1(rest, {
1221
+ get href() {
1222
+ return href() || props.href;
1223
+ },
1224
+ get state() {
1225
+ return JSON.stringify(props.state);
1226
+ },
1227
+ get classList() {
1228
+ return {
1229
+ ...(props.class && {
1230
+ [props.class]: true
1231
+ }),
1232
+ [props.inactiveClass]: !isActive(),
1233
+ [props.activeClass]: isActive(),
1234
+ ...rest.classList
1235
+ };
1236
+ },
1237
+ get ["aria-current"]() {
1238
+ return isActive() ? "page" : undefined;
1239
+ }
1240
+ }), false, false);
1241
+ return _el$;
1242
+ })();
1243
+ }
1244
+ function Navigate(props) {
1245
+ const navigate = useNavigate();
1246
+ const location = useLocation();
1247
+ const {
1248
+ href,
1249
+ state
1250
+ } = props;
1251
+ const path = typeof href === "function" ? href({
1252
+ navigate,
1253
+ location
1254
+ }) : href;
1255
+ navigate(path, {
1256
+ replace: true,
1257
+ state
1258
+ });
1259
+ return null;
1260
+ }
1261
+
1262
+ /**
1263
+ * This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
1264
+ */
1265
+ function createAsync(fn, options) {
1266
+ const [resource] = createResource(() => subFetch(fn), v => v, options);
1267
+ return () => resource();
1268
+ }
1269
+
1270
+ // mock promise while hydrating to prevent fetching
1271
+ class MockPromise {
1272
+ static all() {
1273
+ return new MockPromise();
1274
+ }
1275
+ static allSettled() {
1276
+ return new MockPromise();
1277
+ }
1278
+ static any() {
1279
+ return new MockPromise();
1280
+ }
1281
+ static race() {
1282
+ return new MockPromise();
1283
+ }
1284
+ static reject() {
1285
+ return new MockPromise();
1286
+ }
1287
+ static resolve() {
1288
+ return new MockPromise();
1289
+ }
1290
+ catch() {
1291
+ return new MockPromise();
1292
+ }
1293
+ then() {
1294
+ return new MockPromise();
1295
+ }
1296
+ finally() {
1297
+ return new MockPromise();
1298
+ }
1299
+ }
1300
+ function subFetch(fn) {
1301
+ if (isServer || !sharedConfig.context) return fn();
1302
+ const ogFetch = fetch;
1303
+ const ogPromise = Promise;
1304
+ try {
1305
+ window.fetch = () => new MockPromise();
1306
+ Promise = MockPromise;
1307
+ return fn();
1308
+ } finally {
1309
+ window.fetch = ogFetch;
1310
+ Promise = ogPromise;
1311
+ }
1312
+ }
1313
+
1273
1314
  function redirect(url, init = 302) {
1274
- let responseInit = init;
1275
- if (typeof responseInit === "number") {
1315
+ let responseInit;
1316
+ let revalidate;
1317
+ if (typeof init === "number") {
1276
1318
  responseInit = {
1277
- status: responseInit
1319
+ status: init
1278
1320
  };
1279
- } else if (typeof responseInit.status === "undefined") {
1280
- responseInit.status = 302;
1321
+ } else {
1322
+ ({
1323
+ revalidate,
1324
+ ...responseInit
1325
+ } = init);
1326
+ if (typeof responseInit.status === "undefined") {
1327
+ responseInit.status = 302;
1328
+ }
1281
1329
  }
1282
1330
  const headers = new Headers(responseInit.headers);
1283
1331
  headers.set("Location", url);
1332
+ revalidate && headers.set("X-Revalidate", revalidate.toString());
1284
1333
  const response = new Response(null, {
1285
1334
  ...responseInit,
1286
1335
  headers: headers
@@ -1288,4 +1337,4 @@ function redirect(url, init = 302) {
1288
1337
  return response;
1289
1338
  }
1290
1339
 
1291
- export { A, A as Link, A as NavLink, Navigate, Route, Router, mergeSearchString as _mergeSearchString, action, cache, createAsync, createBeforeLeave, createIntegration, createMemoryHistory, hashIntegration, memoryIntegration, normalizeIntegration, pathIntegration, redirect, revalidate, staticIntegration, useAction, useBeforeLeave, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
1340
+ export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createBeforeLeave, createMemoryHistory, createRouter, redirect, revalidate, useAction, useBeforeLeave, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useSubmission, useSubmissions };