Skip to content

Commit 4d52b28

Browse files
Merge pull request #27 from hyphacoop/perf/remove-timeline-index
perf: use published index with in-memory timeline filtering
2 parents 539f1f0 + c64f43e commit 4d52b28

2 files changed

Lines changed: 80 additions & 43 deletions

File tree

db.js

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,13 @@ export class ActivityPubDB extends EventTarget {
253253
await tx.done()
254254
}
255255

256-
async * searchNotes ({ timeline, attributedTo, inReplyTo } = {}, { skip = 0, limit = DEFAULT_LIMIT, sort = -1 } = {}) {
256+
async * searchNotes ({ timeline, attributedTo, inReplyTo, excludeReplies } = {}, { skip = 0, limit = DEFAULT_LIMIT, sort = -1 } = {}) {
257257
const tx = this.db.transaction(NOTES_STORE, 'readonly')
258258
let indexName, keyRange
259259

260260
if (inReplyTo) {
261261
indexName = IN_REPLY_TO_FIELD
262262
keyRange = IDBKeyRange.only(inReplyTo)
263-
} else if (timeline) {
264-
indexName = 'timeline'
265-
keyRange = IDBKeyRange.only(timeline)
266263
} else if (attributedTo) {
267264
indexName = ATTRIBUTED_TO_FIELD
268265
keyRange = IDBKeyRange.only(attributedTo)
@@ -272,23 +269,68 @@ export class ActivityPubDB extends EventTarget {
272269
}
273270

274271
const index = tx.store.index(indexName)
275-
let cursor = await index.openCursor(keyRange)
276272

277-
const notes = []
278-
while (cursor) {
279-
notes.push(cursor.value)
280-
cursor = await cursor.continue()
281-
}
273+
// For random sort
274+
if (sort === 0) {
275+
const totalNotes = await index.count()
276+
let count = 0 // Add a count variable to keep track of successful yields
277+
278+
while (count < limit) { // Use while loop based on count to ensure we return the correct number of items
279+
const randomSkip = Math.floor(Math.random() * totalNotes)
280+
const cursor = await index.openCursor()
281+
if (!cursor) break // Avoid null cursor cases
282+
283+
// Advance the cursor by randomSkip
284+
if (randomSkip > 0) {
285+
await cursor.advance(randomSkip)
286+
}
287+
288+
const note = cursor.value
289+
290+
// Apply filtering logic
291+
if ((!excludeReplies || !note.inReplyTo) && (!timeline || (note.timeline && note.timeline.includes(timeline)))) {
292+
yield note
293+
count++ // Increment count only after a successful yield
294+
}
295+
}
296+
} else { // For regular sorting (newest/oldest)
297+
const direction = sort > 0 ? 'next' : 'prev'
298+
let cursor = await index.openCursor(keyRange, direction)
299+
300+
let skipped = 0
301+
let count = 0
302+
303+
// Process cursor in a loop to keep the transaction active
304+
while (cursor && count < limit) {
305+
const note = cursor.value
282306

283-
// Now sort notes by 'published' field
284-
notes.sort((a, b) => (sort > 0 ? 1 : -1) * (a.published - b.published))
307+
let includeNote = true
285308

286-
// Apply skip and limit
287-
const selectedNotes = notes.slice(skip, skip + limit)
288-
for (const note of selectedNotes) {
289-
yield note
309+
// Filter by timeline
310+
if (timeline && (!note.timeline || !note.timeline.includes(timeline))) {
311+
includeNote = false
312+
}
313+
// Exclude replies if required
314+
if (excludeReplies && note.inReplyTo) {
315+
includeNote = false
316+
}
317+
318+
// If the note matches the filter criteria, yield it
319+
if (includeNote) {
320+
if (skipped < skip) {
321+
skipped++
322+
} else {
323+
yield note
324+
count++
325+
}
326+
}
327+
328+
// Move to the next cursor
329+
cursor = await cursor.continue()
330+
}
290331
}
291332

333+
// Ensure the transaction completes
292334
await tx.done
293335
}
294336

@@ -300,12 +342,22 @@ export class ActivityPubDB extends EventTarget {
300342
// Add 'following' to timeline if the actor is followed
301343
const isFollowing = await this.isActorFollowed(url)
302344
if (isFollowing) {
303-
for await (const note of this.searchNotes({ attributedTo: actor.id })) {
345+
const tx = this.db.transaction(NOTES_STORE, 'readwrite')
346+
const store = tx.objectStore(NOTES_STORE)
347+
const index = store.index(ATTRIBUTED_TO_FIELD)
348+
const keyRange = IDBKeyRange.only(actor.id)
349+
let cursor = await index.openCursor(keyRange)
350+
351+
while (cursor) {
352+
const note = cursor.value
304353
if (!note.timeline.includes(TIMELINE_FOLLOWING)) {
305354
note.timeline.push(TIMELINE_FOLLOWING)
306-
await this.db.put(NOTES_STORE, note)
355+
cursor.update(note)
307356
}
357+
cursor = await cursor.continue()
308358
}
359+
360+
await tx.done
309361
}
310362

311363
// If actor has an 'outbox', ingest it as a collection
@@ -314,9 +366,6 @@ export class ActivityPubDB extends EventTarget {
314366
} else {
315367
console.error(`No outbox found for actor at URL ${url}`)
316368
}
317-
318-
// This is where we might add more features to our actor ingestion process.
319-
// e.g., if (actor.followers) { ... }
320369
}
321370

322371
async ingestActivityCollection (collectionOrUrl, actorId, isInitial = false) {

timeline.js

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class ReaderTimeline extends HTMLElement {
2626

2727
async connectedCallback () {
2828
this.initializeSortOrder()
29-
this.initializeDefaultFollowedActors().then(() => this.initTimeline())
29+
await this.initializeDefaultFollowedActors()
30+
await this.initTimeline()
3031
}
3132

3233
initializeSortOrder () {
@@ -58,7 +59,7 @@ class ReaderTimeline extends HTMLElement {
5859
while (this.firstChild) {
5960
this.removeChild(this.firstChild)
6061
}
61-
this.loadMore()
62+
await this.loadMore()
6263
}
6364

6465
async initializeDefaultFollowedActors () {
@@ -78,17 +79,10 @@ class ReaderTimeline extends HTMLElement {
7879
async initTimeline () {
7980
if (!hasLoaded) {
8081
hasLoaded = true
81-
82-
const followedActors = await db.getFollowedActors()
83-
84-
// Ensure all followed actors are ingested before loading notes.
85-
await Promise.all(followedActors.map(({ url }) => db.ingestActor(url)))
86-
console.log('All followed actors have been ingested')
87-
88-
// Load the timeline notes after ingestion.
89-
this.resetTimeline()
82+
// No need to re-ingest actors; they've been ingested in initializeDefaultFollowedActors()
83+
await this.resetTimeline()
9084
} else {
91-
this.loadMore() // Start loading notes immediately if already loaded.
85+
await this.loadMore()
9286
}
9387
}
9488

@@ -99,20 +93,14 @@ class ReaderTimeline extends HTMLElement {
9993
const sortValue = this.sort === 'random' ? 0 : (this.sort === 'oldest' ? 1 : -1)
10094

10195
// Fetch notes and render them as they become available
102-
let notesFound = false
103-
for await (const note of db.searchNotes({ timeline: 'following' }, { skip: this.skip, limit: this.limit, sort: sortValue })) {
104-
notesFound = true
96+
for await (const note of db.searchNotes({ timeline: 'following', excludeReplies: true }, { skip: this.skip, limit: this.limit, sort: sortValue })) {
10597
console.log('Loading note:', note)
106-
107-
// Exclude replies from appearing in the timeline
108-
if (!note.inReplyTo) {
109-
this.appendNoteElement(note)
110-
count++
111-
}
98+
this.appendNoteElement(note)
99+
count++
112100
}
113101

114102
this.updateHasMore(count, sortValue)
115-
this.appendLoadMoreIfNeeded() // Ensure this is called even if no notes are found
103+
this.appendLoadMoreIfNeeded()
116104
}
117105

118106
updateHasMore (count, sortValue) {

0 commit comments

Comments
 (0)