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

feat: optional UTXO caching #930

Merged
merged 22 commits into from
May 15, 2023
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/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