Skip to content

Commit

Permalink
feat: cache template file (#1334)
Browse files Browse the repository at this point in the history
Signed-off-by: Jason C. Leach <[email protected]>
  • Loading branch information
jleach authored Dec 10, 2024
1 parent 8335800 commit 43b3ce6
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 63 deletions.
6 changes: 6 additions & 0 deletions packages/legacy/core/App/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,9 @@ export const domain = 'didcomm://invite'
export const tourMargin = 25

export const hitSlop = { top: 44, bottom: 44, left: 44, right: 44 }

export const templateBundleStorageDirectory = 'templates'

export const templateCacheDataFileName = 'proof-templates.json'

export const templateBundleIndexFileName = 'proof-templates.json'
6 changes: 4 additions & 2 deletions packages/legacy/core/App/hooks/proof-request-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { applyTemplateMarkers, useRemoteProofBundleResolver } from '../utils/pro
export const useTemplates = (): Array<ProofRequestTemplate> => {
const [store] = useStore()
const [proofRequestTemplates, setProofRequestTemplates] = useState<ProofRequestTemplate[]>([])
const [{proofTemplateBaseUrl}, logger] = useServices([TOKENS.CONFIG, TOKENS.UTIL_LOGGER])
const [{ proofTemplateBaseUrl }, logger] = useServices([TOKENS.CONFIG, TOKENS.UTIL_LOGGER])
const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl, logger)

useEffect(() => {
Expand All @@ -25,14 +25,16 @@ export const useTemplates = (): Array<ProofRequestTemplate> => {
export const useTemplate = (templateId: string): ProofRequestTemplate | undefined => {
const [store] = useStore()
const [proofRequestTemplate, setProofRequestTemplate] = useState<ProofRequestTemplate | undefined>(undefined)
const [{proofTemplateBaseUrl}] = useServices([TOKENS.CONFIG])
const [{ proofTemplateBaseUrl }] = useServices([TOKENS.CONFIG])
const resolver = useRemoteProofBundleResolver(proofTemplateBaseUrl)

useEffect(() => {
resolver.resolveById(templateId, store.preferences.acceptDevCredentials).then((template) => {
if (template) {
setProofRequestTemplate(applyTemplateMarkers(template))
}
})
}, [resolver, templateId, store.preferences.acceptDevCredentials])

return proofRequestTemplate
}
121 changes: 121 additions & 0 deletions packages/legacy/core/App/utils/fileCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { BaseLogger } from '@credo-ts/core'
import axios, { AxiosInstance } from 'axios'
import { CachesDirectoryPath, readFile, writeFile, exists, mkdir } from 'react-native-fs'

export type CacheDataFile = {
fileEtag: string
updatedAt: Date
}

export class FileCache {
protected axiosInstance: AxiosInstance
protected _fileEtag?: string
protected log?: BaseLogger
private workspace: string
private cacheFileName

public constructor(indexFileBaseUrl: string, workspace: string, cacheDataFileName: string, log?: BaseLogger) {
this.axiosInstance = axios.create({
baseURL: indexFileBaseUrl,
})

this.workspace = workspace
this.cacheFileName = cacheDataFileName
this.log = log
}

set fileEtag(value: string) {
if (!value) {
return
}

this._fileEtag = value
this.saveCacheData({
fileEtag: value,
updatedAt: new Date(),
}).catch((error) => {
this.log?.error(`Failed to save cache data, ${error}`)
})
}

get fileEtag(): string {
return this._fileEtag || ''
}

protected saveCacheData = async (cacheData: CacheDataFile): Promise<boolean> => {
const cacheDataAsString = JSON.stringify(cacheData)

return this.saveFileToLocalStorage(this.cacheFileName, cacheDataAsString)
}

protected saveFileToLocalStorage = async (fileName: string, data: string): Promise<boolean> => {
const pathToFile = `${this.fileStoragePath()}/${fileName}`

try {
await writeFile(pathToFile, data, 'utf8')
this.log?.info(`File ${fileName} saved to ${pathToFile}`)

return true
} catch (error) {
this.log?.error(`Failed to save file ${fileName} to ${pathToFile}, ${error}`)
}

return false
}

protected fileStoragePath = (): string => {
return `${CachesDirectoryPath}/${this.workspace}`
}

protected createWorkingDirectoryIfNotExists = async (): Promise<boolean> => {
const path = this.fileStoragePath()
const pathDoesExist = await exists(path)

if (!pathDoesExist) {
try {
await mkdir(path)
return true
} catch (error) {
this.log?.error(`Failed to create directory ${path}`)
return false
}
}

return true
}

protected loadFileFromLocalStorage = async (fileName: string): Promise<string | undefined> => {
const pathToFile = `${this.fileStoragePath()}/${fileName}`

try {
const fileExists = await this.checkFileExists(fileName)
if (!fileExists) {
this.log?.warn(`Missing ${fileName} from ${pathToFile}`)

return
}

const data = await readFile(pathToFile, 'utf8')
this.log?.info(`File ${fileName} loaded from ${pathToFile}`)

return data
} catch (error) {
this.log?.error(`Failed to load file ${fileName} from ${pathToFile}`)
}
}

protected checkFileExists = async (fileName: string): Promise<boolean> => {
const pathToFile = `${this.fileStoragePath()}/${fileName}`

try {
const fileExists = await exists(pathToFile)
this.log?.info(`File ${fileName} ${fileExists ? 'does' : 'does not'} exist at ${pathToFile}`)

return fileExists
} catch (error) {
this.log?.error(`Failed to check existence of ${fileName} at ${pathToFile}`)
}

return false
}
}
147 changes: 101 additions & 46 deletions packages/legacy/core/App/utils/proofBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
ProofRequestTemplate,
getProofRequestTemplates,
} from '@hyperledger/aries-bifold-verifier'
import axios, { AxiosError } from 'axios'
import { useState, useEffect } from 'react'

import { TOKENS, useServices } from '../container-api'
import { templateBundleStorageDirectory, templateCacheDataFileName, templateBundleIndexFileName } from '../constants'
import { FileCache, CacheDataFile } from './fileCache'

type ProofRequestTemplateFn = (useDevTemplates: boolean) => Array<ProofRequestTemplate>

Expand All @@ -18,19 +19,25 @@ const calculatePreviousYear = (yearOffset: number) => {
}

export const applyTemplateMarkers = (templates: any): any => {
if (!templates) return templates
if (!templates) {
return templates
}

const markerActions: { [key: string]: (param: string) => string } = {
now: () => Math.floor(new Date().getTime() / 1000).toString(),
currentDate: (offset: string) => calculatePreviousYear(parseInt(offset)).toString(),
}

let templateString = JSON.stringify(templates)
// regex to find all markers in the template so we can replace them with computed values
// regex to find all markers in the template so we can replace
// them with computed values
const markers = [...templateString.matchAll(/"@\{(\w+)(?:\((\S*)\))?\}"/gm)]

markers.forEach((marker) => {
const markerValue = markerActions[marker[1] as string](marker[2])
templateString = templateString.replace(marker[0], markerValue)
})

return JSON.parse(templateString)
}

Expand Down Expand Up @@ -64,17 +71,17 @@ export const applyDevRestrictions = (templates: ProofRequestTemplate[]): ProofRe
})
}

