@uniformdev/next-app-router 20.81.1-alpha.12.sha-5d72d595dc → 20.81.1-alpha.15.sha-2b5fd446ca

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.
@@ -1,7 +1,7 @@
1
1
  import { ManifestV2 } from '@uniformdev/context';
2
2
  import { TransformEdgeStreamOptions } from '@uniformdev/context-edge';
3
3
  import { NextRequest, NextFetchEvent } from 'next/server';
4
- import { f as ManifestProvider, E as EdgeRouteFilter, d as EdgeRouteStore, g as ManifestProviderOptions, e as EdgeRouteStoreCache, M as ManifestCache } from './models-D8YUMc4L.js';
4
+ import { f as ManifestProvider, E as EdgeRouteFilter, d as EdgeRouteStore, g as ManifestProviderOptions, e as EdgeRouteStoreCache, M as ManifestCache } from './models-DKZtopoy.js';
5
5
  import { HandleOptions } from './middleware.js';
6
6
 
7
7
  /**
@@ -42,11 +42,19 @@ type UniformEdgeMiddlewareOptions = HandleOptions & {
42
42
  /**
43
43
  * Records what the transform learns about each processed page, such as `vercelEdgeRouteStore`
44
44
  * from `@uniformdev/next-app-router/vercel` or one made with `createEdgeRouteStore`. Filters
45
- * read the record, and processed pages get per-visitor ETags when the origin sends an ETag.
45
+ * read the record, and processed pages get per-visitor ETags unless `etags` is `false`.
46
46
  *
47
47
  * @defaultValue nothing is recorded, and responses get no ETag
48
48
  */
49
49
  store?: EdgeRouteStore;
50
+ /**
51
+ * Gives processed pages per-visitor ETags, from the page placements the `store` records. `false`
52
+ * sends every processed page in full with `private, no-store`, never hashes the page, and records
53
+ * only whether each page has placements.
54
+ *
55
+ * @defaultValue `true`
56
+ */
57
+ etags?: boolean;
50
58
  };
51
59
  /**
52
60
  * Returns middleware that rewrites each request to its Uniform code like `uniformMiddleware`, and
@@ -63,17 +71,20 @@ declare const uniformEdgeMiddleware: (options: UniformEdgeMiddlewareOptions) =>
63
71
  * Handles one request the way {@link uniformEdgeMiddleware} does. The transformed response is
64
72
  * personalized, so it is marked `private, no-store`.
65
73
  *
66
- * When the origin answers 200 with an ETag and the `store` recorded the page's placements for that
67
- * ETag, the variants are chosen before the headers are sent. The response then
68
- * gets an ETag built from the origin's ETag and those choices, and `private, no-cache`. A request
69
- * whose `If-None-Match` matches it gets a `304`. A response that assigns a test or rolls a
70
- * personalization control group carries the visitor state and an ETag that never matches, so the
71
- * visitor's next request is answered in full.
74
+ * When `etags` is on, the origin answers 200 and the `store` recorded the page's placements for
75
+ * its current version, the variants are chosen before the headers are sent. The version is the
76
+ * origin's ETag or, without one, a hash of the untransformed page that the last full response
77
+ * recorded. A page that changed without a new origin ETag is detected when a full response hashes
78
+ * differently, and until then a matching request is answered `304`. The response gets an ETag
79
+ * built from the version and the choices, and `private, no-cache`. A request whose
80
+ * `If-None-Match` matches it gets a `304`. A response that changes the visitor's stored data, such
81
+ * as a score from a signal or a test assignment, carries the visitor state and an ETag that never
82
+ * matches, so the visitor's next request is answered in full.
72
83
  *
73
84
  * `waitUntil` is handed to the manifest provider, the filter and the store for background work, and
74
85
  * defaults to letting the promise run unobserved.
75
86
  */
76
- declare const handleUniformEdgeRoute: ({ request, manifest, onError, filter, store, waitUntil, ...options }: UniformEdgeMiddlewareOptions & {
87
+ declare const handleUniformEdgeRoute: ({ request, manifest, onError, filter, store, etags, waitUntil, ...options }: UniformEdgeMiddlewareOptions & {
77
88
  request: NextRequest;
78
89
  waitUntil?: ManifestProviderOptions["waitUntil"];
79
90
  }) => Promise<Response>;
@@ -110,9 +121,10 @@ type CreateEdgeRouteStoreOptions = {
110
121
  * Creates an edge route store for `uniformEdgeMiddleware`. Records live in memory for `maxAge`,
111
122
  * then in the platform's shared `cache`, tagged with the page's `path:` tag.
112
123
  *
113
- * A page's record keeps whether it has placements and, when the origin sends an ETag, the
114
- * placements visitors reached on that page version. Placements in variants no visitor was shown
115
- * yet are added as visitors reach them, and a new origin ETag replaces them.
124
+ * A page's record keeps whether it has placements and, when the middleware sends ETags, the
125
+ * placements visitors reached on the page's version: its origin ETag or a hash of its content.
126
+ * Placements in variants no visitor was shown yet are added as visitors reach them, and a new
127
+ * version replaces them.
116
128
  *
117
129
  * Publishing a pattern or entry does not expire the pages that use it. Until the record expires,
118
130
  * a page that gained a placement that way is skipped by `learnedEdgeRouteFilter`.
@@ -1,7 +1,7 @@
1
1
  import { ManifestV2 } from '@uniformdev/context';
2
2
  import { TransformEdgeStreamOptions } from '@uniformdev/context-edge';
3
3
  import { NextRequest, NextFetchEvent } from 'next/server';
4
- import { f as ManifestProvider, E as EdgeRouteFilter, d as EdgeRouteStore, g as ManifestProviderOptions, e as EdgeRouteStoreCache, M as ManifestCache } from './models-D8YUMc4L.mjs';
4
+ import { f as ManifestProvider, E as EdgeRouteFilter, d as EdgeRouteStore, g as ManifestProviderOptions, e as EdgeRouteStoreCache, M as ManifestCache } from './models-DKZtopoy.mjs';
5
5
  import { HandleOptions } from './middleware.mjs';
6
6
 
7
7
  /**
@@ -42,11 +42,19 @@ type UniformEdgeMiddlewareOptions = HandleOptions & {
42
42
  /**
43
43
  * Records what the transform learns about each processed page, such as `vercelEdgeRouteStore`
44
44
  * from `@uniformdev/next-app-router/vercel` or one made with `createEdgeRouteStore`. Filters
45
- * read the record, and processed pages get per-visitor ETags when the origin sends an ETag.
45
+ * read the record, and processed pages get per-visitor ETags unless `etags` is `false`.
46
46
  *
47
47
  * @defaultValue nothing is recorded, and responses get no ETag
48
48
  */
49
49
  store?: EdgeRouteStore;
50
+ /**
51
+ * Gives processed pages per-visitor ETags, from the page placements the `store` records. `false`
52
+ * sends every processed page in full with `private, no-store`, never hashes the page, and records
53
+ * only whether each page has placements.
54
+ *
55
+ * @defaultValue `true`
56
+ */
57
+ etags?: boolean;
50
58
  };
51
59
  /**
52
60
  * Returns middleware that rewrites each request to its Uniform code like `uniformMiddleware`, and
@@ -63,17 +71,20 @@ declare const uniformEdgeMiddleware: (options: UniformEdgeMiddlewareOptions) =>
63
71
  * Handles one request the way {@link uniformEdgeMiddleware} does. The transformed response is
64
72
  * personalized, so it is marked `private, no-store`.
65
73
  *
66
- * When the origin answers 200 with an ETag and the `store` recorded the page's placements for that
67
- * ETag, the variants are chosen before the headers are sent. The response then
68
- * gets an ETag built from the origin's ETag and those choices, and `private, no-cache`. A request
69
- * whose `If-None-Match` matches it gets a `304`. A response that assigns a test or rolls a
70
- * personalization control group carries the visitor state and an ETag that never matches, so the
71
- * visitor's next request is answered in full.
74
+ * When `etags` is on, the origin answers 200 and the `store` recorded the page's placements for
75
+ * its current version, the variants are chosen before the headers are sent. The version is the
76
+ * origin's ETag or, without one, a hash of the untransformed page that the last full response
77
+ * recorded. A page that changed without a new origin ETag is detected when a full response hashes
78
+ * differently, and until then a matching request is answered `304`. The response gets an ETag
79
+ * built from the version and the choices, and `private, no-cache`. A request whose
80
+ * `If-None-Match` matches it gets a `304`. A response that changes the visitor's stored data, such
81
+ * as a score from a signal or a test assignment, carries the visitor state and an ETag that never
82
+ * matches, so the visitor's next request is answered in full.
72
83
  *
73
84
  * `waitUntil` is handed to the manifest provider, the filter and the store for background work, and
74
85
  * defaults to letting the promise run unobserved.
75
86
  */
76
- declare const handleUniformEdgeRoute: ({ request, manifest, onError, filter, store, waitUntil, ...options }: UniformEdgeMiddlewareOptions & {
87
+ declare const handleUniformEdgeRoute: ({ request, manifest, onError, filter, store, etags, waitUntil, ...options }: UniformEdgeMiddlewareOptions & {
77
88
  request: NextRequest;
78
89
  waitUntil?: ManifestProviderOptions["waitUntil"];
79
90
  }) => Promise<Response>;
@@ -110,9 +121,10 @@ type CreateEdgeRouteStoreOptions = {
110
121
  * Creates an edge route store for `uniformEdgeMiddleware`. Records live in memory for `maxAge`,
111
122
  * then in the platform's shared `cache`, tagged with the page's `path:` tag.
112
123
  *
113
- * A page's record keeps whether it has placements and, when the origin sends an ETag, the
114
- * placements visitors reached on that page version. Placements in variants no visitor was shown
115
- * yet are added as visitors reach them, and a new origin ETag replaces them.
124
+ * A page's record keeps whether it has placements and, when the middleware sends ETags, the
125
+ * placements visitors reached on the page's version: its origin ETag or a hash of its content.
126
+ * Placements in variants no visitor was shown yet are added as visitors reach them, and a new
127
+ * version replaces them.
116
128
  *
117
129
  * Publishing a pattern or entry does not expire the pages that use it. Until the record expires,
118
130
  * a page that gained a placement that way is skipped by `learnedEdgeRouteFilter`.
package/dist/edge.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- export { C as CreateCachedManifestProviderOptions, a as CreateEdgeRouteStoreOptions, U as UNIFORM_EDGE_ORIGIN_HEADER, b as UniformEdgeMiddlewareOptions, c as createCachedManifestProvider, d as createEdgeRouteStore, h as handleUniformEdgeRoute, u as uniformEdgeMiddleware } from './createCachedManifestProvider-Bpv56Ou-.mjs';
2
- import { E as EdgeRouteFilter } from './models-D8YUMc4L.mjs';
3
- export { a as EdgeRouteFilterOptions, b as EdgeRoutePage, c as EdgeRouteRecord, d as EdgeRouteStore, e as EdgeRouteStoreCache, M as ManifestCache, f as ManifestProvider, g as ManifestProviderOptions, R as RecordedPersonalizationOptions, h as RecordedPlacement, i as RecordedTestOptions } from './models-D8YUMc4L.mjs';
1
+ export { C as CreateCachedManifestProviderOptions, a as CreateEdgeRouteStoreOptions, U as UNIFORM_EDGE_ORIGIN_HEADER, b as UniformEdgeMiddlewareOptions, c as createCachedManifestProvider, d as createEdgeRouteStore, h as handleUniformEdgeRoute, u as uniformEdgeMiddleware } from './createCachedManifestProvider-VL1HGJ-J.mjs';
2
+ import { E as EdgeRouteFilter } from './models-DKZtopoy.mjs';
3
+ export { a as EdgeRouteFilterOptions, b as EdgeRoutePage, c as EdgeRouteRecord, d as EdgeRouteStore, e as EdgeRouteStoreCache, M as ManifestCache, f as ManifestProvider, g as ManifestProviderOptions, R as RecordedPersonalizationOptions, h as RecordedPlacement, i as RecordedTestOptions } from './models-DKZtopoy.mjs';
4
4
  import '@uniformdev/context';
5
5
  import '@uniformdev/context-edge';
6
6
  import 'next/server';
package/dist/edge.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { C as CreateCachedManifestProviderOptions, a as CreateEdgeRouteStoreOptions, U as UNIFORM_EDGE_ORIGIN_HEADER, b as UniformEdgeMiddlewareOptions, c as createCachedManifestProvider, d as createEdgeRouteStore, h as handleUniformEdgeRoute, u as uniformEdgeMiddleware } from './createCachedManifestProvider-DUuhXeA4.js';
2
- import { E as EdgeRouteFilter } from './models-D8YUMc4L.js';
3
- export { a as EdgeRouteFilterOptions, b as EdgeRoutePage, c as EdgeRouteRecord, d as EdgeRouteStore, e as EdgeRouteStoreCache, M as ManifestCache, f as ManifestProvider, g as ManifestProviderOptions, R as RecordedPersonalizationOptions, h as RecordedPlacement, i as RecordedTestOptions } from './models-D8YUMc4L.js';
1
+ export { C as CreateCachedManifestProviderOptions, a as CreateEdgeRouteStoreOptions, U as UNIFORM_EDGE_ORIGIN_HEADER, b as UniformEdgeMiddlewareOptions, c as createCachedManifestProvider, d as createEdgeRouteStore, h as handleUniformEdgeRoute, u as uniformEdgeMiddleware } from './createCachedManifestProvider-DrlaCKdm.js';
2
+ import { E as EdgeRouteFilter } from './models-DKZtopoy.js';
3
+ export { a as EdgeRouteFilterOptions, b as EdgeRoutePage, c as EdgeRouteRecord, d as EdgeRouteStore, e as EdgeRouteStoreCache, M as ManifestCache, f as ManifestProvider, g as ManifestProviderOptions, R as RecordedPersonalizationOptions, h as RecordedPlacement, i as RecordedTestOptions } from './models-DKZtopoy.js';
4
4
  import '@uniformdev/context';
5
5
  import '@uniformdev/context-edge';
6
6
  import 'next/server';
package/dist/edge.js CHANGED
@@ -700,15 +700,37 @@ var buildPlacementTree = (events) => {
700
700
  return roots;
701
701
  };
702
702
 
703
+ // src/edge/createContentHasher.ts
704
+ var createContentHasher = () => {
705
+ let h1 = 3735928559;
706
+ let h2 = 1103547991;
707
+ return {
708
+ update: (chunk) => {
709
+ for (let index = 0; index < chunk.length; index++) {
710
+ const byte = chunk[index];
711
+ h1 = Math.imul(h1 ^ byte, 2654435761);
712
+ h2 = Math.imul(h2 ^ byte, 1597334677);
713
+ }
714
+ },
715
+ digest: () => {
716
+ let a = Math.imul(h1 ^ h1 >>> 16, 2246822507);
717
+ a ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
718
+ let b = Math.imul(h2 ^ h2 >>> 16, 2246822507);
719
+ b ^= Math.imul(a ^ a >>> 13, 3266489909);
720
+ return `${(b >>> 0).toString(16).padStart(8, "0")}${(a >>> 0).toString(16).padStart(8, "0")}`;
721
+ }
722
+ };
723
+ };
724
+
703
725
  // src/edge/createEdgeEtag.ts
704
726
  var createEdgeEtag = async ({
705
- originEtag,
727
+ version,
706
728
  chosen,
707
729
  oneShot
708
730
  }) => {
709
731
  const digest = await crypto.subtle.digest(
710
732
  "SHA-1",
711
- new TextEncoder().encode(`${originEtag}
733
+ new TextEncoder().encode(`${version}
712
734
  ${JSON.stringify(chosen)}`)
713
735
  );
714
736
  const hash = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -716,19 +738,23 @@ ${JSON.stringify(chosen)}`)
716
738
  };
717
739
 
718
740
  // src/edge/hasNewVisitorState.ts
741
+ var import_dequal = require("dequal");
719
742
  var hasNewVisitorState = (context) => {
720
- var _a, _b;
721
- const { tests, personalizeVariants } = context.getServerToClientTransitionState();
722
- if (tests && Object.keys(tests).length > 0) {
723
- return true;
724
- }
725
- const initial = (_b = (_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData) == null ? void 0 : _b.personalizeVariants;
726
- return Object.entries(personalizeVariants != null ? personalizeVariants : {}).some(
727
- ([name, variants]) => variants.some((variant) => {
728
- var _a2;
729
- return !((_a2 = initial == null ? void 0 : initial[name]) == null ? void 0 : _a2.some((known) => known.index === variant.index));
730
- })
731
- );
743
+ var _a;
744
+ const before = normalize((_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData);
745
+ const after = normalize(context.storage.data);
746
+ return !(0, import_dequal.dequal)(before, after);
747
+ };
748
+ var normalize = (data) => {
749
+ var _a, _b, _c, _d, _e, _f;
750
+ return {
751
+ tests: (_a = data == null ? void 0 : data.tests) != null ? _a : {},
752
+ scores: (_b = data == null ? void 0 : data.scores) != null ? _b : {},
753
+ sessionScores: (_c = data == null ? void 0 : data.sessionScores) != null ? _c : {},
754
+ goals: (_d = data == null ? void 0 : data.goals) != null ? _d : {},
755
+ controlGroup: (_e = data == null ? void 0 : data.controlGroup) != null ? _e : false,
756
+ personalizeVariants: (_f = data == null ? void 0 : data.personalizeVariants) != null ? _f : {}
757
+ };
732
758
  };
733
759
 
734
760
  // src/edge/matchesIfNoneMatch.ts
@@ -785,6 +811,7 @@ var choose = (context, placement) => {
785
811
 
786
812
  // src/edge/uniformEdgeMiddleware.ts
787
813
  var UNIFORM_EDGE_ORIGIN_HEADER = "x-uniform-edge-origin";
814
+ var CONTENT_VERSION_PREFIX = "content:";
788
815
  var uniformEdgeMiddleware = (options) => {
789
816
  return async (request, event) => {
790
817
  return handleUniformEdgeRoute({
@@ -800,10 +827,11 @@ var handleUniformEdgeRoute = async ({
800
827
  onError,
801
828
  filter,
802
829
  store,
830
+ etags = true,
803
831
  waitUntil = ignorePromise,
804
832
  ...options
805
833
  }) => {
806
- var _a, _b, _c;
834
+ var _a, _b, _c, _d;
807
835
  if (request.headers.has(UNIFORM_EDGE_ORIGIN_HEADER)) {
808
836
  return import_server2.NextResponse.next();
809
837
  }
@@ -853,18 +881,20 @@ var handleUniformEdgeRoute = async ({
853
881
  headers.delete("content-encoding");
854
882
  headers.delete("etag");
855
883
  headers.delete("last-modified");
856
- const originEtag = originResponse.status === 200 ? originResponse.headers.get("etag") : null;
857
- const chosen = originEtag && ((_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag) === originEtag ? replayRecordedPage(context, record.page) : void 0;
884
+ const isOk = originResponse.status === 200;
885
+ const originEtag = etags && isOk ? originResponse.headers.get("etag") : null;
886
+ const version = originEtag != null ? originEtag : etags && isOk ? (_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag : void 0;
887
+ const chosen = version && ((_c = record == null ? void 0 : record.page) == null ? void 0 : _c.etag) === version ? replayRecordedPage(context, record.page) : void 0;
858
888
  let includeTransitionState = true;
859
- if (originEtag && chosen) {
889
+ if (version && chosen) {
860
890
  const newState = hasNewVisitorState(context);
861
- const etag = await createEdgeEtag({ originEtag, chosen, oneShot: newState });
891
+ const etag = await createEdgeEtag({ version, chosen, oneShot: newState });
862
892
  headers.set("etag", etag);
863
893
  headers.set("cache-control", "private, no-cache");
864
894
  if (!newState) {
865
895
  includeTransitionState = false;
866
896
  if (matchesIfNoneMatch(request.headers.get("if-none-match"), etag)) {
867
- (_c = originResponse.body) == null ? void 0 : _c.cancel().catch(() => {
897
+ (_d = originResponse.body) == null ? void 0 : _d.cancel().catch(() => {
868
898
  });
869
899
  const notModifiedHeaders = new Headers(result.headers);
870
900
  notModifiedHeaders.set("etag", etag);
@@ -875,14 +905,25 @@ var handleUniformEdgeRoute = async ({
875
905
  } else {
876
906
  headers.set("cache-control", "private, no-store");
877
907
  }
878
- const onComplete = store && originResponse.status === 200 ? (placements) => store.observe({
879
- pageState: result.pageState,
880
- hasPlacements: placements.some((placement) => !placement.parent),
881
- ...originEtag ? { page: { etag: originEtag, placements: buildPlacementTree(placements) } } : {},
882
- waitUntil
883
- }) : void 0;
908
+ const hasher = etags && store && isOk && !originEtag ? createContentHasher() : void 0;
909
+ const onComplete = store && isOk ? (placements) => {
910
+ const pageVersion = originEtag != null ? originEtag : hasher ? `${CONTENT_VERSION_PREFIX}${hasher.digest()}` : void 0;
911
+ store.observe({
912
+ pageState: result.pageState,
913
+ hasPlacements: placements.some((placement) => !placement.parent),
914
+ ...pageVersion ? { page: { etag: pageVersion, placements: buildPlacementTree(placements) } } : {},
915
+ waitUntil
916
+ });
917
+ } : void 0;
884
918
  return new Response(
885
- transformBody({ body: originResponse.body, context, onError, onComplete, includeTransitionState }),
919
+ transformBody({
920
+ body: originResponse.body,
921
+ context,
922
+ onError,
923
+ onComplete,
924
+ onChunk: hasher == null ? void 0 : hasher.update,
925
+ includeTransitionState
926
+ }),
886
927
  {
887
928
  status: originResponse.status,
888
929
  statusText: originResponse.statusText,
@@ -926,6 +967,7 @@ var transformBody = ({
926
967
  context,
927
968
  onError,
928
969
  onComplete,
970
+ onChunk,
929
971
  includeTransitionState
930
972
  }) => {
931
973
  if (!body) {
@@ -950,6 +992,9 @@ var transformBody = ({
950
992
  await writer.ready.catch(() => {
951
993
  });
952
994
  const { done, value } = await reader.read();
995
+ if (value) {
996
+ onChunk == null ? void 0 : onChunk(value);
997
+ }
953
998
  return { done, value };
954
999
  }
955
1000
  },
package/dist/edge.mjs CHANGED
@@ -668,15 +668,37 @@ var buildPlacementTree = (events) => {
668
668
  return roots;
669
669
  };
670
670
 
671
+ // src/edge/createContentHasher.ts
672
+ var createContentHasher = () => {
673
+ let h1 = 3735928559;
674
+ let h2 = 1103547991;
675
+ return {
676
+ update: (chunk) => {
677
+ for (let index = 0; index < chunk.length; index++) {
678
+ const byte = chunk[index];
679
+ h1 = Math.imul(h1 ^ byte, 2654435761);
680
+ h2 = Math.imul(h2 ^ byte, 1597334677);
681
+ }
682
+ },
683
+ digest: () => {
684
+ let a = Math.imul(h1 ^ h1 >>> 16, 2246822507);
685
+ a ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
686
+ let b = Math.imul(h2 ^ h2 >>> 16, 2246822507);
687
+ b ^= Math.imul(a ^ a >>> 13, 3266489909);
688
+ return `${(b >>> 0).toString(16).padStart(8, "0")}${(a >>> 0).toString(16).padStart(8, "0")}`;
689
+ }
690
+ };
691
+ };
692
+
671
693
  // src/edge/createEdgeEtag.ts
672
694
  var createEdgeEtag = async ({
673
- originEtag,
695
+ version,
674
696
  chosen,
675
697
  oneShot
676
698
  }) => {
677
699
  const digest = await crypto.subtle.digest(
678
700
  "SHA-1",
679
- new TextEncoder().encode(`${originEtag}
701
+ new TextEncoder().encode(`${version}
680
702
  ${JSON.stringify(chosen)}`)
681
703
  );
682
704
  const hash = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -684,19 +706,23 @@ ${JSON.stringify(chosen)}`)
684
706
  };
685
707
 
686
708
  // src/edge/hasNewVisitorState.ts
709
+ import { dequal } from "dequal";
687
710
  var hasNewVisitorState = (context) => {
688
- var _a, _b;
689
- const { tests, personalizeVariants } = context.getServerToClientTransitionState();
690
- if (tests && Object.keys(tests).length > 0) {
691
- return true;
692
- }
693
- const initial = (_b = (_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData) == null ? void 0 : _b.personalizeVariants;
694
- return Object.entries(personalizeVariants != null ? personalizeVariants : {}).some(
695
- ([name, variants]) => variants.some((variant) => {
696
- var _a2;
697
- return !((_a2 = initial == null ? void 0 : initial[name]) == null ? void 0 : _a2.some((known) => known.index === variant.index));
698
- })
699
- );
711
+ var _a;
712
+ const before = normalize((_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData);
713
+ const after = normalize(context.storage.data);
714
+ return !dequal(before, after);
715
+ };
716
+ var normalize = (data) => {
717
+ var _a, _b, _c, _d, _e, _f;
718
+ return {
719
+ tests: (_a = data == null ? void 0 : data.tests) != null ? _a : {},
720
+ scores: (_b = data == null ? void 0 : data.scores) != null ? _b : {},
721
+ sessionScores: (_c = data == null ? void 0 : data.sessionScores) != null ? _c : {},
722
+ goals: (_d = data == null ? void 0 : data.goals) != null ? _d : {},
723
+ controlGroup: (_e = data == null ? void 0 : data.controlGroup) != null ? _e : false,
724
+ personalizeVariants: (_f = data == null ? void 0 : data.personalizeVariants) != null ? _f : {}
725
+ };
700
726
  };
701
727
 
702
728
  // src/edge/matchesIfNoneMatch.ts
@@ -753,6 +779,7 @@ var choose = (context, placement) => {
753
779
 
754
780
  // src/edge/uniformEdgeMiddleware.ts
755
781
  var UNIFORM_EDGE_ORIGIN_HEADER = "x-uniform-edge-origin";
782
+ var CONTENT_VERSION_PREFIX = "content:";
756
783
  var uniformEdgeMiddleware = (options) => {
757
784
  return async (request, event) => {
758
785
  return handleUniformEdgeRoute({
@@ -768,10 +795,11 @@ var handleUniformEdgeRoute = async ({
768
795
  onError,
769
796
  filter,
770
797
  store,
798
+ etags = true,
771
799
  waitUntil = ignorePromise,
772
800
  ...options
773
801
  }) => {
774
- var _a, _b, _c;
802
+ var _a, _b, _c, _d;
775
803
  if (request.headers.has(UNIFORM_EDGE_ORIGIN_HEADER)) {
776
804
  return NextResponse2.next();
777
805
  }
@@ -821,18 +849,20 @@ var handleUniformEdgeRoute = async ({
821
849
  headers.delete("content-encoding");
822
850
  headers.delete("etag");
823
851
  headers.delete("last-modified");
824
- const originEtag = originResponse.status === 200 ? originResponse.headers.get("etag") : null;
825
- const chosen = originEtag && ((_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag) === originEtag ? replayRecordedPage(context, record.page) : void 0;
852
+ const isOk = originResponse.status === 200;
853
+ const originEtag = etags && isOk ? originResponse.headers.get("etag") : null;
854
+ const version = originEtag != null ? originEtag : etags && isOk ? (_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag : void 0;
855
+ const chosen = version && ((_c = record == null ? void 0 : record.page) == null ? void 0 : _c.etag) === version ? replayRecordedPage(context, record.page) : void 0;
826
856
  let includeTransitionState = true;
827
- if (originEtag && chosen) {
857
+ if (version && chosen) {
828
858
  const newState = hasNewVisitorState(context);
829
- const etag = await createEdgeEtag({ originEtag, chosen, oneShot: newState });
859
+ const etag = await createEdgeEtag({ version, chosen, oneShot: newState });
830
860
  headers.set("etag", etag);
831
861
  headers.set("cache-control", "private, no-cache");
832
862
  if (!newState) {
833
863
  includeTransitionState = false;
834
864
  if (matchesIfNoneMatch(request.headers.get("if-none-match"), etag)) {
835
- (_c = originResponse.body) == null ? void 0 : _c.cancel().catch(() => {
865
+ (_d = originResponse.body) == null ? void 0 : _d.cancel().catch(() => {
836
866
  });
837
867
  const notModifiedHeaders = new Headers(result.headers);
838
868
  notModifiedHeaders.set("etag", etag);
@@ -843,14 +873,25 @@ var handleUniformEdgeRoute = async ({
843
873
  } else {
844
874
  headers.set("cache-control", "private, no-store");
845
875
  }
846
- const onComplete = store && originResponse.status === 200 ? (placements) => store.observe({
847
- pageState: result.pageState,
848
- hasPlacements: placements.some((placement) => !placement.parent),
849
- ...originEtag ? { page: { etag: originEtag, placements: buildPlacementTree(placements) } } : {},
850
- waitUntil
851
- }) : void 0;
876
+ const hasher = etags && store && isOk && !originEtag ? createContentHasher() : void 0;
877
+ const onComplete = store && isOk ? (placements) => {
878
+ const pageVersion = originEtag != null ? originEtag : hasher ? `${CONTENT_VERSION_PREFIX}${hasher.digest()}` : void 0;
879
+ store.observe({
880
+ pageState: result.pageState,
881
+ hasPlacements: placements.some((placement) => !placement.parent),
882
+ ...pageVersion ? { page: { etag: pageVersion, placements: buildPlacementTree(placements) } } : {},
883
+ waitUntil
884
+ });
885
+ } : void 0;
852
886
  return new Response(
853
- transformBody({ body: originResponse.body, context, onError, onComplete, includeTransitionState }),
887
+ transformBody({
888
+ body: originResponse.body,
889
+ context,
890
+ onError,
891
+ onComplete,
892
+ onChunk: hasher == null ? void 0 : hasher.update,
893
+ includeTransitionState
894
+ }),
854
895
  {
855
896
  status: originResponse.status,
856
897
  statusText: originResponse.statusText,
@@ -894,6 +935,7 @@ var transformBody = ({
894
935
  context,
895
936
  onError,
896
937
  onComplete,
938
+ onChunk,
897
939
  includeTransitionState
898
940
  }) => {
899
941
  if (!body) {
@@ -918,6 +960,9 @@ var transformBody = ({
918
960
  await writer.ready.catch(() => {
919
961
  });
920
962
  const { done, value } = await reader.read();
963
+ if (value) {
964
+ onChunk == null ? void 0 : onChunk(value);
965
+ }
921
966
  return { done, value };
922
967
  }
923
968
  },
package/dist/index.d.mts CHANGED
@@ -8,7 +8,7 @@ import { ClientContextComponent } from '@uniformdev/next-app-router-client';
8
8
  import React from 'react';
9
9
  import { a as ResolvedRouteResult, b as ResolvedComposition, R as ResolvedCompositionResult } from './composition-CGU7iH-k.mjs';
10
10
  export { c as ResolvedNotFound, d as ResolvedRedirect } from './composition-CGU7iH-k.mjs';
11
- import { E as EdgeRouteFilter } from './models-D8YUMc4L.mjs';
11
+ import { E as EdgeRouteFilter } from './models-DKZtopoy.mjs';
12
12
  import { b as ResolveRouteFromCodeOptions } from './resolveRouteFromPath-_rauUskz.mjs';
13
13
  export { P as PageStateSource, R as ResolveCompositionByIdOptions, a as ResolveCompositionBySlugOptions, c as ResolveRouteFromPathOptions, d as ResolveRouteFunction, r as resolveCompositionById, e as resolveCompositionBySlug, f as resolveRouteFromCode, g as resolveRouteFromPath } from './resolveRouteFromPath-_rauUskz.mjs';
14
14
  import '@uniformdev/context';
@@ -300,8 +300,9 @@ type ResolveRedirectHrefOptions = {
300
300
  };
301
301
  /**
302
302
  * Builds the URL a redirect route sends the visitor to. Path variables in the target, such as
303
- * `/new/:id`, are filled from `routePath`. Targets on the same domain come back as relative paths;
304
- * absolute targets on another domain come back unchanged.
303
+ * `/new/:id`, and a trailing splat, written `*` or `:splat`, are filled from `routePath`. Targets
304
+ * on the same domain come back as relative paths; absolute targets on another domain come back
305
+ * unchanged.
305
306
  */
306
307
  declare const resolveRedirectHref: ({ route, routePath }: ResolveRedirectHrefOptions) => string;
307
308
 
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import { ClientContextComponent } from '@uniformdev/next-app-router-client';
8
8
  import React from 'react';
9
9
  import { a as ResolvedRouteResult, b as ResolvedComposition, R as ResolvedCompositionResult } from './composition-CGU7iH-k.js';
10
10
  export { c as ResolvedNotFound, d as ResolvedRedirect } from './composition-CGU7iH-k.js';
11
- import { E as EdgeRouteFilter } from './models-D8YUMc4L.js';
11
+ import { E as EdgeRouteFilter } from './models-DKZtopoy.js';
12
12
  import { b as ResolveRouteFromCodeOptions } from './resolveRouteFromPath-CnfnaxY2.js';
13
13
  export { P as PageStateSource, R as ResolveCompositionByIdOptions, a as ResolveCompositionBySlugOptions, c as ResolveRouteFromPathOptions, d as ResolveRouteFunction, r as resolveCompositionById, e as resolveCompositionBySlug, f as resolveRouteFromCode, g as resolveRouteFromPath } from './resolveRouteFromPath-CnfnaxY2.js';
14
14
  import '@uniformdev/context';
@@ -300,8 +300,9 @@ type ResolveRedirectHrefOptions = {
300
300
  };
301
301
  /**
302
302
  * Builds the URL a redirect route sends the visitor to. Path variables in the target, such as
303
- * `/new/:id`, are filled from `routePath`. Targets on the same domain come back as relative paths;
304
- * absolute targets on another domain come back unchanged.
303
+ * `/new/:id`, and a trailing splat, written `*` or `:splat`, are filled from `routePath`. Targets
304
+ * on the same domain come back as relative paths; absolute targets on another domain come back
305
+ * unchanged.
305
306
  */
306
307
  declare const resolveRedirectHref: ({ route, routePath }: ResolveRedirectHrefOptions) => string;
307
308
 
package/dist/index.esm.js CHANGED
@@ -3289,8 +3289,7 @@ var REDIRECT_ORIGIN_PLACEHOLDER = "http://uniform.local";
3289
3289
  var resolveRedirectHref = ({ route, routePath }) => {
3290
3290
  const expanded = getTargetVariableExpandedUrl(
3291
3291
  new URL(routePath, REDIRECT_ORIGIN_PLACEHOLDER).toString(),
3292
- route.redirect,
3293
- (pathSegment) => pathSegment.startsWith(":")
3292
+ route.redirect
3294
3293
  );
3295
3294
  return expanded.startsWith(REDIRECT_ORIGIN_PLACEHOLDER) ? expanded.slice(REDIRECT_ORIGIN_PLACEHOLDER.length) || "/" : expanded;
3296
3295
  };
package/dist/index.js CHANGED
@@ -3303,8 +3303,7 @@ var REDIRECT_ORIGIN_PLACEHOLDER = "http://uniform.local";
3303
3303
  var resolveRedirectHref = ({ route, routePath }) => {
3304
3304
  const expanded = (0, import_redirect.getTargetVariableExpandedUrl)(
3305
3305
  new URL(routePath, REDIRECT_ORIGIN_PLACEHOLDER).toString(),
3306
- route.redirect,
3307
- (pathSegment) => pathSegment.startsWith(":")
3306
+ route.redirect
3308
3307
  );
3309
3308
  return expanded.startsWith(REDIRECT_ORIGIN_PLACEHOLDER) ? expanded.slice(REDIRECT_ORIGIN_PLACEHOLDER.length) || "/" : expanded;
3310
3309
  };
package/dist/index.mjs CHANGED
@@ -3289,8 +3289,7 @@ var REDIRECT_ORIGIN_PLACEHOLDER = "http://uniform.local";
3289
3289
  var resolveRedirectHref = ({ route, routePath }) => {
3290
3290
  const expanded = getTargetVariableExpandedUrl(
3291
3291
  new URL(routePath, REDIRECT_ORIGIN_PLACEHOLDER).toString(),
3292
- route.redirect,
3293
- (pathSegment) => pathSegment.startsWith(":")
3292
+ route.redirect
3294
3293
  );
3295
3294
  return expanded.startsWith(REDIRECT_ORIGIN_PLACEHOLDER) ? expanded.slice(REDIRECT_ORIGIN_PLACEHOLDER.length) || "/" : expanded;
3296
3295
  };
@@ -58,8 +58,9 @@ type EdgeRouteRecord = {
58
58
  hasPlacements: boolean;
59
59
  page?: EdgeRoutePage;
60
60
  };
61
- /** The placements recorded for one version of a page, identified by the origin's ETag. */
61
+ /** The placements recorded for one version of a page. */
62
62
  type EdgeRoutePage = {
63
+ /** The page version: the origin's ETag, or a hash of the page when the origin sends none. */
63
64
  etag: string;
64
65
  placements: RecordedPlacement[];
65
66
  };
@@ -58,8 +58,9 @@ type EdgeRouteRecord = {
58
58
  hasPlacements: boolean;
59
59
  page?: EdgeRoutePage;
60
60
  };
61
- /** The placements recorded for one version of a page, identified by the origin's ETag. */
61
+ /** The placements recorded for one version of a page. */
62
62
  type EdgeRoutePage = {
63
+ /** The page version: the origin's ETag, or a hash of the page when the origin sends none. */
63
64
  etag: string;
64
65
  placements: RecordedPlacement[];
65
66
  };
package/dist/vercel.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as CreateEdgeRouteStoreOptions, C as CreateCachedManifestProviderOptions, b as UniformEdgeMiddlewareOptions } from './createCachedManifestProvider-Bpv56Ou-.mjs';
2
- import { d as EdgeRouteStore, f as ManifestProvider, E as EdgeRouteFilter } from './models-D8YUMc4L.mjs';
1
+ import { a as CreateEdgeRouteStoreOptions, C as CreateCachedManifestProviderOptions, b as UniformEdgeMiddlewareOptions } from './createCachedManifestProvider-VL1HGJ-J.mjs';
2
+ import { d as EdgeRouteStore, f as ManifestProvider, E as EdgeRouteFilter } from './models-DKZtopoy.mjs';
3
3
  import * as next_server from 'next/server';
4
4
  import '@uniformdev/context';
5
5
  import '@uniformdev/context-edge';
@@ -53,7 +53,8 @@ type VercelUniformEdgeMiddlewareOptions = Omit<UniformEdgeMiddlewareOptions, 'ma
53
53
  filter?: EdgeRouteFilter | false;
54
54
  /**
55
55
  * Records what the transform learns about each page. `false` records nothing, so the default
56
- * filter processes every published page and responses get no ETag.
56
+ * filter processes every published page and responses get no ETag. To keep the filter and drop
57
+ * only the ETags, set `etags: false`.
57
58
  *
58
59
  * @defaultValue `vercelEdgeRouteStore()`
59
60
  */
package/dist/vercel.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as CreateEdgeRouteStoreOptions, C as CreateCachedManifestProviderOptions, b as UniformEdgeMiddlewareOptions } from './createCachedManifestProvider-DUuhXeA4.js';
2
- import { d as EdgeRouteStore, f as ManifestProvider, E as EdgeRouteFilter } from './models-D8YUMc4L.js';
1
+ import { a as CreateEdgeRouteStoreOptions, C as CreateCachedManifestProviderOptions, b as UniformEdgeMiddlewareOptions } from './createCachedManifestProvider-DrlaCKdm.js';
2
+ import { d as EdgeRouteStore, f as ManifestProvider, E as EdgeRouteFilter } from './models-DKZtopoy.js';
3
3
  import * as next_server from 'next/server';
4
4
  import '@uniformdev/context';
5
5
  import '@uniformdev/context-edge';
@@ -53,7 +53,8 @@ type VercelUniformEdgeMiddlewareOptions = Omit<UniformEdgeMiddlewareOptions, 'ma
53
53
  filter?: EdgeRouteFilter | false;
54
54
  /**
55
55
  * Records what the transform learns about each page. `false` records nothing, so the default
56
- * filter processes every published page and responses get no ETag.
56
+ * filter processes every published page and responses get no ETag. To keep the filter and drop
57
+ * only the ETags, set `etags: false`.
57
58
  *
58
59
  * @defaultValue `vercelEdgeRouteStore()`
59
60
  */
package/dist/vercel.js CHANGED
@@ -1165,15 +1165,37 @@ var buildPlacementTree = (events) => {
1165
1165
  return roots;
1166
1166
  };
1167
1167
 
1168
+ // src/edge/createContentHasher.ts
1169
+ var createContentHasher = () => {
1170
+ let h1 = 3735928559;
1171
+ let h2 = 1103547991;
1172
+ return {
1173
+ update: (chunk) => {
1174
+ for (let index = 0; index < chunk.length; index++) {
1175
+ const byte = chunk[index];
1176
+ h1 = Math.imul(h1 ^ byte, 2654435761);
1177
+ h2 = Math.imul(h2 ^ byte, 1597334677);
1178
+ }
1179
+ },
1180
+ digest: () => {
1181
+ let a = Math.imul(h1 ^ h1 >>> 16, 2246822507);
1182
+ a ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
1183
+ let b = Math.imul(h2 ^ h2 >>> 16, 2246822507);
1184
+ b ^= Math.imul(a ^ a >>> 13, 3266489909);
1185
+ return `${(b >>> 0).toString(16).padStart(8, "0")}${(a >>> 0).toString(16).padStart(8, "0")}`;
1186
+ }
1187
+ };
1188
+ };
1189
+
1168
1190
  // src/edge/createEdgeEtag.ts
1169
1191
  var createEdgeEtag = async ({
1170
- originEtag,
1192
+ version,
1171
1193
  chosen,
1172
1194
  oneShot
1173
1195
  }) => {
1174
1196
  const digest = await crypto.subtle.digest(
1175
1197
  "SHA-1",
1176
- new TextEncoder().encode(`${originEtag}
1198
+ new TextEncoder().encode(`${version}
1177
1199
  ${JSON.stringify(chosen)}`)
1178
1200
  );
1179
1201
  const hash = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -1181,19 +1203,23 @@ ${JSON.stringify(chosen)}`)
1181
1203
  };
1182
1204
 
1183
1205
  // src/edge/hasNewVisitorState.ts
1206
+ var import_dequal = require("dequal");
1184
1207
  var hasNewVisitorState = (context) => {
1185
- var _a, _b;
1186
- const { tests, personalizeVariants } = context.getServerToClientTransitionState();
1187
- if (tests && Object.keys(tests).length > 0) {
1188
- return true;
1189
- }
1190
- const initial = (_b = (_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData) == null ? void 0 : _b.personalizeVariants;
1191
- return Object.entries(personalizeVariants != null ? personalizeVariants : {}).some(
1192
- ([name, variants]) => variants.some((variant) => {
1193
- var _a2;
1194
- return !((_a2 = initial == null ? void 0 : initial[name]) == null ? void 0 : _a2.some((known) => known.index === variant.index));
1195
- })
1196
- );
1208
+ var _a;
1209
+ const before = normalize((_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData);
1210
+ const after = normalize(context.storage.data);
1211
+ return !(0, import_dequal.dequal)(before, after);
1212
+ };
1213
+ var normalize = (data) => {
1214
+ var _a, _b, _c, _d, _e, _f;
1215
+ return {
1216
+ tests: (_a = data == null ? void 0 : data.tests) != null ? _a : {},
1217
+ scores: (_b = data == null ? void 0 : data.scores) != null ? _b : {},
1218
+ sessionScores: (_c = data == null ? void 0 : data.sessionScores) != null ? _c : {},
1219
+ goals: (_d = data == null ? void 0 : data.goals) != null ? _d : {},
1220
+ controlGroup: (_e = data == null ? void 0 : data.controlGroup) != null ? _e : false,
1221
+ personalizeVariants: (_f = data == null ? void 0 : data.personalizeVariants) != null ? _f : {}
1222
+ };
1197
1223
  };
1198
1224
 
1199
1225
  // src/edge/matchesIfNoneMatch.ts
@@ -1250,6 +1276,7 @@ var choose = (context, placement) => {
1250
1276
 
1251
1277
  // src/edge/uniformEdgeMiddleware.ts
1252
1278
  var UNIFORM_EDGE_ORIGIN_HEADER = "x-uniform-edge-origin";
1279
+ var CONTENT_VERSION_PREFIX = "content:";
1253
1280
  var uniformEdgeMiddleware = (options) => {
1254
1281
  return async (request, event) => {
1255
1282
  return handleUniformEdgeRoute({
@@ -1265,10 +1292,11 @@ var handleUniformEdgeRoute = async ({
1265
1292
  onError,
1266
1293
  filter,
1267
1294
  store,
1295
+ etags = true,
1268
1296
  waitUntil = ignorePromise,
1269
1297
  ...options
1270
1298
  }) => {
1271
- var _a, _b, _c;
1299
+ var _a, _b, _c, _d;
1272
1300
  if (request.headers.has(UNIFORM_EDGE_ORIGIN_HEADER)) {
1273
1301
  return import_server2.NextResponse.next();
1274
1302
  }
@@ -1318,18 +1346,20 @@ var handleUniformEdgeRoute = async ({
1318
1346
  headers.delete("content-encoding");
1319
1347
  headers.delete("etag");
1320
1348
  headers.delete("last-modified");
1321
- const originEtag = originResponse.status === 200 ? originResponse.headers.get("etag") : null;
1322
- const chosen = originEtag && ((_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag) === originEtag ? replayRecordedPage(context, record.page) : void 0;
1349
+ const isOk = originResponse.status === 200;
1350
+ const originEtag = etags && isOk ? originResponse.headers.get("etag") : null;
1351
+ const version = originEtag != null ? originEtag : etags && isOk ? (_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag : void 0;
1352
+ const chosen = version && ((_c = record == null ? void 0 : record.page) == null ? void 0 : _c.etag) === version ? replayRecordedPage(context, record.page) : void 0;
1323
1353
  let includeTransitionState = true;
1324
- if (originEtag && chosen) {
1354
+ if (version && chosen) {
1325
1355
  const newState = hasNewVisitorState(context);
1326
- const etag = await createEdgeEtag({ originEtag, chosen, oneShot: newState });
1356
+ const etag = await createEdgeEtag({ version, chosen, oneShot: newState });
1327
1357
  headers.set("etag", etag);
1328
1358
  headers.set("cache-control", "private, no-cache");
1329
1359
  if (!newState) {
1330
1360
  includeTransitionState = false;
1331
1361
  if (matchesIfNoneMatch(request.headers.get("if-none-match"), etag)) {
1332
- (_c = originResponse.body) == null ? void 0 : _c.cancel().catch(() => {
1362
+ (_d = originResponse.body) == null ? void 0 : _d.cancel().catch(() => {
1333
1363
  });
1334
1364
  const notModifiedHeaders = new Headers(result.headers);
1335
1365
  notModifiedHeaders.set("etag", etag);
@@ -1340,14 +1370,25 @@ var handleUniformEdgeRoute = async ({
1340
1370
  } else {
1341
1371
  headers.set("cache-control", "private, no-store");
1342
1372
  }
1343
- const onComplete = store && originResponse.status === 200 ? (placements) => store.observe({
1344
- pageState: result.pageState,
1345
- hasPlacements: placements.some((placement) => !placement.parent),
1346
- ...originEtag ? { page: { etag: originEtag, placements: buildPlacementTree(placements) } } : {},
1347
- waitUntil
1348
- }) : void 0;
1373
+ const hasher = etags && store && isOk && !originEtag ? createContentHasher() : void 0;
1374
+ const onComplete = store && isOk ? (placements) => {
1375
+ const pageVersion = originEtag != null ? originEtag : hasher ? `${CONTENT_VERSION_PREFIX}${hasher.digest()}` : void 0;
1376
+ store.observe({
1377
+ pageState: result.pageState,
1378
+ hasPlacements: placements.some((placement) => !placement.parent),
1379
+ ...pageVersion ? { page: { etag: pageVersion, placements: buildPlacementTree(placements) } } : {},
1380
+ waitUntil
1381
+ });
1382
+ } : void 0;
1349
1383
  return new Response(
1350
- transformBody({ body: originResponse.body, context, onError, onComplete, includeTransitionState }),
1384
+ transformBody({
1385
+ body: originResponse.body,
1386
+ context,
1387
+ onError,
1388
+ onComplete,
1389
+ onChunk: hasher == null ? void 0 : hasher.update,
1390
+ includeTransitionState
1391
+ }),
1351
1392
  {
1352
1393
  status: originResponse.status,
1353
1394
  statusText: originResponse.statusText,
@@ -1391,6 +1432,7 @@ var transformBody = ({
1391
1432
  context,
1392
1433
  onError,
1393
1434
  onComplete,
1435
+ onChunk,
1394
1436
  includeTransitionState
1395
1437
  }) => {
1396
1438
  if (!body) {
@@ -1415,6 +1457,9 @@ var transformBody = ({
1415
1457
  await writer.ready.catch(() => {
1416
1458
  });
1417
1459
  const { done, value } = await reader.read();
1460
+ if (value) {
1461
+ onChunk == null ? void 0 : onChunk(value);
1462
+ }
1418
1463
  return { done, value };
1419
1464
  }
1420
1465
  },
package/dist/vercel.mjs CHANGED
@@ -1136,15 +1136,37 @@ var buildPlacementTree = (events) => {
1136
1136
  return roots;
1137
1137
  };
1138
1138
 
1139
+ // src/edge/createContentHasher.ts
1140
+ var createContentHasher = () => {
1141
+ let h1 = 3735928559;
1142
+ let h2 = 1103547991;
1143
+ return {
1144
+ update: (chunk) => {
1145
+ for (let index = 0; index < chunk.length; index++) {
1146
+ const byte = chunk[index];
1147
+ h1 = Math.imul(h1 ^ byte, 2654435761);
1148
+ h2 = Math.imul(h2 ^ byte, 1597334677);
1149
+ }
1150
+ },
1151
+ digest: () => {
1152
+ let a = Math.imul(h1 ^ h1 >>> 16, 2246822507);
1153
+ a ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
1154
+ let b = Math.imul(h2 ^ h2 >>> 16, 2246822507);
1155
+ b ^= Math.imul(a ^ a >>> 13, 3266489909);
1156
+ return `${(b >>> 0).toString(16).padStart(8, "0")}${(a >>> 0).toString(16).padStart(8, "0")}`;
1157
+ }
1158
+ };
1159
+ };
1160
+
1139
1161
  // src/edge/createEdgeEtag.ts
1140
1162
  var createEdgeEtag = async ({
1141
- originEtag,
1163
+ version,
1142
1164
  chosen,
1143
1165
  oneShot
1144
1166
  }) => {
1145
1167
  const digest = await crypto.subtle.digest(
1146
1168
  "SHA-1",
1147
- new TextEncoder().encode(`${originEtag}
1169
+ new TextEncoder().encode(`${version}
1148
1170
  ${JSON.stringify(chosen)}`)
1149
1171
  );
1150
1172
  const hash = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -1152,19 +1174,23 @@ ${JSON.stringify(chosen)}`)
1152
1174
  };
1153
1175
 
1154
1176
  // src/edge/hasNewVisitorState.ts
1177
+ import { dequal } from "dequal";
1155
1178
  var hasNewVisitorState = (context) => {
1156
- var _a, _b;
1157
- const { tests, personalizeVariants } = context.getServerToClientTransitionState();
1158
- if (tests && Object.keys(tests).length > 0) {
1159
- return true;
1160
- }
1161
- const initial = (_b = (_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData) == null ? void 0 : _b.personalizeVariants;
1162
- return Object.entries(personalizeVariants != null ? personalizeVariants : {}).some(
1163
- ([name, variants]) => variants.some((variant) => {
1164
- var _a2;
1165
- return !((_a2 = initial == null ? void 0 : initial[name]) == null ? void 0 : _a2.some((known) => known.index === variant.index));
1166
- })
1167
- );
1179
+ var _a;
1180
+ const before = normalize((_a = context.storage.options.transitionStore) == null ? void 0 : _a.initialData);
1181
+ const after = normalize(context.storage.data);
1182
+ return !dequal(before, after);
1183
+ };
1184
+ var normalize = (data) => {
1185
+ var _a, _b, _c, _d, _e, _f;
1186
+ return {
1187
+ tests: (_a = data == null ? void 0 : data.tests) != null ? _a : {},
1188
+ scores: (_b = data == null ? void 0 : data.scores) != null ? _b : {},
1189
+ sessionScores: (_c = data == null ? void 0 : data.sessionScores) != null ? _c : {},
1190
+ goals: (_d = data == null ? void 0 : data.goals) != null ? _d : {},
1191
+ controlGroup: (_e = data == null ? void 0 : data.controlGroup) != null ? _e : false,
1192
+ personalizeVariants: (_f = data == null ? void 0 : data.personalizeVariants) != null ? _f : {}
1193
+ };
1168
1194
  };
1169
1195
 
1170
1196
  // src/edge/matchesIfNoneMatch.ts
@@ -1221,6 +1247,7 @@ var choose = (context, placement) => {
1221
1247
 
1222
1248
  // src/edge/uniformEdgeMiddleware.ts
1223
1249
  var UNIFORM_EDGE_ORIGIN_HEADER = "x-uniform-edge-origin";
1250
+ var CONTENT_VERSION_PREFIX = "content:";
1224
1251
  var uniformEdgeMiddleware = (options) => {
1225
1252
  return async (request, event) => {
1226
1253
  return handleUniformEdgeRoute({
@@ -1236,10 +1263,11 @@ var handleUniformEdgeRoute = async ({
1236
1263
  onError,
1237
1264
  filter,
1238
1265
  store,
1266
+ etags = true,
1239
1267
  waitUntil = ignorePromise,
1240
1268
  ...options
1241
1269
  }) => {
1242
- var _a, _b, _c;
1270
+ var _a, _b, _c, _d;
1243
1271
  if (request.headers.has(UNIFORM_EDGE_ORIGIN_HEADER)) {
1244
1272
  return NextResponse2.next();
1245
1273
  }
@@ -1289,18 +1317,20 @@ var handleUniformEdgeRoute = async ({
1289
1317
  headers.delete("content-encoding");
1290
1318
  headers.delete("etag");
1291
1319
  headers.delete("last-modified");
1292
- const originEtag = originResponse.status === 200 ? originResponse.headers.get("etag") : null;
1293
- const chosen = originEtag && ((_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag) === originEtag ? replayRecordedPage(context, record.page) : void 0;
1320
+ const isOk = originResponse.status === 200;
1321
+ const originEtag = etags && isOk ? originResponse.headers.get("etag") : null;
1322
+ const version = originEtag != null ? originEtag : etags && isOk ? (_b = record == null ? void 0 : record.page) == null ? void 0 : _b.etag : void 0;
1323
+ const chosen = version && ((_c = record == null ? void 0 : record.page) == null ? void 0 : _c.etag) === version ? replayRecordedPage(context, record.page) : void 0;
1294
1324
  let includeTransitionState = true;
1295
- if (originEtag && chosen) {
1325
+ if (version && chosen) {
1296
1326
  const newState = hasNewVisitorState(context);
1297
- const etag = await createEdgeEtag({ originEtag, chosen, oneShot: newState });
1327
+ const etag = await createEdgeEtag({ version, chosen, oneShot: newState });
1298
1328
  headers.set("etag", etag);
1299
1329
  headers.set("cache-control", "private, no-cache");
1300
1330
  if (!newState) {
1301
1331
  includeTransitionState = false;
1302
1332
  if (matchesIfNoneMatch(request.headers.get("if-none-match"), etag)) {
1303
- (_c = originResponse.body) == null ? void 0 : _c.cancel().catch(() => {
1333
+ (_d = originResponse.body) == null ? void 0 : _d.cancel().catch(() => {
1304
1334
  });
1305
1335
  const notModifiedHeaders = new Headers(result.headers);
1306
1336
  notModifiedHeaders.set("etag", etag);
@@ -1311,14 +1341,25 @@ var handleUniformEdgeRoute = async ({
1311
1341
  } else {
1312
1342
  headers.set("cache-control", "private, no-store");
1313
1343
  }
1314
- const onComplete = store && originResponse.status === 200 ? (placements) => store.observe({
1315
- pageState: result.pageState,
1316
- hasPlacements: placements.some((placement) => !placement.parent),
1317
- ...originEtag ? { page: { etag: originEtag, placements: buildPlacementTree(placements) } } : {},
1318
- waitUntil
1319
- }) : void 0;
1344
+ const hasher = etags && store && isOk && !originEtag ? createContentHasher() : void 0;
1345
+ const onComplete = store && isOk ? (placements) => {
1346
+ const pageVersion = originEtag != null ? originEtag : hasher ? `${CONTENT_VERSION_PREFIX}${hasher.digest()}` : void 0;
1347
+ store.observe({
1348
+ pageState: result.pageState,
1349
+ hasPlacements: placements.some((placement) => !placement.parent),
1350
+ ...pageVersion ? { page: { etag: pageVersion, placements: buildPlacementTree(placements) } } : {},
1351
+ waitUntil
1352
+ });
1353
+ } : void 0;
1320
1354
  return new Response(
1321
- transformBody({ body: originResponse.body, context, onError, onComplete, includeTransitionState }),
1355
+ transformBody({
1356
+ body: originResponse.body,
1357
+ context,
1358
+ onError,
1359
+ onComplete,
1360
+ onChunk: hasher == null ? void 0 : hasher.update,
1361
+ includeTransitionState
1362
+ }),
1322
1363
  {
1323
1364
  status: originResponse.status,
1324
1365
  statusText: originResponse.statusText,
@@ -1362,6 +1403,7 @@ var transformBody = ({
1362
1403
  context,
1363
1404
  onError,
1364
1405
  onComplete,
1406
+ onChunk,
1365
1407
  includeTransitionState
1366
1408
  }) => {
1367
1409
  if (!body) {
@@ -1386,6 +1428,9 @@ var transformBody = ({
1386
1428
  await writer.ready.catch(() => {
1387
1429
  });
1388
1430
  const { done, value } = await reader.read();
1431
+ if (value) {
1432
+ onChunk == null ? void 0 : onChunk(value);
1433
+ }
1389
1434
  return { done, value };
1390
1435
  }
1391
1436
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniformdev/next-app-router",
3
- "version": "20.81.1-alpha.12.sha-5d72d595dc",
3
+ "version": "20.81.1-alpha.15.sha-2b5fd446ca",
4
4
  "license": "SEE LICENSE IN LICENSE.txt",
5
5
  "scripts": {
6
6
  "build": "tsup",
@@ -108,7 +108,7 @@
108
108
  "@types/node": "26.0.0",
109
109
  "@types/react": "19.2.17",
110
110
  "@types/react-dom": "19.2.3",
111
- "@uniformdev/context-edge": "20.81.1-alpha.12.sha-5d72d595dc",
111
+ "@uniformdev/context-edge": "20.81.1-alpha.15.sha-2b5fd446ca",
112
112
  "@vercel/functions": "^3.7.2",
113
113
  "next": "16.3.1",
114
114
  "react": "19.2.1",
@@ -118,13 +118,14 @@
118
118
  "vitest": "4.1.11"
119
119
  },
120
120
  "dependencies": {
121
- "@uniformdev/canvas-react": "20.81.1-alpha.12.sha-5d72d595dc",
122
- "@uniformdev/context": "20.81.1-alpha.12.sha-5d72d595dc",
123
- "@uniformdev/next-app-router-client": "20.81.1-alpha.12.sha-5d72d595dc",
124
- "@uniformdev/next-app-router-shared": "20.81.1-alpha.12.sha-5d72d595dc",
125
- "@uniformdev/redirect": "20.81.1-alpha.12.sha-5d72d595dc",
126
- "@uniformdev/richtext": "20.81.1-alpha.12.sha-5d72d595dc",
127
- "@uniformdev/webhooks": "20.81.1-alpha.12.sha-5d72d595dc",
121
+ "@uniformdev/canvas-react": "20.81.1-alpha.15.sha-2b5fd446ca",
122
+ "@uniformdev/context": "20.81.1-alpha.15.sha-2b5fd446ca",
123
+ "@uniformdev/next-app-router-client": "20.81.1-alpha.15.sha-2b5fd446ca",
124
+ "@uniformdev/next-app-router-shared": "20.81.1-alpha.15.sha-2b5fd446ca",
125
+ "@uniformdev/redirect": "20.81.1-alpha.15.sha-2b5fd446ca",
126
+ "@uniformdev/richtext": "20.81.1-alpha.15.sha-2b5fd446ca",
127
+ "@uniformdev/webhooks": "20.81.1-alpha.15.sha-2b5fd446ca",
128
+ "dequal": "^2.0.3",
128
129
  "encoding": "^0.1.13",
129
130
  "server-only": "^0.0.1",
130
131
  "svix": "^1.96.0"
@@ -150,5 +151,5 @@
150
151
  "publishConfig": {
151
152
  "access": "public"
152
153
  },
153
- "gitHead": "5d72d595dcfb73d9d0500f2d33170158f8059cce"
154
+ "gitHead": "2b5fd446caf0ba7503b06dedef66fcdf2d8c8f39"
154
155
  }