Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/euler-v2-sdk/docs/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { buildEulerSDK } from '@eulerxyz/euler-v2-sdk'

// Set EULER_SDK_RPC_URL_1=https://... in the environment for on-chain reads.
const sdk = await buildEulerSDK({
queryCacheConfig: { ttlMs: 10_000 }, // Optional: default cache is 5s
queryCacheConfig: { ttlMs: 10_000 }, // Optional: default success/failure cache is 5s
})
```

Expand Down
10 changes: 7 additions & 3 deletions packages/euler-v2-sdk/docs/caching-external-data-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Pass `buildQuery` once when building the SDK and it propagates to every service

```typescript
const sdk = await buildEulerSDK({
queryCacheConfig: { ttlMs: 5000 }, // Optional: default is enabled with a 5s TTL
queryCacheConfig: { ttlMs: 5000 }, // Optional: default success/failure cache is 5s
buildQuery: myBuildQueryFn,
plugins: [createPythPlugin({ buildQuery: myBuildQueryFn })],
})
Expand All @@ -50,12 +50,16 @@ Configure the built-in cache through `queryCacheConfig`:
```typescript
const sdk = await buildEulerSDK({
queryCacheConfig: {
enabled: true, // default
ttlMs: 5000, // default
enabled: true, // default
ttlMs: 5000, // default success TTL
failureTtlMs: 5000, // default failure TTL
},
})
```

Set `failureTtlMs: 0` when a consumer needs every failed query call to reach
the underlying transport immediately.

Disable it entirely:

```typescript
Expand Down
4 changes: 2 additions & 2 deletions packages/euler-v2-sdk/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Use explicit nested service config when the option is a function or a custom obj
| `rewardsServiceConfig` | `v3` adapter with direct Brevis/Fuul helper reads | Reward campaign data, per-user rewards, and reward claim planning |
| `intrinsicApyServiceConfig` | V3 intrinsic APY API | Underlying yield data for vault assets |
| `buildQuery` | 5s in-memory cache | Wrap all external queries for caching, logging, or profiling |
| `queryCacheConfig` | `{ enabled: true, ttlMs: 5000 }` | Built-in query cache settings when `buildQuery` is not supplied |
| `queryCacheConfig` | `{ enabled: true, ttlMs: 5000, failureTtlMs: 5000 }` | Built-in query cache settings when `buildQuery` is not supplied |
| `plugins` | `[]` | Extend on-chain reads and transaction plans |
| `servicesOverrides` | `{}` | Replace any built-in service with a custom implementation |

Expand All @@ -84,7 +84,7 @@ When `accountServiceConfig.adapter`, `eVaultServiceConfig.adapter`, `eulerEarnSe

Adapter-specific keys override the shared key within the same configuration layer. Layer priority still applies, so `config.pricingApiKey` overrides `pricingServiceConfig.apiKey`, and `pricingServiceConfig.apiKey` overrides `EULER_SDK_PRICING_API_KEY`.

`vaultTypeAdapterConfig` defaults to the V3 `POST /v3/evk/vaults/resolve` endpoint. Pass subgraph config when vault type resolution should use subgraphs:
`vaultTypeAdapterConfig` defaults to the V3 `POST /v3/resolve/vaults` endpoint. Pass subgraph config when vault type resolution should use subgraphs:

