@akinon/next 2.0.0-beta.12 → 2.0.0-beta.13

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 (93) hide show
  1. package/CHANGELOG.md +282 -29
  2. package/api/auth.ts +99 -77
  3. package/api/cache.ts +41 -5
  4. package/api/client.ts +3 -3
  5. package/api/form.ts +85 -0
  6. package/api/image-proxy.ts +75 -0
  7. package/api/product-categories.ts +53 -0
  8. package/api/similar-product-list.ts +63 -0
  9. package/api/similar-products.ts +111 -0
  10. package/api/virtual-try-on.ts +382 -0
  11. package/bin/pz-generate-routes.js +105 -0
  12. package/bin/pz-prebuild.js +1 -1
  13. package/bin/pz-predev.js +1 -0
  14. package/components/accordion.tsx +21 -6
  15. package/components/button.tsx +1 -1
  16. package/components/file-input.tsx +65 -3
  17. package/components/input.tsx +2 -2
  18. package/components/modal.tsx +32 -16
  19. package/components/plugin-module.tsx +61 -3
  20. package/components/select.tsx +2 -2
  21. package/components/selected-payment-option-view.tsx +21 -0
  22. package/data/client/checkout.ts +130 -74
  23. package/data/server/category.ts +11 -9
  24. package/data/server/flatpage.ts +4 -1
  25. package/data/server/form.ts +4 -1
  26. package/data/server/landingpage.ts +4 -1
  27. package/data/server/list.ts +5 -4
  28. package/data/server/menu.ts +4 -1
  29. package/data/server/product.ts +97 -52
  30. package/data/server/seo.ts +4 -1
  31. package/data/server/special-page.ts +5 -4
  32. package/data/server/widget.ts +4 -1
  33. package/data/urls.ts +3 -2
  34. package/hocs/client/with-segment-defaults.tsx +2 -2
  35. package/hocs/server/with-segment-defaults.tsx +65 -20
  36. package/hooks/index.ts +1 -0
  37. package/hooks/use-loyalty-availability.ts +21 -0
  38. package/hooks/use-payment-options.ts +2 -1
  39. package/hooks/use-pz-params.ts +37 -0
  40. package/instrumentation/index.ts +0 -1
  41. package/instrumentation/node.ts +2 -20
  42. package/jest.config.js +7 -1
  43. package/lib/cache-handler.mjs +527 -15
  44. package/lib/cache.ts +260 -31
  45. package/localization/provider.tsx +2 -5
  46. package/middlewares/checkout-provider.ts +1 -1
  47. package/middlewares/complete-gpay.ts +33 -26
  48. package/middlewares/complete-masterpass.ts +34 -26
  49. package/middlewares/complete-wallet.ts +183 -0
  50. package/middlewares/default.ts +346 -235
  51. package/middlewares/index.ts +8 -2
  52. package/middlewares/locale.ts +0 -1
  53. package/middlewares/masterpass-rest-callback.ts +220 -0
  54. package/middlewares/pretty-url.ts +21 -8
  55. package/middlewares/redirection-payment.ts +33 -26
  56. package/middlewares/saved-card-redirection.ts +34 -26
  57. package/middlewares/three-d-redirection.ts +33 -26
  58. package/middlewares/url-redirection.ts +9 -15
  59. package/middlewares/wallet-complete-redirection.ts +207 -0
  60. package/package.json +20 -11
  61. package/plugins.d.ts +19 -4
  62. package/plugins.js +9 -1
  63. package/redux/actions.ts +47 -0
  64. package/redux/middlewares/checkout.ts +20 -8
  65. package/redux/middlewares/index.ts +12 -10
  66. package/redux/middlewares/pre-order/address.ts +1 -1
  67. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +1 -1
  68. package/redux/middlewares/pre-order/data-source-shipping-option.ts +1 -1
  69. package/redux/middlewares/pre-order/delivery-option.ts +1 -1
  70. package/redux/middlewares/pre-order/index.ts +3 -1
  71. package/redux/middlewares/pre-order/installment-option.ts +2 -1
  72. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  73. package/redux/middlewares/pre-order/payment-option.ts +1 -1
  74. package/redux/middlewares/pre-order/pre-order-validation.ts +4 -3
  75. package/redux/middlewares/pre-order/redirection.ts +2 -2
  76. package/redux/middlewares/pre-order/set-pre-order.ts +2 -2
  77. package/redux/middlewares/pre-order/shipping-option.ts +1 -1
  78. package/redux/middlewares/pre-order/shipping-step.ts +1 -1
  79. package/redux/reducers/checkout.ts +9 -1
  80. package/redux/reducers/index.ts +5 -1
  81. package/sentry/index.ts +54 -17
  82. package/types/commerce/checkout.ts +11 -1
  83. package/types/index.ts +96 -6
  84. package/types/next-auth.d.ts +2 -2
  85. package/utils/app-fetch.ts +2 -2
  86. package/utils/generate-commerce-search-params.ts +3 -2
  87. package/utils/get-checkout-path.ts +3 -0
  88. package/utils/index.ts +38 -11
  89. package/utils/override-middleware.ts +1 -0
  90. package/utils/pz-segments.ts +92 -0
  91. package/utils/redirect-ignore.ts +35 -0
  92. package/utils/redirect.ts +9 -3
  93. package/with-pz-config.js +10 -4