export interface ProofBundleResolverType {
export interface IProofBundleResolver {
resolve: (acceptDevRestrictions: boolean) => Promise<ProofRequestTemplate[] | undefined>
resolveById: (templateId: string, acceptDevRestrictions: boolean) => Promise<ProofRequestTemplate | undefined>
}

export const useRemoteProofBundleResolver = (
indexFileBaseUrl: string | undefined,
log?: BaseLogger
): ProofBundleResolverType => {
): IProofBundleResolver => {
const [proofRequestTemplates] = useServices([TOKENS.UTIL_PROOF_TEMPLATE])
const [resolver, setResolver] = useState<ProofBundleResolverType>(new DefaultProofBundleResolver(proofRequestTemplates))
const [resolver, setResolver] = useState<IProofBundleResolver>(new DefaultProofBundleResolver(proofRequestTemplates))

useEffect(() => {
if (indexFileBaseUrl) {
Expand All @@ -87,76 +94,124 @@ export const useRemoteProofBundleResolver = (
return resolver
}

export class RemoteProofBundleResolver implements ProofBundleResolverType {
private remoteServer
export class RemoteProofBundleResolver extends FileCache implements IProofBundleResolver {
private templateData: ProofRequestTemplate[] | undefined
private log?: BaseLogger
private cacheDataFileName = templateCacheDataFileName

public constructor(indexFileBaseUrl: string, log?: BaseLogger) {
this.remoteServer = axios.create({
baseURL: indexFileBaseUrl,
})
this.log = log
super(indexFileBaseUrl, templateBundleStorageDirectory, templateBundleIndexFileName, log)
}

public async resolve(acceptDevRestrictions: boolean): Promise<ProofRequestTemplate[] | undefined> {
if (this.templateData) {
let templateData = this.templateData

if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)
}
let templateData

return Promise.resolve(templateData)
if (!this.templateData) {
await this.checkForUpdates()
}

return this.remoteServer
.get('proof-templates.json')
.then((response) => {
this.log?.info('Fetched proof templates')

try {
let templateData: ProofRequestTemplate[] = response.data
this.templateData = templateData

if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)
}
if (!this.templateData) {
return []
}

return templateData
} catch (error: unknown) {
this.log?.error('Failed to parse proof templates', error as Error)
templateData = this.templateData

return undefined
}
})
.catch((error: AxiosError) => {
this.log?.error('Failed to fetch proof templates', error)
if (acceptDevRestrictions) {
templateData = applyDevRestrictions(this.templateData)
}

return undefined
})
return Promise.resolve(templateData)
}

public async resolveById(
templateId: string,
acceptDevRestrictions: boolean
): Promise<ProofRequestTemplate | undefined> {
let templateData

if (!this.templateData) {
return (await this.resolve(acceptDevRestrictions))?.find((template) => template.id === templateId)
}

let templateData = this.templateData
templateData = this.templateData

if (acceptDevRestrictions) {
templateData = applyDevRestrictions(templateData)
}
const template = templateData.find((template) => template.id === templateId)

return template
return templateData.find((template) => template.id === templateId)
}

public async checkForUpdates(): Promise<void> {
await this.createWorkingDirectoryIfNotExists()

if (!this.fileEtag) {
this.log?.info('Loading cache data')

const cacheData = await this.loadCacheData()
if (cacheData) {
this.fileEtag = cacheData.fileEtag
}
}

this.log?.info('Loading index now')
await this.loadBundleIndex(this.cacheDataFileName)
}

private loadBundleIndex = async (filePath: string) => {
try {
const response = await this.axiosInstance.get(filePath)
const { status } = response
const { etag } = response.headers

if (status !== 200) {
this.log?.error(`Failed to fetch remote resource at ${filePath}`)
// failed to fetch, use the cached index file
// if available
const data = await this.loadFileFromLocalStorage(filePath)
if (data) {
this.log?.info(`Using index file ${filePath} from cache`)
this.templateData = JSON.parse(data)
}

return
}

if (etag && etag === this.fileEtag) {
this.log?.info(`Index file ${filePath} has not changed, etag ${etag}`)
// etag is the same, no need to refresh
this.templateData = response.data

return
}

this.fileEtag = etag
this.templateData = response.data

this.log?.info(`Saving file ${filePath}, etag ${etag}`)
await this.saveFileToLocalStorage(filePath, JSON.stringify(this.templateData))
} catch (error) {
this.log?.error(`Failed to fetch file index ${filePath}`)
}
}

private loadCacheData = async (): Promise<CacheDataFile | undefined> => {
const cacheFileExists = await this.checkFileExists(this.cacheDataFileName)
if (!cacheFileExists) {
return
}

const data = await this.loadFileFromLocalStorage(this.cacheDataFileName)
if (!data) {
return
}

const cacheData: CacheDataFile = JSON.parse(data)

return cacheData
}
}

export class DefaultProofBundleResolver implements ProofBundleResolverType {
export class DefaultProofBundleResolver implements IProofBundleResolver {
private proofRequestTemplates

public constructor(proofRequestTemplates: ProofRequestTemplateFn | undefined) {
Expand Down
Loading

0 comments on commit 43b3ce6

Please sign in to comment.