qwuack
v0.1.4
Published
Redis-backed ledger with running totals optimization
Maintainers
Readme
Features
- O(1) Balance Queries - Uses running totals instead of scanning all entries
- Atomic Transactions - All write operations use Redis MULTI/EXEC
- Dual Client Support - Works with both
redis(node-redis) andioredis - Deduplication - Prevents duplicate entries by ID
- Entry Limits - Configurable maximum entries per account/currency
- Pagination - Efficient cursor-based pagination for large datasets
- Context Breakdown - Track balances by context (deposit, withdrawal, etc.)
Installation
npm install qwuack
# or
bun add qwuackYou also need one of the supported Redis clients:
# Option 1: node-redis
npm install redis
# Option 2: ioredis
npm install ioredisQuick Start
With node-redis
import { createClient } from "redis";
import { Ledger } from "qwuack";
const redis = createClient();
await redis.connect();
const ledger = new Ledger(redis);
// Add an entry
await ledger.addEntry("user_123", "usd", {
id: "txn_001",
context: "deposit",
currency: "usd",
amount: "100.00",
});
// Get balance (O(1) operation)
const balance = await ledger.getBalance("user_123", "usd");
console.log(balance);
// { total: "100.00", byContext: { deposit: "100.00" }, entryCount: 1 }With ioredis
import Redis from "ioredis";
import { Ledger } from "qwuack";
const redis = new Redis();
const ledger = new Ledger(redis);
// Same API as above
await ledger.addEntry("user_123", "usd", {
id: "txn_001",
context: "deposit",
currency: "usd",
amount: "100.00",
});API Reference
new Ledger(redis, config?)
Creates a new Ledger instance.
| Parameter | Type | Description |
|-----------|------|-------------|
| redis | RedisClient \| IORedis | Redis client instance |
| config.maxEntriesPerKey | number | Maximum entries per account/currency (default: 1,000,000) |
| config.keyPrefix | string | Redis key prefix (default: "ledger") |
addEntry(accountId, currency, entry)
Adds a new ledger entry. Throws if entry ID already exists or limit reached.
await ledger.addEntry("user_123", "usd", {
id: "txn_001", // Unique entry ID
context: "deposit", // Category for breakdown
currency: "usd", // Currency code
amount: "100.00", // Amount as string
});removeEntry(accountId, currency, entryId)
Removes an entry and updates running totals. Returns true if removed, false if not found.
const removed = await ledger.removeEntry("user_123", "usd", "txn_001");getEntry(accountId, currency, entryId)
Retrieves a single entry by ID. Returns null if not found.
const entry = await ledger.getEntry("user_123", "usd", "txn_001");getSum(accountId, currency)
Returns the total sum for an account/currency pair. O(1) operation.
const total = await ledger.getSum("user_123", "usd");
// "100.00"getBalance(accountId, currency)
Returns complete balance information including context breakdown. O(1) operation.
const balance = await ledger.getBalance("user_123", "usd");
// {
// total: "250.00",
// byContext: { deposit: "300.00", withdrawal: "-50.00" },
// entryCount: 5
// }getEntriesPaginated(accountId, currency, cursor?, count?)
Retrieves entries with cursor-based pagination.
let cursor = "0";
do {
const result = await ledger.getEntriesPaginated("user_123", "usd", cursor, 100);
console.log(result.entries);
cursor = result.nextCursor;
} while (result.hasMore);clearLedger(accountId, currency)
Removes all entries and totals for an account/currency pair.
await ledger.clearLedger("user_123", "usd");Configuration
const ledger = new Ledger(redis, {
maxEntriesPerKey: 500_000, // Limit entries per account/currency
keyPrefix: "myapp:ledger", // Custom key prefix
});Redis Key Structure
For an account user_123 with currency usd and default prefix:
| Key | Type | Purpose |
|-----|------|---------|
| ledger:user_123:usd | Hash | Stores all entries |
| ledger:user_123:usd:total | String | Running total sum |
| ledger:user_123:usd:ctx | Hash | Running totals by context |
Performance
| Operation | Time Complexity |
|-----------|-----------------|
| addEntry | O(1) |
| removeEntry | O(1) |
| getEntry | O(1) |
| getSum | O(1) |
| getBalance | O(1) |
| getEntriesPaginated | O(n) where n = page size |
| clearLedger | O(1) |
Development
# Install dependencies
bun install
# Run tests
bun test
# Run tests with coverage
bun test --coverage
# Build for npm
bun run buildLicense
MIT
