@solidjs/router 2.0.0-next.17 → 2.0.0-next.19

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 { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, 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
- import { subscribeFlightData, decodeResponsePayload, createServerReference } from '@solidjs/web/server-functions';
3
+ import { subscribeFlightData, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
4
4
  import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
5
5
 
6
6
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
@@ -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);
@@ -862,7 +869,8 @@ function resolveLazySubtree(record) {
862
869
  return record.promise ||= Promise.resolve(record.thunk()).then(m => {
863
870
  const routes = Array.isArray(m) ? m : m.default || m.routes || [];
864
871
  record.resolved = routes;
865
- setLazyTreeVersion(v => v + 1);
872
+ lazyTreeCounter++;
873
+ isServer || setLazyTreeVersion(lazyTreeCounter);
866
874
  return record.resolved;
867
875
  }, e => {
868
876
  // ?? Error(): a held undefined would read as "no failure" and refire
@@ -1241,8 +1249,18 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1241
1249
  }
1242
1250
  return;
1243
1251
  }
1244
- // typed path proxy nodes coerce to their href
1245
- 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);
1246
1264
  const {
1247
1265
  replace,
1248
1266
  resolve,
@@ -1556,18 +1574,26 @@ function Routes(props) {
1556
1574
  return memo(outlet);
1557
1575
  }
1558
1576
  const createOutlet = child => {
1559
- return () => {
1560
- const c = child();
1561
- if (c) {
1562
- return createComponent$1(RouteContextObj, {
1563
- value: c,
1564
- get children() {
1565
- return c.outlet();
1566
- }
1567
- });
1568
- }
1569
- return undefined;
1570
- };
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
+ });
1571
1597
  };
1572
1598
 
1573
1599
  // for data only mode with single flight mutations
@@ -2187,7 +2213,7 @@ const useBeforeLeave = listener => {
2187
2213
  };
2188
2214
 
2189
2215
  const LocationHeader = "Location";
2190
- const PRELOAD_TIMEOUT = 5000;
2216
+ const PRELOAD_TIMEOUT$1 = 5000;
2191
2217
  const CACHE_TIMEOUT = 180000;
2192
2218
  // When this client booted. Flight-registry entries (sharedConfig.has/load)
2193
2219
  // hold values the server computed while rendering THIS page, so their age is
@@ -2213,6 +2239,23 @@ function getCache() {
2213
2239
  return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
2214
2240
  }
2215
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
+
2216
2259
  /**
2217
2260
  * Revalidates the given cache entry/entries.
2218
2261
  */
@@ -2222,6 +2265,8 @@ function revalidate(key, force = true) {
2222
2265
  force && (entry[0] = 0); //force cache miss
2223
2266
  entry[4][1](now); // retrigger live signals
2224
2267
  });
2268
+ const keys = key === undefined ? undefined : Array.isArray(key) ? key : [key];
2269
+ for (const hook of revalidateHooks) hook(keys, force);
2225
2270
  }
