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
34 changes: 15 additions & 19 deletions packages/relay/src/lib/decorators/cache.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@ interface CacheOptions {
ttl?: number;
}

type IArgument = Record<string, any>;

/**
* Iterates through the provided 'params' array and checks if any argument in 'args' at the specified 'index'
* matches one of the pipe-separated values in 'value'. If a match is found, caching should be skipped.
*
* @param args - The IArguments arguments object
* @param args - The arguments passed to the method in an array
* @param params - An array of CacheSingleParam caching rules
* @returns 'true' if any argument matches a rule and caching should be skipped; otherwise, 'false'.
*
Expand All @@ -42,7 +40,7 @@ type IArgument = Record<string, any>;
* value: 'pending|safe'
* }]
*/
const shouldSkipCachingForSingleParams = (args: IArgument[], params: CacheSingleParam[] = []): boolean => {
const shouldSkipCachingForSingleParams = (args: unknown[], params: CacheSingleParam[] = []): boolean => {
for (const item of params) {
const values = item.value.split('|');
if (values.indexOf(args[item.index]) > -1) {
Expand All @@ -65,7 +63,7 @@ const shouldSkipCachingForSingleParams = (args: IArgument[], params: CacheSingle
* a list of field-based skip conditions and checks if any of the fields in the input argument match any of the provided
* values (supports multiple values via pipe '|' separators).
*
* @param args - The function's arguments object (e.g., `IArguments`), where values are accessed by index.
* @param args - The function's arguments object, where values are accessed by index.
* @param params - An array of `CacheNamedParams` defining which arguments and which fields to inspect.
* @returns `true` if any field value matches a skip condition; otherwise, `false`.
*
Expand All @@ -79,7 +77,7 @@ const shouldSkipCachingForSingleParams = (args: IArgument[], params: CacheSingle
* }],
* }]
*/
const shouldSkipCachingForNamedParams = (args: IArgument[], params: CacheNamedParams[] = []): boolean => {
const shouldSkipCachingForNamedParams = (args: unknown[], params: CacheNamedParams[] = []): boolean => {
for (const { index, fields } of params) {
const input = args[index];

Expand All @@ -91,7 +89,7 @@ const shouldSkipCachingForNamedParams = (args: IArgument[], params: CacheNamedPa
// convert "latest|safe" to ["latest", "safe"]
const allowedValues = value.split('|');
// get the actual value from the input object
const actualValue = (input as IArgument)[key];
const actualValue = input[key];

// if the actual value is one of the values that should skip caching, return true
if (allowedValues.includes(actualValue)) {
Expand All @@ -112,20 +110,18 @@ const shouldSkipCachingForNamedParams = (args: IArgument[], params: CacheNamedPa
* - Arguments of type `RequestDetails` are ignored in the key generation.
*
* @param methodName - The name of the method being cached.
* @param args - The arguments passed to the method (typically from `IArguments`).
* @param args - The arguments passed to the method.
* @returns A string that uniquely identifies the method call for caching purposes.
*
* @example
* generateCacheKey('getBlockByNumber', arguments); // should return getBlockByNumber_0x160c_false
*/
const generateCacheKey = (methodName: string, args: IArgument[]) => {
const generateCacheKey = (methodName: string, args: unknown[]) => {
let cacheKey: string = methodName;
for (const [, value] of Object.entries(args)) {
if (value?.constructor?.name != 'RequestDetails') {
for (const value of args) {
if (!(value instanceof RequestDetails)) {
if (value && typeof value === 'object') {
for (const [key, innerValue] of Object.entries(value)) {
cacheKey += `_${key}_${innerValue}`;
}
cacheKey += `_${JSON.stringify(value)}`;
continue;
}

Expand All @@ -137,17 +133,17 @@ const generateCacheKey = (methodName: string, args: IArgument[]) => {
};

/**
* This utility is used to scan through the provided arguments (typically from `IArguments`)
* This utility is used to scan through the provided arguments.
* and return the first value that is identified as an instance of `RequestDetails`.
*
* If no such instance is found, it returns a new `RequestDetails` object with empty defaults.
*
* @param args - The arguments object from a function (typically `IArguments`).
* @param args - The arguments from a function.
* @returns The first found `RequestDetails` instance, or a new one with default values if none is found.
*/
const extractRequestDetails = (args: IArgument): RequestDetails => {
const extractRequestDetails = (args: unknown[]): RequestDetails => {
for (const [, value] of Object.entries(args)) {
if (value?.constructor?.name === 'RequestDetails') {
if (value instanceof RequestDetails) {
return value;
}
}
Expand Down Expand Up @@ -176,7 +172,7 @@ export function cache(cacheService: CacheService, options: CacheOptions = {}) {
return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;

descriptor.value = async function (...args: IArgument[]) {
descriptor.value = async function (...args: unknown[]) {
const requestDetails = extractRequestDetails(args);
const cacheKey = generateCacheKey(method.name, args);

Expand Down
140 changes: 109 additions & 31 deletions packages/relay/tests/lib/decorators/cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'
import { expect } from 'chai';
import sinon from 'sinon';

import { __test__, cache } from '../../../dist/lib/decorators';
import { CacheService } from '../../../dist/lib/services/cacheService/cacheService';
import { __test__, cache } from '../../../src/lib/decorators';
import { CacheService } from '../../../src/lib/services/cacheService/cacheService';
import { RequestDetails } from '../../../src/lib/types';

describe('cache decorator', () => {
Expand Down Expand Up @@ -107,13 +107,13 @@ describe('cache decorator', () => {

describe('shouldSkipCachingForSingleParams', () => {
it('should return false if no skip rules are provided', () => {
const args = ['safe', 'latest'] as unknown as IArguments;
const args = ['safe', 'latest'];
const result = __test__.__private.shouldSkipCachingForSingleParams(args, []);
expect(result).to.be.false;
});

it('should return false if argument exists but is not in skip values', () => {
const args = ['latest', 'earliest'] as unknown as IArguments;
const args = ['latest', 'earliest'];
const params = [
{ index: '0', value: 'pending' },
{ index: '1', value: 'safe|finalized' },
Expand All @@ -123,21 +123,21 @@ describe('cache decorator', () => {
});

it('should return true if a param at index matches any value in the pipe-separated list', () => {
const args = ['earliest', 'safe'] as unknown as IArguments;
const args = ['earliest', 'safe'];
const params = [{ index: '1', value: 'pending|safe' }];
const result = __test__.__private.shouldSkipCachingForSingleParams(args, params);
expect(result).to.be.true;
});

it('should return true if the argument at index is missing (do not cache optional parameters)', () => {
const args = ['latest'] as unknown as IArguments;
const args = ['latest'];
const params = [{ index: '1', value: 'pending|safe' }];
const result = __test__.__private.shouldSkipCachingForSingleParams(args, params);
expect(result).to.be.true;
});

it('should return true if the argument at index is explicitly undefined', () => {
const args = ['finalized', undefined] as unknown as IArguments;
const args = ['finalized', undefined];
const params = [{ index: '1', value: 'pending|safe' }];
const result = __test__.__private.shouldSkipCachingForSingleParams(args, params);
expect(result).to.be.true;
Expand All @@ -146,13 +146,13 @@ describe('cache decorator', () => {

describe('shouldSkipCachingForNamedParams', () => {
it('should return false when no rules are provided', () => {
const args = [{ fromBlock: 'safe' }] as unknown as IArguments;
const args = [{ fromBlock: 'safe' }];
const result = __test__.__private.shouldSkipCachingForNamedParams(args, []);
expect(result).to.be.false;
});

it('should return false if the field value does not match skip values', () => {
const args = [{ fromBlock: 'confirmed' }] as unknown as IArguments;
const args = [{ fromBlock: 'confirmed' }];
const params = [
{
index: '0',
Expand All @@ -164,7 +164,7 @@ describe('cache decorator', () => {
});

it('should return false if none of the multiple fields match', () => {
const args = [{ fromBlock: 'finalized', toBlock: 'earliest' }] as unknown as IArguments;
const args = [{ fromBlock: 'finalized', toBlock: 'earliest' }];
const params = [
{
index: '0',
Expand All @@ -179,7 +179,7 @@ describe('cache decorator', () => {
});

it('should return true if a field matches one of the skip values', () => {
const args = [{ fromBlock: 'pending' }] as unknown as IArguments;
const args = [{ fromBlock: 'pending' }];
const params = [
{
index: '0',
Expand All @@ -191,7 +191,7 @@ describe('cache decorator', () => {
});

it('should return true if multiple fields are specified and one matches', () => {
const args = [{ fromBlock: 'earliest', toBlock: 'latest' }] as unknown as IArguments;
const args = [{ fromBlock: 'earliest', toBlock: 'latest' }];
const params = [
{
index: '0',
Expand All @@ -208,79 +208,157 @@ describe('cache decorator', () => {

describe('generateCacheKey', () => {
it('should return only the method name when args are empty', () => {
const args = [] as unknown as IArguments;
const args = [];

const result = __test__.__private.generateCacheKey('eth_getBalance', args);
expect(result).to.equal('eth_getBalance');
});

it('should append primitive arguments to the cache key', () => {
const args = ['0xabc', 'latest'] as unknown as IArguments;
const args = ['0xabc', 'latest'];

const result = __test__.__private.generateCacheKey('eth_getBalance', args);
expect(result).to.equal('eth_getBalance_0xabc_latest');
});

it('should append object key-value pairs to the cache key', () => {
const args = [{ fromBlock: 'earliest', toBlock: 5644 }] as unknown as IArguments;
const args = [{ fromBlock: 'earliest', toBlock: 5644 }];

const result = __test__.__private.generateCacheKey('eth_getLogs', args);
expect(result).to.equal('eth_getLogs_fromBlock_earliest_toBlock_5644');
expect(result).to.equal('eth_getLogs_{"fromBlock":"earliest","toBlock":5644}');
});

it('should ignore arguments with constructor name "RequestDetails"', () => {
const mockRequestDetails = {
constructor: { name: 'RequestDetails' },
someField: 'shouldBeIgnored',
};
const args = [mockRequestDetails, 'earliest'] as unknown as IArguments;
const mockRequestDetails = new RequestDetails({ requestId: '1', ipAddress: '127.0.0.1' });
const args = [mockRequestDetails, 'earliest'];

const result = __test__.__private.generateCacheKey('eth_call', args);
expect(result).to.equal('eth_call_earliest');
});

it('should not skip null or undefined args', () => {
const args = [undefined, null, 'pending'] as unknown as IArguments;
const args = [undefined, null, 'pending'];

const result = __test__.__private.generateCacheKey('eth_call', args);
expect(result).to.equal('eth_call_undefined_null_pending');
});

it('should process multiple arguments correctly', () => {
const args = [{ fromBlock: '0xabc' }, 5644, 'safe'] as unknown as IArguments;
const args = [{ fromBlock: '0xabc' }, 5644, 'safe'];

const result = __test__.__private.generateCacheKey('eth_getLogs', args);
expect(result).to.equal('eth_getLogs_fromBlock_0xabc_5644_safe');
expect(result).to.equal('eth_getLogs_{"fromBlock":"0xabc"}_5644_safe');
});

it('should work with mixed types including booleans and numbers', () => {
const args = [true, 42, { fromBlock: 'safe' }] as unknown as IArguments;
const args = [true, 42, { fromBlock: 'safe' }];

const result = __test__.__private.generateCacheKey('custom_method', args);
expect(result).to.equal('custom_method_true_42_fromBlock_safe');
expect(result).to.equal('custom_method_true_42_{"fromBlock":"safe"}');
});

it('should serialize nested objects using JSON.stringify', () => {
const args = [
{
tracer: 'callTracer',
tracerConfig: { onlyTopCall: true },
},
];

const result = __test__.__private.generateCacheKey('debug_traceTransaction', args);
expect(result).to.equal('debug_traceTransaction_{"tracer":"callTracer","tracerConfig":{"onlyTopCall":true}}');
});

it('should differentiate between different nested object values', () => {
const args1 = [
{
tracer: 'callTracer',
tracerConfig: { onlyTopCall: true },
},
];

const args2 = [
{
tracer: 'callTracer',
tracerConfig: { onlyTopCall: false },
},
];

const result1 = __test__.__private.generateCacheKey('debug_traceTransaction', args1);
const result2 = __test__.__private.generateCacheKey('debug_traceTransaction', args2);

expect(result1).to.equal('debug_traceTransaction_{"tracer":"callTracer","tracerConfig":{"onlyTopCall":true}}');
expect(result2).to.equal('debug_traceTransaction_{"tracer":"callTracer","tracerConfig":{"onlyTopCall":false}}');
expect(result1).to.not.equal(result2);
});

it('should handle nested objects with multiple properties', () => {
const args = [
{
config: {
timeout: 5000,
retries: 3,
options: { debug: true },
},
},
];

const result = __test__.__private.generateCacheKey('test_method', args);
expect(result).to.equal('test_method_{"config":{"timeout":5000,"retries":3,"options":{"debug":true}}}');
});

it('should handle nested arrays in objects', () => {
const args = [
{
filter: {
addresses: ['0x123', '0x456'],
topics: [null, '0xabc'],
},
},
];

const result = __test__.__private.generateCacheKey('eth_getLogs', args);
expect(result).to.equal('eth_getLogs_{"filter":{"addresses":["0x123","0x456"],"topics":[null,"0xabc"]}}');
});

it('should handle deeply nested objects', () => {
const args = [
{
level1: {
level2: {
level3: {
value: 'deep',
},
},
},
},
];

const result = __test__.__private.generateCacheKey('deep_method', args);
expect(result).to.equal('deep_method_{"level1":{"level2":{"level3":{"value":"deep"}}}}');
});
});

describe('extractRequestDetails', () => {
it('should return the RequestDetails instance if found in args', () => {
const requestDetails = new RequestDetails({ requestId: 'abc123', ipAddress: '127.0.0.1' });
const args = [5644, requestDetails, 'other'] as unknown as IArguments;
const args = [5644, requestDetails, 'other'];

const result = __test__.__private.extractRequestDetails(args);
expect(result.requestId).to.equal('abc123');
expect(result.ipAddress).to.equal('127.0.0.1');
});

it('should return a new default RequestDetails if not found', () => {
const args = [5644, { fromBlock: 'pending' }, 'value'] as unknown as IArguments;
const args = [5644, { fromBlock: 'pending' }, 'value'];

const result = __test__.__private.extractRequestDetails(args);
expect(result.requestId).to.equal('');
expect(result.ipAddress).to.equal('');
});

it('should return new RequestDetails when args is empty', () => {
const args = [] as unknown as IArguments;
const args = [];

const result = __test__.__private.extractRequestDetails(args);
expect(result.requestId).to.equal('');
Expand All @@ -290,14 +368,14 @@ describe('cache decorator', () => {
it('should return the first RequestDetails instance if multiple are present', () => {
const rd1 = new RequestDetails({ requestId: 'first', ipAddress: '1.1.1.1' });
const rd2 = new RequestDetails({ requestId: 'second', ipAddress: '2.2.2.2' });
const args = [rd1, rd2] as unknown as IArguments;
const args = [rd1, rd2];

const result = __test__.__private.extractRequestDetails(args);
expect(result).to.equal(rd1);
});

it('should handle null or undefined values in args', () => {
const args = [undefined, null, 5644] as unknown as IArguments;
const args = [undefined, null, 5644];

const result = __test__.__private.extractRequestDetails(args);
expect(result.requestId).to.equal('');
Expand Down
Loading