Page MenuHomePhabricator

D14140.id46361.diff
No OneTemporary

D14140.id46361.diff

diff --git a/lib/utils/chat-thread-item-loader-cache.js b/lib/utils/chat-thread-item-loader-cache.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/chat-thread-item-loader-cache.js
@@ -0,0 +1,258 @@
+// @flow
+
+import invariant from 'invariant';
+
+type ChatThreadPointer =
+ | {
+ +resolved: true,
+ +threadID: string,
+ +lastUpdatedTime: number,
+ }
+ | {
+ +resolved: false,
+ +threadID: string,
+ +lastUpdatedAtLeastTime: number,
+ +lastUpdatedAtMostTime: number,
+ };
+
+function sortFuncForAtLeastTime(pointer: ChatThreadPointer): number {
+ return pointer.resolved
+ ? pointer.lastUpdatedTime
+ : pointer.lastUpdatedAtLeastTime;
+}
+
+function sortFuncForAtMostTime(pointer: ChatThreadPointer): number {
+ return pointer.resolved
+ ? pointer.lastUpdatedTime
+ : pointer.lastUpdatedAtMostTime;
+}
+
+function insertIntoSortedDescendingArray(
+ pointer: ChatThreadPointer,
+ sortedArray: Array<ChatThreadPointer>,
+ sortFunc: ChatThreadPointer => number,
+) {
+ let i = 0;
+ for (; i < sortedArray.length; i++) {
+ const itemAtIndex = sortedArray[i];
+ if (sortFunc(pointer) > sortFunc(itemAtIndex)) {
+ break;
+ }
+ }
+ sortedArray.splice(i, 0, pointer);
+}
+
+type BaseChatThreadItem = {
+ +lastUpdatedTimeIncludingSidebars: number,
+ ...
+};
+type BaseChatThreadItemLoader<T: BaseChatThreadItem> = {
+ +threadInfo: {
+ +id: string,
+ ...
+ },
+ +lastUpdatedAtLeastTimeIncludingSidebars: number,
+ +lastUpdatedAtMostTimeIncludingSidebars: number,
+ +initialChatThreadItem: T,
+ +getFinalChatThreadItem: () => Promise<T>,
+};
+
+class ChatThreadItemLoaderCache<
+ Item: BaseChatThreadItem,
+ Loader: BaseChatThreadItemLoader<Item> = BaseChatThreadItemLoader<Item>,
+> {
+ chatThreadItemLoaders: $ReadOnlyMap<string, Loader>;
+ resolvedChatThreadItems: Map<string, Item | Promise<Item>> = new Map();
+ currentState: {
+ topNProcessed: number,
+ pointerListOrderedByAtLeastTime: $ReadOnlyArray<ChatThreadPointer>,
+ pointerListOrderedByAtMostTime: $ReadOnlyArray<ChatThreadPointer>,
+ };
+ loadingState: ?{
+ topNProcessed: number,
+ pointerListOrderedByAtLeastTime: Promise<$ReadOnlyArray<ChatThreadPointer>>,
+ pointerListOrderedByAtMostTime: Promise<$ReadOnlyArray<ChatThreadPointer>>,
+ };
+
+ constructor(loaders: $ReadOnlyArray<Loader>) {
+ const chatThreadItemLoaders = new Map<string, Loader>();
+ for (const loader of loaders) {
+ chatThreadItemLoaders.set(loader.threadInfo.id, loader);
+ }
+ this.chatThreadItemLoaders = chatThreadItemLoaders;
+ this.currentState = {
+ topNProcessed: 0,
+ ...this.updateCurrentState(),
+ };
+ }
+
+ updateCurrentState(): {
+ +pointerListOrderedByAtLeastTime: $ReadOnlyArray<ChatThreadPointer>,
+ +pointerListOrderedByAtMostTime: $ReadOnlyArray<ChatThreadPointer>,
+ } {
+ const pointerListOrderedByAtLeastTime: Array<ChatThreadPointer> = [];
+ const pointerListOrderedByAtMostTime: Array<ChatThreadPointer> = [];
+ const loaders = this.chatThreadItemLoaders;
+ for (const threadID of loaders.keys()) {
+ let chatThreadPointer;
+ const resolved = this.resolvedChatThreadItems.get(threadID);
+ if (resolved && !(resolved instanceof Promise)) {
+ chatThreadPointer = {
+ resolved: true,
+ threadID,
+ lastUpdatedTime: resolved.lastUpdatedTimeIncludingSidebars,
+ };
+ } else {
+ const loader = loaders.get(threadID);
+ invariant(loader, 'loader should be set during keys() iteration');
+ chatThreadPointer = {
+ resolved: false,
+ threadID,
+ lastUpdatedAtLeastTime:
+ loader.lastUpdatedAtLeastTimeIncludingSidebars,
+ lastUpdatedAtMostTime: loader.lastUpdatedAtMostTimeIncludingSidebars,
+ };
+ }
+ insertIntoSortedDescendingArray(
+ chatThreadPointer,
+ pointerListOrderedByAtLeastTime,
+ sortFuncForAtLeastTime,
+ );
+ insertIntoSortedDescendingArray(
+ chatThreadPointer,
+ pointerListOrderedByAtMostTime,
+ sortFuncForAtMostTime,
+ );
+ }
+ return { pointerListOrderedByAtLeastTime, pointerListOrderedByAtMostTime };
+ }
+
+ getChatThreadItemForThreadID(threadID: string): Item {
+ const resolved = this.resolvedChatThreadItems.get(threadID);
+ if (resolved && !(resolved instanceof Promise)) {
+ return resolved;
+ }
+ const loader = this.chatThreadItemLoaders.get(threadID);
+ invariant(loader, `loader should exist for threadID ${threadID}`);
+ return loader.initialChatThreadItem;
+ }
+
+ getAllChatThreadItems(): Array<Item> {
+ return this.currentState.pointerListOrderedByAtLeastTime.map(pointer =>
+ this.getChatThreadItemForThreadID(pointer.threadID),
+ );
+ }
+
+ loadMostRecent(n: number): Promise<$ReadOnlyArray<ChatThreadPointer>> {
+ if (this.currentState.topNProcessed >= n) {
+ return Promise.resolve(this.currentState.pointerListOrderedByAtMostTime);
+ } else if (this.loadingState && this.loadingState.topNProcessed >= n) {
+ return this.loadingState.pointerListOrderedByAtMostTime;
+ }
+
+ const getPromise = async () => {
+ let pointerList = this.currentState.pointerListOrderedByAtMostTime;
+
+ while (true) {
+ // First, resolve the first n items
+ const pointerPromises: Array<mixed | Promise<mixed>> = pointerList.map(
+ (pointer, i): mixed | Promise<mixed> => {
+ if (i >= n || pointer.resolved) {
+ return undefined;
+ }
+ const { threadID } = pointer;
+
+ const resolved = this.resolvedChatThreadItems.get(threadID);
+ if (resolved && resolved instanceof Promise) {
+ return resolved;
+ } else if (resolved) {
+ return undefined;
+ }
+
+ const loader = this.chatThreadItemLoaders.get(threadID);
+ invariant(loader, `loader should exist for threadID ${threadID}`);
+ const promise = (async () => {
+ const finalChatThreadItemPromise =
+ loader.getFinalChatThreadItem();
+ this.resolvedChatThreadItems.set(
+ threadID,
+ finalChatThreadItemPromise,
+ );
+ const finalChatThreadItem = await finalChatThreadItemPromise;
+ this.resolvedChatThreadItems.set(threadID, finalChatThreadItem);
+ })();
+ return promise;
+ },
+ );
+ await Promise.all(pointerPromises);
+
+ // Next, reorder them
+ const {
+ pointerListOrderedByAtLeastTime,
+ pointerListOrderedByAtMostTime,
+ } = this.updateCurrentState();
+
+ // Decide if we need to continue
+ let firstNItemsResolved = true;
+ const numItems = Math.min(n, pointerListOrderedByAtMostTime.length);
+ for (let i = 0; i < numItems; i++) {
+ const pointer = pointerListOrderedByAtMostTime[i];
+ if (!pointer.resolved) {
+ firstNItemsResolved = false;
+ break;
+ }
+ }
+ if (
+ firstNItemsResolved &&
+ this.loadingState &&
+ n === this.loadingState.topNProcessed
+ ) {
+ this.currentState = {
+ topNProcessed: n,
+ pointerListOrderedByAtLeastTime,
+ pointerListOrderedByAtMostTime,
+ };
+ this.loadingState = null;
+ break;
+ }
+
+ pointerList = pointerListOrderedByAtMostTime;
+ }
+ };
+
+ let waitForOngoingThenFetchPromise;
+ if (!this.loadingState) {
+ waitForOngoingThenFetchPromise = getPromise();
+ } else {
+ const oldPromise = this.loadingState.pointerListOrderedByAtMostTime;
+ waitForOngoingThenFetchPromise = (async () => {
+ await oldPromise;
+ await getPromise();
+ })();
+ }
+
+ this.loadingState = {
+ topNProcessed: n,
+ pointerListOrderedByAtLeastTime: (async () => {
+ await waitForOngoingThenFetchPromise;
+ return this.currentState.pointerListOrderedByAtLeastTime;
+ })(),
+ pointerListOrderedByAtMostTime: (async () => {
+ await waitForOngoingThenFetchPromise;
+ return this.currentState.pointerListOrderedByAtMostTime;
+ })(),
+ };
+ return this.loadingState.pointerListOrderedByAtMostTime;
+ }
+
+ async loadMostRecentChatThreadItems(
+ n: number,
+ ): Promise<$ReadOnlyArray<Item>> {
+ await this.loadMostRecent(n);
+ return this.currentState.pointerListOrderedByAtMostTime.map(pointer =>
+ this.getChatThreadItemForThreadID(pointer.threadID),
+ );
+ }
+}
+
+export default ChatThreadItemLoaderCache;
diff --git a/lib/utils/chat-thread-item-loader-cache.test.js b/lib/utils/chat-thread-item-loader-cache.test.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/chat-thread-item-loader-cache.test.js
@@ -0,0 +1,121 @@
+// @flow
+
+import ChatThreadItemLoaderCache from './chat-thread-item-loader-cache.js';
+
+type MinChatThreadItem = {
+ +id: string,
+ +lastUpdatedTimeIncludingSidebars: number,
+};
+
+let cache = new ChatThreadItemLoaderCache<MinChatThreadItem>([]);
+const resetCache = () => {
+ cache = new ChatThreadItemLoaderCache<MinChatThreadItem>([
+ {
+ threadInfo: {
+ id: '0',
+ },
+ lastUpdatedAtLeastTimeIncludingSidebars: 500,
+ lastUpdatedAtMostTimeIncludingSidebars: 500,
+ initialChatThreadItem: {
+ id: '0',
+ lastUpdatedTimeIncludingSidebars: 500,
+ },
+ getFinalChatThreadItem: async () => ({
+ id: '0',
+ lastUpdatedTimeIncludingSidebars: 500,
+ }),
+ },
+ {
+ threadInfo: {
+ id: '1',
+ },
+ lastUpdatedAtLeastTimeIncludingSidebars: 475,
+ lastUpdatedAtMostTimeIncludingSidebars: 525,
+ initialChatThreadItem: {
+ id: '1',
+ lastUpdatedTimeIncludingSidebars: 475,
+ },
+ getFinalChatThreadItem: async () => ({
+ id: '1',
+ lastUpdatedTimeIncludingSidebars: 525,
+ }),
+ },
+ {
+ threadInfo: {
+ id: '3',
+ },
+ lastUpdatedAtLeastTimeIncludingSidebars: 450,
+ lastUpdatedAtMostTimeIncludingSidebars: 450,
+ initialChatThreadItem: {
+ id: '3',
+ lastUpdatedTimeIncludingSidebars: 450,
+ },
+ getFinalChatThreadItem: async () => ({
+ id: '3',
+ lastUpdatedTimeIncludingSidebars: 450,
+ }),
+ },
+ {
+ threadInfo: {
+ id: '4',
+ },
+ lastUpdatedAtLeastTimeIncludingSidebars: 425,
+ lastUpdatedAtMostTimeIncludingSidebars: 480,
+ initialChatThreadItem: {
+ id: '4',
+ lastUpdatedTimeIncludingSidebars: 425,
+ },
+ getFinalChatThreadItem: async () => ({
+ id: '4',
+ lastUpdatedTimeIncludingSidebars: 480,
+ }),
+ },
+ ]);
+};
+
+describe('getAllChatThreadItems', () => {
+ beforeAll(() => {
+ resetCache();
+ });
+ it('returns initial ordering', () => {
+ const items = cache.getAllChatThreadItems();
+ expect(items[0].id === '0');
+ expect(items[1].id === '1');
+ expect(items[2].id === '2');
+ expect(items[3].id === '3');
+ });
+});
+
+describe('loadMostRecentChatThreadItems', () => {
+ beforeAll(() => {
+ resetCache();
+ });
+ it('reorders items 0 and 1', async () => {
+ const items = await cache.loadMostRecentChatThreadItems(2);
+ expect(items[0].id === '1');
+ expect(items[1].id === '0');
+ expect(items[2].id === '2');
+ expect(items[3].id === '3');
+ });
+ it('reorders all items', async () => {
+ const items = await cache.loadMostRecentChatThreadItems(4);
+ expect(items[0].id === '1');
+ expect(items[1].id === '0');
+ expect(items[2].id === '3');
+ expect(items[3].id === '2');
+ });
+ it('calls loadMostRecentChatThreadItems twice in a row', async () => {
+ void (async () => {
+ const firstItems = await cache.loadMostRecentChatThreadItems(2);
+ expect(firstItems[0].id === '1');
+ expect(firstItems[1].id === '0');
+ expect(firstItems[2].id === '3');
+ expect(firstItems[3].id === '2');
+ })();
+ const items = await cache.loadMostRecentChatThreadItems(4);
+ expect(items[0].id === '1');
+ expect(items[1].id === '0');
+ expect(items[2].id === '3');
+ expect(items[3].id === '2');
+ });
+});

File Metadata

Mime Type
text/plain
Expires
Sun, Dec 22, 9:29 AM (5 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2678496
Default Alt Text
D14140.id46361.diff (12 KB)

Event Timeline