diff options
Diffstat (limited to 'Timeline/ClientApp/src/app/data')
-rw-r--r-- | Timeline/ClientApp/src/app/data/timeline.ts | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/Timeline/ClientApp/src/app/data/timeline.ts b/Timeline/ClientApp/src/app/data/timeline.ts index 84eb3764..b30f3a7d 100644 --- a/Timeline/ClientApp/src/app/data/timeline.ts +++ b/Timeline/ClientApp/src/app/data/timeline.ts @@ -76,15 +76,138 @@ 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;
+
+interface TimelineCache {
+ timeline: TimelineInfo;
+ lastUpdated: string;
+}
+
interface PostListInfo {
idList: number[];
lastUpdated: string;
}
export class TimelineService {
+ // timeline storage structure:
+ // each timeline has a TimelineCache saved with key created by getTimelineKey
+
+ private getTimelineKey(timelineName: string): string {
+ return `timeline.${timelineName}`;
+ }
+
+ private getCachedTimeline(
+ timelineName: string
+ ): Promise<TimelineInfo | null> {
+ return dataStorage
+ .getItem<TimelineCache | null>(this.getTimelineKey(timelineName))
+ .then((cache) => cache?.timeline ?? null);
+ }
+
+ private async syncTimeline(timelineName: string): Promise<TimelineInfo> {
+ const cache = await dataStorage.getItem<TimelineCache | null>(timelineName);
+
+ const save = (cache: TimelineCache): Promise<TimelineCache> =>
+ dataStorage.setItem<TimelineCache>(
+ this.getTimelineKey(timelineName),
+ cache
+ );
+ const push = (state: TimelineInfoState): void => {
+ this._timelineSubscriptionHub.update(timelineName, () =>
+ Promise.resolve(state)
+ );
+ };
+
+ 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 });
+ } catch (e) {
+ if (e instanceof HttpTimelineNotExistError) {
+ push({ state: 'synced', timeline: null });
+ } else {
+ push({ state: 'offline', timeline: null });
+ }
+ throw e;
+ }
+ } else {
+ try {
+ const res = await getHttpTimelineClient().getTimeline(timelineName, {
+ checkUniqueId: cache.timeline.uniqueId,
+ ifModifiedSince: new Date(cache.lastUpdated),
+ });
+ if (res instanceof NotModified) {
+ result = cache.timeline;
+ await save({ timeline: result, lastUpdated: now.toISOString() });
+ push({ state: 'synced', timeline: result });
+ } else {
+ result = res;
+ await save({ timeline: result, lastUpdated: now.toISOString() });
+ if (res.uniqueId === cache.timeline.uniqueId) {
+ push({ state: 'synced', timeline: result });
+ } else {
+ push({ state: 'new', timeline: result });
+ }
+ }
+ } catch (e) {
+ if (e instanceof HttpTimelineNotExistError) {
+ push({ state: 'new', timeline: null });
+ } else {
+ push({ state: 'offline', timeline: cache.timeline });
+ }
+ 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,
+ };
+ }
+ );
+
+ get timelineHub(): ISubscriptionHub<string, TimelineInfoState> {
+ return this._timelineSubscriptionHub;
+ }
+
// TODO: Remove this! This is currently only used to avoid multiple fetch of timeline. Because post list need to use the timeline id and call this method. But after timeline is also saved locally, this should be removed.
private timelineCache = new Map<string, Promise<TimelineInfo>>();
+ // TODO: Remove this.
getTimeline(timelineName: string): Observable<TimelineInfo> {
const cache = this.timelineCache.get(timelineName);
let promise: Promise<TimelineInfo>;
@@ -145,6 +268,7 @@ export class TimelineService { );
}
+ // TODO: Remove this.
getPosts(timelineName: string): Observable<TimelinePostInfo[]> {
const token = userService.currentUser?.token;
return from(getHttpTimelineClient().listPost(timelineName, token)).pipe(
|