Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
68 changes: 68 additions & 0 deletions packages/sdk/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});