@actuate-media/cms-core 0.20.0 → 0.22.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/LICENSE +21 -21
- package/dist/__tests__/actions/document-crud.test.js +74 -2
- package/dist/__tests__/actions/document-crud.test.js.map +1 -1
- package/dist/__tests__/api/cache-headers.test.d.ts +2 -0
- package/dist/__tests__/api/cache-headers.test.d.ts.map +1 -0
- package/dist/__tests__/api/cache-headers.test.js +135 -0
- package/dist/__tests__/api/cache-headers.test.js.map +1 -0
- package/dist/__tests__/api/comments-routes.test.d.ts +2 -0
- package/dist/__tests__/api/comments-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/comments-routes.test.js +266 -0
- package/dist/__tests__/api/comments-routes.test.js.map +1 -0
- package/dist/__tests__/api/notifications-routes.test.d.ts +2 -0
- package/dist/__tests__/api/notifications-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/notifications-routes.test.js +225 -0
- package/dist/__tests__/api/notifications-routes.test.js.map +1 -0
- package/dist/__tests__/api/preview-and-scheduling.test.js +53 -0
- package/dist/__tests__/api/preview-and-scheduling.test.js.map +1 -1
- package/dist/__tests__/fields/component-block.test.d.ts +2 -0
- package/dist/__tests__/fields/component-block.test.d.ts.map +1 -0
- package/dist/__tests__/fields/component-block.test.js +129 -0
- package/dist/__tests__/fields/component-block.test.js.map +1 -0
- package/dist/__tests__/fields/relations.test.js +62 -8
- package/dist/__tests__/fields/relations.test.js.map +1 -1
- package/dist/__tests__/preview-event-bus.test.d.ts +2 -0
- package/dist/__tests__/preview-event-bus.test.d.ts.map +1 -0
- package/dist/__tests__/preview-event-bus.test.js +89 -0
- package/dist/__tests__/preview-event-bus.test.js.map +1 -0
- package/dist/__tests__/preview-sse-stream.test.d.ts +2 -0
- package/dist/__tests__/preview-sse-stream.test.d.ts.map +1 -0
- package/dist/__tests__/preview-sse-stream.test.js +117 -0
- package/dist/__tests__/preview-sse-stream.test.js.map +1 -0
- package/dist/__tests__/realtime/comments.test.d.ts +2 -0
- package/dist/__tests__/realtime/comments.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/comments.test.js +508 -0
- package/dist/__tests__/realtime/comments.test.js.map +1 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts +2 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.js +170 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.js.map +1 -0
- package/dist/__tests__/realtime/gateway.test.d.ts +2 -0
- package/dist/__tests__/realtime/gateway.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/gateway.test.js +402 -0
- package/dist/__tests__/realtime/gateway.test.js.map +1 -0
- package/dist/__tests__/realtime/notifications.test.d.ts +2 -0
- package/dist/__tests__/realtime/notifications.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/notifications.test.js +317 -0
- package/dist/__tests__/realtime/notifications.test.js.map +1 -0
- package/dist/__tests__/realtime/persistence.test.d.ts +2 -0
- package/dist/__tests__/realtime/persistence.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/persistence.test.js +124 -0
- package/dist/__tests__/realtime/persistence.test.js.map +1 -0
- package/dist/__tests__/schema-preview-features.test.d.ts +2 -0
- package/dist/__tests__/schema-preview-features.test.d.ts.map +1 -0
- package/dist/__tests__/schema-preview-features.test.js +42 -0
- package/dist/__tests__/schema-preview-features.test.js.map +1 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +71 -26
- package/dist/actions.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +734 -188
- package/dist/api/handlers.js.map +1 -1
- package/dist/cache/__tests__/http.test.d.ts +2 -0
- package/dist/cache/__tests__/http.test.d.ts.map +1 -0
- package/dist/cache/__tests__/http.test.js +285 -0
- package/dist/cache/__tests__/http.test.js.map +1 -0
- package/dist/cache/http.d.ts +119 -0
- package/dist/cache/http.d.ts.map +1 -0
- package/dist/cache/http.js +188 -0
- package/dist/cache/http.js.map +1 -0
- package/dist/config/types.d.ts +153 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/fields/component-block.d.ts +23 -0
- package/dist/fields/component-block.d.ts.map +1 -0
- package/dist/fields/component-block.js +137 -0
- package/dist/fields/component-block.js.map +1 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +2 -0
- package/dist/fields/index.js.map +1 -1
- package/dist/fields/relations.d.ts.map +1 -1
- package/dist/fields/relations.js +43 -11
- package/dist/fields/relations.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/page-builder/schema.d.ts +2 -2
- package/dist/preview/event-bus.d.ts +39 -0
- package/dist/preview/event-bus.d.ts.map +1 -0
- package/dist/preview/event-bus.js +73 -0
- package/dist/preview/event-bus.js.map +1 -0
- package/dist/preview/sse-stream.d.ts +50 -0
- package/dist/preview/sse-stream.d.ts.map +1 -0
- package/dist/preview/sse-stream.js +120 -0
- package/dist/preview/sse-stream.js.map +1 -0
- package/dist/realtime/comments.d.ts +217 -0
- package/dist/realtime/comments.d.ts.map +1 -0
- package/dist/realtime/comments.js +282 -0
- package/dist/realtime/comments.js.map +1 -0
- package/dist/realtime/gateway.d.ts +107 -0
- package/dist/realtime/gateway.d.ts.map +1 -0
- package/dist/realtime/gateway.js +215 -0
- package/dist/realtime/gateway.js.map +1 -0
- package/dist/realtime/index.d.ts +18 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +18 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/notifications.d.ts +209 -0
- package/dist/realtime/notifications.d.ts.map +1 -0
- package/dist/realtime/notifications.js +209 -0
- package/dist/realtime/notifications.js.map +1 -0
- package/dist/realtime/persistence.d.ts +72 -0
- package/dist/realtime/persistence.d.ts.map +1 -0
- package/dist/realtime/persistence.js +53 -0
- package/dist/realtime/persistence.js.map +1 -0
- package/package.json +13 -4
- package/prisma/cms-schema.prisma +437 -306
- package/prisma/migrations/0001_init/migration.sql +384 -384
- package/prisma/migrations/0002_folders/migration.sql +39 -39
- package/prisma/migrations/0003_search_and_webhooks/migration.sql +50 -50
- package/prisma/migrations/0004_script_tags/migration.sql +21 -21
- package/prisma/migrations/0005_password_reset_tokens/migration.sql +20 -20
- package/prisma/migrations/0006_page_builder/migration.sql +38 -38
- package/prisma/migrations/migration_lock.toml +3 -3
- package/prisma/schema.prisma +563 -549
- package/generated/browser.ts +0 -109
- package/generated/client.ts +0 -133
- package/generated/commonInputTypes.ts +0 -709
- package/generated/enums.ts +0 -125
- package/generated/internal/class.ts +0 -376
- package/generated/internal/prismaNamespace.ts +0 -2617
- package/generated/internal/prismaNamespaceBrowser.ts +0 -611
- package/generated/models/ApiKey.ts +0 -1550
- package/generated/models/AuditLog.ts +0 -1206
- package/generated/models/BackupRecord.ts +0 -1250
- package/generated/models/ContentLock.ts +0 -1472
- package/generated/models/ContentTemplate.ts +0 -1416
- package/generated/models/Document.ts +0 -3005
- package/generated/models/Folder.ts +0 -1904
- package/generated/models/FormSubmission.ts +0 -1200
- package/generated/models/InAppNotification.ts +0 -1457
- package/generated/models/Media.ts +0 -2340
- package/generated/models/MediaUsage.ts +0 -1472
- package/generated/models/OAuthAccount.ts +0 -1463
- package/generated/models/Redirect.ts +0 -1284
- package/generated/models/Session.ts +0 -1492
- package/generated/models/Site.ts +0 -1206
- package/generated/models/User.ts +0 -3513
- package/generated/models/Version.ts +0 -1511
- package/generated/models/WorkflowState.ts +0 -1514
- package/generated/models.ts +0 -29
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.test.d.ts","sourceRoot":"","sources":["../../../src/cache/__tests__/http.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ACTUATE_TAG_ROOT, actuateTag, cacheKeyFor, cachedRead, invalidationTagsForDocumentWrite, invalidationTagsForGlobalWrite, publicReadCacheControl, } from '../http.js';
|
|
3
|
+
import { setActuateConfig } from '../../config/runtime.js';
|
|
4
|
+
// Reset the per-process config global between cases so a publicReads
|
|
5
|
+
// override in one test doesn't leak into the next. The runtime module
|
|
6
|
+
// stores config on globalThis (see runtime.ts header) so vitest's
|
|
7
|
+
// per-file isolation alone isn't enough.
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
setActuateConfig({});
|
|
10
|
+
});
|
|
11
|
+
describe('actuateTag / tag taxonomy', () => {
|
|
12
|
+
it('produces canonical tag strings that mirror the SDK output', () => {
|
|
13
|
+
expect(ACTUATE_TAG_ROOT).toBe('actuate');
|
|
14
|
+
expect(actuateTag('collection', 'posts')).toBe('actuate:collection:posts');
|
|
15
|
+
expect(actuateTag('doc', 'abc')).toBe('actuate:doc:abc');
|
|
16
|
+
expect(actuateTag('global', 'settings')).toBe('actuate:global:settings');
|
|
17
|
+
expect(actuateTag('path', '/blog/foo')).toBe('actuate:path:/blog/foo');
|
|
18
|
+
});
|
|
19
|
+
it('invalidationTagsForDocumentWrite includes root + collection + doc tags', () => {
|
|
20
|
+
expect(invalidationTagsForDocumentWrite('posts', 'abc')).toEqual([
|
|
21
|
+
'actuate',
|
|
22
|
+
'actuate:collection:posts',
|
|
23
|
+
'actuate:doc:abc',
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
it('invalidationTagsForGlobalWrite includes root + global tag', () => {
|
|
27
|
+
expect(invalidationTagsForGlobalWrite('settings')).toEqual([
|
|
28
|
+
'actuate',
|
|
29
|
+
'actuate:global:settings',
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('publicReadCacheControl', () => {
|
|
34
|
+
const apiKeyRequest = () => new Request('https://cms.example.com/api/cms/collections/posts', {
|
|
35
|
+
headers: { authorization: 'Bearer act_sk_abc123' },
|
|
36
|
+
});
|
|
37
|
+
const sessionRequest = () => new Request('https://cms.example.com/api/cms/collections/posts', {
|
|
38
|
+
// The cookie name is what triggers the session-auth branch — value
|
|
39
|
+
// and validity don't matter for header-emission rules.
|
|
40
|
+
headers: { cookie: 'actuate_session=anything; other=x' },
|
|
41
|
+
});
|
|
42
|
+
it('returns null when cache config is missing entirely', () => {
|
|
43
|
+
expect(publicReadCacheControl(apiKeyRequest(), undefined)).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
it('returns null when publicReads is not opted in', () => {
|
|
46
|
+
expect(publicReadCacheControl(apiKeyRequest(), { publicReads: { enabled: false } })).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
it('emits public s-maxage when enabled and the request has no session cookie', () => {
|
|
49
|
+
const value = publicReadCacheControl(apiKeyRequest(), {
|
|
50
|
+
publicReads: { enabled: true, maxAge: 60, staleWhileRevalidate: 300 },
|
|
51
|
+
});
|
|
52
|
+
expect(value).toBe('public, s-maxage=60, stale-while-revalidate=300');
|
|
53
|
+
});
|
|
54
|
+
it('applies the documented defaults when maxAge/swr are omitted', () => {
|
|
55
|
+
const value = publicReadCacheControl(apiKeyRequest(), { publicReads: { enabled: true } });
|
|
56
|
+
expect(value).toBe('public, s-maxage=60, stale-while-revalidate=300');
|
|
57
|
+
});
|
|
58
|
+
it('emits private/no-store for session-cookie requests even when caching is enabled', () => {
|
|
59
|
+
// This is the security-critical invariant: a shared CDN tier MUST NOT
|
|
60
|
+
// cache a response that may contain per-user state. Refusing the
|
|
61
|
+
// public cache here is what makes the rest of the feature safe.
|
|
62
|
+
const value = publicReadCacheControl(sessionRequest(), {
|
|
63
|
+
publicReads: { enabled: true, maxAge: 60 },
|
|
64
|
+
});
|
|
65
|
+
expect(value).toBe('private, no-store');
|
|
66
|
+
});
|
|
67
|
+
it('does not match a cookie whose VALUE happens to contain actuate_session=', () => {
|
|
68
|
+
const req = new Request('https://cms.example.com/api/cms/collections/posts', {
|
|
69
|
+
headers: {
|
|
70
|
+
// Defense-in-depth: an attacker setting another cookie whose value
|
|
71
|
+
// contains the substring must not flip us into session-cookie mode.
|
|
72
|
+
cookie: 'analytics=actuate_session=fake; tenant=x',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expect(publicReadCacheControl(req, { publicReads: { enabled: true } })).toBe('public, s-maxage=60, stale-while-revalidate=300');
|
|
76
|
+
});
|
|
77
|
+
it('clamps negative maxAge/swr values to 0 instead of emitting nonsense', () => {
|
|
78
|
+
const value = publicReadCacheControl(apiKeyRequest(), {
|
|
79
|
+
publicReads: { enabled: true, maxAge: -10, staleWhileRevalidate: -5 },
|
|
80
|
+
});
|
|
81
|
+
expect(value).toBe('public, s-maxage=0, stale-while-revalidate=0');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ─── Runtime cache (layer 3) ─────────────────────────────────────────────
|
|
85
|
+
describe('cacheKeyFor', () => {
|
|
86
|
+
it('returns the pathname when there is no query string', () => {
|
|
87
|
+
const req = new Request('https://cms.example.com/api/cms/collections/posts');
|
|
88
|
+
expect(cacheKeyFor(req)).toBe('/api/cms/collections/posts');
|
|
89
|
+
});
|
|
90
|
+
it('appends sorted query params so order does not fragment the cache', () => {
|
|
91
|
+
const a = new Request('https://cms.example.com/api/cms/collections/posts?page=2&pageSize=20');
|
|
92
|
+
const b = new Request('https://cms.example.com/api/cms/collections/posts?pageSize=20&page=2');
|
|
93
|
+
expect(cacheKeyFor(a)).toBe(cacheKeyFor(b));
|
|
94
|
+
expect(cacheKeyFor(a)).toBe('/api/cms/collections/posts?page=2&pageSize=20');
|
|
95
|
+
});
|
|
96
|
+
it('keys populated and non-populated reads independently', () => {
|
|
97
|
+
const plain = new Request('https://cms.example.com/api/cms/collections/posts/abc');
|
|
98
|
+
const populated = new Request('https://cms.example.com/api/cms/collections/posts/abc?populate=author');
|
|
99
|
+
expect(cacheKeyFor(plain)).not.toBe(cacheKeyFor(populated));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('cachedRead', () => {
|
|
103
|
+
function makeAdapter(initial) {
|
|
104
|
+
const store = new Map(Object.entries(initial ?? {}));
|
|
105
|
+
return {
|
|
106
|
+
store,
|
|
107
|
+
calls: { get: [], set: [] },
|
|
108
|
+
async get(key) {
|
|
109
|
+
this.calls.get.push(key);
|
|
110
|
+
return store.get(key);
|
|
111
|
+
},
|
|
112
|
+
async set(key, value, options) {
|
|
113
|
+
this.calls.set.push({ key, ttl: options.ttl, tags: options.tags });
|
|
114
|
+
store.set(key, value);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function jsonWrap(payload) {
|
|
119
|
+
return new Response(JSON.stringify(payload), {
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const anonRequest = () => new Request('https://cms.example.com/api/cms/collections/posts');
|
|
125
|
+
it('bypasses the cache when no adapter is configured', async () => {
|
|
126
|
+
const build = vi.fn(async () => ({ data: ['a', 'b'] }));
|
|
127
|
+
const res = await cachedRead(anonRequest(), {
|
|
128
|
+
cache: { publicReads: { enabled: true } },
|
|
129
|
+
tags: ['actuate'],
|
|
130
|
+
wrap: jsonWrap,
|
|
131
|
+
build,
|
|
132
|
+
});
|
|
133
|
+
expect(res.headers.get('X-Actuate-Cache')).toBeNull();
|
|
134
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(await res.json()).toEqual({ data: ['a', 'b'] });
|
|
136
|
+
});
|
|
137
|
+
it('bypasses the cache for session-cookie requests', async () => {
|
|
138
|
+
const adapter = makeAdapter();
|
|
139
|
+
const req = new Request('https://cms.example.com/api/cms/collections/posts', {
|
|
140
|
+
headers: { cookie: 'actuate_session=anything' },
|
|
141
|
+
});
|
|
142
|
+
const build = vi.fn(async () => ({ data: 'priv' }));
|
|
143
|
+
await cachedRead(req, {
|
|
144
|
+
cache: { runtimeCache: { adapter } },
|
|
145
|
+
tags: ['actuate', 'actuate:collection:posts'],
|
|
146
|
+
wrap: jsonWrap,
|
|
147
|
+
build,
|
|
148
|
+
});
|
|
149
|
+
expect(adapter.calls.get).toEqual([]); // never touched the cache
|
|
150
|
+
expect(adapter.calls.set).toEqual([]);
|
|
151
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
152
|
+
});
|
|
153
|
+
it('bypasses the cache when the request opts out via Cache-Control: no-cache', async () => {
|
|
154
|
+
const adapter = makeAdapter({ '/api/cms/collections/posts': { data: ['stale'] } });
|
|
155
|
+
const req = new Request('https://cms.example.com/api/cms/collections/posts', {
|
|
156
|
+
headers: { 'cache-control': 'no-cache' },
|
|
157
|
+
});
|
|
158
|
+
const build = vi.fn(async () => ({ data: ['fresh'] }));
|
|
159
|
+
const res = await cachedRead(req, {
|
|
160
|
+
cache: { runtimeCache: { adapter } },
|
|
161
|
+
tags: ['actuate'],
|
|
162
|
+
wrap: jsonWrap,
|
|
163
|
+
build,
|
|
164
|
+
});
|
|
165
|
+
expect(adapter.calls.get).toEqual([]);
|
|
166
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(await res.json()).toEqual({ data: ['fresh'] });
|
|
168
|
+
});
|
|
169
|
+
it('returns the cached payload on hit without calling build', async () => {
|
|
170
|
+
const cached = { data: ['hit'] };
|
|
171
|
+
const adapter = makeAdapter({ '/api/cms/collections/posts': cached });
|
|
172
|
+
const build = vi.fn();
|
|
173
|
+
const res = await cachedRead(anonRequest(), {
|
|
174
|
+
cache: { runtimeCache: { adapter } },
|
|
175
|
+
tags: ['actuate', 'actuate:collection:posts'],
|
|
176
|
+
wrap: jsonWrap,
|
|
177
|
+
build,
|
|
178
|
+
});
|
|
179
|
+
expect(build).not.toHaveBeenCalled();
|
|
180
|
+
expect(res.headers.get('X-Actuate-Cache')).toBe('HIT');
|
|
181
|
+
expect(await res.json()).toEqual(cached);
|
|
182
|
+
});
|
|
183
|
+
it('runs build on miss and stores the result with tags + ttl', async () => {
|
|
184
|
+
const adapter = makeAdapter();
|
|
185
|
+
const payload = { data: ['fresh'] };
|
|
186
|
+
const build = vi.fn(async () => payload);
|
|
187
|
+
const res = await cachedRead(anonRequest(), {
|
|
188
|
+
cache: { runtimeCache: { adapter, ttl: 120 } },
|
|
189
|
+
tags: ['actuate', 'actuate:collection:posts'],
|
|
190
|
+
wrap: jsonWrap,
|
|
191
|
+
build,
|
|
192
|
+
});
|
|
193
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
194
|
+
expect(adapter.calls.set).toEqual([
|
|
195
|
+
{
|
|
196
|
+
key: '/api/cms/collections/posts',
|
|
197
|
+
ttl: 120,
|
|
198
|
+
tags: ['actuate', 'actuate:collection:posts'],
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
expect(res.headers.get('X-Actuate-Cache')).toBe('MISS');
|
|
202
|
+
expect(await res.json()).toEqual(payload);
|
|
203
|
+
});
|
|
204
|
+
it('falls back to defaults when ttl is omitted', async () => {
|
|
205
|
+
const adapter = makeAdapter();
|
|
206
|
+
await cachedRead(anonRequest(), {
|
|
207
|
+
cache: { runtimeCache: { adapter } },
|
|
208
|
+
tags: ['actuate'],
|
|
209
|
+
wrap: jsonWrap,
|
|
210
|
+
build: async () => ({ data: 1 }),
|
|
211
|
+
});
|
|
212
|
+
expect(adapter.calls.set[0]?.ttl).toBe(60);
|
|
213
|
+
});
|
|
214
|
+
it('never caches a build that returns a Response (e.g. 404)', async () => {
|
|
215
|
+
const adapter = makeAdapter();
|
|
216
|
+
const notFound = new Response(JSON.stringify({ error: 'gone' }), { status: 404 });
|
|
217
|
+
const build = vi.fn(async () => notFound);
|
|
218
|
+
const res = await cachedRead(anonRequest(), {
|
|
219
|
+
cache: { runtimeCache: { adapter } },
|
|
220
|
+
tags: ['actuate'],
|
|
221
|
+
wrap: jsonWrap,
|
|
222
|
+
build,
|
|
223
|
+
});
|
|
224
|
+
expect(res.status).toBe(404);
|
|
225
|
+
expect(adapter.calls.set).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
it('degrades to a fresh fetch when adapter.get throws', async () => {
|
|
228
|
+
const adapter = {
|
|
229
|
+
async get() {
|
|
230
|
+
throw new Error('redis down');
|
|
231
|
+
},
|
|
232
|
+
async set() {
|
|
233
|
+
/* unused */
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
237
|
+
const build = vi.fn(async () => ({ data: 'fresh' }));
|
|
238
|
+
const res = await cachedRead(anonRequest(), {
|
|
239
|
+
cache: { runtimeCache: { adapter } },
|
|
240
|
+
tags: ['actuate'],
|
|
241
|
+
wrap: jsonWrap,
|
|
242
|
+
build,
|
|
243
|
+
});
|
|
244
|
+
expect(build).toHaveBeenCalledTimes(1);
|
|
245
|
+
expect(await res.json()).toEqual({ data: 'fresh' });
|
|
246
|
+
expect(errSpy).toHaveBeenCalled();
|
|
247
|
+
errSpy.mockRestore();
|
|
248
|
+
});
|
|
249
|
+
it('swallows adapter.set errors and still returns the fresh result', async () => {
|
|
250
|
+
const adapter = {
|
|
251
|
+
async get() {
|
|
252
|
+
return undefined;
|
|
253
|
+
},
|
|
254
|
+
async set() {
|
|
255
|
+
throw new Error('write fail');
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
259
|
+
const res = await cachedRead(anonRequest(), {
|
|
260
|
+
cache: { runtimeCache: { adapter } },
|
|
261
|
+
tags: ['actuate'],
|
|
262
|
+
wrap: jsonWrap,
|
|
263
|
+
build: async () => ({ data: 'fresh' }),
|
|
264
|
+
});
|
|
265
|
+
expect(await res.json()).toEqual({ data: 'fresh' });
|
|
266
|
+
expect(res.headers.get('X-Actuate-Cache')).toBe('MISS');
|
|
267
|
+
expect(errSpy).toHaveBeenCalled();
|
|
268
|
+
errSpy.mockRestore();
|
|
269
|
+
});
|
|
270
|
+
it('skips storing payloads larger than ~1.5 MB to stay under the Vercel ceiling', async () => {
|
|
271
|
+
const adapter = makeAdapter();
|
|
272
|
+
// Build a > 1.5 MB payload — ~2 MB worth of repeated string.
|
|
273
|
+
const huge = { data: { body: 'x'.repeat(2_000_000) } };
|
|
274
|
+
await cachedRead(anonRequest(), {
|
|
275
|
+
cache: { runtimeCache: { adapter } },
|
|
276
|
+
tags: ['actuate'],
|
|
277
|
+
wrap: jsonWrap,
|
|
278
|
+
build: async () => huge,
|
|
279
|
+
});
|
|
280
|
+
expect(adapter.calls.set).toEqual([]);
|
|
281
|
+
// ... but the response still returns the fresh data
|
|
282
|
+
expect(adapter.store.size).toBe(0);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
//# sourceMappingURL=http.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.test.js","sourceRoot":"","sources":["../../../src/cache/__tests__/http.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP / CDN cache primitives — the server-side mirror of the
|
|
3
|
+
* `actuateTags()` helper exposed by `@actuate-media/client`.
|
|
4
|
+
*
|
|
5
|
+
* These produce the canonical tag strings that:
|
|
6
|
+
*
|
|
7
|
+
* 1. The SDK emits on every GET it makes (see
|
|
8
|
+
* `packages/client/src/index.ts`), and
|
|
9
|
+
* 2. The action layer fires through `CacheConfig.onInvalidate` after every
|
|
10
|
+
* write so a Next.js host app can call `revalidateTag()` for each one.
|
|
11
|
+
*
|
|
12
|
+
* The cms-core in-process cache adapter (`./index.ts`) is a separate
|
|
13
|
+
* concern and uses its own key/tag taxonomy for in-process bookkeeping —
|
|
14
|
+
* the two layers never share tag strings.
|
|
15
|
+
*/
|
|
16
|
+
import type { CacheConfig } from '../config/types.js';
|
|
17
|
+
/** A single canonical tag for `revalidateTag()`-style invalidation. */
|
|
18
|
+
export type ActuateTagKind = 'collection' | 'doc' | 'global' | 'path';
|
|
19
|
+
/** The wholesale tag added to every SDK request — panic-button invalidation. */
|
|
20
|
+
export declare const ACTUATE_TAG_ROOT = "actuate";
|
|
21
|
+
/**
|
|
22
|
+
* Build a single canonical tag (e.g. `actuate:collection:posts`). Mirrors
|
|
23
|
+
* the SDK's `actuateTag()` so the server can construct invalidation keys
|
|
24
|
+
* that match what the client will actually invalidate.
|
|
25
|
+
*/
|
|
26
|
+
export declare function actuateTag(kind: ActuateTagKind, slug: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Tag set to invalidate after a write. Always includes the wholesale
|
|
29
|
+
* `actuate` tag plus the narrowest specific tags so consumers who only
|
|
30
|
+
* tagged broadly (or only tagged narrowly) both pick up the change.
|
|
31
|
+
*
|
|
32
|
+
* - `create` / `update` / `delete` on a doc → collection + doc tags.
|
|
33
|
+
* - `update` on a global → global tag.
|
|
34
|
+
*
|
|
35
|
+
* Paths (`actuate:path:<path>`) are intentionally NOT auto-invalidated
|
|
36
|
+
* here: the server cannot know which paths resolve to which doc without
|
|
37
|
+
* re-running the resolver, and a stale slug change is a rare-enough
|
|
38
|
+
* event that the host app should handle it in its own webhook.
|
|
39
|
+
*/
|
|
40
|
+
export declare function invalidationTagsForDocumentWrite(collection: string, documentId: string): string[];
|
|
41
|
+
export declare function invalidationTagsForGlobalWrite(slug: string): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Build the `Cache-Control` header value for a public read response.
|
|
44
|
+
*
|
|
45
|
+
* Rules:
|
|
46
|
+
*
|
|
47
|
+
* - Session-cookie authentication ALWAYS returns `private, no-store`.
|
|
48
|
+
* Shared CDNs MUST NOT cache per-user responses, full stop.
|
|
49
|
+
* - When `cache.publicReads.enabled` is false (the default), returns
|
|
50
|
+
* null — caller emits no `Cache-Control` header.
|
|
51
|
+
* - When enabled and the request is unauthenticated or uses an API
|
|
52
|
+
* key, returns
|
|
53
|
+
* `public, s-maxage=<maxAge>, stale-while-revalidate=<swr>`.
|
|
54
|
+
*
|
|
55
|
+
* @param request The inbound request — used to detect session cookies.
|
|
56
|
+
* @param cache `ActuateCMSConfig.cache` (may be undefined).
|
|
57
|
+
* @returns The `Cache-Control` header value, or null when none should be sent.
|
|
58
|
+
*/
|
|
59
|
+
export declare function publicReadCacheControl(request: Request, cache: CacheConfig | undefined): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Returns true when the request carries the Actuate session cookie. We
|
|
62
|
+
* deliberately check the cookie NAME, not its validity — an expired or
|
|
63
|
+
* tampered cookie is still associated with a single user agent, so
|
|
64
|
+
* treating it as "potentially-authenticated" and refusing shared-cache
|
|
65
|
+
* is the safe default.
|
|
66
|
+
*
|
|
67
|
+
* Exported so the runtime-cache layer can reuse the exact same bypass
|
|
68
|
+
* rule as the CDN-cache layer.
|
|
69
|
+
*/
|
|
70
|
+
export declare function hasSessionCookie(request: Request): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Build a stable, content-addressed cache key for a public read.
|
|
73
|
+
*
|
|
74
|
+
* - Path-only when the URL has no query (`/api/cms/collections/posts`).
|
|
75
|
+
* - Path + sorted query when present, so `?page=2&pageSize=20` keys the
|
|
76
|
+
* same way regardless of param order.
|
|
77
|
+
* - Hashing isn't done here — the runtime cache adapter is free to apply
|
|
78
|
+
* its own `keyHashFunction` if it cares about key length.
|
|
79
|
+
*
|
|
80
|
+
* Exported for tests + custom routes that want to opt into the same
|
|
81
|
+
* cache layer.
|
|
82
|
+
*/
|
|
83
|
+
export declare function cacheKeyFor(request: Request): string;
|
|
84
|
+
/**
|
|
85
|
+
* Run a read endpoint through the optional Runtime Cache layer.
|
|
86
|
+
*
|
|
87
|
+
* Contract:
|
|
88
|
+
* - When `cache.runtimeCache.adapter` is unset, OR the request carries
|
|
89
|
+
* a session cookie, OR the caller sent `Cache-Control: no-cache`,
|
|
90
|
+
* this helper just invokes `build()` and returns whatever it
|
|
91
|
+
* produces (either a `Response` for early-exit cases like 404, or
|
|
92
|
+
* plain JSON-able data that gets wrapped via `wrap()`).
|
|
93
|
+
* - On cache hit, the cached payload is returned via `wrap(hit, request)`
|
|
94
|
+
* so the same CDN-tier `Cache-Control` rules apply. Hits get
|
|
95
|
+
* `X-Actuate-Cache: HIT`; misses get `X-Actuate-Cache: MISS`.
|
|
96
|
+
* - On cache miss, `build()` runs. If it returns a `Response` (e.g.
|
|
97
|
+
* 404), that response is passed through untouched — we never cache
|
|
98
|
+
* error / not-found shapes. Otherwise the payload is stored under
|
|
99
|
+
* the cache key with the supplied tags and TTL.
|
|
100
|
+
* - Adapter errors (`get` or `set` throwing) are logged and swallowed.
|
|
101
|
+
* The user-visible request never fails because the cache fell over.
|
|
102
|
+
* - Payloads larger than ~1.5 MB are not stored — keeps us comfortably
|
|
103
|
+
* under the 2 MB Vercel runtime-cache item ceiling.
|
|
104
|
+
*/
|
|
105
|
+
export declare function cachedRead(request: Request, opts: {
|
|
106
|
+
/** Tag set associated with the cache entry. Must match the SDK + onInvalidate tags. */
|
|
107
|
+
tags: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Produces the payload on cache miss. May short-circuit by returning
|
|
110
|
+
* a `Response` (e.g. when emitting a 404); that response is never
|
|
111
|
+
* cached.
|
|
112
|
+
*/
|
|
113
|
+
build: () => Promise<Response | unknown>;
|
|
114
|
+
/** Wraps a plain payload into the final `Response`. Typically this calls `jsonWithCache`. */
|
|
115
|
+
wrap: (payload: unknown, request: Request) => Response;
|
|
116
|
+
/** Active CMS config. Passed in to keep this helper pure (no `globalThis`). */
|
|
117
|
+
cache: CacheConfig | undefined;
|
|
118
|
+
}): Promise<Response>;
|
|
119
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/cache/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAUrD,uEAAuE;AACvE,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;AAErE,gFAAgF;AAChF,eAAO,MAAM,gBAAgB,YAAY,CAAA;AAEzC;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gCAAgC,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjG;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAErE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,WAAW,GAAG,SAAS,GAC7B,MAAM,GAAG,IAAI,CAef;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAO1D;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAQpD;AAaD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE;IACJ,uFAAuF;IACvF,IAAI,EAAE,MAAM,EAAE,CAAA;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,CAAA;IACxC,6FAA6F;IAC7F,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAA;IACtD,+EAA+E;IAC/E,KAAK,EAAE,WAAW,GAAG,SAAS,CAAA;CAC/B,GACA,OAAO,CAAC,QAAQ,CAAC,CAgDnB"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approximate cap on a single runtime cache entry. Vercel's hard limit
|
|
3
|
+
* is 2 MB; we stop short so a slightly-larger-than-expected payload
|
|
4
|
+
* doesn't surface the SDK error path. Anything bigger than this falls
|
|
5
|
+
* through to a normal DB read on every request — slower, but correct.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_RUNTIME_CACHE_BYTES = 1_500_000;
|
|
8
|
+
/** The wholesale tag added to every SDK request — panic-button invalidation. */
|
|
9
|
+
export const ACTUATE_TAG_ROOT = 'actuate';
|
|
10
|
+
/**
|
|
11
|
+
* Build a single canonical tag (e.g. `actuate:collection:posts`). Mirrors
|
|
12
|
+
* the SDK's `actuateTag()` so the server can construct invalidation keys
|
|
13
|
+
* that match what the client will actually invalidate.
|
|
14
|
+
*/
|
|
15
|
+
export function actuateTag(kind, slug) {
|
|
16
|
+
return `${ACTUATE_TAG_ROOT}:${kind}:${slug}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Tag set to invalidate after a write. Always includes the wholesale
|
|
20
|
+
* `actuate` tag plus the narrowest specific tags so consumers who only
|
|
21
|
+
* tagged broadly (or only tagged narrowly) both pick up the change.
|
|
22
|
+
*
|
|
23
|
+
* - `create` / `update` / `delete` on a doc → collection + doc tags.
|
|
24
|
+
* - `update` on a global → global tag.
|
|
25
|
+
*
|
|
26
|
+
* Paths (`actuate:path:<path>`) are intentionally NOT auto-invalidated
|
|
27
|
+
* here: the server cannot know which paths resolve to which doc without
|
|
28
|
+
* re-running the resolver, and a stale slug change is a rare-enough
|
|
29
|
+
* event that the host app should handle it in its own webhook.
|
|
30
|
+
*/
|
|
31
|
+
export function invalidationTagsForDocumentWrite(collection, documentId) {
|
|
32
|
+
return [ACTUATE_TAG_ROOT, actuateTag('collection', collection), actuateTag('doc', documentId)];
|
|
33
|
+
}
|
|
34
|
+
export function invalidationTagsForGlobalWrite(slug) {
|
|
35
|
+
return [ACTUATE_TAG_ROOT, actuateTag('global', slug)];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the `Cache-Control` header value for a public read response.
|
|
39
|
+
*
|
|
40
|
+
* Rules:
|
|
41
|
+
*
|
|
42
|
+
* - Session-cookie authentication ALWAYS returns `private, no-store`.
|
|
43
|
+
* Shared CDNs MUST NOT cache per-user responses, full stop.
|
|
44
|
+
* - When `cache.publicReads.enabled` is false (the default), returns
|
|
45
|
+
* null — caller emits no `Cache-Control` header.
|
|
46
|
+
* - When enabled and the request is unauthenticated or uses an API
|
|
47
|
+
* key, returns
|
|
48
|
+
* `public, s-maxage=<maxAge>, stale-while-revalidate=<swr>`.
|
|
49
|
+
*
|
|
50
|
+
* @param request The inbound request — used to detect session cookies.
|
|
51
|
+
* @param cache `ActuateCMSConfig.cache` (may be undefined).
|
|
52
|
+
* @returns The `Cache-Control` header value, or null when none should be sent.
|
|
53
|
+
*/
|
|
54
|
+
export function publicReadCacheControl(request, cache) {
|
|
55
|
+
// Session-cookie auth always opts out of shared caching. This is
|
|
56
|
+
// non-negotiable: emitting `public` on a session-authed response
|
|
57
|
+
// could leak admin views to other users on the same CDN tier.
|
|
58
|
+
if (hasSessionCookie(request)) {
|
|
59
|
+
return 'private, no-store';
|
|
60
|
+
}
|
|
61
|
+
if (!cache?.publicReads?.enabled)
|
|
62
|
+
return null;
|
|
63
|
+
const maxAge = cache.publicReads.maxAge ?? 60;
|
|
64
|
+
const swr = cache.publicReads.staleWhileRevalidate ?? 300;
|
|
65
|
+
// Clamp negative values to 0 so a misconfigured value doesn't produce
|
|
66
|
+
// a header the CDN treats as undefined behaviour.
|
|
67
|
+
const safeMaxAge = Math.max(0, Math.floor(maxAge));
|
|
68
|
+
const safeSwr = Math.max(0, Math.floor(swr));
|
|
69
|
+
return `public, s-maxage=${safeMaxAge}, stale-while-revalidate=${safeSwr}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns true when the request carries the Actuate session cookie. We
|
|
73
|
+
* deliberately check the cookie NAME, not its validity — an expired or
|
|
74
|
+
* tampered cookie is still associated with a single user agent, so
|
|
75
|
+
* treating it as "potentially-authenticated" and refusing shared-cache
|
|
76
|
+
* is the safe default.
|
|
77
|
+
*
|
|
78
|
+
* Exported so the runtime-cache layer can reuse the exact same bypass
|
|
79
|
+
* rule as the CDN-cache layer.
|
|
80
|
+
*/
|
|
81
|
+
export function hasSessionCookie(request) {
|
|
82
|
+
const cookieHeader = request.headers.get('cookie');
|
|
83
|
+
if (!cookieHeader)
|
|
84
|
+
return false;
|
|
85
|
+
// Match `actuate_session=` as a discrete cookie name, not anywhere in
|
|
86
|
+
// the header text — defense-in-depth against a cookie whose VALUE
|
|
87
|
+
// happens to contain the substring.
|
|
88
|
+
return /(?:^|;\s*)actuate_session=/.test(cookieHeader);
|
|
89
|
+
}
|
|
90
|
+
// ─── Runtime cache (layer 3) ────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Build a stable, content-addressed cache key for a public read.
|
|
93
|
+
*
|
|
94
|
+
* - Path-only when the URL has no query (`/api/cms/collections/posts`).
|
|
95
|
+
* - Path + sorted query when present, so `?page=2&pageSize=20` keys the
|
|
96
|
+
* same way regardless of param order.
|
|
97
|
+
* - Hashing isn't done here — the runtime cache adapter is free to apply
|
|
98
|
+
* its own `keyHashFunction` if it cares about key length.
|
|
99
|
+
*
|
|
100
|
+
* Exported for tests + custom routes that want to opt into the same
|
|
101
|
+
* cache layer.
|
|
102
|
+
*/
|
|
103
|
+
export function cacheKeyFor(request) {
|
|
104
|
+
const url = new URL(request.url);
|
|
105
|
+
const params = Array.from(url.searchParams.entries()).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
106
|
+
if (params.length === 0)
|
|
107
|
+
return url.pathname;
|
|
108
|
+
const sorted = new URLSearchParams(params).toString();
|
|
109
|
+
return `${url.pathname}?${sorted}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Returns true when the caller passed `Cache-Control: no-cache` /
|
|
113
|
+
* `no-store` on the request. Lets debugging tools force a cache miss
|
|
114
|
+
* without flushing the entire layer.
|
|
115
|
+
*/
|
|
116
|
+
function requestOptedOut(request) {
|
|
117
|
+
const cc = request.headers.get('cache-control');
|
|
118
|
+
if (!cc)
|
|
119
|
+
return false;
|
|
120
|
+
return /(?:^|,\s*)(no-cache|no-store)\b/i.test(cc);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run a read endpoint through the optional Runtime Cache layer.
|
|
124
|
+
*
|
|
125
|
+
* Contract:
|
|
126
|
+
* - When `cache.runtimeCache.adapter` is unset, OR the request carries
|
|
127
|
+
* a session cookie, OR the caller sent `Cache-Control: no-cache`,
|
|
128
|
+
* this helper just invokes `build()` and returns whatever it
|
|
129
|
+
* produces (either a `Response` for early-exit cases like 404, or
|
|
130
|
+
* plain JSON-able data that gets wrapped via `wrap()`).
|
|
131
|
+
* - On cache hit, the cached payload is returned via `wrap(hit, request)`
|
|
132
|
+
* so the same CDN-tier `Cache-Control` rules apply. Hits get
|
|
133
|
+
* `X-Actuate-Cache: HIT`; misses get `X-Actuate-Cache: MISS`.
|
|
134
|
+
* - On cache miss, `build()` runs. If it returns a `Response` (e.g.
|
|
135
|
+
* 404), that response is passed through untouched — we never cache
|
|
136
|
+
* error / not-found shapes. Otherwise the payload is stored under
|
|
137
|
+
* the cache key with the supplied tags and TTL.
|
|
138
|
+
* - Adapter errors (`get` or `set` throwing) are logged and swallowed.
|
|
139
|
+
* The user-visible request never fails because the cache fell over.
|
|
140
|
+
* - Payloads larger than ~1.5 MB are not stored — keeps us comfortably
|
|
141
|
+
* under the 2 MB Vercel runtime-cache item ceiling.
|
|
142
|
+
*/
|
|
143
|
+
export async function cachedRead(request, opts) {
|
|
144
|
+
const adapter = opts.cache?.runtimeCache?.adapter;
|
|
145
|
+
const ttl = opts.cache?.runtimeCache?.ttl ?? 60;
|
|
146
|
+
// Bypass paths — no adapter, per-user request, or explicit opt-out.
|
|
147
|
+
if (!adapter || hasSessionCookie(request) || requestOptedOut(request)) {
|
|
148
|
+
const r = await opts.build();
|
|
149
|
+
return r instanceof Response ? r : opts.wrap(r, request);
|
|
150
|
+
}
|
|
151
|
+
const key = cacheKeyFor(request);
|
|
152
|
+
let hit;
|
|
153
|
+
try {
|
|
154
|
+
hit = await adapter.get(key);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
// Read failures never break the request — fall through to a fresh fetch.
|
|
158
|
+
console.error('[actuate][cache] runtime cache get failed', err);
|
|
159
|
+
const r = await opts.build();
|
|
160
|
+
return r instanceof Response ? r : opts.wrap(r, request);
|
|
161
|
+
}
|
|
162
|
+
if (hit !== undefined && hit !== null) {
|
|
163
|
+
const res = opts.wrap(hit, request);
|
|
164
|
+
res.headers.set('X-Actuate-Cache', 'HIT');
|
|
165
|
+
return res;
|
|
166
|
+
}
|
|
167
|
+
const r = await opts.build();
|
|
168
|
+
if (r instanceof Response)
|
|
169
|
+
return r; // 404 / error — never cache.
|
|
170
|
+
try {
|
|
171
|
+
// Approximate size by serialised length; the runtime cache adapter
|
|
172
|
+
// will JSON-stringify again on its way out, so the work isn't
|
|
173
|
+
// duplicated wastefully — and a known-too-big payload skips the
|
|
174
|
+
// write call entirely.
|
|
175
|
+
const size = JSON.stringify(r)?.length ?? 0;
|
|
176
|
+
if (size > 0 && size <= MAX_RUNTIME_CACHE_BYTES) {
|
|
177
|
+
await adapter.set(key, r, { ttl, tags: opts.tags });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
// Write failures never break the request either — log and move on.
|
|
182
|
+
console.error('[actuate][cache] runtime cache set failed', err);
|
|
183
|
+
}
|
|
184
|
+
const res = opts.wrap(r, request);
|
|
185
|
+
res.headers.set('X-Actuate-Cache', 'MISS');
|
|
186
|
+
return res;
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/cache/http.ts"],"names":[],"mappings":"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"}
|