Skip to content

Commit 3a9b173

Browse files
committed
OengusAPI cache can expire
Various tweaks to account for changes to caching Related to Issue #47
1 parent 61e0157 commit 3a9b173

File tree

6 files changed

+85
-35
lines changed

6 files changed

+85
-35
lines changed

components/homepage/Marathons.vue

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<div v-if="homepageMarathons" class="marathons-list-container">
1313
<template v-for="marathonsList in marathonsLists">
14-
<template v-if="homepageMarathons[marathonsList.key].length">
14+
<template v-if="shouldRenderList(marathonsList.key)">
1515
<h4 :key="marathonsList.key + 'title'" class="title" :class="marathonsList.headerClass">
1616
{{ $t(marathonsList.label) }}
1717
</h4>
@@ -89,6 +89,9 @@ export default Vue.extend({
8989
'is-dark': index % 2 === 0,
9090
};
9191
},
92+
shouldRenderList(key: keyof FrontPageMarathons): boolean {
93+
return (this.homepageMarathons?.[key]?.length ?? 0) > 0;
94+
},
9295
...mapActions({
9396
getFrontPage: 'api/marathon/frontPage',
9497
}),

components/marathon/schedule/List.vue

+3-5
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,10 @@ export default Vue.extend({
149149
},
150150
},
151151
mounted(): void {
152-
// Every 5 minutes, forcibly update data
153-
// Eventually, this will be minute-by-minute but not forced, relying on cache expiration
154152
this.interval = setInterval(() => {
155-
this.getSchedule({ id: this.marathonId, forceFetch: true });
156-
this.getScheduleTicker({ id: this.marathonId, forceFetch: true });
157-
}, 300_000);
153+
this.getSchedule(this.marathonId);
154+
this.getScheduleTicker(this.marathonId);
155+
}, 60_000);
158156
this.expandRunHash();
159157
},
160158
destroyed(): void {

components/marathon/sidebar/Sidebar.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export default Vue.extend({
4949
let marathonName = '';
5050
if (this.marathon) {
5151
marathonName = this.marathon.name;
52-
} else if (this.marathonId) {
52+
}
53+
if (!marathonName && this.marathonId) {
5354
const frontPage = (this.$store.state.api.marathon as MarathonState).frontPage;
5455
if (frontPage) {
5556
marathonName = [ ...frontPage.live, ...frontPage.next, ...frontPage.open ].find(marathon => marathon.id === this.marathonId)?.name ?? marathonName;

plugins/oengus.ts

+74-17
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,62 @@ import { ActionContext } from 'vuex';
22
import { NuxtHTTPInstance } from '@nuxt/http';
33
import { Context } from '@nuxt/types';
44

5+
export interface OengusStateCacheable {
6+
_expires: number;
7+
}
8+
export type OengusStateValue<V> = V&OengusStateCacheable|OengusStateCacheable|undefined;
9+
export interface OengusStateValuesById<V> {
10+
[ id: string ]: OengusStateValue<V>;
11+
}
512
export interface OengusState {
6-
[ key: string ]: any;
13+
[ key: string ]: OengusStateValuesById<any>|OengusStateValue<any>;
714
}
815

9-
export interface GetterArgs<U, V> {
16+
/**
17+
* Used to create a specific API endpoint
18+
* U refers to the type retruned by the API
19+
* V refers to the type that ends up in the state and defaults to U
20+
*/
21+
export interface GetterArgs<U, V = U> {
22+
/**
23+
* Final portion of the API path.
24+
* Follows the id, if provided, which in turn follows the basePath.
25+
* e.g. basePath/id/path or basePath/path
26+
*/
1027
path?: string;
28+
/**
29+
* Key these responses are stored in in the state object
30+
* e.g. state[key]
31+
*/
1132
key: string;
12-
transform?(value: U, id?: number|string): V;
33+
/**
34+
* Name of the mutation function to store the responses
35+
* If not provided, uses addKey, e.g. if key is 'users' it picks 'addUsers'
36+
*/
1337
mutation?: string;
38+
/**
39+
* Used to alter the raw API response to whatever is stored in the state
40+
* This can be strip, change, or add new values or even change into a full class
41+
*/
42+
transform?(value: U, id?: number|string): V&OengusStateCacheable;
43+
/**
44+
* How long should responses be cached for
45+
* Can be ignored with forceFetch
46+
* Defaults to 5 minutes
47+
*/
48+
cacheDuration?: number;
1449
}
1550

1651
export interface ExtendedFetch {
1752
id?: number|string;
1853
forceFetch?: boolean;
1954
}
2055

56+
/**
57+
* The actual function returned by a specific API for the action
58+
* T refers to the state
59+
* V refers to the type that ends up in the state
60+
*/
2161
export type GetterFunc<T, V> = (context: ActionContext<T, T>, id?: number|string|ExtendedFetch) => Promise<V|undefined>;
2262

2363
export class OengusAPI<T extends OengusState> {
@@ -30,50 +70,67 @@ export class OengusAPI<T extends OengusState> {
3070
* U is the type returned by the API
3171
* V is the type that will get stored. V is equal to U except when transform is given
3272
*/
33-
public get<U, V = U>({ path, key, transform, mutation }: GetterArgs<U, V>): GetterFunc<T, V> {
73+
public get<U, V = U>(
74+
{ path, key, transform, mutation: _mutation, cacheDuration: _cacheDuration }: GetterArgs<U, V>,
75+
): GetterFunc<T, V> {
76+
const mutation = _mutation ?? `add${key[0].toUpperCase()}${key.slice(1)}`;
77+
// Five minutes unless overridden
78+
const cacheDuration = _cacheDuration ?? 300_000;
3479
return async ({ commit, state }, id) => {
3580
let forceFetch = false;
3681
if (typeof id === 'object') {
3782
forceFetch = id.forceFetch ?? forceFetch;
3883
id = id.id;
3984
}
4085

41-
// Cache check (needs improvements)
42-
let response: U|V = id ? state[key][id] : state[key];
43-
if (response !== undefined && !forceFetch) {
86+
// Cache check
87+
let response: OengusStateValue<U|V> = id ? state[key][id] : state[key];
88+
const cachedTime = response?._expires ?? 0;
89+
if (cachedTime + cacheDuration > Date.now() && !forceFetch) {
4490
return response as V;
4591
}
46-
const updating = response !== undefined;
47-
const resolvedMutation = mutation ?? `add${key[0].toUpperCase()}${key.slice(1)}`;
48-
if (updating) {
92+
93+
if (cachedTime) {
4994
// Mark the entry as updating by changing the cache expired marker
95+
// This allows existing stuff to continue to use the cached value,
96+
commit(mutation, { id, value: { ...response, _expires: Date.now() } });
5097
} else {
51-
// Mark the entry as "being fetched" by marking it `null` (only `undefined` is empty)
52-
commit(resolvedMutation, { id, value: null });
98+
// Mark the entry as "being fetched" by giving it a promise
99+
// Anything that wants to read the value is welcome to await it or use Vuex's observers
100+
commit(mutation, { id, value: { _expires: Date.now() } });
53101
}
54102

55103
// Fetch and store into cache
56104
const route = `${this.basePath}${id ? `/${id}` : ''}${path ? `/${path}` : ''}`;
105+
let apiResponse: U&OengusStateCacheable;
57106
try {
58-
response = await OengusAPI.http.$get(route);
107+
apiResponse = await OengusAPI.http.$get(route);
59108
} catch {
60109
// This isn't intrinsically bad, just catch the error, mark as not fetching, and return nothing
61-
if (updating) {
62-
// What should we do here? Right now, nothing, leave the old value.
110+
if (cachedTime) {
111+
// Reset the old cache timer, maybe the API is just down (should we worry about loops?)
112+
commit(mutation, { id, value: response });
63113
} else {
64-
commit(resolvedMutation, { id, value: undefined });
114+
commit(mutation, { id, value: undefined });
65115
}
66116
return;
67117
}
118+
response = apiResponse;
119+
68120
if (transform) {
69121
response = transform(response as U, id);
70122
}
71-
commit(resolvedMutation, { id, value: response as V });
123+
response._expires = Date.now();
124+
commit(mutation, { id, value: response as OengusStateValue<V> });
72125
return response as V;
73126
};
74127
}
75128
}
76129

130+
/**
131+
* Makes sure anything that OengusAPI needs is created
132+
* All values this write to must be static, so they can be shared among instances
133+
*/
77134
export default function setupOengusAPI({ $http }: Context) {
78135
OengusAPI.http = $http;
79136
}

store/api/user.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ export const mutations: MutationTree<UserState> = {
2525

2626
export const actions: ActionTree<UserState, UserState> = {
2727
get: UserOengusAPI.get<User>({ key: 'users', mutation: 'addUser' }),
28-
exists: UserOengusAPI.get<UserExists, boolean>({
29-
path: 'exists',
30-
key: 'exists',
31-
transform: userExists => !!userExists.exists,
32-
}),
28+
exists: UserOengusAPI.get<UserExists>({ path: 'exists', key: 'exists' }),
3329
search: UserOengusAPI.get<Array<User>>({
3430
path: 'search',
3531
key: 'searches',

types/api/user.d.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ export interface User {
1313
}
1414

1515
export interface UserExists {
16-
exists?: boolean;
17-
}
18-
19-
export interface AddExists {
20-
username: string;
2116
exists: boolean;
2217
}
2318

@@ -28,6 +23,6 @@ export interface AddSearch {
2823

2924
export interface UserState extends OengusState {
3025
users: { [username: string]: User; };
31-
exists: { [username: string]: boolean; };
26+
exists: { [username: string]: UserExists; };
3227
searches: { [query: string]: Array<User>; };
3328
}

0 commit comments

Comments
 (0)