dop-stick
Version:
Source control tooling for versionable-upgradeable smart contracts
407 lines • 16.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbiCache = void 0;
const abi_1 = require("@ethersproject/abi");
const fs_1 = require("fs");
const path_1 = require("path");
const fs_2 = require("fs");
const logger_1 = require("../logsAndMetrics/core/logger");
class AbiCache {
constructor() {
this.cache = null;
this.isDirty = false;
this.savePromise = null;
this.cacheDir = (0, path_1.join)(process.cwd(), '.dopstick-cache');
this.cacheFile = (0, path_1.join)(this.cacheDir, 'typechain-cache.json');
}
static getInstance() {
if (!AbiCache.instance) {
AbiCache.instance = new AbiCache();
}
return AbiCache.instance;
}
async ensureCacheDirectory() {
if (!(0, fs_2.existsSync)(this.cacheDir)) {
await fs_1.promises.mkdir(this.cacheDir, { recursive: true });
}
}
validateSerializedCache(serialized) {
if (!serialized || typeof serialized !== 'object')
return false;
// Check required top-level properties
if (!serialized.version || !serialized.timestamp || !serialized.lookup || !serialized.facets) {
return false;
}
// Ensure events object exists (even if empty)
if (!serialized.events || typeof serialized.events !== 'object') {
serialized.events = { cache: [], timestamps: [] };
}
return true;
}
async load() {
var _a, _b, _c, _d, _e;
try {
await this.ensureCacheDirectory();
if (!(0, fs_2.existsSync)(this.cacheFile)) {
logger_1.Logger.debug('No cache file found');
return null;
}
const startTime = performance.now();
const rawData = await fs_1.promises.readFile(this.cacheFile, 'utf-8');
const serialized = JSON.parse(rawData);
// Version and age checks
if (serialized.version !== AbiCache.VERSION) {
logger_1.Logger.info('Cache version mismatch, invalidating cache');
return null;
}
if (Date.now() - serialized.timestamp > 24 * 60 * 60 * 1000) {
logger_1.Logger.info('Cache expired, invalidating');
return null;
}
// Basic structure validation
if (!serialized.lookup || !serialized.facets) {
logger_1.Logger.warn('Invalid cache structure - missing required properties');
return null;
}
const cache = {
version: serialized.version,
timestamp: serialized.timestamp,
lookup: {
selectorToFacet: new Map((serialized.lookup.selectorToFacet || []).map(([selector, info]) => [
selector,
{
facetName: info.facetName,
functionInfo: this.deserializeFunctionInfo(info.functionInfo)
}
])),
facetToSelectors: new Map((serialized.lookup.facetToSelectors || []).map((entry) => [
entry[0],
new Set(entry[1])
])),
metadata: {
lastUpdate: new Date(((_a = serialized.lookup.metadata) === null || _a === void 0 ? void 0 : _a.lastUpdate) || Date.now()),
version: ((_b = serialized.lookup.metadata) === null || _b === void 0 ? void 0 : _b.version) || AbiCache.VERSION,
entries: ((_c = serialized.lookup.metadata) === null || _c === void 0 ? void 0 : _c.entries) || 0
}
},
facets: new Map((serialized.facets || []).map(([name, data]) => [
name,
{
facetInfo: this.deserializeFacetInfo(data.facetInfo),
timestamp: data.timestamp,
metadata: {
lastProcessed: new Date(data.metadata.lastProcessed),
version: data.metadata.version,
functionCount: data.metadata.functionCount,
eventCount: data.metadata.eventCount
}
}
])),
events: {
cache: new Map((((_d = serialized.events) === null || _d === void 0 ? void 0 : _d.cache) || []).map((entry) => [
entry[0],
new Map(entry[1].map((e) => [
e[0],
this.deserializeEventFragment(e[1])
]))
])),
timestamps: new Map(((_e = serialized.events) === null || _e === void 0 ? void 0 : _e.timestamps) || [])
}
};
const duration = ((performance.now() - startTime) / 1000).toFixed(2);
logger_1.Logger.debug(`Cache loaded successfully in ${duration}s:
- ${cache.facets.size} facets
- ${cache.lookup.selectorToFacet.size} selectors
- ${cache.events.cache.size} event caches
- Version: ${cache.version}`);
return cache;
}
catch (error) {
logger_1.Logger.warn('Failed to load cache:', error);
return null;
}
}
async save(data) {
if (this.savePromise) {
await this.savePromise;
}
this.isDirty = true;
this.cache = data;
this.savePromise = new Promise((resolve) => {
setTimeout(async () => {
if (!this.isDirty) {
resolve();
return;
}
try {
await this.ensureCacheDirectory();
const serialized = {
version: AbiCache.VERSION,
timestamp: Date.now(),
lookup: {
selectorToFacet: Array.from(data.lookup.selectorToFacet.entries()),
facetToSelectors: Array.from(data.lookup.facetToSelectors.entries())
.map(([key, set]) => [key, Array.from(set)]),
metadata: {
lastUpdate: data.lookup.metadata.lastUpdate.toISOString(),
version: data.lookup.metadata.version,
entries: data.lookup.metadata.entries
}
},
facets: Array.from(data.facets.entries()).map(([name, contract]) => [
name,
{
facetInfo: {
...contract.facetInfo,
selectors: Array.from(contract.facetInfo.selectors),
functions: this.mapToObject(contract.facetInfo.functions),
events: this.mapToObject(contract.facetInfo.events),
enhancedFunctions: this.mapToObject(contract.facetInfo.enhancedFunctions),
enhancedEvents: this.mapToObject(contract.facetInfo.enhancedEvents)
},
timestamp: contract.timestamp,
metadata: {
lastProcessed: contract.metadata.lastProcessed.toISOString(),
version: contract.metadata.version,
functionCount: contract.metadata.functionCount,
eventCount: contract.metadata.eventCount
}
}
]),
events: {
cache: Array.from(data.events.cache.entries()).map(([key, value]) => [
key,
Array.from(value.entries())
]),
timestamps: Array.from(data.events.timestamps.entries())
}
};
await fs_1.promises.writeFile(this.cacheFile, JSON.stringify(serialized, null, 2), 'utf-8');
this.isDirty = false;
logger_1.Logger.info('Cache saved successfully');
}
catch (error) {
logger_1.Logger.error('Failed to save cache:', error);
}
finally {
this.savePromise = null;
resolve();
}
}, AbiCache.SAVE_DEBOUNCE);
});
await this.savePromise;
}
// Serialization/Deserialization Helpers
deserializeFunctionInfo(info) {
return {
name: info.name,
fullName: info.fullName,
selector: info.selector,
signature: info.signature,
inputs: this.deserializeParameters(info.inputs),
outputs: this.deserializeParameters(info.outputs),
mutability: info.mutability,
visibility: info.visibility,
fragment: abi_1.FunctionFragment.from(info.fragment)
};
}
deserializeParameters(params) {
return params.map(param => ({
name: param.name,
type: param.type,
...(param.indexed !== undefined && { indexed: param.indexed }),
...(param.components && {
components: this.deserializeParameters(param.components)
})
}));
}
deserializeFacetInfo(facetInfo) {
return {
name: facetInfo.name,
path: facetInfo.path,
abi: facetInfo.abi,
selectors: facetInfo.selectors,
functions: facetInfo.functions,
events: new Map(Object.entries(facetInfo.events || {})),
enhancedFunctions: facetInfo.enhancedFunctions,
enhancedEvents: facetInfo.enhancedEvents
};
}
deserializeEventFragment(event) {
return abi_1.EventFragment.from(event);
}
deserializeEventInfo(event) {
return {
name: event.name,
signature: event.signature,
inputs: this.deserializeParameters(event.inputs),
anonymous: event.anonymous,
fragment: abi_1.EventFragment.from(event.fragment)
};
}
// Helper methods with proper type constraints
mapToObject(map) {
return Object.fromEntries(Array.from(map.entries()));
}
// Cache Validation Methods
validateCache(data) {
try {
if (!data || typeof data !== 'object')
return false;
if (!data.version || !data.timestamp)
return false;
if (!data.lookup || !data.facets)
return false;
// Validate lookup structure
if (!this.validateLookup(data.lookup))
return false;
// Validate facets
if (!this.validateFacets(data.facets))
return false;
return true;
}
catch (error) {
logger_1.Logger.error('Cache validation failed:', error);
return false;
}
}
validateLookup(lookup) {
return !!(lookup.selectorToFacet &&
lookup.facetToSelectors &&
lookup.metadata &&
typeof lookup.metadata.lastUpdate === 'string' &&
typeof lookup.metadata.version === 'string' &&
typeof lookup.metadata.entries === 'number');
}
validateFacets(facets) {
if (!Array.isArray(facets))
return false;
return facets.every(([name, data]) => typeof name === 'string' &&
data.facetInfo &&
typeof data.timestamp === 'number');
}
// Cache Maintenance Methods
async clear() {
try {
this.cache = null;
this.isDirty = false;
if ((0, fs_2.existsSync)(this.cacheFile)) {
await fs_1.promises.unlink(this.cacheFile);
}
logger_1.Logger.info('Cache cleared successfully');
}
catch (error) {
logger_1.Logger.error('Failed to clear cache:', error);
throw error;
}
}
async maintain() {
try {
if (!this.cache)
return;
const now = Date.now();
let entriesRemoved = 0;
// Remove expired entries
for (const [key, value] of this.cache.facets) {
if (now - value.timestamp > 24 * 60 * 60 * 1000) {
this.cache.facets.delete(key);
entriesRemoved++;
}
}
if (entriesRemoved > 0) {
this.isDirty = true;
await this.save(this.cache);
logger_1.Logger.info(`Cache maintenance removed ${entriesRemoved} expired entries`);
}
}
catch (error) {
logger_1.Logger.error('Cache maintenance failed:', error);
}
}
// Performance Optimizations
async optimizeCache() {
if (!this.cache)
return;
const startTime = performance.now();
try {
// Rebuild lookup maps for optimal performance
const newLookup = {
selectorToFacet: new Map(),
facetToSelectors: new Map(),
metadata: this.cache.lookup.metadata
};
for (const [facetName, data] of this.cache.facets) {
const selectors = new Set();
for (const [selector, functionInfo] of data.facetInfo.enhancedFunctions) {
newLookup.selectorToFacet.set(selector, {
facetName,
functionInfo
});
selectors.add(selector);
}
newLookup.facetToSelectors.set(facetName, selectors);
}
this.cache.lookup = newLookup;
this.isDirty = true;
await this.save(this.cache);
const duration = ((performance.now() - startTime) / 1000).toFixed(2);
logger_1.Logger.info(`Cache optimization completed in ${duration}s`);
}
catch (error) {
logger_1.Logger.error('Cache optimization failed:', error);
}
}
// Utility Methods
async getCacheStats() {
if (!this.cache)
return { exists: false };
return {
exists: true,
version: this.cache.version,
age: Date.now() - this.cache.timestamp,
facetCount: this.cache.facets.size,
selectorCount: this.cache.lookup.selectorToFacet.size,
lastUpdate: this.cache.lookup.metadata.lastUpdate,
size: JSON.stringify(this.cache).length
};
}
// Helper methods for type-safe conversions
arrayToSet(arr) {
return new Set(arr);
}
setToArray(set) {
return Array.from(set);
}
serializeLookup(lookup) {
return {
selectorToFacet: Array.from(lookup.selectorToFacet.entries()).map(([selector, info]) => [
selector,
{
facetName: info.facetName,
functionInfo: info.functionInfo
}
]),
facetToSelectors: Array.from(lookup.facetToSelectors.entries()).map(([facet, selectors]) => [
facet,
Array.from(selectors)
]),
metadata: {
lastUpdate: lookup.metadata.lastUpdate.toISOString(),
version: lookup.metadata.version,
entries: lookup.metadata.entries
}
};
}
serializeFacetInfo(facetInfo) {
return {
...facetInfo,
events: Object.fromEntries(facetInfo.events instanceof Map
? facetInfo.events.entries()
: Object.entries(facetInfo.events || {}))
};
}
}
exports.AbiCache = AbiCache;
AbiCache.SAVE_DEBOUNCE = 1000; // 1 second
AbiCache.VERSION = '2.0.0';
//# sourceMappingURL=abi-cache.js.map