@akinon/next 1.93.0-rc.52 → 1.93.0-snapshot-ZERO-3586-20250826205340

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/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 1.93.0-snapshot-ZERO-3586-20250826205340
4
+
5
+ ### Minor Changes
6
+
7
+ - 5dfeea04: ZERO-2801: Revert ZERO-2801
8
+ - 823d82f9: ZERO-3393: Enhance error handling in checkout middleware to ensure errors are checked for existence before processing
9
+ - 412f0e2: ZERO-3586: Enhance caching functionality by adding support for compressed data storage and retrieval, along with a new method for setting multiple key-value pairs.
10
+ - 28c7ea79: ZERO-3427: Refactor redirect utility to handle undefined URL and improve locale handling
11
+ - e1aa030d: ZERO-3473: Refactor locale handling to prioritize cookie value for matched locale
12
+ - 6e6b0a9e: ZERO-3422: Add pz-flow-payment package
13
+ - 63774a6a: ZERO-3351: Add commerce redirection ignore list functionality and related utility
14
+ - 2d9b2b2c9: ZERO-2816: Add segment to headers
15
+ - 5e1feca6: Revert "ZERO-3286: Add notFound handling for chunk URLs starting with \_next"
16
+ - 40a46853: ZERO-3182: Optimize basket update mutation with optimistic update
17
+ - 5f7edd6: ZERO-3571: Enhance Jest configuration by adding base directory resolution and module name mapping
18
+ - 68bbcb27: ZERO-3393: Fix error handling in checkout middleware to check for errors array length
19
+ - d8be48fb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
20
+ - b55acb76: ZERO-2577: Fix pagination bug and update usePagination hook and ensure pagination controls rendering correctly
21
+ - f49bb74f: ZERO-3097: Add setCookie to logging in payment redirection middlewares
22
+ - 0ad91bb: ZERO-3489: Improve error handling in data fetching across multiple pages and server functions
23
+ - 143be2b9: ZERO-3457: Crop styles are customizable and logic improved for rendering similar products modal
24
+ - e9541a13d: ZERO-2816: Add headers to url
25
+ - 9b7d0de6: ZERO-3393: Improve error handling in checkout middleware to support both object and array error formats
26
+ - 72fd4d67: ZERO-3084: Fix URL search parameters encoding in default middleware
27
+ - c53ef7b95: ZERO-2668: The Link component has been updated to improve the logic for handling href values. Previously, if the href was not a string or started with 'http', it would return the href as is. Now, if the href is not provided, it will default to '#' to prevent any potential errors. Additionally, if the href is a string and does not start with 'http', it will be formatted with the locale and pathname, based on the localeUrlStrategy and defaultLocaleValue. This ensures that the correct href is generated based on the localization settings.
28
+ - a8539c8c: ZERO-3439: Enhance locale handling in middleware and redirect utility
29
+ - 16aff543: ZERO-3431: Add test script for redirect utility in package.json
30
+ - 64699d3ff: ZERO-2761: Fix invalid import for plugin module
31
+ - 9f8cd3bc: ZERO-3449: AI Search Active Filters & Crop Style changes have been implemented
32
+ - e974d8e8: ZERO-3406: Fix rc build
33
+ - 89ce46f: ZERO-3493: return 404 status code for pz-not-found pages
34
+ - 8645d90: ZERO-3574:Refactor redirect tests: streamline mock setup, enhance locale handling, and improve URL path resolution logic
35
+ - 7eb51ca9: ZERO-3424 :Update package versions
36
+ - c806fad7: ZERO-3422: Add Flow Payment plugin to the defined plugins list
37
+ - 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
38
+ - 8b1d24eb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
39
+ - d552629f: ZERO-3182: Refactor basketApi to use invalidatesTags and comment out onQueryStarted logic
40
+ - 17f87524e: ZERO-2816: Make the incoming currency lowercase
41
+ - 65d3b862: ZERO-3054: Update headers in appFetch
42
+ - 0abde6bb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
43
+ - 72ad7bb1: ZERO-3422: Add Flow Payment to the list of available plugins
44
+ - c39c7000: ZERO-3420: Refactor Modal component
45
+ - e7cd3a5e: ZERO-3435: Add Accept-Language to requestHeaders
46
+ - bbe18b9ff: ZERO-2575: Fix build error
47
+ - 17bfadc4: ZERO-3275: Disable OpenTelemetry monitoring in production environment
48
+ - 35dfb8f8: ZERO-3363: Refactor URL handling in checkout and redirection middlewares to use url.origin instead of process.env.NEXT_PUBLIC_URL
49
+ - 4920742c: Disable getCachedTranslations
50
+ - b6e5b624: ZERO-3257: Enhance locale middleware to redirect using existing or default locale and support 303 status for POST requests
51
+ - 0de55738: ZERO-3418: Update remotePatterns hostname to allow all subdomains
52
+ - 7e56d6b6: ZERO-2841: Update api tagTypes
53
+ - dfaceffd: ZERO-3356: Add useLoyaltyAvailability hook and update checkout state management
54
+ - 86642cf: ZERO-3531: Add saveSampleProducts endpoint and update URLs in checkout
55
+ - d99a6a7d: ZERO-3457: Fixed the settings prop and made sure everything is customizable.
56
+ - 9dc7298a: ZERO-3416: Refactor Accordion component to enhance props and improve styling flexibility
57
+ - 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
58
+ - 43c182ee: ZERO-3054: Update Redis variable checks to conditionally include CACHE_SECRET
59
+ - c480272: ZERO-3531: Refactor checkoutApi: Remove unnecessary invalidatesTags property from POST request from sample products
60
+ - b00a90b1: ZERO-3436: Preserve query params on redirect
61
+ - facf1ada: ZERO-3445: Add SameSite and Secure attributes
62
+ - 485e8ef8: ZERO-3422: Refactor parameter handling in wallet complete redirection middleware to use forEach
63
+ - 26b2d0b: ZERO-3571: Remove test script execution from prebuild and simplify Jest module name mapping
64
+ - eeb20bea: Revert "ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing"
65
+ - 99b6e7b9: ZERO-3421: Enhance Sentry error handling by adding network error detection logic and refining initialization options
66
+ - 3bf63c8a: ZERO-3286: Add notFound handling for chunk URLs starting with \_next
67
+ - 9be2c081: ZERO-3243: Improve basket update query handling with optimistic updates
68
+ - f7fd459b: ZERO-3445: Refactor setCookie function to include domain handling and improve cookie string construction
69
+ - 4de5303c: ZERO-2504: add cookie filter to api client request
70
+ - dc678c3: ZERO-3523: Enhance redirect tests with dynamic locale handling and settings integration
71
+ - f2c92d5c7: ZERO-2816: Update cookie name
72
+ - a420947: ZERO-3517: Fix optional chaining for rawData in error logging for category data handlers
73
+ - 7bd3d9928: ZERO-2801: Refactor locale middleware to handle single locale configuration
74
+ - acd2afd: ZERO-3431: Fix import statement for findBaseDir in next-config test
75
+ - 2d3f1788: ZERO-3417: Enhance FileInput component with additional props for customization
76
+ - fdd255ee: ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing
77
+ - b434ac8: ZERO-3545: Update fetchCheckout API URL to include page parameter
78
+ - 49eeebfa: ZERO-2909: Add deleteCollectionItem query to wishlistApi
79
+ - 3f9b8d7e7: ZERO-2761: Update plugins.js for akinon-next
80
+ - fee608dd: ZERO-3422: Refactor body handling in wallet complete redirection middleware
81
+ - cbdb5c14: ZERO-3448: fix set cookie domain handling for subdomain locale strategy
82
+ - 0e82301: ZERO-3531: Add saveSampleProducts endpoint
83
+
3
84
  ## 1.93.0-rc.52
