Refactor models to use bundleAsync and lock regions (#171)

This commit is contained in:
Paul Frazee
2023-02-08 11:57:40 -06:00
committed by GitHub
parent 82ea907e8f
commit c2b7796317
11 changed files with 378 additions and 444 deletions
+24
View File
@@ -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
View File
@@ -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,
) {
+76 -108
View File
@@ -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(
+38 -50
View File
@@ -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) {
+21 -36
View File
@@ -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)
}
}
}
+20 -32
View File
@@ -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)
+20 -32
View File
@@ -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)
+38 -50
View File
@@ -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) {