Skip to content

Commit 6ac3cc8

Browse files
committed
feat(progress): move progress to store and adds caching
1 parent e08aea3 commit 6ac3cc8

17 files changed

+226
-133
lines changed

src/components/common/list/ListItem.vue

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ const ListScrollItemTypeLocal = ListScrollItemType;
179179
}"
180180
:data-key="item.id"
181181
:data-index="item.index"
182+
:data-type="item.type"
182183
:line-type="loading ? 'dashed' : lineType"
183184
:color="loading ? 'grey' : color"
184185
@mouseenter="onHover(true)"

src/components/common/list/ListScroll.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const props = defineProps({
5555
scrollThreshold: {
5656
type: Number,
5757
required: false,
58-
default: 0,
58+
default: 300,
5959
},
6060
});
6161

src/components/common/list/use-list-scroll.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ export const useListScroll = <T extends ListScrollSourceItemWithDate<D>, D exten
119119
const array = items.value;
120120
if (!array.length) return [];
121121
return array.map((item, index) => {
122-
const _item: ListScrollItem = { ...item, index, loading: (typeof item.id === 'number' && item.id < 0) || item.type === 'loading' };
122+
const _item: ListScrollItem = {
123+
...item,
124+
index,
125+
loading: (typeof item.id === 'number' && item.id < 0) || item.type === ListScrollItemType.loading,
126+
};
123127

124128
if (!_item.type) _item.type = getType(item);
125129
if (!_item.title) _item.title = getTitle(item);
@@ -193,7 +197,7 @@ export const addLoadMore = (
193197
if (!pagination.value?.pageCount) return array;
194198
if (pagination.value.page === pagination.value.pageCount) return array;
195199
if (array.length && array[array.length - 1].type === ListScrollItemType.loadMore) return array;
196-
const loadMore: ListScrollItem = { id: 'load-more', type: ListScrollItemType.loadMore, index: items.value.length };
200+
const loadMore: ListScrollItem = { id: ListScrollItemType.loadMore, type: ListScrollItemType.loadMore, index: items.value.length };
197201
return [...array, loadMore];
198202
});
199203
};

src/components/views/calendar/CalendarComponent.vue