@@ -1,32 +1,544 @@
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
+ let zstd;
7
+
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
+ })();
16
+
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
+ });
321
+
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
+ });
334
+
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
+
6
357
  CacheHandler.onCreation(async () => {
7
- const redisUrl = `redis://${process.env.CACHE_HOST}:${
8
- process.env.CACHE_PORT
9
- }/${process.env.CACHE_BUCKET ?? '0'}`;
358
+ let client;
359
+ try {
360
+ client = await getRedisClient();
361
+ } catch (error) {
362
+ return {
363
+ handlers: [createLruHandler(CACHE_CONFIG.lru)]
364
+ };
365
+ }
10
366
 
11
- const client = createClient({
12
- url: redisUrl
367
+ const redisHandler = createRedisHandler({
368
+ client,
369
+ timeoutMs: CACHE_CONFIG.redis.timeoutMs,
370
+ keyExpirationStrategy: 'EXPIREAT'
13
371
  });
14
372
 
15
- client.on('error', (error) => {
16
- console.error('Redis client error', { redisUrl, error });
17
- });
373
+ const localHandler = createLruHandler(CACHE_CONFIG.lru);
18
374
 
19
- await client.connect();
375
+ const CACHE_VERSION = 'v2';
376
+ const versionPrefix = `${CACHE_VERSION}_`;
20
377
 
21
- const redisHandler = await createRedisHandler({
22
- client,
23
- timeoutMs: 5000
378
+ const versionKeyString = (key) => `${versionPrefix}${key}`;
379
+ const versionKeyObject = (key) => ({
380
+ ...key,
381
+ key: `${versionPrefix}${key.key}`
24
382
  });
25
383
 
26
- // const localHandler = createLruHandler();
384
+ const versionKey = (key) => {
385
+ return typeof key === 'string'
386
+ ? versionKeyString(key)
387
+ : versionKeyObject(key);
388
+ };
389
+
390
+ const customHandler = {
391
+ name: 'custom-local-then-redis',
392
+ get: async (key, context) => {
393
+ const vKey = versionKey(key);
394
+
395
+ const localResult = await localHandler.get(vKey, context);
396
+
397
+ if (localResult) {
398
+ if (
399
+ localResult &&
400
+ typeof localResult === 'object' &&
401
+ (localResult.__compressed ||
402
+ (localResult.value && localResult.value.__compressed) ||
403
+ localResult.compressed !== undefined)
404
+ ) {
405
+ try {
406
+ const decompressed = await decompressValue(localResult);
407
+ return typeof decompressed === 'string'
408
+ ? JSON.parse(decompressed)
409
+ : decompressed;
410
+ } catch (_) {
411
+ return localResult;
412
+ }
413
+ }
414
+
415
+ return localResult;
416
+ }
417
+
418
+ try {
419
+ const redisResult = await redisHandler.get(vKey, context);
420
+
421
+ if (redisResult) {
422
+ let finalResult = redisResult;
423
+
424
+ if (typeof redisResult === 'string') {
425
+ try {
426
+ finalResult = JSON.parse(redisResult);
427
+ } catch (parseError) {
428
+ finalResult = redisResult;
429
+ }
430
+ }
431
+
432
+ if (
433
+ finalResult &&
434
+ typeof finalResult === 'object' &&
435
+ (finalResult.__compressed ||
436
+ (finalResult.value && finalResult.value.__compressed) ||
437
+ finalResult.compressed !== undefined)
438
+ ) {
439
+ try {
440
+ const decompressed = await decompressValue(finalResult);
441
+ finalResult =
442
+ typeof decompressed === 'string'
443
+ ? JSON.parse(decompressed)
444
+ : decompressed;
445
+ } catch (_) {
446
+ return finalResult;
447
+ }
448
+ }
449
+
450
+ try {
451
+ await localHandler.set(vKey, finalResult, context);
452
+ } catch (_) {
453
+ return finalResult;
454
+ }
455
+ return finalResult;
456
+ }
457
+ } catch (_) {
458
+ return undefined;
459
+ }
460
+
461
+ return undefined;
462
+ },
463
+ set: async (key, value, context) => {
464
+ const vKey = versionKey(key);
465
+
466
+ const stripTags = (val) => {
467
+ if (val && typeof val === 'object') {
468
+ const { tags, ...rest } = val;
469
+ return { ...rest, tags: [] };
470
+ }
471
+ return val;
472
+ };
473
+
474
+ let compressedValue;
475
+ let shouldUseCompressed = false;
476
+
477
+ try {
478
+ compressedValue = await compressValue(value);
479
+
480
+ shouldUseCompressed =
481
+ compressedValue !== value &&
482
+ (compressedValue?.__compressed ||
483
+ compressedValue?.value?.__compressed);
484
+ } catch (error) {
485
+ compressedValue = value;
486
+ shouldUseCompressed = false;
487
+ }
488
+
489
+ let redisSetResult;
490
+
491
+ if (shouldUseCompressed) {
492
+ try {
493
+ await redisHandler.set(vKey, stripTags(compressedValue), context);
494
+
495
+ redisSetResult = { status: 'fulfilled' };
496
+ } catch (compressionError) {
497
+ try {
498
+ await redisHandler.set(vKey, stripTags(value), context);
499
+
500
+ redisSetResult = { status: 'fulfilled' };
501
+ } catch (fallbackError) {
502
+ redisSetResult = { status: 'rejected', reason: fallbackError };
503
+ }
504
+ }
505
+ } else {
506
+ try {
507
+ await redisHandler.set(vKey, stripTags(value), context);
508
+ redisSetResult = { status: 'fulfilled' };
509
+ } catch (error) {
510
+ redisSetResult = { status: 'rejected', reason: error };
511
+ return redisSetResult;
512
+ }
513
+ }
514
+
515
+ let localSetResult;
516
+ try {
517
+ await localHandler.set(vKey, value, context);
518
+ localSetResult = { status: 'fulfilled' };
519
+ } catch (error) {
520
+ localSetResult = { status: 'rejected', reason: error };
521
+ }
522
+ return localSetResult;
523
+ },
524
+ delete: async (key, context) => {
525
+ const vKey = versionKey(key);
526
+
527
+ await Promise.allSettled([
528
+ localHandler.delete?.(vKey, context),
529
+ redisHandler.delete?.(vKey, context)
530
+ ]);
531
+ },
532
+ revalidateTag: async (tags, context) => {
533
+ await Promise.allSettled([
534
+ localHandler.revalidateTag?.(tags, context),
535
+ redisHandler.revalidateTag?.(tags, context)
536
+ ]);
537
+ }
538
+ };
27
539
 
28
540
  return {
29
- handlers: [redisHandler]
541
+ handlers: [customHandler]
30
542
  };
31
543
  });
32
544