@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
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  createPreprPreview,
4
4
  loadTrackingPixel
5
- } from "../chunk-U7TRFVK7.js";
5
+ } from "../chunk-J66UOCK6.js";
6
6
  import "../chunk-S5RWVMDJ.js";
7
- import "../chunk-VQIPTUN4.js";
7
+ import "../chunk-5QIWVDP2.js";
8
8
 
9
9
  // src/nextjs/components.ts
10
10
  import { usePathname, useRouter } from "next/navigation";
@@ -35,10 +35,34 @@ function isReactHydrated(text) {
35
35
  }
36
36
 
37
37
  // src/nextjs/decision-switch.ts
38
- function applyPreprDecisions(decision) {
38
+ function applyPreprDecisions(decision, options = {}) {
39
39
  const state = window;
40
40
  const key = location.pathname + location.search;
41
- if (decision !== void 0) state.__preprDecision = { key, value: decision };
41
+ const previous = state.__preprDecision;
42
+ let value = decision;
43
+ if (value !== void 0 && value !== "*" && options.refresh && previous && previous.key === key && previous.value !== "*") {
44
+ const pairs = (from) => {
45
+ const out = {};
46
+ from.split("~").forEach((pair) => {
47
+ const colon = pair.indexOf(":");
48
+ if (colon > 0) out[pair.slice(0, colon)] = pair;
49
+ });
50
+ return out;
51
+ };
52
+ const before = pairs(previous.value);
53
+ const after = pairs(value);
54
+ document.querySelectorAll("[data-prepr-v][data-prepr-show]").forEach((element) => {
55
+ const range = document.createRange();
56
+ range.selectNodeContents(element);
57
+ if (range.getBoundingClientRect().top >= window.innerHeight) return;
58
+ (element.getAttribute("data-prepr-v") || "").split(" ").forEach((token) => {
59
+ const group = token.slice(0, token.indexOf(":"));
60
+ if (before[group]) after[group] = before[group];
61
+ });
62
+ });
63
+ value = Object.keys(after).map((group) => after[group]).join("~");
64
+ }
65
+ if (value !== void 0) state.__preprDecision = { key, value };
42
66
  const current = state.__preprDecision;
43
67
  if (!current || current.key !== key) return false;
44
68
  document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
@@ -64,6 +88,70 @@ function applyPreprDecisions(decision) {
64
88
  });
65
89
  return true;
66
90
  }
91
+ function requestPreprDecision(key, options = {}) {
92
+ const fresh = 1e4;
93
+ const keep = 18e5;
94
+ const keepStatic = 36e5;
95
+ const state = window;
96
+ const asking = state.__preprAsking = state.__preprAsking || {};
97
+ const read = () => {
98
+ try {
99
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
100
+ return all && typeof all === "object" ? all : {};
101
+ } catch {
102
+ return {};
103
+ }
104
+ };
105
+ const ask = () => {
106
+ if (!asking[key]) {
107
+ asking[key] = fetch(key, {
108
+ headers: { "Prepr-Decide": "1" },
109
+ cache: "no-store",
110
+ credentials: "same-origin",
111
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
112
+ // covers a proxy that never answers.
113
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
114
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
115
+ delete asking[key];
116
+ try {
117
+ const all = read();
118
+ const at = Date.now();
119
+ Object.keys(all).forEach((stored) => {
120
+ const entry = all[stored];
121
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
122
+ delete all[stored];
123
+ });
124
+ const kept = all[key];
125
+ if (answer !== "*" || !kept || kept.value === "*")
126
+ all[key] = { value: answer, at };
127
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
128
+ } catch {
129
+ }
130
+ return answer;
131
+ });
132
+ }
133
+ return asking[key];
134
+ };
135
+ const last = read()[key];
136
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
137
+ const value = last && typeof last.value === "string" ? last.value : null;
138
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
139
+ if (value !== "" && age >= fresh) {
140
+ ask().then((answer) => {
141
+ if (answer !== "*" && answer !== value && options.onChange)
142
+ options.onChange(answer);
143
+ });
144
+ }
145
+ return { result: value, value: Promise.resolve(value) };
146
+ }
147
+ const pending = {
148
+ value: ask()
149
+ };
150
+ pending.value.then((answer) => {
151
+ pending.result = answer;
152
+ });
153
+ return pending;
154
+ }
67
155
 
68
156
  // src/react/components.ts
69
157
  import { useEffect } from "react";
@@ -100,35 +188,22 @@ function PreprToolbar({
100
188
  }, []);
101
189
  return null;
102
190
  }
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
191
  var shown = /* @__PURE__ */ new Map();
128
192
  var historyStep = false;
129
193
  var onPopState = () => {
130
194
  historyStep = true;
131
195
  };
