@preprio/toolkit 0.6.0-alpha.6 → 0.6.0-alpha.8

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 (52) hide show
  1. package/README.md +138 -52
  2. package/dist/astro/{chunk-MWRA3732.js → chunk-R2YPGDVU.js} +1 -1
  3. package/dist/astro/graphql.cjs +58 -10
  4. package/dist/astro/graphql.d.cts +33 -38
  5. package/dist/astro/graphql.d.ts +33 -38
  6. package/dist/astro/graphql.js +59 -11
  7. package/dist/astro/index.cjs +1 -1
  8. package/dist/astro/index.js +1 -1
  9. package/dist/{chunk-Y7DWVOOY.js → chunk-2QGUP7VQ.js} +1 -1
  10. package/dist/{chunk-NSQOGI5K.js → chunk-ATPB7GEC.js} +1 -1
  11. package/dist/{chunk-W6AVX6FI.js → chunk-OY7OKO6Q.js} +52 -5
  12. package/dist/{chunk-OQGEL4TV.js → chunk-T7ESBVBI.js} +1 -1
  13. package/dist/graphql/codegen.cjs +51 -4
  14. package/dist/graphql/codegen.js +3 -3
  15. package/dist/graphql.cjs +89 -10
  16. package/dist/graphql.d.cts +69 -39
  17. package/dist/graphql.d.ts +69 -39
  18. package/dist/graphql.js +38 -9
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.d.cts +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +3 -3
  23. package/dist/nextjs/{chunk-PIQF5ZI5.js → chunk-O2SBSBDG.js} +58 -53
  24. package/dist/nextjs/components.cjs +205 -236
  25. package/dist/nextjs/components.d.cts +27 -22
  26. package/dist/nextjs/components.d.ts +27 -22
  27. package/dist/nextjs/components.js +198 -231
  28. package/dist/nextjs/graphql.cjs +418 -508
  29. package/dist/nextjs/graphql.d.cts +131 -135
  30. package/dist/nextjs/graphql.d.ts +131 -135
  31. package/dist/nextjs/graphql.js +353 -455
  32. package/dist/nextjs/index.cjs +114 -54
  33. package/dist/nextjs/index.d.cts +59 -33
  34. package/dist/nextjs/index.d.ts +59 -33
  35. package/dist/nextjs/index.js +108 -5
  36. package/dist/nuxt/{chunk-PF6GVWHV.js → chunk-SX3MSVPK.js} +1 -1
  37. package/dist/nuxt/graphql.cjs +58 -10
  38. package/dist/nuxt/graphql.d.cts +33 -38
  39. package/dist/nuxt/graphql.d.ts +33 -38
  40. package/dist/nuxt/graphql.js +59 -11
  41. package/dist/nuxt/index.cjs +1 -1
  42. package/dist/nuxt/index.js +1 -1
  43. package/dist/react/components.cjs +1 -1
  44. package/dist/react/components.js +1 -1
  45. package/dist/sveltekit/{chunk-5K7HWMIF.js → chunk-UOASSHZO.js} +1 -1
  46. package/dist/sveltekit/graphql.cjs +58 -10
  47. package/dist/sveltekit/graphql.d.cts +33 -38
  48. package/dist/sveltekit/graphql.d.ts +33 -38
  49. package/dist/sveltekit/graphql.js +59 -11
  50. package/dist/sveltekit/index.cjs +1 -1
  51. package/dist/sveltekit/index.js +1 -1
  52. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  import { NextResponse } from "next/server";
3
3
 
4
4
  // src/version.ts
5
- var VERSION = "0.6.0-alpha.6";
5
+ var VERSION = "0.6.0-alpha.8";
6
6
 
7
7
  // src/core/constants.ts
8
8
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -426,55 +426,6 @@ function isPersonalizedResponse(source) {
426
426
  return PERSONALIZED_VARY.test(headers2.get("vary") ?? "");
427
427
  }
428
428
 
