@decocms/blocks 7.6.0 → 7.7.0

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/blocks",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework-agnostic core: CMS block resolution, section registry, matchers, portable SDK utilities",
6
6
  "repository": {
@@ -2,8 +2,11 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  BundledBlockSource,
4
4
  computeRevision,
5
- KV_KEYS,
5
+ DEPLOYMENTS_KEY,
6
6
  type KVNamespace,
7
+ LIVE_KEY,
8
+ revisionKey,
9
+ snapshotKey,
7
10
  } from "./blockSource";
8
11
  import { djb2Hex } from "../sdk/djb2";
9
12
 
@@ -37,10 +40,15 @@ describe("BundledBlockSource", () => {
37
40
  });
38
41
  });
39
42
 
40
- describe("KV_KEYS", () => {
41
- it("exposes the snapshot and revision key contract", () => {
42
- expect(KV_KEYS.SNAPSHOT).toBe("decofile:current");
43
- expect(KV_KEYS.REVISION).toBe("index:revision");
43
+ describe("KV key layout (per deployment)", () => {
44
+ it("keys the snapshot and revision by deployment id", () => {
45
+ expect(snapshotKey("abc123")).toBe("decofile:abc123");
46
+ expect(revisionKey("abc123")).toBe("index:revision:abc123");
47
+ });
48
+
49
+ it("exposes stable pointer/bookkeeping keys", () => {
50
+ expect(LIVE_KEY).toBe("index:live");
51
+ expect(DEPLOYMENTS_KEY).toBe("index:deployments");
44
52
  });
45
53
  });
46
54
 
@@ -59,9 +67,10 @@ describe("KVNamespace structural type", () => {
59
67
  },
60
68
  };
61
69
 
62
- await kv.put(KV_KEYS.REVISION, "abc");
63
- await expect(kv.get(KV_KEYS.REVISION)).resolves.toBe("abc");
64
- await kv.delete(KV_KEYS.REVISION);
65
- await expect(kv.get(KV_KEYS.REVISION)).resolves.toBeNull();
70
+ const key = revisionKey("abc123");
71
+ await kv.put(key, "abc");
72
+ await expect(kv.get(key)).resolves.toBe("abc");
73
+ await kv.delete(key);
74
+ await expect(kv.get(key)).resolves.toBeNull();
66
75
  });
67
76
  });
