@windrun-huaiin/backend-core 13.0.0 → 14.1.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.
Files changed (110) hide show
  1. package/dist/_virtual/index.js +7 -3
  2. package/dist/_virtual/index.mjs +5 -3
  3. package/dist/_virtual/index2.js +2 -6
  4. package/dist/_virtual/index2.mjs +2 -6
  5. package/dist/index.js +3 -1
  6. package/dist/index.mjs +2 -2
  7. package/dist/lib/index.js +3 -1
  8. package/dist/lib/index.mjs +2 -2
  9. package/dist/lib/stripe-config.d.ts +1 -1
  10. package/dist/lib/stripe-config.d.ts.map +1 -1
  11. package/dist/lib/stripe-config.js +25 -16
  12. package/dist/lib/stripe-config.mjs +25 -16
  13. package/dist/lib/upstash/qstash.d.ts.map +1 -1
  14. package/dist/lib/upstash/qstash.js +66 -62
  15. package/dist/lib/upstash/qstash.mjs +67 -63
  16. package/dist/lib/upstash/redis-counter.d.ts.map +1 -1
  17. package/dist/lib/upstash/redis-counter.js +9 -24
  18. package/dist/lib/upstash/redis-counter.mjs +10 -25
  19. package/dist/lib/upstash/redis-favorite.d.ts.map +1 -1
  20. package/dist/lib/upstash/redis-favorite.js +22 -36
  21. package/dist/lib/upstash/redis-favorite.mjs +23 -37
  22. package/dist/lib/upstash/redis-like.d.ts.map +1 -1
  23. package/dist/lib/upstash/redis-like.js +22 -36
  24. package/dist/lib/upstash/redis-like.mjs +23 -37
  25. package/dist/lib/upstash/redis-lock.d.ts.map +1 -1
  26. package/dist/lib/upstash/redis-lock.js +22 -38
  27. package/dist/lib/upstash/redis-lock.mjs +23 -39
  28. package/dist/lib/upstash/redis-structures.d.ts.map +1 -1
  29. package/dist/lib/upstash/redis-structures.js +77 -113
  30. package/dist/lib/upstash/redis-structures.mjs +78 -114
  31. package/dist/lib/upstash-config.d.ts +9 -1
  32. package/dist/lib/upstash-config.d.ts.map +1 -1
  33. package/dist/lib/upstash-config.js +221 -27
  34. package/dist/lib/upstash-config.mjs +220 -28
  35. package/dist/node_modules/.pnpm/{@upstash_qstash@2.8.4/node_modules/@upstash/qstash/chunk-RQPZUJXG.js → @upstash_qstash@2.10.1/node_modules/@upstash/qstash/chunk-35B33QW3.js} +897 -468
  36. package/dist/node_modules/.pnpm/{@upstash_qstash@2.8.4/node_modules/@upstash/qstash/chunk-RQPZUJXG.mjs → @upstash_qstash@2.10.1/node_modules/@upstash/qstash/chunk-35B33QW3.mjs} +895 -468
  37. package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1/node_modules/@upstash/redis/chunk-LLI2WIYN.js → @upstash_redis@1.37.0/node_modules/@upstash/redis/chunk-IH7W44G6.js} +657 -40
  38. package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1/node_modules/@upstash/redis/chunk-LLI2WIYN.mjs → @upstash_redis@1.37.0/node_modules/@upstash/redis/chunk-IH7W44G6.mjs} +657 -41
  39. package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1 → @upstash_redis@1.37.0}/node_modules/@upstash/redis/nodejs.js +6 -5
  40. package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1 → @upstash_redis@1.37.0}/node_modules/@upstash/redis/nodejs.mjs +2 -2
  41. package/dist/node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.js +1 -1
  42. package/dist/node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.mjs +1 -1
  43. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/jws/flattened/verify.js +6 -6
  44. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/jwt/verify.js +1 -1
  45. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/key/import.js +2 -2
  46. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/epoch.js +3 -1
  47. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_disjoint.js +3 -1
  48. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_jwk.js +1 -1
  49. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_object.js +3 -1
  50. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/jwt_claims_set.js +7 -5
  51. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/secs.js +3 -1
  52. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/validate_algorithms.js +3 -1
  53. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/validate_crit.js +3 -1
  54. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/check_key_length.js +3 -1
  55. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/dsa_digest.js +3 -1
  56. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/get_named_curve.js +4 -2
  57. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/get_sign_verify_key.js +3 -1
  58. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/hmac_digest.js +3 -1
  59. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/is_key_like.js +1 -1
  60. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/is_key_object.js +3 -1
  61. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/jwk_to_key.js +3 -1
  62. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/node_key.js +6 -4
  63. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/sign.js +6 -4
  64. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/verify.js +7 -5
  65. package/dist/services/stripe/webhook-handler.js +4 -2
  66. package/dist/services/stripe/webhook-handler.mjs +3 -1
  67. package/package.json +5 -5
  68. package/src/lib/stripe-config.ts +27 -15
  69. package/src/lib/upstash/qstash.ts +64 -62
  70. package/src/lib/upstash/redis-counter.ts +10 -26
  71. package/src/lib/upstash/redis-favorite.ts +23 -42
  72. package/src/lib/upstash/redis-like.ts +23 -42
  73. package/src/lib/upstash/redis-lock.ts +23 -49
  74. package/src/lib/upstash/redis-structures.ts +82 -131
  75. package/src/lib/upstash-config.ts +231 -24
  76. package/src/services/stripe/webhook-handler.ts +3 -1
  77. package/dist/_virtual/index3.js +0 -5
  78. package/dist/_virtual/index3.mjs +0 -3
  79. package/dist/node_modules/.pnpm/@upstash_lock@0.2.1_typescript@5.9.3/node_modules/@upstash/lock/dist/index.js +0 -191
  80. package/dist/node_modules/.pnpm/@upstash_lock@0.2.1_typescript@5.9.3/node_modules/@upstash/lock/dist/index.mjs +0 -189
  81. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.js +0 -54
  82. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.mjs +0 -51
  83. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.js +0 -44
  84. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.mjs +0 -35
  85. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.js +0 -31
  86. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.mjs +0 -18
  87. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.js +0 -587
  88. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.mjs +0 -527
  89. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.js +0 -447
  90. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.mjs +0 -399
  91. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.js +0 -245
  92. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.mjs +0 -232
  93. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.js +0 -68
  94. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.mjs +0 -62
  95. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.js +0 -39
  96. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.mjs +0 -37
  97. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.js +0 -80
  98. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.mjs +0 -75
  99. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.js +0 -101
  100. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.mjs +0 -86
  101. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.js +0 -102
  102. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.mjs +0 -76
  103. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.js +0 -56
  104. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.mjs +0 -52
  105. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.js +0 -1205
  106. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.mjs +0 -1157
  107. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.js +0 -407
  108. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.mjs +0 -374
  109. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.js +0 -9
  110. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.mjs +0 -7
