Skip to content

Commit aea499c

Browse files
authored
Merge pull request #1999 from AtCoder-NoviSteps/#1995
♻️ Refactoring for AOJ API client (#1995)
2 parents c9c727d + a9a31ff commit aea499c

File tree

9 files changed

+1130
-510
lines changed

9 files changed

+1130
-510
lines changed

src/lib/clients/aizu_online_judge.ts

Lines changed: 394 additions & 504 deletions
Large diffs are not rendered by default.

src/lib/clients/atcoder_problems.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { ContestSiteApiClient } from '$lib/clients/common';
2-
import { ATCODER_PROBLEMS_API_BASE_URL } from '$lib/constants/urls';
1+
import { ContestSiteApiClient } from '$lib/clients/http_client';
2+
33
import type { ContestsForImport } from '$lib/types/contest';
44
import type { TasksForImport } from '$lib/types/task';
55

6+
import { ATCODER_PROBLEMS_API_BASE_URL } from '$lib/constants/urls';
7+
68
/**
79
* The `AtCoderProblemsApiClient` class provides methods to interact with the AtCoder Problems API.
810
* It extends the `ContestSiteApiClient` class and includes methods to fetch contests and tasks.

src/lib/clients/cache.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/**
2+
* A generic cache class that stores data with a timestamp and provides methods to set, get, and delete cache entries.
3+
* The cache automatically removes the oldest entry when the maximum cache size is reached.
4+
* Entries are also automatically invalidated and removed if they exceed a specified time-to-live (TTL).
5+
*
6+
* @template T - The type of data to be stored in the cache.
7+
*/
8+
export class Cache<T> {
9+
private cache: Map<string, CacheEntry<T>> = new Map();
10+
private cleanupInterval: NodeJS.Timeout;
11+
12+
/**
13+
* Constructs an instance of the class with the specified cache time-to-live (TTL) and maximum cache size.
14+
*
15+
* @param timeToLive - The time-to-live for the cache entries, in milliseconds. Defaults to `CACHE_TTL`.
16+
* @param maxSize - The maximum number of entries the cache can hold. Defaults to `MAX_CACHE_SIZE`.
17+
*/
18+
constructor(
19+
private readonly timeToLive: number = DEFAULT_CACHE_TTL,
20+
private readonly maxSize: number = DEFAULT_MAX_CACHE_SIZE,
21+
) {
22+
if (this.timeToLive <= 0) {
23+
throw new Error('TTL must be positive');
24+
}
25+
if (maxSize <= 0) {
26+
throw new Error('Max size must be positive');
27+
}
28+
29+
this.cleanupInterval = setInterval(() => this.cleanup(), this.timeToLive);
30+
}
31+
32+
/**
33+
* Gets the size of the cache.
34+
*
35+
* @returns {number} The number of items in the cache.
36+
*/
37+
get size(): number {
38+
return this.cache.size;
39+
}
40+
41+
/**
42+
* Retrieves the health status of the cache.
43+
*
44+
* @returns An object containing the size of the cache and the timestamp of the oldest entry.
45+
* @property {number} size - The number of entries in the cache.
46+
* @property {number} oldestEntry - The timestamp of the oldest entry in the cache.
47+
*/
48+
get health(): { size: number; oldestEntry: number } {
49+
if (this.cache.size === 0) {
50+
return { size: 0, oldestEntry: 0 };
51+
}
52+
53+
const oldestEntry = Math.min(
54+
...Array.from(this.cache.values()).map((entry) => entry.timestamp),
55+
);
56+
return { size: this.cache.size, oldestEntry };
57+
}
58+
59+
/**
60+
* Sets a new entry in the cache with the specified key and data.
61+
* If the cache size exceeds the maximum limit, the oldest entry is removed.
62+
*
63+
* @param key - The key associated with the data to be cached.
64+
* @param data - The data to be cached.
65+
*
66+
* @throws {Error} If the key is empty, not a string, or longer than 255 characters.
67+
*/
68+
set(key: string, data: T): void {
69+
if (!key || typeof key !== 'string' || key.length > 255) {
70+
throw new Error('Invalid cache key');
71+
}
72+
73+
// Note: Remove existing entry first to avoid counting it twice.
74+
this.cache.delete(key);
75+
76+
if (this.cache.size >= this.maxSize) {
77+
const oldestKey = this.findOldestEntry();
78+
79+
if (oldestKey) {
80+
this.cache.delete(oldestKey);
81+
}
82+
}
83+
84+
this.cache.set(key, { data, timestamp: Date.now() });
85+
}
86+
87+
/**
88+
* Checks if a key exists in the cache without removing expired entries.
89+
*
90+
* @param key - The key to check.
91+
* @returns True if the key exists in the cache, false otherwise.
92+
*/
93+
has(key: string): boolean {
94+
const entry = this.cache.get(key);
95+
96+
if (!entry) {
97+
return false;
98+
}
99+
100+
if (Date.now() - entry.timestamp > this.timeToLive) {
101+
this.cache.delete(key);
102+
return false;
103+
}
104+
105+
return true;
106+
}
107+
108+
/**
109+
* Retrieves an entry from the cache.
110+
*
111+
* @param key - The key associated with the cache entry.
112+
* @returns The cached data if it exists and is not expired, otherwise `undefined`.
113+
*/
114+
get(key: string): T | undefined {
115+
const entry = this.cache.get(key);
116+
117+
if (!entry) {
118+
return undefined;
119+
}
120+
121+
if (Date.now() - entry.timestamp > this.timeToLive) {
122+
this.cache.delete(key);
123+
return undefined;
124+
}
125+
126+
return entry.data;
127+
}
128+
129+
/**
130+
* Disposes of resources used by the cache instance.
131+
*
132+
* This method clears the interval used for cleanup and clears the cache.
133+
* It should be called when the cache instance is no longer needed to prevent memory leaks.
134+
*/
135+
dispose(): void {
136+
clearInterval(this.cleanupInterval);
137+
this.cache.clear();
138+
}
139+
140+
/**
141+
* Clears all entries from the cache.
142+
*/
143+
clear(): void {
144+
this.cache.clear();
145+
}
146+
147+
/**
148+
* Deletes an entry from the cache.
149+
*
150+
* @param key - The key of the entry to delete.
151+
*/
152+
delete(key: string): void {
153+
this.cache.delete(key);
154+
}
155+
156+
/**
157+
* Removes expired entries from the cache.
158+
* This method is called periodically by the cleanup interval.
159+
*/
160+
private cleanup(): void {
161+
const now = Date.now();
162+
163+
for (const [key, entry] of this.cache.entries()) {
164+
if (now - entry.timestamp > this.timeToLive) {
165+
this.cache.delete(key);
166+
}
167+
}
168+
}
169+
170+
/**
171+
* Finds the key of the oldest entry in the cache based on timestamp.
172+
*
173+
* @returns The key of the oldest entry, or undefined if the cache is empty.
174+
*/
175+
private findOldestEntry(): string | undefined {
176+
let oldestKey: string | undefined;
177+
let oldestTime = Infinity;
178+
179+
for (const [key, entry] of this.cache.entries()) {
180+
if (entry.timestamp < oldestTime) {
181+
oldestTime = entry.timestamp;
182+
oldestKey = key;
183+
}
184+
}
185+
186+
return oldestKey;
187+
}
188+
}
189+
190+
/**
191+
* Represents a cache entry with data and a timestamp.
192+
*
193+
* @template T - The type of the cached data.
194+
* @property {T} data - The cached data.
195+
* @property {number} timestamp - The timestamp when the data was cached.
196+
*/
197+
type CacheEntry<T> = {
198+
data: T;
199+
timestamp: number;
200+
};
201+
202+
/**
203+
* The time-to-live (TTL) for the cache, specified in milliseconds.
204+
* This value represents 1 hour.
205+
*/
206+
const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds
207+
/**
208+
* The default maximum number of entries the cache can hold.
209+
* This value represents 50 entries.
210+
*/
211+
const DEFAULT_MAX_CACHE_SIZE = 50;
212+
213+
/**
214+
* Configuration options for caching.
215+
*
216+
* @property {number} [timeToLive] - The duration (in milliseconds) for which a cache entry should remain valid.
217+
* @property {number} [maxSize] - The maximum number of entries that the cache can hold.
218+
*/
219+
220+
export interface CacheConfig {
221+
timeToLive?: number;
222+
maxSize?: number;
223+
}
224+
225+
/**
226+
* Configuration for the API client's caching behavior.
227+
*
228+
* @interface ApiClientConfig
229+
* @property {CacheConfig} contestCache - Configuration for contest-related data caching.
230+
* @property {CacheConfig} taskCache - Configuration for task-related data caching.
231+
*/
232+
export interface ApiClientConfig {
233+
contestCache: CacheConfig;
234+
taskCache: CacheConfig;
235+
}

src/lib/clients/cache_strategy.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Cache } from '$lib/clients/cache';
2+
3+
import type { ContestsForImport } from '$lib/types/contest';
4+
import type { TasksForImport } from '$lib/types/task';
5+
6+
/**
7+
* A strategy for caching contest and task data.
8+
* Separates the caching logic from the data fetching concerns.
9+
*/
10+
export class ContestTaskCache {
11+
/**
12+
* Constructs a cache strategy with the specified contest and task caches.
13+
* @param contestCache - Cache for storing contest import data
14+
* @param taskCache - Cache for storing task import data
15+
*/
16+
constructor(
17+
private readonly contestCache: Cache<ContestsForImport>,
18+
private readonly taskCache: Cache<TasksForImport>,
19+
) {}
20+
21+
/**
22+
* Retrieves data from cache if available, otherwise fetches it using the provided function.
23+
*
24+
* @template T - The type of data being cached and returned
25+
* @param {string} key - The unique identifier for the cached data
26+
* @param {() => Promise<T>} fetchFunction - Function that returns a Promise resolving to data of type T
27+
* @param {Cache<T>} cache - Cache object with get and set methods for type T
28+
* @returns {Promise<T>} - The cached data or newly fetched data
29+
*
30+
* @example
31+
* const result = await cacheInstance.getCachedOrFetch(
32+
* 'contests-123',
33+
* () => api.fetchContests(),
34+
* contestCache
35+
* );
36+
*/
37+
async getCachedOrFetch<T>(
38+
key: string,
39+
fetchFunction: () => Promise<T>,
40+
cache: Cache<T>,
41+
): Promise<T> {
42+
const cachedData = cache.get(key);
43+
44+
if (cachedData) {
45+
console.log(`Using cached data for ${key}`);
46+
return cachedData;
47+
}
48+
49+
console.log(`Cache miss for ${key}, fetching...`);
50+
51+
try {
52+
const contestTasks = await fetchFunction();
53+
cache.set(key, contestTasks);
54+
55+
return contestTasks;
56+
} catch (error) {
57+
console.error(`Failed to fetch contests and/or tasks for ${key}:`, error);
58+
return [] as unknown as T;
59+
}
60+
}
61+
62+
/**
63+
* Gets contests from cache or fetches them.
64+
*/
65+
async getCachedOrFetchContests(
66+
key: string,
67+
fetchFunction: () => Promise<ContestsForImport>,
68+
): Promise<ContestsForImport> {
69+
return this.getCachedOrFetch(key, fetchFunction, this.contestCache);
70+
}
71+
72+
/**
73+
* Gets tasks from cache or fetches them.
74+
*/
75+
async getCachedOrFetchTasks(
76+
key: string,
77+
fetchFunction: () => Promise<TasksForImport>,
78+
): Promise<TasksForImport> {
79+
return this.getCachedOrFetch(key, fetchFunction, this.taskCache);
80+
}
81+
}

0 commit comments

Comments
 (0)