NoriKV TypeScript Client Architecture¶
Understanding the internal design and components of the TypeScript client SDK.
Table of Contents¶
- Overview
- Component Architecture
- Request Flow
- Async/Promise Model
- Connection Management
- Routing & Sharding
- Retry Logic
- Error Handling
Overview¶
The NoriKV TypeScript client is designed as a smart client that: - Routes requests directly to the appropriate shard leader - Maintains connection pools for efficient communication - Implements retry logic with exponential backoff - Tracks cluster topology changes - Provides Promise-based async operations - Optimizes for V8 JavaScript engine
Design Principles¶
- Zero-hop routing: Client routes directly to shard leader (no proxy)
- Async-first: All operations return Promises
- Type-safe: Full TypeScript types for compile-time safety
- Observable: Expose metrics and statistics
- Dual-package: ESM + CommonJS support
Component Architecture¶
┌─────────────────────────────────────────────────────────┐
│ NoriKVClient │
│ (Main API: put, get, delete, topology, stats) │
└──────────────────┬──────────────────────────────────────┘
│
┌──────────┼──────────┬──────────┐
│ │ │ │
┌───────▼────┐ ┌──▼────┐ ┌───▼──────┐ ┌─▼─────────┐
│ Router │ │ Retry │ │ Pool │ │ Topology │
│ │ │Policy │ │ │ │ Manager │
└────────────┘ └───────┘ └──────────┘ └───────────┘
│ │ │
│ │ │
└─────────hash()──────────┤ │
│ │
┌─────────getChannel()────┤ │
│ │ │
│ ┌────▼────┐ │
│ │ gRPC │ │
│ │Channels │ │
│ └────┬────┘ │
│ │ │
│ │ │
└─────────updateView()────┴──────────────┘
Components¶
1. NoriKVClient¶
Responsibility: Main public API and component coordination
Key Methods:
- put(), get(), delete() - Core operations (all async)
- getClusterView() - Topology information
- onTopologyChange() - Subscribe to topology updates
- getStats() - Client statistics
- close() - Resource cleanup
Location: src/client.ts
2. Router¶
Responsibility: Determine which node to send requests to
Key Functions:
- Hash key to shard: xxhash64(key) → jumpConsistentHash(hash, totalShards) → shardId
- Map shard to leader node
- Cache leader information
- Handle leader hints from NOT_LEADER errors
Location: src/internal/router.ts
Algorithm:
1. Hash key using XXHash64 (seed=0) via xxhash-wasm
2. Map hash to shard using Jump Consistent Hash
3. Look up shard leader in topology cache
4. Return leader's address
3. ConnectionPool¶
Responsibility: Manage gRPC channels to cluster nodes
Key Functions: - Create and cache gRPC channels per node - Thread-safe concurrent access - Graceful shutdown
Location: src/internal/conn/pool.ts
Design:
- One gRPC Client per node address
- Lazy initialization (created on first use)
- Channels reused across requests
- Automatic cleanup on client close
4. RetryPolicy¶
Responsibility: Handle transient failures with backoff
Key Functions:
- Exponential backoff: delay = min(initialDelay * 2^attempt, maxDelay)
- Jitter: Add randomness to avoid thundering herd
- Selective retry: Only retry transient errors
- Attempt tracking
Location: src/internal/retry/policy.ts
Retryable Errors:
- Unavailable - Server temporarily unavailable
- Aborted - Operation aborted, safe to retry
- DeadlineExceeded - Timeout, may succeed on retry
- ResourceExhausted - Rate limited, backoff helps
Non-Retryable Errors:
- InvalidArgument - Client error, won't succeed
- NotFound - Key doesn't exist
- FailedPrecondition - CAS conflict, application must retry
- PermissionDenied - Auth error
5. TopologyManager¶
Responsibility: Track cluster membership and shard assignments
Key Functions:
- Store current ClusterView
- Cache shard → leader mappings
- Detect topology changes
- Notify listeners of changes
- Update leader hints
Location: src/internal/topology/manager.ts
Data Structures:
- ClusterView: Current cluster state (epoch, nodes, shards)
- shardLeaderCache: Maplisteners: Array of change callbacks
Request Flow¶
PUT Request Flow¶
Client.put(key, value, options)
│
├─> 1. Validate inputs (key, value not null/empty)
│
├─> 2. Router.getNodeForKey(key)
│ ├─> hash = xxhash64(key)
│ ├─> shardId = jumpConsistentHash(hash, totalShards)
│ └─> leaderAddr = topologyManager.getShardLeader(shardId)
│
├─> 3. ConnectionPool.getChannel(leaderAddr)
│ └─> Return cached or create new gRPC channel
│
├─> 4. RetryPolicy.execute(async () => {
│ ├─> Build gRPC PutRequest
│ ├─> await grpcClient.put(request)
│ └─> Convert response to Version
│ })
│ ├─> On SUCCESS: return Version
│ ├─> On RETRYABLE_ERROR: backoff and retry
│ └─> On NON_RETRYABLE: throw error
│
└─> 5. Return Version to caller
GET Request Flow¶
Similar to PUT, but:
- Uses GetRequest with consistency level
- Returns GetResult (value + version)
- Throws KeyNotFoundError on NOT_FOUND
Error Handling in Flow¶
gRPC Status Error
│
├─> convertGrpcError()
│ ├─> NOT_FOUND → KeyNotFoundError
│ ├─> FAILED_PRECONDITION + "version" → VersionMismatchError
│ ├─> UNAVAILABLE → ConnectionError
│ └─> OTHER → NoriKVError
│
└─> RetryPolicy decides:
├─> Retryable → backoff and retry
└─> Non-retryable → throw to caller
Async/Promise Model¶
Promise-Based API¶
All client operations return Promises:
// All methods are async
async put(key: string | Uint8Array, value: string | Uint8Array, options?: PutOptions): Promise<Version>
async get(key: string | Uint8Array, options?: GetOptions): Promise<GetResult>
async delete(key: string | Uint8Array, options?: DeleteOptions): Promise<boolean>
Async/Await Pattern¶
// Modern async/await
async function example() {
const version = await client.put(key, value);
const result = await client.get(key);
await client.delete(key);
}
// Sequential operations
const v1 = await client.put('k1', 'v1');
const v2 = await client.put('k2', 'v2'); // Waits for v1
// Concurrent operations
const [v1, v2] = await Promise.all([
client.put('k1', 'v1'),
client.put('k2', 'v2'), // Runs concurrently
]);
Error Handling¶
try {
const result = await client.get(key);
} catch (error) {
if (error instanceof KeyNotFoundError) {
// Handle not found
} else if (error instanceof ConnectionError) {
// Handle connection error
}
throw error;
}
Connection Management¶
Channel Lifecycle¶
Node Address
│
├─> First request → Create gRPC Client
│ ├─> Configure: credentials, options
│ └─> Store in pool
│
├─> Subsequent requests → Reuse channel
│
└─> Client.close() → Close all channels
└─> Graceful shutdown with timeout
Channel Configuration¶
const client = new grpc.Client(
address,
grpc.credentials.createInsecure(),
{
'grpc.keepalive_time_ms': 10000,
'grpc.keepalive_timeout_ms': 3000,
}
);
Health Checks¶
- Channels automatically reconnect on failure
- gRPC handles connection health internally
- Failed requests trigger retries (via RetryPolicy)
Routing & Sharding¶
Hash Function: XXHash64¶
Properties: - Fast: Optimized for V8 - Consistent: Same key → same hash - Cross-SDK compatible
Consistent Hashing: Jump Consistent Hash¶
function jumpConsistentHash(key: bigint, numBuckets: number): number {
let b = -1n, j = 0n;
while (j < BigInt(numBuckets)) {
b = j;
key = key * 2862933555777941757n + 1n;
j = BigInt((Number(b) + 1) * (Number((1n << 31n)) / Number((key >> 33n) + 1n)));
}
return Number(b);
}
Properties: - Minimal key movement on shard count changes - O(log n) time complexity - Uniform distribution
Shard → Leader Mapping¶
Leader Cache: - Populated from ClusterView - Updated on topology changes - Updated from NOT_LEADER error hints
Retry Logic¶
Exponential Backoff¶
const delay = Math.min(
initialDelay * Math.pow(2, attempt),
maxDelay
) + Math.random() * jitter;
await new Promise(resolve => setTimeout(resolve, delay));
Example (initialDelay=100ms, maxDelay=5s, jitter=100ms):
Attempt 1: delay = 100ms + random(0-100ms)
Attempt 2: delay = 200ms + random(0-100ms)
Attempt 3: delay = 400ms + random(0-100ms)
Attempt 4: delay = 800ms + random(0-100ms)
Attempt 5: delay = 1600ms + random(0-100ms)
Attempt 6: delay = 3200ms + random(0-100ms)
Attempt 7: delay = 5000ms + random(0-100ms) (capped)
Jitter Benefits¶
- Avoids thundering herd (all clients retry at same time)
- Spreads load during recovery
- Reduces collision probability
Error Handling¶
Error Hierarchy¶
export class NoriKVError extends Error {
constructor(
message: string,
public code: string,
public cause?: Error
) {
super(message);
this.name = 'NoriKVError';
}
}
export class KeyNotFoundError extends NoriKVError {}
export class VersionMismatchError extends NoriKVError {}
export class AlreadyExistsError extends NoriKVError {}
export class ConnectionError extends NoriKVError {}
Error Code Mapping¶
| gRPC Status | NoriKV Error | Retry? |
|---|---|---|
| NOT_FOUND | KeyNotFoundError | No |
| FAILED_PRECONDITION (version) | VersionMismatchError | No |
| FAILED_PRECONDITION (other) | NoriKVError | No |
| ALREADY_EXISTS | AlreadyExistsError | No |
| UNAVAILABLE | ConnectionError | Yes |
| DEADLINE_EXCEEDED | ConnectionError | Yes |
| ABORTED | NoriKVError | Yes |
| RESOURCE_EXHAUSTED | NoriKVError | Yes |
| INVALID_ARGUMENT | NoriKVError | No |
| PERMISSION_DENIED | NoriKVError | No |
Performance Considerations¶
Hot Paths¶
- Hash calculation: Optimized XXHash64 via wasm
- Channel lookup: O(1) Map lookup
- Leader cache: O(1) Map lookup
- Protobuf serialization: Native JavaScript
Memory Usage¶
- Per client: ~1-10 MB (depends on number of nodes)
- Per channel: ~100 KB (gRPC overhead)
- Per request: Minimal (garbage collected)
V8 Optimizations¶
- JIT compilation of hot paths
- Inline caching for property access
- Hidden classes for consistent object shapes
Connection Pooling¶
- Channels reused across requests
- No connection per request overhead
- HTTP/2 multiplexing
TypeScript-Specific Features¶
Full Type Safety¶
import { NoriKVClient, GetResult, Version } from '@norikv/client';
const client: NoriKVClient = new NoriKVClient(config);
const result: GetResult = await client.get(key);
const version: Version = result.version;
Discriminated Unions¶
Generic Type Parameters¶
Browser Compatibility¶
The TypeScript SDK can run in browsers with:
- gRPC-Web: Use @grpc/grpc-js polyfill
- Webpack 5: Configure fallbacks for Node.js modules
- Buffer polyfill: Use buffer package
// webpack.config.js
module.exports = {
resolve: {
fallback: {
buffer: require.resolve('buffer/'),
stream: require.resolve('stream-browserify'),
},
},
};
References¶
- API Guide - Public API documentation
- Troubleshooting Guide - Common issues
- Advanced Patterns - Complex use cases
- Source Code - Implementation