@solidjs/router 2.0.0-next.16 → 2.0.0-next.18

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,4 +1,4 @@
1
- import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, flush, createComponent, createRoot, createEffect, sharedConfig, getObserver, $TRACK, action as action$1 } from 'solid-js';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, flush, createComponent, createRoot, Show, createEffect, sharedConfig, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
2
  import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
3
  import { subscribeFlightData, decodeResponsePayload, createServerReference } from '@solidjs/web/server-functions';
4
4
  import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
@@ -475,6 +475,15 @@ const int = s => /^-?\d+$/.test(s);
475
475
 
476
476
  const encodeParam = value => String(value).split("/").map(encodeURIComponent).join("/");
477
477
 
478
+ /**
479
+ * The global Href brand (`@solidjs/web` reads the same registry symbol).
480
+ * On a paths node it holds the logical path — the routable pathname before
481
+ * `renderPath` decorates it for display (eg. hash mode's `#` prefix) — so
482
+ * `navigate()` and `redirect()` can route the node while `toString()` keeps
483
+ * yielding the display href for the DOM.
484
+ */
485
+ const HREF = Symbol.for("solid.Href");
486
+
478
487
  /**
479
488
  * Creates the runtime path proxy. It is instance-scoped: `renderPath` comes
480
489
  * from the router's history adapter (eg. hash routing prefixes `#`), and
@@ -500,7 +509,7 @@ function createPathsProxy(renderPath = p => p, base = "") {
500
509
  return new Proxy(build, {
501
510
  get(_, prop) {
502
511
  if (prop === "toString") return () => toHref(pathname);
503
- if (typeof prop === "symbol") return prop === Symbol.toPrimitive ? () => toHref(pathname) : undefined;
512
+ if (typeof prop === "symbol") return prop === Symbol.toPrimitive ? () => toHref(pathname) : prop === HREF ? pathname || "/" : undefined;
504
513
  return node(`${pathname}/${prop}`);
505
514
  }
506
515
  });
@@ -508,13 +517,6 @@ function createPathsProxy(renderPath = p => p, base = "") {
508
517
  return node(normalizePath(base));
509
518
  }
510
519
 
511
- // Flash-cookie detection/clearing are cookie utilities on the CORE entry
512
- // (they live beside the cookie codec) — routing is in every app's eager
513
- // graph and must never import the server-functions package, whose client
514
- // half is the fetch transport + the seroval codec. The type below is
515
- // erased at build; only the server-only action path imports that module's
516
- // values (behind isServer, tree-shaken from client bundles).
517
-
518
520
  const MAX_REDIRECTS = 100;
519
521
 
520
522
  /** Consider this API opaque and internal. It is likely to change in the future. */
@@ -817,12 +819,17 @@ const encodeSegment = s => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B
817
819
  const lazyBoundaries = new WeakMap();
818
820
  // Module scope: boundary resolution is global, deterministic state (same
819
821
  // thunk -> same routes), shared by every factory instance and the server's
820
- // flight collector.
822
+ // flight collector. The counter is the source of truth; the client mirrors
823
+ // it into a signal for subscription. Server render is pure (signal writes
824
+ // are inert as of solid 2.0.0-rc.3): there the plain counter serves reads,
825
+ // and re-runs come from the parked computation's promise retry, not from
826
+ // reactivity.
827
+ let lazyTreeCounter = 0;
821
828
  const [lazyTreeVersion, setLazyTreeVersion] = createSignal(0);
822
829
 
823
830
  /** Reactive read of the lazy-subtree version — recompile compiled branches when it changes. */
824
831
  function trackLazySubtrees() {
825
- return lazyTreeVersion();
832
+ return isServer ? lazyTreeCounter : lazyTreeVersion();
826
833
  }
