Skip to content

Commit 3ae32d1

Browse files
committed
♻️ Extract cache and helpers from AOJ API client (#1995)
1 parent a4d5fbe commit 3ae32d1

File tree

2 files changed

+194
-185
lines changed

2 files changed

+194
-185
lines changed

src/lib/clients/aizu_online_judge.ts

Lines changed: 2 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { ContestSiteApiClient } from '$lib/clients/common';
22
import { AOJ_API_BASE_URL } from '$lib/constants/urls';
3+
import { Cache, type ApiClientConfig } from '$lib/clients/cache';
4+
35
import type { ContestForImport, ContestsForImport } from '$lib/types/contest';
46
import type { TasksForImport } from '$lib/types/task';
57

@@ -105,191 +107,6 @@ type ChallengeRoundMap = {
105107
*/
106108
const PENDING = -1;
107109

108-
/**
109-
* The time-to-live (TTL) for the cache, specified in milliseconds.
110-
* This value represents 1 hour.
111-
*/
112-
const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds
113-
const DEFAULT_MAX_CACHE_SIZE = 50;
114-
115-
/**
116-
* Configuration options for caching.
117-
*
118-
* @property {number} [timeToLive] - The duration (in milliseconds) for which a cache entry should remain valid.
119-
* @property {number} [maxSize] - The maximum number of entries that the cache can hold.
120-
*/
121-
interface CacheConfig {
122-
timeToLive?: number;
123-
maxSize?: number;
124-
}
125-
126-
/**
127-
* Represents a cache entry with data and a timestamp.
128-
*
129-
* @template T - The type of the cached data.
130-
* @property {T} data - The cached data.
131-
* @property {number} timestamp - The timestamp when the data was cached.
132-
*/
133-
type CacheEntry<T> = {
134-
data: T;
135-
timestamp: number;
136-
};
137-
138-
/**
139-
* A generic cache class that stores data with a timestamp and provides methods to set, get, and delete cache entries.
140-
* The cache automatically removes the oldest entry when the maximum cache size is reached.
141-
* Entries are also automatically invalidated and removed if they exceed a specified time-to-live (TTL).
142-
*
143-
* @template T - The type of data to be stored in the cache.
144-
*/
145-
class Cache<T> {
146-
private cache: Map<string, CacheEntry<T>> = new Map();
147-
private cleanupInterval: NodeJS.Timeout;
148-
149-
/**
150-
* Constructs an instance of the class with the specified cache time-to-live (TTL) and maximum cache size.
151-
*
152-
* @param timeToLive - The time-to-live for the cache entries, in milliseconds. Defaults to `CACHE_TTL`.
153-
* @param maxSize - The maximum number of entries the cache can hold. Defaults to `MAX_CACHE_SIZE`.
154-
*/
155-
constructor(
156-
private readonly timeToLive: number = DEFAULT_CACHE_TTL,
157-
private readonly maxSize: number = DEFAULT_MAX_CACHE_SIZE,
158-
) {
159-
if (timeToLive <= 0) {
160-
throw new Error('TTL must be positive');
161-
}
162-
if (maxSize <= 0) {
163-
throw new Error('Max size must be positive');
164-
}
165-
166-
this.cleanupInterval = setInterval(() => this.cleanup(), timeToLive);
167-
}
168-
169-
/**
170-
* Gets the size of the cache.
171-
*
172-
* @returns {number} The number of items in the cache.
173-
*/
174-
get size(): number {
175-
return this.cache.size;
176-
}
177-
178-
/**
179-
* Retrieves the health status of the cache.
180-
*
181-
* @returns An object containing the size of the cache and the timestamp of the oldest entry.
182-
* @property {number} size - The number of entries in the cache.
183-
* @property {number} oldestEntry - The timestamp of the oldest entry in the cache.
184-
*/
185-
get health(): { size: number; oldestEntry: number } {
186-
const oldestEntry = Math.min(
187-
...Array.from(this.cache.values()).map((entry) => entry.timestamp),
188-
);
189-
return { size: this.cache.size, oldestEntry };
190-
}
191-
192-
/**
193-
* Sets a new entry in the cache with the specified key and data.
194-
* If the cache size exceeds the maximum limit, the oldest entry is removed.
195-
*
196-
* @param key - The key associated with the data to be cached.
197-
* @param data - The data to be cached.
198-
*/
199-
set(key: string, data: T): void {
200-
if (!key || typeof key !== 'string' || key.length > 255) {
201-
throw new Error('Invalid cache key');
202-
}
203-
204-
if (this.cache.size >= this.maxSize) {
205-
const oldestKey = this.findOldestEntry();
206-
207-
if (oldestKey) {
208-
this.cache.delete(oldestKey);
209-
}
210-
}
211-
212-
this.cache.set(key, { data, timestamp: Date.now() });
213-
}
214-
215-
/**
216-
* Retrieves an entry from the cache.
217-
*
218-
* @param key - The key associated with the cache entry.
219-
* @returns The cached data if it exists and is not expired, otherwise `undefined`.
220-
*/
221-
get(key: string): T | undefined {
222-
const entry = this.cache.get(key);
223-
224-
if (!entry) {
225-
return undefined;
226-
}
227-
228-
if (Date.now() - entry.timestamp > this.timeToLive) {
229-
this.cache.delete(key);
230-
return undefined;
231-
}
232-
233-
return entry.data;
234-
}
235-
236-
/**
237-
* Disposes of resources used by the Aizu Online Judge client.
238-
*
239-
* This method clears the interval used for cleanup and clears the cache.
240-
* It should be called when the client is no longer needed to prevent memory leaks.
241-
*/
242-
dispose(): void {
243-
clearInterval(this.cleanupInterval);
244-
this.cache.clear();
245-
}
246-
247-
/**
248-
* Clears all entries from the cache.
249-
*/
250-
clear(): void {
251-
this.cache.clear();
252-
}
253-
254-
/**
255-
* Deletes an entry from the cache.
256-
*
257-
* @param key - The key of the entry to delete.
258-
*/
259-
delete(key: string): void {
260-
this.cache.delete(key);
261-
}
262-
263-
private cleanup(): void {
264-
const now = Date.now();
265-
266-
for (const [key, entry] of this.cache.entries()) {
267-
if (now - entry.timestamp > this.timeToLive) {
268-
this.cache.delete(key);
269-
}
270-
}
271-
}
272-
273-
private findOldestEntry(): string | undefined {
274-
let oldestKey: string | undefined;
275-
let oldestTime = Infinity;
276-
277-
for (const [key, entry] of this.cache.entries()) {
278-
if (entry.timestamp < oldestTime) {
279-
oldestTime = entry.timestamp;
280-
oldestKey = key;
281-
}
282-
}
283-
284-
return oldestKey;
285-
}
286-
}
287-
288-
interface ApiClientConfig {
289-
contestCache: CacheConfig;
290-
taskCache: CacheConfig;
291-
}
292-
293110
/**
294111
* AojApiClient is a client for interacting with the Aizu Online Judge (AOJ) API.
295112
* It extends the ContestSiteApiClient and provides methods to fetch contests and tasks

src/lib/clients/cache.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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 (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(), 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+
const oldestEntry = Math.min(
50+
...Array.from(this.cache.values()).map((entry) => entry.timestamp),
51+
);
52+
return { size: this.cache.size, oldestEntry };
53+
}
54+
55+
/**
56+
* Sets a new entry in the cache with the specified key and data.
57+
* If the cache size exceeds the maximum limit, the oldest entry is removed.
58+
*
59+
* @param key - The key associated with the data to be cached.
60+
* @param data - The data to be cached.
61+
*/
62+
set(key: string, data: T): void {
63+
if (!key || typeof key !== 'string' || key.length > 255) {
64+
throw new Error('Invalid cache key');
65+
}
66+
67+
if (this.cache.size >= this.maxSize) {
68+
const oldestKey = this.findOldestEntry();
69+
70+
if (oldestKey) {
71+
this.cache.delete(oldestKey);
72+
}
73+
}
74+
75+
this.cache.set(key, { data, timestamp: Date.now() });
76+
}
77+
78+
/**
79+
* Retrieves an entry from the cache.
80+
*
81+
* @param key - The key associated with the cache entry.
82+
* @returns The cached data if it exists and is not expired, otherwise `undefined`.
83+
*/
84+
get(key: string): T | undefined {
85+
const entry = this.cache.get(key);
86+
87+
if (!entry) {
88+
return undefined;
89+
}
90+
91+
if (Date.now() - entry.timestamp > this.timeToLive) {
92+
this.cache.delete(key);
93+
return undefined;
94+
}
95+
96+
return entry.data;
97+
}
98+
99+
/**
100+
* Disposes of resources used by the Aizu Online Judge client.
101+
*
102+
* This method clears the interval used for cleanup and clears the cache.
103+
* It should be called when the client is no longer needed to prevent memory leaks.
104+
*/
105+
dispose(): void {
106+
clearInterval(this.cleanupInterval);
107+
this.cache.clear();
108+
}
109+
110+
/**
111+
* Clears all entries from the cache.
112+
*/
113+
clear(): void {
114+
this.cache.clear();
115+
}
116+
117+
/**
118+
* Deletes an entry from the cache.
119+
*
120+
* @param key - The key of the entry to delete.
121+
*/
122+
delete(key: string): void {
123+
this.cache.delete(key);
124+
}
125+
126+
private cleanup(): void {
127+
const now = Date.now();
128+
129+
for (const [key, entry] of this.cache.entries()) {
130+
if (now - entry.timestamp > this.timeToLive) {
131+
this.cache.delete(key);
132+
}
133+
}
134+
}
135+
136+
private findOldestEntry(): string | undefined {
137+
let oldestKey: string | undefined;
138+
let oldestTime = Infinity;
139+
140+
for (const [key, entry] of this.cache.entries()) {
141+
if (entry.timestamp < oldestTime) {
142+
oldestTime = entry.timestamp;
143+
oldestKey = key;
144+
}
145+
}
146+
147+
return oldestKey;
148+
}
149+
}
150+
151+
/**
152+
* Represents a cache entry with data and a timestamp.
153+
*
154+
* @template T - The type of the cached data.
155+
* @property {T} data - The cached data.
156+
* @property {number} timestamp - The timestamp when the data was cached.
157+
*/
158+
type CacheEntry<T> = {
159+
data: T;
160+
timestamp: number;
161+
};
162+
163+
/**
164+
* The time-to-live (TTL) for the cache, specified in milliseconds.
165+
* This value represents 1 hour.
166+
*/
167+
const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds
168+
const DEFAULT_MAX_CACHE_SIZE = 50;
169+
170+
/**
171+
* Configuration options for caching.
172+
*
173+
* @property {number} [timeToLive] - The duration (in milliseconds) for which a cache entry should remain valid.
174+
* @property {number} [maxSize] - The maximum number of entries that the cache can hold.
175+
*/
176+
177+
interface CacheConfig {
178+
timeToLive?: number;
179+
maxSize?: number;
180+
}
181+
182+
/**
183+
* Configuration for the API client's caching behavior.
184+
*
185+
* @interface ApiClientConfig
186+
* @property {CacheConfig} contestCache - Configuration for contest-related data caching.
187+
* @property {CacheConfig} taskCache - Configuration for task-related data caching.
188+
*/
189+
export interface ApiClientConfig {
190+
contestCache: CacheConfig;
191+
taskCache: CacheConfig;
192+
}

0 commit comments

Comments
 (0)