2226
2271
  function cacheKeyOp(key, fn) {
2227
2272
  key && !Array.isArray(key) && (key = [key]);
@@ -2275,7 +2320,7 @@ function query(fn, name) {
2275
2320
  tracking = true;
2276
2321
  onCleanup(() => cached[4].count--);
2277
2322
  }
2278
- 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)) {
2279
2324
  if (tracking) {
2280
2325
  cached[4].count++;
2281
2326
  cached[4][0](); // track
@@ -2307,7 +2352,7 @@ function query(fn, name) {
2307
2352
  // refetches instead of serving a minutes-old value on a fresh navigation.
2308
2353
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
2309
2354
  const payloadAge = now - bootTime;
2310
- if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT)) {
2355
+ if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1)) {
2311
2356
  adopted = true;
2312
2357
  res = sharedConfig.load(key); // hydrating
2313
2358
  // @ts-ignore at least until we add a delete method to sharedConfig
@@ -2500,8 +2545,9 @@ function handleFormAction(evt, router, actionBase) {
2500
2545
  if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
2501
2546
  // A registry miss on a server-action url is a direct bind whose module
2502
2547
  // never loaded client-side (server components): the url is self-describing
2503
- // (`?id`, bound `?args`), so a generic invocation is synthesized from it —
2504
- // delegation alone is sufficient, the no-JS path stays a no-JS fallback.
2548
+ // (the id in the path, bound `?args` in the query), so a generic invocation
2549
+ // is synthesized from it — delegation alone is sufficient, the no-JS path
2550
+ // stays a no-JS fallback.
2505
2551
  // Client-only actions (`https://action/`) are their module's JS by
2506
2552
  // definition, so a miss there falls through to native submission.
2507
2553
  const handler = actions.get(actionRef) || serverAction && createServerFormAction(actionRef);
@@ -2517,16 +2563,17 @@ function handleFormAction(evt, router, actionBase) {
2517
2563
 
2518
2564
  /**
2519
2565
  * Synthesizes a router action for a server-rendered action url. The url
2520
- * carries everything an invocation needs — the function id and any bound
2521
- * `.with()` arguments (plain JSON in `?args`, which the server prepends for
2522
- * natural-encoding bodies exactly as it does for no-JS posts) — so the
2523
- * FormData is posted to it verbatim through the server-function transport:
2566
+ * carries everything an invocation needs — the function id in the path
2567
+ * (`<endpoint>/<id>`) and any bound `.with()` arguments (plain JSON in
2568
+ * `?args`, which the server prepends for natural-encoding bodies exactly as
2569
+ * it does for no-JS posts) — so the FormData is posted to it verbatim
2570
+ * through the server-function transport:
2524
2571
  * submissions, `aria-busy`, redirects, revalidation, and single-flight all
2525
2572
  * flow through the normal action machinery. Registered under the url, so
2526
2573
  * repeat submits reuse it (and a later real registration overrides it).
2527
2574
  */
2528
2575
  function createServerFormAction(url) {
2529
- const id = new URL(url, mockBase).searchParams.get("id");
2576
+ const id = parseServerFunctionUrl(url);
2530
2577
  if (!id) return undefined;
2531
2578
  // typecheck resolves the server half of the dual module; this path only
2532
2579
  // runs in the browser, where the client transport's signature applies
@@ -2546,8 +2593,9 @@ function createServerFormAction(url) {
2546
2593
  */
2547
2594
  function submitServerForm(router, url, form, data) {
2548
2595
  const handler = actions.get(url) || createServerFormAction(url);
2549
- // no `?id` — not the server function convention; nothing can run it,
2550
- // resubmit natively (submit() bypasses the delegated handler)
2596
+ // not an address (`<endpoint>/<id>`) — not the server function convention;
2597
+ // nothing can run it, resubmit natively (submit() bypasses the delegated
2598
+ // handler)
2551
2599
  if (!handler) return form.submit();
2552
2600
  handler.call({
2553
2601
  r: router,
@@ -2789,8 +2837,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
2789
2837
  }
2790
2838
  // invalidate
2791
2839
  cacheKeyOp(keys, entry => entry[0] = 0);
2792
- // set cache
2840
+ // set cache — and fan the payload out to other keyed stores (live query
2841
+ // channels adopt delivered values instead of reconnecting)
2793
2842
  flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
2843
+ flightData && deliverFlightData(flightData);
2794
2844
  // trigger revalidation inside the same transition as the navigation, so
2795
2845
  // the redirect commits atomically with fresh data: surviving consumers
2796
2846
  // (shared layouts) the flight payload didn't seed refetch and hold the
@@ -2830,6 +2880,366 @@ async function handleResponse(response, error, navigate, metadataHandled) {
2830
2880
  } : undefined;
2831
2881
  }
2832
2882
 
2883
+ // Matches query's preload lifetime: a channel warmed by preload intent
2884
+ // stays held this long waiting for its navigation.
2885
+ const PRELOAD_TIMEOUT = 5000;
2886
+
2887
+ // The live-source brand (registered symbol — no transport import needed):
2888
+ // computations meeting a branded iterable apply live SSR policy (document
2889
+ // face takes the first value; hydration re-runs the compute after adoption
2890
+ // to reconnect). liveQuery writes it on both faces itself — the server-side
2891
+ // resolved iterable and the client-side multicast iterable — so a producer
2892
+ // needs no separate live() declaration to be live here.
2893
+ const LIVE_SOURCE = Symbol.for("solid.LiveSource");
2894
+ const channelMap = new Map();
2895
+ // Status outlives the channel (an "idle"/"closed" read after teardown is
2896
+ // meaningful), so signals live in their own keyed map.
2897
+ const statusMap = new Map();
2898
+ function statusSignal(key) {
2899
+ let s = statusMap.get(key);
2900
+ // ownedWrite: the first write happens synchronously inside whatever
2901
+ // computation pulled the channel open — this is channel-owned state, not
2902
+ // the computation's own.
2903
+ if (!s) statusMap.set(key, s = createSignal("idle", {
2904
+ ownedWrite: true
2905
+ }));
2906
+ return s;
2907
+ }
2908
+ function wake(ch) {
2909
+ const waiters = [...ch.waiters];
2910
+ ch.waiters.clear();
2911
+ for (const w of waiters) w();
2912
+ }
2913
+ const noStatus = () => {};
2914
+ async function connect(ch) {
2915
+ const gen = ch.gen;
2916
+ const setStatus = ch.report ? statusSignal(ch.key)[1] : noStatus;
2917
+ let attempts = 0;
2918
+ setStatus(ch.version ? "reconnecting" : "connecting");
2919
+ while (true) {
2920
+ try {
2921
+ const result = await ch.call();
2922
+ if (gen !== ch.gen || ch.done) {
2923
+ // superseded (reconnect) or torn down while connecting: the arrived
2924
+ // stream must still be ended or its request leaks
2925
+ result?.[Symbol.asyncIterator] && result[Symbol.asyncIterator]().return?.();
2926
+ return;
2927
+ }
2928
+ // A live()-declared producer's own retry loop erases deaths from the
2929
+ // value stream and reports them through onstatus — forward those into
2930
+ // the channel's status so both producer kinds read the same. Its
2931
+ // "closed" is redundant with our own done handling below.
2932
+ if (result !== null && typeof result === "object") {
2933
+ try {
2934
+ result.onstatus = state => {
2935
+ if (gen === ch.gen && state !== "closed") setStatus(state);
2936
+ };
2937
+ } catch {}
2938
+ }
2939
+ const it = result !== null && typeof result === "object" && result[Symbol.asyncIterator] ? result[Symbol.asyncIterator]() : async function* () {
2940
+ yield result;
2941
+ }();
2942
+ ch.close = () => {
2943
+ try {
2944
+ const r = it.return && it.return();
2945
+ if (r && typeof r.then === "function") r.then(undefined, () => {});
2946
+ } catch {}
2947
+ };
2948
+ while (true) {
2949
+ const r = await it.next();
2950
+ if (gen !== ch.gen || ch.done) return;
2951
+ if (r.done) break;
2952
+ ch.latest = r.value;
2953
+ ch.version++;
2954
+ attempts = 0; // healthy value: backoff resets
2955
+ setStatus("connected");
2956
+ wake(ch);
2957
+ }
2958
+ ch.done = true;
2959
+ setStatus("closed");
2960
+ wake(ch);
2961
+ return;
2962
+ } catch (error) {
2963
+ if (gen !== ch.gen || ch.done) return;
2964
+ const status = error?.status;
2965
+ if (!ch.version || typeof status === "number" && status >= 400 && status < 500) {
2966
+ // Terminal: never connected (fail like a normal call), or a
2967
+ // definite rejection — the transport stamps HTTP statuses onto
2968
+ // failures, and a 4xx means the server understood and refused;
2969
+ // retrying cannot change the answer. Consumers rethrow (after
2970
+ // draining the latest value), surfacing to error boundaries.
2971
+ ch.error = error;
2972
+ ch.done = true;
2973
+ setStatus("closed");
2974
+ wake(ch);
2975
+ return;
2976
+ }
2977
+ // A stream that had delivered died transiently (network, 5xx,
2978
+ // severed stream): the channel IS the live layer — reconnect with
2979
+ // backoff, latest value keeps serving meanwhile. (A live()-declared
2980
+ // producer never reaches here; its transport retries.)
2981
+ setStatus("reconnecting");
2982
+ let timer, wakeUp;
2983
+ await new Promise(resolve => {
2984
+ wakeUp = () => resolve();
2985
+ timer = setTimeout(wakeUp, Math.min(500 * 2 ** attempts++, 10000));
2986
+ // connectivity returning wakes the sleep early
2987
+ if (typeof addEventListener === "function") addEventListener("online", wakeUp, {
2988
+ once: true
2989
+ });
2990
+ });
2991
+ clearTimeout(timer);
2992
+ // a timer-woken sleep leaves its once-listener behind otherwise —
2993
+ // one leaked closure per backoff cycle until an online event fires
2994
+ if (typeof removeEventListener === "function") removeEventListener("online", wakeUp);
2995
+ if (gen !== ch.gen || ch.done) return;
2996
+ }
2997
+ }
2998
+ }
2999
+ function teardown(ch) {
3000
+ // Deferred so a same-key resubscribe in the same tick (a memo re-running
3001
+ // its compute) reuses the connection instead of thrashing it.
3002
+ queueMicrotask(() => {
3003
+ if (ch.count > 0 || ch.map.get(ch.key) !== ch) return;
3004
+ const ended = ch.done; // completed/failed on its own — "closed" persists
3005
+ ch.done = true;
3006
+ ch.close();
3007
+ wake(ch);
3008
+ if (ch.retain) return; // request-scoped: latest stays replayable
3009
+ ch.map.delete(ch.key);
3010
+ if (ch.report && !ended) statusSignal(ch.key)[1]("idle");
3011
+ });
3012
+ }
3013
+ function openChannel(map, key, call, report, retain) {
3014
+ let ch = map.get(key);
3015
+ if (!ch) {
3016
+ ch = {
3017
+ key,
3018
+ map,
3019
+ count: 0,
3020
+ version: 0,
3021
+ latest: undefined,
3022
+ error: undefined,
3023
+ done: false,
3024
+ waiters: new Set(),
3025
+ gen: 0,
3026
+ close: () => {},
3027
+ call,
3028
+ report,
3029
+ retain
3030
+ };
3031
+ map.set(key, ch);
3032
+ connect(ch);
3033
+ }
3034
+ return ch;
3035
+ }
3036
+
3037
+ // The consumer half: a branded iterable whose iterators subscribe to the
3038
+ // channel lazily (first pull connects — a hydration trace or speculative
3039
+ // call opens nothing), replay the latest value immediately, then follow
3040
+ // with latest-wins delivery.
3041
+ function subscriberIterable(open) {
3042
+ return {
3043
+ [LIVE_SOURCE]: true,
3044
+ [Symbol.asyncIterator]() {
3045
+ const ch = open();
3046
+ ch.count++;
3047
+ let seen = 0;
3048
+ let released = false;
3049
+ const release = () => {
3050
+ if (released) return;
3051
+ released = true;
3052
+ if (--ch.count === 0) teardown(ch);
3053
+ };
3054
+ const pull = async () => {
3055
+ while (true) {
3056
+ if (ch.error && seen >= ch.version) {
3057
+ release();
3058
+ throw ch.error;
3059
+ }
3060
+ if (ch.version > seen) {
3061
+ seen = ch.version;
3062
+ return {
3063
+ done: false,
3064
+ value: ch.latest
3065
+ };
3066
+ }
3067
+ if (ch.done) {
3068
+ release();
3069
+ return {
3070
+ done: true,
3071
+ value: undefined
3072
+ };
3073
+ }
3074
+ await new Promise(resolve => ch.waiters.add(resolve));
3075
+ }
3076
+ };
3077
+ return {
3078
+ next: () => released ? Promise.resolve({
3079
+ done: true,
3080
+ value: undefined
3081
+ }) : pull(),
3082
+ return(value) {
3083
+ release();
3084
+ return Promise.resolve({
3085
+ done: true,
3086
+ value
3087
+ });
3088
+ }
3089
+ };
3090
+ }
3091
+ };
3092
+ }
3093
+ function reconnectChannel(ch) {
3094
+ ch.gen++;
3095
+ ch.close();
3096
+ ch.error = undefined;
3097
+ ch.done = false;
3098
+ connect(ch);
3099
+ }
3100
+ function push(ch, value) {
3101
+ ch.latest = value;
3102
+ ch.version++;
3103
+ wake(ch);
3104
+ }
3105
+
3106
+ // Live channels participate in both data protocols. Registered on first
3107
+ // liveQuery() call, not at module scope: a side-effectful top level would
3108
+ // anchor this module into graphs that never use it.
3109
+ //
3110
+ // - Explicit revalidate(key) (force) reconnects — the producer re-yields
3111
+ // current state on invocation by contract.
3112
+ // - The post-mutation sweep (force=false) does NOT reconnect: the
3113
+ // connection is alive and is itself the freshness mechanism — a
3114
+ // push-driven producer yields the mutated state on its own, and tearing
3115
+ // down healthy connections on every mutation defeats the model.
3116
+ // - Single-flight payloads deliver INTO open channels, exactly as they seed
3117
+ // the query cache: the mutation response is the round trip, the channel
3118
+ // adopts the value immediately, and the live stream stays authoritative
3119
+ // for everything after.
3120
+ let hooked = false;
3121
+ function hookRevalidate() {
3122
+ if (hooked) return;
3123
+ hooked = true;
3124
+ registerRevalidateHook((keys, force) => {
3125
+ if (!force) return;
3126
+ for (const [k, ch] of channelMap) {
3127
+ if (keys === undefined || matchKey(k, keys)) reconnectChannel(ch);
3128
+ }
3129
+ });
3130
+ registerFlightDataHook(data => {
3131
+ for (const k in data) {
3132
+ const ch = channelMap.get(k);
3133
+ if (!ch || ch.done) continue;
3134
+ const v = data[k];
3135
+ if (v !== null && typeof v === "object" && v[Symbol.asyncIterator]) {
3136
+ // a live key collected server-side arrives stream-shaped: adopt
3137
+ // its yields for as long as it runs (it ends with the response
3138
+ // body); the channel's own connection remains authoritative
3139
+ (async () => {
3140
+ try {
3141
+ for await (const value of v) {
3142
+ if (ch.done) return;
3143
+ push(ch, value);
3144
+ }
3145
+ } catch {}
3146
+ })();
3147
+ } else push(ch, v);
3148
+ }
3149
+ });
3150
+ }
3151
+ /**
3152
+ * Declares a keyed live query over a value-shaped stream: an async-iterable
3153
+ * producer whose yields are successive VALUES of one logical query, with the
3154
+ * contract that the producer re-yields current state on every invocation.
3155
+ * liveQuery IS the live layer — no separate declaration needed:
3156
+ *
3157
+ * - One connection per (name + args) key, shared by every consumer. Late
3158
+ * subscribers receive the latest value immediately, delivery is
3159
+ * latest-wins, and the connection closes when the last consumer leaves.
3160
+ * - A connected stream that dies transiently (network, 5xx) reconnects with
3161
+ * exponential backoff — the latest value keeps serving meanwhile. A
3162
+ * definite rejection (4xx: the transport stamps HTTP statuses onto
3163
+ * failures) ends the channel and surfaces the error to consumers, as does
3164
+ * a first-connect failure.
3165
+ * - Server functions are declared GET at creation (like `query`). Live
3166
+ * queries participate in single-flight on the delivery side: a mutation's
3167
+ * flight payload pushes straight into open channels (the mutation
3168
+ * response is the round trip), while the post-mutation sweep leaves the
3169
+ * healthy connection in place — the live stream stays authoritative.
3170
+ * - Calling under preload intent warms the channel (a temporary hold keeps
3171
+ * it open through the preload window), so navigation renders against an
3172
+ * already-connected stream instead of holding the transition on connect
3173
+ * plus first yield. Outside preload, calling remains free — only a real
3174
+ * pull connects.
3175
+ * - SSR: the document face renders the first value; hydration adopts it and
3176
+ * reconnects. Channels are request-scoped on the server, so every
3177
+ * consumer of a key in one render observes the same value. Explicit
3178
+ * `revalidate(key)` reconnects (the producer re-yields current state on
3179
+ * invocation), and `status(...args)` is a reactive read of the channel's
3180
+ * wire state.
3181
+ *
3182
+ * ```ts
3183
+ * const roomMessages = liveQuery(getMessages, "messages");
3184
+ * // in a component — one connection, however many consumers:
3185
+ * const messages = createMemo(() => roomMessages(props.room));
3186
+ * ```
3187
+ */
3188
+ function liveQuery(fn, name) {
3189
+ hookRevalidate();
3190
+ // liveQuery implies GET, exactly as query does: reads belong on the GET
3191
+ // transport (cacheable URLs, no single-flight enveloping). An explicit
3192
+ // GET(fn) or live(GET(fn)) declaration passes through untouched.
3193
+ if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
3194
+ const rpc = getServerFunctionRPC();
3195
+ if (rpc) fn = rpc.GET(fn);
3196
+ }
3197
+ const liveFn = (...args) => {
3198
+ const key = name + hashKey(args);
3199
+ if (isServer) {
3200
+ const e = getRequestEvent();
3201
+ if (!e) {
3202
+ // No request scope (static render, tests): hand the source through
3203
+ // whole, branded, so the signals layer applies live SSR policy
3204
+ // (first value, then client takeover).
3205
+ const result = fn(...args);
3206
+ const brand = r => {
3207
+ if (r !== null && typeof r === "object" && r[Symbol.asyncIterator]) r[LIVE_SOURCE] = true;
3208
+ return r;
3209
+ };
3210
+ return result !== null && typeof result === "object" && typeof result.then === "function" ? result.then(brand) : brand(result);
3211
+ }
3212
+ // Request-scoped channels: two consumers of one key during one render
3213
+ // must observe the SAME first value (query's request cache gives its
3214
+ // reads the same guarantee). Channels live in the event, retained
3215
+ // past teardown so a later consumer replays the settled value instead
3216
+ // of reinvoking; the producer still closes with its last consumer.
3217
+ const router = e.router || (e.router = {});
3218
+ const channels = router.liveChannels || (router.liveChannels = new Map());
3219
+ return subscriberIterable(() => openChannel(channels, key, () => fn(...args), false, true));
3220
+ }
3221
+ const open = () => openChannel(channelMap, key, () => fn(...args), true, false);
3222
+ // Preload warms the channel, exactly as calling a query in a preload
3223
+ // function warms its cache: connect now, hold a temporary consumer slot
3224
+ // so refcounting doesn't close it before navigation renders. When the
3225
+ // real consumer arrives it inherits the live connection (first pull
3226
+ // replays the already-arrived value — the transition doesn't hold);
3227
+ // when navigation never comes, the hold lapses and teardown closes it.
3228
+ if (getIntent() === "preload") {
3229
+ const ch = open();
3230
+ ch.count++;
3231
+ setTimeout(() => {
3232
+ if (--ch.count === 0) teardown(ch);
3233
+ }, PRELOAD_TIMEOUT);
3234
+ }
3235
+ return subscriberIterable(open);
3236
+ };
3237
+ liveFn.keyFor = (...args) => name + hashKey(args);
3238
+ liveFn.key = name;
3239
+ liveFn.status = (...args) => statusSignal(name + hashKey(args))[0]();
3240
+ return liveFn;
3241
+ }
3242
+
2833
3243
  // The delegation fallback's lazy entry (see data/events.ts). Nothing imports
2834
3244
  // this module statically — it exists so the dynamic import has a target that
2835
3245
  // bundlers can keep as a split point: router-only apps never load the action
@@ -2840,4 +3250,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
2840
3250
  submitServerForm: submitServerForm
2841
3251
  });
2842
3252
 
2843
- 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 };
3253
+ 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 };
package/dist/paths.d.ts CHANGED
@@ -108,6 +108,14 @@ type MultiPathContrib<Ps extends readonly string[], Def, PAcc extends Params> =
108
108
  export type RoutePaths<R extends readonly RouteDefinition[]> = number extends R["length"] ? any : PathEnd<DefaultSearchTypes, {}> & TuplePaths<R, {}>;
109
109
  /** Extracts the params record a paths node binds, as runtime (string-valued) params. */
110
110
  export type PathParamsOf<N> = N extends TypedPath<infer P> ? Flat<P> : Params;
111
+ /**
112
+ * The global Href brand (`@solidjs/web` reads the same registry symbol).
113
+ * On a paths node it holds the logical path — the routable pathname before
114
+ * `renderPath` decorates it for display (eg. hash mode's `#` prefix) — so
115
+ * `navigate()` and `redirect()` can route the node while `toString()` keeps
116
+ * yielding the display href for the DOM.
117
+ */
118
+ export declare const HREF: unique symbol;
111
119
  /**
112
120
  * Creates the runtime path proxy. It is instance-scoped: `renderPath` comes
113
121
  * from the router's history adapter (eg. hash routing prefixes `#`), and
package/dist/paths.js CHANGED
@@ -5,6 +5,14 @@ export const int = ((s) => /^-?\d+$/.test(s));
5
5
  // Runtime
6
6
  // ---------------------------------------------------------------------------
7
7
  const encodeParam = (value) => String(value).split("/").map(encodeURIComponent).join("/");
8
+ /**
9
+ * The global Href brand (`@solidjs/web` reads the same registry symbol).
10
+ * On a paths node it holds the logical path — the routable pathname before
11
+ * `renderPath` decorates it for display (eg. hash mode's `#` prefix) — so
12
+ * `navigate()` and `redirect()` can route the node while `toString()` keeps
13
+ * yielding the display href for the DOM.
14
+ */
15
+ export const HREF = Symbol.for("solid.Href");
8
16
  /**
9
17
  * Creates the runtime path proxy. It is instance-scoped: `renderPath` comes
10
18
  * from the router's history adapter (eg. hash routing prefixes `#`), and
@@ -32,7 +40,11 @@ export function createPathsProxy(renderPath = p => p, base = "") {
32
40
  if (prop === "toString")
33
41
  return () => toHref(pathname);
34
42
  if (typeof prop === "symbol")
35
- return prop === Symbol.toPrimitive ? () => toHref(pathname) : undefined;
43
+ return prop === Symbol.toPrimitive
44
+ ? () => toHref(pathname)
45
+ : prop === HREF
46
+ ? pathname || "/"
47
+ : undefined;
36
48
  return node(`${pathname}/${prop}`);
37
49
  }
38
50
  });