Skip to content

Commit

Permalink
feat(cache): atomic lock
Browse files Browse the repository at this point in the history
  • Loading branch information
KABBOUCHI committed Nov 10, 2023
1 parent 88ac930 commit 9140e70
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 18 deletions.
71 changes: 54 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ const providerWithCustomOptions = new JsonRpcRetryProvider(
import { JsonRpcRetryProvider } from "@instadapp/utils";

const provider = new JsonRpcRetryProvider([
'https://rpc.ankr.com/invalid',
'https://rpc.ankr.com/invalid-2',
'https://rpc.ankr.com/eth',
'https://eth.llamarpc.com',
])
"https://rpc.ankr.com/invalid",
"https://rpc.ankr.com/invalid-2",
"https://rpc.ankr.com/eth",
"https://eth.llamarpc.com",
]);
```

### JsonRpcRetryBatchProvider
Expand Down Expand Up @@ -212,6 +212,15 @@ Cache.extend("mongodb", {
async set(key: string, value: any, seconds?: number) {},
async forget(key: string) {},
async flush() {},
// optional, and experimental
lock(key: string, seconds: number) {
return {
async acquire() {
return true;
},
async release() {},
};
},
});

// Note: you should set this once per life cycle
Expand Down Expand Up @@ -241,34 +250,62 @@ const users = await Cache.remember("users", seconds, async () => {

await Cache.store("memory").get("users:1"); // safe way to switch driver for a moment

// experimental
const isLocked = await Cache.lock("key", 10).get(async () => {
console.log("do something");
});

// or
const lock = Cache.lock("key", 10);

if (await lock.get()) {
console.log("do something");

await lock.release();
}
```

### toJsonRpcProvider

```ts
import { toJsonRpcProvider } from "@instadapp/utils";
import { ethers } from "ethers";

let provider = toJsonRpcProvider(window.ethereum) // Metamask, Rabby, ...
let provider = toJsonRpcProvider(web3)// Web3.js instance, `web3.currentProvider` will be used internally
let provider = toJsonRpcProvider(web3.currentProvider)// Web3.js provider, ex: Metamask, Rabby, WalletConnect, ...
let provider = toJsonRpcProvider("https://rpc.ankr.com/eth") // Http RPC URL
let provider = toJsonRpcProvider(new ethers.providers.JsonRpcProvider('https://rpc.ankr.com/eth')) // ethers JsonRpcProvider instance
let provider = toJsonRpcProvider(window.ethereum); // Metamask, Rabby, ...
let provider = toJsonRpcProvider(web3); // Web3.js instance, `web3.currentProvider` will be used internally
let provider = toJsonRpcProvider(web3.currentProvider); // Web3.js provider, ex: Metamask, Rabby, WalletConnect, ...
let provider = toJsonRpcProvider("https://rpc.ankr.com/eth"); // Http RPC URL
let provider = toJsonRpcProvider(
new ethers.providers.JsonRpcProvider("https://rpc.ankr.com/eth")
); // ethers JsonRpcProvider instance
```

### Blockscan

```ts
import { Blockscan, Chain } from "@instadapp/utils";

const etherscan = new Blockscan(Chain.Mainnet);

await etherscan.getTransactions("0x6975be450864c02b4613023c2152ee0743572325")
await etherscan.getInternalTransactions("0x6975be450864c02b4613023c2152ee0743572325")
await etherscan.getErc20TokenTransferEvents("0x6975be450864c02b4613023c2152ee0743572325")
await etherscan.getErc721TokenTransferEvents("0x6975be450864c02b4613023c2152ee0743572325")
await etherscan.getErc1155TokenTransferEvents("0x6975be450864c02b4613023c2152ee0743572325")
await etherscan.getTransactions("0x6975be450864c02b4613023c2152ee0743572325");
await etherscan.getInternalTransactions(
"0x6975be450864c02b4613023c2152ee0743572325"
);
await etherscan.getErc20TokenTransferEvents(
"0x6975be450864c02b4613023c2152ee0743572325"
);
await etherscan.getErc721TokenTransferEvents(
"0x6975be450864c02b4613023c2152ee0743572325"
);
await etherscan.getErc1155TokenTransferEvents(
"0x6975be450864c02b4613023c2152ee0743572325"
);

const basescan = Blockscan.custom('https://basescan.org','https://api.basescan.org/api')
await basescan.contractSourceCode('0x833589fcd6edb6e08f4c7c32d4f71b54bda02913')
const basescan = Blockscan.custom(
"https://basescan.org",
"https://api.basescan.org/api"
);
await basescan.contractSourceCode("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913");
```

## 💻 Development
Expand Down
37 changes: 36 additions & 1 deletion src/cache/drivers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ICacheDriver, ICacheLock } from '..'

const cache = new Map()

export const memoryCacheDriver = {
export const memoryCacheDriver: ICacheDriver = {
async get (key: string) {
const value = cache.get(key)
const expireOn = cache.get(`${key}:expireOn`)
Expand Down Expand Up @@ -30,5 +32,38 @@ export const memoryCacheDriver = {
cache.clear()

await Promise.resolve()
},
lock (key, seconds) {
return new MemoryLock(key, seconds)
}
}

const cacheLock = new Map()

class MemoryLock implements ICacheLock {
// eslint-disable-next-line no-useless-constructor
constructor (private key: string, private seconds: number) { }

acquire () {
const currentTime = Date.now()
const expiryTime = currentTime + this.seconds * 1000

const existingLock = cacheLock.get(this.key)

if (existingLock && existingLock > currentTime) {
return false
}

cacheLock.set(this.key, expiryTime)

return true
}

release () {
const lockExpiry = cacheLock.get(this.key)

if (lockExpiry && lockExpiry > Date.now()) {
cacheLock.delete(this.key)
}
}
}
33 changes: 33 additions & 0 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { memoryCacheDriver } from './drivers'

export interface ICacheLock {
acquire: () => Promise<boolean>|boolean
release: () => Promise<void>|void
}

export interface ICacheDriver {
get(key: string): Promise<any>
set(key: string, value: any, seconds?: number): Promise<void>
forget(key: string): Promise<void>
flush(): Promise<void>
lock?: (key: string, seconds: number) => ICacheLock
}

export class Cache {
Expand Down Expand Up @@ -93,4 +99,31 @@ export class Cache {

return value
}

static lock (key: string, seconds: number) {
if (!this.driver.lock) {
throw new Error(`Driver ${this.defaultDriver} does not support locking`)
}

const lock = this.driver.lock(key, seconds)

return {
release: lock.release,
get: async (cb?: () => Promise<void> | void): Promise<boolean> => {
if (!cb) {
return await lock.acquire()
}

if (await lock.acquire()) {
await cb()

await lock.release()

return true
}

return false
}
}
}
}
32 changes: 32 additions & 0 deletions test/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,36 @@ describe('cache', () => {

expect(await Cache.get('test')).toBe(69)
})

test('atomic lock', async () => {
const lock = Cache.lock('lock', 10)

if (await lock.get()) {
await wait(1000)

await lock.release()
}
})

test('atomic lock - cb', async () => {
const acquired = await Cache.lock('lock-cb', 10).get(async () => {
await wait(1000)
})

expect(acquired).toBe(true)
})

test('atomic lock - multiple', async () => {
const [acquired, notAcquired] = await Promise.all([
Cache.lock('lock-multiple', 10).get(async () => {
await wait(1000)
}),
Cache.lock('lock-multiple', 10).get(async () => {
await wait(1000)
})
])

expect(acquired).toBe(true)
expect(notAcquired).toBe(false)
})
})

0 comments on commit 9140e70

Please sign in to comment.