Skip to content

NoriKV TypeScript Client API Guide

Complete reference for the NoriKV TypeScript/JavaScript Client SDK.

Table of Contents

Installation

npm install @norikv/client
# or
yarn add @norikv/client
# or
pnpm add @norikv/client

Quick Start

import { NoriKVClient, bytesToString } from '@norikv/client';

const client = new NoriKVClient({
  nodes: ['localhost:9001', 'localhost:9002'],
  totalShards: 1024,
  timeout: 5000,
});

await client.connect();

// Put a value
await client.put('user:alice', 'Alice');

// Get the value
const result = await client.get('user:alice');
console.log(bytesToString(result.value)); // 'Alice'

// Delete
await client.delete('user:alice');

await client.close();

Client Configuration

Basic Configuration

import { NoriKVClient, ClientConfig } from '@norikv/client';

const config: ClientConfig = {
  nodes: ['node1:9001', 'node2:9001'],
  totalShards: 1024,
  timeout: 5000,
};

const client = new NoriKVClient(config);

Configuration Options

Option Type Default Description
nodes string[] Required List of node addresses (host:port)
totalShards number Required Total number of shards in cluster
timeout number 5000 Request timeout in milliseconds
retry RetryConfig See below Retry policy configuration

Retry Configuration

import { RetryConfig } from '@norikv/client';

const retryConfig: RetryConfig = {
  maxAttempts: 10,
  initialDelayMs: 100,
  maxDelayMs: 5000,
  jitterMs: 100,
};

const client = new NoriKVClient({
  nodes: ['localhost:9001'],
  totalShards: 1024,
  retry: retryConfig,
});

Retry Behavior: - Retries transient errors: Unavailable, Aborted, DeadlineExceeded - Does NOT retry: InvalidArgument, NotFound, FailedPrecondition - Uses exponential backoff with jitter

Core Operations

PUT - Write Data

Basic PUT

const key = 'user:123';
const value = JSON.stringify({ name: 'Alice' });

const version = await client.put(key, value);
console.log('Written at version:', version);

PUT with Options

import { PutOptions } from '@norikv/client';

const options: PutOptions = {
  ttlMs: 60000,                    // TTL: 60 seconds
  idempotencyKey: 'order-12345',   // Idempotency key
  ifMatchVersion: expectedVersion, // CAS
};

const version = await client.put(key, value, options);

PutOptions Fields:

Field Type Description
ttlMs number? Time-to-live in milliseconds
idempotencyKey string? Key for idempotent operations
ifMatchVersion Version? Expected version for CAS

GET - Read Data

Basic GET

const result = await client.get('user:123');

const value = bytesToString(result.value);
const version = result.version;

GET with Consistency Level

import { GetOptions, ConsistencyLevel } from '@norikv/client';

const options: GetOptions = {
  consistency: ConsistencyLevel.LINEARIZABLE,
};

const result = await client.get(key, options);

Consistency Levels:

Level Description Use Case
LEASE Default, lease-based read Most operations (fast, usually consistent)
LINEARIZABLE Strictest, always up-to-date Critical reads requiring absolute consistency
STALE_OK May return stale data Read-heavy workloads, caching

DELETE - Remove Data

Basic DELETE

const deleted = await client.delete('user:123');
console.log('Deleted:', deleted);

DELETE with Options

import { DeleteOptions } from '@norikv/client';

const options: DeleteOptions = {
  idempotencyKey: 'delete-order-12345',
  ifMatchVersion: expectedVersion,
};

await client.delete(key, options);

Advanced Features

Compare-And-Swap (CAS)

Optimistic concurrency control using version matching:

// Read current value
const current = await client.get(key);
const value = parseInt(bytesToString(current.value));

// Update with CAS
const newValue = (value + 1).toString();
try {
  await client.put(key, newValue, {
    ifMatchVersion: current.version,
  });
  console.log('CAS succeeded');
} catch (err) {
  if (err instanceof VersionMismatchError) {
    console.log('CAS failed - version changed');
  }
  throw err;
}

Idempotent Operations

Safe retries using idempotency keys:

import { v4 as uuidv4 } from 'uuid';

const idempotencyKey = `payment-${uuidv4()}`;

// First attempt
const v1 = await client.put(key, value, { idempotencyKey });

// Retry with same key (safe - returns same version)
const v2 = await client.put(key, value, { idempotencyKey });

console.log(v1.equals(v2)); // true

Time-To-Live (TTL)

Automatic expiration:

await client.put(key, value, {
  ttlMs: 60000, // Expires in 60 seconds
});

// Key automatically deleted after TTL
await new Promise(resolve => setTimeout(resolve, 61000));

try {
  await client.get(key);
} catch (err) {
  if (err instanceof KeyNotFoundError) {
    console.log('Key expired');
  }
}

Cluster Topology

Monitor cluster changes:

// Get current cluster view
const view = client.getClusterView();
if (view) {
  console.log('Cluster epoch:', view.epoch);
  console.log('Nodes:', view.nodes.length);
}

// Subscribe to topology changes
const unsubscribe = client.onTopologyChange((event) => {
  console.log('Topology changed!');
  console.log('Previous epoch:', event.previousEpoch);
  console.log('Current epoch:', event.currentEpoch);
  console.log('Added nodes:', event.addedNodes);
  console.log('Removed nodes:', event.removedNodes);
});

// Later: unsubscribe
unsubscribe();

Client Statistics

Monitor client performance:

const stats = client.getStats();

console.log('Active connections:', stats.pool.activeConnections);
console.log('Total nodes:', stats.router.totalNodes);
console.log('Cached leaders:', stats.topology.cachedLeaders);

Vector Operations

NoriKV supports vector similarity search for building AI/ML applications, recommendation systems, and semantic search.

Creating a Vector Index

Before inserting vectors, create an index with your configuration:

const created = await client.vectorCreateIndex(
  'embeddings',      // namespace
  1536,              // dimensions
  'cosine',          // distance function: 'euclidean' | 'cosine' | 'inner_product'
  'hnsw'             // index type: 'brute_force' | 'hnsw'
);

if (created) {
  console.log('Index created');
} else {
  console.log('Index already exists');
}

With Options

import { CreateVectorIndexOptions } from '@norikv/client';

const options: CreateVectorIndexOptions = {
  idempotencyKey: 'create-embeddings-index',
};

const created = await client.vectorCreateIndex(
  'embeddings',
  1536,
  'cosine',
  'hnsw',
  options
);

Distance Functions

Value Description Use Case
'euclidean' L2 distance General purpose
'cosine' Cosine similarity (1 - cos) Text embeddings, normalized vectors
'inner_product' Negative inner product Maximum inner product search

Index Types

Value Description Trade-off
'brute_force' Exact linear scan Exact results, O(n) complexity
'hnsw' Hierarchical Navigable Small World Approximate, O(log n) complexity

Inserting Vectors

const embedding = await getEmbedding('Hello world');

const version = await client.vectorInsert(
  'embeddings',    // namespace
  'doc-123',       // unique ID
  embedding        // number[]
);

console.log('Inserted at version:', version);

With Options

import { VectorInsertOptions } from '@norikv/client';

const options: VectorInsertOptions = {
  idempotencyKey: 'insert-doc-123',
};

const version = await client.vectorInsert('embeddings', 'doc-123', embedding, options);

Searching for Similar Vectors

const query = await getEmbedding('Find similar documents');

const result = await client.vectorSearch(
  'embeddings',    // namespace
  query,           // query vector
  10               // k nearest neighbors
);

console.log(`Search took ${result.searchTimeUs}us`);

for (const match of result.matches) {
  console.log(`ID: ${match.id}, Distance: ${match.distance}`);
}

With Options

import { VectorSearchOptions, VectorSearchResult } from '@norikv/client';

const options: VectorSearchOptions = {
  includeVectors: true, // include vector data in results
};

const result: VectorSearchResult = await client.vectorSearch(
  'embeddings',
  query,
  10,
  options
);

for (const match of result.matches) {
  console.log(`ID: ${match.id}, Distance: ${match.distance}`);
  if (match.vector) {
    console.log(`Vector dims: ${match.vector.length}`);
  }
}

