@solvapay/next 1.0.0-preview.7

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/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @solvapay/next
2
+
3
+ Next.js-specific utilities and helpers for SolvaPay SDK.
4
+
5
+ This package provides framework-specific helpers for Next.js API routes with built-in optimizations like request deduplication and caching.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @solvapay/next @solvapay/server next
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Check Subscription
16
+
17
+ The `checkSubscription` helper provides a simple way to check user subscription status in Next.js API routes with built-in request deduplication:
18
+
19
+ ```typescript
20
+ import { NextRequest, NextResponse } from 'next/server';
21
+ import { checkSubscription } from '@solvapay/next';
22
+
23
+ export async function GET(request: NextRequest) {
24
+ const result = await checkSubscription(request);
25
+
26
+ // If result is a NextResponse, it's an error response - return it
27
+ if (result instanceof NextResponse) {
28
+ return result;
29
+ }
30
+
31
+ // Otherwise, return the subscription data
32
+ return NextResponse.json(result);
33
+ }
34
+ ```
35
+
36
+ ### Features
37
+
38
+ - **Automatic Deduplication**: Prevents duplicate API calls by deduplicating concurrent requests
39
+ - **Caching**: Caches results for 2 seconds to prevent duplicate sequential requests
40
+ - **Automatic Cleanup**: Expired cache entries are automatically cleaned up
41
+ - **Memory Safe**: Maximum cache size limits prevent memory issues
42
+
43
+ ### Cache Management
44
+
45
+ ```typescript
46
+ import {
47
+ clearSubscriptionCache,
48
+ clearAllSubscriptionCache,
49
+ getSubscriptionCacheStats
50
+ } from '@solvapay/next';
51
+
52
+ // Clear cache for a specific user
53
+ clearSubscriptionCache(userId);
54
+
55
+ // Clear all cache entries
56
+ clearAllSubscriptionCache();
57
+
58
+ // Get cache statistics
59
+ const stats = getSubscriptionCacheStats();
60
+ console.log(`In-flight: ${stats.inFlight}, Cached: ${stats.cached}`);
61
+ ```
62
+
63
+ ## Requirements
64
+
65
+ - Next.js >= 13.0.0
66
+ - Node.js >= 18.17
67
+
68
+ ## Why a Separate Package?
69
+
70
+ This package is separate from `@solvapay/server` to keep the server package framework-agnostic. Users who use Express, Fastify, or other frameworks don't need Next.js as a dependency.
71
+
package/dist/index.cjs ADDED
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ checkSubscription: () => checkSubscription,
34
+ clearAllSubscriptionCache: () => clearAllSubscriptionCache,
35
+ clearSubscriptionCache: () => clearSubscriptionCache,
36
+ getSubscriptionCacheStats: () => getSubscriptionCacheStats
37
+ });
38
+ module.exports = __toCommonJS(index_exports);
39
+ var import_server = require("next/server");
40
+ var import_server2 = require("@solvapay/server");
41
+ var import_core = require("@solvapay/core");
42
+ function createRequestDeduplicator(options = {}) {
43
+ const {
44
+ cacheTTL = 2e3,
45
+ maxCacheSize = 1e3,
46
+ cacheErrors = true
47
+ } = options;
48
+ const inFlightRequests = /* @__PURE__ */ new Map();
49
+ const resultCache = /* @__PURE__ */ new Map();
50
+ const cacheInvalidatedAt = /* @__PURE__ */ new Map();
51
+ let cleanupInterval = null;
52
+ if (cacheTTL > 0) {
53
+ cleanupInterval = setInterval(() => {
54
+ const now = Date.now();
55
+ const entriesToDelete = [];
56
+ for (const [key, cached] of resultCache.entries()) {
57
+ if (now - cached.timestamp >= cacheTTL) {
58
+ entriesToDelete.push(key);
59
+ }
60
+ }
61
+ for (const key of entriesToDelete) {
62
+ resultCache.delete(key);
63
+ cacheInvalidatedAt.delete(key);
64
+ }
65
+ if (resultCache.size > maxCacheSize) {
66
+ const sortedEntries = Array.from(resultCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
67
+ const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
68
+ for (const [key] of toRemove) {
69
+ resultCache.delete(key);
70
+ cacheInvalidatedAt.delete(key);
71
+ }
72
+ }
73
+ }, Math.min(cacheTTL, 1e3));
74
+ }
75
+ const deduplicate = async (key, fn) => {
76
+ if (cacheTTL > 0) {
77
+ const cached = resultCache.get(key);
78
+ if (cached && Date.now() - cached.timestamp < cacheTTL) {
79
+ return cached.data;
80
+ } else if (cached) {
81
+ resultCache.delete(key);
82
+ }
83
+ }
84
+ let requestPromise = inFlightRequests.get(key);
85
+ if (!requestPromise) {
86
+ const requestStartTime = Date.now();
87
+ requestPromise = (async () => {
88
+ try {
89
+ const result = await fn();
90
+ const invalidatedAt = cacheInvalidatedAt.get(key);
91
+ const shouldCache = !invalidatedAt || invalidatedAt < requestStartTime;
92
+ if (cacheTTL > 0 && shouldCache) {
93
+ resultCache.set(key, {
94
+ data: result,
95
+ timestamp: Date.now()
96
+ });
97
+ }
98
+ return result;
99
+ } catch (error) {
100
+ const invalidatedAt = cacheInvalidatedAt.get(key);
101
+ const shouldCache = !invalidatedAt || invalidatedAt < requestStartTime;
102
+ if (cacheTTL > 0 && cacheErrors && shouldCache) {
103
+ resultCache.set(key, {
104
+ data: error,
105
+ timestamp: Date.now()
106
+ });
107
+ }
108
+ throw error;
109
+ } finally {
110
+ inFlightRequests.delete(key);
111
+ }
112
+ })();
113
+ const existingPromise = inFlightRequests.get(key);
114
+ if (existingPromise) {
115
+ requestPromise = existingPromise;
116
+ } else {
117
+ inFlightRequests.set(key, requestPromise);
118
+ }
119
+ }
120
+ return requestPromise;
121
+ };
122
+ const clearCache = (key) => {
123
+ resultCache.delete(key);
124
+ cacheInvalidatedAt.set(key, Date.now());
125
+ inFlightRequests.delete(key);
126
+ };
127
+ const clearAllCache = () => {
128
+ resultCache.clear();
129
+ cacheInvalidatedAt.clear();
130
+ inFlightRequests.clear();
131
+ };
132
+ const getStats = () => ({
133
+ inFlight: inFlightRequests.size,
134
+ cached: resultCache.size
135
+ });
136
+ return {
137
+ deduplicate,
138
+ clearCache,
139
+ clearAllCache,
140
+ getStats
141
+ };
142
+ }
143
+ var sharedSubscriptionDeduplicator = null;
144
+ function getSharedDeduplicator(options) {
145
+ if (!sharedSubscriptionDeduplicator) {
146
+ sharedSubscriptionDeduplicator = createRequestDeduplicator({
147
+ cacheTTL: 2e3,
148
+ // Cache results for 2 seconds
149
+ maxCacheSize: 1e3,
150
+ // Maximum cache entries
151
+ cacheErrors: true,
152
+ // Cache error results too
153
+ ...options
154
+ });
155
+ }
156
+ return sharedSubscriptionDeduplicator;
157
+ }
158
+ async function checkSubscription(request, options = {}) {
159
+ try {
160
+ const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
161
+ const userIdOrError = requireUserId(request);
162
+ if (userIdOrError instanceof Response) {
163
+ const clonedResponse = userIdOrError.clone();
164
+ const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
165
+ return import_server.NextResponse.json(body, { status: userIdOrError.status });
166
+ }
167
+ const userId = userIdOrError;
168
+ const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
169
+ const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
170
+ const deduplicator = getSharedDeduplicator(options.deduplication);
171
+ const response = await deduplicator.deduplicate(userId, async () => {
172
+ try {
173
+ const solvaPay = options.solvaPay || (0, import_server2.createSolvaPay)();
174
+ const ensuredCustomerRef = await solvaPay.ensureCustomer(userId, userId, {
175
+ email: email || void 0,
176
+ name: name || void 0
177
+ });
178
+ const customer = await solvaPay.getCustomer({ customerRef: ensuredCustomerRef });
179
+ const now = /* @__PURE__ */ new Date();
180
+ const filteredSubscriptions = (customer.subscriptions || []).filter((sub) => {
181
+ const subAny = sub;
182
+ const isCancelled = sub.status === "cancelled" || subAny.cancelledAt;
183
+ if (!isCancelled) {
184
+ return true;
185
+ }
186
+ if (isCancelled && subAny.endDate) {
187
+ const endDate = new Date(subAny.endDate);
188
+ const isFuture = endDate > now;
189
+ return isFuture;
190
+ }
191
+ return false;
192
+ });
193
+ const result = {
194
+ customerRef: customer.customerRef || userId,
195
+ email: customer.email,
196
+ name: customer.name,
197
+ subscriptions: filteredSubscriptions
198
+ };
199
+ return result;
200
+ } catch (error) {
201
+ console.error("[checkSubscription] Error fetching customer:", error);
202
+ return {
203
+ customerRef: userId,
204
+ subscriptions: []
205
+ };
206
+ }
207
+ });
208
+ return response;
209
+ } catch (error) {
210
+ console.error("Check subscription failed:", error);
211
+ if (error instanceof import_core.SolvaPayError) {
212
+ return import_server.NextResponse.json(
213
+ { error: error.message },
214
+ { status: 500 }
215
+ );
216
+ }
217
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
218
+ return import_server.NextResponse.json(
219
+ { error: "Failed to check subscription", details: errorMessage },
220
+ { status: 500 }
221
+ );
222
+ }
223
+ }
224
+ function clearSubscriptionCache(userId) {
225
+ const deduplicator = getSharedDeduplicator();
226
+ deduplicator.clearCache(userId);
227
+ }
228
+ function clearAllSubscriptionCache() {
229
+ const deduplicator = getSharedDeduplicator();
230
+ deduplicator.clearAllCache();
231
+ }
232
+ function getSubscriptionCacheStats() {
233
+ const deduplicator = getSharedDeduplicator();
234
+ return deduplicator.getStats();
235
+ }
236
+ // Annotate the CommonJS export names for ESM import in node:
237
+ 0 && (module.exports = {
238
+ checkSubscription,
239
+ clearAllSubscriptionCache,
240
+ clearSubscriptionCache,
241
+ getSubscriptionCacheStats
242
+ });
@@ -0,0 +1,129 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { SolvaPay } from '@solvapay/server';
3
+
4
+ /**
5
+ * SolvaPay Next.js SDK
6
+ *
7
+ * Framework-specific helpers and utilities for Next.js API routes.
8
+ * These utilities provide common patterns with built-in optimizations
9
+ * like request deduplication and caching.
10
+ */
11
+
12
+ /**
13
+ * Request deduplication and caching options
14
+ */
15
+ interface RequestDeduplicationOptions {
16
+ /**
17
+ * Time-to-live for cached results in milliseconds (default: 2000)
18
+ * Set to 0 to disable caching (only deduplicate concurrent requests)
19
+ */
20
+ cacheTTL?: number;
21
+ /**
22
+ * Maximum cache size before cleanup (default: 1000)
23
+ * When exceeded, oldest entries are removed
24
+ */
25
+ maxCacheSize?: number;
26
+ /**
27
+ * Whether to cache error results (default: true)
28
+ * When false, only successful results are cached
29
+ */
30
+ cacheErrors?: boolean;
31
+ }
32
+ /**
33
+ * Subscription check result
34
+ */
35
+ interface SubscriptionCheckResult {
36
+ customerRef: string;
37
+ email?: string;
38
+ name?: string;
39
+ subscriptions: Array<{
40
+ reference: string;
41
+ planName?: string;
42
+ agentName?: string;
43
+ status?: string;
44
+ startDate?: string;
45
+ [key: string]: unknown;
46
+ }>;
47
+ }
48
+ /**
49
+ * Options for checking subscriptions
50
+ */
51
+ interface CheckSubscriptionOptions {
52
+ /**
53
+ * Request deduplication options
54
+ * Default: { cacheTTL: 2000, maxCacheSize: 1000, cacheErrors: true }
55
+ */
56
+ deduplication?: RequestDeduplicationOptions;
57
+ /**
58
+ * Custom SolvaPay instance (optional)
59
+ * If not provided, a new instance will be created
60
+ */
61
+ solvaPay?: SolvaPay;
62
+ /**
63
+ * Whether to include user email in customer data
64
+ * Default: true
65
+ */
66
+ includeEmail?: boolean;
67
+ /**
68
+ * Whether to include user name in customer data
69
+ * Default: true
70
+ */
71
+ includeName?: boolean;
72
+ }
73
+ /**
74
+ * Check user subscription status
75
+ *
76
+ * This helper function:
77
+ * 1. Extracts user ID from request (via requireUserId from @solvapay/auth)
78
+ * 2. Gets user email and name from Supabase JWT token
79
+ * 3. Ensures customer exists in SolvaPay
80
+ * 4. Returns customer subscription information
81
+ * 5. Handles deduplication automatically
82
+ *
83
+ * @param request - Next.js request object (NextRequest extends Request, so Request is accepted)
84
+ * @param options - Configuration options
85
+ * @returns Subscription check result or error response
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * import { NextRequest } from 'next/server';
90
+ * import { checkSubscription } from '@solvapay/next';
91
+ *
92
+ * export async function GET(request: NextRequest) {
93
+ * const result = await checkSubscription(request);
94
+ * if (result instanceof NextResponse) {
95
+ * return result; // Error response
96
+ * }
97
+ * return NextResponse.json(result);
98
+ * }
99
+ * ```
100
+ */
101
+ declare function checkSubscription(request: Request, options?: CheckSubscriptionOptions): Promise<SubscriptionCheckResult | NextResponse>;
102
+ /**
103
+ * Clear subscription cache for a specific user
104
+ *
105
+ * Useful when you know subscription status has changed
106
+ * (e.g., after a successful checkout or subscription update)
107
+ *
108
+ * @param userId - User ID to clear cache for
109
+ */
110
+ declare function clearSubscriptionCache(userId: string): void;
111
+ /**
112
+ * Clear all subscription cache entries
113
+ *
114
+ * Useful for testing or when you need to force fresh lookups
115
+ */
116
+ declare function clearAllSubscriptionCache(): void;
117
+ /**
118
+ * Get subscription cache statistics
119
+ *
120
+ * Useful for monitoring and debugging
121
+ *
122
+ * @returns Cache statistics
123
+ */
124
+ declare function getSubscriptionCacheStats(): {
125
+ inFlight: number;
126
+ cached: number;
127
+ };
128
+
129
+ export { type CheckSubscriptionOptions, type RequestDeduplicationOptions, type SubscriptionCheckResult, checkSubscription, clearAllSubscriptionCache, clearSubscriptionCache, getSubscriptionCacheStats };
@@ -0,0 +1,129 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { SolvaPay } from '@solvapay/server';
3
+
4
+ /**
5
+ * SolvaPay Next.js SDK
6
+ *
7
+ * Framework-specific helpers and utilities for Next.js API routes.
8
+ * These utilities provide common patterns with built-in optimizations
9
+ * like request deduplication and caching.
10
+ */
11
+
12
+ /**
13
+ * Request deduplication and caching options
14
+ */
15
+ interface RequestDeduplicationOptions {
16
+ /**
17
+ * Time-to-live for cached results in milliseconds (default: 2000)
18
+ * Set to 0 to disable caching (only deduplicate concurrent requests)
19
+ */
20
+ cacheTTL?: number;
21
+ /**
22
+ * Maximum cache size before cleanup (default: 1000)
23
+ * When exceeded, oldest entries are removed
24
+ */
25
+ maxCacheSize?: number;
26
+ /**
27
+ * Whether to cache error results (default: true)
28
+ * When false, only successful results are cached
29
+ */
30
+ cacheErrors?: boolean;
31
+ }
32
+ /**
33
+ * Subscription check result
34
+ */
35
+ interface SubscriptionCheckResult {
36
+ customerRef: string;
37
+ email?: string;
38
+ name?: string;
39
+ subscriptions: Array<{
40
+ reference: string;
41
+ planName?: string;
42
+ agentName?: string;
43
+ status?: string;
44
+ startDate?: string;
45
+ [key: string]: unknown;
46
+ }>;
47
+ }
48
+ /**
49
+ * Options for checking subscriptions
50
+ */
51
+ interface CheckSubscriptionOptions {
52
+ /**
53
+ * Request deduplication options
54
+ * Default: { cacheTTL: 2000, maxCacheSize: 1000, cacheErrors: true }
55
+ */
56
+ deduplication?: RequestDeduplicationOptions;
57
+ /**
58
+ * Custom SolvaPay instance (optional)
59
+ * If not provided, a new instance will be created
60
+ */
61
+ solvaPay?: SolvaPay;
62
+ /**
63
+ * Whether to include user email in customer data
64
+ * Default: true
65
+ */
66
+ includeEmail?: boolean;
67
+ /**
68
+ * Whether to include user name in customer data
69
+ * Default: true
70
+ */
71
+ includeName?: boolean;
72
+ }
73
+ /**
74
+ * Check user subscription status
75
+ *
76
+ * This helper function:
77
+ * 1. Extracts user ID from request (via requireUserId from @solvapay/auth)
78
+ * 2. Gets user email and name from Supabase JWT token
79
+ * 3. Ensures customer exists in SolvaPay
80
+ * 4. Returns customer subscription information
81
+ * 5. Handles deduplication automatically
82
+ *
83
+ * @param request - Next.js request object (NextRequest extends Request, so Request is accepted)
84
+ * @param options - Configuration options
85
+ * @returns Subscription check result or error response
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * import { NextRequest } from 'next/server';
90
+ * import { checkSubscription } from '@solvapay/next';
91
+ *
92
+ * export async function GET(request: NextRequest) {
93
+ * const result = await checkSubscription(request);
94
+ * if (result instanceof NextResponse) {
95
+ * return result; // Error response
96
+ * }
97
+ * return NextResponse.json(result);
98
+ * }
99
+ * ```
100
+ */
101
+ declare function checkSubscription(request: Request, options?: CheckSubscriptionOptions): Promise<SubscriptionCheckResult | NextResponse>;
102
+ /**
103
+ * Clear subscription cache for a specific user
104
+ *
105
+ * Useful when you know subscription status has changed
106
+ * (e.g., after a successful checkout or subscription update)
107
+ *
108
+ * @param userId - User ID to clear cache for
109
+ */
110
+ declare function clearSubscriptionCache(userId: string): void;
111
+ /**
112
+ * Clear all subscription cache entries
113
+ *
114
+ * Useful for testing or when you need to force fresh lookups
115
+ */
116
+ declare function clearAllSubscriptionCache(): void;
117
+ /**
118
+ * Get subscription cache statistics
119
+ *
120
+ * Useful for monitoring and debugging
121
+ *
122
+ * @returns Cache statistics
123
+ */
124
+ declare function getSubscriptionCacheStats(): {
125
+ inFlight: number;
126
+ cached: number;
127
+ };
128
+
129
+ export { type CheckSubscriptionOptions, type RequestDeduplicationOptions, type SubscriptionCheckResult, checkSubscription, clearAllSubscriptionCache, clearSubscriptionCache, getSubscriptionCacheStats };
package/dist/index.js ADDED
@@ -0,0 +1,204 @@
1
+ // src/index.ts
2
+ import { NextResponse } from "next/server";
3
+ import { createSolvaPay } from "@solvapay/server";
4
+ import { SolvaPayError } from "@solvapay/core";
5
+ function createRequestDeduplicator(options = {}) {
6
+ const {
7
+ cacheTTL = 2e3,
8
+ maxCacheSize = 1e3,
9
+ cacheErrors = true
10
+ } = options;
11
+ const inFlightRequests = /* @__PURE__ */ new Map();
12
+ const resultCache = /* @__PURE__ */ new Map();
13
+ const cacheInvalidatedAt = /* @__PURE__ */ new Map();
14
+ let cleanupInterval = null;
15
+ if (cacheTTL > 0) {
16
+ cleanupInterval = setInterval(() => {
17
+ const now = Date.now();
18
+ const entriesToDelete = [];
19
+ for (const [key, cached] of resultCache.entries()) {
20
+ if (now - cached.timestamp >= cacheTTL) {
21
+ entriesToDelete.push(key);
22
+ }
23
+ }
24
+ for (const key of entriesToDelete) {
25
+ resultCache.delete(key);
26
+ cacheInvalidatedAt.delete(key);
27
+ }
28
+ if (resultCache.size > maxCacheSize) {
29
+ const sortedEntries = Array.from(resultCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
30
+ const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
31
+ for (const [key] of toRemove) {
32
+ resultCache.delete(key);
33
+ cacheInvalidatedAt.delete(key);
34
+ }
35
+ }
36
+ }, Math.min(cacheTTL, 1e3));
37
+ }
38
+ const deduplicate = async (key, fn) => {
39
+ if (cacheTTL > 0) {
40
+ const cached = resultCache.get(key);
41
+ if (cached && Date.now() - cached.timestamp < cacheTTL) {
42
+ return cached.data;
43
+ } else if (cached) {
44
+ resultCache.delete(key);
45
+ }
46
+ }
47
+ let requestPromise = inFlightRequests.get(key);
48
+ if (!requestPromise) {
49
+ const requestStartTime = Date.now();
50
+ requestPromise = (async () => {
51
+ try {
52
+ const result = await fn();
53
+ const invalidatedAt = cacheInvalidatedAt.get(key);
54
+ const shouldCache = !invalidatedAt || invalidatedAt < requestStartTime;
55
+ if (cacheTTL > 0 && shouldCache) {
56
+ resultCache.set(key, {
57
+ data: result,
58
+ timestamp: Date.now()
59
+ });
60
+ }
61
+ return result;
62
+ } catch (error) {
63
+ const invalidatedAt = cacheInvalidatedAt.get(key);
64
+ const shouldCache = !invalidatedAt || invalidatedAt < requestStartTime;
65
+ if (cacheTTL > 0 && cacheErrors && shouldCache) {
66
+ resultCache.set(key, {
67
+ data: error,
68
+ timestamp: Date.now()
69
+ });
70
+ }
71
+ throw error;
72
+ } finally {
73
+ inFlightRequests.delete(key);
74
+ }
75
+ })();
76
+ const existingPromise = inFlightRequests.get(key);
77
+ if (existingPromise) {
78
+ requestPromise = existingPromise;
79
+ } else {
80
+ inFlightRequests.set(key, requestPromise);
81
+ }
82
+ }
83
+ return requestPromise;
84
+ };
85
+ const clearCache = (key) => {
86
+ resultCache.delete(key);
87
+ cacheInvalidatedAt.set(key, Date.now());
88
+ inFlightRequests.delete(key);
89
+ };
90
+ const clearAllCache = () => {
91
+ resultCache.clear();
92
+ cacheInvalidatedAt.clear();
93
+ inFlightRequests.clear();
94
+ };
95
+ const getStats = () => ({
96
+ inFlight: inFlightRequests.size,
97
+ cached: resultCache.size
98
+ });
99
+ return {
100
+ deduplicate,
101
+ clearCache,
102
+ clearAllCache,
103
+ getStats
104
+ };
105
+ }
106
+ var sharedSubscriptionDeduplicator = null;
107
+ function getSharedDeduplicator(options) {
108
+ if (!sharedSubscriptionDeduplicator) {
109
+ sharedSubscriptionDeduplicator = createRequestDeduplicator({
110
+ cacheTTL: 2e3,
111
+ // Cache results for 2 seconds
112
+ maxCacheSize: 1e3,
113
+ // Maximum cache entries
114
+ cacheErrors: true,
115
+ // Cache error results too
116
+ ...options
117
+ });
118
+ }
119
+ return sharedSubscriptionDeduplicator;
120
+ }
121
+ async function checkSubscription(request, options = {}) {
122
+ try {
123
+ const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
124
+ const userIdOrError = requireUserId(request);
125
+ if (userIdOrError instanceof Response) {
126
+ const clonedResponse = userIdOrError.clone();
127
+ const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
128
+ return NextResponse.json(body, { status: userIdOrError.status });
129
+ }
130
+ const userId = userIdOrError;
131
+ const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
132
+ const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
133
+ const deduplicator = getSharedDeduplicator(options.deduplication);
134
+ const response = await deduplicator.deduplicate(userId, async () => {
135
+ try {
136
+ const solvaPay = options.solvaPay || createSolvaPay();
137
+ const ensuredCustomerRef = await solvaPay.ensureCustomer(userId, userId, {
138
+ email: email || void 0,
139
+ name: name || void 0
140
+ });
141
+ const customer = await solvaPay.getCustomer({ customerRef: ensuredCustomerRef });
142
+ const now = /* @__PURE__ */ new Date();
143
+ const filteredSubscriptions = (customer.subscriptions || []).filter((sub) => {
144
+ const subAny = sub;
145
+ const isCancelled = sub.status === "cancelled" || subAny.cancelledAt;
146
+ if (!isCancelled) {
147
+ return true;
148
+ }
149
+ if (isCancelled && subAny.endDate) {
150
+ const endDate = new Date(subAny.endDate);
151
+ const isFuture = endDate > now;
152
+ return isFuture;
153
+ }
154
+ return false;
155
+ });
156
+ const result = {
157
+ customerRef: customer.customerRef || userId,
158
+ email: customer.email,
159
+ name: customer.name,
160
+ subscriptions: filteredSubscriptions
161
+ };
162
+ return result;
163
+ } catch (error) {
164
+ console.error("[checkSubscription] Error fetching customer:", error);
165
+ return {
166
+ customerRef: userId,
167
+ subscriptions: []
168
+ };
169
+ }
170
+ });
171
+ return response;
172
+ } catch (error) {
173
+ console.error("Check subscription failed:", error);
174
+ if (error instanceof SolvaPayError) {
175
+ return NextResponse.json(
176
+ { error: error.message },
177
+ { status: 500 }
178
+ );
179
+ }
180
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
181
+ return NextResponse.json(
182
+ { error: "Failed to check subscription", details: errorMessage },
183
+ { status: 500 }
184
+ );
185
+ }
186
+ }
187
+ function clearSubscriptionCache(userId) {
188
+ const deduplicator = getSharedDeduplicator();
189
+ deduplicator.clearCache(userId);
190
+ }
191
+ function clearAllSubscriptionCache() {
192
+ const deduplicator = getSharedDeduplicator();
193
+ deduplicator.clearAllCache();
194
+ }
195
+ function getSubscriptionCacheStats() {
196
+ const deduplicator = getSharedDeduplicator();
197
+ return deduplicator.getStats();
198
+ }
199
+ export {
200
+ checkSubscription,
201
+ clearAllSubscriptionCache,
202
+ clearSubscriptionCache,
203
+ getSubscriptionCacheStats
204
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@solvapay/next",
3
+ "version": "1.0.0-preview.7",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/solvapay/solvapay-sdk.git",
25
+ "directory": "packages/next"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.17"
29
+ },
30
+ "sideEffects": false,
31
+ "dependencies": {
32
+ "@solvapay/auth": "1.0.0-preview.7",
33
+ "@solvapay/core": "1.0.0-preview.7",
34
+ "@solvapay/server": "1.0.0-preview.7"
35
+ },
36
+ "peerDependencies": {
37
+ "next": ">=13.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "next": "^14.0.0",
41
+ "tsup": "^8.0.1",
42
+ "typescript": "^5.5.4",
43
+ "vitest": "^2.0.5",
44
+ "@solvapay/test-utils": "0.0.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.build.json --external next",
48
+ "test": "vitest run || exit 0",
49
+ "test:watch": "vitest",
50
+ "lint": "echo 'no lint yet'"
51
+ }
52
+ }