@akinon/next 1.95.0-snapshot-ZERO-3586-20250903094652 → 1.96.0-rc.55
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 +11 -76
- package/api/cache.ts +5 -39
- package/data/server/category.ts +2 -4
- package/data/server/flatpage.ts +1 -4
- package/data/server/form.ts +1 -4
- package/data/server/landingpage.ts +1 -4
- package/data/server/list.ts +1 -2
- package/data/server/menu.ts +1 -4
- package/data/server/product.ts +1 -2
- package/data/server/seo.ts +1 -4
- package/data/server/special-page.ts +1 -2
- package/data/server/widget.ts +1 -4
- package/lib/cache-handler.mjs +87 -365
- package/lib/cache.ts +25 -252
- package/middlewares/pretty-url.ts +1 -2
- package/package.json +3 -4
- package/types/index.ts +0 -1
- package/with-pz-config.js +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,76 +1,6 @@
|
|
|
1
1
|
# @akinon/next
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
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
|
-
- 63774a6a: ZERO-3351: Add commerce redirection ignore list functionality and related utility
|
|
13
|
-
- 2d9b2b2c: ZERO-2816: Add segment to headers
|
|
14
|
-
- 5e1feca6: Revert "ZERO-3286: Add notFound handling for chunk URLs starting with \_next"
|
|
15
|
-
- 40a46853: ZERO-3182: Optimize basket update mutation with optimistic update
|
|
16
|
-
- 5f7edd6: ZERO-3571: Enhance Jest configuration by adding base directory resolution and module name mapping
|
|
17
|
-
- 68bbcb27: ZERO-3393: Fix error handling in checkout middleware to check for errors array length
|
|
18
|
-
- d8be48fb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
|
|
19
|
-
- b55acb76: ZERO-2577: Fix pagination bug and update usePagination hook and ensure pagination controls rendering correctly
|
|
20
|
-
- f49bb74f: ZERO-3097: Add setCookie to logging in payment redirection middlewares
|
|
21
|
-
- 0ad91bbd: ZERO-3489: Improve error handling in data fetching across multiple pages and server functions
|
|
22
|
-
- 143be2b9: ZERO-3457: Crop styles are customizable and logic improved for rendering similar products modal
|
|
23
|
-
- e9541a13: ZERO-2816: Add headers to url
|
|
24
|
-
- 9b7d0de6: ZERO-3393: Improve error handling in checkout middleware to support both object and array error formats
|
|
25
|
-
- 72fd4d67: ZERO-3084: Fix URL search parameters encoding in default middleware
|
|
26
|
-
- 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.
|
|
27
|
-
- a8539c8c: ZERO-3439: Enhance locale handling in middleware and redirect utility
|
|
28
|
-
- 16aff543: ZERO-3431: Add test script for redirect utility in package.json
|
|
29
|
-
- 64699d3f: ZERO-2761: Fix invalid import for plugin module
|
|
30
|
-
- 9f8cd3bc: ZERO-3449: AI Search Active Filters & Crop Style changes have been implemented
|
|
31
|
-
- e974d8e8: ZERO-3406: Fix rc build
|
|
32
|
-
- 89ce46fc: ZERO-3493: return 404 status code for pz-not-found pages
|
|
33
|
-
- 8645d90: ZERO-3574:Refactor redirect tests: streamline mock setup, enhance locale handling, and improve URL path resolution logic
|
|
34
|
-
- 7eb51ca9: ZERO-3424 :Update package versions
|
|
35
|
-
- 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
|
|
36
|
-
- 8b1d24eb: ZERO-3422: Update fetch method to use dynamic request method in wallet complete redirection middleware
|
|
37
|
-
- d552629f: ZERO-3182: Refactor basketApi to use invalidatesTags and comment out onQueryStarted logic
|
|
38
|
-
- 17f87524: ZERO-2816: Make the incoming currency lowercase
|
|
39
|
-
- 65d3b862: ZERO-3054: Update headers in appFetch
|
|
40
|
-
- c39c7000: ZERO-3420: Refactor Modal component
|
|
41
|
-
- bbe18b9ff: ZERO-2575: Fix build error
|
|
42
|
-
- 35dfb8f8: ZERO-3363: Refactor URL handling in checkout and redirection middlewares to use url.origin instead of process.env.NEXT_PUBLIC_URL
|
|
43
|
-
- 4920742c: Disable getCachedTranslations
|
|
44
|
-
- b6e5b624: ZERO-3257: Enhance locale middleware to redirect using existing or default locale and support 303 status for POST requests
|
|
45
|
-
- 0de55738: ZERO-3418: Update remotePatterns hostname to allow all subdomains
|
|
46
|
-
- 7e56d6b6: ZERO-2841: Update api tagTypes
|
|
47
|
-
- d99a6a7d: ZERO-3457: Fixed the settings prop and made sure everything is customizable.
|
|
48
|
-
- 9dc7298a: ZERO-3416: Refactor Accordion component to enhance props and improve styling flexibility
|
|
49
|
-
- 33377cfd: ZERO-3267: Refactor import statement for ROUTES in error-page component
|
|
50
|
-
- 43c182ee: ZERO-3054: Update Redis variable checks to conditionally include CACHE_SECRET
|
|
51
|
-
- c480272: ZERO-3531: Refactor checkoutApi: Remove unnecessary invalidatesTags property from POST request from sample products
|
|
52
|
-
- b00a90b1: ZERO-3436: Preserve query params on redirect
|
|
53
|
-
- facf1ada: ZERO-3445: Add SameSite and Secure attributes
|
|
54
|
-
- 26b2d0b: ZERO-3571: Remove test script execution from prebuild and simplify Jest module name mapping
|
|
55
|
-
- eeb20bea: Revert "ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing"
|
|
56
|
-
- 99b6e7b9: ZERO-3421: Enhance Sentry error handling by adding network error detection logic and refining initialization options
|
|
57
|
-
- 3bf63c8a: ZERO-3286: Add notFound handling for chunk URLs starting with \_next
|
|
58
|
-
- 9be2c081: ZERO-3243: Improve basket update query handling with optimistic updates
|
|
59
|
-
- f7fd459b: ZERO-3445: Refactor setCookie function to include domain handling and improve cookie string construction
|
|
60
|
-
- 4de5303c: ZERO-2504: add cookie filter to api client request
|
|
61
|
-
- dc678c3: ZERO-3523: Enhance redirect tests with dynamic locale handling and settings integration
|
|
62
|
-
- f2c92d5c: ZERO-2816: Update cookie name
|
|
63
|
-
- a420947d: ZERO-3517: Fix optional chaining for rawData in error logging for category data handlers
|
|
64
|
-
- 7bd3d992: ZERO-2801: Refactor locale middleware to handle single locale configuration
|
|
65
|
-
- acd2afdf: ZERO-3431: Fix import statement for findBaseDir in next-config test
|
|
66
|
-
- 2d3f1788: ZERO-3417: Enhance FileInput component with additional props for customization
|
|
67
|
-
- fdd255ee: ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing
|
|
68
|
-
- 49eeebfa: ZERO-2909: Add deleteCollectionItem query to wishlistApi
|
|
69
|
-
- 3f9b8d7e: ZERO-2761: Update plugins.js for akinon-next
|
|
70
|
-
- cbdb5c14: ZERO-3448: fix set cookie domain handling for subdomain locale strategy
|
|
71
|
-
- 0e823010: ZERO-3531: Add saveSampleProducts endpoint
|
|
72
|
-
|
|
73
|
-
## 1.95.0-rc.54
|
|
3
|
+
## 1.96.0-rc.55
|
|
74
4
|
|
|
75
5
|
### Minor Changes
|
|
76
6
|
|
|
@@ -98,7 +28,7 @@
|
|
|
98
28
|
- 64699d3ff: ZERO-2761: Fix invalid import for plugin module
|
|
99
29
|
- 9f8cd3bc: ZERO-3449: AI Search Active Filters & Crop Style changes have been implemented
|
|
100
30
|
- e974d8e8: ZERO-3406: Fix rc build
|
|
101
|
-
-
|
|
31
|
+
- 89ce46fc: ZERO-3493: return 404 status code for pz-not-found pages
|
|
102
32
|
- 8645d90: ZERO-3574:Refactor redirect tests: streamline mock setup, enhance locale handling, and improve URL path resolution logic
|
|
103
33
|
- 7eb51ca9: ZERO-3424 :Update package versions
|
|
104
34
|
- 7727ae55: ZERO-3073: Refactor basket page to use server-side data fetching and simplify component structure
|
|
@@ -108,7 +38,6 @@
|
|
|
108
38
|
- 65d3b862: ZERO-3054: Update headers in appFetch
|
|
109
39
|
- c39c7000: ZERO-3420: Refactor Modal component
|
|
110
40
|
- bbe18b9ff: ZERO-2575: Fix build error
|
|
111
|
-
- 35dfb8f8: ZERO-3363: Refactor URL handling in checkout and redirection middlewares to use url.origin instead of process.env.NEXT_PUBLIC_URL
|
|
112
41
|
- 4920742c2: Disable getCachedTranslations
|
|
113
42
|
- b6e5b624: ZERO-3257: Enhance locale middleware to redirect using existing or default locale and support 303 status for POST requests
|
|
114
43
|
- 0de55738: ZERO-3418: Update remotePatterns hostname to allow all subdomains
|
|
@@ -122,7 +51,6 @@
|
|
|
122
51
|
- facf1ada: ZERO-3445: Add SameSite and Secure attributes
|
|
123
52
|
- 26b2d0b: ZERO-3571: Remove test script execution from prebuild and simplify Jest module name mapping
|
|
124
53
|
- eeb20bea: Revert "ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing"
|
|
125
|
-
- 99b6e7b9: ZERO-3421: Enhance Sentry error handling by adding network error detection logic and refining initialization options
|
|
126
54
|
- 3bf63c8a: ZERO-3286: Add notFound handling for chunk URLs starting with \_next
|
|
127
55
|
- 9be2c081: ZERO-3243: Improve basket update query handling with optimistic updates
|
|
128
56
|
- f7fd459b: ZERO-3445: Refactor setCookie function to include domain handling and improve cookie string construction
|
|
@@ -131,14 +59,21 @@
|
|
|
131
59
|
- f2c92d5c7: ZERO-2816: Update cookie name
|
|
132
60
|
- a420947d: ZERO-3517: Fix optional chaining for rawData in error logging for category data handlers
|
|
133
61
|
- 7bd3d9928: ZERO-2801: Refactor locale middleware to handle single locale configuration
|
|
134
|
-
-
|
|
62
|
+
- acd2afdf: ZERO-3431: Fix import statement for findBaseDir in next-config test
|
|
135
63
|
- 2d3f1788: ZERO-3417: Enhance FileInput component with additional props for customization
|
|
136
64
|
- fdd255ee: ZERO-3054: Refactor cache handler to use custom Redis handler and implement key hashing
|
|
137
65
|
- 49eeebfa: ZERO-2909: Add deleteCollectionItem query to wishlistApi
|
|
138
66
|
- 3f9b8d7e7: ZERO-2761: Update plugins.js for akinon-next
|
|
139
|
-
- cbdb5c14: ZERO-3448: fix set cookie domain handling for subdomain locale strategy
|
|
140
67
|
- 0e82301: ZERO-3531: Add saveSampleProducts endpoint
|
|
141
68
|
|
|
69
|
+
## 1.95.0
|
|
70
|
+
|
|
71
|
+
### Minor Changes
|
|
72
|
+
|
|
73
|
+
- 35dfb8f: ZERO-3363: Refactor URL handling in checkout and redirection middlewares to use url.origin instead of process.env.NEXT_PUBLIC_URL
|
|
74
|
+
- 99b6e7b: ZERO-3421: Enhance Sentry error handling by adding network error detection logic and refining initialization options
|
|
75
|
+
- cbdb5c1: ZERO-3448: fix set cookie domain handling for subdomain locale strategy
|
|
76
|
+
|
|
142
77
|
## 1.94.0
|
|
143
78
|
|
|
144
79
|
### Minor Changes
|
package/api/cache.ts
CHANGED
|
@@ -21,54 +21,20 @@ async function handleRequest(...args) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const formData = await req.formData();
|
|
24
|
-
const body = {} as {
|
|
25
|
-
key: string;
|
|
26
|
-
value?: string;
|
|
27
|
-
expire?: number;
|
|
28
|
-
keyValuePairs?: string;
|
|
29
|
-
compressed?: string;
|
|
30
|
-
};
|
|
24
|
+
const body = {} as { key: string; value?: string; expire?: number };
|
|
31
25
|
|
|
32
26
|
formData.forEach((value, key) => {
|
|
33
27
|
body[key] = value;
|
|
34
28
|
});
|
|
35
29
|
|
|
36
|
-
const { key, value, expire
|
|
37
|
-
let response:
|
|
30
|
+
const { key, value, expire } = body;
|
|
31
|
+
let response: string | boolean;
|
|
38
32
|
|
|
39
33
|
try {
|
|
40
34
|
if (req.method === 'POST') {
|
|
41
|
-
|
|
42
|
-
response = await Cache.getCompressed(key);
|
|
43
|
-
} else {
|
|
44
|
-
response = await Cache.get(key);
|
|
45
|
-
}
|
|
35
|
+
response = await Cache.get(key);
|
|
46
36
|
} else if (req.method === 'PUT') {
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const parsedKeyValuePairs = JSON.parse(keyValuePairs);
|
|
50
|
-
if (
|
|
51
|
-
typeof parsedKeyValuePairs !== 'object' ||
|
|
52
|
-
parsedKeyValuePairs === null ||
|
|
53
|
-
Array.isArray(parsedKeyValuePairs)
|
|
54
|
-
) {
|
|
55
|
-
throw new Error('Invalid keyValuePairs format - must be an object');
|
|
56
|
-
}
|
|
57
|
-
response = await Cache.mset(parsedKeyValuePairs, expire);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
logger.error('Invalid keyValuePairs in mset request', { error });
|
|
60
|
-
return NextResponse.json(
|
|
61
|
-
{ error: 'Invalid keyValuePairs format' },
|
|
62
|
-
{ status: 400 }
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
if (compressed === 'true') {
|
|
67
|
-
response = await Cache.setCompressed(key, value, expire);
|
|
68
|
-
} else {
|
|
69
|
-
response = await Cache.set(key, value, expire);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
37
|
+
response = await Cache.set(key, value, expire);
|
|
72
38
|
}
|
|
73
39
|
} catch (error) {
|
|
74
40
|
logger.error(error);
|
package/data/server/category.ts
CHANGED
|
@@ -118,8 +118,7 @@ export const getCategoryData = ({
|
|
|
118
118
|
locale,
|
|
119
119
|
getCategoryDataHandler(pk, locale, currency, searchParams, headers),
|
|
120
120
|
{
|
|
121
|
-
expire: 300
|
|
122
|
-
compressed: true
|
|
121
|
+
expire: 300
|
|
123
122
|
}
|
|
124
123
|
);
|
|
125
124
|
};
|
|
@@ -179,8 +178,7 @@ export const getCategoryBySlugData = async ({
|
|
|
179
178
|
locale,
|
|
180
179
|
getCategoryBySlugDataHandler(slug, locale, currency),
|
|
181
180
|
{
|
|
182
|
-
expire: 300
|
|
183
|
-
compressed: true
|
|
181
|
+
expire: 300
|
|
184
182
|
}
|
|
185
183
|
);
|
|
186
184
|
};
|
package/data/server/flatpage.ts
CHANGED
package/data/server/form.ts
CHANGED
package/data/server/list.ts
CHANGED
package/data/server/menu.ts
CHANGED
|
@@ -48,9 +48,6 @@ 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)
|
|
52
|
-
{
|
|
53
|
-
compressed: true
|
|
54
|
-
}
|
|
51
|
+
getMenuHandler(params)
|
|
55
52
|
);
|
|
56
53
|
};
|
package/data/server/product.ts
CHANGED
package/data/server/seo.ts
CHANGED
package/data/server/widget.ts
CHANGED
package/lib/cache-handler.mjs
CHANGED
|
@@ -3,261 +3,7 @@ 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
5
|
|
|
6
|
-
|
|
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
|
-
|
|
6
|
+
// Cache configuration
|
|
261
7
|
const CACHE_CONFIG = {
|
|
262
8
|
lru: {
|
|
263
9
|
maxItemCount: 2000
|
|
@@ -276,6 +22,7 @@ const CACHE_CONFIG = {
|
|
|
276
22
|
version: process.env.ACC_APP_VERSION || ''
|
|
277
23
|
};
|
|
278
24
|
|
|
25
|
+
// Use global to persist across module reloads in development
|
|
279
26
|
const globalForRedis = global;
|
|
280
27
|
if (!globalForRedis.redisClient) {
|
|
281
28
|
globalForRedis.redisClient = null;
|
|
@@ -284,22 +31,42 @@ if (!globalForRedis.redisClient) {
|
|
|
284
31
|
globalForRedis.connectionAttempts = 0;
|
|
285
32
|
}
|
|
286
33
|
|
|
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
|
+
|
|
287
46
|
async function getRedisClient() {
|
|
47
|
+
// If client exists and is ready, return it
|
|
288
48
|
if (globalForRedis.redisClient?.isReady) {
|
|
49
|
+
console_log('Reusing existing Redis connection');
|
|
289
50
|
return globalForRedis.redisClient;
|
|
290
51
|
}
|
|
291
52
|
|
|
53
|
+
// If we're already connecting, wait a bit and retry
|
|
292
54
|
if (globalForRedis.isConnecting) {
|
|
293
55
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
294
56
|
return getRedisClient();
|
|
295
57
|
}
|
|
296
58
|
|
|
59
|
+
// Start new connection
|
|
297
60
|
globalForRedis.isConnecting = true;
|
|
298
61
|
globalForRedis.connectionAttempts++;
|
|
299
62
|
|
|
300
63
|
try {
|
|
301
64
|
const redisUrl = `redis://${CACHE_CONFIG.host}:${CACHE_CONFIG.port}/${CACHE_CONFIG.bucket}`;
|
|
302
65
|
|
|
66
|
+
if (globalForRedis.connectionAttempts === 1) {
|
|
67
|
+
console_log('Creating Redis connection:', redisUrl);
|
|
68
|
+
}
|
|
69
|
+
|
|
303
70
|
const redisClient = createClient({
|
|
304
71
|
url: redisUrl,
|
|
305
72
|
socket: {
|
|
@@ -320,6 +87,7 @@ async function getRedisClient() {
|
|
|
320
87
|
});
|
|
321
88
|
|
|
322
89
|
redisClient.on('error', (error) => {
|
|
90
|
+
// Only log the first connection error to avoid spam
|
|
323
91
|
if (!globalForRedis.hasLoggedConnectionError) {
|
|
324
92
|
if (error.code === 'ECONNREFUSED') {
|
|
325
93
|
console.warn(
|
|
@@ -333,10 +101,12 @@ async function getRedisClient() {
|
|
|
333
101
|
});
|
|
334
102
|
|
|
335
103
|
redisClient.on('connect', () => {
|
|
104
|
+
console_log('Redis connected');
|
|
336
105
|
globalForRedis.hasLoggedConnectionError = false;
|
|
337
106
|
});
|
|
338
107
|
|
|
339
108
|
redisClient.on('ready', () => {
|
|
109
|
+
console_log('Redis ready');
|
|
340
110
|
globalForRedis.hasLoggedConnectionError = false;
|
|
341
111
|
});
|
|
342
112
|
|
|
@@ -345,6 +115,16 @@ async function getRedisClient() {
|
|
|
345
115
|
return redisClient;
|
|
346
116
|
} catch (error) {
|
|
347
117
|
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
|
+
}
|
|
348
128
|
globalForRedis.hasLoggedConnectionError = true;
|
|
349
129
|
}
|
|
350
130
|
globalForRedis.redisClient = null;
|
|
@@ -355,10 +135,13 @@ async function getRedisClient() {
|
|
|
355
135
|
}
|
|
356
136
|
|
|
357
137
|
CacheHandler.onCreation(async () => {
|
|
138
|
+
console_log('Initializing cache handlers...');
|
|
139
|
+
|
|
358
140
|
let client;
|
|
359
141
|
try {
|
|
360
142
|
client = await getRedisClient();
|
|
361
143
|
} catch (error) {
|
|
144
|
+
// Error already logged in getRedisClient, just return local handler
|
|
362
145
|
return {
|
|
363
146
|
handlers: [createLruHandler(CACHE_CONFIG.lru)]
|
|
364
147
|
};
|
|
@@ -367,161 +150,98 @@ CacheHandler.onCreation(async () => {
|
|
|
367
150
|
const redisHandler = createRedisHandler({
|
|
368
151
|
client,
|
|
369
152
|
timeoutMs: CACHE_CONFIG.redis.timeoutMs,
|
|
370
|
-
keyExpirationStrategy: '
|
|
153
|
+
keyExpirationStrategy: 'EXAT'
|
|
371
154
|
});
|
|
372
155
|
|
|
373
156
|
const localHandler = createLruHandler(CACHE_CONFIG.lru);
|
|
374
157
|
|
|
375
|
-
|
|
376
|
-
const versionPrefix = `${
|
|
158
|
+
// Pre-compute version prefix if exists
|
|
159
|
+
const versionPrefix = CACHE_CONFIG.version ? `${CACHE_CONFIG.version}:` : '';
|
|
377
160
|
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
});
|
|
161
|
+
// Create optimized functions for each scenario
|
|
162
|
+
const versionKeyString = versionPrefix
|
|
163
|
+
? (key) => `${versionPrefix}${key}`
|
|
164
|
+
: (key) => key;
|
|
383
165
|
|
|
166
|
+
const versionKeyObject = versionPrefix
|
|
167
|
+
? (key) => ({ ...key, key: `${versionPrefix}${key.key}` })
|
|
168
|
+
: (key) => key;
|
|
169
|
+
|
|
170
|
+
// Main version key function that routes to optimized paths
|
|
384
171
|
const versionKey = (key) => {
|
|
385
172
|
return typeof key === 'string'
|
|
386
173
|
? versionKeyString(key)
|
|
387
174
|
: versionKeyObject(key);
|
|
388
175
|
};
|
|
389
176
|
|
|
177
|
+
// Create a custom handler that checks local first, then Redis
|
|
390
178
|
const customHandler = {
|
|
391
179
|
name: 'custom-local-then-redis',
|
|
392
180
|
get: async (key, context) => {
|
|
393
181
|
const vKey = versionKey(key);
|
|
182
|
+
console_log(
|
|
183
|
+
'GET called for key:',
|
|
184
|
+
typeof vKey === 'string' ? vKey : vKey?.key
|
|
185
|
+
);
|
|
394
186
|
|
|
187
|
+
// Check local cache first
|
|
188
|
+
console_log('Checking local cache...');
|
|
395
189
|
const localResult = await localHandler.get(vKey, context);
|
|
396
190
|
|
|
397
191
|
if (localResult) {
|
|
398
|
-
|
|
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
|
-
|
|
192
|
+
console_log('Found in local cache');
|
|
415
193
|
return localResult;
|
|
416
194
|
}
|
|
417
195
|
|
|
196
|
+
console_log('Not found in local, checking Redis...');
|
|
418
197
|
try {
|
|
419
198
|
const redisResult = await redisHandler.get(vKey, context);
|
|
420
199
|
|
|
421
200
|
if (redisResult) {
|
|
422
|
-
|
|
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
|
-
|
|
201
|
+
console_log('Found in Redis');
|
|
202
|
+
// Sync back to local cache for faster future access
|
|
450
203
|
try {
|
|
451
|
-
await localHandler.set(vKey,
|
|
452
|
-
|
|
453
|
-
|
|
204
|
+
await localHandler.set(vKey, redisResult, context);
|
|
205
|
+
console_log('Synced to local cache');
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console_log('Failed to sync to local:', error.message);
|
|
454
208
|
}
|
|
455
|
-
return
|
|
209
|
+
return redisResult;
|
|
456
210
|
}
|
|
457
|
-
} catch (
|
|
458
|
-
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console_log('Redis error:', error.message);
|
|
459
213
|
}
|
|
460
214
|
|
|
215
|
+
console_log('Not found in any cache');
|
|
461
216
|
return undefined;
|
|
462
217
|
},
|
|
463
218
|
set: async (key, value, context) => {
|
|
464
219
|
const vKey = versionKey(key);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
(
|
|
475
|
-
|
|
476
|
-
} catch (error) {
|
|
477
|
-
compressedValue = value;
|
|
478
|
-
shouldUseCompressed = false;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
let redisSetResult;
|
|
482
|
-
|
|
483
|
-
if (shouldUseCompressed) {
|
|
484
|
-
try {
|
|
485
|
-
await redisHandler.set(vKey, compressedValue, context);
|
|
486
|
-
|
|
487
|
-
redisSetResult = { status: 'fulfilled' };
|
|
488
|
-
} catch (compressionError) {
|
|
489
|
-
try {
|
|
490
|
-
await redisHandler.set(vKey, value, context);
|
|
491
|
-
|
|
492
|
-
redisSetResult = { status: 'fulfilled' };
|
|
493
|
-
} catch (fallbackError) {
|
|
494
|
-
redisSetResult = { status: 'rejected', reason: fallbackError };
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
} else {
|
|
498
|
-
try {
|
|
499
|
-
await redisHandler.set(vKey, value, context);
|
|
500
|
-
redisSetResult = { status: 'fulfilled' };
|
|
501
|
-
} catch (error) {
|
|
502
|
-
redisSetResult = { status: 'rejected', reason: error };
|
|
503
|
-
return redisSetResult;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
let localSetResult;
|
|
508
|
-
try {
|
|
509
|
-
await localHandler.set(vKey, value, context);
|
|
510
|
-
localSetResult = { status: 'fulfilled' };
|
|
511
|
-
} catch (error) {
|
|
512
|
-
localSetResult = { status: 'rejected', reason: error };
|
|
513
|
-
return localSetResult;
|
|
514
|
-
}
|
|
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
|
+
]);
|
|
515
231
|
},
|
|
516
232
|
delete: async (key, context) => {
|
|
517
233
|
const vKey = versionKey(key);
|
|
518
|
-
|
|
234
|
+
console_log(
|
|
235
|
+
'DELETE called for key:',
|
|
236
|
+
typeof vKey === 'string' ? vKey : vKey?.key
|
|
237
|
+
);
|
|
519
238
|
await Promise.allSettled([
|
|
520
239
|
localHandler.delete?.(vKey, context),
|
|
521
240
|
redisHandler.delete?.(vKey, context)
|
|
522
241
|
]);
|
|
523
242
|
},
|
|
524
243
|
revalidateTag: async (tags, context) => {
|
|
244
|
+
console_log('REVALIDATE_TAG called for tags:', tags);
|
|
525
245
|
await Promise.allSettled([
|
|
526
246
|
localHandler.revalidateTag?.(tags, context),
|
|
527
247
|
redisHandler.revalidateTag?.(tags, context)
|
|
@@ -529,6 +249,8 @@ CacheHandler.onCreation(async () => {
|
|
|
529
249
|
}
|
|
530
250
|
};
|
|
531
251
|
|
|
252
|
+
console_log('[Cache Handler] Handlers initialized successfully');
|
|
253
|
+
|
|
532
254
|
return {
|
|
533
255
|
handlers: [customHandler]
|
|
534
256
|
};
|
package/lib/cache.ts
CHANGED
|
@@ -3,65 +3,6 @@ import { RedisClientType } from 'redis';
|
|
|
3
3
|
import Settings from 'settings';
|
|
4
4
|
import { CacheOptions } from '../types';
|
|
5
5
|
import logger from '../utils/log';
|
|
6
|
-
const CACHE_VERSION = 'v2';
|
|
7
|
-
|
|
8
|
-
const compressData = async (data: string): Promise<Uint8Array> => {
|
|
9
|
-
const stream = new CompressionStream('gzip');
|
|
10
|
-
const writer = stream.writable.getWriter();
|
|
11
|
-
const reader = stream.readable.getReader();
|
|
12
|
-
|
|
13
|
-
writer.write(new TextEncoder().encode(data));
|
|
14
|
-
writer.close();
|
|
15
|
-
|
|
16
|
-
const chunks: Uint8Array[] = [];
|
|
17
|
-
let done = false;
|
|
18
|
-
|
|
19
|
-
while (!done) {
|
|
20
|
-
const { value, done: readerDone } = await reader.read();
|
|
21
|
-
done = readerDone;
|
|
22
|
-
if (value) chunks.push(value);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
26
|
-
const result = new Uint8Array(totalLength);
|
|
27
|
-
let offset = 0;
|
|
28
|
-
|
|
29
|
-
for (const chunk of chunks) {
|
|
30
|
-
result.set(chunk, offset);
|
|
31
|
-
offset += chunk.length;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return result;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const decompressData = async (compressed: Uint8Array): Promise<string> => {
|
|
38
|
-
const stream = new DecompressionStream('gzip');
|
|
39
|
-
const writer = stream.writable.getWriter();
|
|
40
|
-
const reader = stream.readable.getReader();
|
|
41
|
-
|
|
42
|
-
writer.write(compressed);
|
|
43
|
-
writer.close();
|
|
44
|
-
|
|
45
|
-
const chunks: Uint8Array[] = [];
|
|
46
|
-
let done = false;
|
|
47
|
-
|
|
48
|
-
while (!done) {
|
|
49
|
-
const { value, done: readerDone } = await reader.read();
|
|
50
|
-
done = readerDone;
|
|
51
|
-
if (value) chunks.push(value);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
55
|
-
const result = new Uint8Array(totalLength);
|
|
56
|
-
let offset = 0;
|
|
57
|
-
|
|
58
|
-
for (const chunk of chunks) {
|
|
59
|
-
result.set(chunk, offset);
|
|
60
|
-
offset += chunk.length;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return new TextDecoder().decode(result);
|
|
64
|
-
};
|
|
65
6
|
|
|
66
7
|
const hashCacheKey = (object?: Record<string, string>) => {
|
|
67
8
|
if (!object) {
|
|
@@ -119,32 +60,8 @@ export const CacheKey = {
|
|
|
119
60
|
export class Cache {
|
|
120
61
|
static PROXY_URL = `${process.env.NEXT_PUBLIC_URL}/api/cache`;
|
|
121
62
|
|
|
122
|
-
private static serializeValue(value: any): string {
|
|
123
|
-
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private static validateKey(key: string): boolean {
|
|
127
|
-
return !(!key || key.trim() === '');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private static validateKeyValuePairs(keyValuePairs: Record<string, any>): {
|
|
131
|
-
isValid: boolean;
|
|
132
|
-
invalidKeys: string[];
|
|
133
|
-
} {
|
|
134
|
-
if (!keyValuePairs || Object.keys(keyValuePairs).length === 0) {
|
|
135
|
-
return { isValid: false, invalidKeys: [] };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const invalidKeys = Object.keys(keyValuePairs).filter(
|
|
139
|
-
(key) => !this.validateKey(key)
|
|
140
|
-
);
|
|
141
|
-
return { isValid: invalidKeys.length === 0, invalidKeys };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
63
|
static formatKey(key: string, locale: string) {
|
|
145
|
-
return encodeURIComponent(
|
|
146
|
-
`${CACHE_VERSION}_${Settings.commerceUrl}_${locale}_${key}`
|
|
147
|
-
);
|
|
64
|
+
return encodeURIComponent(`${Settings.commerceUrl}_${locale}_${key}`);
|
|
148
65
|
}
|
|
149
66
|
|
|
150
67
|
static clientPool: Pool<RedisClientType> = createPool(
|
|
@@ -181,9 +98,9 @@ export class Cache {
|
|
|
181
98
|
return await Cache.clientPool.acquire();
|
|
182
99
|
}
|
|
183
100
|
|
|
184
|
-
static async get(key: string)
|
|
185
|
-
let value
|
|
186
|
-
let client
|
|
101
|
+
static async get(key: string) {
|
|
102
|
+
let value;
|
|
103
|
+
let client;
|
|
187
104
|
|
|
188
105
|
try {
|
|
189
106
|
client = await Cache.getClient();
|
|
@@ -193,7 +110,9 @@ export class Cache {
|
|
|
193
110
|
} else {
|
|
194
111
|
value = null;
|
|
195
112
|
}
|
|
113
|
+
logger.debug('Redis get success', { key, value });
|
|
196
114
|
} catch (error) {
|
|
115
|
+
logger.error('Redis get error', { key, error });
|
|
197
116
|
value = null;
|
|
198
117
|
} finally {
|
|
199
118
|
if (client) {
|
|
@@ -204,13 +123,14 @@ export class Cache {
|
|
|
204
123
|
return value;
|
|
205
124
|
}
|
|
206
125
|
|
|
207
|
-
static async set(key: string, value: any, expire?: number)
|
|
126
|
+
static async set(key: string, value: any, expire?: number) {
|
|
208
127
|
let success = false;
|
|
209
|
-
let client
|
|
128
|
+
let client;
|
|
210
129
|
|
|
211
130
|
try {
|
|
212
131
|
client = await Cache.getClient();
|
|
213
|
-
const serializedValue =
|
|
132
|
+
const serializedValue =
|
|
133
|
+
typeof value === 'object' ? JSON.stringify(value) : value;
|
|
214
134
|
|
|
215
135
|
if (expire) {
|
|
216
136
|
await client.set(key, serializedValue, { EX: expire });
|
|
@@ -219,7 +139,9 @@ export class Cache {
|
|
|
219
139
|
}
|
|
220
140
|
|
|
221
141
|
success = true;
|
|
142
|
+
logger.debug('Redis set success', { key, value });
|
|
222
143
|
} catch (error) {
|
|
144
|
+
logger.error('Redis set error', { key, error });
|
|
223
145
|
success = false;
|
|
224
146
|
} finally {
|
|
225
147
|
if (client) {
|
|
@@ -248,8 +170,7 @@ export class Cache {
|
|
|
248
170
|
|
|
249
171
|
const defaultOptions: CacheOptions = {
|
|
250
172
|
cache: true,
|
|
251
|
-
expire: Settings.redis.defaultExpirationTime
|
|
252
|
-
compressed: process.env.CACHE_COMPRESSION_ENABLED !== 'false'
|
|
173
|
+
expire: Settings.redis.defaultExpirationTime
|
|
253
174
|
};
|
|
254
175
|
|
|
255
176
|
const _options = Object.assign(defaultOptions, options);
|
|
@@ -259,22 +180,21 @@ export class Cache {
|
|
|
259
180
|
_options.expire = 120;
|
|
260
181
|
}
|
|
261
182
|
|
|
183
|
+
logger.debug('Cache wrap', { key, formattedKey, _options });
|
|
184
|
+
|
|
262
185
|
if (_options.cache) {
|
|
263
|
-
let cachedValue
|
|
186
|
+
let cachedValue;
|
|
264
187
|
|
|
265
188
|
if (_options.useProxy) {
|
|
266
189
|
const body = new URLSearchParams();
|
|
267
190
|
|
|
268
191
|
body.append('key', formattedKey);
|
|
269
|
-
if (_options.compressed) {
|
|
270
|
-
body.append('compressed', 'true');
|
|
271
|
-
}
|
|
272
192
|
|
|
273
193
|
cachedValue = await Cache.proxyRequest('POST', body);
|
|
194
|
+
logger.debug('Cache proxy request success', { key });
|
|
195
|
+
logger.trace('Cache proxy request', { key, cachedValue });
|
|
274
196
|
} else {
|
|
275
|
-
cachedValue =
|
|
276
|
-
? await Cache.getCompressed(formattedKey)
|
|
277
|
-
: await Cache.get(formattedKey);
|
|
197
|
+
cachedValue = await Cache.get(formattedKey);
|
|
278
198
|
}
|
|
279
199
|
|
|
280
200
|
if (cachedValue) {
|
|
@@ -282,6 +202,8 @@ export class Cache {
|
|
|
282
202
|
}
|
|
283
203
|
}
|
|
284
204
|
|
|
205
|
+
logger.debug('Redis cache miss. Setting new value...', { key });
|
|
206
|
+
|
|
285
207
|
const data = await handler();
|
|
286
208
|
|
|
287
209
|
if (data && _options.cache) {
|
|
@@ -295,19 +217,14 @@ export class Cache {
|
|
|
295
217
|
'expire',
|
|
296
218
|
String(_options?.expire ?? Settings.redis.defaultExpirationTime)
|
|
297
219
|
);
|
|
298
|
-
if (_options.compressed) {
|
|
299
|
-
body.append('compressed', 'true');
|
|
300
|
-
}
|
|
301
220
|
await Cache.proxyRequest('PUT', body);
|
|
221
|
+
|
|
222
|
+
logger.debug('Cache proxy request', { key, body: body.toString() });
|
|
302
223
|
} catch (error) {
|
|
303
224
|
logger.error('Cache proxy error', error);
|
|
304
225
|
}
|
|
305
226
|
} else {
|
|
306
|
-
|
|
307
|
-
await Cache.setCompressed(formattedKey, data, _options?.expire);
|
|
308
|
-
} else {
|
|
309
|
-
await Cache.set(formattedKey, JSON.stringify(data), _options?.expire);
|
|
310
|
-
}
|
|
227
|
+
await Cache.set(formattedKey, JSON.stringify(data), _options?.expire);
|
|
311
228
|
}
|
|
312
229
|
}
|
|
313
230
|
|
|
@@ -319,7 +236,7 @@ export class Cache {
|
|
|
319
236
|
await fetch(Cache.PROXY_URL, {
|
|
320
237
|
method,
|
|
321
238
|
headers: {
|
|
322
|
-
authorization: process.env.CACHE_SECRET
|
|
239
|
+
authorization: process.env.CACHE_SECRET
|
|
323
240
|
},
|
|
324
241
|
body
|
|
325
242
|
})
|
|
@@ -327,148 +244,4 @@ export class Cache {
|
|
|
327
244
|
|
|
328
245
|
return response;
|
|
329
246
|
}
|
|
330
|
-
|
|
331
|
-
static async mset(
|
|
332
|
-
keyValuePairs: Record<string, any>,
|
|
333
|
-
expire?: number
|
|
334
|
-
): Promise<boolean> {
|
|
335
|
-
const validation = Cache.validateKeyValuePairs(keyValuePairs);
|
|
336
|
-
if (!validation.isValid) {
|
|
337
|
-
if (validation.invalidKeys.length > 0) {
|
|
338
|
-
logger.error('Invalid keys in mset', {
|
|
339
|
-
invalidKeys: validation.invalidKeys
|
|
340
|
-
});
|
|
341
|
-
} else {
|
|
342
|
-
logger.warn('mset called with empty keyValuePairs');
|
|
343
|
-
}
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
let success = false;
|
|
348
|
-
let client: RedisClientType | undefined;
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
client = await Cache.getClient();
|
|
352
|
-
const pipeline = client.multi();
|
|
353
|
-
|
|
354
|
-
Object.entries(keyValuePairs).forEach(([key, value]) => {
|
|
355
|
-
const serializedValue = Cache.serializeValue(value);
|
|
356
|
-
if (expire) {
|
|
357
|
-
pipeline.set(key, serializedValue, { EX: expire });
|
|
358
|
-
} else {
|
|
359
|
-
pipeline.set(key, serializedValue);
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const results = await pipeline.exec();
|
|
364
|
-
|
|
365
|
-
const failures =
|
|
366
|
-
results?.filter((result) => result instanceof Error) || [];
|
|
367
|
-
|
|
368
|
-
if (failures.length > 0) {
|
|
369
|
-
success = false;
|
|
370
|
-
} else {
|
|
371
|
-
success = true;
|
|
372
|
-
}
|
|
373
|
-
} catch (error) {
|
|
374
|
-
success = false;
|
|
375
|
-
} finally {
|
|
376
|
-
if (client) {
|
|
377
|
-
await Cache.clientPool.release(client);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return success;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
static async setCompressed(
|
|
385
|
-
key: string,
|
|
386
|
-
value: any,
|
|
387
|
-
expire?: number
|
|
388
|
-
): Promise<boolean> {
|
|
389
|
-
if (!Cache.validateKey(key)) {
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
let success = false;
|
|
394
|
-
let client: RedisClientType | undefined;
|
|
395
|
-
|
|
396
|
-
try {
|
|
397
|
-
client = await Cache.getClient();
|
|
398
|
-
const serializedValue = Cache.serializeValue(value);
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
const compressed = await compressData(serializedValue);
|
|
402
|
-
const compressedBase64 = Buffer.from(compressed).toString('base64');
|
|
403
|
-
|
|
404
|
-
if (expire) {
|
|
405
|
-
await client.set(key, compressedBase64, { EX: expire });
|
|
406
|
-
} else {
|
|
407
|
-
await client.set(key, compressedBase64);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
success = true;
|
|
411
|
-
} catch (compressionError) {
|
|
412
|
-
if (expire) {
|
|
413
|
-
await client.set(key, serializedValue, { EX: expire });
|
|
414
|
-
} else {
|
|
415
|
-
await client.set(key, serializedValue);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
success = true;
|
|
419
|
-
}
|
|
420
|
-
} catch (error) {
|
|
421
|
-
success = false;
|
|
422
|
-
} finally {
|
|
423
|
-
if (client) {
|
|
424
|
-
await Cache.clientPool.release(client);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return success;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
static async getCompressed(key: string): Promise<unknown> {
|
|
432
|
-
if (!Cache.validateKey(key)) {
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
let value: unknown;
|
|
437
|
-
let client: RedisClientType | undefined;
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
client = await Cache.getClient();
|
|
441
|
-
const compressed = await client.get(key);
|
|
442
|
-
|
|
443
|
-
if (compressed) {
|
|
444
|
-
const compressedBuffer = Buffer.from(compressed, 'base64');
|
|
445
|
-
|
|
446
|
-
try {
|
|
447
|
-
const decompressedString = await decompressData(
|
|
448
|
-
new Uint8Array(compressedBuffer)
|
|
449
|
-
);
|
|
450
|
-
value = JSON.parse(decompressedString);
|
|
451
|
-
return value;
|
|
452
|
-
} catch (decompressionError) {
|
|
453
|
-
try {
|
|
454
|
-
const rawString = compressed;
|
|
455
|
-
const parsedData = JSON.parse(rawString);
|
|
456
|
-
return parsedData;
|
|
457
|
-
} catch (jsonError) {
|
|
458
|
-
return null;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
} else {
|
|
462
|
-
value = null;
|
|
463
|
-
}
|
|
464
|
-
} catch (error) {
|
|
465
|
-
value = null;
|
|
466
|
-
} finally {
|
|
467
|
-
if (client) {
|
|
468
|
-
await Cache.clientPool.release(client);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return value;
|
|
473
|
-
}
|
|
474
247
|
}
|
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.
|
|
4
|
+
"version": "1.96.0-rc.55",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -17,14 +17,13 @@
|
|
|
17
17
|
"test": "jest"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@mongodb-js/zstd": "^2.0.1",
|
|
21
|
-
"@neshca/cache-handler": "1.9.0",
|
|
22
20
|
"@opentelemetry/exporter-trace-otlp-http": "0.46.0",
|
|
23
21
|
"@opentelemetry/resources": "1.19.0",
|
|
24
22
|
"@opentelemetry/sdk-node": "0.46.0",
|
|
25
23
|
"@opentelemetry/sdk-trace-node": "1.19.0",
|
|
26
24
|
"@opentelemetry/semantic-conventions": "1.19.0",
|
|
27
25
|
"@reduxjs/toolkit": "1.9.7",
|
|
26
|
+
"@neshca/cache-handler": "1.9.0",
|
|
28
27
|
"@sentry/nextjs": "9.5.0",
|
|
29
28
|
"cross-spawn": "7.0.3",
|
|
30
29
|
"generic-pool": "3.9.0",
|
|
@@ -35,7 +34,7 @@
|
|
|
35
34
|
"set-cookie-parser": "2.6.0"
|
|
36
35
|
},
|
|
37
36
|
"devDependencies": {
|
|
38
|
-
"@akinon/eslint-plugin-projectzero": "1.
|
|
37
|
+
"@akinon/eslint-plugin-projectzero": "1.96.0-rc.55",
|
|
39
38
|
"@babel/core": "7.26.10",
|
|
40
39
|
"@babel/preset-env": "7.26.9",
|
|
41
40
|
"@babel/preset-typescript": "7.27.0",
|
package/types/index.ts
CHANGED
package/with-pz-config.js
CHANGED