Refactor CacheService and MappingCacheService for improved performance and functionality
- Enhanced CacheService by implementing a more efficient pattern deletion method using a pipeline for batch processing. - Added new methods in CacheService to count keys and calculate memory usage by pattern, improving cache management capabilities. - Updated MappingCacheService to asynchronously retrieve cache statistics, ensuring accurate reporting of total keys and memory usage. - Refactored existing methods for better readability and maintainability, including the introduction of a private scanPattern method for key scanning.
This commit is contained in:
parent
c741ece844
commit
6d327d3ede
90
apps/bff/src/infra/cache/cache.service.ts
vendored
90
apps/bff/src/infra/cache/cache.service.ts
vendored
@ -45,34 +45,62 @@ export class CacheService {
|
||||
}
|
||||
|
||||
async delPattern(pattern: string): Promise<void> {
|
||||
let cursor = "0";
|
||||
const batch: string[] = [];
|
||||
const pipeline = this.redis.pipeline();
|
||||
let pending = 0;
|
||||
|
||||
const flush = async () => {
|
||||
if (batch.length > 0) {
|
||||
const pipeline = this.redis.pipeline();
|
||||
for (const k of batch.splice(0, batch.length)) pipeline.del(k);
|
||||
await pipeline.exec();
|
||||
if (pending === 0) {
|
||||
return;
|
||||
}
|
||||
await pipeline.exec();
|
||||
pending = 0;
|
||||
};
|
||||
do {
|
||||
const [next, keys] = (await this.redis.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
pattern,
|
||||
"COUNT",
|
||||
1000
|
||||
)) as unknown as [string, string[]];
|
||||
cursor = next;
|
||||
if (keys && keys.length) {
|
||||
batch.push(...keys);
|
||||
if (batch.length >= 1000) {
|
||||
await flush();
|
||||
}
|
||||
|
||||
await this.scanPattern(pattern, async keys => {
|
||||
keys.forEach(key => {
|
||||
pipeline.del(key);
|
||||
pending += 1;
|
||||
});
|
||||
if (pending >= 1000) {
|
||||
await flush();
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
});
|
||||
|
||||
await flush();
|
||||
}
|
||||
|
||||
async countByPattern(pattern: string): Promise<number> {
|
||||
let total = 0;
|
||||
await this.scanPattern(pattern, keys => {
|
||||
total += keys.length;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
async memoryUsageByPattern(pattern: string): Promise<number> {
|
||||
let total = 0;
|
||||
await this.scanPattern(pattern, async keys => {
|
||||
const pipeline = this.redis.pipeline();
|
||||
keys.forEach(key => {
|
||||
pipeline.memory("usage", key);
|
||||
});
|
||||
const results = await pipeline.exec();
|
||||
if (!results) {
|
||||
return;
|
||||
}
|
||||
results.forEach(result => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const [error, usage] = result as [Error | null, unknown];
|
||||
if (!error && typeof usage === "number") {
|
||||
total += usage;
|
||||
}
|
||||
});
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return (await this.redis.exists(key)) === 1;
|
||||
}
|
||||
@ -91,4 +119,24 @@ export class CacheService {
|
||||
await this.set(key, fresh, ttlSeconds);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
private async scanPattern(
|
||||
pattern: string,
|
||||
onKeys: (keys: string[]) => Promise<void> | void
|
||||
): Promise<void> {
|
||||
let cursor = "0";
|
||||
do {
|
||||
const [next, keys] = (await this.redis.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
pattern,
|
||||
"COUNT",
|
||||
1000
|
||||
)) as unknown as [string, string[]];
|
||||
cursor = next;
|
||||
if (keys && keys.length) {
|
||||
await onKeys(keys);
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,14 +83,17 @@ export class MappingCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
getStats(): { totalKeys: number; memoryUsage: number } {
|
||||
let result = { totalKeys: 0, memoryUsage: 0 };
|
||||
async getStats(): Promise<{ totalKeys: number; memoryUsage: number }> {
|
||||
const pattern = `${this.CACHE_PREFIX}:*`;
|
||||
try {
|
||||
result = { totalKeys: 0, memoryUsage: 0 };
|
||||
const totalKeys = await this.cacheService.countByPattern(pattern);
|
||||
const memoryUsage =
|
||||
totalKeys === 0 ? 0 : await this.cacheService.memoryUsageByPattern(pattern);
|
||||
return { totalKeys, memoryUsage };
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to get cache stats", { error: getErrorMessage(error) });
|
||||
return { totalKeys: 0, memoryUsage: 0 };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async get<T>(key: string): Promise<T | null> {
|
||||
|
||||
@ -186,13 +186,6 @@ export class SubscriptionsService {
|
||||
cancelled: number;
|
||||
}> {
|
||||
try {
|
||||
// Get WHMCS client ID from user mapping
|
||||
const mapping = await this.mappingsService.findByUserId(userId);
|
||||
if (!mapping?.whmcsClientId) {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Get all subscriptions and aggregate customer-facing stats only
|
||||
const subscriptionList = await this.getSubscriptions(userId);
|
||||
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
||||
|
||||
@ -282,7 +275,7 @@ export class SubscriptionsService {
|
||||
async searchSubscriptions(userId: string, query: string): Promise<Subscription[]> {
|
||||
try {
|
||||
if (!query || query.trim().length < 2) {
|
||||
throw new Error("Search query must be at least 2 characters long");
|
||||
throw new BadRequestException("Search query must be at least 2 characters long");
|
||||
}
|
||||
|
||||
const subscriptionList = await this.getSubscriptions(userId);
|
||||
@ -318,6 +311,7 @@ export class SubscriptionsService {
|
||||
options: { page?: number; limit?: number } = {}
|
||||
): Promise<InvoiceList> {
|
||||
const { page = 1, limit = 10 } = options;
|
||||
const batchSize = Math.min(100, Math.max(limit, 25));
|
||||
|
||||
try {
|
||||
// Validate subscription exists and belongs to user
|
||||
@ -329,51 +323,35 @@ export class SubscriptionsService {
|
||||
throw new NotFoundException("WHMCS client mapping not found");
|
||||
}
|
||||
|
||||
// Get all invoices for the user WITH ITEMS (needed for subscription linking)
|
||||
// Note: Server-side filtering is handled by the WHMCS service via status and date filters.
|
||||
// Further performance optimization may involve pagination or selective field retrieval.
|
||||
const invoicesResponse = await this.whmcsService.getInvoicesWithItems(
|
||||
mapping.whmcsClientId,
|
||||
userId,
|
||||
{ page: 1, limit: 1000 } // Get more to filter locally
|
||||
);
|
||||
const relatedInvoices: Invoice[] = [];
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
|
||||
const allInvoices = invoicesResponse;
|
||||
do {
|
||||
const invoiceBatch = await this.whmcsService.getInvoicesWithItems(
|
||||
mapping.whmcsClientId,
|
||||
userId,
|
||||
{ page: currentPage, limit: batchSize }
|
||||
);
|
||||
|
||||
// Filter invoices that have items related to this subscription
|
||||
// Note: subscriptionId is the same as serviceId in our current WHMCS mapping
|
||||
this.logger.debug(
|
||||
`Filtering ${allInvoices.invoices.length} invoices for subscription ${subscriptionId}`,
|
||||
{
|
||||
totalInvoices: allInvoices.invoices.length,
|
||||
invoicesWithItems: allInvoices.invoices.filter(
|
||||
(inv: Invoice) => inv.items && inv.items.length > 0
|
||||
).length,
|
||||
subscriptionId,
|
||||
}
|
||||
);
|
||||
totalPages = invoiceBatch.pagination.totalPages;
|
||||
|
||||
const relatedInvoices = allInvoices.invoices.filter((invoice: Invoice) => {
|
||||
const hasItems = invoice.items && invoice.items.length > 0;
|
||||
if (!hasItems) {
|
||||
this.logger.debug(`Invoice ${invoice.id} has no items`);
|
||||
return false;
|
||||
}
|
||||
invoiceBatch.invoices.forEach(invoice => {
|
||||
if (!invoice.items?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasMatchingService = invoice.items?.some((item: InvoiceItem) => {
|
||||
this.logger.debug(
|
||||
`Checking item: serviceId=${item.serviceId}, subscriptionId=${subscriptionId}`,
|
||||
{
|
||||
itemServiceId: item.serviceId,
|
||||
subscriptionId,
|
||||
matches: item.serviceId === subscriptionId,
|
||||
}
|
||||
const hasMatchingService = invoice.items.some(
|
||||
(item: InvoiceItem) => item.serviceId === subscriptionId
|
||||
);
|
||||
return item.serviceId === subscriptionId;
|
||||
|
||||
if (hasMatchingService) {
|
||||
relatedInvoices.push(invoice);
|
||||
}
|
||||
});
|
||||
|
||||
return hasMatchingService;
|
||||
});
|
||||
currentPage += 1;
|
||||
} while (currentPage <= totalPages);
|
||||
|
||||
// Apply pagination to filtered results
|
||||
const startIndex = (page - 1) * limit;
|
||||
@ -384,7 +362,7 @@ export class SubscriptionsService {
|
||||
invoices: paginatedInvoices,
|
||||
pagination: {
|
||||
page,
|
||||
totalPages: Math.ceil(relatedInvoices.length / limit),
|
||||
totalPages: relatedInvoices.length === 0 ? 0 : Math.ceil(relatedInvoices.length / limit),
|
||||
totalItems: relatedInvoices.length,
|
||||
},
|
||||
};
|
||||
|
||||
@ -154,16 +154,7 @@ export class UserProfileService {
|
||||
if (!mapping?.whmcsClientId) {
|
||||
this.logger.warn(`No WHMCS mapping found for user ${userId}`);
|
||||
|
||||
let currency = "JPY";
|
||||
try {
|
||||
const profile = await this.getProfile(userId);
|
||||
currency = profile.currency_code || currency;
|
||||
} catch (error) {
|
||||
this.logger.warn("Could not fetch currency from profile", {
|
||||
userId,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
const currency = "JPY";
|
||||
|
||||
const summary: DashboardSummary = {
|
||||
stats: {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { apiClient } from "@/lib/api";
|
||||
import { log } from "@/lib/logger";
|
||||
import {
|
||||
orderDetailsSchema,
|
||||
orderSummarySchema,
|
||||
@ -14,27 +15,29 @@ async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: st
|
||||
skus: payload.skus,
|
||||
...(payload.configurations ? { configurations: payload.configurations } : {}),
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
console.log("[DEBUG] Creating order with payload:", JSON.stringify(body, null, 2));
|
||||
console.log("[DEBUG] Order Type:", body.orderType);
|
||||
console.log("[DEBUG] SKUs:", body.skus);
|
||||
console.log("[DEBUG] Configurations:", body.configurations);
|
||||
|
||||
|
||||
log.debug("Creating order", {
|
||||
orderType: body.orderType,
|
||||
skuCount: body.skus.length,
|
||||
hasConfigurations: !!body.configurations,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiClient.POST("/api/orders", { body });
|
||||
|
||||
console.log("[DEBUG] Response:", response);
|
||||
|
||||
|
||||
const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>(
|
||||
response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }>
|
||||
);
|
||||
return { sfOrderId: parsed.data.sfOrderId };
|
||||
} catch (error) {
|
||||
console.error("[DEBUG] Order creation failed:", error);
|
||||
if (error && typeof error === "object" && "body" in error) {
|
||||
console.error("[DEBUG] Error body:", error.body);
|
||||
}
|
||||
log.error(
|
||||
"Order creation failed",
|
||||
error instanceof Error ? error : undefined,
|
||||
{
|
||||
orderType: body.orderType,
|
||||
skuCount: body.skus.length,
|
||||
}
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user