@@ -37,8 +37,8 @@ export interface BlockSnapshot {
37
37
  * - `BundledBlockSource` — the build-time `blocks.gen` snapshot (fallback /
38
38
  * local dev). A no-op here because `setup.ts` already loads it via
39
39
  * `setBlocks()` at module init.
40
- * - `KVBlockSource` (PR2) — reads `decofile:current` + `index:revision` from a
41
- * Cloudflare KV namespace.
40
+ * - `KVBlockSource` — reads `decofile:<id>` + `index:revision:<id>` (keyed by
41
+ * deployment id) from a Cloudflare KV namespace.
42
42
  */
43
43
  export interface BlockSource {
44
44
  /**
@@ -111,14 +111,81 @@ export interface KVNamespace {
111
111
  }
112
112
 
113
113
  // ---------------------------------------------------------------------------
114
- // KV key namesshared by the runtime reader (PR2), the write-through path
115
- // (PR3), and the CI sync/migrate scripts (PR4). Single source of truth so the
116
- // contract can't drift between read and write sides.
114
+ // KV key layoutkeyed by DEPLOYMENT ID so every code deployment reads its own
115
+ // content snapshot (deploy isolation, cheap rollback, build-time content sync).
116
+ //
117
+ // The deployment id is the git commit sha (see `getDeploymentId` below).
118
+ // Shared by the runtime reader (`kvBlockSource.ts`), the write-through path
119
+ // (`admin/decofile.ts`), and the CI sync/migrate scripts, so the contract can't
120
+ // drift between read and write sides.
121
+ //
122
+ // decofile:<id> full decofile JSON for deployment <id>
123
+ // index:revision:<id> DJB2 hex revision of that snapshot (polled)
124
+ // index:live pointer to the currently-live <id> (set post-deploy)
125
+ // index:deployments JSON [{id, ts}] (newest last) — GC bookkeeping
117
126
  // ---------------------------------------------------------------------------
118
127
 
119
- export const KV_KEYS = {
120
- /** Full decofile JSON (the blocks map). */
121
- SNAPSHOT: "decofile:current",
122
- /** DJB2 hex revision of the snapshot — polled for change detection. */
123
- REVISION: "index:revision",
124
- } as const;
128
+ /** KV key holding the full decofile JSON for deployment `id`. */
129
+ export function snapshotKey(id: string): string {
130
+ return `decofile:${id}`;
131
+ }
132
+
133
+ /** KV key holding the DJB2 hex revision of deployment `id`'s snapshot. */
134
+ export function revisionKey(id: string): string {
135
+ return `index:revision:${id}`;
136
+ }
137
+
138
+ /** Pointer key naming the currently-live deployment id. Written by a code
139
+ * deploy AFTER the new version has activated (so a rolling deploy never points
140
+ * live at a version that isn't serving yet). */
141
+ export const LIVE_KEY = "index:live";
142
+
143
+ /** Bookkeeping key: JSON array of `{ id, ts }` (newest last) tracking known
144
+ * deployment snapshots so the sync script can GC all but the last N. */
145
+ export const DEPLOYMENTS_KEY = "index:deployments";
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Deployment id resolution
149
+ //
150
+ // Which snapshot key does THIS running worker read/write? The deployment id.
151
+ // Kept here (framework-agnostic) alongside the key builders it feeds, so the
152
+ // runtime read path (`@decocms/tanstack` kvHydration) and the write-through
153
+ // path (`@decocms/blocks-admin` decofile) resolve it identically — no drift.
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /** Deploy-time var naming this version's content snapshot key. Set by the
157
+ * deploy command (`wrangler deploy --var DECO_DEPLOYMENT_ID:<sha>`). */
158
+ export const DEPLOYMENT_ID_ENV = "DECO_DEPLOYMENT_ID";
159
+
160
+ /** Fallback var (cache-key version) — the same commit sha, already threaded by
161
+ * some sites via `--var BUILD_HASH:<sha>`. */
162
+ export const BUILD_HASH_ENV = "BUILD_HASH";
163
+
164
+ // Build-time constant injected by the tanstack `decoVitePlugin()` (git
165
+ // rev-parse / WORKERS_CI_COMMIT_SHA) as the last-resort deployment id. Declared
166
+ // here with a `typeof` guard so it's inert where the define isn't applied
167
+ // (e.g. the Next.js build, or this package's own tsc output).
168
+ declare const __DECO_BUILD_HASH__: string | undefined;
169
+
170
+ /**
171
+ * Resolve this worker's deployment id — the key its content is stored under
172
+ * (`decofile:<id>`). Priority:
173
+ * 1. `env.DECO_DEPLOYMENT_ID` — set by the deploy command (authoritative).
174
+ * 2. `env.BUILD_HASH` — the cache-key version var; the same commit sha.
175
+ * 3. `__DECO_BUILD_HASH__` — build-time constant (the commit the bundle, and
176
+ * thus the build-time content sync, was produced from).
177
+ * Returns `null` when none resolves — the caller then serves the bundled
178
+ * snapshot (never another deployment's content).
179
+ */
180
+ export function getDeploymentId(env: Record<string, unknown>): string | null {
181
+ const fromDeploymentVar = env[DEPLOYMENT_ID_ENV];
182
+ if (typeof fromDeploymentVar === "string" && fromDeploymentVar) {
183
+ return fromDeploymentVar;
184
+ }
185
+ const fromBuildHash = env[BUILD_HASH_ENV];
186
+ if (typeof fromBuildHash === "string" && fromBuildHash) return fromBuildHash;
187
+ if (typeof __DECO_BUILD_HASH__ !== "undefined" && __DECO_BUILD_HASH__) {
188
+ return __DECO_BUILD_HASH__;
189
+ }
190
+ return null;
191
+ }
package/src/cms/index.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  export type { BlockSnapshot, BlockSource, KVNamespace } from "./blockSource";
2
- export { BundledBlockSource, computeRevision, KV_KEYS } from "./blockSource";
2
+ export {
3
+ BUILD_HASH_ENV,
4
+ BundledBlockSource,
5
+ computeRevision,
6
+ DEPLOYMENT_ID_ENV,
7
+ DEPLOYMENTS_KEY,
8
+ getDeploymentId,
9
+ LIVE_KEY,
10
+ revisionKey,
11
+ snapshotKey,
12
+ } from "./blockSource";
3
13
  export type { DecoPage, Resolvable } from "./loader";
4
14
  export {
5
15
  findPageByPath,
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  /**
11
11
  * Regression test for the layout-cache index-corruption bug that shipped in
12
- * @decocms/start@6.12.1 and was fixed in 6.12.2 (deco-start commit 73d4f19,
12
+ * @decocms/start@6.12.1 and was fixed in 6.12.2 (blocks commit 73d4f19,
13
13
  * "fix(cms): don't mutate shared cached section objects when stamping
14
14
  * index"). Two live production sites (casaevideo-tanstack, bagaggio-tanstack)
15
15
  * hit this in the wild as an intermittent "footer renders above other
@@ -18,7 +18,7 @@ import { join } from "node:path";
18
18
  * passes the on-disk filename through.
19
19
  *
20
20
  * Replaces the abandoned @decocms/start/node tier's loadAllDecofileBlocks,
21
- * which no longer exists on any reachable deco-start version — ported
21
+ * which no longer exists on any reachable blocks version — ported
22
22
  * fresh rather than resurrected. Namespace-imported node:fs/promises to
23
23
  * match this codebase's existing pattern for Node-only code that must not
24
24
  * break Vite/webpack client bundle analysis (see cms/loader.ts's
@@ -331,7 +331,7 @@ function hasForceEagerParam(ctx?: MatcherContext): boolean {
331
331
  * they CAN hydrate and resolve deferred sections. SPA navigations (TanStack
332
332
  * `<Link>` → `/_serverFn`) also send `empty` but set `isClientNavigation`, and
333
333
  * are excluded here so page-SEO commerce loaders stay off for humans
334
- * (decocms/deco-start#286); their sections already render eagerly via the
334
+ * (decocms/blocks#286); their sections already render eagerly via the
335
335
  * `!isClientNav` branch of the `useAsync` gate.
336
336
  *
337
337
  * Like {@link hasForceEagerParam}, this must stay in lock-step with the edge
@@ -395,7 +395,7 @@ export interface MatcherContext {
395
395
  * deferral: deferral is a streaming-SSR optimization, but a client nav
396
396
  * receives the server-fn JSON in one shot, so deferral adds a round-trip +
397
397
  * skeleton with no benefit (and breaks loaders that need per-request app
398
- * context — see decocms/deco-start#277). Set by the route loaders.
398
+ * context — see decocms/blocks#277). Set by the route loaders.
399
399
  */
400
400
  isClientNavigation?: boolean;
401
401
  }
@@ -667,7 +667,7 @@ export function evaluateMatcher(
667
667
  /**
668
668
  * Matcher types whose decision must stick per user instead of re-rolling every
669
669
  * request. Mirrors the `sticky = "session"` export on the corresponding
670
- * matchers in @decocms/apps (deco-start re-implements matchers inline, so it
670
+ * matchers in @decocms/apps (blocks re-implements matchers inline, so it
671
671
  * declares the set here rather than importing those exports).
672
672
  */
673
673
  const STICKY_MATCHER_TYPES = new Set(["website/matchers/random.ts"]);
package/src/sdk/flags.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * `btoa(encodeURIComponent(JSON.stringify({ active, inactiveDrawn, pct })))`.
19
19
  * - `active` — flag names the visitor was assigned to (`true` branch).
20
20
  * - `inactiveDrawn` — flag names drawn but not matched (`false` branch).
21
- * - `pct` — `{ name: round(traffic*100) }`, a deco-start extension (analytics
21
+ * - `pct` — `{ name: round(traffic*100) }`, a blocks extension (analytics
22
22
  * ignores unknown fields) used as the re-roll fingerprint: when the operator
23
23
  * changes `traffic` and redeploys, the fingerprint no longer matches and the
24
24
  * visitor is re-rolled once, then re-sticks — same self-healing scheme as
@@ -47,7 +47,7 @@ export interface StoredFlag {
47
47
  interface DecoSegment {
48
48
  active?: string[];
49
49
  inactiveDrawn?: string[];
50
- /** deco-start extension: per-flag traffic fingerprint. Ignored by analytics. */
50
+ /** blocks extension: per-flag traffic fingerprint. Ignored by analytics. */
51
51
  pct?: Record<string, number>;
52
52
  }
53
53
 
@@ -42,7 +42,7 @@ export function withInflightTimeout<T>(
42
42
  timer = setTimeout(() => {
43
43
  reject(
44
44
  new Error(
45
- `[deco-start] inflight cache entry "${label}" timed out after ${ms}ms`,
45
+ `[blocks] inflight cache entry "${label}" timed out after ${ms}ms`,
46
46
  ),
47
47
  );
48
48
  }, ms);
@@ -60,6 +60,6 @@ export function createClickhouseCollectorAdapter(_options: ClickhouseCollectorOp
60
60
  "Until then, use Cloudflare-native observability " +
61
61
  "(observability.{logs,traces} in wrangler.jsonc) plus the Workers " +
62
62
  "Analytics Engine meter wired by instrumentWorker(). Track progress " +
63
- "at https://github.com/decocms/deco-start/issues.",
63
+ "at https://github.com/decocms/blocks/issues.",
64
64
  );
65
65
  }