@prezly/theme-kit-nextjs 10.9.1 → 10.9.3

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.
@@ -25,9 +25,11 @@ function configure(config) {
25
25
  return undefined;
26
26
  }
27
27
  function configureStorage(config) {
28
- var caches = [config.memory ? _themeKitCore.ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? (0, _redis.createRedisCache)(_objectSpread({
28
+ var caches = [config.memory ? _themeKitCore.ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? (0, _redis.createRedisCache)(_objectSpread(_objectSpread({
29
29
  ttl: DEFAULT_REDIS_CACHE_TTL
30
- }, config.redis)) : undefined].filter(_isNotUndefined.isNotUndefined);
30
+ }, config.redis), {}, {
31
+ telemetry: config.telemetry
32
+ })) : undefined].filter(_isNotUndefined.isNotUndefined);
31
33
  if (caches.length === 0) {
32
34
  return undefined;
33
35
  }
@@ -10,6 +10,7 @@ export interface Configuration {
10
10
  namespace?: string;
11
11
  /** Extra immutable identity for custom fetch implementations. Enables request sharing. */
12
12
  requestScope?: string;
13
+ telemetry?: ContentDelivery.Telemetry;
13
14
  }
14
15
  export declare function configure(config: Configuration): {
15
16
  storage: ContentDelivery.Cache;
@@ -19,9 +19,11 @@ export function configure(config) {
19
19
  return undefined;
20
20
  }
21
21
  function configureStorage(config) {
22
- var caches = [config.memory ? ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? createRedisCache(_objectSpread({
22
+ var caches = [config.memory ? ContentDelivery.createSharedMemoryCache() : undefined, config.redis ? createRedisCache(_objectSpread(_objectSpread({
23
23
  ttl: DEFAULT_REDIS_CACHE_TTL
24
- }, config.redis)) : undefined].filter(isNotUndefined);
24
+ }, config.redis), {}, {
25
+ telemetry: config.telemetry
26
+ })) : undefined].filter(isNotUndefined);
25
27
  if (caches.length === 0) {
26
28
  return undefined;
27
29
  }
@@ -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,42 @@ 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
92
  if (ttl) {
74
- void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl)).catch(() => undefined);
93
+ void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
75
94
  }
95
+ if (onSource) _themeKitCore.ContentDelivery.notify(onSource, 'redis');
76
96
  return entry.value;
77
97
  })();
78
98
  },
79
99
  set(key, value, version) {
80
100
  return _asyncToGenerator(function* () {
81
- if (!connection.isReady) return;
101
+ if (!connection.isReady) {
102
+ _themeKitCore.ContentDelivery.emit(telemetry, {
103
+ type: 'redis_unavailable',
104
+ command: 'set'
105
+ });
106
+ return;
107
+ }
82
108
  var entry = {
83
109
  value,
84
110
  version
85
111
  };
86
112
  yield command(() => connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
87
113
  EX: ttl
88
- }));
114
+ }), telemetry, 'set');
89
115
  })();
90
116
  },
91
117
  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,42 @@ 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
85
  if (ttl) {
67
- void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl)).catch(() => undefined);
86
+ void command(() => connection.expire("".concat(namespacePrefix).concat(key), ttl), telemetry, 'expire').catch(() => undefined);
68
87
  }
88
+ if (onSource) ContentDelivery.notify(onSource, 'redis');
69
89
  return entry.value;
70
90
  })();
71
91
  },
72
92
  set(key, value, version) {
73
93
  return _asyncToGenerator(function* () {
74
- if (!connection.isReady) return;
94
+ if (!connection.isReady) {
95
+ ContentDelivery.emit(telemetry, {
96
+ type: 'redis_unavailable',
97
+ command: 'set'
98
+ });
99
+ return;
100
+ }
75
101
  var entry = {
76
102
  value,
77
103
  version
78
104
  };
79
105
  yield command(() => connection.set("".concat(namespacePrefix).concat(key), JSON.stringify(entry), {
80
106
  EX: ttl
81
- }));
107
+ }), telemetry, 'set');
82
108
  })();
83
109
  },
84
110
  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', () => ({
@@ -100,4 +105,35 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
100
105
  return function (_x) {
101
106
  return _ref6.apply(this, arguments);
102
107
  };
103
- }());
108
+ }());
109
+ it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
110
+ var _connection4 = connection(false),
111
+ client = _connection4.client,
112
+ options = _connection4.options;
113
+ var events = [];
114
+ var observe = event => {
115
+ events.push(event);
116
+ };
117
+ var cache = (0, _redis2.createRedisCache)(_objectSpread(_objectSpread({}, options), {}, {
118
+ telemetry: {
119
+ observe,
120
+ source: 'prezly'
121
+ }
122
+ }));
123
+ yield cache.get('private-key', 1);
124
+ yield cache.set('private-key', 'private-value', 1);
125
+ expect(events.filter(e => e.type === 'redis_unavailable')).toHaveLength(2);
126
+ expect(events.some(e => e.type === 'redis_command')).toBe(false);
127
+ expect(client.connect).toHaveBeenCalledTimes(1);
128
+ client.isReady = true;
129
+ var source = jest.fn();
130
+ expect(yield cache.namespace('private-room').get('private-key', 1, source)).toBe('cached');
131
+ expect(source).toHaveBeenCalledWith('redis');
132
+ yield cache.set('private-key', 'value', 2);
133
+ yield new Promise(resolve => setImmediate(resolve));
134
+ expect(events.filter(e => e.type === 'redis_command').map(e => e.command).sort()).toEqual(['expire', 'get', 'set']);
135
+ expect(JSON.stringify(events)).not.toContain('private-');
136
+ expect(_redis.createClient).toHaveBeenLastCalledWith(expect.not.objectContaining({
137
+ telemetry: expect.anything()
138
+ }));
139
+ }));
@@ -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';
@@ -98,4 +103,35 @@ it.each(['get', 'set'])('times out stalled %s commands without unsafe cancellati
98
103
  return function (_x) {
99
104
  return _ref6.apply(this, arguments);
100
105
  };
101
- }());
106
+ }());
107
+ it('does not fragment Redis connections by observer and distinguishes unavailable from commands', /*#__PURE__*/_asyncToGenerator(function* () {
108
+ var _connection4 = connection(false),
109
+ client = _connection4.client,
110
+ options = _connection4.options;
111
+ var events = [];
112
+ var observe = event => {
113
+ events.push(event);
114
+ };
115
+ var cache = createRedisCache(_objectSpread(_objectSpread({}, options), {}, {
116
+ telemetry: {
117
+ observe,
118
+ source: 'prezly'
119
+ }
120
+ }));
121
+ yield cache.get('private-key', 1);
122
+ yield cache.set('private-key', 'private-value', 1);
123
+ expect(events.filter(e => e.type === 'redis_unavailable')).toHaveLength(2);
124
+ expect(events.some(e => e.type === 'redis_command')).toBe(false);
125
+ expect(client.connect).toHaveBeenCalledTimes(1);
126
+ client.isReady = true;
127
+ var source = jest.fn();
128
+ expect(yield cache.namespace('private-room').get('private-key', 1, source)).toBe('cached');
129
+ expect(source).toHaveBeenCalledWith('redis');
130
+ yield cache.set('private-key', 'value', 2);
131
+ yield new Promise(resolve => setImmediate(resolve));
132
+ expect(events.filter(e => e.type === 'redis_command').map(e => e.command).sort()).toEqual(['expire', 'get', 'set']);
133
+ expect(JSON.stringify(events)).not.toContain('private-');
134
+ expect(createClient).toHaveBeenLastCalledWith(expect.not.objectContaining({
135
+ telemetry: expect.anything()
136
+ }));
137
+ }));
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.FetchScheduler = void 0;
7
7
  exports.createBoundedFetch = createBoundedFetch;