827
834
  function getLazyBoundary(thunk) {
828
835
  let record = lazyBoundaries.get(thunk);
@@ -834,17 +841,42 @@ function getLazyBoundary(thunk) {
834
841
 
835
842
  /**
836
843
  * Kicks (or joins) a boundary's resolution. Returns the resolved routes
837
- * synchronously once available, the in-flight promise otherwise. Commit is
838
- * always async — even for thunks returning arrays — so the version bump
839
- * never writes a signal from inside a render computation.
844
+ * synchronously once available, the in-flight promise otherwise.
845
+ *
846
+ * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
847
+ * re-fetches a failed dynamic import): a client rejection is held through its
848
+ * settlement flush. The recomputes the settled promise triggers — the parked
849
+ * transition's and the mainline commit's — both consume it here as a
850
+ * synchronous throw, becoming cached error status that reaches the nearest
851
+ * error boundary like a failed lazy() component. The hold clears a microtask
852
+ * after the first delivery, so any later recompute — boundary reset(), a new
853
+ * navigation — finds a clean record and retries the import. Holding through
854
+ * the flush is what keeps the erroring computation from refiring the import
855
+ * in a tight loop while it fails. Server records are shared across requests
856
+ * and hold nothing — each request retries. Commit is always async — even for
857
+ * thunks returning arrays — so the version bump never writes a signal from
858
+ * inside a render computation.
840
859
  */
841
860
  function resolveLazySubtree(record) {
842
861
  if (record.resolved) return record.resolved;
862
+ if (record.error !== undefined) {
863
+ if (!record.sweep) {
864
+ record.sweep = true;
865
+ queueMicrotask(() => record.error = record.sweep = undefined);
866
+ }
867
+ throw record.error;
868
+ }
843
869
  return record.promise ||= Promise.resolve(record.thunk()).then(m => {
844
870
  const routes = Array.isArray(m) ? m : m.default || m.routes || [];
845
871
  record.resolved = routes;
846
- setLazyTreeVersion(v => v + 1);
872
+ lazyTreeCounter++;
873
+ isServer || setLazyTreeVersion(lazyTreeCounter);
847
874
  return record.resolved;
875
+ }, e => {
876
+ // ?? Error(): a held undefined would read as "no failure" and refire
877
+ if (!isServer) record.error = e ?? new Error();
878
+ record.promise = undefined;
879
+ throw e;
848
880
  });
849
881
  }
850
882
 
@@ -992,7 +1024,7 @@ function createLocation(path, state, queryWrapper) {
992
1024
  // doubled leading slashes would otherwise parse as protocol-relative
993
1025
  return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
994
1026
  } catch (err) {
995
- console.error(`Invalid path ${path_}`);
1027
+ DEV && console.error(`Invalid path ${path_}`);
996
1028
  return prev;
997
1029
  }
998
1030
  }, {
@@ -1143,7 +1175,14 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1143
1175
  // from the carried promise's retry on the server; a boundary nested
1144
1176
  // inside a boundary just parks the recomputed chain again.
1145
1177
  const pending = unresolvedLazyMatches(m);
1146
- if (pending.length) throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
1178
+ if (pending.length) {
1179
+ const all = Promise.all(pending.map(resolveLazySubtree));
1180
+ // pre-handle rejections: the failure reaches the app through this
1181
+ // computation's error status, so the raw chain must not also surface
1182
+ // as an unhandled rejection
1183
+ all.catch(() => {});
1184
+ throw new NotReadyError(all);
1185
+ }
1147
1186
  return m;
1148
1187
  });
1149
1188
 
@@ -1153,7 +1192,18 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1153
1192
  // window, and isPending over the location/matches read reports any
1154
1193
  // in-flight fork — including popstate traversals and the lazy-subtree
1155
1194
  // resolution matches() parks on.
1156
- const isRouting = createMemo(() => isNavigating() || isPending(() => (matches(), location.search, location.hash)));
1195
+ const isRouting = createMemo(() => isNavigating() || isPending(() => {
1196
+ // A real error means the navigation settled (failed) — it surfaces
1197
+ // to the app through render reads, not through this derivation.
1198
+ // Not-ready must keep propagating so isPending sees the parking.
1199
+ try {
1200
+ matches();
1201
+ } catch (e) {
1202
+ if (e instanceof NotReadyError) throw e;
1203
+ }
1204
+ location.search;
1205
+ location.hash;
1206
+ }));
1157
1207
  const buildParams = () => mergeParams(matches());
1158
1208
  const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
