Refactor models to use bundleAsync and lock regions (#171)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import {bundleAsync} from '../../../src/lib/async/bundle'
|
||||
|
||||
describe('bundle', () => {
|
||||
it('bundles multiple simultaneous calls into one execution', async () => {
|
||||
let calls = 0
|
||||
const fn = bundleAsync(async () => {
|
||||
calls++
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
return 'hello'
|
||||
})
|
||||
const [res1, res2, res3] = await Promise.all([fn(), fn(), fn()])
|
||||
expect(calls).toEqual(1)
|
||||
expect(res1).toEqual('hello')
|
||||
expect(res2).toEqual('hello')
|
||||
expect(res3).toEqual('hello')
|
||||
})
|
||||
it('does not bundle non-simultaneous calls', async () => {
|
||||
let calls = 0
|
||||
const fn = bundleAsync(async () => {
|
||||
calls++
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
return 'hello'
|
||||
})
|
||||
const res1 = await fn()
|
||||
const res2 = await fn()
|
||||
const res3 = await fn()
|
||||
expect(calls).toEqual(3)
|
||||
expect(res1).toEqual('hello')
|
||||
expect(res2).toEqual('hello')
|
||||
expect(res3).toEqual('hello')
|
||||
})
|
||||
it('is not affected by rejections', async () => {
|
||||
let calls = 0
|
||||
const fn = bundleAsync(async () => {
|
||||
calls++
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
throw new Error()
|
||||
})
|
||||
const res1 = await fn().catch(() => 'reject')
|
||||
const res2 = await fn().catch(() => 'reject')
|
||||
const res3 = await fn().catch(() => 'reject')
|
||||
expect(calls).toEqual(3)
|
||||
expect(res1).toEqual('reject')
|
||||
expect(res2).toEqual('reject')
|
||||
expect(res3).toEqual('reject')
|
||||
})
|
||||
})
|
||||
@@ -34,6 +34,7 @@
|
||||
"@segment/analytics-react-native": "^2.10.1",
|
||||
"@segment/sovran-react-native": "^0.4.5",
|
||||
"@zxing/text-encoding": "^0.9.0",
|
||||
"await-lock": "^2.2.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"email-validator": "^2.0.4",
|
||||
"he": "^1.2.0",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
type BundledFn<Args extends readonly unknown[], Res> = (
|
||||
...args: Args
|
||||
) => Promise<Res>
|
||||
|
||||
/**
|
||||
* A helper which ensures that multiple calls to an async function
|
||||
* only produces one in-flight request at a time.
|
||||
*/
|
||||
export function bundleAsync<Args extends readonly unknown[], Res>(
|
||||
fn: BundledFn<Args, Res>,
|
||||
): BundledFn<Args, Res> {
|
||||
let promise: Promise<Res> | undefined
|
||||
return async (...args) => {
|
||||
if (promise) {
|
||||
return promise
|
||||
}
|
||||
promise = fn(...args)
|
||||
try {
|
||||
return await promise
|
||||
} finally {
|
||||
promise = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
-136
@@ -5,6 +5,8 @@ import {
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
||||
} from '@atproto/api'
|
||||
import AwaitLock from 'await-lock'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
type FeedViewPost = AppBskyFeedFeedViewPost.Main
|
||||
type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
|
||||
type PostView = AppBskyFeedPost.View
|
||||
@@ -188,10 +190,8 @@ export class FeedModel {
|
||||
loadMoreCursor: string | undefined
|
||||
pollCursor: string | undefined
|
||||
|
||||
private _loadPromise: Promise<void> | undefined
|
||||
private _loadMorePromise: Promise<void> | undefined
|
||||
private _loadLatestPromise: Promise<void> | undefined
|
||||
private _updatePromise: Promise<void> | undefined
|
||||
// used to linearize async modifications to state
|
||||
private lock = new AwaitLock()
|
||||
|
||||
// data
|
||||
feed: FeedItemModel[] = []
|
||||
@@ -270,23 +270,26 @@ export class FeedModel {
|
||||
/**
|
||||
* Load for first render
|
||||
*/
|
||||
async setup(isRefreshing = false) {
|
||||
setup = bundleAsync(async (isRefreshing: boolean = false) => {
|
||||
this.rootStore.log.debug('FeedModel:setup', {isRefreshing})
|
||||
if (isRefreshing) {
|
||||
this.isRefreshing = true // set optimistically for UI
|
||||
}
|
||||
if (this._loadPromise) {
|
||||
return this._loadPromise
|
||||
}
|
||||
await this._pendingWork()
|
||||
this.setHasNewLatest(false)
|
||||
this._loadPromise = this._initialLoad(isRefreshing)
|
||||
await this.lock.acquireAsync()
|
||||
try {
|
||||
await this._loadPromise
|
||||
this.setHasNewLatest(false)
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
const res = await this._getFeed({limit: PAGE_SIZE})
|
||||
await this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
} finally {
|
||||
this._loadPromise = undefined
|
||||
this.lock.release()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Register any event listeners. Returns a cleanup function.
|
||||
@@ -306,51 +309,93 @@ export class FeedModel {
|
||||
/**
|
||||
* Load more posts to the end of the feed
|
||||
*/
|
||||
async loadMore() {
|
||||
if (this._loadMorePromise) {
|
||||
return this._loadMorePromise
|
||||
}
|
||||
await this._pendingWork()
|
||||
this._loadMorePromise = this._loadMore()
|
||||
loadMore = bundleAsync(async () => {
|
||||
await this.lock.acquireAsync()
|
||||
try {
|
||||
await this._loadMorePromise
|
||||
if (!this.hasMore || this.hasError) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
try {
|
||||
const res = await this._getFeed({
|
||||
before: this.loadMoreCursor,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
await this._appendAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('FeedView: Failed to load more', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this._loadMorePromise = undefined
|
||||
this.lock.release()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Load more posts to the start of the feed
|
||||
*/
|
||||
async loadLatest() {
|
||||
if (this._loadLatestPromise) {
|
||||
return this._loadLatestPromise
|
||||
}
|
||||
await this._pendingWork()
|
||||
this.setHasNewLatest(false)
|
||||
this._loadLatestPromise = this._loadLatest()
|
||||
loadLatest = bundleAsync(async () => {
|
||||
await this.lock.acquireAsync()
|
||||
try {
|
||||
await this._loadLatestPromise
|
||||
this.setHasNewLatest(false)
|
||||
this._xLoading()
|
||||
try {
|
||||
const res = await this._getFeed({limit: PAGE_SIZE})
|
||||
await this._prependAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('FeedView: Failed to load latest', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this._loadLatestPromise = undefined
|
||||
this.lock.release()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Update content in-place
|
||||
*/
|
||||
async update() {
|
||||
if (this._updatePromise) {
|
||||
return this._updatePromise
|
||||
}
|
||||
await this._pendingWork()
|
||||
this._updatePromise = this._update()
|
||||
update = bundleAsync(async () => {
|
||||
await this.lock.acquireAsync()
|
||||
try {
|
||||
await this._updatePromise
|
||||
if (!this.feed.length) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
let numToFetch = this.feed.length
|
||||
let cursor
|
||||
try {
|
||||
do {
|
||||
const res: GetTimeline.Response = await this._getFeed({
|
||||
before: cursor,
|
||||
limit: Math.min(numToFetch, 100),
|
||||
})
|
||||
if (res.data.feed.length === 0) {
|
||||
break // sanity check
|
||||
}
|
||||
this._updateAll(res)
|
||||
numToFetch -= res.data.feed.length
|
||||
cursor = res.data.cursor
|
||||
} while (cursor && numToFetch > 0)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('FeedView: Failed to update', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this._updatePromise = undefined
|
||||
this.lock.release()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if new posts are available
|
||||
@@ -359,7 +404,6 @@ export class FeedModel {
|
||||
if (this.hasNewLatest) {
|
||||
return
|
||||
}
|
||||
await this._pendingWork()
|
||||
const res = await this._getFeed({limit: 1})
|
||||
const currentLatestUri = this.pollCursor
|
||||
const receivedLatestUri = res.data.feed[0]
|
||||
@@ -404,101 +448,9 @@ export class FeedModel {
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
private async _pendingWork() {
|
||||
if (this._loadPromise) {
|
||||
await this._loadPromise
|
||||
}
|
||||
if (this._loadMorePromise) {
|
||||
await this._loadMorePromise
|
||||
}
|
||||
if (this._loadLatestPromise) {
|
||||
await this._loadLatestPromise
|
||||
}
|
||||
if (this._updatePromise) {
|
||||
await this._updatePromise
|
||||
}
|
||||
}
|
||||
|
||||
private async _initialLoad(isRefreshing = false) {
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
const res = await this._getFeed({limit: PAGE_SIZE})
|
||||
await this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadLatest() {
|
||||
this._xLoading()
|
||||
try {
|
||||
const res = await this._getFeed({limit: PAGE_SIZE})
|
||||
await this._prependAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('FeedView: Failed to load latest', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadMore() {
|
||||
if (!this.hasMore || this.hasError) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
try {
|
||||
const res = await this._getFeed({
|
||||
before: this.loadMoreCursor,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
await this._appendAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('FeedView: Failed to load more', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (!this.feed.length) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
let numToFetch = this.feed.length
|
||||
let cursor
|
||||
try {
|
||||
do {
|
||||
const res: GetTimeline.Response = await this._getFeed({
|
||||
before: cursor,
|
||||
limit: Math.min(numToFetch, 100),
|
||||
})
|
||||
if (res.data.feed.length === 0) {
|
||||
break // sanity check
|
||||
}
|
||||
this._updateAll(res)
|
||||
numToFetch -= res.data.feed.length
|
||||
cursor = res.data.cursor
|
||||
} while (cursor && numToFetch > 0)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('FeedView: Failed to update', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async _replaceAll(
|
||||
res: GetTimeline.Response | GetAuthorFeed.Response,
|
||||
) {
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
AppBskyGraphAssertion,
|
||||
AppBskyGraphFollow,
|
||||
} from '@atproto/api'
|
||||
import AwaitLock from 'await-lock'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {PostThreadViewModel} from './post-thread-view'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
@@ -191,9 +193,8 @@ export class NotificationsViewModel {
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
|
||||
private _loadPromise: Promise<void> | undefined
|
||||
private _loadMorePromise: Promise<void> | undefined
|
||||
private _updatePromise: Promise<void> | undefined
|
||||
// used to linearize async modifications to state
|
||||
private lock = new AwaitLock()
|
||||
|
||||
// data
|
||||
notifications: NotificationsViewItemModel[] = []
|
||||
@@ -250,19 +251,28 @@ export class NotificationsViewModel {
|
||||
/**
|
||||
* Load for first render
|
||||
*/
|
||||
async setup(isRefreshing = false) {
|
||||
setup = bundleAsync(async (isRefreshing: boolean = false) => {
|
||||
this.rootStore.log.debug('NotificationsModel:setup', {isRefreshing})
|
||||
if (isRefreshing) {
|
||||
this.isRefreshing = true // set optimistically for UI
|
||||
}
|
||||
if (this._loadPromise) {
|
||||
return this._loadPromise
|
||||
await this.lock.acquireAsync()
|
||||
try {
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.notification.list(params)
|
||||
await this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
} finally {
|
||||
this.lock.release()
|
||||
}
|
||||
await this._pendingWork()
|
||||
this._loadPromise = this._initialLoad(isRefreshing)
|
||||
await this._loadPromise
|
||||
this._loadPromise = undefined
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Reset and load
|
||||
@@ -274,34 +284,71 @@ export class NotificationsViewModel {
|
||||
/**
|
||||
* Load more posts to the end of the notifications
|
||||
*/
|
||||
async loadMore() {
|
||||
if (this._loadMorePromise) {
|
||||
return this._loadMorePromise
|
||||
loadMore = bundleAsync(async () => {
|
||||
if (!this.hasMore) {
|
||||
return
|
||||
}
|
||||
await this._pendingWork()
|
||||
this._loadMorePromise = this._loadMore()
|
||||
this.lock.acquireAsync()
|
||||
try {
|
||||
await this._loadMorePromise
|
||||
this._xLoading()
|
||||
try {
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
before: this.loadMoreCursor,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.notification.list(params)
|
||||
await this._appendAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('NotificationsView: Failed to load more', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this._loadMorePromise = undefined
|
||||
this.lock.release()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Update content in-place
|
||||
*/
|
||||
async update() {
|
||||
if (this._updatePromise) {
|
||||
return this._updatePromise
|
||||
}
|
||||
await this._pendingWork()
|
||||
this._updatePromise = this._update()
|
||||
update = bundleAsync(async () => {
|
||||
await this.lock.acquireAsync()
|
||||
try {
|
||||
await this._updatePromise
|
||||
if (!this.notifications.length) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
let numToFetch = this.notifications.length
|
||||
let cursor
|
||||
try {
|
||||
do {
|
||||
const res: ListNotifications.Response =
|
||||
await this.rootStore.api.app.bsky.notification.list({
|
||||
before: cursor,
|
||||
limit: Math.min(numToFetch, 100),
|
||||
})
|
||||
if (res.data.notifications.length === 0) {
|
||||
break // sanity check
|
||||
}
|
||||
this._updateAll(res)
|
||||
numToFetch -= res.data.notifications.length
|
||||
cursor = res.data.cursor
|
||||
} while (cursor && numToFetch > 0)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('NotificationsView: Failed to update', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this._updatePromise = undefined
|
||||
this.lock.release()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Update read/unread state
|
||||
@@ -356,88 +403,9 @@ export class NotificationsViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
private async _pendingWork() {
|
||||
if (this._loadPromise) {
|
||||
await this._loadPromise
|
||||
}
|
||||
if (this._loadMorePromise) {
|
||||
await this._loadMorePromise
|
||||
}
|
||||
if (this._updatePromise) {
|
||||
await this._updatePromise
|
||||
}
|
||||
}
|
||||
|
||||
private async _initialLoad(isRefreshing = false) {
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.notification.list(params)
|
||||
await this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadMore() {
|
||||
if (!this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
try {
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
before: this.loadMoreCursor,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.notification.list(params)
|
||||
await this._appendAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('NotificationsView: Failed to load more', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
if (!this.notifications.length) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
let numToFetch = this.notifications.length
|
||||
let cursor
|
||||
try {
|
||||
do {
|
||||
const res: ListNotifications.Response =
|
||||
await this.rootStore.api.app.bsky.notification.list({
|
||||
before: cursor,
|
||||
limit: Math.min(numToFetch, 100),
|
||||
})
|
||||
if (res.data.notifications.length === 0) {
|
||||
break // sanity check
|
||||
}
|
||||
this._updateAll(res)
|
||||
numToFetch -= res.data.notifications.length
|
||||
cursor = res.data.cursor
|
||||
} while (cursor && numToFetch > 0)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle() // don't bubble the error to the user
|
||||
this.rootStore.log.error('NotificationsView: Failed to update', {
|
||||
params: this.params,
|
||||
e,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async _replaceAll(res: ListNotifications.Response) {
|
||||
if (res.data.notifications[0]) {
|
||||
this.mostRecentNotification = new NotificationsViewItemModel(
|
||||
|
||||
@@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import {AppBskyFeedGetRepostedBy as GetRepostedBy} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
import * as apilib from '../lib/api'
|
||||
|
||||
@@ -19,7 +20,6 @@ export class RepostedByViewModel {
|
||||
params: GetRepostedBy.QueryParams
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
private _loadMorePromise: Promise<void> | undefined
|
||||
|
||||
// data
|
||||
uri: string = ''
|
||||
@@ -59,55 +59,7 @@ export class RepostedByViewModel {
|
||||
return this.loadMore(true)
|
||||
}
|
||||
|
||||
async loadMore(isRefreshing = false) {
|
||||
if (this._loadMorePromise) {
|
||||
return this._loadMorePromise
|
||||
}
|
||||
this._loadMorePromise = this._load(isRefreshing)
|
||||
try {
|
||||
await this._loadMorePromise
|
||||
} finally {
|
||||
this._loadMorePromise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
private _xLoading(isRefreshing = false) {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch reposted by view', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// =
|
||||
|
||||
private async _resolveUri() {
|
||||
const urip = new AtUri(this.params.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
try {
|
||||
urip.host = await apilib.resolveName(this.rootStore, urip.host)
|
||||
} catch (e: any) {
|
||||
this.error = e.toString()
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.resolvedUri = urip.toString()
|
||||
})
|
||||
}
|
||||
|
||||
private async _load(replace = false) {
|
||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
if (!this.resolvedUri) {
|
||||
@@ -128,6 +80,42 @@ export class RepostedByViewModel {
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
})
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
private _xLoading(isRefreshing = false) {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch reposted by view', err)
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
private async _resolveUri() {
|
||||
const urip = new AtUri(this.params.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
try {
|
||||
urip.host = await apilib.resolveName(this.rootStore, urip.host)
|
||||
} catch (e: any) {
|
||||
this.error = e.toString()
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.resolvedUri = urip.toString()
|
||||
})
|
||||
}
|
||||
|
||||
private _replaceAll(res: GetRepostedBy.Response) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AppBskyActorGetSuggestions as GetSuggestions} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
@@ -15,7 +16,6 @@ export class SuggestedActorsViewModel {
|
||||
error = ''
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
private _loadMorePromise: Promise<void> | undefined
|
||||
|
||||
// data
|
||||
suggestions: SuggestedActor[] = []
|
||||
@@ -49,41 +49,7 @@ export class SuggestedActorsViewModel {
|
||||
return this.loadMore(true)
|
||||
}
|
||||
|
||||
async loadMore(isRefreshing = false) {
|
||||
if (this._loadMorePromise) {
|
||||
return this._loadMorePromise
|
||||
}
|
||||
this._loadMorePromise = this._load(isRefreshing)
|
||||
try {
|
||||
await this._loadMorePromise
|
||||
} finally {
|
||||
this._loadMorePromise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
private _xLoading(isRefreshing = false) {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch suggested actors', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// =
|
||||
|
||||
private async _load(replace = false) {
|
||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
@@ -121,5 +87,24 @@ export class SuggestedActorsViewModel {
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
})
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
private _xLoading(isRefreshing = false) {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch suggested actors', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
@@ -19,7 +20,6 @@ export class UserFollowersViewModel {
|
||||
params: GetFollowers.QueryParams
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
private _loadMorePromise: Promise<void> | undefined
|
||||
|
||||
// data
|
||||
subject: ActorRef.WithInfo = {
|
||||
@@ -63,17 +63,27 @@ export class UserFollowersViewModel {
|
||||
return this.loadMore(true)
|
||||
}
|
||||
|
||||
async loadMore(isRefreshing = false) {
|
||||
if (this._loadMorePromise) {
|
||||
return this._loadMorePromise
|
||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._loadMorePromise = this._load(isRefreshing)
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
await this._loadMorePromise
|
||||
} finally {
|
||||
this._loadMorePromise = undefined
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
before: replace ? undefined : this.loadMoreCursor,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.graph.getFollowers(params)
|
||||
if (replace) {
|
||||
this._replaceAll(res)
|
||||
} else {
|
||||
this._appendAll(res)
|
||||
}
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
@@ -94,31 +104,9 @@ export class UserFollowersViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
private async _load(replace = false) {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
before: replace ? undefined : this.loadMoreCursor,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.graph.getFollowers(params)
|
||||
if (replace) {
|
||||
this._replaceAll(res)
|
||||
} else {
|
||||
this._appendAll(res)
|
||||
}
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
private _replaceAll(res: GetFollowers.Response) {
|
||||
this.followers = []
|
||||
this._appendAll(res)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
@@ -19,7 +20,6 @@ export class UserFollowsViewModel {
|
||||
params: GetFollows.QueryParams
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
private _loadMorePromise: Promise<void> | undefined
|
||||
|
||||
// data
|
||||
subject: ActorRef.WithInfo = {
|
||||
@@ -63,17 +63,27 @@ export class UserFollowsViewModel {
|
||||
return this.loadMore(true)
|
||||
}
|
||||
|
||||
async loadMore(isRefreshing = false) {
|
||||
if (this._loadMorePromise) {
|
||||
return this._loadMorePromise
|
||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._loadMorePromise = this._load(isRefreshing)
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
await this._loadMorePromise
|
||||
} finally {
|
||||
this._loadMorePromise = undefined
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
before: replace ? undefined : this.loadMoreCursor,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.graph.getFollows(params)
|
||||
if (replace) {
|
||||
this._replaceAll(res)
|
||||
} else {
|
||||
this._appendAll(res)
|
||||
}
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
@@ -94,31 +104,9 @@ export class UserFollowsViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
private async _load(replace = false) {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
const params = Object.assign({}, this.params, {
|
||||
limit: PAGE_SIZE,
|
||||
before: replace ? undefined : this.loadMoreCursor,
|
||||
})
|
||||
const res = await this.rootStore.api.app.bsky.graph.getFollows(params)
|
||||
if (replace) {
|
||||
this._replaceAll(res)
|
||||
} else {
|
||||
this._appendAll(res)
|
||||
}
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
private _replaceAll(res: GetFollows.Response) {
|
||||
this.follows = []
|
||||
this._appendAll(res)
|
||||
|
||||
@@ -3,6 +3,7 @@ import {AtUri} from '../../third-party/uri'
|
||||
import {AppBskyFeedGetVotes as GetVotes} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
import * as apilib from '../lib/api'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
@@ -19,7 +20,6 @@ export class VotesViewModel {
|
||||
params: GetVotes.QueryParams
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
private _loadMorePromise: Promise<void> | undefined
|
||||
|
||||
// data
|
||||
uri: string = ''
|
||||
@@ -56,55 +56,7 @@ export class VotesViewModel {
|
||||
return this.loadMore(true)
|
||||
}
|
||||
|
||||
async loadMore(isRefreshing = false) {
|
||||
if (this._loadMorePromise) {
|
||||
return this._loadMorePromise
|
||||
}
|
||||
this._loadMorePromise = this._load(isRefreshing)
|
||||
try {
|
||||
await this._loadMorePromise
|
||||
} finally {
|
||||
this._loadMorePromise = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
private _xLoading(isRefreshing = false) {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch votes', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// =
|
||||
|
||||
private async _resolveUri() {
|
||||
const urip = new AtUri(this.params.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
try {
|
||||
urip.host = await apilib.resolveName(this.rootStore, urip.host)
|
||||
} catch (e: any) {
|
||||
this.error = e.toString()
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.resolvedUri = urip.toString()
|
||||
})
|
||||
}
|
||||
|
||||
private async _load(replace = false) {
|
||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
@@ -128,6 +80,42 @@ export class VotesViewModel {
|
||||
} catch (e: any) {
|
||||
this._xIdle(e)
|
||||
}
|
||||
})
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
private _xLoading(isRefreshing = false) {
|
||||
this.isLoading = true
|
||||
this.isRefreshing = isRefreshing
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch votes', err)
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
private async _resolveUri() {
|
||||
const urip = new AtUri(this.params.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
try {
|
||||
urip.host = await apilib.resolveName(this.rootStore, urip.host)
|
||||
} catch (e: any) {
|
||||
this.error = e.toString()
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.resolvedUri = urip.toString()
|
||||
})
|
||||
}
|
||||
|
||||
private _replaceAll(res: GetVotes.Response) {
|
||||
|
||||
@@ -3712,6 +3712,11 @@ autoprefixer@^10.4.13:
|
||||
picocolors "^1.0.0"
|
||||
postcss-value-parser "^4.2.0"
|
||||
|
||||
await-lock@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
|
||||
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
|
||||
|
||||
axe-core@^4.4.3:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.1.tgz#79cccdee3e3ab61a8f42c458d4123a6768e6fbce"
|
||||
|
||||
Reference in New Issue
Block a user