@prezly/theme-kit-nextjs 10.9.2 → 10.10.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/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, negative-cache classification and query
417
- canonicalization are separate policies and are unchanged here.
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,15 +19,18 @@ 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;
26
27
  }
27
28
  function configureStorage(config) {
28
- var caches = [config.memory ? _themeKitCore.ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? (0, _redis.createRedisCache)(_objectSpread({
29
+ var caches = [config.memory ? _themeKitCore.ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? (0, _redis.createRedisCache)(_objectSpread(_objectSpread({
29
30
  ttl: DEFAULT_REDIS_CACHE_TTL
30
- }, config.redis)) : undefined].filter(_isNotUndefined.isNotUndefined);
31
+ }, config.redis), {}, {
32
+ telemetry: config.telemetry
33
+ })) : undefined].filter(_isNotUndefined.isNotUndefined);
31
34
  if (caches.length === 0) {
32
35
  return undefined;
33
36
  }
@@ -10,8 +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;
15
+ telemetry?: ContentDelivery.Telemetry;
13
16
  }
14
17
  export declare function configure(config: Configuration): {
15
18
  storage: ContentDelivery.Cache;
16
19
  latestVersion: number;
20
+ negativeTtl: number | undefined;
17
21
  } | undefined;
@@ -13,15 +13,18 @@ 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;
20
21
  }
