diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index 0a31c6a9..0b837f49 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -223,12 +223,27 @@ Raymer * @version 1.0.0 */
+
No claims match your filters.
@@ -302,7 +322,7 @@ import {
GiverReceiverInputInfo,
OnboardPage,
} from "../libs/util";
-import { GiveSummaryRecord } from "../interfaces/records";
+import { GiveSummaryRecord, PlanSummaryRecord } from "../interfaces/records";
import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "../interfaces/give";
@@ -414,16 +434,18 @@ export default class HomeView extends Vue {
allMyDids: Array = [];
apiServer = "";
blockedContactDids: Array = [];
+ // Feed data and state
feedData: GiveRecordWithContactInfo[] = [];
- feedPreviousOldestId?: string;
+ isFeedLoading = false;
+ isBackgroundProcessing = false;
+ feedPreviousOldestId: string | undefined = undefined;
feedLastViewedClaimId?: string;
givenName = "";
+ isRegistered = false;
isAnyFeedFilterOn = false;
// isCreatingIdentifier removed - identity creation now handled by router guard
isFeedFilteredByVisible = false;
isFeedFilteredByNearby = false;
- isFeedLoading = true;
- isRegistered = false;
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
newOffersToUserHitLimit: boolean = false;
@@ -823,9 +845,8 @@ export default class HomeView extends Vue {
}
/**
- * Reloads feed when filter settings change using ultra-concise mixin utilities
- * - Updates filter states
- * - Clears existing feed data
+ * Reloads feed when filters change
+ * - Resets feed data and pagination
* - Triggers new feed load
*
* @public
@@ -840,8 +861,11 @@ export default class HomeView extends Vue {
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
+ // Reset feed state to prevent InfiniteScroll conflicts
this.feedData = [];
this.feedPreviousOldestId = undefined;
+ this.isBackgroundProcessing = false;
+
await this.updateAllFeed();
}
@@ -853,14 +877,59 @@ export default class HomeView extends Vue {
* @param payload Boolean indicating if more items should be loaded
*/
async loadMoreGives(payload: boolean) {
- // Since feed now loads projects along the way, it takes longer
- // and the InfiniteScroll component triggers a load before finished.
- // One alternative is to totally separate the project link loading.
- if (payload && !this.isFeedLoading) {
+ // Prevent loading if already processing or if background processing is active
+ if (payload && !this.isFeedLoading && !this.isBackgroundProcessing) {
+ // Use direct update instead of debounced to avoid conflicts with InfiniteScroll's debouncing
await this.updateAllFeed();
}
}
+ /**
+ * Debounced version of updateAllFeed to prevent rapid successive calls
+ *
+ * @internal
+ * @callGraph
+ * Called by: loadMoreGives()
+ * Calls: updateAllFeed()
+ *
+ * @chain
+ * loadMoreGives() -> debouncedUpdateFeed() -> updateAllFeed()
+ *
+ * @requires
+ * - this.isFeedLoading
+ */
+ private debouncedUpdateFeed = this.debounce(async () => {
+ if (!this.isFeedLoading) {
+ await this.updateAllFeed();
+ }
+ }, 300);
+
+ /**
+ * Creates a debounced function to prevent rapid successive calls
+ *
+ * @internal
+ * @callGraph
+ * Called by: debouncedUpdateFeed()
+ * Calls: None
+ *
+ * @chain
+ * debouncedUpdateFeed() -> debounce()
+ *
+ * @param func Function to debounce
+ * @param delay Delay in milliseconds
+ * @returns Debounced function
+ */
+ private debounce any>(
+ func: T,
+ delay: number
+ ): (...args: Parameters) => void {
+ let timeoutId: NodeJS.Timeout;
+ return (...args: Parameters) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func(...args), delay);
+ };
+ }
+
/**
* Checks if coordinates fall within any search box
*
@@ -921,18 +990,52 @@ export default class HomeView extends Vue {
* - this.feedLastViewedClaimId (via updateFeedLastViewedId)
*/
async updateAllFeed() {
+ const startTime = performance.now();
this.isFeedLoading = true;
let endOfResults = true;
try {
+ const apiStartTime = performance.now();
const results = await this.retrieveGives(
this.apiServer,
this.feedPreviousOldestId,
);
+ const apiTime = performance.now() - apiStartTime;
+
if (results.data.length > 0) {
endOfResults = false;
- // gather any contacts that user has blocked from view
- await this.processFeedResults(results.data);
+
+ // Check if we have cached data for these records
+ const uncachedRecords = this.filterUncachedRecords(results.data);
+
+ if (uncachedRecords.length > 0) {
+ // Process first 5 records immediately for quick display
+ const priorityRecords = uncachedRecords.slice(0, 5);
+ const remainingRecords = uncachedRecords.slice(5);
+
+ // Process priority records first
+ const processStartTime = performance.now();
+ await this.processPriorityRecords(priorityRecords);
+ const processTime = performance.now() - processStartTime;
+
+ // Process remaining records in background
+ if (remainingRecords.length > 0) {
+ this.processRemainingRecords(remainingRecords);
+ }
+
+ // Log performance metrics in development
+ if (process.env.NODE_ENV === 'development') {
+ logger.debug('[HomeView Performance]', {
+ apiTime: `${apiTime.toFixed(2)}ms`,
+ processTime: `${processTime.toFixed(2)}ms`,
+ priorityRecords: priorityRecords.length,
+ remainingRecords: remainingRecords.length,
+ totalRecords: results.data.length,
+ cacheHitRate: `${((results.data.length - uncachedRecords.length) / results.data.length * 100).toFixed(1)}%`
+ });
+ }
+ }
+
await this.updateFeedLastViewedId(results.data);
}
} catch (e) {
@@ -944,6 +1047,100 @@ export default class HomeView extends Vue {
}
this.isFeedLoading = false;
+ const totalTime = performance.now() - startTime;
+
+ // Log total performance in development
+ if (process.env.NODE_ENV === 'development') {
+ logger.debug('[HomeView Feed Update]', {
+ totalTime: `${totalTime.toFixed(2)}ms`,
+ feedDataLength: this.feedData.length
+ });
+ }
+ }
+
+ /**
+ * Processes priority records for immediate display
+ *
+ * @internal
+ * @callGraph
+ * Called by: updateAllFeed()
+ * Calls: processRecordWithCache()
+ *
+ * @chain
+ * updateAllFeed() -> processPriorityRecords()
+ *
+ * @param priorityRecords Array of records to process immediately
+ */
+ private async processPriorityRecords(priorityRecords: GiveSummaryRecord[]) {
+ // Fetch plans for priority records only
+ const planHandleIds = new Set();
+ priorityRecords.forEach(record => {
+ if (record.fulfillsPlanHandleId) {
+ planHandleIds.add(record.fulfillsPlanHandleId);
+ }
+ });
+
+ const planCache = new Map();
+ await this.batchFetchPlans(Array.from(planHandleIds), planCache);
+
+ // Process and display priority records immediately
+ for (const record of priorityRecords) {
+ const processedRecord = await this.processRecordWithCache(record, planCache, true);
+ if (processedRecord) {
+ await nextTick(() => {
+ this.feedData.push(processedRecord);
+ });
+ }
+ }
+ }
+
+ /**
+ * Processes remaining records in background
+ *
+ * @internal
+ * @callGraph
+ * Called by: updateAllFeed()
+ * Calls: processFeedResults()
+ *
+ * @chain
+ * updateAllFeed() -> processRemainingRecords()
+ *
+ * @param remainingRecords Array of records to process in background
+ */
+ private async processRemainingRecords(remainingRecords: GiveSummaryRecord[]) {
+ // Process remaining records without blocking the UI
+ this.isBackgroundProcessing = true;
+
+ // Use a longer delay to ensure InfiniteScroll doesn't trigger prematurely
+ setTimeout(async () => {
+ try {
+ await this.processFeedResults(remainingRecords);
+ } finally {
+ this.isBackgroundProcessing = false;
+ }
+ }, 500); // Increased delay to prevent conflicts with InfiniteScroll
+ }
+
+ /**
+ * Filters out records that are already cached to avoid re-processing
+ *
+ * @internal
+ * @callGraph
+ * Called by: updateAllFeed()
+ * Calls: None
+ *
+ * @chain
+ * updateAllFeed() -> filterUncachedRecords()
+ *
+ * @requires
+ * - this.feedData
+ *
+ * @param records Array of records to filter
+ * @returns Array of records not already in feed data
+ */
+ private filterUncachedRecords(records: GiveSummaryRecord[]): GiveSummaryRecord[] {
+ const existingJwtIds = new Set(this.feedData.map(record => record.jwtId));
+ return records.filter(record => !existingJwtIds.has(record.jwtId));
}
/**
@@ -968,23 +1165,158 @@ export default class HomeView extends Vue {
* @param records Array of feed records to process
*/
private async processFeedResults(records: GiveSummaryRecord[]) {
+ // Pre-fetch all required plans in batch to reduce API calls
+ const planHandleIds = new Set();
+ records.forEach(record => {
+ if (record.fulfillsPlanHandleId) {
+ planHandleIds.add(record.fulfillsPlanHandleId);
+ }
+ });
+
+ // Batch fetch all plans
+ const planCache = new Map();
+ await this.batchFetchPlans(Array.from(planHandleIds), planCache);
+
+ // Process and display records immediately as they're ready
const processedRecords: GiveRecordWithContactInfo[] = [];
for (const record of records) {
- const processedRecord = await this.processRecord(record);
+ const processedRecord = await this.processRecordWithCache(record, planCache);
if (processedRecord) {
processedRecords.push(processedRecord);
+
+ // Display records in batches of 3 for immediate visual feedback
+ if (processedRecords.length % 3 === 0) {
+ await nextTick(() => {
+ this.feedData.push(...processedRecords.slice(-3));
+ });
+ }
}
}
- // Batch update the feed data to reduce reactivity triggers
- await nextTick(() => {
- this.feedData.push(...processedRecords);
- });
+ // Add any remaining records
+ const remainingRecords = processedRecords.slice(Math.floor(processedRecords.length / 3) * 3);
+ if (remainingRecords.length > 0) {
+ await nextTick(() => {
+ this.feedData.push(...remainingRecords);
+ });
+ }
this.feedPreviousOldestId = records[records.length - 1].jwtId;
}
+ /**
+ * Batch fetches multiple plans to reduce API calls
+ *
+ * @internal
+ * @callGraph
+ * Called by: processFeedResults()
+ * Calls: getPlanFromCache()
+ *
+ * @chain
+ * processFeedResults() -> batchFetchPlans()
+ *
+ * @requires
+ * - this.axios
+ * - this.apiServer
+ * - this.activeDid
+ *
+ * @param planHandleIds Array of plan handle IDs to fetch
+ * @param planCache Map to store fetched plans
+ */
+ private async batchFetchPlans(
+ planHandleIds: string[],
+ planCache: Map
+ ) {
+ // Process plans in batches of 10 to avoid overwhelming the API
+ const batchSize = 10;
+ for (let i = 0; i < planHandleIds.length; i += batchSize) {
+ const batch = planHandleIds.slice(i, i + batchSize);
+ await Promise.all(
+ batch.map(async (handleId) => {
+ const plan = await getPlanFromCache(
+ handleId,
+ this.axios,
+ this.apiServer,
+ this.activeDid,
+ );
+ if (plan) {
+ planCache.set(handleId, plan);
+ }
+ })
+ );
+ }
+ }
+
+ /**
+ * Processes a single record with cached plans
+ *
+ * @internal
+ * @callGraph
+ * Called by: processFeedResults()
+ * Calls:
+ * - extractClaim()
+ * - extractGiverDid()
+ * - extractRecipientDid()
+ * - shouldIncludeRecord()
+ * - extractProvider()
+ * - createFeedRecord()
+ *
+ * @chain
+ * processFeedResults() -> processRecordWithCache()
+ *
+ * @requires
+ * - this.isAnyFeedFilterOn
+ * - this.isFeedFilteredByVisible
+ * - this.isFeedFilteredByNearby
+ * - this.activeDid
+ * - this.allContacts
+ *
+ * @param record The record to process
+ * @param planCache Map of cached plans
+ * @param isPriority Whether this is a priority record for quick display
+ * @returns Processed record with contact info if it passes filters, null otherwise
+ */
+ private async processRecordWithCache(
+ record: GiveSummaryRecord,
+ planCache: Map,
+ isPriority: boolean = false
+ ): Promise {
+ const claim = this.extractClaim(record);
+ const giverDid = this.extractGiverDid(claim);
+ const recipientDid = this.extractRecipientDid(claim);
+
+ // For priority records, skip expensive plan lookups initially
+ let fulfillsPlan: FulfillsPlan | undefined;
+ if (!isPriority || record.fulfillsPlanHandleId) {
+ fulfillsPlan = planCache.get(record.fulfillsPlanHandleId || '') ||
+ await this.getFulfillsPlan(record);
+ }
+
+ if (!this.shouldIncludeRecord(record, fulfillsPlan)) {
+ return null;
+ }
+
+ const provider = this.extractProvider(claim);
+ let providedByPlan: ProvidedByPlan | undefined;
+
+ // For priority records, defer provider plan lookup
+ if (!isPriority && provider?.identifier) {
+ providedByPlan = planCache.get(provider.identifier) ||
+ await this.getProvidedByPlan(provider);
+ }
+
+ return this.createFeedRecord(
+ record,
+ claim,
+ giverDid,
+ recipientDid,
+ provider,
+ fulfillsPlan,
+ providedByPlan,
+ );
+ }
+
/**
* Processes a single record and returns it if it passes filters
*
@@ -1153,34 +1485,35 @@ export default class HomeView extends Vue {
record: GiveSummaryRecord,
fulfillsPlan?: FulfillsPlan,
): boolean {
+ // Early exit for blocked contacts
if (this.blockedContactDids.includes(record.issuerDid)) {
return false;
}
+ // If no filters are active, include all records
if (!this.isAnyFeedFilterOn) {
return true;
}
- let anyMatch = false;
+ // Check visibility filter first (faster than location check)
if (this.isFeedFilteredByVisible && containsNonHiddenDid(record)) {
- anyMatch = true;
+ return true;
}
- if (
- !anyMatch &&
- this.isFeedFilteredByNearby &&
- record.fulfillsPlanHandleId
- ) {
+ // Check location filter only if needed and plan exists
+ if (this.isFeedFilteredByNearby && record.fulfillsPlanHandleId) {
if (fulfillsPlan?.locLat && fulfillsPlan?.locLon) {
- anyMatch =
- this.latLongInAnySearchBox(
- fulfillsPlan.locLat,
- fulfillsPlan.locLon,
- ) ?? false;
+ return this.latLongInAnySearchBox(
+ fulfillsPlan.locLat,
+ fulfillsPlan.locLon,
+ ) ?? false;
}
+ // If plan exists but no location data, exclude it
+ return false;
}
- return anyMatch;
+ // If we reach here, no filters matched
+ return false;
}
/**
@@ -1809,5 +2142,28 @@ export default class HomeView extends Vue {
get isUserRegistered() {
return this.isRegistered;
}
+
+ /**
+ * Debug method to verify debugging capabilities work with optimizations
+ *
+ * @public
+ * Called by: Debug testing
+ * @returns Debug information
+ */
+ debugOptimizations() {
+ // This method should be debuggable with breakpoints
+ const debugInfo = {
+ timestamp: new Date().toISOString(),
+ feedDataLength: this.feedData.length,
+ isFeedLoading: this.isFeedLoading,
+ activeDid: this.activeDid,
+ performance: performance.now()
+ };
+
+ console.log('🔍 Debug Info:', debugInfo);
+ debugger; // This should trigger breakpoint in dev tools
+
+ return debugInfo;
+ }
}