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

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-R2YPGDVU.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-U7TRFVK7.js → chunk-2QGUP7VQ.js} +1 -1
  7. package/dist/{chunk-VQIPTUN4.js → chunk-ATPB7GEC.js} +1 -1
  8. package/dist/{chunk-PFJO4TYL.js → chunk-OY7OKO6Q.js} +1 -1
  9. package/dist/{chunk-3QGWCYTL.js → chunk-T7ESBVBI.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-O2SBSBDG.js} +1 -1
  17. package/dist/nextjs/components.cjs +101 -65
  18. package/dist/nextjs/components.d.cts +14 -13
  19. package/dist/nextjs/components.d.ts +14 -13
  20. package/dist/nextjs/components.js +102 -66
  21. package/dist/nextjs/graphql.cjs +71 -24
  22. package/dist/nextjs/graphql.d.cts +5 -5
  23. package/dist/nextjs/graphql.d.ts +5 -5
  24. package/dist/nextjs/graphql.js +71 -24
  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-SX3MSVPK.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-UOASSHZO.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, which replaces the variant only when it differs. 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.8";
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-R2YPGDVU.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.8";
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-R2YPGDVU.js";
11
11
 
12
12
  // src/core/constants.ts
13
13
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -11,7 +11,7 @@ import {
11
11
  ALL_FEATURES_ENABLED,
12
12
  VERSION,
13
13
  resolveFeatures
14
- } from "./chunk-VQIPTUN4.js";
14
+ } from "./chunk-ATPB7GEC.js";
15
15
 
16
16
  // src/core/stega/dom.ts
17
17
  var IGNORED_ANCESTORS = "script, style, noscript";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.6.0-alpha.7";
2
+ var VERSION = "0.6.0-alpha.8";
3
3
 
4
4
  // src/core/features.ts
5
5
  function isEnabled(config) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PreprError
3
- } from "./chunk-3QGWCYTL.js";
3
+ } from "./chunk-T7ESBVBI.js";
4
4
 
5
5
  // src/core/variants/constants.ts
6
6
  var PREPR_CONTEXT_ALIAS = "_preprContext";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  VERSION,
3
3
  resolveFeatures
4
- } from "./chunk-VQIPTUN4.js";
4
+ } from "./chunk-ATPB7GEC.js";
5
5
 
6
6
  // src/core/server.ts
7
7
  var PreprError = class extends Error {
@@ -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-OY7OKO6Q.js";
6
+ import "../chunk-T7ESBVBI.js";
7
+ import "../chunk-ATPB7GEC.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-OY7OKO6Q.js";
25
25
  import {
26
26
  PreprError,
27
27
  validatePreprToken
28
- } from "./chunk-3QGWCYTL.js";
29
- import "./chunk-VQIPTUN4.js";
28
+ } from "./chunk-T7ESBVBI.js";
29
+ import "./chunk-ATPB7GEC.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.8";
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.8";
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.8";
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-2QGUP7VQ.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-T7ESBVBI.js";
36
36
  import {
37
37
  VERSION,
38
38
  resolveFeatures
39
- } from "./chunk-VQIPTUN4.js";
39
+ } from "./chunk-ATPB7GEC.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.8";
6
6
 
7
7
  // src/core/constants.ts
8
8
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -2346,12 +2346,76 @@ function applyPreprDecisions(decision) {
2346
2346
  });
2347
2347
  return true;
2348
2348
  }
2349
+ function requestPreprDecision(key, options = {}) {
2350
+ const fresh = 1e4;
2351
+ const keep = 18e5;
2352
+ const keepStatic = 36e5;
2353
+ const state = window;
2354
+ const asking = state.__preprAsking = state.__preprAsking || {};
2355
+ const read = () => {
2356
+ try {
2357
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
2358
+ return all && typeof all === "object" ? all : {};
2359
+ } catch {
2360
+ return {};
2361
+ }
2362
+ };
2363
+ const ask = () => {
2364
+ if (!asking[key]) {
2365
+ asking[key] = fetch(key, {
2366
+ headers: { "Prepr-Decide": "1" },
2367
+ cache: "no-store",
2368
+ credentials: "same-origin",
2369
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
2370
+ // covers a proxy that never answers.
2371
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
2372
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
2373
+ delete asking[key];
2374
+ try {
2375
+ const all = read();
2376
+ const at = Date.now();
2377
+ Object.keys(all).forEach((stored) => {
2378
+ const entry = all[stored];
2379
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
2380
+ delete all[stored];
2381
+ });
2382
+ const kept = all[key];
2383
+ if (answer !== "*" || !kept || kept.value === "*")
2384
+ all[key] = { value: answer, at };
2385
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
2386
+ } catch {
2387
+ }
2388
+ return answer;
2389
+ });
2390
+ }
2391
+ return asking[key];
2392
+ };
2393
+ const last = read()[key];
2394
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
2395
+ const value = last && typeof last.value === "string" ? last.value : null;
2396
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
2397
+ if (value !== "" && age >= fresh) {
2398
+ ask().then((answer) => {
2399
+ if (answer !== "*" && answer !== value && options.onChange)
2400
+ options.onChange(answer);
2401
+ });
2402
+ }
2403
+ return { result: value, value: Promise.resolve(value) };
2404
+ }
2405
+ const pending = {
2406
+ value: ask()
2407
+ };
2408
+ pending.value.then((answer) => {
2409
+ pending.result = answer;
2410
+ });
2411
+ return pending;
2412
+ }
2349
2413
 