429
- // src/nextjs/bucket.ts
430
- var PREPR_BUCKET_COOKIE = "prepr_ab_bucket";
431
- var isBucket = (value) => value === "A" || value === "B";
432
- function setBucketCookie(request, response, bucket) {
433
- response.cookies.set(PREPR_BUCKET_COOKIE, bucket, {
434
- path: "/",
435
- sameSite: "lax",
436
- // Readable on purpose: PreprStack's inline switch reads it before first
437
- // paint. It holds only "A" or "B".
438
- httpOnly: false,
439
- secure: request.nextUrl.protocol === "https:",
440
- maxAge: 60 * 60 * 24 * 365
441
- });
442
- }
443
- async function resolvePreprBucket(request, response, options) {
444
- const stored = request.cookies.get(PREPR_BUCKET_COOKIE)?.value;
445
- const forced = response.headers.get("x-middleware-request-prepr-abtesting") ?? request.cookies.get(COOKIE_VARIANT)?.value;
446
- if (options.preview && isBucket(forced)) {
447
- if (forced !== stored) setBucketCookie(request, response, forced);
448
- return forced;
449
- }
450
- if (isBucket(stored)) return stored;
451
- if (options.onlyIf) {
452
- const needed = await Promise.resolve().then(options.onlyIf).catch(() => true);
453
- if (!needed) return "A";
454
- }
455
- const customerId = request.cookies.get(COOKIE_UID)?.value ?? response.cookies.get(COOKIE_UID)?.value;
456
- if (!customerId) return "A";
457
- try {
458
- const res = await fetch(options.url, {
459
- method: "POST",
460
- headers: {
461
- "Content-Type": "application/json",
462
- "Prepr-Customer-Id": customerId
463
- },
464
- body: JSON.stringify({ query: "{ __typename }" }),
465
- cache: "no-store",
466
- signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
467
- });
468
- const bucket = res.headers.get("x-prepr-customer-bucket");
469
- if (isBucket(bucket)) {
470
- setBucketCookie(request, response, bucket);
471
- return bucket;
472
- }
473
- } catch {
474
- }
475
- return "A";
476
- }
477
-
478
429
  // src/nextjs/draft.ts
479
430
  import { cookies, draftMode } from "next/headers";
480
431
  import { redirect } from "next/navigation";
@@ -525,6 +476,56 @@ function createPreprDraftRoute(options) {
525
476
  };
526
477
  }
527
478
 