8
+ var _themeKitCore = require("@prezly/theme-kit-core");
9
+ var _telemetry = require("./telemetry.cjs");
8
10
  var _registry$key;
9
11
  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
12
  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); }); }; }
@@ -48,33 +50,62 @@ class FetchScheduler {
48
50
  }
49
51
  }
50
52
  }
51
- run(invoke, signal) {
52
- if (signal !== null && signal !== void 0 && signal.aborted) return Promise.reject(abortReason(signal));
53
+ run(invoke, signal, telemetry) {
54
+ var route = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'other';
55
+ if (signal !== null && signal !== void 0 && signal.aborted) {
56
+ _themeKitCore.ContentDelivery.emit(telemetry, {
57
+ type: 'fetch_queue',
58
+ route,
59
+ outcome: 'aborted',
60
+ seconds: 0
61
+ });
62
+ return Promise.reject(abortReason(signal));
63
+ }
53
64
  return new Promise((_resolve, reject) => {
54
65
  var task = {
55
66
  invoke,
56
67
  resolve: value => _resolve(value),
57
68
  reject,
58
69
  signal,
59
- clearQueueWait: () => {}
70
+ clearQueueWait: () => {},
71
+ telemetry,
72
+ route
60
73
  };
61
74
  if (this.active < this.limits.concurrency) {
62
75
  this.start(task);
63
76
  return;
64
77
  }
65
78
  if (this.queue.length >= this.limits.queueLimit) {
79
+ _themeKitCore.ContentDelivery.emit(telemetry, {
80
+ type: 'fetch_queue',
81
+ route,
82
+ outcome: 'full',
83
+ seconds: 0
84
+ });
66
85
  reject(new Error('Content request queue is full.'));
67
86
  return;
68
87
  }
69
- var cancel = error => {
88
+ task.queuedAt = telemetry ? _themeKitCore.ContentDelivery.telemetryNow() : undefined;
89
+ _themeKitCore.ContentDelivery.emit(telemetry, {
90
+ type: 'fetch_queue',
91
+ route,
92
+ outcome: 'queued'
93
+ });
94
+ var cancel = (error, outcome) => {
70
95
  var index = this.queue.indexOf(task);
71
96
  if (index < 0) return;
72
97
  this.queue.splice(index, 1);
73
98
  task.clearQueueWait();
99
+ _themeKitCore.ContentDelivery.emit(telemetry, {
100
+ type: 'fetch_queue',
101
+ route,
102
+ outcome,
103
+ seconds: task.queuedAt === undefined ? 0 : _themeKitCore.ContentDelivery.elapsedSeconds(task.queuedAt)
104
+ });
74
105
  reject(error);
75
106
  };
76
- var onAbort = () => cancel(abortReason(signal));
77
- var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.')), this.limits.queueTimeout);
107
+ var onAbort = () => cancel(abortReason(signal), 'aborted');
108
+ var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.'), 'wait_timeout'), this.limits.queueTimeout);
78
109
  task.clearQueueWait = () => {
79
110
  clearTimeout(timer);
80
111
  signal === null || signal === void 0 || signal.removeEventListener('abort', onAbort);
@@ -88,6 +119,12 @@ class FetchScheduler {
88
119
  start(task) {
89
120
  var _task$signal2;
90
121
  task.clearQueueWait();
122
+ _themeKitCore.ContentDelivery.emit(task.telemetry, {
123
+ type: 'fetch_queue',
124
+ route: task.route,
125
+ outcome: 'accepted',
126
+ seconds: task.queuedAt === undefined ? 0 : _themeKitCore.ContentDelivery.elapsedSeconds(task.queuedAt)
127
+ });
91
128
  this.active += 1;
92
129
  var controller = new AbortController();
93
130
  var rejectAbort = () => task.reject(abortReason(controller.signal));
@@ -101,7 +138,15 @@ class FetchScheduler {
101
138
  (_task$signal2 = task.signal) === null || _task$signal2 === void 0 || _task$signal2.addEventListener('abort', forwardAbort, {
102
139
  once: true
103
140
  });
104
- var timer = setTimeout(() => controller.abort(new Error('Content request timed out.')), this.limits.requestTimeout);
141
+ var timer = setTimeout(() => {
142
+ if (!controller.signal.aborted) {
143
+ _themeKitCore.ContentDelivery.emit(task.telemetry, {
144
+ type: 'fetch_timeout',
145
+ route: task.route
146
+ });
147
+ controller.abort(new Error('Content request timed out.'));
148
+ }
149
+ }, this.limits.requestTimeout);
105
150
  void Promise.resolve().then(() => {
106
151
  if (controller.signal.aborted) throw abortReason(controller.signal);
107
152
  return task.invoke(controller.signal);
@@ -127,6 +172,7 @@ var registry = globalThis;
127
172
  var sharedContentFetches = (_registry$key = registry[key]) !== null && _registry$key !== void 0 ? _registry$key : registry[key] = new FetchScheduler();
128
173
  function createBoundedFetch(fetchImpl) {
129
174
  var scheduler = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : sharedContentFetches;
175
+ var telemetry = arguments.length > 2 ? arguments[2] : undefined;
130
176
  return (input, init) => {
131
177
  var _init$signal;
132
178
  var signal = (_init$signal = init === null || init === void 0 ? void 0 : init.signal) !== null && _init$signal !== void 0 ? _init$signal : input instanceof Request ? input.signal : undefined;
@@ -147,6 +193,6 @@ function createBoundedFetch(fetchImpl) {
147
193
  return function (_x) {
148
194
  return _ref4.apply(this, arguments);
149
195
  };
150
- }(), signal);
196
+ }(), signal, telemetry, telemetry ? (0, _telemetry.upstreamRoute)(input) : 'other');
151
197
  };
152
198
  }
@@ -10,8 +10,9 @@ export declare class FetchScheduler {
10
10
  private readonly queue;
11
11
  private readonly limits;
12
12
  constructor(limits?: Partial<Limits>);
13
- run<T>(invoke: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal | null): Promise<T>;
13
+ run<T>(invoke: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal | null, telemetry?: ContentDelivery.Telemetry, route?: ContentDelivery.UpstreamRoute): Promise<T>;
14
14
  private start;
15
15
  }
16
- export declare function createBoundedFetch(fetchImpl: typeof fetch, scheduler?: FetchScheduler): typeof fetch;
16
+ export declare function createBoundedFetch(fetchImpl: typeof fetch, scheduler?: FetchScheduler, telemetry?: ContentDelivery.Telemetry): typeof fetch;
17
+ import { ContentDelivery } from '@prezly/theme-kit-core';
17
18
  export {};
@@ -41,33 +41,62 @@ export class FetchScheduler {
41
41
  }
42
42
  }
43
43
  }
44
- run(invoke, signal) {
45
- if (signal !== null && signal !== void 0 && signal.aborted) return Promise.reject(abortReason(signal));
44
+ run(invoke, signal, telemetry) {
45
+ var route = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'other';
46
+ if (signal !== null && signal !== void 0 && signal.aborted) {
47
+ ContentDelivery.emit(telemetry, {
48
+ type: 'fetch_queue',
49
+ route,
50
+ outcome: 'aborted',
51
+ seconds: 0
52
+ });
53
+ return Promise.reject(abortReason(signal));
54
+ }
46
55
  return new Promise((_resolve, reject) => {
47
56
  var task = {
48
57
  invoke,
49
58
  resolve: value => _resolve(value),
50
59
  reject,
51
60
  signal,
52
- clearQueueWait: () => {}
61
+ clearQueueWait: () => {},
62
+ telemetry,
63
+ route
53
64
  };
54
65
  if (this.active < this.limits.concurrency) {
55
66
  this.start(task);
56
67
  return;
57
68
  }
58
69
  if (this.queue.length >= this.limits.queueLimit) {
70
+ ContentDelivery.emit(telemetry, {
71
+ type: 'fetch_queue',
72
+ route,
73
+ outcome: 'full',
74
+ seconds: 0
75
+ });
59
76
  reject(new Error('Content request queue is full.'));
60
77
  return;
61
78
  }
62
- var cancel = error => {
79
+ task.queuedAt = telemetry ? ContentDelivery.telemetryNow() : undefined;
80
+ ContentDelivery.emit(telemetry, {
81
+ type: 'fetch_queue',
82
+ route,
83
+ outcome: 'queued'
84
+ });
85
+ var cancel = (error, outcome) => {
63
86
  var index = this.queue.indexOf(task);
64
87
  if (index < 0) return;
65
88
  this.queue.splice(index, 1);
66
89
  task.clearQueueWait();
90
+ ContentDelivery.emit(telemetry, {
91
+ type: 'fetch_queue',
92
+ route,
93
+ outcome,
94
+ seconds: task.queuedAt === undefined ? 0 : ContentDelivery.elapsedSeconds(task.queuedAt)
95
+ });
67
96
  reject(error);
68
97
  };
69
- var onAbort = () => cancel(abortReason(signal));
70
- var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.')), this.limits.queueTimeout);
98
+ var onAbort = () => cancel(abortReason(signal), 'aborted');
99
+ var timer = setTimeout(() => cancel(new Error('Content request queue wait timed out.'), 'wait_timeout'), this.limits.queueTimeout);
71
100
  task.clearQueueWait = () => {
72
101
  clearTimeout(timer);
73
102
  signal === null || signal === void 0 || signal.removeEventListener('abort', onAbort);
@@ -81,6 +110,12 @@ export class FetchScheduler {
81
110
  start(task) {
82
111
  var _task$signal2;
83
112
  task.clearQueueWait();
113
+ ContentDelivery.emit(task.telemetry, {
114
+ type: 'fetch_queue',
115
+ route: task.route,
116
+ outcome: 'accepted',
117
+ seconds: task.queuedAt === undefined ? 0 : ContentDelivery.elapsedSeconds(task.queuedAt)
118
+ });
84
119
  this.active += 1;
85
120
  var controller = new AbortController();
86
121
  var rejectAbort = () => task.reject(abortReason(controller.signal));
@@ -94,7 +129,15 @@ export class FetchScheduler {
94
129
  (_task$signal2 = task.signal) === null || _task$signal2 === void 0 || _task$signal2.addEventListener('abort', forwardAbort, {
95
130
  once: true
96
131
  });
97
- var timer = setTimeout(() => controller.abort(new Error('Content request timed out.')), this.limits.requestTimeout);
132
+ var timer = setTimeout(() => {
133
+ if (!controller.signal.aborted) {
134
+ ContentDelivery.emit(task.telemetry, {
135
+ type: 'fetch_timeout',
136
+ route: task.route
137
+ });
138
+ controller.abort(new Error('Content request timed out.'));
139
+ }
140
+ }, this.limits.requestTimeout);
98
141
  void Promise.resolve().then(() => {
99
142
  if (controller.signal.aborted) throw abortReason(controller.signal);
100
143
  return task.invoke(controller.signal);
@@ -119,6 +162,7 @@ var registry = globalThis;
119
162
  var sharedContentFetches = (_registry$key = registry[key]) !== null && _registry$key !== void 0 ? _registry$key : registry[key] = new FetchScheduler();
120
163
  export function createBoundedFetch(fetchImpl) {
121
164
  var scheduler = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : sharedContentFetches;
165
+ var telemetry = arguments.length > 2 ? arguments[2] : undefined;
122
166
  return (input, init) => {
123
167
  var _init$signal;
124
168
  var signal = (_init$signal = init === null || init === void 0 ? void 0 : init.signal) !== null && _init$signal !== void 0 ? _init$signal : input instanceof Request ? input.signal : undefined;
@@ -139,6 +183,8 @@ export function createBoundedFetch(fetchImpl) {
139
183
  return function (_x) {
140
184
  return _ref4.apply(this, arguments);
141
185
  };
142
- }(), signal);
186
+ }(), signal, telemetry, telemetry ? upstreamRoute(input) : 'other');
143
187
  };
144
- }
188
+ }
189
+ import { ContentDelivery } from '@prezly/theme-kit-core';
190
+ import { upstreamRoute } from "./telemetry.mjs";
@@ -11,6 +11,7 @@ var _sdk = require("@prezly/sdk");
11
11
  var _themeKitCore = require("@prezly/theme-kit-core");
12
12
  var _index = require("./cache/index.cjs");
13
13
  var _requests = require("./requests.cjs");
14
+ var _telemetry = require("./telemetry.cjs");
14
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
16
  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; }
16
17
  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; }
@@ -22,7 +23,8 @@ var PrezlyAdapter;
22
23
  function connect(config) {
23
24
  var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
24
25
  cacheConfig = _ref.cache,
25
- fetch = _ref.fetch;
26
+ fetch = _ref.fetch,
27
+ observer = _ref.telemetry;
26
28
  function usePrezlyClient() {
27
29
  var _cacheConfig$namespac, _cacheConfig$memory;
28
30
  var _Resolvable$resolve = _themeKitCore.Resolvable.resolve(config),
@@ -32,15 +34,21 @@ var PrezlyAdapter;
32
34
  newsroom = _Resolvable$resolve.newsroom,
33
35
  theme = _Resolvable$resolve.theme,
34
36
  formats = _Resolvable$resolve.formats;
37
+ var telemetry = observer ? {
38
+ observe: observer,
39
+ source: !baseUrl || baseUrl.replace(/\/$/, '') === 'https://api.prezly.com' ? 'prezly' : 'custom'
40
+ } : undefined;
35
41
  var client = (0, _sdk.createPrezlyClient)({
36
- fetch,
42
+ fetch: telemetry ? (0, _telemetry.createObservedFetch)(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'raw') : fetch,
37
43
  accessToken,
38
44
  baseUrl,
39
45
  headers
40
46
  });
41
- var cache = cacheConfig ? (0, _index.configure)(_objectSpread({
47
+ var cache = cacheConfig ? (0, _index.configure)(_objectSpread(_objectSpread({
42
48
  namespace: 'content:'
43
- }, cacheConfig)) : undefined;
49
+ }, cacheConfig), {}, {
50
+ telemetry
51
+ })) : undefined;
44
52
  // Custom fetches may depend on hidden request context. Preserve their
45
53
  // existing cache behavior unless the caller declares that identity.
46
54
  var scope = cache && (!fetch || (cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.requestScope) !== undefined) ? (0, _utils.bytesToHex)((0, _sha.sha256)((0, _jsonStableStringify.default)({
@@ -58,10 +66,11 @@ var PrezlyAdapter;
58
66
  accessToken,
59
67
  baseUrl,
60
68
  headers,
61
- fetch: (0, _requests.createBoundedFetch)(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch)
69
+ fetch: (0, _requests.createBoundedFetch)((0, _telemetry.createObservedFetch)(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'content'), undefined, telemetry)
62
70
  }) : client;
63
71
  var contentDelivery = _themeKitCore.ContentDelivery.createClient(contentClient, newsroom, theme, {
64
72
  formats,
73
+ telemetry,
65
74
  cache: cache ? _objectSpread(_objectSpread({}, cache), {}, {
66
75
  scope
67
76
  }) : undefined
@@ -14,9 +14,10 @@ export declare namespace PrezlyAdapter {
14
14
  interface Options {
15
15
  cache?: CacheConfiguration;
16
16
  fetch?: typeof fetch;
17
+ telemetry?: ContentDelivery.TelemetryObserver;
17
18
  }
18
19
  type CacheConfiguration = CacheConfig;
19
- function connect(config: Resolvable<Configuration>, { cache: cacheConfig, fetch }?: Options): {
20
+ function connect(config: Resolvable<Configuration>, { cache: cacheConfig, fetch, telemetry: observer }?: Options): {
20
21
  usePrezlyClient: () => {
21
22
  client: import("@prezly/sdk").PrezlyClient;
22
23
  contentDelivery: {
@@ -10,12 +10,14 @@ import { createPrezlyClient } from '@prezly/sdk';
10
10
  import { ContentDelivery, Resolvable } from '@prezly/theme-kit-core';
11
11
  import { configure as configureCache } from "./cache/index.mjs";
12
12
  import { createBoundedFetch } from "./requests.mjs";
13
+ import { createObservedFetch } from "./telemetry.mjs";
13
14
  export var PrezlyAdapter;
14
15
  (function (_PrezlyAdapter) {
15
16
  function connect(config) {
16
17
  var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
17
18
  cacheConfig = _ref.cache,
18
- fetch = _ref.fetch;
19
+ fetch = _ref.fetch,
20
+ observer = _ref.telemetry;
19
21
  function usePrezlyClient() {
20
22
  var _cacheConfig$namespac, _cacheConfig$memory;
21
23
  var _Resolvable$resolve = Resolvable.resolve(config),
@@ -25,15 +27,21 @@ export var PrezlyAdapter;
25
27
  newsroom = _Resolvable$resolve.newsroom,
26
28
  theme = _Resolvable$resolve.theme,
27
29
  formats = _Resolvable$resolve.formats;
30
+ var telemetry = observer ? {
31
+ observe: observer,
32
+ source: !baseUrl || baseUrl.replace(/\/$/, '') === 'https://api.prezly.com' ? 'prezly' : 'custom'
33
+ } : undefined;
28
34
  var client = createPrezlyClient({
29
- fetch,
35
+ fetch: telemetry ? createObservedFetch(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'raw') : fetch,
30
36
  accessToken,
31
37
  baseUrl,
32
38
  headers
33
39
  });
34
- var cache = cacheConfig ? configureCache(_objectSpread({
40
+ var cache = cacheConfig ? configureCache(_objectSpread(_objectSpread({
35
41
  namespace: 'content:'
36
- }, cacheConfig)) : undefined;
42
+ }, cacheConfig), {}, {
43
+ telemetry
44
+ })) : undefined;
37
45
  // Custom fetches may depend on hidden request context. Preserve their
38
46
  // existing cache behavior unless the caller declares that identity.
39
47
  var scope = cache && (!fetch || (cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.requestScope) !== undefined) ? bytesToHex(sha256(stableStringify({
@@ -51,10 +59,11 @@ export var PrezlyAdapter;
51
59
  accessToken,
52
60
  baseUrl,
53
61
  headers,
54
- fetch: createBoundedFetch(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch)
62
+ fetch: createBoundedFetch(createObservedFetch(fetch !== null && fetch !== void 0 ? fetch : globalThis.fetch, telemetry, 'content'), undefined, telemetry)
55
63
  }) : client;
56
64
  var contentDelivery = ContentDelivery.createClient(contentClient, newsroom, theme, {
57
65
  formats,
66
+ telemetry,
58
67
  cache: cache ? _objectSpread(_objectSpread({}, cache), {}, {
59
68
  scope
60
69
  }) : undefined
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createObservedFetch = createObservedFetch;
7
+ exports.upstreamRoute = upstreamRoute;
8
+ var _themeKitCore = require("@prezly/theme-kit-core");
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
+ 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
+ function upstreamRoute(input) {
12
+ try {
13
+ var path = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url).pathname;
14
+ if (/^\/v2\/stories\/by-slug\/[^/]+$/.test(path)) return 'story_by_slug';
15
+ if (/^\/v2\/stories(?:\/search)?$/.test(path)) return 'stories';
16
+ if (/^\/v2\/stories\/[^/]+$/.test(path)) return 'story';
17
+ if (/^\/v2\/newsrooms\/[^/]+\/themes\/[^/]+$/.test(path)) return 'theme';
18
+ if (/^\/v2\/newsrooms\/[^/]+\/languages(?:\/[^/]+)?$/.test(path)) return 'languages';
19
+ if (/^\/v2\/newsrooms\/[^/]+\/categories(?:\/[^/]+)?$/.test(path)) return 'categories';
20
+ if (/^\/v2\/newsrooms\/[^/]+\/contacts(?:\/search)?$/.test(path)) return 'contacts';
21
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries(?:\/search)?$/.test(path)) return 'galleries';
22
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries\/[^/]+$/.test(path)) return 'gallery';
23
+ if (/^\/v2\/newsrooms\/[^/]+$/.test(path)) return 'newsroom';
24
+ } catch (_unused) {
25
+ // Invalid/custom inputs keep their original fetch behavior.
26
+ }
27
+ return 'other';
28
+ }
29
+
30
+ /** Observe response headers only; preserve the Response and streaming body. */
31
+ function createObservedFetch(fetchImpl, telemetry, client) {
32
+ if (!telemetry) return fetchImpl;
33
+ return /*#__PURE__*/function () {
34
+ var _ref = _asyncToGenerator(function* (input, init) {
35
+ var route = upstreamRoute(input);
36
+ var start = _themeKitCore.ContentDelivery.telemetryNow();
37
+ var status = 'error';
38
+ try {
39
+ var response = yield fetchImpl(input, init);
40
+ status = response.status >= 200 && response.status < 600 ? "".concat(Math.floor(response.status / 100), "xx") : 'other';
41
+ return response;
42
+ } finally {
43
+ _themeKitCore.ContentDelivery.emit(telemetry, {
44
+ type: 'upstream',
45
+ route,
46
+ client,
47
+ status,
48
+ seconds: _themeKitCore.ContentDelivery.elapsedSeconds(start)
49
+ });
50
+ }
51
+ });
52
+ return function (_x, _x2) {
53
+ return _ref.apply(this, arguments);
54
+ };
55
+ }();
56
+ }
@@ -0,0 +1,4 @@
1
+ import { ContentDelivery } from '@prezly/theme-kit-core';
2
+ export declare function upstreamRoute(input: Parameters<typeof fetch>[0]): ContentDelivery.UpstreamRoute;
3
+ /** Observe response headers only; preserve the Response and streaming body. */
4
+ export declare function createObservedFetch(fetchImpl: typeof fetch, telemetry: ContentDelivery.Telemetry | undefined, client: 'raw' | 'content'): typeof fetch;
@@ -0,0 +1,49 @@
1
+ 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
+ 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
+ import { ContentDelivery } from '@prezly/theme-kit-core';
4
+ export function upstreamRoute(input) {
5
+ try {
6
+ var path = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url).pathname;
7
+ if (/^\/v2\/stories\/by-slug\/[^/]+$/.test(path)) return 'story_by_slug';
8
+ if (/^\/v2\/stories(?:\/search)?$/.test(path)) return 'stories';
9
+ if (/^\/v2\/stories\/[^/]+$/.test(path)) return 'story';
10
+ if (/^\/v2\/newsrooms\/[^/]+\/themes\/[^/]+$/.test(path)) return 'theme';
11
+ if (/^\/v2\/newsrooms\/[^/]+\/languages(?:\/[^/]+)?$/.test(path)) return 'languages';
12
+ if (/^\/v2\/newsrooms\/[^/]+\/categories(?:\/[^/]+)?$/.test(path)) return 'categories';
13
+ if (/^\/v2\/newsrooms\/[^/]+\/contacts(?:\/search)?$/.test(path)) return 'contacts';
14
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries(?:\/search)?$/.test(path)) return 'galleries';
15
+ if (/^\/v2\/newsrooms\/[^/]+\/galleries\/[^/]+$/.test(path)) return 'gallery';
16
+ if (/^\/v2\/newsrooms\/[^/]+$/.test(path)) return 'newsroom';
17
+ } catch (_unused) {
18
+ // Invalid/custom inputs keep their original fetch behavior.
19
+ }
20
+ return 'other';
21
+ }
22
+
23
+ /** Observe response headers only; preserve the Response and streaming body. */
24
+ export function createObservedFetch(fetchImpl, telemetry, client) {
25
+ if (!telemetry) return fetchImpl;
26
+ return /*#__PURE__*/function () {
27
+ var _ref = _asyncToGenerator(function* (input, init) {
28
+ var route = upstreamRoute(input);
29
+ var start = ContentDelivery.telemetryNow();
30
+ var status = 'error';
31
+ try {
32
+ var response = yield fetchImpl(input, init);
33
+ status = response.status >= 200 && response.status < 600 ? "".concat(Math.floor(response.status / 100), "xx") : 'other';
34
+ return response;
35
+ } finally {
36
+ ContentDelivery.emit(telemetry, {
37
+ type: 'upstream',
38
+ route,
39
+ client,
40
+ status,
41
+ seconds: ContentDelivery.elapsedSeconds(start)
42
+ });
43
+ }
44
+ });
45
+ return function (_x, _x2) {
46
+ return _ref.apply(this, arguments);
47
+ };
48
+ }();
49
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+
3
+ var _telemetry = require("./telemetry.cjs");
4
+ var _requests = require("./requests.cjs");
5
+ var _server = require("./server.cjs");
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); }
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); }); }; }
8
+ var nativeFetch = globalThis.fetch;
9
+ var tick = () => new Promise(resolve => setImmediate(resolve));
10
+ afterEach(() => {
11
+ globalThis.fetch = nativeFetch;
12
+ jest.useRealTimers();
13
+ });
14
+ it.each([['/v2/newsrooms/private-id', 'newsroom'], ['/v2/newsrooms/private-id/languages/en_GB', 'languages'], ['/v2/newsrooms/private-id/categories', 'categories'], ['/v2/stories/by-slug/private-slug?token=secret', 'story_by_slug'], ['/v2/stories/search', 'stories'], ['/v2/stories/private-uuid', 'story'], ['/custom/private-path', 'other']])('classifies %s without retaining URL values', (path, expected) => {
15
+ expect((0, _telemetry.upstreamRoute)("https://private-origin.test".concat(path))).toBe(expected);
16
+ });
17
+ it('observes raw response headers without consuming the body or adding a signal', /*#__PURE__*/_asyncToGenerator(function* () {
18
+ var events = [];
19
+ var response = new Response('private-body');
20
+ var read = jest.spyOn(response, 'arrayBuffer');
21
+ var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
22
+ return response;
23
+ }));
24
+ var options = {
25
+ headers: {
26
+ Authorization: 'private-token'
27
+ }
28
+ };
29
+ var observed = (0, _telemetry.createObservedFetch)(fetchMock, {
30
+ observe: e => {
31
+ events.push(e);
32
+ },
33
+ source: 'custom'
34
+ }, 'raw');
35
+ expect(yield observed('https://private-origin.test/v2/stories/private-uuid', options)).toBe(response);
36
+ expect(read).not.toHaveBeenCalled();
37
+ expect(fetchMock).toHaveBeenCalledWith('https://private-origin.test/v2/stories/private-uuid', options);
38
+ expect(events).toEqual([expect.objectContaining({
39
+ type: 'upstream',
40
+ client: 'raw',
41
+ route: 'story',
42
+ source: 'custom',
43
+ status: '2xx'
44
+ })]);
45
+ expect(JSON.stringify(events)).not.toMatch(/private-/);
46
+ }));
47
+ it('keeps telemetry outside the source/cache identity when adapters share work', /*#__PURE__*/_asyncToGenerator(function* () {
48
+ var events = [];
49
+ var config = {
50
+ newsroom: 'private-room',
51
+ accessToken: 'private-token'
52
+ };
53
+ var cache = {
54
+ memory: true,
55
+ latestVersion: 1,
56
+ namespace: 'metrics-adapter-sharing'
57
+ };
58
+ globalThis.fetch = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
59
+ return Response.json({
60
+ newsroom: {
61
+ name: 'room'
62
+ }
63
+ });
64
+ }));
65
+ var observed = _server.PrezlyAdapter.connect(config, {
66
+ cache,
67
+ telemetry: e => {
68
+ events.push(e);
69
+ }
70
+ }).usePrezlyClient();
71
+ var plain = _server.PrezlyAdapter.connect(config, {
72
+ cache
73
+ }).usePrezlyClient();
74
+ yield Promise.all([observed.contentDelivery.newsroom(), plain.contentDelivery.newsroom()]);
75
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
76
+ yield tick();
77
+ yield observed.contentDelivery.newsroom();
78
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
79
+ expect(events.filter(e => e.type === 'upstream')).toHaveLength(1);
80
+ expect(events.some(e => e.type === 'cache_hit')).toBe(true);
81
+ }));
82
+ it('reports queued, full, accepted and completed wait without changing admission limits', /*#__PURE__*/_asyncToGenerator(function* () {
83
+ var events = [];
84
+ var telemetry = {
85
+ observe: e => {
86
+ events.push(e);
87
+ }
88
+ };
89
+ var scheduler = new _requests.FetchScheduler({
90
+ concurrency: 1,
91
+ queueLimit: 1
92
+ });
93
+ var release;
94
+ var active = scheduler.run(() => new Promise(r => {
95
+ release = r;
96
+ }), undefined, telemetry, 'story');
97
+ var queued = scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
98
+ return 42;
99
+ }), undefined, telemetry, 'story');
100
+ yield expect(scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
101
+ return 0;
102
+ }), undefined, telemetry, 'story')).rejects.toThrow('queue is full');
103
+ yield tick();
104
+ release();
105
+ yield active;
106
+ expect(yield queued).toBe(42);
107
+ yield tick();
108
+ expect(events.filter(e => e.type === 'fetch_queue').map(e => e.outcome)).toEqual(['accepted', 'queued', 'full', 'accepted']);
109
+ }));
110
+ it('reports queue timeout and body deadline independently of successful HTTP headers', /*#__PURE__*/_asyncToGenerator(function* () {
111
+ jest.useFakeTimers();
112
+ var events = [];
113
+ var telemetry = {
114
+ observe: e => {
115
+ events.push(e);
116
+ }
117
+ };
118
+ var scheduler = new _requests.FetchScheduler({
119
+ concurrency: 1,
120
+ queueLimit: 1,
121
+ queueTimeout: 10,
122
+ requestTimeout: 30
123
+ });
124
+ var finish;
125
+ var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
126
+ return new Response(new ReadableStream({
127
+ start(controller) {
128
+ finish = () => controller.close();
129
+ }
130
+ }));
131
+ }));
132
+ var fetch = (0, _requests.createBoundedFetch)((0, _telemetry.createObservedFetch)(fetchMock, telemetry, 'content'), scheduler, telemetry);
133
+ var first = fetch('https://example.test/v2/stories/search');
134
+ var failed = expect(first).rejects.toThrow('timed out');
135
+ var next = fetch('https://example.test/v2/stories/search');
136
+ var queued = expect(next).rejects.toThrow('queue wait timed out');
137
+ yield jest.advanceTimersByTimeAsync(31);
138
+ yield failed;
139
+ yield queued;
140
+ expect(events.filter(e => e.type === 'upstream')).toEqual([expect.objectContaining({
141
+ status: '2xx'
142
+ })]);
143
+ expect(events.filter(e => e.type === 'fetch_timeout')).toHaveLength(1);
144
+ expect(events).toContainEqual(expect.objectContaining({
145
+ type: 'fetch_queue',
146
+ outcome: 'wait_timeout'
147
+ }));
148
+ finish();
149
+ yield jest.advanceTimersByTimeAsync(0);
150
+ expect(jest.getTimerCount()).toBe(0);
151
+ }));
152
+ it('a throwing or rejecting observer cannot change successful fetch outcomes', /*#__PURE__*/_asyncToGenerator(function* () {
153
+ var response = new Response('ok');
154
+ for (var observe of [() => {
155
+ throw Error('metrics');
156
+ }, /*#__PURE__*/_asyncToGenerator(function* () {
157
+ throw Error('metrics');
158
+ })]) {
159
+ var fetch = (0, _telemetry.createObservedFetch)(/*#__PURE__*/_asyncToGenerator(function* () {
160
+ return response;
161
+ }), {
162
+ observe
163
+ }, 'raw');
164
+ expect(yield fetch('https://example.test')).toBe(response);
165
+ }
166
+ yield tick();
167
+ }));
@@ -0,0 +1,165 @@
1
+ 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
+ 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
+ import { createObservedFetch, upstreamRoute } from "./telemetry.mjs";
4
+ import { createBoundedFetch, FetchScheduler } from "./requests.mjs";
5
+ import { PrezlyAdapter } from "./server.mjs";
6
+ var nativeFetch = globalThis.fetch;
7
+ var tick = () => new Promise(resolve => setImmediate(resolve));
8
+ afterEach(() => {
9
+ globalThis.fetch = nativeFetch;
10
+ jest.useRealTimers();
11
+ });
12
+ it.each([['/v2/newsrooms/private-id', 'newsroom'], ['/v2/newsrooms/private-id/languages/en_GB', 'languages'], ['/v2/newsrooms/private-id/categories', 'categories'], ['/v2/stories/by-slug/private-slug?token=secret', 'story_by_slug'], ['/v2/stories/search', 'stories'], ['/v2/stories/private-uuid', 'story'], ['/custom/private-path', 'other']])('classifies %s without retaining URL values', (path, expected) => {
13
+ expect(upstreamRoute("https://private-origin.test".concat(path))).toBe(expected);
14
+ });
15
+ it('observes raw response headers without consuming the body or adding a signal', /*#__PURE__*/_asyncToGenerator(function* () {
16
+ var events = [];
17
+ var response = new Response('private-body');
18
+ var read = jest.spyOn(response, 'arrayBuffer');
19
+ var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
20
+ return response;
21
+ }));
22
+ var options = {
23
+ headers: {
24
+ Authorization: 'private-token'
25
+ }
26
+ };
27
+ var observed = createObservedFetch(fetchMock, {
28
+ observe: e => {
29
+ events.push(e);
30
+ },
31
+ source: 'custom'
32
+ }, 'raw');
33
+ expect(yield observed('https://private-origin.test/v2/stories/private-uuid', options)).toBe(response);
34
+ expect(read).not.toHaveBeenCalled();
35
+ expect(fetchMock).toHaveBeenCalledWith('https://private-origin.test/v2/stories/private-uuid', options);
36
+ expect(events).toEqual([expect.objectContaining({
37
+ type: 'upstream',
38
+ client: 'raw',
39
+ route: 'story',
40
+ source: 'custom',
41
+ status: '2xx'
42
+ })]);
43
+ expect(JSON.stringify(events)).not.toMatch(/private-/);
44
+ }));
45
+ it('keeps telemetry outside the source/cache identity when adapters share work', /*#__PURE__*/_asyncToGenerator(function* () {
46
+ var events = [];
47
+ var config = {
48
+ newsroom: 'private-room',
49
+ accessToken: 'private-token'
50
+ };
51
+ var cache = {
52
+ memory: true,
53
+ latestVersion: 1,
54
+ namespace: 'metrics-adapter-sharing'
55
+ };
56
+ globalThis.fetch = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
57
+ return Response.json({
58
+ newsroom: {
59
+ name: 'room'
60
+ }
61
+ });
62
+ }));
63
+ var observed = PrezlyAdapter.connect(config, {
64
+ cache,
65
+ telemetry: e => {
66
+ events.push(e);
67
+ }
68
+ }).usePrezlyClient();
69
+ var plain = PrezlyAdapter.connect(config, {
70
+ cache
71
+ }).usePrezlyClient();
72
+ yield Promise.all([observed.contentDelivery.newsroom(), plain.contentDelivery.newsroom()]);
73
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
74
+ yield tick();
75
+ yield observed.contentDelivery.newsroom();
76
+ expect(globalThis.fetch).toHaveBeenCalledTimes(1);
77
+ expect(events.filter(e => e.type === 'upstream')).toHaveLength(1);
78
+ expect(events.some(e => e.type === 'cache_hit')).toBe(true);
79
+ }));
80
+ it('reports queued, full, accepted and completed wait without changing admission limits', /*#__PURE__*/_asyncToGenerator(function* () {
81
+ var events = [];
82
+ var telemetry = {
83
+ observe: e => {
84
+ events.push(e);
85
+ }
86
+ };
87
+ var scheduler = new FetchScheduler({
88
+ concurrency: 1,
89
+ queueLimit: 1
90
+ });
91
+ var release;
92
+ var active = scheduler.run(() => new Promise(r => {
93
+ release = r;
94
+ }), undefined, telemetry, 'story');
95
+ var queued = scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
96
+ return 42;
97
+ }), undefined, telemetry, 'story');
98
+ yield expect(scheduler.run(/*#__PURE__*/_asyncToGenerator(function* () {
99
+ return 0;
100
+ }), undefined, telemetry, 'story')).rejects.toThrow('queue is full');
101
+ yield tick();
102
+ release();
103
+ yield active;
104
+ expect(yield queued).toBe(42);
105
+ yield tick();
106
+ expect(events.filter(e => e.type === 'fetch_queue').map(e => e.outcome)).toEqual(['accepted', 'queued', 'full', 'accepted']);
107
+ }));
108
+ it('reports queue timeout and body deadline independently of successful HTTP headers', /*#__PURE__*/_asyncToGenerator(function* () {
109
+ jest.useFakeTimers();
110
+ var events = [];
111
+ var telemetry = {
112
+ observe: e => {
113
+ events.push(e);
114
+ }
115
+ };
116
+ var scheduler = new FetchScheduler({
117
+ concurrency: 1,
118
+ queueLimit: 1,
119
+ queueTimeout: 10,
120
+ requestTimeout: 30
121
+ });
122
+ var finish;
123
+ var fetchMock = jest.fn(/*#__PURE__*/_asyncToGenerator(function* () {
124
+ return new Response(new ReadableStream({
125
+ start(controller) {
126
+ finish = () => controller.close();
127
+ }
128
+ }));
129
+ }));
130
+ var fetch = createBoundedFetch(createObservedFetch(fetchMock, telemetry, 'content'), scheduler, telemetry);
131
+ var first = fetch('https://example.test/v2/stories/search');
132
+ var failed = expect(first).rejects.toThrow('timed out');
133
+ var next = fetch('https://example.test/v2/stories/search');
134
+ var queued = expect(next).rejects.toThrow('queue wait timed out');
135
+ yield jest.advanceTimersByTimeAsync(31);
136
+ yield failed;
137
+ yield queued;
138
+ expect(events.filter(e => e.type === 'upstream')).toEqual([expect.objectContaining({
139
+ status: '2xx'
140
+ })]);
141
+ expect(events.filter(e => e.type === 'fetch_timeout')).toHaveLength(1);
142
+ expect(events).toContainEqual(expect.objectContaining({
143
+ type: 'fetch_queue',
144
+ outcome: 'wait_timeout'
145
+ }));
146
+ finish();
147
+ yield jest.advanceTimersByTimeAsync(0);
148
+ expect(jest.getTimerCount()).toBe(0);
149
+ }));
150
+ it('a throwing or rejecting observer cannot change successful fetch outcomes', /*#__PURE__*/_asyncToGenerator(function* () {
151
+ var response = new Response('ok');
152
+ for (var observe of [() => {
153
+ throw Error('metrics');
154
+ }, /*#__PURE__*/_asyncToGenerator(function* () {
155
+ throw Error('metrics');
156
+ })]) {
157
+ var fetch = createObservedFetch(/*#__PURE__*/_asyncToGenerator(function* () {
158
+ return response;
159
+ }), {
160
+ observe
161
+ }, 'raw');
162
+ expect(yield fetch('https://example.test')).toBe(response);
163
+ }
164
+ yield tick();
165
+ }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prezly/theme-kit-nextjs",
3
- "version": "10.9.1",
3
+ "version": "10.9.3",
4
4
  "description": "Data layer and utility library for developing Prezly themes with NextJS",
5
5
  "type": "module",
6
6
  "main": "build/index.mjs",
@@ -42,11 +42,11 @@
42
42
  }
43
43
  },
