@prezly/theme-kit-nextjs 10.9.3 → 10.10.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/README.md +24 -2
- package/build/adapters/prezly/cache/configure.cjs +2 -1
- package/build/adapters/prezly/cache/configure.d.ts +3 -0
- package/build/adapters/prezly/cache/configure.mjs +2 -1
- package/build/adapters/prezly/cache/redis.cjs +16 -6
- package/build/adapters/prezly/cache/redis.mjs +16 -6
- package/build/adapters/prezly/cache/redis.test.cjs +70 -9
- package/build/adapters/prezly/cache/redis.test.mjs +70 -9
- package/package.json +6 -10
package/README.md
CHANGED
|
@@ -413,8 +413,30 @@ new retained key namespaces for every token or invalidation version. This create
|
|
|
413
413
|
a one-time cold cache on upgrade. Roll out gradually and warm representative
|
|
414
414
|
newsrooms before an expected traffic spike. Existing unscoped entries remain
|
|
415
415
|
subject to their configured expiration; do not flush a Redis instance that also
|
|
416
|
-
contains routing data. TTL
|
|
417
|
-
|
|
416
|
+
contains routing data. TTL and query canonicalization are separate policies and
|
|
417
|
+
are unchanged here.
|
|
418
|
+
|
|
419
|
+
### Not-found results
|
|
420
|
+
|
|
421
|
+
`contentDelivery.story()` and `contentDelivery.gallery()` resolve `null` when the
|
|
422
|
+
API answers 403, 404 or 410. A `null` result is a cache hit like any other value:
|
|
423
|
+
only an absent entry falls back to the API, so `false` and `0` are also valid
|
|
424
|
+
cached values. Not-found entries are kept for `negativeTtl` seconds (default 60,
|
|
425
|
+
`ContentDelivery.DEFAULT_NEGATIVE_TTL`). The deadline is written into the cached
|
|
426
|
+
envelope and checked on every read, so a cache layer that ignores the `ttl`
|
|
427
|
+
hint, or a `null` entry written by an older Theme Kit, is a miss rather than a
|
|
428
|
+
stale hit. Memory and Redis also expire these entries on their own, and Redis
|
|
429
|
+
does not renew that lifetime on read. A cache version change still invalidates
|
|
430
|
+
them at once, so publishing a story makes it visible immediately; a story that
|
|
431
|
+
becomes available without a version change appears within the TTL. Errors such
|
|
432
|
+
as 401, 429, 5xx and transport failures throw and are never stored. `undefined`
|
|
433
|
+
results are not stored either.
|
|
434
|
+
|
|
435
|
+
Set `cache.negativeTtl` in `PrezlyAdapter.connect` to change the retention. It
|
|
436
|
+
must be a positive integer number of seconds. Not-found caching needs the
|
|
437
|
+
source/authorization scope described above; a client created with a custom
|
|
438
|
+
`fetch` and no `requestScope` keeps treating `null` as a miss and does not
|
|
439
|
+
store it.
|
|
418
440
|
|
|
419
441
|
### Verification and release
|
|
420
442
|
|
|
@@ -19,7 +19,8 @@ function configure(config) {
|
|
|
19
19
|
if (storage) {
|
|
20
20
|
return {
|
|
21
21
|
storage: config.namespace ? storage.namespace(config.namespace) : storage,
|
|
22
|
-
latestVersion: _themeKitCore.Resolvable.resolve(config.latestVersion)
|
|
22
|
+
latestVersion: _themeKitCore.Resolvable.resolve(config.latestVersion),
|
|
23
|
+
negativeTtl: config.negativeTtl
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
return undefined;
|
|
@@ -10,9 +10,12 @@ export interface Configuration {
|
|
|
10
10
|
namespace?: string;
|
|
11
11
|
/** Extra immutable identity for custom fetch implementations. Enables request sharing. */
|
|
12
12
|
requestScope?: string;
|
|
13
|
+
/** Retention in seconds for not-found results. See `ContentDelivery.DEFAULT_NEGATIVE_TTL`. */
|
|
14
|
+
negativeTtl?: number;
|
|
13
15
|
telemetry?: ContentDelivery.Telemetry;
|
|
14
16
|
}
|
|
15
17
|
export declare function configure(config: Configuration): {
|
|
16
18
|
storage: ContentDelivery.Cache;
|
|
17
19
|
latestVersion: number;
|
|
20
|
+
negativeTtl: number | undefined;
|
|
18
21
|
} | undefined;
|
|
@@ -13,7 +13,8 @@ export function configure(config) {
|
|
|
13
13
|
if (storage) {
|
|
14
14
|
return {
|
|
15
15
|
storage: config.namespace ? storage.namespace(config.namespace) : storage,
|
|
16
|
-
latestVersion: Resolvable.resolve(config.latestVersion)
|
|
16
|
+
latestVersion: Resolvable.resolve(config.latestVersion),
|
|
17
|
+
negativeTtl: config.negativeTtl
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
return undefined;
|
|
@@ -89,14 +89,16 @@ function createRedisCache(_ref) {
|
|
|
89
89
|
if (!cached) return undefined;
|
|
90
90
|
var entry = JSON.parse(cached);
|
|
91
91
|
if (entry.version < latestVersion) return undefined;
|
|
92
|
-
|
|
92
|
+
// Sliding expiry applies to regular content only. An entry with its
|
|
93
|
+
// own short retention must not be renewed to the default lifetime.
|
|
94
|
+
if (ttl && entry.ttl === undefined) {
|
|
93
95
|
void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
|
|
94
96
|
}
|
|
95
97
|
if (onSource) _themeKitCore.ContentDelivery.notify(onSource, 'redis');
|
|
96
98
|
return entry.value;
|
|
97
99
|
})();
|
|
98
100
|
},
|
|
99
|
-
set(key, value, version) {
|
|
101
|
+
set(key, value, version, options) {
|
|
100
102
|
return _asyncToGenerator(function* () {
|
|
101
103
|
if (!connection.isReady) {
|
|
102
104
|
_themeKitCore.ContentDelivery.emit(telemetry, {
|
|
@@ -105,13 +107,21 @@ function createRedisCache(_ref) {
|
|
|
105
107
|
});
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
|
-
|
|
110
|
+
// SET EX takes whole seconds; round a fractional hint up, never down.
|
|
111
|
+
var entry = (options === null || options === void 0 ? void 0 : options.ttl) === undefined ? {
|
|
109
112
|
value,
|
|
110
113
|
version
|
|
114
|
+
} : {
|
|
115
|
+
value,
|
|
116
|
+
version,
|
|
117
|
+
ttl: Math.max(1, Math.ceil(options.ttl))
|
|
111
118
|
};
|
|
112
|
-
yield command(() =>
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
yield command(() => {
|
|
120
|
+
var _entry$ttl;
|
|
121
|
+
return connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
|
|
122
|
+
EX: (_entry$ttl = entry.ttl) !== null && _entry$ttl !== void 0 ? _entry$ttl : ttl
|
|
123
|
+
});
|
|
124
|
+
}, telemetry, 'set');
|
|
115
125
|
})();
|
|
116
126
|
},
|
|
117
127
|
namespace(namespace) {
|
|
@@ -82,14 +82,16 @@ export function createRedisCache(_ref) {
|
|
|
82
82
|
if (!cached) return undefined;
|
|
83
83
|
var entry = JSON.parse(cached);
|
|
84
84
|
if (entry.version < latestVersion) return undefined;
|
|
85
|
-
|
|
85
|
+
// Sliding expiry applies to regular content only. An entry with its
|
|
86
|
+
// own short retention must not be renewed to the default lifetime.
|
|
87
|
+
if (ttl && entry.ttl === undefined) {
|
|
86
88
|
void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
|
|
87
89
|
}
|
|
88
90
|
if (onSource) ContentDelivery.notify(onSource, 'redis');
|
|
89
91
|
return entry.value;
|
|
90
92
|
})();
|
|
91
93
|
},
|
|
92
|
-
set(key, value, version) {
|
|
94
|
+
set(key, value, version, options) {
|
|
93
95
|
return _asyncToGenerator(function* () {
|
|
94
96
|
if (!connection.isReady) {
|
|
95
97
|
ContentDelivery.emit(telemetry, {
|
|
@@ -98,13 +100,21 @@ export function createRedisCache(_ref) {
|
|
|
98
100
|
});
|
|
99
101
|
return;
|
|
100
102
|
}
|
|
101
|
-
|
|
103
|
+
// SET EX takes whole seconds; round a fractional hint up, never down.
|
|
104
|
+
var entry = (options === null || options === void 0 ? void 0 : options.ttl) === undefined ? {
|
|
102
105
|
value,
|
|
103
106
|
version
|
|
107
|
+
} : {
|
|
108
|
+
value,
|
|
109
|
+
version,
|
|
110
|
+
ttl: Math.max(1, Math.ceil(options.ttl))
|
|
104
111
|
};
|
|
105
|
-
yield command(() =>
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
yield command(() => {
|
|
113
|
+
var _entry$ttl;
|
|
114
|
+
return connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
|
|
115
|
+
EX: (_entry$ttl = entry.ttl) !== null && _entry$ttl !== void 0 ? _entry$ttl : ttl
|
|
116
|
+
});
|
|
117
|
+
}, telemetry, 'set');
|
|
108
118
|
})();
|
|
109
119
|
},
|
|
110
120
|
namespace(namespace) {
|
|
@@ -83,12 +83,73 @@ it('preserves namespace, version and sliding expiry and observes expiry rejectio
|
|
|
83
83
|
EX: 300
|
|
84
84
|
});
|
|
85
85
|
}));
|
|
86
|
+
it('stores a short-lived entry with its own expiry and never renews it', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
87
|
+
var _connection3 = connection(),
|
|
88
|
+
client = _connection3.client,
|
|
89
|
+
cache = _connection3.cache;
|
|
90
|
+
yield cache.set('missing', null, 4, {
|
|
91
|
+
ttl: 60
|
|
92
|
+
});
|
|
93
|
+
expect(client.set).toHaveBeenLastCalledWith('test:missing', JSON.stringify({
|
|
94
|
+
value: null,
|
|
95
|
+
version: 4,
|
|
96
|
+
ttl: 60
|
|
97
|
+
}), {
|
|
98
|
+
EX: 60
|
|
99
|
+
});
|
|
100
|
+
client.get.mockResolvedValueOnce(JSON.stringify({
|
|
101
|
+
value: null,
|
|
102
|
+
version: 4,
|
|
103
|
+
ttl: 60
|
|
104
|
+
}));
|
|
105
|
+
var onSource = jest.fn();
|
|
106
|
+
expect(yield cache.get('missing', 4, onSource)).toBeNull();
|
|
107
|
+
expect(onSource).toHaveBeenCalledWith('redis');
|
|
108
|
+
expect(client.expire).not.toHaveBeenCalled();
|
|
109
|
+
client.get.mockResolvedValueOnce(JSON.stringify({
|
|
110
|
+
value: null,
|
|
111
|
+
version: 4,
|
|
112
|
+
ttl: 60
|
|
113
|
+
}));
|
|
114
|
+
expect(yield cache.get('missing', 5)).toBeUndefined();
|
|
115
|
+
}));
|
|
116
|
+
it('rounds a fractional ttl hint up to whole seconds for SET EX', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
117
|
+
var _connection4 = connection(),
|
|
118
|
+
client = _connection4.client,
|
|
119
|
+
cache = _connection4.cache;
|
|
120
|
+
yield cache.set('short', null, 1, {
|
|
121
|
+
ttl: 0.5
|
|
122
|
+
});
|
|
123
|
+
expect(client.set).toHaveBeenLastCalledWith('test:short', JSON.stringify({
|
|
124
|
+
value: null,
|
|
125
|
+
version: 1,
|
|
126
|
+
ttl: 1
|
|
127
|
+
}), {
|
|
128
|
+
EX: 1
|
|
129
|
+
});
|
|
130
|
+
}));
|
|
131
|
+
it.each([false, 0, ''])('returns a stored %p as a hit', /*#__PURE__*/function () {
|
|
132
|
+
var _ref8 = _asyncToGenerator(function* (value) {
|
|
133
|
+
var _connection5 = connection(),
|
|
134
|
+
client = _connection5.client,
|
|
135
|
+
cache = _connection5.cache;
|
|
136
|
+
client.get.mockResolvedValueOnce(JSON.stringify({
|
|
137
|
+
value,
|
|
138
|
+
version: 2
|
|
139
|
+
}));
|
|
140
|
+
expect(yield cache.get('key', 2)).toBe(value);
|
|
141
|
+
expect(client.expire).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
return function (_x) {
|
|
144
|
+
return _ref8.apply(this, arguments);
|
|
145
|
+
};
|
|
146
|
+
}());
|
|
86
147
|
it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellation and recovers', /*#__PURE__*/function () {
|
|
87
|
-
var
|
|
148
|
+
var _ref9 = _asyncToGenerator(function* (method) {
|
|
88
149
|
jest.useFakeTimers();
|
|
89
|
-
var
|
|
90
|
-
client =
|
|
91
|
-
cache =
|
|
150
|
+
var _connection6 = connection(),
|
|
151
|
+
client = _connection6.client,
|
|
152
|
+
cache = _connection6.cache;
|
|
92
153
|
var rejectOperation;
|
|
93
154
|
client[method].mockImplementationOnce(() => new Promise((_, reject) => {
|
|
94
155
|
rejectOperation = reject;
|
|
@@ -102,14 +163,14 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
|
|
|
102
163
|
yield jest.advanceTimersByTimeAsync(0);
|
|
103
164
|
expect(jest.getTimerCount()).toBe(0);
|
|
104
165
|
});
|
|
105
|
-
return function (
|
|
106
|
-
return
|
|
166
|
+
return function (_x2) {
|
|
167
|
+
return _ref9.apply(this, arguments);
|
|
107
168
|
};
|
|
108
169
|
}());
|
|
109
170
|
it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
110
|
-
var
|
|
111
|
-
client =
|
|
112
|
-
options =
|
|
171
|
+
var _connection7 = connection(false),
|
|
172
|
+
client = _connection7.client,
|
|
173
|
+
options = _connection7.options;
|
|
113
174
|
var events = [];
|
|
114
175
|
var observe = event => {
|
|
115
176
|
events.push(event);
|
|
@@ -81,12 +81,73 @@ it('preserves namespace, version and sliding expiry and observes expiry rejectio
|
|
|
81
81
|
EX: 300
|
|
82
82
|
});
|
|
83
83
|
}));
|
|
84
|
+
it('stores a short-lived entry with its own expiry and never renews it', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
85
|
+
var _connection3 = connection(),
|
|
86
|
+
client = _connection3.client,
|
|
87
|
+
cache = _connection3.cache;
|
|
88
|
+
yield cache.set('missing', null, 4, {
|
|
89
|
+
ttl: 60
|
|
90
|
+
});
|
|
91
|
+
expect(client.set).toHaveBeenLastCalledWith('test:missing', JSON.stringify({
|
|
92
|
+
value: null,
|
|
93
|
+
version: 4,
|
|
94
|
+
ttl: 60
|
|
95
|
+
}), {
|
|
96
|
+
EX: 60
|
|
97
|
+
});
|
|
98
|
+
client.get.mockResolvedValueOnce(JSON.stringify({
|
|
99
|
+
value: null,
|
|
100
|
+
version: 4,
|
|
101
|
+
ttl: 60
|
|
102
|
+
}));
|
|
103
|
+
var onSource = jest.fn();
|
|
104
|
+
expect(yield cache.get('missing', 4, onSource)).toBeNull();
|
|
105
|
+
expect(onSource).toHaveBeenCalledWith('redis');
|
|
106
|
+
expect(client.expire).not.toHaveBeenCalled();
|
|
107
|
+
client.get.mockResolvedValueOnce(JSON.stringify({
|
|
108
|
+
value: null,
|
|
109
|
+
version: 4,
|
|
110
|
+
ttl: 60
|
|
111
|
+
}));
|
|
112
|
+
expect(yield cache.get('missing', 5)).toBeUndefined();
|
|
113
|
+
}));
|
|
114
|
+
it('rounds a fractional ttl hint up to whole seconds for SET EX', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
115
|
+
var _connection4 = connection(),
|
|
116
|
+
client = _connection4.client,
|
|
117
|
+
cache = _connection4.cache;
|
|
118
|
+
yield cache.set('short', null, 1, {
|
|
119
|
+
ttl: 0.5
|
|
120
|
+
});
|
|
121
|
+
expect(client.set).toHaveBeenLastCalledWith('test:short', JSON.stringify({
|
|
122
|
+
value: null,
|
|
123
|
+
version: 1,
|
|
124
|
+
ttl: 1
|
|
125
|
+
}), {
|
|
126
|
+
EX: 1
|
|
127
|
+
});
|
|
128
|
+
}));
|
|
129
|
+
it.each([false, 0, ''])('returns a stored %p as a hit', /*#__PURE__*/function () {
|
|
130
|
+
var _ref8 = _asyncToGenerator(function* (value) {
|
|
131
|
+
var _connection5 = connection(),
|
|
132
|
+
client = _connection5.client,
|
|
133
|
+
cache = _connection5.cache;
|
|
134
|
+
client.get.mockResolvedValueOnce(JSON.stringify({
|
|
135
|
+
value,
|
|
136
|
+
version: 2
|
|
137
|
+
}));
|
|
138
|
+
expect(yield cache.get('key', 2)).toBe(value);
|
|
139
|
+
expect(client.expire).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
return function (_x) {
|
|
142
|
+
return _ref8.apply(this, arguments);
|
|
143
|
+
};
|
|
144
|
+
}());
|
|
84
145
|
it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellation and recovers', /*#__PURE__*/function () {
|
|
85
|
-
var
|
|
146
|
+
var _ref9 = _asyncToGenerator(function* (method) {
|
|
86
147
|
jest.useFakeTimers();
|
|
87
|
-
var
|
|
88
|
-
client =
|
|
89
|
-
cache =
|
|
148
|
+
var _connection6 = connection(),
|
|
149
|
+
client = _connection6.client,
|
|
150
|
+
cache = _connection6.cache;
|
|
90
151
|
var rejectOperation;
|
|
91
152
|
client[method].mockImplementationOnce(() => new Promise((_, reject) => {
|
|
92
153
|
rejectOperation = reject;
|
|
@@ -100,14 +161,14 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
|
|
|
100
161
|
yield jest.advanceTimersByTimeAsync(0);
|
|
101
162
|
expect(jest.getTimerCount()).toBe(0);
|
|
102
163
|
});
|
|
103
|
-
return function (
|
|
104
|
-
return
|
|
164
|
+
return function (_x2) {
|
|
165
|
+
return _ref9.apply(this, arguments);
|
|
105
166
|
};
|
|
106
167
|
}());
|
|
107
168
|
it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
108
|
-
var
|
|
109
|
-
client =
|
|
110
|
-
options =
|
|
169
|
+
var _connection7 = connection(false),
|
|
170
|
+
client = _connection7.client,
|
|
171
|
+
options = _connection7.options;
|
|
111
172
|
var events = [];
|
|
112
173
|
var observe = event => {
|
|
113
174
|
events.push(event);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prezly/theme-kit-nextjs",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.10.1",
|
|
4
4
|
"description": "Data layer and utility library for developing Prezly themes with NextJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.mjs",
|
|
@@ -45,10 +45,6 @@
|
|
|
45
45
|
"node": ">= 18.18.0",
|
|
46
46
|
"npm": ">= 8.x"
|
|
47
47
|
},
|
|
48
|
-
"devEngines": {
|
|
49
|
-
"node": ">= 18.18.0",
|
|
50
|
-
"npm": ">= 8.x"
|
|
51
|
-
},
|
|
52
48
|
"repository": {
|
|
53
49
|
"type": "git",
|
|
54
50
|
"url": "git+https://github.com/prezly/theme-kit-js.git",
|
|
@@ -78,14 +74,14 @@
|
|
|
78
74
|
},
|
|
79
75
|
"dependencies": {
|
|
80
76
|
"@noble/hashes": "1.8.0",
|
|
81
|
-
"@prezly/theme-kit-core": "^10.9.3",
|
|
82
|
-
"@prezly/theme-kit-intl": "^10.9.1",
|
|
83
|
-
"@prezly/theme-kit-react": "^10.9.1",
|
|
84
77
|
"@technically/is-not-undefined": "^1.0.0",
|
|
85
78
|
"@technically/omit-undefined": "^1.0.4",
|
|
86
79
|
"json-stable-stringify": "^1.2.1",
|
|
87
80
|
"redis": "^4.7.0",
|
|
88
|
-
"url-pattern": "^1.0.3"
|
|
81
|
+
"url-pattern": "^1.0.3",
|
|
82
|
+
"@prezly/theme-kit-core": "^10.10.1",
|
|
83
|
+
"@prezly/theme-kit-react": "^10.9.1",
|
|
84
|
+
"@prezly/theme-kit-intl": "^10.9.1"
|
|
89
85
|
},
|
|
90
86
|
"devDependencies": {
|
|
91
87
|
"@sentry/nextjs": "7.120.4"
|
|
@@ -94,7 +90,7 @@
|
|
|
94
90
|
"access": "public"
|
|
95
91
|
},
|
|
96
92
|
"sideEffects": false,
|
|
97
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "51b1ce99cf8ce2c886001077bcaf95cb143f86ce",
|
|
98
94
|
"scripts": {
|
|
99
95
|
"prebuild": "rimraf --glob build/* *.tsbuildinfo",
|
|
100
96
|
"build": "pnpm build:types && pnpm build:esm && pnpm build:cjs",
|