@@ -1,4 +1,4 @@
1
- import { getRedis } from '../upstash-config';
1
+ import { withRedis } from '../upstash-config';
2
2
 
3
3
  /**
4
4
  * Set a plain string value with optional TTL (seconds).
@@ -8,30 +8,22 @@ export const setString = async (
8
8
  value: string,
9
9
  ttlSec?: number
10
10
  ): Promise<boolean> => {
11
- const redis = getRedis();
12
- if (!redis) {
13
- return false;
14
- }
11
+ return withRedis(async (redis) => {
12
+ if (ttlSec && ttlSec > 0) {
13
+ await redis.set(key, value, { ex: ttlSec });
14
+ return true;
15
+ }
15
16
 
16
- if (ttlSec && ttlSec > 0) {
17
- await redis.set(key, value, { ex: ttlSec });
17
+ await redis.set(key, value);
18
18
  return true;
19
- }
20
-
21
- await redis.set(key, value);
22
- return true;
19
+ }).then((result) => result ?? false);
23
20
  };
24
21
 
25
22
  /**
26
23
  * Get a plain string value. Returns null if Redis is unavailable or key missing.
27
24
  */
28
25
  export const getString = async (key: string): Promise<string | null> => {
29
- const redis = getRedis();
30
- if (!redis) {
31
- return null;
32
- }
33
-
34
- return redis.get<string>(key);
26
+ return withRedis((redis) => redis.get<string>(key));
35
27
  };
36
28
 