2350
2414
  // src/react/components.ts
2351
2415
  var import_react = require("react");
2352
2416
 
2353
2417
  // src/version.ts
2354
- var VERSION = "0.6.0-alpha.7";
2418
+ var VERSION = "0.6.0-alpha.8";
2355
2419
 
2356
2420
  // src/core/pixel.ts
2357
2421
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -2426,35 +2490,21 @@ function PreprToolbar({
2426
2490
  }, []);
2427
2491
  return null;
2428
2492
  }
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
2493
  var shown = /* @__PURE__ */ new Map();
2454
2494
  var historyStep = false;
2455
2495
  var onPopState = () => {
2456
2496
  historyStep = true;
2457
2497
  };
2498
+ function requestDecision(key, personalized = false) {
2499
+ return requestPreprDecision(key, {
2500
+ personalized,
2501
+ onChange: (value) => {
2502
+ if (location.pathname + location.search !== key) return;
2503
+ applyPreprDecisions(value);
2504
+ shown.set(key, value);
2505
+ }
2506
+ });
2507
+ }
2458
2508
  function linkTarget(event) {
2459
2509
  const anchor = event.target?.closest?.("a[href]");
2460
2510
  if (!(anchor instanceof HTMLAnchorElement)) return null;
@@ -2466,55 +2516,37 @@ function linkTarget(event) {
2466
2516
  if (url.origin !== location.origin || key === location.pathname + location.search) {
2467
2517
  return null;
2468
2518
  }
2469
- return { anchor, key };
2519
+ return key;
2520
+ }
2521
+ var HOVER_DELAY_MS = 65;
2522
+ var hovering = null;
2523
+ function onPointerOver(event) {
2524
+ if (event.pointerType !== "mouse") return;
2525
+ const key = linkTarget(event);
2526
+ if (hovering?.key === key) return;
2527
+ if (hovering) clearTimeout(hovering.timer);
2528
+ hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
2470
2529
  }
2471
2530
  function onPointerDown(event) {
2472
2531
  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
- });
2532
+ const key = linkTarget(event);
2533
+ if (key) requestDecision(key);
2505
2534
  }
2506
2535
  var listeners = 0;