1159
1209
  const params = wrapParams(buildParams);
@@ -1195,12 +1245,22 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1195
1245
  if (!to) ; else if (utils.go) {
1196
1246
  utils.go(to);
1197
1247
  } else {
1198
- console.warn("Router integration does not support relative routing");
1248
+ DEV && console.warn("Router integration does not support relative routing");
1199
1249
  }
1200
1250
  return;
1201
1251
  }
1202
- // typed path proxy nodes coerce to their href
1203
- if (typeof to !== "string") to = to.toString();
1252
+ // A paths node carries its logical path under the Href brand — read
1253
+ // that rather than coercing: toString() renders the *display* href
1254
+ // (eg. hash mode's `#` prefix), which is for the DOM, not for routing.
1255
+ // Foreign Href-branded values without the slot still coerce.
1256
+ if (typeof to !== "string") to = to[HREF] || to.toString();
1257
+ // Display hrefs can still arrive as plain strings: terminating paths
1258
+ // calls type as `string`, and redirect Location headers round-trip
1259
+ // through here. Under hash mode those start with `#` — a spelling no
1260
+ // logical path uses — so map them back through the integration's
1261
+ // parser, exactly like the anchor click handler does. Elsewhere
1262
+ // parsePath is identity and `#...` keeps its URL meaning below.
1263
+ if (to[0] === "#") to = parsePath(to);
1204
1264
  const {
1205
1265
  replace,
1206
1266
  resolve,
@@ -1301,9 +1361,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1301
1361
  const matches = getRouteMatches(branches(), url.pathname);
1302
1362
  // An unresolved lazy subtree in the chain: the placeholder's
1303
1363
  // component.preload (below) kicks the table load; once it lands,
1304
- // preload again so the real inner routes warm too.
1364
+ // preload again so the real inner routes warm too. Preloads are
1365
+ // speculative: a failed load (held sync throw or rejection) is ignored
1366
+ // here — the real navigation surfaces and retries it.
1305
1367
  const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
1306
- boundary && resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
1368
+ if (boundary) {
1369
+ try {
1370
+ resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
1371
+ } catch {}
1372
+ }
1307
1373
  const prevIntent = intent;
1308
1374
  intent = "preload";
1309
1375
  for (let match in matches) {
@@ -1508,18 +1574,26 @@ function Routes(props) {
1508
1574
  return memo(outlet);
1509
1575
  }
1510
1576
  const createOutlet = child => {
1511
- return () => {
1512
- const c = child();
1513
- if (c) {
1514
- return createComponent$1(RouteContextObj, {
1515
- value: c,
1516
- get children() {
1517
- return c.outlet();
1518
- }
1519
- });
1520
- }
1521
- return undefined;
1522
- };
1577
+ // Keyed on the context's identity (#588). `when` compiles to a lazy prop
1578
+ // getter, so child() is tracked inside Show's memo — the outlet function
1579
+ // reads nothing reactive and Show owns the subtree. Levels whose context is
1580
+ // reference-reused (parent layouts) keep their subtree across navigations;
1581
+ // a changed context must re-create the provider, not just re-render under
1582
+ // it: a context value is registered once at provider creation
1583
+ // (setContext(provider, props.value)), so non-keyed reuse would leave new
1584
+ // route content reading the previous level's context.
1585
+ return () => createComponent$1(Show, {
1586
+ get when() {
1587
+ return child();
1588
+ },
1589
+ keyed: true,
1590
+ children: c => createComponent$1(RouteContextObj, {
1591
+ value: c,
1592
+ get children() {
1593
+ return c.outlet();
1594
+ }
1595
+ })
1596
+ });
1523
1597
  };
1524
1598
 
1525
1599
  // for data only mode with single flight mutations
@@ -1982,7 +2056,7 @@ function createRouter(config) {
1982
2056
  // fight it — stale content on click navigations, conflicting link
1983
2057
  // attributes. Compose route trees instead; lazy subtrees are the planned
1984
2058
  // answer for definitions unknown at build time.
1985
- if (useOptionalContext(RouterContextObj)) {
2059
+ if (DEV && useOptionalContext(RouterContextObj)) {
1986
2060
  console.warn("Mounting a router inside another router is not supported. " + "Compose route trees in one createRouter config instead.");
1987
2061
  }
1988
2062
  const root = untrack(() => props.children);
@@ -2139,7 +2213,7 @@ const useBeforeLeave = listener => {
2139
2213
  };
2140
2214
 
2141
2215
  const LocationHeader = "Location";
2142
- const PRELOAD_TIMEOUT = 5000;
2216
+ const PRELOAD_TIMEOUT$1 = 5000;
2143
2217
  const CACHE_TIMEOUT = 180000;
2144
2218
  // When this client booted. Flight-registry entries (sharedConfig.has/load)
2145
2219
  // hold values the server computed while rendering THIS page, so their age is
@@ -2165,6 +2239,23 @@ function getCache() {
2165
2239
  return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
2166
2240
  }
2167
2241
 
2242
+ // Other keyed stores participating in the data protocols (live query
2243
+ // channels) register here — revalidate() stays the one invalidation verb,
2244
+ // and single-flight payloads reach every keyed store, not just the cache.
2245
+ const revalidateHooks = [];
2246
+ function registerRevalidateHook(hook) {
2247
+ revalidateHooks.push(hook);
2248
+ }
2249
+ const flightHooks = [];
2250
+ function registerFlightDataHook(hook) {
2251
+ flightHooks.push(hook);
2252
+ }
2253
+ /** Fans a single-flight payload out to registered keyed stores (the query
2254
+ * cache itself is seeded by the caller via query.set). */
2255
+ function deliverFlightData(data) {
2256
+ for (const hook of flightHooks) hook(data);
2257
+ }
2258
+
2168
2259
  /**
2169
2260
  * Revalidates the given cache entry/entries.
2170
2261
  */
@@ -2174,6 +2265,8 @@ function revalidate(key, force = true) {
2174
2265
  force && (entry[0] = 0); //force cache miss
2175
2266
  entry[4][1](now); // retrigger live signals
2176
2267
  });
2268
+ const keys = key === undefined ? undefined : Array.isArray(key) ? key : [key];
2269
+ for (const hook of revalidateHooks) hook(keys, force);
2177
2270
  }
2178
2271
  function cacheKeyOp(key, fn) {
2179
2272
  key && !Array.isArray(key) && (key = [key]);
@@ -2227,7 +2320,7 @@ function query(fn, name) {
2227
2320
  tracking = true;
2228
2321
  onCleanup(() => cached[4].count--);
2229
2322
  }
2230
- if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
2323
+ if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT$1)) {
2231
2324
  if (tracking) {
2232
2325
  cached[4].count++;
2233
2326
  cached[4][0](); // track
@@ -2259,7 +2352,7 @@ function query(fn, name) {
2259
2352
  // refetches instead of serving a minutes-old value on a fresh navigation.
2260
2353
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
2261
2354
  const payloadAge = now - bootTime;
2262
- if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT)) {
2355
+ if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1)) {
2263
2356
  adopted = true;
2264
2357
  res = sharedConfig.load(key); // hydrating
2265
2358
  // @ts-ignore at least until we add a delete method to sharedConfig
@@ -2741,8 +2834,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
2741
2834
  }
2742
2835
  // invalidate
2743
2836
  cacheKeyOp(keys, entry => entry[0] = 0);
2744
- // set cache
2837
+ // set cache — and fan the payload out to other keyed stores (live query
2838
+ // channels adopt delivered values instead of reconnecting)
2745
2839
  flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
2840
+ flightData && deliverFlightData(flightData);
2746
2841
  // trigger revalidation inside the same transition as the navigation, so
2747
2842
  // the redirect commits atomically with fresh data: surviving consumers
2748
2843
  // (shared layouts) the flight payload didn't seed refetch and hold the
@@ -2782,6 +2877,366 @@ async function handleResponse(response, error, navigate, metadataHandled) {
2782
2877
  } : undefined;
2783
2878
  }