37
29
  /**
@@ -42,78 +34,63 @@ export const setJson = async <T>(
42
34
  value: T,
43
35
  ttlSec?: number
44
36
  ): Promise<boolean> => {
45
- const redis = getRedis();
46
- if (!redis) {
47
- return false;
48
- }
49
-
50
- const payload = JSON.stringify(value);
51
- if (ttlSec && ttlSec > 0) {
52
- await redis.set(key, payload, { ex: ttlSec });
37
+ return withRedis(async (redis) => {
38
+ const payload = JSON.stringify(value);
39
+ if (ttlSec && ttlSec > 0) {
40
+ await redis.set(key, payload, { ex: ttlSec });
41
+ return true;
42
+ }
43
+
44
+ await redis.set(key, payload);
53
45
  return true;
54
- }
55
-
56
- await redis.set(key, payload);
57
- return true;
46
+ }).then((result) => result ?? false);
58
47
  };
59
48
 
60
49
  /**
61
50
  * Get an object stored as JSON string. Returns null if missing or invalid JSON.
62
51
  */
63
52
  export const getJson = async <T>(key: string): Promise<T | null> => {
64
- const redis = getRedis();
65
- if (!redis) {
66
- return null;
67
- }
68
-
69
- const payload = await redis.get<string>(key);
70
- if (!payload) {
71
- return null;
72
- }
73
-
74
- try {
75
- return JSON.parse(payload) as T;
76
- } catch {
77
- return null;
78
- }
53
+ return withRedis(async (redis) => {
54
+ const payload = await redis.get<string>(key);
55
+ if (!payload) {
56
+ return null;
57
+ }
58
+
59
+ try {
60
+ return JSON.parse(payload) as T;
61
+ } catch {
62
+ return null;
63
+ }
64
+ });
79
65
  };
80
66
 
81
67
  /**
82
68
  * Delete a key. Returns false if Redis is unavailable.
83
69
  */
84
70
  export const deleteKey = async (key: string): Promise<boolean> => {
85
- const redis = getRedis();
86
- if (!redis) {
87
- return false;
88
- }
89
-
90
- const deleted = await redis.del(key);
91
- return deleted > 0;
71
+ const result = await withRedis(async (redis) => {
72
+ const deleted = await redis.del(key);
73
+ return deleted > 0;
74
+ });
75
+ return result ?? false;
92
76
  };
93
77
 
94
78
  /**
95
79
  * Set a hash field value.
96
80
  */
97
81
  export const setHashField = async (key: string, field: string, value: string): Promise<boolean> => {
98
- const redis = getRedis();
99
- if (!redis) {
100
- return false;
101
- }
102
-
103
- const result = await redis.hset(key, { [field]: value });
104
- return result > 0;
82
+ const result = await withRedis(async (redis) => {
83
+ const changed = await redis.hset(key, { [field]: value });
84
+ return changed > 0;
85
+ });
86
+ return result ?? false;
105
87
  };
106
88
 
107
89
  /**
108
90
  * Get a hash field value.
109
91
  */
110
92
  export const getHashField = async (key: string, field: string): Promise<string | null> => {
111
- const redis = getRedis();
112
- if (!redis) {
113
- return null;
114
- }
115
-
116
- return redis.hget<string>(key, field);
93
+ return withRedis((redis) => redis.hget<string>(key, field));
117
94
  };
118
95
 
119
96
  /**
@@ -124,61 +101,51 @@ export const setHashJson = async <T>(
124
101
  field: string,
125
102
  value: T
126
103
  ): Promise<boolean> => {
127
- const redis = getRedis();
128
- if (!redis) {
129
- return false;
130
- }
131
-
132
- const payload = JSON.stringify(value);
133
- const result = await redis.hset(key, { [field]: payload });
134
- return result > 0;
104
+ const result = await withRedis(async (redis) => {
105
+ const payload = JSON.stringify(value);
106
+ const changed = await redis.hset(key, { [field]: payload });
107
+ return changed > 0;
108
+ });
109
+ return result ?? false;
135
110
  };
136
111
 
137
112
  /**
138
113
  * Get a hash field stored as JSON string.
139
114
  */
