@harmoni-org/sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +694 -0
- package/dist/index.d.mts +712 -0
- package/dist/index.d.ts +712 -0
- package/dist/index.js +2054 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2013 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harmoni SDK
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# ๐ต Harmoni SDK
|
|
2
|
+
|
|
3
|
+
> A powerful, type-safe SDK for seamless backend integration with comprehensive utilities and tools.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@harmoni-org/sdk)
|
|
6
|
+
[](https://www.npmjs.com/package/@harmoni-org/sdk)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://github.com/HarmOni-Official/harmoni-sdk/actions)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
## โจ Features
|
|
12
|
+
|
|
13
|
+
- ๐ **Modern Architecture** - Built with TypeScript, supports ESM & CJS
|
|
14
|
+
- ๐ **Real Token Refresh** - Auto-refresh with proper request retry on 401
|
|
15
|
+
- ๐ **Smart Retry** - Exponential backoff that doesn't break POST requests
|
|
16
|
+
- ๐ค **Upload Support** - Automatic FormData handling with progress tracking
|
|
17
|
+
- ๐ **SSR-Safe** - Works in Node.js, browsers, and edge runtimes
|
|
18
|
+
- ๐ฏ **Type Safety** - Full TypeScript support with comprehensive types
|
|
19
|
+
- ๐งฉ **Modular Design** - Easy to extend with new modules
|
|
20
|
+
- ๐ฌ **Watch Together** - Real-time synchronized video playback with WebSocket
|
|
21
|
+
- ๐ฌ **Real-time Chat** - Built-in chat for Watch Together rooms
|
|
22
|
+
- ๐ฎ **Player Integration** - Abstract interfaces for any video player (HTML5, VLC, custom)
|
|
23
|
+
- ๐ ๏ธ **Rich Utilities** - String, date, object, validation helpers included
|
|
24
|
+
- ๐ฆ **Tree-shakeable** - Only bundle what you use (~8KB core, ~15KB with utils)
|
|
25
|
+
- โ
**Well Tested** - Comprehensive test coverage
|
|
26
|
+
- ๐ **Production Ready** - Battle-tested patterns, see [PRODUCTION_READY.md](docs/guides/PRODUCTION_READY.md)
|
|
27
|
+
|
|
28
|
+
## ๐ฆ Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @harmoni-org/sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yarn add @harmoni-org/sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add @harmoni/sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## ๐ Quick Start
|
|
43
|
+
|
|
44
|
+
### Basic Usage
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { HarmoniSDK } from '@harmoni-org/sdk';
|
|
48
|
+
|
|
49
|
+
// Initialize the SDK
|
|
50
|
+
const sdk = new HarmoniSDK({
|
|
51
|
+
baseURL: 'https://api.yourbackend.com',
|
|
52
|
+
timeout: 30000,
|
|
53
|
+
autoRefreshToken: true, // Automatically refresh tokens on 401
|
|
54
|
+
retryConfig: {
|
|
55
|
+
maxRetries: 3,
|
|
56
|
+
retryDelay: 1000,
|
|
57
|
+
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Login
|
|
62
|
+
const user = await sdk.auth.login({
|
|
63
|
+
emailOrUsername: 'user@example.com',
|
|
64
|
+
password: 'secure_password',
|
|
65
|
+
});
|
|
66
|
+
console.log('Logged in as:', user.username);
|
|
67
|
+
|
|
68
|
+
// Update user profile
|
|
69
|
+
await sdk.user.updateProfile({
|
|
70
|
+
username: 'newusername',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Watch Together - Synchronized playback
|
|
74
|
+
await sdk.watchTogether.connect();
|
|
75
|
+
const room = await sdk.watchTogether.createRoom({ roomName: 'Movie Night' });
|
|
76
|
+
|
|
77
|
+
sdk.watchTogether.onRoomUpdate((update) => {
|
|
78
|
+
if (update.metadata.action === 'play') player.play();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
sdk.watchTogether.play(); // Syncs to all users
|
|
82
|
+
await sdk.watchTogether.sendMessage('Hello! ๐');
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **Note:** The SDK is fully SSR-safe and works in Node.js, browsers, and edge runtimes.
|
|
86
|
+
|
|
87
|
+
### Advanced Configuration
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { HarmoniSDK } from '@harmoni-org/sdk';
|
|
91
|
+
|
|
92
|
+
const sdk = new HarmoniSDK({
|
|
93
|
+
baseURL: 'https://api.yourbackend.com',
|
|
94
|
+
timeout: 30000,
|
|
95
|
+
headers: {
|
|
96
|
+
'X-Custom-Header': 'value',
|
|
97
|
+
},
|
|
98
|
+
retryConfig: {
|
|
99
|
+
maxRetries: 3,
|
|
100
|
+
retryDelay: 1000,
|
|
101
|
+
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
102
|
+
},
|
|
103
|
+
autoRefreshToken: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Set auth token manually if you have it stored
|
|
107
|
+
sdk.setAuthToken('your-access-token');
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## ๐ API Documentation
|
|
111
|
+
|
|
112
|
+
### Authentication Module
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Check if username is available
|
|
116
|
+
const isAvailable = await sdk.auth.isUsernameUnique('johndoe');
|
|
117
|
+
|
|
118
|
+
// Check if email is available
|
|
119
|
+
const emailAvailable = await sdk.auth.isEmailUnique('john@example.com');
|
|
120
|
+
|
|
121
|
+
// Register a new user
|
|
122
|
+
const user = await sdk.auth.register({
|
|
123
|
+
username: 'johndoe',
|
|
124
|
+
password: 'secure_password',
|
|
125
|
+
email: 'john@example.com', // optional
|
|
126
|
+
});
|
|
127
|
+
// Returns: { id, username, email, token, refreshToken }
|
|
128
|
+
|
|
129
|
+
// Login with email or username
|
|
130
|
+
const user = await sdk.auth.login({
|
|
131
|
+
emailOrUsername: 'johndoe', // can be email or username
|
|
132
|
+
password: 'password',
|
|
133
|
+
});
|
|
134
|
+
// Returns: { id, username, email, token, refreshToken }
|
|
135
|
+
|
|
136
|
+
// Verify current token
|
|
137
|
+
const verified = await sdk.auth.verifyToken();
|
|
138
|
+
// Returns: { user: { id, username, email, createdAt } }
|
|
139
|
+
|
|
140
|
+
// Refresh access token
|
|
141
|
+
const newToken = await sdk.auth.refreshAccessToken();
|
|
142
|
+
// Returns: string (new access token)
|
|
143
|
+
|
|
144
|
+
// Logout
|
|
145
|
+
await sdk.auth.logout();
|
|
146
|
+
|
|
147
|
+
// Password reset flow
|
|
148
|
+
await sdk.auth.requestPasswordReset('user@example.com');
|
|
149
|
+
await sdk.auth.resetPassword('reset-token', 'new-password');
|
|
150
|
+
|
|
151
|
+
// Change password (authenticated)
|
|
152
|
+
await sdk.auth.changePassword('old-password', 'new-password');
|
|
153
|
+
|
|
154
|
+
// Email verification
|
|
155
|
+
await sdk.auth.verifyEmail('verification-token');
|
|
156
|
+
await sdk.auth.resendVerificationEmail('user@example.com');
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Watch Together Module
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Connect to Watch Together service
|
|
163
|
+
await sdk.watchTogether.connect();
|
|
164
|
+
|
|
165
|
+
// Create a room
|
|
166
|
+
const room = await sdk.watchTogether.createRoom({
|
|
167
|
+
roomName: 'Movie Night',
|
|
168
|
+
});
|
|
169
|
+
console.log('Room ID:', room.roomId);
|
|
170
|
+
|
|
171
|
+
// Or join existing room
|
|
172
|
+
const room = await sdk.watchTogether.joinRoom({
|
|
173
|
+
roomId: 'abc123',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Listen for events
|
|
177
|
+
sdk.watchTogether.onRoomUpdate((update) => {
|
|
178
|
+
if (update.metadata.userId !== myUserId) {
|
|
179
|
+
switch (update.metadata.action) {
|
|
180
|
+
case 'play':
|
|
181
|
+
player.play();
|
|
182
|
+
break;
|
|
183
|
+
case 'pause':
|
|
184
|
+
player.pause();
|
|
185
|
+
break;
|
|
186
|
+
case 'seek':
|
|
187
|
+
player.seek(update.roomUpdates.syncState?.time);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Sync check (automatic every 30s, or manual)
|
|
194
|
+
sdk.watchTogether.onSyncState((syncState) => {
|
|
195
|
+
const diff = Math.abs(player.getCurrentTime() - syncState.time);
|
|
196
|
+
if (diff > 1) player.seek(syncState.time);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Control playback (synced to all users)
|
|
200
|
+
sdk.watchTogether.play();
|
|
201
|
+
sdk.watchTogether.pause();
|
|
202
|
+
sdk.watchTogether.seek(125.5);
|
|
203
|
+
|
|
204
|
+
// Send chat messages
|
|
205
|
+
await sdk.watchTogether.sendMessage('Hello everyone!');
|
|
206
|
+
|
|
207
|
+
sdk.watchTogether.onChatMessage((message) => {
|
|
208
|
+
console.log(`${message.username}: ${message.content}`);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Update file info
|
|
212
|
+
sdk.watchTogether.updateFileInfo({
|
|
213
|
+
fileId: 'video-001',
|
|
214
|
+
name: 'movie.mkv',
|
|
215
|
+
fullTime: 7200, // 2 hours
|
|
216
|
+
hash: 'abc123',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Leave room
|
|
220
|
+
sdk.watchTogether.leaveRoom();
|
|
221
|
+
|
|
222
|
+
// Disconnect
|
|
223
|
+
sdk.watchTogether.disconnect();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
See [Watch Together Module Documentation](src/modules/watchTogether/README.md) for complete API reference.
|
|
227
|
+
|
|
228
|
+
### Video Player Integration
|
|
229
|
+
|
|
230
|
+
The SDK provides abstract interfaces for integrating any video player with Watch Together:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { HTML5VideoController, SyncedPlayer } from '@harmoni-org/sdk';
|
|
234
|
+
|
|
235
|
+
// Create player (HTML5, VLC, YouTube, custom, etc.)
|
|
236
|
+
const player = new HTML5VideoController('video-element');
|
|
237
|
+
await player.start();
|
|
238
|
+
|
|
239
|
+
// Connect player to Watch Together
|
|
240
|
+
const syncedPlayer = new SyncedPlayer(player, sdk.watchTogether, {
|
|
241
|
+
getCurrentUserId: () => currentUser.id,
|
|
242
|
+
onPlayerStopped: () => sdk.watchTogether.leaveRoom(),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Load and play - automatically syncs to all users!
|
|
246
|
+
await player.loadMedia('https://example.com/video.mp4');
|
|
247
|
+
await player.play();
|
|
248
|
+
|
|
249
|
+
// When user seeks, pauses, or plays - all users follow
|
|
250
|
+
// When other users control their player - yours follows
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Key Features:**
|
|
254
|
+
|
|
255
|
+
- โ
Abstract interfaces for any player (HTML5, VLC, YouTube, custom)
|
|
256
|
+
- โ
Automatic user action detection (play, pause, seek)
|
|
257
|
+
- โ
Smart sync (distinguishes user actions from sync commands)
|
|
258
|
+
- โ
No feedback loops
|
|
259
|
+
- โ
Built-in HTML5 implementation
|
|
260
|
+
- โ
Create custom controllers for any player
|
|
261
|
+
|
|
262
|
+
**Create Custom Player:**
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { VideoPlayerController } from '@harmoni-org/sdk';
|
|
266
|
+
|
|
267
|
+
class MyCustomPlayer implements VideoPlayerController {
|
|
268
|
+
async play(): Promise<void> {
|
|
269
|
+
// Your play logic
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async getStatus(): Promise<IPlayerState> {
|
|
273
|
+
return {
|
|
274
|
+
isPlaying: this.myPlayer.isPlaying(),
|
|
275
|
+
currentTime: this.myPlayer.getCurrentTime(),
|
|
276
|
+
// ... map your player's state
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Implement other methods...
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Use with Watch Together
|
|
284
|
+
const syncedPlayer = new SyncedPlayer(new MyCustomPlayer(), sdk.watchTogether, options);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
See [Player Integration Guide](docs/features/PLAYER_INTEGRATION.md) for complete documentation.
|
|
288
|
+
|
|
289
|
+
### User Module
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Get user by ID
|
|
293
|
+
const user = await sdk.user.getById('user-id-123');
|
|
294
|
+
|
|
295
|
+
// Get current user profile
|
|
296
|
+
const profile = await sdk.user.getCurrentUser();
|
|
297
|
+
|
|
298
|
+
// Update profile
|
|
299
|
+
await sdk.user.updateProfile({
|
|
300
|
+
username: 'johndoe_new',
|
|
301
|
+
email: 'newemail@example.com',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// List users with pagination
|
|
305
|
+
const users = await sdk.user.list({
|
|
306
|
+
page: 1,
|
|
307
|
+
limit: 20,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Search users
|
|
311
|
+
const results = await sdk.user.search('john', {
|
|
312
|
+
page: 1,
|
|
313
|
+
limit: 10,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Upload avatar
|
|
317
|
+
const file = new File(['...'], 'avatar.jpg');
|
|
318
|
+
const { url } = await sdk.user.uploadAvatar(file);
|
|
319
|
+
|
|
320
|
+
// Delete avatar
|
|
321
|
+
await sdk.user.deleteAvatar();
|
|
322
|
+
|
|
323
|
+
// Delete account
|
|
324
|
+
await sdk.user.deleteAccount();
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## ๐ ๏ธ Utilities
|
|
328
|
+
|
|
329
|
+
### String Utilities
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { stringUtils } from '@harmoni-org/sdk';
|
|
333
|
+
|
|
334
|
+
stringUtils.capitalize('hello'); // 'Hello'
|
|
335
|
+
stringUtils.titleCase('hello world'); // 'Hello World'
|
|
336
|
+
stringUtils.slugify('Hello World!'); // 'hello-world'
|
|
337
|
+
stringUtils.truncate('Long text...', 10); // 'Long te...'
|
|
338
|
+
stringUtils.isValidEmail('test@example.com'); // true
|
|
339
|
+
stringUtils.camelToSnake('myVariable'); // 'my_variable'
|
|
340
|
+
stringUtils.snakeToCamel('my_variable'); // 'myVariable'
|
|
341
|
+
stringUtils.randomString(16); // 'aB3dEf5gH7jK9mN0'
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Date Utilities
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { dateUtils } from '@harmoni-org/sdk';
|
|
348
|
+
|
|
349
|
+
dateUtils.formatDate(new Date(), 'YYYY-MM-DD'); // '2024-01-15'
|
|
350
|
+
dateUtils.timeAgo(new Date('2024-01-01')); // '2 weeks ago'
|
|
351
|
+
dateUtils.addDays(new Date(), 7); // Date 7 days from now
|
|
352
|
+
dateUtils.addHours(new Date(), 3); // Date 3 hours from now
|
|
353
|
+
dateUtils.isToday(new Date()); // true
|
|
354
|
+
dateUtils.isPast(new Date('2023-01-01')); // true
|
|
355
|
+
dateUtils.isFuture(new Date('2025-01-01')); // true
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Object Utilities
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { objectUtils } from '@harmoni-org/sdk';
|
|
362
|
+
|
|
363
|
+
const original = { a: 1, b: { c: 2 } };
|
|
364
|
+
const cloned = objectUtils.deepClone(original);
|
|
365
|
+
|
|
366
|
+
const merged = objectUtils.deepMerge({ a: 1, b: 2 }, { b: 3, c: 4 }); // { a: 1, b: 3, c: 4 }
|
|
367
|
+
|
|
368
|
+
const picked = objectUtils.pick({ a: 1, b: 2, c: 3 }, ['a', 'c']); // { a: 1, c: 3 }
|
|
369
|
+
const omitted = objectUtils.omit({ a: 1, b: 2, c: 3 }, ['b']); // { a: 1, c: 3 }
|
|
370
|
+
|
|
371
|
+
objectUtils.isEmpty({}); // true
|
|
372
|
+
objectUtils.isEmpty({ a: 1 }); // false
|
|
373
|
+
|
|
374
|
+
const value = objectUtils.getNestedValue({ a: { b: { c: 1 } } }, 'a.b.c'); // 1
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Validation Utilities
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import { validationUtils } from '@harmoni-org/sdk';
|
|
381
|
+
|
|
382
|
+
// Email validation
|
|
383
|
+
const emailResult = validationUtils.validateEmail('test@example.com');
|
|
384
|
+
// { valid: true }
|
|
385
|
+
|
|
386
|
+
// Password validation
|
|
387
|
+
const passwordResult = validationUtils.validatePassword('MyP@ssw0rd', {
|
|
388
|
+
minLength: 8,
|
|
389
|
+
requireUppercase: true,
|
|
390
|
+
requireLowercase: true,
|
|
391
|
+
requireNumbers: true,
|
|
392
|
+
requireSpecialChars: true,
|
|
393
|
+
});
|
|
394
|
+
// { valid: true }
|
|
395
|
+
|
|
396
|
+
// URL validation
|
|
397
|
+
const urlResult = validationUtils.validateUrl('https://example.com');
|
|
398
|
+
// { valid: true }
|
|
399
|
+
|
|
400
|
+
// Phone validation
|
|
401
|
+
const phoneResult = validationUtils.validatePhone('+1234567890');
|
|
402
|
+
// { valid: true }
|
|
403
|
+
|
|
404
|
+
// Required field
|
|
405
|
+
const requiredResult = validationUtils.required('value', 'Username');
|
|
406
|
+
// { valid: true }
|
|
407
|
+
|
|
408
|
+
// Length validation
|
|
409
|
+
validationUtils.minLength('test', 3, 'Username'); // { valid: true }
|
|
410
|
+
validationUtils.maxLength('test', 10, 'Username'); // { valid: true }
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Storage Utilities
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import { localStorage, sessionStorage } from '@harmoni-org/sdk';
|
|
417
|
+
|
|
418
|
+
// Local storage (SSR-safe - uses memory fallback in Node.js)
|
|
419
|
+
localStorage.set('user', { id: 1, name: 'John' });
|
|
420
|
+
const user = localStorage.get<{ id: number; name: string }>('user');
|
|
421
|
+
localStorage.remove('user');
|
|
422
|
+
localStorage.clear();
|
|
423
|
+
|
|
424
|
+
// Check if browser storage is available
|
|
425
|
+
if (localStorage.isAvailable()) {
|
|
426
|
+
console.log('Using browser localStorage');
|
|
427
|
+
} else {
|
|
428
|
+
console.log('Using in-memory storage (SSR/Node)');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Session storage
|
|
432
|
+
sessionStorage.set('token', 'abc123');
|
|
433
|
+
const token = sessionStorage.get<string>('token');
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
## ๐ Advanced Usage
|
|
437
|
+
|
|
438
|
+
### File Uploads
|
|
439
|
+
|
|
440
|
+
The SDK automatically handles `FormData` for file uploads:
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
// Upload user avatar
|
|
444
|
+
const file = document.querySelector('input[type="file"]').files[0];
|
|
445
|
+
const result = await sdk.user.uploadAvatar(file);
|
|
446
|
+
console.log('Avatar URL:', result.url);
|
|
447
|
+
|
|
448
|
+
// Upload with progress tracking
|
|
449
|
+
const formData = new FormData();
|
|
450
|
+
formData.append('file', file);
|
|
451
|
+
|
|
452
|
+
await sdk.getHttpClient().post('/uploads', formData, {
|
|
453
|
+
onUploadProgress: (progressEvent) => {
|
|
454
|
+
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
455
|
+
console.log(`Upload progress: ${progress}%`);
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
See [examples/upload-example.ts](examples/upload-example.ts) for more upload patterns.
|
|
461
|
+
|
|
462
|
+
### Token Refresh with Request Retry
|
|
463
|
+
|
|
464
|
+
The SDK automatically:
|
|
465
|
+
|
|
466
|
+
1. Detects 401 errors
|
|
467
|
+
2. Calls refresh token endpoint
|
|
468
|
+
3. Retries the original request with new token
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
const sdk = new HarmoniSDK({
|
|
472
|
+
baseURL: 'https://api.example.com',
|
|
473
|
+
autoRefreshToken: true, // Enable auto-refresh
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// This request will automatically retry if token expires
|
|
477
|
+
const data = await sdk.user.getCurrentUser();
|
|
478
|
+
// If 401 โ refreshes token โ retries request โ returns data
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Smart Retry Logic
|
|
482
|
+
|
|
483
|
+
The SDK only retries safe operations (GET, HEAD, OPTIONS) by default:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
const sdk = new HarmoniSDK({
|
|
487
|
+
baseURL: 'https://api.example.com',
|
|
488
|
+
retryConfig: {
|
|
489
|
+
maxRetries: 3,
|
|
490
|
+
retryDelay: 1000, // Base delay (uses exponential backoff)
|
|
491
|
+
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// GET requests will retry on network errors or 5xx errors
|
|
496
|
+
await sdk.user.list(); // โ
Will retry
|
|
497
|
+
|
|
498
|
+
// POST/PATCH/DELETE won't retry (not idempotent)
|
|
499
|
+
await sdk.auth.login(creds); // โ Won't retry (POST)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Custom Interceptors
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
const sdk = new HarmoniSDK({ baseURL: 'https://api.example.com' });
|
|
506
|
+
|
|
507
|
+
// Add request interceptor
|
|
508
|
+
sdk.getHttpClient().addRequestInterceptor({
|
|
509
|
+
onFulfilled: (config) => {
|
|
510
|
+
console.log('Request:', config.url);
|
|
511
|
+
return config;
|
|
512
|
+
},
|
|
513
|
+
onRejected: (error) => {
|
|
514
|
+
console.error('Request error:', error);
|
|
515
|
+
return Promise.reject(error);
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Add response interceptor
|
|
520
|
+
sdk.getHttpClient().addResponseInterceptor({
|
|
521
|
+
onFulfilled: (response) => {
|
|
522
|
+
console.log('Response:', response.status);
|
|
523
|
+
return response;
|
|
524
|
+
},
|
|
525
|
+
onRejected: (error) => {
|
|
526
|
+
console.error('Response error:', error);
|
|
527
|
+
return Promise.reject(error);
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Custom Error Handling
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { ApiError } from '@harmoni-org/sdk';
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
await sdk.auth.login(credentials);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (ApiError.isApiError(error)) {
|
|
541
|
+
console.error('API Error:', error.status, error.message);
|
|
542
|
+
console.error('Error code:', error.code);
|
|
543
|
+
console.error('Details:', error.details);
|
|
544
|
+
} else {
|
|
545
|
+
console.error('Unknown error:', error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Creating Custom Modules
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
import { HttpClient } from '@harmoni-org/sdk';
|
|
554
|
+
|
|
555
|
+
class CustomModule {
|
|
556
|
+
constructor(private http: HttpClient) {}
|
|
557
|
+
|
|
558
|
+
async customEndpoint() {
|
|
559
|
+
return this.http.get('/custom/endpoint');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Extend the SDK
|
|
564
|
+
const sdk = new HarmoniSDK({ baseURL: 'https://api.example.com' });
|
|
565
|
+
const customModule = new CustomModule(sdk.getHttpClient());
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## ๐๏ธ Project Structure
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
harmoni-sdk/
|
|
572
|
+
โโโ src/
|
|
573
|
+
โ โโโ core/ # Core HTTP client & error handling
|
|
574
|
+
โ โ โโโ http/
|
|
575
|
+
โ โ โโโ errors/
|
|
576
|
+
โ โโโ modules/ # Feature modules (auth, user, etc.)
|
|
577
|
+
โ โ โโโ auth/
|
|
578
|
+
โ โ โโโ user/
|
|
579
|
+
โ โโโ utils/ # Utility functions
|
|
580
|
+
โ โโโ types/ # TypeScript types
|
|
581
|
+
โ โโโ sdk/ # Main SDK class
|
|
582
|
+
โ โโโ index.ts # Main entry point
|
|
583
|
+
โโโ tests/ # Test files
|
|
584
|
+
โโโ .github/workflows/ # CI/CD pipelines
|
|
585
|
+
โโโ dist/ # Build output (ESM + CJS)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## ๐งช Testing
|
|
589
|
+
|
|
590
|
+
```bash
|
|
591
|
+
# Run tests
|
|
592
|
+
npm test
|
|
593
|
+
|
|
594
|
+
# Run tests in watch mode
|
|
595
|
+
npm run test:watch
|
|
596
|
+
|
|
597
|
+
# Generate coverage report
|
|
598
|
+
npm run test -- --coverage
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## ๐จ Development
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
# Install dependencies
|
|
605
|
+
npm install
|
|
606
|
+
|
|
607
|
+
# Start development mode (watch)
|
|
608
|
+
npm run dev
|
|
609
|
+
|
|
610
|
+
# Build the package
|
|
611
|
+
npm run build
|
|
612
|
+
|
|
613
|
+
# Run linter
|
|
614
|
+
npm run lint
|
|
615
|
+
|
|
616
|
+
# Format code
|
|
617
|
+
npm run format
|
|
618
|
+
|
|
619
|
+
# Type check
|
|
620
|
+
npm run typecheck
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
## ๐ Creating a Release
|
|
624
|
+
|
|
625
|
+
This project uses [Changesets](https://github.com/changesets/changesets) for version management.
|
|
626
|
+
|
|
627
|
+
1. Create a changeset:
|
|
628
|
+
|
|
629
|
+
```bash
|
|
630
|
+
npm run changeset
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
2. Commit the changeset files
|
|
634
|
+
3. Push to GitHub
|
|
635
|
+
4. A "Version Packages" PR will be created automatically
|
|
636
|
+
5. Merge the PR to publish to npm
|
|
637
|
+
|
|
638
|
+
## ๐ค Contributing
|
|
639
|
+
|
|
640
|
+
Contributions are welcome! Please follow these steps:
|
|
641
|
+
|
|
642
|
+
1. Fork the repository
|
|
643
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
644
|
+
3. Make your changes
|
|
645
|
+
4. Add tests for your changes
|
|
646
|
+
5. Run tests and linting (`npm test && npm run lint`)
|
|
647
|
+
6. Commit your changes (`git commit -m 'feat: add amazing feature'`)
|
|
648
|
+
7. Push to the branch (`git push origin feature/amazing-feature`)
|
|
649
|
+
8. Open a Pull Request
|
|
650
|
+
|
|
651
|
+
## ๐ License
|
|
652
|
+
|
|
653
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
654
|
+
|
|
655
|
+
## ๐ Additional Resources
|
|
656
|
+
|
|
657
|
+
### Documentation
|
|
658
|
+
|
|
659
|
+
- ๐ [Documentation](docs/README.md) - Complete documentation index
|
|
660
|
+
- ๐ [Production Ready Features](docs/guides/PRODUCTION_READY.md) - Why this SDK is production-ready
|
|
661
|
+
- ๐ [Quick Start Guide](docs/guides/QUICK_START.md) - Get started in 5 minutes
|
|
662
|
+
- ๐ [Migration Guide](docs/guides/MIGRATION.md) - Migrate from direct API calls
|
|
663
|
+
- ๐ ๏ธ [Setup Guide](docs/guides/SETUP.md) - Development and publishing
|
|
664
|
+
- ๐ค [Contributing](CONTRIBUTING.md) - Contribution guidelines
|
|
665
|
+
|
|
666
|
+
### Examples
|
|
667
|
+
|
|
668
|
+
- [Basic Usage](examples/basic-usage.ts) - Common authentication patterns
|
|
669
|
+
- [Advanced Usage](examples/advanced-usage.ts) - Interceptors, error handling, utilities
|
|
670
|
+
- [File Uploads](examples/upload-example.ts) - File upload with progress tracking
|
|
671
|
+
- [Watch Together](examples/watch-together-example.ts) - Real-time synchronized playback
|
|
672
|
+
|
|
673
|
+
### External Links
|
|
674
|
+
|
|
675
|
+
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
|
676
|
+
- [Vitest Documentation](https://vitest.dev/)
|
|
677
|
+
- [Changesets Documentation](https://github.com/changesets/changesets)
|
|
678
|
+
|
|
679
|
+
## ๐ Acknowledgments
|
|
680
|
+
|
|
681
|
+
- Built with [TypeScript](https://www.typescriptlang.org/)
|
|
682
|
+
- Bundled with [tsup](https://tsup.egoist.dev/)
|
|
683
|
+
- Tested with [Vitest](https://vitest.dev/)
|
|
684
|
+
- HTTP client powered by [Axios](https://axios-http.com/)
|
|
685
|
+
|
|
686
|
+
## ๐ Support
|
|
687
|
+
|
|
688
|
+
- ๐ง Email: support@harmoni.dev
|
|
689
|
+
- ๐ Issues: [GitHub Issues](https://github.com/yourusername/harmoni-sdk/issues)
|
|
690
|
+
- ๐ฌ Discussions: [GitHub Discussions](https://github.com/yourusername/harmoni-sdk/discussions)
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
Made with โค๏ธ by the Harmoni Team
|