Skip to content

Commit

Permalink
fix(arns): change from a Set of names to a Map for ArNSNameCache
Browse files Browse the repository at this point in the history
This allows us to `get` the name if we have it, while persisting the debounce. This pattern will align better with our ANT cache and avoids overloading `has` and allows use to use additional data of the base ArNS record in the future - specficially when we start enforcing undername limits.
  • Loading branch information
dtfiedler committed Jan 9, 2025
1 parent 5170e11 commit e5630e7
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 98 deletions.
2 changes: 1 addition & 1 deletion src/init/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const createArNSResolver = ({
return new CompositeArNSResolver({
log,
resolvers,
cache,
resolutionCache: cache,
overrides,
});
};
131 changes: 101 additions & 30 deletions src/resolution/arns-names-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ const createMockNetworkProcess = () => {
items: [
{
name: `name-${callCount}-1`,
processId: 'process-1',
},
{
name: `name-${callCount}-2`,
processId: 'process-2',
},
{
name: `name-${callCount}-3`,
processId: 'process-3',
},
],
nextCursor: undefined,
Expand All @@ -64,7 +67,14 @@ describe('ArNSNamesCache', () => {
});

const names = await cache.getNames();
assert.deepEqual(names, new Set(['name-1-1', 'name-1-2', 'name-1-3']));
assert.deepEqual(
names,
new Map([
['name-1-1', { processId: 'process-1' }],
['name-1-2', { processId: 'process-2' }],
['name-1-3', { processId: 'process-3' }],
]),
);
assert.equal(await cache.getCacheSize(), 3);
});

Expand All @@ -77,11 +87,25 @@ describe('ArNSNamesCache', () => {
});

const names1 = await cache.getNames();
assert.deepEqual(names1, new Set(['name-1-1', 'name-1-2', 'name-1-3']));
assert.deepEqual(
names1,
new Map([
['name-1-1', { processId: 'process-1' }],
['name-1-2', { processId: 'process-2' }],
['name-1-3', { processId: 'process-3' }],
]),
);
assert.equal(await cache.getCacheSize(), 3);

const names2 = await cache.getNames();
assert.deepEqual(names2, new Set(['name-1-1', 'name-1-2', 'name-1-3']));
assert.deepEqual(
names2,
new Map([
['name-1-1', { processId: 'process-1' }],
['name-1-2', { processId: 'process-2' }],
['name-1-3', { processId: 'process-3' }],
]),
);
assert.equal(await cache.getCacheSize(), 3);
});

Expand All @@ -92,11 +116,25 @@ describe('ArNSNamesCache', () => {
});

const names1 = await cache.getNames();
assert.deepEqual(names1, new Set(['name-1-1', 'name-1-2', 'name-1-3']));
assert.deepEqual(
names1,
new Map([
['name-1-1', { processId: 'process-1' }],
['name-1-2', { processId: 'process-2' }],
['name-1-3', { processId: 'process-3' }],
]),
);
assert.equal(await cache.getCacheSize(), 3);

const names2 = await cache.getNames({ forceCacheUpdate: true });
assert.deepEqual(names2, new Set(['name-2-1', 'name-2-2', 'name-2-3']));
assert.deepEqual(
names2,
new Map([
['name-2-1', { processId: 'process-1' }],
['name-2-2', { processId: 'process-2' }],
['name-2-3', { processId: 'process-3' }],
]),
);
assert.equal(await cache.getCacheSize(), 3);
});

Expand All @@ -109,14 +147,28 @@ describe('ArNSNamesCache', () => {
});

const names1 = await cache.getNames();
assert.deepEqual(names1, new Set(['name-1-1', 'name-1-2', 'name-1-3']));
assert.deepEqual(
names1,
new Map([
['name-1-1', { processId: 'process-1' }],
['name-1-2', { processId: 'process-2' }],
['name-1-3', { processId: 'process-3' }],
]),
);
assert.equal(await cache.getCacheSize(), 3);

// Wait for cache to expire
await new Promise((resolve) => setTimeout(resolve, cacheTtl + 10));

const names2 = await cache.getNames();
assert.deepEqual(names2, new Set(['name-2-1', 'name-2-2', 'name-2-3']));
assert.deepEqual(
names2,
new Map([
['name-2-1', { processId: 'process-1' }],
['name-2-2', { processId: 'process-2' }],
['name-2-3', { processId: 'process-3' }],
]),
);
assert.equal(await cache.getCacheSize(), 3);
});

