From 2268dd9bd1f17a585765f649ffe7fb6abbfdca05 Mon Sep 17 00:00:00 2001 From: Ugooweb Date: Thu, 30 Jul 2026 13:56:58 +0100 Subject: [PATCH] sdk: add getRound caching with explicit invalidation --- packages/sdk/README.md | 15 ++++++++ packages/sdk/src/client.test.ts | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index c7f65ea..b0020f9 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -23,3 +23,18 @@ cached for later calls. A mismatch throws `SubRosaNetworkMismatchError` before simulation, signing, or submission, with the conflicting values and a suggested fix. Contract IDs do not encode a Stellar network, so copying a `C...` address between Testnet and Mainnet requires updating all three configuration values. + +## Caching + +You can optionally configure a short-lived cache for `getRound` and `getConfig` reads by providing a `cacheTtl` (in milliseconds): + +```ts +const client = new SubRosaClient({ + // ... other config + cacheTtl: 5000, // 5 seconds +}); +``` + +This reduces avoidable RPC load and latency when dashboards or keeper watch loops re-read the same round repeatedly. + +**Tradeoffs**: Reads may be stale for up to `cacheTtl` milliseconds if another client updates the round. However, the client automatically invalidates its own cache for a round when it successfully executes a state-changing write operation (e.g., `commit`, `reveal`, `settle`) for that round. diff --git a/packages/sdk/src/client.test.ts b/packages/sdk/src/client.test.ts index 4fa21a0..24792a3 100644 --- a/packages/sdk/src/client.test.ts +++ b/packages/sdk/src/client.test.ts @@ -222,3 +222,71 @@ describe("SubRosaClient external submitter failures", () => { }); }); }); + +describe("SubRosaClient caching", () => { + it("caches getRound reads and invalidates on writes", async () => { + let contractCalls = 0; + const client = new SubRosaClient({ + ...BASE_CONFIG, + publicKey: PUBLIC_KEY, + cacheTtl: 60_000, + }); + + Object.defineProperty(client.contract, "get_round", { + configurable: true, + value: async () => { + contractCalls += 1; + return { result: { unwrap: () => ({ round_number: contractCalls }) } }; + }, + }); + + Object.defineProperty(client.contract, "void", { + configurable: true, + value: async () => ({ + async signAndSend() { + return { result: { unwrap: () => {} } }; + }, + }), + }); + + const r1 = await client.getRound(1); + assert.equal((r1 as any).round_number, 1); + assert.equal(contractCalls, 1); + + const r2 = await client.getRound(1); + assert.equal((r2 as any).round_number, 1); + assert.equal(contractCalls, 1, "should use cached result"); + + await client.void(1); + + const r3 = await client.getRound(1); + assert.equal((r3 as any).round_number, 2); + assert.equal(contractCalls, 2, "should fetch new result after invalidation"); + }); + + it("caches getConfig reads", async () => { + let contractCalls = 0; + const client = new SubRosaClient({ + ...BASE_CONFIG, + publicKey: PUBLIC_KEY, + cacheTtl: 60_000, + }); + + Object.defineProperty(client.contract, "get_config", { + configurable: true, + value: async () => { + contractCalls += 1; + return { result: { unwrap: () => ({ version: contractCalls }) } }; + }, + }); + + const c1 = await client.getConfig(); + assert.equal((c1 as any).version, 1); + assert.equal(contractCalls, 1); + + const c2 = await client.getConfig(); + assert.equal((c2 as any).version, 1); + assert.equal(contractCalls, 1, "should use cached config result"); + }); +}); +