@preprio/toolkit 0.6.0-alpha.7 → 0.6.0-alpha.9

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.
Files changed (39) hide show
  1. package/README.md +5 -5
  2. package/dist/astro/{chunk-3EXK2LHI.js → chunk-UN4JADNU.js} +1 -1
  3. package/dist/astro/graphql.js +1 -1
  4. package/dist/astro/index.cjs +1 -1
  5. package/dist/astro/index.js +1 -1
  6. package/dist/{chunk-VQIPTUN4.js → chunk-5QIWVDP2.js} +1 -1
  7. package/dist/{chunk-U7TRFVK7.js → chunk-J66UOCK6.js} +1 -1
  8. package/dist/{chunk-3QGWCYTL.js → chunk-WQBKQG4R.js} +1 -1
  9. package/dist/{chunk-PFJO4TYL.js → chunk-XL4Y7FGW.js} +1 -1
  10. package/dist/graphql/codegen.js +3 -3
  11. package/dist/graphql.js +3 -3
  12. package/dist/index.cjs +1 -1
  13. package/dist/index.d.cts +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +3 -3
  16. package/dist/nextjs/{chunk-CZK7SMBD.js → chunk-CMV6ZP6E.js} +1 -1
  17. package/dist/nextjs/components.cjs +128 -67
  18. package/dist/nextjs/components.d.cts +14 -13
  19. package/dist/nextjs/components.d.ts +14 -13
  20. package/dist/nextjs/components.js +129 -68
  21. package/dist/nextjs/graphql.cjs +99 -27
  22. package/dist/nextjs/graphql.d.cts +5 -5
  23. package/dist/nextjs/graphql.d.ts +5 -5
  24. package/dist/nextjs/graphql.js +99 -27
  25. package/dist/nextjs/index.cjs +1 -1
  26. package/dist/nextjs/index.d.cts +9 -7
  27. package/dist/nextjs/index.d.ts +9 -7
  28. package/dist/nextjs/index.js +1 -1
  29. package/dist/nuxt/{chunk-JL33ENCM.js → chunk-XWOABVMW.js} +1 -1
  30. package/dist/nuxt/graphql.js +1 -1
  31. package/dist/nuxt/index.cjs +1 -1
  32. package/dist/nuxt/index.js +1 -1
  33. package/dist/react/components.cjs +1 -1
  34. package/dist/react/components.js +1 -1
  35. package/dist/sveltekit/{chunk-S7S276V7.js → chunk-3S5RA6G5.js} +1 -1
  36. package/dist/sveltekit/graphql.js +1 -1
  37. package/dist/sveltekit/index.cjs +1 -1
  38. package/dist/sveltekit/index.js +1 -1
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -384,14 +384,14 @@ A/B tests go through the same decision as segment groups: both variants are in t
384
384
 
385
385
  ##### How each visitor gets their variant
386
386
 
387
- Every page view is decided. Nothing is reused from a prefetch, the router cache or another page's view; the only reuse is showing a view again as the visitor just saw it (a reload, Back and Forward).
387
+ Every page view is decided. Nothing is reused from a prefetch, the router cache or another page's view; the browser only reuses a decision this tab already got for the same URL (see **Reuse** below).
388
388
 
389
389
  - **Page loads.** The page is the same for every visitor and is sent at once, from the cache. An inline script at the start of each `PreprStack` asks the proxy for this view's decision as soon as it is parsed, so Prepr answers while the rest of the page, its styles and scripts (the tracking pixel included) still load. The personalized sections show the visitor's variant when the answer arrives; everything else never waits for it.
390
- - **Client-side navigations.** When the visitor presses a link, `PreprPersonalizationSwitch` asks the proxy for the target page's decision, and the click waits for the answer. The new page then paints with the visitor's variants. Pressing, not clicking, starts the request, so most of the wait happens while the button is still going down. Prefetches still bring the page itself; only the decision is fetched per click.
390
+ - **Client-side navigations.** When the visitor hovers over a link (or presses it, on touch devices), `PreprPersonalizationSwitch` asks the proxy for the target page's decision. Clicks are never held: when the answer is in by the click, which hovering mostly ensures, the new page paints with the visitor's variants; otherwise its personalized sections show when the answer arrives. Prefetches still bring the page itself; only the decision is fetched.
391
391
  - **Never the default first.** Until a view's decision is in, its personalized sections are not shown: the default variant keeps its space, invisible, so the page does not move when the visitor's variant appears (only by any difference in height). The default variant only shows when it is the decision: for visitors in none of the segments, for requests `onlyIf` skips, and when Prepr does not answer within `timeoutMs` (default 3000). Navigations started from code ask for a decision when the page opens; the personalized sections appear when it arrives.
