@akinon/next 1.96.0-snapshot-ZERO-35861-20250908151109 → 1.96.0

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 CHANGED
@@ -1,11 +1,11 @@
1
1
  # @akinon/next
2
2
 
3
- ## 1.96.0-snapshot-ZERO-35861-20250908151109
3
+ ## 1.96.0
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - 5ba63ef: ZERO-3586_1: Commit 3586 to 1.93.0 @akinon/next version.
8
- - c91a2bc: ZERO-35861: 1.93.0 fix
7
+ - af5c93a: ZERO-3617: Add pre-order middleware instruction file
8
+ - a420947: ZERO-3517: Fix optional chaining for rawData in error logging for category data handlers
9
9
 
10
10
  ## 1.95.0
11
11
 
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, keyValuePairs, compressed } = body;
37
- let response: any;
30
+ const { key, value, expire } = body;
31
+ let response: string | boolean;
38
32
 
39
33
  try {
40
34
  if (req.method === 'POST') {
41
- if (compressed === 'true') {
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
- if (keyValuePairs) {
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);
@@ -48,8 +48,8 @@ function getCategoryDataHandler(
48
48
  logger.fatal('Error while parsing category data', {
49
49
  handler: 'getCategoryDataHandler',
50
50
  error,
51
- rawData: rawData.startsWith('<!DOCTYPE html>')
52
- ? `${rawData.substring(0, 50)}...`
51
+ rawData: rawData?.startsWith?.('<!DOCTYPE html>')
52
+ ? `${rawData?.substring(0, 50)}...`
53
53
  : rawData
54
54
  });
55
55
  }
@@ -98,8 +98,7 @@ export const getCategoryData = ({
98
98
  locale,
99
99
  getCategoryDataHandler(pk, locale, currency, searchParams, headers),
100
100
  {
101
- expire: 300,
102
- compressed: true
101
+ expire: 300
103
102
  }
104
103
  );
105
104
  };
@@ -139,8 +138,8 @@ function getCategoryBySlugDataHandler(
139
138
  logger.fatal('Error while parsing category data', {
140
139
  handler: 'getCategoryBySlugDataHandler',
141
140
  error,
142
- rawData: rawData.startsWith('<!DOCTYPE html>')
143
- ? `${rawData.substring(0, 50)}...`
141
+ rawData: rawData?.startsWith?.('<!DOCTYPE html>')
142
+ ? `${rawData?.substring(0, 50)}...`
144
143
  : rawData
145
144
  });
146
145
  }
@@ -159,8 +158,7 @@ export const getCategoryBySlugData = async ({
159
158
  locale,
160
159
  getCategoryBySlugDataHandler(slug, locale, currency),
161
160
  {
162
- expire: 300,
163
- compressed: true
161
+ expire: 300
164
162
  }
165
163
  );
166
164
  };
@@ -42,9 +42,6 @@ export const getFlatPageData = ({
42
42
  return Cache.wrap(
43
43
  CacheKey.FlatPage(pk),
44
44
  locale,
45
- getFlatPageDataHandler(pk, locale, currency, headers),
46
- {
47
- compressed: true
48
- }
45
+ getFlatPageDataHandler(pk, locale, currency, headers)
49
46
  );
50
47
  };
@@ -43,9 +43,6 @@ export const getFormData = ({
43
43
  return Cache.wrap(
44
44
  CacheKey.Form(pk),
45
45
  locale,
46
- getFormDataHandler(pk, locale, currency, headers),
47
- {
48
- compressed: true
49
- }
46
+ getFormDataHandler(pk, locale, currency, headers)
50
47
  );
51
48
  };
@@ -71,8 +71,7 @@ export const getListData = async ({
71
71
  locale,
72
72
  getListDataHandler(locale, currency, searchParams, headers),
73
73
  {
74
- expire: 300,
75
- compressed: true
74
+ expire: 300
76
75
  }
77
76
  );
78
77
  };
@@ -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
- }
55
- );
51
+ getMenuHandler(params)
52
+ );
56
53
  };
@@ -116,8 +116,7 @@ export const getProductData = async ({
116
116
  headers
117
117
  }),
118
118
  {
119
- expire: 300,
120
- compressed: true
119
+ expire: 300
121
120
  }
122
121
  );
123
122
  };
@@ -44,9 +44,6 @@ export const getWidgetData = async <T>({
44
44
  CacheKey.Widget(slug),
45
45
  locale,
46
46
  getWidgetDataHandler(slug, locale, currency, headers),
47
- {
48
- ...cacheOptions,
49
- compressed: true
50
- }
47
+ cacheOptions
51
48
  );
52
49
  };
