@solidjs/router 0.17.0-next.5 → 0.17.0-next.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,6 +1,6 @@
1
- import { isServer, getRequestEvent, createComponent as createComponent$1, memo, isResponseEnvelope, delegateEvents, spread, mergeProps, template } from '@solidjs/web';
2
- import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, flush, createComponent, children, merge, createRoot, sharedConfig, getObserver, $TRACK, action as action$1, omit } from 'solid-js';
3
- import { decodeResponse } from '@solidjs/web/server-functions';
1
+ import { isServer, getRequestEvent, isResponseEnvelope, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps, template } from '@solidjs/web';
2
+ import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, children, createSignal, flush, createComponent, getObserver, sharedConfig, $TRACK, action as action$1, merge, createRoot, omit } from 'solid-js';
3
+ import { isServerFunction, getServerFunctionMetadata, GET, decodeResponse, subscribeFlightData } from '@solidjs/web/server-functions';
4
4
 
5
5
  function createBeforeLeave() {
6
6
  let listeners = new Set();
@@ -258,6 +258,63 @@ function setFunctionName(obj, value) {
258
258
  return obj;
259
259
  }
260
260
 
261
+ // The no-JS form convention's cookie codec. When a form posts to a server
262
+ // function without the client runtime (no instance header), the server
263
+ // handler redirects back carrying the outcome in a one-shot "flash" cookie;
264
+ // the next SSR pass reads it here and seeds the router's submission state so
265
+ // useSubmission() renders the result exactly as a scripted submission would.
266
+ // Both halves live in this module so the write (src/server.ts, the handler's
267
+ // handleNoJS) and the read (src/routing.ts, SSR initialization) can never
268
+ // drift apart.
269
+
270
+ const FLASH_COOKIE = "flash";
271
+ function decodeInputValue(value) {
272
+ if (value && typeof value === "object") {
273
+ if (Array.isArray(value.$f)) {
274
+ const form = new FormData();
275
+ for (const [k, v] of value.$f) form.append(k, v);
276
+ return form;
277
+ }
278
+ if (Array.isArray(value.$u)) return new URLSearchParams(value.$u);
279
+ }
280
+ return value;
281
+ }
282
+
283
+ /** The Set-Cookie value clearing the flash cookie after it has been read. */
284
+ function clearFlashCookie() {
285
+ return `${FLASH_COOKIE}=; Max-Age=0; Path=/`;
286
+ }
287
+ const FLASH_MATCHER = new RegExp(`(?:^|;\\s*)${FLASH_COOKIE}=([^;]+)`);
288
+
289
+ /** Whether a Cookie header carries a flash cookie (readable or not). */
290
+ function hasFlashCookie(cookieHeader) {
291
+ return !!cookieHeader && FLASH_MATCHER.test(cookieHeader);
292
+ }
293
+
294
+ /**
295
+ * Decodes the flash cookie out of a request's Cookie header. Returns
296
+ * undefined when absent or unreadable (a malformed cookie must never take
297
+ * down SSR — it is cleared either way).
298
+ */
299
+ function decodeFlashCookie(cookieHeader) {
300
+ if (!cookieHeader) return;
301
+ const match = cookieHeader.match(FLASH_MATCHER);
302
+ if (!match) return;
303
+ try {
304
+ const payload = JSON.parse(decodeURIComponent(match[1]));
305
+ if (!payload || !payload.result) return;
306
+ const result = payload.error ? new Error(payload.result) : payload.result;
307
+ return {
308
+ input: Array.isArray(payload.input) ? payload.input.map(decodeInputValue) : [],
309
+ url: payload.url,
310
+ result: payload.thrown ? undefined : result,
311
+ error: payload.thrown ? result : undefined
312
+ };
313
+ } catch (error) {
314
+ console.error(error);
315
+ }
316
+ }
317
+
261
318
  const MAX_REDIRECTS = 100;
262
319
 
263
320
  /** Consider this API opaque and internal. It is likely to change in the future. */
@@ -548,6 +605,17 @@ function createBranch(routes, index = 0) {
548
605
  function asArray(value) {
549
606
  return Array.isArray(value) ? value : [value];
550
607
  }
608
+
609
+ /**
610
+ * Resolves the route tree an app hands the router — JSX `<Route>` children
611
+ * (`Route` is a data-holder component, evaluating to its merged props),
612
+ * config objects, arrays, or thunks producing either — into route
613
+ * definitions. Shared by the `<Router>` component (which keeps the
614
+ * resolution live) and the server integration's preload runner (one-shot).
615
+ */
616
+ function resolveRouteDefinitions(routes) {
617
+ return children(() => routes);
618
+ }
551
619
  function createBranches(routeDef, base = "", stack = [], branches = []) {
552
620
  const routeDefs = asArray(routeDef);
553
621
  for (let i = 0, len = routeDefs.length; i < len; i++) {
@@ -838,11 +906,26 @@ function createRouterContext(integration, branches, getContext, options = {}) {
838
906
  }
839
907
  intent = prevIntent;
840
908
  }
909
+
910
+ // Seeds the initial submission from a no-JS form post: the server
911
+ // function handler redirected back with the outcome in a one-shot flash
912
+ // cookie (see src/server.ts's handleNoJS), which is read here — and
913
+ // cleared — so the post-redirect SSR renders useSubmission() state
914
+ // exactly as a scripted submission would. An explicitly pre-seeded
915
+ // `event.router.submission` (framework integrations) takes precedence.
841
916
  function initFromFlash() {
842
917
  const e = getRequestEvent();
843
- if (!(e && e.router && e.router.submission)) return [];
918
+ if (!e) return [];
919
+ let submission = e.router && e.router.submission;
920
+ if (!submission) {
921
+ const cookieHeader = e.request.headers.get("cookie");
922
+ submission = decodeFlashCookie(cookieHeader);
923
+ // one-shot: clear it even when unreadable so it can't haunt later renders
924
+ if (hasFlashCookie(cookieHeader) && e.response && e.response.headers) e.response.headers.append("Set-Cookie", clearFlashCookie());
925
+ }
926
+ if (!submission) return [];
844
927
  return [{
845
- ...e.router.submission,
928
+ ...submission,
846
929
  clear() {},
847
930
  retry() {}
848
931
  }];
@@ -892,327 +975,99 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
892
975
  return route;
893
976
  }
894
977
 
895
- const createRouterComponent = router => function IntegratedRouter(props) {
896
- const {
897
- base,
898
- singleFlight,
899
- transformUrl,
900
- root,
901
- rootPreload,
902
- routeChildren
903
- } = untrack(() => ({
904
- base: props.base,
905
- singleFlight: props.singleFlight,
906
- transformUrl: props.transformUrl,
907
- root: props.root,
908
- rootPreload: props.rootPreload,
909
- routeChildren: props.children
910
- }));
911
- const routeDefs = children(() => routeChildren);
912
- const branches = createMemo(() => createBranches(routeDefs(), base || ""));
913
- let context;
914
- const routerState = createRouterContext(router, branches, () => context, {
915
- base,
916
- singleFlight,
917
- transformUrl
918
- });
919
- router.create && router.create(routerState);
920
- return createComponent$1(RouterContextObj, {
921
- value: routerState,
922
- get children() {
923
- return createComponent$1(Root, {
924
- routerState: routerState,
925
- root: root,
926
- preload: rootPreload,
927
- get children() {
928
- return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, {
929
- routerState: routerState,
930
- get branches() {
931
- return branches();
932
- }
933
- })];
934
- }
935
- });
978
+ const LocationHeader = "Location";
979
+ const PRELOAD_TIMEOUT = 5000;
980
+ const CACHE_TIMEOUT = 180000;
981
+ let cacheMap = new Map();
982
+
983
+ // cleanup forward/back cache
984
+ if (!isServer) {
985
+ setInterval(() => {
986
+ const now = Date.now();
987
+ for (let [k, v] of cacheMap.entries()) {
988
+ if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
989
+ cacheMap.delete(k);
990
+ }
936
991
  }
992
+ }, 300000);
993
+ }
994
+ function getCache() {
995
+ if (!isServer) return cacheMap;
996
+ const req = getRequestEvent();
997
+ if (!req) throw new Error("Cannot find cache context");
998
+ return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
999
+ }
1000
+
1001
+ /**
1002
+ * Revalidates the given cache entry/entries.
1003
+ */
1004
+ function revalidate(key, force = true) {
1005
+ const now = Date.now();
1006
+ cacheKeyOp(key, entry => {
1007
+ force && (entry[0] = 0); //force cache miss
1008
+ entry[4][1](now); // retrigger live signals
937
1009
  });
938
- };
939
- function Root(props) {
940
- const location = props.routerState.location;
941
- const params = props.routerState.params;
942
- const data = createMemo(() => props.preload && untrack(() => {
943
- setInPreloadFn(true);
944
- props.preload({
945
- params,
946
- location,
947
- intent: getIntent() || "initial"
948
- });
949
- setInPreloadFn(false);
950
- }));
951
- const RootComp = props.root;
952
- if (RootComp) {
953
- return createComponent$1(RootComp, {
954
- params: params,
955
- location: location,
956
- get data() {
957
- return data();
958
- },
959
- get children() {
960
- return props.children;
961
- }
962
- });
1010
+ }
1011
+ function cacheKeyOp(key, fn) {
1012
+ key && !Array.isArray(key) && (key = [key]);
1013
+ for (let k of cacheMap.keys()) {
1014
+ if (key === undefined || matchKey(k, key)) fn(cacheMap.get(k));
963
1015
  }
964
- return props.children;
965
1016
  }
966
- function Routes(props) {
967
- if (isServer) {
968
- const e = getRequestEvent();
969
- if (e && e.router && e.router.dataOnly) {
970
- dataOnly(e, props.routerState, props.branches);
971
- return;
972
- }
973
- e && ((e.router || (e.router = {})).matches || (e.router.matches = props.routerState.matches().map(({
974
- route,
975
- path,
976
- params
977
- }) => ({
978
- path: route.originalPath,
979
- pattern: route.pattern,
980
- match: path,
981
- params,
982
- info: route.info
983
- }))));
1017
+ function query(fn, name) {
1018
+ // query implies GET: the router primitive is the declaration site, so a
1019
+ // server function handed to query() is wrapped with core `GET(fn)` here,
1020
+ // at query-creation (module scope) — the server half records the method
1021
+ // declaration for dispatch, the client half swaps in the GET transport.
1022
+ // An explicit `GET(fn)` already carries the declaration on the metadata
1023
+ // channel (`getServerFunctionMetadata(fn)?.method === "GET"`) and passes
1024
+ // through; non-server functions are untouched.
1025
+ if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
1026
+ fn = GET(fn);
984
1027
  }
985
- const disposers = [];
986
- let root;
987
- let prevMatches;
988
- // dispose the detached per-route roots when this component unmounts, otherwise
989
- // they stay subscribed to `matches` and crash on a later navigation (#451)
990
- onCleanup(() => disposers.forEach(dispose => dispose()));
991
- // Route roots must outlive re-runs of the `routeStates` memo below, so they
992
- // are created under the owner of this component rather than the memo's
993
- // computation (which disposes its children every time it re-runs).
994
- const owner = getOwner();
995
- const routeStates = createMemo(prev => {
996
- const nextMatches = props.routerState.matches();
997
- const previousMatches = prevMatches;
998
- let equal = previousMatches && nextMatches.length === previousMatches.length;
999
- const next = [];
1000
- for (let i = 0, len = nextMatches.length; i < len; i++) {
1001
- const prevMatch = previousMatches && previousMatches[i];
1002
- const nextMatch = nextMatches[i];
1003
- if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) {
1004
- next[i] = prev[i];
1005
- } else {
1006
- equal = false;
1007
- if (disposers[i]) {
1008
- disposers[i]();
1028
+ const cachedFn = (...args) => {
1029
+ const cache = getCache();
1030
+ const intent = getIntent();
1031
+ const inPreloadFn = getInPreloadFn();
1032
+ const owner = getOwner();
1033
+ const navigate = owner ? useNavigate() : undefined;
1034
+ const now = Date.now();
1035
+ const key = name + hashKey(args);
1036
+ let cached = cache.get(key);
1037
+ let tracking;
1038
+ if (isServer) {
1039
+ const e = getRequestEvent();
1040
+ if (e) {
1041
+ const dataOnly = (e.router || (e.router = {})).dataOnly;
1042
+ if (dataOnly) {
1043
+ const data = e && (e.router.data || (e.router.data = {}));
1044
+ if (data && key in data) return data[key];
1045
+ if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) {
1046
+ data[key] = undefined;
1047
+ return Promise.resolve();
1048
+ }
1009
1049
  }
1010
- runWithOwner(owner, () => createRoot(dispose => {
1011
- disposers[i] = dispose;
1012
- const routeKey = nextMatch.route.key;
1013
- // Retain the last matches in which this route participated so
1014
- // that its components and preloads never observe another
1015
- // route's params/path while this route is being torn down.
1016
- const matchesAtLevel = createMemo(prev => {
1017
- const routeMatches = props.routerState.matches();
1018
- const m = routeMatches[i];
1019
- return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
1020
- });
1021
- next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => matchesAtLevel()[i], matchesAtLevel);
1022
- }));
1023
1050
  }
1024
1051
  }
1025
- disposers.splice(nextMatches.length).forEach(dispose => dispose());
1026
- if (prev && equal) {
1027
- prevMatches = nextMatches;
1028
- return prev;
1029
- }
1030
- root = next[0];
1031
- prevMatches = nextMatches;
1032
- return next;
1033
- });
1034
- const outlet = createOutlet(() => routeStates() && root);
1035
- return memo(outlet);
1036
- }
1037
- const createOutlet = child => {
1038
- return () => {
1039
- const c = child();
1040
- if (c) {
1041
- return createComponent$1(RouteContextObj, {
1042
- value: c,
1043
- get children() {
1044
- return c.outlet();
1045
- }
1046
- });
1052
+ if (getObserver() && !isServer) {
1053
+ tracking = true;
1054
+ onCleanup(() => cached[4].count--);
1047
1055
  }
1048
- return undefined;
1049
- };
1050
- };
1051
- const Route = props => {
1052
- const childRoutes = children(() => props.children);
1053
- return merge(props, {
1054
- get children() {
1055
- return childRoutes();
1056
- }
1057
- });
1058
- };
1059
-
1060
- // for data only mode with single flight mutations
1061
- function dataOnly(event, routerState, branches) {
1062
- const url = new URL(event.request.url);
1063
- const prevMatches = getRouteMatches(branches, new URL(event.router.previousUrl || event.request.url).pathname);
1064
- const matches = getRouteMatches(branches, url.pathname);
1065
- for (let match = 0; match < matches.length; match++) {
1066
- if (!prevMatches[match] || matches[match].route !== prevMatches[match].route) event.router.dataOnly = true;
1067
- const {
1068
- route,
1069
- params
1070
- } = matches[match];
1071
- route.preload && route.preload({
1072
- params,
1073
- location: routerState.location,
1074
- intent: "preload"
1075
- });
1076
- }
1077
- }
1078
-
1079
- function createRouter(config) {
1080
- let ignore = false;
1081
- const wrap = value => typeof value === "string" ? {
1082
- value
1083
- } : value;
1084
- const [read, write] = createSignal(wrap(config.get()), {
1085
- equals: (a, b) => a.value === b.value && a.state === b.state,
1086
- ownedWrite: true
1087
- });
1088
- const signal = [read, next => {
1089
- !ignore && config.set(next);
1090
- if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
1091
- write(next);
1092
- }];
1093
- config.init && onCleanup(config.init((value = config.get()) => {
1094
- ignore = true;
1095
- signal[1](wrap(value));
1096
- ignore = false;
1097
- }));
1098
- return createRouterComponent({
1099
- signal,
1100
- create: config.create,
1101
- utils: config.utils
1102
- });
1103
- }
1104
- function bindEvent(target, type, handler) {
1105
- target.addEventListener(type, handler);
1106
- return () => target.removeEventListener(type, handler);
1107
- }
1108
- function scrollToHash(hash, fallbackTop) {
1109
- const el = hash && document.getElementById(hash);
1110
- if (el) {
1111
- el.scrollIntoView();
1112
- } else if (fallbackTop) {
1113
- window.scrollTo(0, 0);
1114
- }
1115
- }
1116
-
1117
- function getPath(url) {
1118
- const u = new URL(url);
1119
- return u.pathname + u.search;
1120
- }
1121
- function StaticRouter(props) {
1122
- let e;
1123
- const obj = {
1124
- value: props.url || (e = getRequestEvent()) && getPath(e.request.url) || ""
1125
- };
1126
- return createRouterComponent({
1127
- signal: [() => obj, next => Object.assign(obj, next)]
1128
- })(props);
1129
- }
1130
-
1131
- const LocationHeader = "Location";
1132
- const PRELOAD_TIMEOUT = 5000;
1133
- const CACHE_TIMEOUT = 180000;
1134
- let cacheMap = new Map();
1135
-
1136
- // cleanup forward/back cache
1137
- if (!isServer) {
1138
- setInterval(() => {
1139
- const now = Date.now();
1140
- for (let [k, v] of cacheMap.entries()) {
1141
- if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
1142
- cacheMap.delete(k);
1143
- }
1144
- }
1145
- }, 300000);
1146
- }
1147
- function getCache() {
1148
- if (!isServer) return cacheMap;
1149
- const req = getRequestEvent();
1150
- if (!req) throw new Error("Cannot find cache context");
1151
- return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
1152
- }
1153
-
1154
- /**
1155
- * Revalidates the given cache entry/entries.
1156
- */
1157
- function revalidate(key, force = true) {
1158
- const now = Date.now();
1159
- cacheKeyOp(key, entry => {
1160
- force && (entry[0] = 0); //force cache miss
1161
- entry[4][1](now); // retrigger live signals
1162
- });
1163
- }
1164
- function cacheKeyOp(key, fn) {
1165
- key && !Array.isArray(key) && (key = [key]);
1166
- for (let k of cacheMap.keys()) {
1167
- if (key === undefined || matchKey(k, key)) fn(cacheMap.get(k));
1168
- }
1169
- }
1170
- function query(fn, name) {
1171
- // prioritize GET for server functions
1172
- if (fn.GET) fn = fn.GET;
1173
- const cachedFn = (...args) => {
1174
- const cache = getCache();
1175
- const intent = getIntent();
1176
- const inPreloadFn = getInPreloadFn();
1177
- const owner = getOwner();
1178
- const navigate = owner ? useNavigate() : undefined;
1179
- const now = Date.now();
1180
- const key = name + hashKey(args);
1181
- let cached = cache.get(key);
1182
- let tracking;
1183
- if (isServer) {
1184
- const e = getRequestEvent();
1185
- if (e) {
1186
- const dataOnly = (e.router || (e.router = {})).dataOnly;
1187
- if (dataOnly) {
1188
- const data = e && (e.router.data || (e.router.data = {}));
1189
- if (data && key in data) return data[key];
1190
- if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) {
1191
- data[key] = undefined;
1192
- return Promise.resolve();
1193
- }
1194
- }
1195
- }
1196
- }
1197
- if (getObserver() && !isServer) {
1198
- tracking = true;
1199
- onCleanup(() => cached[4].count--);
1200
- }
1201
- if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
1202
- if (tracking) {
1203
- cached[4].count++;
1204
- cached[4][0](); // track
1205
- }
1206
- if (cached[3] === "preload" && intent !== "preload") {
1207
- cached[0] = now;
1208
- }
1209
- let res = cached[1];
1210
- if (intent !== "preload") {
1211
- res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
1212
- !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
1213
- }
1214
- inPreloadFn && "then" in res && res.catch(() => {});
1215
- return res;
1056
+ if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
1057
+ if (tracking) {
1058
+ cached[4].count++;
1059
+ cached[4][0](); // track
1060
+ }
1061
+ if (cached[3] === "preload" && intent !== "preload") {
1062
+ cached[0] = now;
1063
+ }
1064
+ let res = cached[1];
1065
+ if (intent !== "preload") {
1066
+ res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
1067
+ !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
1068
+ }
1069
+ inPreloadFn && "then" in res && res.catch(() => {});
1070
+ return res;
1216
1071
  }
1217
1072
  let res;
1218
1073
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
@@ -1362,11 +1217,10 @@ function actionImpl(fn, options = {}) {
1362
1217
  const form = this.f;
1363
1218
  const submitHooks = current[submitHooksSymbol];
1364
1219
  const settledHooks = current[settledHooksSymbol];
1365
- const runMutation = () => (router.singleFlight && fn.withOptions ? fn.withOptions({
1366
- headers: {
1367
- "X-Single-Flight": "true"
1368
- }
1369
- }) : fn)(...variables);
1220
+ // Single-flight opt-in is no longer per call: the router's registered
1221
+ // flight-data consumer (see setupFlightDataConsumer) makes the transport
1222
+ // send the request header itself, so the mutation is just called.
1223
+ const runMutation = () => fn(...variables);
1370
1224
  const run = action$1(async function* (context) {
1371
1225
  context.optimistic?.();
1372
1226
  try {
@@ -1480,11 +1334,54 @@ async function settleActionResult(result) {
1480
1334
  }
1481
1335
  return result;
1482
1336
  }
1337
+
1338
+ /**
1339
+ * Registers the router as the single-flight consumer of the server function
1340
+ * transport. Subscribing is the opt-in: while registered, the transport
1341
+ * sends the `X-Single-Flight` request header on mutations and delivers the
1342
+ * folded payload here — fresh route data is seeded into the `query` cache
1343
+ * and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
1344
+ * applied, all before the action sees its plain return value. Called by the
1345
+ * Router component on the client unless `singleFlight={false}`, which now
1346
+ * simply means "never subscribe" — no consumer, no request header, no
1347
+ * collection work on the server. Returns the unsubscribe function.
1348
+ */
1349
+ function setupFlightDataConsumer(router) {
1350
+ return subscribeFlightData((data, {
1351
+ response
1352
+ }) => applyResponseMetadata(response, router.navigatorFactory(), data));
1353
+ }
1354
+
1355
+ /**
1356
+ * Applies a server function response's integration metadata: `X-Revalidate`
1357
+ * keys invalidate, `Location` navigates (hard for absolute urls), flight
1358
+ * data seeds the query cache, and matching entries revalidate. Shared by
1359
+ * the flight-data consumer and the action response path (which still sees
1360
+ * metadata-bearing responses when no flight data was collected).
1361
+ */
1362
+ async function applyResponseMetadata(metadata, navigate, flightData) {
1363
+ let keys;
1364
+ if (metadata) {
1365
+ if (metadata.headers.has("X-Revalidate")) keys = metadata.headers.get("X-Revalidate").split(",");
1366
+ if (metadata.headers.has("Location")) {
1367
+ const locationUrl = metadata.headers.get("Location") || "/";
1368
+ if (locationUrl.startsWith("http")) {
1369
+ window.location.href = locationUrl;
1370
+ } else {
1371
+ navigate(locationUrl);
1372
+ }
1373
+ }
1374
+ }
1375
+ // invalidate
1376
+ cacheKeyOp(keys, entry => entry[0] = 0);
1377
+ // set cache
1378
+ flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
1379
+ // trigger revalidation
1380
+ await revalidate(keys, false);
1381
+ }
1483
1382
  async function handleResponse(response, error, navigate) {
1484
1383
  let data;
1485
- let custom;
1486
- let keys;
1487
- let flightKeys;
1384
+ let flightData;
1488
1385
  let metadata;
1489
1386
  if (isResponseEnvelope(response)) {
1490
1387
  // client-only respond(): the value rides in memory beside the metadata
@@ -1492,40 +1389,268 @@ async function handleResponse(response, error, navigate) {
1492
1389
  metadata = response.response;
1493
1390
  } else if (response instanceof Response) {
1494
1391
  metadata = response;
1495
- // responses the transport hands over whole (redirects, revalidation,
1496
- // single-flight) carry a codec-encoded body the router decodes itself
1392
+ // responses the transport hands over whole (redirects, revalidation)
1393
+ // carry a codec-encoded body the router decodes itself. With the
1394
+ // flight-data consumer registered single-flight payloads never reach
1395
+ // this path, but a manually opted-in call (no consumer) still can —
1396
+ // unwrap the standardized { value, data } shape for it too.
1497
1397
  if (response.body) {
1498
1398
  data = await decodeResponse(response);
1499
1399
  if (response.headers.has("X-Single-Flight")) {
1500
- custom = data;
1501
- data = custom._$value;
1502
- delete custom._$value;
1503
- flightKeys = Object.keys(custom);
1400
+ const payload = data;
1401
+ data = payload.value;
1402
+ flightData = payload.data;
1504
1403
  }
1505
1404
  }
1506
1405
  } else if (error) return {
1507
1406
  error: response
1508
1407
  };else data = response;
1509
- if (metadata) {
1510
- if (metadata.headers.has("X-Revalidate")) keys = metadata.headers.get("X-Revalidate").split(",");
1511
- if (metadata.headers.has("Location")) {
1512
- const locationUrl = metadata.headers.get("Location") || "/";
1513
- if (locationUrl.startsWith("http")) {
1514
- window.location.href = locationUrl;
1408
+ await applyResponseMetadata(metadata, navigate, flightData);
1409
+ return data != null ? {
1410
+ data
1411
+ } : undefined;
1412
+ }
1413
+
1414
+ const createRouterComponent = router => function IntegratedRouter(props) {
1415
+ const {
1416
+ base,
1417
+ singleFlight,
1418
+ transformUrl,
1419
+ root,
1420
+ rootPreload,
1421
+ routeChildren
1422
+ } = untrack(() => ({
1423
+ base: props.base,
1424
+ singleFlight: props.singleFlight,
1425
+ transformUrl: props.transformUrl,
1426
+ root: props.root,
1427
+ rootPreload: props.rootPreload,
1428
+ routeChildren: props.children
1429
+ }));
1430
+ const routeDefs = resolveRouteDefinitions(routeChildren);
1431
+ const branches = createMemo(() => createBranches(routeDefs(), base || ""));
1432
+ let context;
1433
+ const routerState = createRouterContext(router, branches, () => context, {
1434
+ base,
1435
+ singleFlight,
1436
+ transformUrl
1437
+ });
1438
+ router.create && router.create(routerState);
1439
+ if (!isServer && routerState.singleFlight) {
1440
+ // Registering the consumer IS the single-flight opt-in: the transport
1441
+ // sends the request header while one is subscribed. `singleFlight={false}`
1442
+ // simply never subscribes, so the server is never asked to collect.
1443
+ onCleanup(setupFlightDataConsumer(routerState));
1444
+ }
1445
+ return createComponent$1(RouterContextObj, {
1446
+ value: routerState,
1447
+ get children() {
1448
+ return createComponent$1(Root, {
1449
+ routerState: routerState,
1450
+ root: root,
1451
+ preload: rootPreload,
1452
+ get children() {
1453
+ return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, {
1454
+ routerState: routerState,
1455
+ get branches() {
1456
+ return branches();
1457
+ }
1458
+ })];
1459
+ }
1460
+ });
1461
+ }
1462
+ });
1463
+ };
1464
+ function Root(props) {
1465
+ const location = props.routerState.location;
1466
+ const params = props.routerState.params;
1467
+ const data = createMemo(() => props.preload && untrack(() => {
1468
+ setInPreloadFn(true);
1469
+ props.preload({
1470
+ params,
1471
+ location,
1472
+ intent: getIntent() || "initial"
1473
+ });
1474
+ setInPreloadFn(false);
1475
+ }));
1476
+ const RootComp = props.root;
1477
+ if (RootComp) {
1478
+ return createComponent$1(RootComp, {
1479
+ params: params,
1480
+ location: location,
1481
+ get data() {
1482
+ return data();
1483
+ },
1484
+ get children() {
1485
+ return props.children;
1486
+ }
1487
+ });
1488
+ }
1489
+ return props.children;
1490
+ }
1491
+ function Routes(props) {
1492
+ if (isServer) {
1493
+ const e = getRequestEvent();
1494
+ if (e && e.router && e.router.dataOnly) {
1495
+ dataOnly(e, props.routerState, props.branches);
1496
+ return;
1497
+ }
1498
+ e && ((e.router || (e.router = {})).matches || (e.router.matches = props.routerState.matches().map(({
1499
+ route,
1500
+ path,
1501
+ params
1502
+ }) => ({
1503
+ path: route.originalPath,
1504
+ pattern: route.pattern,
1505
+ match: path,
1506
+ params,
1507
+ info: route.info
1508
+ }))));
1509
+ }
1510
+ const disposers = [];
1511
+ let root;
1512
+ let prevMatches;
1513
+ // dispose the detached per-route roots when this component unmounts, otherwise
1514
+ // they stay subscribed to `matches` and crash on a later navigation (#451)
1515
+ onCleanup(() => disposers.forEach(dispose => dispose()));
1516
+ // Route roots must outlive re-runs of the `routeStates` memo below, so they
1517
+ // are created under the owner of this component rather than the memo's
1518
+ // computation (which disposes its children every time it re-runs).
1519
+ const owner = getOwner();
1520
+ const routeStates = createMemo(prev => {
1521
+ const nextMatches = props.routerState.matches();
1522
+ const previousMatches = prevMatches;
1523
+ let equal = previousMatches && nextMatches.length === previousMatches.length;
1524
+ const next = [];
1525
+ for (let i = 0, len = nextMatches.length; i < len; i++) {
1526
+ const prevMatch = previousMatches && previousMatches[i];
1527
+ const nextMatch = nextMatches[i];
1528
+ if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) {
1529
+ next[i] = prev[i];
1515
1530
  } else {
1516
- navigate(locationUrl);
1531
+ equal = false;
1532
+ if (disposers[i]) {
1533
+ disposers[i]();
1534
+ }
1535
+ runWithOwner(owner, () => createRoot(dispose => {
1536
+ disposers[i] = dispose;
1537
+ const routeKey = nextMatch.route.key;
1538
+ // Retain the last matches in which this route participated so
1539
+ // that its components and preloads never observe another
1540
+ // route's params/path while this route is being torn down.
1541
+ const matchesAtLevel = createMemo(prev => {
1542
+ const routeMatches = props.routerState.matches();
1543
+ const m = routeMatches[i];
1544
+ return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
1545
+ });
1546
+ next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => matchesAtLevel()[i], matchesAtLevel);
1547
+ }));
1517
1548
  }
1518
1549
  }
1550
+ disposers.splice(nextMatches.length).forEach(dispose => dispose());
1551
+ if (prev && equal) {
1552
+ prevMatches = nextMatches;
1553
+ return prev;
1554
+ }
1555
+ root = next[0];
1556
+ prevMatches = nextMatches;
1557
+ return next;
1558
+ });
1559
+ const outlet = createOutlet(() => routeStates() && root);
1560
+ return memo(outlet);
1561
+ }
1562
+ const createOutlet = child => {
1563
+ return () => {
1564
+ const c = child();
1565
+ if (c) {
1566
+ return createComponent$1(RouteContextObj, {
1567
+ value: c,
1568
+ get children() {
1569
+ return c.outlet();
1570
+ }
1571
+ });
1572
+ }
1573
+ return undefined;
1574
+ };
1575
+ };
1576
+ const Route = props => {
1577
+ const childRoutes = children(() => props.children);
1578
+ return merge(props, {
1579
+ get children() {
1580
+ return childRoutes();
1581
+ }
1582
+ });
1583
+ };
1584
+
1585
+ // for data only mode with single flight mutations
1586
+ function dataOnly(event, routerState, branches) {
1587
+ const url = new URL(event.request.url);
1588
+ const prevMatches = getRouteMatches(branches, new URL(event.router.previousUrl || event.request.url).pathname);
1589
+ const matches = getRouteMatches(branches, url.pathname);
1590
+ for (let match = 0; match < matches.length; match++) {
1591
+ if (!prevMatches[match] || matches[match].route !== prevMatches[match].route) event.router.dataOnly = true;
1592
+ const {
1593
+ route,
1594
+ params
1595
+ } = matches[match];
1596
+ route.preload && route.preload({
1597
+ params,
1598
+ location: routerState.location,
1599
+ intent: "preload"
1600
+ });
1519
1601
  }
1520
- // invalidate
1521
- cacheKeyOp(keys, entry => entry[0] = 0);
1522
- // set cache
1523
- flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
1524
- // trigger revalidation
1525
- await revalidate(keys, false);
1526
- return data != null ? {
1527
- data
1528
- } : undefined;
1602
+ }
1603
+
1604
+ function createRouter(config) {
1605
+ let ignore = false;
1606
+ const wrap = value => typeof value === "string" ? {
1607
+ value
1608
+ } : value;
1609
+ const [read, write] = createSignal(wrap(config.get()), {
1610
+ equals: (a, b) => a.value === b.value && a.state === b.state,
1611
+ ownedWrite: true
1612
+ });
1613
+ const signal = [read, next => {
1614
+ !ignore && config.set(next);
1615
+ if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
1616
+ write(next);
1617
+ }];
1618
+ config.init && onCleanup(config.init((value = config.get()) => {
1619
+ ignore = true;
1620
+ signal[1](wrap(value));
1621
+ ignore = false;
1622
+ }));
1623
+ return createRouterComponent({
1624
+ signal,
1625
+ create: config.create,
1626
+ utils: config.utils
1627
+ });
1628
+ }
1629
+ function bindEvent(target, type, handler) {
1630
+ target.addEventListener(type, handler);
1631
+ return () => target.removeEventListener(type, handler);
1632
+ }
1633
+ function scrollToHash(hash, fallbackTop) {
1634
+ const el = hash && document.getElementById(hash);
1635
+ if (el) {
1636
+ el.scrollIntoView();
1637
+ } else if (fallbackTop) {
1638
+ window.scrollTo(0, 0);
1639
+ }
1640
+ }
1641
+
1642
+ function getPath(url) {
1643
+ const u = new URL(url);
1644
+ return u.pathname + u.search;
1645
+ }
1646
+ function StaticRouter(props) {
1647
+ let e;
1648
+ const obj = {
1649
+ value: props.url || (e = getRequestEvent()) && getPath(e.request.url) || ""
1650
+ };
1651
+ return createRouterComponent({
1652
+ signal: [() => obj, next => Object.assign(obj, next)]
1653
+ })(props);
1529
1654
  }
1530
1655
 
1531
1656
  function setupNativeEvents({