From 769b40a0f32e4c00c2be00f1ce1bac98dc7d99cc Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 8 Aug 2020 20:40:07 +0800 Subject: ... --- Timeline/ClientApp/src/app/data/timeline.ts | 194 +++++++++++---------- .../src/app/timeline/TimelinePageTemplate.tsx | 6 +- 2 files changed, 108 insertions(+), 92 deletions(-) diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts index fa019adb..477d410b 100644 --- a/Timeline/ClientApp/src/app/data/timeline.ts +++ b/Timeline/ClientApp/src/app/data/timeline.ts @@ -1,12 +1,13 @@ import React from 'react'; import XRegExp from 'xregexp'; -import { Observable, from } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable, from, combineLatest } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; import { convertError } from '../utilities/rxjs'; import { dataStorage } from './common'; import { SubscriptionHub, ISubscriptionHub } from './SubscriptionHub'; +import syncStatusHub from './SyncStatusHub'; import { UserAuthInfo, checkLogin, userService, userInfoService } from './user'; @@ -74,18 +75,18 @@ export const timelineVisibilityTooltipTranslationMap: Record< export class TimelineNotExistError extends Error {} export class TimelineNameConflictError extends Error {} -export type TimelineWithSyncState = +export type TimelineWithSyncStatus = | { - syncState: 'loadcache'; // Loading cache now. - timeline?: undefined; + type: 'cache'; + timeline: TimelineInfo; } | { - syncState: - | 'syncing' // Cache loaded and syncing for the first time. - | 'offline' // Sync failed and use cache. Null timeline means no cache. - | 'synced' // Sync succeeded. Null timeline means the timeline does not exist. - | 'new'; // This is a new timeline different from cached one. Null timeline means the timeline does not exist. + type: 'offline' | 'synced'; timeline: TimelineInfo | null; + } + | { + type: 'notexist'; + timeline?: undefined; }; export interface TimelinePostsWithSyncState { @@ -99,107 +100,123 @@ export interface TimelinePostsWithSyncState { posts: TimelinePostInfo[]; } -type FetchAndCacheTimelineResult = TimelineInfo | 'offline' | 'notexist'; - type FetchAndCachePostsResult = | TimelinePostInfo[] | 'notexist' | 'forbid' | 'offline'; -export class TimelineService { - private getTimelineKey(timelineName: string): string { - return `timeline.${timelineName}`; - } +type TimelineData = Omit & { + owner: string; + members: string[]; +}; +export class TimelineService { private getCachedTimeline( timelineName: string - ): Promise { - return dataStorage.getItem( - this.getTimelineKey(timelineName) - ); + ): Promise { + return dataStorage.getItem(`timeline.${timelineName}`); } - private async fetchAndCacheTimeline( - timelineName: string - ): Promise { + private convertHttpTimelineToData(timeline: HttpTimelineInfo): TimelineData { + return { + ...timeline, + owner: timeline.owner.username, + members: timeline.members.map((m) => m.username), + }; + } + + private async syncTimeline(timelineName: string): Promise { + const syncStatusKey = `timeline.${timelineName}`; + if (syncStatusHub.get(syncStatusKey)) return; + syncStatusHub.begin(syncStatusKey); + try { - const timeline = await getHttpTimelineClient().getTimeline(timelineName); - await dataStorage.setItem( - this.getTimelineKey(timelineName), + const httpTimeline = await getHttpTimelineClient().getTimeline( + timelineName + ); + const timeline = this.convertHttpTimelineToData(httpTimeline); + await dataStorage.setItem( + `timeline.${timelineName}`, timeline ); - return timeline; + + syncStatusHub.end(syncStatusKey); + this._timelineHub + .getLine(timelineName) + ?.next({ type: 'synced', timeline }); } catch (e) { + syncStatusHub.end(syncStatusKey); if (e instanceof HttpTimelineNotExistError) { - return 'notexist'; + this._timelineHub + .getLine(timelineName) + ?.next({ type: 'synced', timeline: null }); } else if (e instanceof HttpNetworkError) { - return 'offline'; + const cache = await this.getCachedTimeline(timelineName); + if (cache == null) + this._timelineHub + .getLine(timelineName) + ?.next({ type: 'offline', timeline: null }); + else + this._timelineHub + .getLine(timelineName) + ?.next({ type: 'offline', timeline: cache }); } else { throw e; } } } - private async syncTimeline(timelineName: string): Promise { - const line = this._timelineSubscriptionHub.getLine(timelineName); - - if (line == null) { - console.log('No subscription, skip sync!'); - return; - } - - const old = line.value; - - if ( - old != null && - (old.syncState === 'loadcache' || old.syncState === 'syncing') - ) { - return; - } - - const next = line.next.bind(line); - - if (old == undefined) { - next({ syncState: 'loadcache' }); - const timeline = await this.getCachedTimeline(timelineName); - next({ syncState: 'syncing', timeline }); - } else { - next({ syncState: 'syncing', timeline: old?.timeline }); - } - - const result = await this.fetchAndCacheTimeline(timelineName); - - if (result === 'offline') { - next({ syncState: 'offline', timeline: line.value?.timeline ?? null }); - } else if (result === 'notexist') { - if (line.value?.timeline != null) { - next({ syncState: 'new', timeline: null }); - } else { - next({ syncState: 'synced', timeline: null }); + private _timelineHub = new SubscriptionHub< + string, + | { + type: 'cache'; + timeline: TimelineData; } - } else { - if (result.uniqueId === line.value?.timeline?.uniqueId) { - next({ syncState: 'synced', timeline: result }); - } else { - next({ syncState: 'new', timeline: result }); + | { + type: 'offline' | 'synced'; + timeline: TimelineData | null; } - } - } - - private _timelineSubscriptionHub = new SubscriptionHub< - string, - TimelineWithSyncState >({ - setup: (key) => { - void this.syncTimeline(key); + setup: (key, line) => { + void this.getCachedTimeline(key).then((timeline) => { + if (timeline != null) { + line.next({ type: 'cache', timeline }); + } + return this.syncTimeline(key); + }); }, - destroyable: (_, value) => - value?.syncState !== 'loadcache' && value?.syncState !== 'syncing', }); - get timelineHub(): ISubscriptionHub { - return this._timelineSubscriptionHub; + getTimeline$(timelineName: string): Observable { + return this._timelineHub.getObservable(timelineName).pipe( + switchMap((state) => { + if (state.timeline != null) { + return combineLatest( + [state.timeline.owner, ...state.timeline.members].map((u) => + userInfoService.getUser$(u) + ) + ).pipe( + map((users) => { + return { + type: 'cache', + timeline: { + ...state.timeline, + owner: users[0], + members: users.slice(1), + }, + } as TimelineWithSyncStatus; + }) + ); + } else { + return [ + { + ...state, + } as TimelineWithSyncStatus, + ]; + } + }) + ); } createTimeline(timelineName: string): Observable { @@ -591,17 +608,16 @@ export function validateTimelineName(name: string): boolean { export function useTimelineInfo( timelineName: string -): TimelineWithSyncState | undefined { - const [state, setState] = React.useState( +): TimelineWithSyncStatus | undefined { + const [state, setState] = React.useState( undefined ); React.useEffect(() => { - const subscription = timelineService.timelineHub.subscribe( - timelineName, - (data) => { + const subscription = timelineService + .getTimeline$(timelineName) + .subscribe((data) => { setState(data); - } - ); + }); return () => { subscription.unsubscribe(); }; diff --git a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx index edb9564a..89101f8f 100644 --- a/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx +++ b/Timeline/ClientApp/src/app/timeline/TimelinePageTemplate.tsx @@ -52,9 +52,9 @@ export default function TimelinePageTemplate( const error: string | undefined = (() => { if (timelineState != null) { - const { syncState, timeline } = timelineState; - if (syncState === 'offline' && timeline == null) return 'Network Error'; - if (syncState === 'synced' && timeline == null) + const { type, timeline } = timelineState; + if (type === 'offline' && timeline == null) return 'Network Error'; + if (type === 'synced' && timeline == null) return t(props.notFoundI18nKey); } return undefined; -- cgit v1.2.3