Skip to content

Commit

Permalink
feat: optional UTXO caching (FuelLabs#930)
Browse files Browse the repository at this point in the history
* use cache here

* add coverage

* add coverage

* cs

* actually test

* lower ttl

* fix test

* pr review tweaks

* rewrite using safeExec

* refactor

---------

Co-authored-by: Anderson Arboleya <[email protected]>
  • Loading branch information
Cameron Manavian and arboleya authored May 15, 2023
1 parent 52d1b14 commit 5b0ce1c
Show file tree
Hide file tree
Showing 8 changed files with 595 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-peas-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/providers": patch
---

Added optional caching
2 changes: 1 addition & 1 deletion packages/fuel-gauge/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const deployContract = async (factory: ContractFactory, useCache: boolean = true
let walletInstance: WalletUnlocked;
const createWallet = async () => {
if (walletInstance) return walletInstance;
const provider = new Provider('http://127.0.0.1:4000/graphql');
const provider = new Provider('http://127.0.0.1:4000/graphql', { cacheUtxo: 10 });
walletInstance = await generateTestWallet(provider, [
[5_000_000, NativeAssetId],
[5_000_000, '0x0101010101010101010101010101010101010101010101010101010101010101'],
Expand Down
1 change: 1 addition & 0 deletions packages/providers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"lodash.clonedeep": "^4.5.0"
},
"devDependencies": {
"@fuel-ts/utils": "workspace:*",
"@graphql-codegen/cli": "^2.13.7",
"@graphql-codegen/typescript": "^2.8.0",
"@graphql-codegen/typescript-graphql-request": "^4.5.7",
Expand Down
129 changes: 129 additions & 0 deletions packages/providers/src/memory-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { BytesLike } from '@ethersproject/bytes';
import { hexlify } from '@ethersproject/bytes';
import { randomBytes } from '@fuel-ts/keystore';

import { MemoryCache } from './memory-cache';

const CACHE_ITEMS = [hexlify(randomBytes(8)), randomBytes(8), randomBytes(8)];

describe('Memory Cache', () => {
it('can construct [valid numerical ttl]', () => {
const memCache = new MemoryCache(1000);

expect(memCache.ttl).toEqual(1000);
});

it('can construct [invalid numerical ttl]', () => {
expect(() => new MemoryCache(-1)).toThrow(/Invalid TTL: -1. Use a value greater than zero./);
});

it('can construct [invalid mistyped ttl]', () => {
// @ts-expect-error intentional invalid input
expect(() => new MemoryCache('bogus')).toThrow(
/Invalid TTL: bogus. Use a value greater than zero./
);
});

it('can construct [missing ttl]', () => {
const memCache = new MemoryCache();

expect(memCache.ttl).toEqual(30_000);
});

it('can get [unknown key]', () => {
const memCache = new MemoryCache(1000);

expect(
memCache.get('0xda5d131c490db33333333333333333334444444444444444444455555555556666')
).toEqual(undefined);
});

it('can get active [no data]', () => {
const EXPECTED: BytesLike[] = [];
const memCache = new MemoryCache(100);

expect(memCache.getActiveData()).toStrictEqual(EXPECTED);
});

it('can set', () => {
const ttl = 1000;
const expiresAt = Date.now() + ttl;
const memCache = new MemoryCache(ttl);

expect(memCache.set(CACHE_ITEMS[0])).toBeGreaterThanOrEqual(expiresAt);
});

it('can get [valid key]', () => {
const KEY = CACHE_ITEMS[1];
const memCache = new MemoryCache(100);

memCache.set(KEY);

expect(memCache.get(KEY)).toEqual(KEY);
});

it('can get [valid key bytes like]', () => {
const KEY = CACHE_ITEMS[2];
const memCache = new MemoryCache(100);

memCache.set(KEY);

expect(memCache.get(KEY)).toEqual(KEY);
});

it('can get [valid key, expired content]', async () => {
const KEY = randomBytes(8);
const memCache = new MemoryCache(1);

memCache.set(KEY);

await new Promise((resolve) => {
setTimeout(resolve, 10);
});

expect(memCache.get(KEY)).toEqual(undefined);
});

it('can get, disabling auto deletion [valid key, expired content]', async () => {
const KEY = randomBytes(8);
const memCache = new MemoryCache(1);

memCache.set(KEY);

await new Promise((resolve) => {
setTimeout(resolve, 10);
});

expect(memCache.get(KEY, false)).toEqual(KEY);
});

it('can delete', () => {
const KEY = randomBytes(8);
const memCache = new MemoryCache(100);

memCache.set(KEY);
memCache.del(KEY);

expect(memCache.get(KEY)).toEqual(undefined);
});

it('can get active [with data]', () => {
const EXPECTED: BytesLike[] = [CACHE_ITEMS[0], CACHE_ITEMS[1], CACHE_ITEMS[2]];
const memCache = new MemoryCache(100);

expect(memCache.getActiveData()).toStrictEqual(EXPECTED);
});

it('can get all [with data + expired data]', async () => {
const KEY = randomBytes(8);
const EXPECTED: BytesLike[] = [CACHE_ITEMS[0], CACHE_ITEMS[1], CACHE_ITEMS[2], KEY];
const memCache = new MemoryCache(1);
memCache.set(KEY);

await new Promise((resolve) => {
setTimeout(resolve, 10);
});

expect(memCache.getAllData()).toStrictEqual(EXPECTED);
});
});
74 changes: 74 additions & 0 deletions packages/providers/src/memory-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { BytesLike } from '@ethersproject/bytes';
import { hexlify } from '@ethersproject/bytes';

type Cache = {
[key: string]: {
expires: number;
value: BytesLike;
};
};
const cache: Cache = {}; // it's a cache hash ~~> cash?

const DEFAULT_TTL_IN_MS = 30 * 1000; // 30seconds

export class MemoryCache {
ttl: number;
constructor(ttlInMs: number = DEFAULT_TTL_IN_MS) {
this.ttl = ttlInMs;

if (typeof ttlInMs !== 'number' || this.ttl <= 0) {
throw new Error(`Invalid TTL: ${this.ttl}. Use a value greater than zero.`);
}
}

get(value: BytesLike, isAutoExpiring = true): BytesLike | undefined {
const key = hexlify(value);
if (cache[key]) {
if (!isAutoExpiring || cache[key].expires > Date.now()) {
return cache[key].value;
}

this.del(value);
}

return undefined;
}

set(value: BytesLike): number {
const expiresAt = Date.now() + this.ttl;
const key = hexlify(value);
cache[key] = {
expires: expiresAt,
value,
};

return expiresAt;
}

getAllData(): BytesLike[] {
return Object.keys(cache).reduce((list, key) => {
const data = this.get(key, false);
if (data) {
list.push(data);
}

return list;
}, [] as BytesLike[]);
}

getActiveData(): BytesLike[] {
return Object.keys(cache).reduce((list, key) => {
const data = this.get(key);
if (data) {
list.push(data);
}

return list;
}, [] as BytesLike[]);
}

del(value: BytesLike) {
const key = hexlify(value);
delete cache[key];
}
}
Loading

0 comments on commit 5b0ce1c

Please sign in to comment.