@preprio/toolkit 0.6.0-alpha.1 → 0.6.0-alpha.3

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 (42) hide show
  1. package/README.md +46 -30
  2. package/dist/astro/{chunk-O7N6GPAH.js → chunk-CUDYSNJD.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-VI6QZTBM.js → chunk-3YZANSVY.js} +1 -1
  7. package/dist/{chunk-TBKSBXGH.js → chunk-FC53KN4H.js} +1 -1
  8. package/dist/{chunk-3JLWZQ37.js → chunk-UH2OK4XC.js} +1 -1
  9. package/dist/{chunk-YWIKKWBR.js → chunk-VY5D3IED.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-5XGJJVGX.js +544 -0
  17. package/dist/nextjs/components.cjs +166 -9
  18. package/dist/nextjs/components.d.cts +31 -3
  19. package/dist/nextjs/components.d.ts +31 -3
  20. package/dist/nextjs/components.js +166 -10
  21. package/dist/nextjs/graphql.cjs +588 -50
  22. package/dist/nextjs/graphql.d.cts +95 -28
  23. package/dist/nextjs/graphql.d.ts +95 -28
  24. package/dist/nextjs/graphql.js +287 -50
  25. package/dist/nextjs/index.cjs +3 -1
  26. package/dist/nextjs/index.d.cts +32 -3
  27. package/dist/nextjs/index.d.ts +32 -3
  28. package/dist/nextjs/index.js +8 -313
  29. package/dist/nextjs/{server-CjpELYvq.d.ts → server-BL-sqZvu.d.cts} +1 -1
  30. package/dist/nextjs/{server-CjpELYvq.d.cts → server-BL-sqZvu.d.ts} +1 -1
  31. package/dist/nuxt/{chunk-IQDSBPDN.js → chunk-KROR7RXW.js} +1 -1
  32. package/dist/nuxt/graphql.js +1 -1
  33. package/dist/nuxt/index.cjs +1 -1
  34. package/dist/nuxt/index.js +1 -1
  35. package/dist/react/components.cjs +1 -1
  36. package/dist/react/components.js +1 -1
  37. package/dist/sveltekit/{chunk-OKYZ6HPH.js → chunk-INDMNPEM.js} +1 -1
  38. package/dist/sveltekit/graphql.js +1 -1
  39. package/dist/sveltekit/index.cjs +1 -1
  40. package/dist/sveltekit/index.js +1 -1
  41. package/package.json +1 -1
  42. package/dist/nextjs/chunk-BQ6IGSH6.js +0 -231
@@ -22,6 +22,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var components_exports = {};
23
23
  __export(components_exports, {
24
24
  PreprAbSwitch: () => PreprAbSwitch,
25
+ PreprPersonalizationSwitch: () => PreprPersonalizationSwitch,
25
26
  PreprToolbar: () => PreprToolbar,
26
27
  PreprTrackingPixel: () => PreprTrackingPixel,
27
28
  PreprVariantTracking: () => PreprVariantTracking
@@ -2315,6 +2316,131 @@ function isReactHydrated(text) {
2315
2316
  return true;
2316
2317
  }
2317
2318
 
2319
+ // src/core/refresh.ts
2320
+ var PREPR_REFRESH_COOKIE = "prepr_refresh";
2321
+ var PREPR_REFRESH_HEADER = "Prepr-Refresh";
2322
+ var REFRESH_MAX_PATHS = 5;
2323
+ var SAFE_PATH = /^\/(?![/\\])[\w\-./%]{0,200}$/;
2324
+ function safePath(path) {
2325
+ return SAFE_PATH.test(path) ? path : void 0;
2326
+ }
2327
+ function isFresh(at, freshFor, now = Date.now()) {
2328
+ const age = now - at;
2329
+ return at > 0 && age >= 0 && age < freshFor;
2330
+ }
2331
+ function cleanEntries(entries) {
2332
+ const out = [];
2333
+ for (const entry of entries) {
2334
+ if (out.length === REFRESH_MAX_PATHS) break;
2335
+ if (safePath(entry.path) && !out.some((e2) => e2.path === entry.path)) {
2336
+ out.push(entry);
2337
+ }
2338
+ }
2339
+ return out;
2340
+ }
2341
+ function parseRefresh(raw) {
2342
+ return cleanEntries(
2343
+ (raw ?? "").split("|").map((pair) => {
2344
+ const split = pair.lastIndexOf("~");
2345
+ const at = Number(split < 0 ? "" : pair.slice(split + 1));
2346
+ return {
2347
+ path: split < 0 ? pair : pair.slice(0, split),
2348
+ at: Number.isFinite(at) && at > 0 ? Math.floor(at) : 0
2349
+ };
2350
+ })
2351
+ );
2352
+ }
2353
+
2354
+ // src/core/refresh-scheduler.ts
2355
+ var VIEW_DELAYS_MS = [3e3, 1e4];
2356
+ var INTENT_MIN_AGE_MS = 2e3;
2357
+ var INTENT_EVENTS = ["pointerover", "focusin", "touchstart"];
2358
+ var LISTENER = { capture: true, passive: true };
2359
+ var COOKIE_PATTERN = new RegExp(`(?:^|; )${PREPR_REFRESH_COOKIE}=([^;]*)`);
2360
+ var UNSUPPORTED_KEY = "prepr_refresh_unsupported";
2361
+ function readEntries() {
2362
+ const match = document.cookie.match(COOKIE_PATTERN);
2363
+ try {
2364
+ return parseRefresh(match ? decodeURIComponent(match[1]) : void 0);
2365
+ } catch {
2366
+ return [];
2367
+ }
2368
+ }
2369
+ function unsupported() {
2370
+ try {
2371
+ return sessionStorage.getItem(UNSUPPORTED_KEY) === "1";
2372
+ } catch {
2373
+ return false;
2374
+ }
2375
+ }
2376
+ function createPreprRefreshScheduler(options = {}) {
2377
+ const freshFor = options.freshFor ?? 1e4;
2378
+ let timers = [];
2379
+ let inFlight = false;
2380
+ let stopped = unsupported();
2381
+ function refresh() {
2382
+ if (inFlight || stopped) return;
2383
+ const paths = readEntries().map((entry) => entry.path);
2384
+ if (paths.length === 0) return;
2385
+ inFlight = true;
2386
+ const controller = new AbortController();
2387
+ fetch(location.pathname, {
2388
+ headers: { [PREPR_REFRESH_HEADER]: paths.join("|") },
2389
+ cache: "no-store",
2390
+ credentials: "same-origin",
2391
+ signal: controller.signal
2392
+ }).then((response) => {
2393
+ if (response.status === 204) return;
2394
+ stopped = true;
2395
+ controller.abort();
2396
+ try {
2397
+ sessionStorage.setItem(UNSUPPORTED_KEY, "1");
2398
+ } catch {
2399
+ }
2400
+ }).catch(() => void 0).finally(() => {
2401
+ inFlight = false;
2402
+ });
2403
+ }
2404
+ function onIntent(event) {
2405
+ const anchor = event.target?.closest?.("a[href]");
2406
+ if (!(anchor instanceof HTMLAnchorElement)) return;
2407
+ let url;
2408
+ try {
2409
+ url = new URL(anchor.href, location.href);
2410
+ } catch {
2411
+ return;
2412
+ }
2413
+ if (url.origin !== location.origin) return;
2414
+ const entry = readEntries().find((e2) => e2.path === url.pathname);
2415
+ if (entry && !isFresh(entry.at, INTENT_MIN_AGE_MS)) refresh();
2416
+ }
2417
+ function onVisibility() {
2418
+ if (document.visibilityState === "visible" && readEntries().some((entry) => !isFresh(entry.at, freshFor))) {
2419
+ refresh();
2420
+ }
2421
+ }
2422
+ for (const type of INTENT_EVENTS) {
2423
+ document.addEventListener(type, onIntent, LISTENER);
2424
+ }
2425
+ document.addEventListener("visibilitychange", onVisibility, {
2426
+ passive: true
2427
+ });
2428
+ return {
2429
+ view() {
2430
+ timers.forEach(clearTimeout);
2431
+ timers = VIEW_DELAYS_MS.map((delay) => setTimeout(refresh, delay));
2432
+ },
2433
+ stop() {
2434
+ timers.forEach(clearTimeout);
2435
+ timers = [];
2436
+ for (const type of INTENT_EVENTS) {
2437
+ document.removeEventListener(type, onIntent, LISTENER);
2438
+ }
2439
+ document.removeEventListener("visibilitychange", onVisibility);
2440
+ }
2441
+ };
2442
+ }
2443
+
2318
2444
  // src/nextjs/ab-switch.ts
2319
2445
  function applyPreprAbBucket() {
2320
2446
  const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
@@ -2338,7 +2464,11 @@ function applyPreprAbBucket() {
2338
2464
  state.__preprAbSheet.textContent = css;
2339
2465
  }
2340
2466
  }
2341
- function applyPreprDecisions() {
2467
+ function applyPreprDecisions(view) {
2468
+ const viewState = window;
2469
+ const current = view === void 0 ? location.pathname + location.search : view;
2470
+ if (viewState.__preprDecisionsView === current) return;
2471
+ viewState.__preprDecisionsView = current;
2342
2472
  const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
2343
2473
  let raw;
2344
2474
  try {
@@ -2383,7 +2513,7 @@ function applyPreprDecisions() {
2383
2513
  var import_react = require("react");
2384
2514
 
2385
2515
  // src/version.ts
2386
- var VERSION = "0.6.0-alpha.1";
2516
+ var VERSION = "0.6.0-alpha.3";
2387
2517
 
2388
2518
  // src/core/pixel.ts
2389
2519
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -2461,8 +2591,30 @@ function PreprToolbar({
2461
2591
  function PreprAbSwitch() {
2462
2592
  (0, import_react2.useLayoutEffect)(() => {
2463
2593
  applyPreprAbBucket();
2594
+ });
2595
+ return null;
2596
+ }
2597
+ function PreprPersonalizationSwitch({
2598
+ refresh = true,
2599
+ freshFor
2600
+ }) {
2601
+ const pathname = (0, import_navigation.usePathname)();
2602
+ const scheduler = (0, import_react2.useRef)(null);
2603
+ (0, import_react2.useLayoutEffect)(() => {
2464
2604
  applyPreprDecisions();
2465
2605
  });
2606
+ (0, import_react2.useEffect)(() => {
2607
+ if (!refresh) return;
2608
+ const started = createPreprRefreshScheduler({ freshFor });
2609
+ scheduler.current = started;
2610
+ return () => {
2611
+ started.stop();
2612
+ scheduler.current = null;
2613
+ };
2614
+ }, [refresh, freshFor]);
2615
+ (0, import_react2.useEffect)(() => {
2616
+ scheduler.current?.view();
2617
+ }, [pathname, refresh, freshFor]);
2466
2618
  return null;
2467
2619
  }
2468
2620
  var VARIANT_KEY_ATTRIBUTE = "data-prepr-variant-key";
@@ -2474,15 +2626,19 @@ function PreprVariantTracking({
2474
2626
  const ref = (0, import_react2.useRef)(null);
2475
2627
  const keys = variantKeys.join(" ");
2476
2628
  (0, import_react2.useLayoutEffect)(() => {
2477
- const elements = [...ref.current?.children ?? []];
2478
- const list = keys.split(" ");
2479
- const pairs = elements.length === list.length ? elements.map((element, i2) => [element, list[i2]]) : elements.slice(0, 1).map((element) => [element, list[0]]);
2480
- const tagged = pairs.filter(([element, key]) => {
2481
- if (element.hasAttribute(VARIANT_KEY_ATTRIBUTE)) return false;
2482
- element.setAttribute(VARIANT_KEY_ATTRIBUTE, key);
2483
- return true;
2629
+ let tagged = [];
2630
+ const timer = setTimeout(() => {
2631
+ const elements = [...ref.current?.children ?? []];
2632
+ const list = keys.split(" ");
2633
+ const pairs = elements.length === list.length ? elements.map((element, i2) => [element, list[i2]]) : elements.slice(0, 1).map((element) => [element, list[0]]);
2634
+ tagged = pairs.filter(([element, key]) => {
2635
+ if (element.hasAttribute(VARIANT_KEY_ATTRIBUTE)) return false;
2636
+ element.setAttribute(VARIANT_KEY_ATTRIBUTE, key);
2637
+ return true;
2638
+ });
2484
2639
  });
2485
2640
  return () => {
2641
+ clearTimeout(timer);
2486
2642
  for (const [element] of tagged) {
2487
2643
  element.removeAttribute(VARIANT_KEY_ATTRIBUTE);
2488
2644
  }
@@ -2497,6 +2653,7 @@ function PreprVariantTracking({
2497
2653
  // Annotate the CommonJS export names for ESM import in node:
2498
2654
  0 && (module.exports = {
2499
2655
  PreprAbSwitch,
2656
+ PreprPersonalizationSwitch,
2500
2657
  PreprToolbar,
2501
2658
  PreprTrackingPixel,
2502
2659
  PreprVariantTracking
@@ -23,14 +23,42 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
23
23
  */
24
24
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
25
25
  /**
26
- * Applies the visitor's A/B bucket, and the segment variants decided in the
27
- * proxy, after client-side navigation. A full page
26
+ * Applies the visitor's A/B bucket after client-side navigation. A full page
28
27
  * load is covered by the inline script `PreprStack` renders, but React does
29
28
  * not run inline scripts that arrive with a client-side navigation; a layout
30
29
  * effect still runs before the browser paints the new page. Rendered by
31
30
  * `PreprStack`; you do not need it yourself.
32
31
  */
33
32
  declare function PreprAbSwitch(): null;
33
+ interface PreprPersonalizationSwitchProps {
34
+ /**
35
+ * Keep segment decisions fresh in the background. Default `true`. Leave it
36
+ * on for the instance in your root layout; `PreprStack` renders its own
37
+ * with `false`.
38
+ */
39
+ refresh?: boolean;
40
+ /** Same as `createPreprProxy`'s `freshFor`. Default 10 000 ms. */
41
+ freshFor?: number;
42
+ }
43
+ /**
44
+ * Shows the segment variants decided in the proxy for the groups above the
45
+ * fold, before the page paints, and keeps those decisions fresh while the
46
+ * visitor browses: a short while after each page view, when the visitor
47
+ * points at a link to a personalized page, and when the tab comes back.
48
+ * Because the next page's decision is already in place when it opens, a
49
+ * visitor who joins a segment mid-visit sees it on their next page view,
50
+ * without anything on screen changing.
51
+ *
52
+ * Mount it once in the root layout. The background refreshes are answered by
53
+ * `createPreprProxy`; with a proxy that calls `resolveDecisions` itself, the
54
+ * first refresh gets a page back and refreshing stops for the browser
55
+ * session. The decisions themselves come from `createPreprProxy` or
56
+ * `resolveDecisions` in `proxy.ts`.
57
+ *
58
+ * @example
59
+ * <PreprPersonalizationSwitch />
60
+ */
61
+ declare function PreprPersonalizationSwitch({ refresh, freshFor, }: PreprPersonalizationSwitchProps): null;
34
62
  /**
35
63
  * Wraps one rendered variant and, once hydrated, puts the variant keys on the
36
64
  * elements inside, where Prepr's tracking pixel counts impressions and the
@@ -52,4 +80,4 @@ declare function PreprVariantTracking({ variantKeys, ab, children, }: {
52
80
  children?: ReactNode;
53
81
  }): ReactElement;
54
82
 
55
- export { PreprAbSwitch, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
83
+ export { PreprAbSwitch, PreprPersonalizationSwitch, type PreprPersonalizationSwitchProps, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
@@ -23,14 +23,42 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
23
23
  */
24
24
  declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
25
25
  /**
26
- * Applies the visitor's A/B bucket, and the segment variants decided in the
27
- * proxy, after client-side navigation. A full page
26
+ * Applies the visitor's A/B bucket after client-side navigation. A full page
28
27
  * load is covered by the inline script `PreprStack` renders, but React does
29
28
  * not run inline scripts that arrive with a client-side navigation; a layout
30
29
  * effect still runs before the browser paints the new page. Rendered by
31
30
  * `PreprStack`; you do not need it yourself.
32
31
  */
33
32
  declare function PreprAbSwitch(): null;
33
+ interface PreprPersonalizationSwitchProps {
34
+ /**
35
+ * Keep segment decisions fresh in the background. Default `true`. Leave it
36
+ * on for the instance in your root layout; `PreprStack` renders its own
37
+ * with `false`.
38
+ */
39
+ refresh?: boolean;
40
+ /** Same as `createPreprProxy`'s `freshFor`. Default 10 000 ms. */
41
+ freshFor?: number;
42
+ }
43
+ /**
44
+ * Shows the segment variants decided in the proxy for the groups above the
45
+ * fold, before the page paints, and keeps those decisions fresh while the
46
+ * visitor browses: a short while after each page view, when the visitor
47
+ * points at a link to a personalized page, and when the tab comes back.
48
+ * Because the next page's decision is already in place when it opens, a
49
+ * visitor who joins a segment mid-visit sees it on their next page view,
50
+ * without anything on screen changing.
51
+ *
52
+ * Mount it once in the root layout. The background refreshes are answered by
53
+ * `createPreprProxy`; with a proxy that calls `resolveDecisions` itself, the
54
+ * first refresh gets a page back and refreshing stops for the browser
55
+ * session. The decisions themselves come from `createPreprProxy` or
56
+ * `resolveDecisions` in `proxy.ts`.
57
+ *
58
+ * @example
59
+ * <PreprPersonalizationSwitch />
60
+ */
61
+ declare function PreprPersonalizationSwitch({ refresh, freshFor, }: PreprPersonalizationSwitchProps): null;
34
62
  /**
35
63
  * Wraps one rendered variant and, once hydrated, puts the variant keys on the
36
64
  * elements inside, where Prepr's tracking pixel counts impressions and the
@@ -52,4 +80,4 @@ declare function PreprVariantTracking({ variantKeys, ab, children, }: {
52
80
  children?: ReactNode;
53
81
  }): ReactElement;
54
82
 
55
- export { PreprAbSwitch, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
83
+ export { PreprAbSwitch, PreprPersonalizationSwitch, type PreprPersonalizationSwitchProps, PreprToolbar, PreprTrackingPixel, type PreprTrackingPixelProps, PreprVariantTracking };
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  createPreprPreview,
4
4
  loadTrackingPixel
5
- } from "../chunk-VI6QZTBM.js";
5
+ } from "../chunk-3YZANSVY.js";
6
6
  import "../chunk-S5RWVMDJ.js";
7
- import "../chunk-YWIKKWBR.js";
7
+ import "../chunk-VY5D3IED.js";
8
8
 
9
9
  // src/nextjs/components.ts
10
10
  import { usePathname, useRouter } from "next/navigation";
@@ -34,6 +34,131 @@ function isReactHydrated(text) {
34
34
  return true;
35
35
  }
36
36
 
37
+ // src/core/refresh.ts
38
+ var PREPR_REFRESH_COOKIE = "prepr_refresh";
39
+ var PREPR_REFRESH_HEADER = "Prepr-Refresh";
40
+ var REFRESH_MAX_PATHS = 5;
41
+ var SAFE_PATH = /^\/(?![/\\])[\w\-./%]{0,200}$/;
42
+ function safePath(path) {
43
+ return SAFE_PATH.test(path) ? path : void 0;
44
+ }
45
+ function isFresh(at, freshFor, now = Date.now()) {
46
+ const age = now - at;
47
+ return at > 0 && age >= 0 && age < freshFor;
48
+ }
49
+ function cleanEntries(entries) {
50
+ const out = [];
51
+ for (const entry of entries) {
52
+ if (out.length === REFRESH_MAX_PATHS) break;
53
+ if (safePath(entry.path) && !out.some((e) => e.path === entry.path)) {
54
+ out.push(entry);
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+ function parseRefresh(raw) {
60
+ return cleanEntries(
61
+ (raw ?? "").split("|").map((pair) => {
62
+ const split = pair.lastIndexOf("~");
63
+ const at = Number(split < 0 ? "" : pair.slice(split + 1));
64
+ return {
65
+ path: split < 0 ? pair : pair.slice(0, split),
66
+ at: Number.isFinite(at) && at > 0 ? Math.floor(at) : 0
67
+ };
68
+ })
69
+ );
70
+ }
71
+
72
+ // src/core/refresh-scheduler.ts
73
+ var VIEW_DELAYS_MS = [3e3, 1e4];
74
+ var INTENT_MIN_AGE_MS = 2e3;
75
+ var INTENT_EVENTS = ["pointerover", "focusin", "touchstart"];
76
+ var LISTENER = { capture: true, passive: true };
77
+ var COOKIE_PATTERN = new RegExp(`(?:^|; )${PREPR_REFRESH_COOKIE}=([^;]*)`);
78
+ var UNSUPPORTED_KEY = "prepr_refresh_unsupported";
79
+ function readEntries() {
80
+ const match = document.cookie.match(COOKIE_PATTERN);
81
+ try {
82
+ return parseRefresh(match ? decodeURIComponent(match[1]) : void 0);
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+ function unsupported() {
88
+ try {
89
+ return sessionStorage.getItem(UNSUPPORTED_KEY) === "1";
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ function createPreprRefreshScheduler(options = {}) {
95
+ const freshFor = options.freshFor ?? 1e4;
96
+ let timers = [];
97
+ let inFlight = false;
98
+ let stopped = unsupported();
99
+ function refresh() {
100
+ if (inFlight || stopped) return;
101
+ const paths = readEntries().map((entry) => entry.path);
102
+ if (paths.length === 0) return;
103
+ inFlight = true;
104
+ const controller = new AbortController();
105
+ fetch(location.pathname, {
106
+ headers: { [PREPR_REFRESH_HEADER]: paths.join("|") },
107
+ cache: "no-store",
108
+ credentials: "same-origin",
109
+ signal: controller.signal
110
+ }).then((response) => {
111
+ if (response.status === 204) return;
112
+ stopped = true;
113
+ controller.abort();
114
+ try {
115
+ sessionStorage.setItem(UNSUPPORTED_KEY, "1");
116
+ } catch {
117
+ }
118
+ }).catch(() => void 0).finally(() => {
119
+ inFlight = false;
120
+ });
121
+ }
122
+ function onIntent(event) {
123
+ const anchor = event.target?.closest?.("a[href]");
124
+ if (!(anchor instanceof HTMLAnchorElement)) return;
125
+ let url;
126
+ try {
127
+ url = new URL(anchor.href, location.href);
128
+ } catch {
129
+ return;
130
+ }
131
+ if (url.origin !== location.origin) return;
132
+ const entry = readEntries().find((e) => e.path === url.pathname);
133
+ if (entry && !isFresh(entry.at, INTENT_MIN_AGE_MS)) refresh();
134
+ }
135
+ function onVisibility() {
136
+ if (document.visibilityState === "visible" && readEntries().some((entry) => !isFresh(entry.at, freshFor))) {
137
+ refresh();
138
+ }
139
+ }
140
+ for (const type of INTENT_EVENTS) {
141
+ document.addEventListener(type, onIntent, LISTENER);
142
+ }
143
+ document.addEventListener("visibilitychange", onVisibility, {
144
+ passive: true
145
+ });
146
+ return {
147
+ view() {
148
+ timers.forEach(clearTimeout);
149
+ timers = VIEW_DELAYS_MS.map((delay) => setTimeout(refresh, delay));
150
+ },
151
+ stop() {
152
+ timers.forEach(clearTimeout);
153
+ timers = [];
154
+ for (const type of INTENT_EVENTS) {
155
+ document.removeEventListener(type, onIntent, LISTENER);
156
+ }
157
+ document.removeEventListener("visibilitychange", onVisibility);
158
+ }
159
+ };
160
+ }
161
+
37
162
  // src/nextjs/ab-switch.ts
38
163
  function applyPreprAbBucket() {
39
164
  const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
@@ -57,7 +182,11 @@ function applyPreprAbBucket() {
57
182
  state.__preprAbSheet.textContent = css;
58
183
  }
59
184
  }
60
- function applyPreprDecisions() {
185
+ function applyPreprDecisions(view) {
186
+ const viewState = window;
187
+ const current = view === void 0 ? location.pathname + location.search : view;
188
+ if (viewState.__preprDecisionsView === current) return;
189
+ viewState.__preprDecisionsView = current;
61
190
  const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
62
191
  let raw;
63
192
  try {
@@ -136,8 +265,30 @@ function PreprToolbar({
136
265
  function PreprAbSwitch() {
137
266
  useLayoutEffect(() => {
138
267
  applyPreprAbBucket();
268
+ });
269
+ return null;
270
+ }
271
+ function PreprPersonalizationSwitch({
272
+ refresh = true,
273
+ freshFor
274
+ }) {
275
+ const pathname = usePathname();
276
+ const scheduler = useRef(null);
277
+ useLayoutEffect(() => {
139
278
  applyPreprDecisions();
140
279
  });
280
+ useEffect2(() => {
281
+ if (!refresh) return;
282
+ const started = createPreprRefreshScheduler({ freshFor });
283
+ scheduler.current = started;
284
+ return () => {
285
+ started.stop();
286
+ scheduler.current = null;
287
+ };
288
+ }, [refresh, freshFor]);
289
+ useEffect2(() => {
290
+ scheduler.current?.view();
291
+ }, [pathname, refresh, freshFor]);
141
292
  return null;
142
293
  }
143
294
  var VARIANT_KEY_ATTRIBUTE = "data-prepr-variant-key";
@@ -149,15 +300,19 @@ function PreprVariantTracking({
149
300
  const ref = useRef(null);
150
301
  const keys = variantKeys.join(" ");
151
302
  useLayoutEffect(() => {
152
- const elements = [...ref.current?.children ?? []];
153
- const list = keys.split(" ");
154
- const pairs = elements.length === list.length ? elements.map((element, i) => [element, list[i]]) : elements.slice(0, 1).map((element) => [element, list[0]]);
155
- const tagged = pairs.filter(([element, key]) => {
156
- if (element.hasAttribute(VARIANT_KEY_ATTRIBUTE)) return false;
157
- element.setAttribute(VARIANT_KEY_ATTRIBUTE, key);
158
- return true;
303
+ let tagged = [];
304
+ const timer = setTimeout(() => {
305
+ const elements = [...ref.current?.children ?? []];
306
+ const list = keys.split(" ");
307
+ const pairs = elements.length === list.length ? elements.map((element, i) => [element, list[i]]) : elements.slice(0, 1).map((element) => [element, list[0]]);
308
+ tagged = pairs.filter(([element, key]) => {
309
+ if (element.hasAttribute(VARIANT_KEY_ATTRIBUTE)) return false;
310
+ element.setAttribute(VARIANT_KEY_ATTRIBUTE, key);
311
+ return true;
312
+ });
159
313
  });
160
314
  return () => {
315
+ clearTimeout(timer);
161
316
  for (const [element] of tagged) {
162
317
  element.removeAttribute(VARIANT_KEY_ATTRIBUTE);
163
318
  }
@@ -171,6 +326,7 @@ function PreprVariantTracking({
171
326
  }
172
327
  export {
173
328
  PreprAbSwitch,
329
+ PreprPersonalizationSwitch,
174
330
  PreprToolbar,
175
331
  PreprTrackingPixel,
176
332
  PreprVariantTracking