@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.
- package/README.md +46 -30
- package/dist/astro/{chunk-O7N6GPAH.js → chunk-CUDYSNJD.js} +1 -1
- package/dist/astro/graphql.js +1 -1
- package/dist/astro/index.cjs +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/{chunk-VI6QZTBM.js → chunk-3YZANSVY.js} +1 -1
- package/dist/{chunk-TBKSBXGH.js → chunk-FC53KN4H.js} +1 -1
- package/dist/{chunk-3JLWZQ37.js → chunk-UH2OK4XC.js} +1 -1
- package/dist/{chunk-YWIKKWBR.js → chunk-VY5D3IED.js} +1 -1
- package/dist/graphql/codegen.js +3 -3
- package/dist/graphql.js +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/nextjs/chunk-5XGJJVGX.js +544 -0
- package/dist/nextjs/components.cjs +166 -9
- package/dist/nextjs/components.d.cts +31 -3
- package/dist/nextjs/components.d.ts +31 -3
- package/dist/nextjs/components.js +166 -10
- package/dist/nextjs/graphql.cjs +588 -50
- package/dist/nextjs/graphql.d.cts +95 -28
- package/dist/nextjs/graphql.d.ts +95 -28
- package/dist/nextjs/graphql.js +287 -50
- package/dist/nextjs/index.cjs +3 -1
- package/dist/nextjs/index.d.cts +32 -3
- package/dist/nextjs/index.d.ts +32 -3
- package/dist/nextjs/index.js +8 -313
- package/dist/nextjs/{server-CjpELYvq.d.ts → server-BL-sqZvu.d.cts} +1 -1
- package/dist/nextjs/{server-CjpELYvq.d.cts → server-BL-sqZvu.d.ts} +1 -1
- package/dist/nuxt/{chunk-IQDSBPDN.js → chunk-KROR7RXW.js} +1 -1
- package/dist/nuxt/graphql.js +1 -1
- package/dist/nuxt/index.cjs +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/react/components.cjs +1 -1
- package/dist/react/components.js +1 -1
- package/dist/sveltekit/{chunk-OKYZ6HPH.js → chunk-INDMNPEM.js} +1 -1
- package/dist/sveltekit/graphql.js +1 -1
- package/dist/sveltekit/index.cjs +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/package.json +1 -1
- 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.
|
|
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
|
-
|
|
2478
|
-
const
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
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
|
|
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
|
|
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-
|
|
5
|
+
} from "../chunk-3YZANSVY.js";
|
|
6
6
|
import "../chunk-S5RWVMDJ.js";
|
|
7
|
-
import "../chunk-
|
|
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
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|