392
- - **Reloads and Back/Forward.** Back and Forward show a page as this tab last showed it, at once, without asking Prepr. A reload of the same URL within 10 seconds of the decision, with no other page view in between, reuses it too (kept in `sessionStorage`); after that, or after any other page view, the browser asks again. A URL with other UTM parameters is another view. Prepr needs a few seconds to process a visitor's activity anyway, so asking again sooner would mostly get the same answer.
392
+ - **Reuse.** Back and Forward show a page as this tab last showed it, at once, without asking Prepr. Any other view of a URL this tab got a decision for (kept in `sessionStorage`) shows it at once too: under 10 seconds old without asking again, since Prepr needs a few seconds to process a visitor's activity anyway; up to 30 minutes old while a new decision is asked for in the background. When that one differs, it changes only the sections still below the screen; a section the visitor can see, or has scrolled past, keeps its variant for that view, so nothing they are reading is swapped or moves under them. The next view shows the new decision. A page without personalization answers with no groups, which is trusted for an hour with no background check, so links to it cost nothing; when that page renders personalization by then (published since), it asks at once. A URL with other UTM parameters is another view; a failed answer is kept for 10 seconds and never replaces a real one.
393
393
  - **"Right" means what Prepr knows when the visitor clicks.** Prepr assigns segments from the visitor's tracked activity. If it has not yet processed the pixel event of the previous page view, the decision cannot include it, and the visitor sees that segment from a later page view.
394
- - **Costs.** The page carries the HTML of every variant, and an item with groups nested inside it once per combination of their variants. `render` gets `defaultVariant`: give `priority` images only to the default variant and `loading="lazy"` to the others, or the browser downloads every variant's hero image. Every page view and every link press on a site with personalization is one decision request (a press on touch devices too, also when it turns into a scroll), answered from Prepr's cache when other visitors had the same answer.
394
+ - **Costs.** The page carries the HTML of every variant, and an item with groups nested inside it once per combination of their variants. `render` gets `defaultVariant`: give `priority` images only to the default variant and `loading="lazy"` to the others, or the browser downloads every variant's hero image. Every page load with personalization, and every link hovered or pressed (also a press that turns into a scroll), is at most one decision request when this tab has no fresh decision for that URL, answered from Prepr's cache when other visitors had the same answer.
395
395
  - Without JavaScript, visitors see the default variants.
396
396
 
397
397
  ##### Deploying
@@ -1054,7 +1054,7 @@ The toolbar. Takes the props returned by `getToolbarProps`, and renders nothing
1054
1054
 
1055
1055
  #### `PreprPersonalizationSwitch`
1056
1056
 
