@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.
- package/CHANGELOG.md +1172 -28
- package/__tests__/next-config.test.ts +1 -10
- package/__tests__/redirect.test.ts +758 -0
- package/api/image-proxy.ts +75 -0
- package/api/similar-product-list.ts +84 -0
- package/api/similar-products.ts +120 -0
- package/bin/pz-prebuild.js +0 -1
- package/components/accordion.tsx +20 -5
- package/components/file-input.tsx +65 -3
- package/components/input.tsx +2 -0
- package/components/link.tsx +16 -12
- package/components/modal.tsx +32 -16
- package/components/plugin-module.tsx +35 -3
- package/components/selected-payment-option-view.tsx +11 -0
- package/data/client/checkout.ts +25 -4
- package/data/server/basket.ts +72 -0
- package/data/server/category.ts +48 -28
- package/data/server/flatpage.ts +16 -12
- package/data/server/landingpage.ts +16 -12
- package/data/server/list.ts +23 -13
- package/data/server/product.ts +66 -39
- package/data/server/special-page.ts +16 -12
- package/data/urls.ts +7 -2
- package/hocs/server/with-segment-defaults.tsx +5 -2
- package/hooks/use-localization.ts +2 -3
- package/hooks/use-loyalty-availability.ts +21 -0
- package/instrumentation/node.ts +15 -13
- package/jest.config.js +7 -1
- package/lib/cache-handler.mjs +225 -16
- package/lib/cache.ts +2 -0
- package/middlewares/checkout-provider.ts +1 -1
- package/middlewares/complete-gpay.ts +6 -2
- package/middlewares/complete-masterpass.ts +7 -2
- package/middlewares/default.ts +232 -183
- package/middlewares/index.ts +3 -1
- package/middlewares/locale.ts +9 -1
- package/middlewares/redirection-payment.ts +6 -2
- package/middlewares/saved-card-redirection.ts +7 -2
- package/middlewares/three-d-redirection.ts +7 -2
- package/middlewares/url-redirection.ts +9 -15
- package/middlewares/wallet-complete-redirection.ts +203 -0
- package/package.json +3 -3
- package/plugins.d.ts +10 -0
- package/plugins.js +4 -1
- package/redux/middlewares/checkout.ts +15 -2
- package/redux/reducers/checkout.ts +9 -1
- package/sentry/index.ts +54 -17
- package/types/commerce/order.ts +1 -0
- package/types/index.ts +42 -1
- package/utils/app-fetch.ts +7 -2
- package/utils/index.ts +34 -10
- package/utils/redirect-ignore.ts +35 -0
- package/utils/redirect.ts +31 -6
- package/with-pz-config.js +1 -5
package/lib/cache-handler.mjs
CHANGED
|
@@ -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-
|
|
3
|
+
import createRedisHandler from '@neshca/cache-handler/redis-strings';
|
|
4
4
|
import { createClient } from 'redis';
|
|
5
|
+
import logger from '../utils/log';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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 =
|
|
134
|
+
const redisHandler = createRedisHandler({
|
|
22
135
|
client,
|
|
23
|
-
timeoutMs:
|
|
136
|
+
timeoutMs: CACHE_CONFIG.redis.timeoutMs,
|
|
137
|
+
keyExpirationStrategy: 'EXAT'
|
|
24
138
|
});
|
|
25
139
|
|
|
26
|
-
|
|
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: [
|
|
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') ? '' :
|
|
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,
|