479
+ // src/core/graphql/cache-tags.ts
480
+ var MAX_TAGS = 128;
481
+ var MAX_TAG_LENGTH = 256;
482
+ var PREPR_ANY_ITEM_TAG = "prepr:any";
483
+ var PREPR_SCHEMA_TAG = "prepr:schema";
484
+ var preprItemTag = (id) => `prepr:${id}`;
485
+ var NOT_ALPHANUMERIC = /[^a-z0-9]/g;
486
+ var preprModelTag = (model) => `prepr:model:${model.toLowerCase().replace(NOT_ALPHANUMERIC, "")}`;
487
+ function preprModelTagsFor(document, data, schema) {
488
+ const operation = document.definitions.find(
489
+ (definition) => definition.kind === "OperationDefinition" && definition.operation === "query"
490
+ );
491
+ const root = schema.types[schema.queryType];
492
+ if (!operation || !root) return [];
493
+ const models = (type) => {
494
+ const members = schema.types[type]?.possibleTypes ?? [];
495
+ return members.length > 0 ? members : [type];
496
+ };
497
+ const tags = /* @__PURE__ */ new Set();
498
+ for (const selection of operation.selectionSet.selections) {
499
+ if (selection.kind !== "Field") continue;
500
+ const type = root.fields[selection.name.value]?.type;
501
+ const info = type ? schema.types[type] : void 0;
502
+ if (!type || !info) continue;
503
+ const items = info.fields.items?.type;
504
+ const key = (selection.alias ?? selection.name).value;
505
+ const found = typeof data === "object" && data !== null && data[key] != null;
506
+ const affected = items ? models(items) : found ? [] : models(type);
507
+ for (const model of affected) tags.add(preprModelTag(model));
508
+ }
509
+ return [...tags];
510
+ }
511
+ function preprCacheTags(data) {
512
+ const tags = /* @__PURE__ */ new Set();
513
+ const stack = [data];
514
+ while (stack.length > 0) {
515
+ const value = stack.pop();
516
+ if (typeof value !== "object" || value === null) continue;
517
+ const id = value._id;
518
+ if (typeof id === "string" && id !== "") {
519
+ const tag = preprItemTag(id);
520
+ if (tag.length > MAX_TAG_LENGTH) return [PREPR_ANY_ITEM_TAG];
521
+ tags.add(tag);
522
+ if (tags.size > MAX_TAGS) return [PREPR_ANY_ITEM_TAG];
523
+ }
524
+ for (const child of Object.values(value)) stack.push(child);
525
+ }
526
+ return [...tags];
527
+ }
528
+
528
529
  export {
529
530
  createPreprMiddleware,
530
531
  PreprError,
@@ -537,8 +538,12 @@ export {
537
538
  getPreprHeaders,
538
539
  getToolbarProps,
539
540
  isPersonalizedResponse,
540
- PREPR_BUCKET_COOKIE,
541
- resolvePreprBucket,
542
541
  preprDraftRedirect,
543
- createPreprDraftRoute
542
+ createPreprDraftRoute,
543
+ PREPR_ANY_ITEM_TAG,
544
+ PREPR_SCHEMA_TAG,
545
+ preprItemTag,
546
+ preprModelTag,
547
+ preprModelTagsFor,
548
+ preprCacheTags
544
549
  };
@@ -21,7 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/nextjs/components.ts
22
22
  var components_exports = {};
23
23
  __export(components_exports, {
24
- PreprAbSwitch: () => PreprAbSwitch,
24
+ PreprDecisionApplier: () => PreprDecisionApplier,
25
25
  PreprPersonalizationSwitch: () => PreprPersonalizationSwitch,
26
26
  PreprToolbar: () => PreprToolbar,
27
27
  PreprTrackingPixel: () => PreprTrackingPixel,
@@ -458,11 +458,11 @@ function createStegaAutoClean(options) {
458
458
  const settled = stripText ? info?.cleaned : info?.original;
459
459
  if (info && textContent === settled) {
460
460
  if (!info.decoded) return false;
461
- const shown = resolveEditTarget(textNode);
462
- if (!shown) return false;
461
+ const shown2 = resolveEditTarget(textNode);
462
+ if (!shown2) return false;
463
463
  pendingHidden.delete(textNode);
464
- if (shown.hasAttribute("data-prepr-encoded")) return false;
465
- tagEncodedElement(shown, info.decoded);
464
+ if (shown2.hasAttribute("data-prepr-encoded")) return false;
465
+ tagEncodedElement(shown2, info.decoded);
466
466
  return true;
467
467
  }
468
468
  try {
@@ -1055,7 +1055,7 @@ var DEFAULT_STATE = {
1055
1055
  };
1056
1056
  function createToolbarStore(initial = {}) {
1057
1057
  let state = { ...DEFAULT_STATE, ...initial };
1058
- const listeners = /* @__PURE__ */ new Set();
1058
+ const listeners2 = /* @__PURE__ */ new Set();
1059
1059
  function get() {
1060
1060
  return state;
1061
1061
  }
@@ -1064,14 +1064,14 @@ function createToolbarStore(initial = {}) {
1064
1064
  const hasChange = keys.some((key) => patch[key] !== state[key]);
1065
1065
  if (!hasChange) return;
1066
1066
  state = { ...state, ...patch };
1067
- for (const listener of listeners) {
1067
+ for (const listener of listeners2) {
1068
1068
  listener(state);
1069
1069
  }
1070
1070
  }
1071
1071
  function subscribe(fn) {
1072
- listeners.add(fn);
1072
+ listeners2.add(fn);
1073
1073
  return () => {
1074
- listeners.delete(fn);
1074
+ listeners2.delete(fn);
1075
1075
  };
1076
1076
  }
1077
1077
  return { get, set, subscribe };
@@ -2316,204 +2316,106 @@ function isReactHydrated(text) {
2316
2316
  return true;
2317
2317
  }
2318
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;
2319
+ // src/nextjs/decision-switch.ts
2320
+ function applyPreprDecisions(decision) {
2321
+ const state = window;
2322
+ const key = location.pathname + location.search;
2323
+ if (decision !== void 0) state.__preprDecision = { key, value: decision };
2324
+ const current = state.__preprDecision;
2325
+ if (!current || current.key !== key) return false;
2326
+ document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
2327
+ marker.setAttribute("data-prepr-decided", "");
2328
+ });
2329
+ const defaults = current.value === "*";
2330
+ const chosen = {};
2331
+ current.value.split("~").forEach((pair) => {
2332
+ const colon = pair.indexOf(":");
2333
+ if (colon > 0)
2334
+ chosen[pair.slice(0, colon)] = pair.slice(colon + 1).split("|");
2335
+ });
2336
+ document.querySelectorAll("[data-prepr-v]").forEach((element) => {
2337
+ const nested = element.hasAttribute("data-prepr-nested");
2338
+ const show = defaults ? element.hasAttribute("data-prepr-default") : (element.getAttribute("data-prepr-v") || "").split(" ").every((token) => {
2339
+ const colon = token.indexOf(":");
2340
+ const variant = token.slice(colon + 1);
2341
+ const variants = chosen[token.slice(0, colon)];
2342
+ return !!variants && (nested ? variants[0] === variant : variants.indexOf(variant) >= 0);
2402
2343
  });
2403
- }
2404
- function onIntent(event) {
2405
- const anchor = event.target?.closest?.("a[href]");
2406
- if (!(anchor instanceof HTMLAnchorElement)) return;
2407
- let url;
2344
+ if (show) element.setAttribute("data-prepr-show", "");
2345
+ else element.removeAttribute("data-prepr-show");
2346
+ });
2347
+ return true;
2348
+ }
2349
+ function requestPreprDecision(key, options = {}) {
2350
+ const fresh = 1e4;
2351
+ const keep = 18e5;
2352
+ const keepStatic = 36e5;
2353
+ const state = window;
2354
+ const asking = state.__preprAsking = state.__preprAsking || {};
2355
+ const read = () => {
2408
2356
  try {
2409
- url = new URL(anchor.href, location.href);
2357
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
2358
+ return all && typeof all === "object" ? all : {};
2410
2359
  } 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);
2360
+ return {};
2440
2361
  }
2441
2362
  };
2442
- }
2443
-
2444
- // src/nextjs/ab-switch.ts
2445
- function applyPreprAbBucket() {
2446
- const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
2447
- const bucket = match ? match[1] : "A";
2448
- const state = window;
2449
- if (state.__preprAbBucket === bucket) return;
2450
- state.__preprAbBucket = bucket;
2451
- const css = bucket === "A" ? "" : '[data-prepr-ab="A"]{display:none!important}[data-prepr-ab="' + bucket + '"]{display:contents!important}';
2452
- try {
2453
- if (!state.__preprAbSheet) {
2454
- const sheet = new CSSStyleSheet();
2455
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
2456
- state.__preprAbSheet = sheet;
2363
+ const ask = () => {
2364
+ if (!asking[key]) {
2365
+ asking[key] = fetch(key, {
2366
+ headers: { "Prepr-Decide": "1" },
2367
+ cache: "no-store",
2368
+ credentials: "same-origin",
2369
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
2370
+ // covers a proxy that never answers.
2371
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
2372
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
2373
+ delete asking[key];
2374
+ try {
2375
+ const all = read();
2376
+ const at = Date.now();
2377
+ Object.keys(all).forEach((stored) => {
2378
+ const entry = all[stored];
2379
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
2380
+ delete all[stored];
2381
+ });
2382
+ const kept = all[key];
2383
+ if (answer !== "*" || !kept || kept.value === "*")
2384
+ all[key] = { value: answer, at };
2385
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
2386
+ } catch {
2387
+ }
2388
+ return answer;
2389
+ });
2457
2390
  }
2458
- state.__preprAbSheet.replaceSync(css);
2459
- } catch {
2460
- if (!state.__preprAbSheet) {
2461
- state.__preprAbSheet = document.createElement("style");
2462
- document.head.appendChild(state.__preprAbSheet);
2391
+ return asking[key];
2392
+ };
2393
+ const last = read()[key];
2394
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
2395
+ const value = last && typeof last.value === "string" ? last.value : null;
2396
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
2397
+ if (value !== "" && age >= fresh) {
2398
+ ask().then((answer) => {
2399
+ if (answer !== "*" && answer !== value && options.onChange)
2400
+ options.onChange(answer);
2401
+ });
2463
2402
  }
2464
- state.__preprAbSheet.textContent = css;
2465
- }
2466
- }
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;
2472
- const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
2473
- let raw;
2474
- try {
2475
- raw = match ? decodeURIComponent(match[1]) : "";
2476
- } catch {
2477
- raw = "";
2403
+ return { result: value, value: Promise.resolve(value) };
2478
2404
  }
2479
- const state = window;
2480
- if (state.__preprDecisions === raw) return;
2481
- state.__preprDecisions = raw;
2482
- const id = /^[\w-]+$/;
2483
- let css = "";
2484
- raw.split("~").forEach((pair) => {
2485
- const at = pair.indexOf(":");
2486
- const group = pair.slice(0, at);
2487
- if (at < 1 || !id.test(group)) return;
2488
- const selector = '[data-prepr-group="' + group + '"]';
2489
- css += selector + "{display:none!important}";
2490
- pair.slice(at + 1).split("|").forEach((variant) => {
2491
- if (id.test(variant)) {
2492
- css += selector + '[data-prepr-variant="' + variant + '"]{display:contents!important}';
2493
- }
2494
- });
2405
+ const pending = {
2406
+ value: ask()
2407
+ };
2408
+ pending.value.then((answer) => {
2409
+ pending.result = answer;
2495
2410
  });
2496
- try {
2497
- if (!state.__preprDecisionSheet) {
2498
- const sheet = new CSSStyleSheet();
2499
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
2500
- state.__preprDecisionSheet = sheet;
2501
- }
2502
- state.__preprDecisionSheet.replaceSync(css);
2503
- } catch {
2504
- if (!state.__preprDecisionSheet) {
2505
- state.__preprDecisionSheet = document.createElement("style");
2506
- document.head.appendChild(state.__preprDecisionSheet);
2507
- }
2508
- state.__preprDecisionSheet.textContent = css;
2509
- }
2411
+ return pending;
2510
2412
  }
2511
2413
 
2512
2414
  // src/react/components.ts
2513
2415
  var import_react = require("react");
2514
2416
 
2515
2417
  // src/version.ts
2516
- var VERSION = "0.6.0-alpha.6";
2418
+ var VERSION = "0.6.0-alpha.8";
2517
2419
 
2518
2420
  // src/core/pixel.ts
2519
2421
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -2588,62 +2490,124 @@ function PreprToolbar({
2588
2490
  }, []);
2589
2491
  return null;
2590
2492
  }
2591
- function PreprAbSwitch() {
2592
- (0, import_react2.useLayoutEffect)(() => {
2593
- applyPreprAbBucket();
2493
+ var shown = /* @__PURE__ */ new Map();
2494
+ var historyStep = false;
2495
+ var onPopState = () => {
2496
+ historyStep = true;
2497
+ };
2498
+ function requestDecision(key, personalized = false) {
2499
+ return requestPreprDecision(key, {
2500
+ personalized,
2501
+ onChange: (value) => {
2502
+ if (location.pathname + location.search !== key) return;
2503
+ applyPreprDecisions(value);
2504
+ shown.set(key, value);
2505
+ }
2594
2506
  });
2507
+ }
2508
+ function linkTarget(event) {
2509
+ const anchor = event.target?.closest?.("a[href]");
2510
+ if (!(anchor instanceof HTMLAnchorElement)) return null;
2511
+ if (anchor.target && anchor.target !== "_self" || anchor.hasAttribute("download")) {
2512
+ return null;
2513
+ }
2514
+ const url = new URL(anchor.href, location.href);
2515
+ const key = url.pathname + url.search;
2516
+ if (url.origin !== location.origin || key === location.pathname + location.search) {
2517
+ return null;
2518
+ }
2519
+ return key;
2520
+ }
2521
+ var HOVER_DELAY_MS = 65;
2522
+ var hovering = null;
2523
+ function onPointerOver(event) {
2524
+ if (event.pointerType !== "mouse") return;
2525
+ const key = linkTarget(event);
2526
+ if (hovering?.key === key) return;
2527
+ if (hovering) clearTimeout(hovering.timer);
2528
+ hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
2529
+ }
2530
+ function onPointerDown(event) {
2531
+ if (event.button !== 0) return;
2532
+ const key = linkTarget(event);
2533
+ if (key) requestDecision(key);
2534
+ }
2535
+ var listeners = 0;
2536
+ function useDecisionRequests() {
2537
+ (0, import_react2.useEffect)(() => {
2538
+ if (listeners++ === 0) {
2539
+ window.addEventListener("pointerover", onPointerOver, { passive: true });
2540
+ window.addEventListener("pointerdown", onPointerDown, {
2541
+ capture: true,
2542
+ passive: true
2543
+ });
2544
+ window.addEventListener("popstate", onPopState);
2545
+ }
2546
+ return () => {
2547
+ if (--listeners === 0) {
2548
+ window.removeEventListener("pointerover", onPointerOver);
2549
+ window.removeEventListener("pointerdown", onPointerDown, true);
2550
+ window.removeEventListener("popstate", onPopState);
2551
+ }
2552
+ };
2553
+ }, []);
2554
+ }
2555
+ function PreprPersonalizationSwitch() {
2556
+ useDecisionRequests();
2595
2557
  return null;
2596
2558
  }
2597
- var viewCount = 0;
2598
- function PreprPersonalizationSwitch({
2599
- refresh = true,
2600
- freshFor
2601
- }) {
2602
- const [scheduler, setScheduler] = (0, import_react2.useState)(
2603
- null
2604
- );
2605
- const viewUrl = (0, import_react2.useRef)(null);
2559
+ function PreprDecisionApplier() {
2560
+ useDecisionRequests();
2606
2561
  (0, import_react2.useLayoutEffect)(() => {
2607
- const url = location.pathname + location.search;
2608
- if (viewUrl.current === url) return;
2609
- viewUrl.current = url;
2610
- applyPreprDecisions(`${url}#${++viewCount}`);
2611
- });
2612
- (0, import_react2.useEffect)(() => {
2613
- if (!refresh) return;
2614
- const started = createPreprRefreshScheduler({ freshFor });
2615
- setScheduler(started);
2562
+ const key = location.pathname + location.search;
2563
+ const remember = () => {
2564
+ const state = window.__preprDecision;
2565
+ if (state) shown.set(key, state.value);
2566
+ };
2567
+ const previous = historyStep ? shown.get(key) : void 0;
2568
+ historyStep = false;
2569
+ if (applyPreprDecisions(previous)) {
2570
+ remember();
2571
+ return;
2572
+ }
2573
+ const pending = requestDecision(key, true);
2574
+ if (pending.result !== void 0) {
2575
+ applyPreprDecisions(pending.result);
2576
+ remember();
2577
+ return;
2578
+ }
2579
+ let live = true;
2580
+ void pending.value.then((value) => {
2581
+ if (live && location.pathname + location.search === key) {
2582
+ applyPreprDecisions(value);
2583
+ remember();
2584
+ }
2585
+ });
2616
2586
  return () => {
2617
- started.stop();
2618
- setScheduler(null);
2587
+ live = false;
2619
2588
  };
2620
- }, [refresh, freshFor]);
2621
- return scheduler ? (0, import_react2.createElement)(
2622
- import_react2.Suspense,
2623
- { fallback: null },
2624
- (0, import_react2.createElement)(ViewCounter, { scheduler })
2625
- ) : null;
2626
- }
2627
- function ViewCounter({
2628
- scheduler
2629
- }) {
2630
- const pathname = (0, import_navigation.usePathname)();
2631
- (0, import_react2.useEffect)(() => {
2632
- scheduler.view();
2633
- }, [pathname, scheduler]);
2589
+ });
2634
2590
  return null;
2635
2591
  }
2636
2592
  var VARIANT_KEY_ATTRIBUTE = "data-prepr-variant-key";
2637
2593
  function PreprVariantTracking({
2638
2594
  variantKeys,
2639
- ab,
2640
2595
  children
2641
2596
  }) {
2642
2597
  const ref = (0, import_react2.useRef)(null);
2643
2598
  const keys = variantKeys.join(" ");
2644
2599
  (0, import_react2.useLayoutEffect)(() => {
2645
2600
  let tagged = [];
2646
- const timer = setTimeout(() => {
2601
+ const wrapper = ref.current?.closest("[data-prepr-v]");
2602
+ const shown2 = () => !wrapper || wrapper.hasAttribute("data-prepr-show");
2603
+ const untag = () => {
2604
+ for (const [element] of tagged)
2605
+ element.removeAttribute(VARIANT_KEY_ATTRIBUTE);
2606
+ tagged = [];
2607
+ };
2608
+ const tag = () => {
2609
+ if (!shown2()) return untag();
2610
+ if (tagged.length > 0) return;
2647
2611
  const elements = [...ref.current?.children ?? []];
2648
2612
  const list = keys.split(" ");
2649
2613
  const pairs = elements.length === list.length ? elements.map((element, i2) => [element, list[i2]]) : elements.slice(0, 1).map((element) => [element, list[0]]);
@@ -2652,23 +2616,28 @@ function PreprVariantTracking({
2652
2616
  element.setAttribute(VARIANT_KEY_ATTRIBUTE, key);
2653
2617
  return true;
2654
2618
  });
2619
+ };
2620
+ const timer = setTimeout(tag);
2621
+ const observer = wrapper && new MutationObserver(tag);
2622
+ observer?.observe(wrapper, {
2623
+ attributes: true,
2624
+ attributeFilter: ["data-prepr-show"]
2655
2625
  });
2656
2626
  return () => {
2657
2627
  clearTimeout(timer);
2658
- for (const [element] of tagged) {
2659
- element.removeAttribute(VARIANT_KEY_ATTRIBUTE);
2660
- }
2628
+ observer?.disconnect();
2629
+ untag();
2661
2630
  };
2662
2631
  }, [keys]);
2663
2632
  return (0, import_react2.createElement)(
2664
2633
  "div",
2665
- ab === void 0 ? { ref, style: { display: "contents" } } : { ref, "data-prepr-ab": ab },
2634
+ { ref, style: { display: "contents" } },
2666
2635
  children
2667
2636
  );
2668
2637
  }
2669
2638
  // Annotate the CommonJS export names for ESM import in node:
2670
2639
  0 && (module.exports = {
2671
- PreprAbSwitch,
2640
+ PreprDecisionApplier,
2672
2641
  PreprPersonalizationSwitch,
2673
2642
  PreprToolbar,
2674
2643
  PreprTrackingPixel,