✨ feat: enhance SSR cache management with dependency tracking and entry-level invalidation
This commit is contained in:
@@ -21,6 +21,7 @@ meta:
|
|||||||
- source: path
|
- source: path
|
||||||
filter: true
|
filter: true
|
||||||
- source: validUntil
|
- source: validUntil
|
||||||
|
- dependencies
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
public:
|
public:
|
||||||
@@ -64,3 +65,11 @@ fields:
|
|||||||
label:
|
label:
|
||||||
de: Gültig bis
|
de: Gültig bis
|
||||||
en: Valid until
|
en: Valid until
|
||||||
|
|
||||||
|
- name: dependencies
|
||||||
|
type: string[]
|
||||||
|
index: [single]
|
||||||
|
meta:
|
||||||
|
label:
|
||||||
|
de: Abhängigkeiten
|
||||||
|
en: Dependencies
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
var utils = require("./lib/utils")
|
var utils = require("./lib/utils")
|
||||||
|
|
||||||
;(function () {
|
;(function () {
|
||||||
utils.clearSSRCache()
|
const col = context.collection()
|
||||||
|
const collectionName = col && col.name ? col.name : null
|
||||||
|
const req = context.request()
|
||||||
|
const method = req.method
|
||||||
|
const entryId = (context.data && !Array.isArray(context.data) && context.data.id) || req.param("id") || null
|
||||||
|
|
||||||
|
utils.clearSSRCache(collectionName, entryId, method)
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
const generateUrlString = (text) => {
|
|
||||||
if (text) {
|
|
||||||
return text
|
|
||||||
.replace(/[^a-zA-Z0-9 \/]/g, "")
|
|
||||||
.replace(/\s/g, "-")
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generateUrlString,
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,10 @@ const { apiSsrBaseURL, ssrPublishCheckCollections } = require("../config")
|
|||||||
function ssrRequest(cacheKey, endpoint, query, options) {
|
function ssrRequest(cacheKey, endpoint, query, options) {
|
||||||
let url = endpoint + (query ? "?" + query : "")
|
let url = endpoint + (query ? "?" + query : "")
|
||||||
|
|
||||||
|
// track which collections/entries contribute to this SSR render
|
||||||
|
// endpoint may contain path segments (e.g. "content/abc123") or query strings
|
||||||
|
const collectionName = endpoint.split("?")[0].split("/")[0]
|
||||||
|
|
||||||
if (ssrPublishCheckCollections?.includes(endpoint)) {
|
if (ssrPublishCheckCollections?.includes(endpoint)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let validUntil = context.ssrCacheValidUntil
|
let validUntil = context.ssrCacheValidUntil
|
||||||
@@ -63,6 +67,29 @@ function ssrRequest(cacheKey, endpoint, query, options) {
|
|||||||
// json is go data structure and incompatible with js, so we need to convert it
|
// json is go data structure and incompatible with js, so we need to convert it
|
||||||
const r = { data: JSON.parse(JSON.stringify(json)), count: count }
|
const r = { data: JSON.parse(JSON.stringify(json)), count: count }
|
||||||
|
|
||||||
|
// track dependencies: "col:id" for single-entry, "col:*" for list queries
|
||||||
|
// @ts-ignore – dynamic property set by get_read.js
|
||||||
|
if (context.ssrDeps) {
|
||||||
|
let entryId = null
|
||||||
|
|
||||||
|
if (!Array.isArray(r.data) && r.data && r.data.id) {
|
||||||
|
// direct ID lookup (COLLECTION/ID) – API returned single object
|
||||||
|
entryId = r.data.id
|
||||||
|
} else if (options?.limit === 1 && Array.isArray(r.data) && r.data.length === 1 && r.data[0] && r.data[0].id) {
|
||||||
|
// filter-based detail query with limit:1
|
||||||
|
entryId = r.data[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryId) {
|
||||||
|
// @ts-ignore
|
||||||
|
context.ssrDeps[collectionName + ":" + entryId] = true
|
||||||
|
} else {
|
||||||
|
// list query – any change to this collection affects this page
|
||||||
|
// @ts-ignore
|
||||||
|
context.ssrDeps[collectionName + ":*"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
context.ssrCache[cacheKey] = r
|
context.ssrCache[cacheKey] = r
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
const { cryptchaSiteId } = require("../config-client")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {any} str
|
* @param {any} str
|
||||||
@@ -9,81 +7,44 @@ function log(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert object to string
|
* clear SSR cache – entry-level invalidation
|
||||||
* @param {any} obj object
|
*
|
||||||
* @returns {string}
|
* Dependencies are stored as strings: "col:id" (detail) or "col:*" (list).
|
||||||
*/
|
* - POST (new entry): only list deps affected → delete where "col:*"
|
||||||
function obj2str(obj) {
|
* - PUT/DELETE (entry): detail + list deps affected → delete where "col:id" OR "col:*"
|
||||||
if (Array.isArray(obj)) {
|
* - No args: full wipe (manual clear)
|
||||||
return JSON.stringify(
|
*
|
||||||
obj.map(function (idx) {
|
* @param {string} [collectionName] - collection name
|
||||||
return obj2str(idx)
|
* @param {string} [entryId] - specific entry ID (for PUT/DELETE)
|
||||||
})
|
* @param {string} [method] - HTTP method (POST/PUT/DELETE)
|
||||||
)
|
|
||||||
} else if (typeof obj === "object" && obj !== null) {
|
|
||||||
var elements = Object.keys(obj)
|
|
||||||
.sort()
|
|
||||||
.map(function (key) {
|
|
||||||
var val = obj2str(obj[key])
|
|
||||||
if (val) {
|
|
||||||
return key + ":" + val
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var elementsCleaned = []
|
|
||||||
for (var i = 0; i < elements.length; i++) {
|
|
||||||
if (elements[i]) elementsCleaned.push(elements[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
return "{" + elementsCleaned.join("|") + "}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj) return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear SSR cache
|
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
function clearSSRCache() {
|
function clearSSRCache(collectionName, entryId, method) {
|
||||||
var info = context.db.deleteMany("ssr", {})
|
/** @type {any} */
|
||||||
context.response.header("X-SSR-Cleared", info.removed)
|
let filter = {}
|
||||||
return info.removed
|
const m = method ? method.toUpperCase() : ""
|
||||||
}
|
|
||||||
|
|
||||||
function cryptchaCheck() {
|
if (collectionName && entryId && (m === "PUT" || m === "DELETE")) {
|
||||||
const solutionId = context.data._sId
|
// entry updated or deleted: invalidate pages that reference this specific entry OR list this collection
|
||||||
const solution = context.data._s
|
filter = {
|
||||||
|
$or: [{ dependencies: collectionName + ":" + entryId }, { dependencies: collectionName + ":*" }],
|
||||||
const body = JSON.stringify({ solution: solution })
|
|
||||||
console.log(body)
|
|
||||||
const resp = context.http.fetch(
|
|
||||||
"https://cryptcha.webmakers.de/api/command?siteId=" +
|
|
||||||
cryptchaSiteId +
|
|
||||||
"&cmd=check&clear=1&solutionId=" +
|
|
||||||
solutionId,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const j = resp.body.json()
|
|
||||||
|
|
||||||
const corrent = j.status == "ok" && resp.status == 200
|
|
||||||
if (!corrent) {
|
|
||||||
throw {
|
|
||||||
status: 400,
|
|
||||||
log: false,
|
|
||||||
error: "incorrect data",
|
|
||||||
}
|
}
|
||||||
|
} else if (collectionName) {
|
||||||
|
// new entry (POST) or unknown method: invalidate all pages that list this collection
|
||||||
|
filter = { dependencies: collectionName + ":*" }
|
||||||
}
|
}
|
||||||
return true
|
// else: no args → full wipe (empty filter)
|
||||||
|
|
||||||
|
// @ts-ignore – filter uses MongoDB operators not in DbReadOptions type
|
||||||
|
const info = context.db.deleteMany("ssr", { filter: filter })
|
||||||
|
context.response.header("X-SSR-Cleared", info.removed)
|
||||||
|
if (collectionName) {
|
||||||
|
context.response.header("X-SSR-Cleared-Collection", collectionName)
|
||||||
|
}
|
||||||
|
return info.removed
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
log,
|
log,
|
||||||
clearSSRCache,
|
clearSSRCache,
|
||||||
obj2str,
|
|
||||||
cryptchaCheck,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ const { ssrRequest } = require("../lib/ssr-server")
|
|||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
context.ssrCache = {}
|
context.ssrCache = {}
|
||||||
|
// @ts-ignore – tracks dependencies as { "col:id": true, "col:*": true }
|
||||||
|
context.ssrDeps = {}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
context.ssrRequest = ssrRequest
|
context.ssrRequest = ssrRequest
|
||||||
|
|
||||||
@@ -165,6 +167,22 @@ const { ssrRequest } = require("../lib/ssr-server")
|
|||||||
tpl = tpl.replace("<!--HEAD-->", head)
|
tpl = tpl.replace("<!--HEAD-->", head)
|
||||||
tpl = tpl.replace("<!--HTML-->", html)
|
tpl = tpl.replace("<!--HTML-->", html)
|
||||||
tpl = tpl.replace("<!--SSR.ERROR-->", error ? "<!--" + error + "-->" : "")
|
tpl = tpl.replace("<!--SSR.ERROR-->", error ? "<!--" + error + "-->" : "")
|
||||||
|
|
||||||
|
// Deduplicate deps: if "col:*" exists, drop all "col:<id>" for that collection
|
||||||
|
// @ts-ignore – ssrDeps is set dynamically above
|
||||||
|
var depsKeys = context.ssrDeps ? Object.keys(context.ssrDeps) : []
|
||||||
|
/** @type {{[key: string]: boolean}} */
|
||||||
|
var wildcardCols = {}
|
||||||
|
depsKeys.forEach(function (k) {
|
||||||
|
if (k.endsWith(":*")) wildcardCols[k.split(":")[0]] = true
|
||||||
|
})
|
||||||
|
depsKeys = depsKeys.filter(function (k) {
|
||||||
|
if (k.endsWith(":*")) return true
|
||||||
|
return !wildcardCols[k.split(":")[0]]
|
||||||
|
})
|
||||||
|
|
||||||
|
// @ts-ignore – append deps for debugging
|
||||||
|
comment += ", deps: [" + depsKeys.join(", ") + "]"
|
||||||
tpl = tpl.replace("<!--SSR.COMMENT-->", comment ? "<!--" + comment + "-->" : "")
|
tpl = tpl.replace("<!--SSR.COMMENT-->", comment ? "<!--" + comment + "-->" : "")
|
||||||
|
|
||||||
if (cacheIt && !noCache) {
|
if (cacheIt && !noCache) {
|
||||||
@@ -174,6 +192,8 @@ const { ssrRequest } = require("../lib/ssr-server")
|
|||||||
content: tpl,
|
content: tpl,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
validUntil: context.ssrCacheValidUntil,
|
validUntil: context.ssrCacheValidUntil,
|
||||||
|
// dependency strings: "col:id" for detail, "col:*" for list (deduplicated)
|
||||||
|
dependencies: depsKeys,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"start:ssr": "SSR=1 node scripts/esbuild-wrapper.js start",
|
"start:ssr": "SSR=1 node scripts/esbuild-wrapper.js start",
|
||||||
"build": "node scripts/esbuild-wrapper.js build",
|
"build": "node scripts/esbuild-wrapper.js build",
|
||||||
"build:admin": "node scripts/esbuild-wrapper.js build esbuild.config.admin.js",
|
"build:admin": "node scripts/esbuild-wrapper.js build esbuild.config.admin.js",
|
||||||
"build:server": "node scripts/esbuild-wrapper.js build esbuild.config.server.js && babel --config-file ./babel.config.server.json _temp/app.server.js -o _temp/app.server.babeled.js && esbuild _temp/app.server.babeled.js --outfile=api/hooks/lib/app.server.js --bundle --sourcemap --platform=node",
|
"build:server": "node scripts/esbuild-wrapper.js build esbuild.config.server.js && babel --config-file ./babel.config.server.json _temp/app.server.js -o _temp/app.server.babeled.js && esbuild _temp/app.server.babeled.js --outfile=api/hooks/lib/app.server.js --bundle --sourcemap --platform=node --banner:js='// @ts-nocheck'",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:e2e": "playwright test tests/e2e",
|
"test:e2e": "playwright test tests/e2e",
|
||||||
"test:api": "playwright test tests/api",
|
"test:api": "playwright test tests/api",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
|
||||||
"include": ["frontend/src/**/*", "types/**/*", "./../../cms/tibi-types", "api/**/*"],
|
"include": ["frontend/src/**/*", "types/**/*", "./../../cms/tibi-types", "api/**/*"],
|
||||||
|
"exclude": ["**/app.server.js", "**/app.server.js.map", "_temp/**"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"typeRoots": ["./node_modules/@types", "./types"],
|
"typeRoots": ["./node_modules/@types", "./types"],
|
||||||
|
|||||||
Reference in New Issue
Block a user