-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ const onScrollBottom = async () => {
105105
ref="listRef"
106106
:items="list"
107107
:loading="loading"
108-
:scroll-threshold="300"
109108
episode
110109
:scroll-into-view="centerItem?.id ? [centerItem?.id] : []"
111110
@on-scroll-into-view="e => onScrollIntoOutOfView(false, e.ref)"

src/components/views/history/HistoryComponent.vue

-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ const { scrolled, listRef, onClick } = useBackToTop();
4444
:loading="loading"
4545
:pagination="pagination"
4646
:page-size="pageSize"
47-
:scroll-threshold="300"
4847
@on-scroll="scrolled = true"
4948
@on-scroll-top="scrolled = false"
5049
@on-scroll-bottom="onScroll"

src/components/views/progress/ProgressComponent.vue

+15-24
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,36 @@
11
<script lang="ts" setup>
2-
import { onMounted, ref } from 'vue';
3-
4-
import type { ListScrollItem } from '~/models/list-scroll.model';
2+
import { onMounted } from 'vue';
53
64
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
75
import { useBackToTop } from '~/components/common/buttons/use-back-to-top';
86
import ListScroll from '~/components/common/list/ListScroll.vue';
9-
import { progressToListItem } from '~/models/progress.model';
10-
import { TraktService } from '~/services/trakt.service';
7+
8+
import { useProgressStore, useProgressStoreRefs } from '~/stores/data/progress.store';
119
import { useI18n } from '~/utils';
1210
1311
const i18n = useI18n('progress');
1412
15-
const loading = ref(true);
16-
17-
const fetchProgress = async (): Promise<ListScrollItem[]> => {
18-
loading.value = true;
19-
try {
20-
const items = await TraktService.progress();
21-
return items.map((item, index) => ({ ...progressToListItem(item), index }));
22-
} catch (error) {
23-
console.error(error);
24-
throw error;
25-
} finally {
26-
loading.value = false;
27-
}
28-
};
29-
30-
const list = ref<ListScrollItem[]>([]);
13+
const { progress, loading } = useProgressStoreRefs();
14+
const { fetchProgress } = useProgressStore();
3115
3216
onMounted(async () => {
33-
list.value = await fetchProgress();
34-
console.info('list', list.value);
17+
await fetchProgress();
3518
});
3619
3720
const { scrolled, listRef, onClick } = useBackToTop();
3821
</script>
3922

4023
<template>
4124
<div class="container">
42-
<ListScroll ref="listRef" :loading="loading" :items="list" episode hide-date>
25+
<ListScroll
26+
ref="listRef"
27+
:loading="loading"
28+
:items="progress"
29+
episode
30+
hide-date
31+
@on-scroll="scrolled = true"
32+
@on-scroll-top="scrolled = false"
33+
>
4334
<template #default>
4435
<!-- TODO buttons here-->
4536
</template>

src/components/views/search/SearchComponent.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ const { scrolled, listRef, onClick } = useBackToTop();
4343
hide-date
4444
:items="list"
4545
:loading="loading"
46-
:scroll-threshold="300"
4746
@on-scroll="scrolled = true"
47+
@on-scroll-top="scrolled = false"
4848
@on-scroll-bottom="onScroll"
4949
>
5050
<template #default>

src/components/views/watchlist/WatchlistComponent.vue

-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ const { scrolled, listRef, onClick } = useBackToTop();
5454
:loading="loading"
5555
:pagination="pagination"
5656
:page-size="pageSize"
57-
:scroll-threshold="300"
5857
@on-scroll="scrolled = true"
5958
@on-scroll-top="scrolled = false"
6059
@on-scroll-bottom="onScroll"

src/models/progress.model.ts

-51
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
import type { ListScrollItem, ListScrollSourceItem } from '~/models/list-scroll.model';
2-
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
3-
import type { TraktShow } from '~/models/trakt/trakt-show.model';
4-
5-
import { getContent, getTags, getTitle } from '~/components/common/list/use-list-scroll';
6-
71
export type ProgressItem = {
82
episodeId: string;
93
episodeNumber: string;
@@ -23,48 +17,3 @@ export type ProgressItem = {
2317
type: 'show' | 'season' | 'episode';
2418
url: string;
2519
};
26-
27-
const titleRegex = /(.*)\s\d+x\d+\s"([^"]+)"/;
28-
export const progressToListItem = (progress: ProgressItem): Omit<ListScrollItem, 'index'> => {
29-
const match = titleRegex.exec(progress.fullTitle);
30-
31-
const episode: ListScrollSourceItem['episode'] = {
32-
ids: {
33-
trakt: Number(progress.episodeId),
34-
} as TraktEpisode['ids'],
35-
title: match ? match[2] : progress.fullTitle,
36-
season: Number(progress.seasonNumber),
37-
number: Number(progress.episodeNumber),
38-
};
39-
40-
const show: ListScrollSourceItem['show'] = {
41-
ids: {
42-
trakt: Number(progress.showId),
43-
} as TraktShow['ids'],
44-
title: match ? match[1] : progress.fullTitle,
45-
} as ListScrollSourceItem['show'];
46-
47-
const poster = progress.fanart ?? progress.screenshot;
48-
49-
return {
50-
id: Number(progress.episodeId ?? progress.seasonId ?? progress.showId),
51-
title: getTitle({ show, episode }),
52-
content: getContent({ show, episode }),
53-
poster,
54-
date: {
55-
current: new Date(progress.firstAired),
56-
},
57-
type: progress.type,
58-
tags: getTags({ episode }, progress.type),
59-
meta: {
60-
source: progress,
61-
episode,
62-
show,
63-
ids: {
64-
show: progress.showId ? Number(progress.showId) : undefined,
65-
season: progress.seasonId ? Number(progress.seasonId) : undefined,
66-
episode: progress.episodeId ? Number(progress.episodeId) : undefined,
67-
},
68-
},
69-
};
70-
};

src/services/common/base-client.ts

+53-34
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,14 @@ type ClientEndpointCall<Parameter extends RecursiveRecord = Record<string, never
106106
export interface ClientEndpoint<Parameter extends RecursiveRecord = Record<string, never>, Response = unknown> {
107107
(param?: Parameter, init?: BaseInit): Promise<ResponseOrTypedResponse<Response>>;
108108
}
109+
109110
export type BaseCacheOption = { force?: boolean; retention?: number; evictOnError?: boolean };
110111

