-
Notifications
You must be signed in to change notification settings - Fork 175
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf: lazy-load the thread context (#1774)
* perf: lazy-load the thread context fixes #898 * more tests * test: more tests * simplify implementation
- Loading branch information
1 parent
9e09ba6
commit 836b0e3
Showing
8 changed files
with
511 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// utilities for working with Maps | ||
|
||
export function mapBy (items, func) { | ||
const map = new Map() | ||
for (const item of items) { | ||
map.set(func(item), item) | ||
} | ||
return map | ||
} | ||
|
||
export function multimapBy (items, func) { | ||
const map = new Map() | ||
for (const item of items) { | ||
const key = func(item) | ||
if (map.has(key)) { | ||
map.get(key).push(item) | ||
} else { | ||
map.set(key, [item]) | ||
} | ||
} | ||
return map | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// This is designed to exactly mimic Mastodon's ordering for threads. As described by Gargron: | ||
// "statuses are ordered in the postgresql query and then any of OP's self-replies bubble to the top" | ||
// Source: https://github.com/tootsuite/mastodon/blob/ef15246/app/models/concerns/status_threading_concern.rb | ||
import { concat } from './arrays' | ||
import { compareTimelineItemSummaries } from './statusIdSorting' | ||
import { mapBy, multimapBy } from './maps' | ||
|
||
export function sortItemSummariesForThread (summaries, statusId) { | ||
const ancestors = [] | ||
const descendants = [] | ||
const summariesById = mapBy(summaries, _ => _.id) | ||
const summariesByReplyId = multimapBy(summaries, _ => _.replyId) | ||
|
||
const status = summariesById.get(statusId) | ||
if (!status) { | ||
// bail out, for some reason we can't find the status (should never happen) | ||
return summaries | ||
} | ||
|
||
// find ancestors | ||
let currentStatus = status | ||
do { | ||
currentStatus = summariesById.get(currentStatus.replyId) | ||
if (currentStatus) { | ||
ancestors.unshift(currentStatus) | ||
} | ||
} while (currentStatus) | ||
|
||
// find descendants | ||
// This mirrors the depth-first ordering used in the Postgres query in the Mastodon implementation | ||
const stack = [status] | ||
while (stack.length) { | ||
const current = stack.shift() | ||
const newChildren = (summariesByReplyId.get(current.id) || []).sort(compareTimelineItemSummaries) | ||
Array.prototype.unshift.apply(stack, newChildren) | ||
if (current.id !== status.id) { // the status is not a descendant of itself | ||
descendants.push(current) | ||
} | ||
} | ||
|
||
// Normally descendants are sorted in depth-first order, via normal ID sorting | ||
// but replies that come from the account they're actually replying to get promoted | ||
// This only counts if it's an unbroken self-reply, e.g. in the case of | ||
// A -> A -> A -> B -> A -> A | ||
// B has broken the chain, so only the first three As are considered unbroken self-replies | ||
const isUnbrokenSelfReply = (descendant) => { | ||
let current = descendant | ||
while (true) { | ||
if (current.accountId !== status.accountId) { | ||
return false | ||
} | ||
const parent = summariesById.get(current.replyId) | ||
if (!parent) { | ||
break | ||
} | ||
current = parent | ||
} | ||
return current.id === statusId | ||
} | ||
|
||
const promotedDescendants = [] | ||
const otherDescendants = [] | ||
for (const descendant of descendants) { | ||
(isUnbrokenSelfReply(descendant) ? promotedDescendants : otherDescendants).push(descendant) | ||
} | ||
|
||
return concat( | ||
ancestors, | ||
[status], | ||
promotedDescendants, | ||
otherDescendants | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.