140
115
  export const getHashJson = async <T>(key: string, field: string): Promise<T | null> => {
141
- const redis = getRedis();
142
- if (!redis) {
143
- return null;
144
- }
145
-
146
- const payload = await redis.hget<string>(key, field);
147
- if (!payload) {
148
- return null;
149
- }
150
-
151
- try {
152
- return JSON.parse(payload) as T;
153
- } catch {
154
- return null;
155
- }
116
+ return withRedis(async (redis) => {
117
+ const payload = await redis.hget<string>(key, field);
118
+ if (!payload) {
119
+ return null;
120
+ }
121
+
122
+ try {
123
+ return JSON.parse(payload) as T;
124
+ } catch {
125
+ return null;
126
+ }
127
+ });
156
128
  };
157
129
 
158
130
  /**
159
131
  * Get all hash fields.
160
132
  */
161
133
  export const getHashAll = async (key: string): Promise<Record<string, string> | null> => {
162
- const redis = getRedis();
163
- if (!redis) {
164
- return null;
165
- }
166
-
167
- const result = await redis.hgetall<Record<string, string>>(key);
168
- return result ?? {};
134
+ return withRedis(async (redis) => {
135
+ const result = await redis.hgetall<Record<string, string>>(key);
136
+ return result ?? {};
137
+ });
169
138
  };
170
139
 
171
140
  /**
172
141
  * Remove a hash field.
173
142
  */
174
143
  export const deleteHashField = async (key: string, field: string): Promise<boolean> => {
175
- const redis = getRedis();
176
- if (!redis) {
177
- return false;
178
- }
179
-
180
- const removed = await redis.hdel(key, field);
181
- return removed > 0;
144
+ const result = await withRedis(async (redis) => {
145
+ const removed = await redis.hdel(key, field);
146
+ return removed > 0;
147
+ });
148
+ return result ?? false;
182
149
  };
183
150
 
184
151
  type ListDirection = 'left' | 'right';
@@ -191,18 +158,15 @@ export const pushList = async (
191
158
  values: string[],
192
159
  direction: ListDirection = 'right'
193
160
  ): Promise<number | null> => {
194
- const redis = getRedis();
195
- if (!redis) {
196
- return null;
197
- }
198
-
199
- if (values.length === 0) {
200
- return redis.llen(key);
201
- }
202
-
203
- return direction === 'left'
204
- ? redis.lpush(key, ...values)
205
- : redis.rpush(key, ...values);
161
+ return withRedis((redis) => {
162
+ if (values.length === 0) {
163
+ return redis.llen(key);
164
+ }
165
+
166
+ return direction === 'left'
167
+ ? redis.lpush(key, ...values)
168
+ : redis.rpush(key, ...values);
169
+ });
206
170
  };
207
171
 
208
172
  /**
@@ -212,12 +176,9 @@ export const popList = async (
212
176
  key: string,
213
177
  direction: ListDirection = 'right'
214
178
  ): Promise<string | null> => {
215
- const redis = getRedis();
216
- if (!redis) {
217
- return null;
218
- }
219
-
220
- return direction === 'left' ? redis.lpop<string>(key) : redis.rpop<string>(key);
179
+ return withRedis((redis) =>
180
+ direction === 'left' ? redis.lpop<string>(key) : redis.rpop<string>(key)
181
+ );
221
182
  };
222
183
 
223
184
  /**
@@ -228,22 +189,12 @@ export const rangeList = async (
228
189
  start = 0,
229
190
  stop = -1
230
191
  ): Promise<string[] | null> => {
231
- const redis = getRedis();
232
- if (!redis) {
233
- return null;
234
- }
235
-
236
- return redis.lrange<string>(key, start, stop);
192
+ return withRedis((redis) => redis.lrange<string>(key, start, stop));
237
193
  };
238
194
 
239
195
  /**
240
196
  * Get list length.
241
197
  */
242
198
  export const listLength = async (key: string): Promise<number | null> => {
243
- const redis = getRedis();
244
- if (!redis) {
245
- return null;
246
- }
247
-
248
- return redis.llen(key);
199
+ return withRedis((redis) => redis.llen(key));
249
200
  };
@@ -3,52 +3,259 @@ import { Client as QstashClient } from '@upstash/qstash';
3
3
 
4
4
  let cachedRedis: Redis | null = null;
