@windrun-huaiin/backend-core 13.0.0 → 14.0.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 (104) 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 +2 -0
  6. package/dist/index.mjs +1 -1
  7. package/dist/lib/index.js +2 -0
  8. package/dist/lib/index.mjs +1 -1
  9. package/dist/lib/upstash/qstash.d.ts.map +1 -1
  10. package/dist/lib/upstash/qstash.js +66 -62
  11. package/dist/lib/upstash/qstash.mjs +67 -63
  12. package/dist/lib/upstash/redis-counter.d.ts.map +1 -1
  13. package/dist/lib/upstash/redis-counter.js +9 -24
  14. package/dist/lib/upstash/redis-counter.mjs +10 -25
  15. package/dist/lib/upstash/redis-favorite.d.ts.map +1 -1
  16. package/dist/lib/upstash/redis-favorite.js +22 -36
  17. package/dist/lib/upstash/redis-favorite.mjs +23 -37
  18. package/dist/lib/upstash/redis-like.d.ts.map +1 -1
  19. package/dist/lib/upstash/redis-like.js +22 -36
  20. package/dist/lib/upstash/redis-like.mjs +23 -37
  21. package/dist/lib/upstash/redis-lock.d.ts.map +1 -1
  22. package/dist/lib/upstash/redis-lock.js +22 -38
  23. package/dist/lib/upstash/redis-lock.mjs +23 -39
  24. package/dist/lib/upstash/redis-structures.d.ts.map +1 -1
  25. package/dist/lib/upstash/redis-structures.js +77 -113
  26. package/dist/lib/upstash/redis-structures.mjs +78 -114
  27. package/dist/lib/upstash-config.d.ts +9 -1
  28. package/dist/lib/upstash-config.d.ts.map +1 -1
  29. package/dist/lib/upstash-config.js +221 -27
  30. package/dist/lib/upstash-config.mjs +220 -28
  31. 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
  32. 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
  33. 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
  34. 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
  35. package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1 → @upstash_redis@1.37.0}/node_modules/@upstash/redis/nodejs.js +6 -5
  36. package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1 → @upstash_redis@1.37.0}/node_modules/@upstash/redis/nodejs.mjs +2 -2
  37. package/dist/node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.js +1 -1
  38. package/dist/node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.mjs +1 -1
  39. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/jws/flattened/verify.js +6 -6
  40. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/jwt/verify.js +1 -1
  41. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/key/import.js +2 -2
  42. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/epoch.js +3 -1
  43. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_disjoint.js +3 -1
  44. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_jwk.js +1 -1
  45. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_object.js +3 -1
  46. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/jwt_claims_set.js +7 -5
  47. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/secs.js +3 -1
  48. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/validate_algorithms.js +3 -1
  49. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/validate_crit.js +3 -1
  50. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/check_key_length.js +3 -1
  51. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/dsa_digest.js +3 -1
  52. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/get_named_curve.js +4 -2
  53. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/get_sign_verify_key.js +3 -1
  54. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/hmac_digest.js +3 -1
  55. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/is_key_like.js +1 -1
  56. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/is_key_object.js +3 -1
  57. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/jwk_to_key.js +3 -1
  58. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/node_key.js +6 -4
  59. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/sign.js +6 -4
  60. package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/verify.js +7 -5
  61. package/dist/prisma/prisma.d.ts +1 -1
  62. package/dist/prisma/prisma.d.ts.map +1 -1
  63. package/package.json +5 -5
  64. package/src/lib/upstash/qstash.ts +64 -62
  65. package/src/lib/upstash/redis-counter.ts +10 -26
  66. package/src/lib/upstash/redis-favorite.ts +23 -42
  67. package/src/lib/upstash/redis-like.ts +23 -42
  68. package/src/lib/upstash/redis-lock.ts +23 -49
  69. package/src/lib/upstash/redis-structures.ts +82 -131
  70. package/src/lib/upstash-config.ts +231 -24
  71. package/dist/_virtual/index3.js +0 -5
  72. package/dist/_virtual/index3.mjs +0 -3
  73. package/dist/node_modules/.pnpm/@upstash_lock@0.2.1_typescript@5.9.3/node_modules/@upstash/lock/dist/index.js +0 -191
  74. package/dist/node_modules/.pnpm/@upstash_lock@0.2.1_typescript@5.9.3/node_modules/@upstash/lock/dist/index.mjs +0 -189
  75. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.js +0 -54
  76. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.mjs +0 -51
  77. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.js +0 -44
  78. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.mjs +0 -35
  79. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.js +0 -31
  80. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.mjs +0 -18
  81. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.js +0 -587
  82. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.mjs +0 -527
  83. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.js +0 -447
  84. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.mjs +0 -399
  85. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.js +0 -245
  86. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.mjs +0 -232
  87. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.js +0 -68
  88. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.mjs +0 -62
  89. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.js +0 -39
  90. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.mjs +0 -37
  91. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.js +0 -80
  92. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.mjs +0 -75
  93. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.js +0 -101
  94. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.mjs +0 -86
  95. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.js +0 -102
  96. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.mjs +0 -76
  97. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.js +0 -56
  98. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.mjs +0 -52
  99. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.js +0 -1205
  100. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.mjs +0 -1157
  101. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.js +0 -407
  102. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.mjs +0 -374
  103. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.js +0 -9
  104. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.mjs +0 -7
