Skip to content

Commit fd3c7db

Browse files
committed
feat(cache): adds cache evicting on last activities
1 parent da75a0d commit fd3c7db

13 files changed

+197
-20
lines changed

src/components/container/LoadingBarProvider.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useLoadingBar } from 'naive-ui';
33
44
import { LoadingBarService } from '~/services/loading-bar.service';
55
6-
LoadingBarService.instance = useLoadingBar();
6+
LoadingBarService.init(useLoadingBar());
77
</script>
88

99
<template>

src/models/trakt/trakt-client.model.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,9 @@ export interface TraktClientEndpoint<Parameter extends TraktApiParams = Record<s
9999
(param?: Parameter, init?: TraktApiInit): Promise<TraktApiResponse<Response>>;
100100
}
101101

102-
export type TraktClientCachedEndpoint<Parameter extends TraktApiParams = Record<string, never>, Response = unknown> = (
103-
param?: Parameter,
104-
init?: BaseInit,
105-
cacheOptions?: BaseCacheOption,
106-
) => Promise<TraktApiResponse<Response>>;
102+
export type TraktClientCachedEndpoint<Parameter extends TraktApiParams = Record<string, never>, Response = unknown> = {
103+
evict: (param?: Parameter, init?: BaseInit) => Promise<string | undefined>;
104+
} & ((param?: Parameter, init?: BaseInit, cacheOptions?: BaseCacheOption) => Promise<TraktApiResponse<Response>>);
107105

108106
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -- To allow type extension
109107
export class TraktClientEndpoint<

