Skip to content

Commit

Permalink
Add deleteReadMatch to CachedReadContract (#64)
Browse files Browse the repository at this point in the history
* Add isMatch from lodash

* Convert undefined to null in createSimpleCacheKey, parse keys in cache.entries()

* Add deleteReadMatch to CachedReadContract

* Add changeset

* Upgrade typescript version
  • Loading branch information
DannyDelott authored Apr 29, 2024
1 parent c333adc commit 52edea9
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 13 deletions.
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<
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 @@ export interface CachedReadContract<TAbi extends Abi = Abi>
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

0 comments on commit 52edea9

Please sign in to comment.