@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.
- package/README.md +46 -30
- package/dist/astro/{chunk-U7VUQL57.js → chunk-TLUVG5P7.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-VV4PRH7W.js → chunk-EE3GXKCF.js} +1 -1
- package/dist/{chunk-7HSMUULV.js → chunk-MZW2RVGY.js} +1 -1
- package/dist/{chunk-UYYTYSOD.js → chunk-XQ4DDVSW.js} +1 -1
- package/dist/{chunk-DS3QA3HF.js → chunk-YNZUYENT.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-HP2SJJSL.js +544 -0
- package/dist/nextjs/components.cjs +166 -2
- package/dist/nextjs/components.d.cts +32 -4
- package/dist/nextjs/components.d.ts +32 -4
- package/dist/nextjs/components.js +169 -4
- 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 +33 -3
- package/dist/nextjs/index.d.ts +33 -3
- package/dist/nextjs/index.js +8 -313
- package/dist/nextjs/{server-CjpELYvq.d.cts → server-BL-sqZvu.d.cts} +1 -1
- package/dist/nextjs/{server-CjpELYvq.d.ts → server-BL-sqZvu.d.ts} +1 -1
- package/dist/nuxt/{chunk-GMWF6PQ5.js → chunk-QVY3D5B2.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-5SQUG2L4.js → chunk-VSN5EX3U.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-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.
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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-
|
|
5
|
+
} from "../chunk-YNZUYENT.js";
|
|
6
6
|
import "../chunk-S5RWVMDJ.js";
|
|
7
|
-
import "../chunk-
|
|
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
|