UNPKG

@atlaskit/mention

Version:

A React component used to display user profiles in a list for 'Mention' functionality

180 lines (175 loc) 6.27 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import { MentionNameStatus } from '../types'; import { fireAnalyticsMentionHydrationEvent } from '../util/analytics'; /** A queue for user ids */ export class DefaultMentionNameResolver { constructor(client, analyticsProps = {}, onResolvedAll = () => {}) { _defineProperty(this, "nameCache", new Map()); _defineProperty(this, "nameQueue", new Map()); _defineProperty(this, "nameStartTime", new Map()); _defineProperty(this, "processingQueue", new Map()); _defineProperty(this, "debounce", 0); _defineProperty(this, "debounceOnResolve", null); _defineProperty(this, "isOnResolvedAllCalled", false); _defineProperty(this, "processQueue", () => { clearTimeout(this.debounce); this.debounce = 0; const { queue, extraQueue } = this.splitQueueAtLimit(); this.nameQueue = extraQueue; this.processingQueue = mergeNameResolverQueues(this.processingQueue, queue); this.client.lookupMentionNames(Array.from(queue.keys())).then(response => { response.forEach(mentionDetail => { const { id } = mentionDetail; queue.delete(id); this.resolveQueueItem(mentionDetail); }); queue.forEach((_callback, id) => { // No response from client for these ids treat as unknown this.resolveQueueItem({ id, status: MentionNameStatus.UNKNOWN }); }); }).catch(() => { // Service completely failed, reject all items in the queue queue.forEach((_callback, id) => { this.resolveQueueItem({ id, status: MentionNameStatus.SERVICE_ERROR }); }); }); // Make sure anything left in the queue gets processed. if (this.nameQueue.size > 0) { this.scheduleProcessQueue(); } else { this.scheduleOnAllResolved(); } }); this.client = client; this.fireHydrationEvent = fireAnalyticsMentionHydrationEvent(analyticsProps); // If provided, this will be called once all pending mentions in the queue are resolved. // A sample usage is scrolling to a mention on page load, after the mentions have loadad. this.onResolvedAll = onResolvedAll; } lookupName(id) { const name = this.nameCache.get(id); if (name) { this.fireAnalytics(true, name); if (this.nameQueue.size === 0) { this.scheduleOnAllResolved(); } return name; } return new Promise(resolve => { const processingItems = this.processingQueue.get(id); if (processingItems) { this.processingQueue.set(id, [...processingItems, resolve]); } const queuedItems = this.nameQueue.get(id) || []; this.nameQueue.set(id, [...queuedItems, resolve]); if (queuedItems.length === 0 && !processingItems) { this.nameStartTime.set(id, Date.now()); } this.scheduleProcessQueue(); if (this.isQueueAtLimit()) { this.processQueue(); } }); } cacheName(id, name) { this.nameCache.set(id, { id, name, status: MentionNameStatus.OK }); } scheduleProcessQueue() { if (!this.debounce) { this.debounce = window.setTimeout(this.processQueue, DefaultMentionNameResolver.waitForBatch); } } scheduleOnAllResolved() { if (this.debounceOnResolve) { clearTimeout(this.debounceOnResolve); } this.debounceOnResolve = window.setTimeout(() => { if (this.isOnResolvedAllCalled) { return; } this.onResolvedAll(); this.isOnResolvedAllCalled = true; }, DefaultMentionNameResolver.waitForResolveAll); } isQueueAtLimit() { return this.nameQueue.size >= this.client.getLookupLimit(); } splitQueueAtLimit() { const values = Array.from(this.nameQueue.entries()); const splitPoint = this.client.getLookupLimit(); return { queue: new Map(values.slice(0, splitPoint)), extraQueue: new Map(values.slice(splitPoint)) }; } resolveQueueItem(mentionDetail) { const { id } = mentionDetail; const resolvers = this.processingQueue.get(id); if (resolvers) { this.processingQueue.delete(id); this.nameCache.set(id, mentionDetail); resolvers.forEach(resolve => { try { resolve(mentionDetail); } catch { // ignore - exception in consumer } }); this.fireAnalytics(false, mentionDetail); } } fireAnalytics(fromCache, mentionDetail) { const { id } = mentionDetail; const action = mentionDetail.status === MentionNameStatus.OK ? 'completed' : 'failed'; const start = this.nameStartTime.get(id); const duration = start ? Date.now() - start : 0; this.nameStartTime.delete(id); this.fireHydrationEvent(action, id, fromCache, duration); } } /** * Merge the two queues making sure to merge callback arrays for items in queueB already in queueA. * This addresses [this ticket](https://product-fabric.atlassian.net/browse/QS-3789). */ _defineProperty(DefaultMentionNameResolver, "waitForBatch", 100); // ms _defineProperty(DefaultMentionNameResolver, "waitForResolveAll", 800); export function mergeNameResolverQueues(queueA, queueB) { const queueBeingMerged = new Map([...queueA]); // now add the items from the second queue that are not already in the // merged queue being built [...queueB].forEach(item => { const [key, queueBCallbacks] = item; const itemAlreadyInMergedQueue = queueBeingMerged.has(key); if (!itemAlreadyInMergedQueue) { queueBeingMerged.set(key, queueBCallbacks); } else { var _queueBeingMerged$get; // item already in merged queue, merge the callback arrays const queueACallbacks = (_queueBeingMerged$get = queueBeingMerged.get(key)) !== null && _queueBeingMerged$get !== void 0 ? _queueBeingMerged$get : []; const mergedCallbacks = new Set([...queueBCallbacks, ...queueACallbacks]); const deduplicatedCallbacks = Array.from(mergedCallbacks.values()); // prevents calling them twice queueBeingMerged.set(key, deduplicatedCallbacks); } }); return queueBeingMerged; }