@preprio/toolkit 0.6.0-alpha.2 → 0.6.0-alpha.4

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-U7VUQL57.js → chunk-TLUVG5P7.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-VV4PRH7W.js → chunk-EE3GXKCF.js} +1 -1
  7. package/dist/{chunk-7HSMUULV.js → chunk-MZW2RVGY.js} +1 -1
  8. package/dist/{chunk-UYYTYSOD.js → chunk-XQ4DDVSW.js} +1 -1
  9. package/dist/{chunk-DS3QA3HF.js → chunk-YNZUYENT.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-HP2SJJSL.js +544 -0
  17. package/dist/nextjs/components.cjs +166 -2
  18. package/dist/nextjs/components.d.cts +32 -4
  19. package/dist/nextjs/components.d.ts +32 -4
  20. package/dist/nextjs/components.js +169 -4
  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 +33 -3
  27. package/dist/nextjs/index.d.ts +33 -3
  28. package/dist/nextjs/index.js +8 -313
  29. package/dist/nextjs/{server-CjpELYvq.d.cts → server-BL-sqZvu.d.cts} +1 -1
  30. package/dist/nextjs/{server-CjpELYvq.d.ts → server-BL-sqZvu.d.ts} +1 -1
  31. package/dist/nuxt/{chunk-GMWF6PQ5.js → chunk-QVY3D5B2.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-5SQUG2L4.js → chunk-VSN5EX3U.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-ZIUKEGEN.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.2";
2516
+ var VERSION = "0.6.0-alpha.4";
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,41 @@ 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 [scheduler, setScheduler] = (0, import_react2.useState)(
2602
+ null
2603
+ );
2604
+ (0, import_react2.useLayoutEffect)(() => {
2464
2605
  applyPreprDecisions();
2465
2606
  });
2607
+ (0, import_react2.useEffect)(() => {
2608
+ if (!refresh) return;
2609
+ const started = createPreprRefreshScheduler({ freshFor });
2610
+ setScheduler(started);
2611
+ return () => {
2612
+ started.stop();
2613
+ setScheduler(null);
2614
+ };
2615
+ }, [refresh, freshFor]);
2616
+ return scheduler ? (0, import_react2.createElement)(
2617
+ import_react2.Suspense,
2618
+ { fallback: null },
2619
+ (0, import_react2.createElement)(ViewCounter, { scheduler })
2620
+ ) : null;
2621
+ }
2622
+ function ViewCounter({
2623
+ scheduler
2624
+ }) {
2625
+ const pathname = (0, import_navigation.usePathname)();
2626
+ (0, import_react2.useEffect)(() => {
2627
+ scheduler.view();
2628
+ }, [pathname, scheduler]);
2466
2629
  return null;
2467
2630
  }
2468
2631
  var VARIANT_KEY_ATTRIBUTE = "data-prepr-variant-key";
@@ -2501,6 +2664,7 @@ function PreprVariantTracking({
2501
2664
  // Annotate the CommonJS export names for ESM import in node:
2502
2665
  0 && (module.exports = {
2503
2666
  PreprAbSwitch,
2667
+ PreprPersonalizationSwitch,
2504
2668
  PreprToolbar,
2505
2669
  PreprTrackingPixel,
2506
2670
  PreprVariantTracking
@@ -1,4 +1,4 @@
1
- import { ReactNode, ReactElement } from 'react';
1
+ import { ReactElement, ReactNode } from 'react';
2
2
  import { k as PreprToolbarComponentProps } from '../types-BYH9XqmN.cjs';
3
3
  import { P as PreprPixelConfig } from '../pixel-DwW7cXJm.cjs';
4
4
 
@@ -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): ReactElement | 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 };
@@ -1,4 +1,4 @@
1
- import { ReactNode, ReactElement } from 'react';
1
+ import { ReactElement, ReactNode } from 'react';
2
2
  import { k as PreprToolbarComponentProps } from '../types-BYH9XqmN.js';
3
3
  import { P as PreprPixelConfig } from '../pixel-DwW7cXJm.js';
4
4
 
@@ -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): ReactElement | 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-DS3QA3HF.js";
5
+ } from "../chunk-YNZUYENT.js";
6
6
  import "../chunk-S5RWVMDJ.js";
7
- import "../chunk-VV4PRH7W.js";
7
+ import "../chunk-EE3GXKCF.js";
8
8
 
9
9
  // src/nextjs/components.ts
10
10
  import { usePathname, useRouter } from "next/navigation";
@@ -12,7 +12,9 @@ import {
12
12
  createElement,
13
13
  useEffect as useEffect2,
14
14
  useLayoutEffect,
15
- useRef
15
+ useRef,
16
+ useState,
17
+ Suspense
16
18
  } from "react";
17
19
 
18
20
  // src/react/hydration.ts
@@ -34,6 +36,131 @@ function isReactHydrated(text) {
34
36
  return true;
35
37
  }
36
38
 
39
+ // src/core/refresh.ts
40
+ var PREPR_REFRESH_COOKIE = "prepr_refresh";
41
+ var PREPR_REFRESH_HEADER = "Prepr-Refresh";
42
+ var REFRESH_MAX_PATHS = 5;
43
+ var SAFE_PATH = /^\/(?![/\\])[\w\-./%]{0,200}$/;
44
+ function safePath(path) {
45
+ return SAFE_PATH.test(path) ? path : void 0;
46
+ }
47
+ function isFresh(at, freshFor, now = Date.now()) {
48
+ const age = now - at;
49
+ return at > 0 && age >= 0 && age < freshFor;
50
+ }
51
+ function cleanEntries(entries) {
52
+ const out = [];
53
+ for (const entry of entries) {
54
+ if (out.length === REFRESH_MAX_PATHS) break;
55
+ if (safePath(entry.path) && !out.some((e) => e.path === entry.path)) {
56
+ out.push(entry);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ function parseRefresh(raw) {
62
+ return cleanEntries(
63
+ (raw ?? "").split("|").map((pair) => {
64
+ const split = pair.lastIndexOf("~");
65
+ const at = Number(split < 0 ? "" : pair.slice(split + 1));
66
+ return {
67
+ path: split < 0 ? pair : pair.slice(0, split),
68
+ at: Number.isFinite(at) && at > 0 ? Math.floor(at) : 0
69
+ };
70
+ })
71
+ );
72
+ }
73
+
74
+ // src/core/refresh-scheduler.ts
75
+ var VIEW_DELAYS_MS = [3e3, 1e4];
76
+ var INTENT_MIN_AGE_MS = 2e3;
77
+ var INTENT_EVENTS = ["pointerover", "focusin", "touchstart"];
78
+ var LISTENER = { capture: true, passive: true };
79
+ var COOKIE_PATTERN = new RegExp(`(?:^|; )${PREPR_REFRESH_COOKIE}=([^;]*)`);
80
+ var UNSUPPORTED_KEY = "prepr_refresh_unsupported";
81
+ function readEntries() {
82
+ const match = document.cookie.match(COOKIE_PATTERN);
83
+ try {
84
+ return parseRefresh(match ? decodeURIComponent(match[1]) : void 0);
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+ function unsupported() {
90
+ try {
91
+ return sessionStorage.getItem(UNSUPPORTED_KEY) === "1";
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ function createPreprRefreshScheduler(options = {}) {
97
+ const freshFor = options.freshFor ?? 1e4;
98
+ let timers = [];
99
+ let inFlight = false;
100
+ let stopped = unsupported();
101
+ function refresh() {
102
+ if (inFlight || stopped) return;
103
+ const paths = readEntries().map((entry) => entry.path);
104
+ if (paths.length === 0) return;
105
+ inFlight = true;
106
+ const controller = new AbortController();
107
+ fetch(location.pathname, {
108
+ headers: { [PREPR_REFRESH_HEADER]: paths.join("|") },
109
+ cache: "no-store",
110
+ credentials: "same-origin",
111
+ signal: controller.signal
112
+ }).then((response) => {
113
+ if (response.status === 204) return;
114
+ stopped = true;
115
+ controller.abort();
116
+ try {
117
+ sessionStorage.setItem(UNSUPPORTED_KEY, "1");
118
+ } catch {
119
+ }
120
+ }).catch(() => void 0).finally(() => {
121
+ inFlight = false;
122
+ });
123
+ }
124
+ function onIntent(event) {
125
+ const anchor = event.target?.closest?.("a[href]");
126
+ if (!(anchor instanceof HTMLAnchorElement)) return;
127
+ let url;
128
+ try {
129
+ url = new URL(anchor.href, location.href);
130
+ } catch {
131
+ return;
132
+ }
133
+ if (url.origin !== location.origin) return;
134
+ const entry = readEntries().find((e) => e.path === url.pathname);
135
+ if (entry && !isFresh(entry.at, INTENT_MIN_AGE_MS)) refresh();
136
+ }
137
+ function onVisibility() {
138
+ if (document.visibilityState === "visible" && readEntries().some((entry) => !isFresh(entry.at, freshFor))) {
139
+ refresh();
140
+ }
141
+ }
142
+ for (const type of INTENT_EVENTS) {
143
+ document.addEventListener(type, onIntent, LISTENER);
144
+ }
145
+ document.addEventListener("visibilitychange", onVisibility, {
146
+ passive: true
147
+ });
148
+ return {
149
+ view() {
150
+ timers.forEach(clearTimeout);
151
+ timers = VIEW_DELAYS_MS.map((delay) => setTimeout(refresh, delay));
152
+ },
153
+ stop() {
154
+ timers.forEach(clearTimeout);
155
+ timers = [];
156
+ for (const type of INTENT_EVENTS) {
157
+ document.removeEventListener(type, onIntent, LISTENER);
158
+ }
159
+ document.removeEventListener("visibilitychange", onVisibility);
160
+ }
161
+ };
162
+ }
163
+
37
164
  // src/nextjs/ab-switch.ts
38
165
  function applyPreprAbBucket() {
39
166
  const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
@@ -57,7 +184,11 @@ function applyPreprAbBucket() {
57
184
  state.__preprAbSheet.textContent = css;
58
185
  }
59
186
  }
60
- function applyPreprDecisions() {
187
+ function applyPreprDecisions(view) {
188
+ const viewState = window;
189
+ const current = view === void 0 ? location.pathname + location.search : view;
190
+ if (viewState.__preprDecisionsView === current) return;
191
+ viewState.__preprDecisionsView = current;
61
192
  const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
62
193
  let raw;
63
194
  try {
@@ -136,8 +267,41 @@ function PreprToolbar({
136
267
  function PreprAbSwitch() {
137
268
  useLayoutEffect(() => {
138
269
  applyPreprAbBucket();
270
+ });
271
+ return null;
272
+ }
273
+ function PreprPersonalizationSwitch({
274
+ refresh = true,
275
+ freshFor
276
+ }) {
277
+ const [scheduler, setScheduler] = useState(
278
+ null
279
+ );
280
+ useLayoutEffect(() => {
139
281
  applyPreprDecisions();
140
282
  });
283
+ useEffect2(() => {
284
+ if (!refresh) return;
285
+ const started = createPreprRefreshScheduler({ freshFor });
286
+ setScheduler(started);
287
+ return () => {
288
+ started.stop();
289
+ setScheduler(null);
290
+ };
291
+ }, [refresh, freshFor]);
292
+ return scheduler ? createElement(
293
+ Suspense,
294
+ { fallback: null },
295
+ createElement(ViewCounter, { scheduler })
296
+ ) : null;
297
+ }
298
+ function ViewCounter({
299
+ scheduler
300
+ }) {
301
+ const pathname = usePathname();
302
+ useEffect2(() => {
303
+ scheduler.view();
304
+ }, [pathname, scheduler]);
141
305
  return null;
142
306
  }
143
307
  var VARIANT_KEY_ATTRIBUTE = "data-prepr-variant-key";
@@ -175,6 +339,7 @@ function PreprVariantTracking({
175
339
  }
176
340
  export {
177
341
  PreprAbSwitch,
342
+ PreprPersonalizationSwitch,
178
343
  PreprToolbar,
179
344
  PreprTrackingPixel,
180
345
  PreprVariantTracking