@solidjs/router 0.17.0-next.4 → 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,5 +1,6 @@
1
- import { isServer, getRequestEvent, createComponent as createComponent$1, memo, 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';
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';
3
4
 
4
5
  function createBeforeLeave() {
5
6
  let listeners = new Set();
@@ -257,6 +258,63 @@ function setFunctionName(obj, value) {
257
258
  return obj;
258
259
  }
259
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
+
260
318
  const MAX_REDIRECTS = 100;
261
319
 
262
320
  /** Consider this API opaque and internal. It is likely to change in the future. */
@@ -547,6 +605,17 @@ function createBranch(routes, index = 0) {
547
605
  function asArray(value) {
548
606
  return Array.isArray(value) ? value : [value];
549
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
+ }
550
619
  function createBranches(routeDef, base = "", stack = [], branches = []) {
551
620
  const routeDefs = asArray(routeDef);
552
621
  for (let i = 0, len = routeDefs.length; i < len; i++) {
@@ -837,11 +906,26 @@ function createRouterContext(integration, branches, getContext, options = {}) {
837
906
  }
838
907
  intent = prevIntent;
839
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.
840
916
  function initFromFlash() {
841
917
  const e = getRequestEvent();
842
- 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 [];
843
927
  return [{
844
- ...e.router.submission,
928
+ ...submission,
845
929
  clear() {},
846
930
  retry() {}
847
931
  }];
@@ -891,327 +975,99 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
891
975
  return route;
892
976
  }
893
977
 
894
- const createRouterComponent = router => function IntegratedRouter(props) {
895
- const {
896
- base,
897
- singleFlight,
898
- transformUrl,
899
- root,
900
- rootPreload,
901
- routeChildren
902
- } = untrack(() => ({
903
- base: props.base,
904
- singleFlight: props.singleFlight,
905
- transformUrl: props.transformUrl,
906
- root: props.root,
907
- rootPreload: props.rootPreload,
908
- routeChildren: props.children
909
- }));
910
- const routeDefs = children(() => routeChildren);
911
- const branches = createMemo(() => createBranches(routeDefs(), base || ""));
912
- let context;
913
- const routerState = createRouterContext(router, branches, () => context, {
914
- base,
915
- singleFlight,
916
- transformUrl
917
- });
918
- router.create && router.create(routerState);
919
- return createComponent$1(RouterContextObj, {
920
- value: routerState,
921
- get children() {
922
- return createComponent$1(Root, {
923
- routerState: routerState,
924
- root: root,
925
- preload: rootPreload,
926
- get children() {
927
- return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, {
928
- routerState: routerState,
929
- get branches() {
930
- return branches();
931
- }
932
- })];
933
- }
934
- });
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
+ }
935
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
936
1009
  });
937
- };
938
- function Root(props) {
939
- const location = props.routerState.location;
940
- const params = props.routerState.params;
941
- const data = createMemo(() => props.preload && untrack(() => {
942
- setInPreloadFn(true);
943
- props.preload({
944
- params,
945
- location,
946
- intent: getIntent() || "initial"
947
- });
948
- setInPreloadFn(false);
949
- }));
950
- const RootComp = props.root;
951
- if (RootComp) {
952
- return createComponent$1(RootComp, {
953
- params: params,
954
- location: location,
955
- get data() {
956
- return data();
957
- },
958
- get children() {
959
- return props.children;
960
- }
961
- });
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));
962
1015
  }
963
- return props.children;
964
1016
  }
965
- function Routes(props) {
966
- if (isServer) {
967
- const e = getRequestEvent();
968
- if (e && e.router && e.router.dataOnly) {
969
- dataOnly(e, props.routerState, props.branches);
970
- return;
971
- }
972
- e && ((e.router || (e.router = {})).matches || (e.router.matches = props.routerState.matches().map(({
973
- route,
974
- path,
975
- params
976
- }) => ({
977
- path: route.originalPath,
978
- pattern: route.pattern,
979
- match: path,
980
- params,
981
- info: route.info
982
- }))));
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);
983
1027
  }
984
- const disposers = [];
985
- let root;
986
- let prevMatches;
987
- // dispose the detached per-route roots when this component unmounts, otherwise
988
- // they stay subscribed to `matches` and crash on a later navigation (#451)
989
- onCleanup(() => disposers.forEach(dispose => dispose()));
990
- // Route roots must outlive re-runs of the `routeStates` memo below, so they
991
- // are created under the owner of this component rather than the memo's
992
- // computation (which disposes its children every time it re-runs).
993
- const owner = getOwner();
994
- const routeStates = createMemo(prev => {
995
- const nextMatches = props.routerState.matches();
996
- const previousMatches = prevMatches;
997
- let equal = previousMatches && nextMatches.length === previousMatches.length;
998
- const next = [];
999
- for (let i = 0, len = nextMatches.length; i < len; i++) {
1000
- const prevMatch = previousMatches && previousMatches[i];
1001
- const nextMatch = nextMatches[i];
1002
- if (prev && prevMatch && nextMatch.route.key === prevMatch.route.key) {
1003
- next[i] = prev[i];
1004
- } else {
1005
- equal = false;
1006
- if (disposers[i]) {
1007
- 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
+ }
1008
1049
  }
1009
- runWithOwner(owner, () => createRoot(dispose => {
1010
- disposers[i] = dispose;
1011
- const routeKey = nextMatch.route.key;
1012
- // Retain the last matches in which this route participated so
1013
- // that its components and preloads never observe another
1014
- // route's params/path while this route is being torn down.
1015
- const matchesAtLevel = createMemo(prev => {
1016
- const routeMatches = props.routerState.matches();
1017
- const m = routeMatches[i];
1018
- return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
1019
- });
1020
- next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => matchesAtLevel()[i], matchesAtLevel);
1021
- }));
1022
1050
  }
1023
1051
  }
1024
- disposers.splice(nextMatches.length).forEach(dispose => dispose());
1025
- if (prev && equal) {
1026
- prevMatches = nextMatches;
1027
- return prev;
1028
- }
1029
- root = next[0];
1030
- prevMatches = nextMatches;
1031
- return next;
1032
- });
1033
- const outlet = createOutlet(() => routeStates() && root);
1034
- return memo(outlet);
1035
- }
1036
- const createOutlet = child => {
1037
- return () => {
1038
- const c = child();
1039
- if (c) {
1040
- return createComponent$1(RouteContextObj, {
1041
- value: c,
1042
- get children() {
1043
- return c.outlet();
1044
- }
1045
- });
1052
+ if (getObserver() && !isServer) {
1053
+ tracking = true;
1054
+ onCleanup(() => cached[4].count--);
1046
1055
  }
1047
- return undefined;
1048
- };
1049
- };
1050
- const Route = props => {
1051
- const childRoutes = children(() => props.children);
1052
- return merge(props, {
1053
- get children() {
1054
- return childRoutes();
1055
- }
1056
- });
1057
- };
1058
-
1059
- // for data only mode with single flight mutations
1060
- function dataOnly(event, routerState, branches) {
1061
- const url = new URL(event.request.url);
1062
- const prevMatches = getRouteMatches(branches, new URL(event.router.previousUrl || event.request.url).pathname);
1063
- const matches = getRouteMatches(branches, url.pathname);
1064
- for (let match = 0; match < matches.length; match++) {
1065
- if (!prevMatches[match] || matches[match].route !== prevMatches[match].route) event.router.dataOnly = true;
1066
- const {
1067
- route,
1068
- params
1069
- } = matches[match];
1070
- route.preload && route.preload({
1071
- params,
1072
- location: routerState.location,
1073
- intent: "preload"
1074
- });
1075
- }
1076
- }
1077
-
1078
- function createRouter(config) {
1079
- let ignore = false;
1080
- const wrap = value => typeof value === "string" ? {
1081
- value
1082
- } : value;
1083
- const [read, write] = createSignal(wrap(config.get()), {
1084
- equals: (a, b) => a.value === b.value && a.state === b.state,
1085
- ownedWrite: true
1086
- });
1087
- const signal = [read, next => {
1088
- !ignore && config.set(next);
1089
- if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
1090
- write(next);
1091
- }];
1092
- config.init && onCleanup(config.init((value = config.get()) => {
1093
- ignore = true;
1094
- signal[1](wrap(value));
1095
- ignore = false;
1096
- }));
1097
- return createRouterComponent({
1098
- signal,
1099
- create: config.create,
1100
- utils: config.utils
1101
- });
1102
- }
1103
- function bindEvent(target, type, handler) {
1104
- target.addEventListener(type, handler);
1105
- return () => target.removeEventListener(type, handler);
1106
- }
1107
- function scrollToHash(hash, fallbackTop) {
1108
- const el = hash && document.getElementById(hash);
1109
- if (el) {
1110
- el.scrollIntoView();
1111
- } else if (fallbackTop) {
1112
- window.scrollTo(0, 0);
1113
- }
1114
- }
1115
-
1116
- function getPath(url) {
1117
- const u = new URL(url);
1118
- return u.pathname + u.search;
1119
- }
1120
- function StaticRouter(props) {
1121
- let e;
1122
- const obj = {
1123
- value: props.url || (e = getRequestEvent()) && getPath(e.request.url) || ""
1124
- };
1125
- return createRouterComponent({
1126
- signal: [() => obj, next => Object.assign(obj, next)]
1127
- })(props);
1128
- }
1129
-
1130
- const LocationHeader = "Location";
1131
- const PRELOAD_TIMEOUT = 5000;
1132
- const CACHE_TIMEOUT = 180000;
1133
- let cacheMap = new Map();
1134
-
1135
- // cleanup forward/back cache
1136
- if (!isServer) {
1137
- setInterval(() => {
1138
- const now = Date.now();
1139
- for (let [k, v] of cacheMap.entries()) {
1140
- if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
1141
- cacheMap.delete(k);
1142
- }
1143
- }
1144
- }, 300000);
1145
- }
1146
- function getCache() {
1147
- if (!isServer) return cacheMap;
1148
- const req = getRequestEvent();
1149
- if (!req) throw new Error("Cannot find cache context");
1150
- return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
1151
- }
1152
-
1153
- /**
1154
- * Revalidates the given cache entry/entries.
1155
- */
1156
- function revalidate(key, force = true) {
1157
- const now = Date.now();
1158
- cacheKeyOp(key, entry => {
1159
- force && (entry[0] = 0); //force cache miss
1160
- entry[4][1](now); // retrigger live signals
1161
- });
1162
- }
1163
- function cacheKeyOp(key, fn) {
1164
- key && !Array.isArray(key) && (key = [key]);
1165
- for (let k of cacheMap.keys()) {
1166
- if (key === undefined || matchKey(k, key)) fn(cacheMap.get(k));
1167
- }
1168
- }
1169
- function query(fn, name) {
1170
- // prioritize GET for server functions
1171
- if (fn.GET) fn = fn.GET;
1172
- const cachedFn = (...args) => {
1173
- const cache = getCache();
1174
- const intent = getIntent();
1175
- const inPreloadFn = getInPreloadFn();
1176
- const owner = getOwner();
1177
- const navigate = owner ? useNavigate() : undefined;
1178
- const now = Date.now();
1179
- const key = name + hashKey(args);
1180
- let cached = cache.get(key);
1181
- let tracking;
1182
- if (isServer) {
1183
- const e = getRequestEvent();
1184
- if (e) {
1185
- const dataOnly = (e.router || (e.router = {})).dataOnly;
1186
- if (dataOnly) {
1187
- const data = e && (e.router.data || (e.router.data = {}));
1188
- if (data && key in data) return data[key];
1189
- if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) {
1190
- data[key] = undefined;
1191
- return Promise.resolve();
1192
- }
1193
- }
1194
- }
1195
- }
1196
- if (getObserver() && !isServer) {
1197
- tracking = true;
1198
- onCleanup(() => cached[4].count--);
1199
- }
1200
- if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
1201
- if (tracking) {
1202
- cached[4].count++;
1203
- cached[4][0](); // track
1204
- }
1205
- if (cached[3] === "preload" && intent !== "preload") {
1206
- cached[0] = now;
1207
- }
1208
- let res = cached[1];
1209
- if (intent !== "preload") {
1210
- res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
1211
- !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
1212
- }
1213
- inPreloadFn && "then" in res && res.catch(() => {});
1214
- 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;
1215
1071
  }
