@akinon/next 1.94.0 → 1.95.0-snapshot-ZERO-3586-20250901132537

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 (53) hide show
  1. package/CHANGELOG.md +1330 -49
  2. package/__tests__/next-config.test.ts +1 -10
  3. package/__tests__/redirect.test.ts +319 -0
  4. package/api/cache.ts +41 -5
  5. package/api/image-proxy.ts +75 -0
  6. package/api/similar-product-list.ts +84 -0
  7. package/api/similar-products.ts +120 -0
  8. package/components/accordion.tsx +20 -5
  9. package/components/file-input.tsx +65 -3
  10. package/components/input.tsx +2 -0
  11. package/components/link.tsx +16 -12
  12. package/components/modal.tsx +32 -16
  13. package/components/plugin-module.tsx +30 -3
  14. package/data/client/checkout.ts +5 -4
  15. package/data/server/basket.ts +72 -0
  16. package/data/server/category.ts +52 -30
  17. package/data/server/flatpage.ts +20 -13
  18. package/data/server/form.ts +4 -1
  19. package/data/server/landingpage.ts +20 -13
  20. package/data/server/list.ts +25 -14
  21. package/data/server/menu.ts +4 -1
  22. package/data/server/product.ts +68 -40
  23. package/data/server/seo.ts +4 -1
  24. package/data/server/special-page.ts +18 -13
  25. package/data/server/widget.ts +4 -1
  26. package/data/urls.ts +5 -1
  27. package/hocs/server/with-segment-defaults.tsx +5 -2
  28. package/hooks/use-localization.ts +2 -3
  29. package/jest.config.js +7 -1
  30. package/lib/cache-handler.mjs +359 -85
  31. package/lib/cache.ts +254 -25
  32. package/middlewares/checkout-provider.ts +1 -1
  33. package/middlewares/complete-gpay.ts +2 -1
  34. package/middlewares/complete-masterpass.ts +2 -1
  35. package/middlewares/default.ts +50 -13
  36. package/middlewares/locale.ts +9 -1
  37. package/middlewares/pretty-url.ts +2 -1
  38. package/middlewares/redirection-payment.ts +2 -1
  39. package/middlewares/saved-card-redirection.ts +2 -1
  40. package/middlewares/three-d-redirection.ts +2 -1
  41. package/middlewares/url-redirection.ts +9 -15
  42. package/package.json +4 -3
  43. package/plugins.d.ts +8 -0
  44. package/plugins.js +3 -1
  45. package/redux/middlewares/checkout.ts +5 -1
  46. package/sentry/index.ts +54 -17
  47. package/types/commerce/order.ts +1 -0
  48. package/types/index.ts +43 -1
  49. package/utils/app-fetch.ts +7 -2
  50. package/utils/index.ts +34 -10
  51. package/utils/redirect-ignore.ts +35 -0
  52. package/utils/redirect.ts +31 -6
  53. package/with-pz-config.js +2 -5
@@ -2,8 +2,236 @@ import { CacheHandler } from '@neshca/cache-handler';
2
2
  import createLruHandler from '@neshca/cache-handler/local-lru';
3
3
  import createRedisHandler from '@neshca/cache-handler/redis-strings';
4
4
  import { createClient } from 'redis';