Getting a Vector by ID

const vector = await client.vectorGet('embeddings', 'doc-123');

if (vector) {
  console.log(`Vector has ${vector.length} dimensions`);
} else {
  console.log('Vector not found');
}

Deleting Vectors

const deleted = await client.vectorDelete('embeddings', 'doc-123');

if (deleted) {
  console.log('Vector deleted');
} else {
  console.log('Vector not found');
}

With Options

import { VectorDeleteOptions } from '@norikv/client';

const options: VectorDeleteOptions = {
  idempotencyKey: 'delete-doc-123',
};

const deleted = await client.vectorDelete('embeddings', 'doc-123', options);

Dropping a Vector Index

const dropped = await client.vectorDropIndex('embeddings');

if (dropped) {
  console.log('Index dropped');
} else {
  console.log('Index did not exist');
}

Complete Vector Example

import { NoriKVClient } from '@norikv/client';

async function main() {
  const client = new NoriKVClient({
    nodes: ['localhost:9001'],
    totalShards: 1024,
  });

  await client.connect();

  try {
    // Create index
    await client.vectorCreateIndex('products', 768, 'cosine', 'hnsw');

    // Insert product embeddings
    const productEmbedding = await getProductEmbedding('Red running shoes');
    await client.vectorInsert('products', 'prod-001', productEmbedding);

    // Search for similar products
    const queryEmbedding = await getProductEmbedding('Athletic footwear');
    const results = await client.vectorSearch('products', queryEmbedding, 5);

    console.log('Similar products:');
    for (const match of results.matches) {
      console.log(`  ${match.id} (distance: ${match.distance.toFixed(4)})`);
    }

    // Cleanup
    await client.vectorDelete('products', 'prod-001');
    await client.vectorDropIndex('products');
  } finally {
    await client.close();
  }
}

async function getProductEmbedding(text: string): Promise<number[]> {
  // Call your embedding model here
  return new Array(768).fill(0);
}

main().catch(console.error);

Error Handling

Error Types

import {
  KeyNotFoundError,
  VersionMismatchError,
  AlreadyExistsError,
  ConnectionError,
  NoriKVError,
} from '@norikv/client';

Handling Specific Errors

try {
  const result = await client.get(key);
} catch (err) {
  if (err instanceof KeyNotFoundError) {
    console.log('Key not found');
  } else if (err instanceof ConnectionError) {
    console.log('Connection error:', err.message);
  } else if (err instanceof NoriKVError) {
    console.log('Error:', err.code, err.message);
  } else {
    throw err;
  }
}

Retry Pattern

async function retryOperation<T>(
  operation: () => Promise<T>,
  maxAttempts: number = 3
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (err) {
      if (!(err instanceof ConnectionError)) {
        throw err; // Non-retryable
      }

      if (attempt === maxAttempts) {
        throw err; // Give up
      }

      // Exponential backoff
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, attempt) * 100)
      );
    }
  }
  throw new Error('Unreachable');
}

// Usage
const result = await retryOperation(() => client.put(key, value));

Graceful Degradation

async function getWithFallback(
  client: NoriKVClient,
  key: string,
  defaultValue: string
): Promise<string> {
  try {
    const result = await client.get(key);
    return bytesToString(result.value);
  } catch (err) {
    console.warn('Failed to get key, using default:', err);
    return defaultValue;
  }
}

Best Practices

1. Always await connect() and close()

const client = new NoriKVClient(config);
await client.connect();

try {
  // Use client
} finally {
  await client.close();
}

2. Reuse Client Instances

//  Good: Single client instance
let client: NoriKVClient;

async function init() {
  client = new NoriKVClient(config);
  await client.connect();
}

//  Bad: Creating client per request
async function handleRequest() {
  const client = new NoriKVClient(config);
  await client.connect();
  await client.close(); // Closes connections!
}

3. Use TypeScript Types

import { GetResult, Version, PutOptions } from '@norikv/client';

async function updateUser(userId: string, data: UserData): Promise<Version> {
  const key = `user:${userId}`;
  const value = JSON.stringify(data);

  const options: PutOptions = {
    ttlMs: 3600000,
  };

  return await client.put(key, value, options);
}