2507
2536
  function useDecisionRequests() {
2508
2537
  (0, import_react2.useEffect)(() => {
2509
2538
  if (listeners++ === 0) {
2510
- window.addEventListener("pointerdown", onPointerDown, true);
2511
- window.addEventListener("click", onClick, true);
2539
+ window.addEventListener("pointerover", onPointerOver, { passive: true });
2540
+ window.addEventListener("pointerdown", onPointerDown, {
2541
+ capture: true,
2542
+ passive: true
2543
+ });
2512
2544
  window.addEventListener("popstate", onPopState);
2513
2545
  }
2514
2546
  return () => {
2515
2547
  if (--listeners === 0) {
2548
+ window.removeEventListener("pointerover", onPointerOver);
2516
2549
  window.removeEventListener("pointerdown", onPointerDown, true);
2517
- window.removeEventListener("click", onClick, true);
2518
2550
  window.removeEventListener("popstate", onPopState);
2519
2551
  }
2520
2552
  };
@@ -2538,12 +2570,16 @@ function PreprDecisionApplier() {
2538
2570
  remember();
2539
2571
  return;
2540
2572
  }
2573
+ const pending = requestDecision(key, true);
2574
+ if (pending.result !== void 0) {
2575
+ applyPreprDecisions(pending.result);
2576
+ remember();
2577
+ return;
2578
+ }
2541
2579
  let live = true;
2542
- const started = window.__preprPending;
2543
- const value = started?.key === key ? started.value : requestDecision(key).value;
2544
- void value.then((value2) => {
2580
+ void pending.value.then((value) => {
2545
2581
  if (live && location.pathname + location.search === key) {
2546
- applyPreprDecisions(value2);
2582
+ applyPreprDecisions(value);
2547
2583
  remember();
2548
2584
  }
2549
2585
  });
@@ -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
  /**
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  createPreprPreview,
4
4
  loadTrackingPixel
5
- } from "../chunk-U7TRFVK7.js";
5
+ } from "../chunk-2QGUP7VQ.js";
6
6
  import "../chunk-S5RWVMDJ.js";
7
- import "../chunk-VQIPTUN4.js";
7
+ import "../chunk-ATPB7GEC.js";
8
8
 
9
9
  // src/nextjs/components.ts
10
10
  import { usePathname, useRouter } from "next/navigation";
@@ -64,6 +64,70 @@ function applyPreprDecisions(decision) {
64
64
  });
65
65
  return true;
66
66
  }
67
+ function requestPreprDecision(key, options = {}) {
68
+ const fresh = 1e4;
69
+ const keep = 18e5;
70
+ const keepStatic = 36e5;
71
+ const state = window;
72
+ const asking = state.__preprAsking = state.__preprAsking || {};
73
+ const read = () => {
74
+ try {
75
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
76
+ return all && typeof all === "object" ? all : {};
77
+ } catch {
78
+ return {};
79
+ }
80
+ };
81
+ const ask = () => {
82
+ if (!asking[key]) {
83
+ asking[key] = fetch(key, {
84
+ headers: { "Prepr-Decide": "1" },
85
+ cache: "no-store",
86
+ credentials: "same-origin",
87
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
88
+ // covers a proxy that never answers.
89
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
90
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
91
+ delete asking[key];
92
+ try {
93
+ const all = read();
94
+ const at = Date.now();
95
+ Object.keys(all).forEach((stored) => {
96
+ const entry = all[stored];
97
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
98
+ delete all[stored];
99
+ });
100
+ const kept = all[key];
101
+ if (answer !== "*" || !kept || kept.value === "*")
102
+ all[key] = { value: answer, at };
103
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
104
+ } catch {
105
+ }
106
+ return answer;
107
+ });
108
+ }
109
+ return asking[key];
110
+ };
111
+ const last = read()[key];
112
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
113
+ const value = last && typeof last.value === "string" ? last.value : null;
114
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
115
+ if (value !== "" && age >= fresh) {
116
+ ask().then((answer) => {
117
+ if (answer !== "*" && answer !== value && options.onChange)
118
+ options.onChange(answer);
119
+ });
120
+ }
121
+ return { result: value, value: Promise.resolve(value) };
122
+ }
123
+ const pending = {
124
+ value: ask()
125
+ };
126
+ pending.value.then((answer) => {
127
+ pending.result = answer;
128
+ });
129
+ return pending;
130
+ }
67
131
 
68
132
  // src/react/components.ts
69
133
  import { useEffect } from "react";
@@ -100,35 +164,21 @@ function PreprToolbar({
100
164
  }, []);
101
165
  return null;
102
166
  }
103
- var DECIDE_HEADER = "Prepr-Decide";
104
- var DECIDE_TIMEOUT_MS = 6e3;
105
- function requestDecision(key) {
106
- const pending = { key, value: null };
107
- pending.value = fetch(key, {
108
- headers: { [DECIDE_HEADER]: "1" },
109
- cache: "no-store",
110
- credentials: "same-origin",
111
- signal: AbortSignal.timeout(DECIDE_TIMEOUT_MS)
112
- }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((value) => {
113
- pending.result = value;
114
- try {
115
- sessionStorage.setItem(
116
- "prepr_decision",
117
- JSON.stringify({ key, value, at: Date.now() })
118
- );
119
- } catch {
120
- }
121
- return value;
122
- });
123
- return pending;
124
- }
125
- var pressed = null;
126
- var replaying = null;
127
167
  var shown = /* @__PURE__ */ new Map();
128
168
  var historyStep = false;
129
169
  var onPopState = () => {
130
170
  historyStep = true;
131
171
  };
172
+ function requestDecision(key, personalized = false) {
173
+ return requestPreprDecision(key, {
174
+ personalized,
175
+ onChange: (value) => {
176
+ if (location.pathname + location.search !== key) return;
177
+ applyPreprDecisions(value);
178
+ shown.set(key, value);
179
+ }
180
+ });
181
+ }
132
182
  function linkTarget(event) {
133
183
  const anchor = event.target?.closest?.("a[href]");
134
184
  if (!(anchor instanceof HTMLAnchorElement)) return null;
@@ -140,55 +190,37 @@ function linkTarget(event) {
140
190
  if (url.origin !== location.origin || key === location.pathname + location.search) {
141
191
  return null;
142
192
  }
143
- return { anchor, key };
193
+ return key;
194
+ }
195
+ var HOVER_DELAY_MS = 65;
196
+ var hovering = null;
197
+ function onPointerOver(event) {
198
+ if (event.pointerType !== "mouse") return;
199
+ const key = linkTarget(event);
200
+ if (hovering?.key === key) return;
201
+ if (hovering) clearTimeout(hovering.timer);
202
+ hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
144
203
  }
145
204
  function onPointerDown(event) {
146
205
  if (event.button !== 0) return;
147
- const target = linkTarget(event);
148
- if (!target || pressed?.key === target.key && pressed.result === void 0) {
149
- return;
150
- }
151
- pressed = requestDecision(target.key);
152
- }
153
- function onClick(event) {
154
- const target = linkTarget(event);
155
- if (!target) return;
156
- if (replaying === target.anchor) {
157
- replaying = null;
158
- return;
159
- }
160
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
161
- return;
162
- }
163
- const pending = pressed && pressed.key === target.key ? pressed : requestDecision(target.key);
164
- pressed = null;
165
- const record = (value) => {
166
- window.__preprDecision = { key: target.key, value };
167
- };
168
- if (pending.result !== void 0) {
169
- record(pending.result);
170
- return;
171
- }
172
- event.preventDefault();
173
- event.stopImmediatePropagation();
174
- void pending.value.then((value) => {
175
- record(value);
176
- replaying = target.anchor;
177
- target.anchor.click();
178
- });
206
+ const key = linkTarget(event);
207
+ if (key) requestDecision(key);
179
208
  }
180
209
  var listeners = 0;
181
210
  function useDecisionRequests() {
182
211
  useEffect2(() => {
183
212
  if (listeners++ === 0) {
184
- window.addEventListener("pointerdown", onPointerDown, true);
185
- window.addEventListener("click", onClick, true);
213
+ window.addEventListener("pointerover", onPointerOver, { passive: true });
214
+ window.addEventListener("pointerdown", onPointerDown, {
215
+ capture: true,
216
+ passive: true
217
+ });
186
218
  window.addEventListener("popstate", onPopState);
187
219
  }
188
220
  return () => {
189
221
  if (--listeners === 0) {
222
+ window.removeEventListener("pointerover", onPointerOver);
190
223
  window.removeEventListener("pointerdown", onPointerDown, true);
191
- window.removeEventListener("click", onClick, true);
192
224
  window.removeEventListener("popstate", onPopState);
193
225
  }
194
226
  };
@@ -212,12 +244,16 @@ function PreprDecisionApplier() {
212
244
  remember();
213
245
  return;
214
246
  }
247
+ const pending = requestDecision(key, true);
248
+ if (pending.result !== void 0) {
249
+ applyPreprDecisions(pending.result);
250
+ remember();
251
+ return;
252
+ }
215
253
  let live = true;
216
- const started = window.__preprPending;
217
- const value = started?.key === key ? started.value : requestDecision(key).value;
218
- void value.then((value2) => {
254
+ void pending.value.then((value) => {
219
255
  if (live && location.pathname + location.search === key) {
220
- applyPreprDecisions(value2);
256
+ applyPreprDecisions(value);
221
257
  remember();
222
258
  }
223
259
  });
@@ -101,7 +101,7 @@ function preprCacheTags(data) {
101
101
  }
102
102
 
103
103
  // src/version.ts
104
- var VERSION = "0.6.0-alpha.7";
104
+ var VERSION = "0.6.0-alpha.8";
105
105
 
106
106
  // src/core/features.ts
107
107
  function isEnabled(config) {
@@ -1480,35 +1480,82 @@ function applyPreprDecisions(decision) {
1480
1480
  });
1481
1481
  return true;
1482
1482
  }
1483
- function startPreprDecision() {
1483
+ function requestPreprDecision(key, options = {}) {
1484
+ const fresh = 1e4;
1485
+ const keep = 18e5;
1486
+ const keepStatic = 36e5;
1484
1487
  const state = window;
1485
- const key = location.pathname + location.search;
1486
- if (state.__preprPending && state.__preprPending.key === key) return;
1487
- let last;
1488
- try {
1489
- last = JSON.parse(sessionStorage.getItem("prepr_decision") || "{}");
1490
- } catch {
1491
- last = {};
1492
- }
1493
- const age = Date.now() - (last.at || 0);
1494
- const value = last.key === key && typeof last.value === "string" && age >= 0 && age < 1e4 ? Promise.resolve(last.value) : fetch(key, {
1495
- headers: { "Prepr-Decide": "1" },
1496
- cache: "no-store",
1497
- credentials: "same-origin"
1498
- }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1488
+ const asking = state.__preprAsking = state.__preprAsking || {};
1489
+ const read = () => {
1499
1490
  try {
1500
- sessionStorage.setItem(
1501
- "prepr_decision",
1502
- JSON.stringify({ key, value: answer, at: Date.now() })
1503
- );
1491
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
1492
+ return all && typeof all === "object" ? all : {};
1504
1493
  } catch {
1494
+ return {};
1505
1495
  }
1506
- return answer;
1496
+ };
1497
+ const ask = () => {
1498
+ if (!asking[key]) {
1499
+ asking[key] = fetch(key, {
1500
+ headers: { "Prepr-Decide": "1" },
1501
+ cache: "no-store",
1502
+ credentials: "same-origin",
1503
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
1504
+ // covers a proxy that never answers.
1505
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
1506
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1507
+ delete asking[key];
1508
+ try {
1509
+ const all = read();
1510
+ const at = Date.now();
1511
+ Object.keys(all).forEach((stored) => {
1512
+ const entry = all[stored];
1513
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
1514
+ delete all[stored];
1515
+ });
1516
+ const kept = all[key];
1517
+ if (answer !== "*" || !kept || kept.value === "*")
1518
+ all[key] = { value: answer, at };
1519
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
1520
+ } catch {
1521
+ }
1522
+ return answer;
1523
+ });
1524
+ }
1525
+ return asking[key];
1526
+ };
1527
+ const last = read()[key];
1528
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
1529
+ const value = last && typeof last.value === "string" ? last.value : null;
1530
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
1531
+ if (value !== "" && age >= fresh) {
1532
+ ask().then((answer) => {
1533
+ if (answer !== "*" && answer !== value && options.onChange)
1534
+ options.onChange(answer);
1535
+ });
1536
+ }
1537
+ return { result: value, value: Promise.resolve(value) };
1538
+ }
1539
+ const pending = {
1540
+ value: ask()
1541
+ };
1542
+ pending.value.then((answer) => {
1543
+ pending.result = answer;
1507
1544
  });
1508
- state.__preprPending = { key, value };
1509
- value.then((answer) => {
1545
+ return pending;
1546
+ }
1547
+ function startPreprDecision() {
1548
+ const state = window;
1549
+ const key = location.pathname + location.search;
1550
+ const apply = (answer) => {
1510
1551
  if (location.pathname + location.search === key) state.__preprApply(answer);
1552
+ };
1553
+ const pending = state.__preprRequest(key, {
1554
+ onChange: apply,
1555
+ personalized: true
1511
1556
  });
1557
+ if (pending.result !== void 0) apply(pending.result);
1558
+ else pending.value.then(apply);
1512
1559
  }
1513
1560
 
1514
1561
  // src/nextjs/stack.ts
@@ -1518,7 +1565,7 @@ var DEFAULT_ATTRIBUTE = "data-prepr-default";
1518
1565
  var NESTED_ATTRIBUTE = "data-prepr-nested";
1519
1566
  var GROUP_BASE_CSS = `[${VARIANT_ATTRIBUTE}]{display:none}[${VARIANT_ATTRIBUTE}][data-prepr-show]{display:contents}[data-prepr-decision]:not([data-prepr-decided])~[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents;visibility:hidden}`;
1520
1567
  var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
1521
- var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};(${startPreprDecision.toString()})();`;
1568
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
1522
1569
  var APPLY_SCRIPT = "__preprApply()";
1523
1570
  function variantKeysOf(items) {
1524
1571
  return items.flatMap((item) => {
@@ -326,7 +326,7 @@ interface PreprProxyOptions {
326
326
  *
327
327
  * Pages themselves never wait for Prepr: they are the same for every visitor
328
328
  * and pass straight through. The browser sends a decision request for each
329
- * page view, from the page load and when a visitor presses a link; the proxy
329
+ * page view, from the page load and when a visitor points at a link; the proxy
330
330
  * answers it with the visitor's variant of every A/B test and personalization
331
331
  * group on that page, asked from Prepr in one request, as text and no page.
332
332
  * Add your own cookies or headers to the response it returns.
@@ -398,10 +398,10 @@ interface PreprStackProps<T> {
398
398
  * stack or nested inside an item, renders every variant, hidden. The page
399
399
  * is the same for every visitor and never waits for Prepr: an inline script
400
400
  * asks `createPreprProxy` for this page view's decision while the page
401
- * loads, and shows the visitor's variant when it answers. A click's
402
- * decision (requested by `PreprPersonalizationSwitch`) is in before the
403
- * next page paints. Nothing streams, and the default variant never shows
404
- * first to be replaced.
401
+ * loads, and shows the visitor's variant when it answers.
402
+ * `PreprPersonalizationSwitch` asks for a link's decision on hover, so it
403
+ * is mostly in before the next page paints. Nothing streams, and the
404
+ * default variant never shows first to be replaced.
405
405
  *
406
406
  * The page carries the HTML of every variant: an item with nested groups
407
407
  * repeats once per combination of their variants.
@@ -326,7 +326,7 @@ interface PreprProxyOptions {
326
326
  *
327
327
  * Pages themselves never wait for Prepr: they are the same for every visitor
328
328
  * and pass straight through. The browser sends a decision request for each
329
- * page view, from the page load and when a visitor presses a link; the proxy
329
+ * page view, from the page load and when a visitor points at a link; the proxy
330
330
  * answers it with the visitor's variant of every A/B test and personalization
331
331
  * group on that page, asked from Prepr in one request, as text and no page.
332
332
  * Add your own cookies or headers to the response it returns.
@@ -398,10 +398,10 @@ interface PreprStackProps<T> {
398
398
  * stack or nested inside an item, renders every variant, hidden. The page
399
399
  * is the same for every visitor and never waits for Prepr: an inline script
400
400
  * asks `createPreprProxy` for this page view's decision while the page
401
- * loads, and shows the visitor's variant when it answers. A click's
402
- * decision (requested by `PreprPersonalizationSwitch`) is in before the
403
- * next page paints. Nothing streams, and the default variant never shows
404
- * first to be replaced.
401
+ * loads, and shows the visitor's variant when it answers.
402
+ * `PreprPersonalizationSwitch` asks for a link's decision on hover, so it
403
+ * is mostly in before the next page paints. Nothing streams, and the
404
+ * default variant never shows first to be replaced.
405
405
  *
406
406
  * The page carries the HTML of every variant: an item with nested groups
407
407
  * repeats once per combination of their variants.
@@ -11,7 +11,7 @@ import {
11
11
  preprModelTag,
12
12
  preprModelTagsFor,
13
13
  validatePreprToken
14
- } from "./chunk-CZK7SMBD.js";
14
+ } from "./chunk-O2SBSBDG.js";
15
15
 
16
16
  // src/nextjs/graphql.ts
17
17
  import * as nextCache from "next/cache";
@@ -1067,35 +1067,82 @@ function applyPreprDecisions(decision) {
1067
1067
  });
1068
1068
  return true;
1069
1069
  }
1070
- function startPreprDecision() {
1070
+ function requestPreprDecision(key, options = {}) {
1071
+ const fresh = 1e4;
1072
+ const keep = 18e5;
1073
+ const keepStatic = 36e5;
1071
1074
  const state = window;
1072
- const key = location.pathname + location.search;
1073
- if (state.__preprPending && state.__preprPending.key === key) return;
1074
- let last;
1075
- try {
1076
- last = JSON.parse(sessionStorage.getItem("prepr_decision") || "{}");
1077
- } catch {
1078
- last = {};
1079
- }
1080
- const age = Date.now() - (last.at || 0);
1081
- const value = last.key === key && typeof last.value === "string" && age >= 0 && age < 1e4 ? Promise.resolve(last.value) : fetch(key, {
1082
- headers: { "Prepr-Decide": "1" },
1083
- cache: "no-store",
1084
- credentials: "same-origin"
1085
- }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1075
+ const asking = state.__preprAsking = state.__preprAsking || {};
1076
+ const read = () => {
1086
1077
  try {
1087
- sessionStorage.setItem(
1088
- "prepr_decision",
1089
- JSON.stringify({ key, value: answer, at: Date.now() })
1090
- );
1078
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
1079
+ return all && typeof all === "object" ? all : {};
1091
1080
  } catch {
1081
+ return {};
1092
1082
  }
1093
- return answer;
1083
+ };
1084
+ const ask = () => {
1085
+ if (!asking[key]) {
1086
+ asking[key] = fetch(key, {
1087
+ headers: { "Prepr-Decide": "1" },
1088
+ cache: "no-store",
1089
+ credentials: "same-origin",
1090
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
1091
+ // covers a proxy that never answers.
1092
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
1093
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1094
+ delete asking[key];
1095
+ try {
1096
+ const all = read();
1097
+ const at = Date.now();
1098
+ Object.keys(all).forEach((stored) => {
1099
+ const entry = all[stored];
1100
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
1101
+ delete all[stored];
1102
+ });
1103
+ const kept = all[key];
1104
+ if (answer !== "*" || !kept || kept.value === "*")
1105
+ all[key] = { value: answer, at };
1106
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
1107
+ } catch {
1108
+ }
1109
+ return answer;
1110
+ });
1111
+ }
1112
+ return asking[key];
1113
+ };
1114
+ const last = read()[key];
1115
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
1116
+ const value = last && typeof last.value === "string" ? last.value : null;
1117
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
1118
+ if (value !== "" && age >= fresh) {
1119
+ ask().then((answer) => {
1120
+ if (answer !== "*" && answer !== value && options.onChange)
1121
+ options.onChange(answer);
1122
+ });
1123
+ }
1124
+ return { result: value, value: Promise.resolve(value) };
1125
+ }
1126
+ const pending = {
1127
+ value: ask()
1128
+ };
1129
+ pending.value.then((answer) => {
1130
+ pending.result = answer;
1094
1131
  });
1095
- state.__preprPending = { key, value };
1096
- value.then((answer) => {
1132
+ return pending;
1133
+ }
1134
+ function startPreprDecision() {
1135
+ const state = window;
1136
+ const key = location.pathname + location.search;
1137
+ const apply = (answer) => {
1097
1138
  if (location.pathname + location.search === key) state.__preprApply(answer);
1139
+ };
1140
+ const pending = state.__preprRequest(key, {
1141
+ onChange: apply,
1142
+ personalized: true
1098
1143
  });
1144
+ if (pending.result !== void 0) apply(pending.result);
1145
+ else pending.value.then(apply);
1099
1146
  }
1100
1147
 
1101
1148
  // src/nextjs/stack.ts
@@ -1105,7 +1152,7 @@ var DEFAULT_ATTRIBUTE = "data-prepr-default";
1105
1152
  var NESTED_ATTRIBUTE = "data-prepr-nested";
1106
1153
  var GROUP_BASE_CSS = `[${VARIANT_ATTRIBUTE}]{display:none}[${VARIANT_ATTRIBUTE}][data-prepr-show]{display:contents}[data-prepr-decision]:not([data-prepr-decided])~[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents;visibility:hidden}`;
1107
1154
  var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
1108
- var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};(${startPreprDecision.toString()})();`;
1155
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
1109
1156
  var APPLY_SCRIPT = "__preprApply()";
1110
1157
  function variantKeysOf(items) {
1111
1158
  return items.flatMap((item) => {
@@ -43,7 +43,7 @@ module.exports = __toCommonJS(nextjs_exports);
43
43
  var import_server = require("next/server");
44
44
 
45
45
  // src/version.ts
46
- var VERSION = "0.6.0-alpha.7";
46
+ var VERSION = "0.6.0-alpha.8";
47
47
 
48
48
  // src/core/constants.ts
49
49
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -175,15 +175,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
175
175
  */
176
176
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
177
177
  /**
178
- * Decides every client-side navigation to another page: when the visitor
179
- * presses a same-site link, it asks `createPreprProxy` which variants they
180
- * get on that page, and holds the click until the answer is in. The new page
181
- * then paints with those variants, never with the default variant first.
182
- * Pressing starts the request, so the wait mostly overlaps the click itself.
178
+ * Gets each client-side navigation's decision in before the click: when the
179
+ * visitor hovers over a same-site link (or presses it, on touch devices), it
180
+ * asks `createPreprProxy` which variants they get on that page. Clicks are
181
+ * never held: the new page paints with those variants when the answer is in,
182
+ * and keeps its personalized sections hidden until then otherwise. Pages
183
+ * without personalization answer with no groups and are not asked again for
184
+ * an hour.
183
185
  *
184
186
  * Mount it once in the root layout, so links on pages without a
185
- * `PreprStack` are covered too. Clicks with a modifier key, links to other
186
- * sites and links that open a new tab or download are left alone.
187
+ * `PreprStack` are covered too. Links to other sites and links that open a
188
+ * new tab or download are left alone.
187
189
  *
188
190
  * @example
189
191
  * <PreprPersonalizationSwitch />
@@ -175,15 +175,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
175
175
  */
176
176
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
177
177
  /**
178
- * Decides every client-side navigation to another page: when the visitor
179
- * presses a same-site link, it asks `createPreprProxy` which variants they
180
- * get on that page, and holds the click until the answer is in. The new page
181
- * then paints with those variants, never with the default variant first.
182
- * Pressing starts the request, so the wait mostly overlaps the click itself.
178
+ * Gets each client-side navigation's decision in before the click: when the
179
+ * visitor hovers over a same-site link (or presses it, on touch devices), it
180
+ * asks `createPreprProxy` which variants they get on that page. Clicks are
181
+ * never held: the new page paints with those variants when the answer is in,
182
+ * and keeps its personalized sections hidden until then otherwise. Pages
183
+ * without personalization answer with no groups and are not asked again for
184
+ * an hour.
183
185
  *
184
186
  * Mount it once in the root layout, so links on pages without a
185
- * `PreprStack` are covered too. Clicks with a modifier key, links to other
186
- * sites and links that open a new tab or download are left alone.
187
+ * `PreprStack` are covered too. Links to other sites and links that open a
188
+ * new tab or download are left alone.
187
189
  *
188
190
  * @example
189
191
  * <PreprPersonalizationSwitch />
@@ -15,7 +15,7 @@ import {
15
15
  preprItemTag,
16
16
  preprModelTag,
17
17
  validatePreprToken
18
- } from "./chunk-CZK7SMBD.js";
18
+ } from "./chunk-O2SBSBDG.js";
19
19
 
20
20
  // src/nextjs/webhook.ts
21
21
  import { revalidateTag } from "next/cache";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.6.0-alpha.7";
2
+ var VERSION = "0.6.0-alpha.8";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -4,7 +4,7 @@ import {
4
4
  getPreprHeadersFromHeaders,
5
5
  isPersonalizedResponse,
6
6
  validatePreprToken
7
- } from "./chunk-JL33ENCM.js";
7
+ } from "./chunk-SX3MSVPK.js";
8
8
 
9
9
  // src/core/variants/constants.ts
10
10
  var PREPR_CONTEXT_ALIAS = "_preprContext";
@@ -36,7 +36,7 @@ __export(nuxt_exports, {
36
36
  module.exports = __toCommonJS(nuxt_exports);
37
37
 
38
38
  // src/version.ts
39
- var VERSION = "0.6.0-alpha.7";
39
+ var VERSION = "0.6.0-alpha.8";
40
40
 
41
41
  // src/core/constants.ts
42
42
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -11,7 +11,7 @@ import {
11
11
  handlePreprRequest,
12
12
  isPersonalizedResponse,
13
13
  validatePreprToken
14
- } from "./chunk-JL33ENCM.js";
14
+ } from "./chunk-SX3MSVPK.js";
15
15
 
16
16
  // src/nuxt/server.ts
17
17
  function getPreprUUID(headers) {
@@ -2313,7 +2313,7 @@ function isReactHydrated(text) {
2313
2313
  }
2314
2314
 
2315
2315
  // src/version.ts
2316
- var VERSION = "0.6.0-alpha.7";
2316
+ var VERSION = "0.6.0-alpha.8";
2317
2317
 
2318
2318
  // src/core/pixel.ts
2319
2319
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -2289,7 +2289,7 @@ function isReactHydrated(text) {
2289
2289
  }
2290
2290
 
2291
2291
  // src/version.ts
2292
- var VERSION = "0.6.0-alpha.7";
2292
+ var VERSION = "0.6.0-alpha.8";
2293
2293
 
2294
2294
  // src/core/pixel.ts
2295
2295
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.6.0-alpha.7";
2
+ var VERSION = "0.6.0-alpha.8";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -4,7 +4,7 @@ import {
4
4
  getPreprHeadersFromLocals,
5
5
  isPersonalizedResponse,
6
6
  validatePreprToken
7
- } from "./chunk-S7S276V7.js";
7
+ } from "./chunk-UOASSHZO.js";
8
8
 
9
9
  // src/core/variants/constants.ts
10
10
  var PREPR_CONTEXT_ALIAS = "_preprContext";
@@ -36,7 +36,7 @@ __export(sveltekit_exports, {
36
36
  module.exports = __toCommonJS(sveltekit_exports);
37
37
 
38
38
  // src/version.ts
39
- var VERSION = "0.6.0-alpha.7";
39
+ var VERSION = "0.6.0-alpha.8";
40
40
 
41
41
  // src/core/constants.ts
42
42
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -11,7 +11,7 @@ import {
11
11
  isPersonalizedResponse,
12
12
  preprHandle,
13
13
  validatePreprToken
14
- } from "./chunk-S7S276V7.js";
14
+ } from "./chunk-UOASSHZO.js";
15
15
 
16
16
  // src/sveltekit/server.ts
17
17
  function getPreprUUID(headers) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preprio/toolkit",
3
- "version": "0.6.0-alpha.7",
3
+ "version": "0.6.0-alpha.8",
4
4
  "description": "Prepr personalization, A/B testing and preview toolkit for React, Next.js, Nuxt, Astro, SvelteKit and Express",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Prepr B.V. (https://prepr.io)",