src/models/trakt/trakt-sync.model.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export type TraktSyncActivity = {
4242
export type TraktSyncActivities = {
4343
/** Timestamp in ISO 8601 GMT format (YYYY-MM-DD'T'hh:mm:ss.sssZ) */
4444
all: string;
45+
collaborations: Pick<TraktSyncActivity, 'updated_at'>;
46+
recommendations: Pick<TraktSyncActivity, 'updated_at'>;
47+
4548
movies: Pick<
4649
TraktSyncActivity,
4750
'watched_at' | 'collected_at' | 'rated_at' | 'watchlisted_at' | 'favorited_at' | 'commented_at' | 'paused_at' | 'hidden_at'
@@ -55,7 +58,7 @@ export type TraktSyncActivities = {
5558
favorites: Pick<TraktSyncActivity, 'updated_at'>;
5659
account: Pick<TraktSyncActivity, 'settings_at' | 'followed_at' | 'following_at' | 'pending_at' | 'requested_at'>;
5760
saved_filters: Pick<TraktSyncActivity, 'updated_at'>;
58-
noted: Pick<TraktSyncActivity, 'updated_at'>;
61+
notes: Pick<TraktSyncActivity, 'updated_at'>;
5962
};
6063

6164
export type TraktSyncProgress<T extends 'movie' | 'episode' | Any = Any> = {

src/services/common/base-client.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,9 @@ export interface ClientEndpoint<Parameter extends RecursiveRecord = Record<strin
110110

111111
export type BaseCacheOption = { force?: boolean; retention?: number; evictOnError?: boolean };
112112

113-
type ClientEndpointCache<Parameter extends RecursiveRecord = Record<string, never>, Response = unknown> = (
114-
param?: Parameter,
115-
init?: BaseInit,
116-
cacheOptions?: BaseCacheOption,
117-
) => Promise<TypedResponse<Response>>;
113+
type ClientEndpointCache<Parameter extends RecursiveRecord = Record<string, never>, Response = unknown> = {
114+
evict: (param?: Parameter, init?: BaseInit) => Promise<string | undefined>;
115+
} & ((param?: Parameter, init?: BaseInit, cacheOptions?: BaseCacheOption) => Promise<TypedResponse<Response>>);
118116

119117
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
120118
export class ClientEndpoint<
@@ -203,7 +201,7 @@ export const getCachedFunction = <
203201
retention?: BaseTemplateOptions['cache'];
204202
},
205203
): ClientEndpointCache<Parameter, ResponseBody> => {
206-
const cachedFn: ClientEndpointCache<Parameter, ResponseBody> = async (param, init, cacheOptions) => {
204+
const cachedFn = async (param: Parameter, init: BaseInit, cacheOptions: BaseCacheOption) => {
207205
const _key = typeof key === 'function' ? key(param, init) : key;
208206
const cached = await cache.get(_key);
209207
if (cached && !cacheOptions?.force) {
@@ -242,7 +240,7 @@ export const getCachedFunction = <
242240
};
243241

244242
Object.defineProperty(cachedFn, 'evict', { value: evictFn });
245-
return cachedFn;
243+
return cachedFn as ClientEndpointCache<Parameter, ResponseBody>;
246244
};
247245

248246
/**

src/services/loading-bar.service.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
1+
import { ref } from 'vue';
2+
13
import type { useLoadingBar } from 'naive-ui';
24

35
export class LoadingBarService {
4-
static instance: ReturnType<typeof useLoadingBar>;
6+
private static instance: ReturnType<typeof useLoadingBar>;
7+
private static loading = ref(false);
8+
9+
static init(instance: ReturnType<typeof useLoadingBar>) {
10+
this.instance = instance;
11+
}
12+
13+
static get isLoading() {
14+
return this.loading.value;
15+
}
516

617
static start() {
718
if (!this.instance) console.warn('LoadingBarService instance is not initialized');
819
this.instance.start();
20+
this.loading.value = true;
921
}
1022

1123
static finish() {
24+
if (!this.isLoading) return;
1225
if (!this.instance) console.warn('LoadingBarService instance is not initialized');
1326
this.instance.finish();
27+
this.loading.value = false;
1428
}
1529

1630
static error() {
31+
if (!this.isLoading) return;
1732
if (!this.instance) console.warn('LoadingBarService instance is not initialized');
1833
this.instance.error();
34+
this.loading.value = false;
1935
}
2036
}

src/services/trakt.service.ts

+30
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,34 @@ export class TraktService {
311311
return response.json();
312312
},
313313
};
314+
315+
static async activity() {
316+
const response = await this.traktClient.sync.lastActivities();
317+
return response.json();
318+
}
319+
320+
static evict = {
321+
tmdb: TraktService.caches.tmdb.clear,
322+
trakt: TraktService.caches.trakt.clear,
323+
tvdb: TraktService.caches.tvdb.clear,
324+
history: TraktService.traktClient.sync.history.get.cached.evict,
325+
watchlist: TraktService.traktClient.sync.watchlist.get.cached.evict,
326+
favorites: TraktService.traktClient.sync.favorites.get.cached.evict,
327+
collection: TraktService.traktClient.sync.collection.get.cached.evict,
328+
lists: () =>
329+
Promise.all([
330+
TraktService.traktClient.users.lists.get.cached.evict(),
331+
TraktService.traktClient.users.lists.collaborations.cached.evict(),
332+
TraktService.traktClient.users.list.items.get.cached.evict(),
333+
]),
334+
calendar: () =>
335+
Promise.all([
336+
TraktService.traktClient.calendars.my.movies.cached.evict(),
337+
TraktService.traktClient.calendars.my.shows.new.cached.evict(),
338+
TraktService.traktClient.calendars.my.shows.premieres.cached.evict(),
339+
TraktService.traktClient.calendars.my.shows.finales.cached.evict(),
340+
TraktService.traktClient.calendars.my.shows.get.cached.evict(),
341+
]),
342+
progress: () => Promise.all([TraktService.cachedProgress.evict(), TraktService.traktClient.shows.progress.watched.cached.evict()]),
343+
};
314344
}

src/stores/data/activity.store.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { defineStore, storeToRefs } from 'pinia';
2+
3+
import { ref, watch } from 'vue';
4+
5+
import type { TraktSyncActivities } from '~/models/trakt/trakt-sync.model';
6+
7+
import { TraktService } from '~/services/trakt.service';
8+
import { storage } from '~/utils/browser/browser-storage.utils';
9+
import { compareDateObject, toDateObject } from '~/utils/date.utils';
10+
11+
export const useActivityStore = defineStore('data.activity', () => {
12+
const activity = ref<TraktSyncActivities>();
13+
const loading = ref(false);
14+
15+
const clearState = () => {
16+
activity.value = undefined;
17+
};
18+
19+
const saveState = async () => storage.local.set('data.activity', activity.value);
20+
const restoreState = async () => {
21+
const state = await storage.local.get<TraktSyncActivities>('data.activity');
22+
if (state) activity.value = state;
23+
};
24+
25+
const fetchActivity = async () => {
26+
console.info('Fetching activity');
27+
loading.value = true;
28+
29+
try {
30+
activity.value = await TraktService.activity();
31+
await saveState();
32+
} catch (error) {
33+
console.error('Failed to fetch activity', error);
34+
throw error;
35+
} finally {
36+
loading.value = false;
37+
}
38+
};
39+
40+
const initActivityStore = async () => {
41+
await restoreState();
42+
43+
watch(activity, (next, prev) => {
44+
const changed = compareDateObject(toDateObject(prev), toDateObject(next));
45+
console.info('Activity changed', changed);
46+
47+
if (
48+
changed?.episodes?.watched_at ||
49+
changed?.movies?.watched_at ||
50+
changed?.shows?.hidden_at ||
51+
changed?.seasons?.hidden_at ||
52+
changed?.movies?.hidden_at
53+
) {
54+
TraktService.evict.progress();
55+
TraktService.evict.history();
56+
TraktService.evict.calendar();
57+
console.info('Evicted progress, history and calendar');
58+
}
59+
if (
60+
changed?.watchlist?.updated_at ||
61+
changed?.episodes?.watchlisted_at ||
62+
changed?.seasons?.watchlisted_at ||
63+
changed?.shows?.watchlisted_at ||
64+
changed?.movies?.watchlisted_at
65+
) {
66+
TraktService.evict.watchlist();
67+
console.info('Evicted watchlist');
68+
}
69+
if (changed?.collaborations?.updated_at || changed?.lists?.updated_at) {
70+
TraktService.evict.lists();
71+
console.info('Evicted lists');
72+
}
73+
if (changed?.episodes?.collected_at || changed?.movies?.collected_at) {
74+
TraktService.evict.collection();
75+
console.info('Evicted collection');
76+
}
77+
if (changed?.favorites?.updated_at) {
78+
TraktService.evict.favorites();
79+
console.info('Evicted favorites');
80+
}
81+
});
82+
83+
await fetchActivity();
84+
};
85+
86+
return { activity, loading, fetchActivity, clearState, saveState, restoreState, initActivityStore };
87+
});
88+
89+
export const useActivityStoreRefs = () => storeToRefs(useActivityStore());

src/utils/cache.utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export type CacheStore<T = unknown> = {
1313
get(key: string): CacheStoreEntity<T> | Promise<CacheStoreEntity<T>> | undefined;
1414
set(key: string, value: CacheStoreEntity<T>): CacheStore<T> | Promise<CacheStore<T>>;
1515
delete(key: string): boolean | Promise<boolean>;
16-
clear(regex?: string): void;
16+
clear(regex?: string): void | Promise<void>;
1717
/** the duration in milliseconds after which the cache will be cleared */
1818
retention?: number;
1919
/** if true, the cache will be deleted if an error occurs */

src/utils/date.utils.ts

+31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { RecursiveRecord, RecursiveType } from '~/utils/typescript.utils';
2+
13
export class DateUtils {
24
static clone = (date: Date) => new Date(date);
35

@@ -16,3 +18,32 @@ export class DateUtils {
1618
next: (weeks = 1, date?: Date) => this.next(weeks, date, 7),
1719
};
1820
}
21+
22+
export const toDateObject = <T extends RecursiveRecord<string>>(record?: T): RecursiveType<T, Date> | undefined => {
23+
if (!record) return record;
24+
return Object.fromEntries(
25+
Object.entries(record).map(([key, value]) => {
26+
if (typeof value === 'string') return [key, new Date(value)];
27+
if (typeof value === 'object') return [key, toDateObject(value)];
28+
return [key, value];
29+
}),
30+
);
31+
};
32+
33+
const isDate = (value: unknown): value is Date => value instanceof Date;
34+
35+
export const compareDateObject = <T extends RecursiveRecord<Date>>(a?: T, b?: T): RecursiveType<T, boolean> => {
36+
if (!a || !b) throw new Error('Cannot compare undefined objects');
37+
return Object.fromEntries(
38+
Object.keys(a).map(key => {
39+
const _key: keyof RecursiveRecord<Date> = key as keyof RecursiveRecord<Date>;
40+
if (a && b) {
41+
const aValue = a[_key];
42+
const bValue = b[_key];
43+
if (isDate(aValue) && isDate(bValue)) return [_key, aValue.getTime() !== bValue.getTime()];
44+
if (!isDate(aValue) && !isDate(bValue)) return [_key, compareDateObject(aValue, bValue)];
45+
}
46+
return [_key, a !== b];
47+
}),
48+
);
49+
};

src/utils/regex.utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export const LargeRange = /^(100000|\d{0,5})-(100000|\d{0,5})$/;
55
export const VeryLargeRange = /^(300000|[1-2]?\d{0,5})-(300000|[1-2]?\d{0,5})$/;
66
export const DateISO8601 = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?(Z|[+-]\d{2}:[0-5]\d)?$/;
77
export const DateISO8601Short = /^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
8+
export const ExactMatchRegex = /[/\-\\^$*+?.()|[\]{}]/g;

src/utils/typescript.utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ export type RecursiveRecord<T = any> =
1818
export type GenericFunction = (...args: any) => any;
1919

2020
export type ArrayElement<ArrayType extends readonly unknown[] | undefined> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
21+
22+
export type RecursiveType<T, R> = {
23+
[K in keyof T]: T[K] extends object ? RecursiveType<T[K], R> : R;
24+
};

src/views/popup/main.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import defineComponent from '~/web/define-component';
22

3-
const root = document?.getElementById('app-popup');
3+
const resizeContainer = () => {
4+
const root = document?.getElementById('app-popup');
45

5-
if (root) {
6+
if (!root) return;
67
root.style.width = `${window.innerWidth}px`;
78
root.style.height = `${window.innerHeight}px`;
8-
}
9+
};
910

1011
defineComponent({ baseUrl: import.meta.env.VITE_BASE })
11-
.then(() => console.info('Web Component defined'))
12+
.then(() => {
13+
resizeContainer();
14+
console.info('Web Component defined');
15+
})
1216
.catch(err => console.error('Failed to define component', err));

src/web/init-services.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TraktService } from '~/services/trakt.service';
22
import { useAppStateStore } from '~/stores/app-state.store';
3+
import { useActivityStore } from '~/stores/data/activity.store';
34
import { useHistoryStore } from '~/stores/data/history.store';
45
import { useImageStore } from '~/stores/data/image.store';
56
import { useListsStore, useListStore } from '~/stores/data/list.store';
@@ -32,5 +33,7 @@ export const initServices = async () => {
3233
useShowStore().initShowStore(),
3334
]);
3435

36+
await useActivityStore().initActivityStore();
37+
3538
setAppReady(true);
3639
};

0 commit comments

Comments
 (0)