5
5
  let cachedQstash: QstashClient | null = null;
6
- let redisInitAttempted = false;
7
- let qstashInitAttempted = false;
6
+ let redisInitPromise: Promise<Redis | null> | null = null;
7
+ let qstashInitPromise: Promise<QstashClient | null> | null = null;
8
+
9
+ let redisWarnedMissingEnv = false;
10
+ let redisWarnedInvalidEnv = false;
11
+ let redisWarnedInitError = false;
12
+ let redisWarnedHealthCheck = false;
13
+ let redisWarnedHealthSchedule = false;
14
+
15
+ let qstashWarnedMissingEnv = false;
16
+ let qstashWarnedInitError = false;
17
+ let qstashWarnedHealthCheck = false;
18
+ let qstashWarnedHealthSchedule = false;
19
+
20
+ let redisHealthTimer: ReturnType<typeof setTimeout> | null = null;
21
+ let qstashHealthTimer: ReturnType<typeof setTimeout> | null = null;
22
+
23
+ const isNonEmpty = (value: string | undefined): value is string =>
24
+ typeof value === 'string' && value.trim().length > 0;
25
+
26
+ const isValidUrl = (value: string): boolean => {
27
+ try {
28
+ const parsed = new URL(value);
29
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
30
+ } catch {
31
+ return false;
32
+ }
33
+ };
34
+
35
+ const parseMinutes = (value: string | undefined, fallback: number): number => {
36
+ if (!isNonEmpty(value)) {
37
+ return fallback;
38
+ }
39
+ const minutes = Number(value);
40
+ if (!Number.isFinite(minutes) || minutes <= 0) {
41
+ return fallback;
42
+ }
43
+ return minutes;
44
+ };
45
+
46
+ const getRedisHealthIntervalMinutes = (): number =>
47
+ parseMinutes(process.env.UPSTASH_REDIS_HEALTHCHECK_INTERVAL_MINUTES, 10);
48
+
49
+ const getQstashHealthIntervalMinutes = (): number =>
50
+ parseMinutes(process.env.UPSTASH_QSTASH_HEALTHCHECK_INTERVAL_MINUTES, 10);
51
+
52
+ const getQstashHealthcheckUrl = (): string =>
53
+ process.env.UPSTASH_QSTASH_HEALTHCHECK_URL ?? 'https://qstash.upstash.io/v2/topics';
54
+
55
+ const scheduleRedisHealthCheck = (): void => {
56
+ if (redisHealthTimer || !cachedRedis) {
57
+ return;
58
+ }
59
+ const delayMs = getRedisHealthIntervalMinutes() * 60_000;
60
+ redisHealthTimer = setTimeout(async () => {
61
+ redisHealthTimer = null;
62
+ if (!cachedRedis) {
63
+ return;
64
+ }
65
+ try {
66
+ await cachedRedis.ping();
67
+ } catch (error) {
68
+ cachedRedis = null;
69
+ if (!redisWarnedHealthCheck) {
70
+ redisWarnedHealthCheck = true;
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ console.warn(`[Upstash Config] Redis health check failed: ${message}`);
73
+ }
74
+ } finally {
75
+ try {
76
+ scheduleRedisHealthCheck();
77
+ } catch (error) {
78
+ if (!redisWarnedHealthSchedule) {
79
+ redisWarnedHealthSchedule = true;
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ console.warn(`[Upstash Config] Redis health check schedule failed: ${message}`);
82
+ }
83
+ }
84
+ }
85
+ }, delayMs);
86
+ };
87
+
88
+ const checkQstashHealth = async (token: string): Promise<void> => {
89
+ const response = await fetch(getQstashHealthcheckUrl(), {
90
+ method: 'GET',
91
+ headers: {
92
+ Authorization: `Bearer ${token}`,
93
+ },
94
+ cache: 'no-store',
95
+ });
96
+ if (!response.ok) {
97
+ throw new Error(`HTTP ${response.status}`);
98
+ }
99
+ };
100
+
101
+ const scheduleQstashHealthCheck = (token: string): void => {
102
+ if (qstashHealthTimer || !cachedQstash) {
103
+ return;
104
+ }
105
+ const delayMs = getQstashHealthIntervalMinutes() * 60_000;
106
+ qstashHealthTimer = setTimeout(async () => {
107
+ qstashHealthTimer = null;
108
+ if (!cachedQstash) {
109
+ return;
110
+ }
111
+ try {
112
+ await checkQstashHealth(token);
113
+ } catch (error) {
114
+ cachedQstash = null;
115
+ if (!qstashWarnedHealthCheck) {
116
+ qstashWarnedHealthCheck = true;
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ console.warn(`[Upstash Config] QStash health check failed: ${message}`);
119
+ }
120
+ } finally {
121
+ try {
122
+ scheduleQstashHealthCheck(token);
123
+ } catch (error) {
124
+ if (!qstashWarnedHealthSchedule) {
125
+ qstashWarnedHealthSchedule = true;
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ console.warn(`[Upstash Config] QStash health check schedule failed: ${message}`);
128
+ }
129
+ }
130
+ }
131
+ }, delayMs);
132
+ };
8
133
 