21
22
  function configureStorage(config) {
22
- var caches = [config.memory ? ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? createRedisCache(_objectSpread({
23
+ var caches = [config.memory ? ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? createRedisCache(_objectSpread(_objectSpread({
23
24
  ttl: DEFAULT_REDIS_CACHE_TTL
24
- }, config.redis)) : undefined].filter(isNotUndefined);
25
+ }, config.redis), {}, {
26
+ telemetry: config.telemetry
27
+ })) : undefined].filter(isNotUndefined);
25
28
  if (caches.length === 0) {
26
29
  return undefined;
27
30
  }
@@ -4,9 +4,10 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.createRedisCache = createRedisCache;
7
+ var _themeKitCore = require("@prezly/theme-kit-core");
7
8
  var _jsonStableStringify = _interopRequireDefault(require("json-stable-stringify"));
8
9
  var _redis = require("redis");
9
- var _excluded = ["ttl", "prefix"];
10
+ var _excluded = ["ttl", "prefix", "telemetry"];
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
12
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
12
13
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -19,21 +20,32 @@ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.
19
20
  function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
20
21
  var COMMAND_TIMEOUT = 1000;
21
22
  var CONNECTIONS = new Map();
22
- function command(_x) {
23
+ function command(_x, _x2) {
23
24
  return _command.apply(this, arguments);
24
25
  }
25
26
  function _command() {
26
- _command = _asyncToGenerator(function* (invoke) {
27
+ _command = _asyncToGenerator(function* (invoke, telemetry) {
28
+ var operation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'get';
27
29
  var timer;
30
+ var start = telemetry ? _themeKitCore.ContentDelivery.telemetryNow() : 0;
31
+ var outcome = 'error';
28
32
  try {
29
33
  // node-redis 4 can corrupt its command queue when aborting a command
30
34
  // already sent to the socket. Leave that operation tracked by the client;
31
35
  // commandsQueueMaxLength bounds all outstanding replies, including these.
32
- return yield Promise.race([Promise.resolve().then(invoke), new Promise((_, reject) => {
36
+ var value = yield Promise.race([Promise.resolve().then(invoke), new Promise((_, reject) => {
33
37
  timer = setTimeout(() => reject(new Error('Content cache command timed out.')), COMMAND_TIMEOUT);
34
38
  })]);
39
+ outcome = 'success';
40
+ return value;
35
41
  } finally {
36
42
  clearTimeout(timer);
43
+ _themeKitCore.ContentDelivery.emit(telemetry, {
44
+ type: 'redis_command',
45
+ command: operation,
46
+ outcome,
47
+ seconds: telemetry ? _themeKitCore.ContentDelivery.elapsedSeconds(start) : 0
48
+ });
37
49
  }
38
50
  });
39
51
  return _command.apply(this, arguments);
@@ -42,6 +54,7 @@ function createRedisCache(_ref) {
42
54
  var ttl = _ref.ttl,
43
55
  _ref$prefix = _ref.prefix,
44
56
  prefix = _ref$prefix === void 0 ? '' : _ref$prefix,
57
+ telemetry = _ref.telemetry,
45
58
  options = _objectWithoutProperties(_ref, _excluded);
46
59
  var connectionKey = (0, _jsonStableStringify.default)(options);
47
60
  var client = CONNECTIONS.get(connectionKey);
@@ -63,29 +76,52 @@ function createRedisCache(_ref) {
63
76
  function createCache() {
64
77
  var namespacePrefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
65
78
  return {
66
- get(key, latestVersion) {
79
+ get(key, latestVersion, onSource) {
67
80
  return _asyncToGenerator(function* () {
68
- if (!connection.isReady) return undefined;
69
- var cached = yield command(() => connection.get("".concat(namespacePrefix).concat(key)));
81
+ if (!connection.isReady) {
82
+ _themeKitCore.ContentDelivery.emit(telemetry, {
83
+ type: 'redis_unavailable',
84
+ command: 'get'
85
+ });
86
+ return undefined;
87
+ }
88
+ var cached = yield command(() => connection.get("".concat(namespacePrefix).concat(key)), telemetry, 'get');
70
89
  if (!cached) return undefined;
71
90
  var entry = JSON.parse(cached);
72
91
  if (entry.version < latestVersion) return undefined;
73
- if (ttl) {
74
- void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl)).catch(() => undefined);
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) {
95
+ void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
75
96
  }
97
+ if (onSource) _themeKitCore.ContentDelivery.notify(onSource, 'redis');
76
98
  return entry.value;
77
99
  })();
78
100
  },
79
- set(key, value, version) {
101
+ set(key, value, version, options) {
80
102
  return _asyncToGenerator(function* () {
81
- if (!connection.isReady) return;
82
- var entry = {
103
+ if (!connection.isReady) {
104
+ _themeKitCore.ContentDelivery.emit(telemetry, {
105
+ type: 'redis_unavailable',
106
+ command: 'set'
107
+ });
108
+ return;
109
+ }
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 ? {
83
112
  value,
84
113
  version
114
+ } : {
115
+ value,
116
+ version,
117
+ ttl: Math.max(1, Math.ceil(options.ttl))
85
118
  };
86
- yield command(() => connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
87
- EX: ttl
88
- }));
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');
89
125
  })();
90
126
  },
91
127
  namespace(namespace) {
@@ -1,9 +1,10 @@
1
- import type { ContentDelivery } from '@prezly/theme-kit-core';
1
+ import { ContentDelivery } from '@prezly/theme-kit-core';
2
2
  import { type RedisClientOptions } from 'redis';
3
3
  type Seconds = number;
4
4
  type Options = RedisClientOptions & {
5
5
  ttl?: Seconds;
6
6
  prefix?: string;
7
+ telemetry?: ContentDelivery.Telemetry;
7
8
  };
8
- export declare function createRedisCache({ ttl, prefix, ...options }: Options): ContentDelivery.Cache;
9
+ export declare function createRedisCache({ ttl, prefix, telemetry, ...options }: Options): ContentDelivery.Cache;
9
10
  export {};
@@ -1,4 +1,4 @@
1
- var _excluded = ["ttl", "prefix"];
1
+ var _excluded = ["ttl", "prefix", "telemetry"];
2
2
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
3
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
4
  function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
@@ -8,25 +8,37 @@ function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i
8
8
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
9
9
  function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
10
10
  function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
11
+ import { ContentDelivery } from '@prezly/theme-kit-core';
11
12
  import stableStringify from 'json-stable-stringify';
12
13
  import { createClient } from 'redis';
13
14
  var COMMAND_TIMEOUT = 1000;
14
15
  var CONNECTIONS = new Map();
15
- function command(_x) {
16
+ function command(_x, _x2) {
16
17
  return _command.apply(this, arguments);
17
18
  }
18
19
  function _command() {
19
- _command = _asyncToGenerator(function* (invoke) {
20
+ _command = _asyncToGenerator(function* (invoke, telemetry) {
21
+ var operation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'get';
20
22
  var timer;
23
+ var start = telemetry ? ContentDelivery.telemetryNow() : 0;
24
+ var outcome = 'error';
21
25
  try {
22
26
  // node-redis 4 can corrupt its command queue when aborting a command
23
27
  // already sent to the socket. Leave that operation tracked by the client;
24
28
  // commandsQueueMaxLength bounds all outstanding replies, including these.
25
- return yield Promise.race([Promise.resolve().then(invoke), new Promise((_, reject) => {
29
+ var value = yield Promise.race([Promise.resolve().then(invoke), new Promise((_, reject) => {
26
30
  timer = setTimeout(() => reject(new Error('Content cache command timed out.')), COMMAND_TIMEOUT);
27
31
  })]);
32
+ outcome = 'success';
33
+ return value;
28
34
  } finally {
29
35
  clearTimeout(timer);
36
+ ContentDelivery.emit(telemetry, {
37
+ type: 'redis_command',
38
+ command: operation,
39
+ outcome,
40
+ seconds: telemetry ? ContentDelivery.elapsedSeconds(start) : 0
41
+ });
30
42
  }
31
43
  });
32
44
  return _command.apply(this, arguments);
@@ -35,6 +47,7 @@ export function createRedisCache(_ref) {
35
47
  var ttl = _ref.ttl,
36
48
  _ref$prefix = _ref.prefix,
37
49
  prefix = _ref$prefix === void 0 ? '' : _ref$prefix,
50
+ telemetry = _ref.telemetry,
38
51
  options = _objectWithoutProperties(_ref, _excluded);
39
52
  var connectionKey = stableStringify(options);
40
53
  var client = CONNECTIONS.get(connectionKey);
@@ -56,29 +69,52 @@ export function createRedisCache(_ref) {
56
69
  function createCache() {
57
70
  var namespacePrefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
58
71
  return {
59
- get(key, latestVersion) {
72
+ get(key, latestVersion, onSource) {
60
73
  return _asyncToGenerator(function* () {
61
- if (!connection.isReady) return undefined;
62
- var cached = yield command(() => connection.get("".concat(namespacePrefix).concat(key)));
74
+ if (!connection.isReady) {
75
+ ContentDelivery.emit(telemetry, {
76
+ type: 'redis_unavailable',
77
+ command: 'get'
78
+ });
79
+ return undefined;
80
+ }
81
+ var cached = yield command(() => connection.get("".concat(namespacePrefix).concat(key)), telemetry, 'get');
63
82
  if (!cached) return undefined;
64
83
  var entry = JSON.parse(cached);
65
84
  if (entry.version < latestVersion) return undefined;
66
- if (ttl) {
67
- void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl)).catch(() => undefined);
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) {
88
+ void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
68
89
  }
90
+ if (onSource) ContentDelivery.notify(onSource, 'redis');
69
91
  return entry.value;
70
92
  })();
71
93
  },
72
- set(key, value, version) {
94
+ set(key, value, version, options) {
73
95
  return _asyncToGenerator(function* () {
74
- if (!connection.isReady) return;
75
- var entry = {
96
+ if (!connection.isReady) {
97
+ ContentDelivery.emit(telemetry, {
98
+ type: 'redis_unavailable',
99
+ command: 'set'
100
+ });
101
+ return;
102
+ }
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 ? {
76
105
  value,
77
106
  version
107
+ } : {
108
+ value,
109
+ version,
110
+ ttl: Math.max(1, Math.ceil(options.ttl))
78
111
  };
79
- yield command(() => connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
80
- EX: ttl
81
- }));
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');
82
118
  })();
83
119
  },
84
120
  namespace(namespace) {
@@ -2,6 +2,11 @@
2
2
 
3
3
  var _redis = require("redis");
4
4
  var _redis2 = require("./redis.cjs");
5
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
6
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
7
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
8
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
9
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
5
10
  function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
6
11
  function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
7
12
  jest.mock('redis', () => ({
@@ -78,12 +83,73 @@ it('preserves namespace, version and sliding expiry and observes expiry rejectio
78
83
  EX: 300
79
84
  });
80
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
+ }());
81
147
  it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellation and recovers', /*#__PURE__*/function () {
82
- var _ref6 = _asyncToGenerator(function* (method) {
148
+ var _ref9 = _asyncToGenerator(function* (method) {
83
149
  jest.useFakeTimers();
84
- var _connection3 = connection(),
85
- client = _connection3.client,
86
- cache = _connection3.cache;
150
+ var _connection6 = connection(),
151
+ client = _connection6.client,
152
+ cache = _connection6.cache;
87
153
  var rejectOperation;
88
154
  client[method].mockImplementationOnce(() => new Promise((_, reject) => {
89
155
  rejectOperation = reject;
@@ -97,7 +163,38 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
97
163
  yield jest.advanceTimersByTimeAsync(0);
98
164
  expect(jest.getTimerCount()).toBe(0);
99
165
  });
100
- return function (_x) {
101
- return _ref6.apply(this, arguments);
166
+ return function (_x2) {
167
+ return _ref9.apply(this, arguments);
102
168
  };
103
- }());
169
+ }());
170
+ it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
171
+ var _connection7 = connection(false),
172
+ client = _connection7.client,
173
+ options = _connection7.options;
174
+ var events = [];
175
+ var observe = event => {
176
+ events.push(event);
177
+ };
178
+ var cache = (0, _redis2.createRedisCache)(_objectSpread(_objectSpread({}, options), {}, {
179
+ telemetry: {
180
+ observe,
181
+ source: 'prezly'
182
+ }
183
+ }));
184
+ yield cache.get('private-key', 1);
185
+ yield cache.set('private-key', 'private-value', 1);
186
+ expect(events.filter(e => e.type === 'redis_unavailable')).toHaveLength(2);
187
+ expect(events.some(e => e.type === 'redis_command')).toBe(false);
188
+ expect(client.connect).toHaveBeenCalledTimes(1);
189
+ client.isReady = true;
190
+ var source = jest.fn();
191
+ expect(yield cache.namespace('private-room').get('private-key', 1, source)).toBe('cached');
192
+ expect(source).toHaveBeenCalledWith('redis');
193
+ yield cache.set('private-key', 'value', 2);
194
+ yield new Promise(resolve => setImmediate(resolve));
195
+ expect(events.filter(e => e.type === 'redis_command').map(e => e.command).sort()).toEqual(['expire', 'get', 'set']);
196
+ expect(JSON.stringify(events)).not.toContain('private-');
197
+ expect(_redis.createClient).toHaveBeenLastCalledWith(expect.not.objectContaining({
198
+ telemetry: expect.anything()
199
+ }));
200
+ }));
@@ -1,3 +1,8 @@
1
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
2
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
3
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
4
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
5
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
1
6
  function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
2
7
  function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
3
8
  import { createClient } from 'redis';
@@ -76,12 +81,73 @@ it('preserves namespace, version and sliding expiry and observes expiry rejectio
76
81
  EX: 300
77
82
  });
78
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
+ }());
79
145
  it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellation and recovers', /*#__PURE__*/function () {
80
- var _ref6 = _asyncToGenerator(function* (method) {
146
+ var _ref9 = _asyncToGenerator(function* (method) {
81
147
  jest.useFakeTimers();
82
- var _connection3 = connection(),
83
- client = _connection3.client,
84
- cache = _connection3.cache;
148
+ var _connection6 = connection(),
149
+ client = _connection6.client,
150
+ cache = _connection6.cache;
85
151
  var rejectOperation;
86
152
  client[method].mockImplementationOnce(() => new Promise((_, reject) => {
87
153
  rejectOperation = reject;
@@ -95,7 +161,38 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
95
161
  yield jest.advanceTimersByTimeAsync(0);
96
162
  expect(jest.getTimerCount()).toBe(0);
97
163
  });
98
- return function (_x) {
99
- return _ref6.apply(this, arguments);
164
+ return function (_x2) {
165
+ return _ref9.apply(this, arguments);
100
166
  };
101
- }());
167
+ }());
168
+ it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
169
+ var _connection7 = connection(false),
170
+ client = _connection7.client,
171
+ options = _connection7.options;
172
+ var events = [];
173
+ var observe = event => {
174
+ events.push(event);
175
+ };
176
+ var cache = createRedisCache(_objectSpread(_objectSpread({}, options), {}, {
177
+ telemetry: {
178
+ observe,
179
+ source: 'prezly'
180
+ }
181
+ }));
182
+ yield cache.get('private-key', 1);
183
+ yield cache.set('private-key', 'private-value', 1);
184
+ expect(events.filter(e => e.type === 'redis_unavailable')).toHaveLength(2);
185
+ expect(events.some(e => e.type === 'redis_command')).toBe(false);
186
+ expect(client.connect).toHaveBeenCalledTimes(1);
187
+ client.isReady = true;
188
+ var source = jest.fn();
189
+ expect(yield cache.namespace('private-room').get('private-key', 1, source)).toBe('cached');
190
+ expect(source).toHaveBeenCalledWith('redis');
191
+ yield cache.set('private-key', 'value', 2);
192
+ yield new Promise(resolve => setImmediate(resolve));
193
+ expect(events.filter(e => e.type === 'redis_command').map(e => e.command).sort()).toEqual(['expire', 'get', 'set']);
194
+ expect(JSON.stringify(events)).not.toContain('private-');
195
+ expect(createClient).toHaveBeenLastCalledWith(expect.not.objectContaining({
196
+ telemetry: expect.anything()
197
+ }));
198
+ }));