Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deleteReadMatch to CachedReadContract #64

Merged
merged 5 commits into from
Apr 29, 2024
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
5 changes: 5 additions & 0 deletions .changeset/grumpy-carpets-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@delvtech/evm-client": patch
---

Add deleteReadMatch method to CachedReadContract
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"engines": {
"node": ">=18"
},
"resolutions": {
"typescript": "^5.4.5"
},
"packageManager": "[email protected]",
"workspaces": [
"apps/*",
Expand Down
8 changes: 5 additions & 3 deletions packages/evm-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@
}
},
"dependencies": {
"lru-cache": "^10.0.1",
"fast-safe-stringify": "^2.1.1"
"@types/lodash.ismatch": "^4.4.9",
"fast-safe-stringify": "^2.1.1",
"lodash.ismatch": "^4.4.0",
"lru-cache": "^10.0.1"
},
"devDependencies": {
"@repo/eslint-config": "*",
Expand All @@ -80,7 +82,7 @@
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsup": "^8.0.2",
"typescript": "^5.3.3",
"typescript": "^5.4.5",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.2.2"
},
Expand Down
13 changes: 12 additions & 1 deletion packages/evm-client/src/cache/factories/createLruSimpleCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,20 @@ export function createLruSimpleCache<
>(options: LRUCache.Options<string, TValue, void>): SimpleCache<TValue, TKey> {
const cache = new LRUCache(options);

function* entriesGenerator(
originalGenerator: Generator<[TKey, TValue]>,
): Generator<[TKey, TValue]> {
for (const [key, value] of originalGenerator) {
// Modify the entry here before yielding it
const modifiedEntry = [JSON.parse(key as string), value];
yield modifiedEntry as [TKey, TValue];
}
}

return {
get entries() {
return cache.entries() as Generator<[TKey, TValue]>;
// Keys need to be returned in the same format as they were given to the cache
return entriesGenerator(cache.entries() as Generator<[TKey, TValue]>);
},

get(key) {
Expand Down
7 changes: 4 additions & 3 deletions packages/evm-client/src/cache/utils/createSimpleCacheKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type DefinedValue = NonNullable<
* The method ensures that any given raw key, regardless of its structure,
* is converted into a format suitable for consistent cache key referencing.
*
* - For primitives (string, number, boolean), it returns them directly.
* - For scalar (string, number, boolean), it returns them directly.
* - For arrays, it recursively processes each element.
* - For objects, it sorts the keys and then recursively processes each value, ensuring consistent key generation.
* - For other types, it attempts to convert the raw key to a string.
Expand All @@ -28,9 +28,10 @@ export function createSimpleCacheKey(rawKey: DefinedValue): SimpleCacheKey {
case 'object': {
if (Array.isArray(rawKey)) {
return rawKey.map((value) =>
// undefined or null values in arrays are left as is
// undefined or null values are converted to null to follow the
// precedent set by JSON.stringify
value === undefined || value === null
? value
? null
: createSimpleCacheKey(value),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,76 @@ describe('createCachedReadContract', () => {
expect(stub?.callCount).toBe(2);
});

it('deletes cached reads from function name only', async () => {
const contract = new ReadContractStub(ERC20ABI);
const cachedContract = createCachedReadContract({ contract });

contract.stubRead({
functionName: 'balanceOf',
value: 100n,
args: { owner: ALICE },
});
contract.stubRead({
functionName: 'balanceOf',
value: 200n,
args: { owner: BOB },
});

// Get both alice and bob's balance
const aliceValue = await cachedContract.read('balanceOf', { owner: ALICE });
expect(aliceValue).toBe(100n);

const bobValue = await cachedContract.read('balanceOf', { owner: BOB });
expect(bobValue).toBe(200n);

// Deleting anything that matches a balanceOf call
cachedContract.deleteReadMatch('balanceOf');

// Request bob and alice's balance again
const aliceValue2 = await cachedContract.read('balanceOf', {
owner: ALICE,
});
expect(aliceValue2).toBe(100n);
const bobValue2 = await cachedContract.read('balanceOf', { owner: BOB });
expect(bobValue2).toBe(200n);

const stub = contract.getReadStub('balanceOf');
expect(stub?.callCount).toBe(4);
});

it('deletes cached reads with partial args', async () => {
const contract = new ReadContractStub(ERC20ABI);
const cachedContract = createCachedReadContract({ contract });

const aliceArgs = { owner: ALICE, spender: BOB } as const;
contract.stubRead({
functionName: 'allowance',
value: 100n,
args: aliceArgs,
});

const bobArgs = { owner: BOB, spender: ALICE } as const;
contract.stubRead({
functionName: 'allowance',
value: 200n,
args: bobArgs,
});

// Get both alice and bob's allowance
await cachedContract.read('allowance', aliceArgs);
await cachedContract.read('allowance', bobArgs);

// Deleting any allowance calls where BOB is the spender
cachedContract.deleteReadMatch('allowance', { spender: BOB });

// Request bob and alice's allowance again
await cachedContract.read('allowance', aliceArgs);
await cachedContract.read('allowance', bobArgs);

const stub = contract.getReadStub('allowance');
expect(stub?.callCount).toBe(3);
});

it('clears the cache', async () => {
const contract = new ReadContractStub(ERC20ABI);
const cachedContract = createCachedReadContract({ contract });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Abi } from 'abitype';
import isMatch from 'lodash.ismatch';
import { createLruSimpleCache } from 'src/cache/factories/createLruSimpleCache';
import { SimpleCache, SimpleCacheKey } from 'src/cache/types/SimpleCache';
import { createSimpleCacheKey } from 'src/cache/utils/createSimpleCacheKey';
Expand Down Expand Up @@ -61,7 +62,6 @@ export function createCachedReadContract<TAbi extends Abi = Abi>({
options,
},
]),

callback: () => contract.read(functionName, args, options),
});
},
Expand Down Expand Up @@ -92,6 +92,30 @@ export function createCachedReadContract<TAbi extends Abi = Abi>({
cache.delete(key);
},

deleteReadMatch(...args) {
const [functionName, functionArgs, options] = args;

const sourceKey = createSimpleCacheKey([
namespace,
'read',
{
address: contract.address,
functionName,
args: functionArgs,
options,
},
]);

for (const [key] of cache.entries) {
if (
typeof key === 'object' &&
isMatch(key, sourceKey as SimpleCacheKey[])
) {
cache.delete(key);
}
}
},

/**
* Gets events from the contract. First checks the cache, and if not present,
* fetches from the contract and then caches the result.
Expand Down
24 changes: 23 additions & 1 deletion packages/evm-client/src/contract/types/CachedContract.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { Abi } from 'abitype';
import {
ContractReadArgs,
ContractReadOptions,
ReadContract,
ReadWriteContract,
} from 'src/contract/types/Contract';
import { FunctionName } from 'src/contract/types/Function';
import { FunctionArgs, FunctionName } from 'src/contract/types/Function';
import { SimpleCache } from 'src/exports';

interface DeleteReadOptionsObj<

Check warning on line 11 in packages/evm-client/src/contract/types/CachedContract.ts

View workflow job for this annotation

GitHub Actions / verify (lint)

'DeleteReadOptionsObj' is defined but never used
TAbi extends Abi,
TFunctionName extends FunctionName<TAbi>,
> {
functionName: TFunctionName;
args: FunctionArgs<TAbi, TFunctionName>;
options: ContractReadOptions;
partialMatch?: boolean;
}

export interface CachedReadContract<TAbi extends Abi = Abi>
extends ReadContract<TAbi> {
cache: SimpleCache;
Expand All @@ -15,8 +26,19 @@
deleteRead<TFunctionName extends FunctionName<TAbi>>(
...[functionName, args, options]: ContractReadArgs<TAbi, TFunctionName>
): void;
deleteReadMatch<TFunctionName extends FunctionName<TAbi>>(
...[functionName, args, options]: DeepPartial<
ContractReadArgs<TAbi, TFunctionName>
>
): void;
}

export interface CachedReadWriteContract<TAbi extends Abi = Abi>
extends CachedReadContract<TAbi>,
ReadWriteContract<TAbi> {}



type DeepPartial<T> = Partial<{
[K in keyof T]: DeepPartial<T[K]>;
}>;
25 changes: 21 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,18 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==

"@types/lodash.ismatch@^4.4.9":
version "4.4.9"
resolved "https://registry.yarnpkg.com/@types/lodash.ismatch/-/lodash.ismatch-4.4.9.tgz#97b4317f7dc3975bb51660a0f9a055ac7b67b134"
integrity sha512-qWihnStOPKH8urljLGm6ZOEdN/5Bt4vxKR81tL3L4ArUNLvcf9RW3QSnPs21eix5BiqioSWq4aAXD4Iep+d0fw==
dependencies:
"@types/lodash" "*"

"@types/lodash@*":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3"
integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==

"@types/minimist@^1.2.0":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
Expand Down Expand Up @@ -3260,6 +3272,11 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==

lodash.ismatch@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==

lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
Expand Down Expand Up @@ -4755,10 +4772,10 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"

typescript@^5.3.3:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
typescript@^5.3.3, typescript@^5.4.5:
version "5.4.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==

ufo@^1.3.2:
version "1.4.0"
Expand Down
Loading