@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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name2 in all)
@@ -15,12 +17,20 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/nextjs/graphql.ts
21
31
  var graphql_exports = {};
22
32
  __export(graphql_exports, {
23
- PREPR_DECISIONS_COOKIE: () => PREPR_DECISIONS_COOKIE,
33
+ PREPR_ANY_ITEM_TAG: () => PREPR_ANY_ITEM_TAG,
24
34
  PREPR_DEFAULT_VARIANT: () => PREPR_DEFAULT_VARIANT,
25
35
  PreprError: () => PreprError,
26
36
  PreprStack: () => PreprStack,
@@ -29,6 +39,8 @@ __export(graphql_exports, {
29
39
  decisionsFrom: () => decisionsFrom,
30
40
  isPersonalizedResponse: () => isPersonalizedResponse,
31
41
  loadPreprSchema: () => loadPreprSchema,
42
+ preprCacheTags: () => preprCacheTags,
43
+ preprModelTag: () => preprModelTag,
32
44
  preprVariantProps: () => preprVariantProps,
33
45
  resolveVariants: () => resolveVariants,
34
46
  splitStack: () => splitStack,
@@ -36,9 +48,60 @@ __export(graphql_exports, {
36
48
  toVariantsQuery: () => toVariantsQuery
37
49
  });
38
50
  module.exports = __toCommonJS(graphql_exports);
51
+ var nextCache = __toESM(require("next/cache"), 1);
52
+
53
+ // src/core/graphql/cache-tags.ts
54
+ var MAX_TAGS = 128;
55
+ var MAX_TAG_LENGTH = 256;
56
+ var PREPR_ANY_ITEM_TAG = "prepr:any";
57
+ var PREPR_SCHEMA_TAG = "prepr:schema";
58
+ var preprItemTag = (id) => `prepr:${id}`;
59
+ var NOT_ALPHANUMERIC = /[^a-z0-9]/g;
60
+ var preprModelTag = (model) => `prepr:model:${model.toLowerCase().replace(NOT_ALPHANUMERIC, "")}`;
61
+ function preprModelTagsFor(document2, data, schema) {
62
+ const operation = document2.definitions.find(
63
+ (definition) => definition.kind === "OperationDefinition" && definition.operation === "query"
64
+ );
65
+ const root = schema.types[schema.queryType];
66
+ if (!operation || !root) return [];
67
+ const models = (type) => {
68
+ const members = schema.types[type]?.possibleTypes ?? [];
69
+ return members.length > 0 ? members : [type];
70
+ };
71
+ const tags = /* @__PURE__ */ new Set();
72
+ for (const selection of operation.selectionSet.selections) {
73
+ if (selection.kind !== "Field") continue;
74
+ const type = root.fields[selection.name.value]?.type;
75
+ const info = type ? schema.types[type] : void 0;
76
+ if (!type || !info) continue;
77
+ const items = info.fields.items?.type;
78
+ const key = (selection.alias ?? selection.name).value;
79
+ const found = typeof data === "object" && data !== null && data[key] != null;
80
+ const affected = items ? models(items) : found ? [] : models(type);
81
+ for (const model of affected) tags.add(preprModelTag(model));
82
+ }
83
+ return [...tags];
84
+ }
85
+ function preprCacheTags(data) {
86
+ const tags = /* @__PURE__ */ new Set();
87
+ const stack = [data];
88
+ while (stack.length > 0) {
89
+ const value = stack.pop();
90
+ if (typeof value !== "object" || value === null) continue;
91
+ const id = value._id;
92
+ if (typeof id === "string" && id !== "") {
93
+ const tag = preprItemTag(id);
94
+ if (tag.length > MAX_TAG_LENGTH) return [PREPR_ANY_ITEM_TAG];
95
+ tags.add(tag);
96
+ if (tags.size > MAX_TAGS) return [PREPR_ANY_ITEM_TAG];
97
+ }
98
+ for (const child of Object.values(value)) stack.push(child);
99
+ }
100
+ return [...tags];
101
+ }
39
102
 
40
103
  // src/version.ts
41
- var VERSION = "0.6.0-alpha.6";
104
+ var VERSION = "0.6.0-alpha.8";
42
105
 
43
106
  // src/core/features.ts
44
107
  function isEnabled(config) {
@@ -202,7 +265,10 @@ function decisionSelectionSet(set, typeName, path, ctx) {
202
265
  path,
203
266
  ctx
204
267
  );
205
- if (pruned) {
268
+ if (!pruned) continue;
269
+ if (fragment.typeCondition.name.value === typeName) {
270
+ selections.push(...pruned.selections);
271
+ } else {
206
272
  selections.push(
207
273
  inlineFragment(fragment.typeCondition.name.value, [
208
274
  ...pruned.selections
@@ -212,13 +278,19 @@ function decisionSelectionSet(set, typeName, path, ctx) {
212
278
  continue;
213
279
  }
214
280
  if (selection.kind === "InlineFragment") {
281
+ const condition = selection.typeCondition?.name.value ?? typeName;
215
282
  const pruned = decisionSelectionSet(
216
283
  selection.selectionSet,
217
- selection.typeCondition?.name.value ?? typeName,
284
+ condition,
218
285
  path,
219
286
  ctx
220
287
  );
221
- if (pruned) selections.push({ ...selection, selectionSet: pruned });
288
+ if (!pruned) continue;
289
+ if (condition === typeName && !selection.directives?.length) {
290
+ selections.push(...pruned.selections);
291
+ } else {
292
+ selections.push({ ...selection, selectionSet: pruned });
293
+ }
222
294
  continue;
223
295
  }
224
296
  if (!selection.selectionSet) continue;
@@ -292,20 +364,58 @@ function toDecisionDocument(document2, schema, groupPaths, options) {
292
364
  const used = /* @__PURE__ */ new Set();
293
365
  collectVariables(pruned, used);
294
366
  collectVariables(operation.directives, used);
367
+ const shared = sharedContextFragment(schema);
295
368
  return {
296
369
  kind: "Document",
297
370
  definitions: [
371
+ ...shared ? [shared.definition] : [],
298
372
  {
299
373
  ...operation,
300
374
  name: operation.name && name(`${operation.name.value}Decision`),
301
375
  variableDefinitions: (operation.variableDefinitions ?? []).filter(
302
376
  (definition) => used.has(definition.variable.name.value)
303
377
  ),
304
- selectionSet: pruned
378
+ selectionSet: shared ? shared.apply(pruned) : pruned
305
379
  }
306
380
  ]
307
381
  };
308
382
  }
383
+ function sharedContextFragment(schema) {
384
+ const contextTypes = new Set(
385
+ Object.values(schema.types).flatMap(
386
+ (type) => type.hasContext ? [type.fields._context.type] : []
387
+ )
388
+ );
389
+ if (contextTypes.size !== 1) return null;
390
+ const [contextType] = contextTypes;
391
+ const fragmentName = "c";
392
+ const definition = {
393
+ kind: "FragmentDefinition",
394
+ name: name(fragmentName),
395
+ typeCondition: { kind: "NamedType", name: name(contextType) },
396
+ selectionSet: selectionSet(
397
+ ["kind", "group_id", "variant_id"].map(
398
+ (field) => ({ kind: "Field", name: name(field) })
399
+ )
400
+ )
401
+ };
402
+ const spread = {
403
+ kind: "FragmentSpread",
404
+ name: name(fragmentName)
405
+ };
406
+ const apply = (set) => selectionSet(
407
+ set.selections.map((selection) => {
408
+ if (selection.kind === "Field" && selection.alias?.value === PREPR_CONTEXT_ALIAS) {
409
+ return { ...selection, selectionSet: selectionSet([spread]) };
410
+ }
411
+ return "selectionSet" in selection && selection.selectionSet ? {
412
+ ...selection,
413
+ selectionSet: apply(selection.selectionSet)
414
+ } : selection;
415
+ })
416
+ );
417
+ return { definition, apply };
418
+ }
309
419
 
310
420
  // src/core/variants/probe-document.ts
311
421
  function probeSelectionSet(set, fragments) {
@@ -886,6 +996,7 @@ function createVaryProbe(run, options) {
886
996
  }
887
997
 
888
998
  // src/core/graphql/client.ts
999
+ var clientSchemas = /* @__PURE__ */ new WeakMap();
889
1000
  function isParseableUrl(url) {
890
1001
  try {
891
1002
  new URL(url);
@@ -922,7 +1033,7 @@ function createPreprClient(options) {
922
1033
  ));
923
1034
  const variantsDocuments = /* @__PURE__ */ new Map();
924
1035
  const decisionDocuments = /* @__PURE__ */ new Map();
925
- return {
1036
+ const client = {
926
1037
  async query(document2, variables, queryOptions = {}) {
927
1038
  const { variants, ...rest } = queryOptions;
928
1039
  if (!variants) return run(document2, variables, rest);
@@ -934,10 +1045,10 @@ function createPreprClient(options) {
934
1045
  return run(derived, variables, rest);
935
1046
  },
936
1047
  async decide(document2, variables, queryOptions = {}) {
937
- const { from, topLevelOnly, ...rest } = queryOptions;
1048
+ const { from, ...rest } = queryOptions;
938
1049
  const paths = from === void 0 ? void 0 : personalizationPaths(from);
939
1050
  if (paths && paths.size === 0) return /* @__PURE__ */ new Map();
940
- const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : topLevelOnly ? "top" : "*";
1051
+ const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : "*";
941
1052
  let byPaths = decisionDocuments.get(document2);
942
1053
  if (!byPaths) {
943
1054
  byPaths = /* @__PURE__ */ new Map();
@@ -945,9 +1056,7 @@ function createPreprClient(options) {
945
1056
  }
946
1057
  let derived = byPaths.get(signature);
947
1058
  if (derived === void 0) {
948
- derived = toDecisionDocument(document2, await loadSchema(), paths, {
949
- topLevelOnly: !paths && topLevelOnly
950
- });
1059
+ derived = toDecisionDocument(document2, await loadSchema(), paths);
951
1060
  byPaths.set(signature, derived);
952
1061
  }
953
1062
  if (!derived) return /* @__PURE__ */ new Map();
@@ -961,6 +1070,8 @@ function createPreprClient(options) {
961
1070
  (vary) => vary === null || SEGMENTS_VARY.test(vary)
962
1071
  )
963
1072
  };
1073
+ clientSchemas.set(client, loadSchema);
1074
+ return client;
964
1075
  }
965
1076
 
966
1077
  // src/core/graphql/schema-loader.ts
@@ -987,69 +1098,15 @@ async function loadPreprSchema(url, options = {}) {
987
1098
  return schema;
988
1099
  }
989
1100
 
990
- // src/core/refresh.ts
991
- var PREPR_REFRESH_COOKIE = "prepr_refresh";
992
- var PREPR_REFRESH_HEADER = "Prepr-Refresh";
993
- var REFRESH_MAX_PATHS = 5;
994
- var SAFE_PATH = /^\/(?![/\\])[\w\-./%]{0,200}$/;
995
- function safePath(path) {
996
- return SAFE_PATH.test(path) ? path : void 0;
997
- }
998
- function isFresh(at, freshFor, now = Date.now()) {
999
- const age = now - at;
1000
- return at > 0 && age >= 0 && age < freshFor;
1001
- }
1002
- function cleanEntries(entries) {
1003
- const out = [];
1004
- for (const entry of entries) {
1005
- if (out.length === REFRESH_MAX_PATHS) break;
1006
- if (safePath(entry.path) && !out.some((e) => e.path === entry.path)) {
1007
- out.push(entry);
1008
- }
1009
- }
1010
- return out;
1011
- }
1012
- function parseRefresh(raw) {
1013
- return cleanEntries(
1014
- (raw ?? "").split("|").map((pair) => {
1015
- const split = pair.lastIndexOf("~");
1016
- const at = Number(split < 0 ? "" : pair.slice(split + 1));
1017
- return {
1018
- path: split < 0 ? pair : pair.slice(0, split),
1019
- at: Number.isFinite(at) && at > 0 ? Math.floor(at) : 0
1020
- };
1021
- })
1022
- );
1023
- }
1024
- function serializeRefresh(entries) {
1025
- return cleanEntries(entries).map((entry) => `${entry.path}~${Math.floor(entry.at)}`).join("|");
1026
- }
1027
- function parseRefreshPaths(header) {
1028
- return cleanEntries(
1029
- (header ?? "").split("|").map((path) => ({ path, at: 0 }))
1030
- ).map((entry) => entry.path);
1031
- }
1032
-
1033
1101
  // src/nextjs/decisions.ts
1034
- var PREPR_DECISIONS_COOKIE = "prepr_decisions";
1102
+ var PREPR_DECIDE_HEADER = "Prepr-Decide";
1103
+ var DEFAULT_DECISION = "*";
1035
1104
  var DECISIONS_MAX_GROUPS = 40;
1036
1105
  var SAFE_ID = /^[\w-]+$/;
1037
- var SEEN_MAX_PAGES = 1e3;
1038
- function parseDecisions(raw) {
1039
- const out = /* @__PURE__ */ new Map();
1040
- for (const pair of (raw ?? "").split("~")) {
1041
- const at = pair.indexOf(":");
1042
- const group = pair.slice(0, at);
1043
- if (at < 1 || !SAFE_ID.test(group)) continue;
1044
- out.set(
1045
- group,
1046
- pair.slice(at + 1).split("|").filter((variant) => SAFE_ID.test(variant))
1047
- );
1048
- }
1049
- return out;
1050
- }
1051
1106
  function serializeDecisions(decisions) {
1052
- return [...decisions].slice(0, DECISIONS_MAX_GROUPS).map(([group, variants]) => `${group}:${variants.join("|")}`).join("~");
1107
+ return [...decisions].filter(([group]) => SAFE_ID.test(group)).slice(0, DECISIONS_MAX_GROUPS).map(
1108
+ ([group, variants]) => `${group}:${variants.filter((v) => SAFE_ID.test(v)).join("|")}`
1109
+ ).join("~");
1053
1110
  }
1054
1111
  function forwardedHeaders(request, response) {
1055
1112
  const overridden = response.headers.get("x-middleware-override-headers");
@@ -1062,109 +1119,16 @@ function forwardedHeaders(request, response) {
1062
1119
  return getPreprHeadersFromHeaders(headers2);
1063
1120
  }
1064
1121
  var preprClientInternals = /* @__PURE__ */ new WeakMap();
1065
- var DEFAULT_FRESH_FOR_MS = 1e4;
1066
- async function decidePage(client, seenGroups, headers2, document2, variables, timeoutMs = 1e3) {
1067
- if (!await client.hasSegments(document2, variables)) return void 0;
1068
- const decided = await client.decide(document2, variables, {
1069
- topLevelOnly: true,
1070
- headers: headers2,
1071
- init: { signal: AbortSignal.timeout(timeoutMs) }
1072
- });
1073
- let byVariables = seenGroups.get(document2);
1074
- if (!byVariables) {
1075
- byVariables = /* @__PURE__ */ new Map();
1076
- seenGroups.set(document2, byVariables);
1077
- }
1078
- const pageKey = JSON.stringify(variables ?? null);
1079
- let groups = byVariables.get(pageKey);
1080
- if (!groups) {
1081
- if (byVariables.size >= SEEN_MAX_PAGES) {
1082
- byVariables.delete(byVariables.keys().next().value);
1083
- }
1084
- groups = /* @__PURE__ */ new Set();
1085
- byVariables.set(pageKey, groups);
1086
- }
1087
- for (const group of decided.keys()) groups.add(group);
1088
- return decided;
1089
- }
1090
- function writeDecisions(request, response, decided, paths) {
1091
- const next = /* @__PURE__ */ new Map();
1092
- for (const decisions of decided) {
1093
- for (const [group, variants] of decisions) {
1094
- if (SAFE_ID.test(group) && !next.has(group)) {
1095
- next.set(
1096
- group,
1097
- variants.filter((v) => SAFE_ID.test(v))
1098
- );
1099
- }
1100
- }
1101
- }
1102
- for (const [group, variants] of parseDecisions(
1103
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1104
- )) {
1105
- if (!next.has(group)) next.set(group, variants);
1106
- }
1107
- const options = {
1108
- path: "/",
1109
- sameSite: "lax",
1110
- httpOnly: false,
1111
- secure: request.nextUrl.protocol === "https:"
1112
- };
1113
- if (next.size > 0) {
1114
- response.cookies.set(
1115
- PREPR_DECISIONS_COOKIE,
1116
- serializeDecisions(next),
1117
- options
1118
- );
1119
- }
1120
- if (paths.length > 0) {
1121
- const at = Date.now();
1122
- response.cookies.set(
1123
- PREPR_REFRESH_COOKIE,
1124
- serializeRefresh([
1125
- ...paths.map((path) => ({ path, at })),
1126
- ...parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value)
1127
- ]),
1128
- options
1129
- );
1130
- }
1131
- }
1132
- function pagePath(request) {
1133
- return request.nextUrl.basePath + request.nextUrl.pathname;
1134
- }
1135
- function contextChanged(request, headers2) {
1136
- return headers2.get("Prepr-Customer-Id-Created") === "true" || headers2.has("Prepr-Segments") || [...request.nextUrl.searchParams.keys()].some(
1137
- (key) => key.startsWith("utm_")
1138
- );
1139
- }
1140
- async function resolveDecisionsWith(client, seenGroups, request, response, document2, variables, options) {
1122
+ async function decideView(client, headers2, page, timeoutMs = 3e3) {
1123
+ const init = { signal: AbortSignal.timeout(timeoutMs) };
1141
1124
  try {
1142
- if (options?.onlyIf && !await options.onlyIf()) return;
1143
- const known = seenGroups.get(document2)?.get(JSON.stringify(variables ?? null));
1144
- const path = pagePath(request);
1145
- const headers2 = mergePreprHeaders(forwardedHeaders(request, response));
1146
- const stored = parseDecisions(
1147
- request.cookies.get(PREPR_DECISIONS_COOKIE)?.value
1148
- );
1149
- const decidedAt = parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value).find(
1150
- (entry) => entry.path === path
1151
- )?.at ?? 0;
1152
- const fresh = isFresh(decidedAt, options?.freshFor ?? DEFAULT_FRESH_FOR_MS);
1153
- if (fresh && known && !contextChanged(request, headers2) && [...known].every((group) => stored.has(group))) {
1154
- return;
1155
- }
1156
- const decided = await decidePage(
1157
- client,
1158
- seenGroups,
1159
- headers2,
1160
- document2,
1161
- variables,
1162
- options?.timeoutMs
1163
- );
1164
- if (!decided || decided.size === 0) return;
1165
- writeDecisions(request, response, [decided], [path]);
1125
+ const decided = page.decision ? decisionsFrom(
1126
+ (await client.query(page.decision, page.variables, { headers: headers2, init })).data
1127
+ ) : await client.decide(page.document, page.variables, { headers: headers2, init });
1128
+ return serializeDecisions(decided);
1166
1129
  } catch (error) {
1167
1130
  if (isFrameworkSignal(error)) throw error;
1131
+ return DEFAULT_DECISION;
1168
1132
  }
1169
1133
  }
1170
1134
 
@@ -1181,66 +1145,6 @@ async function getPreprHeaders() {
1181
1145
  // src/nextjs/proxy.ts
1182
1146
  var import_server9 = require("next/server");
1183
1147
 
1184
- // src/core/constants.ts
1185
- var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
1186
- var COOKIE_SEGMENT = "Prepr-Segments";
1187
- var COOKIE_VARIANT = "Prepr-ABtesting";
1188
- var COOKIE_UID = "__prepr_uid";
1189
- var COOKIE_HUBSPOT = "hubspotutk";
1190
- var PARAM_SEGMENT = "prepr_preview_segment";
1191
- var PARAM_VARIANT = "prepr_preview_ab";
1192
- var PARAM_HIDE_BAR = "prepr_hide_bar";
1193
- var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
1194
-
1195
- // src/nextjs/bucket.ts
1196
- var PREPR_BUCKET_COOKIE = "prepr_ab_bucket";
1197
- var isBucket = (value) => value === "A" || value === "B";
1198
- function setBucketCookie(request, response, bucket) {
1199
- response.cookies.set(PREPR_BUCKET_COOKIE, bucket, {
1200
- path: "/",
1201
- sameSite: "lax",
1202
- // Readable on purpose: PreprStack's inline switch reads it before first
1203
- // paint. It holds only "A" or "B".
1204
- httpOnly: false,
1205
- secure: request.nextUrl.protocol === "https:",
1206
- maxAge: 60 * 60 * 24 * 365
1207
- });
1208
- }
1209
- async function resolvePreprBucket(request, response, options) {
1210
- const stored = request.cookies.get(PREPR_BUCKET_COOKIE)?.value;
1211
- const forced = response.headers.get("x-middleware-request-prepr-abtesting") ?? request.cookies.get(COOKIE_VARIANT)?.value;
1212
- if (options.preview && isBucket(forced)) {
1213
- if (forced !== stored) setBucketCookie(request, response, forced);
1214
- return forced;
1215
- }
1216
- if (isBucket(stored)) return stored;
1217
- if (options.onlyIf) {
1218
- const needed = await Promise.resolve().then(options.onlyIf).catch(() => true);
1219
- if (!needed) return "A";
1220
- }
1221
- const customerId = request.cookies.get(COOKIE_UID)?.value ?? response.cookies.get(COOKIE_UID)?.value;
1222
- if (!customerId) return "A";
1223
- try {
1224
- const res = await fetch(options.url, {
1225
- method: "POST",
1226
- headers: {
1227
- "Content-Type": "application/json",
1228
- "Prepr-Customer-Id": customerId
1229
- },
1230
- body: JSON.stringify({ query: "{ __typename }" }),
1231
- cache: "no-store",
1232
- signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
1233
- });
1234
- const bucket = res.headers.get("x-prepr-customer-bucket");
1235
- if (isBucket(bucket)) {
1236
- setBucketCookie(request, response, bucket);
1237
- return bucket;
1238
- }
1239
- } catch {
1240
- }
1241
- return "A";
1242
- }
1243
-
1244
1148
  // src/nextjs/draft.ts
1245
1149
  var import_headers2 = require("next/headers");
1246
1150
  var import_navigation = require("next/navigation");
@@ -1268,6 +1172,17 @@ function preprDraftRedirect(request, options) {
1268
1172
  // src/nextjs/middleware.ts
1269
1173
  var import_server8 = require("next/server");
1270
1174
 
1175
+ // src/core/constants.ts
1176
+ var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
1177
+ var COOKIE_SEGMENT = "Prepr-Segments";
1178
+ var COOKIE_VARIANT = "Prepr-ABtesting";
1179
+ var COOKIE_UID = "__prepr_uid";
1180
+ var COOKIE_HUBSPOT = "hubspotutk";
1181
+ var PARAM_SEGMENT = "prepr_preview_segment";
1182
+ var PARAM_VARIANT = "prepr_preview_ab";
1183
+ var PARAM_HIDE_BAR = "prepr_hide_bar";
1184
+ var ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
1185
+
1271
1186
  // src/core/middleware.ts
1272
1187
  var UTM_PARAMS = [
1273
1188
  "utm_source",
@@ -1478,7 +1393,7 @@ function createPreprProxy(options) {
1478
1393
  "createPreprProxy"
1479
1394
  );
1480
1395
  }
1481
- const { client, seenGroups, url } = internals;
1396
+ const { client } = internals;
1482
1397
  const preview = options.preview ?? false;
1483
1398
  const allowed = async (request) => {
1484
1399
  if (!options.onlyIf) return true;
@@ -1495,66 +1410,31 @@ function createPreprProxy(options) {
1495
1410
  return null;
1496
1411
  }
1497
1412
  };
1498
- async function refresh(request, response, paths) {
1499
- const answer = new import_server9.NextResponse(null, {
1500
- status: 204,
1501
- headers: { "Cache-Control": "no-store" }
1502
- });
1503
- for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
1413
+ async function answerDecision(request, response, target) {
1414
+ let decision = DEFAULT_DECISION;
1504
1415
  const site = request.headers.get("sec-fetch-site");
1505
- if (request.method !== "GET" || site !== null && site !== "same-origin" || !await allowed(request)) {
1506
- return answer;
1416
+ if (target && request.method === "GET" && (site === null || site === "same-origin") && await allowed(request)) {
1417
+ decision = await decideView(
1418
+ client,
1419
+ mergePreprHeaders(forwardedHeaders(request, response)),
1420
+ target,
1421
+ options.timeoutMs
1422
+ );
1507
1423
  }
1508
- const own = new Set(
1509
- parseRefresh(request.cookies.get(PREPR_REFRESH_COOKIE)?.value).map(
1510
- (entry) => entry.path
1511
- )
1512
- );
1513
- const basePath = request.nextUrl.basePath;
1514
- const headers2 = mergePreprHeaders(forwardedHeaders(request, response));
1515
- const decided = await Promise.all(
1516
- paths.map(async (path) => {
1517
- if (!own.has(path) || !path.startsWith(basePath)) return void 0;
1518
- const pathname = path.slice(basePath.length) || "/";
1519
- if (!isPagePath(pathname)) return void 0;
1520
- const target = lookup(new URL(pathname, request.url));
1521
- if (!target) return void 0;
1522
- try {
1523
- const decisions = await decidePage(
1524
- client,
1525
- seenGroups,
1526
- headers2,
1527
- target.document,
1528
- target.variables,
1529
- options.timeoutMs
1530
- );
1531
- return decisions?.size ? [path, decisions] : void 0;
1532
- } catch (error) {
1533
- if (isFrameworkSignal(error)) throw error;
1534
- return void 0;
1535
- }
1536
- })
1537
- );
1538
- const found = decided.filter((entry) => entry !== void 0);
1539
- writeDecisions(
1540
- request,
1541
- answer,
1542
- found.map(([, decisions]) => decisions),
1543
- found.map(([path]) => path)
1544
- );
1424
+ const answer = new import_server9.NextResponse(decision, {
1425
+ headers: {
1426
+ "Cache-Control": "private, no-store",
1427
+ "Content-Type": "text/plain; charset=utf-8"
1428
+ }
1429
+ });
1430
+ for (const cookie of response.cookies.getAll()) answer.cookies.set(cookie);
1545
1431
  return answer;
1546
1432
  }
1547
1433
  return async (request) => {
1548
- if (request.headers.has(PREPR_REFRESH_HEADER)) {
1549
- return refresh(
1550
- request,
1551
- createPreprMiddleware(request, { preview, features: options.features }),
1552
- parseRefreshPaths(request.headers.get(PREPR_REFRESH_HEADER))
1553
- );
1554
- }
1555
1434
  const rendersPage = (request.method === "GET" || request.method === "HEAD") && isPagePath(request.nextUrl.pathname);
1556
1435
  const target = rendersPage ? lookup(request.nextUrl) : null;
1557
- if (target && preview) {
1436
+ const decisionRequest = request.headers.has(PREPR_DECIDE_HEADER);
1437
+ if (target && preview && !decisionRequest) {
1558
1438
  const draft = preprDraftRedirect(request, { preview });
1559
1439
  if (draft) return draft;
1560
1440
  }
@@ -1562,24 +1442,7 @@ function createPreprProxy(options) {
1562
1442
  preview,
1563
1443
  features: options.features
1564
1444
  });
1565
- if (target && await allowed(request)) {
1566
- await Promise.all([
1567
- resolvePreprBucket(request, response, {
1568
- url,
1569
- preview,
1570
- onlyIf: () => options.client.hasAbTests(target.document, target.variables)
1571
- }),
1572
- resolveDecisionsWith(
1573
- client,
1574
- seenGroups,
1575
- request,
1576
- response,
1577
- target.document,
1578
- target.variables,
1579
- { freshFor: options.freshFor, timeoutMs: options.timeoutMs }
1580
- )
1581
- ]);
1582
- }
1445
+ if (decisionRequest) return answerDecision(request, response, target);
1583
1446
  return response;
1584
1447
  };
1585
1448
  }
@@ -1587,235 +1450,267 @@ function createPreprProxy(options) {
1587
1450
  // src/nextjs/stack.ts
1588
1451
  var import_react = require("react");
1589
1452
 
1590
- // src/nextjs/ab-switch.ts
1591
- function applyPreprAbBucket() {
1592
- const match = document.cookie.match(/(?:^|; )prepr_ab_bucket=([AB])(?:;|$)/);
1593
- const bucket = match ? match[1] : "A";
1453
+ // src/nextjs/decision-switch.ts
1454
+ function applyPreprDecisions(decision) {
1594
1455
  const state = window;
1595
- if (state.__preprAbBucket === bucket) return;
1596
- state.__preprAbBucket = bucket;
1597
- const css = bucket === "A" ? "" : '[data-prepr-ab="A"]{display:none!important}[data-prepr-ab="' + bucket + '"]{display:contents!important}';
1598
- try {
1599
- if (!state.__preprAbSheet) {
1600
- const sheet = new CSSStyleSheet();
1601
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1602
- state.__preprAbSheet = sheet;
1456
+ const key = location.pathname + location.search;
1457
+ if (decision !== void 0) state.__preprDecision = { key, value: decision };
1458
+ const current = state.__preprDecision;
1459
+ if (!current || current.key !== key) return false;
1460
+ document.querySelectorAll("[data-prepr-decision]").forEach((marker) => {
1461
+ marker.setAttribute("data-prepr-decided", "");
1462
+ });
1463
+ const defaults = current.value === "*";
1464
+ const chosen = {};
1465
+ current.value.split("~").forEach((pair) => {
1466
+ const colon = pair.indexOf(":");
1467
+ if (colon > 0)
1468
+ chosen[pair.slice(0, colon)] = pair.slice(colon + 1).split("|");
1469
+ });
1470
+ document.querySelectorAll("[data-prepr-v]").forEach((element) => {
1471
+ const nested = element.hasAttribute("data-prepr-nested");
1472
+ const show = defaults ? element.hasAttribute("data-prepr-default") : (element.getAttribute("data-prepr-v") || "").split(" ").every((token) => {
1473
+ const colon = token.indexOf(":");
1474
+ const variant = token.slice(colon + 1);
1475
+ const variants = chosen[token.slice(0, colon)];
1476
+ return !!variants && (nested ? variants[0] === variant : variants.indexOf(variant) >= 0);
1477
+ });
1478
+ if (show) element.setAttribute("data-prepr-show", "");
1479
+ else element.removeAttribute("data-prepr-show");
1480
+ });
1481
+ return true;
1482
+ }
1483
+ function requestPreprDecision(key, options = {}) {
1484
+ const fresh = 1e4;
1485
+ const keep = 18e5;
1486
+ const keepStatic = 36e5;
1487
+ const state = window;
1488
+ const asking = state.__preprAsking = state.__preprAsking || {};
1489
+ const read = () => {
1490
+ try {
1491
+ const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
1492
+ return all && typeof all === "object" ? all : {};
1493
+ } catch {
1494
+ return {};
1603
1495
  }
1604
- state.__preprAbSheet.replaceSync(css);
1605
- } catch {
1606
- if (!state.__preprAbSheet) {
1607
- state.__preprAbSheet = document.createElement("style");
1608
- document.head.appendChild(state.__preprAbSheet);
1496
+ };
1497
+ const ask = () => {
1498
+ if (!asking[key]) {
1499
+ asking[key] = fetch(key, {
1500
+ headers: { "Prepr-Decide": "1" },
1501
+ cache: "no-store",
1502
+ credentials: "same-origin",
1503
+ // The proxy gives up on Prepr after its own `timeoutMs`; this only
1504
+ // covers a proxy that never answers.
1505
+ signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
1506
+ }).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
1507
+ delete asking[key];
1508
+ try {
1509
+ const all = read();
1510
+ const at = Date.now();
1511
+ Object.keys(all).forEach((stored) => {
1512
+ const entry = all[stored];
1513
+ if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
1514
+ delete all[stored];
1515
+ });
1516
+ const kept = all[key];
1517
+ if (answer !== "*" || !kept || kept.value === "*")
1518
+ all[key] = { value: answer, at };
1519
+ sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
1520
+ } catch {
1521
+ }
1522
+ return answer;
1523
+ });
1609
1524
  }
1610
- state.__preprAbSheet.textContent = css;
1525
+ return asking[key];
1526
+ };
1527
+ const last = read()[key];
1528
+ const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
1529
+ const value = last && typeof last.value === "string" ? last.value : null;
1530
+ if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
1531
+ if (value !== "" && age >= fresh) {
1532
+ ask().then((answer) => {
1533
+ if (answer !== "*" && answer !== value && options.onChange)
1534
+ options.onChange(answer);
1535
+ });
1536
+ }
1537
+ return { result: value, value: Promise.resolve(value) };
1611
1538
  }
1539
+ const pending = {
1540
+ value: ask()
1541
+ };
1542
+ pending.value.then((answer) => {
1543
+ pending.result = answer;
1544
+ });
1545
+ return pending;
1612
1546
  }
1613
- function applyPreprDecisions(view) {
1614
- const viewState = window;
1615
- const current = view === void 0 ? location.pathname + location.search : view;
1616
- if (viewState.__preprDecisionsView === current) return;
1617
- viewState.__preprDecisionsView = current;
1618
- const match = document.cookie.match(/(?:^|; )prepr_decisions=([^;]*)/);
1619
- let raw;
1620
- try {
1621
- raw = match ? decodeURIComponent(match[1]) : "";
1622
- } catch {
1623
- raw = "";
1624
- }
1547
+ function startPreprDecision() {
1625
1548
  const state = window;
1626
- if (state.__preprDecisions === raw) return;
1627
- state.__preprDecisions = raw;
1628
- const id = /^[\w-]+$/;
1629
- let css = "";
1630
- raw.split("~").forEach((pair) => {
1631
- const at = pair.indexOf(":");
1632
- const group = pair.slice(0, at);
1633
- if (at < 1 || !id.test(group)) return;
1634
- const selector = '[data-prepr-group="' + group + '"]';
1635
- css += selector + "{display:none!important}";
1636
- pair.slice(at + 1).split("|").forEach((variant) => {
1637
- if (id.test(variant)) {
1638
- css += selector + '[data-prepr-variant="' + variant + '"]{display:contents!important}';
1639
- }
1640
- });
1549
+ const key = location.pathname + location.search;
1550
+ const apply = (answer) => {
1551
+ if (location.pathname + location.search === key) state.__preprApply(answer);
1552
+ };
1553
+ const pending = state.__preprRequest(key, {
1554
+ onChange: apply,
1555
+ personalized: true
1641
1556
  });
1642
- try {
1643
- if (!state.__preprDecisionSheet) {
1644
- const sheet = new CSSStyleSheet();
1645
- document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);
1646
- state.__preprDecisionSheet = sheet;
1647
- }
1648
- state.__preprDecisionSheet.replaceSync(css);
1649
- } catch {
1650
- if (!state.__preprDecisionSheet) {
1651
- state.__preprDecisionSheet = document.createElement("style");
1652
- document.head.appendChild(state.__preprDecisionSheet);
1653
- }
1654
- state.__preprDecisionSheet.textContent = css;
1655
- }
1557
+ if (pending.result !== void 0) apply(pending.result);
1558
+ else pending.value.then(apply);
1656
1559
  }
1657
1560
 
1658
1561
  // src/nextjs/stack.ts
1659
1562
  var import_components = require("./components.cjs");
1660
- var AB_ATTRIBUTE = "data-prepr-ab";
1661
- var AB_BUCKETS = ["A", "B"];
1662
- var AB_BASE_CSS = `[${AB_ATTRIBUTE}]{display:contents}[${AB_ATTRIBUTE}]:not([${AB_ATTRIBUTE}="A"]){display:none}`;
1663
- var AB_SWITCH_SCRIPT = `(${applyPreprAbBucket.toString()})();(${applyPreprDecisions.toString()})()`;
1664
- var GROUP_ATTRIBUTE = "data-prepr-group";
1665
- var VARIANT_ATTRIBUTE = "data-prepr-variant";
1563
+ var VARIANT_ATTRIBUTE = "data-prepr-v";
1666
1564
  var DEFAULT_ATTRIBUTE = "data-prepr-default";
1667
- var GROUP_BASE_CSS = `[${GROUP_ATTRIBUTE}]{display:contents}[${GROUP_ATTRIBUTE}]:not([${DEFAULT_ATTRIBUTE}]){display:none}`;
1565
+ var NESTED_ATTRIBUTE = "data-prepr-nested";
1566
+ var GROUP_BASE_CSS = `[${VARIANT_ATTRIBUTE}]{display:none}[${VARIANT_ATTRIBUTE}][data-prepr-show]{display:contents}[data-prepr-decision]:not([data-prepr-decided])~[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents;visibility:hidden}`;
1567
+ var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
1568
+ var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
1569
+ var APPLY_SCRIPT = "__preprApply()";
1668
1570
  function variantKeysOf(items) {
1669
1571
  return items.flatMap((item) => {
1670
- const key = preprVariantProps(item)["data-prepr-variant-key"];
1572
+ const key = preprVariantProps(item)["data-prepr-variant-key"] ?? nestedKeyOf(item);
1671
1573
  return key ? [key] : [];
1672
1574
  });
1673
1575
  }
1674
- function renderBuckets(items, render, options) {
1675
- const {
1676
- bucket,
1677
- decisions,
1678
- abTest,
1679
- index,
1680
- track,
1681
- placeholder = false,
1682
- defaultVariant = true
1683
- } = options;
1684
- const renderVariant = (variant, key) => {
1685
- const resolved = resolveVariants(items, { decisions, bucket: variant });
1686
- const ab = abTest && !bucket ? variant : void 0;
1687
- const content = render(resolved, {
1688
- index,
1689
- placeholder,
1690
- defaultVariant: defaultVariant && (ab === void 0 || ab === "A")
1691
- });
1692
- const variantKeys = track ? variantKeysOf(resolved) : [];
1693
- if (variantKeys.length > 0) {
1694
- return (0, import_react.createElement)(
1695
- import_components.PreprVariantTracking,
1696
- { key, variantKeys, ab },
1697
- content
1698
- );
1576
+ function nestedKeyOf(value) {
1577
+ if (!value || typeof value !== "object") return void 0;
1578
+ for (const child of Object.values(value)) {
1579
+ const items = Array.isArray(child) ? child : [child];
1580
+ for (const item of items) {
1581
+ const key = contextOf(item)?.variant_key ?? (item && nestedKeyOf(item));
1582
+ if (key) return key;
1583
+ }
1584
+ }
1585
+ return void 0;
1586
+ }
1587
+ function renderItems(items, render, options) {
1588
+ const resolved = resolveVariants(items, {
1589
+ decisions: options.decisions,
1590
+ bucket: options.bucket
1591
+ });
1592
+ const content = render(resolved, {
1593
+ index: options.index,
1594
+ defaultVariant: options.defaultVariant
1595
+ });
1596
+ const variantKeys = options.track ? variantKeysOf(resolved) : [];
1597
+ return variantKeys.length > 0 ? (0, import_react.createElement)(import_components.PreprVariantTracking, { variantKeys }, content) : content;
1598
+ }
1599
+ function decidedGroups(items, bucket) {
1600
+ const groups = /* @__PURE__ */ new Map();
1601
+ const walk = (value) => {
1602
+ if (Array.isArray(value)) return value.forEach(walk);
1603
+ if (!value || typeof value !== "object") return;
1604
+ const ctx = contextOf(value);
1605
+ const decided = ctx?.kind === "PERSONALIZATION" || ctx?.kind === "AB_TEST" && !bucket;
1606
+ if (decided && ctx.variant_id) {
1607
+ const group = groups.get(ctx.group_id) ?? {
1608
+ variants: /* @__PURE__ */ new Set(),
1609
+ fallback: ctx.kind === "AB_TEST" ? "A" : PREPR_DEFAULT_VARIANT
1610
+ };
1611
+ group.variants.add(ctx.variant_id);
1612
+ groups.set(ctx.group_id, group);
1699
1613
  }
1700
- if (ab) return (0, import_react.createElement)("div", { key, [AB_ATTRIBUTE]: ab }, content);
1701
- return content;
1614
+ Object.values(value).forEach(walk);
1702
1615
  };
1703
- if (!abTest || bucket) return renderVariant(bucket);
1704
- return AB_BUCKETS.map((variant) => renderVariant(variant, variant));
1705
- }
1706
- function renderAllVariants(block, render, options) {
1707
- const group = block.key.slice("group:".length);
1708
- const variants = [
1709
- ...new Set(
1710
- block.items.flatMap((item) => {
1711
- const ctx = item[PREPR_CONTEXT_ALIAS];
1712
- return ctx?.variant_id ? [ctx.variant_id] : [];
1713
- })
1714
- )
1715
- ];
1716
- return variants.map(
1717
- (variant) => (0, import_react.createElement)(
1616
+ walk(items);
1617
+ return groups;
1618
+ }
1619
+ function renderVariants(block, groups, render, options) {
1620
+ const top = block.key.startsWith("group:") ? block.key.slice("group:".length) : void 0;
1621
+ const nested = !(top !== void 0 && groups.size === 1);
1622
+ const choices = [...groups].map(([group, { variants, fallback }]) => {
1623
+ const ids = nested ? [fallback, ...variants] : [...variants];
1624
+ return [...new Set(ids)].map((variant) => [group, variant]);
1625
+ });
1626
+ const combinations = choices.reduce(
1627
+ (all, pairs) => all.flatMap((combo) => pairs.map((p) => [...combo, p])),
1628
+ [[]]
1629
+ );
1630
+ return combinations.map((combination) => {
1631
+ const pairs = combination.map(([group, variant]) => `${group}:${variant}`);
1632
+ const isDefault = combination.every(
1633
+ ([group, variant]) => variant === groups.get(group).fallback
1634
+ );
1635
+ return (0, import_react.createElement)(
1718
1636
  "div",
1719
1637
  {
1720
- key: variant,
1721
- [GROUP_ATTRIBUTE]: group,
1722
- [VARIANT_ATTRIBUTE]: variant,
1723
- ...variant === PREPR_DEFAULT_VARIANT ? { [DEFAULT_ATTRIBUTE]: "" } : {}
1638
+ key: pairs.join(" "),
1639
+ [VARIANT_ATTRIBUTE]: pairs.join(" "),
1640
+ ...isDefault ? { [DEFAULT_ATTRIBUTE]: "" } : {},
1641
+ ...nested ? { [NESTED_ATTRIBUTE]: "" } : {},
1642
+ // The decision script marks the shown wrapper before hydration.
1643
+ suppressHydrationWarning: true
1724
1644
  },
1725
- renderBuckets(block.items, render, {
1645
+ renderItems(block.items, render, {
1726
1646
  bucket: options.bucket,
1727
- decisions: /* @__PURE__ */ new Map([[group, [variant]]]),
1728
- abTest: false,
1647
+ decisions: new Map(combination.map(([g, v]) => [g, [v]])),
1729
1648
  index: block.index,
1730
1649
  track: options.track,
1731
- defaultVariant: variant === PREPR_DEFAULT_VARIANT
1650
+ defaultVariant: isDefault
1732
1651
  })
1733
- )
1734
- );
1735
- }
1736
- async function PersonalizedBlock({
1737
- items,
1738
- decisions,
1739
- bucket,
1740
- abTest,
1741
- index,
1742
- render,
1743
- track
1744
- }) {
1745
- const decided = await decisions.catch((error) => {
1746
- if (isFrameworkSignal(error)) throw error;
1747
- return void 0;
1748
- });
1749
- return renderBuckets(items, render, {
1750
- bucket,
1751
- decisions: decided,
1752
- abTest,
1753
- index,
1754
- track
1652
+ );
1755
1653
  });
1756
1654
  }
1757
1655
  function PreprStack({
1758
1656
  items,
1759
- decisions,
1760
1657
  render,
1761
1658
  bucket,
1762
1659
  nonce,
1763
- trackVariants = false,
1764
- aboveTheFold = 1
1660
+ trackVariants = false
1765
1661
  }) {
1766
- decisions.catch(() => void 0);
1767
- const blocks = splitStack(items);
1768
- const decidedInProxy = (block) => block.index < aboveTheFold && block.personalized && !block.abTest && block.key.startsWith("group:");
1769
- const hasDecided = blocks.some(decidedInProxy);
1770
- const needsSwitch = !bucket && blocks.some((block) => block.abTest) || hasDecided;
1662
+ const blocks = splitStack(items).map((block) => ({
1663
+ ...block,
1664
+ groups: decidedGroups(block.items, bucket)
1665
+ }));
1666
+ const varies = blocks.some((block) => block.groups.size > 0);
1771
1667
  return (0, import_react.createElement)(
1772
1668
  import_react.Fragment,
1773
1669
  null,
1774
- needsSwitch && (0, import_react.createElement)(
1670
+ varies && (0, import_react.createElement)(
1775
1671
  import_react.Fragment,
1776
- { key: "prepr-ab" },
1672
+ { key: "prepr-switch" },
1777
1673
  (0, import_react.createElement)("style", {
1778
1674
  nonce,
1779
- dangerouslySetInnerHTML: {
1780
- __html: AB_BASE_CSS + (hasDecided ? GROUP_BASE_CSS : "")
1781
- }
1675
+ dangerouslySetInnerHTML: { __html: GROUP_BASE_CSS }
1676
+ }),
1677
+ (0, import_react.createElement)("noscript", {
1678
+ dangerouslySetInnerHTML: { __html: NOSCRIPT_HTML }
1782
1679
  }),
1783
1680
  (0, import_react.createElement)("script", {
1784
1681
  nonce,
1785
- dangerouslySetInnerHTML: { __html: AB_SWITCH_SCRIPT }
1682
+ dangerouslySetInnerHTML: { __html: DECISION_SCRIPT }
1786
1683
  }),
1787
- (0, import_react.createElement)(import_components.PreprAbSwitch),
1788
- hasDecided && (0, import_react.createElement)(import_components.PreprPersonalizationSwitch, { refresh: false })
1684
+ (0, import_react.createElement)(import_components.PreprDecisionApplier),
1685
+ // Marked decided by the decision switch; never laid out.
1686
+ (0, import_react.createElement)("template", {
1687
+ "data-prepr-decision": "",
1688
+ suppressHydrationWarning: true
1689
+ })
1789
1690
  ),
1790
1691
  ...blocks.map((block) => {
1791
- if (decidedInProxy(block)) {
1692
+ if (block.groups.size === 0) {
1792
1693
  return (0, import_react.createElement)(
1793
1694
  import_react.Fragment,
1794
1695
  { key: block.key },
1795
- renderAllVariants(block, render, { bucket, track: trackVariants })
1696
+ renderItems(block.items, render, {
1697
+ bucket,
1698
+ index: block.index,
1699
+ track: trackVariants,
1700
+ defaultVariant: true
1701
+ })
1796
1702
  );
1797
1703
  }
1798
- const fallback = renderBuckets(block.items, render, {
1799
- bucket,
1800
- abTest: block.abTest,
1801
- index: block.index,
1802
- track: trackVariants && !block.personalized,
1803
- placeholder: block.personalized
1804
- });
1805
- if (!block.personalized) {
1806
- return (0, import_react.createElement)(import_react.Fragment, { key: block.key }, fallback);
1807
- }
1808
1704
  return (0, import_react.createElement)(
1809
- import_react.Suspense,
1810
- { key: block.key, fallback },
1811
- (0, import_react.createElement)(PersonalizedBlock, {
1812
- items: block.items,
1813
- decisions,
1705
+ import_react.Fragment,
1706
+ { key: block.key },
1707
+ renderVariants(block, block.groups, render, {
1814
1708
  bucket,
1815
- abTest: block.abTest,
1816
- index: block.index,
1817
- render,
1818
1709
  track: trackVariants
1710
+ }),
1711
+ (0, import_react.createElement)("script", {
1712
+ nonce,
1713
+ dangerouslySetInnerHTML: { __html: APPLY_SCRIPT }
1819
1714
  })
1820
1715
  );
1821
1716
  })
@@ -1827,33 +1722,59 @@ function createPreprClient2(options) {
1827
1722
  const { cacheTags = ["prepr"], revalidate, ...coreOptions } = options;
1828
1723
  const client = createPreprClient({
1829
1724
  // The schema introspection goes through the Data Cache: shared across
1830
- // server instances, refreshed hourly and by revalidateTag on publish.
1725
+ // server instances, refreshed hourly and by a `schema.changed` webhook.
1831
1726
  schemaInit: {
1832
- next: { tags: cacheTags, revalidate: 3600 }
1727
+ next: { tags: [...cacheTags, PREPR_SCHEMA_TAG], revalidate: 3600 }
1833
1728
  },
1834
1729
  ...coreOptions
1835
1730
  });
1836
- const seenGroups = /* @__PURE__ */ new Map();
1837
1731
  const nextClient = {
1838
1732
  async query(document2, variables, queryOptions = {}) {
1839
- const { personalize = false, ...rest } = queryOptions;
1840
- const init = rest.init;
1841
- const tagged = {
1733
+ const { personalize = false, tagItems = false, ...rest } = queryOptions;
1734
+ const { next, ...init } = rest.init ?? {};
1735
+ if (tagItems && typeof nextCache.cacheTag !== "function") {
1736
+ throw new Error("tagItems requires Next.js 16 or later.");
1737
+ }
1738
+ const options2 = {
1842
1739
  ...rest,
1843
- init: {
1740
+ // With tagItems, `next` is dropped too: tags or revalidate alongside
1741
+ // `no-store` are a conflict Next.js resolves by ignoring the store.
1742
+ // Otherwise tags are attached unconditionally; the core client drops
1743
+ // them again whenever the request ends up `no-store` (personalized,
1744
+ // per-request headers, or preview).
1745
+ init: tagItems ? { ...init, cache: "no-store" } : {
1844
1746
  ...init,
1845
1747
  next: {
1846
- tags: cacheTags,
1847
1748
  ...revalidate === void 0 ? {} : { revalidate },
1848
- ...init?.next
1749
+ ...next,
1750
+ // Merged, so a per-query tag never drops the client's own,
1751
+ // which the publish webhook relies on.
1752
+ tags: [...cacheTags, ...next?.tags ?? []]
1849
1753
  }
1850
1754
  }
1851
1755
  };
1852
- if (!personalize) return client.query(document2, variables, tagged);
1853
- return client.query(document2, variables, {
1854
- ...tagged,
1855
- headers: mergePreprHeaders(await getPreprHeaders(), rest.headers)
1856
- });
1756
+ if (personalize) {
1757
+ options2.headers = mergePreprHeaders(
1758
+ await getPreprHeaders(),
1759
+ rest.headers
1760
+ );
1761
+ }
1762
+ const result = await client.query(document2, variables, options2);
1763
+ if (tagItems) {
1764
+ let modelTags;
1765
+ try {
1766
+ modelTags = preprModelTagsFor(
1767
+ documentOf(document2),
1768
+ result.data,
1769
+ await clientSchemas.get(client)()
1770
+ );
1771
+ } catch {
1772
+ modelTags = [PREPR_ANY_ITEM_TAG];
1773
+ }
1774
+ nextCache.cacheTag(...cacheTags, ...next?.tags ?? [], ...modelTags);
1775
+ nextCache.cacheTag(...preprCacheTags(result.data));
1776
+ }
1777
+ return result;
1857
1778
  },
1858
1779
  async decide(document2, variables, queryOptions = {}) {
1859
1780
  return client.decide(document2, variables, {
@@ -1864,27 +1785,14 @@ function createPreprClient2(options) {
1864
1785
  )
1865
1786
  });
1866
1787
  },
1867
- hasAbTests: (document2, variables) => client.hasAbTests(document2, variables),
1868
- resolveDecisions: (request, response, document2, variables, options2) => resolveDecisionsWith(
1869
- client,
1870
- seenGroups,
1871
- request,
1872
- response,
1873
- document2,
1874
- variables,
1875
- options2
1876
- )
1788
+ hasAbTests: (document2, variables) => client.hasAbTests(document2, variables)
1877
1789
  };
1878
- preprClientInternals.set(nextClient, {
1879
- client,
1880
- seenGroups,
1881
- url: coreOptions.url
1882
- });
1790
+ preprClientInternals.set(nextClient, { client });
1883
1791
  return nextClient;
1884
1792
  }
1885
1793
  // Annotate the CommonJS export names for ESM import in node:
1886
1794
  0 && (module.exports = {
1887
- PREPR_DECISIONS_COOKIE,
1795
+ PREPR_ANY_ITEM_TAG,
1888
1796
  PREPR_DEFAULT_VARIANT,
1889
1797
  PreprError,
1890
1798
  PreprStack,
@@ -1893,6 +1801,8 @@ function createPreprClient2(options) {
1893
1801
  decisionsFrom,
1894
1802
  isPersonalizedResponse,
1895
1803
  loadPreprSchema,
1804
+ preprCacheTags,
1805
+ preprModelTag,
1896
1806
  preprVariantProps,
1897
1807
  resolveVariants,
1898
1808
  splitStack,