1216
1072
  let res;
1217
1073
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
@@ -1250,6 +1106,14 @@ function query(fn, name) {
1250
1106
  return res;
1251
1107
  function handleResponse(error) {
1252
1108
  return async v => {
1109
+ let enveloped;
1110
+ let hasEnveloped = false;
1111
+ if (isResponseEnvelope(v)) {
1112
+ // respond(): the value rides in memory beside the metadata
1113
+ enveloped = v.value;
1114
+ hasEnveloped = true;
1115
+ v = v.response;
1116
+ }
1253
1117
  if (v instanceof Response) {
1254
1118
  const e = getRequestEvent();
1255
1119
  if (e) {
@@ -1265,7 +1129,12 @@ function query(fn, name) {
1265
1129
  });else if (!isServer) window.location.href = url;else if (e) e.response.status = 302;
1266
1130
  return;
1267
1131
  }
1268
- if (v.customBody) v = await v.customBody();
1132
+ if (hasEnveloped) v = enveloped;else if (v.body) {
1133
+ // responses the transport hands over whole (revalidation) carry a
1134
+ // codec-encoded body; anything else (a raw user Response) stays whole
1135
+ const decoded = await decodeResponse(v);
1136
+ if (decoded !== undefined) v = decoded;
1137
+ }
1269
1138
  }
1270
1139
  if (error) throw v;
1271
1140
  cached[2] = v;
@@ -1348,11 +1217,10 @@ function actionImpl(fn, options = {}) {
1348
1217
  const form = this.f;
1349
1218
  const submitHooks = current[submitHooksSymbol];
1350
1219
  const settledHooks = current[settledHooksSymbol];
1351
- const runMutation = () => (router.singleFlight && fn.withOptions ? fn.withOptions({
1352
- headers: {
1353
- "X-Single-Flight": "true"
1354
- }
1355
- }) : 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);
1356
1224
  const run = action$1(async function* (context) {
1357
1225
  context.optimistic?.();
1358
1226
  try {
@@ -1466,43 +1334,325 @@ async function settleActionResult(result) {
1466
1334
  }
1467
1335
  return result;
1468
1336
  }
1469
- async function handleResponse(response, error, navigate) {
1470
- let data;
1471
- let custom;
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) {
1472
1363
  let keys;
1473
- let flightKeys;
1474
- if (response instanceof Response) {
1475
- if (response.headers.has("X-Revalidate")) keys = response.headers.get("X-Revalidate").split(",");
1476
- if (response.customBody) {
1477
- data = custom = await response.customBody();
1478
- if (response.headers.has("X-Single-Flight")) {
1479
- data = data._$value;
1480
- delete custom._$value;
1481
- flightKeys = Object.keys(custom);
1482
- }
1483
- }
1484
- if (response.headers.has("Location")) {
1485
- const locationUrl = response.headers.get("Location") || "/";
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") || "/";
1486
1368
  if (locationUrl.startsWith("http")) {
1487
1369
  window.location.href = locationUrl;
1488
1370
  } else {
1489
1371
  navigate(locationUrl);
1490
1372
  }
1491
1373
  }
1492
- } else if (error) return {
1493
- error: response
1494
- };else data = response;
1374
+ }
1495
1375
  // invalidate
1496
1376
  cacheKeyOp(keys, entry => entry[0] = 0);
1497
1377
  // set cache
1498
- flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
1378
+ flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
1499
1379
  // trigger revalidation
1500
1380
  await revalidate(keys, false);
1381
+ }
1382
+ async function handleResponse(response, error, navigate) {
1383
+ let data;
1384
+ let flightData;
1385
+ let metadata;
1386
+ if (isResponseEnvelope(response)) {
1387
+ // client-only respond(): the value rides in memory beside the metadata
1388
+ data = response.value;
1389
+ metadata = response.response;
1390
+ } else if (response instanceof Response) {
1391
+ metadata = response;
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.
1397
+ if (response.body) {
1398
+ data = await decodeResponse(response);
1399
+ if (response.headers.has("X-Single-Flight")) {
1400
+ const payload = data;
1401
+ data = payload.value;
1402
+ flightData = payload.data;
1403
+ }
1404
+ }
1405
+ } else if (error) return {
1406
+ error: response
1407
+ };else data = response;
1408
+ await applyResponseMetadata(metadata, navigate, flightData);
1501
1409
  return data != null ? {
1502
1410
  data
1503
1411
  } : undefined;
1504
1412
  }
1505
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];
1530
+ } else {
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
+ }));
1548
+ }
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
+ });
1601
+ }
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);
1654
+ }
1655
+
1506
1656
  function setupNativeEvents({
1507
1657
  preload = true,
1508
1658
  explicitLinks = false,
@@ -1840,57 +1990,4 @@ function Navigate(props) {
1840
1990
  return null;
1841
1991
  }
1842
1992
 
1843
- function redirect(url, init = 302) {
1844
- let responseInit;
1845
- let revalidate;
1846
- if (typeof init === "number") {
1847
- responseInit = {
1848
- status: init
1849
- };
1850
- } else {
1851
- ({
1852
- revalidate,
1853
- ...responseInit
1854
- } = init);
1855
- if (typeof responseInit.status === "undefined") {
1856
- responseInit.status = 302;
1857
- }
1858
- }
1859
- const headers = new Headers(responseInit.headers);
1860
- headers.set("Location", url);
1861
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
1862
- const response = new Response(null, {
1863
- ...responseInit,
1864
- headers: headers
1865
- });
1866
- return response;
1867
- }
1868
- function reload(init = {}) {
1869
- const {
1870
- revalidate,
1871
- ...responseInit
1872
- } = init;
1873
- const headers = new Headers(responseInit.headers);
1874
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
1875
- return new Response(null, {
1876
- ...responseInit,
1877
- headers
1878
- });
1879
- }
1880
- function json(data, init = {}) {
1881
- const {
1882
- revalidate,
1883
- ...responseInit
1884
- } = init;
1885
- const headers = new Headers(responseInit.headers);
1886
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
1887
- headers.set("Content-Type", "application/json");
1888
- const response = new Response(JSON.stringify(data), {
1889
- ...responseInit,
1890
- headers
1891
- });
1892
- response.customBody = () => data;
1893
- return response;
1894
- }
1895
-
1896
- export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmissions };
1993
+ export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, createBeforeLeave, createMemoryHistory, createRouter, keepDepth, notifyIfNotBlocked, query, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmissions };