-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented a simple SQL based TTL cache
- Loading branch information
1 parent
6471773
commit d1abd91
Showing
10 changed files
with
234 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -286,11 +286,13 @@ Configuration can be set using environment variables | |
| `DWN_REGISTRATION_PROOF_OF_WORK_SEED` | Seed to generate the challenge nonce from, this allows all DWN instances in a cluster to generate the same challenge. | unset | | ||
| `DWN_REGISTRATION_PROOF_OF_WORK_ENABLED` | Require new users to complete a proof-of-work challenge | `false` | | ||
| `DWN_REGISTRATION_PROOF_OF_WORK_INITIAL_MAX_HASH` | Initial maximum allowed hash in 64 char HEX string. The more leading zeros (smaller number) the higher the difficulty. | `false` | | ||
| `DWN_TERMS_OF_SERVICE_FILE_PATH` | Required terms of service agreement if set. Value is path to the terms of service file. | unset | | ||
| `DWN_STORAGE` | URL to use for storage by default. See [Storage Options](#storage-options) for details | `level://data` | | ||
| `DWN_STORAGE_MESSAGES` | URL to use for storage of messages. | value of `DWN_STORAGE` | | ||
| `DWN_STORAGE_DATA` | URL to use for data storage | value of `DWN_STORAGE` | | ||
| `DWN_STORAGE_EVENTS` | URL to use for event storage | value of `DWN_STORAGE` | | ||
| `DWN_TERMS_OF_SERVICE_FILE_PATH` | Required terms of service agreement if set. Value is path to the terms of service file. | unset | | ||
| `DWN_TTL_CACHE_URL` | URL of the TTL cache used by the DWN. Currently only supports SQL databases. | `sqlite://` | | ||
|
||
|
||
Check failure on line 296 in README.md GitHub Actions / lintMultiple consecutive blank lines
|
||
### Storage Options | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import type { Dialect } from '@tbd54566975/dwn-sql-store'; | ||
import { Kysely } from 'kysely'; | ||
|
||
/** | ||
* The SqlTtlCache is responsible for storing and retrieving cache data with TTL (Time-to-Live). | ||
*/ | ||
export class SqlTtlCache { | ||
private static readonly cacheTableName = 'cacheEntries'; | ||
private static readonly cleanupIntervalInSeconds = 60; | ||
|
||
private db: Kysely<CacheDatabase>; | ||
private cleanupTimer: NodeJS.Timeout; | ||
|
||
private constructor(sqlDialect: Dialect) { | ||
this.db = new Kysely<CacheDatabase>({ dialect: sqlDialect }); | ||
} | ||
|
||
/** | ||
* Creates a new SqlTtlCache instance. | ||
*/ | ||
public static async create(sqlDialect: Dialect): Promise<SqlTtlCache> { | ||
const cacheManager = new SqlTtlCache(sqlDialect); | ||
|
||
await cacheManager.initialize(); | ||
|
||
return cacheManager; | ||
} | ||
|
||
private async initialize(): Promise<void> { | ||
await this.db.schema | ||
.createTable(SqlTtlCache.cacheTableName) | ||
.ifNotExists() | ||
.addColumn('key', 'text', (column) => column.primaryKey()) | ||
.addColumn('value', 'text') | ||
.addColumn('expiry', 'integer') | ||
.execute(); | ||
|
||
// Add an index to the expiry column | ||
await this.db.schema | ||
.createIndex('index_expiry') | ||
.ifNotExists() | ||
.on(SqlTtlCache.cacheTableName) | ||
.column('expiry') | ||
.execute(); | ||
|
||
// Start the cleanup timer | ||
this.startCleanupTimer(); | ||
} | ||
|
||
/** | ||
* Starts a timer to periodically clean up expired cache entries. | ||
*/ | ||
private startCleanupTimer(): void { | ||
this.cleanupTimer = setInterval(async () => { | ||
await this.cleanUpExpiredEntries(); | ||
}, SqlTtlCache.cleanupIntervalInSeconds * 1000); | ||
} | ||
|
||
/** | ||
* Inserts a cache entry. | ||
* @param ttl The time-to-live in seconds. | ||
*/ | ||
public async insert(key: string, value: object, ttl: number): Promise<void> { | ||
const expiry = Date.now() + (ttl * 1000); | ||
|
||
const objectString = JSON.stringify(value); | ||
|
||
await this.db | ||
.insertInto(SqlTtlCache.cacheTableName) | ||
.values({ key, value: objectString, expiry }) | ||
.execute(); | ||
} | ||
|
||
/** | ||
* Retrieves a cache entry if it is not expired and cleans up expired entries. | ||
*/ | ||
public async get(key: string): Promise<object | undefined> { | ||
// clean up expired entries but no need to await for it to finish | ||
this.cleanUpExpiredEntries(); | ||
|
||
const result = await this.db | ||
.selectFrom(SqlTtlCache.cacheTableName) | ||
.select('key') | ||
.select('value') | ||
.select('expiry') | ||
.where('key', '=', key) | ||
.execute(); | ||
|
||
if (result.length === 0) { | ||
return undefined; | ||
} | ||
|
||
const entry = result[0]; | ||
|
||
// if the entry is expired, don't return it and delete it | ||
if (Date.now() >= entry.expiry) { | ||
this.delete(key); // no need to await | ||
return undefined; | ||
} | ||
|
||
return JSON.parse(entry.value); | ||
} | ||
|
||
/** | ||
* Deletes a cache entry. | ||
*/ | ||
public async delete(key: string): Promise<void> { | ||
await this.db | ||
.deleteFrom(SqlTtlCache.cacheTableName) | ||
.where('key', '=', key) | ||
.execute(); | ||
} | ||
|
||
/** | ||
* Periodically clean up expired cache entries. | ||
*/ | ||
public async cleanUpExpiredEntries(): Promise<void> { | ||
await this.db | ||
.deleteFrom(SqlTtlCache.cacheTableName) | ||
.where('expiry', '<', Date.now()) | ||
.execute(); | ||
} | ||
} | ||
|
||
interface CacheEntry { | ||
key: string; | ||
value: string; | ||
expiry: number; | ||
} | ||
|
||
interface CacheDatabase { | ||
cacheEntries: CacheEntry; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.