196
+ function requestDecision(key, personalized = false) {
197
+ return requestPreprDecision(key, {
198
+ personalized,
199
+ onChange: (value) => {
200
+ if (location.pathname + location.search !== key) return;
201
+ applyPreprDecisions(value, { refresh: true });
202
+ const state = window.__preprDecision;
203
+ if (state) shown.set(key, state.value);
204
+ }
205
+ });
206
+ }
132
207
  function linkTarget(event) {
133
208
  const anchor = event.target?.closest?.("a[href]");
134
209
  if (!(anchor instanceof HTMLAnchorElement)) return null;
@@ -140,55 +215,37 @@ function linkTarget(event) {
140
215
  if (url.origin !== location.origin || key === location.pathname + location.search) {
141
216
  return null;
142
217
  }
143
- return { anchor, key };
218
+ return key;
219
+ }
220
+ var HOVER_DELAY_MS = 65;
221
+ var hovering = null;
222
+ function onPointerOver(event) {
223
+ if (event.pointerType !== "mouse") return;
224
+ const key = linkTarget(event);
225
+ if (hovering?.key === key) return;
226
+ if (hovering) clearTimeout(hovering.timer);
227
+ hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
144
228
  }
145
229
  function onPointerDown(event) {
146
230
  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
- });
231
+ const key = linkTarget(event);
232
+ if (key) requestDecision(key);
179
233
  }
180
234
  var listeners = 0;