5
+ import * as zstdWasm from '@bokuweb/zstd-wasm';
6
+
7
+ let zstd;
8
+
9
+ (async () => {
10
+ try {
11
+ await zstdWasm.init();
12
+ zstd = zstdWasm;
13
+ } catch (error) {
14
+ zstd = false;
15
+ }
16
+ })();
17
+
18
+ const getZstd = () => {
19
+ return zstd;
20
+ };
21
+
22
+ const compressValue = async (value) => {
23
+ try {
24
+ if (value && typeof value === 'object' && value.value !== undefined) {
25
+ const nestedValue = value.value;
26
+ const serializedNestedValue =
27
+ typeof nestedValue === 'string'
28
+ ? nestedValue
29
+ : JSON.stringify(nestedValue);
30
+ const originalSize = Buffer.byteLength(serializedNestedValue, 'utf8');
31
+
32
+ if (originalSize < 1024) {
33
+ const result = {
34
+ ...value,
35
+ tags: Array.isArray(value.tags) ? value.tags : []
36
+ };
37
+ return result;
38
+ }
39
+
40
+ const zstdLib = getZstd();
41
+ let compressed;
42
+
43
+ if (zstdLib && zstdLib !== false) {
44
+ compressed = zstdLib.compress(
45
+ Buffer.from(serializedNestedValue, 'utf8'),
46
+ 3
47
+ );
48
+ } else {
49
+ return {
50
+ ...value,
51
+ tags: Array.isArray(value.tags) ? value.tags : []
52
+ };
53
+ }
54
+
55
+ const compressedBase64 = Buffer.from(compressed).toString('base64');
56
+
57
+ const result = {
58
+ ...value,
59
+ tags: Array.isArray(value.tags) ? value.tags : [],
60
+ lifespan: {
61
+ ...value.lifespan,
62
+ expireAge: value.lifespan?.revalidate || value.lifespan?.expireAge,
63
+ expireAt:
64
+ value.lifespan?.lastModifiedAt && value.lifespan?.revalidate
65
+ ? value.lifespan.lastModifiedAt + value.lifespan.revalidate
66
+ : value.lifespan?.expireAt
67
+ },
68
+ value: {
69
+ __compressed: true,
70
+ __method: 'zstd',
71
+ __originalSize: originalSize,
72
+ __compressedSize: compressed.length,
73
+ __data: compressedBase64
74
+ }
75
+ };
76
+
77
+ return result;
78
+ }
79
+
80
+ const serializedValue =
81
+ typeof value === 'string' ? value : JSON.stringify(value);
82
+ const originalSize = Buffer.byteLength(serializedValue, 'utf8');
83
+
84
+ if (originalSize < 1024) {
85
+ if (
86
+ value &&
87
+ typeof value === 'object' &&
88
+ value.lastModified === undefined &&
89
+ value.lifespan === undefined &&
90
+ value.value === undefined
91
+ ) {
92
+ return {
93
+ ...value,
94
+ tags: value.tags || [],
95
+ lastModified: Date.now(),
96
+ lifespan: {
97
+ expireAt: Math.floor(Date.now() / 1000) + 3600
98
+ }
99
+ };
100
+ }
101
+ if (
102
+ value &&
103
+ typeof value === 'object' &&
104
+ value.lifespan &&
105
+ value.lifespan.revalidate
106
+ ) {
107
+ return {
108
+ ...value,
109
+ lifespan: {
110
+ ...value.lifespan,
111
+ expireAge: value.lifespan.revalidate,
112
+ expireAt:
113
+ value.lifespan.lastModifiedAt && value.lifespan.revalidate
114
+ ? value.lifespan.lastModifiedAt + value.lifespan.revalidate
115
+ : value.lifespan.expireAt
116
+ }
117
+ };
118
+ }
119
+ return value;
120
+ }
121
+
122
+ const zstdLib = getZstd();
123
+ let compressed;
124
+
125
+ if (zstdLib && zstdLib !== false) {
126
+ compressed = zstdLib.compress(Buffer.from(serializedValue, 'utf8'), 3);
127
+ } else {
128
+ if (
129
+ value &&
130
+ typeof value === 'object' &&
131
+ value.lastModified === undefined &&
132
+ value.lifespan === undefined &&
133
+ value.value === undefined
134
+ ) {
135
+ return {
136
+ ...value,
137
+ tags: value.tags || [],
138
+ lastModified: Date.now(),
139
+ lifespan: {
140
+ expireAt: Math.floor(Date.now() / 1000) + 3600
141
+ }
142
+ };
143
+ }
144
+ return value;
145
+ }
146
+
147
+ const compressedBase64 = Buffer.from(compressed).toString('base64');
148
+
149
+ const compressedResult = {
150
+ __compressed: true,
151
+ __method: 'zstd',
152
+ __originalSize: originalSize,
153
+ __compressedSize: compressed.length,
154
+ __data: compressedBase64,
155
+ tags: [],
156
+ lastModified: Date.now(),
157
+ lifespan: { expireAt: Math.floor(Date.now() / 1000) + 3600 }
158
+ };
159
+
160
+ return compressedResult;
161
+ } catch (error) {
162
+ console.warn(
163
+ '[Cache Handler] Compression failed, storing uncompressed:',
164
+ error.message
165
+ );
166
+ return value;
167
+ }
168
+ };
169
+
170
+ const decompressValue = async (compressedData) => {
171
+ try {
172
+ if (
173
+ compressedData &&
174
+ typeof compressedData === 'object' &&
175
+ compressedData.value &&
176
+ typeof compressedData.value === 'object' &&
177
+ compressedData.value.__compressed
178
+ ) {
179
+ const compressedNestedValue = compressedData.value;
180
+ const compressedBuffer = Buffer.from(
181
+ compressedNestedValue.__data,
182
+ 'base64'
183
+ );
184
+ let decompressed;
185
+
186
+ if (compressedNestedValue.__method === 'zstd') {
187
+ const zstdLib = getZstd();
188
+ if (zstdLib && zstdLib !== false) {
189
+ decompressed = zstdLib.decompress(compressedBuffer).toString('utf8');
190
+ } else {
191
+ throw new Error('zstd not available for decompression');
192
+ }
193
+ } else {
194
+ throw new Error(
195
+ 'gzip decompression no longer supported - please invalidate cache'
196
+ );
197
+ }
198
+
199
+ return {
200
+ ...compressedData,
201
+ value: JSON.parse(decompressed)
202
+ };
203
+ }
204
+
205
+ if (
206
+ compressedData &&
207
+ typeof compressedData === 'object' &&
208
+ compressedData.__compressed
209
+ ) {
210
+ const compressedBuffer = Buffer.from(compressedData.__data, 'base64');
211
+ let decompressed;
212
+
213
+ if (compressedData.__method === 'zstd') {
214
+ const zstdLib = getZstd();
215
+ if (zstdLib && zstdLib !== false) {
216
+ decompressed = zstdLib.decompress(compressedBuffer).toString('utf8');
217
+ } else {
218
+ throw new Error('zstd not available for decompression');
219
+ }
220
+ } else {
221
+ throw new Error(
222
+ 'gzip decompression no longer supported - please invalidate cache'
223
+ );
224
+ }
225
+
226
+ return JSON.parse(decompressed);
227
+ }
228
+
229
+ return compressedData;
230
+ } catch (error) {
231
+ return compressedData;
232
+ }
233
+ };
5
234
 