2784
2879
 
2880
+ // Matches query's preload lifetime: a channel warmed by preload intent
2881
+ // stays held this long waiting for its navigation.
2882
+ const PRELOAD_TIMEOUT = 5000;
2883
+
2884
+ // The live-source brand (registered symbol — no transport import needed):
2885
+ // computations meeting a branded iterable apply live SSR policy (document
2886
+ // face takes the first value; hydration re-runs the compute after adoption
2887
+ // to reconnect). liveQuery writes it on both faces itself — the server-side
2888
+ // resolved iterable and the client-side multicast iterable — so a producer
2889
+ // needs no separate live() declaration to be live here.
2890
+ const LIVE_SOURCE = Symbol.for("solid.LiveSource");
2891
+ const channelMap = new Map();
2892
+ // Status outlives the channel (an "idle"/"closed" read after teardown is
2893
+ // meaningful), so signals live in their own keyed map.
2894
+ const statusMap = new Map();
2895
+ function statusSignal(key) {
2896
+ let s = statusMap.get(key);
2897
+ // ownedWrite: the first write happens synchronously inside whatever
2898
+ // computation pulled the channel open — this is channel-owned state, not
2899
+ // the computation's own.
2900
+ if (!s) statusMap.set(key, s = createSignal("idle", {
2901
+ ownedWrite: true
2902
+ }));
2903
+ return s;
2904
+ }
2905
+ function wake(ch) {
2906
+ const waiters = [...ch.waiters];
2907
+ ch.waiters.clear();
2908
+ for (const w of waiters) w();
2909
+ }
2910
+ const noStatus = () => {};
2911
+ async function connect(ch) {
2912
+ const gen = ch.gen;
2913
+ const setStatus = ch.report ? statusSignal(ch.key)[1] : noStatus;
2914
+ let attempts = 0;
2915
+ setStatus(ch.version ? "reconnecting" : "connecting");
2916
+ while (true) {
2917
+ try {
2918
+ const result = await ch.call();
2919
+ if (gen !== ch.gen || ch.done) {
2920
+ // superseded (reconnect) or torn down while connecting: the arrived
2921
+ // stream must still be ended or its request leaks
2922
+ result?.[Symbol.asyncIterator] && result[Symbol.asyncIterator]().return?.();
2923
+ return;
2924
+ }
2925
+ // A live()-declared producer's own retry loop erases deaths from the
2926
+ // value stream and reports them through onstatus — forward those into
2927
+ // the channel's status so both producer kinds read the same. Its
2928
+ // "closed" is redundant with our own done handling below.
2929
+ if (result !== null && typeof result === "object") {
2930
+ try {
2931
+ result.onstatus = state => {
2932
+ if (gen === ch.gen && state !== "closed") setStatus(state);
2933
+ };
2934
+ } catch {}
2935
+ }
2936
+ const it = result !== null && typeof result === "object" && result[Symbol.asyncIterator] ? result[Symbol.asyncIterator]() : async function* () {
2937
+ yield result;
2938
+ }();
2939
+ ch.close = () => {
2940
+ try {
2941
+ const r = it.return && it.return();
2942
+ if (r && typeof r.then === "function") r.then(undefined, () => {});
2943
+ } catch {}
2944
+ };
2945
+ while (true) {
2946
+ const r = await it.next();
2947
+ if (gen !== ch.gen || ch.done) return;
2948
+ if (r.done) break;
2949
+ ch.latest = r.value;
2950
+ ch.version++;
2951
+ attempts = 0; // healthy value: backoff resets
2952
+ setStatus("connected");
2953
+ wake(ch);
2954
+ }
2955
+ ch.done = true;
2956
+ setStatus("closed");
2957
+ wake(ch);
2958
+ return;
2959
+ } catch (error) {
2960
+ if (gen !== ch.gen || ch.done) return;
2961
+ const status = error?.status;
2962
+ if (!ch.version || typeof status === "number" && status >= 400 && status < 500) {
2963
+ // Terminal: never connected (fail like a normal call), or a
2964
+ // definite rejection — the transport stamps HTTP statuses onto
2965
+ // failures, and a 4xx means the server understood and refused;
2966
+ // retrying cannot change the answer. Consumers rethrow (after
2967
+ // draining the latest value), surfacing to error boundaries.
2968
+ ch.error = error;
2969
+ ch.done = true;
2970
+ setStatus("closed");
2971
+ wake(ch);
2972
+ return;
2973
+ }
2974
+ // A stream that had delivered died transiently (network, 5xx,
2975
+ // severed stream): the channel IS the live layer — reconnect with
2976
+ // backoff, latest value keeps serving meanwhile. (A live()-declared
2977
+ // producer never reaches here; its transport retries.)
2978
+ setStatus("reconnecting");
2979
+ let timer, wakeUp;
2980
+ await new Promise(resolve => {
2981
+ wakeUp = () => resolve();
2982
+ timer = setTimeout(wakeUp, Math.min(500 * 2 ** attempts++, 10000));
2983
+ // connectivity returning wakes the sleep early
2984
+ if (typeof addEventListener === "function") addEventListener("online", wakeUp, {
2985
+ once: true
2986
+ });
2987
+ });
2988
+ clearTimeout(timer);
2989
+ // a timer-woken sleep leaves its once-listener behind otherwise —
2990
+ // one leaked closure per backoff cycle until an online event fires
2991
+ if (typeof removeEventListener === "function") removeEventListener("online", wakeUp);
2992
+ if (gen !== ch.gen || ch.done) return;
2993
+ }
2994
+ }
2995
+ }
2996
+ function teardown(ch) {
2997
+ // Deferred so a same-key resubscribe in the same tick (a memo re-running
2998
+ // its compute) reuses the connection instead of thrashing it.
2999
+ queueMicrotask(() => {
3000
+ if (ch.count > 0 || ch.map.get(ch.key) !== ch) return;
3001
+ const ended = ch.done; // completed/failed on its own — "closed" persists
3002
+ ch.done = true;
3003
+ ch.close();
3004
+ wake(ch);
3005
+ if (ch.retain) return; // request-scoped: latest stays replayable
3006
+ ch.map.delete(ch.key);
3007
+ if (ch.report && !ended) statusSignal(ch.key)[1]("idle");
3008
+ });
3009
+ }
3010
+ function openChannel(map, key, call, report, retain) {
3011
+ let ch = map.get(key);
3012
+ if (!ch) {
3013
+ ch = {
3014
+ key,
3015
+ map,
3016
+ count: 0,
3017
+ version: 0,
3018
+ latest: undefined,
3019
+ error: undefined,
3020
+ done: false,
3021
+ waiters: new Set(),
3022
+ gen: 0,
3023
+ close: () => {},
3024
+ call,
3025
+ report,
3026
+ retain
3027
+ };
3028
+ map.set(key, ch);
3029
+ connect(ch);
3030
+ }
3031
+ return ch;
3032
+ }
3033
+
3034
+ // The consumer half: a branded iterable whose iterators subscribe to the
3035
+ // channel lazily (first pull connects — a hydration trace or speculative
3036
+ // call opens nothing), replay the latest value immediately, then follow
3037
+ // with latest-wins delivery.
3038
+ function subscriberIterable(open) {
3039
+ return {
3040
+ [LIVE_SOURCE]: true,
3041
+ [Symbol.asyncIterator]() {
3042
+ const ch = open();
3043
+ ch.count++;
3044
+ let seen = 0;
3045
+ let released = false;
3046
+ const release = () => {
3047
+ if (released) return;
3048
+ released = true;
3049
+ if (--ch.count === 0) teardown(ch);
3050
+ };
3051
+ const pull = async () => {
3052
+ while (true) {
3053
+ if (ch.error && seen >= ch.version) {
3054
+ release();
3055
+ throw ch.error;
3056
+ }
3057
+ if (ch.version > seen) {
3058
+ seen = ch.version;
3059
+ return {
3060
+ done: false,
3061
+ value: ch.latest
3062
+ };
3063
+ }
3064
+ if (ch.done) {
3065
+ release();
3066
+ return {
3067
+ done: true,
3068
+ value: undefined
3069
+ };
3070
+ }
3071
+ await new Promise(resolve => ch.waiters.add(resolve));
3072
+ }
3073
+ };
3074
+ return {
3075
+ next: () => released ? Promise.resolve({
3076
+ done: true,
3077
+ value: undefined
3078
+ }) : pull(),
3079
+ return(value) {
3080
+ release();
3081
+ return Promise.resolve({
3082
+ done: true,
3083
+ value
3084
+ });
3085
+ }
3086
+ };
3087
+ }
3088
+ };
3089
+ }
3090
+ function reconnectChannel(ch) {
3091
+ ch.gen++;
3092
+ ch.close();
3093
+ ch.error = undefined;
3094
+ ch.done = false;
3095
+ connect(ch);
3096
+ }
3097
+ function push(ch, value) {
3098
+ ch.latest = value;
3099
+ ch.version++;
3100
+ wake(ch);
3101
+ }
3102
+
3103
+ // Live channels participate in both data protocols. Registered on first
3104
+ // liveQuery() call, not at module scope: a side-effectful top level would
3105
+ // anchor this module into graphs that never use it.
3106
+ //
3107
+ // - Explicit revalidate(key) (force) reconnects — the producer re-yields
3108
+ // current state on invocation by contract.
3109
+ // - The post-mutation sweep (force=false) does NOT reconnect: the
3110
+ // connection is alive and is itself the freshness mechanism — a
3111
+ // push-driven producer yields the mutated state on its own, and tearing
3112
+ // down healthy connections on every mutation defeats the model.
3113
+ // - Single-flight payloads deliver INTO open channels, exactly as they seed
3114
+ // the query cache: the mutation response is the round trip, the channel
3115
+ // adopts the value immediately, and the live stream stays authoritative
3116
+ // for everything after.
3117
+ let hooked = false;
3118
+ function hookRevalidate() {
3119
+ if (hooked) return;
3120
+ hooked = true;
3121
+ registerRevalidateHook((keys, force) => {
3122
+ if (!force) return;
3123
+ for (const [k, ch] of channelMap) {
3124
+ if (keys === undefined || matchKey(k, keys)) reconnectChannel(ch);
3125
+ }
3126
+ });
3127
+ registerFlightDataHook(data => {
3128
+ for (const k in data) {
3129
+ const ch = channelMap.get(k);
3130
+ if (!ch || ch.done) continue;
3131
+ const v = data[k];
3132
+ if (v !== null && typeof v === "object" && v[Symbol.asyncIterator]) {
3133
+ // a live key collected server-side arrives stream-shaped: adopt
3134
+ // its yields for as long as it runs (it ends with the response
3135
+ // body); the channel's own connection remains authoritative
3136
+ (async () => {
3137
+ try {
3138
+ for await (const value of v) {
3139
+ if (ch.done) return;
3140
+ push(ch, value);
3141
+ }
3142
+ } catch {}
3143
+ })();
3144
+ } else push(ch, v);
3145
+ }
3146
+ });
3147
+ }
3148
+ /**
3149
+ * Declares a keyed live query over a value-shaped stream: an async-iterable
3150
+ * producer whose yields are successive VALUES of one logical query, with the
3151
+ * contract that the producer re-yields current state on every invocation.
3152
+ * liveQuery IS the live layer — no separate declaration needed:
3153
+ *
3154
+ * - One connection per (name + args) key, shared by every consumer. Late
3155
+ * subscribers receive the latest value immediately, delivery is
3156
+ * latest-wins, and the connection closes when the last consumer leaves.
3157
+ * - A connected stream that dies transiently (network, 5xx) reconnects with
3158
+ * exponential backoff — the latest value keeps serving meanwhile. A
3159
+ * definite rejection (4xx: the transport stamps HTTP statuses onto
3160
+ * failures) ends the channel and surfaces the error to consumers, as does
3161
+ * a first-connect failure.
3162
+ * - Server functions are declared GET at creation (like `query`). Live
3163
+ * queries participate in single-flight on the delivery side: a mutation's
3164
+ * flight payload pushes straight into open channels (the mutation
3165
+ * response is the round trip), while the post-mutation sweep leaves the
3166
+ * healthy connection in place — the live stream stays authoritative.
3167
+ * - Calling under preload intent warms the channel (a temporary hold keeps
3168
+ * it open through the preload window), so navigation renders against an
3169
+ * already-connected stream instead of holding the transition on connect
3170
+ * plus first yield. Outside preload, calling remains free — only a real
3171
+ * pull connects.
3172
+ * - SSR: the document face renders the first value; hydration adopts it and
3173
+ * reconnects. Channels are request-scoped on the server, so every
3174
+ * consumer of a key in one render observes the same value. Explicit
3175
+ * `revalidate(key)` reconnects (the producer re-yields current state on
3176
+ * invocation), and `status(...args)` is a reactive read of the channel's
3177
+ * wire state.
3178
+ *
3179
+ * ```ts
3180
+ * const roomMessages = liveQuery(getMessages, "messages");
3181
+ * // in a component — one connection, however many consumers:
3182
+ * const messages = createMemo(() => roomMessages(props.room));
3183
+ * ```
3184
+ */
3185
+ function liveQuery(fn, name) {
3186
+ hookRevalidate();
3187
+ // liveQuery implies GET, exactly as query does: reads belong on the GET
3188
+ // transport (cacheable URLs, no single-flight enveloping). An explicit
3189
+ // GET(fn) or live(GET(fn)) declaration passes through untouched.
3190
+ if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
3191
+ const rpc = getServerFunctionRPC();
3192
+ if (rpc) fn = rpc.GET(fn);
3193
+ }
3194
+ const liveFn = (...args) => {
3195
+ const key = name + hashKey(args);
3196
+ if (isServer) {
3197
+ const e = getRequestEvent();
3198
+ if (!e) {
3199
+ // No request scope (static render, tests): hand the source through
3200
+ // whole, branded, so the signals layer applies live SSR policy
3201
+ // (first value, then client takeover).
3202
+ const result = fn(...args);
3203
+ const brand = r => {
3204
+ if (r !== null && typeof r === "object" && r[Symbol.asyncIterator]) r[LIVE_SOURCE] = true;
3205
+ return r;
3206
+ };
3207
+ return result !== null && typeof result === "object" && typeof result.then === "function" ? result.then(brand) : brand(result);
3208
+ }
3209
+ // Request-scoped channels: two consumers of one key during one render
3210
+ // must observe the SAME first value (query's request cache gives its
3211
+ // reads the same guarantee). Channels live in the event, retained
3212
+ // past teardown so a later consumer replays the settled value instead
3213
+ // of reinvoking; the producer still closes with its last consumer.
3214
+ const router = e.router || (e.router = {});
3215
+ const channels = router.liveChannels || (router.liveChannels = new Map());
3216
+ return subscriberIterable(() => openChannel(channels, key, () => fn(...args), false, true));
3217
+ }
3218
+ const open = () => openChannel(channelMap, key, () => fn(...args), true, false);
3219
+ // Preload warms the channel, exactly as calling a query in a preload
3220
+ // function warms its cache: connect now, hold a temporary consumer slot
3221
+ // so refcounting doesn't close it before navigation renders. When the
3222
+ // real consumer arrives it inherits the live connection (first pull
3223
+ // replays the already-arrived value — the transition doesn't hold);
3224
+ // when navigation never comes, the hold lapses and teardown closes it.
3225
+ if (getIntent() === "preload") {
3226
+ const ch = open();
3227
+ ch.count++;
3228
+ setTimeout(() => {
3229
+ if (--ch.count === 0) teardown(ch);
3230
+ }, PRELOAD_TIMEOUT);
3231
+ }
3232
+ return subscriberIterable(open);
3233
+ };
3234
+ liveFn.keyFor = (...args) => name + hashKey(args);
3235
+ liveFn.key = name;
3236
+ liveFn.status = (...args) => statusSignal(name + hashKey(args))[0]();
3237
+ return liveFn;
3238
+ }
3239
+
2785
3240
  // The delegation fallback's lazy entry (see data/events.ts). Nothing imports
2786
3241
  // this module statically — it exists so the dynamic import has a target that
2787
3242
  // bundlers can keep as a split point: router-only apps never load the action
@@ -2792,4 +3247,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
2792
3247
  submitServerForm: submitServerForm
2793
3248
  });
2794
3249
 
2795
- export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, memoryHistory, query, revalidate, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
3250
+ export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };