From 6f91093cb0655749074c66e46b9ea021db83105a Mon Sep 17 00:00:00 2001 From: ranjeet2063 Date: Sun, 6 Sep 2026 05:12:22 +0545 Subject: [PATCH] feat(factory): make negative-cache TTL configurable via ConduitConfig (closes #602) --- README.md | 24 ++++++++++++++++-- src/factory.ts | 13 +++++++--- src/index.ts | 5 ++-- src/tests/factory.test.ts | 51 +++++++++++++++++++++++++++++++++++++++ src/types/index.ts | 5 ++++ 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9768445..c2bd0d9 100644 --- a/README.md +++ b/README.md @@ -784,7 +784,27 @@ The SDK can be configured via environment variables or explicit constructor opti --- +--- + +## ConduitConfig Reference + +| Field | Type | Default | Effect | +|---|---|---|---| +| network | `'mainnet' \| 'testnet' \| 'local'` | (required) | Which Stellar network to connect to. | +| keypair | `Keypair` | undefined | Signing keypair used for mutating operations. | +| wallet | `WalletAdapter` | undefined | Browser/mobile wallet adapter (e.g. WalletConnect). | +| signer | `Signer` | undefined | Custom signer plugin (KMS/HSM). Takes precedence over keypair. | +| rpcUrl | `string` | Network default | Override the default Soroban RPC endpoint. | +| factoryAddress | `string` | Network default | Override the deployed DripFactory contract ID. | +| governorAddress | `string` | Network default | Override the deployed DripGovernor contract ID. | +| confirmationPollIntervalMs | `number` | 1000 | Poll interval for transaction confirmation. | +| confirmationMaxAttempts | `number` | 30 | Maximum confirmation polling attempts. | +| fee | `string` | undefined | Explicit inclusion fee in stroops. Takes precedence over feeMultiplier. | +| feeMultiplier | `number` | 1 | Multiplier applied to BASE_FEE when fee is not set. | +| negativeAddressCacheTtlMs | `number` | 30000 | Negative cache TTL in ms for null streamAddress queries in FactoryModule. | + +--- + ## License -MIT — see [`LICENSE`](./LICENSE). -\n## ConduitConfig reference\n\n| Field | Type | Default | Effect |\n|---|---|---|---|\n| network | \`mainnet | testnet | local\` | (required) | Which Stellar network to connect to. |\n| keypair | \`Keypair\` | undefined | Signing keypair used for mutating operations. |\n| wallet | \`WalletAdapter\` | undefined | Browser/mobile wallet adapter (e.g. WalletConnect). |\n| signer | \`Signer\` | undefined | Custom signer plugin (KMS/HSM). Takes precedence over keypair. |\n| rpcUrl | \`string\` | Network default | Override the default Soroban RPC endpoint. |\n| factoryAddress | \`string\` | Network default | Override the deployed DripFactory contract ID. |\n| governorAddress | \`string\` | Network default | Override the deployed DripGovernor contract ID. |\n| confirmationPollIntervalMs | \`number\` | 1000 | Poll interval for transaction confirmation. |\n| confirmationMaxAttempts | \`number\` | 30 | Maximum confirmation polling attempts. |\n| fee | \`string\` | undefined | Explicit inclusion fee in stroops. Takes precedence over feeMultiplier. |\n| feeMultiplier | \`number\` | 1 | Multiplier applied to BASE_FEE when fee is not set. |\n\n \ No newline at end of file +MIT — see [`LICENSE`](./LICENSE). \ No newline at end of file diff --git a/src/factory.ts b/src/factory.ts index 0980f52..fccc6bc 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -24,12 +24,13 @@ import { SUPPORTED_NETWORKS, UnsupportedChainError } from './errors.js'; * `clearAddressCache()` (#568). A *found* address is immutable and cached * for the module's lifetime. */ -const NEGATIVE_ADDRESS_CACHE_TTL_MS = 30_000; +export const DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS = 30_000; export class FactoryModule { private readonly rpcUrl: string; private readonly passphrase: string; private readonly factoryId: string; + private readonly negativeAddressCacheTtlMs: number; /** * Active wallet adapter, if the client was configured with `wallet` (or a @@ -47,9 +48,9 @@ export class FactoryModule { // streamId -> contract address. A resolved (non-null) address is immutable // and cached for the module's lifetime; a `null` result is cached with a - // short TTL (see NEGATIVE_ADDRESS_CACHE_TTL_MS) so a dashboard polling + // configurable TTL (see DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS) so a dashboard polling // list() over a page with a few archived/pending ids does not re-issue a - // stream_address simulation for each of them on every refresh (#568). + // stream_address simulation for each of them on every refresh (#568, #602). private readonly addressCache = new Map(); private readonly negativeCacheExpiry = new Map(); @@ -60,6 +61,10 @@ export class FactoryModule { if (!(SUPPORTED_NETWORKS as readonly string[]).includes(config.network)) { throw new UnsupportedChainError(config.network); } + this.negativeAddressCacheTtlMs = + config.negativeAddressCacheTtlMs !== undefined + ? Math.max(0, config.negativeAddressCacheTtlMs) + : DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS; this.rpcUrl = config.rpcUrl ?? DEFAULT_RPC[config.network]; this.passphrase = NETWORK_PASSPHRASE[config.network]; // There is no known default DripFactory deployment for any network — @@ -181,7 +186,7 @@ export class FactoryModule { private _cacheNegative(key: string): void { this.addressCache.set(key, null); - this.negativeCacheExpiry.set(key, Date.now() + NEGATIVE_ADDRESS_CACHE_TTL_MS); + this.negativeCacheExpiry.set(key, Date.now() + this.negativeAddressCacheTtlMs); } /** diff --git a/src/index.ts b/src/index.ts index 403282e..170a8c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,9 +73,10 @@ export { timeoutSignal, } from './utils.js'; -// RPC server lifecycle -export { getServer, clearServerCache } from './soroban.js'; +// RPC server lifecycle & fee resolution +export { getServer, clearServerCache, resolveFee } from './soroban.js'; export { getTokenDecimals, clearTokenDecimalsCache } from './soroban.js'; +export { FactoryModule, DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS } from './factory.js'; export { formatAddress, diff --git a/src/tests/factory.test.ts b/src/tests/factory.test.ts index 1d59336..2ded9cd 100644 --- a/src/tests/factory.test.ts +++ b/src/tests/factory.test.ts @@ -191,6 +191,57 @@ describe('FactoryModule — streamAddress()', () => { await factory.streamAddress(1n); expect(mockSimulate).toHaveBeenCalledTimes(2); }); + + it('honours custom negativeAddressCacheTtlMs and expires after the configured duration (#602)', async () => { + const { FactoryModule } = await import('../factory.js'); + mockSimulate + .mockResolvedValueOnce(makeVoidScVal()) + .mockResolvedValueOnce(makeU32ScVal(1)); + + // Configure a short 30ms negative cache TTL + const factory = new FactoryModule({ + ...cfg(), + negativeAddressCacheTtlMs: 30, + }); + + const first = await factory.streamAddress(99n); + expect(first).toBeNull(); + expect(mockSimulate).toHaveBeenCalledTimes(1); + + // Immediate repeat query is served from negative cache + const second = await factory.streamAddress(99n); + expect(second).toBeNull(); + expect(mockSimulate).toHaveBeenCalledTimes(1); + + // Wait for TTL to expire + await new Promise((resolve) => setTimeout(resolve, 45)); + + // Third query after TTL expiration triggers a fresh resolution + const third = await factory.streamAddress(99n); + expect(third).not.toBeNull(); + expect(mockSimulate).toHaveBeenCalledTimes(2); + }); + + it('immediately re-queries when negativeAddressCacheTtlMs is 0 (#602)', async () => { + const { FactoryModule } = await import('../factory.js'); + mockSimulate + .mockResolvedValueOnce(makeVoidScVal()) + .mockResolvedValueOnce(makeVoidScVal()); + + const factory = new FactoryModule({ + ...cfg(), + negativeAddressCacheTtlMs: 0, + }); + + const first = await factory.streamAddress(101n); + expect(first).toBeNull(); + expect(mockSimulate).toHaveBeenCalledTimes(1); + + // With 0ms TTL, next query expires immediately and queries RPC + const second = await factory.streamAddress(101n); + expect(second).toBeNull(); + expect(mockSimulate).toHaveBeenCalledTimes(2); + }); }); describe('FactoryModule — cache consolidation with StreamsModule', () => { diff --git a/src/types/index.ts b/src/types/index.ts index 3be6bef..324bdda 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -50,6 +50,11 @@ export interface ConduitConfig { * Ignored when `fee` is set. Defaults to `1` (BASE_FEE, unchanged). */ feeMultiplier?: number; + /** + * Negative cache TTL in milliseconds for null (not-found) streamAddress queries in FactoryModule. + * Defaults to 30,000 ms (30 seconds) (see #602). + */ + negativeAddressCacheTtlMs?: number; } export interface StreamInfo {