@akinon/next 1.92.0 → 1.93.0-rc.47

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 (54) hide show
  1. package/CHANGELOG.md +1172 -28
  2. package/__tests__/next-config.test.ts +1 -10
  3. package/__tests__/redirect.test.ts +758 -0
  4. package/api/image-proxy.ts +75 -0
  5. package/api/similar-product-list.ts +84 -0
  6. package/api/similar-products.ts +120 -0
  7. package/bin/pz-prebuild.js +0 -1
  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 +35 -3
  14. package/components/selected-payment-option-view.tsx +11 -0
  15. package/data/client/checkout.ts +25 -4
  16. package/data/server/basket.ts +72 -0
  17. package/data/server/category.ts +48 -28
  18. package/data/server/flatpage.ts +16 -12
  19. package/data/server/landingpage.ts +16 -12
  20. package/data/server/list.ts +23 -13
  21. package/data/server/product.ts +66 -39
  22. package/data/server/special-page.ts +16 -12
  23. package/data/urls.ts +7 -2
  24. package/hocs/server/with-segment-defaults.tsx +5 -2
  25. package/hooks/use-localization.ts +2 -3
  26. package/hooks/use-loyalty-availability.ts +21 -0
  27. package/instrumentation/node.ts +15 -13
  28. package/jest.config.js +7 -1
  29. package/lib/cache-handler.mjs +225 -16
  30. package/lib/cache.ts +2 -0
  31. package/middlewares/checkout-provider.ts +1 -1
  32. package/middlewares/complete-gpay.ts +6 -2
  33. package/middlewares/complete-masterpass.ts +7 -2
  34. package/middlewares/default.ts +232 -183
  35. package/middlewares/index.ts +3 -1
  36. package/middlewares/locale.ts +9 -1
  37. package/middlewares/redirection-payment.ts +6 -2
  38. package/middlewares/saved-card-redirection.ts +7 -2
  39. package/middlewares/three-d-redirection.ts +7 -2
  40. package/middlewares/url-redirection.ts +9 -15
  41. package/middlewares/wallet-complete-redirection.ts +203 -0
  42. package/package.json +3 -3
  43. package/plugins.d.ts +10 -0
  44. package/plugins.js +4 -1
  45. package/redux/middlewares/checkout.ts +15 -2
  46. package/redux/reducers/checkout.ts +9 -1
  47. package/sentry/index.ts +54 -17
  48. package/types/commerce/order.ts +1 -0
  49. package/types/index.ts +42 -1
  50. package/utils/app-fetch.ts +7 -2
  51. package/utils/index.ts +34 -10
  52. package/utils/redirect-ignore.ts +35 -0
  53. package/utils/redirect.ts +31 -6
  54. package/with-pz-config.js +1 -5
@@ -1,32 +1,241 @@
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
+ import logger from '../utils/log';
5
6
 
