perf: AdminAnalyticsFacade.load() - forkJoin instead of 4 nested subscriptions
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
orders/products/categories/reviews don't depend on each other but were fetched serially, 4 levels deep. forkJoin runs them in parallel. Also fixes a real race: a rapid setDateRange() double-call previously had no cancellation, so a stale in-flight chain could resolve after and overwrite a newer one. Added a cancelPreviousLoad$ subject with takeUntil. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||||
import { take } from 'rxjs/operators';
|
import { forkJoin, Subject } from 'rxjs';
|
||||||
|
import { take, takeUntil } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AdminAnalyticsDateRange,
|
AdminAnalyticsDateRange,
|
||||||
AdminAnalyticsSeriesPoint,
|
AdminAnalyticsSeriesPoint,
|
||||||
@@ -62,7 +63,13 @@ export class AdminAnalyticsFacade {
|
|||||||
|
|
||||||
readonly warnings = computed(() => this.recommendations().filter(card => card.severity !== 'info'));
|
readonly warnings = computed(() => this.recommendations().filter(card => card.severity !== 'info'));
|
||||||
|
|
||||||
|
private readonly cancelPreviousLoad$ = new Subject<void>();
|
||||||
|
|
||||||
load(): void {
|
load(): void {
|
||||||
|
// Cancel any still-in-flight previous load so a rapid setDateRange() double-call
|
||||||
|
// can't have a stale response overwrite a newer one.
|
||||||
|
this.cancelPreviousLoad$.next();
|
||||||
|
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.error.set(false);
|
this.error.set(false);
|
||||||
this.dashboardFacade.ensureLoaded();
|
this.dashboardFacade.ensureLoaded();
|
||||||
@@ -74,10 +81,13 @@ export class AdminAnalyticsFacade {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const fail = (): void => { this.loading.set(false); this.error.set(true); };
|
forkJoin({
|
||||||
|
orderResult: this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 100000 }),
|
||||||
this.ordersGateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe({
|
productResult: this.productsGateway.loadProducts({ search: '', categoryId: null, visibility: 'all', stock: 'all', includeArchived: true, sort: 'title', page: 1, pageSize: 100000 }),
|
||||||
next: orderResult => {
|
categories: this.categoriesGateway.loadCategories({ search: '', visibility: 'all', includeDeleted: true }),
|
||||||
|
reviewResult: this.moderationGateway.loadReviews({ search: '', status: 'all', rating: 'all', page: 1, pageSize: 100000 }),
|
||||||
|
}).pipe(take(1), takeUntil(this.cancelPreviousLoad$)).subscribe({
|
||||||
|
next: ({ orderResult, productResult, categories, reviewResult }) => {
|
||||||
const cutoff = Date.now() - this.dateRange() * 24 * 60 * 60 * 1000;
|
const cutoff = Date.now() - this.dateRange() * 24 * 60 * 60 * 1000;
|
||||||
const inRange = orderResult.items.filter(order => new Date(order.createdAt).getTime() >= cutoff);
|
const inRange = orderResult.items.filter(order => new Date(order.createdAt).getTime() >= cutoff);
|
||||||
|
|
||||||
@@ -89,43 +99,28 @@ export class AdminAnalyticsFacade {
|
|||||||
const ordersCount = inRange.length;
|
const ordersCount = inRange.length;
|
||||||
const uniqueCustomers = new Set(inRange.map(order => order.customer.email)).size;
|
const uniqueCustomers = new Set(inRange.map(order => order.customer.email)).size;
|
||||||
|
|
||||||
this.productsGateway.loadProducts({ search: '', categoryId: null, visibility: 'all', stock: 'all', includeArchived: true, sort: 'title', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe({
|
const products = productResult.items;
|
||||||
next: productResult => {
|
const reviews = reviewResult.items;
|
||||||
this.categoriesGateway.loadCategories({ search: '', visibility: 'all', includeDeleted: true }).pipe(take(1)).subscribe({
|
|
||||||
next: categories => {
|
|
||||||
this.moderationGateway.loadReviews({ search: '', status: 'all', rating: 'all', page: 1, pageSize: 100000 }).pipe(take(1)).subscribe({
|
|
||||||
next: reviewResult => {
|
|
||||||
const products = productResult.items;
|
|
||||||
const reviews = reviewResult.items;
|
|
||||||
|
|
||||||
this.summary.set({
|
this.summary.set({
|
||||||
revenueTotal,
|
revenueTotal,
|
||||||
currency: inRange[0]?.currency ?? 'RUB',
|
currency: inRange[0]?.currency ?? 'RUB',
|
||||||
ordersCount,
|
ordersCount,
|
||||||
avgOrderValue: ordersCount > 0 ? Math.round(revenueTotal / ordersCount) : 0,
|
avgOrderValue: ordersCount > 0 ? Math.round(revenueTotal / ordersCount) : 0,
|
||||||
productsCount: products.length,
|
productsCount: products.length,
|
||||||
categoriesCount: categories.length,
|
categoriesCount: categories.length,
|
||||||
customersCount: uniqueCustomers,
|
customersCount: uniqueCustomers,
|
||||||
conversionRate: null,
|
conversionRate: null,
|
||||||
});
|
|
||||||
|
|
||||||
this.lowStockProducts.set(this.buildLowStock(products));
|
|
||||||
this.productAnalytics.set(this.buildProductAnalytics(products));
|
|
||||||
this.marketplaceHealth.set(this.buildMarketplaceHealth(products, categories, reviews, orderResult.items));
|
|
||||||
this.recommendations.set(this.buildRecommendations(products, categories));
|
|
||||||
|
|
||||||
this.loading.set(false);
|
|
||||||
},
|
|
||||||
error: fail
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: fail
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: fail
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.lowStockProducts.set(this.buildLowStock(products));
|
||||||
|
this.productAnalytics.set(this.buildProductAnalytics(products));
|
||||||
|
this.marketplaceHealth.set(this.buildMarketplaceHealth(products, categories, reviews, orderResult.items));
|
||||||
|
this.recommendations.set(this.buildRecommendations(products, categories));
|
||||||
|
|
||||||
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
error: fail
|
error: () => { this.loading.set(false); this.error.set(true); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user