9
134
  /**
10
- * Get the Upstash Redis client. Returns null when required env vars are missing.
135
+ * Get the Upstash Redis client. Returns null when required env vars are missing/invalid.
136
+ *
137
+ * Singleton semantics:
138
+ * - read-through cached instance only
11
139
  */
12
140
  export const getRedis = (): Redis | null => {
141
+ return cachedRedis;
142
+ };
143
+
144
+ const ensureRedis = async (): Promise<Redis | null> => {
13
145
  if (cachedRedis) {
14
146
  return cachedRedis;
15
147
  }
16
- if (redisInitAttempted) {
17
- return null;
148
+ if (redisInitPromise) {
149
+ return redisInitPromise;
18
150
  }
19
151
 
20
- redisInitAttempted = true;
21
- const { UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN } = process.env;
22
- if (!UPSTASH_REDIS_REST_URL || !UPSTASH_REDIS_REST_TOKEN) {
23
- console.warn('[Upstash Config] Redis Missed configration UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN, then disabled');
24
- return null;
25
- }
152
+ redisInitPromise = (async () => {
153
+ const { UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN } = process.env;
154
+ if (!isNonEmpty(UPSTASH_REDIS_REST_URL) || !isNonEmpty(UPSTASH_REDIS_REST_TOKEN)) {
155
+ if (!redisWarnedMissingEnv) {
156
+ redisWarnedMissingEnv = true;
157
+ console.warn(
158
+ '[Upstash Config] Redis disabled: missing UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN'
159
+ );
160
+ }
161
+ return null;
162
+ }
26
163
 
27
- cachedRedis = new Redis({
28
- url: UPSTASH_REDIS_REST_URL,
29
- token: UPSTASH_REDIS_REST_TOKEN,
30
- });
31
- return cachedRedis;
164
+ if (!isValidUrl(UPSTASH_REDIS_REST_URL)) {
165
+ if (!redisWarnedInvalidEnv) {
166
+ redisWarnedInvalidEnv = true;
167
+ console.warn('[Upstash Config] Redis disabled: UPSTASH_REDIS_REST_URL is not a valid URL');
168
+ }
169
+ return null;
170
+ }
171
+
172
+ try {
173
+ const client = new Redis({
174
+ url: UPSTASH_REDIS_REST_URL,
175
+ token: UPSTASH_REDIS_REST_TOKEN,
176
+ });
177
+ await client.ping();
178
+ cachedRedis = client;
179
+ scheduleRedisHealthCheck();
180
+ return cachedRedis;
181
+ } catch (error) {
182
+ if (!redisWarnedInitError) {
183
+ redisWarnedInitError = true;
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ console.warn(`[Upstash Config] Redis init failed: ${message}`);
186
+ }
187
+ return null;
188
+ } finally {
189
+ redisInitPromise = null;
190
+ }
191
+ })();
192
+
193
+ return redisInitPromise;
32
194
  };
33
195
 
34
196
  /**
35
197
  * Get the Upstash QStash client. Returns null when required env vars are missing.
198
+ *
199
+ * Singleton semantics:
200
+ * - read-through cached instance only
36
201
  */
37
202
  export const getQstash = (): QstashClient | null => {
203
+ return cachedQstash;
204
+ };
205
+
206
+ const ensureQstash = async (): Promise<QstashClient | null> => {
38
207
  if (cachedQstash) {
39
208
  return cachedQstash;
40
209
  }
41
- if (qstashInitAttempted) {
42
- return null;
210
+ if (qstashInitPromise) {
211
+ return qstashInitPromise;
43
212
  }
44
213
 
45
- qstashInitAttempted = true;
46
- const { QSTASH_TOKEN } = process.env;
47
- if (!QSTASH_TOKEN) {
48
- console.warn('[Upstash Config] QStash Missed configration QSTASH_TOKEN, then disabled');
214
+ qstashInitPromise = (async () => {
215
+ const { QSTASH_TOKEN } = process.env;
216
+ if (!isNonEmpty(QSTASH_TOKEN)) {
217
+ if (!qstashWarnedMissingEnv) {
218
+ qstashWarnedMissingEnv = true;
219
+ console.warn('[Upstash Config] QStash disabled: missing QSTASH_TOKEN');
220
+ }
221
+ return null;
222
+ }
223
+
224
+ try {
225
+ const client = new QstashClient({ token: QSTASH_TOKEN });
226
+ await checkQstashHealth(QSTASH_TOKEN);
227
+ cachedQstash = client;
228
+ scheduleQstashHealthCheck(QSTASH_TOKEN);
229
+ return cachedQstash;
230
+ } catch (error) {
231
+ if (!qstashWarnedInitError) {
232
+ qstashWarnedInitError = true;
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ console.warn(`[Upstash Config] QStash init failed: ${message}`);
235
+ }
236
+ return null;
237
+ } finally {
238
+ qstashInitPromise = null;
239
+ }
240
+ })();
241
+
242
+ return qstashInitPromise;
243
+ };
244
+
245
+ export const withRedis = async <T>(fn: (redis: Redis) => Promise<T> | T): Promise<T | null> => {
246
+ const redis = await ensureRedis();
247
+ if (!redis) {
49
248
  return null;
50
249
  }
250
+ return fn(redis);
251
+ };
51
252
 
52
- cachedQstash = new QstashClient({ token: QSTASH_TOKEN });
53
- return cachedQstash;
253
+ export const withQstash = async <T>(
254
+ fn: (qstash: QstashClient) => Promise<T> | T
255
+ ): Promise<T | null> => {
256
+ const qstash = await ensureQstash();
257
+ if (!qstash) {
258
+ return null;
259
+ }
260
+ return fn(qstash);
54
261
  };
@@ -13,7 +13,7 @@ import {
13
13
  import { Transaction } from '@/db/prisma-model-type';
14
14
  import { oneTimeExpiredDays } from '@/lib/credit-init';
15
15
  import { getCreditsFromPriceId } from '@/lib/money-price-config';
16
- import { fetchPaymentId, stripe } from '@/lib/stripe-config';
16
+ import { fetchPaymentId, getStripe } from '@/lib/stripe-config';
17
17
  import Stripe from 'stripe';
18
18
  import { viewLocalTime } from '@windrun-huaiin/lib/utils';
19
19
 
@@ -150,6 +150,7 @@ async function handleSubscriptionCheckoutInit(
150
150
  }
151
151
 
152
152
  const subscriptionId = session.subscription as string;
153
+ const stripe = getStripe();
153
154
 
154
155
  // ===== STEP 1: FETCH EXTERNAL API DATA (BEFORE TRANSACTION) =====
155
156
  // 2. Get COMPLETE Stripe subscription details including billing period
@@ -416,6 +417,7 @@ async function handleSubscriptionDeleted(stripeSubscription: Stripe.Subscription
416
417
 
417
418
  async function handleAsyncPaymentSucceeded(session: Stripe.Checkout.Session) {
418
419
  console.log(`Async payment succeeded: ${session.id}`);
420
+ const stripe = getStripe();
419
421
 
420
422
  // Retrieve the latest session state to ensure payment_status is up to date
421
423
  const latestSession = await stripe.checkout.sessions.retrieve(session.id);
@@ -1,5 +0,0 @@
1
- 'use strict';
2
-
3
- var cryptoJs = {exports: {}};
4
-
5
- exports.__module = cryptoJs;
@@ -1,3 +0,0 @@
1
- var cryptoJs = {exports: {}};
2
-
3
- export { cryptoJs as __module };