mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 20:37:21 +00:00
reorganized storage adapter and added test suites for adapter and fields (#124)
* reorganized storage adapter and added test suites for adapter and fields * added build command in ci pipeline * updated workflow to also run node tests * updated workflow: try with separate tasks * updated workflow: try with separate tasks * updated workflow: added tsx as dev dependency * updated workflow: try with find instead of glob
This commit is contained in:
127
app/src/core/cache/adapters/CloudflareKvCache.ts
vendored
127
app/src/core/cache/adapters/CloudflareKvCache.ts
vendored
@@ -1,127 +0,0 @@
|
||||
import type { ICacheItem, ICachePool } from "../cache-interface";
|
||||
|
||||
export class CloudflareKVCachePool<Data = any> implements ICachePool<Data> {
|
||||
constructor(private namespace: KVNamespace) {}
|
||||
|
||||
supports = () => ({
|
||||
metadata: true,
|
||||
clear: false,
|
||||
});
|
||||
|
||||
async get(key: string): Promise<ICacheItem<Data>> {
|
||||
const result = await this.namespace.getWithMetadata<any>(key);
|
||||
const hit = result.value !== null && typeof result.value !== "undefined";
|
||||
// Assuming metadata is not supported directly;
|
||||
// you may adjust if Cloudflare KV supports it in future.
|
||||
return new CloudflareKVCacheItem(key, result.value ?? undefined, hit, result.metadata) as any;
|
||||
}
|
||||
|
||||
async getMany(keys: string[] = []): Promise<Map<string, ICacheItem<Data>>> {
|
||||
const items = new Map<string, ICacheItem<Data>>();
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
const item = await this.get(key);
|
||||
items.set(key, item);
|
||||
}),
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
const data = await this.namespace.get(key);
|
||||
return data !== null;
|
||||
}
|
||||
|
||||
async clear(): Promise<boolean> {
|
||||
// Cloudflare KV does not support clearing all keys in one operation
|
||||
return false;
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
await this.namespace.delete(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteMany(keys: string[]): Promise<boolean> {
|
||||
const results = await Promise.all(keys.map((key) => this.delete(key)));
|
||||
return results.every((result) => result);
|
||||
}
|
||||
|
||||
async save(item: CloudflareKVCacheItem<Data>): Promise<boolean> {
|
||||
await this.namespace.put(item.key(), (await item.value()) as string, {
|
||||
expirationTtl: item._expirationTtl,
|
||||
metadata: item.metadata(),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async put(
|
||||
key: string,
|
||||
value: any,
|
||||
options?: { ttl?: number; expiresAt?: Date; metadata?: Record<string, string> },
|
||||
): Promise<boolean> {
|
||||
const item = new CloudflareKVCacheItem(key, value, true, options?.metadata);
|
||||
|
||||
if (options?.expiresAt) item.expiresAt(options.expiresAt);
|
||||
if (options?.ttl) item.expiresAfter(options.ttl);
|
||||
|
||||
return await this.save(item);
|
||||
}
|
||||
}
|
||||
|
||||
export class CloudflareKVCacheItem<Data = any> implements ICacheItem<Data> {
|
||||
_expirationTtl: number | undefined;
|
||||
|
||||
constructor(
|
||||
private _key: string,
|
||||
private data: Data | undefined,
|
||||
private _hit: boolean = false,
|
||||
private _metadata: Record<string, string> = {},
|
||||
) {}
|
||||
|
||||
key(): string {
|
||||
return this._key;
|
||||
}
|
||||
|
||||
value(): Data | undefined {
|
||||
if (this.data) {
|
||||
try {
|
||||
return JSON.parse(this.data as string);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return this.data ?? undefined;
|
||||
}
|
||||
|
||||
metadata(): Record<string, string> {
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
hit(): boolean {
|
||||
return this._hit;
|
||||
}
|
||||
|
||||
set(value: Data, metadata: Record<string, string> = {}): this {
|
||||
this.data = value;
|
||||
this._metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
expiresAt(expiration: Date | null): this {
|
||||
// Cloudflare KV does not support specific date expiration; calculate ttl instead.
|
||||
if (expiration) {
|
||||
const now = new Date();
|
||||
const ttl = (expiration.getTime() - now.getTime()) / 1000;
|
||||
return this.expiresAfter(Math.max(0, Math.floor(ttl)));
|
||||
}
|
||||
return this.expiresAfter(null);
|
||||
}
|
||||
|
||||
expiresAfter(time: number | null): this {
|
||||
// Dummy implementation as Cloudflare KV requires setting expiration during PUT operation.
|
||||
// This method will be effectively implemented in the Cache Pool save methods.
|
||||
this._expirationTtl = time ?? undefined;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
139
app/src/core/cache/adapters/MemoryCache.ts
vendored
139
app/src/core/cache/adapters/MemoryCache.ts
vendored
@@ -1,139 +0,0 @@
|
||||
import type { ICacheItem, ICachePool } from "../cache-interface";
|
||||
|
||||
export class MemoryCache<Data = any> implements ICachePool<Data> {
|
||||
private cache: Map<string, MemoryCacheItem<Data>> = new Map();
|
||||
private maxSize?: number;
|
||||
|
||||
constructor(options?: { maxSize?: number }) {
|
||||
this.maxSize = options?.maxSize;
|
||||
}
|
||||
|
||||
supports = () => ({
|
||||
metadata: true,
|
||||
clear: true,
|
||||
});
|
||||
|
||||
async get(key: string): Promise<MemoryCacheItem<Data>> {
|
||||
if (!this.cache.has(key)) {
|
||||
// use undefined to denote a miss initially
|
||||
return new MemoryCacheItem<Data>(key, undefined!);
|
||||
}
|
||||
return this.cache.get(key)!;
|
||||
}
|
||||
|
||||
async getMany(keys: string[] = []): Promise<Map<string, MemoryCacheItem<Data>>> {
|
||||
const items = new Map<string, MemoryCacheItem<Data>>();
|
||||
for (const key of keys) {
|
||||
items.set(key, await this.get(key));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async has(key: string): Promise<boolean> {
|
||||
return this.cache.has(key) && this.cache.get(key)!.hit();
|
||||
}
|
||||
|
||||
async clear(): Promise<boolean> {
|
||||
this.cache.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<boolean> {
|
||||
return this.cache.delete(key);
|
||||
}
|
||||
|
||||
async deleteMany(keys: string[]): Promise<boolean> {
|
||||
let success = true;
|
||||
for (const key of keys) {
|
||||
if (!this.delete(key)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
async save(item: MemoryCacheItem<Data>): Promise<boolean> {
|
||||
this.checkSizeAndPurge();
|
||||
this.cache.set(item.key(), item);
|
||||
return true;
|
||||
}
|
||||
|
||||
async put(
|
||||
key: string,
|
||||
value: Data,
|
||||
options: { expiresAt?: Date; ttl?: number; metadata?: Record<string, string> } = {},
|
||||
): Promise<boolean> {
|
||||
const item = await this.get(key);
|
||||
item.set(value, options.metadata || {});
|
||||
if (options.expiresAt) {
|
||||
item.expiresAt(options.expiresAt);
|
||||
} else if (typeof options.ttl === "number") {
|
||||
item.expiresAfter(options.ttl);
|
||||
}
|
||||
return this.save(item);
|
||||
}
|
||||
|
||||
private checkSizeAndPurge(): void {
|
||||
if (!this.maxSize) return;
|
||||
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
// Implement logic to purge items, e.g., LRU (Least Recently Used)
|
||||
// For simplicity, clear the oldest item inserted
|
||||
const keyToDelete = this.cache.keys().next().value;
|
||||
this.cache.delete(keyToDelete!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryCacheItem<Data = any> implements ICacheItem<Data> {
|
||||
private _key: string;
|
||||
private _value: Data | undefined;
|
||||
private expiration: Date | null = null;
|
||||
private _metadata: Record<string, string> = {};
|
||||
|
||||
constructor(key: string, value: Data, metadata: Record<string, string> = {}) {
|
||||
this._key = key;
|
||||
this.set(value, metadata);
|
||||
}
|
||||
|
||||
key(): string {
|
||||
return this._key;
|
||||
}
|
||||
|
||||
metadata(): Record<string, string> {
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
value(): Data | undefined {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
hit(): boolean {
|
||||
if (this.expiration !== null && new Date() > this.expiration) {
|
||||
return false;
|
||||
}
|
||||
return this.value() !== undefined;
|
||||
}
|
||||
|
||||
set(value: Data, metadata: Record<string, string> = {}): this {
|
||||
this._value = value;
|
||||
this._metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
expiresAt(expiration: Date | null): this {
|
||||
this.expiration = expiration;
|
||||
return this;
|
||||
}
|
||||
|
||||
expiresAfter(time: number | null): this {
|
||||
if (typeof time === "number") {
|
||||
const expirationDate = new Date();
|
||||
expirationDate.setSeconds(expirationDate.getSeconds() + time);
|
||||
this.expiration = expirationDate;
|
||||
} else {
|
||||
this.expiration = null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
178
app/src/core/cache/cache-interface.ts
vendored
178
app/src/core/cache/cache-interface.ts
vendored
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* CacheItem defines an interface for interacting with objects inside a cache.
|
||||
* based on https://www.php-fig.org/psr/psr-6/
|
||||
*/
|
||||
export interface ICacheItem<Data = any> {
|
||||
/**
|
||||
* Returns the key for the current cache item.
|
||||
*
|
||||
* The key is loaded by the Implementing Library, but should be available to
|
||||
* the higher level callers when needed.
|
||||
*
|
||||
* @returns The key string for this cache item.
|
||||
*/
|
||||
key(): string;
|
||||
|
||||
/**
|
||||
* Retrieves the value of the item from the cache associated with this object's key.
|
||||
*
|
||||
* The value returned must be identical to the value originally stored by set().
|
||||
*
|
||||
* If isHit() returns false, this method MUST return null. Note that null
|
||||
* is a legitimate cached value, so the isHit() method SHOULD be used to
|
||||
* differentiate between "null value was found" and "no value was found."
|
||||
*
|
||||
* @returns The value corresponding to this cache item's key, or undefined if not found.
|
||||
*/
|
||||
value(): Data | undefined;
|
||||
|
||||
/**
|
||||
* Retrieves the metadata of the item from the cache associated with this object's key.
|
||||
*/
|
||||
metadata(): Record<string, string>;
|
||||
|
||||
/**
|
||||
* Confirms if the cache item lookup resulted in a cache hit.
|
||||
*
|
||||
* Note: This method MUST NOT have a race condition between calling isHit()
|
||||
* and calling get().
|
||||
*
|
||||
* @returns True if the request resulted in a cache hit. False otherwise.
|
||||
*/
|
||||
hit(): boolean;
|
||||
|
||||
/**
|
||||
* Sets the value represented by this cache item.
|
||||
*
|
||||
* The value argument may be any item that can be serialized by PHP,
|
||||
* although the method of serialization is left up to the Implementing
|
||||
* Library.
|
||||
*
|
||||
* @param value The serializable value to be stored.
|
||||
* @param metadata The metadata to be associated with the item.
|
||||
* @returns The invoked object.
|
||||
*/
|
||||
set(value: Data, metadata?: Record<string, string>): this;
|
||||
|
||||
/**
|
||||
* Sets the expiration time for this cache item.
|
||||
*
|
||||
* @param expiration The point in time after which the item MUST be considered expired.
|
||||
* If null is passed explicitly, a default value MAY be used. If none is set,
|
||||
* the value should be stored permanently or for as long as the
|
||||
* implementation allows.
|
||||
* @returns The called object.
|
||||
*/
|
||||
expiresAt(expiration: Date | null): this;
|
||||
|
||||
/**
|
||||
* Sets the expiration time for this cache item.
|
||||
*
|
||||
* @param time The period of time from the present after which the item MUST be considered
|
||||
* expired. An integer parameter is understood to be the time in seconds until
|
||||
* expiration. If null is passed explicitly, a default value MAY be used.
|
||||
* If none is set, the value should be stored permanently or for as long as the
|
||||
* implementation allows.
|
||||
* @returns The called object.
|
||||
*/
|
||||
expiresAfter(time: number | null): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CachePool generates CacheItem objects.
|
||||
* based on https://www.php-fig.org/psr/psr-6/
|
||||
*/
|
||||
export interface ICachePool<Data = any> {
|
||||
supports(): {
|
||||
metadata: boolean;
|
||||
clear: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a Cache Item representing the specified key.
|
||||
* This method must always return a CacheItemInterface object, even in case of
|
||||
* a cache miss. It MUST NOT return null.
|
||||
*
|
||||
* @param key The key for which to return the corresponding Cache Item.
|
||||
* @throws Error If the key string is not a legal value an Error MUST be thrown.
|
||||
* @returns The corresponding Cache Item.
|
||||
*/
|
||||
get(key: string): Promise<ICacheItem<Data>>;
|
||||
|
||||
/**
|
||||
* Returns a traversable set of cache items.
|
||||
*
|
||||
* @param keys An indexed array of keys of items to retrieve.
|
||||
* @throws Error If any of the keys in keys are not a legal value an Error MUST be thrown.
|
||||
* @returns A traversable collection of Cache Items keyed by the cache keys of
|
||||
* each item. A Cache item will be returned for each key, even if that
|
||||
* key is not found. However, if no keys are specified then an empty
|
||||
* traversable MUST be returned instead.
|
||||
*/
|
||||
getMany(keys?: string[]): Promise<Map<string, ICacheItem<Data>>>;
|
||||
|
||||
/**
|
||||
* Confirms if the cache contains specified cache item.
|
||||
*
|
||||
* Note: This method MAY avoid retrieving the cached value for performance reasons.
|
||||
* This could result in a race condition with CacheItemInterface.get(). To avoid
|
||||
* such situation use CacheItemInterface.isHit() instead.
|
||||
*
|
||||
* @param key The key for which to check existence.
|
||||
* @throws Error If the key string is not a legal value an Error MUST be thrown.
|
||||
* @returns True if item exists in the cache, false otherwise.
|
||||
*/
|
||||
has(key: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Deletes all items in the pool.
|
||||
* @returns True if the pool was successfully cleared. False if there was an error.
|
||||
*/
|
||||
clear(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Removes the item from the pool.
|
||||
*
|
||||
* @param key The key to delete.
|
||||
* @throws Error If the key string is not a legal value an Error MUST be thrown.
|
||||
* @returns True if the item was successfully removed. False if there was an error.
|
||||
*/
|
||||
delete(key: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Removes multiple items from the pool.
|
||||
*
|
||||
* @param keys An array of keys that should be removed from the pool.
|
||||
* @throws Error If any of the keys in keys are not a legal value an Error MUST be thrown.
|
||||
* @returns True if the items were successfully removed. False if there was an error.
|
||||
*/
|
||||
deleteMany(keys: string[]): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Persists a cache item immediately.
|
||||
*
|
||||
* @param item The cache item to save.
|
||||
* @returns True if the item was successfully persisted. False if there was an error.
|
||||
*/
|
||||
save(item: ICacheItem<Data>): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Persists any deferred cache items.
|
||||
* @returns True if all not-yet-saved items were successfully saved or there were none. False otherwise.
|
||||
*/
|
||||
put(
|
||||
key: string,
|
||||
value: any,
|
||||
options?: { expiresAt?: Date; metadata?: Record<string, string> },
|
||||
): Promise<boolean>;
|
||||
put(
|
||||
key: string,
|
||||
value: any,
|
||||
options?: { ttl?: number; metadata?: Record<string, string> },
|
||||
): Promise<boolean>;
|
||||
put(
|
||||
key: string,
|
||||
value: any,
|
||||
options?: ({ ttl?: number } | { expiresAt?: Date }) & { metadata?: Record<string, string> },
|
||||
): Promise<boolean>;
|
||||
}
|
||||
48
app/src/core/test/index.ts
Normal file
48
app/src/core/test/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
export type Matcher<T = unknown> = {
|
||||
toEqual: (expected: T, failMsg?: string) => void;
|
||||
toBe: (expected: T, failMsg?: string) => void;
|
||||
toBeUndefined: (failMsg?: string) => void;
|
||||
toBeString: (failMsg?: string) => void;
|
||||
toBeOneOf: (expected: T | Array<T> | Iterable<T>, failMsg?: string) => void;
|
||||
toBeDefined: (failMsg?: string) => void;
|
||||
};
|
||||
export type TestFn = (() => void | Promise<unknown>) | ((done: (err?: unknown) => void) => void);
|
||||
export interface Test {
|
||||
(label: string, fn: TestFn, options?: any): void;
|
||||
if: (condition: boolean) => (label: string, fn: TestFn, options?: any) => void;
|
||||
skip: (label: string, fn: () => void) => void;
|
||||
skipIf: (condition: boolean) => (label: string, fn: TestFn) => void;
|
||||
}
|
||||
export type TestRunner = {
|
||||
test: Test;
|
||||
expect: <T = unknown>(
|
||||
actual?: T,
|
||||
failMsg?: string,
|
||||
) => Matcher<T> & {
|
||||
resolves: Matcher<Awaited<T>>;
|
||||
rejects: Matcher<Awaited<T>>;
|
||||
};
|
||||
};
|
||||
|
||||
export async function retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
condition: (result: T) => boolean,
|
||||
retries: number,
|
||||
delay: number,
|
||||
): Promise<T> {
|
||||
let lastError: Error | null = null;
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
const result = await fn();
|
||||
if (condition(result)) {
|
||||
return result;
|
||||
} else {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
Reference in New Issue
Block a user