1057
- From `@preprio/toolkit/nextjs`. Decides every client-side navigation: when the visitor presses a same-site link, it asks `createPreprProxy` for the target page's decision and holds the click until the answer is in (see [How each visitor gets their variant](#how-each-visitor-gets-their-variant)). Mount it once in the root layout, so links on pages without a `PreprStack` are covered too. Clicks with a modifier key, links to other sites and links that open a new tab or download are left alone. The decision requests are answered by `createPreprProxy` only. Takes no props and renders nothing.
1057
+ From `@preprio/toolkit/nextjs`. Gets each client-side navigation's decision in before the click: when the visitor hovers over a same-site link (or presses it, on touch devices), it asks `createPreprProxy` for the target page's decision. Clicks are never held (see [How each visitor gets their variant](#how-each-visitor-gets-their-variant)). Mount it once in the root layout, so links on pages without a `PreprStack` are covered too. Links to other sites and links that open a new tab or download are left alone. The decision requests are answered by `createPreprProxy` only. Takes no props and renders nothing.
1058
1058
 
1059
1059
  #### `PreprPreview`
1060
1060
 
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.6.0-alpha.7";
2
+ var VERSION = "0.6.0-alpha.9";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -3,7 +3,7 @@ import {
3
3
  getPreprHeadersFromHeaders,
4
4
  isPersonalizedResponse,
5
5
  validatePreprToken
6
- } from "./chunk-3EXK2LHI.js";
6
+ } from "./chunk-UN4JADNU.js";
7
7
 
8
8
  // src/core/variants/constants.ts
9
9
  var PREPR_CONTEXT_ALIAS = "_preprContext";
@@ -31,7 +31,7 @@ __export(astro_exports, {
31
31
  module.exports = __toCommonJS(astro_exports);
32
32
 
33
33
  // src/version.ts
34
- var VERSION = "0.6.0-alpha.7";
34
+ var VERSION = "0.6.0-alpha.9";
35
35
 
36
36
  // src/core/features.ts
37
37
  function isEnabled(config) {
@@ -7,7 +7,7 @@ import {
7
7
  getToolbarPropsFromHeaders,
8
8
  isPersonalizedResponse,
9
9
  resolveFeatures
10
- } from "./chunk-3EXK2LHI.js";
10
+ } from "./chunk-UN4JADNU.js";
11
11
 
12
12
  // src/core/constants.ts
13
13
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.6.0-alpha.7";
2
+ var VERSION = "0.6.0-alpha.9";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -11,7 +11,7 @@ import {
11
11
  ALL_FEATURES_ENABLED,
12
12
  VERSION,
13
13
  resolveFeatures
14
- } from "./chunk-VQIPTUN4.js";
14
+ } from "./chunk-5QIWVDP2.js";
15
15
 
16
16
  // src/core/stega/dom.ts
17
17
  var IGNORED_ANCESTORS = "script, style, noscript";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  VERSION,
3
3
  resolveFeatures
4
- } from "./chunk-VQIPTUN4.js";
4
+ } from "./chunk-5QIWVDP2.js";
5
5
 
6
6
  // src/core/server.ts
7
7
  var PreprError = class extends Error {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PreprError
3
- } from "./chunk-3QGWCYTL.js";
3
+ } from "./chunk-WQBKQG4R.js";
4
4
 
5
5
  // src/core/variants/constants.ts
6
6
  var PREPR_CONTEXT_ALIAS = "_preprContext";
@@ -2,9 +2,9 @@ import {
2
2
  toDecisionQuery,
3
3
  toSchemaInfo,
4
4
  toVariantsQuery
5
- } from "../chunk-PFJO4TYL.js";
6
- import "../chunk-3QGWCYTL.js";
7
- import "../chunk-VQIPTUN4.js";
5
+ } from "../chunk-XL4Y7FGW.js";
6
+ import "../chunk-WQBKQG4R.js";
7
+ import "../chunk-5QIWVDP2.js";
8
8
 
9
9
  // src/codegen/index.ts
10
10
  function typeRef(type) {
package/dist/graphql.js CHANGED
@@ -21,12 +21,12 @@ import {
21
21
  toSchemaInfo,
22
22
  toVariantsDocument,
23
23
  toVariantsQuery
24
- } from "./chunk-PFJO4TYL.js";
24
+ } from "./chunk-XL4Y7FGW.js";
25
25
  import {
26
26
  PreprError,
27
27
  validatePreprToken
28
- } from "./chunk-3QGWCYTL.js";
29
- import "./chunk-VQIPTUN4.js";
28
+ } from "./chunk-WQBKQG4R.js";
29
+ import "./chunk-5QIWVDP2.js";
30
30
 
31
31
  // src/core/graphql/cache-tags.ts
32
32
  var MAX_TAGS = 128;