44
44
  "engines": {
45
- "node": ">= 16.x",
45
+ "node": ">= 18.18.0",
46
46
  "npm": ">= 8.x"
47
47
  },
48
48
  "devEngines": {
49
- "node": ">= 16.x",
49
+ "node": ">= 18.18.0",
50
50
  "npm": ">= 8.x"
51
51
  },
52
52
  "repository": {
@@ -78,14 +78,14 @@
78
78
  },
79
79
  "dependencies": {
80
80
  "@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",
81
84
  "@technically/is-not-undefined": "^1.0.0",
82
85
  "@technically/omit-undefined": "^1.0.4",
83
86
  "json-stable-stringify": "^1.2.1",
84
87
  "redis": "^4.7.0",
85
- "url-pattern": "^1.0.3",
86
- "@prezly/theme-kit-core": "^10.9.1",
87
- "@prezly/theme-kit-react": "^10.9.1",
88
- "@prezly/theme-kit-intl": "^10.9.1"
88
+ "url-pattern": "^1.0.3"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@sentry/nextjs": "7.120.4"
@@ -94,7 +94,7 @@
94
94
  "access": "public"
95
95
  },
96
96
  "sideEffects": false,
97
- "gitHead": "51b1ce99cf8ce2c886001077bcaf95cb143f86ce",
97
+ "gitHead": "31134742a0ff3f3e669283f94843c3222eac8ebd",
98
98
  "scripts": {
99
99
  "prebuild": "rimraf --glob build/* *.tsbuildinfo",
100
100
  "build": "pnpm build:types && pnpm build:esm && pnpm build:cjs",
@@ -111,7 +111,7 @@
111
111
  "test:unit": "jest",
112
112
  "test:redis": "node tests/test-redis-resilience.cjs",
113
113
  "test:build": "pnpm test:build:cjs && pnpm test:build:esm && pnpm test:build:types",
114
- "test:build:types": "tsc --esModuleInterop --noEmit build/index.d.ts",
114
+ "test:build:types": "tsc --esModuleInterop --noEmit --target es2020 --moduleResolution node tests/test-build-types.ts",
115
115
  "test:build:esm": "node tests/test-build.mjs",
116
116
  "test:build:cjs": "node tests/test-build.cjs"
117
117
  }