From ff94145165d6877e47db7ec11c4d3b802f6f3532 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 3 Aug 2020 17:26:40 +0800 Subject: ... --- Timeline/ClientApp/src/app/data/timeline.ts | 133 +++++++++++++--------------- 1 file changed, 62 insertions(+), 71 deletions(-) (limited to 'Timeline/ClientApp/src/app/data/timeline.ts') diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts index b30f3a7d..8680e9b8 100644 --- a/Timeline/ClientApp/src/app/data/timeline.ts +++ b/Timeline/ClientApp/src/app/data/timeline.ts @@ -32,7 +32,7 @@ import { HttpTimelineNameConflictError, HttpTimelineGenericPostInfo, } from '../http/timeline'; -import { BlobWithEtag, NotModified } from '../http/common'; +import { BlobWithEtag, NotModified, HttpNetworkError } from '../http/common'; export type TimelineInfo = HttpTimelineInfo; export type TimelineChangePropertyRequest = HttpTimelinePatchRequest; @@ -76,23 +76,17 @@ export interface TimelinePostListState { posts: TimelinePostInfo[]; } -export interface TimelineInfoLoadingState { - state: 'loading'; // Loading from cache. - timeline: null; -} - -export interface TimelineInfoNonLoadingState { - state: - | 'syncing' // Cache loaded and syncing now. If null means there is no cache for the timeline. - | 'offline' // Sync failed and use cache. - | 'synced' // Sync succeeded. If null means the timeline does not exist. - | 'new'; // This is a new timeline different from cached one. If null means the timeline does not exist. - timeline: TimelineInfo | null; -} - -export type TimelineInfoState = - | TimelineInfoLoadingState - | TimelineInfoNonLoadingState; +export type TimelineWithSyncState = + | { + syncState: + | 'offline' // Sync failed and use cache. Null timeline means no cache. + | 'synced'; // Sync succeeded. Null timeline means the timeline does not exist. + timeline: TimelineInfo | null; + } + | { + syncState: 'new'; // This is a new timeline different from cached one. + timeline: TimelineInfo; + }; interface TimelineCache { timeline: TimelineInfo; @@ -112,43 +106,35 @@ export class TimelineService { return `timeline.${timelineName}`; } - private getCachedTimeline( + private async fetchAndCacheTimeline( timelineName: string - ): Promise { - return dataStorage - .getItem(this.getTimelineKey(timelineName)) - .then((cache) => cache?.timeline ?? null); - } - - private async syncTimeline(timelineName: string): Promise { + ): Promise< + | { timeline: TimelineInfo; type: 'new' | 'cache' | 'synced' } + | 'offline' + | 'notexist' + > { const cache = await dataStorage.getItem(timelineName); + const key = this.getTimelineKey(timelineName); const save = (cache: TimelineCache): Promise => - dataStorage.setItem( - this.getTimelineKey(timelineName), - cache - ); - const push = (state: TimelineInfoState): void => { - this._timelineSubscriptionHub.update(timelineName, () => - Promise.resolve(state) - ); - }; + dataStorage.setItem(key, cache); - let result: TimelineInfo; const now = new Date(); if (cache == null) { try { - const res = await getHttpTimelineClient().getTimeline(timelineName); - result = res; - await save({ timeline: result, lastUpdated: now.toISOString() }); - push({ state: 'synced', timeline: result }); + const timeline = await getHttpTimelineClient().getTimeline( + timelineName + ); + await save({ timeline, lastUpdated: now.toISOString() }); + return { timeline, type: 'synced' }; } catch (e) { if (e instanceof HttpTimelineNotExistError) { - push({ state: 'synced', timeline: null }); + return 'notexist'; + } else if (e instanceof HttpNetworkError) { + return 'offline'; } else { - push({ state: 'offline', timeline: null }); + throw e; } - throw e; } } else { try { @@ -157,50 +143,55 @@ export class TimelineService { ifModifiedSince: new Date(cache.lastUpdated), }); if (res instanceof NotModified) { - result = cache.timeline; - await save({ timeline: result, lastUpdated: now.toISOString() }); - push({ state: 'synced', timeline: result }); + const { timeline } = cache; + await save({ timeline, lastUpdated: now.toISOString() }); + return { timeline, type: 'synced' }; } else { - result = res; - await save({ timeline: result, lastUpdated: now.toISOString() }); + const timeline = res; + await save({ timeline, lastUpdated: now.toISOString() }); if (res.uniqueId === cache.timeline.uniqueId) { - push({ state: 'synced', timeline: result }); + return { timeline, type: 'synced' }; } else { - push({ state: 'new', timeline: result }); + return { timeline, type: 'new' }; } } } catch (e) { if (e instanceof HttpTimelineNotExistError) { - push({ state: 'new', timeline: null }); - } else { - push({ state: 'offline', timeline: cache.timeline }); + await dataStorage.removeItem(key); + return 'notexist'; + } else if (e instanceof HttpNetworkError) { + return { timeline: cache.timeline, type: 'cache' }; } throw e; } } - return result; } private _timelineSubscriptionHub = new SubscriptionHub< string, - TimelineInfoState - >( - (key) => key, - () => ({ - state: 'loading', - timeline: null, - }), - async (key) => { - const result = await this.getCachedTimeline(key); - void this.syncTimeline(key); - return { - state: 'syncing', - timeline: result, - }; - } - ); + TimelineWithSyncState + >({ + setup: (key, next) => { + void this.fetchAndCacheTimeline(key).then((result) => { + if (result === 'offline') { + next({ syncState: 'offline', timeline: null }); + } else if (result === 'notexist') { + next({ syncState: 'synced', timeline: null }); + } else { + const { type, timeline } = result; + if (type === 'cache') { + next({ syncState: 'offline', timeline }); + } else if (type === 'synced') { + next({ syncState: 'synced', timeline }); + } else { + next({ syncState: 'new', timeline }); + } + } + }); + }, + }); - get timelineHub(): ISubscriptionHub { + get timelineHub(): ISubscriptionHub { return this._timelineSubscriptionHub; } -- cgit v1.2.3