-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
218 additions
and
3 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,110 @@ | ||
import { atom, onMount } from 'nanostores' | ||
|
||
import { | ||
BROKEN_CATEGORY, | ||
type CategoryValue, | ||
GENERAL_CATEGORY, | ||
loadCategories | ||
} from './category.js' | ||
import { client } from './client.js' | ||
import { BROKEN_FEED, type FeedValue, loadFeed } from './feed.js' | ||
import { loadPosts } from './post.js' | ||
import { readonlyExport } from './utils/stores.js' | ||
|
||
export type SlowCategoriesTree = [CategoryValue, [FeedValue, number][]][] | ||
|
||
export type SlowCategoriesValue = | ||
| { | ||
isLoading: false | ||
tree: SlowCategoriesTree | ||
} | ||
| { isLoading: true } | ||
|
||
let $categories = atom<SlowCategoriesValue>({ isLoading: true }) | ||
|
||
async function findSlowCategories(): Promise<SlowCategoriesTree> { | ||
let [posts, categories] = await Promise.all([ | ||
loadPosts({ reading: 'slow' }), | ||
loadCategories() | ||
]) | ||
|
||
let general: [FeedValue, number][] = [] | ||
let byCategory: Record<string, [FeedValue, number][]> = {} | ||
let broken: [FeedValue, number][] = [] | ||
|
||
let postsByFeed: Record<string, number> = {} | ||
for (let post of posts) { | ||
postsByFeed[post.feedId] = (postsByFeed[post.feedId] ?? 0) + 1 | ||
} | ||
|
||
await Promise.all( | ||
Object.entries(postsByFeed).map(async ([feedId, unread]) => { | ||
let feed = (await loadFeed(feedId)) ?? BROKEN_FEED | ||
let category = feed.categoryId | ||
if (feed.categoryId === 'general') { | ||
general.push([feed, unread]) | ||
} | ||
if (category === 'general' || categories.find(i => i.id === category)) { | ||
let list = byCategory[category] ?? (byCategory[category] = []) | ||
list.push([feed, unread]) | ||
} else { | ||
broken.push([feed, unread]) | ||
} | ||
}) | ||
) | ||
|
||
let categoriesByName = categories.sort((a, b) => { | ||
return a.title.localeCompare(b.title) | ||
}) | ||
|
||
let result: SlowCategoriesTree = [] | ||
if (general.length > 0) { | ||
result.push([GENERAL_CATEGORY, general]) | ||
} | ||
for (let category of categoriesByName) { | ||
let list = byCategory[category.id] | ||
if (list) { | ||
result.push([category, list]) | ||
} | ||
} | ||
if (broken.length > 0) { | ||
result.push([BROKEN_CATEGORY, broken]) | ||
} | ||
|
||
return result | ||
} | ||
|
||
onMount($categories, () => { | ||
$categories.set({ isLoading: true }) | ||
|
||
let unbindLog: (() => void) | undefined | ||
let unbindClient = client.subscribe(loguxClient => { | ||
unbindLog?.() | ||
unbindLog = undefined | ||
|
||
if (loguxClient) { | ||
findSlowCategories().then(tree => { | ||
$categories.set({ isLoading: false, tree }) | ||
}) | ||
|
||
unbindLog = loguxClient.log.on('add', action => { | ||
if ( | ||
action.type.startsWith('categories/') || | ||
action.type.startsWith('feeds/') || | ||
action.type.startsWith('posts/') | ||
) { | ||
findSlowCategories().then(tree => { | ||
$categories.set({ isLoading: false, tree }) | ||
}) | ||
} | ||
}) | ||
} | ||
}) | ||
|
||
return () => { | ||
unbindLog?.() | ||
unbindClient() | ||
} | ||
}) | ||
|
||
export const slowCategories = readonlyExport($categories) |
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,104 @@ | ||
import { cleanStores, keepMount } from 'nanostores' | ||
import { deepStrictEqual } from 'node:assert' | ||
import { afterEach, beforeEach, test } from 'node:test' | ||
import { setTimeout } from 'node:timers/promises' | ||
|
||
import { | ||
addCategory, | ||
addFeed, | ||
addPost, | ||
BROKEN_CATEGORY, | ||
BROKEN_FEED, | ||
changeCategory, | ||
deletePost, | ||
GENERAL_CATEGORY, | ||
loadCategory, | ||
loadFeed, | ||
slowCategories, | ||
testFeed, | ||
testPost | ||
} from '../index.js' | ||
import { cleanClientTest, enableClientTest } from './utils.js' | ||
|
||
beforeEach(() => { | ||
enableClientTest() | ||
}) | ||
|
||
afterEach(async () => { | ||
cleanStores(slowCategories) | ||
await setTimeout(10) | ||
await cleanClientTest() | ||
}) | ||
|
||
test('has empty slow categories from beginning', async () => { | ||
deepStrictEqual(slowCategories.get(), { isLoading: true }) | ||
await setTimeout(100) | ||
deepStrictEqual(slowCategories.get(), { | ||
isLoading: false, | ||
tree: [] | ||
}) | ||
}) | ||
|
||
test('returns slow feeds', async () => { | ||
keepMount(slowCategories) | ||
let categoryB = await addCategory({ title: 'B' }) | ||
let categoryA = await addCategory({ title: 'A' }) | ||
let feedA1 = await addFeed(testFeed({ categoryId: categoryA })) | ||
let feedA2 = await addFeed(testFeed({ categoryId: categoryA })) | ||
let feedA3 = await addFeed(testFeed({ categoryId: categoryA })) | ||
let feedB = await addFeed(testFeed({ categoryId: categoryB })) | ||
let feedC = await addFeed(testFeed()) | ||
let feedD = await addFeed(testFeed({ categoryId: 'unknown' })) | ||
await addPost(testPost({ feedId: feedA1, reading: 'slow' })) | ||
await addPost(testPost({ feedId: feedA1, reading: 'slow' })) | ||
await addPost(testPost({ feedId: feedA1, reading: 'fast' })) | ||
await addPost(testPost({ feedId: feedA2, reading: 'slow' })) | ||
await addPost(testPost({ feedId: feedA3, reading: 'fast' })) | ||
await addPost(testPost({ feedId: feedB, reading: 'slow' })) | ||
await addPost(testPost({ feedId: feedC, reading: 'slow' })) | ||
await addPost(testPost({ feedId: feedD, reading: 'slow' })) | ||
let post9 = await addPost(testPost({ feedId: 'unknown', reading: 'slow' })) | ||
|
||
await setTimeout(10) | ||
deepStrictEqual(slowCategories.get(), { | ||
isLoading: false, | ||
tree: [ | ||
[ | ||
GENERAL_CATEGORY, | ||
[ | ||
[await loadFeed(feedC), 1], | ||
[BROKEN_FEED, 1] | ||
] | ||
], | ||
[ | ||
await loadCategory(categoryA), | ||
[ | ||
[await loadFeed(feedA1), 2], | ||
[await loadFeed(feedA2), 1] | ||
] | ||
], | ||
[await loadCategory(categoryB), [[await loadFeed(feedB), 1]]], | ||
[BROKEN_CATEGORY, [[await loadFeed(feedD), 1]]] | ||
] | ||
}) | ||
|
||
await changeCategory(categoryA, { title: 'New A' }) | ||
await deletePost(post9) | ||
|
||
await setTimeout(10) | ||
deepStrictEqual(slowCategories.get(), { | ||
isLoading: false, | ||
tree: [ | ||
[GENERAL_CATEGORY, [[await loadFeed(feedC), 1]]], | ||
[await loadCategory(categoryB), [[await loadFeed(feedB), 1]]], | ||
[ | ||
await loadCategory(categoryA), | ||
[ | ||
[await loadFeed(feedA1), 2], | ||
[await loadFeed(feedA2), 1] | ||
] | ||
], | ||
[BROKEN_CATEGORY, [[await loadFeed(feedD), 1]]] | ||
] | ||
}) | ||
}) |