@decocms/blocks 7.5.4 → 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 +1 -1
- package/src/cms/blockSource.test.ts +18 -9
- package/src/cms/blockSource.ts +78 -11
- package/src/cms/index.ts +11 -1
- package/src/cms/layoutCacheRace.test.ts +1 -1
- package/src/cms/loadDecofileDirectory.ts +1 -1
- package/src/cms/resolve.ts +3 -3
- package/src/sdk/flags.ts +2 -2
- package/src/sdk/inflightTimeout.ts +1 -1
- package/src/sdk/otelAdapters/clickhouseCollector.ts +1 -1
package/package.json
CHANGED
|
@@ -2,8 +2,11 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
BundledBlockSource,
|
|
4
4
|
computeRevision,
|
|
5
|
-
|
|
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("
|
|
41
|
-
it("
|
|
42
|
-
expect(
|
|
43
|
-
expect(
|
|
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
|
-
|
|
63
|
-
await
|
|
64
|
-
await kv.
|
|
65
|
-
await
|
|
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
|
});
|
package/src/cms/blockSource.ts
CHANGED
|
@@ -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`
|
|
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
|
|
115
|
-
// (
|
|
116
|
-
//
|
|
114
|
+
// KV key layout — keyed 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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
package/src/cms/resolve.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
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 (
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
`[
|
|
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/
|
|
63
|
+
"at https://github.com/decocms/blocks/issues.",
|
|
64
64
|
);
|
|
65
65
|
}
|