Expand All @@ -129,7 +181,7 @@ describe('ArNSNamesCache', () => {
throw new Error('Temporary failure');
}
return {
items: [{ name: 'success-after-retry' }],
items: [{ name: 'success-after-retry', processId: 'process-1' }],
nextCursor: undefined,
};
},
Expand All @@ -143,7 +195,10 @@ describe('ArNSNamesCache', () => {
});

const names = await cache.getNames();
assert.deepEqual(names, new Set(['success-after-retry']));
assert.deepEqual(
names,
new Map([['success-after-retry', { processId: 'process-1' }]]),
);
assert.equal(callCount, 3);
});

Expand Down Expand Up @@ -211,7 +266,7 @@ describe('ArNSNamesCache', () => {
return { items: [], nextCursor: undefined };
}
return {
items: [{ name: 'success' }],
items: [{ name: 'success', processId: 'process-1' }],
nextCursor: undefined,
};
},
Expand All @@ -225,7 +280,7 @@ describe('ArNSNamesCache', () => {
});

const names = await cache.getNames();
assert.deepEqual(names, new Set(['success']));
assert.deepEqual(names, new Map([['success', { processId: 'process-1' }]]));
assert.equal(callCount, 2);
});

Expand All @@ -236,7 +291,7 @@ describe('ArNSNamesCache', () => {
callCount++;
if (callCount === 1) {
return {
items: [{ name: 'initial-success' }],
items: [{ name: 'initial-success', processId: 'process-1' }],
nextCursor: undefined,
};
}
Expand All @@ -252,10 +307,16 @@ describe('ArNSNamesCache', () => {
});

const initialNames = await cache.getNames();
assert.deepEqual(initialNames, new Set(['initial-success']));
assert.deepEqual(
initialNames,
new Map([['initial-success', { processId: 'process-1' }]]),
);

const updatedNames = await cache.getNames({ forceCacheUpdate: true });
assert.deepEqual(updatedNames, new Set(['initial-success']));
assert.deepEqual(
updatedNames,
new Map([['initial-success', { processId: 'process-1' }]]),
);
assert.equal(callCount, 4); // 1 initial + 3 retry attempts
});

Expand Down Expand Up @@ -291,20 +352,21 @@ describe('ArNSNamesCache', () => {
if (callCount === 0) {
callCount++;
return {
items: [{ name: 'name-0' }],
items: [{ name: 'name-0', processId: 'process-0' }],
nextCursor: undefined,
};
}
callCount++;
return {
items: [{ name: 'name-0' }, { name: 'name-1' }],
items: [
{ name: 'name-0', processId: 'process-0' },
{ name: 'name-1', processId: 'process-1' },
],
nextCursor: undefined,
};
},
} as unknown as AoARIORead;

log.level = 'debug';

