@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.
- package/dist/_virtual/index.js +7 -3
- package/dist/_virtual/index.mjs +5 -3
- package/dist/_virtual/index2.js +2 -6
- package/dist/_virtual/index2.mjs +2 -6
- package/dist/index.js +3 -1
- package/dist/index.mjs +2 -2
- package/dist/lib/index.js +3 -1
- package/dist/lib/index.mjs +2 -2
- package/dist/lib/stripe-config.d.ts +1 -1
- package/dist/lib/stripe-config.d.ts.map +1 -1
- package/dist/lib/stripe-config.js +25 -16
- package/dist/lib/stripe-config.mjs +25 -16
- package/dist/lib/upstash/qstash.d.ts.map +1 -1
- package/dist/lib/upstash/qstash.js +66 -62
- package/dist/lib/upstash/qstash.mjs +67 -63
- package/dist/lib/upstash/redis-counter.d.ts.map +1 -1
- package/dist/lib/upstash/redis-counter.js +9 -24
- package/dist/lib/upstash/redis-counter.mjs +10 -25
- package/dist/lib/upstash/redis-favorite.d.ts.map +1 -1
- package/dist/lib/upstash/redis-favorite.js +22 -36
- package/dist/lib/upstash/redis-favorite.mjs +23 -37
- package/dist/lib/upstash/redis-like.d.ts.map +1 -1
- package/dist/lib/upstash/redis-like.js +22 -36
- package/dist/lib/upstash/redis-like.mjs +23 -37
- package/dist/lib/upstash/redis-lock.d.ts.map +1 -1
- package/dist/lib/upstash/redis-lock.js +22 -38
- package/dist/lib/upstash/redis-lock.mjs +23 -39
- package/dist/lib/upstash/redis-structures.d.ts.map +1 -1
- package/dist/lib/upstash/redis-structures.js +77 -113
- package/dist/lib/upstash/redis-structures.mjs +78 -114
- package/dist/lib/upstash-config.d.ts +9 -1
- package/dist/lib/upstash-config.d.ts.map +1 -1
- package/dist/lib/upstash-config.js +221 -27
- package/dist/lib/upstash-config.mjs +220 -28
- 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
- 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
- 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
- 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
- package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1 → @upstash_redis@1.37.0}/node_modules/@upstash/redis/nodejs.js +6 -5
- package/dist/node_modules/.pnpm/{@upstash_redis@1.36.1 → @upstash_redis@1.37.0}/node_modules/@upstash/redis/nodejs.mjs +2 -2
- package/dist/node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.js +1 -1
- package/dist/node_modules/.pnpm/crypto-js@4.2.0/node_modules/crypto-js/index.mjs +1 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/jws/flattened/verify.js +6 -6
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/jwt/verify.js +1 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/key/import.js +2 -2
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/epoch.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_disjoint.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_jwk.js +1 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/is_object.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/jwt_claims_set.js +7 -5
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/secs.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/validate_algorithms.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/lib/validate_crit.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/check_key_length.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/dsa_digest.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/get_named_curve.js +4 -2
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/get_sign_verify_key.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/hmac_digest.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/is_key_like.js +1 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/is_key_object.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/jwk_to_key.js +3 -1
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/node_key.js +6 -4
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/sign.js +6 -4
- package/dist/node_modules/.pnpm/jose@5.10.0/node_modules/jose/dist/node/esm/runtime/verify.js +7 -5
- package/dist/services/stripe/webhook-handler.js +4 -2
- package/dist/services/stripe/webhook-handler.mjs +3 -1
- package/package.json +5 -5
- package/src/lib/stripe-config.ts +27 -15
- package/src/lib/upstash/qstash.ts +64 -62
- package/src/lib/upstash/redis-counter.ts +10 -26
- package/src/lib/upstash/redis-favorite.ts +23 -42
- package/src/lib/upstash/redis-like.ts +23 -42
- package/src/lib/upstash/redis-lock.ts +23 -49
- package/src/lib/upstash/redis-structures.ts +82 -131
- package/src/lib/upstash-config.ts +231 -24
- package/src/services/stripe/webhook-handler.ts +3 -1
- package/dist/_virtual/index3.js +0 -5
- package/dist/_virtual/index3.mjs +0 -3
- package/dist/node_modules/.pnpm/@upstash_lock@0.2.1_typescript@5.9.3/node_modules/@upstash/lock/dist/index.js +0 -191
- package/dist/node_modules/.pnpm/@upstash_lock@0.2.1_typescript@5.9.3/node_modules/@upstash/lock/dist/index.mjs +0 -189
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.js +0 -54
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.mjs +0 -51
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.js +0 -44
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.mjs +0 -35
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.js +0 -31
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.mjs +0 -18
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.js +0 -587
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.mjs +0 -527
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.js +0 -447
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.mjs +0 -399
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.js +0 -245
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.mjs +0 -232
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.js +0 -68
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.mjs +0 -62
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.js +0 -39
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.mjs +0 -37
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.js +0 -80
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.mjs +0 -75
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.js +0 -101
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.mjs +0 -86
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.js +0 -102
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.mjs +0 -76
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.js +0 -56
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.mjs +0 -52
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.js +0 -1205
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.mjs +0 -1157
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.js +0 -407
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.mjs +0 -374
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.js +0 -9
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.mjs +0 -7
package/src/lib/stripe-config.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
2
|
import { Apilogger, userService, subscriptionService } from '../services/database/index';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
let stripeInstance: Stripe | null = null;
|
|
5
|
+
|
|
6
|
+
export const getStripe = (): Stripe => {
|
|
7
|
+
const apiKey = process.env.STRIPE_SECRET_KEY;
|
|
8
|
+
if (!apiKey) {
|
|
9
|
+
throw new Error('STRIPE_SECRET_KEY is not configured');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!stripeInstance) {
|
|
13
|
+
stripeInstance = new Stripe(apiKey, {
|
|
14
|
+
apiVersion: '2025-11-17.clover',
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return stripeInstance;
|
|
19
|
+
};
|
|
8
20
|
|
|
9
21
|
// Helper function to validate webhook signature
|
|
10
22
|
export const validateStripeWebhook = (
|
|
@@ -12,7 +24,7 @@ export const validateStripeWebhook = (
|
|
|
12
24
|
signature: string,
|
|
13
25
|
secret: string
|
|
14
26
|
): Stripe.Event => {
|
|
15
|
-
return
|
|
27
|
+
return getStripe().webhooks.constructEvent(payload, signature, secret);
|
|
16
28
|
};
|
|
17
29
|
|
|
18
30
|
export interface BasicCheckoutSessionParams {
|
|
@@ -95,7 +107,7 @@ export const createCheckoutSession = async (
|
|
|
95
107
|
const logId = await Apilogger.logStripeOutgoing('createCheckoutSession', params);
|
|
96
108
|
|
|
97
109
|
try {
|
|
98
|
-
const session = await
|
|
110
|
+
const session = await getStripe().checkout.sessions.create(sessionParams);
|
|
99
111
|
|
|
100
112
|
// Update log record with response
|
|
101
113
|
Apilogger.updateResponse(logId, {
|
|
@@ -116,7 +128,7 @@ export const createCheckoutSession = async (
|
|
|
116
128
|
|
|
117
129
|
// 根据发票ID去查支付ID
|
|
118
130
|
export const fetchPaymentId = async (invoiceId: string ): Promise<string> => {
|
|
119
|
-
const fullInvoice = await
|
|
131
|
+
const fullInvoice = await getStripe().invoices.retrieve(invoiceId, {
|
|
120
132
|
expand: ['payments']
|
|
121
133
|
});
|
|
122
134
|
const payment = fullInvoice.payments?.data[0];
|
|
@@ -147,7 +159,7 @@ export const createOrGetCustomer = async (params: {
|
|
|
147
159
|
|
|
148
160
|
if (user.stripeCusId) {
|
|
149
161
|
try {
|
|
150
|
-
const customer = await
|
|
162
|
+
const customer = await getStripe().customers.retrieve(user.stripeCusId);
|
|
151
163
|
if ('deleted' in customer) {
|
|
152
164
|
await setStripeCustomerId(null);
|
|
153
165
|
} else {
|
|
@@ -164,7 +176,7 @@ export const createOrGetCustomer = async (params: {
|
|
|
164
176
|
}
|
|
165
177
|
|
|
166
178
|
if (user.email) {
|
|
167
|
-
const existingCustomers = await
|
|
179
|
+
const existingCustomers = await getStripe().customers.list({
|
|
168
180
|
email: user.email,
|
|
169
181
|
limit: 1,
|
|
170
182
|
});
|
|
@@ -201,7 +213,7 @@ export const createOrGetCustomer = async (params: {
|
|
|
201
213
|
});
|
|
202
214
|
|
|
203
215
|
try {
|
|
204
|
-
const customer = await
|
|
216
|
+
const customer = await getStripe().customers.create(customerParams);
|
|
205
217
|
await setStripeCustomerId(customer.id);
|
|
206
218
|
|
|
207
219
|
// Update log record with response
|
|
@@ -228,13 +240,13 @@ export const updateSubscription = async (params: {
|
|
|
228
240
|
}): Promise<Stripe.Subscription> => {
|
|
229
241
|
const { subscriptionId, priceId, prorationBehavior = 'create_prorations' } = params;
|
|
230
242
|
|
|
231
|
-
const subscription = await
|
|
243
|
+
const subscription = await getStripe().subscriptions.retrieve(subscriptionId);
|
|
232
244
|
|
|
233
245
|
// Create log record with request
|
|
234
246
|
const logId = await Apilogger.logStripeOutgoing('updateSubscription', params);
|
|
235
247
|
|
|
236
248
|
try {
|
|
237
|
-
const updatedSubscription = await
|
|
249
|
+
const updatedSubscription = await getStripe().subscriptions.update(subscriptionId, {
|
|
238
250
|
items: [
|
|
239
251
|
{
|
|
240
252
|
id: subscription.items.data[0].id,
|
|
@@ -267,7 +279,7 @@ export const createCustomerPortalSession = async (params: {
|
|
|
267
279
|
const logId = await Apilogger.logStripeOutgoing('createCustomerPortalSession', params);
|
|
268
280
|
|
|
269
281
|
try {
|
|
270
|
-
const session = await
|
|
282
|
+
const session = await getStripe().billingPortal.sessions.create({
|
|
271
283
|
customer: params.customerId,
|
|
272
284
|
return_url: params.returnUrl,
|
|
273
285
|
});
|
|
@@ -301,11 +313,11 @@ export const cancelSubscription = async (
|
|
|
301
313
|
let result: Stripe.Subscription;
|
|
302
314
|
|
|
303
315
|
if (cancelAtPeriodEnd) {
|
|
304
|
-
result = await
|
|
316
|
+
result = await getStripe().subscriptions.update(subscriptionId, {
|
|
305
317
|
cancel_at_period_end: true,
|
|
306
318
|
});
|
|
307
319
|
} else {
|
|
308
|
-
result = await
|
|
320
|
+
result = await getStripe().subscriptions.cancel(subscriptionId);
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
// Update log record with response
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Receiver } from '@upstash/qstash';
|
|
2
|
-
import {
|
|
2
|
+
import { withQstash } from '../upstash-config';
|
|
3
3
|
|
|
4
4
|
let cachedReceiver: Receiver | null = null;
|
|
5
|
-
let
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
111
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import type { Redis } from '@upstash/redis';
|
|
3
|
-
import { getRedis } from '../upstash-config';
|
|
1
|
+
import { withRedis } from '../upstash-config';
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
14
|
-
const LockCtor = Lock as unknown as new (...args: any[]) => UpstashLock;
|
|
11
|
+
const generateToken = (): string => {
|
|
15
12
|
try {
|
|
16
|
-
return
|
|
13
|
+
return crypto.randomUUID();
|
|
17
14
|
} catch {
|
|
18
|
-
return
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
return
|
|
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
|
|
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
|
|
57
|
+
await releaseLock(key, token);
|
|
84
58
|
}
|
|
85
59
|
};
|