@akinon/next 2.0.0-beta.2 → 2.0.0-beta.20

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.
Files changed (189) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +377 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +133 -44
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-prebuild.js +1 -0
  19. package/bin/pz-predev.js +1 -0
  20. package/bin/pz-run-tests.js +99 -0
  21. package/bin/run-prebuild-tests.js +46 -0
  22. package/components/accordion.tsx +20 -5
  23. package/components/button.tsx +51 -36
  24. package/components/client-root.tsx +138 -2
  25. package/components/file-input.tsx +65 -3
  26. package/components/index.ts +1 -0
  27. package/components/input.tsx +1 -1
  28. package/components/link.tsx +46 -16
  29. package/components/logger-popup.tsx +213 -0
  30. package/components/modal.tsx +32 -16
  31. package/components/plugin-module.tsx +62 -3
  32. package/components/price.tsx +2 -2
  33. package/components/select.tsx +1 -1
  34. package/components/selected-payment-option-view.tsx +21 -0
  35. package/components/theme-editor/blocks/accordion-block.tsx +136 -0
  36. package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
  37. package/components/theme-editor/blocks/button-block.tsx +593 -0
  38. package/components/theme-editor/blocks/counter-block.tsx +348 -0
  39. package/components/theme-editor/blocks/divider-block.tsx +20 -0
  40. package/components/theme-editor/blocks/embed-block.tsx +208 -0
  41. package/components/theme-editor/blocks/group-block.tsx +116 -0
  42. package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
  43. package/components/theme-editor/blocks/icon-block.tsx +230 -0
  44. package/components/theme-editor/blocks/image-block.tsx +137 -0
  45. package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
  46. package/components/theme-editor/blocks/input-block.tsx +123 -0
  47. package/components/theme-editor/blocks/link-block.tsx +216 -0
  48. package/components/theme-editor/blocks/lottie-block.tsx +325 -0
  49. package/components/theme-editor/blocks/map-block.tsx +89 -0
  50. package/components/theme-editor/blocks/slider-block.tsx +595 -0
  51. package/components/theme-editor/blocks/tab-block.tsx +10 -0
  52. package/components/theme-editor/blocks/text-block.tsx +52 -0
  53. package/components/theme-editor/blocks/video-block.tsx +122 -0
  54. package/components/theme-editor/components/action-toolbar.tsx +305 -0
  55. package/components/theme-editor/components/designer-overlay.tsx +74 -0
  56. package/components/theme-editor/components/with-designer-features.tsx +142 -0
  57. package/components/theme-editor/dynamic-font-loader.tsx +79 -0
  58. package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
  59. package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
  60. package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
  61. package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
  62. package/components/theme-editor/placeholder-registry.ts +31 -0
  63. package/components/theme-editor/sections/before-after-section.tsx +245 -0
  64. package/components/theme-editor/sections/contact-form-section.tsx +563 -0
  65. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
  66. package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
  67. package/components/theme-editor/sections/divider-section.tsx +62 -0
  68. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
  69. package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
  70. package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
  71. package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
  72. package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
  73. package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
  74. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
  75. package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
  76. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
  77. package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
  78. package/components/theme-editor/sections/section-wrapper.tsx +135 -0
  79. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
  80. package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
  81. package/components/theme-editor/sections/tabs-section.tsx +578 -0
  82. package/components/theme-editor/theme-block.tsx +102 -0
  83. package/components/theme-editor/theme-placeholder-client.tsx +218 -0
  84. package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
  85. package/components/theme-editor/theme-placeholder.tsx +288 -0
  86. package/components/theme-editor/theme-section.tsx +1224 -0
  87. package/components/theme-editor/theme-settings-context.tsx +13 -0
  88. package/components/theme-editor/utils/index.ts +792 -0
  89. package/components/theme-editor/utils/iterator-utils.ts +234 -0
  90. package/components/theme-editor/utils/publish-window.ts +86 -0
  91. package/components/theme-editor/utils/visibility-rules.ts +188 -0
  92. package/data/client/account.ts +17 -2
  93. package/data/client/api.ts +2 -0
  94. package/data/client/basket.ts +66 -5
  95. package/data/client/checkout.ts +391 -99
  96. package/data/client/misc.ts +38 -2
  97. package/data/client/product.ts +19 -2
  98. package/data/client/user.ts +16 -8
  99. package/data/server/category.ts +11 -9
  100. package/data/server/flatpage.ts +11 -4
  101. package/data/server/form.ts +15 -4
  102. package/data/server/landingpage.ts +11 -4
  103. package/data/server/list.ts +5 -4
  104. package/data/server/menu.ts +11 -3
  105. package/data/server/product.ts +111 -55
  106. package/data/server/seo.ts +14 -4
  107. package/data/server/special-page.ts +5 -4
  108. package/data/server/widget.ts +90 -5
  109. package/data/urls.ts +16 -5
  110. package/hocs/client/with-segment-defaults.tsx +2 -2
  111. package/hocs/server/with-segment-defaults.tsx +65 -20
  112. package/hooks/index.ts +4 -0
  113. package/hooks/use-localization.ts +24 -10
  114. package/hooks/use-logger-context.tsx +114 -0
  115. package/hooks/use-logger.ts +92 -0
  116. package/hooks/use-loyalty-availability.ts +21 -0
  117. package/hooks/use-payment-options.ts +2 -1
  118. package/hooks/use-pz-params.ts +37 -0
  119. package/hooks/use-router.ts +51 -14
  120. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  121. package/instrumentation/index.ts +10 -1
  122. package/instrumentation/node.ts +2 -20
  123. package/jest.config.js +25 -0
  124. package/lib/cache-handler.mjs +534 -16
  125. package/lib/cache.ts +272 -37
  126. package/localization/index.ts +2 -1
  127. package/localization/provider.tsx +2 -5
  128. package/middlewares/bfcache-headers.ts +18 -0
  129. package/middlewares/checkout-provider.ts +1 -1
  130. package/middlewares/complete-gpay.ts +32 -26
  131. package/middlewares/complete-masterpass.ts +33 -26
  132. package/middlewares/complete-wallet.ts +182 -0
  133. package/middlewares/default.ts +360 -215
  134. package/middlewares/index.ts +10 -2
  135. package/middlewares/locale.ts +34 -11
  136. package/middlewares/masterpass-rest-callback.ts +230 -0
  137. package/middlewares/oauth-login.ts +200 -57
  138. package/middlewares/pretty-url.ts +21 -8
  139. package/middlewares/redirection-payment.ts +32 -26
  140. package/middlewares/saved-card-redirection.ts +33 -26
  141. package/middlewares/three-d-redirection.ts +32 -26
  142. package/middlewares/url-redirection.ts +11 -1
  143. package/middlewares/wallet-complete-redirection.ts +206 -0
  144. package/package.json +25 -10
  145. package/plugins.d.ts +19 -4
  146. package/plugins.js +10 -1
  147. package/redux/actions.ts +47 -0
  148. package/redux/middlewares/checkout.ts +63 -138
  149. package/redux/middlewares/index.ts +14 -10
  150. package/redux/middlewares/pre-order/address.ts +7 -2
  151. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  152. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  153. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  154. package/redux/middlewares/pre-order/index.ts +16 -10
  155. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  156. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  157. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  158. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  159. package/redux/middlewares/pre-order/redirection.ts +8 -2
  160. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  161. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  162. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  163. package/redux/reducers/checkout.ts +23 -3
  164. package/redux/reducers/index.ts +11 -3
  165. package/redux/reducers/root.ts +7 -2
  166. package/redux/reducers/widget.ts +80 -0
  167. package/sentry/index.ts +69 -13
  168. package/tailwind/content.js +16 -0
  169. package/types/commerce/account.ts +5 -1
  170. package/types/commerce/checkout.ts +35 -1
  171. package/types/commerce/widget.ts +33 -0
  172. package/types/index.ts +101 -6
  173. package/types/next-auth.d.ts +2 -2
  174. package/types/widget.ts +80 -0
  175. package/utils/app-fetch.ts +7 -2
  176. package/utils/generate-commerce-search-params.ts +3 -2
  177. package/utils/get-checkout-path.ts +3 -0
  178. package/utils/get-root-hostname.ts +28 -0
  179. package/utils/index.ts +64 -10
  180. package/utils/localization.ts +4 -0
  181. package/utils/mobile-3d-iframe.ts +8 -2
  182. package/utils/override-middleware.ts +7 -12
  183. package/utils/pz-segments.ts +92 -0
  184. package/utils/redirect-ignore.ts +35 -0
  185. package/utils/redirect.ts +9 -3
  186. package/utils/redirection-iframe.ts +8 -2
  187. package/utils/widget-styles.ts +107 -0
  188. package/views/error-page.tsx +93 -0
  189. package/with-pz-config.js +13 -6