6
- // Cache configuration
7
235
  const CACHE_CONFIG = {
8
236
  lru: {
9
237
  maxItemCount: 2000
@@ -22,7 +250,6 @@ const CACHE_CONFIG = {
22
250
  version: process.env.ACC_APP_VERSION || ''
23
251
  };
24
252
 
25
- // Use global to persist across module reloads in development
26
253
  const globalForRedis = global;
27
254
  if (!globalForRedis.redisClient) {
28
255
  globalForRedis.redisClient = null;
@@ -31,42 +258,22 @@ if (!globalForRedis.redisClient) {
31
258
  globalForRedis.connectionAttempts = 0;
32
259
  }
33
260
 
34
- // Logging configuration
35
- const debugValue = process.env.NEXT_PRIVATE_DEBUG_CACHE;
36
- const debug = debugValue === 'true' || debugValue === '1';
37
-
38
- let console_log;
39
- if (debug) {
40
- // eslint-disable-next-line no-console
41
- console_log = (...args) => console.log('[Cache Handler]', ...args);
42
- } else {
43
- console_log = () => {};
44
- }
45
-
46
261
  async function getRedisClient() {
47
- // If client exists and is ready, return it
48
262
  if (globalForRedis.redisClient?.isReady) {
49
- console_log('Reusing existing Redis connection');
50
263
  return globalForRedis.redisClient;
51
264
  }
52
265
 
53
- // If we're already connecting, wait a bit and retry
54
266
  if (globalForRedis.isConnecting) {
55
267
  await new Promise((resolve) => setTimeout(resolve, 100));
56
268
  return getRedisClient();
57
269
  }
58
270
 
59
- // Start new connection
60
271
  globalForRedis.isConnecting = true;
61
272
  globalForRedis.connectionAttempts++;
62
273
 
63
274
  try {
64
275
  const redisUrl = `redis://${CACHE_CONFIG.host}:${CACHE_CONFIG.port}/${CACHE_CONFIG.bucket}`;
65
276
 
66
- if (globalForRedis.connectionAttempts === 1) {
67
- console_log('Creating Redis connection:', redisUrl);
68
- }
69
-
70
277
  const redisClient = createClient({
71
278
  url: redisUrl,
72
279
  socket: {
@@ -87,7 +294,6 @@ async function getRedisClient() {
87
294
  });
88
295
 
89
296
  redisClient.on('error', (error) => {
90
- // Only log the first connection error to avoid spam
91
297
  if (!globalForRedis.hasLoggedConnectionError) {
92
298
  if (error.code === 'ECONNREFUSED') {
93
299
  console.warn(
@@ -101,12 +307,10 @@ async function getRedisClient() {
101
307
  });
102
308
 
103
309
  redisClient.on('connect', () => {
104
- console_log('Redis connected');
105
310
  globalForRedis.hasLoggedConnectionError = false;
106
311
  });
107
312
 
108
313
  redisClient.on('ready', () => {
109
- console_log('Redis ready');
110
314
  globalForRedis.hasLoggedConnectionError = false;
111
315
  });
112
316
 
@@ -115,16 +319,6 @@ async function getRedisClient() {
115
319
  return redisClient;
116
320
  } catch (error) {
117
321
  if (!globalForRedis.hasLoggedConnectionError) {
118
- if (error.code === 'ECONNREFUSED') {
119
- console.warn(
120
- '[Cache Handler] Could not connect to Redis - using local cache only'
121
- );
122
- } else {
123
- console.error(
124
- '[Cache Handler] Failed to connect to Redis:',
125
- error.message
126
- );
127
- }
128
322
  globalForRedis.hasLoggedConnectionError = true;
129
323
  }
130
324
  globalForRedis.redisClient = null;
@@ -135,13 +329,10 @@ async function getRedisClient() {
135
329
  }
136
330
 
137
331
  CacheHandler.onCreation(async () => {
138
- console_log('Initializing cache handlers...');
139
-
140
332
  let client;
141
333
  try {
142
334
  client = await getRedisClient();
143
335
  } catch (error) {
144
- // Error already logged in getRedisClient, just return local handler
145
336
  return {
146
337
  handlers: [createLruHandler(CACHE_CONFIG.lru)]
147
338
  };
@@ -150,98 +341,183 @@ CacheHandler.onCreation(async () => {
150
341
  const redisHandler = createRedisHandler({
151
342
  client,
152
343
  timeoutMs: CACHE_CONFIG.redis.timeoutMs,
153
- keyExpirationStrategy: 'EXAT'
344
+ keyExpirationStrategy: 'EXPIREAT'
154
345
  });
155
346
 
156
347
  const localHandler = createLruHandler(CACHE_CONFIG.lru);
157
348
 
158
- // Pre-compute version prefix if exists
159
- const versionPrefix = CACHE_CONFIG.version ? `${CACHE_CONFIG.version}:` : '';
349
+ const CACHE_VERSION = 'v2';
350
+ const versionPrefix = `${CACHE_VERSION}_`;
160
351
 
161
- // Create optimized functions for each scenario
162
- const versionKeyString = versionPrefix
163
- ? (key) => `${versionPrefix}${key}`
164
- : (key) => key;
165
-
166
- const versionKeyObject = versionPrefix
167
- ? (key) => ({ ...key, key: `${versionPrefix}${key.key}` })
168
- : (key) => key;
352
+ const versionKeyString = (key) => `${versionPrefix}${key}`;
353
+ const versionKeyObject = (key) => ({
354
+ ...key,
355
+ key: `${versionPrefix}${key.key}`
356
+ });
169
357
 
170
- // Main version key function that routes to optimized paths
171
358
  const versionKey = (key) => {
172
359
  return typeof key === 'string'
173
360
  ? versionKeyString(key)
174
361
  : versionKeyObject(key);
175
362
  };
176
363
 
177
- // Create a custom handler that checks local first, then Redis
178
364
  const customHandler = {
179
365
  name: 'custom-local-then-redis',
180
366
  get: async (key, context) => {
181
367
  const vKey = versionKey(key);
182
- console_log(
183
- 'GET called for key:',
184
- typeof vKey === 'string' ? vKey : vKey?.key
185
- );
186
368
 
187
- // Check local cache first
188
- console_log('Checking local cache...');
189
369
  const localResult = await localHandler.get(vKey, context);
190
370
 
191
371
  if (localResult) {
192
- console_log('Found in local cache');
372
+ if (
373
+ localResult &&
374
+ typeof localResult === 'object' &&
375
+ (localResult.__compressed ||
376
+ (localResult.value && localResult.value.__compressed) ||
377
+ localResult.compressed !== undefined)
378
+ ) {
379
+ try {
380
+ const decompressed = await decompressValue(localResult);
381
+ return typeof decompressed === 'string'
382
+ ? JSON.parse(decompressed)
383
+ : decompressed;
384
+ } catch (error) {
385
+ console.warn(
386
+ '[Cache Handler] Failed to decompress local cache value:',
387
+ error.message
388
+ );
389
+ return localResult;
390
+ }
391
+ }
392
+
193
393
  return localResult;
194
394
  }
195
395
 
196
- console_log('Not found in local, checking Redis...');
197
396
  try {
198
397
  const redisResult = await redisHandler.get(vKey, context);
199
398
 
200
399
  if (redisResult) {
201
- console_log('Found in Redis');
202
- // Sync back to local cache for faster future access
400
+ let finalResult = redisResult;
401
+
402
+ if (typeof redisResult === 'string') {
403
+ try {
404
+ finalResult = JSON.parse(redisResult);
405
+ } catch (parseError) {
406
+ finalResult = redisResult;
407
+ }
408
+ }
409
+
410
+ if (
411
+ finalResult &&
412
+ typeof finalResult === 'object' &&
413
+ (finalResult.__compressed ||
414
+ (finalResult.value && finalResult.value.__compressed) ||
415
+ finalResult.compressed !== undefined)
416
+ ) {
417
+ try {
418
+ const decompressed = await decompressValue(finalResult);
419
+ finalResult =
420
+ typeof decompressed === 'string'
421
+ ? JSON.parse(decompressed)
422
+ : decompressed;
423
+ } catch (error) {
424
+ console.warn(
425
+ '[Cache Handler] Failed to decompress Redis cache value:',
426
+ error.message
427
+ );
428
+ }
429
+ }
430
+
203
431
  try {
204
- await localHandler.set(vKey, redisResult, context);
205
- console_log('Synced to local cache');
432
+ await localHandler.set(vKey, finalResult, context);
206
433
  } catch (error) {
207
- console_log('Failed to sync to local:', error.message);
434
+ console.warn(
435
+ '[Cache Handler] Failed to sync to local:',
436
+ error.message
437
+ );
208
438
  }
209
- return redisResult;
439
+ return finalResult;
210
440
  }
211
441
  } catch (error) {
212
- console_log('Redis error:', error.message);
442
+ console.warn('[Cache Handler] Redis error:', error.message);
213
443
  }
214
444
 
215
- console_log('Not found in any cache');
216
445
  return undefined;
217
446
  },
218
447
  set: async (key, value, context) => {
219
448
  const vKey = versionKey(key);
220
- console_log(
221
- 'SET called for key:',
222
- typeof vKey === 'string' ? vKey : vKey?.key
223
- );
224
- // Set to both caches
225
- await Promise.allSettled([
226
- localHandler.set(vKey, value, context),
227
- redisHandler
228
- .set(vKey, value, context)
229
- .catch((error) => console_log('Redis SET error:', error.message))
230
- ]);
449
+
450
+ let compressedValue;
451
+ let shouldUseCompressed = false;
452
+
453
+ try {
454
+ compressedValue = await compressValue(value);
455
+
456
+ shouldUseCompressed =
457
+ compressedValue !== value &&
458
+ (compressedValue?.__compressed ||
459
+ compressedValue?.value?.__compressed);
460
+ } catch (error) {
461
+ console.warn(
462
+ '[Cache Handler] Compression failed, using original value:',
463
+ error.message
464
+ );
465
+ compressedValue = value;
466
+ shouldUseCompressed = false;
467
+ }
468
+
469
+ let redisSetResult;
470
+
471
+ if (shouldUseCompressed) {
472
+ try {
473
+ await redisHandler.set(vKey, compressedValue, context);
474
+
475
+ redisSetResult = { status: 'fulfilled' };
476
+ } catch (compressionError) {
477
+ try {
478
+ await redisHandler.set(vKey, value, context);
479
+
480
+ redisSetResult = { status: 'fulfilled' };
481
+ } catch (fallbackError) {
482
+ redisSetResult = { status: 'rejected', reason: fallbackError };
483
+ }
484
+ }
485
+ } else {
486
+ try {
487
+ await redisHandler.set(vKey, value, context);
488
+ redisSetResult = { status: 'fulfilled' };
489
+ } catch (error) {
490
+ redisSetResult = { status: 'rejected', reason: error };
491
+ }
492
+ }
493
+
494
+ let localSetResult;
495
+ try {
496
+ await localHandler.set(vKey, value, context);
497
+ localSetResult = { status: 'fulfilled' };
498
+ } catch (error) {
499
+ localSetResult = { status: 'rejected', reason: error };
500
+ }
501
+
502
+ const results = [localSetResult, redisSetResult];
503
+
504
+ console.warn('SET Results:', {
505
+ local: results[0].status,
506
+ redis: results[1].status,
507
+ localError: results[0].reason?.message,
508
+ redisError: results[1].reason?.message,
509
+ compressionUsed: shouldUseCompressed
510
+ });
231
511
  },
232
512
  delete: async (key, context) => {
233
513
  const vKey = versionKey(key);
234
- console_log(
235
- 'DELETE called for key:',
236
- typeof vKey === 'string' ? vKey : vKey?.key
237
- );
514
+
238
515
  await Promise.allSettled([
239
516
  localHandler.delete?.(vKey, context),
240
517
  redisHandler.delete?.(vKey, context)
241
518
  ]);
242
519
  },
243
520
  revalidateTag: async (tags, context) => {
244
- console_log('REVALIDATE_TAG called for tags:', tags);
245
521
  await Promise.allSettled([
246
522
  localHandler.revalidateTag?.(tags, context),
247
523
  redisHandler.revalidateTag?.(tags, context)
@@ -249,8 +525,6 @@ CacheHandler.onCreation(async () => {
249
525
  }
250
526
  };
251
527
 
252
- console_log('[Cache Handler] Handlers initialized successfully');
253
-
254
528
  return {
255
529
  handlers: [customHandler]
256
530
  };