111112
type ClientEndpointCache<Parameter extends RecursiveRecord = Record<string, never>, Response = unknown> = (
112113
param?: Parameter,
113114
init?: BaseInit,
114115
cacheOptions?: BaseCacheOption,
115-
) => Promise<ResponseOrTypedResponse<Response>>;
116+
) => Promise<TypedResponse<Response>>;
116117

117118
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
118119
export class ClientEndpoint<
@@ -183,6 +184,52 @@ const cloneResponse = <T>(response: TypedResponse<T>, cache?: TypedResponse<T>['
183184
return clone as TypedResponse<T>;
184185
};
185186

187+
export const getCachedFunction = <
188+
Parameter extends RecursiveRecord = RecursiveRecord,
189+
ResponseBody = unknown,
190+
ResponseType extends Response = Response,
191+
>(
192+
clientFn: ClientEndpointCall<Parameter, ResponseBody>,
193+
{
194+
key,
195+
cache,
196+
retention,
197+
}: {
198+
key: string | ((param?: Parameter, init?: BaseInit) => string);
199+
cache: CacheStore<ResponseType>;
200+
retention?: BaseTemplateOptions['cache'];
201+
},
202+
): ClientEndpointCache<Parameter, ResponseBody> => {
203+
return async (param, init, cacheOptions) => {
204+
const _key = typeof key === 'function' ? key(param, init) : key;
205+
const cached = await cache.get(_key);
206+
if (cached && !cacheOptions?.force) {
207+
let templateRetention = typeof retention === 'number' ? retention : undefined;
208+
if (typeof retention === 'object') templateRetention = retention.retention;
209+
const _retention = cacheOptions?.retention ?? templateRetention ?? cache.retention;
210+
if (!_retention) return cloneResponse<ResponseType>(cached.value, { previous: cached, current: cached, isCache: true });
211+
const expires = cached.cachedAt + _retention;
212+
if (expires > Date.now()) return cloneResponse(cached.value, { previous: cached, current: cached, isCache: true });
213+
}
214+
215+
try {
216+
const result: TypedResponse<ResponseBody> = await clientFn(param, init);
217+
const cacheEntry: CacheStoreEntity<ResponseType> = {
218+
cachedAt: Date.now(),
219+
value: cloneResponse(result) as ResponseType,
220+
};
221+
await cache.set(_key, cacheEntry);
222+
result.cache = { previous: cached, current: cacheEntry, isCache: false };
223+
return result;
224+
} catch (error) {
225+
if (cacheOptions?.evictOnError ?? (typeof retention === 'object' ? retention?.evictOnError : undefined) ?? cache.evictOnError) {
226+
cache.delete(_key);
227+
}
228+
throw error;
229+
}
230+
};
231+
};
232+
186233
/**
187234
* Represents a client with common functionality.
188235
*
@@ -285,39 +332,11 @@ export abstract class BaseClient<
285332
if (isApiTemplate(template)) {
286333
const fn: ClientEndpointCall = (param, init) => this._call(template, param, init);
287334

288-
const cachedFn: ClientEndpointCache = async (param, init, cacheOptions) => {
289-
const key = JSON.stringify({ template: template.config, param, init });
290-
291-
const cached = await this._cache.get(key);
292-
if (cached && !cacheOptions?.force) {
293-
let templateRetention = typeof template.opts?.cache === 'number' ? template.opts.cache : undefined;
294-
if (typeof template.opts?.cache === 'object') templateRetention = template.opts.cache.retention;
295-
const retention = cacheOptions?.retention ?? templateRetention ?? this._cache.retention;
296-
if (!retention) return cloneResponse(cached.value, { previous: cached, current: cached, isCache: true });
297-
const expires = cached.cachedAt + retention;
298-
if (expires > Date.now()) return cloneResponse(cached.value, { previous: cached, current: cached, isCache: true });
299-
}
300-
301-
try {
302-
const result = await fn(param, init);
303-
const cacheEntry = {
304-
cachedAt: Date.now(),
305-
value: cloneResponse(result) as ResponseType,
306-
};
307-
await this._cache.set(key, cacheEntry);
308-
result.cache = { previous: cached, current: cacheEntry, isCache: false };
309-
return result;
310-
} catch (error) {
311-
if (
312-
cacheOptions?.evictOnError ??
313-
(typeof template.opts?.cache === 'object' ? template.opts?.cache?.evictOnError : undefined) ??
314-
this._cache.evictOnError
315-
) {
316-
this._cache.delete(key);
317-
}
318-
throw error;
319-
}
320-
};
335+
const cachedFn: ClientEndpointCache = getCachedFunction(fn, {
336+
key: (param: unknown, init: unknown) => JSON.stringify({ template: template.config, param, init }),
337+
cache: this._cache,
338+
retention: template.opts?.cache,
339+
});
321340

322341
const parseUrl = (param: Record<string, unknown> = {}) => {
323342
const _params = template.transform?.(param) ?? param;

src/services/trakt.service.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { TraktWatchlistGetQuery } from '~/models/trakt/trakt-watchlist.mode
1212
import type { SettingsAuth, UserSetting } from '~/models/trakt-service.model';
1313
import type { TvdbApiResponse } from '~/models/tvdb/tvdb-client.model';
1414

15+
import { getCachedFunction, type TypedResponse } from '~/services/common/base-client';
1516
import { LoadingBarService } from '~/services/loading-bar.service';
1617
import { tmdbApi } from '~/services/tmdb-client/api/tmdb-api.endpoints';
1718
import { TmdbClient } from '~/services/tmdb-client/clients/tmdb-client';
@@ -266,14 +267,37 @@ export class TraktService {
266267
return { data: await response.json(), pagination: response.pagination };
267268
}
268269

269-
static async progress() {
270-
const response = await fetch('https://trakt.tv/dashboard/on_deck', {
271-
credentials: 'include',
272-
});
270+
private static cachedProgress = getCachedFunction(
271+
async () => {
272+
const response = await fetch('https://trakt.tv/dashboard/on_deck', {
273+
credentials: 'include',
274+
});
273275

274-
const htmlString = await response.text();
275-
const htmlDoc = new DOMParser().parseFromString(htmlString, 'text/html');
276+
const htmlString = await response.text();
277+
const htmlDoc = new DOMParser().parseFromString(htmlString, 'text/html');
278+
const data = Array.from(htmlDoc.querySelectorAll<HTMLAnchorElement>('a[class="watch"]')).map(
279+
a => ({ ...a.dataset }) as unknown as ProgressItem,
280+
);
276281

277-
return Array.from(htmlDoc.querySelectorAll<HTMLAnchorElement>('a[class="watch"]')).map(a => ({ ...a.dataset }) as unknown as ProgressItem);
282+
return new Response(JSON.stringify(data)) as TypedResponse<ProgressItem[]>;
283+
},
284+
{
285+
cache: this.caches.trakt,
286+
retention: CacheRetention.Day,
287+
key: JSON.stringify({
288+
template: {
289+
method: 'GET',
290+
url: 'https://trakt.tv/dashboard/on_deck',
291+
},
292+
init: {
293+
credentials: 'include',
294+
},
295+
}),
296+
},
297+
);
298+
299+
static async progress() {
300+
const response = await this.cachedProgress();
301+
return response.json();
278302
}
279303
}

src/stores/data/calendar.store.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,13 @@ export const useCalendarStore = defineStore('data.calendar', () => {
176176
if (mode === 'reload') {
177177
calendar.value = [...spacedData];
178178
} else if (mode === 'start') {
179-
calendar.value = [...spacedData, ...calendar.value.filter(c => c.type !== 'loading')];
179+
calendar.value = [...spacedData, ...calendar.value.filter(c => c.type !== ListScrollItemType.loading)];
180180
} else if (mode === 'end') {
181-
calendar.value = [...calendar.value.filter(c => c.type !== 'loading'), ...spacedData];
181+
calendar.value = [...calendar.value.filter(c => c.type !== ListScrollItemType.loading), ...spacedData];
182182
}
183183
} catch (e) {
184184
console.error('Failed to fetch history');
185-
calendar.value = calendar.value.filter(c => c.type !== 'loading');
185+
calendar.value = calendar.value.filter(c => c.type !== ListScrollItemType.loading);
186186
throw e;
187187
} finally {
188188
clearTimeout(timeout);

0 commit comments

Comments
 (0)