@harmoni-org/sdk
v0.0.3
Published
Harmoni SDK - API abstraction layer with utilities and helpers
Maintainers
Readme
🎵 Harmoni SDK
A powerful, type-safe SDK for seamless backend integration with comprehensive utilities and tools.
✨ Features
- 🚀 Modern Architecture - Built with TypeScript, supports ESM & CJS
- 🔐 Real Token Refresh - Auto-refresh with proper request retry on 401
- 💾 Session Persistence - Automatic token storage & restoration across app reloads
- 📱 Cross-Platform - Works in browsers, React Native, Expo, Node.js, and more
- 🔄 Smart Retry - Exponential backoff that doesn't break POST requests
- 📤 Upload Support - Automatic FormData handling with progress tracking
- 🌐 SSR-Safe - Works in Node.js, browsers, and edge runtimes
- 🎯 Type Safety - Full TypeScript support with comprehensive types
- 🧩 Modular Design - Easy to extend with new modules
- 🎬 Watch Together - Real-time synchronized video playback with WebSocket
- 💬 Real-time Chat - Built-in chat for Watch Together rooms
- 🎮 Player Integration - Abstract interfaces for any video player (HTML5, VLC, custom)
- 🛠️ Rich Utilities - String, date, object, validation helpers included
- 📦 Tree-shakeable - Only bundle what you use (~8KB core, ~15KB with utils)
- ✅ Well Tested - Comprehensive test coverage
- 📚 Production Ready - Battle-tested patterns, see PRODUCTION_READY.md
📦 Installation
npm install @harmoni-org/sdkyarn add @harmoni-org/sdkpnpm add @harmoni-org/sdk🎮 Try the Demo
Want to see it in action? Check out the demo project that uses the published npm package:
cd demo
npm install
npm run demo # Basic usage demo
npm run demo:watch # Watch Together demo
npm run demo:player # Player integration demoSee demo folder for complete examples using @harmoni-org/sdk from npm!
🚀 Quick Start
Basic Usage
import { HarmoniSDK } from '@harmoni-org/sdk';
// Initialize the SDK
const sdk = new HarmoniSDK({
baseURL: 'https://api.yourbackend.com',
timeout: 30000,
autoRefreshToken: true, // Automatically refresh tokens on 401
// autoRestoreToken: true (default) - Token persists across reloads
retryConfig: {
maxRetries: 3,
retryDelay: 1000,
retryableStatuses: [408, 429, 500, 502, 503, 504],
},
});
// Login - token automatically saved to localStorage
const user = await sdk.auth.login({
emailOrUsername: '[email protected]',
password: 'secure_password',
});
console.log('Logged in as:', user.username);
// 🔄 User closes and reopens app...
// ✅ Token is automatically restored! No extra code needed.
// Update user profile
await sdk.user.updateProfile({
username: 'newusername',
});
// Watch Together - Synchronized playback
await sdk.watchTogether.connect();
const room = await sdk.watchTogether.createRoom({ roomName: 'Movie Night' });
sdk.watchTogether.onRoomUpdate((update) => {
if (update.metadata.action === 'play') player.play();
});
sdk.watchTogether.play(); // Syncs to all users
await sdk.watchTogether.sendMessage('Hello! 👋');Note: The SDK is fully SSR-safe and works in Node.js, browsers, and edge runtimes.
Advanced Configuration
import { HarmoniSDK } from '@harmoni-org/sdk';
const sdk = new HarmoniSDK({
baseURL: 'https://api.yourbackend.com',
timeout: 30000,
headers: {
'X-Custom-Header': 'value',
},
retryConfig: {
maxRetries: 3,
retryDelay: 1000,
retryableStatuses: [408, 429, 500, 502, 503, 504],
},
autoRefreshToken: true,
});
// Set auth token manually if you have it stored
sdk.setAuthToken('your-access-token');📖 API Documentation
Authentication Module
// Check if username is available
const isAvailable = await sdk.auth.isUsernameUnique('johndoe');
// Check if email is available
const emailAvailable = await sdk.auth.isEmailUnique('[email protected]');
// Register a new user
const user = await sdk.auth.register({
username: 'johndoe',
password: 'secure_password',
email: '[email protected]', // optional
});
// Returns: { id, username, email, token, refreshToken }
// Login with email or username
const user = await sdk.auth.login({
emailOrUsername: 'johndoe', // can be email or username
password: 'password',
});
// Returns: { id, username, email, token, refreshToken }
// Verify current token
const verified = await sdk.auth.verifyToken();
// Returns: { user: { id, username, email, createdAt } }
// Refresh access token
const newToken = await sdk.auth.refreshAccessToken();
// Returns: string (new access token)
// Logout
await sdk.auth.logout();
// Password reset flow
await sdk.auth.requestPasswordReset('[email protected]');
await sdk.auth.resetPassword('reset-token', 'new-password');
// Change password (authenticated)
await sdk.auth.changePassword('old-password', 'new-password');
// Email verification
await sdk.auth.verifyEmail('verification-token');
await sdk.auth.resendVerificationEmail('[email protected]');Watch Together Module
// Connect to Watch Together service
await sdk.watchTogether.connect();
// Create a room
const room = await sdk.watchTogether.createRoom({
roomName: 'Movie Night',
});
console.log('Room ID:', room.roomId);
// Or join existing room
const room = await sdk.watchTogether.joinRoom({
roomId: 'abc123',
});
// Listen for events
sdk.watchTogether.onRoomUpdate((update) => {
if (update.metadata.userId !== myUserId) {
switch (update.metadata.action) {
case 'play':
player.play();
break;
case 'pause':
player.pause();
break;
case 'seek':
player.seek(update.roomUpdates.syncState?.time);
break;
}
}
});
// Sync check (automatic every 30s, or manual)
sdk.watchTogether.onSyncState((syncState) => {
const diff = Math.abs(player.getCurrentTime() - syncState.time);
if (diff > 1) player.seek(syncState.time);
});
// Control playback (synced to all users)
sdk.watchTogether.play();
sdk.watchTogether.pause();
sdk.watchTogether.seek(125.5);
// Send chat messages
await sdk.watchTogether.sendMessage('Hello everyone!');
sdk.watchTogether.onChatMessage((message) => {
console.log(`${message.username}: ${message.content}`);
});
// Update file info
sdk.watchTogether.updateFileInfo({
fileId: 'video-001',
name: 'movie.mkv',
fullTime: 7200, // 2 hours
hash: 'abc123',
});
// Leave room
sdk.watchTogether.leaveRoom();
// Disconnect
sdk.watchTogether.disconnect();See Watch Together Module Documentation for complete API reference.
Video Player Integration
The SDK provides abstract interfaces for integrating any video player with Watch Together:
import { HTML5VideoController, SyncedPlayer } from '@harmoni-org/sdk';
// Create player (HTML5, VLC, YouTube, custom, etc.)
const player = new HTML5VideoController('video-element');
await player.start();
// Connect player to Watch Together
const syncedPlayer = new SyncedPlayer(player, sdk.watchTogether, {
getCurrentUserId: () => currentUser.id,
onPlayerStopped: () => sdk.watchTogether.leaveRoom(),
});
// Load and play - automatically syncs to all users!
await player.loadMedia('https://example.com/video.mp4');
await player.play();
// When user seeks, pauses, or plays - all users follow
// When other users control their player - yours followsKey Features:
- ✅ Abstract interfaces for any player (HTML5, VLC, YouTube, custom)
- ✅ Automatic user action detection (play, pause, seek)
- ✅ Smart sync (distinguishes user actions from sync commands)
- ✅ No feedback loops
- ✅ Built-in HTML5 implementation
- ✅ Create custom controllers for any player
Create Custom Player:
import { VideoPlayerController } from '@harmoni-org/sdk';
class MyCustomPlayer implements VideoPlayerController {
async play(): Promise<void> {
// Your play logic
}
async getStatus(): Promise<IPlayerState> {
return {
isPlaying: this.myPlayer.isPlaying(),
currentTime: this.myPlayer.getCurrentTime(),
// ... map your player's state
};
}
// Implement other methods...
}
// Use with Watch Together
const syncedPlayer = new SyncedPlayer(new MyCustomPlayer(), sdk.watchTogether, options);See Player Integration Guide for complete documentation.
User Module
// Get user by ID
const user = await sdk.user.getById('user-id-123');
// Get current user profile
const profile = await sdk.user.getCurrentUser();
// Update profile
await sdk.user.updateProfile({
username: 'johndoe_new',
email: '[email protected]',
});
// List users with pagination
const users = await sdk.user.list({
page: 1,
limit: 20,
});
// Search users
const results = await sdk.user.search('john', {
page: 1,
limit: 10,
});
// Upload avatar
const file = new File(['...'], 'avatar.jpg');
const { url } = await sdk.user.uploadAvatar(file);
// Delete avatar
await sdk.user.deleteAvatar();
// Delete account
await sdk.user.deleteAccount();🛠️ Utilities
String Utilities
import { stringUtils } from '@harmoni-org/sdk';
stringUtils.capitalize('hello'); // 'Hello'
stringUtils.titleCase('hello world'); // 'Hello World'
stringUtils.slugify('Hello World!'); // 'hello-world'
stringUtils.truncate('Long text...', 10); // 'Long te...'
stringUtils.isValidEmail('[email protected]'); // true
stringUtils.camelToSnake('myVariable'); // 'my_variable'
stringUtils.snakeToCamel('my_variable'); // 'myVariable'
stringUtils.randomString(16); // 'aB3dEf5gH7jK9mN0'Date Utilities
import { dateUtils } from '@harmoni-org/sdk';
dateUtils.formatDate(new Date(), 'YYYY-MM-DD'); // '2024-01-15'
dateUtils.timeAgo(new Date('2024-01-01')); // '2 weeks ago'
dateUtils.addDays(new Date(), 7); // Date 7 days from now
dateUtils.addHours(new Date(), 3); // Date 3 hours from now
dateUtils.isToday(new Date()); // true
dateUtils.isPast(new Date('2023-01-01')); // true
dateUtils.isFuture(new Date('2025-01-01')); // trueObject Utilities
import { objectUtils } from '@harmoni-org/sdk';
const original = { a: 1, b: { c: 2 } };
const cloned = objectUtils.deepClone(original);
const merged = objectUtils.deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 }); // { a: 1, b: 3, c: 4 }
const picked = objectUtils.pick({ a: 1, b: 2, c: 3 }, ['a', 'c']); // { a: 1, c: 3 }
const omitted = objectUtils.omit({ a: 1, b: 2, c: 3 }, ['b']); // { a: 1, c: 3 }
objectUtils.isEmpty({}); // true
objectUtils.isEmpty({ a: 1 }); // false
const value = objectUtils.getNestedValue({ a: { b: { c: 1 } } }, 'a.b.c'); // 1Validation Utilities
import { validationUtils } from '@harmoni-org/sdk';
// Email validation
const emailResult = validationUtils.validateEmail('[email protected]');
// { valid: true }
// Password validation
const passwordResult = validationUtils.validatePassword('MyP@ssw0rd', {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
});
// { valid: true }
// URL validation
const urlResult = validationUtils.validateUrl('https://example.com');
// { valid: true }
// Phone validation
const phoneResult = validationUtils.validatePhone('+1234567890');
// { valid: true }
// Required field
const requiredResult = validationUtils.required('value', 'Username');
// { valid: true }
// Length validation
validationUtils.minLength('test', 3, 'Username'); // { valid: true }
validationUtils.maxLength('test', 10, 'Username'); // { valid: true }Storage Utilities
import { localStorage, sessionStorage } from '@harmoni-org/sdk';
// Local storage (SSR-safe - uses memory fallback in Node.js)
localStorage.set('user', { id: 1, name: 'John' });
const user = localStorage.get<{ id: number; name: string }>('user');
localStorage.remove('user');
localStorage.clear();
// Check if browser storage is available
if (localStorage.isAvailable()) {
console.log('Using browser localStorage');
} else {
console.log('Using in-memory storage (SSR/Node)');
}
// Session storage
sessionStorage.set('token', 'abc123');
const token = sessionStorage.get<string>('token');🔌 Advanced Usage
File Uploads
The SDK automatically handles FormData for file uploads:
// Upload user avatar
const file = document.querySelector('input[type="file"]').files[0];
const result = await sdk.user.uploadAvatar(file);
console.log('Avatar URL:', result.url);
// Upload with progress tracking
const formData = new FormData();
formData.append('file', file);
await sdk.getHttpClient().post('/uploads', formData, {
onUploadProgress: (progressEvent) => {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`Upload progress: ${progress}%`);
},
});See examples/upload-example.ts for more upload patterns.
Token Refresh with Request Retry
The SDK automatically:
- Detects 401 errors
- Calls refresh token endpoint
- Retries the original request with new token
const sdk = new HarmoniSDK({
baseURL: 'https://api.example.com',
autoRefreshToken: true, // Enable auto-refresh
});
// This request will automatically retry if token expires
const data = await sdk.user.getCurrentUser();
// If 401 → refreshes token → retries request → returns dataSmart Retry Logic
The SDK only retries safe operations (GET, HEAD, OPTIONS) by default:
const sdk = new HarmoniSDK({
baseURL: 'https://api.example.com',
retryConfig: {
maxRetries: 3,
retryDelay: 1000, // Base delay (uses exponential backoff)
retryableStatuses: [408, 429, 500, 502, 503, 504],
},
});
// GET requests will retry on network errors or 5xx errors
await sdk.user.list(); // ✅ Will retry
// POST/PATCH/DELETE won't retry (not idempotent)
await sdk.auth.login(creds); // ❌ Won't retry (POST)Custom Interceptors
const sdk = new HarmoniSDK({ baseURL: 'https://api.example.com' });
// Add request interceptor
sdk.getHttpClient().addRequestInterceptor({
onFulfilled: (config) => {
console.log('Request:', config.url);
return config;
},
onRejected: (error) => {
console.error('Request error:', error);
return Promise.reject(error);
},
});
// Add response interceptor
sdk.getHttpClient().addResponseInterceptor({
onFulfilled: (response) => {
console.log('Response:', response.status);
return response;
},
onRejected: (error) => {
console.error('Response error:', error);
return Promise.reject(error);
},
});Custom Error Handling
import { ApiError } from '@harmoni-org/sdk';
try {
await sdk.auth.login(credentials);
} catch (error) {
if (ApiError.isApiError(error)) {
console.error('API Error:', error.status, error.message);
console.error('Error code:', error.code);
console.error('Details:', error.details);
} else {
console.error('Unknown error:', error);
}
}Creating Custom Modules
import { HttpClient } from '@harmoni-org/sdk';
class CustomModule {
constructor(private http: HttpClient) {}
async customEndpoint() {
return this.http.get('/custom/endpoint');
}
}
// Extend the SDK
const sdk = new HarmoniSDK({ baseURL: 'https://api.example.com' });
const customModule = new CustomModule(sdk.getHttpClient());🏗️ Project Structure
harmoni-sdk/
├── src/
│ ├── core/ # Core HTTP client & error handling
│ │ ├── http/
│ │ └── errors/
│ ├── modules/ # Feature modules (auth, user, etc.)
│ │ ├── auth/
│ │ └── user/
│ ├── utils/ # Utility functions
│ ├── types/ # TypeScript types
│ ├── sdk/ # Main SDK class
│ └── index.ts # Main entry point
├── tests/ # Test files
├── .github/workflows/ # CI/CD pipelines
└── dist/ # Build output (ESM + CJS)🧪 Testing
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run test -- --coverage🔨 Development
# Install dependencies
npm install
# Start development mode (watch)
npm run dev
# Build the package
npm run build
# Run linter
npm run lint
# Format code
npm run format
# Type check
npm run typecheck📝 Creating a Release
This project uses Changesets for version management.
- Create a changeset:
npm run changeset- Commit the changeset files
- Push to GitHub
- A "Version Packages" PR will be created automatically
- Merge the PR to publish to npm
🤝 Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for your changes
- Run tests and linting (
npm test && npm run lint) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
📚 Additional Resources
Documentation
- 📚 Documentation - Complete documentation index
- 📖 Production Ready Features - Why this SDK is production-ready
- 🚀 Quick Start Guide - Get started in 5 minutes
- 🔐 Authentication Guide - Complete auth documentation
- 💾 Session Persistence Guide - Session management
- 📱 Cross-Platform Guide - Platform-specific implementations
- 🔄 Migration Guide - Migrate from direct API calls
- 🛠️ Setup Guide - Development and publishing
- 🤝 Contributing - Contribution guidelines
Examples
- Basic Usage - Common authentication patterns
- Advanced Usage - Interceptors, error handling, utilities
- File Uploads - File upload with progress tracking
- Watch Together - Real-time synchronized playback
External Links
🙏 Acknowledgments
- Built with TypeScript
- Bundled with tsup
- Tested with Vitest
- HTTP client powered by Axios
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
Made with ❤️ by the Harmoni Team
