@decocms/tanstack 7.22.0 → 7.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/tanstack",
3
- "version": "7.22.0",
3
+ "version": "7.22.1",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for TanStack Start + Cloudflare Workers",
6
6
  "repository": {
@@ -24,9 +24,9 @@
24
24
  "lint:unused": "knip"
25
25
  },
26
26
  "dependencies": {
27
- "@decocms/blocks": "7.22.0",
28
- "@decocms/blocks-admin": "7.22.0",
29
- "@decocms/blocks-cli": "7.22.0",
27
+ "@decocms/blocks": "7.22.1",
28
+ "@decocms/blocks-admin": "7.22.1",
29
+ "@decocms/blocks-cli": "7.22.1",
30
30
  "@deco-cx/warp-node": "^0.3.16",
31
31
  "fast-json-patch": "^3.1.0",
32
32
  "ws": "^8.18.0"
@@ -430,3 +430,53 @@ describe("POST /_cache/purge-loaders", () => {
430
430
  expect(res.status).toBe(404);
431
431
  });
432
432
  });
433
+
434
+ describe("X-Cache-Segment — custom SegmentKey fields (#284)", () => {
435
+ afterEach(() => {
436
+ __resetKvHydrationStateForTests();
437
+ setBlocks({});
438
+ });
439
+
440
+ const HTML_SERVER_ENTRY = {
441
+ fetch: async (_req: Request) =>
442
+ new Response("<html>hi</html>", {
443
+ status: 200,
444
+ headers: { "content-type": "text/html; charset=utf-8" },
445
+ }),
446
+ };
447
+
448
+ it("includes a custom segment field in X-Cache-Segment instead of dropping it", async () => {
449
+ const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
450
+ observability: false,
451
+ buildSegment: () => ({ device: "desktop", regionId: "RJ", delivery: "store-42" }),
452
+ });
453
+ const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
454
+ expect(res.headers.get("X-Cache-Segment")).toBe("desktop|r=RJ|delivery=store-42");
455
+ });
456
+
457
+ it("produces different X-Cache-Segment hashes for requests that only differ by a custom field", async () => {
458
+ let delivery = "store-1";
459
+ const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
460
+ observability: false,
461
+ buildSegment: () => ({ device: "desktop", delivery }),
462
+ });
463
+
464
+ const first = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
465
+ const firstSegment = first.headers.get("X-Cache-Segment");
466
+
467
+ delivery = "store-2";
468
+ const second = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
469
+ const secondSegment = second.headers.get("X-Cache-Segment");
470
+
471
+ expect(firstSegment).not.toBe(secondSegment);
472
+ });
473
+
474
+ it("keeps the existing hash format for segments using only known fields (back-compat)", async () => {
475
+ const worker = createDecoWorkerEntry(HTML_SERVER_ENTRY, {
476
+ observability: false,
477
+ buildSegment: () => ({ device: "desktop", salesChannel: "1", flags: ["b", "a"] }),
478
+ });
479
+ const res = await worker.fetch(new Request("https://example.com/"), EMPTY_ENV, MOCK_CTX);
480
+ expect(res.headers.get("X-Cache-Segment")).toBe("desktop|sc=1|f=a,b");
481
+ });
482
+ });
@@ -172,6 +172,13 @@ export interface SegmentKey {
172
172
  regionId?: string;
173
173
  /** Sorted list of active A/B flag names for cache cohort splitting. */
174
174
  flags?: string[];
175
+ /**
176
+ * Custom cache dimensions beyond the known fields above (e.g. a
177
+ * store/CEP-based delivery selector). `hashSegment` serializes these
178
+ * automatically — unlike the known fields, there's no dedicated shorthand
179
+ * key, so don't reuse a name already declared above.
180
+ */
181
+ [customField: string]: string | boolean | string[] | undefined;
175
182
  }
176
183
 
177
184
  /**
@@ -983,12 +990,34 @@ export function createDecoWorkerEntry(
983
990
  return detectCacheProfile(target);
984
991
  }
985
992
 
993
+ const KNOWN_SEGMENT_FIELDS = new Set([
994
+ "device",
995
+ "loggedIn",
996
+ "salesChannel",
997
+ "regionId",
998
+ "flags",
999
+ ]);
1000
+
986
1001
  function hashSegment(seg: SegmentKey): string {
987
1002
  const parts: string[] = [seg.device];
988
1003
  if (seg.loggedIn) parts.push("auth");
989
1004
  if (seg.salesChannel) parts.push(`sc=${seg.salesChannel}`);
990
1005
  if (seg.regionId) parts.push(`r=${seg.regionId}`);
991
1006
  if (seg.flags?.length) parts.push(`f=${seg.flags.sort().join(",")}`);
1007
+ // Custom dimensions (anything beyond the known fields above) are
1008
+ // serialized generically so they aren't silently dropped from the
1009
+ // cache key — see #284.
1010
+ const customKeys = Object.keys(seg)
1011
+ .filter((k) => !KNOWN_SEGMENT_FIELDS.has(k) && seg[k] !== undefined)
1012
+ .sort();
1013
+ for (const k of customKeys) {
1014
+ const v = seg[k];
1015
+ if (v === "" || v === false || (Array.isArray(v) && v.length === 0)) {
1016
+ continue;
1017
+ }
1018
+ const serialized = Array.isArray(v) ? [...v].sort().join(",") : v === true ? "" : String(v);
1019
+ parts.push(serialized ? `${k}=${serialized}` : k);
1020
+ }
992
1021
  return parts.join("|");
993
1022
  }
994
1023