181
235
  function useDecisionRequests() {
182
236
  useEffect2(() => {
183
237
  if (listeners++ === 0) {
184
- window.addEventListener("pointerdown", onPointerDown, true);
185
- window.addEventListener("click", onClick, true);
238
+ window.addEventListener("pointerover", onPointerOver, { passive: true });
239
+ window.addEventListener("pointerdown", onPointerDown, {
240
+ capture: true,
241
+ passive: true
242
+ });
186
243
  window.addEventListener("popstate", onPopState);
187
244
  }
188
245
  return () => {
189
246
  if (--listeners === 0) {
247
+ window.removeEventListener("pointerover", onPointerOver);
190
248
  window.removeEventListener("pointerdown", onPointerDown, true);
191
- window.removeEventListener("click", onClick, true);
192
249
  window.removeEventListener("popstate", onPopState);
193
250
  }
194
251
  };
@@ -212,12 +269,16 @@ function PreprDecisionApplier() {
212
269
  remember();
213
270
  return;
214
271
  }
272
+ const pending = requestDecision(key, true);
273
+ if (pending.result !== void 0) {
274
+ applyPreprDecisions(pending.result);
275
+ remember();
276
+ return;
277
+ }
215
278
  let live = true;
216
- const started = window.__preprPending;
217
- const value = started?.key === key ? started.value : requestDecision(key).value;
218
- void value.then((value2) => {
279
+ void pending.value.then((value) => {
219
280
  if (live && location.pathname + location.search === key) {
220
- applyPreprDecisions(value2);
281
+ applyPreprDecisions(value);
221
282
  remember();
222
283
  }
223
284
  });
@@ -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.9";
105
105
 
106
106
  // src/core/features.ts
107
107
  function isEnabled(config) {
@@ -1451,10 +1451,34 @@ function createPreprProxy(options) {
1451
1451
  var import_react = require("react");
1452
1452
 
1453
1453
  // src/nextjs/decision-switch.ts
1454
- function applyPreprDecisions(decision) {
1454
+ function applyPreprDecisions(decision, options = {}) {
1455
1455
  const state = window;
1456
1456
  const key = location.pathname + location.search;
1457
- if (decision !== void 0) state.__preprDecision = { key, value: decision };
1457
+ const previous = state.__preprDecision;
1458
+ let value = decision;
1459
+ if (value !== void 0 && value !== "*" && options.refresh && previous && previous.key === key && previous.value !== "*") {
1460
+ const pairs = (from) => {
1461
+ const out = {};
1462
+ from.split("~").forEach((pair) => {
1463
+ const colon = pair.indexOf(":");
1464
+ if (colon > 0) out[pair.slice(0, colon)] = pair;
1465
+ });
1466
+ return out;
1467
+ };
1468
+ const before = pairs(previous.value);
1469
+ const after = pairs(value);
1470
+ document.querySelectorAll("[data-prepr-v][data-prepr-show]").forEach((element) => {
1471
+ const range = document.createRange();
1472
+ range.selectNodeContents(element);
1473
+ if (range.getBoundingClientRect().top >= window.innerHeight) return;
1474
+ (element.getAttribute("data-prepr-v") || "").split(" ").forEach((token) => {
1475
+ const group = token.slice(0, token.indexOf(":"));
1476
+ if (before[group]) after[group] = before[group];
1477
+ });
1478
+ });
1479
+ value = Object.keys(after).map((group) => after[group]).join("~");
1480
+ }
1481
+ if (value !== void 0) state.__preprDecision = { key, value };
1458
1482
  const current = state.__preprDecision;
1459
1483
  if (!current || current.key !== key) return false;
1460
1484
  document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
@@ -1480,35 +1504,83 @@ function applyPreprDecisions(decision) {
1480
1504
  });
1481
1505
  return true;
1482
1506
  }
1483
- function startPreprDecision() {
1507
+ function requestPreprDecision(key, options = {}) {
1508
+ const fresh = 1e4;
1509
+ const keep = 18e5;
1510
+ const keepStatic = 36e5;
1484
1511
  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) => {
1512
+ const asking = state.__preprAsking = state.__preprAsking || {};
1513
+ const read = () => {
1499
1514
  try {
1500
- sessionStorage.setItem(
1501
- "prepr_decision",
1502
- JSON.stringify({ key, value: answer, at: Date.now() })
1503
- );
1515
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
1516
+ return all && typeof all === "object" ? all : {};
1504
1517
  } catch {
1518
+ return {};
1505
1519
  }
1506
- return answer;
1520
+ };
1521
+ const ask = () => {
1522
+ if (!asking[key]) {
1523
+ asking[key] = fetch(key, {
1524
+ headers: { "Prepr-Decide": "1" },
1525
+ cache: "no-store",
1526
+ credentials: "same-origin",
1527
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
1528
+ // covers a proxy that never answers.
1529
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
1530
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1531
+ delete asking[key];
1532
+ try {
1533
+ const all = read();
1534
+ const at = Date.now();
1535
+ Object.keys(all).forEach((stored) => {
1536
+ const entry = all[stored];
1537
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
1538
+ delete all[stored];
1539
+ });
1540
+ const kept = all[key];
1541
+ if (answer !== "*" || !kept || kept.value === "*")
1542
+ all[key] = { value: answer, at };
1543
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
1544
+ } catch {
1545
+ }
1546
+ return answer;
1547
+ });
1548
+ }
1549
+ return asking[key];
1550
+ };
1551
+ const last = read()[key];
1552
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
1553
+ const value = last && typeof last.value === "string" ? last.value : null;
1554
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
1555
+ if (value !== "" && age >= fresh) {
1556
+ ask().then((answer) => {
1557
+ if (answer !== "*" && answer !== value && options.onChange)
1558
+ options.onChange(answer);
1559
+ });
1560
+ }
1561
+ return { result: value, value: Promise.resolve(value) };
1562
+ }
1563
+ const pending = {
1564
+ value: ask()
1565
+ };
1566
+ pending.value.then((answer) => {
1567
+ pending.result = answer;
1507
1568
  });
1508
- state.__preprPending = { key, value };
1509
- value.then((answer) => {
1510
- if (location.pathname + location.search === key) state.__preprApply(answer);
1569
+ return pending;
1570
+ }
1571
+ function startPreprDecision() {
1572
+ const state = window;
1573
+ const key = location.pathname + location.search;
1574
+ const apply = (answer, refresh) => {
1575
+ if (location.pathname + location.search === key)
1576
+ state.__preprApply(answer, { refresh });
1577
+ };
1578
+ const pending = state.__preprRequest(key, {
1579
+ onChange: (answer) => apply(answer, true),
1580
+ personalized: true
1511
1581
  });
1582
+ if (pending.result !== void 0) apply(pending.result);
1583
+ else pending.value.then((answer) => apply(answer));
1512
1584
  }
1513
1585
 
1514
1586
  // src/nextjs/stack.ts
@@ -1518,7 +1590,7 @@ var DEFAULT_ATTRIBUTE = "data-prepr-default";
1518
1590
  var NESTED_ATTRIBUTE = "data-prepr-nested";
1519
1591
  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
1592
  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()})();`;
1593
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
1522
1594
  var APPLY_SCRIPT = "__preprApply()";
1523
1595
  function variantKeysOf(items) {
1524
1596
  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-CMV6ZP6E.js";
15
15
 
16
16
  // src/nextjs/graphql.ts
17
17
  import * as nextCache from "next/cache";
@@ -1038,10 +1038,34 @@ import {
1038
1038
  } from "react";
1039
1039
 
1040
1040
  // src/nextjs/decision-switch.ts
1041
- function applyPreprDecisions(decision) {
1041
+ function applyPreprDecisions(decision, options = {}) {
1042
1042
  const state = window;
1043
1043
  const key = location.pathname + location.search;
1044
- if (decision !== void 0) state.__preprDecision = { key, value: decision };
1044
+ const previous = state.__preprDecision;
1045
+ let value = decision;
1046
+ if (value !== void 0 && value !== "*" && options.refresh && previous && previous.key === key && previous.value !== "*") {
1047
+ const pairs = (from) => {
1048
+ const out = {};
1049
+ from.split("~").forEach((pair) => {
1050
+ const colon = pair.indexOf(":");
1051
+ if (colon > 0) out[pair.slice(0, colon)] = pair;
1052
+ });
1053
+ return out;
1054
+ };
1055
+ const before = pairs(previous.value);
1056
+ const after = pairs(value);
1057
+ document.querySelectorAll("[data-prepr-v][data-prepr-show]").forEach((element) => {
1058
+ const range = document.createRange();
1059
+ range.selectNodeContents(element);
1060
+ if (range.getBoundingClientRect().top >= window.innerHeight) return;
1061
+ (element.getAttribute("data-prepr-v") || "").split(" ").forEach((token) => {
1062
+ const group = token.slice(0, token.indexOf(":"));
1063
+ if (before[group]) after[group] = before[group];
1064
+ });
1065
+ });
1066
+ value = Object.keys(after).map((group) => after[group]).join("~");
1067
+ }
1068
+ if (value !== void 0) state.__preprDecision = { key, value };
1045
1069
  const current = state.__preprDecision;
1046
1070
  if (!current || current.key !== key) return false;
1047
1071
  document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
@@ -1067,35 +1091,83 @@ function applyPreprDecisions(decision) {
1067
1091
  });
1068
1092
  return true;
1069
1093
  }
1070
- function startPreprDecision() {
1094
+ function requestPreprDecision(key, options = {}) {
1095
+ const fresh = 1e4;
1096
+ const keep = 18e5;
1097
+ const keepStatic = 36e5;
1071
1098
  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) => {
1099
+ const asking = state.__preprAsking = state.__preprAsking || {};
1100
+ const read = () => {
1086
1101
  try {
1087
- sessionStorage.setItem(
1088
- "prepr_decision",
1089
- JSON.stringify({ key, value: answer, at: Date.now() })
1090
- );
1102
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
1103
+ return all && typeof all === "object" ? all : {};
1091
1104
  } catch {
1105
+ return {};
1092
1106
  }
1093
- return answer;
1107
+ };
1108
+ const ask = () => {
1109
+ if (!asking[key]) {
1110
+ asking[key] = fetch(key, {
1111
+ headers: { "Prepr-Decide": "1" },
1112
+ cache: "no-store",
1113
+ credentials: "same-origin",
1114
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
1115
+ // covers a proxy that never answers.
1116
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
1117
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1118
+ delete asking[key];
1119
+ try {
1120
+ const all = read();
1121
+ const at = Date.now();
1122
+ Object.keys(all).forEach((stored) => {
1123
+ const entry = all[stored];
1124
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
1125
+ delete all[stored];
1126
+ });
1127
+ const kept = all[key];
1128
+ if (answer !== "*" || !kept || kept.value === "*")
1129
+ all[key] = { value: answer, at };
1130
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
1131
+ } catch {
1132
+ }
1133
+ return answer;
1134
+ });
1135
+ }
1136
+ return asking[key];
1137
+ };
1138
+ const last = read()[key];
1139
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
1140
+ const value = last && typeof last.value === "string" ? last.value : null;
1141
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
1142
+ if (value !== "" && age >= fresh) {
1143
+ ask().then((answer) => {
1144
+ if (answer !== "*" && answer !== value && options.onChange)
1145
+ options.onChange(answer);
1146
+ });
1147
+ }
1148
+ return { result: value, value: Promise.resolve(value) };
1149
+ }
1150
+ const pending = {
1151
+ value: ask()
1152
+ };
1153
+ pending.value.then((answer) => {
1154
+ pending.result = answer;
1094
1155
  });
1095
- state.__preprPending = { key, value };
1096
- value.then((answer) => {
1097
- if (location.pathname + location.search === key) state.__preprApply(answer);
1156
+ return pending;
1157
+ }
1158
+ function startPreprDecision() {
1159
+ const state = window;
1160
+ const key = location.pathname + location.search;
1161
+ const apply = (answer, refresh) => {
1162
+ if (location.pathname + location.search === key)
1163
+ state.__preprApply(answer, { refresh });
1164
+ };
1165
+ const pending = state.__preprRequest(key, {
1166
+ onChange: (answer) => apply(answer, true),
1167
+ personalized: true
1098
1168
  });
1169
+ if (pending.result !== void 0) apply(pending.result);
1170
+ else pending.value.then((answer) => apply(answer));
1099
1171
  }
1100
1172
 
1101
1173
  // src/nextjs/stack.ts
@@ -1105,7 +1177,7 @@ var DEFAULT_ATTRIBUTE = "data-prepr-default";
1105
1177
  var NESTED_ATTRIBUTE = "data-prepr-nested";
1106
1178
  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
1179
  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()})();`;
1180
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
1109
1181
  var APPLY_SCRIPT = "__preprApply()";
1110
1182
  function variantKeysOf(items) {
1111
1183
  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.9";
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 />