@@ -1,32 +1,550 @@
1
1
  import { CacheHandler } from '@neshca/cache-handler';
2
2
  import createLruHandler from '@neshca/cache-handler/local-lru';
3
- import createRedisHandler from '@neshca/cache-handler/redis-stack';
3
+ import createRedisHandler from '@neshca/cache-handler/redis-strings';
4
4
  import { createClient } from 'redis';
5
5
 
6
- CacheHandler.onCreation(async () => {
7
- const redisUrl = `redis://${process.env.CACHE_HOST}:${
8
- process.env.CACHE_PORT
9
- }/${process.env.CACHE_BUCKET ?? '0'}`;
6
+ let zstd;
10
7
 
11
- const client = createClient({
12
- url: redisUrl
13
- });
8
+ (async () => {
9
+ try {
10
+ const { compress, decompress } = await import('@mongodb-js/zstd');
11
+ zstd = { compress, decompress, type: 'native' };
12
+ } catch (_) {
13
+ zstd = false;
14
+ }
15
+ })();
14
16
 
15
- client.on('error', (error) => {
16
- console.error('Redis client error', { redisUrl, error });
17
- });
17
+ const getZstd = () => {
18
+ return zstd;
19
+ };
20
+
21
+ const compressValue = async (value) => {
22
+ try {
23
+ if (value && typeof value === 'object' && value.value !== undefined) {
24
+ const nestedValue = value.value;
25
+ const serializedNestedValue =
26
+ typeof nestedValue === 'string'
27
+ ? nestedValue
28
+ : JSON.stringify(nestedValue);
29
+ const originalSize = Buffer.byteLength(serializedNestedValue, 'utf8');
30
+
31
+ if (originalSize < 1024) {
32
+ const result = {
33
+ ...value,
34
+ tags: Array.isArray(value.tags) ? value.tags : []
35
+ };
36
+ return result;
37
+ }
38
+
39
+ const zstdLib = getZstd();
40
+ let compressed;
41
+
42
+ if (zstdLib && zstdLib !== false) {
43
+ const inputBuffer = Buffer.from(serializedNestedValue, 'utf8');
44
+
45
+ if (
46
+ typeof zstdLib.compress === 'function' &&
47
+ zstdLib.compress.constructor.name === 'AsyncFunction'
48
+ ) {
49
+ compressed = await zstdLib.compress(inputBuffer, 3);
50
+ } else {
51
+ compressed = zstdLib.compress(inputBuffer, 3);
52
+ }
53
+ } else {
54
+ return {
55
+ ...value,
56
+ tags: Array.isArray(value.tags) ? value.tags : []
57
+ };
58
+ }
59
+
60
+ const compressedBase64 = Buffer.from(compressed).toString('base64');
61
+
62
+ const result = {
63
+ ...value,
64
+ tags: Array.isArray(value.tags) ? value.tags : [],
65
+ lifespan: {
66
+ ...value.lifespan,
67
+ expireAge: value.lifespan?.revalidate || value.lifespan?.expireAge,
68
+ expireAt:
69
+ value.lifespan?.lastModifiedAt && value.lifespan?.revalidate
70
+ ? value.lifespan.lastModifiedAt + value.lifespan.revalidate
71
+ : value.lifespan?.expireAt
72
+ },
73
+ value: {
74
+ __compressed: true,
75
+ __method: 'zstd',
76
+ __originalSize: originalSize,
77
+ __compressedSize: compressed.length,
78
+ __data: compressedBase64
79
+ }
80
+ };
81
+
82
+ return result;
83
+ }
84
+
85
+ const serializedValue =
86
+ typeof value === 'string' ? value : JSON.stringify(value);
87
+ const originalSize = Buffer.byteLength(serializedValue, 'utf8');
88
+
89
+ if (originalSize < 1024) {
90
+ if (
91
+ value &&
92
+ typeof value === 'object' &&
93
+ value.lastModified === undefined &&
94
+ value.lifespan === undefined &&
95
+ value.value === undefined
96
+ ) {
97
+ return {
98
+ ...value,
99
+ tags: value.tags || [],
100
+ lastModified: Date.now(),
101
+ lifespan: {
102
+ expireAt: Math.floor(Date.now() / 1000) + 3600
103
+ }
104
+ };
105
+ }
106
+ if (
107
+ value &&
108
+ typeof value === 'object' &&
109
+ value.lifespan &&
110
+ value.lifespan.revalidate
111
+ ) {
112
+ return {
113
+ ...value,
114
+ lifespan: {
115
+ ...value.lifespan,
116
+ expireAge: value.lifespan.revalidate,
117
+ expireAt:
118
+ value.lifespan.lastModifiedAt && value.lifespan.revalidate
119
+ ? value.lifespan.lastModifiedAt + value.lifespan.revalidate
120
+ : value.lifespan.expireAt
121
+ }
122
+ };
123
+ }
124
+ return value;
125
+ }
126
+
127
+ const zstdLib = getZstd();
128
+ let compressed;
129
+
130
+ if (zstdLib && zstdLib !== false) {
131
+ const inputBuffer = Buffer.from(serializedValue, 'utf8');
132
+
133
+ if (
134
+ typeof zstdLib.compress === 'function' &&
135
+ zstdLib.compress.constructor.name === 'AsyncFunction'
136
+ ) {
137
+ compressed = await zstdLib.compress(inputBuffer, 3);
138
+ } else {
139
+ compressed = zstdLib.compress(inputBuffer, 3);
140
+ }
141
+ } else {
142
+ if (
143
+ value &&
144
+ typeof value === 'object' &&
145
+ value.lastModified === undefined &&
146
+ value.lifespan === undefined &&
147
+ value.value === undefined
148
+ ) {
149
+ return {
150
+ ...value,
151
+ tags: value.tags || [],
152
+ lastModified: Date.now(),
153
+ lifespan: {
154
+ expireAt: Math.floor(Date.now() / 1000) + 3600
155
+ }
156
+ };
157
+ }
158
+ return value;
159
+ }
160
+
161
+ const compressedBase64 = Buffer.from(compressed).toString('base64');
162
+
163
+ const compressedResult = {
164
+ __compressed: true,
165
+ __method: 'zstd',
166
+ __originalSize: originalSize,
167
+ __compressedSize: compressed.length,
168
+ __data: compressedBase64,
169
+ tags: [],
170
+ lastModified: Date.now(),
171
+ lifespan: { expireAt: Math.floor(Date.now() / 1000) + 3600 }
172
+ };
173
+
174
+ return compressedResult;
175
+ } catch (_) {
176
+ return value;
177
+ }
178
+ };
179
+
180
+ const decompressValue = async (compressedData) => {
181
+ try {
182
+ if (
183
+ compressedData &&
184
+ typeof compressedData === 'object' &&
185
+ compressedData.value &&
186
+ typeof compressedData.value === 'object' &&
187
+ compressedData.value.__compressed
188
+ ) {
189
+ const compressedNestedValue = compressedData.value;
190
+ const compressedBuffer = Buffer.from(
191
+ compressedNestedValue.__data,
192
+ 'base64'
193
+ );
194
+ let decompressed;
195
+
196
+ if (compressedNestedValue.__method === 'zstd') {
197
+ const zstdLib = getZstd();
198
+ if (zstdLib && zstdLib !== false) {
199
+ if (
200
+ typeof zstdLib.decompress === 'function' &&
201
+ zstdLib.decompress.constructor.name === 'AsyncFunction'
202
+ ) {
203
+ const decompressedBuffer = await zstdLib.decompress(
204
+ compressedBuffer
205
+ );
206
+ decompressed = decompressedBuffer.toString('utf8');
207
+ } else {
208
+ decompressed = zstdLib
209
+ .decompress(compressedBuffer)
210
+ .toString('utf8');
211
+ }
212
+ } else {
213
+ throw new Error('zstd not available for decompression');
214
+ }
215
+ }
216
+
217
+ return {
218
+ ...compressedData,
219
+ value: JSON.parse(decompressed)
220
+ };
221
+ }
222
+
223
+ if (
224
+ compressedData &&
225
+ typeof compressedData === 'object' &&
226
+ compressedData.__compressed
227
+ ) {
228
+ const compressedBuffer = Buffer.from(compressedData.__data, 'base64');
229
+ let decompressed;
230
+
231
+ if (compressedData.__method === 'zstd') {
232
+ const zstdLib = getZstd();
233
+ if (zstdLib && zstdLib !== false) {
234
+ if (
235
+ typeof zstdLib.decompress === 'function' &&
236
+ zstdLib.decompress.constructor.name === 'AsyncFunction'
237
+ ) {
238
+ const decompressedBuffer = await zstdLib.decompress(
239
+ compressedBuffer
240
+ );
241
+ decompressed = decompressedBuffer.toString('utf8');
242
+ } else {
243
+ decompressed = zstdLib
244
+ .decompress(compressedBuffer)
245
+ .toString('utf8');
246
+ }
247
+ } else {
248
+ throw new Error('zstd not available for decompression');
249
+ }
250
+ }
251
+
252
+ return JSON.parse(decompressed);
253
+ }
254
+
255
+ return compressedData;
256
+ } catch (error) {
257
+ return compressedData;
258
+ }
259
+ };
260
+
261
+ const CACHE_CONFIG = {
262
+ lru: {
263
+ maxItemCount: 2000
264
+ },
265
+ redis: {
266
+ timeoutMs: 5000,
267
+ reconnectStrategy: {
268
+ maxRetries: 3,
269
+ retryDelay: (retries) => Math.min(retries * 100, 3000)
270
+ },
271
+ connectTimeout: 500
272
+ },
273
+ host: process.env.CACHE_HOST || 'localhost',
274
+ port: process.env.CACHE_PORT || '6379',
275
+ bucket: process.env.CACHE_BUCKET ?? '0',
276
+ version: process.env.ACC_APP_VERSION || ''
277
+ };
278
+
279
+ const globalForRedis = global;
280
+ if (!globalForRedis.redisClient) {
281
+ globalForRedis.redisClient = null;
282
+ globalForRedis.isConnecting = false;
283
+ globalForRedis.hasLoggedConnectionError = false;
284
+ globalForRedis.connectionAttempts = 0;
285
+ }
286
+
287
+ async function getRedisClient() {
288
+ if (globalForRedis.redisClient?.isReady) {
289
+ return globalForRedis.redisClient;
290
+ }
291
+
292
+ if (globalForRedis.isConnecting) {
293
+ await new Promise((resolve) => setTimeout(resolve, 100));
294
+ return getRedisClient();
295
+ }
296
+
297
+ globalForRedis.isConnecting = true;
298
+ globalForRedis.connectionAttempts++;
299
+
300
+ try {
301
+ const redisUrl = `redis://${CACHE_CONFIG.host}:${CACHE_CONFIG.port}/${CACHE_CONFIG.bucket}`;
302
+
303
+ const redisClient = createClient({
304
+ url: redisUrl,
305
+ socket: {
306
+ reconnectStrategy: (retries) => {
307
+ if (retries > CACHE_CONFIG.redis.reconnectStrategy.maxRetries) {
308
+ if (!globalForRedis.hasLoggedConnectionError) {
309
+ console.warn(
310
+ '[Cache Handler] Redis is not available, falling back to local cache only'
311
+ );
312
+ globalForRedis.hasLoggedConnectionError = true;
313
+ }
314
+ return false;
315
+ }
316
+ return CACHE_CONFIG.redis.reconnectStrategy.retryDelay(retries);
317
+ },
318
+ connectTimeout: CACHE_CONFIG.redis.connectTimeout
319
+ }
320
+ });
18
321
 
19
- await client.connect();
322
+ redisClient.on('error', (error) => {
323
+ if (!globalForRedis.hasLoggedConnectionError) {
324
+ if (error.code === 'ECONNREFUSED') {
325
+ console.warn(
326
+ '[Cache Handler] Redis connection refused. Is Redis running? Falling back to local cache.'
327
+ );
328
+ } else {
329
+ console.error('[Cache Handler] Redis client error:', error.message);
330
+ }
331
+ globalForRedis.hasLoggedConnectionError = true;
332
+ }
333
+ });
20
334
 
21
- const redisHandler = await createRedisHandler({
335
+ redisClient.on('connect', () => {
336
+ globalForRedis.hasLoggedConnectionError = false;
337
+ });
338
+
339
+ redisClient.on('ready', () => {
340
+ globalForRedis.hasLoggedConnectionError = false;
341
+ });
342
+
343
+ await redisClient.connect();
344
+ globalForRedis.redisClient = redisClient;
345
+ return redisClient;
346
+ } catch (error) {
347
+ if (!globalForRedis.hasLoggedConnectionError) {
348
+ globalForRedis.hasLoggedConnectionError = true;
349
+ }
350
+ globalForRedis.redisClient = null;
351
+ throw error;
352
+ } finally {
353
+ globalForRedis.isConnecting = false;
354
+ }
355
+ }
356
+
357
+ CacheHandler.onCreation(async () => {
358
+ let client;
359
+ try {
360
+ client = await getRedisClient();
361
+ } catch (error) {
362
+ return {
363
+ handlers: [createLruHandler(CACHE_CONFIG.lru)]
364
+ };
365
+ }
366
+
367
+ const redisOptions = {
22
368
  client,
23
- timeoutMs: 5000
369
+ timeoutMs: CACHE_CONFIG.redis.timeoutMs,
370
+ keyExpirationStrategy: 'EXPIREAT'
371
+ };
372
+
373
+ if (process.env.CACHE_PASSWORD) {
374
+ redisOptions.password = process.env.CACHE_PASSWORD;
375
+ }
376
+
377
+ const redisHandler = createRedisHandler(redisOptions);
378
+
379
+ const localHandler = createLruHandler(CACHE_CONFIG.lru);
380
+
381
+ const CACHE_VERSION = 'v2';
382
+ const versionPrefix = `${CACHE_VERSION}_`;
383
+
384
+ const versionKeyString = (key) => `${versionPrefix}${key}`;
385
+ const versionKeyObject = (key) => ({
386
+ ...key,
387
+ key: `${versionPrefix}${key.key}`
24
388
  });
25
389
 
26
- const localHandler = createLruHandler();
390
+ const versionKey = (key) => {
391
+ return typeof key === 'string'
392
+ ? versionKeyString(key)
393
+ : versionKeyObject(key);
394
+ };
395
+
396
+ const customHandler = {
397
+ name: 'custom-local-then-redis',
398
+ get: async (key, context) => {
399
+ const vKey = versionKey(key);
400
+
401
+ const localResult = await localHandler.get(vKey, context);
402
+
403
+ if (localResult) {
404
+ if (
405
+ localResult &&
406
+ typeof localResult === 'object' &&
407
+ (localResult.__compressed ||
408
+ (localResult.value && localResult.value.__compressed) ||
409
+ localResult.compressed !== undefined)
410
+ ) {
411
+ try {
412
+ const decompressed = await decompressValue(localResult);
413
+ return typeof decompressed === 'string'
414
+ ? JSON.parse(decompressed)
415
+ : decompressed;
416
+ } catch (_) {
417
+ return localResult;
418
+ }
419
+ }
420
+
421
+ return localResult;
422
+ }
423
+
424
+ try {
425
+ const redisResult = await redisHandler.get(vKey, context);
426
+
427
+ if (redisResult) {
428
+ let finalResult = redisResult;
429
+
430
+ if (typeof redisResult === 'string') {
431
+ try {
432
+ finalResult = JSON.parse(redisResult);
433
+ } catch (parseError) {
434
+ finalResult = redisResult;
435
+ }
436
+ }
437
+
438
+ if (
439
+ finalResult &&
440
+ typeof finalResult === 'object' &&
441
+ (finalResult.__compressed ||
442
+ (finalResult.value && finalResult.value.__compressed) ||
443
+ finalResult.compressed !== undefined)
444
+ ) {
445
+ try {
446
+ const decompressed = await decompressValue(finalResult);
447
+ finalResult =
448
+ typeof decompressed === 'string'
449
+ ? JSON.parse(decompressed)
450
+ : decompressed;
451
+ } catch (_) {
452
+ return finalResult;
453
+ }
454
+ }
455
+
456
+ try {
457
+ await localHandler.set(vKey, finalResult, context);
458
+ } catch (_) {
459
+ return finalResult;
460
+ }
461
+ return finalResult;
462
+ }
463
+ } catch (_) {
464
+ return undefined;
465
+ }
466
+
467
+ return undefined;
468
+ },
469
+ set: async (key, value, context) => {
470
+ const vKey = versionKey(key);
471
+
472
+ const stripTags = (val) => {
473
+ if (val && typeof val === 'object') {
474
+ const { tags, ...rest } = val;
475
+ return { ...rest, tags: [] };
476
+ }
477
+ return val;
478
+ };
479
+
480
+ let compressedValue;
481
+ let shouldUseCompressed = false;
482
+
483
+ try {
484
+ compressedValue = await compressValue(value);
485
+
486
+ shouldUseCompressed =
487
+ compressedValue !== value &&
488
+ (compressedValue?.__compressed ||
489
+ compressedValue?.value?.__compressed);
490
+ } catch (error) {
491
+ compressedValue = value;
492
+ shouldUseCompressed = false;
493
+ }
494
+
495
+ let redisSetResult;
496
+
497
+ if (shouldUseCompressed) {
498
+ try {
499
+ await redisHandler.set(vKey, stripTags(compressedValue), context);
500
+
501
+ redisSetResult = { status: 'fulfilled' };
502
+ } catch (compressionError) {
503
+ try {
504
+ await redisHandler.set(vKey, stripTags(value), context);
505
+
506
+ redisSetResult = { status: 'fulfilled' };
507
+ } catch (fallbackError) {
508
+ redisSetResult = { status: 'rejected', reason: fallbackError };
509
+ }
510
+ }
511
+ } else {
512
+ try {
513
+ await redisHandler.set(vKey, stripTags(value), context);
514
+ redisSetResult = { status: 'fulfilled' };
515
+ } catch (error) {
516
+ redisSetResult = { status: 'rejected', reason: error };
517
+ return redisSetResult;
518
+ }
519
+ }
520
+
521
+ let localSetResult;
522
+ try {
523
+ await localHandler.set(vKey, value, context);
524
+ localSetResult = { status: 'fulfilled' };
525
+ } catch (error) {
526
+ localSetResult = { status: 'rejected', reason: error };
527
+ }
528
+ return localSetResult;
529
+ },
530
+ delete: async (key, context) => {
531
+ const vKey = versionKey(key);
532
+
533
+ await Promise.allSettled([
534
+ localHandler.delete?.(vKey, context),
535
+ redisHandler.delete?.(vKey, context)
536
+ ]);
537
+ },
538
+ revalidateTag: async (tags, context) => {
539
+ await Promise.allSettled([
540
+ localHandler.revalidateTag?.(tags, context),
541
+ redisHandler.revalidateTag?.(tags, context)
542
+ ]);
543
+ }
544
+ };
27
545
 
28
546
  return {
29
- handlers: [redisHandler, localHandler]
547
+ handlers: [customHandler]
30
548
  };
31
549
  });
32
550