Skip to content

Commit

Permalink
feature: cache system for contract s view
Browse files Browse the repository at this point in the history
  • Loading branch information
wpdas committed Feb 6, 2024
1 parent f0b8173 commit b537a38
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 19 deletions.
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Automatic transforms for [JSON](https://www.json.org/json-en.html) data
- Client side events to tell when the api is ready
- Helpful react hooks
- Cache System for contract `view`

## Installing

Expand Down Expand Up @@ -89,7 +90,7 @@ You can also invoke a new instance anywhere, anytime with a new configuration if
// Invoking a Contract API
import { getContractApi } from '@wpdas/naxios'

// New contract instance
// New contract instance (This also supports cache)
const contract = await getContractApi({
contractId: ANOTHER_CONTRACT_ID,
network: 'testnet',
Expand All @@ -114,11 +115,45 @@ const walletApi = await getWalletApi({
walletApi.signInModal()
```

### Cache System

There are two kinds of cache systems to be used. They are `Memory Cache` and `Storage Cache`.

`Memory Cache`: will be cleared when the app refreshes, as its data lives in memory only. <br/>
`Storage Cache`: The data will remain even when the browser tab is refreshed. Data is persisted using Local Storage.

When instantiating a cache, you need to provide the `expirationTime` (in seconds). This is used to know when the cache should be returned instead of making a real contract call. When the cache expires, a real call to the contract is made. Each contract's method has its own time of expiration.

```ts
// web3Api.ts with cache
import naxios, { StorageCache } from '@wpdas/naxios'

const naxiosInstance = new naxios({
contractId: CONTRACT_ID,
network: 'testnet',
cache: new StorageCache({ expirationTime: 60 }),
})

export const contractApi = naxiosInstance.contractApi()
```

Then, to use cached `view`, you can just pass the configuration object saying you want to use cached data.

```ts
import { contractApi } from './web3Api'

const args: {}
const config: { useCache: true }

contractApi.view('get_greeting', args, config).then((response) => console.log(response))
```

#### Contract API Reference

- `view`: Make a read-only call to retrieve information from the network. It has the following parameters:
- `method`: Contract's method name
- `props?`: an optional parameter with `args` for the contract's method
- `method`: Contract's method name.
- `props?`: an optional parameter with `args` for the contract's method.
- `config?`: currently, this has only the `useCache` prop. When useCache is true, this is going to use non-expired cached data instead of calling the contract's method.
- `call`: Call a method that changes the contract's state. This is payable. It has the following parameters:
- `method`: Contract's method name
- `props?`: an optional parameter with `args` for the contract's method, `gas`, `deposit` to be attached and `callbackUrl` if you want to take the user to a specific page after a transaction succeeds.
Expand Down Expand Up @@ -293,8 +328,9 @@ useEffect(() => {

- `ready`: boolean indicating whether the contract API is ready.
- `view`: Make a read-only call to retrieve information from the network. It has the following parameters:
- `method`: Contract's method name
- `props?`: an optional parameter with `args` for the contract's method
- `method`: Contract's method name.
- `props?`: an optional parameter with `args` for the contract's method.
- `config?`: currently, this has only the `useCache` prop. When useCache is true, this is going to use non-expired cached data instead of calling the contract's method.
- `call`: Call a method that changes the contract's state. This is payable. It has the following parameters:
- `method`: Contract's method name
- `props?`: an optional parameter with `args` for the contract's method, `gas`, `deposit` to be attached and `callbackUrl` if you want to take the user to a specific page after a transaction succeeds.
Expand Down
Binary file modified md/naxios-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wpdas/naxios",
"version": "1.3.0",
"version": "1.4.0",
"description": "Promise based NEAR Contract and NEAR Wallet client for browser",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down
59 changes: 59 additions & 0 deletions src/cache/MemoryCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { CacheConstructor, CacheI, Data } from './types'

let _memoryCache: Record<string, any> = {}

/**
* Memory Cache - It get cleared when the app refreshes
*/
class MemoryCache implements CacheI {
private expirationTime = 60 // Default is 1 minute

constructor({ expirationTime }: CacheConstructor) {
this.expirationTime = expirationTime || this.expirationTime
}

private prepareData(data: any) {
return {
expiresAt: Date.now() + this.expirationTime * 1000,
data,
}
}

setItem<T>(key: string, value: T) {
return new Promise<void>((resolve) => {
const updatedCache = { ..._memoryCache }
updatedCache[key] = this.prepareData(value)
_memoryCache = updatedCache
resolve()
})
}

getItem<T>(key: string) {
return new Promise<T | null>((resolve) => {
const item = (_memoryCache[key] as Data<T>) || null

if (item && Date.now() < item.expiresAt) {
// Use cached info
resolve(item.data)
return
}
resolve(null)
})
}

removeItem(key: string) {
return new Promise<void>((resolve) => {
const updatedCache: Record<string, any> = {}
Object.keys(_memoryCache).forEach((currentKey: any) => {
if (key !== currentKey) {
updatedCache[currentKey] = _memoryCache[currentKey]
}
})

_memoryCache = updatedCache
resolve()
})
}
}

export default MemoryCache
77 changes: 77 additions & 0 deletions src/cache/StorageCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { CacheConstructor, CacheI, Data } from './types'

let isLocalStorageAccessible = true

try {
// just try to read it
window.localStorage
} catch (error) {
isLocalStorageAccessible = false
}

/**
* Storage Cache - Data is persisted using Local Storage
*/
class StorageCache implements CacheI {
private expirationTime = 60 // Default is 1 minute

constructor({ expirationTime }: CacheConstructor) {
this.expirationTime = expirationTime || this.expirationTime
}

private prepareData(data: any) {
return {
expiresAt: Date.now() + this.expirationTime * 1000,
data,
}
}

setItem<T>(key: string, value: T) {
return new Promise<void>((resolve) => {
if (!isLocalStorageAccessible) {
resolve()
return
}

const data = this.prepareData(value)

// persist data
localStorage.setItem(key, JSON.stringify(data))
resolve()
})
}

getItem<T>(key: string) {
return new Promise<T | null>((resolve) => {
if (!isLocalStorageAccessible) {
resolve(null)
return
}

const cachedDataStr = localStorage.getItem(key)
const cachedData = cachedDataStr ? (JSON.parse(cachedDataStr) as Data<T>) : null

if (cachedData && Date.now() < cachedData.expiresAt) {
// Use cached info
resolve(cachedData.data)
return
}

resolve(null)
})
}

removeItem(key: string) {
return new Promise<void>((resolve) => {
if (!isLocalStorageAccessible) {
resolve()
return
}

localStorage.removeItem(key)
resolve()
})
}
}

export default StorageCache
2 changes: 2 additions & 0 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as MemoryCache } from './MemoryCache'
export { default as StorageCache } from './StorageCache'
17 changes: 17 additions & 0 deletions src/cache/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface CacheI {
setItem: <T>(key: string, value: T) => Promise<void>
getItem: <T>(key: string) => Promise<T | null>
removeItem: (key: string) => Promise<void>
}

export interface Data<T> {
expiresAt: number
data: T
}

export interface CacheConstructor {
/**
* Expiration time in seconds. After expiration, this is going to fetch real data instead of using cached data.
*/
expirationTime?: number
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './naxios'
export * from './hooks'
export * from './managers/types'
export * from './utils'
export * from './cache'
50 changes: 41 additions & 9 deletions src/managers/contract-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,30 @@ import { Transaction as CoreTransaction } from '@near-wallet-selector/core'
import { NO_DEPOSIT, THIRTY_TGAS } from './constants'
import { QueryResponseKind } from 'near-api-js/lib/providers/provider'
import WalletManager from './wallet-manager'
import { ChangeMethodArgs, Transaction, ViewMethodArgs } from './types'
import {
BuildViewInterfaceConfig,
BuildViewInterfaceProps,
ChangeMethodArgs,
ContractManagerConfig,
Transaction,
ViewMethodArgs,
} from './types'
import MemoryCache from '../cache/MemoryCache'
import StorageCache from '../cache/StorageCache'

type ResultType = QueryResponseKind & { result: any }

class ContractManager {
private walletManager: WalletManager
private cache?: MemoryCache | StorageCache

constructor(config: { walletManager: WalletManager; onInit?: () => void }) {
this.walletManager = config.walletManager
constructor({ walletManager, cache, onInit }: ContractManagerConfig) {
this.walletManager = walletManager
this.cache = cache

this.init().then(() => {
if (config.onInit) {
config.onInit()
if (onInit) {
onInit()
}
})
}
Expand All @@ -27,7 +38,20 @@ class ContractManager {
}

// Build View Method Interface
private async buildViewInterface<R>({ method = '', args = {} }) {
private async buildViewInterface<R>(props: BuildViewInterfaceProps) {
const { method = '', args = {}, config } = props

// Check if there's cached information, if so, returns it
// item name is composed of: contractAddress:method
if (config?.useCache && this.cache) {
const cachedData = await this.cache.getItem<R>(`${this.walletManager.contractId}:${method}`)

if (cachedData) {
return cachedData
}
}

// If there's no cache, go forward...
if (!this.walletManager.walletSelector) {
await this.walletManager.initNear()
}
Expand All @@ -43,7 +67,14 @@ class ContractManager {
finality: 'optimistic',
})) as ResultType

return JSON.parse(Buffer.from(res.result).toString()) as R
const outcome = JSON.parse(Buffer.from(res.result).toString()) as R

// If cache is avaiable, store data on it
if (config?.useCache && this.cache) {
await this.cache.setItem<R>(`${this.walletManager.contractId}:${method}`, outcome)
}

return outcome
}

// Build Call Method Interface
Expand Down Expand Up @@ -131,10 +162,11 @@ class ContractManager {
* [view] Make a read-only call to retrieve information from the network
* @param method Contract's method name
* @param {A} props.args - Function parameters
* @param config Additional configuration (cache)
* @returns
*/
async view<A extends {}, R>(method: string, props?: ViewMethodArgs<A>) {
return this.buildViewInterface<R>({ method, ...props })
async view<A extends {}, R>(method: string, props?: ViewMethodArgs<A>, config?: BuildViewInterfaceConfig) {
return this.buildViewInterface<R>({ method, args: { ...props }, config })
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/managers/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import MemoryCache from '../cache/MemoryCache'
import { WalletModuleFactory } from '@near-wallet-selector/core'
import WalletManager from './wallet-manager'
import StorageCache from '@lib/cache/StorageCache'

export type ViewMethodArgs<A> = {
args: A
Expand Down Expand Up @@ -37,3 +40,32 @@ export type Config = {
walletSelectorModules?: WalletModuleFactory[]
onInit?: () => void
}

export type ContractManagerConfig = {
walletManager: WalletManager
onInit?: () => void
cache?: MemoryCache | StorageCache
}

export type BuildViewInterfaceProps = {
method: string
args: {}
config?: BuildViewInterfaceConfig
}

export type BuildViewInterfaceConfig = {
/**
* Use cached data (if avaiable and not expired)?
*/
useCache?: boolean
}

// Naxios Constructor
export type NaxiosConstructor = {
contractId: string
network: Network
walletSelectorModules?: WalletModuleFactory[]
cache?: MemoryCache | StorageCache
}

export type GetContractApi = NaxiosConstructor
Loading

0 comments on commit b537a38

Please sign in to comment.