diff --git a/package.json b/package.json index 42ffadc..d1fb4cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@debros/network-ts-sdk", - "version": "0.2.5", + "version": "0.3.0", "description": "TypeScript SDK for DeBros Network Gateway", "type": "module", "main": "./dist/index.js", diff --git a/src/cache/client.ts b/src/cache/client.ts new file mode 100644 index 0000000..0bbdea2 --- /dev/null +++ b/src/cache/client.ts @@ -0,0 +1,114 @@ +import { HttpClient } from "../core/http"; + +export interface CacheGetRequest { + dmap: string; + key: string; +} + +export interface CacheGetResponse { + key: string; + value: any; + dmap: string; +} + +export interface CachePutRequest { + dmap: string; + key: string; + value: any; + ttl?: string; // Duration string like "1h", "30m" +} + +export interface CachePutResponse { + status: string; + key: string; + dmap: string; +} + +export interface CacheDeleteRequest { + dmap: string; + key: string; +} + +export interface CacheDeleteResponse { + status: string; + key: string; + dmap: string; +} + +export interface CacheScanRequest { + dmap: string; + match?: string; // Optional regex pattern +} + +export interface CacheScanResponse { + keys: string[]; + count: number; + dmap: string; +} + +export interface CacheHealthResponse { + status: string; + service: string; +} + +export class CacheClient { + private httpClient: HttpClient; + + constructor(httpClient: HttpClient) { + this.httpClient = httpClient; + } + + /** + * Check cache service health + */ + async health(): Promise { + return this.httpClient.get("/v1/cache/health"); + } + + /** + * Get a value from cache + */ + async get(dmap: string, key: string): Promise { + return this.httpClient.post("/v1/cache/get", { + dmap, + key, + }); + } + + /** + * Put a value into cache + */ + async put( + dmap: string, + key: string, + value: any, + ttl?: string + ): Promise { + return this.httpClient.post("/v1/cache/put", { + dmap, + key, + value, + ttl, + }); + } + + /** + * Delete a value from cache + */ + async delete(dmap: string, key: string): Promise { + return this.httpClient.post("/v1/cache/delete", { + dmap, + key, + }); + } + + /** + * Scan keys in a distributed map, optionally matching a regex pattern + */ + async scan(dmap: string, match?: string): Promise { + return this.httpClient.post("/v1/cache/scan", { + dmap, + match, + }); + } +} diff --git a/src/index.ts b/src/index.ts index c490fde..9bdae9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { AuthClient } from "./auth/client"; import { DBClient } from "./db/client"; import { PubSubClient } from "./pubsub/client"; import { NetworkClient } from "./network/client"; +import { CacheClient } from "./cache/client"; import { WSClientConfig } from "./core/ws"; import { StorageAdapter, @@ -23,6 +24,7 @@ export interface Client { db: DBClient; pubsub: PubSubClient; network: NetworkClient; + cache: CacheClient; } export function createClient(config: ClientConfig): Client { @@ -52,16 +54,17 @@ export function createClient(config: ClientConfig): Client { wsURL, }); const network = new NetworkClient(httpClient); + const cache = new CacheClient(httpClient); return { auth, db, pubsub, network, + cache, }; } -// Re-exports export { HttpClient } from "./core/http"; export { WSClient } from "./core/ws"; export { AuthClient } from "./auth/client"; @@ -70,6 +73,7 @@ export { QueryBuilder } from "./db/qb"; export { Repository } from "./db/repository"; export { PubSubClient, Subscription } from "./pubsub/client"; export { NetworkClient } from "./network/client"; +export { CacheClient } from "./cache/client"; export { SDKError } from "./errors"; export { MemoryStorage, LocalStorageAdapter } from "./auth/types"; export type { StorageAdapter, AuthConfig, WhoAmI } from "./auth/types"; @@ -86,3 +90,14 @@ export type { ProxyRequest, ProxyResponse, } from "./network/client"; +export type { + CacheGetRequest, + CacheGetResponse, + CachePutRequest, + CachePutResponse, + CacheDeleteRequest, + CacheDeleteResponse, + CacheScanRequest, + CacheScanResponse, + CacheHealthResponse, +} from "./cache/client"; diff --git a/tests/e2e/cache.test.ts b/tests/e2e/cache.test.ts new file mode 100644 index 0000000..ff96c28 --- /dev/null +++ b/tests/e2e/cache.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { createTestClient, skipIfNoGateway } from "./setup"; + +describe("Cache", () => { + if (skipIfNoGateway()) { + console.log("Skipping cache tests - gateway not available"); + return; + } + + const testDMap = "test-cache"; + + beforeEach(async () => { + // Clean up test keys before each test + const client = await createTestClient(); + try { + const keys = await client.cache.scan(testDMap); + for (const key of keys.keys) { + await client.cache.delete(testDMap, key); + } + } catch (err) { + // Ignore errors during cleanup + } + }); + + it("should check cache health", async () => { + const client = await createTestClient(); + const health = await client.cache.health(); + expect(health.status).toBe("ok"); + expect(health.service).toBe("olric"); + }); + + it("should put and get a value", async () => { + const client = await createTestClient(); + const testKey = "test-key-1"; + const testValue = "test-value-1"; + + // Put value + const putResult = await client.cache.put(testDMap, testKey, testValue); + expect(putResult.status).toBe("ok"); + expect(putResult.key).toBe(testKey); + expect(putResult.dmap).toBe(testDMap); + + // Get value + const getResult = await client.cache.get(testDMap, testKey); + expect(getResult.key).toBe(testKey); + expect(getResult.value).toBe(testValue); + expect(getResult.dmap).toBe(testDMap); + }); + + it("should put and get complex objects", async () => { + const client = await createTestClient(); + const testKey = "test-key-2"; + const testValue = { + name: "John", + age: 30, + tags: ["developer", "golang"], + }; + + // Put object + await client.cache.put(testDMap, testKey, testValue); + + // Get object + const getResult = await client.cache.get(testDMap, testKey); + expect(getResult.value).toBeDefined(); + expect(getResult.value.name).toBe(testValue.name); + expect(getResult.value.age).toBe(testValue.age); + }); + + it("should put value with TTL", async () => { + const client = await createTestClient(); + const testKey = "test-key-ttl"; + const testValue = "ttl-value"; + + // Put with TTL + const putResult = await client.cache.put( + testDMap, + testKey, + testValue, + "5m" + ); + expect(putResult.status).toBe("ok"); + + // Verify value exists + const getResult = await client.cache.get(testDMap, testKey); + expect(getResult.value).toBe(testValue); + }); + + it("should delete a value", async () => { + const client = await createTestClient(); + const testKey = "test-key-delete"; + const testValue = "delete-me"; + + // Put value + await client.cache.put(testDMap, testKey, testValue); + + // Verify it exists + const before = await client.cache.get(testDMap, testKey); + expect(before.value).toBe(testValue); + + // Delete value + const deleteResult = await client.cache.delete(testDMap, testKey); + expect(deleteResult.status).toBe("ok"); + expect(deleteResult.key).toBe(testKey); + + // Verify it's deleted + try { + await client.cache.get(testDMap, testKey); + expect.fail("Expected get to fail after delete"); + } catch (err: any) { + expect(err.message).toBeDefined(); + } + }); + + it("should scan keys", async () => { + const client = await createTestClient(); + + // Put multiple keys + await client.cache.put(testDMap, "key-1", "value-1"); + await client.cache.put(testDMap, "key-2", "value-2"); + await client.cache.put(testDMap, "key-3", "value-3"); + + // Scan all keys + const scanResult = await client.cache.scan(testDMap); + expect(scanResult.count).toBeGreaterThanOrEqual(3); + expect(scanResult.keys).toContain("key-1"); + expect(scanResult.keys).toContain("key-2"); + expect(scanResult.keys).toContain("key-3"); + expect(scanResult.dmap).toBe(testDMap); + }); + + it("should scan keys with regex match", async () => { + const client = await createTestClient(); + + // Put keys with different patterns + await client.cache.put(testDMap, "user-1", "value-1"); + await client.cache.put(testDMap, "user-2", "value-2"); + await client.cache.put(testDMap, "session-1", "value-3"); + + // Scan with regex match + const scanResult = await client.cache.scan(testDMap, "^user-"); + expect(scanResult.count).toBeGreaterThanOrEqual(2); + expect(scanResult.keys).toContain("user-1"); + expect(scanResult.keys).toContain("user-2"); + expect(scanResult.keys).not.toContain("session-1"); + }); + + it("should handle non-existent key gracefully", async () => { + const client = await createTestClient(); + const nonExistentKey = "non-existent-key"; + + try { + await client.cache.get(testDMap, nonExistentKey); + expect.fail("Expected get to fail for non-existent key"); + } catch (err: any) { + expect(err.message).toBeDefined(); + } + }); + + it("should handle empty dmap name", async () => { + const client = await createTestClient(); + + try { + await client.cache.get("", "test-key"); + expect.fail("Expected get to fail with empty dmap"); + } catch (err: any) { + expect(err.message).toBeDefined(); + } + }); +});