@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
package/dist/graphql.cjs CHANGED
@@ -20,12 +20,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/graphql.ts
21
21
  var graphql_exports = {};
22
22
  __export(graphql_exports, {
23
+ PREPR_ANY_ITEM_TAG: () => PREPR_ANY_ITEM_TAG,
23
24
  PREPR_DEFAULT_VARIANT: () => PREPR_DEFAULT_VARIANT,
24
25
  PreprError: () => PreprError,
25
26
  createPreprClient: () => createPreprClient,
26
27
  decisionsFrom: () => decisionsFrom,
27
28
  isPersonalizedResponse: () => isPersonalizedResponse,
28
29
  loadPreprSchema: () => loadPreprSchema,
30
+ preprCacheTags: () => preprCacheTags,
31
+ preprModelTag: () => preprModelTag,
29
32
  preprVariantProps: () => preprVariantProps,
30
33
  resolveVariants: () => resolveVariants,
31
34
  splitStack: () => splitStack,
@@ -34,6 +37,31 @@ __export(graphql_exports, {
34
37
  });
35
38
  module.exports = __toCommonJS(graphql_exports);
36
39
 
40
+ // src/core/graphql/cache-tags.ts
41
+ var MAX_TAGS = 128;
42
+ var MAX_TAG_LENGTH = 256;
43
+ var PREPR_ANY_ITEM_TAG = "prepr:any";
44
+ var preprItemTag = (id) => `prepr:${id}`;
45
+ var NOT_ALPHANUMERIC = /[^a-z0-9]/g;
46
+ var preprModelTag = (model) => `prepr:model:${model.toLowerCase().replace(NOT_ALPHANUMERIC, "")}`;
47
+ function preprCacheTags(data) {
48
+ const tags = /* @__PURE__ */ new Set();
49
+ const stack = [data];
50
+ while (stack.length > 0) {
51
+ const value = stack.pop();
52
+ if (typeof value !== "object" || value === null) continue;
53
+ const id = value._id;
54
+ if (typeof id === "string" && id !== "") {
55
+ const tag = preprItemTag(id);
56
+ if (tag.length > MAX_TAG_LENGTH) return [PREPR_ANY_ITEM_TAG];
57
+ tags.add(tag);
58
+ if (tags.size > MAX_TAGS) return [PREPR_ANY_ITEM_TAG];
59
+ }
60
+ for (const child of Object.values(value)) stack.push(child);
61
+ }
62
+ return [...tags];
63
+ }
64
+
37
65
  // src/core/server.ts
38
66
  var PreprError = class extends Error {
39
67
  constructor(message, code, context, originalError) {
@@ -148,7 +176,10 @@ function decisionSelectionSet(set, typeName, path, ctx) {
148
176
  path,
149
177
  ctx
150
178
  );
151
- if (pruned) {
179
+ if (!pruned) continue;
180
+ if (fragment.typeCondition.name.value === typeName) {
181
+ selections.push(...pruned.selections);
182
+ } else {
152
183
  selections.push(
153
184
  inlineFragment(fragment.typeCondition.name.value, [
154
185
  ...pruned.selections
@@ -158,13 +189,19 @@ function decisionSelectionSet(set, typeName, path, ctx) {
158
189
  continue;
159
190
  }
160
191
  if (selection.kind === "InlineFragment") {
192
+ const condition = selection.typeCondition?.name.value ?? typeName;
161
193
  const pruned = decisionSelectionSet(
162
194
  selection.selectionSet,
163
- selection.typeCondition?.name.value ?? typeName,
195
+ condition,
164
196
  path,
165
197
  ctx
166
198
  );
167
- if (pruned) selections.push({ ...selection, selectionSet: pruned });
199
+ if (!pruned) continue;
200
+ if (condition === typeName && !selection.directives?.length) {
201
+ selections.push(...pruned.selections);
202
+ } else {
203
+ selections.push({ ...selection, selectionSet: pruned });
204
+ }
168
205
  continue;
169
206
  }
170
207
  if (!selection.selectionSet) continue;
@@ -238,20 +275,58 @@ function toDecisionDocument(document2, schema, groupPaths, options) {
238
275
  const used = /* @__PURE__ */ new Set();
239
276
  collectVariables(pruned, used);
240
277
  collectVariables(operation.directives, used);
278
+ const shared = sharedContextFragment(schema);
241
279
  return {
242
280
  kind: "Document",
243
281
  definitions: [
282
+ ...shared ? [shared.definition] : [],
244
283
  {
245
284
  ...operation,
246
285
  name: operation.name && name(`${operation.name.value}Decision`),
247
286
  variableDefinitions: (operation.variableDefinitions ?? []).filter(
248
287
  (definition) => used.has(definition.variable.name.value)
249
288
  ),
250
- selectionSet: pruned
289
+ selectionSet: shared ? shared.apply(pruned) : pruned
251
290
  }
252
291
  ]
253
292
  };
254
293
  }
294
+ function sharedContextFragment(schema) {
295
+ const contextTypes = new Set(
296
+ Object.values(schema.types).flatMap(
297
+ (type) => type.hasContext ? [type.fields._context.type] : []
298
+ )
299
+ );
300
+ if (contextTypes.size !== 1) return null;
301
+ const [contextType] = contextTypes;
302
+ const fragmentName = "c";
303
+ const definition = {
304
+ kind: "FragmentDefinition",
305
+ name: name(fragmentName),
306
+ typeCondition: { kind: "NamedType", name: name(contextType) },
307
+ selectionSet: selectionSet(
308
+ ["kind", "group_id", "variant_id"].map(
309
+ (field) => ({ kind: "Field", name: name(field) })
310
+ )
311
+ )
312
+ };
313
+ const spread = {
314
+ kind: "FragmentSpread",
315
+ name: name(fragmentName)
316
+ };
317
+ const apply = (set) => selectionSet(
318
+ set.selections.map((selection) => {
319
+ if (selection.kind === "Field" && selection.alias?.value === PREPR_CONTEXT_ALIAS) {
320
+ return { ...selection, selectionSet: selectionSet([spread]) };
321
+ }
322
+ return "selectionSet" in selection && selection.selectionSet ? {
323
+ ...selection,
324
+ selectionSet: apply(selection.selectionSet)
325
+ } : selection;
326
+ })
327
+ );
328
+ return { definition, apply };
329
+ }
255
330
 
256
331
  // src/core/variants/probe-document.ts
257
332
  function probeSelectionSet(set, fragments) {
@@ -825,6 +900,7 @@ function createVaryProbe(run, options) {
825
900
  }
826
901
 
827
902
  // src/core/graphql/client.ts
903
+ var clientSchemas = /* @__PURE__ */ new WeakMap();
828
904
  function isParseableUrl(url) {
829
905
  try {
830
906
  new URL(url);
@@ -861,7 +937,7 @@ function createPreprClient(options) {
861
937
  ));
862
938
  const variantsDocuments = /* @__PURE__ */ new Map();
863
939
  const decisionDocuments = /* @__PURE__ */ new Map();
864
- return {
940
+ const client = {
865
941
  async query(document2, variables, queryOptions = {}) {
866
942
  const { variants, ...rest } = queryOptions;
867
943
  if (!variants) return run(document2, variables, rest);
@@ -873,10 +949,10 @@ function createPreprClient(options) {
873
949
  return run(derived, variables, rest);
874
950
  },
875
951
  async decide(document2, variables, queryOptions = {}) {
876
- const { from, topLevelOnly, ...rest } = queryOptions;
952
+ const { from, ...rest } = queryOptions;
877
953
  const paths = from === void 0 ? void 0 : personalizationPaths(from);
878
954
  if (paths && paths.size === 0) return /* @__PURE__ */ new Map();
879
- const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : topLevelOnly ? "top" : "*";
955
+ const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : "*";
880
956
  let byPaths = decisionDocuments.get(document2);
881
957
  if (!byPaths) {
882
958
  byPaths = /* @__PURE__ */ new Map();
@@ -884,9 +960,7 @@ function createPreprClient(options) {
884
960
  }
885
961
  let derived = byPaths.get(signature);
886
962
  if (derived === void 0) {
887
- derived = toDecisionDocument(document2, await loadSchema(), paths, {
888
- topLevelOnly: !paths && topLevelOnly
889
- });
963
+ derived = toDecisionDocument(document2, await loadSchema(), paths);
890
964
  byPaths.set(signature, derived);
891
965
  }
892
966
  if (!derived) return /* @__PURE__ */ new Map();
@@ -900,6 +974,8 @@ function createPreprClient(options) {
900
974
  (vary) => vary === null || SEGMENTS_VARY.test(vary)
901
975
  )
902
976
  };
977
+ clientSchemas.set(client, loadSchema);
978
+ return client;
903
979
  }
904
980
 
905
981
  // src/core/graphql/schema-loader.ts
@@ -927,12 +1003,15 @@ async function loadPreprSchema(url, options = {}) {
927
1003
  }
928
1004
  // Annotate the CommonJS export names for ESM import in node:
929
1005
  0 && (module.exports = {
1006
+ PREPR_ANY_ITEM_TAG,
930
1007
  PREPR_DEFAULT_VARIANT,
931
1008
  PreprError,
932
1009
  createPreprClient,
933
1010
  decisionsFrom,
934
1011
  isPersonalizedResponse,
935
1012
  loadPreprSchema,
1013
+ preprCacheTags,
1014
+ preprModelTag,
936
1015
  preprVariantProps,
937
1016
  resolveVariants,
938
1017
  splitStack,
@@ -96,39 +96,6 @@ interface PreprVariantProps {
96
96
  */
97
97
  declare function preprVariantProps(item: unknown): PreprVariantProps;
98
98
 
99
- /**
100
- * The schema facts the query rewrites need, from `loadPreprSchema`. Pass it
101
- * along; its shape is not part of the public API.
102
- */
103
- type PreprSchema = PreprSchemaInfo;
104
- /**
105
- * Your query, asking for every personalization and A/B variant: what
106
- * `query(..., { variants: true })` sends, as a string for your own GraphQL
107
- * client. The result is the same for every visitor, so cache it like any
108
- * other content query, and render it with `resolveVariants` or `PreprStack`.
109
- *
110
- * A query without personalizable fields comes back unchanged (minified).
111
- */
112
- declare function toVariantsQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema): string;
113
- /**
114
- * The per-visitor decision query for the same query: send it with the
115
- * visitor's Prepr headers, never cached, and pass the response's `data` to
116
- * `decisionsFrom`.
117
- *
118
- * Pass the variants result as `from` whenever you have it: the query then
119
- * asks only about the lists that hold segment groups on this page, which is
120
- * a few hundred bytes instead of the whole page and markedly faster for a
121
- * visitor Prepr has not seen before. Without `from` it asks about every
122
- * personalizable list. `topLevelOnly` stops at the first personalizable list
123
- * on each path, for middleware that decides only the top of a stack.
124
- *
125
- * Null when there is nothing to decide: send no request then.
126
- */
127
- declare function toDecisionQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema, options?: {
128
- from?: unknown;
129
- topLevelOnly?: boolean;
130
- }): string | null;
131
-
132
99
  /**
133
100
  * A GraphQL document accepted by `query`: a plain string, graphql-codegen's
134
101
  * `TypedDocumentString` (`documentMode: 'string'`), or any `DocumentNode`,
@@ -222,11 +189,6 @@ interface PreprDecideOptions extends Omit<PreprQueryOptions, 'variants'> {
222
189
  * none, e.g. on pages with only A/B tests or no personalization.
223
190
  */
224
191
  from?: unknown;
225
- /**
226
- * @internal Ask only about the first personalizable list on each path,
227
- * without `from`: what middleware needs for groups at the top of a stack.
228
- */
229
- topLevelOnly?: boolean;
230
192
  }
231
193
  interface PreprQueryResult<TResult> {
232
194
  data: TResult;
@@ -271,6 +233,74 @@ interface PreprClient {
271
233
  hasSegments<TVariables>(document: PreprDocument<unknown, TVariables>, variables?: TVariables): Promise<boolean>;
272
234
  }
273
235
 
236
+ /**
237
+ * The schema facts the query rewrites need, from `loadPreprSchema`. Pass it
238
+ * along; its shape is not part of the public API.
239
+ */
240
+ type PreprSchema = PreprSchemaInfo;
241
+ /**
242
+ * Your query, asking for every personalization and A/B variant: what
243
+ * `query(..., { variants: true })` sends, as a string for your own GraphQL
244
+ * client. The result is the same for every visitor, so cache it like any
245
+ * other content query, and render it with `resolveVariants` or `PreprStack`.
246
+ *
247
+ * A query without personalizable fields comes back unchanged (minified).
248
+ */
249
+ declare function toVariantsQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema): string;
250
+ /**
251
+ * The per-visitor decision query for the same query: send it with the
252
+ * visitor's Prepr headers, never cached, and pass the response's `data` to
253
+ * `decisionsFrom`.
254
+ *
255
+ * Pass the variants result as `from` whenever you have it: the query then
256
+ * asks only about the lists that hold segment groups on this page, which is
257
+ * a few hundred bytes instead of the whole page and markedly faster for a
258
+ * visitor Prepr has not seen before. Without `from` it asks about every
259
+ * personalizable list. `topLevelOnly` stops at the first personalizable list
260
+ * on each path, for middleware that decides only the top of a stack.
261
+ *
262
+ * Null when there is nothing to decide: send no request then.
263
+ */
264
+ declare function toDecisionQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema, options?: {
265
+ from?: unknown;
266
+ topLevelOnly?: boolean;
267
+ }): string | null;
268
+
269
+ /**
270
+ * Tag for pages showing more items than fit in Next.js's tag limits. Every
271
+ * webhook event revalidates it, so those pages refresh on any change.
272
+ */
273
+ declare const PREPR_ANY_ITEM_TAG = "prepr:any";
274
+ /**
275
+ * Cache tag for every page listing items of one model. `tagItems` adds it to
276
+ * list queries and to lookups that found nothing; add it yourself with
277
+ * `cacheTag()` for other pages that depend on which items exist.
278
+ * `createPreprWebhookRoute` revalidates it when an item of that model is
279
+ * published, unpublished or deleted.
280
+ *
281
+ * The name is compared case-insensitively and without spaces or
282
+ * punctuation, so the model's name in Prepr (`'Blog post'`, as webhooks send
283
+ * it) and its GraphQL type (`BlogPost`) give the same tag. A model whose API
284
+ * ID was renamed away from its name gets two different tags.
285
+ *
286
+ * @param model The model's singular name in Prepr or its GraphQL type name.
287
+ */
288
+ declare const preprModelTag: (model: string) => string;
289
+ /**
290
+ * Cache tags for every Prepr item in a query result: `prepr:<_id>` for each
291
+ * object with a string `_id`, at any depth. Pair it with `cacheTag()` inside
292
+ * `'use cache'`, and a publish webhook revalidates only the pages that show
293
+ * the changed item. Run the query with `variants: true` so the tags cover
294
+ * every variant's items, not only those of one visitor.
295
+ *
296
+ * Next.js accepts at most 128 tags of 256 characters per `cacheTag()` call.
297
+ * A result past either limit returns `[PREPR_ANY_ITEM_TAG]` instead, which
298
+ * every webhook event revalidates.
299
+ *
300
+ * @param data A parsed JSON query result, such as `query()`'s `data`.
301
+ */
302
+ declare function preprCacheTags(data: unknown): string[];
303
+
274
304
  /**
275
305
  * Create a GraphQL client for the Prepr API.
276
306
  *
@@ -302,4 +332,4 @@ declare function loadPreprSchema(url: string, options?: {
302
332
  fetch?: typeof fetch;
303
333
  }): Promise<PreprSchemaInfo>;
304
334
 
305
- export { PREPR_DEFAULT_VARIANT, type PreprClient, type PreprClientOptions, type PreprDecideOptions, type PreprDecisions, type PreprDocument, PreprHeaders, type PreprQueryOptions, type PreprQueryResult, type PreprSchema, type PreprStackBlock, type PreprStegaMode, type PreprVariantProps, type ResolveVariantsOptions, createPreprClient, decisionsFrom, loadPreprSchema, preprVariantProps, resolveVariants, splitStack, toDecisionQuery, toVariantsQuery };
335
+ export { PREPR_ANY_ITEM_TAG, PREPR_DEFAULT_VARIANT, type PreprClient, type PreprClientOptions, type PreprDecideOptions, type PreprDecisions, type PreprDocument, PreprHeaders, type PreprQueryOptions, type PreprQueryResult, type PreprSchema, type PreprStackBlock, type PreprStegaMode, type PreprVariantProps, type ResolveVariantsOptions, createPreprClient, decisionsFrom, loadPreprSchema, preprCacheTags, preprModelTag, preprVariantProps, resolveVariants, splitStack, toDecisionQuery, toVariantsQuery };
package/dist/graphql.d.ts CHANGED
@@ -96,39 +96,6 @@ interface PreprVariantProps {
96
96
  */
97
97
  declare function preprVariantProps(item: unknown): PreprVariantProps;
98
98
 
99
- /**
100
- * The schema facts the query rewrites need, from `loadPreprSchema`. Pass it
101
- * along; its shape is not part of the public API.
102
- */
103
- type PreprSchema = PreprSchemaInfo;
104
- /**
105
- * Your query, asking for every personalization and A/B variant: what
106
- * `query(..., { variants: true })` sends, as a string for your own GraphQL
107
- * client. The result is the same for every visitor, so cache it like any
108
- * other content query, and render it with `resolveVariants` or `PreprStack`.
109
- *
110
- * A query without personalizable fields comes back unchanged (minified).
111
- */
112
- declare function toVariantsQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema): string;
113
- /**
114
- * The per-visitor decision query for the same query: send it with the
115
- * visitor's Prepr headers, never cached, and pass the response's `data` to
116
- * `decisionsFrom`.
117
- *
118
- * Pass the variants result as `from` whenever you have it: the query then
119
- * asks only about the lists that hold segment groups on this page, which is
120
- * a few hundred bytes instead of the whole page and markedly faster for a
121
- * visitor Prepr has not seen before. Without `from` it asks about every
122
- * personalizable list. `topLevelOnly` stops at the first personalizable list
123
- * on each path, for middleware that decides only the top of a stack.
124
- *
125
- * Null when there is nothing to decide: send no request then.
126
- */
127
- declare function toDecisionQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema, options?: {
128
- from?: unknown;
129
- topLevelOnly?: boolean;
130
- }): string | null;
131
-
132
99
  /**
133
100
  * A GraphQL document accepted by `query`: a plain string, graphql-codegen's
134
101
  * `TypedDocumentString` (`documentMode: 'string'`), or any `DocumentNode`,
@@ -222,11 +189,6 @@ interface PreprDecideOptions extends Omit<PreprQueryOptions, 'variants'> {
222
189
  * none, e.g. on pages with only A/B tests or no personalization.
223
190
  */
224
191
  from?: unknown;
225
- /**
226
- * @internal Ask only about the first personalizable list on each path,
227
- * without `from`: what middleware needs for groups at the top of a stack.
228
- */
229
- topLevelOnly?: boolean;
230
192
  }
231
193
  interface PreprQueryResult<TResult> {
232
194
  data: TResult;
@@ -271,6 +233,74 @@ interface PreprClient {
271
233
  hasSegments<TVariables>(document: PreprDocument<unknown, TVariables>, variables?: TVariables): Promise<boolean>;
272
234
  }
273
235
 
236
+ /**
237
+ * The schema facts the query rewrites need, from `loadPreprSchema`. Pass it
238
+ * along; its shape is not part of the public API.
239
+ */
240
+ type PreprSchema = PreprSchemaInfo;
241
+ /**
242
+ * Your query, asking for every personalization and A/B variant: what
243
+ * `query(..., { variants: true })` sends, as a string for your own GraphQL
244
+ * client. The result is the same for every visitor, so cache it like any
245
+ * other content query, and render it with `resolveVariants` or `PreprStack`.
246
+ *
247
+ * A query without personalizable fields comes back unchanged (minified).
248
+ */
249
+ declare function toVariantsQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema): string;
250
+ /**
251
+ * The per-visitor decision query for the same query: send it with the
252
+ * visitor's Prepr headers, never cached, and pass the response's `data` to
253
+ * `decisionsFrom`.
254
+ *
255
+ * Pass the variants result as `from` whenever you have it: the query then
256
+ * asks only about the lists that hold segment groups on this page, which is
257
+ * a few hundred bytes instead of the whole page and markedly faster for a
258
+ * visitor Prepr has not seen before. Without `from` it asks about every
259
+ * personalizable list. `topLevelOnly` stops at the first personalizable list
260
+ * on each path, for middleware that decides only the top of a stack.
261
+ *
262
+ * Null when there is nothing to decide: send no request then.
263
+ */
264
+ declare function toDecisionQuery(document: PreprDocument<unknown, unknown> | string, schema: PreprSchema, options?: {
265
+ from?: unknown;
266
+ topLevelOnly?: boolean;
267
+ }): string | null;
268
+
269
+ /**
270
+ * Tag for pages showing more items than fit in Next.js's tag limits. Every
271
+ * webhook event revalidates it, so those pages refresh on any change.
272
+ */
273
+ declare const PREPR_ANY_ITEM_TAG = "prepr:any";
274
+ /**
275
+ * Cache tag for every page listing items of one model. `tagItems` adds it to
276
+ * list queries and to lookups that found nothing; add it yourself with
277
+ * `cacheTag()` for other pages that depend on which items exist.
278
+ * `createPreprWebhookRoute` revalidates it when an item of that model is
279
+ * published, unpublished or deleted.
280
+ *
281
+ * The name is compared case-insensitively and without spaces or
282
+ * punctuation, so the model's name in Prepr (`'Blog post'`, as webhooks send
283
+ * it) and its GraphQL type (`BlogPost`) give the same tag. A model whose API
284
+ * ID was renamed away from its name gets two different tags.
285
+ *
286
+ * @param model The model's singular name in Prepr or its GraphQL type name.
287
+ */
288
+ declare const preprModelTag: (model: string) => string;
289
+ /**
290
+ * Cache tags for every Prepr item in a query result: `prepr:<_id>` for each
291
+ * object with a string `_id`, at any depth. Pair it with `cacheTag()` inside
292
+ * `'use cache'`, and a publish webhook revalidates only the pages that show
293
+ * the changed item. Run the query with `variants: true` so the tags cover
294
+ * every variant's items, not only those of one visitor.
295
+ *
296
+ * Next.js accepts at most 128 tags of 256 characters per `cacheTag()` call.
297
+ * A result past either limit returns `[PREPR_ANY_ITEM_TAG]` instead, which
298
+ * every webhook event revalidates.
299
+ *
300
+ * @param data A parsed JSON query result, such as `query()`'s `data`.
301
+ */
302
+ declare function preprCacheTags(data: unknown): string[];
303
+
274
304
  /**
275
305
  * Create a GraphQL client for the Prepr API.
276
306
  *
@@ -302,4 +332,4 @@ declare function loadPreprSchema(url: string, options?: {
302
332
  fetch?: typeof fetch;
303
333
  }): Promise<PreprSchemaInfo>;
304
334
 
305
- export { PREPR_DEFAULT_VARIANT, type PreprClient, type PreprClientOptions, type PreprDecideOptions, type PreprDecisions, type PreprDocument, PreprHeaders, type PreprQueryOptions, type PreprQueryResult, type PreprSchema, type PreprStackBlock, type PreprStegaMode, type PreprVariantProps, type ResolveVariantsOptions, createPreprClient, decisionsFrom, loadPreprSchema, preprVariantProps, resolveVariants, splitStack, toDecisionQuery, toVariantsQuery };
335
+ export { PREPR_ANY_ITEM_TAG, PREPR_DEFAULT_VARIANT, type PreprClient, type PreprClientOptions, type PreprDecideOptions, type PreprDecisions, type PreprDocument, PreprHeaders, type PreprQueryOptions, type PreprQueryResult, type PreprSchema, type PreprStackBlock, type PreprStegaMode, type PreprVariantProps, type ResolveVariantsOptions, createPreprClient, decisionsFrom, loadPreprSchema, preprCacheTags, preprModelTag, preprVariantProps, resolveVariants, splitStack, toDecisionQuery, toVariantsQuery };
package/dist/graphql.js CHANGED
@@ -21,12 +21,37 @@ import {
21
21
  toSchemaInfo,
22
22
  toVariantsDocument,
23
23
  toVariantsQuery
24
- } from "./chunk-W6AVX6FI.js";
24
+ } from "./chunk-OY7OKO6Q.js";
25
25
  import {
26
26
  PreprError,
27
27
  validatePreprToken
28
- } from "./chunk-OQGEL4TV.js";
29
- import "./chunk-NSQOGI5K.js";
28
+ } from "./chunk-T7ESBVBI.js";
29
+ import "./chunk-ATPB7GEC.js";
30
+
31
+ // src/core/graphql/cache-tags.ts
32
+ var MAX_TAGS = 128;
33
+ var MAX_TAG_LENGTH = 256;
34
+ var PREPR_ANY_ITEM_TAG = "prepr:any";
35
+ var preprItemTag = (id) => `prepr:${id}`;
36
+ var NOT_ALPHANUMERIC = /[^a-z0-9]/g;
37
+ var preprModelTag = (model) => `prepr:model:${model.toLowerCase().replace(NOT_ALPHANUMERIC, "")}`;
38
+ function preprCacheTags(data) {
39
+ const tags = /* @__PURE__ */ new Set();
40
+ const stack = [data];
41
+ while (stack.length > 0) {
42
+ const value = stack.pop();
43
+ if (typeof value !== "object" || value === null) continue;
44
+ const id = value._id;
45
+ if (typeof id === "string" && id !== "") {
46
+ const tag = preprItemTag(id);
47
+ if (tag.length > MAX_TAG_LENGTH) return [PREPR_ANY_ITEM_TAG];
48
+ tags.add(tag);
49
+ if (tags.size > MAX_TAGS) return [PREPR_ANY_ITEM_TAG];
50
+ }
51
+ for (const child of Object.values(value)) stack.push(child);
52
+ }
53
+ return [...tags];
54
+ }
30
55
 
31
56
  // src/core/variants/probe-document.ts
32
57
  function probeSelectionSet(set, fragments) {
@@ -304,6 +329,7 @@ function createVaryProbe(run, options) {
304
329
  }
305
330
 
306
331
  // src/core/graphql/client.ts
332
+ var clientSchemas = /* @__PURE__ */ new WeakMap();
307
333
  function isParseableUrl(url) {
308
334
  try {
309
335
  new URL(url);
@@ -340,7 +366,7 @@ function createPreprClient(options) {
340
366
  ));
341
367
  const variantsDocuments = /* @__PURE__ */ new Map();
342
368
  const decisionDocuments = /* @__PURE__ */ new Map();
343
- return {
369
+ const client = {
344
370
  async query(document, variables, queryOptions = {}) {
345
371
  const { variants, ...rest } = queryOptions;
346
372
  if (!variants) return run(document, variables, rest);
@@ -352,10 +378,10 @@ function createPreprClient(options) {
352
378
  return run(derived, variables, rest);
353
379
  },
354
380
  async decide(document, variables, queryOptions = {}) {
355
- const { from, topLevelOnly, ...rest } = queryOptions;
381
+ const { from, ...rest } = queryOptions;
356
382
  const paths = from === void 0 ? void 0 : personalizationPaths(from);
357
383
  if (paths && paths.size === 0) return /* @__PURE__ */ new Map();
358
- const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : topLevelOnly ? "top" : "*";
384
+ const signature = paths ? [...paths].map(([path, types]) => `${path}:${[...types].sort().join(",")}`).sort().join("|") : "*";
359
385
  let byPaths = decisionDocuments.get(document);
360
386
  if (!byPaths) {
361
387
  byPaths = /* @__PURE__ */ new Map();
@@ -363,9 +389,7 @@ function createPreprClient(options) {
363
389
  }
364
390
  let derived = byPaths.get(signature);
365
391
  if (derived === void 0) {
366
- derived = toDecisionDocument(document, await loadSchema(), paths, {
367
- topLevelOnly: !paths && topLevelOnly
368
- });
392
+ derived = toDecisionDocument(document, await loadSchema(), paths);
369
393
  byPaths.set(signature, derived);
370
394
  }
371
395
  if (!derived) return /* @__PURE__ */ new Map();
@@ -379,6 +403,8 @@ function createPreprClient(options) {
379
403
  (vary) => vary === null || SEGMENTS_VARY.test(vary)
380
404
  )
381
405
  };
406
+ clientSchemas.set(client, loadSchema);
407
+ return client;
382
408
  }
383
409
 
384
410
  // src/core/graphql/schema-loader.ts
@@ -405,12 +431,15 @@ async function loadPreprSchema(url, options = {}) {
405
431
  return schema;
406
432
  }
407
433
  export {
434
+ PREPR_ANY_ITEM_TAG,
408
435
  PREPR_DEFAULT_VARIANT,
409
436
  PreprError,
410
437
  createPreprClient,
411
438
  decisionsFrom,
412
439
  isPersonalizedResponse,
413
440
  loadPreprSchema,
441
+ preprCacheTags,
442
+ preprModelTag,
414
443
  preprVariantProps,
415
444
  resolveVariants,
416
445
  splitStack,
package/dist/index.cjs CHANGED
@@ -45,7 +45,7 @@ __export(src_exports, {
45
45
  module.exports = __toCommonJS(src_exports);
46
46
 
47
47
  // src/version.ts
48
- var VERSION = "0.6.0-alpha.6";
48
+ var VERSION = "0.6.0-alpha.8";
49
49
 
50
50
  // src/core/features.ts
51
51
  function isEnabled(config) {
package/dist/index.d.cts CHANGED
@@ -7,7 +7,7 @@ export { P as PreprPixelConfig, l as loadTrackingPixel, s as setTrackingParam, t
7
7
  * Package version; must match package.json. Lives in its own leaf module so
8
8
  * core can import it without cycling through the barrel.
9
9
  */
10
- declare const VERSION = "0.6.0-alpha.6";
10
+ declare const VERSION = "0.6.0-alpha.8";
11
11
 
12
12
  /**
13
13
  * Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { P as PreprPixelConfig, l as loadTrackingPixel, s as setTrackingParam, t
7
7
  * Package version; must match package.json. Lives in its own leaf module so
8
8
  * core can import it without cycling through the barrel.
9
9
  */
10
- declare const VERSION = "0.6.0-alpha.6";
10
+ declare const VERSION = "0.6.0-alpha.8";
11
11
 
12
12
  /**
13
13
  * Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  loadTrackingPixel,
16
16
  setTrackingParam,
17
17
  trackEvent
18
- } from "./chunk-Y7DWVOOY.js";
18
+ } from "./chunk-2QGUP7VQ.js";
19
19
  import {
20
20
  isPersonalizedResponse
21
21
  } from "./chunk-3NCEVJJ3.js";
@@ -32,11 +32,11 @@ import {
32
32
  getPreprUUIDFromHeaders,
33
33
  getToolbarPropsFromHeaders,
34
34
  validatePreprToken
35
- } from "./chunk-OQGEL4TV.js";
35
+ } from "./chunk-T7ESBVBI.js";
36
36
  import {
37
37
  VERSION,
38
38
  resolveFeatures
39
- } from "./chunk-NSQOGI5K.js";
39
+ } from "./chunk-ATPB7GEC.js";
40
40
 
41
41
  // src/core/middleware.ts
42
42
  var UTM_PARAMS = [