4
85
 
5
86
  ## 1.93.0-rc.51
package/api/cache.ts CHANGED
@@ -21,20 +21,33 @@ async function handleRequest(...args) {
21
21
  }
22
22
 
23
23
  const formData = await req.formData();
24
- const body = {} as { key: string; value?: string; expire?: number };
24
+ const body = {} as { key: string; value?: string; expire?: number; keyValuePairs?: string };
25
25
 
26
26
  formData.forEach((value, key) => {
27
27
  body[key] = value;
28
28
  });
29
29
 
30
- const { key, value, expire } = body;
30
+ const { key, value, expire, keyValuePairs } = body;
31
31
  let response: string | boolean;
32
32
 
33
33
  try {
34
34
  if (req.method === 'POST') {
35
35
  response = await Cache.get(key);
36
36
  } else if (req.method === 'PUT') {
37
- response = await Cache.set(key, value, expire);
37
+ if (keyValuePairs) {
38
+ try {
39
+ const parsedKeyValuePairs = JSON.parse(keyValuePairs);
40
+ if (typeof parsedKeyValuePairs !== 'object' || parsedKeyValuePairs === null || Array.isArray(parsedKeyValuePairs)) {
41
+ throw new Error('Invalid keyValuePairs format - must be an object');
42
+ }
43
+ response = await Cache.mset(parsedKeyValuePairs, expire);
44
+ } catch (error) {
45
+ logger.error('Invalid keyValuePairs in mset request', { error });
46
+ return NextResponse.json({ error: 'Invalid keyValuePairs format' }, { status: 400 });
47
+ }
48
+ } else {
49
+ response = await Cache.set(key, value, expire);
50
+ }
38
51
  }
39
52
  } catch (error) {
40
53
  logger.error(error);
@@ -118,7 +118,8 @@ export const getCategoryData = ({
118
118
  locale,
119
119
  getCategoryDataHandler(pk, locale, currency, searchParams, headers),
120
120
  {
121
- expire: 300
121
+ expire: 300,
122
+ compressed: true
122
123
  }
123
124
  );
124
125
  };
@@ -178,7 +179,8 @@ export const getCategoryBySlugData = async ({
178
179
  locale,
179
180
  getCategoryBySlugDataHandler(slug, locale, currency),
180
181
  {
181
- expire: 300
182
+ expire: 300,
183
+ compressed: true // Compress category data for memory savings
182
184
  }
183
185
  );
184
186
  };
@@ -81,7 +81,8 @@ export const getListData = async ({
81
81
  locale,
82
82
  getListDataHandler(locale, currency, searchParams, headers),
83
83
  {
84
- expire: 300
84
+ expire: 300,
85
+ compressed: true
85
86
  }
86
87
  );
87
88
  };
@@ -48,6 +48,9 @@ export const getMenu = async (params?: MenuHandlerParams) => {
48
48
  return Cache.wrap(
49
49
  CacheKey.Menu(params?.depth ?? DEFAULT_DEPTH, params?.parent),
50
50
  params?.locale ?? ServerVariables.locale,
51
- getMenuHandler(params)
51
+ getMenuHandler(params),
52
+ {
53
+ compressed: true
54
+ }
52
55
  );
53
56
  };
@@ -54,7 +54,8 @@ export const getSpecialPageData = async ({
54
54
  locale,
55
55
  getSpecialPageDataHandler(pk, locale, currency, searchParams, headers),
56
56
  {
57
- expire: 300
57
+ expire: 300,
58
+ compressed: true
58
59
  }
59
60
  );
60
61
  };
package/lib/cache.ts CHANGED
@@ -3,6 +3,11 @@ import { RedisClientType } from 'redis';
3
3
  import Settings from 'settings';
4
4
  import { CacheOptions } from '../types';
5
5
  import logger from '../utils/log';
6
+ import { gzip, gunzip } from 'zlib';
7
+ import { promisify } from 'util';
8
+
9
+ const gzipAsync = promisify(gzip);
10
+ const gunzipAsync = promisify(gunzip);
6
11
 
7
12
  const hashCacheKey = (object?: Record<string, string>) => {
8
13
  if (!object) {
@@ -60,6 +65,28 @@ export const CacheKey = {
60
65
  export class Cache {
61
66
  static PROXY_URL = `${process.env.NEXT_PUBLIC_URL}/api/cache`;
62
67
 
68
+ private static serializeValue(value: any): string {
69
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
70
+ }
71
+
72
+ private static validateKey(key: string): boolean {
73
+ return !(!key || key.trim() === '');
74
+ }
75
+
76
+ private static validateKeyValuePairs(keyValuePairs: Record<string, any>): {
77
+ isValid: boolean;
78
+ invalidKeys: string[];
79
+ } {
80
+ if (!keyValuePairs || Object.keys(keyValuePairs).length === 0) {
81
+ return { isValid: false, invalidKeys: [] };
82
+ }
83
+
84
+ const invalidKeys = Object.keys(keyValuePairs).filter(
85
+ (key) => !this.validateKey(key)
86
+ );
87
+ return { isValid: invalidKeys.length === 0, invalidKeys };
88
+ }
89
+
63
90
  static formatKey(key: string, locale: string) {
64
91
  return encodeURIComponent(`${Settings.commerceUrl}_${locale}_${key}`);
65
92
  }
@@ -98,9 +125,9 @@ export class Cache {
98
125
  return await Cache.clientPool.acquire();
99
126
  }
100
127
 
101
- static async get(key: string) {
102
- let value;
103
- let client;
128
+ static async get(key: string): Promise<any> {
129
+ let value: any;
130
+ let client: RedisClientType | undefined;
104
131
 
105
132
  try {
106
133
  client = await Cache.getClient();
@@ -123,14 +150,13 @@ export class Cache {
123
150
  return value;
124
151
  }
125
152
 
126
- static async set(key: string, value: any, expire?: number) {
153
+ static async set(key: string, value: any, expire?: number): Promise<boolean> {
127
154
  let success = false;
128
- let client;
155
+ let client: RedisClientType | undefined;
129
156
 
130
157
  try {
131
158
  client = await Cache.getClient();
132
- const serializedValue =
133
- typeof value === 'object' ? JSON.stringify(value) : value;
159
+ const serializedValue = Cache.serializeValue(value);
134
160
 
135
161
  if (expire) {
136
162
  await client.set(key, serializedValue, { EX: expire });
@@ -183,7 +209,7 @@ export class Cache {
183
209
  logger.debug('Cache wrap', { key, formattedKey, _options });
184
210
 
185
211
  if (_options.cache) {
186
- let cachedValue;
212
+ let cachedValue: any;
187
213
 
188
214
  if (_options.useProxy) {
189
215
  const body = new URLSearchParams();
@@ -194,7 +220,9 @@ export class Cache {
194
220
  logger.debug('Cache proxy request success', { key });
195
221
  logger.trace('Cache proxy request', { key, cachedValue });
196
222
  } else {
197
- cachedValue = await Cache.get(formattedKey);
223
+ cachedValue = _options.compressed
224
+ ? await Cache.getCompressed(formattedKey)
225
+ : await Cache.get(formattedKey);
198
226
  }
199
227
 
200
228
  if (cachedValue) {
@@ -224,7 +252,11 @@ export class Cache {
224
252
  logger.error('Cache proxy error', error);
225
253
  }
226
254
  } else {
227
- await Cache.set(formattedKey, JSON.stringify(data), _options?.expire);
255
+ if (_options.compressed) {
256
+ await Cache.setCompressed(formattedKey, data, _options?.expire);
257
+ } else {
258
+ await Cache.set(formattedKey, JSON.stringify(data), _options?.expire);
259
+ }
228
260
  }
229
261
  }
230
262
 
@@ -244,4 +276,183 @@ export class Cache {
244
276
 
245
277
  return response;
246
278
  }
279
+
280
+ static async mset(
281
+ keyValuePairs: Record<string, any>,
282
+ expire?: number
283
+ ): Promise<boolean> {
284
+ const validation = Cache.validateKeyValuePairs(keyValuePairs);
285
+ if (!validation.isValid) {
286
+ if (validation.invalidKeys.length > 0) {
287
+ logger.error('Invalid keys in mset', {
288
+ invalidKeys: validation.invalidKeys
289
+ });
290
+ } else {
291
+ logger.warn('mset called with empty keyValuePairs');
292
+ }
293
+ return false;
294
+ }
295
+
296
+ let success = false;
297
+ let client: RedisClientType | undefined;
298
+
299
+ try {
300
+ client = await Cache.getClient();
301
+ const pipeline = client.multi();
302
+
303
+ Object.entries(keyValuePairs).forEach(([key, value]) => {
304
+ const serializedValue = Cache.serializeValue(value);
305
+ if (expire) {
306
+ pipeline.set(key, serializedValue, { EX: expire });
307
+ } else {
308
+ pipeline.set(key, serializedValue);
309
+ }
310
+ });
311
+
312
+ const results = await pipeline.exec();
313
+
314
+ const failures =
315
+ results?.filter((result) => result instanceof Error) || [];
316
+
317
+ if (failures.length > 0) {
318
+ logger.warn('Some mset operations failed', {
319
+ totalOperations: results?.length || 0,
320
+ failures: failures.length,
321
+ keys: Object.keys(keyValuePairs)
322
+ });
323
+ success = false;
324
+ } else {
325
+ success = true;
326
+ logger.debug('Redis mset success', {
327
+ keys: Object.keys(keyValuePairs),
328
+ expire,
329
+ operationsCount: Object.keys(keyValuePairs).length
330
+ });
331
+ }
332
+ } catch (error) {
333
+ logger.error('Redis mset error', {
334
+ keys: Object.keys(keyValuePairs),
335
+ error
336
+ });
337
+ success = false;
338
+ } finally {
339
+ if (client) {
340
+ await Cache.clientPool.release(client);
341
+ }
342
+ }
343
+
344
+ return success;
345
+ }
346
+
347
+ static async setCompressed(
348
+ key: string,
349
+ value: any,
350
+ expire?: number
351
+ ): Promise<boolean> {
352
+ if (!Cache.validateKey(key)) {
353
+ logger.error('Invalid key in setCompressed', { key });
354
+ return false;
355
+ }
356
+
357
+ let success = false;
358
+ let client: RedisClientType | undefined;
359
+
360
+ try {
361
+ client = await Cache.getClient();
362
+ const serializedValue = Cache.serializeValue(value);
363
+
364
+ if (serializedValue.length > 1024 * 1024) {
365
+ logger.warn('Large data being compressed', {
366
+ key,
367
+ size: serializedValue.length
368
+ });
369
+ }
370
+
371
+ const compressed = await gzipAsync(Buffer.from(serializedValue, 'utf8'));
372
+
373
+ if (expire) {
374
+ await client.set(key, compressed, { EX: expire });
375
+ } else {
376
+ await client.set(key, compressed);
377
+ }
378
+
379
+ success = true;
380
+ const compressionRatio = (
381
+ (1 - compressed.length / serializedValue.length) *
382
+ 100
383
+ ).toFixed(2);
384
+ logger.debug('Redis setCompressed success', {
385
+ key,
386
+ originalSize: serializedValue.length,
387
+ compressedSize: compressed.length,
388
+ compressionRatio: `${compressionRatio}%`
389
+ });
390
+ } catch (error) {
391
+ logger.error('Redis setCompressed error', { key, error });
392
+ success = false;
393
+ } finally {
394
+ if (client) {
395
+ await Cache.clientPool.release(client);
396
+ }
397
+ }
398
+
399
+ return success;
400
+ }
401
+
402
+ static async getCompressed(
403
+ key: string,
404
+ maxDecompressedSize: number = 10 * 1024 * 1024
405
+ ): Promise<any> {
406
+ if (!Cache.validateKey(key)) {
407
+ logger.error('Invalid key in getCompressed', { key });
408
+ return null;
409
+ }
410
+
411
+ let value: any;
412
+ let client: RedisClientType | undefined;
413
+
414
+ try {
415
+ client = await Cache.getClient();
416
+ const compressed = await client.get(key);
417
+
418
+ if (compressed) {
419
+ const compressedBuffer = Buffer.isBuffer(compressed)
420
+ ? compressed
421
+ : Buffer.from(compressed, 'binary');
422
+
423
+ const decompressed = await gunzipAsync(compressedBuffer as any);
424
+
425
+ if (decompressed.length > maxDecompressedSize) {
426
+ logger.error('Decompressed data exceeds size limit', {
427
+ key,
428
+ size: decompressed.length,
429
+ limit: maxDecompressedSize
430
+ });
431
+ throw new Error(
432
+ `Decompressed data too large: ${decompressed.length} bytes`
433
+ );
434
+ }
435
+
436
+ value = JSON.parse(decompressed.toString('utf8'));
437
+
438
+ logger.debug('Redis getCompressed success', {
439
+ key,
440
+ compressedSize: compressedBuffer.length,
441
+ decompressedSize: decompressed.length
442
+ });
443
+ } else {
444
+ value = null;
445
+ logger.debug('Redis getCompressed: key not found', { key });
446
+ }
447
+ } catch (error) {
448
+ logger.error('Redis getCompressed error', { key, error });
449
+ value = null;
450
+ } finally {
451
+ if (client) {
452
+ await Cache.clientPool.release(client);
453
+ }
454
+ }
455
+
456
+ return value;
457
+ }
247
458
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "1.93.0-rc.52",
4
+ "version": "1.93.0-snapshot-ZERO-3586-20250826205340",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "set-cookie-parser": "2.6.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@akinon/eslint-plugin-projectzero": "1.93.0-rc.52",
37
+ "@akinon/eslint-plugin-projectzero": "1.93.0-snapshot-ZERO-3586-20250826205340",
38
38
  "@babel/core": "7.26.10",
39
39
  "@babel/preset-env": "7.26.9",
40
40
  "@babel/preset-typescript": "7.27.0",
package/types/index.ts CHANGED
@@ -222,6 +222,7 @@ export interface CacheOptions {
222
222
  cache?: boolean;
223
223
  expire?: number;
224
224
  useProxy?: boolean;
225
+ compressed?: boolean;
225
226
  }
226
227
 
227
228
  export interface SetCookieOptions {