@pylonsync/react 0.4.9 → 0.4.11

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/Link.d.ts CHANGED
@@ -2,7 +2,10 @@ import React from "react";
2
2
  declare global {
3
3
  interface Window {
4
4
  __pylon?: {
5
- prefetch: (href: string) => Promise<void>;
5
+ prefetch: (href: string, opts?: {
6
+ /** Also fetch the page payload (costs a server render — hover only). */
7
+ document?: boolean;
8
+ }) => Promise<void>;
6
9
  navigate: (href: string, opts?: {
7
10
  push?: boolean;
8
11
  replace?: boolean;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.9",
6
+ "version": "0.4.11",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "prepack": "bun run build"
15
15
  },
16
16
  "dependencies": {
17
- "@pylonsync/sdk": "0.4.9",
18
- "@pylonsync/sync": "0.4.9"
17
+ "@pylonsync/sdk": "0.4.11",
18
+ "@pylonsync/sync": "0.4.11"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "react": ">=19.0.0"
package/src/Link.tsx CHANGED
@@ -8,12 +8,12 @@
8
8
  // route entry, and re-renders the React root in place. Layouts
9
9
  // that match across the two routes survive reconciliation, so
10
10
  // their state persists.
11
- // - Prefetches on viewport entry (IntersectionObserver) by default.
12
- // The runtime injects `<link rel=prefetch>` for the HTML and
13
- // `<link rel=modulepreload>` for the shared chunks the user
14
- // will need.
15
- // - Hover/touch escalation: if the link isn't in the viewport
16
- // long enough, hovering still triggers prefetch.
11
+ // - Prefetches in two stages, because the two halves cost very
12
+ // different things. On viewport entry (IntersectionObserver) the
13
+ // runtime modulepreloads the destination's client chunks — static,
14
+ // immutable, cacheable. On hover/touch it also fetches the page
15
+ // itself, which costs a server render, so that waits for a real
16
+ // signal of intent rather than firing for a screenful of links.
17
17
  // - Modifier keys (cmd/ctrl/shift/alt) or middle-click fall
18
18
  // through to the browser's default behavior (open in new tab,
19
19
  // etc.) — matches Next.js semantics.
@@ -29,7 +29,13 @@ import React, { useEffect, useRef } from "react";
29
29
  declare global {
30
30
  interface Window {
31
31
  __pylon?: {
32
- prefetch: (href: string) => Promise<void>;
32
+ prefetch: (
33
+ href: string,
34
+ opts?: {
35
+ /** Also fetch the page payload (costs a server render — hover only). */
36
+ document?: boolean;
37
+ },
38
+ ) => Promise<void>;
33
39
  navigate: (
34
40
  href: string,
35
41
  opts?: { push?: boolean; replace?: boolean; seed?: unknown },
@@ -115,12 +121,19 @@ export function Link({
115
121
  }
116
122
 
117
123
  const el = ref.current;
118
- let prefetched = false;
119
- const doPrefetch = () => {
120
- if (prefetched) return;
121
- prefetched = true;
124
+ // Chunks warm once, on sight. The page payload warms on intent — and may
125
+ // re-warm on a later hover, since the runtime expires payloads that have
126
+ // gone stale.
127
+ let warmedChunks = false;
128
+ const warmChunks = () => {
129
+ if (warmedChunks) return;
130
+ warmedChunks = true;
122
131
  window.__pylon?.prefetch(href);
123
132
  };
133
+ const warmPage = () => {
134
+ warmedChunks = true;
135
+ window.__pylon?.prefetch(href, { document: true });
136
+ };
124
137
 
125
138
  let io: IntersectionObserver | null = null;
126
139
  if (typeof IntersectionObserver !== "undefined") {
@@ -128,7 +141,7 @@ export function Link({
128
141
  (entries) => {
129
142
  for (const e of entries) {
130
143
  if (e.isIntersecting) {
131
- doPrefetch();
144
+ warmChunks();
132
145
  io?.disconnect();
133
146
  }
134
147
  }
@@ -138,14 +151,13 @@ export function Link({
138
151
  io.observe(el);
139
152
  }
140
153
 
141
- const onHover = () => doPrefetch();
142
- el.addEventListener("mouseenter", onHover, { once: true });
143
- el.addEventListener("touchstart", onHover, { once: true, passive: true });
154
+ el.addEventListener("mouseenter", warmPage);
155
+ el.addEventListener("touchstart", warmPage, { passive: true });
144
156
 
145
157
  return () => {
146
158
  io?.disconnect();
147
- el.removeEventListener("mouseenter", onHover);
148
- el.removeEventListener("touchstart", onHover);
159
+ el.removeEventListener("mouseenter", warmPage);
160
+ el.removeEventListener("touchstart", warmPage);
149
161
  };
150
162
  }, [href, prefetch]);
151
163