@@ -1,8 +1,9 @@
1
1
  import { Receiver } from '@upstash/qstash';
2
- import { getQstash } from '../upstash-config';
2
+ import { withQstash } from '../upstash-config';
3
3
 
4
4
  let cachedReceiver: Receiver | null = null;
5
- let receiverInitAttempted = false;
5
+ let receiverWarnedMissingEnv = false;
6
+ let receiverWarnedInitError = false;
6
7
 
7
8
  const isTruthy = (value: string | undefined): boolean =>
8
9
  value === '1' || value === 'true' || value === 'TRUE';
@@ -14,22 +15,33 @@ const getReceiver = (): Receiver | null => {
14
15
  if (cachedReceiver) {
15
16
  return cachedReceiver;
16
17
  }
17
- if (receiverInitAttempted) {
18
- return null;
19
- }
20
- receiverInitAttempted = true;
21
18
 
22
19
  const currentSigningKey = process.env.QSTASH_CURRENT_SIGNING_KEY;
23
20
  const nextSigningKey = process.env.QSTASH_NEXT_SIGNING_KEY;
24
21
  if (!currentSigningKey || !nextSigningKey) {
22
+ if (!receiverWarnedMissingEnv) {
23
+ receiverWarnedMissingEnv = true;
24
+ console.warn(
25
+ '[Upstash Config] QStash Receiver disabled: missing QSTASH_CURRENT_SIGNING_KEY or QSTASH_NEXT_SIGNING_KEY'
26
+ );
27
+ }
25
28
  return null;
26
29
  }
27
30
 
28
- cachedReceiver = new Receiver({
29
- currentSigningKey,
30
- nextSigningKey,
31
- });
32
- return cachedReceiver;
31
+ try {
32
+ cachedReceiver = new Receiver({
33
+ currentSigningKey,
34
+ nextSigningKey,
35
+ });
36
+ return cachedReceiver;
37
+ } catch (error) {
38
+ if (!receiverWarnedInitError) {
39
+ receiverWarnedInitError = true;
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ console.warn(`[Upstash Config] QStash Receiver init failed: ${message}`);
42
+ }
43
+ return null;
44
+ }
33
45
  };
34
46
 
35
47
  export type PublishBody = Record<string, unknown> | string | number | boolean | null;
