@trieb.work/nextjs-turbo-redis-cache 1.16.1 → 1.16.2

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.
@@ -444,21 +444,32 @@ export default class RedisStringsHandler {
444
444
  'assertClientIsReady called more than 10 times without being ready.',
445
445
  );
446
446
  }
447
- await Promise.race([
448
- Promise.all([
449
- this.sharedTagsMap.waitUntilReady(),
450
- this.revalidatedTagsMap.waitUntilReady(),
451
- ]),
452
- new Promise((_, reject) =>
453
- setTimeout(() => {
454
- reject(
455
- new Error(
456
- 'assertClientIsReady: Timeout waiting for Redis maps to be ready',
457
- ),
458
- );
459
- }, 30_000),
460
- ),
461
- ]);
447
+ let readyTimeout: ReturnType<typeof setTimeout> | undefined;
448
+ try {
449
+ await Promise.race([
450
+ Promise.all([
451
+ this.sharedTagsMap.waitUntilReady(),
452
+ this.revalidatedTagsMap.waitUntilReady(),
453
+ ]),
454
+ new Promise((_, reject) => {
455
+ readyTimeout = setTimeout(() => {
456
+ reject(
457
+ new Error(
458
+ 'assertClientIsReady: Timeout waiting for Redis maps to be ready',
459
+ ),
460
+ );
461
+ }, 30_000);
462
+ }),
463
+ ]);
464
+ } finally {
465
+ // Always clear the timeout: once the race is won (the common case after
466
+ // the initial sync), the timer would otherwise stay pending for the full
467
+ // 30s. Timers capture the ambient async context (AsyncLocalStorage),
468
+ // so in a Next.js server every leaked timer pins the request's store —
469
+ // and with it the whole response object graph — for at least 30s on
470
+ // every single cache operation.
471
+ clearTimeout(readyTimeout);
472
+ }
462
473
  this.clientReadyCalls = 0;
463
474
  if (!this.client.isReady) {
464
475
  throw new Error(
@@ -85,6 +85,34 @@ describe('RedisStringsHandler', () => {
85
85
  expect(res).toBeNull();
86
86
  expect(consoleErrorSpy).not.toHaveBeenCalled();
87
87
  });
88
+
89
+ it('does not leave the readiness timeout timer pending after an operation', async () => {
90
+ vi.useFakeTimers();
91
+ try {
92
+ const handler = new RedisStringsHandler({
93
+ redisUrl: 'redis://localhost:6379',
94
+ keyPrefix: 'test:',
95
+ database: 0,
96
+ getTimeoutMs: 1,
97
+ redisGetDeduplication: false,
98
+ });
99
+
100
+ const timersBefore = vi.getTimerCount();
101
+ await handler.get('missing-key', {
102
+ kind: 'APP_PAGE',
103
+ isRoutePPREnabled: false,
104
+ isFallback: false,
105
+ });
106
+
107
+ // assertClientIsReady() races waitUntilReady() against a 30s timeout;
108
+ // the timer must not stay pending once the race is settled, because
109
+ // pending timers retain the ambient async context (and with it, in a
110
+ // Next.js server, the whole per-request object graph).
111
+ expect(vi.getTimerCount()).toBe(timersBefore);
112
+ } finally {
113
+ vi.useRealTimers();
114
+ }
115
+ });
88
116
  });
89
117
 
90
118
  describe('Public exports', () => {