6
- CacheHandler.onCreation(async () => {
7
- const redisUrl = `redis://${process.env.CACHE_HOST}:${
8
- process.env.CACHE_PORT
9
- }/${process.env.CACHE_BUCKET ?? '0'}`;
7
+ // Cache configuration
8
+ const CACHE_CONFIG = {
9
+ lru: {
10
+ maxItemCount: 2000
11
+ },
12
+ redis: {
13
+ timeoutMs: 5000,
14
+ reconnectStrategy: {
15
+ maxRetries: 3,
16
+ retryDelay: (retries) => Math.min(retries * 100, 3000)
17
+ },
18
+ connectTimeout: 500
19
+ },
20
+ host: process.env.CACHE_HOST || 'localhost',
21
+ port: process.env.CACHE_PORT || '6379',
22
+ bucket: process.env.CACHE_BUCKET ?? '0',
23
+ version: process.env.ACC_APP_VERSION || ''
24
+ };
10
25
 
11
- const client = createClient({
12
- url: redisUrl
13
- });
26
+ // Use global to persist across module reloads in development
27
+ const globalForRedis = global;
28
+ if (!globalForRedis.redisClient) {
29
+ globalForRedis.redisClient = null;
30
+ globalForRedis.isConnecting = false;
31
+ globalForRedis.hasLoggedConnectionError = false;
32
+ globalForRedis.connectionAttempts = 0;
33
+ }
14
34
 
15
- client.on('error', (error) => {
16
- console.error('Redis client error', { redisUrl, error });
17
- });
35
+ async function getRedisClient() {
36
+ // If client exists and is ready, return it
37
+ if (globalForRedis.redisClient?.isReady) {
38
+ logger.trace('Reusing existing Redis connection');
39
+ return globalForRedis.redisClient;
40
+ }
41
+
42
+ // If we're already connecting, wait a bit and retry
43
+ if (globalForRedis.isConnecting) {
44
+ await new Promise((resolve) => setTimeout(resolve, 100));
45
+ return getRedisClient();
46
+ }
47
+
48
+ // Start new connection
49
+ globalForRedis.isConnecting = true;
50
+ globalForRedis.connectionAttempts++;
51
+
52
+ try {
53
+ const redisUrl = `redis://${CACHE_CONFIG.host}:${CACHE_CONFIG.port}/${CACHE_CONFIG.bucket}`;
54
+
55
+ if (globalForRedis.connectionAttempts === 1) {
56
+ logger.debug('Creating Redis connection', { url: redisUrl });
57
+ }
58
+
59
+ const redisClient = createClient({
60
+ url: redisUrl,
61
+ socket: {
62
+ reconnectStrategy: (retries) => {
63
+ if (retries > CACHE_CONFIG.redis.reconnectStrategy.maxRetries) {
64
+ if (!globalForRedis.hasLoggedConnectionError) {
65
+ logger.debug(
66
+ 'Redis is not available, falling back to local cache only'
67
+ );
68
+ globalForRedis.hasLoggedConnectionError = true;
69
+ }
70
+ return false;
71
+ }
72
+ return CACHE_CONFIG.redis.reconnectStrategy.retryDelay(retries);
73
+ },
74
+ connectTimeout: CACHE_CONFIG.redis.connectTimeout
75
+ }
76
+ });
77
+
78
+ redisClient.on('error', (error) => {
79
+ // Only log the first connection error to avoid spam
80
+ if (!globalForRedis.hasLoggedConnectionError) {
81
+ if (error.code === 'ECONNREFUSED') {
82
+ logger.debug(
83
+ 'Redis connection refused. Is Redis running? Falling back to local cache.'
84
+ );
85
+ } else {
86
+ logger.warn('Redis client error', { error: error.message });
87
+ }
88
+ globalForRedis.hasLoggedConnectionError = true;
89
+ }
90
+ });
91
+
92
+ redisClient.on('connect', () => {
93
+ logger.debug('Redis connected');
94
+ globalForRedis.hasLoggedConnectionError = false;
95
+ });
96
+
97
+ redisClient.on('ready', () => {
98
+ logger.debug('Redis ready');
99
+ globalForRedis.hasLoggedConnectionError = false;
100
+ });
101
+
102
+ await redisClient.connect();
103
+ globalForRedis.redisClient = redisClient;
104
+ return redisClient;
105
+ } catch (error) {
106
+ if (!globalForRedis.hasLoggedConnectionError) {
107
+ if (error.code === 'ECONNREFUSED') {
108
+ logger.debug('Could not connect to Redis - using local cache only');
109
+ } else {
110
+ logger.error('Failed to connect to Redis', { error: error.message });
111
+ }
112
+ globalForRedis.hasLoggedConnectionError = true;
113
+ }
114
+ globalForRedis.redisClient = null;
115
+ throw error;
116
+ } finally {
117
+ globalForRedis.isConnecting = false;
118
+ }
119
+ }
120
+
121
+ CacheHandler.onCreation(async () => {
122
+ logger.debug('Initializing cache handlers...');
18
123
 
19
- await client.connect();
124
+ let client;
125
+ try {
126
+ client = await getRedisClient();
127
+ } catch (error) {
128
+ // Error already logged in getRedisClient, just return local handler
129
+ return {
130
+ handlers: [createLruHandler(CACHE_CONFIG.lru)]
131
+ };
132
+ }
20
133
 
21
- const redisHandler = await createRedisHandler({
134
+ const redisHandler = createRedisHandler({
22
135
  client,
23
- timeoutMs: 5000
136
+ timeoutMs: CACHE_CONFIG.redis.timeoutMs,
137
+ keyExpirationStrategy: 'EXAT'
24
138
  });
25
139
 
26
- // const localHandler = createLruHandler();
140
+ const localHandler = createLruHandler(CACHE_CONFIG.lru);
141
+
142
+ // Pre-compute version prefix if exists
143
+ const versionPrefix = CACHE_CONFIG.version ? `${CACHE_CONFIG.version}:` : '';
144
+
145
+ // Create optimized functions for each scenario
146
+ const versionKeyString = versionPrefix
147
+ ? (key) => `${versionPrefix}${key}`
148
+ : (key) => key;
149
+
150
+ const versionKeyObject = versionPrefix
151
+ ? (key) => ({ ...key, key: `${versionPrefix}${key.key}` })
152
+ : (key) => key;
153
+
154
+ // Main version key function that routes to optimized paths
155
+ const versionKey = (key) => {
156
+ return typeof key === 'string'
157
+ ? versionKeyString(key)
158
+ : versionKeyObject(key);
159
+ };
160
+
161
+ // Create a custom handler that checks local first, then Redis
162
+ const customHandler = {
163
+ name: 'custom-local-then-redis',
164
+ get: async (key, context) => {
165
+ const vKey = versionKey(key);
166
+ logger.trace('GET called for key', {
167
+ key: typeof vKey === 'string' ? vKey : vKey?.key
168
+ });
169
+
170
+ // Check local cache first
171
+ logger.trace('Checking local cache...');
172
+ const localResult = await localHandler.get(vKey, context);
173
+
174
+ if (localResult) {
175
+ logger.trace('Found in local cache');
176
+ return localResult;
177
+ }
178
+
179
+ logger.trace('Not found in local, checking Redis...');
180
+ try {
181
+ const redisResult = await redisHandler.get(vKey, context);
182
+
183
+ if (redisResult) {
184
+ logger.trace('Found in Redis');
185
+ // Sync back to local cache for faster future access
186
+ try {
187
+ await localHandler.set(vKey, redisResult, context);
188
+ logger.trace('Synced to local cache');
189
+ } catch (error) {
190
+ logger.debug('Failed to sync to local', { error: error.message });
191
+ }
192
+ return redisResult;
193
+ }
194
+ } catch (error) {
195
+ logger.warn('Redis GET operation failed', { error: error.message });
196
+ }
197
+
198
+ logger.trace('Not found in any cache');
199
+ return undefined;
200
+ },
201
+ set: async (key, value, context) => {
202
+ const vKey = versionKey(key);
203
+ logger.trace('SET called for key', {
204
+ key: typeof vKey === 'string' ? vKey : vKey?.key
205
+ });
206
+ // Set to both caches
207
+ await Promise.allSettled([
208
+ localHandler.set(vKey, value, context),
209
+ redisHandler
210
+ .set(vKey, value, context)
211
+ .catch((error) =>
212
+ logger.warn('Redis SET operation failed', { error: error.message })
213
+ )
214
+ ]);
215
+ },
216
+ delete: async (key, context) => {
217
+ const vKey = versionKey(key);
218
+ logger.trace('DELETE called for key', {
219
+ key: typeof vKey === 'string' ? vKey : vKey?.key
220
+ });
221
+ await Promise.allSettled([
222
+ localHandler.delete?.(vKey, context),
223
+ redisHandler.delete?.(vKey, context)
224
+ ]);
225
+ },
226
+ revalidateTag: async (tags, context) => {
227
+ logger.debug('REVALIDATE_TAG called for tags', { tags });
228
+ await Promise.allSettled([
229
+ localHandler.revalidateTag?.(tags, context),
230
+ redisHandler.revalidateTag?.(tags, context)
231
+ ]);
232
+ }
233
+ };
234
+
235
+ logger.info('Cache handlers initialized successfully');
27
236
 
28
237
  return {
29
- handlers: [redisHandler]
238
+ handlers: [customHandler]
30
239
  };
31
240
  });
32
241
 
package/lib/cache.ts CHANGED
@@ -31,6 +31,8 @@ export const CacheKey = {
31
31
  `category_${pk}_${encodeURIComponent(
32
32
  JSON.stringify(searchParams)
33
33
  )}${hashCacheKey(headers)}`,
34
+ Basket: (namespace?: string) => `basket${namespace ? `_${namespace}` : ''}`,
35
+ AllBaskets: () => 'all_baskets',
34
36
  CategorySlug: (slug: string) => `category_${slug}`,
35
37
  SpecialPage: (
36
38
  pk: number,
@@ -64,7 +64,7 @@ const withCheckoutProvider =
64
64
  const location = request.headers.get('location');
65
65
  const redirectUrl = new URL(
66
66
  request.headers.get('location'),
67
- location.startsWith('http') ? '' : process.env.NEXT_PUBLIC_URL
67
+ location.startsWith('http') ? '' : url.origin
68
68
  );
69
69
 
70
70
  redirectUrl.pathname = getUrlPathWithLocale(
@@ -34,6 +34,7 @@ const withCompleteGpay =
34
34
  const url = req.nextUrl.clone();
35
35
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
36
  const sessionId = req.cookies.get('osessionid');
37
+ const currentLocale = req.middlewareParams?.rewrites?.locale;
37
38
 
38
39
  if (url.search.indexOf('GPayCompletePage') === -1) {
39
40
  return middleware(req, event);
@@ -45,7 +46,9 @@ const withCompleteGpay =
45
46
  'Content-Type': 'application/x-www-form-urlencoded',
46
47
  Cookie: req.headers.get('cookie') ?? '',
47
48
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
- 'x-forwarded-for': ip
49
+ 'x-forwarded-for': ip,
50
+ 'Accept-Language':
51
+ currentLocale ?? req.cookies.get('pz-locale')?.value ?? ''
49
52
  };
50
53
 
51
54
  try {
@@ -145,7 +148,8 @@ const withCompleteGpay =
145
148
  logger.info('Redirecting to order success page', {
146
149
  middleware: 'complete-gpay',
147
150
  redirectUrlWithLocale,
148
- ip
151
+ ip,
152
+ setCookie: request.headers.get('set-cookie')
149
153
  });
150
154
 
151
155
  // Using POST method while redirecting causes an error,
@@ -4,6 +4,7 @@ import { Buffer } from 'buffer';
4
4
  import logger from '../utils/log';
5
5
  import { getUrlPathWithLocale } from '../utils/localization';
6
6
  import { PzNextRequest } from '.';
7
+ import { ServerVariables } from '../utils/server-variables';
7
8
 
8
9
  const streamToString = async (stream: ReadableStream<Uint8Array> | null) => {
9
10
  if (stream) {
@@ -34,6 +35,7 @@ const withCompleteMasterpass =
34
35
  const url = req.nextUrl.clone();
35
36
  const ip = req.headers.get('x-forwarded-for') ?? '';
36
37
  const sessionId = req.cookies.get('osessionid');
38
+ const currentLocale = req.middlewareParams?.rewrites?.locale;
37
39
 
38
40
  if (url.search.indexOf('MasterpassCompletePage') === -1) {
39
41
  return middleware(req, event);
@@ -45,7 +47,9 @@ const withCompleteMasterpass =
45
47
  'Content-Type': 'application/x-www-form-urlencoded',
46
48
  Cookie: req.headers.get('cookie') ?? '',
47
49
  'x-currency': req.cookies.get('pz-currency')?.value ?? '',
48
- 'x-forwarded-for': ip
50
+ 'x-forwarded-for': ip,
51
+ 'Accept-Language':
52
+ currentLocale ?? req.cookies.get('pz-locale')?.value ?? ''
49
53
  };
50
54
 
51
55
  try {
@@ -145,7 +149,8 @@ const withCompleteMasterpass =
145
149
  logger.info('Redirecting to order success page', {
146
150
  middleware: 'complete-masterpass',
147
151
  redirectUrlWithLocale,
148
- ip
152
+ ip,
153
+ setCookie: request.headers.get('set-cookie')
149
154
  });
150
155
 
151
156
  // Using POST method while redirecting causes an error,