```typescript
vaultTypeAdapterConfig: {
Expand Down
2 changes: 1 addition & 1 deletion packages/euler-v2-sdk/docs/data-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ See [Cross-Service Data Population](./cross-service-data-population.md) for the
The resolved query decorator is selected as:

1. Consumer-provided `buildQuery`, if present
2. Otherwise the built-in in-memory cache from `queryCacheConfig` (enabled by default with `ttlMs: 5000`)
2. Otherwise the built-in in-memory cache from `queryCacheConfig` (enabled by default with `ttlMs: 5000` and `failureTtlMs: 5000`)

This means custom query decorators replace the SDK's default cache rather than layering on top of it automatically.

Expand Down
5 changes: 3 additions & 2 deletions packages/euler-v2-sdk/src/sdk/buildSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export interface BuildSDKOptions<
intrinsicApyServiceConfig?: IntrinsicApyServiceConfig;
oracleAdapterServiceConfig?: OracleAdapterServiceConfig;
feeFlowServiceConfig?: FeeFlowServiceConfig;
/** Default in-memory cache applied to all decorated `query*` methods. Enabled by default with a 5s TTL. */
/** Default in-memory cache applied to all decorated `query*` methods. Enabled by default with 5s success and failure TTLs. */
queryCacheConfig?: QueryCacheConfig;
/** Optional query decorator applied to all query* functions across all services. Use for global logging, caching, profiling, etc. */
buildQuery?: BuildQueryFn;
Expand Down Expand Up @@ -1362,7 +1362,8 @@ export async function buildEulerSDK<
rewardsV3Adapter.fetchUserRewards(chainId, address),
]);
if (v3Result.status !== "fulfilled") {
if (directResult.status === "fulfilled") return directResult.value;
if (directResult.status === "fulfilled")
return directResult.value;
throw directResult.reason;
}
const directRewards =
Expand Down
17 changes: 15 additions & 2 deletions packages/euler-v2-sdk/src/utils/buildQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ export type BuildQueryFn = <T extends (...args: any[]) => Promise<any>>(
export interface QueryCacheConfig {
enabled?: boolean;
ttlMs?: number;
failureTtlMs?: number;
}

const DEFAULT_QUERY_CACHE_TTL_MS = 5_000;
const DEFAULT_QUERY_FAILURE_TTL_MS = 5_000;

function normalizeAddress(value: string): string {
if (/^0x[0-9a-fA-F]{40}$/.test(value)) {
Expand Down Expand Up @@ -108,6 +110,7 @@ export function createQueryCacheBuildQuery(
): BuildQueryFn {
const enabled = config?.enabled ?? true;
const ttlMs = config?.ttlMs ?? DEFAULT_QUERY_CACHE_TTL_MS;
const failureTtlMs = config?.failureTtlMs ?? DEFAULT_QUERY_FAILURE_TTL_MS;

return <T extends (...args: any[]) => Promise<any>>(
queryName: string,
Expand All @@ -123,11 +126,13 @@ export function createQueryCacheBuildQuery(
expiresAt: number;
value?: Awaited<ReturnType<T>>;
promise?: Promise<Awaited<ReturnType<T>>>;
error?: unknown;
}
>();

const wrapped = (async (...args: Parameters<T>) => {
const cacheKey = context?.getCacheKey(args) ?? getEulerSdkQueryKey(queryName, args);
const cacheKey =
context?.getCacheKey(args) ?? getEulerSdkQueryKey(queryName, args);
if (cacheKey === null) {
return fn(...args);
}
Expand All @@ -137,6 +142,7 @@ export function createQueryCacheBuildQuery(
if (cached && cached.expiresAt > now) {
if (cached.promise) return cached.promise;
if ("value" in cached) return cached.value as Awaited<ReturnType<T>>;
if ("error" in cached) throw cached.error;
}

const promise = fn(...args)
Expand All @@ -150,7 +156,14 @@ export function createQueryCacheBuildQuery(
.catch((error) => {
const current = cache.get(cacheKey);
if (current?.promise === promise) {
cache.delete(cacheKey);
if (failureTtlMs > 0) {
cache.set(cacheKey, {
error,
expiresAt: Date.now() + failureTtlMs,
});
} else {
cache.delete(cacheKey);
}
}
throw error;
});
Expand Down
45 changes: 43 additions & 2 deletions packages/euler-v2-sdk/test/readPathInfra.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function makeDeployment(chainId = 1) {
} as const;
}

test("buildQuery cache dedupes, clears rejected promises, and decorate query methods", async () => {
test("buildQuery cache dedupes, short-caches failures, and decorate query methods", async () => {
let runs = 0;
const cached = createQueryCacheBuildQuery({ ttlMs: 60_000 })(
"queryExample",
Expand All @@ -114,7 +114,48 @@ test("buildQuery cache dedupes, clears rejected promises, and decorate query met

await assert.rejects(() => cached("boom"), /boom/);
await assert.rejects(() => cached("boom"), /boom/);
assert.equal(runs, 6);
assert.equal(runs, 5);

const retryableFailures = createQueryCacheBuildQuery({
failureTtlMs: 0,
ttlMs: 60_000,
})(
"queryRetryableFailures",
async () => {
runs += 1;
throw new Error("retryable");
},
{},
);
await assert.rejects(() => retryableFailures(), /retryable/);
await assert.rejects(() => retryableFailures(), /retryable/);
assert.equal(runs, 7);

const originalNow = Date.now;
try {
let now = 1_000;
Date.now = () => now;
let cooledRuns = 0;
const cooledFailures = createQueryCacheBuildQuery({
failureTtlMs: 10,
ttlMs: 60_000,
})(
"queryCooledFailures",
async () => {
cooledRuns += 1;
throw new Error("cooled");
},
{},
);
await assert.rejects(() => cooledFailures(), /cooled/);
await assert.rejects(() => cooledFailures(), /cooled/);
assert.equal(cooledRuns, 1);
now += 11;
await assert.rejects(() => cooledFailures(), /cooled/);
assert.equal(cooledRuns, 2);
} finally {
Date.now = originalNow;
}

const passthrough = createQueryCacheBuildQuery({ enabled: false })(
"queryDisabled",
Expand Down