const cache = new ArNSNamesCache({
log,
networkProcess: mockNetworkProcess,
Expand All @@ -316,13 +378,13 @@ describe('ArNSNamesCache', () => {
assert.equal(callCount, 1);

// check a missing name, this should instantiate the debounce timeout to refresh the cache in 1 second
const missingName = await cache.has('name-1');
assert.equal(missingName, false, 'Name should not be cached');
const missingName = await cache.getName('name-1');
assert.equal(missingName, undefined, 'Name should not be cached');
assert.equal(callCount, 1);

// it should not trigger a refresh if the name is requested again within the ttl
const missingName2 = await cache.has('name-1');
assert.equal(missingName2, false, 'Name should not be cached');
const missingName2 = await cache.getName('name-1');
assert.equal(missingName2, undefined, 'Name should not be cached');
assert.equal(callCount, 1);

// wait the ttl + 5ms and assert that it does trigger a refresh and getArNSRecords is called again
Expand All @@ -335,7 +397,13 @@ describe('ArNSNamesCache', () => {

// assert that the names are refreshed and the cache size is updated
const names = await cache.getNames();
assert.deepEqual(names, new Set(['name-0', 'name-1']));
assert.deepEqual(
names,
new Map([
['name-0', { processId: 'process-0' }],
['name-1', { processId: 'process-1' }],
]),
);
assert.equal(await cache.getCacheSize(), 2);
});

Expand All @@ -344,7 +412,10 @@ describe('ArNSNamesCache', () => {
const mockNetworkProcess = {
async getArNSRecords() {
callCount++;
return { items: [{ name: 'name-1' }], nextCursor: undefined };
return {
items: [{ name: 'name-1', processId: 'process-1' }],
nextCursor: undefined,
};
},
} as unknown as AoARIORead;

Expand All @@ -361,19 +432,19 @@ describe('ArNSNamesCache', () => {
await new Promise((resolve) => setTimeout(resolve, 5));

// request a hit
const hitName = await cache.has('name-1');
assert.equal(hitName, true);
const hitName = await cache.getName('name-1');
assert.deepEqual(hitName, { processId: 'process-1' });
assert.equal(callCount, 1);

// assert that getArNS records is not called again if name is requested between cache hit cache and ttl
const hitName2 = await cache.has('name-1');
assert.equal(hitName2, true);
const hitName2 = await cache.getName('name-1');
assert.deepEqual(hitName2, { processId: 'process-1' });
assert.equal(callCount, 1);

// wait the ttl and assert that it does trigger a refresh
await new Promise((resolve) => setTimeout(resolve, 15));
const debouncedName = await cache.has('name-1');
assert.equal(debouncedName, true);
const debouncedName = await cache.getName('name-1');
assert.deepEqual(debouncedName, { processId: 'process-1' });
assert.equal(callCount, 2);
});
});
29 changes: 15 additions & 14 deletions src/resolution/arns-names-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
AOProcess,
ARIO,
fetchAllArNSRecords,
AoArNSNameData,
} from '@ar.io/sdk';
import * as config from '../config.js';
import { connect } from '@permaweb/aoconnect';
Expand All @@ -39,8 +40,8 @@ const DEFAULT_CACHE_HIT_DEBOUNCE_TTL =
export class ArNSNamesCache {
private log: winston.Logger;
private networkProcess: AoARIORead;
private namesCache: Promise<Set<string>>;
private lastSuccessfulNames: Set<string> | null = null;
private namesCache: Promise<Map<string, AoArNSNameData>>;
private lastSuccessfulNames: Map<string, AoArNSNameData> | null = null;
private lastCacheTime = 0;
private cacheTtl: number;
private maxRetries: number;
Expand Down Expand Up @@ -96,7 +97,7 @@ export class ArNSNamesCache {
forceCacheUpdate = false,
}: {
forceCacheUpdate?: boolean;
} = {}): Promise<Set<string>> {
} = {}): Promise<Map<string, AoArNSNameData>> {
const log = this.log.child({ method: 'getNames' });

const expiredTtl = Date.now() - this.lastCacheTime > this.cacheTtl;
Expand All @@ -117,14 +118,16 @@ export class ArNSNamesCache {
return this.namesCache;
}

async has(name: string): Promise<boolean> {
async getName(name: string): Promise<AoArNSNameData | undefined> {
const names = await this.getNames();
const nameExists = names.has(name);
const nameData = names.get(name);
// schedule the next debounce based on the cache hit or miss
await this.scheduleCacheRefresh(
nameExists ? this.cacheHitDebounceTtl : this.cacheMissDebounceTtl,
nameData !== undefined
? this.cacheHitDebounceTtl
: this.cacheMissDebounceTtl,
);
return nameExists;
return nameData;
}

private async scheduleCacheRefresh(ttl: number): Promise<void> {
Expand Down Expand Up @@ -154,7 +157,7 @@ export class ArNSNamesCache {
return names.size;
}

private async getNamesFromContract(): Promise<Set<string>> {
private async getNamesFromContract(): Promise<Map<string, AoArNSNameData>> {
const log = this.log.child({ method: 'getNamesFromContract' });
log.info('Starting to fetch names from contract');

Expand All @@ -166,20 +169,18 @@ export class ArNSNamesCache {
contract: this.networkProcess,
});

const names = new Set(Object.keys(records));

// to be safe, we will throw here to force a retry if no names returned
if (names.size === 0) {
if (Object.keys(records).length === 0) {
throw new Error('Failed to fetch ArNS names');
}

log.info(
`Successfully fetched ${names.size} names from contract on attempt ${attempt}`,
`Successfully fetched ${Object.keys(records).length} names from contract on attempt ${attempt}`,
);

this.lastSuccessfulNames = names;
this.lastSuccessfulNames = new Map(Object.entries(records));

return names;
return this.lastSuccessfulNames;
} catch (error) {
lastError = error as Error;

Expand Down
Loading

0 comments on commit e5630e7

Please sign in to comment.