@@ -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
- 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
-
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: 'EXPIREAT'
153
+ keyExpirationStrategy: 'EXAT'
371
154
  });
372
155
 
373
156
  const localHandler = createLruHandler(CACHE_CONFIG.lru);
374
157
 
375
- const CACHE_VERSION = 'v2';
376
- const versionPrefix = `${CACHE_VERSION}_`;
158
+ // Pre-compute version prefix if exists
159
+ const versionPrefix = CACHE_CONFIG.version ? `${CACHE_CONFIG.version}:` : '';
377
160
 
378
- const versionKeyString = (key) => `${versionPrefix}${key}`;
379
- const versionKeyObject = (key) => ({
380
- ...key,
381
- key: `${versionPrefix}${key.key}`
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
- 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
-
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
- 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
-
201
+ console_log('Found in Redis');
202
+ // Sync back to local cache for faster future access
450
203
  try {
451
- await localHandler.set(vKey, finalResult, context);
452
- } catch (_) {
453
- return finalResult;
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 finalResult;
209
+ return redisResult;
456
210
  }
457
- } catch (_) {
458
- return undefined;
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
- let compressedValue;
467
- let shouldUseCompressed = false;
468
-
469
- try {
470
- compressedValue = await compressValue(value);
471
-
472
- shouldUseCompressed =
473
- compressedValue !== value &&
474
- (compressedValue?.__compressed ||
475
- compressedValue?.value?.__compressed);
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) {
@@ -90,8 +31,6 @@ export const CacheKey = {
90
31
  `category_${pk}_${encodeURIComponent(
91
32
  JSON.stringify(searchParams)
92
33
  )}${hashCacheKey(headers)}`,
93
- Basket: (namespace?: string) => `basket${namespace ? `_${namespace}` : ''}`,
94
- AllBaskets: () => 'all_baskets',
95
34
  CategorySlug: (slug: string) => `category_${slug}`,
96
35
  SpecialPage: (
97
36
  pk: number,
@@ -119,32 +58,8 @@ export const CacheKey = {
119
58
  export class Cache {
120
59
  static PROXY_URL = `${process.env.NEXT_PUBLIC_URL}/api/cache`;
121
60
 
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
61
  static formatKey(key: string, locale: string) {
145
- return encodeURIComponent(
146
- `${CACHE_VERSION}_${Settings.commerceUrl}_${locale}_${key}`
147
- );
62
+ return encodeURIComponent(`${Settings.commerceUrl}_${locale}_${key}`);
148
63
  }
149
64
 
150
65
  static clientPool: Pool<RedisClientType> = createPool(
@@ -181,9 +96,9 @@ export class Cache {
181
96
  return await Cache.clientPool.acquire();
182
97
  }
183
98
 
184
- static async get(key: string): Promise<any> {
185
- let value: any;
186
- let client: RedisClientType | undefined;
99
+ static async get(key: string) {
100
+ let value;
101
+ let client;
187
102
 
188
103
  try {
189
104
  client = await Cache.getClient();
@@ -193,7 +108,9 @@ export class Cache {
193
108
  } else {
194
109
  value = null;
195
110
  }
111
+ logger.debug('Redis get success', { key, value });
196
112
  } catch (error) {
113
+ logger.error('Redis get error', { key, error });
197
114
  value = null;
198
115
  } finally {
199
116
  if (client) {
@@ -204,13 +121,14 @@ export class Cache {
204
121
  return value;
205
122
  }
206
123
 
207
- static async set(key: string, value: any, expire?: number): Promise<boolean> {
124
+ static async set(key: string, value: any, expire?: number) {
208
125
  let success = false;
209
- let client: RedisClientType | undefined;
126
+ let client;
210
127
 
211
128
  try {
212
129
  client = await Cache.getClient();
213
- const serializedValue = Cache.serializeValue(value);
130
+ const serializedValue =
131
+ typeof value === 'object' ? JSON.stringify(value) : value;
214
132
 
215
133
  if (expire) {
216
134
  await client.set(key, serializedValue, { EX: expire });
@@ -219,7 +137,9 @@ export class Cache {
219
137
  }
220
138
 
221
139
  success = true;
140
+ logger.debug('Redis set success', { key, value });
222
141
  } catch (error) {
142
+ logger.error('Redis set error', { key, error });
223
143
  success = false;
224
144
  } finally {
225
145
  if (client) {
@@ -248,8 +168,7 @@ export class Cache {
248
168
 
249
169
  const defaultOptions: CacheOptions = {
250
170
  cache: true,
251
- expire: Settings.redis.defaultExpirationTime,
252
- compressed: process.env.CACHE_COMPRESSION_ENABLED !== 'false'
171
+ expire: Settings.redis.defaultExpirationTime
253
172
  };
254
173
 
255
174
  const _options = Object.assign(defaultOptions, options);
@@ -259,22 +178,21 @@ export class Cache {
259
178
  _options.expire = 120;
260
179
  }
261
180
 
181
+ logger.debug('Cache wrap', { key, formattedKey, _options });
182
+
262
183
  if (_options.cache) {
263
- let cachedValue: any;
184
+ let cachedValue;
264
185
 
265
186
  if (_options.useProxy) {
266
187
  const body = new URLSearchParams();
267
188
 
268
189
  body.append('key', formattedKey);
269
- if (_options.compressed) {
270
- body.append('compressed', 'true');
271
- }
272
190
 
273
191
  cachedValue = await Cache.proxyRequest('POST', body);
192
+ logger.debug('Cache proxy request success', { key });
193
+ logger.trace('Cache proxy request', { key, cachedValue });
274
194
  } else {
275
- cachedValue = _options.compressed
276
- ? await Cache.getCompressed(formattedKey)
277
- : await Cache.get(formattedKey);
195
+ cachedValue = await Cache.get(formattedKey);
278
196
  }
279
197
 
280
198
  if (cachedValue) {
@@ -282,6 +200,8 @@ export class Cache {
282
200
  }
283
201
  }
284
202
 
203
+ logger.debug('Redis cache miss. Setting new value...', { key });
204
+
285
205
  const data = await handler();
286
206
 
287
207
  if (data && _options.cache) {
@@ -295,19 +215,14 @@ export class Cache {
295
215
  'expire',
296
216
  String(_options?.expire ?? Settings.redis.defaultExpirationTime)
297
217
  );
298
- if (_options.compressed) {
299
- body.append('compressed', 'true');
300
- }
301
218
  await Cache.proxyRequest('PUT', body);
219
+
220
+ logger.debug('Cache proxy request', { key, body: body.toString() });
302
221
  } catch (error) {
303
222
  logger.error('Cache proxy error', error);
304
223
  }
305
224
  } else {
306
- if (_options.compressed) {
307
- await Cache.setCompressed(formattedKey, data, _options?.expire);
308
- } else {
309
- await Cache.set(formattedKey, JSON.stringify(data), _options?.expire);
310
- }
225
+ await Cache.set(formattedKey, JSON.stringify(data), _options?.expire);
311
226
  }
312
227
  }
313
228
 
@@ -319,7 +234,7 @@ export class Cache {
319
234
  await fetch(Cache.PROXY_URL, {
320
235
  method,
321
236
  headers: {
322
- authorization: process.env.CACHE_SECRET || ''
237
+ authorization: process.env.CACHE_SECRET
323
238
  },
324
239
  body
325
240
  })
@@ -327,148 +242,4 @@ export class Cache {
327
242
 
328
243
  return response;
329
244
  }
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
245
  }
@@ -53,8 +53,7 @@ const resolvePrettyUrl = async (
53
53
  locale,
54
54
  resolvePrettyUrlHandler(pathname, ip),
55
55
  {
56
- useProxy: true,
57
- compressed: true
56
+ useProxy: true
58
57
  }
59
58
  );
60
59
  };
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.96.0-snapshot-ZERO-35861-20250908151109",
4
+ "version": "1.96.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -17,8 +17,7 @@
17
17
  "test": "jest"
18
18
  },
19
19
  "dependencies": {
20
- "@mongodb-js/zstd": "^2.0.1",
21
- "@neshca/cache-handler": "1.9.0",
20
+ "@neshca/cache-handler": "1.5.1",
22
21
  "@opentelemetry/exporter-trace-otlp-http": "0.46.0",
23
22
  "@opentelemetry/resources": "1.19.0",
24
23
  "@opentelemetry/sdk-node": "0.46.0",
@@ -35,7 +34,7 @@
35
34
  "set-cookie-parser": "2.6.0"
36
35
  },
37
36
  "devDependencies": {
38
- "@akinon/eslint-plugin-projectzero": "1.96.0-snapshot-ZERO-35861-20250908151109",
37
+ "@akinon/eslint-plugin-projectzero": "1.96.0",
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
@@ -216,7 +216,6 @@ export interface CacheOptions {
216
216
  cache?: boolean;
217
217
  expire?: number;
218
218
  useProxy?: boolean;
219
- compressed?: boolean;
220
219
  }
221
220
 
222
221
  export interface SetCookieOptions {