4. Handle Errors Properly

async function safeGet(key: string): Promise<string | null> {
  try {
    const result = await client.get(key);
    return bytesToString(result.value);
  } catch (err) {
    if (err instanceof KeyNotFoundError) {
      return null;
    }
    throw err;
  }
}

5. Use Async/Await Consistently

//  Good: Clean async/await
async function processUser(userId: string) {
  const userData = await client.get(`user:${userId}`);
  const processed = await processData(userData.value);
  await client.put(`processed:${userId}`, processed);
}

//  Bad: Mixing promises and async/await
async function processUserBad(userId: string) {
  return client.get(`user:${userId}`).then(userData => {
    return processData(userData.value).then(processed => {
      return client.put(`processed:${userId}`, processed);
    });
  });
}

6. Use Idempotency Keys for Important Operations

async function createOrder(orderId: string, data: OrderData) {
  await client.put(`order:${orderId}`, JSON.stringify(data), {
    idempotencyKey: `create-order-${orderId}`,
  });
}

7. Choose Appropriate Consistency

// For critical reads
const result = await client.get(key, {
  consistency: ConsistencyLevel.LINEARIZABLE,
});

// For cache-like reads
const result = await client.get(key, {
  consistency: ConsistencyLevel.STALE_OK,
});

8. Use Proper Encoding

import { stringToBytes, bytesToString } from '@norikv/client';

// Always use UTF-8 encoding helpers
const value = stringToBytes('Hello, World!');
await client.put(key, value);

const result = await client.get(key);
const text = bytesToString(result.value);

Performance Tips

1. Batch Operations with Promise.all

// Process multiple operations concurrently
await Promise.all(
  keys.map(key => client.put(key, value))
);

2. Connection Pooling (Automatic)

The client maintains connection pools internally - no external pooling needed.

3. Avoid Creating Clients Per Request

Reuse client instances across requests for better performance.

4. Use Appropriate Value Sizes

  • Optimal: 100 bytes - 10 KB
  • Maximum: Limited by memory and network

5. Monitor Performance

const start = Date.now();
await client.put(key, value);
const duration = Date.now() - start;
console.log(`PUT took ${duration}ms`);

Complete Example

import {
  NoriKVClient,
  ClientConfig,
  PutOptions,
  GetOptions,
  ConsistencyLevel,
  bytesToString,
  stringToBytes,
} from '@norikv/client';

async function main() {
  // Configure with retry policy
  const config: ClientConfig = {
    nodes: ['localhost:9001', 'localhost:9002'],
    totalShards: 1024,
    timeout: 5000,
    retry: {
      maxAttempts: 5,
      initialDelayMs: 100,
      maxDelayMs: 2000,
    },
  };

  const client = new NoriKVClient(config);
  await client.connect();

  try {
    // Write with TTL and idempotency
    const key = 'session:abc123';
    const value = JSON.stringify({ userId: 42 });

    const putOpts: PutOptions = {
      ttlMs: 3600000, // 1 hour
      idempotencyKey: 'session-create-abc123',
    };

    const version = await client.put(key, stringToBytes(value), putOpts);
    console.log('Written:', version);

    // Read with linearizable consistency
    const getOpts: GetOptions = {
      consistency: ConsistencyLevel.LINEARIZABLE,
    };

    const result = await client.get(key, getOpts);
    console.log('Read:', bytesToString(result.value));

    // Update with CAS
    const newValue = JSON.stringify({ userId: 42, active: true });

    try {
      await client.put(key, stringToBytes(newValue), {
        ifMatchVersion: result.version,
      });
      console.log('CAS succeeded');
    } catch (err) {
      if (err instanceof VersionMismatchError) {
        console.log('CAS failed - retry needed');
      }
    }

    // Monitor topology
    client.onTopologyChange((event) => {
      console.log('Cluster changed: epoch', event.currentEpoch);
    });

    // Get statistics
    const stats = client.getStats();
    console.log('Stats:', stats);

  } finally {
    await client.close();
  }
}

main().catch(console.error);

Next Steps