package/dist/index.cjs CHANGED
@@ -45,7 +45,7 @@ __export(src_exports, {
45
45
  module.exports = __toCommonJS(src_exports);
46
46
 
47
47
  // src/version.ts
48
- var VERSION = "0.6.0-alpha.7";
48
+ var VERSION = "0.6.0-alpha.9";
49
49
 
50
50
  // src/core/features.ts
51
51
  function isEnabled(config) {
package/dist/index.d.cts CHANGED
@@ -7,7 +7,7 @@ export { P as PreprPixelConfig, l as loadTrackingPixel, s as setTrackingParam, t
7
7
  * Package version; must match package.json. Lives in its own leaf module so
8
8
  * core can import it without cycling through the barrel.
9
9
  */
10
- declare const VERSION = "0.6.0-alpha.7";
10
+ declare const VERSION = "0.6.0-alpha.9";
11
11
 
12
12
  /**
13
13
  * Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { P as PreprPixelConfig, l as loadTrackingPixel, s as setTrackingParam, t
7
7
  * Package version; must match package.json. Lives in its own leaf module so
8
8
  * core can import it without cycling through the barrel.
9
9
  */
10
- declare const VERSION = "0.6.0-alpha.7";
10
+ declare const VERSION = "0.6.0-alpha.9";
11
11
 
12
12
  /**
13
13
  * Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  loadTrackingPixel,
16
16
  setTrackingParam,
17
17
  trackEvent
18
- } from "./chunk-U7TRFVK7.js";
18
+ } from "./chunk-J66UOCK6.js";
19
19
  import {
20
20
  isPersonalizedResponse
21
21
  } from "./chunk-3NCEVJJ3.js";
@@ -32,11 +32,11 @@ import {
32
32
  getPreprUUIDFromHeaders,
33
33
  getToolbarPropsFromHeaders,
34
34
  validatePreprToken
35
- } from "./chunk-3QGWCYTL.js";
35
+ } from "./chunk-WQBKQG4R.js";
36
36
  import {
37
37
  VERSION,
38
38
  resolveFeatures
39
- } from "./chunk-VQIPTUN4.js";
39
+ } from "./chunk-5QIWVDP2.js";
40
40
 
41
41
  // src/core/middleware.ts
42
42
  var UTM_PARAMS = [
@@ -2,7 +2,7 @@
2
2
  import { NextResponse } from "next/server";
3
3
 
4
4
  // src/version.ts
5
- var VERSION = "0.6.0-alpha.7";
5
+ var VERSION = "0.6.0-alpha.9";
6
6
 
7
7
  // src/core/constants.ts
8
8
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -2317,10 +2317,34 @@ function isReactHydrated(text) {
2317
2317
  }
2318
2318
 
2319
2319
  // src/nextjs/decision-switch.ts
2320
- function applyPreprDecisions(decision) {
2320
+ function applyPreprDecisions(decision, options = {}) {
2321
2321
  const state = window;
2322
2322
  const key = location.pathname + location.search;
2323
- if (decision !== void 0) state.__preprDecision = { key, value: decision };
2323
+ const previous = state.__preprDecision;
2324
+ let value = decision;
2325
+ if (value !== void 0 && value !== "*" && options.refresh && previous && previous.key === key && previous.value !== "*") {
2326
+ const pairs = (from) => {
2327
+ const out = {};
2328
+ from.split("~").forEach((pair) => {
2329
+ const colon = pair.indexOf(":");
2330
+ if (colon > 0) out[pair.slice(0, colon)] = pair;
2331
+ });
2332
+ return out;
2333
+ };
2334
+ const before = pairs(previous.value);
2335
+ const after = pairs(value);
2336
+ document.querySelectorAll("[data-prepr-v][data-prepr-show]").forEach((element) => {
2337
+ const range = document.createRange();
2338
+ range.selectNodeContents(element);
2339
+ if (range.getBoundingClientRect().top >= window.innerHeight) return;
2340
+ (element.getAttribute("data-prepr-v") || "").split(" ").forEach((token) => {
2341
+ const group = token.slice(0, token.indexOf(":"));
2342
+ if (before[group]) after[group] = before[group];
2343
+ });
2344
+ });
2345
+ value = Object.keys(after).map((group) => after[group]).join("~");
2346
+ }
2347
+ if (value !== void 0) state.__preprDecision = { key, value };
2324
2348
  const current = state.__preprDecision;
2325
2349
  if (!current || current.key !== key) return false;
2326
2350
  document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
@@ -2346,12 +2370,76 @@ function applyPreprDecisions(decision) {
2346
2370
  });
2347
2371
  return true;
2348
2372
  }
2373
+ function requestPreprDecision(key, options = {}) {
2374
+ const fresh = 1e4;
2375
+ const keep = 18e5;
2376
+ const keepStatic = 36e5;
2377
+ const state = window;
2378
+ const asking = state.__preprAsking = state.__preprAsking || {};
2379
+ const read = () => {
2380
+ try {
2381
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
2382
+ return all && typeof all === "object" ? all : {};
2383
+ } catch {
2384
+ return {};
2385
+ }
2386
+ };
2387
+ const ask = () => {
2388
+ if (!asking[key]) {
2389
+ asking[key] = fetch(key, {
2390
+ headers: { "Prepr-Decide": "1" },
2391
+ cache: "no-store",
2392
+ credentials: "same-origin",
2393
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
2394
+ // covers a proxy that never answers.
2395
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
2396
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
2397
+ delete asking[key];
2398
+ try {
2399
+ const all = read();
2400
+ const at = Date.now();
2401
+ Object.keys(all).forEach((stored) => {
2402
+ const entry = all[stored];
2403
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
2404
+ delete all[stored];
2405
+ });
2406
+ const kept = all[key];
2407
+ if (answer !== "*" || !kept || kept.value === "*")
2408
+ all[key] = { value: answer, at };
2409
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
2410
+ } catch {
2411
+ }
2412
+ return answer;
2413
+ });
2414
+ }
2415
+ return asking[key];
2416
+ };
2417
+ const last = read()[key];
2418
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
2419
+ const value = last && typeof last.value === "string" ? last.value : null;
2420
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
2421
+ if (value !== "" && age >= fresh) {
2422
+ ask().then((answer) => {
2423
+ if (answer !== "*" && answer !== value && options.onChange)
2424
+ options.onChange(answer);
2425
+ });
2426
+ }
2427
+ return { result: value, value: Promise.resolve(value) };
2428
+ }
2429
+ const pending = {
2430
+ value: ask()
2431
+ };
2432
+ pending.value.then((answer) => {
2433
+ pending.result = answer;
2434
+ });
2435
+ return pending;
2436
+ }
2349
2437
 
2350
2438
  // src/react/components.ts
2351
2439
  var import_react = require("react");
2352
2440
 
2353
2441
  // src/version.ts
2354
- var VERSION = "0.6.0-alpha.7";
2442
+ var VERSION = "0.6.0-alpha.9";
2355
2443
 
2356
2444
  // src/core/pixel.ts
2357
2445
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -2426,35 +2514,22 @@ function PreprToolbar({
2426
2514
  }, []);
2427
2515
  return null;
2428
2516
  }
2429
- var DECIDE_HEADER = "Prepr-Decide";
2430
- var DECIDE_TIMEOUT_MS = 6e3;
2431
- function requestDecision(key) {
2432
- const pending = { key, value: null };
2433
- pending.value = fetch(key, {
2434
- headers: { [DECIDE_HEADER]: "1" },
2435
- cache: "no-store",
2436
- credentials: "same-origin",
2437
- signal: AbortSignal.timeout(DECIDE_TIMEOUT_MS)
2438
- }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((value) => {
2439
- pending.result = value;
2440
- try {
2441
- sessionStorage.setItem(
2442
- "prepr_decision",
2443
- JSON.stringify({ key, value, at: Date.now() })
2444
- );
2445
- } catch {
2446
- }
2447
- return value;
2448
- });
2449
- return pending;
2450
- }
2451
- var pressed = null;
2452
- var replaying = null;
2453
2517
  var shown = /* @__PURE__ */ new Map();
2454
2518
  var historyStep = false;
2455
2519
  var onPopState = () => {
2456
2520
  historyStep = true;
2457
2521
  };
2522
+ function requestDecision(key, personalized = false) {
2523
+ return requestPreprDecision(key, {
2524
+ personalized,
2525
+ onChange: (value) => {
2526
+ if (location.pathname + location.search !== key) return;
2527
+ applyPreprDecisions(value, { refresh: true });
2528
+ const state = window.__preprDecision;
2529
+ if (state) shown.set(key, state.value);
2530
+ }
2531
+ });
2532
+ }
2458
2533
  function linkTarget(event) {
2459
2534
  const anchor = event.target?.closest?.("a[href]");
2460
2535
  if (!(anchor instanceof HTMLAnchorElement)) return null;
@@ -2466,55 +2541,37 @@ function linkTarget(event) {
2466
2541
  if (url.origin !== location.origin || key === location.pathname + location.search) {
2467
2542
  return null;
2468
2543
  }
2469
- return { anchor, key };
2544
+ return key;
2545
+ }
2546
+ var HOVER_DELAY_MS = 65;
2547
+ var hovering = null;
2548
+ function onPointerOver(event) {
2549
+ if (event.pointerType !== "mouse") return;
2550
+ const key = linkTarget(event);
2551
+ if (hovering?.key === key) return;
2552
+ if (hovering) clearTimeout(hovering.timer);
2553
+ hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
2470
2554
  }
2471
2555
  function onPointerDown(event) {
2472
2556
  if (event.button !== 0) return;
2473
- const target = linkTarget(event);
2474
- if (!target || pressed?.key === target.key && pressed.result === void 0) {
2475
- return;
2476
- }
2477
- pressed = requestDecision(target.key);
2478
- }
2479
- function onClick(event) {
2480
- const target = linkTarget(event);
2481
- if (!target) return;
2482
- if (replaying === target.anchor) {
2483
- replaying = null;
2484
- return;
2485
- }
2486
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
2487
- return;
2488
- }
2489
- const pending = pressed && pressed.key === target.key ? pressed : requestDecision(target.key);
2490
- pressed = null;
2491
- const record = (value) => {
2492
- window.__preprDecision = { key: target.key, value };
2493
- };
2494
- if (pending.result !== void 0) {
2495
- record(pending.result);
2496
- return;
2497
- }
2498
- event.preventDefault();
2499
- event.stopImmediatePropagation();
2500
- void pending.value.then((value) => {
2501
- record(value);
2502
- replaying = target.anchor;
2503
- target.anchor.click();
2504
- });
2557
+ const key = linkTarget(event);
2558
+ if (key) requestDecision(key);
2505
2559
  }
2506
2560
  var listeners = 0;
2507
2561
  function useDecisionRequests() {
2508
2562
  (0, import_react2.useEffect)(() => {
2509
2563
  if (listeners++ === 0) {
2510
- window.addEventListener("pointerdown", onPointerDown, true);
2511
- window.addEventListener("click", onClick, true);
2564
+ window.addEventListener("pointerover", onPointerOver, { passive: true });
2565
+ window.addEventListener("pointerdown", onPointerDown, {
2566
+ capture: true,
2567
+ passive: true
2568
+ });
2512
2569
  window.addEventListener("popstate", onPopState);
2513
2570
  }
2514
2571
  return () => {
2515
2572
  if (--listeners === 0) {
2573
+ window.removeEventListener("pointerover", onPointerOver);
2516
2574
  window.removeEventListener("pointerdown", onPointerDown, true);
2517
- window.removeEventListener("click", onClick, true);
2518
2575
  window.removeEventListener("popstate", onPopState);
2519
2576
  }
2520
2577
  };
@@ -2538,12 +2595,16 @@ function PreprDecisionApplier() {
2538
2595
  remember();
2539
2596
  return;
2540
2597
  }
2598
+ const pending = requestDecision(key, true);
2599
+ if (pending.result !== void 0) {
2600
+ applyPreprDecisions(pending.result);
2601
+ remember();
2602
+ return;
2603
+ }
2541
2604
  let live = true;
2542
- const started = window.__preprPending;
2543
- const value = started?.key === key ? started.value : requestDecision(key).value;
2544
- void value.then((value2) => {
2605
+ void pending.value.then((value) => {
2545
2606
  if (live && location.pathname + location.search === key) {
2546
- applyPreprDecisions(value2);
2607
+ applyPreprDecisions(value);
2547
2608
  remember();
2548
2609
  }
2549
2610
  });
@@ -23,15 +23,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
23
23
  */
24
24
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
25
25
  /**
26
- * Decides every client-side navigation to another page: when the visitor
27
- * presses a same-site link, it asks `createPreprProxy` which variants they
28
- * get on that page, and holds the click until the answer is in. The new page
29
- * then paints with those variants, never with the default variant first.
30
- * Pressing starts the request, so the wait mostly overlaps the click itself.
26
+ * Gets each client-side navigation's decision in before the click: when the
27
+ * visitor hovers over a same-site link (or presses it, on touch devices), it
28
+ * asks `createPreprProxy` which variants they get on that page. Clicks are
29
+ * never held: the new page paints with those variants when the answer is in,
30
+ * and keeps its personalized sections hidden until then otherwise. Pages
31
+ * without personalization answer with no groups and are not asked again for
32
+ * an hour.
31
33
  *
32
34
  * Mount it once in the root layout, so links on pages without a
33
- * `PreprStack` are covered too. Clicks with a modifier key, links to other
34
- * sites and links that open a new tab or download are left alone.
35
+ * `PreprStack` are covered too. Links to other sites and links that open a
36
+ * new tab or download are left alone.
35
37
  *
36
38
  * @example
37
39
  * <PreprPersonalizationSwitch />
@@ -39,12 +41,11 @@ declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps)
39
41
  declare function PreprPersonalizationSwitch(): null;
40
42
  /**
41
43
  * Applies this page view's decision to the variant wrappers of the
42
- * `PreprStack` it is rendered by. A click's decision is in before the page
43
- * paints; Back and Forward show the page as this tab last showed it. On a
44
- * page load, and any other view without a decision (a navigation started
45
- * from code, or Back to a page this tab never showed), its personalized
46
- * sections stay hidden until the decision request answers. Rendered by
47
- * `PreprStack`; you do not need it yourself.
44
+ * `PreprStack` it is rendered by, before the page paints when the decision is
45
+ * already in: asked for on hover, or kept from an earlier view of the page.
46
+ * Back and Forward show the page as this tab last showed it. Any other view
47
+ * keeps its personalized sections hidden until the decision request answers.
48
+ * Rendered by `PreprStack`; you do not need it yourself.
48
49
  */
49
50
  declare function PreprDecisionApplier(): null;
50
51
  /**
@@ -23,15 +23,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
23
23
  */
24
24
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
25
25
  /**
26
- * Decides every client-side navigation to another page: when the visitor
27
- * presses a same-site link, it asks `createPreprProxy` which variants they
28
- * get on that page, and holds the click until the answer is in. The new page
29
- * then paints with those variants, never with the default variant first.
30
- * Pressing starts the request, so the wait mostly overlaps the click itself.
26
+ * Gets each client-side navigation's decision in before the click: when the
27
+ * visitor hovers over a same-site link (or presses it, on touch devices), it
28
+ * asks `createPreprProxy` which variants they get on that page. Clicks are
29
+ * never held: the new page paints with those variants when the answer is in,
30
+ * and keeps its personalized sections hidden until then otherwise. Pages
31
+ * without personalization answer with no groups and are not asked again for
32
+ * an hour.
31
33
  *
32
34
  * Mount it once in the root layout, so links on pages without a
33
- * `PreprStack` are covered too. Clicks with a modifier key, links to other
34
- * sites and links that open a new tab or download are left alone.
35
+ * `PreprStack` are covered too. Links to other sites and links that open a
36
+ * new tab or download are left alone.
35
37
  *
36
38
  * @example
37
39
  * <PreprPersonalizationSwitch />
@@ -39,12 +41,11 @@ declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps)
39
41
  declare function PreprPersonalizationSwitch(): null;
40
42
  /**
41
43
  * Applies this page view's decision to the variant wrappers of the
42
- * `PreprStack` it is rendered by. A click's decision is in before the page
43
- * paints; Back and Forward show the page as this tab last showed it. On a
44
- * page load, and any other view without a decision (a navigation started
45
- * from code, or Back to a page this tab never showed), its personalized
46
- * sections stay hidden until the decision request answers. Rendered by
47
- * `PreprStack`; you do not need it yourself.
44
+ * `PreprStack` it is rendered by, before the page paints when the decision is
45
+ * already in: asked for on hover, or kept from an earlier view of the page.
46
+ * Back and Forward show the page as this tab last showed it. Any other view
47
+ * keeps its personalized sections hidden until the decision request answers.
48
+ * Rendered by `PreprStack`; you do not need it yourself.
48
49
  */
49
50
  declare function PreprDecisionApplier(): null;
50
51
  /**