@@ -43,16 +55,13 @@ export interface PublishMessageOptions {
43
55
  * Publish a message. Returns message id or null if QStash is unavailable.
44
56
  */
45
57
  export const publishMessage = async (options: PublishMessageOptions): Promise<string | null> => {
46
- const client = getQstash();
47
- if (!client) {
48
- return null;
49
- }
50
-
51
- const result = await (client as any).publishJSON({
52
- url: options.url,
53
- body: options.body,
58
+ return withQstash(async (client) => {
59
+ const result = await (client as any).publishJSON({
60
+ url: options.url,
61
+ body: options.body,
62
+ });
63
+ return typeof result === 'string' ? result : result?.messageId ?? null;
54
64
  });
55
- return typeof result === 'string' ? result : result?.messageId ?? null;
56
65
  };
57
66
 
58
67
  /**
@@ -61,17 +70,14 @@ export const publishMessage = async (options: PublishMessageOptions): Promise<st
61
70
  export const publishDelayedMessage = async (
62
71
  options: PublishMessageOptions & { delaySec: number }
63
72
  ): Promise<string | null> => {
64
- const client = getQstash();
65
- if (!client) {
66
- return null;
67
- }
68
-
69
- const result = await (client as any).publishJSON({
70
- url: options.url,
71
- body: options.body,
72
- delay: options.delaySec,
73
+ return withQstash(async (client) => {
74
+ const result = await (client as any).publishJSON({
75
+ url: options.url,
76
+ body: options.body,
77
+ delay: options.delaySec,
78
+ });
79
+ return typeof result === 'string' ? result : result?.messageId ?? null;
73
80
  });
74
- return typeof result === 'string' ? result : result?.messageId ?? null;
75
81
  };
76
82
 
77
83
  export interface ScheduleMessageOptions extends PublishMessageOptions {
@@ -82,46 +88,42 @@ export interface ScheduleMessageOptions extends PublishMessageOptions {
82
88
  * Schedule a recurring message. Returns schedule id or null if QStash is unavailable.
83
89
  */
84
90
  export const scheduleMessage = async (options: ScheduleMessageOptions): Promise<string | null> => {
85
- const client = getQstash();
86
- if (!client) {
87
- return null;
88
- }
89
-
90
- const anyClient = client as any;
91
- const result =
92
- (await anyClient.schedules?.create?.({
93
- url: options.url,
94
- body: options.body,
95
- cron: options.cron,
96
- })) ??
97
- (await anyClient.publishJSON?.({
98
- url: options.url,
99
- body: options.body,
100
- cron: options.cron,
101
- }));
102
-
103
- return typeof result === 'string' ? result : result?.scheduleId ?? result?.id ?? null;
91
+ return withQstash(async (client) => {
92
+ const anyClient = client as any;
93
+ const result =
94
+ (await anyClient.schedules?.create?.({
95
+ url: options.url,
96
+ body: options.body,
97
+ cron: options.cron,
98
+ })) ??
99
+ (await anyClient.publishJSON?.({
100
+ url: options.url,
101
+ body: options.body,
102
+ cron: options.cron,
103
+ }));
104
+
105
+ return typeof result === 'string' ? result : result?.scheduleId ?? result?.id ?? null;
106
+ });
104
107
  };
105
108
 
106
109
  /**
107
110
  * Cancel a scheduled message. Returns false if QStash is unavailable.
108
111
  */
109
112
  export const cancelSchedule = async (scheduleId: string): Promise<boolean> => {
110
- const client = getQstash();
111
- if (!client) {
113
+ const result = await withQstash(async (client) => {
114
+ const anyClient = client as any;
115
+ if (anyClient.schedules?.delete) {
116
+ await anyClient.schedules.delete(scheduleId);
117
+ return true;
118
+ }
119
+ if (anyClient.schedules?.remove) {
120
+ await anyClient.schedules.remove(scheduleId);
121
+ return true;
122
+ }
112
123
  return false;
113
- }
124
+ });
114
125
 
115
- const anyClient = client as any;
116
- if (anyClient.schedules?.delete) {
117
- await anyClient.schedules.delete(scheduleId);
118
- return true;
119
- }
120
- if (anyClient.schedules?.remove) {
121
- await anyClient.schedules.remove(scheduleId);
122
- return true;
123
- }
124
- return false;
126
+ return result ?? false;
125
127
  };
126
128
 
127
129
  export interface VerifyQstashOptions {
@@ -1,51 +1,35 @@
1
- import { getRedis } from '../upstash-config';
1
+ import { withRedis } from '../upstash-config';
2
2
 
3
3
  /**
4
4
  * Increment a counter (e.g. views, forwards). Returns null if Redis is unavailable.
5
5
  */
6
6
  export const incrCounter = async (key: string, delta = 1): Promise<number | null> => {
7
- const redis = getRedis();
8
- if (!redis) {
9
- return null;
10
- }
11
- return redis.incrby(key, delta);
7
+ return withRedis((redis) => redis.incrby(key, delta));
12
8
  };
13
9
 
14
10
  /**
15
11
  * Get a counter value. Returns null if Redis is unavailable.
16
12
  */
17
13
  export const getCounter = async (key: string): Promise<number | null> => {
18
- const redis = getRedis();
19
- if (!redis) {
20
- return null;
21
- }
22
- const value = await redis.get<number>(key);
23
- return value ?? 0;
14
+ return withRedis(async (redis) => {
15
+ const value = await redis.get<number>(key);
16
+ return value ?? 0;
17
+ });
24
18
  };
25
19
 
26
20
  /**
27
21
  * Increment a unique counter via SET (e.g. unique views). Returns null if Redis is unavailable.
28
22
  */
29
23
  export const incrUniqueCounter = async (setKey: string, memberId: string): Promise<number | null> => {
30
- const redis = getRedis();
31
- if (!redis) {
32
- return null;
33
- }
34
-
35
- const added = await redis.sadd(setKey, memberId);
36
- if (added === 1) {
24
+ return withRedis(async (redis) => {
25
+ await redis.sadd(setKey, memberId);
37
26
  return redis.scard(setKey);
38
- }
39
- return redis.scard(setKey);
27
+ });
40
28
  };
41
29
 
42
30
  /**
43
31
  * Get unique counter value (SET cardinality). Returns null if Redis is unavailable.
44
32
  */
45
33
  export const getUniqueCounter = async (setKey: string): Promise<number | null> => {
46
- const redis = getRedis();
47
- if (!redis) {
48
- return null;
49
- }
50
- return redis.scard(setKey);
34
+ return withRedis((redis) => redis.scard(setKey));
51
35
  };
@@ -1,4 +1,4 @@
1
- import { getRedis } from '../upstash-config';
1
+ import { withRedis } from '../upstash-config';
2
2
 
3
3
  const favoriteTargetKey = (targetId: string): string => `favorite:target:${targetId}`;
4
4
  const favoriteUserKey = (userId: string): string => `favorite:user:${userId}`;
@@ -7,69 +7,50 @@ const favoriteUserKey = (userId: string): string => `favorite:user:${userId}`;
7
7
  * Favorite a target. Returns true if added, false if already favorited, null if Redis is unavailable.
8
8
  */
9
9
  export const addFavorite = async (targetId: string, userId: string): Promise<boolean | null> => {
10
- const redis = getRedis();
11
- if (!redis) {
12
- return null;
13
- }
14
-
15
- const added = await redis.sadd(favoriteTargetKey(targetId), userId);
16
- if (added === 1) {
17
- await redis.sadd(favoriteUserKey(userId), targetId);
18
- return true;
19
- }
20
- return false;
10
+ return withRedis(async (redis) => {
11
+ const added = await redis.sadd(favoriteTargetKey(targetId), userId);
12
+ if (added === 1) {
13
+ await redis.sadd(favoriteUserKey(userId), targetId);
14
+ return true;
15
+ }
16
+ return false;
17
+ });
21
18
  };
22
19
 
23
20
  /**
24
21
  * Remove a favorite. Returns true if removed, false if not found, null if Redis is unavailable.
25
22
  */
26
23
  export const removeFavorite = async (targetId: string, userId: string): Promise<boolean | null> => {
27
- const redis = getRedis();
28
- if (!redis) {
29
- return null;
30
- }
31
-
32
- const removed = await redis.srem(favoriteTargetKey(targetId), userId);
33
- if (removed === 1) {
34
- await redis.srem(favoriteUserKey(userId), targetId);
35
- return true;
36
- }
37
- return false;
24
+ return withRedis(async (redis) => {
25
+ const removed = await redis.srem(favoriteTargetKey(targetId), userId);
26
+ if (removed === 1) {
27
+ await redis.srem(favoriteUserKey(userId), targetId);
28
+ return true;
29
+ }
30
+ return false;
31
+ });
38
32
  };
39
33
 
40
34
  /**
41
35
  * Check whether a user has favorited a target. Returns null if Redis is unavailable.
42
36
  */
43
37
  export const isFavorited = async (targetId: string, userId: string): Promise<boolean | null> => {
44
- const redis = getRedis();
45
- if (!redis) {
46
- return null;
47
- }
48
-
49
- const result = await redis.sismember(favoriteTargetKey(targetId), userId);
50
- return result === 1;
38
+ return withRedis(async (redis) => {
39
+ const result = await redis.sismember(favoriteTargetKey(targetId), userId);
40
+ return result === 1;
41
+ });
51
42
  };
52
43
 
53
44
  /**
54
45
  * Get favorite count for a target. Returns null if Redis is unavailable.
55
46
  */
56
47
  export const getFavoriteCount = async (targetId: string): Promise<number | null> => {
57
- const redis = getRedis();
58
- if (!redis) {
59
- return null;
60
- }
61
-
62
- return redis.scard(favoriteTargetKey(targetId));
48
+ return withRedis((redis) => redis.scard(favoriteTargetKey(targetId)));
63
49
  };
64
50
 
65
51
  /**
66
52
  * Get target ids favorited by a user. Returns null if Redis is unavailable.
67
53
  */
68
54
  export const getUserFavorites = async (userId: string): Promise<string[] | null> => {
69
- const redis = getRedis();
70
- if (!redis) {
71
- return null;
72
- }
73
-
74
- return redis.smembers<string[]>(favoriteUserKey(userId));
55
+ return withRedis((redis) => redis.smembers<string[]>(favoriteUserKey(userId)));
75
56
  };
@@ -1,4 +1,4 @@
1
- import { getRedis } from '../upstash-config';
1
+ import { withRedis } from '../upstash-config';
2
2
 
3
3
  const likeTargetKey = (targetId: string): string => `like:target:${targetId}`;
4
4
  const likeUserKey = (userId: string): string => `like:user:${userId}`;
@@ -7,69 +7,50 @@ const likeUserKey = (userId: string): string => `like:user:${userId}`;
7
7
  * Like a target. Returns true if the like was added, false if it already existed, null if Redis is unavailable.
8
8
  */
9
9
  export const likeTarget = async (targetId: string, userId: string): Promise<boolean | null> => {
10
- const redis = getRedis();
11
- if (!redis) {
12
- return null;
13
- }
14
-
15
- const added = await redis.sadd(likeTargetKey(targetId), userId);
16
- if (added === 1) {
17
- await redis.sadd(likeUserKey(userId), targetId);
18
- return true;
19
- }
20
- return false;
10
+ return withRedis(async (redis) => {
11
+ const added = await redis.sadd(likeTargetKey(targetId), userId);
12
+ if (added === 1) {
13
+ await redis.sadd(likeUserKey(userId), targetId);
14
+ return true;
15
+ }
16
+ return false;
17
+ });
21
18
  };
22
19
 
23
20
  /**
24
21
  * Unlike a target. Returns true if removed, false if it didn't exist, null if Redis is unavailable.
25
22
  */
26
23
  export const unlikeTarget = async (targetId: string, userId: string): Promise<boolean | null> => {
27
- const redis = getRedis();
28
- if (!redis) {
29
- return null;
30
- }
31
-
32
- const removed = await redis.srem(likeTargetKey(targetId), userId);
33
- if (removed === 1) {
34
- await redis.srem(likeUserKey(userId), targetId);
35
- return true;
36
- }
37
- return false;
24
+ return withRedis(async (redis) => {
25
+ const removed = await redis.srem(likeTargetKey(targetId), userId);
26
+ if (removed === 1) {
27
+ await redis.srem(likeUserKey(userId), targetId);
28
+ return true;
29
+ }
30
+ return false;
31
+ });
38
32
  };
39
33
 
40
34
  /**
41
35
  * Check whether a user liked a target. Returns null if Redis is unavailable.
42
36
  */
43
37
  export const isTargetLiked = async (targetId: string, userId: string): Promise<boolean | null> => {
44
- const redis = getRedis();
45
- if (!redis) {
46
- return null;
47
- }
48
-
49
- const result = await redis.sismember(likeTargetKey(targetId), userId);
50
- return result === 1;
38
+ return withRedis(async (redis) => {
39
+ const result = await redis.sismember(likeTargetKey(targetId), userId);
40
+ return result === 1;
41
+ });
51
42
  };
52
43
 
53
44
  /**
54
45
  * Get like count for a target (unique by user). Returns null if Redis is unavailable.
55
46
  */
56
47
  export const getTargetLikeCount = async (targetId: string): Promise<number | null> => {
57
- const redis = getRedis();
58
- if (!redis) {
59
- return null;
60
- }
61
-
62
- return redis.scard(likeTargetKey(targetId));
48
+ return withRedis((redis) => redis.scard(likeTargetKey(targetId)));
63
49
  };
64
50
 
65
51
  /**
66
52
  * Get target ids liked by a user. Returns null if Redis is unavailable.
67
53
  */
68
54
  export const getUserLikedTargets = async (userId: string): Promise<string[] | null> => {
69
- const redis = getRedis();
70
- if (!redis) {
71
- return null;
72
- }
73
-
74
- return redis.smembers<string[]>(likeUserKey(userId));
55
+ return withRedis((redis) => redis.smembers<string[]>(likeUserKey(userId)));
75
56
  };
@@ -1,62 +1,41 @@
1
- import { Lock } from '@upstash/lock';
2
- import type { Redis } from '@upstash/redis';
3
- import { getRedis } from '../upstash-config';
1
+ import { withRedis } from '../upstash-config';
4
2
 
5
- type UpstashLock = {
6
- acquire: (key: string, ttlMs: number) => Promise<string | null>;
7
- release: (key: string, token: string) => Promise<boolean>;
8
- };
9
-
10
- let cachedLock: UpstashLock | null = null;
11
- let lockInitAttempted = false;
3
+ const unlockScript = `
4
+ if redis.call("get", KEYS[1]) == ARGV[1] then
5
+ return redis.call("del", KEYS[1])
6
+ else
7
+ return 0
8
+ end
9
+ `;
12
10
 
13
- const createLock = (redis: Redis): UpstashLock => {
14
- const LockCtor = Lock as unknown as new (...args: any[]) => UpstashLock;
11
+ const generateToken = (): string => {
15
12
  try {
16
- return new LockCtor({ redis });
13
+ return crypto.randomUUID();
17
14
  } catch {
18
- return new LockCtor(redis);
15
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
19
16
  }
20
17
  };
21
18
 
22
- const getLock = (): UpstashLock | null => {
23
- if (cachedLock) {
24
- return cachedLock;
25
- }
26
- if (lockInitAttempted) {
27
- return null;
28
- }
29
- lockInitAttempted = true;
30
-
31
- const redis = getRedis();
32
- if (!redis) {
33
- return null;
34
- }
35
-
36
- cachedLock = createLock(redis);
37
- return cachedLock;
38
- };
39
-
40
19
  /**
41
20
  * Acquire a distributed lock. Returns the lock token or null when unavailable.
42
21
  */
43
22
  export const acquireLock = async (key: string, ttlMs: number): Promise<string | null> => {
44
- const lock = getLock();
45
- if (!lock) {
46
- return null;
47
- }
48
- return lock.acquire(key, ttlMs);
23
+ return withRedis(async (redis) => {
24
+ const token = generateToken();
25
+ const result = await redis.set(key, token, { nx: true, px: ttlMs });
26
+ return result === 'OK' ? token : null;
27
+ });
49
28
  };
50
29
 
51
30
  /**
52
31
  * Release a distributed lock. Returns false when the lock client is unavailable.
53
32
  */
54
33
  export const releaseLock = async (key: string, token: string): Promise<boolean> => {
55
- const lock = getLock();
56
- if (!lock) {
57
- return false;
58
- }
59
- return lock.release(key, token);
34
+ const result = await withRedis(async (redis) => {
35
+ const released = await redis.eval(unlockScript, [key], [token]);
36
+ return Number(released) === 1;
37
+ });
38
+ return result ?? false;
60
39
  };
61
40
 
62
41
  /**
@@ -67,12 +46,7 @@ export const withLock = async <T>(
67
46
  ttlMs: number,
68
47
  fn: () => Promise<T> | T
69
48
  ): Promise<T | null> => {
70
- const lock = getLock();
71
- if (!lock) {
72
- return null;
73
- }
74
-
75
- const token = await lock.acquire(key, ttlMs);
49
+ const token = await acquireLock(key, ttlMs);
76
50
  if (!token) {
77
51
  return null;
78
52
  }
@@ -80,6 +54,6 @@ export const withLock = async <T>(
80
54
  try {
81
55
  return await fn();
82
56
  } finally {
83
- await lock.release(key, token);
57
+ await releaseLock(key, token);
84
58
  }
85
59
  };