Last active
September 23, 2023 06:34
-
-
Save gichamba/7b93b74b1cde88b59b2eca489f660c50 to your computer and use it in GitHub Desktop.
Charging Engine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @typescript-eslint/no-floating-promises */ | |
/* eslint-disable @typescript-eslint/strict-boolean-expressions */ | |
/* eslint-disable @typescript-eslint/restrict-template-expressions */ | |
/* eslint-disable @typescript-eslint/no-misused-promises */ | |
import express from "express"; | |
import { json } from "body-parser"; | |
import { createClient } from "redis"; | |
const DEFAULT_BALANCE = 100; | |
interface ChargeResult { | |
isAuthorized: boolean; | |
remainingBalance: number; | |
charges: number; | |
} | |
interface EvalOptions { | |
keys?: string[]; | |
arguments?: string[]; | |
} | |
interface LockResult { | |
success: number; | |
balance: number; | |
} | |
async function connect(): Promise<ReturnType<typeof createClient>> { | |
const url = `redis://${process.env.REDIS_HOST ?? "localhost"}:${process.env.REDIS_PORT ?? "6379"}`; | |
const client = createClient({ url }); | |
await client.connect(); | |
return client; | |
} | |
async function reset(account: string): Promise<void> { | |
const balanceKey = `${account}/balance`; | |
const client = await connect(); | |
try { | |
await client.set(balanceKey, DEFAULT_BALANCE); | |
} finally { | |
await client.disconnect(); | |
} | |
} | |
// Added function to set a lock and retrieve balance in a single transaction | |
async function setLock(client: any, account: string): Promise<LockResult> { | |
const balanceVersionKey = `${account}/balance/version`; | |
const balanceVersionValue = generateRandomString(10); | |
const balanceKey = `${account}/balance`; | |
// We need the key to self destruct after 30 milliseconds | |
// to remove the need of manually having to delete it | |
const ttlMilliseconds = 30; | |
// Use a Lua script to set the key if it doesn't exist and set TTL | |
// And get the account balance | |
const luaScript = ` | |
local success | |
local balance = redis.call("GET", KEYS[1]) | |
if redis.call("EXISTS", KEYS[2]) == 0 then | |
redis.call("SET", KEYS[2], ARGV[1]) | |
redis.call("PEXPIRE", KEYS[2], ARGV[2]) | |
success = 1 | |
else | |
success = 0 | |
end | |
return {success, balance} | |
`; | |
// Define EvalOptions | |
const evalOptions: EvalOptions = { | |
keys: [balanceKey, balanceVersionKey], | |
arguments: [balanceVersionValue, ttlMilliseconds.toString()], | |
}; | |
const result = await client.eval(luaScript, evalOptions); | |
const lockResult: LockResult = { | |
success: result[0], | |
balance: parseInt(result[1]), | |
}; | |
return lockResult; | |
} | |
// updated charge function to use setLock function | |
async function charge(account: string, charges: number): Promise<ChargeResult> { | |
const client = await connect(); | |
const balanceKey = `${account}/balance`; | |
try { | |
const lockResult = await setLock(client, account); | |
const balance = lockResult.balance; | |
if (lockResult.success === 0 || charges > balance) { | |
return { isAuthorized: false, remainingBalance: balance, charges: 0 }; | |
} | |
const newBalance = balance - charges; | |
await client.set(balanceKey, newBalance); | |
return { isAuthorized: true, remainingBalance: newBalance, charges }; | |
} finally { | |
await client.disconnect(); | |
} | |
} | |
// Added function to generate random string | |
const generateRandomString = (length: number): string => { | |
let result = ""; | |
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
const charactersLength = characters.length; | |
for (let i = 0; i < length; i++) { | |
result += characters.charAt(Math.floor(Math.random() * charactersLength)); | |
} | |
return result; | |
}; | |
export function buildApp(): express.Application { | |
const app = express(); | |
app.use(json()); | |
app.post("/reset", async (req, res) => { | |
try { | |
const account = req.body.account ?? "account"; | |
await reset(account); | |
console.log(`Successfully reset account ${account}`); | |
res.sendStatus(204); | |
} catch (e) { | |
console.error("Error while resetting account", e); | |
res.status(500).json({ error: String(e) }); | |
} | |
}); | |
app.post("/charge", async (req, res) => { | |
try { | |
const account = req.body.account ?? "account"; | |
const result = await charge(account, req.body.charges ?? 10); | |
// Changed what is logged to make it easier to observe operation success and account balance | |
console.log(result); | |
res.status(200).json(result); | |
} catch (e) { | |
console.error("Error while charging account", e); | |
res.status(500).json({ error: String(e) }); | |
} | |
}); | |
return app; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment