Skip to content

Commit

Permalink
feat: did caching
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Jan 29, 2024
1 parent 3d1903a commit 1623cab
Show file tree
Hide file tree
Showing 16 changed files with 195 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/anoncreds-rs/tests/LocalDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DidsApi } from '@aries-framework/core'

export class LocalDidResolver implements DidResolver {
public readonly supportedMethods = ['sov', 'indy']
public readonly allowsCaching = false

public async resolve(agentContext: AgentContext, did: string): Promise<DidResolutionResult> {
const didDocumentMetadata = {}
Expand Down
1 change: 1 addition & 0 deletions packages/cheqd/src/dids/CheqdDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { filterResourcesByNameAndType, getClosestResourceVersion, renderResource

export class CheqdDidResolver implements DidResolver {
public readonly supportedMethods = ['cheqd']
public readonly allowsCaching = true

public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise<DidResolutionResult> {
const didDocumentMetadata = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { DidRecord, DidDocumentRole, DidRepository } from '../../dids'
export class InMemoryDidRegistry implements DidRegistrar, DidResolver {
public readonly supportedMethods = ['inmemory']

public readonly allowsCaching = false

private dids: Record<string, DidDocument> = {}

public async create(agentContext: AgentContext, options: DidCreateOptions): Promise<DidCreateResult> {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/modules/dids/domain/DidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { ParsedDid, DidResolutionResult, DidResolutionOptions } from '../ty

export interface DidResolver {
readonly supportedMethods: string[]
readonly allowsCaching: boolean

resolve(
agentContext: AgentContext,
did: string,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { DidJwk } from './DidJwk'
export class JwkDidResolver implements DidResolver {
public readonly supportedMethods = ['jwk']

/**
* No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching
*/
public readonly allowsCaching = false

public async resolve(agentContext: AgentContext, did: string): Promise<DidResolutionResult> {
const didDocumentMetadata = {}

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/modules/dids/methods/key/KeyDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { DidKey } from './DidKey'
export class KeyDidResolver implements DidResolver {
public readonly supportedMethods = ['key']

/**
* No remote resolving done, did document is dynamically constructed. To not pollute the cache we don't allow caching
*/
public readonly allowsCaching = false

public async resolve(agentContext: AgentContext, did: string): Promise<DidResolutionResult> {
const didDocumentMetadata = {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { didToNumAlgo4DidDocument, isShortFormDidPeer4 } from './peerDidNumAlgo4
export class PeerDidResolver implements DidResolver {
public readonly supportedMethods = ['peer']

/**
* No remote resolving done, did document is fetched from storage. To not pollute the cache we don't allow caching
*/
public readonly allowsCaching = false

public async resolve(agentContext: AgentContext, did: string): Promise<DidResolutionResult> {
const didRepository = agentContext.dependencyManager.resolve(DidRepository)

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/modules/dids/methods/web/WebDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { DidDocument } from '../../domain'
export class WebDidResolver implements DidResolver {
public readonly supportedMethods

public readonly allowsCaching = true

// FIXME: Would be nice if we don't have to provide a did resolver instance
private _resolverInstance = new Resolver()
private resolver = didWeb.getResolver()
Expand Down
65 changes: 64 additions & 1 deletion packages/core/src/modules/dids/services/DidResolverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { InjectionSymbols } from '../../../constants'
import { AriesFrameworkError } from '../../../error'
import { Logger } from '../../../logger'
import { injectable, inject } from '../../../plugins'
import { JsonTransformer } from '../../../utils'
import { CacheModuleConfig } from '../../cache'
import { DidsModuleConfig } from '../DidsModuleConfig'
import { DidDocument } from '../domain'
import { parseDid } from '../domain/parse'

@injectable()
Expand Down Expand Up @@ -53,9 +56,69 @@ export class DidResolverService {
}
}

return resolver.resolve(agentContext, parsed.did, parsed, options)
// extract caching options and set defaults
const { useCache = true, cacheDurationInSeconds = 300, persistInCache = true } = options
const cacheKey = `did:resolver:${parsed.did}`

if (resolver.allowsCaching && useCache) {
const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache
// FIXME: in multi-tenancy it can be that the same cache is used for different agent contexts
// This may become a problem when resolving dids, as you can get back a cache hit for a different
// tenant. did:peer has disabled caching, and I think we should just recommend disabling caching
// for these private dids
// We could allow providing a custom cache prefix in the resolver options, so that the cache key
// can be recognized in the cache implementation
const cachedDidDocument = await cache.get<DidResolutionResult & { didDocument: Record<string, unknown> }>(
agentContext,
cacheKey
)

if (cachedDidDocument) {
return {
...cachedDidDocument,
didDocument: JsonTransformer.fromJSON(cachedDidDocument.didDocument, DidDocument),
didResolutionMetadata: {
...cachedDidDocument.didResolutionMetadata,
servedFromCache: true,
},
}
}
}

let resolutionResult = await resolver.resolve(agentContext, parsed.did, parsed, options)
// Avoid overwriting existing document
resolutionResult = {
...resolutionResult,
didResolutionMetadata: {
...resolutionResult.didResolutionMetadata,
resolutionTime: Date.now(),
// Did resolver implementation might use did method specific caching strategy
// We only set to false if not defined by the resolver
servedFromCache: resolutionResult.didResolutionMetadata.servedFromCache ?? false,
},
}

if (resolutionResult.didDocument && resolver.allowsCaching && persistInCache) {
const cache = agentContext.dependencyManager.resolve(CacheModuleConfig).cache
await cache.set(
agentContext,
cacheKey,
{
...resolutionResult,
didDocument: resolutionResult.didDocument.toJSON(),
},
// Set cache duration
cacheDurationInSeconds
)
}

return resolutionResult
}

/**
* Resolve a did document. This uses the default resolution options, and thus
* will use caching if available.
*/
public async resolveDidDocument(agentContext: AgentContext, did: string) {
const {
didDocument,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,35 @@ import type { DidResolver } from '../../domain'

import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../tests/helpers'
import { JsonTransformer } from '../../../../utils/JsonTransformer'
import { CacheModuleConfig, InMemoryLruCache } from '../../../cache'
import { DidsModuleConfig } from '../../DidsModuleConfig'
import didKeyEd25519Fixture from '../../__tests__/__fixtures__/didKeyEd25519.json'
import { DidDocument } from '../../domain'
import { parseDid } from '../../domain/parse'
import { DidResolverService } from '../DidResolverService'

const didResolverMock = {
allowsCaching: true,
supportedMethods: ['key'],
resolve: jest.fn(),
} as DidResolver

const cache = new InMemoryLruCache({ limit: 10 })
const agentConfig = getAgentConfig('DidResolverService')
const agentContext = getAgentContext()
const agentContext = getAgentContext({
registerInstances: [[CacheModuleConfig, new CacheModuleConfig({ cache })]],
})

describe('DidResolverService', () => {
const didResolverService = new DidResolverService(
agentConfig.logger,
new DidsModuleConfig({ resolvers: [didResolverMock] })
)

afterEach(() => {
jest.clearAllMocks()
})

it('should correctly find and call the correct resolver for a specified did', async () => {
const returnValue = {
didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument),
Expand All @@ -33,14 +42,72 @@ describe('DidResolverService', () => {
mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue)

const result = await didResolverService.resolve(agentContext, 'did:key:xxxx', { someKey: 'string' })
expect(result).toEqual(returnValue)
expect(result).toEqual({
...returnValue,
didResolutionMetadata: {
...returnValue.didResolutionMetadata,
resolutionTime: expect.any(Number),
servedFromCache: false,
},
})

expect(didResolverMock.resolve).toHaveBeenCalledTimes(1)
expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:xxxx', parseDid('did:key:xxxx'), {
someKey: 'string',
})
})

it('should return cached did document when resolved multiple times within caching duration', async () => {
const returnValue = {
didDocument: JsonTransformer.fromJSON(didKeyEd25519Fixture, DidDocument),
didDocumentMetadata: {},
didResolutionMetadata: {
contentType: 'application/did+ld+json',
},
}
mockFunction(didResolverMock.resolve).mockResolvedValue(returnValue)

const result = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' })
const cachedValue = await cache.get(agentContext, 'did:resolver:did:key:cached')

expect(result).toEqual({
...returnValue,
didResolutionMetadata: {
...returnValue.didResolutionMetadata,
resolutionTime: expect.any(Number),
servedFromCache: false,
},
})

expect(cachedValue).toEqual({
...returnValue,
didDocument: returnValue.didDocument.toJSON(),
didResolutionMetadata: {
...returnValue.didResolutionMetadata,
resolutionTime: expect.any(Number),
servedFromCache: false,
},
})

expect(didResolverMock.resolve).toHaveBeenCalledTimes(1)
expect(didResolverMock.resolve).toHaveBeenCalledWith(agentContext, 'did:key:cached', parseDid('did:key:cached'), {
someKey: 'string',
})

const resultCached = await didResolverService.resolve(agentContext, 'did:key:cached', { someKey: 'string' })
expect(resultCached).toEqual({
...returnValue,
didResolutionMetadata: {
...returnValue.didResolutionMetadata,
resolutionTime: expect.any(Number),
servedFromCache: true,
},
})

// Still called once because served from cache
expect(didResolverMock.resolve).toHaveBeenCalledTimes(1)
})

it("should return an error with 'invalidDid' if the did string couldn't be parsed", async () => {
const did = 'did:__Asd:asdfa'

Expand Down
25 changes: 24 additions & 1 deletion packages/core/src/modules/dids/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,34 @@ import type { DidDocument } from './domain'
import type { DIDDocumentMetadata, DIDResolutionMetadata, DIDResolutionOptions, ParsedDID } from 'did-resolver'

export type ParsedDid = ParsedDID
export type DidResolutionOptions = DIDResolutionOptions
export type DidDocumentMetadata = DIDDocumentMetadata

export interface DidResolutionOptions extends DIDResolutionOptions {
/**
* Whether to resolve the did document from the cache.
*
* @default true
*/
useCache?: boolean

/**
* Whether to persist the did document in the cache.
*
* @default true
*/
persistInCache?: boolean

/**
* How many seconds to persist the resolved document
*
* @default 3600
*/
cacheDurationInSeconds?: number
}

export interface DidResolutionMetadata extends DIDResolutionMetadata {
message?: string
servedFromCache?: boolean
}

export interface DidResolutionResult {
Expand Down
2 changes: 2 additions & 0 deletions packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { addServicesFromEndpointsAttrib } from './didSovUtil'
export class IndySdkIndyDidResolver implements DidResolver {
public readonly supportedMethods = ['indy']

public readonly allowsCaching = true

public async resolve(agentContext: AgentContext, did: string): Promise<DidResolutionResult> {
const didDocumentMetadata = {}

Expand Down
2 changes: 2 additions & 0 deletions packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovU
export class IndySdkSovDidResolver implements DidResolver {
public readonly supportedMethods = ['sov']

public readonly allowsCaching = true

public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise<DidResolutionResult> {
const didDocumentMetadata = {}

Expand Down
2 changes: 2 additions & 0 deletions packages/indy-vdr/src/dids/IndyVdrIndyDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { buildDidDocument } from './didIndyUtil'
export class IndyVdrIndyDidResolver implements DidResolver {
public readonly supportedMethods = ['indy']

public readonly allowsCaching = true

public async resolve(agentContext: AgentContext, did: string): Promise<DidResolutionResult> {
const didDocumentMetadata = {}
try {
Expand Down
2 changes: 2 additions & 0 deletions packages/indy-vdr/src/dids/IndyVdrSovDidResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovU
export class IndyVdrSovDidResolver implements DidResolver {
public readonly supportedMethods = ['sov']

public readonly allowsCaching = true

public async resolve(agentContext: AgentContext, did: string, parsed: ParsedDid): Promise<DidResolutionResult> {
const didDocumentMetadata = {}

Expand Down
8 changes: 7 additions & 1 deletion packages/tenants/tests/tenants.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { InitConfig } from '@aries-framework/core'

import { ConnectionsModule, OutOfBandRecord, Agent } from '@aries-framework/core'
import { ConnectionsModule, OutOfBandRecord, Agent, CacheModule, InMemoryLruCache } from '@aries-framework/core'
import { agentDependencies } from '@aries-framework/node'

import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport'
Expand Down Expand Up @@ -39,6 +39,9 @@ const agent1 = new Agent({
connections: new ConnectionsModule({
autoAcceptConnections: true,
}),
cache: new CacheModule({
cache: new InMemoryLruCache({ limit: 500 }),
}),
},
dependencies: agentDependencies,
})
Expand All @@ -51,6 +54,9 @@ const agent2 = new Agent({
connections: new ConnectionsModule({
autoAcceptConnections: true,
}),
cache: new CacheModule({
cache: new InMemoryLruCache({ limit: 500 }),
}),
},
dependencies: agentDependencies,
})
Expand Down

0 comments on commit 1623cab

Please sign in to comment.